component

Wall

Wall segment with door/window openings and a fixed collider.

⌖ Explore the library in 3D → · View as Markdown

Tune every prop in the workshop →

Add it

npx @runek/cli add wall

Pulls @react-three/rapier@^2.2.0, @runek/core@^0.15.0.

Use it

import { Wall } from './runek/Wall'

<Wall position={[0, 0, 0]} />

Or as a node in a world file (worlds as data):

{ "type": "Wall", "props": { "position": [0, 0, 0] } }

Props

Prop Type Default Description
position [x, y, z] [0, 0, 0] Position [x, y, z] in units (1 unit = 1 m, Y-up).
rotation [x, y, z] [0, 0, 0] Euler rotation [x, y, z] in radians.
seed number Accepted for the component contract; this component has no seeded variation yet.
width number 4 Length along the wall’s local X axis, in units.
height number 3 Height, in units.
thickness number 0.2 Thickness, in units.
color color palette wall Defaults to the world palette’s wall slot.
openings WallOpening[] Holes cut into the wall (doors, windows). Must not overlap horizontally.

WallOpening

Prop Type Default Description
offset number Horizontal center offset from the wall center, in units.
width number required
height number required
sill number Height of the opening’s base above the wall base, in units.

Migrate

v0.11.0 → v0.12.0. The single opening prop became openings, an array, so one wall can carry a door and windows together. The WallOpening shape itself is unchanged (offset, width, height, sill).

v0.11.0
<Wall
  width={9}
  opening={{ width: 1, height: 2.1 }}
/>
v0.12.0
<Wall
  width={9}
  openings={[
    { width: 1, height: 2.1 },
    { offset: 3, width: 1.3, height: 1.2, sill: 1 },
  ]}
/>

Registry manifest

registry manifest /r/components/wall.json Self-contained JSON: inlined source plus resolved dependencies, exactly what runek add wall fetches.

Browse the whole catalog in the gallery →.

Source (Wall.tsx)
import { RigidBody } from '@react-three/rapier'
import { useWorld, type Vec3 } from '@runek/core'

/** A rectangular hole cut into a wall, for a door or window. */
export interface WallOpening {
  /** Horizontal center offset from the wall center, in units. */
  offset?: number
  width: number
  height: number
  /** Height of the opening's base above the wall base, in units. */
  sill?: number
}

export interface WallProps {
  position?: Vec3
  rotation?: Vec3
  /** Accepted for the component contract; this component has no seeded variation yet. */
  seed?: number
  /** Length along the wall's local X axis, in units. */
  width?: number
  height?: number
  thickness?: number
  /** Defaults to the world palette's `wall` slot. */
  color?: string
  /** Holes cut into the wall (doors, windows). Must not overlap horizontally. */
  openings?: WallOpening[]
}

interface Segment {
  x: number
  y: number
  w: number
  h: number
}

const EPS = 1e-4

function segments(width: number, height: number, openings?: WallOpening[]): Segment[] {
  if (!openings?.length) return [{ x: 0, y: height / 2, w: width, h: height }]

  const sorted = [...openings].sort((a, b) => (a.offset ?? 0) - (b.offset ?? 0))
  const out: Segment[] = []
  // Left edge of the un-emitted remainder; advances past each opening in turn.
  let cursor = -width / 2

  for (const opening of sorted) {
    const { offset = 0, width: ow, height: oh, sill = 0 } = opening
    const left = Math.max(offset - ow / 2, cursor)
    const right = Math.min(offset + ow / 2, width / 2)
    if (right - left <= EPS) continue

    const pierW = left - cursor
    if (pierW > EPS) out.push({ x: cursor + pierW / 2, y: height / 2, w: pierW, h: height })

    const mid = (left + right) / 2
    const clampedW = right - left
    if (sill > EPS) out.push({ x: mid, y: sill / 2, w: clampedW, h: sill })

    const top = sill + oh
    const topH = height - top
    if (topH > EPS) out.push({ x: mid, y: (top + height) / 2, w: clampedW, h: topH })

    cursor = right
  }

  const lastW = width / 2 - cursor
  if (lastW > EPS) out.push({ x: cursor + lastW / 2, y: height / 2, w: lastW, h: height })

  return out
}

export function Wall({
  position = [0, 0, 0],
  rotation = [0, 0, 0],
  width = 4,
  height = 3,
  thickness = 0.2,
  color,
  openings,
}: WallProps) {
  const { unit, palette } = useWorld()
  const wallColor = color ?? palette.wall
  const w = width * unit
  const h = height * unit
  const t = thickness * unit
  const scaled = openings?.map((o) => ({
    offset: (o.offset ?? 0) * unit,
    width: o.width * unit,
    height: o.height * unit,
    sill: (o.sill ?? 0) * unit,
  }))

  return (
    <RigidBody type="fixed" colliders="cuboid" position={position} rotation={rotation}>
      {segments(w, h, scaled).map((s) => (
        <mesh
          key={`${s.x.toFixed(3)}:${s.y.toFixed(3)}`}
          castShadow
          receiveShadow
          position={[s.x, s.y, 0]}
        >
          <boxGeometry args={[s.w, s.h, t]} />
          <meshStandardMaterial color={wallColor} />
        </mesh>
      ))}
    </RigidBody>
  )
}