🚀 Auto-deploy: BrainWind atualizado em 27/07/2026 16:55:52
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
import React, { useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { Text, Line } from '@react-three/drei';
|
||||
import type { ShelterCondition, OpeningPosition } from '@/lib/nbr-tables/table-shelters';
|
||||
|
||||
export interface Shelter3DProps {
|
||||
condition: ShelterCondition;
|
||||
depth: number;
|
||||
width: number;
|
||||
height: number;
|
||||
theta: number;
|
||||
backClosure: number;
|
||||
leftClosure: number;
|
||||
rightClosure: number;
|
||||
openingPos: OpeningPosition;
|
||||
viewMode: '3d' | 'elevation';
|
||||
}
|
||||
|
||||
const WindArrow: React.FC<{
|
||||
start: [number, number, number];
|
||||
end: [number, number, number];
|
||||
color?: string;
|
||||
speed?: number;
|
||||
}> = ({ start, end, color = '#38bdf8', speed = 1.5 }) => {
|
||||
const arrowRef = useRef<THREE.Group>(null);
|
||||
useFrame(({ clock }) => {
|
||||
if (arrowRef.current) {
|
||||
const t = (clock.getElapsedTime() * speed) % 1;
|
||||
const x = start[0] + (end[0] - start[0]) * t;
|
||||
const y = start[1] + (end[1] - start[1]) * t;
|
||||
const z = start[2] + (end[2] - start[2]) * t;
|
||||
arrowRef.current.position.set(x, y, z);
|
||||
}
|
||||
});
|
||||
|
||||
const dir = new THREE.Vector3(...end).sub(new THREE.Vector3(...start));
|
||||
const len = dir.length();
|
||||
const angleZ = Math.atan2(dir.y, dir.x);
|
||||
|
||||
return (
|
||||
<group ref={arrowRef}>
|
||||
<mesh rotation={[0, 0, angleZ - Math.PI / 2]}>
|
||||
<coneGeometry args={[0.25, 0.6, 8]} />
|
||||
<meshStandardMaterial color={color} emissive={color} emissiveIntensity={0.6} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
export const Shelter3D: React.FC<Shelter3DProps> = ({
|
||||
condition,
|
||||
depth,
|
||||
width,
|
||||
height,
|
||||
theta,
|
||||
backClosure,
|
||||
leftClosure,
|
||||
rightClosure,
|
||||
openingPos,
|
||||
viewMode,
|
||||
}) => {
|
||||
const isElevation = viewMode === 'elevation';
|
||||
|
||||
// Geometria básica
|
||||
const halfW = width / 2;
|
||||
const halfD = depth / 2;
|
||||
const thetaRad = (theta * Math.PI) / 180;
|
||||
const slopeRise = depth * Math.tan(thetaRad);
|
||||
|
||||
// Paredes com abertura no topo ou na base
|
||||
const renderWallWithGap = (
|
||||
w: number,
|
||||
h: number,
|
||||
closure: number,
|
||||
pos: OpeningPosition,
|
||||
rotY = 0,
|
||||
position: [number, number, number] = [0, 0, 0]
|
||||
) => {
|
||||
if (closure <= 0) return null;
|
||||
|
||||
const op = closure / 100;
|
||||
const wallH = h * op;
|
||||
let yPos = position[1] + wallH / 2;
|
||||
|
||||
if (pos === 'top' && op < 1) {
|
||||
// Parede na parte de baixo, fresta no topo
|
||||
yPos = position[1] + wallH / 2;
|
||||
} else if (pos === 'bottom' && op < 1) {
|
||||
// Parede suspensa na parte superior, vão na base
|
||||
yPos = position[1] + h - wallH / 2;
|
||||
}
|
||||
|
||||
return (
|
||||
<group position={position} rotation={[0, rotY, 0]}>
|
||||
<mesh position={[0, yPos - position[1], 0]}>
|
||||
<boxGeometry args={[w, wallH, 0.15]} />
|
||||
<meshStandardMaterial
|
||||
color="#64748b"
|
||||
transparent={op < 1}
|
||||
opacity={Math.max(0.3, op)}
|
||||
roughness={0.4}
|
||||
/>
|
||||
</mesh>
|
||||
{/* Linha de contorno */}
|
||||
<lineSegments>
|
||||
<edgesGeometry args={[new THREE.BoxGeometry(w, wallH, 0.15)]} />
|
||||
<lineBasicMaterial color="#94a3b8" />
|
||||
</lineSegments>
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<group position={[0, -height / 2, 0]}>
|
||||
{/* Plano do Solo */}
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]} receiveShadow>
|
||||
<planeGeometry args={[width * 2, depth * 2]} />
|
||||
<meshStandardMaterial color="#1e293b" roughness={0.9} />
|
||||
</mesh>
|
||||
<gridHelper args={[Math.max(width, depth) * 2, 20, '#475569', '#334155']} position={[0, 0.01, 0]} />
|
||||
|
||||
{/* 1. Estrutura Metálica Principal (Pilares e Vigas em Balanço - Tubos Quadrados) */}
|
||||
{[-halfW + 0.6, halfW - 0.6].map((xPos, idx) => (
|
||||
<group key={`frame-${idx}`} position={[xPos, 0, 0]}>
|
||||
{/* Pilar traseiro (lado direito na elevação) */}
|
||||
<mesh position={[0, height / 2, halfD - 0.25]}>
|
||||
<boxGeometry args={[0.35, height, 0.35]} />
|
||||
<meshStandardMaterial color="#cbd5e1" metalness={0.7} roughness={0.3} />
|
||||
</mesh>
|
||||
{/* Viga horizontal em balanço (TUBO QUAD. 150x150) */}
|
||||
<group position={[0, height, 0]} rotation={[thetaRad, 0, 0]}>
|
||||
<mesh position={[0, 0, 0]}>
|
||||
<boxGeometry args={[0.25, 0.3, depth]} />
|
||||
<meshStandardMaterial color="#cbd5e1" metalness={0.7} roughness={0.3} />
|
||||
</mesh>
|
||||
</group>
|
||||
</group>
|
||||
))}
|
||||
|
||||
{/* 2. Cobertura / Marquise */}
|
||||
<group position={[0, height, 0]} rotation={[thetaRad, 0, 0]}>
|
||||
<mesh position={[0, 0.15, 0]}>
|
||||
<boxGeometry args={[width + 0.6, 0.15, depth + 0.4]} />
|
||||
<meshStandardMaterial color="#3b82f6" roughness={0.3} metalness={0.4} />
|
||||
</mesh>
|
||||
<lineSegments position={[0, 0.15, 0]}>
|
||||
<edgesGeometry args={[new THREE.BoxGeometry(width + 0.6, 0.15, depth + 0.4)]} />
|
||||
<lineBasicMaterial color="#60a5fa" />
|
||||
</lineSegments>
|
||||
</group>
|
||||
|
||||
{/* 3. Fechamentos de Fundo e Laterais (com suporte a fresta/abertura) */}
|
||||
{/* Parede de Fundo (Traseira Z = +halfD) */}
|
||||
{renderWallWithGap(width, height, backClosure, openingPos, 0, [0, 0, halfD])}
|
||||
|
||||
{/* Parede Lateral Esquerda (X = -halfW) */}
|
||||
{renderWallWithGap(depth, height, leftClosure, 'distributed', Math.PI / 2, [-halfW, 0, 0])}
|
||||
|
||||
{/* Parede Lateral Direita (X = +halfW) */}
|
||||
{renderWallWithGap(depth, height, rightClosure, 'distributed', Math.PI / 2, [halfW, 0, 0])}
|
||||
|
||||
{/* 4. Linhas de Corrente e Vetores de Vento (Ilustrativo e Didático) */}
|
||||
<group position={[0, 0, 0]}>
|
||||
{/* Vento de entrada (Barlavento) */}
|
||||
<WindArrow start={[0, height * 0.5, -halfD - 3]} end={[0, height * 0.5, -halfD + 1]} color="#38bdf8" speed={1.2} />
|
||||
<WindArrow start={[-halfW * 0.5, height * 0.7, -halfD - 3]} end={[-halfW * 0.5, height * 0.7, -halfD + 1]} color="#38bdf8" speed={1.4} />
|
||||
<WindArrow start={[halfW * 0.5, height * 0.4, -halfD - 3]} end={[halfW * 0.5, height * 0.4, -halfD + 1]} color="#38bdf8" speed={1.3} />
|
||||
|
||||
{/* Linhas de corrente sobre o telhado */}
|
||||
<WindArrow start={[0, height + 1, -halfD - 2]} end={[0, height + slopeRise + 1.5, halfD + 2]} color="#60a5fa" speed={1.6} />
|
||||
|
||||
{/* Fresta na parede de fundo: se houver abertura no topo, mostrar vento escapando pela fresta superior! */}
|
||||
{backClosure > 0 && backClosure < 100 && openingPos === 'top' && (
|
||||
<>
|
||||
<WindArrow start={[0, height * 0.85, 0]} end={[0, height * 0.95, halfD + 2.5]} color="#34d399" speed={1.8} />
|
||||
<Text position={[0, height + 0.3, halfD]} fontSize={0.4} color="#34d399">
|
||||
Alívio (Fresta Topo)
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Fresta na base */}
|
||||
{backClosure > 0 && backClosure < 100 && openingPos === 'bottom' && (
|
||||
<>
|
||||
<WindArrow start={[0, 0.4, 0]} end={[0, 0.4, halfD + 2.5]} color="#facc15" speed={1.8} />
|
||||
<Text position={[0, 1.2, halfD]} fontSize={0.4} color="#facc15">
|
||||
Vão Livre Inferior
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</group>
|
||||
|
||||
{/* Textos Informativos 3D */}
|
||||
<Text position={[0, height + slopeRise + 0.8, 0]} fontSize={0.45} color="#e2e8f0">
|
||||
{isElevation ? 'CORTE A-A (Elevação)' : 'Abrigo / Pórtico (NBR 6123)'}
|
||||
</Text>
|
||||
<Text position={[0, 0.3, -halfD - 1]} fontSize={0.35} color="#38bdf8">
|
||||
VENTO FRONTAL
|
||||
</Text>
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
export default Shelter3D;
|
||||
Reference in New Issue
Block a user