component
Floor
Flat floor slab with a fixed collider; optional stairwell opening.
⌖ Explore the library in 3D → · View as Markdown
Add it
npx @runek/cli add floor
Pulls @react-three/rapier@^2.2.0, @runek/core@^0.15.0.
Use it
import { Floor } from './runek/Floor'
<Floor position={[0, 0, 0]} />
Or as a node in a world file (worlds as data):
{ "type": "Floor", "props": { "position": [0, 0, 0] } }
Props
| Prop | Type | Default | Description |
|---|---|---|---|
position |
[x, y, z] |
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. | |
size |
[number, number] |
[8, 8] |
[width, depth] in units. The top surface sits at the component origin. |
thickness |
number |
0.2 |
Thickness, in units. |
opening |
FloorOpening |
A hole in the slab (stairwell); the slab splits into strips around it. | |
color |
color |
palette floor |
Defaults to the world palette’s floor slot. |
FloorOpening
| Prop | Type | Default | Description |
|---|---|---|---|
x |
number |
Hole center offset from the slab center along X, in units. | |
z |
number |
Hole center offset from the slab center along Z, in units. | |
width |
number |
required | |
depth |
number |
required |
Registry manifest
registry manifest /r/components/floor.json Self-contained JSON: inlined source plus resolved dependencies, exactly whatrunek add floor fetches.
Browse the whole catalog in the gallery →.
Source (2 files)
Floor.tsx
import { RigidBody } from '@react-three/rapier'
import { useWorld, type Vec3 } from '@runek/core'
import { floorSurface } from './surfaces/floor'
/** A rectangular hole in the slab, e.g. a stairwell. */
export interface FloorOpening {
/** Hole center offset from the slab center along X, in units. */
x?: number
/** Hole center offset from the slab center along Z, in units. */
z?: number
width: number
depth: number
}
export interface FloorProps {
position?: Vec3
rotation?: Vec3
/** Accepted for the component contract; this component has no seeded variation yet. */
seed?: number
/** `[width, depth]` in units. The top surface sits at the component origin. */
size?: [number, number]
thickness?: number
/** A hole in the slab (stairwell); the slab splits into strips around it. */
opening?: FloorOpening
/** Defaults to the world palette's `floor` slot. */
color?: string
}
interface Slab {
x: number
z: number
w: number
d: number
}
const EPS = 1e-4
function slabs(w: number, d: number, opening?: FloorOpening): Slab[] {
if (!opening) return [{ x: 0, z: 0, w, d }]
const { x = 0, z = 0, width, depth } = opening
const left = Math.max(x - width / 2, -w / 2)
const right = Math.min(x + width / 2, w / 2)
const near = Math.max(z - depth / 2, -d / 2)
const far = Math.min(z + depth / 2, d / 2)
if (right - left <= EPS || far - near <= EPS) return [{ x: 0, z: 0, w, d }]
const out: Slab[] = []
// Full-depth strips either side of the hole, short strips before/behind it.
const leftW = left + w / 2
if (leftW > EPS) out.push({ x: -w / 2 + leftW / 2, z: 0, w: leftW, d })
const rightW = w / 2 - right
if (rightW > EPS) out.push({ x: right + rightW / 2, z: 0, w: rightW, d })
const holeW = right - left
const nearD = near + d / 2
if (nearD > EPS) out.push({ x: (left + right) / 2, z: -d / 2 + nearD / 2, w: holeW, d: nearD })
const farD = d / 2 - far
if (farD > EPS) out.push({ x: (left + right) / 2, z: far + farD / 2, w: holeW, d: farD })
return out
}
export function Floor({
position,
rotation = [0, 0, 0],
size = [8, 8],
thickness = 0.2,
opening,
color,
}: FloorProps) {
const { unit, palette, ground } = useWorld()
const floorColor = color ?? palette.floor
const w = size[0] * unit
const d = size[1] * unit
const t = thickness * unit
const scaled = opening && {
x: (opening.x ?? 0) * unit,
z: (opening.z ?? 0) * unit,
width: opening.width * unit,
depth: opening.depth * unit,
}
// Top surface sits at the world ground baseline by default; explicit position wins.
const pos = position ?? [0, ground, 0]
return (
<RigidBody type="fixed" colliders="cuboid" position={pos} rotation={rotation}>
{slabs(w, d, scaled).map((s) => (
<mesh
key={`${s.x.toFixed(3)}:${s.z.toFixed(3)}`}
receiveShadow
position={[s.x, -t / 2, s.z]}
>
<boxGeometry args={[s.w, t, s.d]} />
<meshStandardMaterial color={floorColor} />
</mesh>
))}
</RigidBody>
)
}
Floor.surface = floorSurface surfaces/floor.ts
import type { SurfaceDef } from '@runek/core'
interface FloorShape {
position?: unknown
size?: [number, number]
opening?: { x?: number; z?: number; width: number; depth: number }
}
/** Floor's slab top, minus its stairwell opening. An unplaced Floor sits at `world.ground`. */
export const floorSurface: SurfaceDef = {
kind: 'deck',
build(props, { unit, ground }) {
const { position, size = [8, 8], opening } = props as FloorShape
const top = position === undefined ? ground : 0
const hw = (size[0] * unit) / 2
const hd = (size[1] * unit) / 2
const hole = opening && {
x: (opening.x ?? 0) * unit,
z: (opening.z ?? 0) * unit,
hw: (opening.width * unit) / 2,
hd: (opening.depth * unit) / 2,
}
return (x, z) => {
if (Math.abs(x) > hw || Math.abs(z) > hd) return null
if (hole && Math.abs(x - hole.x) < hole.hw && Math.abs(z - hole.z) < hole.hd) return null
return top
}
},
}