🚀 Auto-deploy: BrainWind atualizado em 27/07/2026 18:39:28

This commit is contained in:
2026-07-27 18:39:28 +00:00
parent 71ce942faa
commit 812a180bd8
8 changed files with 535 additions and 286 deletions
+77 -48
View File
@@ -1,7 +1,10 @@
import React, { useRef } from 'react';
import * as THREE from 'three';
import { useFrame } from '@react-three/fiber';
import { Text, Line } from '@react-three/drei';
import { Text, Grid, Environment } from '@react-three/drei';
import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls';
import SceneCanvas from '../SceneCanvas';
import { useCanvasTheme } from '@/lib/theme';
import type { ShelterCondition, OpeningPosition } from '@/lib/nbr-tables/table-shelters';
export interface Shelter3DProps {
@@ -14,6 +17,7 @@ export interface Shelter3DProps {
leftClosure: number;
rightClosure: number;
openingPos: OpeningPosition;
backRange?: [number, number];
viewMode: '3d' | 'elevation';
}
@@ -24,23 +28,26 @@ const WindArrow: React.FC<{
speed?: number;
}> = ({ start, end, color = '#38bdf8', speed = 1.5 }) => {
const arrowRef = useRef<THREE.Group>(null);
useFrame(({ clock }) => {
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();
}
});
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]}>
<group ref={arrowRef} quaternion={quaternion}>
<mesh>
<coneGeometry args={[0.25, 0.6, 8]} />
<meshStandardMaterial color={color} emissive={color} emissiveIntensity={0.6} />
</mesh>
@@ -48,8 +55,8 @@ const WindArrow: React.FC<{
);
};
export const Shelter3D: React.FC<Shelter3DProps> = ({
condition,
function ShelterModel({
condition: _condition,
depth,
width,
height,
@@ -58,8 +65,9 @@ export const Shelter3D: React.FC<Shelter3DProps> = ({
leftClosure,
rightClosure,
openingPos,
backRange,
viewMode,
}) => {
}: Shelter3DProps) {
const isElevation = viewMode === 'elevation';
// Geometria básica
@@ -68,25 +76,32 @@ export const Shelter3D: React.FC<Shelter3DProps> = ({
const thetaRad = (theta * Math.PI) / 180;
const slopeRise = depth * Math.tan(thetaRad);
// Paredes com abertura no topo ou na base
// Paredes com abertura no topo ou na base (sem caixas/linhas brancas deslocadas)
const renderWallWithGap = (
w: number,
h: number,
closure: number,
pos: OpeningPosition,
rotY = 0,
position: [number, number, number] = [0, 0, 0]
position: [number, number, number] = [0, 0, 0],
customRange?: [number, number]
) => {
if (closure <= 0) return null;
const op = closure / 100;
const wallH = h * op;
let op = closure / 100;
let wallH = h * op;
let yPos = position[1] + wallH / 2;
if (pos === 'top' && op < 1) {
if (customRange && closure < 100) {
const bottomPct = Math.max(0, Math.min(100, customRange[0])) / 100;
const topPct = Math.max(0, Math.min(100, customRange[1])) / 100;
op = Math.max(0.01, topPct - bottomPct);
wallH = h * op;
yPos = position[1] + h * bottomPct + wallH / 2;
} else if (pos === 'top' && closure < 100) {
// Parede na parte de baixo, fresta no topo
yPos = position[1] + wallH / 2;
} else if (pos === 'bottom' && op < 1) {
} else if (pos === 'bottom' && closure < 100) {
// Parede suspensa na parte superior, vão na base
yPos = position[1] + h - wallH / 2;
}
@@ -102,24 +117,12 @@ export const Shelter3D: React.FC<Shelter3DProps> = ({
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]}>
@@ -144,15 +147,11 @@ export const Shelter3D: React.FC<Shelter3DProps> = ({
<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])}
{renderWallWithGap(width, height, backClosure, openingPos, 0, [0, 0, halfD], backRange)}
{/* Parede Lateral Esquerda (X = -halfW) */}
{renderWallWithGap(depth, height, leftClosure, 'distributed', Math.PI / 2, [-halfW, 0, 0])}
@@ -162,44 +161,74 @@ export const Shelter3D: React.FC<Shelter3DProps> = ({
{/* 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} />
{/* Vento de entrada (Barlavento) entrando pela frente 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 - 2]} end={[0, height + slopeRise + 1.5, halfD + 2]} color="#60a5fa" speed={1.6} />
<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' && (
{/* 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.3, halfD]} fontSize={0.4} color="#34d399">
<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' && (
{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]} fontSize={0.4} color="#facc15">
<Text position={[0, 1.2, halfD]} rotation={[0, Math.PI, 0]} 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">
{/* 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]} fontSize={0.35} color="#38bdf8">
<Text position={[0, 0.3, -halfD - 1.2]} rotation={[0, Math.PI, 0]} fontSize={0.35} color="#38bdf8">
VENTO FRONTAL
</Text>
</group>
);
};
}
export function Shelter3D(props: Shelter3DProps) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const { width, depth, height } = props;
const dist = Math.max(width, depth) * 1.8 + 8;
return (
<SceneCanvas
moduleId="shelter"
shadows
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>}
>
<ambientLight intensity={isDark ? 0.4 : 0.7} />
<directionalLight position={[10, 20, 15]} intensity={isDark ? 1.0 : 1.3} castShadow />
<ShelterModel {...props} />
<Grid
infiniteGrid
fadeDistance={Math.max(width, depth) * 3}
sectionColor={isDark ? '#475569' : '#94a3b8'}
cellColor={isDark ? '#1e293b' : '#cbd5e1'}
position={[0, -height / 2 - 0.01, 0]}
/>
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.05} target={[0, 0, 0]} />
<Environment preset="city" />
</SceneCanvas>
);
}
export default Shelter3D;