Files
Art-gallery/client/src/components/hall-details/classical.tsx
T
Danila KhodjaefandClaude Opus 5 f2a256132c Give every movement its own period interior.
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>
2026-08-14 18:44:05 +03:00

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>
);
}