Add movement gallery wings with period interiors and expand timeline features.
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>
This commit is contained in:
co-authored by
Cursor
parent
0972b5df99
commit
df29848d89
@@ -0,0 +1,259 @@
|
||||
import { useMemo } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import type { GalleryWindowSpec, GalleryWindowStyle } from '../data/movement-interior-styles';
|
||||
|
||||
const WALL_HEIGHT = 4.2;
|
||||
|
||||
interface Props {
|
||||
windows: GalleryWindowSpec[];
|
||||
halfW: number;
|
||||
halfD: number;
|
||||
trimColor: string;
|
||||
}
|
||||
|
||||
function windowWorldPosition(
|
||||
spec: GalleryWindowSpec,
|
||||
halfW: number,
|
||||
halfD: number
|
||||
): { position: [number, number, number]; rotation: [number, number, number] } {
|
||||
const inset = 0.1;
|
||||
switch (spec.wall) {
|
||||
case 'back':
|
||||
return {
|
||||
position: [spec.x, spec.y, -halfD + inset],
|
||||
rotation: [0, 0, 0],
|
||||
};
|
||||
case 'left':
|
||||
return {
|
||||
position: [-halfW + inset, spec.y, spec.x],
|
||||
rotation: [0, Math.PI / 2, 0],
|
||||
};
|
||||
case 'right':
|
||||
return {
|
||||
position: [halfW - inset, spec.y, spec.x],
|
||||
rotation: [0, -Math.PI / 2, 0],
|
||||
};
|
||||
case 'ceiling':
|
||||
return {
|
||||
position: [spec.x, WALL_HEIGHT - 0.06, spec.x === 0 ? 0 : spec.x],
|
||||
rotation: [Math.PI / 2, 0, 0],
|
||||
};
|
||||
default:
|
||||
return { position: [0, spec.y, -halfD + inset], rotation: [0, 0, 0] };
|
||||
}
|
||||
}
|
||||
|
||||
function WindowFrame({
|
||||
style,
|
||||
width,
|
||||
height,
|
||||
trimColor,
|
||||
}: {
|
||||
style: GalleryWindowStyle;
|
||||
width: number;
|
||||
height: number;
|
||||
trimColor: string;
|
||||
}) {
|
||||
const frameW = 0.08;
|
||||
const depth = 0.12;
|
||||
|
||||
const arch =
|
||||
style === 'roman-arch' || style === 'gothic-lancet' || style === 'art-nouveau';
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* Outer frame */}
|
||||
<mesh position={[0, 0, -depth / 2]}>
|
||||
<boxGeometry args={[width + frameW * 2, height + frameW * 2, depth]} />
|
||||
<meshStandardMaterial color={trimColor} roughness={0.35} metalness={0.45} />
|
||||
</mesh>
|
||||
|
||||
{arch && style === 'gothic-lancet' && (
|
||||
<mesh position={[0, height / 2 + 0.15, -depth / 2 + 0.02]}>
|
||||
<coneGeometry args={[width / 2 + frameW, 0.5, 4]} />
|
||||
<meshStandardMaterial color={trimColor} roughness={0.4} metalness={0.35} />
|
||||
</mesh>
|
||||
)}
|
||||
|
||||
{style === 'roman-arch' && (
|
||||
<mesh position={[0, height / 2 + 0.05, -depth / 2 + 0.02]} rotation={[0, 0, Math.PI]}>
|
||||
<sphereGeometry args={[width / 2, 16, 8, 0, Math.PI * 2, 0, Math.PI / 2]} />
|
||||
<meshStandardMaterial color={trimColor} roughness={0.4} metalness={0.35} />
|
||||
</mesh>
|
||||
)}
|
||||
|
||||
{style === 'factory' && (
|
||||
<>
|
||||
{[-width / 3, 0, width / 3].map((ox) => (
|
||||
<mesh key={ox} position={[ox, 0, -depth / 2 + 0.02]}>
|
||||
<boxGeometry args={[0.06, height, depth + 0.02]} />
|
||||
<meshStandardMaterial color="#3a3a3a" roughness={0.5} metalness={0.6} />
|
||||
</mesh>
|
||||
))}
|
||||
{[height / 4, -height / 4].map((oy) => (
|
||||
<mesh key={oy} position={[0, oy, -depth / 2 + 0.02]}>
|
||||
<boxGeometry args={[width, 0.06, depth + 0.02]} />
|
||||
<meshStandardMaterial color="#3a3a3a" roughness={0.5} metalness={0.6} />
|
||||
</mesh>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{style === 'glass-block' &&
|
||||
Array.from({ length: 4 }, (_, row) =>
|
||||
Array.from({ length: 3 }, (_, col) => (
|
||||
<mesh
|
||||
key={`${row}-${col}`}
|
||||
position={[
|
||||
-width / 3 + col * (width / 3),
|
||||
-height / 4 + row * (height / 4),
|
||||
-depth / 2 + 0.03,
|
||||
]}
|
||||
>
|
||||
<boxGeometry args={[width / 3 - 0.06, height / 4 - 0.06, 0.04]} />
|
||||
<meshStandardMaterial
|
||||
color="#d0e8f8"
|
||||
roughness={0.1}
|
||||
metalness={0.05}
|
||||
transparent
|
||||
opacity={0.75}
|
||||
/>
|
||||
</mesh>
|
||||
))
|
||||
)}
|
||||
|
||||
{style === 'sash' && (
|
||||
<mesh position={[0, 0, -depth / 2 + 0.03]}>
|
||||
<boxGeometry args={[0.05, height, depth]} />
|
||||
<meshStandardMaterial color={trimColor} roughness={0.45} metalness={0.3} />
|
||||
</mesh>
|
||||
)}
|
||||
|
||||
{style === 'baroque-pair' && (
|
||||
<mesh position={[0, 0, -depth / 2 + 0.03]}>
|
||||
<boxGeometry args={[0.06, height, depth]} />
|
||||
<meshStandardMaterial color={trimColor} roughness={0.3} metalness={0.55} />
|
||||
</mesh>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
function SingleWindow({
|
||||
spec,
|
||||
trimColor,
|
||||
}: {
|
||||
spec: GalleryWindowSpec;
|
||||
trimColor: string;
|
||||
}) {
|
||||
const glassColor = useMemo(() => new THREE.Color(spec.lightColor), [spec.lightColor]);
|
||||
|
||||
return (
|
||||
<group>
|
||||
<WindowFrame style={spec.style} width={spec.width} height={spec.height} trimColor={trimColor} />
|
||||
|
||||
{/* Sky / daylight pane */}
|
||||
<mesh position={[0, 0, 0.02]}>
|
||||
<planeGeometry args={[spec.width - 0.12, spec.height - 0.12]} />
|
||||
<meshStandardMaterial
|
||||
color={spec.lightColor}
|
||||
emissive={spec.lightColor}
|
||||
emissiveIntensity={0.85}
|
||||
roughness={0.05}
|
||||
metalness={0.02}
|
||||
toneMapped={false}
|
||||
transparent
|
||||
opacity={0.92}
|
||||
/>
|
||||
</mesh>
|
||||
|
||||
{/* Soft sky gradient overlay */}
|
||||
<mesh position={[0, spec.height * 0.15, 0.03]}>
|
||||
<planeGeometry args={[spec.width - 0.2, spec.height * 0.5]} />
|
||||
<meshBasicMaterial color="#ffffff" transparent opacity={0.25} toneMapped={false} />
|
||||
</mesh>
|
||||
|
||||
{/* Daylight into room */}
|
||||
<spotLight
|
||||
position={[0, 0, 0.15]}
|
||||
angle={Math.min(1.2, (spec.width / Math.max(spec.height, 0.5)) * 0.55)}
|
||||
penumbra={0.95}
|
||||
intensity={spec.lightIntensity}
|
||||
distance={14}
|
||||
color={spec.lightColor}
|
||||
castShadow={false}
|
||||
/>
|
||||
<pointLight
|
||||
position={[0, 0, 0.25]}
|
||||
intensity={spec.lightIntensity * 0.45}
|
||||
distance={10}
|
||||
color={glassColor}
|
||||
decay={2}
|
||||
/>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GalleryWindows({ windows, halfW, halfD, trimColor }: Props) {
|
||||
return (
|
||||
<group>
|
||||
{windows.map((spec, i) => {
|
||||
const { position, rotation } = windowWorldPosition(spec, halfW, halfD);
|
||||
return (
|
||||
<group key={`${spec.wall}-${spec.x}-${i}`} position={position} rotation={rotation}>
|
||||
<SingleWindow spec={spec} trimColor={trimColor} />
|
||||
</group>
|
||||
);
|
||||
})}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
/** Ceiling-mounted gallery track lights for even illumination. */
|
||||
export function GalleryTrackLights({
|
||||
width,
|
||||
depth,
|
||||
intensity,
|
||||
color,
|
||||
}: {
|
||||
width: number;
|
||||
depth: number;
|
||||
intensity: number;
|
||||
color: string;
|
||||
}) {
|
||||
const positions = useMemo(() => {
|
||||
const pts: [number, number, number][] = [];
|
||||
const cols = Math.max(2, Math.min(5, Math.floor(width / 4)));
|
||||
const rows = Math.max(1, Math.min(3, Math.floor(depth / 6)));
|
||||
for (let c = 0; c < cols; c++) {
|
||||
for (let r = 0; r < rows; r++) {
|
||||
const x = -width / 2 + (width / (cols + 1)) * (c + 1);
|
||||
const z = -depth / 2 + (depth / (rows + 1)) * (r + 1);
|
||||
pts.push([x, WALL_HEIGHT - 0.15, z]);
|
||||
}
|
||||
}
|
||||
return pts;
|
||||
}, [width, depth]);
|
||||
|
||||
return (
|
||||
<group>
|
||||
{positions.map(([x, y, z], i) => (
|
||||
<group key={i} position={[x, y, z]}>
|
||||
<mesh rotation={[Math.PI, 0, 0]}>
|
||||
<cylinderGeometry args={[0.02, 0.025, 0.12, 8]} />
|
||||
<meshStandardMaterial color="#2a2a2a" metalness={0.8} roughness={0.25} />
|
||||
</mesh>
|
||||
<spotLight
|
||||
position={[0, -0.04, 0]}
|
||||
angle={0.55}
|
||||
penumbra={0.85}
|
||||
intensity={intensity}
|
||||
distance={8}
|
||||
color={color}
|
||||
castShadow={false}
|
||||
/>
|
||||
</group>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user