feat: unified 0 and 90 degree PDF envelope and category descriptors
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
import { useMemo } from 'react';
|
||||
import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
import SceneCanvas from '../SceneCanvas';
|
||||
import FallbackDiagram from '../FallbackDiagram';
|
||||
|
||||
export interface Vault3DInput {
|
||||
span: number;
|
||||
length: number;
|
||||
rise: number;
|
||||
cpi: number;
|
||||
cpeProfile: Record<string, number>;
|
||||
}
|
||||
|
||||
function vaultColor(cpe: number, cpi: number): THREE.Color {
|
||||
const p = cpe - cpi;
|
||||
const intensity = Math.min(1, Math.abs(p) / 1.5);
|
||||
if (p > 0) {
|
||||
return new THREE.Color(`hsl(${215 - intensity * 10}, ${70 + intensity * 25}%, ${Math.max(35, 60 - intensity * 25)}%)`);
|
||||
}
|
||||
return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, 60 - intensity * 20)}%)`);
|
||||
}
|
||||
|
||||
function VaultModel({ span, length, rise, cpi, cpeProfile }: Vault3DInput) {
|
||||
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]);
|
||||
|
||||
// Divide em zonas (1, 2, 3, 4, 5, 6)
|
||||
const zones = useMemo(() => {
|
||||
const arr: { cpe: number; startIdx: number; endIdx: number }[] = [];
|
||||
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}`;
|
||||
arr.push({ cpe: cpeProfile[key] ?? -0.5, startIdx, endIdx });
|
||||
}
|
||||
return arr;
|
||||
}, [cpeProfile, segments]);
|
||||
|
||||
// Shape para fechar os tímpanos (paredes frontais/traseiras em arco)
|
||||
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]);
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* Casca da Abóbada */}
|
||||
{zones.map((zone, idx) => {
|
||||
const color = vaultColor(zone.cpe, cpi);
|
||||
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);
|
||||
}
|
||||
return (
|
||||
<mesh key={idx} 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>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Tímpano Traseiro (Z = 0) */}
|
||||
<mesh position={[0, 0, 0]} castShadow receiveShadow>
|
||||
<shapeGeometry args={[archShape]} />
|
||||
<meshStandardMaterial color="#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="#cbd5e1" opacity={0.7} transparent side={THREE.DoubleSide} roughness={0.5} />
|
||||
</mesh>
|
||||
|
||||
{/* Rótulo de dimensões */}
|
||||
<Text
|
||||
position={[0, rise + 0.6, length / 2]}
|
||||
fontSize={Math.max(0.3, Math.min(0.6, span / 20))}
|
||||
color="#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 }: Vault3DInput) {
|
||||
const fallback = (
|
||||
<FallbackDiagram
|
||||
type="vault"
|
||||
props={{ span, length, rise, cpi, cpeProfile }}
|
||||
/>
|
||||
);
|
||||
|
||||
const maxDimension = Math.max(span, length, rise);
|
||||
|
||||
return (
|
||||
<SceneCanvas
|
||||
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} />
|
||||
<Grid infiniteGrid fadeDistance={maxDimension * 5} sectionColor="#94a3b8" cellColor="#cbd5e1" position={[0, -0.01, 0]} />
|
||||
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.05} />
|
||||
<Environment preset="city" />
|
||||
</SceneCanvas>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user