component
Pool
A built swimming pool flush with the deck: coping rim, walkable plastered basin with corner exit steps, and a translucent water surface.
⌖ Explore the library in 3D → · View as Markdown
Add it
npx @runek/cli add pool
Pulls @react-three/rapier@^2.2.0, @runek/core@^0.15.0.
Use it
import { Pool } from './runek/Pool'
<Pool position={[0, 0, 0]} />
Or as a node in a world file (worlds as data):
{ "type": "Pool", "props": { "position": [0, 0, 0] } }
Props
| Prop | Type | Default | Description |
|---|---|---|---|
size |
[number, number] |
[6, 3.5] |
Water surface [width, depth], in units. |
depth |
number |
1.6 |
Basin depth below the deck, in units. |
coping |
number |
0.35 |
Coping ledge width around the rim, in units. |
steps |
boolean |
true |
Build exit steps into one corner of the basin. |
copingColor |
color |
palette wall |
Coping color; defaults to the world palette’s wall slot. |
basinColor |
color |
"#bfe0e6" |
Basin plaster color. |
waterColor |
color |
palette waterShallow |
Defaults to the world palette’s waterShallow 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/pool.json Self-contained JSON: inlined source plus resolved dependencies, exactly whatrunek add pool fetches.
Browse the whole catalog in the gallery →.
Source (Pool.tsx)
import { CuboidCollider, RigidBody } from '@react-three/rapier'
import { useWorld, type WorldComponentProps } from '@runek/core'
export interface PoolProps extends WorldComponentProps {
/** Water surface `[width, depth]`, in units. */
size?: [number, number]
/** Basin depth below the deck, in units. */
depth?: number
/** Coping ledge width around the rim, in units. */
coping?: number
/** Build exit steps into one corner of the basin. */
steps?: boolean
/** Coping color; defaults to the world palette's `wall` slot. */
copingColor?: string
/** Basin plaster color. */
basinColor?: string
/** Defaults to the world palette's `waterShallow` slot. */
waterColor?: string
}
/**
* A built swimming pool, flush with the deck: a raised coping rim, a plastered basin you can
* walk (and jump) into, and a still translucent water surface just below the lip. The rim's top
* ledge sits at the component origin, so it drops into any `Slab`/`Floor` terrace at deck Y.
* For natural water, use `Lake`; for open sea, `Ocean`.
*/
export function Pool({
position = [0, 0, 0],
rotation = [0, 0, 0],
size = [6, 3.5],
depth = 1.6,
coping = 0.35,
steps = true,
copingColor,
basinColor = '#bfe0e6',
waterColor,
}: PoolProps) {
const { unit, palette } = useWorld()
const rim = copingColor ?? palette.wall
const water = waterColor ?? palette.waterShallow
const w = size[0] * unit
const d = size[1] * unit
const dep = depth * unit
const c = coping * unit
const lip = 0.07 * unit
const wallT = 0.12 * unit
const halfW = w / 2
const halfD = d / 2
const copingRun = [
// [x, z, length, depth-extent, rotY] — four ledge boxes framing the water
{ x: 0, z: -halfD - c / 2, w: w + c * 2, d: c },
{ x: 0, z: halfD + c / 2, w: w + c * 2, d: c },
{ x: -halfW - c / 2, z: 0, w: c, d },
{ x: halfW + c / 2, z: 0, w: c, d },
]
return (
<RigidBody type="fixed" colliders={false} position={position} rotation={rotation}>
{copingRun.map((s) => (
<mesh
key={`${s.x.toFixed(3)}:${s.z.toFixed(3)}`}
castShadow
receiveShadow
position={[s.x, 0, s.z]}
>
<boxGeometry args={[s.w, lip * 2, s.d]} />
<meshStandardMaterial color={rim} />
</mesh>
))}
{/* basin walls */}
{[
{ x: 0, z: -halfD + wallT / 2, w, d: wallT },
{ x: 0, z: halfD - wallT / 2, w, d: wallT },
{ x: -halfW + wallT / 2, z: 0, w: wallT, d },
{ x: halfW - wallT / 2, z: 0, w: wallT, d },
].map((s) => (
<mesh
key={`b${s.x.toFixed(3)}:${s.z.toFixed(3)}`}
receiveShadow
position={[s.x, -dep / 2, s.z]}
>
<boxGeometry args={[s.w, dep, s.d]} />
<meshStandardMaterial color={basinColor} />
</mesh>
))}
{/* basin floor */}
<mesh receiveShadow position={[0, -dep + wallT / 2, 0]}>
<boxGeometry args={[w, wallT, d]} />
<meshStandardMaterial color={basinColor} />
</mesh>
{/* water, just under the lip */}
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.14 * unit, 0]}>
<planeGeometry args={[w - wallT * 2, d - wallT * 2]} />
<meshStandardMaterial color={water} transparent opacity={0.55} roughness={0.05} />
</mesh>
{steps &&
[0, 1, 2].map((i) => {
// Corner stack descending into the basin: tread i tops out at -stepH·(i+1)
// and drops to the floor, so each box doubles as its own riser.
const stepH = dep / 4
const run = 0.4 * unit
const top = -stepH * (i + 1)
const boxH = dep - stepH * (i + 1)
const x = halfW - wallT - run * (i + 0.5)
const z = halfD - wallT - run
return (
<group key={`s${i}`}>
<mesh receiveShadow position={[x, top - boxH / 2, z]}>
<boxGeometry args={[run, boxH, run * 2]} />
<meshStandardMaterial color={basinColor} />
</mesh>
<CuboidCollider args={[run / 2, boxH / 2, run]} position={[x, top - boxH / 2, z]} />
</group>
)
})}
<CuboidCollider args={[w / 2, wallT / 2, d / 2]} position={[0, -dep + wallT / 2, 0]} />
<CuboidCollider args={[w / 2 + c, lip, c / 2]} position={[0, 0, -halfD - c / 2]} />
<CuboidCollider args={[w / 2 + c, lip, c / 2]} position={[0, 0, halfD + c / 2]} />
<CuboidCollider args={[c / 2, lip, d / 2]} position={[-halfW - c / 2, 0, 0]} />
<CuboidCollider args={[c / 2, lip, d / 2]} position={[halfW + c / 2, 0, 0]} />
</RigidBody>
)
}