Files
Art-gallery/client/src/components/GalleryWindows.tsx
T
Danila KhodjaefandCursor 1853222e01 Fix movement-hall black walls and column clipping.
Cap shared hall lights (no per-painting spots) so MeshStandard walls stay within WebGL limits, tuck classical/neoclassical details into engaged corner pilasters, and document the light budget.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-31 00:26:14 +03:00

254 lines
7.4 KiB
TypeScript

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>
{/* One fill light per window — keep total scene lights well under WebGL limits. */}
<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 * Math.PI * 0.85}
distance={14}
decay={0}
color={spec.lightColor}
/>
</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][] = [];
// Cap fixtures so MeshStandard hall materials stay within WebGL light limits.
const cols = Math.max(2, Math.min(3, Math.floor(width / 5)));
const rows = Math.max(1, Math.min(2, Math.floor(depth / 8)));
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.7}
penumbra={0.9}
intensity={intensity}
distance={12}
decay={0}
color={color}
/>
</group>
))}
</group>
);
}