import { useMemo } from 'react'; import { CEILING_Y, DOOR_CLEAR_HALF, DOOR_CLEAR_Y, EMBED, WALL_INNER, shade, type RoomDims, type WallId, } from './geometry'; /** Wall-flush building blocks shared by the period interiors in this folder. */ interface SurfaceProps { color: string; roughness?: number; metalness?: number; emissive?: string; emissiveIntensity?: number; } /** A moulding running the length of one wall, flush against its inner face. */ export function WallBand({ wall, room, y, height, proud = 0.1, span = 1, color, roughness = 0.7, metalness = 0.06, emissive, emissiveIntensity, }: SurfaceProps & { wall: WallId; room: RoomDims; y: number; height: number; proud?: number; span?: number; }) { const alongZ = wall === 'left' || wall === 'right'; const thick = proud + EMBED * 2; const off = WALL_INNER + proud / 2 - EMBED; const matProps = { color, roughness, metalness, emissive, emissiveIntensity, }; // End-wall bands that sit in the door height must leave a clear gap for exits. const crossesDoor = !alongZ && y - height / 2 < DOOR_CLEAR_Y && room.halfW > DOOR_CLEAR_HALF + 0.35; if (crossesDoor) { const z = wall === 'back' ? -room.halfD + off : room.halfD - off; const sideLen = room.halfW - DOOR_CLEAR_HALF; const leftX = (-room.halfW - DOOR_CLEAR_HALF) / 2; const rightX = (room.halfW + DOOR_CLEAR_HALF) / 2; return ( ); } const len = (alongZ ? room.depth : room.width) * span; const args: [number, number, number] = alongZ ? [thick, height, len] : [len, height, thick]; const pos: [number, number, number] = wall === 'back' ? [0, y, -room.halfD + off] : wall === 'front' ? [0, y, room.halfD - off] : wall === 'left' ? [-room.halfW + off, y, 0] : [room.halfW - off, y, 0]; return ( ); } /** A moulding ring running right around the room at one height. */ export function CorniceRing({ room, y, height, proud = 0.12, walls = ['back', 'front', 'left', 'right'], ...surface }: SurfaceProps & { room: RoomDims; y: number; height: number; proud?: number; walls?: WallId[]; }) { return ( {walls.map((wall) => ( ))} ); } /** String course / picture rail on the two side walls only. */ export function SideRails({ room, y, height, proud = 0.1, ...surface }: SurfaceProps & { room: RoomDims; y: number; height: number; proud?: number }) { return ( {(['left', 'right'] as const).map((wall) => ( ))} ); } /** * Vertical shaft with carved flutes, read as alternating light stone and * shadowed grooves. Sits flush: the caller places it against a wall face. */ export function FlutedShaft({ height, radius, color, flutes = 12, taper = 0.9, facing = 'x', }: { height: number; radius: number; color: string; flutes?: number; taper?: number; /** Wall normal axis — grooves are only cut on the exposed half. */ facing?: 'x' | 'z'; }) { const groove = shade(color, -0.28); const arcs = useMemo( () => Array.from({ length: flutes }, (_, i) => (i / flutes) * Math.PI * 2), [flutes] ); return ( {arcs.map((a, i) => { const dx = Math.cos(a) * radius * 0.94; const dz = Math.sin(a) * radius * 0.94; // Skip the grooves buried in the wall behind the shaft. if (facing === 'x' ? dx > 0 : dz > 0) return null; return ( ); })} ); } export function TuscanCapital({ y, size, color }: { y: number; size: number; color: string }) { return ( ); } /** Ionic capital — abacus over a pair of spiral volutes. */ export function IonicCapital({ y, size, color, axis = 'z', }: { y: number; size: number; color: string; /** Axis the volute pair spreads along. */ axis?: 'x' | 'z'; }) { return ( {[-1, 1].map((s) => ( ))} ); } /** Moulded plinth under a shaft. */ export function ShaftBase({ size, color }: { size: number; color: string }) { return ( ); } /** Semicircular arch drawn as a chord of straight voussoir blocks. */ export function RoundArch({ span, rise, thickness = 0.14, depth = 0.16, color, segments = 11, roughness = 0.72, metalness = 0.05, }: { span: number; rise: number; thickness?: number; depth?: number; color: string; segments?: number; roughness?: number; metalness?: number; }) { const blocks = useMemo(() => { const out: { x: number; y: number; len: number; rot: number }[] = []; for (let s = 0; s < segments; s++) { const t0 = s / segments; const t1 = (s + 1) / segments; const at = (t: number) => { const a = Math.PI * (1 - t); return { x: Math.cos(a) * (span / 2), y: Math.sin(a) * rise }; }; const p0 = at(t0); const p1 = at(t1); out.push({ x: (p0.x + p1.x) / 2, y: (p0.y + p1.y) / 2, len: Math.hypot(p1.x - p0.x, p1.y - p0.y) * 1.08, rot: Math.atan2(p1.y - p0.y, p1.x - p0.x), }); } return out; }, [span, rise, segments]); return ( {blocks.map((b, i) => ( ))} ); } /** Recessed coffer grid under the ceiling plane. */ export function CofferedCeiling({ room, y = CEILING_Y, cell = 2.3, panelColor, ribColor, ribWidth = 0.16, ribDrop = 0.12, rosette, }: { room: RoomDims; y?: number; cell?: number; panelColor: string; ribColor: string; ribWidth?: number; ribDrop?: number; rosette?: string; }) { const grid = useMemo(() => { const cols = Math.max(2, Math.min(6, Math.round(room.width / cell))); const rows = Math.max(2, Math.min(8, Math.round(room.depth / cell))); const cw = room.width / cols; const cd = room.depth / rows; const cells: { x: number; z: number }[] = []; for (let c = 0; c < cols; c++) { for (let r = 0; r < rows; r++) { cells.push({ x: -room.halfW + cw * (c + 0.5), z: -room.halfD + cd * (r + 0.5) }); } } return { cells, cw, cd, cols, rows }; }, [room.width, room.depth, room.halfW, room.halfD, cell]); return ( {grid.cells.map((c, i) => ( {rosette && ( )} ))} {/* Ribs between the coffers */} {Array.from({ length: grid.cols + 1 }, (_, c) => ( ))} {Array.from({ length: grid.rows + 1 }, (_, r) => ( ))} ); } /** Exposed structural beams spanning the room. */ export function CeilingBeams({ room, positions, axis = 'x', width = 0.22, height = 0.3, y = CEILING_Y - 0.16, color, roughness = 0.82, metalness = 0.05, }: SurfaceProps & { room: RoomDims; positions: number[]; /** Axis the beams run along. */ axis?: 'x' | 'z'; width?: number; height?: number; y?: number; }) { return ( {positions.map((p, i) => ( ))} ); } /** Plaster ceiling rose, the anchor a salon chandelier hangs from. */ export function CeilingRose({ y = CEILING_Y, radius = 1.0, color, rings = 3, }: { y?: number; radius?: number; color: string; rings?: number; }) { return ( {Array.from({ length: rings }, (_, i) => { const t = (i + 1) / rings; return ( ); })} ); } /** * Hanging light fixture. `light` adds a real point light — keep at most two per * hall so the shared track lighting stays inside the WebGL light budget. */ export function HangingFixture({ position, dropLength = 0.6, radius = 0.42, arms = 6, metalColor, flameColor = '#ffcf80', glowColor = '#ff9830', light = false, lightColor = '#ffb060', lightIntensity = 0.8, lightDistance = 7, }: { position: [number, number, number]; dropLength?: number; radius?: number; arms?: number; metalColor: string; flameColor?: string; glowColor?: string; light?: boolean; lightColor?: string; lightIntensity?: number; lightDistance?: number; }) { const ringY = position[1] - dropLength; return ( {Array.from({ length: arms }, (_, i) => { const a = (i / arms) * Math.PI * 2; return ( ); })} {light && ( )} ); } /** * Wall light mounted above the frame line. Emissive only — sconces are for * period character, not for lighting the hall. */ export function WallSconce({ wall, room, along, y = 2.86, armColor, glowColor = '#ffb45a', shadeColor = '#ffdca8', }: { wall: 'left' | 'right' | 'back'; room: RoomDims; /** Offset along the wall from its centre. */ along: number; y?: number; armColor: string; glowColor?: string; shadeColor?: string; }) { const pos: [number, number, number] = wall === 'back' ? [along, y, -room.halfD + WALL_INNER + 0.02] : wall === 'left' ? [-room.halfW + WALL_INNER + 0.02, y, along] : [room.halfW - WALL_INNER - 0.02, y, along]; const rot: [number, number, number] = wall === 'back' ? [0, 0, 0] : wall === 'left' ? [0, Math.PI / 2, 0] : [0, -Math.PI / 2, 0]; return ( ); }