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>
718 lines
26 KiB
TypeScript
718 lines
26 KiB
TypeScript
import { useMemo } from 'react';
|
|
import {
|
|
CeilingBeams,
|
|
CorniceRing,
|
|
HangingFixture,
|
|
SideRails,
|
|
WallBand,
|
|
} from './primitives';
|
|
import {
|
|
shade,
|
|
useBayZ,
|
|
useCorners,
|
|
useRepeat,
|
|
type PeriodProps,
|
|
} from './geometry';
|
|
|
|
/**
|
|
* Fauvist studio — Fauvism.
|
|
*
|
|
* The Collioure summer room: close-set whitewashed rafters, plank shutters
|
|
* folded back against the wall beside every window, and a plain painted rail.
|
|
* The architecture stays vernacular so the wall colour does the work.
|
|
*/
|
|
export function FauvistDetails({ room, trim, windows }: PeriodProps) {
|
|
const rafters = useBayZ(room.depth, room.halfD, 1.5, 14);
|
|
const timber = '#d8c8a8';
|
|
|
|
const shutters = useMemo(
|
|
() =>
|
|
(windows ?? [])
|
|
.filter((w) => w.wall === 'left' || w.wall === 'right')
|
|
.flatMap((w) =>
|
|
[-1, 1].map((dir) => ({
|
|
wall: w.wall as 'left' | 'right',
|
|
z: w.x + dir * (w.width / 2 + w.width * 0.28),
|
|
y: w.y,
|
|
h: w.height,
|
|
w: w.width * 0.5,
|
|
}))
|
|
),
|
|
[windows]
|
|
);
|
|
|
|
return (
|
|
<group>
|
|
<CeilingBeams
|
|
room={room}
|
|
positions={rafters}
|
|
axis="x"
|
|
width={0.1}
|
|
height={0.16}
|
|
y={4.02}
|
|
color={timber}
|
|
roughness={0.88}
|
|
metalness={0.02}
|
|
/>
|
|
<CeilingBeams
|
|
room={room}
|
|
positions={[0]}
|
|
axis="z"
|
|
width={0.2}
|
|
height={0.24}
|
|
y={3.86}
|
|
color={shade(timber, -0.24)}
|
|
roughness={0.88}
|
|
metalness={0.02}
|
|
/>
|
|
|
|
{/* Folded-back plank shutters flanking the windows */}
|
|
{shutters.map((s, i) => (
|
|
<group
|
|
key={i}
|
|
position={[(s.wall === 'left' ? -1 : 1) * (room.halfW - 0.13), s.y, s.z]}
|
|
>
|
|
<mesh>
|
|
<boxGeometry args={[0.06, s.h, s.w]} />
|
|
<meshStandardMaterial color={trim} roughness={0.8} metalness={0.04} />
|
|
</mesh>
|
|
{[-0.3, 0, 0.3].map((t) => (
|
|
<mesh key={t} position={[0.04, s.h * t, 0]}>
|
|
<boxGeometry args={[0.02, s.h * 0.22, s.w * 0.86]} />
|
|
<meshStandardMaterial color={shade(trim, -0.22)} roughness={0.84} metalness={0.03} />
|
|
</mesh>
|
|
))}
|
|
</group>
|
|
))}
|
|
|
|
<SideRails room={room} y={2.6} height={0.08} proud={0.11} color={timber} roughness={0.82} metalness={0.03} />
|
|
<WallBand wall="back" room={room} y={2.6} height={0.08} proud={0.11} color={timber} roughness={0.82} metalness={0.03} />
|
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
|
<WallBand key={wall} wall={wall} room={room} y={0.12} height={0.24} proud={0.07} color={trim} roughness={0.8} metalness={0.04} />
|
|
))}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Expressionist room — Expressionism.
|
|
*
|
|
* Die Brücke's carved-plank interior pushed towards the raked sets of the
|
|
* period's cinema: battens canted at alternating angles above the hang, a
|
|
* zig-zag frieze, and ceiling members that refuse to run square.
|
|
*/
|
|
export function ExpressionistDetails({ room, trim }: PeriodProps) {
|
|
const bays = useBayZ(room.depth, room.halfD, 2.2, 11);
|
|
const plank = '#5a3f27';
|
|
const light = shade(plank, 0.3);
|
|
|
|
return (
|
|
<group>
|
|
{/* Canted battens, alternating lean */}
|
|
{bays.map((z, i) => (
|
|
<group key={i}>
|
|
{[-1, 1].map((side) => (
|
|
<mesh
|
|
key={side}
|
|
position={[side * (room.halfW - 0.07), 3.28, z]}
|
|
rotation={[i % 2 === 0 ? 0.3 : -0.3, 0, 0]}
|
|
>
|
|
<boxGeometry args={[0.08, 1.5, 0.17]} />
|
|
<meshStandardMaterial color={i % 2 === 0 ? plank : light} roughness={0.84} metalness={0.04} />
|
|
</mesh>
|
|
))}
|
|
</group>
|
|
))}
|
|
|
|
{/* Zig-zag frieze under the ceiling */}
|
|
{(['left', 'right'] as const).map((wall) =>
|
|
bays.map((z, i) => (
|
|
<mesh
|
|
key={`${wall}-${i}`}
|
|
position={[(wall === 'left' ? -1 : 1) * (room.halfW - 0.08), 4.06, z]}
|
|
rotation={[i % 2 === 0 ? 0.62 : -0.62, 0, 0]}
|
|
>
|
|
<boxGeometry args={[0.1, 0.42, 0.1]} />
|
|
<meshStandardMaterial color={trim} roughness={0.7} metalness={0.1} />
|
|
</mesh>
|
|
))
|
|
)}
|
|
|
|
{/* Ceiling members deliberately off-square */}
|
|
{bays
|
|
.filter((_, i) => i % 3 === 0)
|
|
.map((z, i) => (
|
|
<mesh key={i} position={[0, 3.98, z]} rotation={[0, i % 2 === 0 ? 0.14 : -0.14, 0]}>
|
|
<boxGeometry args={[room.width * 0.97, 0.2, 0.15]} />
|
|
<meshStandardMaterial color={shade(plank, -0.24)} roughness={0.88} metalness={0.03} />
|
|
</mesh>
|
|
))}
|
|
|
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
|
<WallBand key={wall} wall={wall} room={room} y={0.13} height={0.26} proud={0.09} color={shade(plank, -0.3)} roughness={0.86} metalness={0.03} />
|
|
))}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Cubist studio — Cubism.
|
|
*
|
|
* The Bateau-Lavoir atelier: slim cast-iron columns in the corners, riveted tie
|
|
* rods across the room, and a cornice broken into faceted plaster planes — the
|
|
* building itself analysed into flat facets.
|
|
*/
|
|
export function CubistAtelierDetails({ room, trim }: PeriodProps) {
|
|
const corners = useCorners(room.halfW, room.halfD, 0.24);
|
|
const ties = useBayZ(room.depth, room.halfD, 3.6, 8);
|
|
const iron = '#4d4a45';
|
|
const facet = '#dedad2';
|
|
|
|
const facets = useRepeat(room.depth, 1.15, 22);
|
|
|
|
return (
|
|
<group>
|
|
{/* Cast-iron columns in the corner pockets */}
|
|
{corners.map(([x, z], i) => (
|
|
<group key={i} position={[x, 0, z]}>
|
|
<mesh position={[0, 0.1, 0]}>
|
|
<boxGeometry args={[0.3, 0.2, 0.3]} />
|
|
<meshStandardMaterial color={shade(iron, -0.2)} roughness={0.6} metalness={0.45} />
|
|
</mesh>
|
|
<mesh position={[0, 2.1, 0]}>
|
|
<cylinderGeometry args={[0.09, 0.11, 3.8, 10]} />
|
|
<meshStandardMaterial color={iron} roughness={0.52} metalness={0.55} />
|
|
</mesh>
|
|
<mesh position={[0, 4.02, 0]}>
|
|
<boxGeometry args={[0.34, 0.18, 0.34]} />
|
|
<meshStandardMaterial color={iron} roughness={0.5} metalness={0.58} />
|
|
</mesh>
|
|
</group>
|
|
))}
|
|
|
|
{/* Riveted tie rods spanning the room, with a turnbuckle at mid-span */}
|
|
{ties.map((z, i) => (
|
|
<group key={i} position={[0, 3.8, z]}>
|
|
<mesh rotation={[0, 0, Math.PI / 2]}>
|
|
<cylinderGeometry args={[0.035, 0.035, room.width - 0.2, 8]} />
|
|
<meshStandardMaterial color={iron} roughness={0.46} metalness={0.62} />
|
|
</mesh>
|
|
<mesh rotation={[0, 0, Math.PI / 2]}>
|
|
<cylinderGeometry args={[0.06, 0.06, 0.3, 8]} />
|
|
<meshStandardMaterial color={shade(iron, -0.2)} roughness={0.4} metalness={0.7} />
|
|
</mesh>
|
|
</group>
|
|
))}
|
|
|
|
{/* Faceted cornice: flat planes tilted alternately */}
|
|
{(['left', 'right'] as const).map((wall) =>
|
|
facets.map((z, i) => (
|
|
<mesh
|
|
key={`${wall}-${i}`}
|
|
position={[(wall === 'left' ? -1 : 1) * (room.halfW - 0.1), 4.02, z]}
|
|
rotation={[0, 0, i % 2 === 0 ? 0.34 : -0.34]}
|
|
>
|
|
<boxGeometry args={[0.2, 0.22, 0.86]} />
|
|
<meshStandardMaterial
|
|
color={i % 3 === 0 ? shade(facet, -0.14) : facet}
|
|
roughness={0.82}
|
|
metalness={0.05}
|
|
/>
|
|
</mesh>
|
|
))
|
|
)}
|
|
<CorniceRing room={room} y={4.15} height={0.08} proud={0.13} color={trim} roughness={0.6} metalness={0.2} />
|
|
|
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
|
<WallBand key={wall} wall={wall} room={room} y={0.1} height={0.2} proud={0.07} color={shade(iron, 0.24)} roughness={0.7} metalness={0.16} />
|
|
))}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Futurist loft — Futurism.
|
|
*
|
|
* Sant'Elia's Città Nuova reduced to one room: riveted steel stanchions,
|
|
* diagonal wind bracing across the upper wall, and a gantry rail running the
|
|
* length of the hall. Structure celebrated, nothing concealed.
|
|
*/
|
|
export function FuturistDetails({ room, trim }: PeriodProps) {
|
|
const bays = useBayZ(room.depth, room.halfD, 4.0, 8);
|
|
const steel = '#8f9298';
|
|
const dark = shade(steel, -0.42);
|
|
|
|
return (
|
|
<group>
|
|
{/* Riveted stanchions */}
|
|
{bays.map((z, i) => (
|
|
<group key={i}>
|
|
{[-1, 1].map((side) => (
|
|
<group key={side} position={[side * (room.halfW - 0.06), 0, z]}>
|
|
<mesh position={[0, 2.05, 0]}>
|
|
<boxGeometry args={[0.09, 4.1, 0.36]} />
|
|
<meshStandardMaterial color={steel} roughness={0.42} metalness={0.72} />
|
|
</mesh>
|
|
{[0.6, 1.7, 2.8, 3.8].map((y) => (
|
|
<mesh key={y} position={[side * -0.03, y, 0]}>
|
|
<boxGeometry args={[0.06, 0.1, 0.44]} />
|
|
<meshStandardMaterial color={dark} roughness={0.38} metalness={0.8} />
|
|
</mesh>
|
|
))}
|
|
</group>
|
|
))}
|
|
</group>
|
|
))}
|
|
|
|
{/* Diagonal wind bracing between the stanchions, above the hang */}
|
|
{bays.slice(0, -1).map((z, i) => {
|
|
const mid = (z + bays[i + 1]) / 2;
|
|
const run = bays[i + 1] - z;
|
|
return (
|
|
<group key={i}>
|
|
{[-1, 1].map((side) =>
|
|
[-1, 1].map((dir) => (
|
|
<mesh
|
|
key={`${side}-${dir}`}
|
|
position={[side * (room.halfW - 0.1), 3.4, mid]}
|
|
rotation={[dir * Math.atan2(run - 0.4, 0.9), 0, 0]}
|
|
>
|
|
<boxGeometry args={[0.06, Math.hypot(run - 0.4, 0.9), 0.06]} />
|
|
<meshStandardMaterial color={steel} roughness={0.4} metalness={0.75} />
|
|
</mesh>
|
|
))
|
|
)}
|
|
</group>
|
|
);
|
|
})}
|
|
|
|
{/* Gantry rail and its brackets */}
|
|
<SideRails room={room} y={2.72} height={0.12} proud={0.15} color={dark} roughness={0.36} metalness={0.82} />
|
|
<CorniceRing room={room} y={4.13} height={0.1} proud={0.16} color={trim} roughness={0.34} metalness={0.7} />
|
|
|
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
|
<WallBand key={wall} wall={wall} room={room} y={0.11} height={0.22} proud={0.09} color={dark} roughness={0.4} metalness={0.7} />
|
|
))}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Suprematist white cube — Suprematism.
|
|
*
|
|
* The 0,10 room: a bare white shell, a shelf slung across the upper corner
|
|
* where Malevich hung the Black Square in the icon position, and thin black and
|
|
* red geometric bands drifting off the axis of the walls.
|
|
*/
|
|
export function SuprematistDetails({ room, trim }: PeriodProps) {
|
|
const bars = useMemo(() => {
|
|
const seeds = [-0.62, -0.24, 0.18, 0.55];
|
|
return seeds.map((t, i) => ({
|
|
z: t,
|
|
y: 3.0 + (i % 3) * 0.28,
|
|
len: 0.9 + (i % 2) * 0.7,
|
|
tilt: i % 2 === 0 ? 0.22 : -0.34,
|
|
color: i % 3 === 0 ? '#c8302a' : '#141414',
|
|
}));
|
|
}, []);
|
|
|
|
return (
|
|
<group>
|
|
{/* Icon-corner shelf, carried on two plain brackets */}
|
|
<group position={[room.halfW - 0.46, 3.3, -room.halfD + 0.46]} rotation={[0, Math.PI / 4, 0]}>
|
|
<mesh>
|
|
<boxGeometry args={[1.16, 0.05, 0.34]} />
|
|
<meshStandardMaterial color="#f2f2f2" roughness={0.72} metalness={0.04} />
|
|
</mesh>
|
|
{[-0.4, 0.4].map((t) => (
|
|
<mesh key={t} position={[t, -0.16, 0.02]} rotation={[0, 0, 0]}>
|
|
<boxGeometry args={[0.05, 0.28, 0.24]} />
|
|
<meshStandardMaterial color="#e2e2e2" roughness={0.74} metalness={0.04} />
|
|
</mesh>
|
|
))}
|
|
</group>
|
|
|
|
{/* Drifting bands above the hang */}
|
|
{(['left', 'right'] as const).map((wall) =>
|
|
bars.map((b, i) => (
|
|
<mesh
|
|
key={`${wall}-${i}`}
|
|
position={[(wall === 'left' ? -1 : 1) * (room.halfW - 0.07), b.y, b.z * room.halfD]}
|
|
rotation={[b.tilt, 0, 0]}
|
|
>
|
|
<boxGeometry args={[0.04, 0.14, b.len]} />
|
|
<meshStandardMaterial color={b.color} roughness={0.66} metalness={0.06} />
|
|
</mesh>
|
|
))
|
|
)}
|
|
|
|
{/* Shadow-gap reveal instead of a cornice */}
|
|
<CorniceRing room={room} y={4.04} height={0.05} proud={0.03} color="#d2d2d2" roughness={0.8} metalness={0.04} />
|
|
<CorniceRing room={room} y={4.16} height={0.06} proud={0.1} color={trim} roughness={0.7} metalness={0.08} />
|
|
|
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
|
<WallBand key={wall} wall={wall} room={room} y={0.07} height={0.14} proud={0.06} color="#dcdcdc" roughness={0.76} metalness={0.05} />
|
|
))}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Constructivist space — Constructivism.
|
|
*
|
|
* Rodchenko's lattice made architecture: red-painted diagonal trusses under the
|
|
* ceiling, angled Proun panels folded into the corners, and exposed steel ties.
|
|
* Every member is doing structural work and says so.
|
|
*/
|
|
export function ConstructivistDetails({ room, trim }: PeriodProps) {
|
|
const bays = useBayZ(room.depth, room.halfD, 2.8, 10);
|
|
const corners = useCorners(room.halfW, room.halfD, 0.3);
|
|
const red = '#b8342a';
|
|
const steel = '#9a9a96';
|
|
|
|
return (
|
|
<group>
|
|
{/* Lattice truss under the ceiling — top chord, bottom chord, diagonals */}
|
|
<CeilingBeams room={room} positions={[-0.9, 0.9]} axis="z" width={0.1} height={0.12} y={4.06} color={steel} roughness={0.44} metalness={0.6} />
|
|
<CeilingBeams room={room} positions={[-0.9, 0.9]} axis="z" width={0.09} height={0.1} y={3.66} color={steel} roughness={0.44} metalness={0.6} />
|
|
{bays.map((z, i) =>
|
|
[-0.9, 0.9].map((x) => (
|
|
<mesh key={`${i}-${x}`} position={[x, 3.86, z]} rotation={[i % 2 === 0 ? 0.62 : -0.62, 0, 0]}>
|
|
<boxGeometry args={[0.07, 0.62, 0.07]} />
|
|
<meshStandardMaterial color={red} roughness={0.56} metalness={0.3} />
|
|
</mesh>
|
|
))
|
|
)}
|
|
|
|
{/* Proun panels folded into the corners */}
|
|
{corners.map(([x, z], i) => (
|
|
<group key={i} position={[x, 2.9, z]} rotation={[0, Math.PI / 4 + (i % 2) * 0.2, 0]}>
|
|
<mesh rotation={[0, 0, 0.18]}>
|
|
<boxGeometry args={[0.66, 1.5, 0.05]} />
|
|
<meshStandardMaterial color={i % 2 === 0 ? red : '#141414'} roughness={0.66} metalness={0.12} />
|
|
</mesh>
|
|
<mesh position={[0.1, -0.3, 0.05]} rotation={[0, 0, -0.5]}>
|
|
<boxGeometry args={[0.5, 0.14, 0.04]} />
|
|
<meshStandardMaterial color={steel} roughness={0.42} metalness={0.6} />
|
|
</mesh>
|
|
</group>
|
|
))}
|
|
|
|
{/* Steel tie rails at the hang line and the skirting */}
|
|
<SideRails room={room} y={2.64} height={0.07} proud={0.12} color={steel} roughness={0.4} metalness={0.68} />
|
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
|
<WallBand key={wall} wall={wall} room={room} y={0.1} height={0.2} proud={0.08} color={red} roughness={0.62} metalness={0.2} />
|
|
))}
|
|
<CorniceRing room={room} y={4.16} height={0.06} proud={0.11} color={trim} roughness={0.5} metalness={0.4} />
|
|
</group>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Dada salon — Dada.
|
|
*
|
|
* A bourgeois room being dismantled: cornice fragments that stop and restart at
|
|
* different heights, a picture rail hung visibly out of level, and a service
|
|
* pipe left running across the wall where the moulding should be.
|
|
*/
|
|
export function DadaDetails({ room, trim }: PeriodProps) {
|
|
const fragments = useMemo(
|
|
() =>
|
|
[
|
|
{ wall: 'left' as const, y: 4.04, span: 0.3, proud: 0.14 },
|
|
{ wall: 'left' as const, y: 3.78, span: 0.18, proud: 0.1 },
|
|
{ wall: 'right' as const, y: 4.12, span: 0.44, proud: 0.16 },
|
|
{ wall: 'right' as const, y: 3.9, span: 0.22, proud: 0.09 },
|
|
{ wall: 'back' as const, y: 4.06, span: 0.35, proud: 0.13 },
|
|
],
|
|
[]
|
|
);
|
|
const plaster = '#e6dac4';
|
|
const pipe = '#6a6259';
|
|
|
|
return (
|
|
<group>
|
|
{/* Cornice, in pieces */}
|
|
{fragments.map((f, i) => (
|
|
<WallBand
|
|
key={i}
|
|
wall={f.wall}
|
|
room={room}
|
|
y={f.y}
|
|
height={0.13}
|
|
proud={f.proud}
|
|
span={f.span}
|
|
color={i % 2 === 0 ? plaster : shade(plaster, -0.18)}
|
|
roughness={0.76}
|
|
metalness={0.05}
|
|
/>
|
|
))}
|
|
|
|
{/* Picture rail, visibly out of level */}
|
|
{[-1, 1].map((side) => (
|
|
<mesh key={side} position={[side * (room.halfW - 0.07), 2.62, 0]} rotation={[side * 0.035, 0, 0]}>
|
|
<boxGeometry args={[0.1, 0.08, room.depth * 0.86]} />
|
|
<meshStandardMaterial color={trim} roughness={0.6} metalness={0.18} />
|
|
</mesh>
|
|
))}
|
|
|
|
{/* Service pipe run, with a clumsy elbow into the ceiling */}
|
|
<mesh position={[-room.halfW + 0.16, 3.34, 0]} rotation={[Math.PI / 2, 0, 0]}>
|
|
<cylinderGeometry args={[0.06, 0.06, room.depth * 0.8, 10]} />
|
|
<meshStandardMaterial color={pipe} roughness={0.64} metalness={0.4} />
|
|
</mesh>
|
|
<mesh position={[-room.halfW + 0.16, 3.7, room.depth * 0.4]}>
|
|
<cylinderGeometry args={[0.06, 0.06, 0.75, 10]} />
|
|
<meshStandardMaterial color={pipe} roughness={0.64} metalness={0.4} />
|
|
</mesh>
|
|
|
|
{/* Skirting that changes its mind halfway along */}
|
|
<WallBand wall="left" room={room} y={0.11} height={0.22} proud={0.08} span={0.55} color={plaster} roughness={0.78} metalness={0.04} />
|
|
<WallBand wall="right" room={room} y={0.15} height={0.3} proud={0.1} span={0.8} color={shade(plaster, -0.26)} roughness={0.78} metalness={0.04} />
|
|
<WallBand wall="back" room={room} y={0.11} height={0.22} proud={0.08} color={plaster} roughness={0.78} metalness={0.04} />
|
|
|
|
<HangingFixture
|
|
position={[room.halfW * 0.32, 3.9, room.halfD * 0.2]}
|
|
dropLength={0.7}
|
|
radius={0.2}
|
|
arms={3}
|
|
metalColor={trim}
|
|
flameColor="#fff0cc"
|
|
glowColor="#ffb45a"
|
|
light
|
|
lightColor="#ffd0a0"
|
|
lightIntensity={0.7}
|
|
lightDistance={8}
|
|
/>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Surrealist interior — Surrealism.
|
|
*
|
|
* The 1938 Exposition Internationale: sacks slung under the ceiling, drapery
|
|
* pulled across the cornice, and a door standing in a corner where no door can
|
|
* lead anywhere. The room is furnished as an image, not as a gallery.
|
|
*/
|
|
export function SurrealistDetails({ room, trim }: PeriodProps) {
|
|
const sacks = useBayZ(room.depth, room.halfD, 2.6, 9);
|
|
|
|
return (
|
|
<group>
|
|
{/* Sacks hanging under the ceiling */}
|
|
{sacks.map((z, i) =>
|
|
[-1, 0, 1].map((c) => (
|
|
<mesh
|
|
key={`${i}-${c}`}
|
|
position={[c * room.width * 0.26, 3.86 - ((i + c) % 3) * 0.09, z]}
|
|
rotation={[0, ((i + c) % 4) * 0.4, 0.1 * ((i % 2) * 2 - 1)]}
|
|
>
|
|
<sphereGeometry args={[0.34, 10, 8]} />
|
|
<meshStandardMaterial color={i % 2 === 0 ? '#3a3128' : '#2c261f'} roughness={0.96} metalness={0.02} />
|
|
</mesh>
|
|
))
|
|
)}
|
|
|
|
{/* Drapery gathered along the top of the side walls */}
|
|
{(['left', 'right'] as const).map((wall) =>
|
|
sacks.map((z, i) => (
|
|
<mesh
|
|
key={`${wall}-${i}`}
|
|
position={[(wall === 'left' ? -1 : 1) * (room.halfW - 0.13), 3.7, z]}
|
|
rotation={[0, 0, 0]}
|
|
>
|
|
<cylinderGeometry args={[0.13, 0.09, 0.86, 8]} />
|
|
<meshStandardMaterial color="#38304a" roughness={0.92} metalness={0.03} />
|
|
</mesh>
|
|
))
|
|
)}
|
|
|
|
{/* A door that leads nowhere, standing in the corner pocket */}
|
|
<group position={[-room.halfW + 0.34, 0, -room.halfD + 0.34]} rotation={[0, Math.PI / 4, 0]}>
|
|
<mesh position={[0, 1.05, 0]}>
|
|
<boxGeometry args={[0.72, 2.1, 0.07]} />
|
|
<meshStandardMaterial color="#4a3a2a" roughness={0.78} metalness={0.06} />
|
|
</mesh>
|
|
<mesh position={[0, 1.12, 0]}>
|
|
<boxGeometry args={[0.84, 2.24, 0.05]} />
|
|
<meshStandardMaterial color={shade('#4a3a2a', -0.3)} roughness={0.8} metalness={0.05} />
|
|
</mesh>
|
|
<mesh position={[0.26, 1.02, 0.06]}>
|
|
<sphereGeometry args={[0.05, 10, 10]} />
|
|
<meshStandardMaterial color={trim} roughness={0.34} metalness={0.66} />
|
|
</mesh>
|
|
</group>
|
|
|
|
<CorniceRing room={room} y={4.14} height={0.09} proud={0.12} color={trim} roughness={0.62} metalness={0.2} />
|
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
|
<WallBand key={wall} wall={wall} room={room} y={0.11} height={0.22} proud={0.08} color="#2e2822" roughness={0.82} metalness={0.05} />
|
|
))}
|
|
|
|
{/* A brazier glow in the opposite corner */}
|
|
<group position={[room.halfW - 0.44, 0, room.halfD - 0.44]}>
|
|
<mesh position={[0, 0.34, 0]}>
|
|
<cylinderGeometry args={[0.22, 0.15, 0.28, 10]} />
|
|
<meshStandardMaterial color="#2a2622" roughness={0.7} metalness={0.35} />
|
|
</mesh>
|
|
<mesh position={[0, 0.5, 0]}>
|
|
<sphereGeometry args={[0.16, 10, 8]} />
|
|
<meshStandardMaterial color="#ff9a40" emissive="#ff6a12" emissiveIntensity={1.1} toneMapped={false} />
|
|
</mesh>
|
|
<pointLight position={[0, 0.7, 0]} intensity={0.8} distance={7} color="#ff8a3c" />
|
|
</group>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* NYC loft — Abstract Expressionism.
|
|
*
|
|
* A Tenth Street cast-iron building: fluted iron columns on their corbelled
|
|
* caps, a pressed-tin ceiling field, and the sprinkler main running the length
|
|
* of the room. Industrial fabric left exactly as the painters found it.
|
|
*/
|
|
export function LoftDetails({ room, trim }: PeriodProps) {
|
|
const corners = useCorners(room.halfW, room.halfD, 0.26);
|
|
const joists = useBayZ(room.depth, room.halfD, 1.7, 13);
|
|
const iron = '#5c5852';
|
|
|
|
return (
|
|
<group>
|
|
{/* Cast-iron columns with corbelled caps */}
|
|
{corners.map(([x, z], i) => (
|
|
<group key={i} position={[x, 0, z]}>
|
|
<mesh position={[0, 0.12, 0]}>
|
|
<cylinderGeometry args={[0.2, 0.24, 0.24, 12]} />
|
|
<meshStandardMaterial color={shade(iron, -0.2)} roughness={0.62} metalness={0.42} />
|
|
</mesh>
|
|
<mesh position={[0, 2.05, 0]}>
|
|
<cylinderGeometry args={[0.11, 0.13, 3.6, 12]} />
|
|
<meshStandardMaterial color={iron} roughness={0.56} metalness={0.48} />
|
|
</mesh>
|
|
<mesh position={[0, 3.92, 0]}>
|
|
<cylinderGeometry args={[0.22, 0.13, 0.2, 12]} />
|
|
<meshStandardMaterial color={iron} roughness={0.54} metalness={0.5} />
|
|
</mesh>
|
|
<mesh position={[0, 4.06, 0]}>
|
|
<boxGeometry args={[0.4, 0.14, 0.4]} />
|
|
<meshStandardMaterial color={shade(iron, 0.1)} roughness={0.54} metalness={0.5} />
|
|
</mesh>
|
|
</group>
|
|
))}
|
|
|
|
{/* Pressed-tin ceiling field on close-set joists */}
|
|
<CeilingBeams
|
|
room={room}
|
|
positions={joists}
|
|
axis="x"
|
|
width={0.08}
|
|
height={0.09}
|
|
y={4.05}
|
|
color="#b4b0a6"
|
|
roughness={0.48}
|
|
metalness={0.42}
|
|
/>
|
|
<CeilingBeams
|
|
room={room}
|
|
positions={[-room.width * 0.24, 0, room.width * 0.24]}
|
|
axis="z"
|
|
width={0.14}
|
|
height={0.16}
|
|
y={3.96}
|
|
color={iron}
|
|
roughness={0.6}
|
|
metalness={0.4}
|
|
/>
|
|
|
|
{/* Sprinkler main with drops */}
|
|
<mesh position={[0, 3.78, 0]} rotation={[Math.PI / 2, 0, 0]}>
|
|
<cylinderGeometry args={[0.055, 0.055, room.depth * 0.92, 10]} />
|
|
<meshStandardMaterial color="#7a3f2c" roughness={0.66} metalness={0.4} />
|
|
</mesh>
|
|
{joists
|
|
.filter((_, i) => i % 4 === 0)
|
|
.map((z, i) => (
|
|
<mesh key={i} position={[0, 3.68, z]}>
|
|
<cylinderGeometry args={[0.02, 0.025, 0.16, 6]} />
|
|
<meshStandardMaterial color="#8a4a34" roughness={0.6} metalness={0.45} />
|
|
</mesh>
|
|
))}
|
|
|
|
{/* Steel skirting angle */}
|
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
|
<WallBand key={wall} wall={wall} room={room} y={0.09} height={0.18} proud={0.08} color={trim} roughness={0.56} metalness={0.44} />
|
|
))}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Pop Art gallery — Pop Art.
|
|
*
|
|
* The 1960s dealer's room: a flat white shell, recessed fluorescent troffers in
|
|
* a ceiling grid, an aluminium reveal at the floor, and a shadow gap where a
|
|
* cornice used to be. The architecture is engineered to disappear.
|
|
*/
|
|
export function WhiteCubeDetails({ room, trim }: PeriodProps) {
|
|
const troffers = useMemo(() => {
|
|
const cols = Math.max(2, Math.min(3, Math.round(room.width / 4.5)));
|
|
const rows = Math.max(2, Math.min(6, Math.round(room.depth / 3.4)));
|
|
const out: [number, number][] = [];
|
|
for (let c = 0; c < cols; c++) {
|
|
for (let r = 0; r < rows; r++) {
|
|
out.push([
|
|
-room.halfW + (room.width / (cols + 1)) * (c + 1),
|
|
-room.halfD + (room.depth / (rows + 1)) * (r + 1),
|
|
]);
|
|
}
|
|
}
|
|
return out;
|
|
}, [room.width, room.depth, room.halfW, room.halfD]);
|
|
|
|
return (
|
|
<group>
|
|
{/* Recessed fluorescent troffers */}
|
|
{troffers.map(([x, z], i) => (
|
|
<group key={i} position={[x, 4.08, z]}>
|
|
<mesh>
|
|
<boxGeometry args={[1.24, 0.06, 0.62]} />
|
|
<meshStandardMaterial color="#c8c8c8" roughness={0.4} metalness={0.5} />
|
|
</mesh>
|
|
<mesh position={[0, -0.04, 0]}>
|
|
<boxGeometry args={[1.1, 0.03, 0.5]} />
|
|
<meshStandardMaterial
|
|
color="#ffffff"
|
|
emissive="#f4f8ff"
|
|
emissiveIntensity={0.85}
|
|
toneMapped={false}
|
|
/>
|
|
</mesh>
|
|
</group>
|
|
))}
|
|
|
|
{/* Ceiling grid tees between the fittings */}
|
|
<CeilingBeams
|
|
room={room}
|
|
positions={troffers.map(([, z]) => z).filter((z, i, a) => a.indexOf(z) === i)}
|
|
axis="x"
|
|
width={0.05}
|
|
height={0.04}
|
|
y={4.1}
|
|
color="#d6d6d6"
|
|
roughness={0.5}
|
|
metalness={0.36}
|
|
/>
|
|
|
|
{/* Shadow gap and aluminium skirting reveal */}
|
|
<CorniceRing room={room} y={4.02} height={0.06} proud={0.02} color="#c0c0c0" roughness={0.66} metalness={0.16} />
|
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
|
<group key={wall}>
|
|
<WallBand wall={wall} room={room} y={0.06} height={0.12} proud={0.05} color={trim} roughness={0.42} metalness={0.5} />
|
|
<WallBand wall={wall} room={room} y={0.14} height={0.03} proud={0.02} color="#9a9a9a" roughness={0.6} metalness={0.3} />
|
|
</group>
|
|
))}
|
|
</group>
|
|
);
|
|
}
|