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

Tune every prop in the workshop →

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