component

Floor

Flat floor slab with a fixed collider; optional stairwell opening.

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

Tune every prop in the workshop →

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 what runek 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
    }
  },
}