component
Slab
A rounded structural plate (pill or ellipse) for curved-modernist decks, cantilevered roofs, terraces, or a lawn disc. Walkable, with a single convex-hull collider.
⌖ Explore the library in 3D → · View as Markdown
Add it
npx @runek/cli add slab
Pulls @react-three/rapier@^2.2.0, @runek/core@^0.15.0, three@^0.184.0.
Use it
import { Slab } from './runek/Slab'
<Slab position={[0, 0, 0]} />
Or as a node in a world file (worlds as data):
{ "type": "Slab", "props": { "position": [0, 0, 0] } }
Props
| Prop | Type | Default | Description |
|---|---|---|---|
size |
[number, number] |
[8, 8] |
Footprint [width, depth], in units. The top surface sits at the component origin. |
shape |
"pill" | "disc" |
"pill" |
pill is a rounded rectangle, disc an ellipse. |
cornerRadius |
number |
Pill corner radius, in units; clamped to half the smaller extent (a full stadium). | |
thickness |
number |
0.35 |
Thickness, in units. |
curveSegments |
number |
24 |
Curve smoothness of the rounded edges. |
color |
color |
palette wall |
Defaults to the world palette’s wall slot. |
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. |
Registry manifest
registry manifest /r/components/slab.json Self-contained JSON: inlined source plus resolved dependencies, exactly whatrunek add slab fetches.
Browse the whole catalog in the gallery →.
Source (2 files)
Slab.tsx
import { RigidBody } from '@react-three/rapier'
import { useWorld, type WorldComponentProps } from '@runek/core'
import { useEffect, useMemo } from 'react'
import { ExtrudeGeometry, Shape } from 'three'
import { slabSurface } from './surfaces/slab'
export type SlabShape = 'pill' | 'disc'
export interface SlabProps extends WorldComponentProps {
/** Footprint `[width, depth]`, in units. The top surface sits at the component origin. */
size?: [number, number]
/** `pill` is a rounded rectangle, `disc` an ellipse. */
shape?: SlabShape
/** Pill corner radius, in units; clamped to half the smaller extent (a full stadium). */
cornerRadius?: number
thickness?: number
/** Curve smoothness of the rounded edges. */
curveSegments?: number
/** Defaults to the world palette's `wall` slot. */
color?: string
}
/**
* A rounded structural plate: an extruded pill or ellipse you can stack into curved-modernist
* floor decks, cantilevered roofs, terraces, or a lawn disc. Convex by construction, so a single
* hull collider carries it. Flat-topped and walkable; the top surface sits at the origin like
* `Floor`, so decks and their furniture share a Y.
*/
export function Slab({
position = [0, 0, 0],
rotation = [0, 0, 0],
size = [8, 8],
shape = 'pill',
cornerRadius,
thickness = 0.35,
curveSegments = 24,
color,
}: SlabProps) {
const { unit, palette } = useWorld()
const slabColor = color ?? palette.wall
const w = size[0] * unit
const d = size[1] * unit
const t = thickness * unit
const r = Math.min((cornerRadius ?? Math.min(size[0], size[1]) / 2) * unit, Math.min(w, d) / 2)
const geometry = useMemo(() => {
const outline = new Shape()
if (shape === 'disc') {
outline.absellipse(0, 0, w / 2, d / 2, 0, Math.PI * 2, false, 0)
} else {
const hw = w / 2
const hd = d / 2
outline.moveTo(-hw + r, -hd)
outline.lineTo(hw - r, -hd)
outline.absarc(hw - r, -hd + r, r, -Math.PI / 2, 0, false)
outline.lineTo(hw, hd - r)
outline.absarc(hw - r, hd - r, r, 0, Math.PI / 2, false)
outline.lineTo(-hw + r, hd)
outline.absarc(-hw + r, hd - r, r, Math.PI / 2, Math.PI, false)
outline.lineTo(-hw, -hd + r)
outline.absarc(-hw + r, -hd + r, r, Math.PI, Math.PI * 1.5, false)
}
const g = new ExtrudeGeometry(outline, { depth: t, bevelEnabled: false, curveSegments })
g.rotateX(-Math.PI / 2) // shape XY → world XZ, extrusion → up
g.translate(0, -t, 0) // top surface at the origin
return g
}, [shape, w, d, r, t, curveSegments])
useEffect(() => () => geometry.dispose(), [geometry])
return (
<RigidBody type="fixed" colliders="hull" position={position} rotation={rotation}>
<mesh geometry={geometry} castShadow receiveShadow>
<meshStandardMaterial color={slabColor} />
</mesh>
</RigidBody>
)
}
Slab.surface = slabSurface surfaces/slab.ts
import type { SurfaceDef } from '@runek/core'
interface SlabShape {
size?: [number, number]
shape?: 'pill' | 'disc'
cornerRadius?: number
}
/** Slab's top: a rounded rectangle (pill) or an ellipse (disc) at the origin. */
export const slabSurface: SurfaceDef = {
kind: 'deck',
build(props, { unit }) {
const { size = [8, 8], shape = 'pill', cornerRadius } = props as SlabShape
const hw = (size[0] * unit) / 2
const hd = (size[1] * unit) / 2
if (shape === 'disc') {
return (x, z) => ((x / hw) ** 2 + (z / hd) ** 2 <= 1 ? 0 : null)
}
const r = Math.min((cornerRadius ?? Math.min(size[0], size[1]) / 2) * unit, Math.min(hw, hd))
return (x, z) => {
const ax = Math.abs(x)
const az = Math.abs(z)
if (ax > hw || az > hd) return null
const cx = ax - (hw - r)
const cz = az - (hd - r)
return cx > 0 && cz > 0 && cx * cx + cz * cz > r * r ? null : 0
}
},
}