Movement galleries split large catalogs into chronological wings (~55 works), use era-themed 3D interiors with side-wall windows, wing navigator on the back exit, and front archways between wings. Also adds painting annotations, timeline event guides, portrait hover highlights, and documentation/API updates. Co-authored-by: Cursor <cursoragent@cursor.com>
223 lines
7.4 KiB
TypeScript
223 lines
7.4 KiB
TypeScript
import { useMemo } from 'react';
|
|
import * as THREE from 'three';
|
|
import type { MovementInteriorStyle } from '../data/movement-interior-styles';
|
|
|
|
interface Props {
|
|
style: MovementInteriorStyle;
|
|
width: number;
|
|
depth: number;
|
|
halfW: number;
|
|
halfD: number;
|
|
}
|
|
|
|
function PalazzoDetails({ width, depth, halfW, halfD, trim }: Props & { 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]} castShadow>
|
|
<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 MedievalDetails({ halfW, halfD }: Pick<Props, 'halfW' | 'halfD'>) {
|
|
const torchPositions = useMemo(
|
|
() =>
|
|
[
|
|
[-halfW + 0.2, -halfD * 0.5],
|
|
[halfW - 0.2, -halfD * 0.5],
|
|
[-halfW + 0.2, halfD * 0.3],
|
|
[halfW - 0.2, halfD * 0.3],
|
|
] as [number, number][],
|
|
[halfW, halfD]
|
|
);
|
|
|
|
return (
|
|
<group>
|
|
{torchPositions.map(([x, z], i) => (
|
|
<group key={i} position={[x, 2.2, z]}>
|
|
<mesh>
|
|
<boxGeometry args={[0.08, 0.35, 0.12]} />
|
|
<meshStandardMaterial color="#3a3028" roughness={0.9} />
|
|
</mesh>
|
|
<pointLight position={[0, 0.15, 0.08]} intensity={0.65} distance={5} color="#ff9830" />
|
|
<mesh position={[0, 0.2, 0.06]}>
|
|
<sphereGeometry args={[0.06, 8, 8]} />
|
|
<meshStandardMaterial color="#ffb040" emissive="#ff8010" emissiveIntensity={0.8} toneMapped={false} />
|
|
</mesh>
|
|
</group>
|
|
))}
|
|
|
|
{/* Rough stone courses */}
|
|
{[-halfD + 0.08, halfD - 0.08].map((z, i) => (
|
|
<mesh key={i} position={[0, 1.5, z]}>
|
|
<boxGeometry args={[0.04, 3, 0.04]} />
|
|
<meshStandardMaterial color="#6a6458" roughness={0.98} />
|
|
</mesh>
|
|
))}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
function NeoclassicalDetails({ halfW, halfD, trim }: Pick<Props, 'halfW' | 'halfD'> & { trim: string }) {
|
|
const columns = [
|
|
[-halfW + 0.45, -halfD + 0.6],
|
|
[halfW - 0.45, -halfD + 0.6],
|
|
[-halfW + 0.45, halfD - 0.6],
|
|
[halfW - 0.45, halfD - 0.6],
|
|
] as [number, number][];
|
|
|
|
return (
|
|
<group>
|
|
{columns.map(([x, z], i) => (
|
|
<group key={i} position={[x, 0, z]}>
|
|
<mesh position={[0, 1.85, 0]}>
|
|
<cylinderGeometry args={[0.18, 0.2, 3.7, 12]} />
|
|
<meshStandardMaterial color="#f0ece8" roughness={0.72} metalness={0.12} />
|
|
</mesh>
|
|
<mesh position={[0, 3.85, 0]}>
|
|
<boxGeometry args={[0.42, 0.12, 0.42]} />
|
|
<meshStandardMaterial color={trim} roughness={0.4} metalness={0.35} />
|
|
</mesh>
|
|
</group>
|
|
))}
|
|
<mesh position={[0, 3.88, -halfD + 0.12]}>
|
|
<boxGeometry args={[2.8, 0.14, 0.2]} />
|
|
<meshStandardMaterial color={trim} roughness={0.45} metalness={0.3} />
|
|
</mesh>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
function ClassicalDetails({ halfW, trim }: Pick<Props, 'halfW'> & { trim: string }) {
|
|
return (
|
|
<group>
|
|
{[
|
|
[-halfW + 0.5, 0],
|
|
[halfW - 0.5, 0],
|
|
].map(([x, z], i) => (
|
|
<group key={i} position={[x, 0, z]}>
|
|
<mesh position={[0, 2, 0]}>
|
|
<cylinderGeometry args={[0.16, 0.18, 4, 10]} />
|
|
<meshStandardMaterial color="#e0d8c8" roughness={0.88} />
|
|
</mesh>
|
|
<mesh position={[0, 4.05, 0]}>
|
|
<boxGeometry args={[0.36, 0.1, 0.36]} />
|
|
<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;
|
|
|
|
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 'medieval':
|
|
return <MedievalDetails halfW={halfW} halfD={halfD} />;
|
|
case 'neoclassical':
|
|
return <NeoclassicalDetails halfW={halfW} halfD={halfD} trim={trim} />;
|
|
case 'classical':
|
|
return <ClassicalDetails halfW={halfW} trim={trim} />;
|
|
case 'salon':
|
|
return <SalonDetails trim={trim} />;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|