component
Fence
Posts and rails with seeded weathering; encloses yards and paths. One footprint collider.
⌖ Explore the library in 3D → · View as Markdown
Add it
npx @runek/cli add fence
Pulls @react-three/rapier@^2.2.0, @runek/core@^0.15.0.
Use it
import { Fence } from './runek/Fence'
<Fence position={[0, 0, 0]} seed={1} />
Or as a node in a world file (worlds as data):
{ "type": "Fence", "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. |
length |
number |
6 |
Total length along local X, in units. |
height |
number |
1.1 |
Height, in units. |
postSpacing |
number |
1.5 |
Spacing between posts, in units. |
rails |
number |
2 |
Number of horizontal rails. |
color |
color |
palette wood |
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/fence.json Self-contained JSON: inlined source plus resolved dependencies, exactly whatrunek add fence fetches.
Browse the whole catalog in the gallery →.
Source (Fence.tsx)
import { CuboidCollider, RigidBody } from '@react-three/rapier'
import { rng, useWorld, type Vec3 } from '@runek/core'
import { useMemo } from 'react'
export interface FenceProps {
position?: Vec3
rotation?: Vec3
/** Total length along local X, in units. */
length?: number
height?: number
/** Spacing between posts, in units. */
postSpacing?: number
/** Number of horizontal rails. */
rails?: number
/** Defaults to the world palette's `wood` slot. */
color?: string
seed?: number
}
interface Post {
x: number
/** Small seeded lean, for a weathered look. */
lean: number
}
/** A run of posts and rails. One footprint collider, not one body per post. */
export function Fence({
position = [0, 0, 0],
rotation = [0, 0, 0],
length = 6,
height = 1.1,
postSpacing = 1.5,
rails = 2,
color,
seed = 1,
}: FenceProps) {
const { unit, palette } = useWorld()
const woodColor = color ?? palette.wood
const L = length * unit
const h = height * unit
const postW = 0.1 * unit
const railT = 0.06 * unit
const depth = 0.08 * unit
const posts = useMemo<Post[]>(() => {
const next = rng(seed)
const span = postSpacing * unit
const n = Math.max(2, Math.round(L / span) + 1)
const step = L / (n - 1)
return Array.from({ length: n }, (_, i) => ({
x: -L / 2 + i * step,
lean: (next() - 0.5) * 0.05,
}))
}, [L, postSpacing, seed, unit])
const railYs = useMemo(
() => Array.from({ length: rails }, (_, i) => h * (0.85 - i * 0.42)),
[rails, h],
)
return (
<RigidBody type="fixed" colliders={false} position={position} rotation={rotation}>
<CuboidCollider args={[L / 2 + postW, h / 2, depth / 2]} position={[0, h / 2, 0]} />
{posts.map((p) => (
<mesh
key={`post-${p.x.toFixed(3)}`}
castShadow
receiveShadow
position={[p.x, h / 2, 0]}
rotation={[0, 0, p.lean]}
>
<boxGeometry args={[postW, h, depth]} />
<meshStandardMaterial color={woodColor} roughness={0.85} />
</mesh>
))}
{railYs.map((y) => (
<mesh key={`rail-${y.toFixed(3)}`} castShadow receiveShadow position={[0, y, 0]}>
<boxGeometry args={[L, railT, railT]} />
<meshStandardMaterial color={woodColor} roughness={0.85} />
</mesh>
))}
</RigidBody>
)
}
Fence.groundSitting = true