← gallery

structures

Wall

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

Install

npx runek add wall

Pulls: core@react-three/rapier@^2.2.0

Props

export interface WallProps {
  position?: Vec3
  rotation?: Vec3
  /** 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
  opening?: WallOpening
}

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
  /** 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
  opening?: WallOpening
}

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

const EPS = 1e-4

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

  const { offset = 0, width: ow, height: oh, sill = 0 } = opening
  const left = offset - ow / 2
  const right = offset + ow / 2
  const top = sill + oh
  const out: Segment[] = []

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

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

  if (sill > EPS) out.push({ x: offset, y: sill / 2, w: ow, h: sill })

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

  return out
}

export function Wall({
  position = [0, 0, 0],
  rotation = [0, 0, 0],
  width = 4,
  height = 3,
  thickness = 0.2,
  color,
  opening,
}: WallProps) {
  const { unit, palette } = useWorld()
  const wallColor = color ?? palette.wall
  const w = width * unit
  const h = height * unit
  const t = thickness * unit
  const scaled: WallOpening | undefined = opening && {
    offset: (opening.offset ?? 0) * unit,
    width: opening.width * unit,
    height: opening.height * unit,
    sill: (opening.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>
  )
}