component

Arch

Freestanding gateway: two piers and a semicircular arch of voussoirs. Composes with Wall.

⌖ Explore the library in 3D → · View as Markdown

Tune every prop in the workshop →

Add it

npx @runek/cli add arch

Pulls @react-three/rapier@^2.2.0, @runek/core@^0.15.0.

Use it

import { Arch } from './runek/Arch'

<Arch position={[0, 0, 0]} />

Or as a node in a world file (worlds as data):

{ "type": "Arch", "props": { "position": [0, 0, 0] } }

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.
seed number Accepted for the component contract; this component has no seeded variation yet.
width number 2.4 Clear opening width, in units.
height number 2.6 Height to the springline (top of the piers), in units.
depth number 0.6 Depth, in units.
thickness number 0.4 Pier thickness, in units.
blocks number 9 Voussoir blocks forming the semicircular arch.
color color palette stone Defaults to the world palette’s stone slot.

Registry manifest

registry manifest /r/components/arch.json Self-contained JSON: inlined source plus resolved dependencies, exactly what runek add arch fetches.

Browse the whole catalog in the gallery →.

Source (Arch.tsx)
import { CuboidCollider, RigidBody } from '@react-three/rapier'
import { useWorld, type Vec3 } from '@runek/core'
import { useMemo } from 'react'

export interface ArchProps {
  position?: Vec3
  rotation?: Vec3
  /** Accepted for the component contract; this component has no seeded variation yet. */
  seed?: number
  /** Clear opening width, in units. */
  width?: number
  /** Height to the springline (top of the piers), in units. */
  height?: number
  depth?: number
  /** Pier thickness, in units. */
  thickness?: number
  /** Voussoir blocks forming the semicircular arch. */
  blocks?: number
  /** Defaults to the world palette's `stone` slot. */
  color?: string
}

/** A freestanding gateway: two piers and a semicircular arch of voussoirs.
 *  Composes with `Wall` for an entrance. The piers carry the colliders; the
 *  arch crown sits overhead. */
export function Arch({
  position = [0, 0, 0],
  rotation = [0, 0, 0],
  width = 2.4,
  height = 2.6,
  depth = 0.6,
  thickness = 0.4,
  blocks = 9,
  color,
}: ArchProps) {
  const { unit, palette } = useWorld()
  const stone = color ?? palette.stone
  const w = width * unit
  const h = height * unit
  const d = depth * unit
  const th = thickness * unit
  const pierX = w / 2 + th / 2
  const rMid = w / 2 + th / 2

  const voussoirs = useMemo(() => {
    const arc = ((Math.PI * rMid) / blocks) * 1.06
    return Array.from({ length: blocks }, (_, i) => {
      const a = ((i + 0.5) / blocks) * Math.PI
      return { x: rMid * Math.cos(a), y: h + rMid * Math.sin(a), rot: a, arc }
    })
  }, [rMid, blocks, h])

  return (
    <RigidBody type="fixed" colliders={false} position={position} rotation={rotation}>
      <CuboidCollider args={[th / 2, h / 2, d / 2]} position={[-pierX, h / 2, 0]} />
      <CuboidCollider args={[th / 2, h / 2, d / 2]} position={[pierX, h / 2, 0]} />

      {[-1, 1].map((s) => (
        <mesh key={`pier-${s}`} castShadow receiveShadow position={[s * pierX, h / 2, 0]}>
          <boxGeometry args={[th, h, d]} />
          <meshStandardMaterial color={stone} roughness={0.9} />
        </mesh>
      ))}

      {voussoirs.map((v) => (
        <mesh
          key={`v-${v.x.toFixed(3)}:${v.y.toFixed(3)}`}
          castShadow
          receiveShadow
          position={[v.x, v.y, 0]}
          rotation={[0, 0, v.rot]}
        >
          <boxGeometry args={[th, v.arc, d]} />
          <meshStandardMaterial color={stone} roughness={0.9} />
        </mesh>
      ))}
    </RigidBody>
  )
}

Arch.groundSitting = true