component
Wall
Wall segment with door/window openings and a fixed collider.
⌖ Explore the library in 3D → · View as Markdown
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 },
]}
/>
- Mechanical migration: wrap the object in an array —
opening={X}becomesopenings={[X]}; in world files,"opening": {…}becomes"openings": [{…}]. - Openings are laid out by
offset(center offset from the wall center, in units) and must not overlap horizontally.
Registry manifest
registry manifest /r/components/wall.json Self-contained JSON: inlined source plus resolved dependencies, exactly whatrunek 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>
)
}