component
CurvedWall
An arc of wall around the origin — solid concrete or a floor-to-ceiling glass curtain wall with mullions. Compose several arcs to leave door gaps; chords carry cuboid colliders.
⌖ Explore the library in 3D → · View as Markdown
Add it
npx @runek/cli add curvedwall
Pulls @react-three/rapier@^2.2.0, @runek/core@^0.15.0, three@^0.184.0.
Use it
import { CurvedWall } from './runek/CurvedWall'
<CurvedWall position={[0, 0, 0]} />
Or as a node in a world file (worlds as data):
{ "type": "CurvedWall", "props": { "position": [0, 0, 0] } }
Props
| Prop | Type | Default | Description |
|---|---|---|---|
radius |
number |
6 |
Arc centerline radius, in units. The component origin is the circle’s center. |
arc |
number |
1.5707963267948966 |
Sweep angle in radians, centered on the local +Z axis. |
height |
number |
3 |
Height, in units. |
thickness |
number |
0.15 |
Thickness, in units. |
style |
"solid" | "glass" |
"solid" |
glass renders a transparent curtain wall with mullions; solid a plain wall. |
segments |
number |
Chord segments; defaults from radius × arc, more segments reading as smoother. |
|
color |
color |
palette wall |
Solid wall color; defaults to the world palette’s wall slot. |
glassColor |
color |
"#b7d8e8" |
Glass tint (glass style). |
frameColor |
color |
palette metal |
Mullion and rail color (glass style); defaults to the palette’s metal 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/curvedwall.json Self-contained JSON: inlined source plus resolved dependencies, exactly whatrunek add curvedwall fetches.
Browse the whole catalog in the gallery →.
Source (CurvedWall.tsx)
import { CuboidCollider, RigidBody } from '@react-three/rapier'
import { useWorld, type WorldComponentProps } from '@runek/core'
import { useLayoutEffect, useMemo, useRef } from 'react'
import * as THREE from 'three'
export type CurvedWallStyle = 'solid' | 'glass'
export interface CurvedWallProps extends WorldComponentProps {
/** Arc centerline radius, in units. The component origin is the circle's center. */
radius?: number
/** Sweep angle in radians, centered on the local +Z axis. */
arc?: number
height?: number
thickness?: number
/** `glass` renders a transparent curtain wall with mullions; `solid` a plain wall. */
style?: CurvedWallStyle
/** Chord segments; defaults from `radius × arc`, more segments reading as smoother. */
segments?: number
/** Solid wall color; defaults to the world palette's `wall` slot. */
color?: string
/** Glass tint (glass style). */
glassColor?: string
/** Mullion and rail color (glass style); defaults to the palette's `metal` slot. */
frameColor?: string
}
interface Chord {
/** Chord midpoint angle from local +Z. */
angle: number
x: number
z: number
length: number
}
/**
* An arc of wall around the component origin — the curved-architecture counterpart of `Wall`.
* `solid` reads as concrete; `glass` as a floor-to-ceiling curtain wall with mullions at every
* chord joint. Leave door gaps by composing several arcs. Chords render instanced and each
* carries a cuboid collider, so the wall blocks like the curve it draws.
*/
export function CurvedWall({
position = [0, 0, 0],
rotation = [0, 0, 0],
radius = 6,
arc = Math.PI / 2,
height = 3,
thickness = 0.15,
style = 'solid',
segments,
color,
glassColor = '#b7d8e8',
frameColor,
}: CurvedWallProps) {
const { unit, palette } = useWorld()
const wallColor = color ?? palette.wall
const frame = frameColor ?? palette.metal
const R = radius * unit
const h = height * unit
const t = thickness * unit
const count = segments ?? Math.min(32, Math.max(4, Math.round((R * arc) / (0.9 * unit))))
const glass = style === 'glass'
const chordsRef = useRef<THREE.InstancedMesh>(null)
const mullionsRef = useRef<THREE.InstancedMesh>(null)
const railsRef = useRef<THREE.InstancedMesh>(null)
const { chords, joints } = useMemo(() => {
const step = arc / count
// A hair of overlap keeps seams closed on the outside of the curve.
const chordLength = 2 * R * Math.sin(step / 2) + t * 0.5
const chords: Chord[] = []
const joints: { angle: number; x: number; z: number }[] = []
for (let i = 0; i < count; i++) {
const angle = -arc / 2 + (i + 0.5) * step
chords.push({ angle, x: Math.sin(angle) * R, z: Math.cos(angle) * R, length: chordLength })
}
for (let i = 0; i <= count; i++) {
const angle = -arc / 2 + i * step
joints.push({ angle, x: Math.sin(angle) * R, z: Math.cos(angle) * R })
}
return { chords, joints }
}, [R, arc, count, t])
const railH = 0.1 * unit
useLayoutEffect(() => {
const mesh = chordsRef.current
if (!mesh) return
const dummy = new THREE.Object3D()
const paneH = glass ? h - railH * 2 : h
chords.forEach((c, i) => {
dummy.position.set(c.x, glass ? railH + paneH / 2 : h / 2, c.z)
dummy.rotation.set(0, c.angle, 0)
dummy.scale.set(c.length, paneH, glass ? t * 0.4 : t)
dummy.updateMatrix()
mesh.setMatrixAt(i, dummy.matrix)
})
mesh.instanceMatrix.needsUpdate = true
}, [chords, h, t, glass, railH])
useLayoutEffect(() => {
const mesh = mullionsRef.current
if (!mesh || !glass) return
const dummy = new THREE.Object3D()
joints.forEach((j, i) => {
dummy.position.set(j.x, h / 2, j.z)
dummy.rotation.set(0, j.angle, 0)
dummy.scale.set(t * 0.8, h, t * 1.2)
dummy.updateMatrix()
mesh.setMatrixAt(i, dummy.matrix)
})
mesh.instanceMatrix.needsUpdate = true
}, [joints, h, t, glass])
useLayoutEffect(() => {
const mesh = railsRef.current
if (!mesh || !glass) return
const dummy = new THREE.Object3D()
chords.forEach((c, i) => {
for (let level = 0; level < 2; level++) {
dummy.position.set(c.x, level === 0 ? railH / 2 : h - railH / 2, c.z)
dummy.rotation.set(0, c.angle, 0)
dummy.scale.set(c.length, railH, t * 1.1)
dummy.updateMatrix()
mesh.setMatrixAt(i * 2 + level, dummy.matrix)
}
})
mesh.instanceMatrix.needsUpdate = true
}, [chords, h, t, glass, railH])
return (
<RigidBody type="fixed" colliders={false} position={position} rotation={rotation}>
{chords.map((c) => (
<CuboidCollider
key={c.angle.toFixed(4)}
args={[c.length / 2, h / 2, t / 2]}
position={[c.x, h / 2, c.z]}
rotation={[0, c.angle, 0]}
/>
))}
<instancedMesh
key={`c${count}${style}`}
ref={chordsRef}
args={[undefined, undefined, count]}
castShadow={!glass}
receiveShadow
>
<boxGeometry />
{glass ? (
<meshStandardMaterial color={glassColor} transparent opacity={0.28} roughness={0.08} />
) : (
<meshStandardMaterial color={wallColor} />
)}
</instancedMesh>
{glass && (
<>
<instancedMesh
key={`m${count}`}
ref={mullionsRef}
args={[undefined, undefined, count + 1]}
castShadow
>
<boxGeometry />
<meshStandardMaterial color={frame} roughness={0.6} />
</instancedMesh>
<instancedMesh
key={`r${count}`}
ref={railsRef}
args={[undefined, undefined, count * 2]}
castShadow
>
<boxGeometry />
<meshStandardMaterial color={frame} roughness={0.6} />
</instancedMesh>
</>
)}
</RigidBody>
)
}