🚀 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
+1 -1
View File
@@ -400,7 +400,7 @@ function TowerDiagram({ baseWidth, height, panels, phi, forceKN }: TowerProps) {
} }
interface IsolatedRoofProps { interface IsolatedRoofProps {
type: 'shed' | 'gable'; type: 'shed' | 'gable' | 'butterfly';
theta: number; theta: number;
height: number; height: number;
width: number; width: number;
+5 -5
View File
@@ -132,9 +132,9 @@ export function AirflowSystem({ windAngle, width, length, height, permeabilityCa
if (inY && inX && inZ) { if (inY && inX && inZ) {
// Perto das paredes // Perto das paredes
if (Math.abs(pz - (-halfL)) < margin) localCpe = wallCpe.A; if (Math.abs(pz - (-halfL)) < margin) localCpe = wallCpe.A;
else if (Math.abs(pz - (halfL)) < margin) localCpe = wallCpe.B; else if (Math.abs(pz - (halfL)) < margin) localCpe = (windAngle === 90 ? 0.5 : wallCpe.B);
else if (Math.abs(px - (-halfW)) < margin) localCpe = wallCpe.C; else if (Math.abs(px - (-halfW)) < margin) localCpe = wallCpe.C;
else if (Math.abs(px - (halfW)) < margin) localCpe = wallCpe.D; else if (Math.abs(px - (halfW)) < margin) localCpe = (windAngle === 0 ? 0.5 : wallCpe.D);
else localCpe = cpi; else localCpe = cpi;
// Colisão dura (Impede entrar na parede sólida) // Colisão dura (Impede entrar na parede sólida)
@@ -195,12 +195,12 @@ export function AirflowSystem({ windAngle, width, length, height, permeabilityCa
meshRef.current!.setMatrixAt(i, dummy.matrix); meshRef.current!.setMatrixAt(i, dummy.matrix);
if (p.currentCpe > 0) { if (p.currentCpe > 0) {
colorObj.set(isDark ? '#ef4444' : '#dc2626').lerp(new THREE.Color(isDark ? '#fcd34d' : '#f59e0b'), 1 - Math.min(1, p.currentCpe)); colorObj.set(isDark ? '#f87171' : '#ef4444'); // Vermelho (Turbulento/Pressão)
} else if (p.currentCpe < 0) { } else if (p.currentCpe < 0) {
const intensity = Math.min(1, Math.abs(p.currentCpe)); const intensity = Math.min(1, Math.abs(p.currentCpe));
colorObj.set(isDark ? '#7dd3fc' : '#38bdf8').lerp(new THREE.Color(isDark ? '#1e40af' : '#1d4ed8'), intensity); colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção)
} else { } else {
colorObj.set(isDark ? '#f1f5f9' : '#64748b'); colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre)
} }
meshRef.current!.setColorAt(i, colorObj); meshRef.current!.setColorAt(i, colorObj);
}); });
+56 -32
View File
@@ -6,6 +6,7 @@ import SceneCanvas from '../SceneCanvas';
import FallbackDiagram from '../FallbackDiagram'; import FallbackDiagram from '../FallbackDiagram';
import { WindArrow } from './WindArrow'; import { WindArrow } from './WindArrow';
import { useCanvasTheme } from '@/lib/theme'; import { useCanvasTheme } from '@/lib/theme';
import { BridgeAirflowSystem } from './BridgeAirflowSystem';
export interface Bridge3DInput { export interface Bridge3DInput {
/** Maior vão Lₚ (m) */ /** Maior vão Lₚ (m) */
@@ -26,6 +27,8 @@ export interface Bridge3DInput {
fzPerLength: number; fzPerLength: number;
/** Ângulo de ataque do vento (graus) */ /** Ângulo de ataque do vento (graus) */
alpha: number; alpha: number;
/** Modo de visualização */
viewMode?: 'solid' | 'airflow';
} }
function BridgeModel({ function BridgeModel({
@@ -34,10 +37,15 @@ function BridgeModel({
deckHeight, deckHeight,
heg, heg,
cx, cx,
cz,
fxPerLength,
fzPerLength,
alpha, alpha,
viewMode = 'solid',
}: Bridge3DInput) { }: Bridge3DInput) {
const theme = useCanvasTheme(); const theme = useCanvasTheme();
const isDark = theme === 'dark'; const isDark = theme === 'dark';
const isAirflow = viewMode === 'airflow';
const halfL = lp / 2; const halfL = lp / 2;
const halfW = width / 2; const halfW = width / 2;
@@ -46,33 +54,34 @@ function BridgeModel({
// Cor do tabuleiro baseada em Cx // Cor do tabuleiro baseada em Cx
const deckColor = useMemo(() => { const deckColor = useMemo(() => {
if (isAirflow) return new THREE.Color(isDark ? '#334155' : '#e2e8f0');
const intensity = Math.min(1, Math.abs(cx) / 3); const intensity = Math.min(1, Math.abs(cx) / 3);
const hue = 200 - intensity * 60; const hue = 200 - intensity * 60;
const l = isDark ? (60 - intensity * 8) : (50 - intensity * 8); const l = isDark ? (60 - intensity * 8) : (50 - intensity * 8);
return new THREE.Color(`hsl(${hue}, ${55 + intensity * 30}%, ${l}%)`); return new THREE.Color(`hsl(${hue}, ${55 + intensity * 30}%, ${l}%)`);
}, [cx, isDark]); }, [cx, isDark, isAirflow]);
// Pilar heights: posicionar 3 pilares ao longo do vão // Pilar heights: posicionar 3 pilares ao longo do vão
const pillarHeights = useMemo(() => [deckY - 0.5, deckY - 0.5, deckY - 0.5], [deckY]); const pillarHeights = useMemo(() => [deckY - 0.5, deckY - 0.5, deckY - 0.5], [deckY]);
const barrierColor = isDark ? '#cbd5e1' : '#94a3b8'; const barrierColor = isDark ? (isAirflow ? '#475569' : '#cbd5e1') : (isAirflow ? '#cbd5e1' : '#94a3b8');
const pillarColor = isDark ? '#94a3b8' : '#64748b'; const pillarColor = isDark ? (isAirflow ? '#334155' : '#94a3b8') : (isAirflow ? '#e2e8f0' : '#64748b');
return ( return (
<group> <group>
{/* Tabuleiro (deck) */} {/* Tabuleiro (deck) */}
<mesh position={[0, deckY, 0]} castShadow receiveShadow> <mesh position={[0, deckY, 0]} castShadow receiveShadow>
<boxGeometry args={[lp, deckThickness, width]} /> <boxGeometry args={[lp, deckThickness, width]} />
<meshStandardMaterial color={deckColor} roughness={0.5} /> <meshStandardMaterial color={deckColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.5} />
</mesh> </mesh>
{/* Guarda-rodas/barreira lateral */} {/* Guarda-rodas/barreira lateral */}
<mesh position={[0, deckY + deckThickness / 2 + 0.3, halfW - 0.15]} castShadow> <mesh position={[0, deckY + deckThickness / 2 + 0.3, halfW - 0.15]} castShadow>
<boxGeometry args={[lp, 0.5, 0.1]} /> <boxGeometry args={[lp, 0.5, 0.1]} />
<meshStandardMaterial color={barrierColor} roughness={0.7} /> <meshStandardMaterial color={barrierColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.7} />
</mesh> </mesh>
<mesh position={[0, deckY + deckThickness / 2 + 0.3, -halfW + 0.15]} castShadow> <mesh position={[0, deckY + deckThickness / 2 + 0.3, -halfW + 0.15]} castShadow>
<boxGeometry args={[lp, 0.5, 0.1]} /> <boxGeometry args={[lp, 0.5, 0.1]} />
<meshStandardMaterial color={barrierColor} roughness={0.7} /> <meshStandardMaterial color={barrierColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.7} />
</mesh> </mesh>
{/* Pilares (3 ao longo do comprimento) */} {/* Pilares (3 ao longo do comprimento) */}
@@ -87,53 +96,68 @@ function BridgeModel({
})} })}
{/* Solo / água */} {/* Solo / água */}
<mesh position={[0, -0.5, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow> {!isAirflow && (
<planeGeometry args={[lp * 1.6, width * 3]} /> <mesh position={[0, -0.5, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
<meshStandardMaterial color="#60a5fa" opacity={0.4} transparent roughness={0.3} /> <planeGeometry args={[lp * 1.6, width * 3]} />
</mesh> <meshStandardMaterial color="#60a5fa" opacity={0.4} transparent roughness={0.3} />
</mesh>
)}
{/* Seta de Vento (bate no meio do tabuleiro) */} {/* Seta de Vento */}
<WindArrow {isAirflow ? (
target={[0, deckY + deckThickness / 2, 0]} <group>
direction={[0, Math.sin((alpha * Math.PI) / 180), Math.cos((alpha * Math.PI) / 180)]} <WindArrow
scale={4} target={[-halfL * 0.8, deckY, -halfW - 2]}
/> direction={[0, 0, 1]}
<Text scale={4}
position={[0, deckY + deckThickness + 1.5, 0]} />
fontSize={1.2} <Text
color={isDark ? "#93c5fd" : "#1e40af"} position={[-halfL * 0.8, deckY + 1.5, -halfW - 4]}
anchorX="center" rotation={[0, Math.PI / 4, 0]}
anchorY="bottom" fontSize={1.2}
> color={isDark ? '#60a5fa' : '#3b82f6'}
Lp={lp}m | B={width}m | Cx={cx.toFixed(2)} anchorX="center"
</Text> anchorY="bottom"
>
Vento
</Text>
<BridgeAirflowSystem width={width} lp={lp} deckHeight={deckHeight} cx={cx} />
</group>
) : (
<WindArrow
target={[0, deckY + deckThickness / 2, 0]}
direction={[0, Math.sin((alpha * Math.PI) / 180), Math.cos((alpha * Math.PI) / 180)]}
scale={4}
/>
)}
</group> </group>
); );
} }
export default function Bridge3DViewer(input: Bridge3DInput) { export default function Bridge3DViewer(props: Bridge3DInput) {
const { lp, width, deckHeight, heg, cx, cz, fxPerLength, fzPerLength, alpha, viewMode = 'solid' } = props;
const cameraDistance = Math.max(lp * 1.2, width * 2, deckHeight * 2, 15);
const theme = useCanvasTheme(); const theme = useCanvasTheme();
const isDark = theme === 'dark'; const isDark = theme === 'dark';
const { lp, deckHeight, width } = input;
const dist = Math.max(lp * 0.6, deckHeight * 2);
const fallback = ( const fallback = (
<FallbackDiagram <FallbackDiagram
type="bridge" type="bridge"
props={input} props={{ lp, width, deckHeight, heg, cx, cz, fxPerLength, fzPerLength }}
/> />
); );
return ( return (
<SceneCanvas moduleId="ponte" <SceneCanvas moduleId="pontes"
shadows shadows
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
gl={{ preserveDrawingBuffer: true, antialias: true }} gl={{ preserveDrawingBuffer: true, antialias: true }}
camera={{ position: [dist * 0.8, deckHeight + width, dist], fov: 45 }} camera={{ position: [-cameraDistance * 0.8, cameraDistance * 0.6, -cameraDistance], fov: 45 }}
fallback={fallback} fallback={fallback}
> >
<ambientLight intensity={0.6} /> <ambientLight intensity={0.6} />
<directionalLight position={[lp, deckHeight * 3, width * 3]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} /> <directionalLight position={[lp, deckHeight * 3, width * 3]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
<BridgeModel {...input} /> <BridgeModel {...props} viewMode={viewMode} />
<Grid <Grid
infiniteGrid infiniteGrid
fadeDistance={lp * 0.5} fadeDistance={lp * 0.5}
@@ -0,0 +1,132 @@
import React, { useRef, useMemo, useEffect } from 'react';
import * as THREE from 'three';
import { useFrame } from '@react-three/fiber';
import { useCanvasTheme } from '@/lib/theme';
interface Particle {
position: THREE.Vector3;
baseSpeed: number;
wobbleSpeed: number;
wobbleOffset: number;
currentCpe: number;
}
interface BridgeAirflowSystemProps {
width: number;
lp: number;
deckHeight: number;
cx: number;
}
export const BridgeAirflowSystem: React.FC<BridgeAirflowSystemProps> = ({
width,
lp,
deckHeight,
cx,
}) => {
const meshRef = useRef<THREE.InstancedMesh>(null);
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const COUNT = 100;
const dummy = useMemo(() => new THREE.Object3D(), []);
const colorObj = useMemo(() => new THREE.Color(), []);
const halfWidth = width / 2;
const halfLp = lp / 2;
const particles = useMemo(() => {
const arr: Particle[] = [];
for (let i = 0; i < COUNT; i++) {
arr.push({
position: new THREE.Vector3(
(Math.random() - 0.5) * lp,
Math.random() * (deckHeight + 10),
-halfWidth - 10 - Math.random() * 20
),
baseSpeed: 0.075 + Math.random() * 0.05,
wobbleSpeed: 0.5 + Math.random() * 1.5,
wobbleOffset: Math.random() * Math.PI * 2,
currentCpe: 0,
});
}
return arr;
}, [width, lp, deckHeight]);
useEffect(() => {
if (meshRef.current) {
for (let i = 0; i < COUNT; i++) {
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
meshRef.current.setColorAt(i, colorObj);
}
meshRef.current.instanceColor!.needsUpdate = true;
}
}, [isDark, colorObj]);
useFrame((state) => {
if (!meshRef.current) return;
const time = state.clock.elapsedTime;
particles.forEach((p, i) => {
const px = p.position.x;
const py = p.position.y;
const pz = p.position.z;
let currentSpeed = p.baseSpeed;
let localCpe = 0;
let deflectY = 0;
const inX = Math.abs(px) < halfLp + 1;
const inZ = Math.abs(pz) < halfWidth + 1;
if (inX && inZ) {
localCpe = cx;
// Tabuleiro deflete o vento
if (Math.abs(py - deckHeight) < 2) {
const dist = -pz;
const lift = Math.max(0, deckHeight + 1 - py) / (dist + 1);
deflectY += lift * currentSpeed * 0.6 * (py < deckHeight ? -1 : 1);
}
}
p.currentCpe += (localCpe - p.currentCpe) * 0.1;
p.position.z += currentSpeed;
p.position.y += deflectY;
p.position.y += Math.sin(time * p.wobbleSpeed * 10 + p.wobbleOffset) * 0.01;
if (p.position.z > halfWidth + 10) {
p.position.z = -halfWidth - 10 - Math.random() * 5;
p.position.y = Math.random() * (deckHeight + 10);
p.position.x = (Math.random() - 0.5) * lp;
p.currentCpe = 0;
}
dummy.position.copy(p.position);
dummy.rotation.set(0, 0, 0);
dummy.rotateX(Math.PI / 2);
dummy.scale.set(1, 1 + currentSpeed * 6, 1);
dummy.updateMatrix();
meshRef.current!.setMatrixAt(i, dummy.matrix);
if (p.currentCpe > 0) {
colorObj.set(isDark ? '#f87171' : '#ef4444');
} else if (p.currentCpe < 0) {
const intensity = Math.min(1, Math.abs(p.currentCpe));
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity);
} else {
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
}
meshRef.current!.setColorAt(i, colorObj);
});
meshRef.current.instanceMatrix.needsUpdate = true;
meshRef.current.instanceColor!.needsUpdate = true;
});
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, COUNT]}>
<cylinderGeometry args={[0.04, 0.04, 1.0, 4]} />
<meshBasicMaterial transparent opacity={0.6} depthWrite={false} />
</instancedMesh>
);
};
@@ -95,7 +95,7 @@ export function CylinderAirflowSystem({ diameter, height }: CylinderAirflowSyste
} else if (Math.abs(px) <= R * 0.6 && r < R * 1.5) { } else if (Math.abs(px) <= R * 0.6 && r < R * 1.5) {
p.currentCpe = -1.2; // Laterais (forte sucção) p.currentCpe = -1.2; // Laterais (forte sucção)
} else if (px > R * 0.3 && Math.abs(pz) < R * 1.2) { } else if (px > R * 0.3 && Math.abs(pz) < R * 1.2) {
p.currentCpe = -0.5; // Esteira (sucção moderada) p.currentCpe = 0.5; // Esteira (agora positivo para ficar vermelho)
} else { } else {
p.currentCpe = 0; p.currentCpe = 0;
} }
@@ -123,16 +123,12 @@ export function CylinderAirflowSystem({ diameter, height }: CylinderAirflowSyste
meshRef.current!.setMatrixAt(i, dummy.matrix); meshRef.current!.setMatrixAt(i, dummy.matrix);
if (p.currentCpe > 0) { if (p.currentCpe > 0) {
colorObj colorObj.set(isDark ? '#f87171' : '#ef4444'); // Vermelho (Turbulento/Pressão)
.set(isDark ? '#ef4444' : '#dc2626')
.lerp(new THREE.Color(isDark ? '#fcd34d' : '#f59e0b'), 1 - Math.min(1, p.currentCpe));
} else if (p.currentCpe < 0) { } else if (p.currentCpe < 0) {
const intensity = Math.min(1, Math.abs(p.currentCpe)); const intensity = Math.min(1, Math.abs(p.currentCpe));
colorObj colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção)
.set(isDark ? '#7dd3fc' : '#38bdf8')
.lerp(new THREE.Color(isDark ? '#1e40af' : '#1d4ed8'), intensity);
} else { } else {
colorObj.set(isDark ? '#f1f5f9' : '#64748b'); colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre)
} }
meshRef.current!.setColorAt(i, colorObj); meshRef.current!.setColorAt(i, colorObj);
}); });
+2 -2
View File
@@ -133,7 +133,7 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
})} })}
{/* Seta indicativa de direção do vento */} {/* Seta indicativa de direção do vento */}
<group position={[radius + 2.5, wallHeight / 2, 0]} rotation={[0, 0, Math.PI / 2]}> <group position={[-radius - 2.5, wallHeight / 2, 0]} rotation={[0, 0, -Math.PI / 2]}>
<mesh castShadow> <mesh castShadow>
<coneGeometry args={[0.3, 0.8, 16]} /> <coneGeometry args={[0.3, 0.8, 16]} />
<meshStandardMaterial color="#3b82f6" roughness={0.3} /> <meshStandardMaterial color="#3b82f6" roughness={0.3} />
@@ -146,7 +146,7 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
{/* Texto Vento */} {/* Texto Vento */}
<Text <Text
position={[radius + 2.5, wallHeight / 2 + 1.2, 0]} position={[-radius - 2.5, wallHeight / 2 + 1.2, 0]}
rotation={[0, 0, 0]} rotation={[0, 0, 0]}
fontSize={1.2} fontSize={1.2}
color="#3b82f6" color="#3b82f6"
@@ -123,8 +123,10 @@ export function DomeAirflowSystem({ diameter, rise, wallHeight }: DomeAirflowSys
p.currentCpe = 0.6; // Barlavento p.currentCpe = 0.6; // Barlavento
} else if (Math.abs(px) <= R * 0.4 && py > wallHeight) { } else if (Math.abs(px) <= R * 0.4 && py > wallHeight) {
p.currentCpe = -1.0; // Topo da cúpula (forte sucção) p.currentCpe = -1.0; // Topo da cúpula (forte sucção)
} else if (px > R * 0.2) {
p.currentCpe = 0.5; // Sotavento / Esteira (agora positivo para ficar vermelho)
} else { } else {
p.currentCpe = -0.5; // Lateral / Sotavento p.currentCpe = -0.5; // Laterais / transição
} }
} else { } else {
p.currentCpe = 0; p.currentCpe = 0;
@@ -151,16 +153,12 @@ export function DomeAirflowSystem({ diameter, rise, wallHeight }: DomeAirflowSys
meshRef.current!.setMatrixAt(i, dummy.matrix); meshRef.current!.setMatrixAt(i, dummy.matrix);
if (p.currentCpe > 0) { if (p.currentCpe > 0) {
colorObj colorObj.set(isDark ? '#f87171' : '#ef4444'); // Vermelho (Turbulento/Pressão)
.set(isDark ? '#ef4444' : '#dc2626')
.lerp(new THREE.Color(isDark ? '#fcd34d' : '#f59e0b'), 1 - Math.min(1, p.currentCpe));
} else if (p.currentCpe < 0) { } else if (p.currentCpe < 0) {
const intensity = Math.min(1, Math.abs(p.currentCpe)); const intensity = Math.min(1, Math.abs(p.currentCpe));
colorObj colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção)
.set(isDark ? '#7dd3fc' : '#38bdf8')
.lerp(new THREE.Color(isDark ? '#1e40af' : '#1d4ed8'), intensity);
} else { } else {
colorObj.set(isDark ? '#f1f5f9' : '#64748b'); colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre)
} }
meshRef.current!.setColorAt(i, colorObj); meshRef.current!.setColorAt(i, colorObj);
}); });
+61 -33
View File
@@ -4,12 +4,12 @@ import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls';
import * as THREE from 'three'; import * as THREE from 'three';
import SceneCanvas from '../SceneCanvas'; import SceneCanvas from '../SceneCanvas';
import FallbackDiagram from '../FallbackDiagram'; import FallbackDiagram from '../FallbackDiagram';
import { WindArrow } from './WindArrow';
import { useCanvasTheme } from '@/lib/theme'; import { useCanvasTheme } from '@/lib/theme';
import { IsolatedRoofAirflowSystem } from './IsolatedRoofAirflowSystem';
export interface IsolatedRoof3DInput { export interface IsolatedRoof3DInput {
/** Tipo de cobertura: 'shed' (uma água) ou 'gable' (duas águas) */ /** Tipo de cobertura: 'shed' (uma água), 'gable' (duas águas) ou 'butterfly' (borboleta) */
type: 'shed' | 'gable'; type: 'shed' | 'gable' | 'butterfly';
/** Inclinação θ (graus) */ /** Inclinação θ (graus) */
theta: number; theta: number;
/** Altura livre dos suportes (m) */ /** Altura livre dos suportes (m) */
@@ -26,6 +26,7 @@ export interface IsolatedRoof3DInput {
cpeTop: number; cpeTop: number;
/** Força resultante na cobertura (kN) */ /** Força resultante na cobertura (kN) */
forceKN: number; forceKN: number;
viewMode?: 'solid' | 'airflow';
} }
function pressureColor(cpe: number, isDark: boolean): THREE.Color { function pressureColor(cpe: number, isDark: boolean): THREE.Color {
@@ -50,9 +51,11 @@ function IsolatedRoofModel({
cpeWindward, cpeWindward,
cpeLeeward, cpeLeeward,
forceKN, forceKN,
viewMode = 'solid',
}: IsolatedRoof3DInput) { }: IsolatedRoof3DInput) {
const theme = useCanvasTheme(); const theme = useCanvasTheme();
const isDark = theme === 'dark'; const isDark = theme === 'dark';
const isAirflow = viewMode === 'airflow';
const labelColor = isDark ? '#cbd5e1' : '#475569'; const labelColor = isDark ? '#cbd5e1' : '#475569';
const textColor = isDark ? '#93c5fd' : '#1a202c'; const textColor = isDark ? '#93c5fd' : '#1a202c';
const lineMaterialColor = isDark ? '#94a3b8' : '#64748b'; const lineMaterialColor = isDark ? '#94a3b8' : '#64748b';
@@ -81,7 +84,7 @@ function IsolatedRoofModel({
{ pos: [-halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff }, { pos: [-halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
{ pos: [halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff }, { pos: [halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
); );
} else { } else if (type === 'gable') {
list.push( list.push(
{ pos: [-halfWidth, height / 2, -halfDepth], h: height }, { pos: [-halfWidth, height / 2, -halfDepth], h: height },
{ pos: [halfWidth, height / 2, -halfDepth], h: height }, { pos: [halfWidth, height / 2, -halfDepth], h: height },
@@ -90,6 +93,15 @@ function IsolatedRoofModel({
{ pos: [-halfWidth, (height + h_half) / 2, 0], h: height + h_half }, { pos: [-halfWidth, (height + h_half) / 2, 0], h: height + h_half },
{ pos: [halfWidth, (height + h_half) / 2, 0], h: height + h_half }, { pos: [halfWidth, (height + h_half) / 2, 0], h: height + h_half },
); );
} else if (type === 'butterfly') {
list.push(
{ pos: [-halfWidth, (height + h_half) / 2, -halfDepth], h: height + h_half },
{ pos: [halfWidth, (height + h_half) / 2, -halfDepth], h: height + h_half },
{ pos: [-halfWidth, (height + h_half) / 2, halfDepth], h: height + h_half },
{ pos: [halfWidth, (height + h_half) / 2, halfDepth], h: height + h_half },
{ pos: [-halfWidth, height / 2, 0], h: height },
{ pos: [halfWidth, height / 2, 0], h: height },
);
} }
return list; return list;
}, [type, depth, width, height, h_diff, h_half, halfDepth, halfWidth]); }, [type, depth, width, height, h_diff, h_half, halfDepth, halfWidth]);
@@ -106,25 +118,9 @@ function IsolatedRoofModel({
{type === 'shed' ? ( {type === 'shed' ? (
// Uma água (Shed): dividida em metade barlavento e metade sotavento // Uma água (Shed): dividida em metade barlavento e metade sotavento
<group> <group>
{/* Metade Barlavento (Z < 0) */} <mesh position={[0, height + h_diff / 2, 0]} rotation={[-thetaRad, 0, 0]} castShadow receiveShadow>
<mesh <boxGeometry args={[width, 0.08, depth / Math.cos(thetaRad)]} />
position={[0, height + h_diff / 4, -depth / 4]} <meshStandardMaterial color={windwardColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.4} />
rotation={[-thetaRad, 0, 0]}
castShadow
receiveShadow
>
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
<meshStandardMaterial color={windwardColor} opacity={0.9} transparent roughness={0.4} />
</mesh>
{/* Metade Sotavento (Z > 0) */}
<mesh
position={[0, height + (3 * h_diff) / 4, depth / 4]}
rotation={[-thetaRad, 0, 0]}
castShadow
receiveShadow
>
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
<meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} />
</mesh> </mesh>
</group> </group>
) : ( ) : (
@@ -133,22 +129,22 @@ function IsolatedRoofModel({
{/* Água Esquerda / Barlavento (Z < 0) */} {/* Água Esquerda / Barlavento (Z < 0) */}
<mesh <mesh
position={[0, height + h_half / 2, -depth / 4]} position={[0, height + h_half / 2, -depth / 4]}
rotation={[-thetaRad, 0, 0]} rotation={[type === 'butterfly' ? thetaRad : -thetaRad, 0, 0]}
castShadow castShadow
receiveShadow receiveShadow
> >
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} /> <boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
<meshStandardMaterial color={windwardColor} opacity={0.9} transparent roughness={0.4} /> <meshStandardMaterial color={windwardColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.4} />
</mesh> </mesh>
{/* Água Direita / Sotavento (Z > 0) */} {/* Água Direita / Sotavento (Z > 0) */}
<mesh <mesh
position={[0, height + h_half / 2, depth / 4]} position={[0, height + h_half / 2, depth / 4]}
rotation={[thetaRad, 0, 0]} rotation={[type === 'butterfly' ? -thetaRad : thetaRad, 0, 0]}
castShadow castShadow
receiveShadow receiveShadow
> >
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} /> <boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
<meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} /> <meshStandardMaterial color={leewardColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.4} />
</mesh> </mesh>
</group> </group>
)} )}
@@ -161,11 +157,27 @@ function IsolatedRoofModel({
</mesh> </mesh>
))} ))}
{/* Vetor de vento (Seta verde/azul batendo no elemento) */} {/* Vetor de vento (Seta mais visível com texto) */}
<WindArrow <group position={[0, centerY, -halfDepth - 3.5]} rotation={[Math.PI / 2, 0, 0]}>
target={new THREE.Vector3(0, centerY, 0)} <mesh castShadow>
direction={new THREE.Vector3(0, 0, 1)} <coneGeometry args={[0.4, 1.2, 16]} />
/> <meshStandardMaterial color="#3b82f6" roughness={0.3} />
</mesh>
<mesh position={[0, -1.0, 0]} castShadow>
<cylinderGeometry args={[0.15, 0.15, 2.0, 16]} />
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
</mesh>
</group>
<Text
position={[0, centerY + 1.2, -halfDepth - 3.5]}
rotation={[0, -3 * Math.PI / 4, 0]}
fontSize={1.2}
color="#3b82f6"
anchorX="center"
anchorY="bottom"
>
Vento
</Text>
{/* === LINHAS DE COTA (CAD-Style) === */} {/* === LINHAS DE COTA (CAD-Style) === */}
{/* Cota de Altura (h) */} {/* Cota de Altura (h) */}
@@ -248,6 +260,8 @@ function IsolatedRoofModel({
{/* Rótulo Superior */} {/* Rótulo Superior */}
<Text <Text
position={[0, centerY + 3, 0]} position={[0, centerY + 3, 0]}
rotation={[0, -3 * Math.PI / 4, 0]}
fontSize={Math.max(0.4, width / 15)}
color={textColor} color={textColor}
anchorX="center" anchorX="center"
anchorY="bottom" anchorY="bottom"
@@ -268,6 +282,7 @@ export default function IsolatedRoof3DViewer({
cpeLeeward, cpeLeeward,
cpeTop, cpeTop,
forceKN, forceKN,
viewMode = 'solid',
}: IsolatedRoof3DInput) { }: IsolatedRoof3DInput) {
const cameraDistance = Math.max(Math.max(width, depth) * 1.3, height * 1.5, 8); const cameraDistance = Math.max(Math.max(width, depth) * 1.3, height * 1.5, 8);
@@ -281,8 +296,9 @@ export default function IsolatedRoof3DViewer({
return ( return (
<SceneCanvas moduleId="cobertura_isolada" <SceneCanvas moduleId="cobertura_isolada"
shadows shadows
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
gl={{ preserveDrawingBuffer: true, antialias: true }} gl={{ preserveDrawingBuffer: true, antialias: true }}
camera={{ position: [cameraDistance, cameraDistance * 0.8, cameraDistance], fov: 40 }} camera={{ position: [-cameraDistance * 0.8, cameraDistance * 0.8, -cameraDistance], fov: 40 }}
fallback={fallback} fallback={fallback}
> >
<ambientLight intensity={0.7} /> <ambientLight intensity={0.7} />
@@ -303,7 +319,19 @@ export default function IsolatedRoof3DViewer({
cpeLeeward={cpeLeeward} cpeLeeward={cpeLeeward}
cpeTop={cpeTop} cpeTop={cpeTop}
forceKN={forceKN} forceKN={forceKN}
viewMode={viewMode}
/> />
{viewMode === 'airflow' && (
<IsolatedRoofAirflowSystem
type={type}
theta={theta}
height={height}
width={width}
depth={depth}
cpeWindward={cpeWindward}
cpeLeeward={cpeLeeward}
/>
)}
<Grid infiniteGrid fadeDistance={cameraDistance * 5} sectionColor="#94a3b8" cellColor="#cbd5e1" position={[0, -0.01, 0]} /> <Grid infiniteGrid fadeDistance={cameraDistance * 5} sectionColor="#94a3b8" cellColor="#cbd5e1" position={[0, -0.01, 0]} />
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.05} /> <OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.05} />
<Environment preset="city" /> <Environment preset="city" />
@@ -0,0 +1,170 @@
import React, { useRef, useMemo, useEffect } from 'react';
import * as THREE from 'three';
import { useFrame } from '@react-three/fiber';
import { useCanvasTheme } from '@/lib/theme';
interface Particle {
position: THREE.Vector3;
baseSpeed: number;
wobbleSpeed: number;
wobbleOffset: number;
currentCpe: number;
}
interface IsolatedRoofAirflowSystemProps {
type: 'shed' | 'gable' | 'butterfly';
theta: number;
height: number;
width: number;
depth: number;
cpeWindward: number;
cpeLeeward: number;
}
export const IsolatedRoofAirflowSystem: React.FC<IsolatedRoofAirflowSystemProps> = ({
type,
theta,
height,
width,
depth,
cpeWindward,
cpeLeeward,
}) => {
const meshRef = useRef<THREE.InstancedMesh>(null);
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const COUNT = 100;
const dummy = useMemo(() => new THREE.Object3D(), []);
const colorObj = useMemo(() => new THREE.Color(), []);
const thetaRad = (theta * Math.PI) / 180;
const halfDepth = depth / 2;
const halfWidth = width / 2;
const h_diff = depth * Math.tan(thetaRad);
const h_half = halfDepth * Math.tan(thetaRad);
const particles = useMemo(() => {
const arr: Particle[] = [];
for (let i = 0; i < COUNT; i++) {
arr.push({
position: new THREE.Vector3(
(Math.random() - 0.5) * width * 3.0,
Math.random() * (height + h_diff + 3),
-halfDepth - 5 - Math.random() * 20
),
baseSpeed: 0.075 + Math.random() * 0.05,
wobbleSpeed: 0.5 + Math.random() * 1.5,
wobbleOffset: Math.random() * Math.PI * 2,
currentCpe: 0,
});
}
return arr;
}, [width, height, depth, h_diff]);
useEffect(() => {
if (meshRef.current) {
for (let i = 0; i < COUNT; i++) {
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
meshRef.current.setColorAt(i, colorObj);
}
meshRef.current.instanceColor!.needsUpdate = true;
}
}, [isDark, colorObj]);
useFrame((state) => {
if (!meshRef.current) return;
const time = state.clock.elapsedTime;
particles.forEach((p, i) => {
const px = p.position.x;
const py = p.position.y;
const pz = p.position.z;
let currentSpeed = p.baseSpeed;
let localCpe = 0;
let deflectY = 0;
const inX = Math.abs(px) < halfWidth + 1;
const inZ = Math.abs(pz) < halfDepth + 2;
if (inX && inZ) {
if (type === 'butterfly') {
if (pz < 0) {
localCpe = cpeWindward;
// Vento acompanha a descida em direção ao vale
if (py > height + 0.2) {
deflectY -= currentSpeed * 0.3;
}
} else {
localCpe = cpeLeeward;
// Vento bate na segunda água que sobe e é defletido para cima
if (py < height + h_half + 2) {
deflectY += currentSpeed * 0.6;
}
}
} else {
if (pz < 0) {
localCpe = cpeWindward;
const maxH = type === 'shed' ? height + h_diff : height + h_half;
if (py < maxH + 1) {
const dist = -pz;
const lift = Math.max(0, maxH - py) / (dist + 1);
deflectY += lift * currentSpeed * 0.4;
}
} else {
localCpe = cpeLeeward;
if (cpeLeeward < 0) {
deflectY -= currentSpeed * 0.15;
p.position.y += Math.sin(time * 5 + p.wobbleOffset) * 0.05;
}
}
}
if (pz > halfDepth && py < height + h_diff) {
localCpe = 0.5; // Esteira
deflectY -= currentSpeed * 0.2;
}
}
p.currentCpe += (localCpe - p.currentCpe) * 0.1;
p.position.z += currentSpeed;
p.position.y += deflectY;
p.position.y += Math.sin(time * p.wobbleSpeed * 10 + p.wobbleOffset) * 0.01;
if (p.position.z > halfDepth + 10) {
p.position.z = -halfDepth - 10 - Math.random() * 5;
p.position.y = Math.random() * (height + h_diff + 3);
p.position.x = (Math.random() - 0.5) * width * 3.0;
p.currentCpe = 0;
}
dummy.position.copy(p.position);
dummy.rotation.set(0, 0, 0);
dummy.rotateX(Math.PI / 2);
dummy.scale.set(1, 1 + currentSpeed * 6, 1);
dummy.updateMatrix();
meshRef.current!.setMatrixAt(i, dummy.matrix);
if (p.currentCpe > 0) {
colorObj.set(isDark ? '#f87171' : '#ef4444');
} else if (p.currentCpe < 0) {
const intensity = Math.min(1, Math.abs(p.currentCpe));
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity);
} else {
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
}
meshRef.current!.setColorAt(i, colorObj);
});
meshRef.current.instanceMatrix.needsUpdate = true;
meshRef.current.instanceColor!.needsUpdate = true;
});
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, COUNT]}>
<cylinderGeometry args={[0.04, 0.04, 1.0, 4]} />
<meshBasicMaterial transparent opacity={0.6} depthWrite={false} />
</instancedMesh>
);
};
+50 -77
View File
@@ -2,9 +2,11 @@ import React, { useRef } from 'react';
import * as THREE from 'three'; import * as THREE from 'three';
import { useFrame } from '@react-three/fiber'; import { useFrame } from '@react-three/fiber';
import { Text, Grid, Environment } from '@react-three/drei'; import { Text, Grid, Environment } from '@react-three/drei';
import { WindArrow as GlobalWindArrow } from './WindArrow';
import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls'; import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls';
import SceneCanvas from '../SceneCanvas'; import SceneCanvas from '../SceneCanvas';
import { useCanvasTheme } from '@/lib/theme'; import { useCanvasTheme } from '@/lib/theme';
import { ShelterAirflowSystem } from './ShelterAirflowSystem';
import type { ShelterCondition, OpeningPosition, ShelterWindAngle } from '@/lib/nbr-tables/table-shelters'; import type { ShelterCondition, OpeningPosition, ShelterWindAngle } from '@/lib/nbr-tables/table-shelters';
export interface Shelter3DProps { export interface Shelter3DProps {
@@ -19,43 +21,9 @@ export interface Shelter3DProps {
openingPos: OpeningPosition; openingPos: OpeningPosition;
backRange?: [number, number]; backRange?: [number, number];
windAngle?: ShelterWindAngle; 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({ function ShelterModel({
condition: _condition, condition: _condition,
depth, depth,
@@ -71,10 +39,13 @@ function ShelterModel({
viewMode, viewMode,
}: Shelter3DProps) { }: Shelter3DProps) {
const isElevation = viewMode === 'elevation'; const isElevation = viewMode === 'elevation';
const theme = useCanvasTheme();
const isDark = theme === 'dark';
// Geometria básica // Geometria básica
const halfW = width / 2; const halfW = width / 2;
const halfD = depth / 2; const halfD = depth / 2;
const isAirflow = viewMode === 'airflow';
const thetaRad = (theta * Math.PI) / 180; const thetaRad = (theta * Math.PI) / 180;
const slopeRise = depth * Math.tan(thetaRad); const slopeRise = depth * Math.tan(thetaRad);
@@ -161,50 +132,51 @@ function ShelterModel({
{/* Parede Lateral Direita (X = +halfW) */} {/* Parede Lateral Direita (X = +halfW) */}
{renderWallWithGap(depth, height, rightClosure, 'distributed', Math.PI / 2, [halfW, 0, 0])} {renderWallWithGap(depth, height, rightClosure, 'distributed', Math.PI / 2, [halfW, 0, 0])}
{/* 4. Linhas de Corrente e Vetores de Vento (Ilustrativo e Didático) */} {/* Seta de Vento Global (Sempre visível para não pular de posição) */}
<group position={[0, 0, 0]} rotation={[0, -((windAngle || 0) * Math.PI) / 180, 0]}> <group>
{/* Vento de entrada (Barlavento) entrando em direção ao abrigo */} <GlobalWindArrow
<WindArrow start={[0, height * 0.5, -halfD - 4]} end={[0, height * 0.5, -halfD + 1]} color="#38bdf8" speed={1.2} /> target={[-halfW * 1.2, height * 1.5 - 1.5, -halfD - 5 + 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} /> direction={[0, 0, 1]}
<WindArrow start={[halfW * 0.5, height * 0.4, -halfD - 4]} end={[halfW * 0.5, height * 0.4, -halfD + 1]} color="#38bdf8" speed={1.3} /> scale={4}
/>
{/* Linhas de corrente sobre o telhado */} <Text
<WindArrow start={[0, height + 1, -halfD - 3]} end={[0, height + slopeRise + 1.5, halfD + 2]} color="#60a5fa" speed={1.6} /> position={[-halfW * 1.2, height * 1.5, -halfD - 5]}
rotation={[0, Math.PI / 4, 0]}
{/* Fresta na parede de fundo: se houver abertura no topo, mostrar vento escapando pela fresta superior */} fontSize={1.2}
{backClosure > 0 && backClosure < 100 && (openingPos === 'top' || (backRange && backRange[1] < 98)) && ( color={isDark ? '#60a5fa' : '#3b82f6'}
<> anchorX="center"
<WindArrow start={[0, height * 0.85, 0]} end={[0, height * 0.95, halfD + 2.5]} color="#34d399" speed={1.8} /> anchorY="bottom"
<Text position={[0, height + 0.35, halfD]} rotation={[0, Math.PI, 0]} fontSize={0.4} color="#34d399"> >
Alívio (Fresta Topo) Vento
</Text> </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>
</>
)}
</group> </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) */} {/* 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"> {!isAirflow && (
{isElevation ? 'CORTE A-A (Elevação)' : 'Abrigo / Pórtico (NBR 6123)'} <>
</Text> <Text position={[0, height + slopeRise + 1.2, 0]} rotation={[0, Math.PI, 0]} fontSize={0.45} color="#e2e8f0">
<Text position={[0, 0.3, -halfD - 1.2]} rotation={[0, Math.PI, 0]} fontSize={0.35} color="#38bdf8"> {isElevation ? 'CORTE A-A (Elevação)' : 'Abrigo / Pórtico (NBR 6123)'}
{`VENTO ${windAngle === 0 ? 'FRONTAL (0°)' : windAngle === 45 ? 'OBLÍQUO (45°)' : 'LATERAL (90°)'}`} </Text>
</Text> <Text position={[0, height + slopeRise + 0.5, 0]} rotation={[0, Math.PI, 0]} fontSize={0.32} color="#f43f5e">
<Text position={[0, height + slopeRise + 0.25, 0]} rotation={[0, Math.PI, 0]} fontSize={0.32} color="#f43f5e"> Arrancamento [+Fz ]
Arrancamento [+Fz ] </Text>
</Text> <Text position={[0, height * 0.5, halfD + 1.2]} rotation={[0, Math.PI, 0]} fontSize={0.32} color="#fb923c">
<Text position={[0, height * 0.5, halfD + 1.2]} rotation={[0, Math.PI, 0]} fontSize={0.32} color="#fb923c"> Empuxo no Fundo [+Fx ]
Empuxo no Fundo [+Fx ] </Text>
</Text> </>
)}
</group> </group>
); );
} }
@@ -212,13 +184,14 @@ function ShelterModel({
export function Shelter3D(props: Shelter3DProps) { export function Shelter3D(props: Shelter3DProps) {
const theme = useCanvasTheme(); const theme = useCanvasTheme();
const isDark = theme === 'dark'; const isDark = theme === 'dark';
const { width, depth, height } = props; const { width, depth, height, viewMode } = props;
const dist = Math.max(width, depth) * 1.8 + 8; const dist = Math.max(width, depth) * 1.8 + 8;
return ( return (
<SceneCanvas <SceneCanvas
moduleId="shelter" moduleId="shelter"
shadows shadows
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
gl={{ preserveDrawingBuffer: true, antialias: true }} gl={{ preserveDrawingBuffer: true, antialias: true }}
camera={{ position: [dist * 0.75, height * 1.1, -dist * 0.85], fov: 45 }} 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>} fallback={<div className="flex items-center justify-center h-full text-muted-foreground">3D indisponível</div>}
@@ -0,0 +1,134 @@
import React, { useRef, useMemo, useEffect } from 'react';
import * as THREE from 'three';
import { useFrame } from '@react-three/fiber';
import { useCanvasTheme } from '@/lib/theme';
interface Particle {
position: THREE.Vector3;
baseSpeed: number;
wobbleSpeed: number;
wobbleOffset: number;
currentCpe: number;
}
interface ShelterAirflowSystemProps {
width: number;
depth: number;
height: number;
theta: number;
}
export const ShelterAirflowSystem: React.FC<ShelterAirflowSystemProps> = ({
width,
depth,
height,
theta,
}) => {
const meshRef = useRef<THREE.InstancedMesh>(null);
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const COUNT = 100;
const dummy = useMemo(() => new THREE.Object3D(), []);
const colorObj = useMemo(() => new THREE.Color(), []);
const halfWidth = width / 2;
const halfDepth = depth / 2;
const thetaRad = (theta * Math.PI) / 180;
const h_diff = depth * Math.tan(thetaRad);
const particles = useMemo(() => {
const arr: Particle[] = [];
for (let i = 0; i < COUNT; i++) {
arr.push({
position: new THREE.Vector3(
(Math.random() - 0.5) * width * 3,
Math.random() * (height + h_diff + 5),
-halfDepth - 10 - Math.random() * 20
),
baseSpeed: 0.075 + Math.random() * 0.05,
wobbleSpeed: 0.5 + Math.random() * 1.5,
wobbleOffset: Math.random() * Math.PI * 2,
currentCpe: 0,
});
}
return arr;
}, [width, depth, height, h_diff]);
useEffect(() => {
if (meshRef.current) {
for (let i = 0; i < COUNT; i++) {
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
meshRef.current.setColorAt(i, colorObj);
}
meshRef.current.instanceColor!.needsUpdate = true;
}
}, [isDark, colorObj]);
useFrame((state) => {
if (!meshRef.current) return;
const time = state.clock.elapsedTime;
particles.forEach((p, i) => {
const px = p.position.x;
const py = p.position.y;
const pz = p.position.z;
let currentSpeed = p.baseSpeed;
let localCpe = 0;
let deflectY = 0;
const inX = Math.abs(px) < halfWidth + 1;
const inZ = Math.abs(pz) < halfDepth + 1;
if (inX && inZ) {
localCpe = 0.5;
// Teto inclinado deflete o vento
if (py < height + h_diff + 1 && py > height - 1) {
const dist = -pz;
const lift = Math.max(0, height + h_diff - py) / (dist + 1);
deflectY += lift * currentSpeed * 0.4;
}
}
p.currentCpe += (localCpe - p.currentCpe) * 0.1;
p.position.z += currentSpeed;
p.position.y += deflectY;
p.position.y += Math.sin(time * p.wobbleSpeed * 10 + p.wobbleOffset) * 0.01;
if (p.position.z > halfDepth + 10) {
p.position.z = -halfDepth - 10 - Math.random() * 5;
p.position.y = Math.random() * (height + h_diff + 5);
p.position.x = (Math.random() - 0.5) * width * 3;
p.currentCpe = 0;
}
dummy.position.copy(p.position);
dummy.rotation.set(0, 0, 0);
dummy.rotateX(Math.PI / 2);
dummy.scale.set(1, 1 + currentSpeed * 6, 1);
dummy.updateMatrix();
meshRef.current!.setMatrixAt(i, dummy.matrix);
if (p.currentCpe > 0) {
colorObj.set(isDark ? '#f87171' : '#ef4444');
} else if (p.currentCpe < 0) {
const intensity = Math.min(1, Math.abs(p.currentCpe));
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity);
} else {
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
}
meshRef.current!.setColorAt(i, colorObj);
});
meshRef.current.instanceMatrix.needsUpdate = true;
meshRef.current.instanceColor!.needsUpdate = true;
});
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, COUNT]}>
<cylinderGeometry args={[0.04, 0.04, 1.0, 4]} />
<meshBasicMaterial transparent opacity={0.6} depthWrite={false} />
</instancedMesh>
);
};
@@ -152,16 +152,12 @@ export function SignAirflowSystem({ length, height, groundClearance, alpha, cf }
meshRef.current!.setMatrixAt(i, dummy.matrix); meshRef.current!.setMatrixAt(i, dummy.matrix);
if (p.currentCpe > 0) { if (p.currentCpe > 0) {
colorObj colorObj.set(isDark ? '#f87171' : '#ef4444'); // Vermelho (Turbulento/Pressão)
.set(isDark ? '#ef4444' : '#dc2626')
.lerp(new THREE.Color(isDark ? '#fcd34d' : '#f59e0b'), 1 - Math.min(1, p.currentCpe));
} else if (p.currentCpe < 0) { } else if (p.currentCpe < 0) {
const intensity = Math.min(1, Math.abs(p.currentCpe)); const intensity = Math.min(1, Math.abs(p.currentCpe));
colorObj colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção)
.set(isDark ? '#7dd3fc' : '#38bdf8')
.lerp(new THREE.Color(isDark ? '#1e40af' : '#1d4ed8'), intensity);
} else { } else {
colorObj.set(isDark ? '#f1f5f9' : '#64748b'); colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre)
} }
meshRef.current!.setColorAt(i, colorObj); meshRef.current!.setColorAt(i, colorObj);
}); });
+34 -7
View File
@@ -5,6 +5,7 @@ import * as THREE from 'three';
import SceneCanvas from '../SceneCanvas'; import SceneCanvas from '../SceneCanvas';
import { WindArrow } from './WindArrow'; import { WindArrow } from './WindArrow';
import FallbackDiagram from '../FallbackDiagram'; import FallbackDiagram from '../FallbackDiagram';
import { TowerAirflowSystem } from './TowerAirflowSystem';
export interface Tower3DInput { export interface Tower3DInput {
/** Forma da seção */ /** Forma da seção */
@@ -23,6 +24,8 @@ export interface Tower3DInput {
alphaWind: 0 | 45 | 90; alphaWind: 0 | 45 | 90;
/** Força total estimada na torre (kN) */ /** Força total estimada na torre (kN) */
forceKN: number; forceKN: number;
/** Modo de visualização */
viewMode?: 'solid' | 'airflow';
} }
/** /**
@@ -136,9 +139,11 @@ function TowerModel({
phi, phi,
alphaWind, alphaWind,
forceKN, forceKN,
viewMode = 'solid',
}: Tower3DInput) { }: Tower3DInput) {
const theme = useCanvasTheme(); const theme = useCanvasTheme();
const isDark = theme === 'dark'; const isDark = theme === 'dark';
const isAirflow = viewMode === 'airflow';
const geometry = useMemo( const geometry = useMemo(
() => buildTowerGeometry(section, panels, baseWidth, height), () => buildTowerGeometry(section, panels, baseWidth, height),
@@ -201,11 +206,32 @@ function TowerModel({
); );
})} })}
{/* Seta de Vento única atingindo o meio da torre */} {/* Seta de Vento */}
<WindArrow {isAirflow ? (
target={[0, height / 2, 0]} <group>
direction={forceDir} <WindArrow
/> target={[-baseWidth * 1.5, height / 2, -baseWidth * 1.5]}
direction={[0, 0, 1]}
scale={4}
/>
<Text
position={[-baseWidth * 1.5, height / 2 + 1.5, -baseWidth * 1.5 - 2]}
rotation={[0, Math.PI / 4, 0]}
fontSize={1.2}
color={isDark ? '#60a5fa' : '#3b82f6'}
anchorX="center"
anchorY="bottom"
>
Vento
</Text>
<TowerAirflowSystem baseWidth={baseWidth} height={height} phi={phi} />
</group>
) : (
<WindArrow
target={[0, height / 2, 0]}
direction={forceDir}
/>
)}
{/* Labels indicativos */} {/* Labels indicativos */}
<mesh position={[baseWidth / 2 + 0.5, 0.5, 0]}> <mesh position={[baseWidth / 2 + 0.5, 0.5, 0]}>
@@ -228,7 +254,7 @@ function TowerModel({
export default function Tower3DViewer(input: Tower3DInput) { export default function Tower3DViewer(input: Tower3DInput) {
const theme = useCanvasTheme(); const theme = useCanvasTheme();
const isDark = theme === 'dark'; const isDark = theme === 'dark';
const { baseWidth, height } = input; const { baseWidth, height, viewMode = 'solid' } = input;
const dist = Math.max(baseWidth * 3, height * 1.2); const dist = Math.max(baseWidth * 3, height * 1.2);
const fallback = ( const fallback = (
@@ -241,13 +267,14 @@ export default function Tower3DViewer(input: Tower3DInput) {
return ( return (
<SceneCanvas moduleId="torre" <SceneCanvas moduleId="torre"
shadows shadows
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
gl={{ preserveDrawingBuffer: true, antialias: true }} gl={{ preserveDrawingBuffer: true, antialias: true }}
camera={{ position: [dist, height * 0.6, dist], fov: 45 }} camera={{ position: [dist, height * 0.6, dist], fov: 45 }}
fallback={fallback} fallback={fallback}
> >
<ambientLight intensity={0.6} /> <ambientLight intensity={0.6} />
<directionalLight position={[dist, height, dist]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} /> <directionalLight position={[dist, height, dist]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
<TowerModel {...input} /> <TowerModel {...input} viewMode={viewMode} />
<Grid <Grid
infiniteGrid infiniteGrid
fadeDistance={height * 2} fadeDistance={height * 2}
@@ -0,0 +1,130 @@
import React, { useRef, useMemo, useEffect } from 'react';
import * as THREE from 'three';
import { useFrame } from '@react-three/fiber';
import { useCanvasTheme } from '@/lib/theme';
interface Particle {
position: THREE.Vector3;
baseSpeed: number;
wobbleSpeed: number;
wobbleOffset: number;
currentCpe: number;
}
interface TowerAirflowSystemProps {
baseWidth: number;
height: number;
phi: number;
}
export const TowerAirflowSystem: React.FC<TowerAirflowSystemProps> = ({
baseWidth,
height,
phi,
}) => {
const meshRef = useRef<THREE.InstancedMesh>(null);
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const COUNT = 100;
const dummy = useMemo(() => new THREE.Object3D(), []);
const colorObj = useMemo(() => new THREE.Color(), []);
const halfWidth = baseWidth / 2;
const particles = useMemo(() => {
const arr: Particle[] = [];
for (let i = 0; i < COUNT; i++) {
arr.push({
position: new THREE.Vector3(
(Math.random() - 0.5) * baseWidth * 3,
Math.random() * (height + 5),
-halfWidth - 10 - Math.random() * 20
),
baseSpeed: 0.075 + Math.random() * 0.05,
wobbleSpeed: 0.5 + Math.random() * 1.5,
wobbleOffset: Math.random() * Math.PI * 2,
currentCpe: 0,
});
}
return arr;
}, [baseWidth, height]);
useEffect(() => {
if (meshRef.current) {
for (let i = 0; i < COUNT; i++) {
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
meshRef.current.setColorAt(i, colorObj);
}
meshRef.current.instanceColor!.needsUpdate = true;
}
}, [isDark, colorObj]);
useFrame((state) => {
if (!meshRef.current) return;
const time = state.clock.elapsedTime;
particles.forEach((p, i) => {
const px = p.position.x;
const py = p.position.y;
const pz = p.position.z;
let currentSpeed = p.baseSpeed;
let localCpe = 0;
let deflectX = 0;
const inX = Math.abs(px) < halfWidth + 2;
const inZ = Math.abs(pz) < halfWidth + 2;
if (inX && inZ && py < height) {
localCpe = 0.8;
// Se a torre é densa (phi alto), o ar tenta desviar pelas laterais
if (pz < 0) {
const dist = halfWidth - Math.abs(px);
if (dist > 0) {
deflectX += (px > 0 ? 1 : -1) * currentSpeed * phi * 0.5;
}
}
}
p.currentCpe += (localCpe - p.currentCpe) * 0.1;
p.position.z += currentSpeed;
p.position.x += deflectX;
p.position.y += Math.sin(time * p.wobbleSpeed * 10 + p.wobbleOffset) * 0.01;
if (p.position.z > halfWidth + 10) {
p.position.z = -halfWidth - 10 - Math.random() * 5;
p.position.y = Math.random() * (height + 5);
p.position.x = (Math.random() - 0.5) * baseWidth * 3;
p.currentCpe = 0;
}
dummy.position.copy(p.position);
dummy.rotation.set(0, 0, 0);
dummy.rotateX(Math.PI / 2);
dummy.scale.set(1, 1 + currentSpeed * 6, 1);
dummy.updateMatrix();
meshRef.current!.setMatrixAt(i, dummy.matrix);
if (p.currentCpe > 0) {
colorObj.set(isDark ? '#f87171' : '#ef4444');
} else if (p.currentCpe < 0) {
const intensity = Math.min(1, Math.abs(p.currentCpe));
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity);
} else {
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
}
meshRef.current!.setColorAt(i, colorObj);
});
meshRef.current.instanceMatrix.needsUpdate = true;
meshRef.current.instanceColor!.needsUpdate = true;
});
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, COUNT]}>
<cylinderGeometry args={[0.04, 0.04, 1.0, 4]} />
<meshBasicMaterial transparent opacity={0.6} depthWrite={false} />
</instancedMesh>
);
};
+24
View File
@@ -175,6 +175,30 @@ function VaultModel({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDire
<meshStandardMaterial color={isDark ? "#334155" : "#cbd5e1"} opacity={isAirflow ? 0.25 : 0.7} transparent side={THREE.DoubleSide} roughness={0.5} /> <meshStandardMaterial color={isDark ? "#334155" : "#cbd5e1"} opacity={isAirflow ? 0.25 : 0.7} transparent side={THREE.DoubleSide} roughness={0.5} />
</mesh> </mesh>
{/* Seta indicativa de direção do vento */}
<group position={[-span / 2 - 2.5, rise / 2, length / 2]} rotation={[0, 0, -Math.PI / 2]}>
<mesh castShadow>
<coneGeometry args={[0.3, 0.8, 16]} />
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
</mesh>
<mesh position={[0, -0.6, 0]} castShadow>
<cylinderGeometry args={[0.1, 0.1, 1.2, 16]} />
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
</mesh>
</group>
{/* Texto Vento */}
<Text
position={[-span / 2 - 2.5, rise / 2 + 1.2, length / 2]}
rotation={[0, 0, 0]}
fontSize={1.2}
color="#3b82f6"
anchorX="center"
anchorY="bottom"
>
Vento
</Text>
{/* Rótulo de dimensões */} {/* Rótulo de dimensões */}
<Text <Text
position={[0, rise + 1.5, length / 2]} position={[0, rise + 1.5, length / 2]}
@@ -103,6 +103,8 @@ export function VaultAirflowSystem({ span, length, rise, viewDirection = 'perpen
p.currentCpe = 0.7; // Compressão a barlavento p.currentCpe = 0.7; // Compressão a barlavento
} else if (px >= -span * 0.15 && px <= span * 0.15) { } else if (px >= -span * 0.15 && px <= span * 0.15) {
p.currentCpe = -0.9; // Forte sucção no topo p.currentCpe = -0.9; // Forte sucção no topo
} else if (px > span / 4) {
p.currentCpe = 0.5; // Sotavento / Esteira (agora positivo para ficar vermelho)
} else { } else {
p.currentCpe = -0.5; // Sucção a sotavento p.currentCpe = -0.5; // Sucção a sotavento
} }
@@ -170,16 +172,12 @@ export function VaultAirflowSystem({ span, length, rise, viewDirection = 'perpen
// Colorir // Colorir
if (p.currentCpe > 0) { if (p.currentCpe > 0) {
colorObj colorObj.set(isDark ? '#f87171' : '#ef4444'); // Vermelho (Turbulento/Pressão)
.set(isDark ? '#ef4444' : '#dc2626')
.lerp(new THREE.Color(isDark ? '#fcd34d' : '#f59e0b'), 1 - Math.min(1, p.currentCpe));
} else if (p.currentCpe < 0) { } else if (p.currentCpe < 0) {
const intensity = Math.min(1, Math.abs(p.currentCpe)); const intensity = Math.min(1, Math.abs(p.currentCpe));
colorObj colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção)
.set(isDark ? '#7dd3fc' : '#38bdf8')
.lerp(new THREE.Color(isDark ? '#1e40af' : '#1d4ed8'), intensity);
} else { } else {
colorObj.set(isDark ? '#f1f5f9' : '#64748b'); colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre)
} }
meshRef.current!.setColorAt(i, colorObj); meshRef.current!.setColorAt(i, colorObj);
}); });
+16
View File
@@ -102,6 +102,22 @@ export function calculateIsolatedGableRoof(input: IsolatedGableRoofInput): Isola
}; };
} }
/**
* Cobertura isolada em V (Borboleta).
* Extrapolação baseada na inversão geométrica das duas águas.
* A NBR 6123 não possui tabela explícita, portanto applies = false.
*/
export function calculateIsolatedButterflyRoof(input: IsolatedGableRoofInput): IsolatedGableRoofResult {
const base = calculateIsolatedGableRoof(input);
return {
// Barlavento (descendo): forte sucção no bordo de ataque
cpb: { cpb1: -Math.abs(base.cpa.cpa1), cpb2: -Math.abs(base.cpa.cpa2) },
// Sotavento (subindo): atua como uma barreira, sofrendo pressão
cpa: { cpa1: Math.abs(base.cpb.cpb1), cpa2: Math.abs(base.cpb.cpb2) },
applies: false,
};
}
/** Força de atrito na cobertura isolada: F = 0,05 · q · a · b (sec. 7.2.2) */ /** Força de atrito na cobertura isolada: F = 0,05 · q · a · b (sec. 7.2.2) */
export function frictionForceIsolatedRoof(q: number, a: number, b: number): number { export function frictionForceIsolatedRoof(q: number, a: number, b: number): number {
return Number((0.05 * q * a * b).toFixed(3)); return Number((0.05 * q * a * b).toFixed(3));
+22 -1
View File
@@ -1,4 +1,5 @@
import React, { useMemo } from 'react'; import React, { useState, useMemo } from 'react';
import { Wind, Box } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { useHydrationStore } from '@/store/hydrationStore'; import { useHydrationStore } from '@/store/hydrationStore';
@@ -47,6 +48,7 @@ const BridgeModule: React.FC = () => {
const [fv, setFv] = React.useState(0.6); const [fv, setFv] = React.useState(0.6);
const [vk, setVk] = React.useState(45); const [vk, setVk] = React.useState(45);
const [alpha, setAlpha] = React.useState(0); const [alpha, setAlpha] = React.useState(0);
const [viewMode, setViewMode] = useState<'solid' | 'airflow'>('solid');
const [vf, setVf] = React.useState(100); // Velocidade crítica de flutter da seção const [vf, setVf] = React.useState(100); // Velocidade crítica de flutter da seção
const classification = useMemo( const classification = useMemo(
@@ -214,6 +216,24 @@ const BridgeModule: React.FC = () => {
Classe {classification.bridgeClass} Classe {classification.bridgeClass}
</Badge> </Badge>
</div> </div>
<div className="absolute top-4 right-16 z-10">
<div className="flex bg-background/80 backdrop-blur-sm border border-border/80 rounded-md shadow-sm p-1">
<button
onClick={() => setViewMode('solid')}
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'solid' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
>
<Box className="w-3.5 h-3.5 inline-block mr-1.5" />
Sólido
</button>
<button
onClick={() => setViewMode('airflow')}
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'airflow' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
>
<Wind className="w-3.5 h-3.5 inline-block mr-1.5" />
Fluxo
</button>
</div>
</div>
<div className="absolute top-4 right-4 z-10"> <div className="absolute top-4 right-4 z-10">
<EducationalManual type="bridge" params={{ alpha }} /> <EducationalManual type="bridge" params={{ alpha }} />
</div> </div>
@@ -227,6 +247,7 @@ const BridgeModule: React.FC = () => {
fxPerLength={forces.fxPerLength} fxPerLength={forces.fxPerLength}
fzPerLength={forces.fzPerLength} fzPerLength={forces.fzPerLength}
alpha={alpha} alpha={alpha}
viewMode={viewMode}
/> />
</div> </div>
+44 -13
View File
@@ -1,11 +1,12 @@
import React, { useMemo } from 'react'; import React, { useMemo, useState } from 'react';
import { Box, Wind } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Slider } from '@/components/ui/slider'; import { Slider } from '@/components/ui/slider';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { useWindStore } from '@/store/appStore'; import { useWindStore } from '@/store/appStore';
import { calculateIsolatedShedRoof, calculateIsolatedGableRoof, frictionForceIsolatedRoof } from '@/lib/nbr-tables/table-24-25'; import { calculateIsolatedShedRoof, calculateIsolatedGableRoof, calculateIsolatedButterflyRoof, frictionForceIsolatedRoof } from '@/lib/nbr-tables/table-24-25';
import IsolatedRoof3DViewer from '@/components/three/IsolatedRoof3D'; import IsolatedRoof3DViewer from '@/components/three/IsolatedRoof3D';
import SceneCapturePanel from '../components/SceneCapturePanel'; import SceneCapturePanel from '../components/SceneCapturePanel';
import ExportMenu from '../components/ExportMenu'; import ExportMenu from '../components/ExportMenu';
@@ -17,17 +18,22 @@ import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generi
const IsolatedRoofModule: React.FC = () => { const IsolatedRoofModule: React.FC = () => {
const { q } = useWindStore(); const { q } = useWindStore();
const [type, setType] = React.useState<'shed' | 'gable'>('shed'); const [type, setType] = React.useState<'shed' | 'gable' | 'butterfly'>('shed');
const [theta, setTheta] = React.useState(15); const [theta, setTheta] = React.useState(15);
const [height, setHeight] = React.useState(2); const [height, setHeight] = React.useState(2);
const [width, setWidth] = React.useState(10); const [width, setWidth] = useState(10);
const [depth, setDepth] = React.useState(10); const [depth, setDepth] = useState(10);
const [viewMode, setViewMode] = useState<'solid' | 'airflow'>('solid');
const result = useMemo(() => { const result = useMemo(() => {
if (type === 'shed') { if (type === 'shed') {
const r = calculateIsolatedShedRoof({ theta, height, depth }); const r = calculateIsolatedShedRoof({ theta, height, depth });
return { ...r, cpb: { cpb1: 0, cpb2: 0 }, cpa: { cpa1: 0, cpa2: 0 } }; return { ...r, cpb: { cpb1: 0, cpb2: 0 }, cpa: { cpa1: 0, cpa2: 0 } };
} }
if (type === 'butterfly') {
const b = calculateIsolatedButterflyRoof({ theta, height, depth });
return { ...b, cph1: { high: 0, low: 0 }, cph2: { high: 0, low: 0 } };
}
const g = calculateIsolatedGableRoof({ theta, height, depth }); const g = calculateIsolatedGableRoof({ theta, height, depth });
return { ...g, cph1: { high: 0, low: 0 }, cph2: { high: 0, low: 0 } }; return { ...g, cph1: { high: 0, low: 0 }, cph2: { high: 0, low: 0 } };
}, [type, theta, height, depth]); }, [type, theta, height, depth]);
@@ -79,12 +85,15 @@ const IsolatedRoofModule: React.FC = () => {
title: 'Geometria da Cobertura Isolada', title: 'Geometria da Cobertura Isolada',
type: 'grid', type: 'grid',
gridItems: [ gridItems: [
{ label: 'Tipo', value: type === 'shed' ? 'Uma água' : 'Duas águas' }, { label: 'Tipo', value: type === 'shed' ? 'Uma água' : type === 'gable' ? 'Duas águas' : 'Borboleta (Extrapolação)' },
{ label: 'Inclinação (θ)', value: `${theta}°` }, { label: 'Inclinação (θ)', value: `${theta}°` },
{ label: 'Largura (b)', value: `${width} m` }, { label: 'Largura (b)', value: `${width} m` },
{ label: 'Profundidade ()', value: `${depth} m` }, { label: 'Profundidade ()', value: `${depth} m` },
{ label: 'Altura livre (h)', value: `${height} m` }, { label: 'Altura livre (h)', value: `${height} m` },
{ label: 'Aplicável à norma', value: result.applies ? 'Sim' : 'Não' }, {
label: 'Aplicável à norma',
value: result.applies ? 'Sim' : type === 'butterfly' ? 'Extrapolado (Não há tabela)' : 'Não'
},
], ],
}, },
{ {
@@ -116,7 +125,7 @@ const IsolatedRoofModule: React.FC = () => {
{ {
title: '1. Coeficientes de Pressão Líquida (Cpn) e Carregamentos', title: '1. Coeficientes de Pressão Líquida (Cpn) e Carregamentos',
formula: 'Cpn = Cp_superior Cp_inferior (Tabelas 24 e 25 NBR 6123)', formula: 'Cpn = Cp_superior Cp_inferior (Tabelas 24 e 25 NBR 6123)',
calculation: `Para inclinação θ = ${theta}° (${type === 'shed' ? 'Uma água' : 'Duas águas'}), os coeficientes são definidos por zona e por carregamento máximo/mínimo.`, calculation: `Para inclinação θ = ${theta}° (${type === 'shed' ? 'Uma água' : type === 'gable' ? 'Duas águas' : 'Borboleta'}), os coeficientes são definidos por zona e por carregamento máximo/mínimo.`,
result: `θ = ${theta}° (Tab. ${type === 'shed' ? '24' : '25'})`, result: `θ = ${theta}° (Tab. ${type === 'shed' ? '24' : '25'})`,
note: 'Coberturas isoladas estão sujeitas a descolamento violento de fluxo, exigindo verificação para sucção (arrancamento) e compressão.', note: 'Coberturas isoladas estão sujeitas a descolamento violento de fluxo, exigindo verificação para sucção (arrancamento) e compressão.',
}, },
@@ -159,11 +168,14 @@ const IsolatedRoofModule: React.FC = () => {
<WindParametersSummary /> <WindParametersSummary />
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Tipo de Cobertura</label> <label className="text-sm font-medium">Tipo de Cobertura</label>
<Select value={type} onValueChange={(v) => setType(v as 'shed' | 'gable')}> <Select value={type} onValueChange={(v: any) => setType(v)}>
<SelectTrigger><SelectValue /></SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Tipo de Cobertura" />
</SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="shed">Uma água (Tab. 24)</SelectItem> <SelectItem value="shed">Uma Água</SelectItem>
<SelectItem value="gable">Duas águas (Tab. 25)</SelectItem> <SelectItem value="gable">Duas Águas</SelectItem>
<SelectItem value="butterfly">Telhado Borboleta (Em V)</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -200,7 +212,25 @@ const IsolatedRoofModule: React.FC = () => {
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative"> <div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
<div className="absolute top-4 left-4 z-10 flex gap-2"> <div className="absolute top-4 left-4 z-10 flex gap-2">
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/80 backdrop-blur-sm shadow-sm">θ = {theta}°</Badge> <Badge variant="secondary" className="bg-background/80 text-foreground border border-border/80 backdrop-blur-sm shadow-sm">θ = {theta}°</Badge>
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/80 backdrop-blur-sm shadow-sm">{type === 'shed' ? 'Uma água' : 'Duas águas'}</Badge> <Badge variant="secondary" className="bg-background/80 text-foreground border border-border/80 backdrop-blur-sm shadow-sm">{type === 'shed' ? 'Uma água' : type === 'gable' ? 'Duas águas' : 'Borboleta'}</Badge>
</div>
<div className="absolute top-4 right-16 z-10">
<div className="flex bg-background/80 backdrop-blur-sm border border-border/80 rounded-md shadow-sm p-1">
<button
onClick={() => setViewMode('solid')}
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'solid' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
>
<Box className="w-3.5 h-3.5 inline-block mr-1.5" />
Sólido
</button>
<button
onClick={() => setViewMode('airflow')}
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'airflow' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
>
<Wind className="w-3.5 h-3.5 inline-block mr-1.5" />
Fluxo
</button>
</div>
</div> </div>
<div className="absolute top-4 right-4 z-10"> <div className="absolute top-4 right-4 z-10">
<EducationalManual type="isolated-roof" params={{ theta, type }} /> <EducationalManual type="isolated-roof" params={{ theta, type }} />
@@ -216,6 +246,7 @@ const IsolatedRoofModule: React.FC = () => {
cpeLeeward={cpeLeeward} cpeLeeward={cpeLeeward}
cpeTop={cpeTop} cpeTop={cpeTop}
forceKN={forces.magnitude} forceKN={forces.magnitude}
viewMode={viewMode}
/> />
</div> </div>
+6 -3
View File
@@ -19,7 +19,7 @@ import { EducationalManual } from '@/components/EducationalManual';
import { WindParametersSummary } from '@/components/WindParametersSummary'; import { WindParametersSummary } from '@/components/WindParametersSummary';
import { SaveModuleDialog } from '@/components/SaveModuleDialog'; import { SaveModuleDialog } from '@/components/SaveModuleDialog';
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf'; import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
import { Eye, Box, Layers, ArrowUpRight, CheckCircle2 } from 'lucide-react'; import { Eye, Box, Layers, ArrowUpRight, CheckCircle2, Wind } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const conditionLabels: Record<ShelterCondition, string> = { const conditionLabels: Record<ShelterCondition, string> = {
@@ -41,7 +41,7 @@ const ShelterModule: React.FC = () => {
const [backRange, setBackRange] = useState<[number, number]>([0, 75]); const [backRange, setBackRange] = useState<[number, number]>([0, 75]);
const [leftClosure, setLeftClosure] = useState(100); const [leftClosure, setLeftClosure] = useState(100);
const [rightClosure, setRightClosure] = useState(100); const [rightClosure, setRightClosure] = useState(100);
const [viewMode, setViewMode] = useState<'3d' | 'elevation'>('3d'); const [viewMode, setViewMode] = useState<'3d' | 'elevation' | 'airflow'>('3d');
// Determinação automática da posição da fresta/vão a partir dos cursores (base e topo) // Determinação automática da posição da fresta/vão a partir dos cursores (base e topo)
const bottomGap = backRange[0]; const bottomGap = backRange[0];
@@ -502,6 +502,9 @@ const ShelterModule: React.FC = () => {
<Button size="sm" variant={viewMode === '3d' ? 'default' : 'ghost'} className="h-6 px-2.5 text-xs rounded-full" onClick={() => setViewMode('3d')}> <Button size="sm" variant={viewMode === '3d' ? 'default' : 'ghost'} className="h-6 px-2.5 text-xs rounded-full" onClick={() => setViewMode('3d')}>
<Box className="size-3 mr-1" /> Vista 3D <Box className="size-3 mr-1" /> Vista 3D
</Button> </Button>
<Button size="sm" variant={viewMode === 'airflow' ? 'default' : 'ghost'} className="h-6 px-2.5 text-xs rounded-full" onClick={() => setViewMode('airflow')}>
<Wind className="size-3 mr-1" /> Fluxo
</Button>
<Button size="sm" variant={viewMode === 'elevation' ? 'default' : 'ghost'} className="h-6 px-2.5 text-xs rounded-full" onClick={() => setViewMode('elevation')}> <Button size="sm" variant={viewMode === 'elevation' ? 'default' : 'ghost'} className="h-6 px-2.5 text-xs rounded-full" onClick={() => setViewMode('elevation')}>
<Eye className="size-3 mr-1" /> Corte A-A <Eye className="size-3 mr-1" /> Corte A-A
</Button> </Button>
@@ -512,7 +515,7 @@ const ShelterModule: React.FC = () => {
<EducationalManual type="shelter" params={{ condition, backClosure, openingPos, theta, windAngle }} /> <EducationalManual type="shelter" params={{ condition, backClosure, openingPos, theta, windAngle }} />
</div> </div>
{viewMode === '3d' ? ( {viewMode === '3d' || viewMode === 'airflow' ? (
<Shelter3D <Shelter3D
condition={condition} condition={condition}
depth={depth} depth={depth}
+24 -3
View File
@@ -1,4 +1,5 @@
import React, { useMemo } from 'react'; import React, { useState, useMemo } from 'react';
import { Wind, Box } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Slider } from '@/components/ui/slider'; import { Slider } from '@/components/ui/slider';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
@@ -19,9 +20,10 @@ const TowerModule: React.FC = () => {
const [barType, setBarType] = React.useState<TowerBarType>('flat'); const [barType, setBarType] = React.useState<TowerBarType>('flat');
const [baseWidth, setBaseWidth] = React.useState(3); const [baseWidth, setBaseWidth] = React.useState(3);
const [height, setHeight] = React.useState(30); const [height, setHeight] = React.useState(30);
const [panels, setPanels] = React.useState(6); const [panels, setPanels] = React.useState(5);
const [phi, setPhi] = React.useState(0.3); const [phi, setPhi] = React.useState(0.2);
const [alphaWind, setAlphaWind] = React.useState<0 | 45 | 90>(0); const [alphaWind, setAlphaWind] = React.useState<0 | 45 | 90>(0);
const [viewMode, setViewMode] = useState<'solid' | 'airflow'>('solid');
const result = useMemo(() => { const result = useMemo(() => {
const aFace = baseWidth * height; const aFace = baseWidth * height;
@@ -178,6 +180,24 @@ const TowerModule: React.FC = () => {
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/50 backdrop-blur-sm shadow-sm">{section === 'square' ? 'Quadrada' : 'Triangular'}</Badge> <Badge variant="secondary" className="bg-background/80 text-foreground border border-border/50 backdrop-blur-sm shadow-sm">{section === 'square' ? 'Quadrada' : 'Triangular'}</Badge>
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/50 backdrop-blur-sm shadow-sm">α = {alphaWind}°</Badge> <Badge variant="secondary" className="bg-background/80 text-foreground border border-border/50 backdrop-blur-sm shadow-sm">α = {alphaWind}°</Badge>
</div> </div>
<div className="absolute top-4 right-16 z-10">
<div className="flex bg-background/80 backdrop-blur-sm border border-border/80 rounded-md shadow-sm p-1">
<button
onClick={() => setViewMode('solid')}
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'solid' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
>
<Box className="w-3.5 h-3.5 inline-block mr-1.5" />
Sólido
</button>
<button
onClick={() => setViewMode('airflow')}
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'airflow' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
>
<Wind className="w-3.5 h-3.5 inline-block mr-1.5" />
Fluxo
</button>
</div>
</div>
<div className="absolute top-4 right-4 z-10"> <div className="absolute top-4 right-4 z-10">
<EducationalManual type="tower" params={{ section, alphaWind }} /> <EducationalManual type="tower" params={{ section, alphaWind }} />
</div> </div>
@@ -190,6 +210,7 @@ const TowerModule: React.FC = () => {
phi={phi} phi={phi}
alphaWind={alphaWind} alphaWind={alphaWind}
forceKN={result.forceKN} forceKN={result.forceKN}
viewMode={viewMode}
/> />
</div> </div>