131 lines
3.4 KiB
TypeScript
131 lines
3.4 KiB
TypeScript
|
|
import { useMemo } from 'react';
|
||
|
|
import * as THREE from 'three';
|
||
|
|
import { Html } from '@react-three/drei';
|
||
|
|
import { BUILDINGS, COLORS } from '../constants';
|
||
|
|
|
||
|
|
interface BuildingsProps {
|
||
|
|
detailMode?: boolean;
|
||
|
|
onBuildingClick?: (building: string) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
function WindowGrid({ width, height, depth }: { width: number; height: number; depth: number }) {
|
||
|
|
const windows = useMemo(() => {
|
||
|
|
const cols = 4;
|
||
|
|
const rows = 3;
|
||
|
|
const winW = 1.5;
|
||
|
|
const winH = 0.8;
|
||
|
|
const winD = 0.05;
|
||
|
|
const gapX = (width - cols * winW) / (cols + 1);
|
||
|
|
const gapY = (height - rows * winH) / (rows + 1);
|
||
|
|
const items: { pos: [number, number, number] }[] = [];
|
||
|
|
|
||
|
|
for (let r = 0; r < rows; r++) {
|
||
|
|
for (let c = 0; c < cols; c++) {
|
||
|
|
const x = -width / 2 + gapX + winW / 2 + c * (winW + gapX);
|
||
|
|
const y = -height / 2 + gapY + winH / 2 + r * (winH + gapY);
|
||
|
|
items.push({ pos: [x, y, depth / 2 + winD / 2] });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return items;
|
||
|
|
}, [width, height, depth]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<group>
|
||
|
|
{windows.map((w, i) => (
|
||
|
|
<mesh key={i} position={w.pos}>
|
||
|
|
<boxGeometry args={[1.5, 0.8, 0.05]} />
|
||
|
|
<meshStandardMaterial
|
||
|
|
color="#ffcc66"
|
||
|
|
emissive="#ffcc66"
|
||
|
|
emissiveIntensity={0.3}
|
||
|
|
transparent
|
||
|
|
opacity={0.6}
|
||
|
|
/>
|
||
|
|
</mesh>
|
||
|
|
))}
|
||
|
|
</group>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function Building({
|
||
|
|
label,
|
||
|
|
position,
|
||
|
|
size,
|
||
|
|
opacity,
|
||
|
|
onClick,
|
||
|
|
}: {
|
||
|
|
label: string;
|
||
|
|
position: [number, number, number];
|
||
|
|
size: [number, number, number];
|
||
|
|
opacity: number;
|
||
|
|
onClick?: () => void;
|
||
|
|
}) {
|
||
|
|
const [w, h, d] = size;
|
||
|
|
|
||
|
|
const edgesGeo = useMemo(() => {
|
||
|
|
const box = new THREE.BoxGeometry(w, h, d);
|
||
|
|
return new THREE.EdgesGeometry(box);
|
||
|
|
}, [w, h, d]);
|
||
|
|
|
||
|
|
const labelStyle: React.CSSProperties = {
|
||
|
|
fontSize: '13px',
|
||
|
|
color: COLORS.text,
|
||
|
|
background: 'rgba(6, 30, 62, 0.8)',
|
||
|
|
padding: '2px 8px',
|
||
|
|
borderRadius: '4px',
|
||
|
|
border: '1px solid rgba(0, 212, 255, 0.2)',
|
||
|
|
whiteSpace: 'nowrap',
|
||
|
|
pointerEvents: 'none',
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<group position={position} onClick={onClick ? (e) => { e.stopPropagation(); onClick(); } : undefined}>
|
||
|
|
{/* Main body */}
|
||
|
|
<mesh castShadow receiveShadow>
|
||
|
|
<boxGeometry args={[w, h, d]} />
|
||
|
|
<meshStandardMaterial
|
||
|
|
color={COLORS.buildingBase}
|
||
|
|
transparent
|
||
|
|
opacity={opacity}
|
||
|
|
/>
|
||
|
|
</mesh>
|
||
|
|
|
||
|
|
{/* Edge highlight */}
|
||
|
|
<lineSegments geometry={edgesGeo}>
|
||
|
|
<lineBasicMaterial color="#00d4ff" transparent opacity={0.3} />
|
||
|
|
</lineSegments>
|
||
|
|
|
||
|
|
{/* Windows on front face */}
|
||
|
|
<WindowGrid width={w} height={h} depth={d} />
|
||
|
|
|
||
|
|
{/* Label */}
|
||
|
|
<Html position={[0, h / 2 + 1.5, 0]} center>
|
||
|
|
<div style={labelStyle}>{label}</div>
|
||
|
|
</Html>
|
||
|
|
</group>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function Buildings({ detailMode, onBuildingClick }: BuildingsProps) {
|
||
|
|
const opacity = detailMode ? 0.15 : 0.85;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<group>
|
||
|
|
<Building
|
||
|
|
label={BUILDINGS.east.label}
|
||
|
|
position={[...BUILDINGS.east.position]}
|
||
|
|
size={[...BUILDINGS.east.size]}
|
||
|
|
opacity={opacity}
|
||
|
|
onClick={onBuildingClick ? () => onBuildingClick('east') : undefined}
|
||
|
|
/>
|
||
|
|
<Building
|
||
|
|
label={BUILDINGS.west.label}
|
||
|
|
position={[...BUILDINGS.west.position]}
|
||
|
|
size={[...BUILDINGS.west.size]}
|
||
|
|
opacity={opacity}
|
||
|
|
onClick={onBuildingClick ? () => onBuildingClick('west') : undefined}
|
||
|
|
/>
|
||
|
|
</group>
|
||
|
|
);
|
||
|
|
}
|