component
Dock
A plank jetty on pilings reaching out over water; walkable deck colliders, side stringers, and seaward mooring posts.
⌖ Explore the library in 3D → · View as Markdown
Add it
npx @runek/cli add dock
Pulls @react-three/rapier@^2.2.0, @runek/core@^0.15.0.
Use it
import { Dock } from './runek/Dock'
<Dock position={[0, 0, 0]} seed={1} />
Or as a node in a world file (worlds as data):
{ "type": "Dock", "props": { "position": [0, 0, 0], "seed": 1 } }
Props
| Prop | Type | Default | Description |
|---|---|---|---|
length |
number |
10 |
How far the jetty reaches out from the shore (local +Z), in units. |
width |
number |
3 |
Walkway width (local X), in units. |
depth |
number |
4 |
How deep the pilings sink below the deck surface, in units. |
color |
color |
Deck color; defaults to the world palette’s wood. |
|
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/dock.json Self-contained JSON: inlined source plus resolved dependencies, exactly whatrunek add dock fetches.
Browse the whole catalog in the gallery →.
Source (2 files)
Dock.tsx
import { CuboidCollider, RigidBody } from '@react-three/rapier'
import { rng, useWorld, type WorldComponentProps } from '@runek/core'
import { useMemo } from 'react'
import { dockSurface } from './surfaces/dock'
export interface DockProps extends WorldComponentProps {
/** How far the jetty reaches out from the shore (local +Z), in units. */
length?: number
/** Walkway width (local X), in units. */
width?: number
/** How deep the pilings sink below the deck surface, in units. */
depth?: number
/** Deck color; defaults to the world palette's `wood`. */
color?: string
}
/** A plank jetty on pilings that reaches out over water. The deck is the gameplay surface
* (a few collider slabs, not one per plank, like `Bridge`); the pilings, side stringers, and
* the two seaward mooring posts are decorative. The local origin sits at the deck surface on
* the shore end, so `position` is where the jetty meets land and it runs out along local +Z. */
export function Dock({
position = [0, 0, 0],
rotation = [0, 0, 0],
seed = 1,
length = 10,
width = 3,
depth = 4,
color,
}: DockProps) {
const { unit, palette } = useWorld()
const deckColor = color ?? palette.wood
const postColor = palette.woodDark
const L = length * unit
const W = width * unit
const D = depth * unit
const deckT = 0.12 * unit
const deckY = -deckT / 2
const postX = W / 2 - 0.25 * unit
const postR = 0.16 * unit
const plankCount = Math.max(6, Math.round(length / 0.4))
const planks = useMemo(() => {
const next = rng(seed)
const pd = L / plankCount
return Array.from({ length: plankCount }, (_, i) => ({
z: (i + 0.5) * pd,
d: pd * (0.84 + next() * 0.1),
}))
}, [L, plankCount, seed])
const SLABS = 5
const slabLen = L / SLABS
// rows of pilings ("bents") along the jetty, including both ends
const bents = useMemo(() => {
const n = Math.max(2, Math.round(length / 3))
return Array.from({ length: n + 1 }, (_, i) => (i / n) * L)
}, [length, L])
return (
<RigidBody type="fixed" colliders={false} position={position} rotation={rotation}>
{/* deck collider slabs */}
{Array.from({ length: SLABS }, (_, i) => (i + 0.5) * slabLen).map((z) => (
<CuboidCollider
key={`col-${z.toFixed(3)}`}
args={[W / 2, deckT / 2, slabLen / 2]}
position={[0, deckY, z]}
/>
))}
{/* cross planks */}
{planks.map((p) => (
<mesh key={`plank-${p.z.toFixed(3)}`} castShadow receiveShadow position={[0, deckY, p.z]}>
<boxGeometry args={[W, deckT, p.d]} />
<meshStandardMaterial color={deckColor} roughness={0.85} />
</mesh>
))}
{/* side stringers under the deck */}
{[-1, 1].map((s) => (
<mesh key={`stringer-${s}`} position={[s * (W / 2 - 0.1 * unit), deckY - deckT, L / 2]}>
<boxGeometry args={[0.12 * unit, 0.2 * unit, L]} />
<meshStandardMaterial color={postColor} roughness={0.85} />
</mesh>
))}
{/* pilings, sunk into the water */}
{bents.map((z) =>
[-1, 1].map((s) => (
<mesh key={`pile-${z.toFixed(3)}-${s}`} castShadow position={[s * postX, -D / 2, z]}>
<cylinderGeometry args={[postR, postR, D, 8]} />
<meshStandardMaterial color={postColor} roughness={0.9} />
</mesh>
)),
)}
{/* seaward mooring posts */}
{[-1, 1].map((s) => (
<mesh key={`bollard-${s}`} castShadow position={[s * postX, 0.25 * unit, L - 0.3 * unit]}>
<cylinderGeometry args={[0.18 * unit, 0.2 * unit, 0.8 * unit, 8]} />
<meshStandardMaterial color={postColor} roughness={0.9} />
</mesh>
))}
</RigidBody>
)
}
Dock.surface = dockSurface surfaces/dock.ts
import type { SurfaceDef } from '@runek/core'
/** Dock's deck top: its origin sits on the deck at the shore end, running out along local +Z. */
export const dockSurface: SurfaceDef = {
kind: 'deck',
build(props, { unit }) {
const hw = (((props.width as number | undefined) ?? 3) * unit) / 2
const length = ((props.length as number | undefined) ?? 10) * unit
return (x, z) => (Math.abs(x) <= hw && z >= 0 && z <= length ? 0 : null)
},
}