component
Tableware
Tableware to set a table: a plate, bowl, glass, cup, or water jug turned from a profile, or a whole place setting. Drinks fill to a line; plates and bowls stack.
⌖ Explore the library in 3D → · View as Markdown
Add it
npx @runek/cli add tableware
Pulls @runek/core@^0.15.0, three@^0.184.0.
Use it
import { Tableware } from './runek/Tableware'
<Tableware position={[0, 0, 0]} seed={1} />
Or as a node in a world file (worlds as data):
{ "type": "Tableware", "props": { "position": [0, 0, 0], "seed": 1 } }
Props
| Prop | Type | Default | Description |
|---|---|---|---|
kind |
"glass" | "plate" | "bowl" | "cup" | "jug" | "setting" |
"plate" |
Which piece; a plate by default. |
size |
number |
1 |
Scale on the piece’s true size (a 26 cm plate, an 11 cm glass, a 23 cm jug). |
stack |
number |
1 |
Plates or bowls stacked on each other. |
fill |
number |
How full a glass, cup, or jug is, 0 to 1. Seeded when unset. | |
drinkColor |
color |
"#9fd3e8" |
The drink: water by default. |
color |
color |
Glaze for plates, bowls, and cups. Seeded (mostly white) when unset. | |
glassColor |
color |
"#d8eef6" |
Glass for glasses and the jug. |
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 |
1 |
Seed for the deterministic variation: same seed, same result. |
Registry manifest
registry manifest /r/components/tableware.json Self-contained JSON: inlined source plus resolved dependencies, exactly whatrunek add tableware fetches.
Browse the whole catalog in the gallery →.
Source (Tableware.tsx)
import { pick, range, rng, sub, useWorld, type WorldComponentProps } from '@runek/core'
import { useMemo } from 'react'
import { LatheGeometry, Vector2 } from 'three'
/** One piece, or a whole place `setting` (a plate, a glass, a knife, and a fork). */
export type TablewareKind = 'plate' | 'bowl' | 'glass' | 'cup' | 'jug' | 'setting'
export interface TablewareProps extends WorldComponentProps {
/** Which piece; a plate by default. */
kind?: TablewareKind
/** Scale on the piece's true size (a 26 cm plate, an 11 cm glass, a 23 cm jug). */
size?: number
/** Plates or bowls stacked on each other. */
stack?: number
/** How full a glass, cup, or jug is, 0 to 1. Seeded when unset. */
fill?: number
/** The drink: water by default. */
drinkColor?: string
/** Glaze for plates, bowls, and cups. Seeded (mostly white) when unset. */
color?: string
/** Glass for glasses and the jug. */
glassColor?: string
}
type Profile = [number, number][]
// Closed outlines in meters, (radius, height), from the axis at the foot round to the axis on top.
// `inner` is the cavity wall from the bottom up, which the drink fills.
const SHAPES: Record<Exclude<TablewareKind, 'setting'>, { outline: Profile; inner?: Profile }> = {
plate: {
outline: [
[0, 0],
[0.08, 0],
[0.085, 0.006],
[0.13, 0.02],
[0.127, 0.024],
[0.082, 0.01],
[0, 0.01],
],
},
bowl: {
outline: [
[0, 0],
[0.035, 0],
[0.04, 0.008],
[0.075, 0.05],
[0.08, 0.07],
[0.075, 0.07],
[0.07, 0.052],
[0.035, 0.012],
[0, 0.012],
],
},
glass: {
outline: [
[0, 0],
[0.03, 0],
[0.036, 0.11],
[0.033, 0.11],
[0.027, 0.008],
[0, 0.008],
],
inner: [
[0.027, 0.008],
[0.033, 0.11],
],
},
cup: {
outline: [
[0, 0],
[0.038, 0],
[0.041, 0.09],
[0.037, 0.09],
[0.034, 0.008],
[0, 0.008],
],
inner: [
[0.034, 0.008],
[0.037, 0.09],
],
},
jug: {
outline: [
[0, 0],
[0.058, 0],
[0.068, 0.05],
[0.064, 0.14],
[0.052, 0.2],
[0.058, 0.23],
[0.054, 0.23],
[0.048, 0.2],
[0.06, 0.14],
[0.064, 0.05],
[0.055, 0.006],
[0, 0.006],
],
inner: [
[0.055, 0.006],
[0.064, 0.05],
[0.06, 0.14],
[0.048, 0.2],
[0.054, 0.23],
],
},
}
const GLAZES = ['#f4f1ea', '#f4f1ea', '#ece6d8', '#dfe8ee', '#2f4a6b', '#c96f4a', '#3f6b52']
const SILVER = '#c8ccd2'
function lathe(points: Profile, k: number) {
return new LatheGeometry(
points.map(([r, y]) => new Vector2(r * k, y * k)),
28,
)
}
/** The drink: the cavity's wall cut at the fill line, closed to the axis top and bottom. */
function liquid(inner: Profile, fill: number, k: number) {
const top = inner[0][1] + (inner[inner.length - 1][1] - inner[0][1]) * fill * 0.92
const pts: Profile = [[0, inner[0][1]]]
for (let i = 0; i < inner.length; i++) {
const [r, y] = inner[i]
if (y <= top) {
pts.push([r * 0.97, y])
continue
}
const [r0, y0] = inner[i - 1]
pts.push([(r0 + ((r - r0) * (top - y0)) / (y - y0)) * 0.97, top])
break
}
if (pts[pts.length - 1][1] < top) pts.push([pts[pts.length - 1][0], top])
pts.push([0, top])
return lathe(pts, k)
}
function Piece({
kind,
k,
glaze,
glass,
drink,
fill,
stack = 1,
}: {
kind: Exclude<TablewareKind, 'setting'>
k: number
glaze: string
glass: string
drink: string
fill: number
stack?: number
}) {
const shape = SHAPES[kind]
const body = useMemo(() => lathe(shape.outline, k), [shape, k])
const water = useMemo(
() => (shape.inner && fill > 0 ? liquid(shape.inner, fill, k) : null),
[shape, fill, k],
)
const glassy = kind === 'glass' || kind === 'jug'
const material = glassy ? (
<meshPhysicalMaterial
color={glass}
roughness={0.05}
metalness={0}
transparent
opacity={0.35}
depthWrite={false}
side={2}
/>
) : (
<meshStandardMaterial color={glaze} roughness={0.25} side={2} />
)
const step = (kind === 'bowl' ? 0.03 : 0.012) * k
const layers = kind === 'plate' || kind === 'bowl' ? Math.max(1, Math.round(stack)) : 1
return (
<group>
{Array.from({ length: layers }, (_, i) => (
<mesh key={i} geometry={body} position={[0, i * step, 0]} castShadow>
{material}
</mesh>
))}
{water && (
<mesh geometry={water} renderOrder={-1}>
<meshStandardMaterial color={drink} roughness={0.1} transparent opacity={0.75} />
</mesh>
)}
{(kind === 'cup' || kind === 'jug') && (
// the handle: a half ring on the +X side
<mesh
position={[(kind === 'cup' ? 0.04 : 0.06) * k, (kind === 'cup' ? 0.048 : 0.13) * k, 0]}
rotation={[0, 0, -Math.PI / 2]}
castShadow
>
<torusGeometry args={[(kind === 'cup' ? 0.022 : 0.045) * k, 0.005 * k, 8, 16, Math.PI]} />
{material}
</mesh>
)}
</group>
)
}
/**
* Tableware to set a `Table` or a `Counter`: a plate, a bowl, a glass, a cup, or a water jug,
* each turned from a profile, or a whole place `setting`. Glasses, cups, and the jug hold a drink
* to a `fill` line. Its origin is the foot, so it sits on the top it is placed on; a setting faces
* the diner along local +Z. Plates and bowls stack. Decorative: no collider.
*/
export function Tableware({
position = [0, 0, 0],
rotation = [0, 0, 0],
seed = 1,
kind = 'plate',
size = 1,
stack = 1,
fill,
drinkColor = '#9fd3e8',
color,
glassColor = '#d8eef6',
}: TablewareProps) {
const { unit } = useWorld()
const k = size * unit
const look = useMemo(() => {
const r = rng(sub(seed, 0))
return { glaze: color ?? pick(r, GLAZES), fill: fill ?? range(r, 0.35, 0.85) }
}, [seed, color, fill])
const shared = { k, glaze: look.glaze, glass: glassColor, drink: drinkColor, fill: look.fill }
if (kind !== 'setting') {
return (
<group position={position} rotation={rotation}>
<Piece kind={kind} stack={stack} {...shared} />
</group>
)
}
const flat = 0.004 * k
return (
<group position={position} rotation={rotation}>
<Piece kind="plate" {...shared} />
<group position={[0.15 * k, 0, -0.14 * k]}>
<Piece kind="glass" {...shared} />
</group>
{/* knife on the right: the handle toward the diner, the blade away */}
<group position={[0.165 * k, flat / 2, 0]}>
<mesh position={[0, 0, 0.05 * k]} castShadow>
<boxGeometry args={[0.014 * k, flat, 0.1 * k]} />
<meshStandardMaterial color={SILVER} metalness={0.4} roughness={0.3} />
</mesh>
<mesh position={[-0.002 * k, 0, -0.055 * k]} castShadow>
<boxGeometry args={[0.02 * k, flat * 0.6, 0.11 * k]} />
<meshStandardMaterial color={SILVER} metalness={0.4} roughness={0.3} />
</mesh>
</group>
{/* fork on the left: a handle, a neck, four tines */}
<group position={[-0.165 * k, flat / 2, 0]}>
<mesh position={[0, 0, 0.055 * k]} castShadow>
<boxGeometry args={[0.012 * k, flat, 0.1 * k]} />
<meshStandardMaterial color={SILVER} metalness={0.4} roughness={0.3} />
</mesh>
<mesh position={[0, 0, -0.015 * k]}>
<boxGeometry args={[0.022 * k, flat, 0.04 * k]} />
<meshStandardMaterial color={SILVER} metalness={0.4} roughness={0.3} />
</mesh>
{[-1.5, -0.5, 0.5, 1.5].map((t) => (
<mesh key={t} position={[t * 0.0055 * k, 0, -0.06 * k]}>
<boxGeometry args={[0.003 * k, flat, 0.05 * k]} />
<meshStandardMaterial color={SILVER} metalness={0.4} roughness={0.3} />
</mesh>
))}
</group>
</group>
)
}