Movement halls routed 24 of the 26 movements through 8 generic detail kinds, four of which (modern, industrial, atelier, museum) rendered nothing at all, and `salon` was a single ceiling ring shared by eight movements. `details` is now a 26-value MovementDetailKind, one per movement, each built from the architecture of the same period as the movement itself. Gothic and Byzantine are unchanged and stay in MovementHallDetails.tsx as the reference implementations; the rest live under components/hall-details/, split by era, over shared wall-flush primitives. hall-details/geometry.ts records the budget the Gothic and Byzantine components established, since frames hang 0.23 m proud with a 0.7 m clear margin at each wall end: relief limits above and below the frame line, corner pockets for freestanding masses, doorway and title-band clearance, and a cap of two added lights per hall. Halls now pass their computed windows down so upper-wall panels, arches and roundels step around the openings. Also holds the pale interiors back from blowing out: gallery wall tints are authored around 0.50-0.56 luminance instead of near-white, and the 19 halls that read too bright take lightScale 0.30, matching the basilica. Verified by rendering all 26 interiors offline at two hall sizes and measuring the scene graph: nothing through walls, floor or ceiling, maximum 0.17 m intrusion into the hang zone, at most two added lights per hall. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
272 lines
10 KiB
TypeScript
272 lines
10 KiB
TypeScript
import {
|
|
CofferedCeiling,
|
|
CorniceRing,
|
|
FlutedShaft,
|
|
HangingFixture,
|
|
RoundArch,
|
|
ShaftBase,
|
|
TuscanCapital,
|
|
WallBand,
|
|
} from './primitives';
|
|
import {
|
|
shade,
|
|
useBayZ,
|
|
useCorners,
|
|
useRepeat,
|
|
useWallClearance,
|
|
type PeriodProps,
|
|
} from './geometry';
|
|
|
|
/**
|
|
* Roman atrium — Ancient / Classical.
|
|
*
|
|
* Engaged Tuscan half-columns on a bay rhythm carry a compressed but complete
|
|
* entablature (architrave, plain frieze, dentil course, projecting cornice).
|
|
* Below the frame line the walls carry the Pompeian red socle of a Fourth Style
|
|
* room; a bronze polycandelon hangs over the centre in place of ceiling lights.
|
|
*/
|
|
export function RomanAtriumDetails({ room, trim }: PeriodProps) {
|
|
const bays = useBayZ(room.depth, room.halfD, 4.0, 9);
|
|
const corners = useCorners(room.halfW, room.halfD, 0.1);
|
|
const stone = '#e6dcc6';
|
|
const shadowStone = shade(stone, -0.14);
|
|
const socle = '#7c3227';
|
|
|
|
const dentils = useRepeat(room.depth, 0.46, 30);
|
|
|
|
return (
|
|
<group>
|
|
{/* Pompeian socle with a black skirting course */}
|
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
|
<group key={wall}>
|
|
<WallBand wall={wall} room={room} y={0.62} height={1.16} proud={0.07} color={socle} roughness={0.8} metalness={0.04} />
|
|
<WallBand wall={wall} room={room} y={0.09} height={0.18} proud={0.09} color="#241a14" roughness={0.7} metalness={0.06} />
|
|
<WallBand wall={wall} room={room} y={1.24} height={0.08} proud={0.1} color={stone} roughness={0.66} metalness={0.06} />
|
|
</group>
|
|
))}
|
|
|
|
{/* Engaged half-columns on the bay rhythm */}
|
|
{bays.map((z, i) => (
|
|
<group key={i}>
|
|
{[-1, 1].map((side) => (
|
|
<group key={side} position={[side * (room.halfW - 0.115), 0, z]}>
|
|
<ShaftBase size={0.15} color={shadowStone} />
|
|
<group position={[0, 0.26, 0]}>
|
|
<FlutedShaft height={3.34} radius={0.135} color={stone} flutes={10} facing="x" />
|
|
</group>
|
|
<TuscanCapital y={3.72} size={0.15} color={stone} />
|
|
</group>
|
|
))}
|
|
</group>
|
|
))}
|
|
|
|
{/* Entablature: architrave, frieze and dentils along the colonnaded walls */}
|
|
{(['left', 'right'] as const).map((wall) => (
|
|
<group key={wall}>
|
|
<WallBand wall={wall} room={room} y={3.9} height={0.1} proud={0.16} color={shadowStone} roughness={0.7} />
|
|
<WallBand wall={wall} room={room} y={3.99} height={0.11} proud={0.13} color={stone} roughness={0.74} />
|
|
{dentils.map((z, i) => (
|
|
<mesh
|
|
key={i}
|
|
position={[(wall === 'left' ? -1 : 1) * (room.halfW - 0.16), 4.07, z]}
|
|
>
|
|
<boxGeometry args={[0.16, 0.07, 0.16]} />
|
|
<meshStandardMaterial color={shade(stone, 0.06)} roughness={0.7} metalness={0.05} />
|
|
</mesh>
|
|
))}
|
|
</group>
|
|
))}
|
|
|
|
{/* Projecting cornice returning around the whole room */}
|
|
<CorniceRing room={room} y={4.13} height={0.09} proud={0.19} color={trim} roughness={0.62} metalness={0.12} />
|
|
|
|
{/* Corner antae — square pilasters closing the order at each corner */}
|
|
{corners.map(([x, z], i) => (
|
|
<mesh key={i} position={[x, 1.95, z]}>
|
|
<boxGeometry args={[0.2, 3.9, 0.2]} />
|
|
<meshStandardMaterial color={shade(stone, 0.04)} roughness={0.76} metalness={0.04} />
|
|
</mesh>
|
|
))}
|
|
|
|
<HangingFixture
|
|
position={[0, 4.05, 0]}
|
|
dropLength={0.72}
|
|
radius={0.46}
|
|
arms={8}
|
|
metalColor={trim}
|
|
light
|
|
lightColor="#ffb862"
|
|
lightIntensity={0.9}
|
|
lightDistance={9}
|
|
/>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Florentine palazzo — Early Renaissance.
|
|
*
|
|
* Brunelleschi's grammar: flat pietra serena pilasters set against lime plaster,
|
|
* a blind arcade of semicircular arches springing between them, glazed roundels
|
|
* in the spandrels, and a painted-and-beamed quattrocento ceiling.
|
|
*/
|
|
export function QuattrocentoDetails({ room, trim, windows }: PeriodProps) {
|
|
const bays = useBayZ(room.depth, room.halfD, 3.8, 9);
|
|
const clearLeft = useWallClearance(windows, 'left');
|
|
const clearRight = useWallClearance(windows, 'right');
|
|
const serena = '#8b8f8a';
|
|
const serenaLight = shade(serena, 0.16);
|
|
const step = bays.length > 1 ? bays[1] - bays[0] : room.depth / 2;
|
|
|
|
return (
|
|
<group>
|
|
{/* Pietra serena dado with a moulded cap */}
|
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
|
<group key={wall}>
|
|
<WallBand wall={wall} room={room} y={0.6} height={1.2} proud={0.05} color={shade(serena, 0.24)} roughness={0.72} metalness={0.05} />
|
|
<WallBand wall={wall} room={room} y={1.24} height={0.09} proud={0.08} color={serena} roughness={0.62} metalness={0.07} />
|
|
</group>
|
|
))}
|
|
|
|
{/* Flat pilasters carrying the arcade */}
|
|
{bays.map((z, i) => (
|
|
<group key={i}>
|
|
{[-1, 1].map((side) => (
|
|
<group key={side} position={[side * (room.halfW - 0.05), 0, z]}>
|
|
<mesh position={[0, 1.78, 0]}>
|
|
<boxGeometry args={[0.08, 3.56, 0.34]} />
|
|
<meshStandardMaterial color={serena} roughness={0.66} metalness={0.07} />
|
|
</mesh>
|
|
<mesh position={[0, 0.11, 0]}>
|
|
<boxGeometry args={[0.11, 0.22, 0.44]} />
|
|
<meshStandardMaterial color={shade(serena, -0.08)} roughness={0.7} metalness={0.06} />
|
|
</mesh>
|
|
{/* Simplified Corinthian capital */}
|
|
<mesh position={[0, 3.68, 0]}>
|
|
<boxGeometry args={[0.1, 0.2, 0.44]} />
|
|
<meshStandardMaterial color={serenaLight} roughness={0.6} metalness={0.08} />
|
|
</mesh>
|
|
<mesh position={[0, 3.82, 0]}>
|
|
<boxGeometry args={[0.12, 0.09, 0.5]} />
|
|
<meshStandardMaterial color={serenaLight} roughness={0.58} metalness={0.08} />
|
|
</mesh>
|
|
</group>
|
|
))}
|
|
</group>
|
|
))}
|
|
|
|
{/* Blind arcade between the pilasters, plus a roundel in each spandrel */}
|
|
{bays.slice(0, -1).map((z, i) => {
|
|
const mid = z + step / 2;
|
|
return (
|
|
<group key={i}>
|
|
{([-1, 1] as const).map((side) => (
|
|
<group key={side} position={[side * (room.halfW - 0.05), 3.88, mid]} rotation={[0, Math.PI / 2, 0]}>
|
|
<RoundArch
|
|
span={step - 0.34}
|
|
rise={0.24}
|
|
thickness={0.12}
|
|
depth={0.09}
|
|
color={serena}
|
|
segments={9}
|
|
roughness={0.64}
|
|
metalness={0.07}
|
|
/>
|
|
{/* Glazed tondo, only where no window claims the bay */}
|
|
{(side < 0 ? clearLeft : clearRight)(mid, 0.22) && (
|
|
<mesh position={[0, -0.5, 0]}>
|
|
<torusGeometry args={[0.17, 0.035, 8, 20]} />
|
|
<meshStandardMaterial color={trim} roughness={0.42} metalness={0.35} />
|
|
</mesh>
|
|
)}
|
|
</group>
|
|
))}
|
|
</group>
|
|
);
|
|
})}
|
|
|
|
{/* Painted beam ceiling over the arcade */}
|
|
<CorniceRing room={room} y={4.11} height={0.1} proud={0.14} color={serena} roughness={0.64} metalness={0.07} />
|
|
<CofferedCeiling
|
|
room={room}
|
|
y={4.09}
|
|
cell={2.0}
|
|
panelColor="#e2d3ba"
|
|
ribColor="#8a5a30"
|
|
ribWidth={0.14}
|
|
ribDrop={0.14}
|
|
/>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Roman palazzo — High Renaissance.
|
|
*
|
|
* Bramante's cinquecento order: paired fluted Corinthian pilasters on a wide
|
|
* bay, a full marble entablature, panelled dado, and a rosetted coffer field
|
|
* modelled on the Pantheon soffit.
|
|
*/
|
|
export function CinquecentoDetails({ room, trim }: PeriodProps) {
|
|
const bays = useBayZ(room.depth, room.halfD, 5.2, 6);
|
|
const marble = '#efe9dd';
|
|
const grey = shade(marble, -0.16);
|
|
|
|
return (
|
|
<group>
|
|
{/* Panelled marble dado */}
|
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
|
<group key={wall}>
|
|
<WallBand wall={wall} room={room} y={0.58} height={1.16} proud={0.06} color={grey} roughness={0.44} metalness={0.1} />
|
|
<WallBand wall={wall} room={room} y={1.2} height={0.1} proud={0.09} color={marble} roughness={0.38} metalness={0.12} />
|
|
<WallBand wall={wall} room={room} y={0.07} height={0.14} proud={0.1} color={shade(grey, -0.18)} roughness={0.4} metalness={0.12} />
|
|
</group>
|
|
))}
|
|
|
|
{/* Paired fluted pilasters */}
|
|
{bays.map((z, i) => (
|
|
<group key={i}>
|
|
{[-1, 1].map((side) =>
|
|
[-0.19, 0.19].map((dz) => (
|
|
<group key={`${side}-${dz}`} position={[side * (room.halfW - 0.13), 0, z + dz]}>
|
|
<ShaftBase size={0.12} color={grey} />
|
|
<group position={[0, 0.24, 0]}>
|
|
<FlutedShaft height={3.32} radius={0.11} color={marble} flutes={8} facing="x" />
|
|
</group>
|
|
<mesh position={[0, 3.66, 0]}>
|
|
<cylinderGeometry args={[0.15, 0.1, 0.24, 12]} />
|
|
<meshStandardMaterial color={marble} roughness={0.42} metalness={0.1} />
|
|
</mesh>
|
|
<mesh position={[0, 3.82, 0]}>
|
|
<boxGeometry args={[0.12, 0.1, 0.34]} />
|
|
<meshStandardMaterial color={shade(marble, 0.06)} roughness={0.4} metalness={0.11} />
|
|
</mesh>
|
|
</group>
|
|
))
|
|
)}
|
|
</group>
|
|
))}
|
|
|
|
{/* Full entablature on the pilastered walls, cornice all round */}
|
|
{(['left', 'right'] as const).map((wall) => (
|
|
<group key={wall}>
|
|
<WallBand wall={wall} room={room} y={3.92} height={0.1} proud={0.15} color={marble} roughness={0.42} metalness={0.1} />
|
|
<WallBand wall={wall} room={room} y={4.02} height={0.1} proud={0.12} color={grey} roughness={0.46} metalness={0.1} />
|
|
</group>
|
|
))}
|
|
<CorniceRing room={room} y={4.12} height={0.09} proud={0.18} color={trim} roughness={0.4} metalness={0.28} />
|
|
|
|
<CofferedCeiling
|
|
room={room}
|
|
y={4.08}
|
|
cell={2.6}
|
|
panelColor="#f2ede3"
|
|
ribColor="#ded5c4"
|
|
ribWidth={0.2}
|
|
ribDrop={0.18}
|
|
rosette={trim}
|
|
/>
|
|
</group>
|
|
);
|
|
}
|