🚀 Auto-deploy: BrainWind atualizado em 29/07/2026 12:00:49

This commit is contained in:
2026-07-29 12:00:49 +00:00
parent fde885ca60
commit a30125c7ce
21 changed files with 932 additions and 210 deletions
+50 -77
View File
@@ -2,9 +2,11 @@ import React, { useRef } from 'react';
import * as THREE from 'three';
import { useFrame } from '@react-three/fiber';
import { Text, Grid, Environment } from '@react-three/drei';
import { WindArrow as GlobalWindArrow } from './WindArrow';
import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls';
import SceneCanvas from '../SceneCanvas';
import { useCanvasTheme } from '@/lib/theme';
import { ShelterAirflowSystem } from './ShelterAirflowSystem';
import type { ShelterCondition, OpeningPosition, ShelterWindAngle } from '@/lib/nbr-tables/table-shelters';
export interface Shelter3DProps {
@@ -19,43 +21,9 @@ export interface Shelter3DProps {
openingPos: OpeningPosition;
backRange?: [number, number];
windAngle?: ShelterWindAngle;
viewMode: '3d' | 'elevation';
viewMode: '3d' | 'elevation' | 'airflow';
}
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);
const quaternion = React.useMemo(() => {
const dir = new THREE.Vector3(...end).sub(new THREE.Vector3(...start)).normalize();
return new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
}, [start, end]);
useFrame(({ clock, invalidate }) => {
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);
invalidate();
}
});
return (
<group ref={arrowRef} quaternion={quaternion}>
<mesh>
<coneGeometry args={[0.25, 0.6, 8]} />
<meshStandardMaterial color={color} emissive={color} emissiveIntensity={0.6} />
</mesh>
</group>
);
};
function ShelterModel({
condition: _condition,
depth,
@@ -71,10 +39,13 @@ function ShelterModel({
viewMode,
}: Shelter3DProps) {
const isElevation = viewMode === 'elevation';
const theme = useCanvasTheme();
const isDark = theme === 'dark';
// Geometria básica
const halfW = width / 2;
const halfD = depth / 2;
const isAirflow = viewMode === 'airflow';
const thetaRad = (theta * Math.PI) / 180;
const slopeRise = depth * Math.tan(thetaRad);
@@ -161,50 +132,51 @@ function ShelterModel({
{/* 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]} rotation={[0, -((windAngle || 0) * Math.PI) / 180, 0]}>
{/* Vento de entrada (Barlavento) entrando em direção ao abrigo */}
<WindArrow start={[0, height * 0.5, -halfD - 4]} end={[0, height * 0.5, -halfD + 1]} color="#38bdf8" speed={1.2} />
<WindArrow start={[-halfW * 0.5, height * 0.7, -halfD - 4]} end={[-halfW * 0.5, height * 0.7, -halfD + 1]} color="#38bdf8" speed={1.4} />
<WindArrow start={[halfW * 0.5, height * 0.4, -halfD - 4]} 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 - 3]} 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' || (backRange && backRange[1] < 98)) && (
<>
<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.35, halfD]} rotation={[0, Math.PI, 0]} fontSize={0.4} color="#34d399">
Alívio (Fresta Topo)
</Text>
</>
)}
{/* Fresta na base */}
{backClosure > 0 && backClosure < 100 && (openingPos === 'bottom' || (backRange && backRange[0] > 2)) && (
<>
<WindArrow start={[0, 0.4, 0]} end={[0, 0.4, halfD + 2.5]} color="#facc15" speed={1.8} />
<Text position={[0, 1.2, halfD]} rotation={[0, Math.PI, 0]} fontSize={0.4} color="#facc15">
Vão Livre Inferior
</Text>
</>
)}
{/* Seta de Vento Global (Sempre visível para não pular de posição) */}
<group>
<GlobalWindArrow
target={[-halfW * 1.2, height * 1.5 - 1.5, -halfD - 5 + 2]}
direction={[0, 0, 1]}
scale={4}
/>
<Text
position={[-halfW * 1.2, height * 1.5, -halfD - 5]}
rotation={[0, Math.PI / 4, 0]}
fontSize={1.2}
color={isDark ? '#60a5fa' : '#3b82f6'}
anchorX="center"
anchorY="bottom"
>
Vento
</Text>
</group>
{/* Partículas de Ar */}
{isAirflow && (
<group>
<ShelterAirflowSystem
width={width}
depth={depth}
height={height}
theta={theta}
/>
</group>
)}
{/* Textos Informativos 3D - Virados para quem olha da frente (-Z) */}
<Text position={[0, height + slopeRise + 0.8, 0]} rotation={[0, Math.PI, 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.2]} rotation={[0, Math.PI, 0]} fontSize={0.35} color="#38bdf8">
{`VENTO ${windAngle === 0 ? 'FRONTAL (0°)' : windAngle === 45 ? 'OBLÍQUO (45°)' : 'LATERAL (90°)'}`}
</Text>
<Text position={[0, height + slopeRise + 0.25, 0]} rotation={[0, Math.PI, 0]} fontSize={0.32} color="#f43f5e">
Arrancamento [+Fz ]
</Text>
<Text position={[0, height * 0.5, halfD + 1.2]} rotation={[0, Math.PI, 0]} fontSize={0.32} color="#fb923c">
Empuxo no Fundo [+Fx ]
</Text>
{!isAirflow && (
<>
<Text position={[0, height + slopeRise + 1.2, 0]} rotation={[0, Math.PI, 0]} fontSize={0.45} color="#e2e8f0">
{isElevation ? 'CORTE A-A (Elevação)' : 'Abrigo / Pórtico (NBR 6123)'}
</Text>
<Text position={[0, height + slopeRise + 0.5, 0]} rotation={[0, Math.PI, 0]} fontSize={0.32} color="#f43f5e">
Arrancamento [+Fz ]
</Text>
<Text position={[0, height * 0.5, halfD + 1.2]} rotation={[0, Math.PI, 0]} fontSize={0.32} color="#fb923c">
Empuxo no Fundo [+Fx ]
</Text>
</>
)}
</group>
);
}
@@ -212,13 +184,14 @@ function ShelterModel({
export function Shelter3D(props: Shelter3DProps) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const { width, depth, height } = props;
const { width, depth, height, viewMode } = props;
const dist = Math.max(width, depth) * 1.8 + 8;
return (
<SceneCanvas
moduleId="shelter"
shadows
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
gl={{ preserveDrawingBuffer: true, antialias: true }}
camera={{ position: [dist * 0.75, height * 1.1, -dist * 0.85], fov: 45 }}
fallback={<div className="flex items-center justify-center h-full text-muted-foreground">3D indisponível</div>}