Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2a256132c |
@@ -37,7 +37,8 @@ Gallery/
|
|||||||
│ │ ├── components/GalleryLoadingMarker.tsx # Loading spinner overlay/banner (catalog, portraits, halls)
|
│ │ ├── components/GalleryLoadingMarker.tsx # Loading spinner overlay/banner (catalog, portraits, halls)
|
||||||
│ │ ├── components/GalleryWindows.tsx # Side-wall daylight windows (movement)
|
│ │ ├── components/GalleryWindows.tsx # Side-wall daylight windows (movement)
|
||||||
│ │ ├── components/HallPassage.tsx # Open archway between movement wings
|
│ │ ├── components/HallPassage.tsx # Open archway between movement wings
|
||||||
│ │ ├── components/MovementHallDetails.tsx # Period architectural details
|
│ │ ├── components/MovementHallDetails.tsx # Period architecture dispatcher (Gothic + Byzantine reference)
|
||||||
|
│ │ ├── components/hall-details/ # One period interior per movement + shared primitives
|
||||||
│ │ ├── data/movement-interior-styles.ts # Per-movement interior themes
|
│ │ ├── data/movement-interior-styles.ts # Per-movement interior themes
|
||||||
│ │ ├── utils/movementHallLayout.ts # Wing split + window gap placement
|
│ │ ├── utils/movementHallLayout.ts # Wing split + window gap placement
|
||||||
│ │ ├── utils/galleryProceduralTextures.ts # Hi-res wall/floor textures
|
│ │ ├── utils/galleryProceduralTextures.ts # Hi-res wall/floor textures
|
||||||
@@ -353,13 +354,14 @@ Enter from the home page by clicking a **movement name** on the movement flow. F
|
|||||||
| Wall order | Same U-shaped hang as artist halls (left → end → right) |
|
| Wall order | Same U-shaped hang as artist halls (left → end → right) |
|
||||||
| Frame captions | **Year · artist** label below each frame |
|
| Frame captions | **Year · artist** label below each frame |
|
||||||
| Period interior | Each of the 26 seeded movements maps to a unique style in `movement-interior-styles.ts` (Italian palazzo, Baroque palace, Byzantine basilica, NYC loft, white cube, etc.). Style keys use a legacy id map (`DB_ID_TO_STYLE_KEY`: gallery ids **1–26** → authored keys **27–52**, with Northern/High Renaissance swapped) so Byzantine gets the basilica, not Gothic stone |
|
| Period interior | Each of the 26 seeded movements maps to a unique style in `movement-interior-styles.ts` (Italian palazzo, Baroque palace, Byzantine basilica, NYC loft, white cube, etc.). Style keys use a legacy id map (`DB_ID_TO_STYLE_KEY`: gallery ids **1–26** → authored keys **27–52**, with Northern/High Renaissance swapped) so Byzantine gets the basilica, not Gothic stone |
|
||||||
|
| Period architecture | Each style names a `details` kind (26 of them) that selects one component under `components/hall-details/` — the hall is built from the architecture of the **same period as the movement** (Roman entablature, Brunelleschi arcade, Flemish beam ceiling, Horta whiplash brackets, Rodchenko lattice, 1960s troffer grid). Gothic and Byzantine remain the reference implementations in `MovementHallDetails.tsx`; the geometry budget every module works to is documented in `hall-details/geometry.ts` |
|
||||||
| Wall materials | Hi-res **procedural textures** with normal maps (`galleryProceduralTextures.ts`): real-world stone, marble, wood panelling, brick, velvet, stucco, **marble revetment** (framed book-matched panels), **Cosmatesque paving** (`marble-opus-sectile`), **coffered timber** ceilings, **blind-arcaded ashlar** (`gothic-ashlar`) and **ribbed vaulting** (`gothic-vault`) — plus **single-colour painted walls** (`painted-lime`, `painted-oil-matte`, `painted-oil-satin`, `painted-emulsion`, `painted-flat`) tinted per movement for Renaissance salons through modern white cubes |
|
| Wall materials | Hi-res **procedural textures** with normal maps (`galleryProceduralTextures.ts`): real-world stone, marble, wood panelling, brick, velvet, stucco, **marble revetment** (framed book-matched panels), **Cosmatesque paving** (`marble-opus-sectile`), **coffered timber** ceilings, **blind-arcaded ashlar** (`gothic-ashlar`) and **ribbed vaulting** (`gothic-vault`) — plus **single-colour painted walls** (`painted-lime`, `painted-oil-matte`, `painted-oil-satin`, `painted-emulsion`, `painted-flat`) tinted per movement for Renaissance salons through modern white cubes |
|
||||||
| Architecture in texture | Period halls that need arcades, vaults, or panelling draw that **relief into the texture** rather than adding geometry: `gothic-ashlar` paints a full storey (plinth → blind arcade → string course → triforium → cornice) into one tile whose `metersPerRepeat` equals `WALL_HEIGHT`, so it maps **once vertically** onto the wall; `gothic-vault` paints a quadripartite bay with tiercerons and a boss. Light/dark banding plus a high `normalStrengthFor()` makes it read as carved stone, keeping the mesh and light budget flat |
|
| Architecture in texture | Period halls that need arcades, vaults, or panelling draw that **relief into the texture** rather than adding geometry: `gothic-ashlar` paints a full storey (plinth → blind arcade → string course → triforium → cornice) into one tile whose `metersPerRepeat` equals `WALL_HEIGHT`, so it maps **once vertically** onto the wall; `gothic-vault` paints a quadripartite bay with tiercerons and a boss. Light/dark banding plus a high `normalStrengthFor()` makes it read as carved stone, keeping the mesh and light budget flat |
|
||||||
| Door-flanking panels | Wall segments beside exit doors and passages take the hall's own wall texture (`GalleryWall` accepts `kind`/`tint`) instead of rendering as flat single-colour blocks next to textured walls |
|
| Door-flanking panels | Wall segments beside exit doors and passages take the hall's own wall texture (`GalleryWall` accepts `kind`/`tint`) instead of rendering as flat single-colour blocks next to textured walls |
|
||||||
| Textures | Wall/floor/ceiling maps applied via `useTexturedMaterial`; movement **tints** drive painted-wall hue |
|
| Textures | Wall/floor/ceiling maps applied via `useTexturedMaterial`; movement **tints** drive painted-wall hue |
|
||||||
| Windows | **Side walls only** — placed in gaps between frames when possible; if a wall is packed, high **clerestory** windows are still added so the hall keeps daylight (`computeSideWallWindows`). Styles are drawn in `GalleryWindows.tsx`; `gothic-lancet` builds a two-centred arch head from chords with a glazed spandrel, mullions, and transoms |
|
| Windows | **Side walls only** — placed in gaps between frames when possible; if a wall is packed, high **clerestory** windows are still added so the hall keeps daylight (`computeSideWallWindows`). Styles are drawn in `GalleryWindows.tsx`; `gothic-lancet` builds a two-centred arch head from chords with a glazed spandrel, mullions, and transoms |
|
||||||
| Lighting | Shared hall lights only (ambient / hemisphere / directional + capped ceiling track spots + one fill per window). See **light budget** below |
|
| Lighting | Shared hall lights only (ambient / hemisphere / directional + capped ceiling track spots + one fill per window). See **light budget** below |
|
||||||
| Period details | `MovementHallDetails.tsx` — classical / neoclassical use **shallow engaged corner pilasters** (never freestanding mid-hall or proud corner shafts that cover frames); `byzantine` adds engaged porphyry colonnettes with basket capitals, a marble revetment dado, and hanging brass polycandela; `gothic` adds bay-spaced compound piers with vault springers, transverse ribs arching across the nave, and a moulded string course — all flush to the side walls |
|
| Period details | `MovementHallDetails.tsx` dispatches to one module per movement. `byzantine` adds engaged porphyry colonnettes with basket capitals, a marble revetment dado, and hanging brass polycandela; `gothic` adds bay-spaced compound piers with vault springers, transverse ribs arching across the nave, and a moulded string course. Every other movement follows the same rules in `components/hall-details/`: orders and piers stay **flush to the side walls** (≤ 0.17 m proud), freestanding masses only in the corner pockets, and no more than two added lights per hall — see `hall-details/geometry.ts` |
|
||||||
| Back wall | Single wing: solid display wall. Multi-wing: **Exit double doors** → **Wing navigator** or **Exit to Timeline** |
|
| Back wall | Single wing: solid display wall. Multi-wing: **Exit double doors** → **Wing navigator** or **Exit to Timeline** |
|
||||||
| Front / entrance wall | Single wing: **Exit double doors** (leave the way you entered). Multi-wing: **“Next wing →”** archway when a later wing exists |
|
| Front / entrance wall | Single wing: **Exit double doors** (leave the way you entered). Multi-wing: **“Next wing →”** archway when a later wing exists |
|
||||||
| Influence lamps | Same golden upside-down emissive fixtures as artist halls when `has_influence_links` is true (shared rail: 40 cm above the tallest frame in the wing) |
|
| Influence lamps | Same golden upside-down emissive fixtures as artist halls when `has_influence_links` is true (shared rail: 40 cm above the tallest frame in the wing) |
|
||||||
@@ -395,7 +397,11 @@ Full guide: [tours.md](tours.md).
|
|||||||
|
|
||||||
**Light budget (critical):** Three.js forward shading cannot compile `MeshStandardMaterial` with dozens of dynamic lights. Large wings used to add **one `spotLight` per painting** (plus influence lamps), which pushed the scene to ~100–200 lights — walls, floor, and ceiling then rendered as a black void while painting canvases (`MeshBasicMaterial`) still showed. Halls therefore use **shared lighting only**: ambient / hemisphere / directional fill, a small set of ceiling track spots (`GalleryTrackLights`, capped), and **one** window spot each. Painting canvases stay unlit (`MeshBasicMaterial`); frames and influence fixtures use **emissive** accents instead of per-frame lights. Intensities are scaled for three.js physical lights (post-r155).
|
**Light budget (critical):** Three.js forward shading cannot compile `MeshStandardMaterial` with dozens of dynamic lights. Large wings used to add **one `spotLight` per painting** (plus influence lamps), which pushed the scene to ~100–200 lights — walls, floor, and ceiling then rendered as a black void while painting canvases (`MeshBasicMaterial`) still showed. Halls therefore use **shared lighting only**: ambient / hemisphere / directional fill, a small set of ceiling track spots (`GalleryTrackLights`, capped), and **one** window spot each. Painting canvases stay unlit (`MeshBasicMaterial`); frames and influence fixtures use **emissive** accents instead of per-frame lights. Intensities are scaled for three.js physical lights (post-r155).
|
||||||
|
|
||||||
Shared lights carry a **brightness floor** so dark period styles never render as a black void. A style that is *meant* to be dim (a basilica lit by window shafts, not flooded) sets **`lightScale`** in `movement-interior-styles.ts` — it multiplies the scene ambient/hemisphere/directional/fill lights, the hall point lights, the ceiling track spots, and the HDR `environmentIntensity`. It defaults to **1**, so only styles that opt in are affected. Window fill lights are deliberately **not** scaled, so the daylight shafts still read against the darker room.
|
Shared lights carry a **brightness floor** so dark period styles never render as a black void. That floor is high enough to blow out a pale wall, so **`lightScale`** in `movement-interior-styles.ts` is the knob that holds a hall back — it multiplies the scene ambient/hemisphere/directional/fill lights, the hall point lights, the ceiling track spots, and the HDR `environmentIntensity`. Window fill lights are deliberately **not** scaled, so daylight shafts still read against the darker room.
|
||||||
|
|
||||||
|
It defaults to **1**, but most halls now set it: Gothic uses **0.26**, Byzantine **0.30**, and the 19 formerly pale interiors (Roman through Pop Art) also use **0.30**. Only the five halls that are already dark by albedo — Symbolist, Expressionist, Surrealist, Baroque, Romantic — stay at 1, since scaling their lights would crush walls that already sit at 0.15–0.27 luminance.
|
||||||
|
|
||||||
|
**Wall brightness is albedo × `lightScale`.** Gallery walls are authored well below white for that reason: the reworked interiors sit around **0.50–0.56** wall luminance (0.66 for the two white-cube halls), landing at an effective 0.14–0.20 — the range Gothic and Byzantine established. Reach for `lightScale` before darkening a tint further; the two multiply, and dimming the hall dims the paintings with it.
|
||||||
|
|
||||||
**Period architecture:** Freestanding columns must not sit in the walkway or in the corner pocket in front of frames (hang margin is only ~0.7 m from each wall end). Classical / neoclassical details use **engaged corner pilasters** flush to the walls (`MovementHallDetails.tsx`).
|
**Period architecture:** Freestanding columns must not sit in the walkway or in the corner pocket in front of frames (hang margin is only ~0.7 m from each wall end). Classical / neoclassical details use **engaged corner pilasters** flush to the walls (`MovementHallDetails.tsx`).
|
||||||
|
|
||||||
|
|||||||
@@ -278,9 +278,38 @@ To add or fix a branch, edit `MOVEMENT_LINEAGE` in that file and rebuild the cli
|
|||||||
|
|
||||||
## Movement gallery interiors (frontend)
|
## Movement gallery interiors (frontend)
|
||||||
|
|
||||||
Separate from movement lineage layout, `client/src/data/movement-interior-styles.ts` defines a **unique 3D interior** for each seeded art movement (26 styles): wall/floor/ceiling textures, trim colours, window style, and architectural details (pilasters, coffered ceilings, etc.). Textures are generated procedurally in `client/src/utils/galleryProceduralTextures.ts` (colour maps in sRGB, normal maps in linear/`NoColorSpace`). Period mesh details live in `client/src/components/MovementHallDetails.tsx` — classical / neoclassical use **shallow engaged corner pilasters** so shafts never cover frames; Byzantine adds porphyry colonnettes / polycandela; Gothic (`details: 'gothic'`) adds bay-spaced compound piers with vault springers and transverse ribs flush to the side walls.
|
Separate from movement lineage layout, `client/src/data/movement-interior-styles.ts` defines a **unique 3D interior** for each seeded art movement (26 styles): wall/floor/ceiling textures, trim colours, window style, and the `details` kind that selects its architecture. Textures are generated procedurally in `client/src/utils/galleryProceduralTextures.ts` (colour maps in sRGB, normal maps in linear/`NoColorSpace`).
|
||||||
|
|
||||||
Gothic surfaces include **`gothic-ashlar`** (full-storey blind arcade mapped once vertically via `metersPerRepeat === WALL_HEIGHT`), **`gothic-vault`** (quadripartite rib bay), and **`gothic-stone-floor`**. Door-flanking wall panels use the hall wall texture via `GalleryWall` `kind`/`tint` so they match the textured walls. Deliberately dim naves set **`lightScale`** (Byzantine / Gothic) so shared lights stay soft while window shafts stay bright.
|
### Period architecture per movement
|
||||||
|
|
||||||
|
Every movement is built from the architecture of **its own period** — no two movements share an interior. `client/src/components/MovementHallDetails.tsx` dispatches on `style.details` (`MovementDetailKind`, 26 values) to one component:
|
||||||
|
|
||||||
|
| Module | Movements |
|
||||||
|
|--------|-----------|
|
||||||
|
| `MovementHallDetails.tsx` (reference implementations) | `gothic` — bay-spaced compound piers, vault springers, transverse ribs · `byzantine` — porphyry colonnettes, basket capitals, revetment dado, hanging polycandela |
|
||||||
|
| `hall-details/classical.tsx` | `roman` (Tuscan half-columns, dentil entablature, Pompeian socle) · `quattrocento` (pietra serena pilasters, blind arcade, tondi) · `cinquecento` (paired fluted Corinthian pilasters, Pantheon coffers) |
|
||||||
|
| `hall-details/courtly.tsx` | `flemish` (linenfold wainscot, beam-and-corbel ceiling) · `mannerist` (herm pilasters, banded rustication, broken cornice) · `baroque` (solomonic corner columns, modillions, velvet field panels) · `rococo` (boiserie, rocaille cartouches, painted cove) · `neoclassical` (fluted Ionic order, meander frieze) |
|
||||||
|
| `hall-details/nineteenth.tsx` | `gothic-revival` · `bourgeois` · `north-light` · `paris-atelier` · `symbolist` · `art-nouveau` |
|
||||||
|
| `hall-details/modernism.tsx` | `fauvist` · `expressionist` · `cubist-atelier` · `futurist` · `suprematist` · `constructivist` · `dada` · `surrealist` · `loft` · `white-cube` |
|
||||||
|
|
||||||
|
`hall-details/primitives.tsx` holds the wall-flush building blocks (`WallBand`, `CorniceRing`, `FlutedShaft`, capitals, `CofferedCeiling`, `HangingFixture`, `WallSconce`); `hall-details/geometry.ts` holds the shared hooks and the **geometry budget** every module has to obey, because frames hang 0.23 m proud of the wall face with a 0.7 m clear margin at each wall end:
|
||||||
|
|
||||||
|
- relief below the frame line stays within **0.11 m** proud; friezes and cornices above 2.6 m within **0.17 m**
|
||||||
|
- freestanding masses only inside the **0.34 m corner pocket**; nothing under 2.6 m within 1.45 m of a wall centre (doorways)
|
||||||
|
- the end wall carries the hall title between **3.4 m and 3.9 m** — keep it clear
|
||||||
|
- side-wall windows are placed at runtime between 2.4 m and 3.9 m, so panels, arches and roundels up there are filtered through **`useWallClearance`** (the hall passes its computed windows into `MovementHallDetails`)
|
||||||
|
- at most **two added lights per hall**, so hall materials stay inside the WebGL light limits the shared track lighting already budgets for
|
||||||
|
|
||||||
|
Gothic surfaces include **`gothic-ashlar`** (full-storey blind arcade mapped once vertically via `metersPerRepeat === WALL_HEIGHT`), **`gothic-vault`** (quadripartite rib bay), and **`gothic-stone-floor`**. Door-flanking wall panels use the hall wall texture via `GalleryWall` `kind`/`tint` so they match the textured walls.
|
||||||
|
|
||||||
|
### Wall brightness
|
||||||
|
|
||||||
|
Halls are lit by a shared rig with a high brightness floor, so a pale wall tint clips to flat white and loses its texture. Two settings control this together, and they **multiply**:
|
||||||
|
|
||||||
|
- **tints** — gallery walls are authored around **0.50–0.56** relative luminance (0.66 for the Suprematist and Pop white cubes, 0.15–0.27 for the deliberately dark halls). Nothing sits near white.
|
||||||
|
- **`lightScale`** — Gothic **0.26**, Byzantine **0.30**, and every formerly pale interior **0.30**; the already-dark halls keep **1**. Window fills are not scaled, so daylight shafts stay bright.
|
||||||
|
|
||||||
|
When a hall reads too bright, prefer `lightScale`: it keeps the material colour believable (Byzantine's warm stone is a genuine `#a89880`, simply lit dimly) and avoids greying tints toward neutral. Remember that dimming a hall dims its paintings too — the lighting is shared, with no per-painting spots.
|
||||||
|
|
||||||
Wing layout (up to 55 works per wing, U-shaped hang, window gap placement with packed-wall **clerestory** fallback) lives in `client/src/utils/movementHallLayout.ts`. Visit order fills the **left wall** (first work at the entrance), then the **far/end wall**, then the **right** (last work at the entrance). The same hang applies to artist halls and guided tours. Hall lighting is **shared** (no per-painting spotlights) so `MeshStandardMaterial` walls stay within WebGL light limits — see [basics.md](basics.md) § Shared 3D behaviour. To change a movement’s look, edit its entry in `movement-interior-styles.ts` and rebuild the client.
|
Wing layout (up to 55 works per wing, U-shaped hang, window gap placement with packed-wall **clerestory** fallback) lives in `client/src/utils/movementHallLayout.ts`. Visit order fills the **left wall** (first work at the entrance), then the **far/end wall**, then the **right** (last work at the entrance). The same hang applies to artist halls and guided tours. Hall lighting is **shared** (no per-painting spotlights) so `MeshStandardMaterial` walls stay within WebGL light limits — see [basics.md](basics.md) § Shared 3D behaviour. To change a movement’s look, edit its entry in `movement-interior-styles.ts` and rebuild the client.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,38 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import * as THREE from 'three';
|
import type { MovementInteriorStyle, GalleryWindowSpec } from '../data/movement-interior-styles';
|
||||||
import type { MovementInteriorStyle } from '../data/movement-interior-styles';
|
import type { RoomDims } from './hall-details/geometry';
|
||||||
|
import {
|
||||||
|
CinquecentoDetails,
|
||||||
|
QuattrocentoDetails,
|
||||||
|
RomanAtriumDetails,
|
||||||
|
} from './hall-details/classical';
|
||||||
|
import {
|
||||||
|
BaroqueStateDetails,
|
||||||
|
FlemishDetails,
|
||||||
|
ManneristDetails,
|
||||||
|
NeoclassicalDetails,
|
||||||
|
RococoDetails,
|
||||||
|
} from './hall-details/courtly';
|
||||||
|
import {
|
||||||
|
ArtNouveauDetails,
|
||||||
|
BourgeoisSalonDetails,
|
||||||
|
GothicRevivalDetails,
|
||||||
|
NorthLightSalonDetails,
|
||||||
|
ParisAtelierDetails,
|
||||||
|
SymbolistDetails,
|
||||||
|
} from './hall-details/nineteenth';
|
||||||
|
import {
|
||||||
|
ConstructivistDetails,
|
||||||
|
CubistAtelierDetails,
|
||||||
|
DadaDetails,
|
||||||
|
ExpressionistDetails,
|
||||||
|
FauvistDetails,
|
||||||
|
FuturistDetails,
|
||||||
|
LoftDetails,
|
||||||
|
SuprematistDetails,
|
||||||
|
SurrealistDetails,
|
||||||
|
WhiteCubeDetails,
|
||||||
|
} from './hall-details/modernism';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
style: MovementInteriorStyle;
|
style: MovementInteriorStyle;
|
||||||
@@ -8,99 +40,11 @@ interface Props {
|
|||||||
depth: number;
|
depth: number;
|
||||||
halfW: number;
|
halfW: number;
|
||||||
halfD: number;
|
halfD: number;
|
||||||
|
/** Runtime window placement, so upper-wall relief can step around openings. */
|
||||||
|
windows?: GalleryWindowSpec[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function PalazzoDetails({ width, depth, halfW, halfD, trim }: Props & { trim: string }) {
|
function GothicDetails({ width, depth, halfW, halfD, trim }: Omit<Props, 'style' | 'windows'> & { trim: string }) {
|
||||||
const pilasterPositions = useMemo(
|
|
||||||
() =>
|
|
||||||
[
|
|
||||||
[-halfW + 0.35, -halfD + 0.35],
|
|
||||||
[halfW - 0.35, -halfD + 0.35],
|
|
||||||
[-halfW + 0.35, halfD - 0.35],
|
|
||||||
[halfW - 0.35, halfD - 0.35],
|
|
||||||
] as [number, number][],
|
|
||||||
[halfW, halfD]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group>
|
|
||||||
{pilasterPositions.map(([x, z], i) => (
|
|
||||||
<group key={i} position={[x, 0, z]}>
|
|
||||||
<mesh position={[0, 1.8, 0]}>
|
|
||||||
<boxGeometry args={[0.22, 3.6, 0.22]} />
|
|
||||||
<meshStandardMaterial color="#e8dcc8" roughness={0.85} metalness={0.05} />
|
|
||||||
</mesh>
|
|
||||||
<mesh position={[0, 3.65, 0]}>
|
|
||||||
<boxGeometry args={[0.28, 0.14, 0.28]} />
|
|
||||||
<meshStandardMaterial color={trim} roughness={0.35} metalness={0.55} />
|
|
||||||
</mesh>
|
|
||||||
</group>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Wainscoting rail */}
|
|
||||||
{[
|
|
||||||
[0, -halfD + 0.09, width, 0.12] as const,
|
|
||||||
[0, halfD - 0.09, width, 0.12] as const,
|
|
||||||
[-halfW + 0.09, 0, 0.12, depth] as const,
|
|
||||||
[halfW - 0.09, 0, 0.12, depth] as const,
|
|
||||||
].map(([x, z, w, d], i) => (
|
|
||||||
<mesh key={i} position={[x, 1.05, z]}>
|
|
||||||
<boxGeometry args={[w, 0.08, d]} />
|
|
||||||
<meshStandardMaterial color="#ddd0b8" roughness={0.78} metalness={0.08} />
|
|
||||||
</mesh>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Coffered ceiling */}
|
|
||||||
{Array.from({ length: Math.min(6, Math.floor(width / 2.5)) }, (_, col) =>
|
|
||||||
Array.from({ length: Math.min(5, Math.floor(depth / 2.8)) }, (_, row) => {
|
|
||||||
const cx = -halfW + 1.4 + col * 2.4;
|
|
||||||
const cz = -halfD + 1.5 + row * 2.6;
|
|
||||||
return (
|
|
||||||
<mesh key={`${col}-${row}`} position={[cx, 4.12, cz]} rotation={[Math.PI / 2, 0, 0]}>
|
|
||||||
<boxGeometry args={[2.0, 2.2, 0.06]} />
|
|
||||||
<meshStandardMaterial color="#f5efe4" roughness={0.88} />
|
|
||||||
</mesh>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function BaroqueDetails({ width, depth, halfW, halfD, trim }: Props & { trim: string }) {
|
|
||||||
return (
|
|
||||||
<group>
|
|
||||||
{/* Gilded cornice ring */}
|
|
||||||
{[
|
|
||||||
[0, -halfD + 0.06, width, 0.1] as const,
|
|
||||||
[0, halfD - 0.06, width, 0.1] as const,
|
|
||||||
[-halfW + 0.06, 0, 0.1, depth] as const,
|
|
||||||
[halfW - 0.06, 0, 0.1, depth] as const,
|
|
||||||
].map(([x, z, w, d], i) => (
|
|
||||||
<mesh key={i} position={[x, 3.95, z]}>
|
|
||||||
<boxGeometry args={[w, 0.18, d]} />
|
|
||||||
<meshStandardMaterial color={trim} roughness={0.25} metalness={0.75} emissive="#3a2808" emissiveIntensity={0.08} />
|
|
||||||
</mesh>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Wall panels */}
|
|
||||||
{[-halfW + 0.12, halfW - 0.12].map((x, i) => (
|
|
||||||
<mesh key={i} position={[x, 2.1, 0]} rotation={[0, Math.PI / 2, 0]}>
|
|
||||||
<boxGeometry args={[depth * 0.85, 2.8, 0.04]} />
|
|
||||||
<meshStandardMaterial color="#3a1820" roughness={0.88} metalness={0.06} />
|
|
||||||
</mesh>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<pointLight position={[0, 3.6, 0]} intensity={0.9} distance={Math.max(width, depth)} color="#ffd080" />
|
|
||||||
<mesh position={[0, 3.5, 0]}>
|
|
||||||
<torusGeometry args={[0.55, 0.04, 8, 24]} />
|
|
||||||
<meshStandardMaterial color={trim} roughness={0.2} metalness={0.85} emissive="#5a4010" emissiveIntensity={0.15} />
|
|
||||||
</mesh>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function GothicDetails({ width, depth, halfW, halfD, trim }: Props & { trim: string }) {
|
|
||||||
// Compound piers along the side walls, spaced by bay, each rising into the
|
// Compound piers along the side walls, spaced by bay, each rising into the
|
||||||
// vault springer. Shafts stay flush against the wall (0.16 m proud) so they
|
// vault springer. Shafts stay flush against the wall (0.16 m proud) so they
|
||||||
// never intrude on the ~0.7 m frame hang margin.
|
// never intrude on the ~0.7 m frame hang margin.
|
||||||
@@ -292,102 +236,75 @@ function ByzantineDetails({ halfW, halfD, trim }: Pick<Props, 'halfW' | 'halfD'>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NeoclassicalDetails({ halfW, halfD, trim }: Pick<Props, 'halfW' | 'halfD'> & { trim: string }) {
|
/**
|
||||||
// Shallow engaged corner pilasters only. Freestanding columns (~0.5m into the
|
* Period architecture for a movement hall.
|
||||||
// room) overlap the outermost frames — hang margin is only ~0.7m from each end.
|
*
|
||||||
const pilasters = useMemo(
|
* Every movement has its own interior, built from the architecture of the same
|
||||||
() =>
|
* period as the movement itself. The Gothic nave and Byzantine basilica below
|
||||||
[
|
* are the reference implementations; everything else lives in
|
||||||
[-halfW + 0.06, -halfD + 0.06],
|
* `hall-details/`, sharing the geometry budget documented in
|
||||||
[halfW - 0.06, -halfD + 0.06],
|
* `hall-details/primitives.tsx`.
|
||||||
[-halfW + 0.06, halfD - 0.06],
|
*/
|
||||||
[halfW - 0.06, halfD - 0.06],
|
export default function MovementHallDetails({ style, width, depth, halfW, halfD, windows }: Props) {
|
||||||
] as [number, number][],
|
|
||||||
[halfW, halfD]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group>
|
|
||||||
{pilasters.map(([x, z], i) => (
|
|
||||||
<group key={i} position={[x, 0, z]}>
|
|
||||||
<mesh position={[0, 1.85, 0]}>
|
|
||||||
<boxGeometry args={[0.12, 3.7, 0.12]} />
|
|
||||||
<meshStandardMaterial color="#f0ece8" roughness={0.72} metalness={0.12} />
|
|
||||||
</mesh>
|
|
||||||
<mesh position={[0, 3.78, 0]}>
|
|
||||||
<boxGeometry args={[0.18, 0.1, 0.18]} />
|
|
||||||
<meshStandardMaterial color={trim} roughness={0.4} metalness={0.35} />
|
|
||||||
</mesh>
|
|
||||||
</group>
|
|
||||||
))}
|
|
||||||
{/* Cornice above wall title (title sits ~y 3.75) */}
|
|
||||||
<mesh position={[0, 4.08, -halfD + 0.1]}>
|
|
||||||
<boxGeometry args={[Math.max(2.4, halfW * 2 - 0.5), 0.1, 0.16]} />
|
|
||||||
<meshStandardMaterial color={trim} roughness={0.45} metalness={0.3} />
|
|
||||||
</mesh>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ClassicalDetails({ halfW, halfD, trim }: Pick<Props, 'halfW' | 'halfD'> & { trim: string }) {
|
|
||||||
// Engaged corner shafts only — anything proud of the corner pocket covers frames.
|
|
||||||
const columns = useMemo(
|
|
||||||
() =>
|
|
||||||
[
|
|
||||||
[-halfW + 0.06, -halfD + 0.06],
|
|
||||||
[halfW - 0.06, -halfD + 0.06],
|
|
||||||
[-halfW + 0.06, halfD - 0.06],
|
|
||||||
[halfW - 0.06, halfD - 0.06],
|
|
||||||
] as [number, number][],
|
|
||||||
[halfW, halfD]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group>
|
|
||||||
{columns.map(([x, z], i) => (
|
|
||||||
<group key={i} position={[x, 0, z]}>
|
|
||||||
<mesh position={[0, 2, 0]}>
|
|
||||||
<cylinderGeometry args={[0.08, 0.09, 4, 10]} />
|
|
||||||
<meshStandardMaterial color="#e0d8c8" roughness={0.88} />
|
|
||||||
</mesh>
|
|
||||||
<mesh position={[0, 4.05, 0]}>
|
|
||||||
<boxGeometry args={[0.18, 0.1, 0.18]} />
|
|
||||||
<meshStandardMaterial color={trim} roughness={0.5} metalness={0.25} />
|
|
||||||
</mesh>
|
|
||||||
</group>
|
|
||||||
))}
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SalonDetails({ trim }: { trim: string }) {
|
|
||||||
return (
|
|
||||||
<mesh position={[0, 4.05, 0]} rotation={[Math.PI / 2, 0, 0]}>
|
|
||||||
<ringGeometry args={[0.3, 1.2, 32]} />
|
|
||||||
<meshStandardMaterial color={trim} roughness={0.35} metalness={0.5} side={THREE.DoubleSide} />
|
|
||||||
</mesh>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MovementHallDetails({ style, width, depth, halfW, halfD }: Props) {
|
|
||||||
const trim = style.tints.trim;
|
const trim = style.tints.trim;
|
||||||
|
const room: RoomDims = useMemo(
|
||||||
|
() => ({ width, depth, halfW, halfD }),
|
||||||
|
[width, depth, halfW, halfD]
|
||||||
|
);
|
||||||
|
const period = { room, trim, windows };
|
||||||
|
|
||||||
switch (style.details) {
|
switch (style.details) {
|
||||||
case 'palazzo':
|
|
||||||
return <PalazzoDetails style={style} width={width} depth={depth} halfW={halfW} halfD={halfD} trim={trim} />;
|
|
||||||
case 'baroque':
|
|
||||||
return <BaroqueDetails style={style} width={width} depth={depth} halfW={halfW} halfD={halfD} trim={trim} />;
|
|
||||||
case 'gothic':
|
case 'gothic':
|
||||||
return <GothicDetails style={style} width={width} depth={depth} halfW={halfW} halfD={halfD} trim={trim} />;
|
return <GothicDetails width={width} depth={depth} halfW={halfW} halfD={halfD} trim={trim} />;
|
||||||
case 'byzantine':
|
case 'byzantine':
|
||||||
return <ByzantineDetails halfW={halfW} halfD={halfD} trim={trim} />;
|
return <ByzantineDetails halfW={halfW} halfD={halfD} trim={trim} />;
|
||||||
|
case 'roman':
|
||||||
|
return <RomanAtriumDetails {...period} />;
|
||||||
|
case 'quattrocento':
|
||||||
|
return <QuattrocentoDetails {...period} />;
|
||||||
|
case 'cinquecento':
|
||||||
|
return <CinquecentoDetails {...period} />;
|
||||||
|
case 'flemish':
|
||||||
|
return <FlemishDetails {...period} />;
|
||||||
|
case 'mannerist':
|
||||||
|
return <ManneristDetails {...period} />;
|
||||||
|
case 'baroque':
|
||||||
|
return <BaroqueStateDetails {...period} />;
|
||||||
|
case 'rococo':
|
||||||
|
return <RococoDetails {...period} />;
|
||||||
case 'neoclassical':
|
case 'neoclassical':
|
||||||
return <NeoclassicalDetails halfW={halfW} halfD={halfD} trim={trim} />;
|
return <NeoclassicalDetails {...period} />;
|
||||||
case 'classical':
|
case 'gothic-revival':
|
||||||
return <ClassicalDetails halfW={halfW} halfD={halfD} trim={trim} />;
|
return <GothicRevivalDetails {...period} />;
|
||||||
case 'salon':
|
case 'bourgeois':
|
||||||
return <SalonDetails trim={trim} />;
|
return <BourgeoisSalonDetails {...period} />;
|
||||||
default:
|
case 'north-light':
|
||||||
return null;
|
return <NorthLightSalonDetails {...period} />;
|
||||||
|
case 'paris-atelier':
|
||||||
|
return <ParisAtelierDetails {...period} />;
|
||||||
|
case 'symbolist':
|
||||||
|
return <SymbolistDetails {...period} />;
|
||||||
|
case 'art-nouveau':
|
||||||
|
return <ArtNouveauDetails {...period} />;
|
||||||
|
case 'fauvist':
|
||||||
|
return <FauvistDetails {...period} />;
|
||||||
|
case 'expressionist':
|
||||||
|
return <ExpressionistDetails {...period} />;
|
||||||
|
case 'cubist-atelier':
|
||||||
|
return <CubistAtelierDetails {...period} />;
|
||||||
|
case 'futurist':
|
||||||
|
return <FuturistDetails {...period} />;
|
||||||
|
case 'suprematist':
|
||||||
|
return <SuprematistDetails {...period} />;
|
||||||
|
case 'constructivist':
|
||||||
|
return <ConstructivistDetails {...period} />;
|
||||||
|
case 'dada':
|
||||||
|
return <DadaDetails {...period} />;
|
||||||
|
case 'surrealist':
|
||||||
|
return <SurrealistDetails {...period} />;
|
||||||
|
case 'loft':
|
||||||
|
return <LoftDetails {...period} />;
|
||||||
|
case 'white-cube':
|
||||||
|
return <WhiteCubeDetails {...period} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,12 @@ import type {
|
|||||||
import { galleryImageUrlCandidates, imageUrl, api } from '../api/client';
|
import { galleryImageUrlCandidates, imageUrl, api } from '../api/client';
|
||||||
import { comparePaintingsChronological, paintingHasCuratorNotes, paintingHasInfluenceLinks, paintingWallCaption } from '../utils/paintingUtils';
|
import { comparePaintingsChronological, paintingHasCuratorNotes, paintingHasInfluenceLinks, paintingWallCaption } from '../utils/paintingUtils';
|
||||||
import { cloneSurfaceTexture, getSurfaceTexture, type SurfaceTextureKind } from '../utils/galleryProceduralTextures';
|
import { cloneSurfaceTexture, getSurfaceTexture, type SurfaceTextureKind } from '../utils/galleryProceduralTextures';
|
||||||
import { resolveMovementInteriorStyle, type MovementInteriorStyle, type GalleryWindowSpec } from '../data/movement-interior-styles';
|
import {
|
||||||
|
resolveMovementInteriorStyle,
|
||||||
|
type MovementDetailKind,
|
||||||
|
type MovementInteriorStyle,
|
||||||
|
type GalleryWindowSpec,
|
||||||
|
} from '../data/movement-interior-styles';
|
||||||
import { useTexturedMaterial } from '../hooks/useTexturedMaterial';
|
import { useTexturedMaterial } from '../hooks/useTexturedMaterial';
|
||||||
import MovementHallDetails from './MovementHallDetails';
|
import MovementHallDetails from './MovementHallDetails';
|
||||||
import GalleryWindows, { GalleryTrackLights } from './GalleryWindows';
|
import GalleryWindows, { GalleryTrackLights } from './GalleryWindows';
|
||||||
@@ -84,6 +89,15 @@ interface TourGalleryProps extends BaseGalleryProps {
|
|||||||
|
|
||||||
type Props = ArtistGalleryProps | MovementGalleryProps | TourGalleryProps;
|
type Props = ArtistGalleryProps | MovementGalleryProps | TourGalleryProps;
|
||||||
|
|
||||||
|
/** Period interiors built from steel, concrete or a white cube read better under the city HDR. */
|
||||||
|
const CITY_ENVIRONMENT_DETAILS = new Set<MovementDetailKind>([
|
||||||
|
'futurist',
|
||||||
|
'suprematist',
|
||||||
|
'constructivist',
|
||||||
|
'loft',
|
||||||
|
'white-cube',
|
||||||
|
]);
|
||||||
|
|
||||||
const WALL_HEIGHT = 4.2;
|
const WALL_HEIGHT = 4.2;
|
||||||
const WALL_THICKNESS = 0.18;
|
const WALL_THICKNESS = 0.18;
|
||||||
const MOUNT_OFFSET = 0.16;
|
const MOUNT_OFFSET = 0.16;
|
||||||
@@ -1739,7 +1753,14 @@ function ArtistHall({
|
|||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{interiorStyle && (
|
{interiorStyle && (
|
||||||
<MovementHallDetails style={interiorStyle} width={width} depth={depth} halfW={halfW} halfD={halfD} />
|
<MovementHallDetails
|
||||||
|
style={interiorStyle}
|
||||||
|
width={width}
|
||||||
|
depth={depth}
|
||||||
|
halfW={halfW}
|
||||||
|
halfD={halfD}
|
||||||
|
windows={computedWindows}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
@@ -2635,7 +2656,7 @@ export default function VirtualGallery(props: Props) {
|
|||||||
>
|
>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<Environment
|
<Environment
|
||||||
preset={interiorStyle?.details === 'modern' || interiorStyle?.details === 'industrial' ? 'city' : 'warehouse'}
|
preset={interiorStyle && CITY_ENVIRONMENT_DETAILS.has(interiorStyle.details) ? 'city' : 'warehouse'}
|
||||||
environmentIntensity={(interiorStyle ? 0.55 : 0.25) * lightScale}
|
environmentIntensity={(interiorStyle ? 0.55 : 0.25) * lightScale}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
import {
|
||||||
|
CeilingBeams,
|
||||||
|
CeilingRose,
|
||||||
|
CofferedCeiling,
|
||||||
|
CorniceRing,
|
||||||
|
FlutedShaft,
|
||||||
|
HangingFixture,
|
||||||
|
IonicCapital,
|
||||||
|
ShaftBase,
|
||||||
|
WallBand,
|
||||||
|
} from './primitives';
|
||||||
|
import {
|
||||||
|
shade,
|
||||||
|
useBayZ,
|
||||||
|
useCorners,
|
||||||
|
useRepeat,
|
||||||
|
useWallClearance,
|
||||||
|
type PeriodProps,
|
||||||
|
} from './geometry';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flemish panel hall — Northern Renaissance.
|
||||||
|
*
|
||||||
|
* Oak linenfold wainscot under a moulded cap rail, carved corbels carrying an
|
||||||
|
* exposed beam-and-joist ceiling, and the brass ring chandelier that hangs in
|
||||||
|
* the background of half the period's interiors.
|
||||||
|
*/
|
||||||
|
export function FlemishDetails({ room, trim }: PeriodProps) {
|
||||||
|
const beams = useBayZ(room.depth, room.halfD, 2.3, 10);
|
||||||
|
const oak = '#6f4a2c';
|
||||||
|
const oakLight = shade(oak, 0.2);
|
||||||
|
|
||||||
|
const folds = useRepeat(room.depth, 0.55, 34);
|
||||||
|
|
||||||
|
const backFolds = useRepeat(room.width, 0.55, 34);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{/* Wainscot ground, cap rail and skirting */}
|
||||||
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
||||||
|
<group key={wall}>
|
||||||
|
<WallBand wall={wall} room={room} y={0.66} height={1.24} proud={0.06} color={oak} roughness={0.66} metalness={0.08} />
|
||||||
|
<WallBand wall={wall} room={room} y={1.32} height={0.12} proud={0.1} color={oakLight} roughness={0.54} metalness={0.1} />
|
||||||
|
<WallBand wall={wall} room={room} y={0.08} height={0.16} proud={0.09} color={shade(oak, -0.3)} roughness={0.6} metalness={0.09} />
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Linenfold ribs across the wainscot */}
|
||||||
|
{(['left', 'right'] as const).map((wall) =>
|
||||||
|
folds.map((z, i) => (
|
||||||
|
<mesh
|
||||||
|
key={`${wall}-${i}`}
|
||||||
|
position={[(wall === 'left' ? -1 : 1) * (room.halfW - 0.07), 0.68, z]}
|
||||||
|
>
|
||||||
|
<cylinderGeometry args={[0.035, 0.035, 1.18, 6]} />
|
||||||
|
<meshStandardMaterial color={oakLight} roughness={0.6} metalness={0.09} />
|
||||||
|
</mesh>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
{backFolds.map((x, i) => (
|
||||||
|
<mesh key={`b-${i}`} position={[x, 0.68, -room.halfD + 0.07]}>
|
||||||
|
<cylinderGeometry args={[0.035, 0.035, 1.18, 6]} />
|
||||||
|
<meshStandardMaterial color={oakLight} roughness={0.6} metalness={0.09} />
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Carved corbels carrying the beams */}
|
||||||
|
{beams.map((z, i) => (
|
||||||
|
<group key={i}>
|
||||||
|
{[-1, 1].map((side) => (
|
||||||
|
<mesh key={side} position={[side * (room.halfW - 0.11), 3.84, z]}>
|
||||||
|
<boxGeometry args={[0.2, 0.24, 0.2]} />
|
||||||
|
<meshStandardMaterial color={shade(oak, -0.12)} roughness={0.66} metalness={0.08} />
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<CeilingBeams room={room} positions={beams} axis="x" width={0.18} height={0.26} y={3.98} color={shade(oak, -0.2)} roughness={0.72} metalness={0.06} />
|
||||||
|
<CorniceRing room={room} y={4.14} height={0.08} proud={0.12} color={oakLight} roughness={0.56} metalness={0.1} />
|
||||||
|
|
||||||
|
<HangingFixture
|
||||||
|
position={[0, 3.94, 0]}
|
||||||
|
dropLength={0.5}
|
||||||
|
radius={0.4}
|
||||||
|
arms={8}
|
||||||
|
metalColor={trim}
|
||||||
|
flameColor="#ffe0a0"
|
||||||
|
light
|
||||||
|
lightColor="#ffca80"
|
||||||
|
lightIntensity={0.85}
|
||||||
|
lightDistance={8}
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mannerist gallery — Mannerism.
|
||||||
|
*
|
||||||
|
* The order used against itself: herm pilasters that taper the wrong way,
|
||||||
|
* banded rustication blocking the shafts, oversized scroll consoles, and a
|
||||||
|
* cornice deliberately broken over each bay.
|
||||||
|
*/
|
||||||
|
export function ManneristDetails({ room, trim, windows }: PeriodProps) {
|
||||||
|
const clearLeft = useWallClearance(windows, 'left');
|
||||||
|
const clearRight = useWallClearance(windows, 'right');
|
||||||
|
const bays = useBayZ(room.depth, room.halfD, 4.4, 7);
|
||||||
|
const stone = '#ddc9ae';
|
||||||
|
const dark = shade(stone, -0.34);
|
||||||
|
const bands = [0.55, 1.55, 2.35, 3.05];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{/* Heavy blocked socle */}
|
||||||
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
||||||
|
<WallBand key={wall} wall={wall} room={room} y={0.5} height={1.0} proud={0.08} color={dark} roughness={0.82} metalness={0.05} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Herm pilasters — wider at the shoulder than at the foot */}
|
||||||
|
{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.9, 0]}>
|
||||||
|
<boxGeometry args={[0.09, 3.8, 0.3]} />
|
||||||
|
<meshStandardMaterial color={stone} roughness={0.78} metalness={0.05} />
|
||||||
|
</mesh>
|
||||||
|
{/* Rustication blocks interrupting the shaft */}
|
||||||
|
{bands.map((y) => (
|
||||||
|
<mesh key={y} position={[side * 0.03, y, 0]}>
|
||||||
|
<boxGeometry args={[0.14, 0.3, 0.44]} />
|
||||||
|
<meshStandardMaterial color={shade(stone, -0.16)} roughness={0.86} metalness={0.04} />
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
{/* Scroll console instead of a capital */}
|
||||||
|
<mesh position={[0, 3.86, 0]} rotation={[0, Math.PI / 2, 0]}>
|
||||||
|
<torusGeometry args={[0.14, 0.055, 8, 16, Math.PI * 1.5]} />
|
||||||
|
<meshStandardMaterial color={shade(stone, 0.12)} roughness={0.66} metalness={0.08} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, 4.03, 0]}>
|
||||||
|
<boxGeometry args={[0.16, 0.12, 0.46]} />
|
||||||
|
<meshStandardMaterial color={trim} roughness={0.5} metalness={0.24} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Broken cornice — segments that stop short of each bay */}
|
||||||
|
{(['left', 'right'] as const).map((wall) => (
|
||||||
|
<group key={wall}>
|
||||||
|
<WallBand wall={wall} room={room} y={3.96} height={0.12} proud={0.14} span={0.62} color={stone} roughness={0.72} metalness={0.06} />
|
||||||
|
<WallBand wall={wall} room={room} y={4.1} height={0.08} proud={0.17} span={0.86} color={trim} roughness={0.52} metalness={0.26} />
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
<CorniceRing room={room} y={4.1} height={0.08} proud={0.15} walls={['back', 'front']} color={trim} roughness={0.52} metalness={0.26} />
|
||||||
|
|
||||||
|
{/* Cartouches slung between the bays, clear of the window openings */}
|
||||||
|
{bays.slice(0, -1).map((z, i) => {
|
||||||
|
const mid = (z + bays[i + 1]) / 2;
|
||||||
|
return (
|
||||||
|
<group key={i}>
|
||||||
|
{([-1, 1] as const)
|
||||||
|
.filter((side) => (side < 0 ? clearLeft : clearRight)(mid, 0.24))
|
||||||
|
.map((side) => (
|
||||||
|
<group key={side} position={[side * (room.halfW - 0.07), 3.6, mid]} rotation={[0, Math.PI / 2, 0]}>
|
||||||
|
<mesh>
|
||||||
|
<sphereGeometry args={[0.17, 12, 8]} />
|
||||||
|
<meshStandardMaterial color={shade(stone, 0.16)} roughness={0.66} metalness={0.08} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, 0, 0.04]}>
|
||||||
|
<torusGeometry args={[0.19, 0.03, 6, 18]} />
|
||||||
|
<meshStandardMaterial color={trim} roughness={0.44} metalness={0.4} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baroque state gallery — Baroque.
|
||||||
|
*
|
||||||
|
* Twisted solomonic half-columns at the corners, gilt-framed velvet field
|
||||||
|
* panels between the bays, a modillion cornice, and a crystal-and-gilt
|
||||||
|
* chandelier over the centre of the room.
|
||||||
|
*/
|
||||||
|
export function BaroqueStateDetails({ room, trim, windows }: PeriodProps) {
|
||||||
|
const bays = useBayZ(room.depth, room.halfD, 4.2, 8);
|
||||||
|
const corners = useCorners(room.halfW, room.halfD, 0.16);
|
||||||
|
const clearLeft = useWallClearance(windows, 'left');
|
||||||
|
const clearRight = useWallClearance(windows, 'right');
|
||||||
|
const gilt = trim;
|
||||||
|
|
||||||
|
const modillions = useRepeat(room.depth, 0.86, 26);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{/* Gilt-edged velvet field panels between the bays, clear of the windows */}
|
||||||
|
{bays.slice(0, -1).map((z, i) => {
|
||||||
|
const mid = (z + bays[i + 1]) / 2;
|
||||||
|
const w = Math.min(1.5, Math.max(0.7, (bays[i + 1] - z) * 0.4));
|
||||||
|
return (
|
||||||
|
<group key={i}>
|
||||||
|
{([-1, 1] as const)
|
||||||
|
.filter((side) => (side < 0 ? clearLeft : clearRight)(mid, w / 2 + 0.1))
|
||||||
|
.map((side) => (
|
||||||
|
<group key={side} position={[side * (room.halfW - 0.055), 3.2, mid]}>
|
||||||
|
<mesh>
|
||||||
|
<boxGeometry args={[0.03, 1.0, w]} />
|
||||||
|
<meshStandardMaterial color="#4a1220" roughness={0.9} metalness={0.04} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[-side * 0.015, 0, 0]}>
|
||||||
|
<boxGeometry args={[0.04, 1.12, w + 0.12]} />
|
||||||
|
<meshStandardMaterial color={gilt} roughness={0.3} metalness={0.7} emissive="#3a2808" emissiveIntensity={0.06} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Solomonic corner columns — twist read as stacked, rotated drums */}
|
||||||
|
{corners.map(([x, z], i) => (
|
||||||
|
<group key={i} position={[x, 0, z]}>
|
||||||
|
<ShaftBase size={0.13} color="#2a1a12" />
|
||||||
|
{Array.from({ length: 14 }, (_, d) => (
|
||||||
|
<mesh key={d} position={[0, 0.34 + d * 0.24, 0]} rotation={[0, d * 0.42, 0]}>
|
||||||
|
<boxGeometry args={[0.2, 0.24, 0.13]} />
|
||||||
|
<meshStandardMaterial color={gilt} roughness={0.32} metalness={0.68} emissive="#2e2006" emissiveIntensity={0.05} />
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
<mesh position={[0, 3.86, 0]}>
|
||||||
|
<cylinderGeometry args={[0.2, 0.13, 0.28, 10]} />
|
||||||
|
<meshStandardMaterial color={gilt} roughness={0.28} metalness={0.75} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Modillion brackets under a heavy gilt cornice */}
|
||||||
|
{(['left', 'right'] as const).map((wall) =>
|
||||||
|
modillions.map((z, i) => (
|
||||||
|
<mesh key={`${wall}-${i}`} position={[(wall === 'left' ? -1 : 1) * (room.halfW - 0.17), 3.92, z]}>
|
||||||
|
<boxGeometry args={[0.18, 0.16, 0.12]} />
|
||||||
|
<meshStandardMaterial color={shade(gilt, 0.1)} roughness={0.34} metalness={0.62} />
|
||||||
|
</mesh>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<CorniceRing room={room} y={4.06} height={0.18} proud={0.19} color={gilt} roughness={0.24} metalness={0.78} emissive="#3a2808" emissiveIntensity={0.08} />
|
||||||
|
|
||||||
|
<CeilingRose y={4.1} radius={1.15} color={gilt} rings={4} />
|
||||||
|
<HangingFixture
|
||||||
|
position={[0, 4.05, 0]}
|
||||||
|
dropLength={0.5}
|
||||||
|
radius={0.6}
|
||||||
|
arms={10}
|
||||||
|
metalColor={gilt}
|
||||||
|
flameColor="#fff0c0"
|
||||||
|
glowColor="#ffbe58"
|
||||||
|
light
|
||||||
|
lightColor="#ffd08a"
|
||||||
|
lightIntensity={1.0}
|
||||||
|
lightDistance={11}
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rococo salon — Rococo.
|
||||||
|
*
|
||||||
|
* Boiserie panelling with rounded corner arcs, asymmetric rocaille cartouches,
|
||||||
|
* a deep painted cove instead of a cornice, and a light gilt lustre. Everything
|
||||||
|
* is kept thin and pale: the period's relief is shallow by design.
|
||||||
|
*/
|
||||||
|
export function RococoDetails({ room, trim }: PeriodProps) {
|
||||||
|
const bays = useBayZ(room.depth, room.halfD, 3.4, 9);
|
||||||
|
const gilt = trim;
|
||||||
|
const boiserie = '#efe6ee';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{/* Boiserie dado with a moulded chair rail */}
|
||||||
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
||||||
|
<group key={wall}>
|
||||||
|
<WallBand wall={wall} room={room} y={0.55} height={1.04} proud={0.045} color={boiserie} roughness={0.6} metalness={0.08} />
|
||||||
|
<WallBand wall={wall} room={room} y={1.12} height={0.07} proud={0.08} color={gilt} roughness={0.34} metalness={0.52} />
|
||||||
|
<WallBand wall={wall} room={room} y={0.07} height={0.14} proud={0.08} color={boiserie} roughness={0.62} metalness={0.07} />
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Panel styles with quarter-round corners, and a rocaille cartouche above */}
|
||||||
|
{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.04, 4.0, 0.07]} />
|
||||||
|
<meshStandardMaterial color={gilt} roughness={0.36} metalness={0.5} />
|
||||||
|
</mesh>
|
||||||
|
<group position={[0, 3.5, 0]} rotation={[0, Math.PI / 2, 0]}>
|
||||||
|
<mesh>
|
||||||
|
<torusGeometry args={[0.2, 0.028, 6, 18, Math.PI * 1.35]} />
|
||||||
|
<meshStandardMaterial color={gilt} roughness={0.3} metalness={0.6} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0.1, -0.14, 0]} rotation={[0, 0, 0.7]}>
|
||||||
|
<torusGeometry args={[0.12, 0.022, 6, 14, Math.PI * 1.1]} />
|
||||||
|
<meshStandardMaterial color={gilt} roughness={0.3} metalness={0.6} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Painted cove: two stacked, slightly stepped bands reading as a curve */}
|
||||||
|
<CorniceRing room={room} y={3.86} height={0.16} proud={0.08} color={shade(boiserie, 0.04)} roughness={0.72} metalness={0.05} />
|
||||||
|
<CorniceRing room={room} y={4.0} height={0.16} proud={0.14} color={boiserie} roughness={0.7} metalness={0.05} />
|
||||||
|
<CorniceRing room={room} y={4.13} height={0.07} proud={0.17} color={gilt} roughness={0.3} metalness={0.6} />
|
||||||
|
|
||||||
|
<CeilingRose y={4.09} radius={0.9} color={gilt} rings={3} />
|
||||||
|
<HangingFixture
|
||||||
|
position={[0, 4.02, 0]}
|
||||||
|
dropLength={0.42}
|
||||||
|
radius={0.42}
|
||||||
|
arms={8}
|
||||||
|
metalColor={gilt}
|
||||||
|
flameColor="#fff4dc"
|
||||||
|
glowColor="#ffd58c"
|
||||||
|
light
|
||||||
|
lightColor="#ffe6c0"
|
||||||
|
lightIntensity={0.8}
|
||||||
|
lightDistance={10}
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Neoclassical museum — Neoclassicism.
|
||||||
|
*
|
||||||
|
* Schinkel's museum grammar: fluted Ionic pilasters on a wide bay, a Greek-key
|
||||||
|
* meander frieze, a dentilled cornice, and a plain coffered soffit around the
|
||||||
|
* lantern. Deliberately cool and flat next to the Baroque hall.
|
||||||
|
*/
|
||||||
|
export function NeoclassicalDetails({ room, trim }: PeriodProps) {
|
||||||
|
const bays = useBayZ(room.depth, room.halfD, 4.8, 6);
|
||||||
|
const stone = '#f1eee8';
|
||||||
|
const shadowStone = shade(stone, -0.14);
|
||||||
|
|
||||||
|
const meander = useRepeat(room.depth, 0.72, 24);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{/* Plinth course under the hang */}
|
||||||
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
||||||
|
<group key={wall}>
|
||||||
|
<WallBand wall={wall} room={room} y={0.36} height={0.72} proud={0.05} color={shadowStone} roughness={0.72} metalness={0.08} />
|
||||||
|
<WallBand wall={wall} room={room} y={0.75} height={0.08} proud={0.08} color={stone} roughness={0.62} metalness={0.1} />
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Fluted Ionic pilasters */}
|
||||||
|
{bays.map((z, i) => (
|
||||||
|
<group key={i}>
|
||||||
|
{[-1, 1].map((side) => (
|
||||||
|
<group key={side} position={[side * (room.halfW - 0.13), 0, z]}>
|
||||||
|
<ShaftBase size={0.13} color={shadowStone} />
|
||||||
|
<group position={[0, 0.26, 0]}>
|
||||||
|
<FlutedShaft height={3.3} radius={0.12} color={stone} flutes={10} facing="x" />
|
||||||
|
</group>
|
||||||
|
<IonicCapital y={3.68} size={0.15} color={stone} axis="z" />
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Meander frieze — alternating key blocks read as a running fret */}
|
||||||
|
{(['left', 'right'] as const).map((wall) => (
|
||||||
|
<group key={wall}>
|
||||||
|
<WallBand wall={wall} room={room} y={3.9} height={0.24} proud={0.1} color={shade(stone, 0.03)} roughness={0.66} metalness={0.09} />
|
||||||
|
{meander.map((z, i) => (
|
||||||
|
<group key={i} position={[(wall === 'left' ? -1 : 1) * (room.halfW - 0.14), 3.9, z]}>
|
||||||
|
<mesh position={[0, i % 2 === 0 ? 0.06 : -0.06, 0]}>
|
||||||
|
<boxGeometry args={[0.06, 0.05, 0.34]} />
|
||||||
|
<meshStandardMaterial color={trim} roughness={0.5} metalness={0.28} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, 0, 0.15]}>
|
||||||
|
<boxGeometry args={[0.06, 0.16, 0.05]} />
|
||||||
|
<meshStandardMaterial color={trim} roughness={0.5} metalness={0.28} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<CorniceRing room={room} y={4.07} height={0.1} proud={0.16} color={stone} roughness={0.6} metalness={0.1} />
|
||||||
|
<CorniceRing room={room} y={4.15} height={0.06} proud={0.19} color={trim} roughness={0.48} metalness={0.24} />
|
||||||
|
|
||||||
|
<CofferedCeiling
|
||||||
|
room={room}
|
||||||
|
y={4.06}
|
||||||
|
cell={2.9}
|
||||||
|
panelColor="#f7f6f2"
|
||||||
|
ribColor="#e2ded4"
|
||||||
|
ribWidth={0.18}
|
||||||
|
ribDrop={0.13}
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import type { GalleryWindowSpec } from '../../data/movement-interior-styles';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared geometry budget for every period interior.
|
||||||
|
*
|
||||||
|
* Movement halls hang frames 0.23 m proud of the inner wall face
|
||||||
|
* (`MOUNT_OFFSET` + `WALL_STANDOFF`), centred at eye height with tops around
|
||||||
|
* 2.4 m, and each wall keeps a 0.7 m clear margin at both ends
|
||||||
|
* (`WALL_PADDING` in `movementHallLayout.ts`). Period architecture therefore
|
||||||
|
* has to stay inside these limits, exactly as the Gothic nave and Byzantine
|
||||||
|
* basilica do:
|
||||||
|
*
|
||||||
|
* - relief below the frame line: at most 0.11 m proud of the wall face
|
||||||
|
* - friezes and cornices: 2.6 m and above, at most 0.17 m proud
|
||||||
|
* - freestanding masses: only inside the 0.34 m corner pocket at a corner
|
||||||
|
* - doorways: nothing below 2.6 m within 1.45 m of a wall centre
|
||||||
|
* - the end wall carries the hall title from 3.4 m to 3.9 m — keep it clear
|
||||||
|
* - side-wall windows are placed at runtime between 2.4 m and 3.9 m, so
|
||||||
|
* anything wider than a mullion up there goes through `useWallClearance`
|
||||||
|
* - added lights: at most two per hall, so hall materials stay inside the
|
||||||
|
* WebGL light limits the shared track lighting already budgets for
|
||||||
|
*/
|
||||||
|
/** Half of WALL_THICKNESS — distance from a wall's centre plane to its inner face. */
|
||||||
|
export const WALL_INNER = 0.09;
|
||||||
|
/** Highest a detail can sit before it breaks the ceiling plane at 4.2 m. */
|
||||||
|
export const CEILING_Y = 4.13;
|
||||||
|
/** Sink bands this far into the wall so they never z-fight with the surface. */
|
||||||
|
export const EMBED = 0.02;
|
||||||
|
|
||||||
|
export type WallId = 'back' | 'front' | 'left' | 'right';
|
||||||
|
|
||||||
|
export interface RoomDims {
|
||||||
|
width: number;
|
||||||
|
depth: number;
|
||||||
|
halfW: number;
|
||||||
|
halfD: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Every period module takes the same three inputs. */
|
||||||
|
export interface PeriodProps {
|
||||||
|
room: RoomDims;
|
||||||
|
/** The movement's trim colour, already blended with its accent. */
|
||||||
|
trim: string;
|
||||||
|
/** Windows the hall placed at runtime, so upper-wall relief can avoid them. */
|
||||||
|
windows?: GalleryWindowSpec[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lighten (amount > 0) or darken (amount < 0) a hex colour. */
|
||||||
|
export function shade(hex: string, amount: number): string {
|
||||||
|
const n = parseInt(hex.replace('#', ''), 16);
|
||||||
|
const ch = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((c) =>
|
||||||
|
Math.max(0, Math.min(255, Math.round(amount >= 0 ? c + (255 - c) * amount : c * (1 + amount))))
|
||||||
|
);
|
||||||
|
return `#${((ch[0] << 16) | (ch[1] << 8) | ch[2]).toString(16).padStart(6, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bay rhythm along the depth axis — the spacing that drives piers and beams. */
|
||||||
|
export function useBayZ(depth: number, halfD: number, spacing = 4.2, max = 9): number[] {
|
||||||
|
return useMemo(() => {
|
||||||
|
const count = Math.max(2, Math.min(max, Math.floor(depth / spacing)));
|
||||||
|
const step = depth / (count + 1);
|
||||||
|
return Array.from({ length: count }, (_, i) => -halfD + step * (i + 1));
|
||||||
|
}, [depth, halfD, spacing, max]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evenly spaced positions centred on a wall, for the repeating small members —
|
||||||
|
* dentils, linenfold ribs, modillions, stencil motifs, quatrefoils.
|
||||||
|
*/
|
||||||
|
export function useRepeat(span: number, step: number, max: number): number[] {
|
||||||
|
return useMemo(() => {
|
||||||
|
const count = Math.min(max, Math.max(3, Math.floor((span - 0.6) / step)));
|
||||||
|
const run = (count - 1) * step;
|
||||||
|
return Array.from({ length: count }, (_, i) => -run / 2 + i * step);
|
||||||
|
}, [span, step, max]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Side-wall windows are placed at runtime in whatever gaps the hang leaves, so
|
||||||
|
* any panel, arch or roundel a period module paints across the upper wall has
|
||||||
|
* to step around them. Returns a predicate: true when the span is clear.
|
||||||
|
*/
|
||||||
|
export function useWallClearance(
|
||||||
|
windows: GalleryWindowSpec[] | undefined,
|
||||||
|
wall: 'left' | 'right' | 'back'
|
||||||
|
) {
|
||||||
|
return useMemo(() => {
|
||||||
|
const spans = (windows ?? [])
|
||||||
|
.filter((w) => w.wall === wall)
|
||||||
|
.map((w) => [w.x - w.width / 2 - 0.18, w.x + w.width / 2 + 0.18] as [number, number]);
|
||||||
|
return (pos: number, halfSpan = 0.2) =>
|
||||||
|
!spans.some(([a, b]) => pos + halfSpan > a && pos - halfSpan < b);
|
||||||
|
}, [windows, wall]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The four corner columns of the room, each inside its hang-margin pocket. */
|
||||||
|
export function useCorners(halfW: number, halfD: number, inset = 0.08): [number, number][] {
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
[-halfW + inset, -halfD + inset],
|
||||||
|
[halfW - inset, -halfD + inset],
|
||||||
|
[-halfW + inset, halfD - inset],
|
||||||
|
[halfW - inset, halfD - inset],
|
||||||
|
] as [number, number][],
|
||||||
|
[halfW, halfD, inset]
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,717 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,507 @@
|
|||||||
|
import {
|
||||||
|
CeilingBeams,
|
||||||
|
CeilingRose,
|
||||||
|
CorniceRing,
|
||||||
|
HangingFixture,
|
||||||
|
SideRails,
|
||||||
|
WallBand,
|
||||||
|
WallSconce,
|
||||||
|
} from './primitives';
|
||||||
|
import {
|
||||||
|
shade,
|
||||||
|
useBayZ,
|
||||||
|
useRepeat,
|
||||||
|
useWallClearance,
|
||||||
|
type PeriodProps,
|
||||||
|
} from './geometry';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Romantic gallery — Romanticism (Gothic Revival).
|
||||||
|
*
|
||||||
|
* Pugin's revival vocabulary rather than a real nave: dark walnut linenfold
|
||||||
|
* panelling, a cusped blind arcade of pointed arches, a quatrefoil frieze, and
|
||||||
|
* gasoliers. The pointed arch here is applied ornament, which is exactly what
|
||||||
|
* separates the revival from the Gothic hall it quotes.
|
||||||
|
*/
|
||||||
|
export function GothicRevivalDetails({ room, trim, windows }: PeriodProps) {
|
||||||
|
const bays = useBayZ(room.depth, room.halfD, 3.8, 8);
|
||||||
|
const clearLeft = useWallClearance(windows, 'left');
|
||||||
|
const clearRight = useWallClearance(windows, 'right');
|
||||||
|
const walnut = '#4c3323';
|
||||||
|
const walnutLight = shade(walnut, 0.22);
|
||||||
|
|
||||||
|
const quatrefoils = useRepeat(room.depth, 0.95, 24);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{/* Panelled dado with a heavy moulded rail */}
|
||||||
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
||||||
|
<group key={wall}>
|
||||||
|
<WallBand wall={wall} room={room} y={0.62} height={1.2} proud={0.06} color={walnut} roughness={0.68} metalness={0.08} />
|
||||||
|
<WallBand wall={wall} room={room} y={1.28} height={0.13} proud={0.11} color={walnutLight} roughness={0.52} metalness={0.12} />
|
||||||
|
<WallBand wall={wall} room={room} y={0.09} height={0.18} proud={0.1} color={shade(walnut, -0.35)} roughness={0.62} metalness={0.08} />
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Colonnettes between the bays, carrying an applied pointed arcade */}
|
||||||
|
{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.0, 0]}>
|
||||||
|
<cylinderGeometry args={[0.055, 0.065, 3.9, 8]} />
|
||||||
|
<meshStandardMaterial color={walnutLight} roughness={0.6} metalness={0.1} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, 3.98, 0]}>
|
||||||
|
<boxGeometry args={[0.13, 0.16, 0.22]} />
|
||||||
|
<meshStandardMaterial color={trim} roughness={0.5} metalness={0.2} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Cusped pointed arches over each bay, stepping around the windows */}
|
||||||
|
{bays.slice(0, -1).map((z, i) => {
|
||||||
|
const mid = (z + bays[i + 1]) / 2;
|
||||||
|
// Capped so the raking limbs stay under the ceiling in deep halls.
|
||||||
|
const half = Math.min(1.1, (bays[i + 1] - z) / 2 - 0.1);
|
||||||
|
return (
|
||||||
|
<group key={i}>
|
||||||
|
{([-1, 1] as const)
|
||||||
|
.filter((side) => (side < 0 ? clearLeft : clearRight)(mid, half))
|
||||||
|
.map((side) => (
|
||||||
|
<group key={side} position={[side * (room.halfW - 0.06), 0, mid]}>
|
||||||
|
{[-1, 1].map((dir) => (
|
||||||
|
<mesh
|
||||||
|
key={dir}
|
||||||
|
position={[0, 3.62, (dir * half) / 2]}
|
||||||
|
rotation={[dir * 0.42, 0, 0]}
|
||||||
|
>
|
||||||
|
<boxGeometry args={[0.09, half * 1.12, 0.09]} />
|
||||||
|
<meshStandardMaterial color={walnutLight} roughness={0.58} metalness={0.1} />
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
<mesh position={[0, 3.9, 0]}>
|
||||||
|
<sphereGeometry args={[0.07, 8, 8]} />
|
||||||
|
<meshStandardMaterial color={trim} roughness={0.44} metalness={0.28} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Quatrefoil frieze under the cornice */}
|
||||||
|
{(['left', 'right'] as const).map((wall) =>
|
||||||
|
quatrefoils.map((z, i) => (
|
||||||
|
<group
|
||||||
|
key={`${wall}-${i}`}
|
||||||
|
position={[(wall === 'left' ? -1 : 1) * (room.halfW - 0.1), 4.02, z]}
|
||||||
|
rotation={[0, Math.PI / 2, 0]}
|
||||||
|
>
|
||||||
|
{[0, 1, 2, 3].map((q) => {
|
||||||
|
const a = (q / 4) * Math.PI * 2 + Math.PI / 4;
|
||||||
|
return (
|
||||||
|
<mesh key={q} position={[Math.cos(a) * 0.07, Math.sin(a) * 0.07, 0]}>
|
||||||
|
<sphereGeometry args={[0.055, 8, 6]} />
|
||||||
|
<meshStandardMaterial color={shade(walnut, 0.34)} roughness={0.6} metalness={0.1} />
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CorniceRing room={room} y={4.14} height={0.1} proud={0.15} color={walnutLight} roughness={0.5} metalness={0.14} />
|
||||||
|
|
||||||
|
{[-1, 1].map((side) => (
|
||||||
|
<WallSconce
|
||||||
|
key={side}
|
||||||
|
wall={side < 0 ? 'left' : 'right'}
|
||||||
|
room={room}
|
||||||
|
along={side * room.halfD * 0.3}
|
||||||
|
y={2.9}
|
||||||
|
armColor={trim}
|
||||||
|
glowColor="#ff9c46"
|
||||||
|
shadeColor="#ffd8a0"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<HangingFixture
|
||||||
|
position={[0, 4.0, 0]}
|
||||||
|
dropLength={0.55}
|
||||||
|
radius={0.36}
|
||||||
|
arms={6}
|
||||||
|
metalColor={trim}
|
||||||
|
flameColor="#ffdca0"
|
||||||
|
glowColor="#ff9a3c"
|
||||||
|
light
|
||||||
|
lightColor="#ffb268"
|
||||||
|
lightIntensity={0.8}
|
||||||
|
lightDistance={9}
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Realist picture gallery — Realism.
|
||||||
|
*
|
||||||
|
* The mid-century bourgeois interior the Realists exhibited against: a plaster
|
||||||
|
* dado and picture rail, run cornice with a moulded frieze, a ceiling rose, and
|
||||||
|
* gas brackets. Restrained, machine-made mouldings — no carving anywhere.
|
||||||
|
*/
|
||||||
|
export function BourgeoisSalonDetails({ room, trim }: PeriodProps) {
|
||||||
|
const plaster = '#efe8dc';
|
||||||
|
const shadow = shade(plaster, -0.12);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{/* Dado, chair rail, skirting */}
|
||||||
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
||||||
|
<group key={wall}>
|
||||||
|
<WallBand wall={wall} room={room} y={0.5} height={0.96} proud={0.04} color={shadow} roughness={0.78} metalness={0.05} />
|
||||||
|
<WallBand wall={wall} room={room} y={1.03} height={0.09} proud={0.09} color={plaster} roughness={0.7} metalness={0.06} />
|
||||||
|
<WallBand wall={wall} room={room} y={0.09} height={0.18} proud={0.1} color={plaster} roughness={0.74} metalness={0.05} />
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Picture rail just above the hang, as the period actually hung its walls */}
|
||||||
|
<SideRails room={room} y={2.52} height={0.07} proud={0.09} color={trim} roughness={0.55} metalness={0.2} />
|
||||||
|
|
||||||
|
{/* Run cornice: cavetto, bead and a moulded frieze */}
|
||||||
|
<CorniceRing room={room} y={3.9} height={0.1} proud={0.07} color={shadow} roughness={0.76} metalness={0.05} />
|
||||||
|
<CorniceRing room={room} y={4.0} height={0.12} proud={0.13} color={plaster} roughness={0.72} metalness={0.05} />
|
||||||
|
<CorniceRing room={room} y={4.13} height={0.07} proud={0.17} color={shade(plaster, 0.06)} roughness={0.7} metalness={0.06} />
|
||||||
|
|
||||||
|
<CeilingRose y={4.09} radius={0.85} color={shade(plaster, 0.04)} rings={3} />
|
||||||
|
|
||||||
|
{[-1, 1].map((side) => (
|
||||||
|
<WallSconce
|
||||||
|
key={side}
|
||||||
|
wall={side < 0 ? 'left' : 'right'}
|
||||||
|
room={room}
|
||||||
|
along={side * room.halfD * 0.34}
|
||||||
|
y={2.78}
|
||||||
|
armColor={trim}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<HangingFixture
|
||||||
|
position={[0, 4.04, 0]}
|
||||||
|
dropLength={0.62}
|
||||||
|
radius={0.3}
|
||||||
|
arms={4}
|
||||||
|
metalColor={trim}
|
||||||
|
flameColor="#fff0cc"
|
||||||
|
glowColor="#ffbe6a"
|
||||||
|
light
|
||||||
|
lightColor="#ffd8a8"
|
||||||
|
lightIntensity={0.75}
|
||||||
|
lightDistance={9}
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Impressionist salon — Impressionism.
|
||||||
|
*
|
||||||
|
* The 1874 Boulevard des Capucines idea of a hung room: an iron-and-glass
|
||||||
|
* lantern overhead, a coved plaster cornice, and a velvet-faced picture rail
|
||||||
|
* with visible hanging rods above the frames. Nothing carved, everything pale.
|
||||||
|
*/
|
||||||
|
export function NorthLightSalonDetails({ room, trim }: PeriodProps) {
|
||||||
|
const iron = '#6d6a63';
|
||||||
|
const plaster = '#f4f1ea';
|
||||||
|
const trusses = useBayZ(room.depth, room.halfD, 2.4, 11);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{/* Skylight lantern: glazing bars across a shallow raised centre */}
|
||||||
|
<mesh position={[0, 4.11, 0]}>
|
||||||
|
<boxGeometry args={[Math.min(room.width - 2.2, 6.4), 0.06, Math.min(room.depth - 2.2, 14)]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
color="#ffffff"
|
||||||
|
emissive="#eaf2ff"
|
||||||
|
emissiveIntensity={0.55}
|
||||||
|
roughness={0.4}
|
||||||
|
metalness={0.05}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
<CeilingBeams
|
||||||
|
room={room}
|
||||||
|
positions={trusses}
|
||||||
|
axis="x"
|
||||||
|
width={0.07}
|
||||||
|
height={0.1}
|
||||||
|
y={4.05}
|
||||||
|
color={iron}
|
||||||
|
roughness={0.5}
|
||||||
|
metalness={0.55}
|
||||||
|
/>
|
||||||
|
{[-1, 1].map((side) => (
|
||||||
|
<mesh key={side} position={[(side * Math.min(room.width - 2.2, 6.4)) / 2, 4.03, 0]}>
|
||||||
|
<boxGeometry args={[0.1, 0.18, Math.min(room.depth - 2.2, 14)]} />
|
||||||
|
<meshStandardMaterial color={iron} roughness={0.48} metalness={0.6} />
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Picture rail with hanging rods dropping to the frame line */}
|
||||||
|
<SideRails room={room} y={2.6} height={0.09} proud={0.1} color={trim} roughness={0.52} metalness={0.24} />
|
||||||
|
<WallBand wall="back" room={room} y={2.6} height={0.09} proud={0.1} color={trim} roughness={0.52} metalness={0.24} />
|
||||||
|
|
||||||
|
{/* Coved cornice */}
|
||||||
|
<CorniceRing room={room} y={3.94} height={0.14} proud={0.06} color={shade(plaster, -0.06)} roughness={0.8} metalness={0.04} />
|
||||||
|
<CorniceRing room={room} y={4.07} height={0.1} proud={0.12} color={plaster} roughness={0.78} metalness={0.04} />
|
||||||
|
|
||||||
|
{/* Dark skirting so the pale walls sit on something */}
|
||||||
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
||||||
|
<WallBand key={wall} wall={wall} room={room} y={0.1} height={0.2} proud={0.09} color="#b6a88f" roughness={0.7} metalness={0.06} />
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Montmartre atelier — Post-Impressionism.
|
||||||
|
*
|
||||||
|
* A working painter's room rather than a gallery: a king-post roof truss under
|
||||||
|
* the slope, a cast-iron stove flue climbing the end wall, plain board skirting
|
||||||
|
* and a rough shelf rail carrying the studio's clutter line.
|
||||||
|
*/
|
||||||
|
export function ParisAtelierDetails({ room, trim }: PeriodProps) {
|
||||||
|
const trusses = useBayZ(room.depth, room.halfD, 3.1, 8);
|
||||||
|
const timber = '#8a6a48';
|
||||||
|
const iron = '#3a3632';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{/* Roof trusses: tie beam, king post and raking struts */}
|
||||||
|
{trusses.map((z, i) => {
|
||||||
|
// Struts run from the head of the king post down to the tie beam, and
|
||||||
|
// have to stay inside the 0.3 m the ceiling leaves above the beam.
|
||||||
|
const run = room.halfW - 0.6;
|
||||||
|
const rise = 0.16;
|
||||||
|
const strut = Math.hypot(run, rise);
|
||||||
|
return (
|
||||||
|
<group key={i} position={[0, 0, z]}>
|
||||||
|
<mesh position={[0, 3.9, 0]}>
|
||||||
|
<boxGeometry args={[room.width - 0.1, 0.16, 0.16]} />
|
||||||
|
<meshStandardMaterial color={timber} roughness={0.86} metalness={0.03} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, 4.02, 0]}>
|
||||||
|
<boxGeometry args={[0.13, 0.24, 0.13]} />
|
||||||
|
<meshStandardMaterial color={timber} roughness={0.86} metalness={0.03} />
|
||||||
|
</mesh>
|
||||||
|
{[-1, 1].map((side) => (
|
||||||
|
<mesh
|
||||||
|
key={side}
|
||||||
|
position={[(side * run) / 2, 4.06, 0]}
|
||||||
|
rotation={[0, 0, side * Math.atan2(-rise, run)]}
|
||||||
|
>
|
||||||
|
<boxGeometry args={[strut, 0.09, 0.11]} />
|
||||||
|
<meshStandardMaterial color={shade(timber, -0.16)} roughness={0.88} metalness={0.03} />
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Stove flue running up the end wall and out along the ceiling */}
|
||||||
|
<mesh position={[room.halfW - 0.28, 2.1, -room.halfD + 0.28]}>
|
||||||
|
<cylinderGeometry args={[0.09, 0.09, 4.0, 10]} />
|
||||||
|
<meshStandardMaterial color={iron} roughness={0.62} metalness={0.42} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[room.halfW - 0.28, 4.02, -room.halfD + 0.28]}>
|
||||||
|
<cylinderGeometry args={[0.12, 0.12, 0.14, 10]} />
|
||||||
|
<meshStandardMaterial color={shade(iron, 0.14)} roughness={0.58} metalness={0.5} />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
{/* Board skirting and a rough shelf rail above the hang */}
|
||||||
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
||||||
|
<group key={wall}>
|
||||||
|
<WallBand wall={wall} room={room} y={0.12} height={0.24} proud={0.06} color={shade(timber, -0.2)} roughness={0.88} metalness={0.03} />
|
||||||
|
<WallBand wall={wall} room={room} y={2.66} height={0.06} proud={0.13} color={timber} roughness={0.84} metalness={0.04} />
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* A single bare bulb on a flex, the way the ateliers were actually lit */}
|
||||||
|
<mesh position={[0, 3.7, 0]}>
|
||||||
|
<cylinderGeometry args={[0.008, 0.008, 0.5, 6]} />
|
||||||
|
<meshStandardMaterial color={iron} roughness={0.6} metalness={0.3} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, 3.42, 0]}>
|
||||||
|
<sphereGeometry args={[0.09, 12, 12]} />
|
||||||
|
<meshStandardMaterial color="#fff2d0" emissive="#ffc061" emissiveIntensity={0.95} toneMapped={false} />
|
||||||
|
</mesh>
|
||||||
|
<pointLight position={[0, 3.4, 0]} intensity={0.9} distance={9} color={trim} />
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Symbolist chamber — Symbolism.
|
||||||
|
*
|
||||||
|
* The aesthetic interior: a stencilled frieze of repeating motifs, a heavy
|
||||||
|
* portière rail, dark lacquered dado, and low amber sconces. Light is kept
|
||||||
|
* pooled and low, which is the whole point of the period's rooms.
|
||||||
|
*/
|
||||||
|
export function SymbolistDetails({ room, trim }: PeriodProps) {
|
||||||
|
const lacquer = '#12281f';
|
||||||
|
const stencil = shade(trim, 0.1);
|
||||||
|
|
||||||
|
const motifs = useRepeat(room.depth, 0.8, 26);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{/* Lacquered dado under a gilt bead */}
|
||||||
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
||||||
|
<group key={wall}>
|
||||||
|
<WallBand wall={wall} room={room} y={0.54} height={1.04} proud={0.05} color={lacquer} roughness={0.42} metalness={0.16} />
|
||||||
|
<WallBand wall={wall} room={room} y={1.1} height={0.06} proud={0.09} color={trim} roughness={0.38} metalness={0.44} />
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Stencilled frieze — a repeating lotus/flame motif under the cornice */}
|
||||||
|
{(['left', 'right'] as const).map((wall) => (
|
||||||
|
<group key={wall}>
|
||||||
|
<WallBand wall={wall} room={room} y={3.86} height={0.34} proud={0.04} color={shade(lacquer, 0.1)} roughness={0.6} metalness={0.12} />
|
||||||
|
{motifs.map((z, i) => (
|
||||||
|
<group key={i} position={[(wall === 'left' ? -1 : 1) * (room.halfW - 0.11), 3.86, z]} rotation={[0, Math.PI / 2, 0]}>
|
||||||
|
<mesh>
|
||||||
|
<coneGeometry args={[0.09, 0.24, 5]} />
|
||||||
|
<meshStandardMaterial color={stencil} roughness={0.44} metalness={0.36} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, -0.16, 0]}>
|
||||||
|
<sphereGeometry args={[0.045, 8, 6]} />
|
||||||
|
<meshStandardMaterial color={stencil} roughness={0.44} metalness={0.36} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<CorniceRing room={room} y={4.08} height={0.12} proud={0.13} color={shade(lacquer, 0.16)} roughness={0.5} metalness={0.18} />
|
||||||
|
<CorniceRing room={room} y={4.16} height={0.05} proud={0.16} color={trim} roughness={0.36} metalness={0.5} />
|
||||||
|
|
||||||
|
{/* Portière rail across the end wall */}
|
||||||
|
<WallBand wall="back" room={room} y={2.72} height={0.08} proud={0.12} color={trim} roughness={0.4} metalness={0.44} />
|
||||||
|
|
||||||
|
{[-0.36, 0.36].map((t) => (
|
||||||
|
<WallSconce
|
||||||
|
key={t}
|
||||||
|
wall={t < 0 ? 'left' : 'right'}
|
||||||
|
room={room}
|
||||||
|
along={t * room.halfD}
|
||||||
|
y={2.94}
|
||||||
|
armColor={trim}
|
||||||
|
glowColor="#ff8a2e"
|
||||||
|
shadeColor="#ffcf94"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<HangingFixture
|
||||||
|
position={[0, 4.04, 0]}
|
||||||
|
dropLength={0.66}
|
||||||
|
radius={0.26}
|
||||||
|
arms={5}
|
||||||
|
metalColor={trim}
|
||||||
|
flameColor="#ffd9a0"
|
||||||
|
glowColor="#ff8f2c"
|
||||||
|
light
|
||||||
|
lightColor="#ffa050"
|
||||||
|
lightIntensity={0.7}
|
||||||
|
lightDistance={8}
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Art Nouveau salon — Art Nouveau.
|
||||||
|
*
|
||||||
|
* Horta's structural line: whiplash brackets springing from the wall into the
|
||||||
|
* ceiling, tendrils running the cornice, and organic sconces. The curve is
|
||||||
|
* carried by the structure itself rather than applied as ornament.
|
||||||
|
*/
|
||||||
|
export function ArtNouveauDetails({ room, trim }: PeriodProps) {
|
||||||
|
const bays = useBayZ(room.depth, room.halfD, 3.0, 10);
|
||||||
|
const iron = shade(trim, -0.24);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{/* Whiplash brackets: a stem up the wall curling into the ceiling */}
|
||||||
|
{bays.map((z, i) => (
|
||||||
|
<group key={i}>
|
||||||
|
{[-1, 1].map((side) => (
|
||||||
|
<group key={side} position={[side * (room.halfW - 0.07), 0, z]}>
|
||||||
|
<mesh position={[0, 2.05, 0]}>
|
||||||
|
<cylinderGeometry args={[0.045, 0.06, 4.0, 8]} />
|
||||||
|
<meshStandardMaterial color={iron} roughness={0.44} metalness={0.42} />
|
||||||
|
</mesh>
|
||||||
|
{[0, 1, 2, 3].map((s) => {
|
||||||
|
const t = (s + 1) / 4;
|
||||||
|
return (
|
||||||
|
<mesh
|
||||||
|
key={s}
|
||||||
|
position={[-side * (0.1 + t * 0.32), 4.02 - t * t * 0.22, 0]}
|
||||||
|
rotation={[0, 0, side * (0.35 + t * 0.7)]}
|
||||||
|
>
|
||||||
|
<cylinderGeometry args={[0.035, 0.045, 0.34, 6]} />
|
||||||
|
<meshStandardMaterial color={iron} roughness={0.44} metalness={0.42} />
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{/* Bud terminal */}
|
||||||
|
<mesh position={[-side * 0.52, 3.78, 0]}>
|
||||||
|
<sphereGeometry args={[0.07, 10, 8]} />
|
||||||
|
<meshStandardMaterial color={trim} roughness={0.36} metalness={0.5} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Tendril cornice — a wave riding the top of every wall */}
|
||||||
|
<CorniceRing room={room} y={4.05} height={0.11} proud={0.11} color={shade(trim, 0.18)} roughness={0.5} metalness={0.3} />
|
||||||
|
<CorniceRing room={room} y={4.15} height={0.06} proud={0.15} color={trim} roughness={0.42} metalness={0.42} />
|
||||||
|
|
||||||
|
{/* Sinuous dado line */}
|
||||||
|
{(['left', 'right', 'back'] as const).map((wall) => (
|
||||||
|
<group key={wall}>
|
||||||
|
<WallBand wall={wall} room={room} y={0.11} height={0.22} proud={0.08} color={shade(trim, -0.3)} roughness={0.52} metalness={0.28} />
|
||||||
|
<WallBand wall={wall} room={room} y={0.86} height={0.05} proud={0.07} color={trim} roughness={0.44} metalness={0.4} />
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{[-0.3, 0.3].map((t) => (
|
||||||
|
<WallSconce
|
||||||
|
key={t}
|
||||||
|
wall={t < 0 ? 'left' : 'right'}
|
||||||
|
room={room}
|
||||||
|
along={t * room.halfD}
|
||||||
|
y={2.88}
|
||||||
|
armColor={trim}
|
||||||
|
glowColor="#8fd66a"
|
||||||
|
shadeColor="#e8ffd8"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<HangingFixture
|
||||||
|
position={[0, 4.0, 0]}
|
||||||
|
dropLength={0.5}
|
||||||
|
radius={0.44}
|
||||||
|
arms={7}
|
||||||
|
metalColor={trim}
|
||||||
|
flameColor="#f2ffe0"
|
||||||
|
glowColor="#a8d86a"
|
||||||
|
light
|
||||||
|
lightColor="#dff2c0"
|
||||||
|
lightIntensity={0.75}
|
||||||
|
lightDistance={9}
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,513 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { CEILING_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 len = (alongZ ? room.depth : room.width) * span;
|
||||||
|
const thick = proud + EMBED * 2;
|
||||||
|
const off = WALL_INNER + proud / 2 - EMBED;
|
||||||
|
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 (
|
||||||
|
<mesh position={pos}>
|
||||||
|
<boxGeometry args={args} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={color}
|
||||||
|
roughness={roughness}
|
||||||
|
metalness={metalness}
|
||||||
|
emissive={emissive}
|
||||||
|
emissiveIntensity={emissiveIntensity}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 (
|
||||||
|
<group>
|
||||||
|
{walls.map((wall) => (
|
||||||
|
<WallBand key={wall} wall={wall} room={room} y={y} height={height} proud={proud} {...surface} />
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 (
|
||||||
|
<group>
|
||||||
|
{(['left', 'right'] as const).map((wall) => (
|
||||||
|
<WallBand key={wall} wall={wall} room={room} y={y} height={height} proud={proud} {...surface} />
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<group>
|
||||||
|
<mesh position={[0, height / 2, 0]}>
|
||||||
|
<cylinderGeometry args={[radius * taper, radius, height, 16]} />
|
||||||
|
<meshStandardMaterial color={color} roughness={0.74} metalness={0.05} />
|
||||||
|
</mesh>
|
||||||
|
{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 (
|
||||||
|
<mesh key={i} position={[dx, height / 2, dz]}>
|
||||||
|
<cylinderGeometry args={[radius * 0.16, radius * 0.16, height * 0.98, 6]} />
|
||||||
|
<meshStandardMaterial color={groove} roughness={0.88} metalness={0.03} />
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TuscanCapital({ y, size, color }: { y: number; size: number; color: string }) {
|
||||||
|
return (
|
||||||
|
<group position={[0, y, 0]}>
|
||||||
|
<mesh>
|
||||||
|
<cylinderGeometry args={[size * 1.15, size * 0.85, size * 0.42, 14]} />
|
||||||
|
<meshStandardMaterial color={color} roughness={0.7} metalness={0.05} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, size * 0.36, 0]}>
|
||||||
|
<boxGeometry args={[size * 2.5, size * 0.28, size * 2.5]} />
|
||||||
|
<meshStandardMaterial color={shade(color, 0.08)} roughness={0.68} metalness={0.05} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 (
|
||||||
|
<group position={[0, y, 0]}>
|
||||||
|
<mesh position={[0, size * 0.1, 0]}>
|
||||||
|
<boxGeometry args={[size * 2.4, size * 0.34, size * 1.5]} />
|
||||||
|
<meshStandardMaterial color={color} roughness={0.66} metalness={0.06} />
|
||||||
|
</mesh>
|
||||||
|
{[-1, 1].map((s) => (
|
||||||
|
<mesh
|
||||||
|
key={s}
|
||||||
|
position={axis === 'z' ? [0, -size * 0.12, s * size * 0.85] : [s * size * 0.85, -size * 0.12, 0]}
|
||||||
|
rotation={axis === 'z' ? [0, Math.PI / 2, 0] : [0, 0, 0]}
|
||||||
|
>
|
||||||
|
<torusGeometry args={[size * 0.38, size * 0.14, 8, 16]} />
|
||||||
|
<meshStandardMaterial color={shade(color, 0.06)} roughness={0.62} metalness={0.07} />
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Moulded plinth under a shaft. */
|
||||||
|
export function ShaftBase({ size, color }: { size: number; color: string }) {
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
<mesh position={[0, 0.08, 0]}>
|
||||||
|
<boxGeometry args={[size * 2.4, 0.16, size * 2.4]} />
|
||||||
|
<meshStandardMaterial color={color} roughness={0.78} metalness={0.04} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, 0.21, 0]}>
|
||||||
|
<cylinderGeometry args={[size * 1.0, size * 1.15, 0.1, 14]} />
|
||||||
|
<meshStandardMaterial color={shade(color, 0.06)} roughness={0.72} metalness={0.05} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 (
|
||||||
|
<group>
|
||||||
|
{blocks.map((b, i) => (
|
||||||
|
<mesh key={i} position={[b.x, b.y, 0]} rotation={[0, 0, b.rot]}>
|
||||||
|
<boxGeometry args={[b.len, thickness, depth]} />
|
||||||
|
<meshStandardMaterial color={color} roughness={roughness} metalness={metalness} />
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 (
|
||||||
|
<group>
|
||||||
|
{grid.cells.map((c, i) => (
|
||||||
|
<group key={i} position={[c.x, y, c.z]}>
|
||||||
|
<mesh position={[0, 0.01, 0]}>
|
||||||
|
<boxGeometry args={[grid.cw - ribWidth, 0.03, grid.cd - ribWidth]} />
|
||||||
|
<meshStandardMaterial color={panelColor} roughness={0.86} metalness={0.04} />
|
||||||
|
</mesh>
|
||||||
|
{rosette && (
|
||||||
|
<mesh position={[0, -ribDrop * 0.35, 0]} rotation={[Math.PI / 2, 0, 0]}>
|
||||||
|
<ringGeometry args={[grid.cw * 0.06, grid.cw * 0.15, 12]} />
|
||||||
|
<meshStandardMaterial color={rosette} roughness={0.4} metalness={0.5} side={2} />
|
||||||
|
</mesh>
|
||||||
|
)}
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
{/* Ribs between the coffers */}
|
||||||
|
{Array.from({ length: grid.cols + 1 }, (_, c) => (
|
||||||
|
<mesh key={`c${c}`} position={[-room.halfW + grid.cw * c, y - ribDrop / 2, 0]}>
|
||||||
|
<boxGeometry args={[ribWidth, ribDrop, room.depth]} />
|
||||||
|
<meshStandardMaterial color={ribColor} roughness={0.7} metalness={0.08} />
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
{Array.from({ length: grid.rows + 1 }, (_, r) => (
|
||||||
|
<mesh key={`r${r}`} position={[0, y - ribDrop / 2, -room.halfD + grid.cd * r]}>
|
||||||
|
<boxGeometry args={[room.width, ribDrop, ribWidth]} />
|
||||||
|
<meshStandardMaterial color={ribColor} roughness={0.7} metalness={0.08} />
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 (
|
||||||
|
<group>
|
||||||
|
{positions.map((p, i) => (
|
||||||
|
<mesh key={i} position={axis === 'x' ? [0, y, p] : [p, y, 0]}>
|
||||||
|
<boxGeometry
|
||||||
|
args={axis === 'x' ? [room.width, height, width] : [width, height, room.depth]}
|
||||||
|
/>
|
||||||
|
<meshStandardMaterial color={color} roughness={roughness} metalness={metalness} />
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 (
|
||||||
|
<group position={[0, y, 0]} rotation={[Math.PI / 2, 0, 0]}>
|
||||||
|
{Array.from({ length: rings }, (_, i) => {
|
||||||
|
const t = (i + 1) / rings;
|
||||||
|
return (
|
||||||
|
<mesh key={i} position={[0, 0, i * 0.012]}>
|
||||||
|
<ringGeometry args={[radius * t * 0.55, radius * t, 32]} />
|
||||||
|
<meshStandardMaterial color={shade(color, i * 0.06)} roughness={0.7} metalness={0.12} side={2} />
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<group position={[position[0], 0, position[2]]}>
|
||||||
|
<mesh position={[0, position[1] - dropLength / 2, 0]}>
|
||||||
|
<cylinderGeometry args={[0.012, 0.012, dropLength, 6]} />
|
||||||
|
<meshStandardMaterial color={shade(metalColor, -0.4)} roughness={0.42} metalness={0.7} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, ringY, 0]} rotation={[Math.PI / 2, 0, 0]}>
|
||||||
|
<torusGeometry args={[radius, 0.028, 8, 22]} />
|
||||||
|
<meshStandardMaterial color={metalColor} roughness={0.26} metalness={0.82} />
|
||||||
|
</mesh>
|
||||||
|
{Array.from({ length: arms }, (_, i) => {
|
||||||
|
const a = (i / arms) * Math.PI * 2;
|
||||||
|
return (
|
||||||
|
<mesh key={i} position={[Math.cos(a) * radius, ringY - 0.09, Math.sin(a) * radius]}>
|
||||||
|
<sphereGeometry args={[0.05, 8, 8]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={flameColor}
|
||||||
|
emissive={glowColor}
|
||||||
|
emissiveIntensity={0.9}
|
||||||
|
toneMapped={false}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{light && (
|
||||||
|
<pointLight
|
||||||
|
position={[0, ringY - 0.09, 0]}
|
||||||
|
intensity={lightIntensity}
|
||||||
|
distance={lightDistance}
|
||||||
|
color={lightColor}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<group position={pos} rotation={rot}>
|
||||||
|
<mesh position={[0, 0, 0.03]}>
|
||||||
|
<boxGeometry args={[0.16, 0.22, 0.05]} />
|
||||||
|
<meshStandardMaterial color={armColor} roughness={0.34} metalness={0.68} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, 0.06, 0.09]} rotation={[Math.PI / 2.6, 0, 0]}>
|
||||||
|
<cylinderGeometry args={[0.012, 0.012, 0.16, 6]} />
|
||||||
|
<meshStandardMaterial color={armColor} roughness={0.34} metalness={0.68} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, 0.15, 0.13]}>
|
||||||
|
<sphereGeometry args={[0.075, 10, 10]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={shadeColor}
|
||||||
|
emissive={glowColor}
|
||||||
|
emissiveIntensity={0.75}
|
||||||
|
toneMapped={false}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -54,9 +54,42 @@ export interface MovementInteriorStyle {
|
|||||||
trackLights: number;
|
trackLights: number;
|
||||||
/** Scales the shared hall/scene lights. <1 for deliberately dim period interiors. */
|
/** Scales the shared hall/scene lights. <1 for deliberately dim period interiors. */
|
||||||
lightScale: number;
|
lightScale: number;
|
||||||
details: 'palazzo' | 'baroque' | 'gothic' | 'byzantine' | 'neoclassical' | 'salon' | 'modern' | 'classical' | 'museum' | 'industrial' | 'atelier';
|
/**
|
||||||
|
* Which period architecture the hall is built from. Every movement has its
|
||||||
|
* own — the interiors are not shared between movements — and each kind maps
|
||||||
|
* to one component under `components/hall-details/`.
|
||||||
|
*/
|
||||||
|
details: MovementDetailKind;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MovementDetailKind =
|
||||||
|
| 'roman'
|
||||||
|
| 'byzantine'
|
||||||
|
| 'gothic'
|
||||||
|
| 'quattrocento'
|
||||||
|
| 'cinquecento'
|
||||||
|
| 'flemish'
|
||||||
|
| 'mannerist'
|
||||||
|
| 'baroque'
|
||||||
|
| 'rococo'
|
||||||
|
| 'neoclassical'
|
||||||
|
| 'gothic-revival'
|
||||||
|
| 'bourgeois'
|
||||||
|
| 'north-light'
|
||||||
|
| 'paris-atelier'
|
||||||
|
| 'symbolist'
|
||||||
|
| 'art-nouveau'
|
||||||
|
| 'fauvist'
|
||||||
|
| 'expressionist'
|
||||||
|
| 'cubist-atelier'
|
||||||
|
| 'futurist'
|
||||||
|
| 'suprematist'
|
||||||
|
| 'constructivist'
|
||||||
|
| 'dada'
|
||||||
|
| 'surrealist'
|
||||||
|
| 'loft'
|
||||||
|
| 'white-cube';
|
||||||
|
|
||||||
function windows(...specs: GalleryWindowSpec[]): GalleryWindowSpec[] {
|
function windows(...specs: GalleryWindowSpec[]): GalleryWindowSpec[] {
|
||||||
return specs;
|
return specs;
|
||||||
}
|
}
|
||||||
@@ -99,9 +132,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'Roman atrium',
|
'Roman atrium',
|
||||||
'Marble-clad villa · mosaic floor · clerestory daylight',
|
'Marble-clad villa · mosaic floor · clerestory daylight',
|
||||||
{ wall: 'limestone', ceiling: 'plaster-warm', floor: 'mosaic-roman' },
|
{ wall: 'limestone', ceiling: 'plaster-warm', floor: 'mosaic-roman' },
|
||||||
{ wall: '#e8e0d0', ceiling: '#f5f0e8', floor: '#c8b898', trim: '#a89878' },
|
{ wall: '#8e8980', ceiling: '#9c9994', floor: '#c8b898', trim: '#a89878' },
|
||||||
{
|
{
|
||||||
details: 'classical',
|
details: 'roman',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
titleColor: '#5a4830',
|
titleColor: '#5a4830',
|
||||||
ambient: 0.62,
|
ambient: 0.62,
|
||||||
warmLight: '#fff8e8',
|
warmLight: '#fff8e8',
|
||||||
@@ -175,9 +211,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'Florentine palazzo',
|
'Florentine palazzo',
|
||||||
'Lime-washed walls · terracotta accents · arched windows',
|
'Lime-washed walls · terracotta accents · arched windows',
|
||||||
{ wall: 'painted-lime', wallSide: 'stucco-cream', ceiling: 'fresco-worn', floor: 'terracotta-tiles' },
|
{ wall: 'painted-lime', wallSide: 'stucco-cream', ceiling: 'fresco-worn', floor: 'terracotta-tiles' },
|
||||||
{ wall: '#f4ebe0', ceiling: '#faf6ee', floor: '#c89070', trim: '#c9a227' },
|
{ wall: '#89847e', ceiling: '#96948f', floor: '#c89070', trim: '#c9a227' },
|
||||||
{
|
{
|
||||||
details: 'palazzo',
|
details: 'quattrocento',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
titleColor: '#6b4423',
|
titleColor: '#6b4423',
|
||||||
windows: windows(
|
windows: windows(
|
||||||
{ wall: 'back', x: -2.2, y: 2.6, width: 1.3, height: 1.7, style: 'roman-arch', lightColor: '#fff4e0', lightIntensity: 3.0 },
|
{ wall: 'back', x: -2.2, y: 2.6, width: 1.3, height: 1.7, style: 'roman-arch', lightColor: '#fff4e0', lightIntensity: 3.0 },
|
||||||
@@ -191,9 +230,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'Roman palazzo',
|
'Roman palazzo',
|
||||||
'Marble and stucco · coffered ceiling · checker floor',
|
'Marble and stucco · coffered ceiling · checker floor',
|
||||||
{ wall: 'marble-veined-carrara', wallSide: 'stucco-cream', ceiling: 'plaster-warm', floor: 'marble-checker' },
|
{ wall: 'marble-veined-carrara', wallSide: 'stucco-cream', ceiling: 'plaster-warm', floor: 'marble-checker' },
|
||||||
{ wall: '#f0ece4', wallSide: '#efe4d4', ceiling: '#faf6ee', floor: '#ddd8cc', trim: '#c9a227' },
|
{ wall: '#918f8a', wallSide: '#8f8980', ceiling: '#9c9994', floor: '#ddd8cc', trim: '#c9a227' },
|
||||||
{
|
{
|
||||||
details: 'palazzo',
|
details: 'cinquecento',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
titleColor: '#6b4423',
|
titleColor: '#6b4423',
|
||||||
windows: windows(
|
windows: windows(
|
||||||
{ wall: 'back', x: 0, y: 2.8, width: 2.8, height: 1.8, style: 'baroque-pair', lightColor: '#fff8f0', lightIntensity: 3.8 },
|
{ wall: 'back', x: 0, y: 2.8, width: 2.8, height: 1.8, style: 'baroque-pair', lightColor: '#fff8f0', lightIntensity: 3.8 },
|
||||||
@@ -208,9 +250,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'Flemish panel hall',
|
'Flemish panel hall',
|
||||||
'Oak wainscoting · leaded glass · herringbone floor',
|
'Oak wainscoting · leaded glass · herringbone floor',
|
||||||
{ wall: 'oak-panel', wallSide: 'plaster-warm', ceiling: 'dark-wood-panel', floor: 'parquet-herringbone' },
|
{ wall: 'oak-panel', wallSide: 'plaster-warm', ceiling: 'dark-wood-panel', floor: 'parquet-herringbone' },
|
||||||
{ wall: '#8a6848', wallSide: '#f0ebe3', ceiling: '#3a2818', floor: '#8a6848', trim: '#5c4030' },
|
{ wall: '#8a6848', wallSide: '#8c8985', ceiling: '#3a2818', floor: '#8a6848', trim: '#5c4030' },
|
||||||
{
|
{
|
||||||
details: 'salon',
|
details: 'flemish',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
titleColor: '#f0e8d8',
|
titleColor: '#f0e8d8',
|
||||||
warmLight: '#ffe8c8',
|
warmLight: '#ffe8c8',
|
||||||
windows: windows(
|
windows: windows(
|
||||||
@@ -226,9 +271,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'Mannerist gallery',
|
'Mannerist gallery',
|
||||||
'Dramatic stucco · elongated windows · veined marble',
|
'Dramatic stucco · elongated windows · veined marble',
|
||||||
{ wall: 'stucco-terracotta', ceiling: 'gilded-stucco', floor: 'marble-veined-emerald' },
|
{ wall: 'stucco-terracotta', ceiling: 'gilded-stucco', floor: 'marble-veined-emerald' },
|
||||||
{ wall: '#d8b898', ceiling: '#d8c070', floor: '#dce8e0', trim: '#b8860b' },
|
{ wall: '#98816b', ceiling: '#a79456', floor: '#dce8e0', trim: '#b8860b' },
|
||||||
{
|
{
|
||||||
details: 'baroque',
|
details: 'mannerist',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
titleColor: '#4a2818',
|
titleColor: '#4a2818',
|
||||||
windows: windows(
|
windows: windows(
|
||||||
{ wall: 'back', x: -1.8, y: 2.7, width: 1.0, height: 2.2, style: 'roman-arch', lightColor: '#fff0d8', lightIntensity: 2.8 },
|
{ wall: 'back', x: -1.8, y: 2.7, width: 1.0, height: 2.2, style: 'roman-arch', lightColor: '#fff0d8', lightIntensity: 2.8 },
|
||||||
@@ -264,9 +312,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'Rococo salon',
|
'Rococo salon',
|
||||||
'Pastel painted walls · gilt trim · chevron parquet',
|
'Pastel painted walls · gilt trim · chevron parquet',
|
||||||
{ wall: 'painted-oil-satin', ceiling: 'silk-pale', floor: 'parquet-chevrons' },
|
{ wall: 'painted-oil-satin', ceiling: 'silk-pale', floor: 'parquet-chevrons' },
|
||||||
{ wall: '#e8dce8', ceiling: '#faf6f0', floor: '#c8a882', trim: '#d4af37' },
|
{ wall: '#918a91', ceiling: '#9b9995', floor: '#c8a882', trim: '#d4af37' },
|
||||||
{
|
{
|
||||||
details: 'salon',
|
details: 'rococo',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
titleColor: '#6a4858',
|
titleColor: '#6a4858',
|
||||||
warmLight: '#fff0f0',
|
warmLight: '#fff0f0',
|
||||||
windows: windows(
|
windows: windows(
|
||||||
@@ -281,9 +332,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'Neoclassical museum',
|
'Neoclassical museum',
|
||||||
'White painted walls · coffered ceiling · skylit salon',
|
'White painted walls · coffered ceiling · skylit salon',
|
||||||
{ wall: 'painted-flat', ceiling: 'plaster-white', floor: 'marble-veined-carrara' },
|
{ wall: 'painted-flat', ceiling: 'plaster-white', floor: 'marble-veined-carrara' },
|
||||||
{ wall: '#f2f0ec', ceiling: '#fafafa', floor: '#eceae6', trim: '#b8b0a4' },
|
{ wall: '#908f8d', ceiling: '#9e9e9e', floor: '#eceae6', trim: '#b8b0a4' },
|
||||||
{
|
{
|
||||||
details: 'neoclassical',
|
details: 'neoclassical',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
titleColor: '#4a4844',
|
titleColor: '#4a4844',
|
||||||
ambient: 0.65,
|
ambient: 0.65,
|
||||||
windows: windows(
|
windows: windows(
|
||||||
@@ -301,7 +355,7 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
{ wall: 'walnut-panel', ceiling: 'dark-plaster', floor: 'parquet-herringbone' },
|
{ wall: 'walnut-panel', ceiling: 'dark-plaster', floor: 'parquet-herringbone' },
|
||||||
{ wall: '#5a4030', ceiling: '#2a2420', floor: '#6a5040', trim: '#8a7050' },
|
{ wall: '#5a4030', ceiling: '#2a2420', floor: '#6a5040', trim: '#8a7050' },
|
||||||
{
|
{
|
||||||
details: 'salon',
|
details: 'gothic-revival',
|
||||||
titleColor: '#e8dcc8',
|
titleColor: '#e8dcc8',
|
||||||
ambient: 0.5,
|
ambient: 0.5,
|
||||||
warmLight: '#ffe0c0',
|
warmLight: '#ffe0c0',
|
||||||
@@ -317,9 +371,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'Realist picture gallery',
|
'Realist picture gallery',
|
||||||
'Warm painted walls · bourgeois salon · oak parquet',
|
'Warm painted walls · bourgeois salon · oak parquet',
|
||||||
{ wall: 'painted-emulsion', ceiling: 'painted-emulsion', floor: 'parquet-herringbone' },
|
{ wall: 'painted-emulsion', ceiling: 'painted-emulsion', floor: 'parquet-herringbone' },
|
||||||
{ wall: '#e8e2d8', ceiling: '#f5f0e8', floor: '#a08060', trim: '#8a7050' },
|
{ wall: '#88847e', ceiling: '#97948f', floor: '#a08060', trim: '#8a7050' },
|
||||||
{
|
{
|
||||||
details: 'salon',
|
details: 'bourgeois',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
windows: windows(
|
windows: windows(
|
||||||
{ wall: 'back', x: 0, y: 2.5, width: 2.4, height: 1.6, style: 'sash', lightColor: '#f0f4ff', lightIntensity: 3.2 },
|
{ wall: 'back', x: 0, y: 2.5, width: 2.4, height: 1.6, style: 'sash', lightColor: '#f0f4ff', lightIntensity: 3.2 },
|
||||||
{ wall: 'left', x: 0, y: 2.7, width: 1.6, height: 1.3, style: 'sash', lightColor: '#f0f4ff', lightIntensity: 2.4 }
|
{ wall: 'left', x: 0, y: 2.7, width: 1.6, height: 1.3, style: 'sash', lightColor: '#f0f4ff', lightIntensity: 2.4 }
|
||||||
@@ -331,9 +388,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'Impressionist salon',
|
'Impressionist salon',
|
||||||
'North-light skylight · pale painted walls · herringbone floor',
|
'North-light skylight · pale painted walls · herringbone floor',
|
||||||
{ wall: 'painted-emulsion', ceiling: 'painted-flat', floor: 'parquet-herringbone' },
|
{ wall: 'painted-emulsion', ceiling: 'painted-flat', floor: 'parquet-herringbone' },
|
||||||
{ wall: '#f0ebe3', ceiling: '#fafafa', floor: '#c8a882', trim: '#c9a96e' },
|
{ wall: '#8f8c87', ceiling: '#9e9e9e', floor: '#c8a882', trim: '#c9a96e' },
|
||||||
{
|
{
|
||||||
details: 'salon',
|
details: 'north-light',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
ambient: 0.68,
|
ambient: 0.68,
|
||||||
windows: windows(
|
windows: windows(
|
||||||
{ wall: 'ceiling', x: 0, y: 0, width: 5.0, height: 3.0, style: 'skylight', lightColor: '#ffffff', lightIntensity: 6.0 },
|
{ wall: 'ceiling', x: 0, y: 0, width: 5.0, height: 3.0, style: 'skylight', lightColor: '#ffffff', lightIntensity: 6.0 },
|
||||||
@@ -348,9 +408,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'Montmartre atelier',
|
'Montmartre atelier',
|
||||||
'Painted studio walls · large north window · worn floorboards',
|
'Painted studio walls · large north window · worn floorboards',
|
||||||
{ wall: 'painted-emulsion', ceiling: 'painted-emulsion', floor: 'parquet-herringbone' },
|
{ wall: 'painted-emulsion', ceiling: 'painted-emulsion', floor: 'parquet-herringbone' },
|
||||||
{ wall: '#e8e0d0', ceiling: '#f0ebe0', floor: '#9a7858', trim: '#8a6848' },
|
{ wall: '#847f76', ceiling: '#918f88', floor: '#9a7858', trim: '#8a6848' },
|
||||||
{
|
{
|
||||||
details: 'atelier',
|
details: 'paris-atelier',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
windows: windows(
|
windows: windows(
|
||||||
{ wall: 'left', x: 0, y: 2.4, width: 2.8, height: 2.0, style: 'factory', lightColor: '#f0f8ff', lightIntensity: 4.5 },
|
{ wall: 'left', x: 0, y: 2.4, width: 2.8, height: 2.0, style: 'factory', lightColor: '#f0f8ff', lightIntensity: 4.5 },
|
||||||
{ wall: 'back', x: 0, y: 2.8, width: 1.0, height: 1.2, style: 'sash', lightColor: '#f0f4ff', lightIntensity: 2.0 }
|
{ wall: 'back', x: 0, y: 2.8, width: 1.0, height: 1.2, style: 'sash', lightColor: '#f0f4ff', lightIntensity: 2.0 }
|
||||||
@@ -364,7 +427,7 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
{ wall: 'painted-oil-matte', ceiling: 'dark-plaster', floor: 'parquet-herringbone' },
|
{ wall: 'painted-oil-matte', ceiling: 'dark-plaster', floor: 'parquet-herringbone' },
|
||||||
{ wall: '#1a4030', ceiling: '#1a1814', floor: '#4a3828', trim: '#8a7050' },
|
{ wall: '#1a4030', ceiling: '#1a1814', floor: '#4a3828', trim: '#8a7050' },
|
||||||
{
|
{
|
||||||
details: 'salon',
|
details: 'symbolist',
|
||||||
titleColor: '#d8e8c8',
|
titleColor: '#d8e8c8',
|
||||||
ambient: 0.55,
|
ambient: 0.55,
|
||||||
warmLight: '#ffd898',
|
warmLight: '#ffd898',
|
||||||
@@ -380,9 +443,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'Art Nouveau salon',
|
'Art Nouveau salon',
|
||||||
'Painted plaster · stained glass · terrazzo floor',
|
'Painted plaster · stained glass · terrazzo floor',
|
||||||
{ wall: 'painted-oil-satin', ceiling: 'silk-gold', floor: 'terrazzo' },
|
{ wall: 'painted-oil-satin', ceiling: 'silk-gold', floor: 'terrazzo' },
|
||||||
{ wall: '#f0ece4', ceiling: '#d8c890', floor: '#d8d0c4', trim: '#6a9868' },
|
{ wall: '#8f8c87', ceiling: '#a0946b', floor: '#d8d0c4', trim: '#6a9868' },
|
||||||
{
|
{
|
||||||
details: 'salon',
|
details: 'art-nouveau',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
titleColor: '#3a5840',
|
titleColor: '#3a5840',
|
||||||
windows: windows(
|
windows: windows(
|
||||||
{ wall: 'back', x: 0, y: 2.5, width: 2.2, height: 2.0, style: 'art-nouveau', lightColor: '#e8ffe8', lightIntensity: 3.2 },
|
{ wall: 'back', x: 0, y: 2.5, width: 2.2, height: 2.0, style: 'art-nouveau', lightColor: '#e8ffe8', lightIntensity: 3.2 },
|
||||||
@@ -396,9 +462,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'Fauvist studio',
|
'Fauvist studio',
|
||||||
'Bold painted walls · flooded with color and light',
|
'Bold painted walls · flooded with color and light',
|
||||||
{ wall: 'painted-oil-matte', wallSide: 'painted-oil-matte', ceiling: 'painted-emulsion', floor: 'parquet-herringbone' },
|
{ wall: 'painted-oil-matte', wallSide: 'painted-oil-matte', ceiling: 'painted-emulsion', floor: 'parquet-herringbone' },
|
||||||
{ wall: '#e89060', wallSide: '#e8c860', ceiling: '#f8f0e0', floor: '#a07048', trim: '#c04020' },
|
{ wall: '#e89060', wallSide: '#e8c860', ceiling: '#9e998e', floor: '#a07048', trim: '#c04020' },
|
||||||
{
|
{
|
||||||
details: 'atelier',
|
details: 'fauvist',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
titleColor: '#402010',
|
titleColor: '#402010',
|
||||||
ambient: 0.7,
|
ambient: 0.7,
|
||||||
windows: windows(
|
windows: windows(
|
||||||
@@ -414,7 +483,7 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
{ wall: 'dark-wood-panel', ceiling: 'dark-plaster', floor: 'parquet-herringbone' },
|
{ wall: 'dark-wood-panel', ceiling: 'dark-plaster', floor: 'parquet-herringbone' },
|
||||||
{ wall: '#3a2818', ceiling: '#2a2018', floor: '#5a4030', trim: '#8a6040' },
|
{ wall: '#3a2818', ceiling: '#2a2018', floor: '#5a4030', trim: '#8a6040' },
|
||||||
{
|
{
|
||||||
details: 'atelier',
|
details: 'expressionist',
|
||||||
titleColor: '#f0d8b0',
|
titleColor: '#f0d8b0',
|
||||||
ambient: 0.52,
|
ambient: 0.52,
|
||||||
windows: windows(
|
windows: windows(
|
||||||
@@ -428,9 +497,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'Cubist studio',
|
'Cubist studio',
|
||||||
'Paris atelier · factory windows · painted plaster',
|
'Paris atelier · factory windows · painted plaster',
|
||||||
{ wall: 'painted-emulsion', ceiling: 'painted-flat', floor: 'parquet-herringbone' },
|
{ wall: 'painted-emulsion', ceiling: 'painted-flat', floor: 'parquet-herringbone' },
|
||||||
{ wall: '#e8e4dc', ceiling: '#f5f5f5', floor: '#9a8870', trim: '#888888' },
|
{ wall: '#8c8a85', ceiling: '#999999', floor: '#9a8870', trim: '#888888' },
|
||||||
{
|
{
|
||||||
details: 'atelier',
|
details: 'cubist-atelier',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
ambient: 0.65,
|
ambient: 0.65,
|
||||||
windows: windows(
|
windows: windows(
|
||||||
{ wall: 'left', x: 0, y: 2.5, width: 3.5, height: 2.4, style: 'factory', lightColor: '#f0f8ff', lightIntensity: 5.5 },
|
{ wall: 'left', x: 0, y: 2.5, width: 3.5, height: 2.4, style: 'factory', lightColor: '#f0f8ff', lightIntensity: 5.5 },
|
||||||
@@ -443,9 +515,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'Futurist loft',
|
'Futurist loft',
|
||||||
'Steel and glass · industrial concrete · sweeping daylight',
|
'Steel and glass · industrial concrete · sweeping daylight',
|
||||||
{ wall: 'concrete-raw', ceiling: 'concrete-raw', floor: 'industrial-floor' },
|
{ wall: 'concrete-raw', ceiling: 'concrete-raw', floor: 'industrial-floor' },
|
||||||
{ wall: '#a8a8a8', ceiling: '#989898', floor: '#888880', trim: '#606060' },
|
{ wall: '#858585', ceiling: '#7a7a7a', floor: '#888880', trim: '#606060' },
|
||||||
{
|
{
|
||||||
details: 'industrial',
|
details: 'futurist',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
titleColor: '#303030',
|
titleColor: '#303030',
|
||||||
ambient: 0.62,
|
ambient: 0.62,
|
||||||
fog: '#181818',
|
fog: '#181818',
|
||||||
@@ -462,9 +537,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'Suprematist white cube',
|
'Suprematist white cube',
|
||||||
'Matte white paint · geometric light · polished floor',
|
'Matte white paint · geometric light · polished floor',
|
||||||
{ wall: 'painted-flat', ceiling: 'painted-flat', floor: 'concrete-polished' },
|
{ wall: 'painted-flat', ceiling: 'painted-flat', floor: 'concrete-polished' },
|
||||||
{ wall: '#ffffff', ceiling: '#ffffff', floor: '#e0e0e0', trim: '#cccccc' },
|
{ wall: '#a8a8a8', ceiling: '#b3b3b3', floor: '#e0e0e0', trim: '#cccccc' },
|
||||||
{
|
{
|
||||||
details: 'modern',
|
details: 'suprematist',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
titleColor: '#222222',
|
titleColor: '#222222',
|
||||||
ambient: 0.72,
|
ambient: 0.72,
|
||||||
fog: '#1a1a1a',
|
fog: '#1a1a1a',
|
||||||
@@ -482,9 +560,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'Constructivist space',
|
'Constructivist space',
|
||||||
'Glass block · concrete · angular daylight',
|
'Glass block · concrete · angular daylight',
|
||||||
{ wall: 'concrete-block', ceiling: 'concrete-raw', floor: 'concrete-polished' },
|
{ wall: 'concrete-block', ceiling: 'concrete-raw', floor: 'concrete-polished' },
|
||||||
{ wall: '#b0b0b0', ceiling: '#a0a0a0', floor: '#c0c0c0', trim: '#808080' },
|
{ wall: '#858585', ceiling: '#7a7a7a', floor: '#c0c0c0', trim: '#808080' },
|
||||||
{
|
{
|
||||||
details: 'industrial',
|
details: 'constructivist',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
titleColor: '#303030',
|
titleColor: '#303030',
|
||||||
ambient: 0.65,
|
ambient: 0.65,
|
||||||
windows: windows(
|
windows: windows(
|
||||||
@@ -500,9 +581,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'Dada salon',
|
'Dada salon',
|
||||||
'Eclectic bourgeois room · painted walls and exposed brick',
|
'Eclectic bourgeois room · painted walls and exposed brick',
|
||||||
{ wall: 'painted-emulsion', wallSide: 'brick', ceiling: 'painted-emulsion', floor: 'parquet-herringbone' },
|
{ wall: 'painted-emulsion', wallSide: 'brick', ceiling: 'painted-emulsion', floor: 'parquet-herringbone' },
|
||||||
{ wall: '#e8dcc8', wallSide: '#8a5040', ceiling: '#f0ebe0', floor: '#8a6848', trim: '#6a4830' },
|
{ wall: '#8b8478', wallSide: '#8a5040', ceiling: '#97948d', floor: '#8a6848', trim: '#6a4830' },
|
||||||
{
|
{
|
||||||
details: 'salon',
|
details: 'dada',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
windows: windows(
|
windows: windows(
|
||||||
{ wall: 'back', x: -1.5, y: 2.5, width: 1.0, height: 1.4, style: 'sash', lightColor: '#f0f4ff', lightIntensity: 2.5 },
|
{ wall: 'back', x: -1.5, y: 2.5, width: 1.0, height: 1.4, style: 'sash', lightColor: '#f0f4ff', lightIntensity: 2.5 },
|
||||||
{ wall: 'back', x: 1.5, y: 2.6, width: 0.8, height: 1.8, style: 'gothic-lancet', lightColor: '#ffe8c0', lightIntensity: 2.0 },
|
{ wall: 'back', x: 1.5, y: 2.6, width: 0.8, height: 1.8, style: 'gothic-lancet', lightColor: '#ffe8c0', lightIntensity: 2.0 },
|
||||||
@@ -518,7 +602,7 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
{ wall: 'painted-oil-matte', ceiling: 'dark-plaster', floor: 'parquet-herringbone' },
|
{ wall: 'painted-oil-matte', ceiling: 'dark-plaster', floor: 'parquet-herringbone' },
|
||||||
{ wall: '#1a2848', ceiling: '#2a2428', floor: '#5a4838', trim: '#8a7050' },
|
{ wall: '#1a2848', ceiling: '#2a2428', floor: '#5a4838', trim: '#8a7050' },
|
||||||
{
|
{
|
||||||
details: 'salon',
|
details: 'surrealist',
|
||||||
titleColor: '#e8dcc8',
|
titleColor: '#e8dcc8',
|
||||||
ambient: 0.58,
|
ambient: 0.58,
|
||||||
warmLight: '#ffd898',
|
warmLight: '#ffd898',
|
||||||
@@ -535,9 +619,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'NYC loft',
|
'NYC loft',
|
||||||
'Raw brick and painted plaster · north-light factory windows',
|
'Raw brick and painted plaster · north-light factory windows',
|
||||||
{ wall: 'brick', wallSide: 'painted-flat', ceiling: 'concrete-raw', floor: 'industrial-floor' },
|
{ wall: 'brick', wallSide: 'painted-flat', ceiling: 'concrete-raw', floor: 'industrial-floor' },
|
||||||
{ wall: '#8a5040', wallSide: '#f5f5f5', ceiling: '#989898', floor: '#888880', trim: '#606060' },
|
{ wall: '#8a5040', wallSide: '#858585', ceiling: '#989898', floor: '#888880', trim: '#606060' },
|
||||||
{
|
{
|
||||||
details: 'industrial',
|
details: 'loft',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
titleColor: '#303030',
|
titleColor: '#303030',
|
||||||
ambient: 0.65,
|
ambient: 0.65,
|
||||||
windows: windows(
|
windows: windows(
|
||||||
@@ -551,9 +638,12 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
'Pop Art gallery',
|
'Pop Art gallery',
|
||||||
'White painted walls · fluorescent daylight · polished concrete',
|
'White painted walls · fluorescent daylight · polished concrete',
|
||||||
{ wall: 'painted-flat', ceiling: 'painted-flat', floor: 'concrete-polished' },
|
{ wall: 'painted-flat', ceiling: 'painted-flat', floor: 'concrete-polished' },
|
||||||
{ wall: '#ffffff', ceiling: '#ffffff', floor: '#d8d8d8', trim: '#cccccc' },
|
{ wall: '#a8a8a8', ceiling: '#b3b3b3', floor: '#d8d8d8', trim: '#cccccc' },
|
||||||
{
|
{
|
||||||
details: 'modern',
|
details: 'white-cube',
|
||||||
|
// Lit like the basilica: shared lights held back so the walls read as
|
||||||
|
// surfaces rather than blowing out. Window shafts are not scaled.
|
||||||
|
lightScale: 0.3,
|
||||||
titleColor: '#222222',
|
titleColor: '#222222',
|
||||||
ambient: 0.75,
|
ambient: 0.75,
|
||||||
warmLight: '#ffffff',
|
warmLight: '#ffffff',
|
||||||
|
|||||||
Reference in New Issue
Block a user