component
Crate
Slatted wooden crate with seeded plank shades; stacks via a cuboid collider.
⌖ Explore the library in 3D → · View as Markdown
Add it
npx @runek/cli add crate
Pulls @react-three/rapier@^2.2.0, @runek/core@^0.15.0, three@^0.184.0.
Use it
import { Crate } from './runek/Crate'
<Crate position={[0, 0, 0]} seed={1} />
Or as a node in a world file (worlds as data):
{ "type": "Crate", "props": { "position": [0, 0, 0], "seed": 1 } }
Props
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
size |
number |
0.8 |
Edge length, in units. |
color |
color |
palette wood |
Plank color; defaults to the world palette’s wood slot. |
seed |
number |
1 |
Seed for the deterministic variation: same seed, same result. |
Registry manifest
registry manifest /r/components/crate.json Self-contained JSON: inlined source plus resolved dependencies, exactly whatrunek add crate fetches.
Browse the whole catalog in the gallery →.
Source (Crate.tsx)
import { CuboidCollider, RigidBody } from '@react-three/rapier'
import { rng, useWorld, type Vec3 } from '@runek/core'
import { useMemo } from 'react'
import * as THREE from 'three'
export interface CrateProps {
position?: Vec3
rotation?: Vec3
/** Edge length, in units. */
size?: number
/** Plank color; defaults to the world palette's `wood` slot. */
color?: string
seed?: number
}
interface Beam {
pos: Vec3
size: Vec3
color: string
}
/** A slatted wooden crate: a paneled core inside a 12-edge plank frame, each plank
* seeded a slightly different shade. A clean cube with a cuboid collider, so it
* stacks. The world-building staple (see also Barrel). */
export function Crate({
position = [0, 0, 0],
rotation = [0, 0, 0],
size = 0.8,
color,
seed = 1,
}: CrateProps) {
const { unit, palette } = useWorld()
const wood = color ?? palette.wood
const s = size * unit
const b = 0.09 * s
const c = s / 2 - b / 2
const beams = useMemo<Beam[]>(() => {
const next = rng(seed)
const base = new THREE.Color(wood)
const tint = new THREE.Color()
const shade = () =>
tint
.copy(base)
.multiplyScalar(0.82 + next() * 0.32)
.getStyle()
const out: Beam[] = []
for (const sx of [-1, 1])
for (const sz of [-1, 1])
out.push({ pos: [sx * c, 0, sz * c], size: [b, s, b], color: shade() })
for (const sy of [-1, 1])
for (const sz of [-1, 1])
out.push({ pos: [0, sy * c, sz * c], size: [s, b, b], color: shade() })
for (const sy of [-1, 1])
for (const sx of [-1, 1])
out.push({ pos: [sx * c, sy * c, 0], size: [b, b, s], color: shade() })
return out
}, [s, b, c, wood, seed])
return (
<RigidBody type="fixed" colliders={false} position={position} rotation={rotation}>
<CuboidCollider args={[s / 2, s / 2, s / 2]} position={[0, s / 2, 0]} />
<group position={[0, s / 2, 0]}>
<mesh castShadow receiveShadow>
<boxGeometry args={[s * 0.9, s * 0.9, s * 0.9]} />
<meshStandardMaterial color={palette.woodDark} roughness={0.9} />
</mesh>
{beams.map((bm) => (
<mesh
key={`beam-${bm.pos[0].toFixed(2)}:${bm.pos[1].toFixed(2)}:${bm.pos[2].toFixed(2)}`}
castShadow
receiveShadow
position={bm.pos}
>
<boxGeometry args={bm.size} />
<meshStandardMaterial color={bm.color} roughness={0.85} />
</mesh>
))}
</group>
</RigidBody>
)
}
Crate.groundSitting = true