229 lines
7.6 KiB
TypeScript
229 lines
7.6 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { Grid, Environment, Text } from '@react-three/drei';
|
|
import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls';
|
|
import * as THREE from 'three';
|
|
import SceneCanvas from '../SceneCanvas';
|
|
import FallbackDiagram from '../FallbackDiagram';
|
|
import { useCanvasTheme } from '@/lib/theme';
|
|
|
|
export interface Vault3DInput {
|
|
span: number;
|
|
length: number;
|
|
rise: number;
|
|
cpi: number;
|
|
cpeProfile: Record<string, number>;
|
|
cpeParallel?: Record<string, number>;
|
|
viewDirection?: 'perpendicular' | 'parallel';
|
|
}
|
|
|
|
function vaultColor(cpe: number, cpi: number, isDark: boolean): THREE.Color {
|
|
const p = cpe - cpi;
|
|
const intensity = Math.min(1, Math.abs(p) / 1.5);
|
|
const l = isDark ? (65 - intensity * 15) : (60 - intensity * 25);
|
|
if (p > 0) {
|
|
return new THREE.Color(`hsl(${215 - intensity * 10}, ${70 + intensity * 25}%, ${Math.max(35, l)}%)`);
|
|
}
|
|
return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, l + 5)}%)`);
|
|
}
|
|
|
|
function VaultModel({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDirection = 'perpendicular' }: Vault3DInput) {
|
|
const theme = useCanvasTheme();
|
|
const isDark = theme === 'dark';
|
|
const segments = 64;
|
|
|
|
const points = useMemo(() => {
|
|
const pts: THREE.Vector3[] = [];
|
|
for (let i = 0; i <= segments; i++) {
|
|
const t = i / segments;
|
|
const x = -span / 2 + t * span;
|
|
const y = rise * Math.sin(t * Math.PI);
|
|
pts.push(new THREE.Vector3(x, y, 0));
|
|
}
|
|
return pts;
|
|
}, [span, rise, segments]);
|
|
|
|
const archShape = useMemo(() => {
|
|
const s = new THREE.Shape();
|
|
s.moveTo(-span / 2, 0);
|
|
for (let i = 0; i <= segments; i++) {
|
|
const t = i / segments;
|
|
const x = -span / 2 + t * span;
|
|
const y = rise * Math.sin(t * Math.PI);
|
|
s.lineTo(x, y);
|
|
}
|
|
s.lineTo(span / 2, 0);
|
|
s.closePath();
|
|
return s;
|
|
}, [span, rise, segments]);
|
|
|
|
const renderPerpendicular = () => {
|
|
const zones = [];
|
|
const zoneSize = segments / 6;
|
|
for (let z = 0; z < 6; z++) {
|
|
const startIdx = Math.floor(z * zoneSize);
|
|
const endIdx = Math.floor((z + 1) * zoneSize);
|
|
const key = `zone${z + 1}`;
|
|
zones.push({ cpe: cpeProfile[key] ?? -0.5, startIdx, endIdx, label: `${z + 1}` });
|
|
}
|
|
|
|
return zones.map((zone, idx) => {
|
|
const color = vaultColor(zone.cpe, cpi, isDark);
|
|
const verts: number[] = [];
|
|
for (let i = zone.startIdx; i <= zone.endIdx; i++) {
|
|
verts.push(points[i].x, points[i].y, points[i].z);
|
|
verts.push(points[i].x, points[i].y, length);
|
|
}
|
|
const indices: number[] = [];
|
|
for (let i = 0; i < (zone.endIdx - zone.startIdx); i++) {
|
|
const a = i * 2;
|
|
const b = i * 2 + 1;
|
|
const c = i * 2 + 2;
|
|
const d = i * 2 + 3;
|
|
indices.push(a, b, c, b, d, c);
|
|
}
|
|
|
|
const midIdx = Math.floor((zone.startIdx + zone.endIdx) / 2);
|
|
const midPt = points[midIdx];
|
|
|
|
return (
|
|
<group key={idx}>
|
|
<mesh castShadow receiveShadow>
|
|
<bufferGeometry>
|
|
<bufferAttribute attach="attributes-position" args={[new Float32Array(verts), 3]} />
|
|
<bufferAttribute attach="index" args={[new Uint16Array(indices), 1]} />
|
|
</bufferGeometry>
|
|
<meshStandardMaterial color={color} opacity={0.92} transparent side={THREE.DoubleSide} roughness={0.4} />
|
|
</mesh>
|
|
<Text
|
|
position={[midPt.x, midPt.y + 0.5, length / 2]}
|
|
fontSize={Math.max(0.48, span / 18.75)}
|
|
color={isDark ? "#f8fafc" : "#0f172a"}
|
|
anchorX="center"
|
|
anchorY="middle"
|
|
>
|
|
{zone.label}
|
|
</Text>
|
|
</group>
|
|
);
|
|
});
|
|
};
|
|
|
|
const renderParallel = () => {
|
|
const zones = ['A', 'B', 'C', 'D'];
|
|
const zStep = length / 4;
|
|
|
|
return zones.map((label, idx) => {
|
|
const zStart = idx * zStep;
|
|
const zEnd = (idx + 1) * zStep;
|
|
const cpe = cpeParallel?.[label] ?? -0.5;
|
|
const color = vaultColor(cpe, cpi, isDark);
|
|
|
|
const verts: number[] = [];
|
|
for (let i = 0; i <= segments; i++) {
|
|
verts.push(points[i].x, points[i].y, zStart);
|
|
verts.push(points[i].x, points[i].y, zEnd);
|
|
}
|
|
const indices: number[] = [];
|
|
for (let i = 0; i < segments; i++) {
|
|
const a = i * 2;
|
|
const b = i * 2 + 1;
|
|
const c = i * 2 + 2;
|
|
const d = i * 2 + 3;
|
|
indices.push(a, b, c, b, d, c);
|
|
}
|
|
|
|
return (
|
|
<group key={idx}>
|
|
<mesh castShadow receiveShadow>
|
|
<bufferGeometry>
|
|
<bufferAttribute attach="attributes-position" args={[new Float32Array(verts), 3]} />
|
|
<bufferAttribute attach="index" args={[new Uint16Array(indices), 1]} />
|
|
</bufferGeometry>
|
|
<meshStandardMaterial color={color} opacity={0.92} transparent side={THREE.DoubleSide} roughness={0.4} />
|
|
</mesh>
|
|
<Text
|
|
position={[0, rise + 0.5, (zStart + zEnd) / 2]}
|
|
fontSize={Math.max(0.48, span / 18.75)}
|
|
color={isDark ? "#f8fafc" : "#0f172a"}
|
|
anchorX="center"
|
|
anchorY="middle"
|
|
rotation={[-Math.PI / 2, 0, 0]}
|
|
>
|
|
{label}
|
|
</Text>
|
|
</group>
|
|
);
|
|
});
|
|
};
|
|
|
|
return (
|
|
<group>
|
|
{viewDirection === 'perpendicular' ? renderPerpendicular() : renderParallel()}
|
|
|
|
{/* Tímpano Traseiro (Z = 0) */}
|
|
<mesh position={[0, 0, 0]} castShadow receiveShadow>
|
|
<shapeGeometry args={[archShape]} />
|
|
<meshStandardMaterial color={isDark ? "#334155" : "#cbd5e1"} opacity={0.7} transparent side={THREE.DoubleSide} roughness={0.5} />
|
|
</mesh>
|
|
|
|
{/* Tímpano Frontal (Z = length) */}
|
|
<mesh position={[0, 0, length]} castShadow receiveShadow>
|
|
<shapeGeometry args={[archShape]} />
|
|
<meshStandardMaterial color={isDark ? "#334155" : "#cbd5e1"} opacity={0.7} transparent side={THREE.DoubleSide} roughness={0.5} />
|
|
</mesh>
|
|
|
|
{/* Rótulo de dimensões */}
|
|
<Text
|
|
position={[0, rise + 1.5, length / 2]}
|
|
fontSize={Math.max(0.3, Math.min(0.6, span / 20))}
|
|
color={isDark ? "#cbd5e1" : "#1a202c"}
|
|
anchorX="center"
|
|
anchorY="bottom"
|
|
>
|
|
Vão = {span}m | Compr = {length}m | Flecha = {rise}m
|
|
</Text>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
export default function Vault3DViewer({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDirection = 'perpendicular' }: Vault3DInput) {
|
|
const theme = useCanvasTheme();
|
|
const isDark = theme === 'dark';
|
|
|
|
const fallback = (
|
|
<FallbackDiagram
|
|
type="vault"
|
|
props={{ span, length, rise, cpi, cpeProfile }}
|
|
/>
|
|
);
|
|
|
|
const maxDimension = Math.max(span, length, rise);
|
|
|
|
return (
|
|
<SceneCanvas moduleId="abobada"
|
|
shadows
|
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
|
camera={{ position: [span * 1.2, rise * 1.5, length * 1.2], fov: 40 }}
|
|
fallback={fallback}
|
|
>
|
|
<ambientLight intensity={0.7} />
|
|
<directionalLight
|
|
position={[span, rise * 3, length * 1.5]}
|
|
intensity={1.2}
|
|
castShadow
|
|
shadow-mapSize-width={1024}
|
|
shadow-mapSize-height={1024}
|
|
/>
|
|
<VaultModel span={span} length={length} rise={rise} cpi={cpi} cpeProfile={cpeProfile} cpeParallel={cpeParallel} viewDirection={viewDirection} />
|
|
<Grid
|
|
infiniteGrid
|
|
fadeDistance={maxDimension * 5}
|
|
sectionColor={isDark ? "#475569" : "#94a3b8"}
|
|
cellColor={isDark ? "#1e293b" : "#cbd5e1"}
|
|
position={[0, -0.01, 0]}
|
|
/>
|
|
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.05} />
|
|
<Environment preset="city" />
|
|
</SceneCanvas>
|
|
);
|
|
} |