392 lines
16 KiB
TypeScript
392 lines
16 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
|
|
import * as THREE from 'three';
|
|
import { useGalpaoStore } from '../store/galpaoStore';
|
|
import { useWindStore } from '../store/appStore';
|
|
import SceneCanvas from './SceneCanvas';
|
|
import FallbackDiagram from './FallbackDiagram';
|
|
|
|
function pressureColor(cpe: number, cpi: number): THREE.Color {
|
|
const p = cpe - cpi;
|
|
const intensity = Math.min(1, Math.abs(p) / 1.2);
|
|
if (p > 0) {
|
|
const h = 215 - intensity * 10;
|
|
const s = 70 + intensity * 25;
|
|
const l = Math.max(35, 65 - intensity * 25);
|
|
return new THREE.Color(`hsl(${h}, ${s}%, ${l}%)`);
|
|
}
|
|
const h = 0;
|
|
const s = 70 + intensity * 25;
|
|
const l = Math.max(40, 65 - intensity * 20);
|
|
return new THREE.Color(`hsl(${h}, ${s}%, ${l}%)`);
|
|
}
|
|
|
|
function PressureArrow({
|
|
center,
|
|
normal,
|
|
p,
|
|
}: {
|
|
center: [number, number, number];
|
|
normal: [number, number, number];
|
|
p: number;
|
|
}) {
|
|
const { q } = useWindStore();
|
|
const force = p * q; // kN/m2
|
|
|
|
const normVec = useMemo(() => new THREE.Vector3(...normal).normalize(), [normal]);
|
|
const centerVec = useMemo(() => new THREE.Vector3(...center), [center]);
|
|
const isPressure = p > 0;
|
|
const dir = useMemo(() => isPressure ? normVec.clone().negate() : normVec.clone(), [isPressure, normVec]);
|
|
|
|
const quat = useMemo(() => {
|
|
const q = new THREE.Quaternion();
|
|
q.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
|
return new THREE.Euler().setFromQuaternion(q);
|
|
}, [dir]);
|
|
|
|
if (Math.abs(force) < 0.05) return null;
|
|
|
|
const length = Math.max(0.6, Math.min(3.0, Math.abs(force) * 1.5));
|
|
const color = isPressure ? '#3b82f6' : '#ef4444';
|
|
|
|
const start = isPressure ? centerVec.clone().sub(dir.clone().multiplyScalar(length)) : centerVec;
|
|
const end = isPressure ? centerVec : centerVec.clone().add(dir.clone().multiplyScalar(length));
|
|
const mid = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5);
|
|
|
|
const headLen = Math.min(0.4, length * 0.4);
|
|
|
|
return (
|
|
<group>
|
|
{length - headLen > 0 && (
|
|
<mesh position={mid.toArray()} rotation={[quat.x, quat.y, quat.z]}>
|
|
<cylinderGeometry args={[0.08, 0.08, length - headLen, 8]} />
|
|
<meshStandardMaterial color={color} />
|
|
</mesh>
|
|
)}
|
|
<mesh position={end.toArray()} rotation={[quat.x, quat.y, quat.z]}>
|
|
<coneGeometry args={[0.2, headLen, 8]} />
|
|
<meshStandardMaterial color={color} />
|
|
</mesh>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
export function WarehouseModel() {
|
|
const { width, length, height, roofPitch, wallCpe, roofCpe } = useGalpaoStore();
|
|
const { windAngle, cpi } = useWindStore();
|
|
|
|
const roofHeight = (width / 2) * Math.tan((roofPitch * Math.PI) / 180);
|
|
const theta = (roofPitch * Math.PI) / 180;
|
|
const widthSlope = width / 2 / Math.cos(theta);
|
|
|
|
const wallAColor = useMemo(() => pressureColor(wallCpe.A, cpi), [wallCpe.A, cpi]);
|
|
const wallBColor = useMemo(() => pressureColor(wallCpe.B, cpi), [wallCpe.B, cpi]);
|
|
const wallCColor = useMemo(() => pressureColor(wallCpe.C, cpi), [wallCpe.C, cpi]);
|
|
const wallDColor = useMemo(() => pressureColor(wallCpe.D, cpi), [wallCpe.D, cpi]);
|
|
const roofEColor = useMemo(() => pressureColor(roofCpe.E, cpi), [roofCpe.E, cpi]);
|
|
const roofFColor = useMemo(() => pressureColor(roofCpe.F, cpi), [roofCpe.F, cpi]);
|
|
const roofGColor = useMemo(() => pressureColor(roofCpe.G, cpi), [roofCpe.G, cpi]);
|
|
const roofHColor = useMemo(() => pressureColor(roofCpe.H, cpi), [roofCpe.H, cpi]);
|
|
|
|
const wallThickness = 0.15;
|
|
const isParallel = windAngle === 90;
|
|
|
|
// Shapes para os oitões (gables)
|
|
const leftShape = useMemo(() => {
|
|
const s = new THREE.Shape();
|
|
s.moveTo(-width / 2, 0);
|
|
s.lineTo(0, roofHeight);
|
|
s.lineTo(0, 0);
|
|
s.closePath();
|
|
return s;
|
|
}, [width, roofHeight]);
|
|
|
|
const rightShape = useMemo(() => {
|
|
const s = new THREE.Shape();
|
|
s.moveTo(0, 0);
|
|
s.lineTo(0, roofHeight);
|
|
s.lineTo(width / 2, 0);
|
|
s.closePath();
|
|
return s;
|
|
}, [width, roofHeight]);
|
|
|
|
return (
|
|
<group>
|
|
{/* === PAREDES === */}
|
|
{/* Lateral Esquerda (X = -width/2) */}
|
|
<group position={[-width / 2, height / 2, 0]}>
|
|
<mesh position={[0, 0, -length / 4]} castShadow receiveShadow>
|
|
<boxGeometry args={[wallThickness, height, length / 2]} />
|
|
<meshStandardMaterial color={isParallel ? wallCColor : wallAColor} roughness={0.4} />
|
|
</mesh>
|
|
<PressureArrow center={[0, 0, -length / 4]} normal={[-1, 0, 0]} p={(isParallel ? wallCpe.C : wallCpe.A) - cpi} />
|
|
|
|
<mesh position={[0, 0, length / 4]} castShadow receiveShadow>
|
|
<boxGeometry args={[wallThickness, height, length / 2]} />
|
|
<meshStandardMaterial color={isParallel ? wallDColor : wallAColor} roughness={0.4} />
|
|
</mesh>
|
|
<PressureArrow center={[0, 0, length / 4]} normal={[-1, 0, 0]} p={(isParallel ? wallCpe.D : wallCpe.A) - cpi} />
|
|
</group>
|
|
|
|
{/* Lateral Direita (X = width/2) */}
|
|
<group position={[width / 2, height / 2, 0]}>
|
|
<mesh position={[0, 0, -length / 4]} castShadow receiveShadow>
|
|
<boxGeometry args={[wallThickness, height, length / 2]} />
|
|
<meshStandardMaterial color={isParallel ? wallCColor : wallBColor} roughness={0.4} />
|
|
</mesh>
|
|
<PressureArrow center={[0, 0, -length / 4]} normal={[1, 0, 0]} p={(isParallel ? wallCpe.C : wallCpe.B) - cpi} />
|
|
|
|
<mesh position={[0, 0, length / 4]} castShadow receiveShadow>
|
|
<boxGeometry args={[wallThickness, height, length / 2]} />
|
|
<meshStandardMaterial color={isParallel ? wallDColor : wallBColor} roughness={0.4} />
|
|
</mesh>
|
|
<PressureArrow center={[0, 0, length / 4]} normal={[1, 0, 0]} p={(isParallel ? wallCpe.D : wallCpe.B) - cpi} />
|
|
</group>
|
|
|
|
{/* Parede Traseira (Z = -length/2) */}
|
|
<group position={[0, height / 2, -length / 2]}>
|
|
<mesh position={[-width / 4, 0, 0]} castShadow receiveShadow>
|
|
<boxGeometry args={[width / 2, height, wallThickness]} />
|
|
<meshStandardMaterial color={isParallel ? wallAColor : wallCColor} roughness={0.4} />
|
|
</mesh>
|
|
<PressureArrow center={[-width / 4, 0, 0]} normal={[0, 0, -1]} p={(isParallel ? wallCpe.A : wallCpe.C) - cpi} />
|
|
|
|
<mesh position={[width / 4, 0, 0]} castShadow receiveShadow>
|
|
<boxGeometry args={[width / 2, height, wallThickness]} />
|
|
<meshStandardMaterial color={isParallel ? wallAColor : wallDColor} roughness={0.4} />
|
|
</mesh>
|
|
<PressureArrow center={[width / 4, 0, 0]} normal={[0, 0, -1]} p={(isParallel ? wallCpe.A : wallCpe.D) - cpi} />
|
|
</group>
|
|
|
|
{/* Parede Frontal (Z = length/2) */}
|
|
<group position={[0, height / 2, length / 2]}>
|
|
<mesh position={[-width / 4, 0, 0]} castShadow receiveShadow>
|
|
<boxGeometry args={[width / 2, height, wallThickness]} />
|
|
<meshStandardMaterial color={isParallel ? wallBColor : wallCColor} roughness={0.4} />
|
|
</mesh>
|
|
<PressureArrow center={[-width / 4, 0, 0]} normal={[0, 0, 1]} p={(isParallel ? wallCpe.B : wallCpe.C) - cpi} />
|
|
|
|
<mesh position={[width / 4, 0, 0]} castShadow receiveShadow>
|
|
<boxGeometry args={[width / 2, height, wallThickness]} />
|
|
<meshStandardMaterial color={isParallel ? wallBColor : wallDColor} roughness={0.4} />
|
|
</mesh>
|
|
<PressureArrow center={[width / 4, 0, 0]} normal={[0, 0, 1]} p={(isParallel ? wallCpe.B : wallCpe.D) - cpi} />
|
|
</group>
|
|
|
|
{/* === OITÕES (GABLES) === */}
|
|
{/* Oitão Frontal (Z = length/2) */}
|
|
<group position={[0, height, length / 2]}>
|
|
<mesh>
|
|
<shapeGeometry args={[leftShape]} />
|
|
<meshStandardMaterial color={isParallel ? wallBColor : wallCColor} side={THREE.DoubleSide} />
|
|
</mesh>
|
|
<mesh>
|
|
<shapeGeometry args={[rightShape]} />
|
|
<meshStandardMaterial color={isParallel ? wallBColor : wallDColor} side={THREE.DoubleSide} />
|
|
</mesh>
|
|
</group>
|
|
|
|
{/* Oitão Traseiro (Z = -length/2) */}
|
|
<group position={[0, height, -length / 2]} rotation={[0, Math.PI, 0]}>
|
|
<mesh>
|
|
<shapeGeometry args={[leftShape]} />
|
|
<meshStandardMaterial color={isParallel ? wallAColor : wallDColor} side={THREE.DoubleSide} />
|
|
</mesh>
|
|
<mesh>
|
|
<shapeGeometry args={[rightShape]} />
|
|
<meshStandardMaterial color={isParallel ? wallAColor : wallCColor} side={THREE.DoubleSide} />
|
|
</mesh>
|
|
</group>
|
|
|
|
{/* === TELHADO (DUAS ÁGUAS) === */}
|
|
{/* Água Esquerda (X < 0) */}
|
|
<group position={[-width / 4, height + roofHeight / 2, 0]} rotation={[0, 0, theta]}>
|
|
{/* Seg 1 (Traseiro-Fim) */}
|
|
<mesh position={[0, 0, -3 * length / 8]} castShadow receiveShadow>
|
|
<boxGeometry args={[widthSlope, 0.08, length / 4]} />
|
|
<meshStandardMaterial color={isParallel ? roofEColor : roofEColor} roughness={0.4} />
|
|
</mesh>
|
|
<PressureArrow center={[0, 0.04, -3 * length / 8]} normal={[0, 1, 0]} p={roofCpe.E - cpi} />
|
|
|
|
{/* Seg 2 (Traseiro-Meio) */}
|
|
<mesh position={[0, 0, -length / 8]} castShadow receiveShadow>
|
|
<boxGeometry args={[widthSlope, 0.08, length / 4]} />
|
|
<meshStandardMaterial color={isParallel ? roofFColor : roofFColor} roughness={0.4} />
|
|
</mesh>
|
|
<PressureArrow center={[0, 0.04, -length / 8]} normal={[0, 1, 0]} p={roofCpe.F - cpi} />
|
|
|
|
{/* Seg 3 (Frontal-Meio) */}
|
|
<mesh position={[0, 0, length / 8]} castShadow receiveShadow>
|
|
<boxGeometry args={[widthSlope, 0.08, length / 4]} />
|
|
<meshStandardMaterial color={isParallel ? roofGColor : roofFColor} roughness={0.4} />
|
|
</mesh>
|
|
<PressureArrow center={[0, 0.04, length / 8]} normal={[0, 1, 0]} p={(isParallel ? roofCpe.G : roofCpe.F) - cpi} />
|
|
|
|
{/* Seg 4 (Frontal-Fim) */}
|
|
<mesh position={[0, 0, 3 * length / 8]} castShadow receiveShadow>
|
|
<boxGeometry args={[widthSlope, 0.08, length / 4]} />
|
|
<meshStandardMaterial color={isParallel ? roofHColor : roofEColor} roughness={0.4} />
|
|
</mesh>
|
|
<PressureArrow center={[0, 0.04, 3 * length / 8]} normal={[0, 1, 0]} p={(isParallel ? roofCpe.H : roofCpe.E) - cpi} />
|
|
|
|
<Text
|
|
position={[0, 0.6, 0]}
|
|
rotation={[-Math.PI / 2, 0, 0]}
|
|
fontSize={Math.max(0.3, Math.min(0.6, width / 20))}
|
|
color="#ffffff"
|
|
anchorX="center"
|
|
anchorY="middle"
|
|
>
|
|
{isParallel ? 'Telhado (Zonas E/F/G/H)' : 'Telhado E/F (Barlavento)'}
|
|
</Text>
|
|
</group>
|
|
|
|
{/* Água Direita (X > 0) */}
|
|
<group position={[width / 4, height + roofHeight / 2, 0]} rotation={[0, 0, -theta]}>
|
|
{/* Seg 1 (Traseiro-Fim) */}
|
|
<mesh position={[0, 0, -3 * length / 8]} castShadow receiveShadow>
|
|
<boxGeometry args={[widthSlope, 0.08, length / 4]} />
|
|
<meshStandardMaterial color={isParallel ? roofEColor : roofGColor} roughness={0.4} />
|
|
</mesh>
|
|
<PressureArrow center={[0, 0.04, -3 * length / 8]} normal={[0, 1, 0]} p={(isParallel ? roofCpe.E : roofCpe.G) - cpi} />
|
|
|
|
{/* Seg 2 (Traseiro-Meio) */}
|
|
<mesh position={[0, 0, -length / 8]} castShadow receiveShadow>
|
|
<boxGeometry args={[widthSlope, 0.08, length / 4]} />
|
|
<meshStandardMaterial color={isParallel ? roofFColor : roofHColor} roughness={0.4} />
|
|
</mesh>
|
|
<PressureArrow center={[0, 0.04, -length / 8]} normal={[0, 1, 0]} p={(isParallel ? roofCpe.F : roofCpe.H) - cpi} />
|
|
|
|
{/* Seg 3 (Frontal-Meio) */}
|
|
<mesh position={[0, 0, length / 8]} castShadow receiveShadow>
|
|
<boxGeometry args={[widthSlope, 0.08, length / 4]} />
|
|
<meshStandardMaterial color={isParallel ? roofGColor : roofHColor} roughness={0.4} />
|
|
</mesh>
|
|
<PressureArrow center={[0, 0.04, length / 8]} normal={[0, 1, 0]} p={(isParallel ? roofCpe.G : roofCpe.H) - cpi} />
|
|
|
|
{/* Seg 4 (Frontal-Fim) */}
|
|
<mesh position={[0, 0, 3 * length / 8]} castShadow receiveShadow>
|
|
<boxGeometry args={[widthSlope, 0.08, length / 4]} />
|
|
<meshStandardMaterial color={isParallel ? roofHColor : roofGColor} roughness={0.4} />
|
|
</mesh>
|
|
<PressureArrow center={[0, 0.04, 3 * length / 8]} normal={[0, 1, 0]} p={(isParallel ? roofCpe.H : roofCpe.G) - cpi} />
|
|
|
|
<Text
|
|
position={[0, 0.6, 0]}
|
|
rotation={[-Math.PI / 2, 0, 0]}
|
|
fontSize={Math.max(0.3, Math.min(0.6, width / 20))}
|
|
color="#ffffff"
|
|
anchorX="center"
|
|
anchorY="middle"
|
|
>
|
|
{isParallel ? 'Telhado (Zonas E/F/G/H)' : 'Telhado G/H (Sotavento)'}
|
|
</Text>
|
|
</group>
|
|
|
|
{/* Cumeeira */}
|
|
<mesh position={[0, height + roofHeight + 0.02, 0]}>
|
|
<boxGeometry args={[0.08, 0.04, length + 0.1]} />
|
|
<meshStandardMaterial color="#2d3748" roughness={0.5} />
|
|
</mesh>
|
|
|
|
{/* Bordas Laterais do Telhado (Beirais) */}
|
|
<mesh position={[width / 2 + 0.02, height, 0]}>
|
|
<boxGeometry args={[0.04, 0.08, length]} />
|
|
<meshStandardMaterial color="#2d3748" />
|
|
</mesh>
|
|
<mesh position={[-width / 2 - 0.02, height, 0]}>
|
|
<boxGeometry args={[0.04, 0.08, length]} />
|
|
<meshStandardMaterial color="#2d3748" />
|
|
</mesh>
|
|
|
|
{/* === RÓTULOS 3D === */}
|
|
{/* Rótulo Parede Frontal */}
|
|
<Text
|
|
position={[0, height / 2, length / 2 + 1.0]}
|
|
fontSize={Math.max(0.3, Math.min(0.6, width / 20))}
|
|
color="#1a202c"
|
|
anchorX="center"
|
|
anchorY="middle"
|
|
>
|
|
{isParallel ? 'Parede B (Sotavento)' : 'Parede C (Barlavento)'}
|
|
</Text>
|
|
|
|
{/* Rótulo Parede Traseira */}
|
|
<Text
|
|
position={[0, height / 2, -length / 2 - 1.0]}
|
|
rotation={[0, Math.PI, 0]}
|
|
fontSize={Math.max(0.3, Math.min(0.6, width / 20))}
|
|
color="#1a202c"
|
|
anchorX="center"
|
|
anchorY="middle"
|
|
>
|
|
{isParallel ? 'Parede A (Barlavento)' : 'Parede D (Sotavento)'}
|
|
</Text>
|
|
|
|
{/* Rótulo Parede Esquerda */}
|
|
<Text
|
|
position={[-width / 2 - 1.0, height / 2, 0]}
|
|
rotation={[0, -Math.PI / 2, 0]}
|
|
fontSize={Math.max(0.3, Math.min(0.6, length / 20))}
|
|
color="#1a202c"
|
|
anchorX="center"
|
|
anchorY="middle"
|
|
>
|
|
{isParallel ? 'Parede C (Lateral)' : 'Parede A (Lateral)'}
|
|
</Text>
|
|
|
|
{/* Rótulo Parede Direita */}
|
|
<Text
|
|
position={[width / 2 + 1.0, height / 2, 0]}
|
|
rotation={[0, Math.PI / 2, 0]}
|
|
fontSize={Math.max(0.3, Math.min(0.6, length / 20))}
|
|
color="#1a202c"
|
|
anchorX="center"
|
|
anchorY="middle"
|
|
>
|
|
{isParallel ? 'Parede D (Lateral)' : 'Parede B (Lateral)'}
|
|
</Text>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
export default function Warehouse3DViewer() {
|
|
const { width, length, height, roofPitch, wallCpe, roofCpe } = useGalpaoStore();
|
|
const { windAngle, cpi } = useWindStore();
|
|
|
|
const fallback = (
|
|
<FallbackDiagram
|
|
type="warehouse"
|
|
props={{ width, length, height, roofPitch, wallCpe, roofCpe, windAngle, cpi }}
|
|
/>
|
|
);
|
|
|
|
const maxDimension = Math.max(width, length, height);
|
|
|
|
return (
|
|
<SceneCanvas
|
|
shadows
|
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
|
camera={{ position: [width * 1.3, height * 1.5, length * 1.3], fov: 40 }}
|
|
fallback={fallback}
|
|
>
|
|
<ambientLight intensity={0.7} />
|
|
<directionalLight
|
|
position={[width * 1.5, height * 3, length * 1.5]}
|
|
intensity={1.2}
|
|
castShadow
|
|
shadow-mapSize-width={1024}
|
|
shadow-mapSize-height={1024}
|
|
shadow-camera-far={maxDimension * 10}
|
|
shadow-camera-left={-maxDimension}
|
|
shadow-camera-right={maxDimension}
|
|
shadow-camera-top={maxDimension}
|
|
shadow-camera-bottom={-maxDimension}
|
|
/>
|
|
<WarehouseModel />
|
|
<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>
|
|
);
|
|
} |