diff --git a/app/src/components/FallbackDiagram.tsx b/app/src/components/FallbackDiagram.tsx index 170e88c..0fc7e48 100644 --- a/app/src/components/FallbackDiagram.tsx +++ b/app/src/components/FallbackDiagram.tsx @@ -400,7 +400,7 @@ function TowerDiagram({ baseWidth, height, panels, phi, forceKN }: TowerProps) { } interface IsolatedRoofProps { - type: 'shed' | 'gable'; + type: 'shed' | 'gable' | 'butterfly'; theta: number; height: number; width: number; diff --git a/app/src/components/three/AirflowSystem.tsx b/app/src/components/three/AirflowSystem.tsx index 7f12fa3..48a80ba 100644 --- a/app/src/components/three/AirflowSystem.tsx +++ b/app/src/components/three/AirflowSystem.tsx @@ -132,9 +132,9 @@ export function AirflowSystem({ windAngle, width, length, height, permeabilityCa if (inY && inX && inZ) { // Perto das paredes 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.D; + else if (Math.abs(px - (halfW)) < margin) localCpe = (windAngle === 0 ? 0.5 : wallCpe.D); else localCpe = cpi; // 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); 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) { 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 { - colorObj.set(isDark ? '#f1f5f9' : '#64748b'); + colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre) } meshRef.current!.setColorAt(i, colorObj); }); diff --git a/app/src/components/three/Bridge3D.tsx b/app/src/components/three/Bridge3D.tsx index ee48946..8a37cbf 100644 --- a/app/src/components/three/Bridge3D.tsx +++ b/app/src/components/three/Bridge3D.tsx @@ -6,6 +6,7 @@ import SceneCanvas from '../SceneCanvas'; import FallbackDiagram from '../FallbackDiagram'; import { WindArrow } from './WindArrow'; import { useCanvasTheme } from '@/lib/theme'; +import { BridgeAirflowSystem } from './BridgeAirflowSystem'; export interface Bridge3DInput { /** Maior vão Lₚ (m) */ @@ -26,6 +27,8 @@ export interface Bridge3DInput { fzPerLength: number; /** Ângulo de ataque do vento (graus) */ alpha: number; + /** Modo de visualização */ + viewMode?: 'solid' | 'airflow'; } function BridgeModel({ @@ -34,10 +37,15 @@ function BridgeModel({ deckHeight, heg, cx, + cz, + fxPerLength, + fzPerLength, alpha, + viewMode = 'solid', }: Bridge3DInput) { const theme = useCanvasTheme(); const isDark = theme === 'dark'; + const isAirflow = viewMode === 'airflow'; const halfL = lp / 2; const halfW = width / 2; @@ -46,33 +54,34 @@ function BridgeModel({ // Cor do tabuleiro baseada em Cx const deckColor = useMemo(() => { + if (isAirflow) return new THREE.Color(isDark ? '#334155' : '#e2e8f0'); const intensity = Math.min(1, Math.abs(cx) / 3); const hue = 200 - intensity * 60; const l = isDark ? (60 - intensity * 8) : (50 - intensity * 8); 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 const pillarHeights = useMemo(() => [deckY - 0.5, deckY - 0.5, deckY - 0.5], [deckY]); - const barrierColor = isDark ? '#cbd5e1' : '#94a3b8'; - const pillarColor = isDark ? '#94a3b8' : '#64748b'; + const barrierColor = isDark ? (isAirflow ? '#475569' : '#cbd5e1') : (isAirflow ? '#cbd5e1' : '#94a3b8'); + const pillarColor = isDark ? (isAirflow ? '#334155' : '#94a3b8') : (isAirflow ? '#e2e8f0' : '#64748b'); return ( {/* Tabuleiro (deck) */} - + {/* Guarda-rodas/barreira lateral */} - + - + {/* Pilares (3 ao longo do comprimento) */} @@ -87,53 +96,68 @@ function BridgeModel({ })} {/* Solo / água */} - - - - + {!isAirflow && ( + + + + + )} - {/* Seta de Vento (bate no meio do tabuleiro) */} - - - Lp={lp}m | B={width}m | Cx={cx.toFixed(2)} - + {/* Seta de Vento */} + {isAirflow ? ( + + + + Vento + + + + ) : ( + + )} ); } -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 isDark = theme === 'dark'; - const { lp, deckHeight, width } = input; - const dist = Math.max(lp * 0.6, deckHeight * 2); const fallback = ( ); return ( - - + = ({ + width, + lp, + deckHeight, + cx, +}) => { + const meshRef = useRef(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 ( + + + + + ); +}; diff --git a/app/src/components/three/CylinderAirflowSystem.tsx b/app/src/components/three/CylinderAirflowSystem.tsx index d358ecd..6a1254f 100644 --- a/app/src/components/three/CylinderAirflowSystem.tsx +++ b/app/src/components/three/CylinderAirflowSystem.tsx @@ -95,7 +95,7 @@ export function CylinderAirflowSystem({ diameter, height }: CylinderAirflowSyste } else if (Math.abs(px) <= R * 0.6 && r < R * 1.5) { p.currentCpe = -1.2; // Laterais (forte sucção) } 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 { p.currentCpe = 0; } @@ -123,16 +123,12 @@ export function CylinderAirflowSystem({ diameter, height }: CylinderAirflowSyste meshRef.current!.setMatrixAt(i, dummy.matrix); 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) { 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 { - colorObj.set(isDark ? '#f1f5f9' : '#64748b'); + colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre) } meshRef.current!.setColorAt(i, colorObj); }); diff --git a/app/src/components/three/Dome3D.tsx b/app/src/components/three/Dome3D.tsx index f205171..1d63ca1 100644 --- a/app/src/components/three/Dome3D.tsx +++ b/app/src/components/three/Dome3D.tsx @@ -133,7 +133,7 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp })} {/* Seta indicativa de direção do vento */} - + @@ -146,7 +146,7 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp {/* Texto Vento */} wallHeight) { 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 { - p.currentCpe = -0.5; // Lateral / Sotavento + p.currentCpe = -0.5; // Laterais / transição } } else { p.currentCpe = 0; @@ -151,16 +153,12 @@ export function DomeAirflowSystem({ diameter, rise, wallHeight }: DomeAirflowSys meshRef.current!.setMatrixAt(i, dummy.matrix); 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) { 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 { - colorObj.set(isDark ? '#f1f5f9' : '#64748b'); + colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre) } meshRef.current!.setColorAt(i, colorObj); }); diff --git a/app/src/components/three/IsolatedRoof3D.tsx b/app/src/components/three/IsolatedRoof3D.tsx index 7b9e560..7606a19 100644 --- a/app/src/components/three/IsolatedRoof3D.tsx +++ b/app/src/components/three/IsolatedRoof3D.tsx @@ -4,12 +4,12 @@ import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls'; import * as THREE from 'three'; import SceneCanvas from '../SceneCanvas'; import FallbackDiagram from '../FallbackDiagram'; -import { WindArrow } from './WindArrow'; import { useCanvasTheme } from '@/lib/theme'; +import { IsolatedRoofAirflowSystem } from './IsolatedRoofAirflowSystem'; export interface IsolatedRoof3DInput { - /** Tipo de cobertura: 'shed' (uma água) ou 'gable' (duas águas) */ - type: 'shed' | 'gable'; + /** Tipo de cobertura: 'shed' (uma água), 'gable' (duas águas) ou 'butterfly' (borboleta) */ + type: 'shed' | 'gable' | 'butterfly'; /** Inclinação θ (graus) */ theta: number; /** Altura livre dos suportes (m) */ @@ -26,6 +26,7 @@ export interface IsolatedRoof3DInput { cpeTop: number; /** Força resultante na cobertura (kN) */ forceKN: number; + viewMode?: 'solid' | 'airflow'; } function pressureColor(cpe: number, isDark: boolean): THREE.Color { @@ -50,9 +51,11 @@ function IsolatedRoofModel({ cpeWindward, cpeLeeward, forceKN, + viewMode = 'solid', }: IsolatedRoof3DInput) { const theme = useCanvasTheme(); const isDark = theme === 'dark'; + const isAirflow = viewMode === 'airflow'; const labelColor = isDark ? '#cbd5e1' : '#475569'; const textColor = isDark ? '#93c5fd' : '#1a202c'; 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 }, ); - } else { + } else if (type === 'gable') { list.push( { 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 }, ); + } 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; }, [type, depth, width, height, h_diff, h_half, halfDepth, halfWidth]); @@ -106,25 +118,9 @@ function IsolatedRoofModel({ {type === 'shed' ? ( // Uma água (Shed): dividida em metade barlavento e metade sotavento - {/* Metade Barlavento (Z < 0) */} - - - - - {/* Metade Sotavento (Z > 0) */} - - - + + + ) : ( @@ -133,22 +129,22 @@ function IsolatedRoofModel({ {/* Água Esquerda / Barlavento (Z < 0) */} - + {/* Água Direita / Sotavento (Z > 0) */} - - + )} @@ -161,11 +157,27 @@ function IsolatedRoofModel({ ))} - {/* Vetor de vento (Seta verde/azul batendo no elemento) */} - + {/* Vetor de vento (Seta mais visível com texto) */} + + + + + + + + + + + + Vento + {/* === LINHAS DE COTA (CAD-Style) === */} {/* Cota de Altura (h) */} @@ -248,6 +260,8 @@ function IsolatedRoofModel({ {/* Rótulo Superior */} @@ -303,7 +319,19 @@ export default function IsolatedRoof3DViewer({ cpeLeeward={cpeLeeward} cpeTop={cpeTop} forceKN={forceKN} + viewMode={viewMode} /> + {viewMode === 'airflow' && ( + + )} diff --git a/app/src/components/three/IsolatedRoofAirflowSystem.tsx b/app/src/components/three/IsolatedRoofAirflowSystem.tsx new file mode 100644 index 0000000..58c8121 --- /dev/null +++ b/app/src/components/three/IsolatedRoofAirflowSystem.tsx @@ -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 = ({ + type, + theta, + height, + width, + depth, + cpeWindward, + cpeLeeward, +}) => { + const meshRef = useRef(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 ( + + + + + ); +}; diff --git a/app/src/components/three/Shelter3D.tsx b/app/src/components/three/Shelter3D.tsx index fb45b77..c176d20 100644 --- a/app/src/components/three/Shelter3D.tsx +++ b/app/src/components/three/Shelter3D.tsx @@ -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(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 ( - - - - - - - ); -}; - 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) */} - - {/* Vento de entrada (Barlavento) entrando em direção ao abrigo */} - - - - - {/* Linhas de corrente sobre o telhado */} - - - {/* 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)) && ( - <> - - - Alívio (Fresta Topo) - - - )} - - {/* Fresta na base */} - {backClosure > 0 && backClosure < 100 && (openingPos === 'bottom' || (backRange && backRange[0] > 2)) && ( - <> - - - Vão Livre Inferior - - - )} + {/* Seta de Vento Global (Sempre visível para não pular de posição) */} + + + + Vento + + {/* Partículas de Ar */} + {isAirflow && ( + + + + )} + {/* Textos Informativos 3D - Virados para quem olha da frente (-Z) */} - - {isElevation ? 'CORTE A-A (Elevação)' : 'Abrigo / Pórtico (NBR 6123)'} - - - {`VENTO ${windAngle === 0 ? 'FRONTAL (0°)' : windAngle === 45 ? 'OBLÍQUO (45°)' : 'LATERAL (90°)'}`} - - - Arrancamento [+Fz ↑] - - - Empuxo no Fundo [+Fx →] - + {!isAirflow && ( + <> + + {isElevation ? 'CORTE A-A (Elevação)' : 'Abrigo / Pórtico (NBR 6123)'} + + + Arrancamento [+Fz ↑] + + + Empuxo no Fundo [+Fx →] + + + )} ); } @@ -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 ( 3D indisponível} diff --git a/app/src/components/three/ShelterAirflowSystem.tsx b/app/src/components/three/ShelterAirflowSystem.tsx new file mode 100644 index 0000000..e80487b --- /dev/null +++ b/app/src/components/three/ShelterAirflowSystem.tsx @@ -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 = ({ + width, + depth, + height, + theta, +}) => { + const meshRef = useRef(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 ( + + + + + ); +}; diff --git a/app/src/components/three/SignAirflowSystem.tsx b/app/src/components/three/SignAirflowSystem.tsx index 1640658..8a383a0 100644 --- a/app/src/components/three/SignAirflowSystem.tsx +++ b/app/src/components/three/SignAirflowSystem.tsx @@ -152,16 +152,12 @@ export function SignAirflowSystem({ length, height, groundClearance, alpha, cf } meshRef.current!.setMatrixAt(i, dummy.matrix); 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) { 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 { - colorObj.set(isDark ? '#f1f5f9' : '#64748b'); + colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre) } meshRef.current!.setColorAt(i, colorObj); }); diff --git a/app/src/components/three/Tower3D.tsx b/app/src/components/three/Tower3D.tsx index 4813a7a..1b9cf33 100644 --- a/app/src/components/three/Tower3D.tsx +++ b/app/src/components/three/Tower3D.tsx @@ -5,6 +5,7 @@ import * as THREE from 'three'; import SceneCanvas from '../SceneCanvas'; import { WindArrow } from './WindArrow'; import FallbackDiagram from '../FallbackDiagram'; +import { TowerAirflowSystem } from './TowerAirflowSystem'; export interface Tower3DInput { /** Forma da seção */ @@ -23,6 +24,8 @@ export interface Tower3DInput { alphaWind: 0 | 45 | 90; /** Força total estimada na torre (kN) */ forceKN: number; + /** Modo de visualização */ + viewMode?: 'solid' | 'airflow'; } /** @@ -136,9 +139,11 @@ function TowerModel({ phi, alphaWind, forceKN, + viewMode = 'solid', }: Tower3DInput) { const theme = useCanvasTheme(); const isDark = theme === 'dark'; + const isAirflow = viewMode === 'airflow'; const geometry = useMemo( () => buildTowerGeometry(section, panels, baseWidth, height), @@ -201,11 +206,32 @@ function TowerModel({ ); })} - {/* Seta de Vento única atingindo o meio da torre */} - + {/* Seta de Vento */} + {isAirflow ? ( + + + + Vento + + + + ) : ( + + )} {/* Labels indicativos */} @@ -228,7 +254,7 @@ function TowerModel({ export default function Tower3DViewer(input: Tower3DInput) { const theme = useCanvasTheme(); const isDark = theme === 'dark'; - const { baseWidth, height } = input; + const { baseWidth, height, viewMode = 'solid' } = input; const dist = Math.max(baseWidth * 3, height * 1.2); const fallback = ( @@ -241,13 +267,14 @@ export default function Tower3DViewer(input: Tower3DInput) { return ( - + = ({ + baseWidth, + height, + phi, +}) => { + const meshRef = useRef(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 ( + + + + + ); +}; diff --git a/app/src/components/three/Vault3D.tsx b/app/src/components/three/Vault3D.tsx index 1c52ba0..2ca2885 100644 --- a/app/src/components/three/Vault3D.tsx +++ b/app/src/components/three/Vault3D.tsx @@ -175,6 +175,30 @@ function VaultModel({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDire + {/* Seta indicativa de direção do vento */} + + + + + + + + + + + + {/* Texto Vento */} + + Vento + + {/* Rótulo de dimensões */} 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) { 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 { - colorObj.set(isDark ? '#f1f5f9' : '#64748b'); + colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre) } meshRef.current!.setColorAt(i, colorObj); }); diff --git a/app/src/lib/nbr-tables/table-24-25.ts b/app/src/lib/nbr-tables/table-24-25.ts index c6d9d1b..541aa49 100644 --- a/app/src/lib/nbr-tables/table-24-25.ts +++ b/app/src/lib/nbr-tables/table-24-25.ts @@ -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) */ export function frictionForceIsolatedRoof(q: number, a: number, b: number): number { return Number((0.05 * q * a * b).toFixed(3)); diff --git a/app/src/pages/BridgeModule.tsx b/app/src/pages/BridgeModule.tsx index aa84302..165f721 100644 --- a/app/src/pages/BridgeModule.tsx +++ b/app/src/pages/BridgeModule.tsx @@ -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 { useHydrationStore } from '@/store/hydrationStore'; @@ -47,6 +48,7 @@ const BridgeModule: React.FC = () => { const [fv, setFv] = React.useState(0.6); const [vk, setVk] = React.useState(45); 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 classification = useMemo( @@ -214,6 +216,24 @@ const BridgeModule: React.FC = () => { Classe {classification.bridgeClass} +
+
+ + +
+
@@ -227,6 +247,7 @@ const BridgeModule: React.FC = () => { fxPerLength={forces.fxPerLength} fzPerLength={forces.fzPerLength} alpha={alpha} + viewMode={viewMode} /> diff --git a/app/src/pages/IsolatedRoofModule.tsx b/app/src/pages/IsolatedRoofModule.tsx index 8a3771c..8cce3d7 100644 --- a/app/src/pages/IsolatedRoofModule.tsx +++ b/app/src/pages/IsolatedRoofModule.tsx @@ -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 { Slider } from '@/components/ui/slider'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; 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 SceneCapturePanel from '../components/SceneCapturePanel'; import ExportMenu from '../components/ExportMenu'; @@ -17,17 +18,22 @@ import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generi const IsolatedRoofModule: React.FC = () => { 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 [height, setHeight] = React.useState(2); - const [width, setWidth] = React.useState(10); - const [depth, setDepth] = React.useState(10); + const [width, setWidth] = useState(10); + const [depth, setDepth] = useState(10); + const [viewMode, setViewMode] = useState<'solid' | 'airflow'>('solid'); const result = useMemo(() => { if (type === 'shed') { const r = calculateIsolatedShedRoof({ theta, height, depth }); 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 }); return { ...g, cph1: { high: 0, low: 0 }, cph2: { high: 0, low: 0 } }; }, [type, theta, height, depth]); @@ -79,12 +85,15 @@ const IsolatedRoofModule: React.FC = () => { title: 'Geometria da Cobertura Isolada', type: 'grid', 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: 'Largura (b)', value: `${width} m` }, { label: 'Profundidade (ℓ)', value: `${depth} 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', 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'})`, 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 = () => {
- setType(v)}> + + + - Uma água (Tab. 24) - Duas águas (Tab. 25) + Uma Água + Duas Águas + Telhado Borboleta (Em V)
@@ -200,7 +212,25 @@ const IsolatedRoofModule: React.FC = () => {
θ = {theta}° - {type === 'shed' ? 'Uma água' : 'Duas águas'} + {type === 'shed' ? 'Uma água' : type === 'gable' ? 'Duas águas' : 'Borboleta'} +
+
+
+ + +
@@ -216,6 +246,7 @@ const IsolatedRoofModule: React.FC = () => { cpeLeeward={cpeLeeward} cpeTop={cpeTop} forceKN={forces.magnitude} + viewMode={viewMode} />
diff --git a/app/src/pages/ShelterModule.tsx b/app/src/pages/ShelterModule.tsx index 4ada2b1..ed0786c 100644 --- a/app/src/pages/ShelterModule.tsx +++ b/app/src/pages/ShelterModule.tsx @@ -19,7 +19,7 @@ import { EducationalManual } from '@/components/EducationalManual'; import { WindParametersSummary } from '@/components/WindParametersSummary'; import { SaveModuleDialog } from '@/components/SaveModuleDialog'; 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'; const conditionLabels: Record = { @@ -41,7 +41,7 @@ const ShelterModule: React.FC = () => { const [backRange, setBackRange] = useState<[number, number]>([0, 75]); const [leftClosure, setLeftClosure] = 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) const bottomGap = backRange[0]; @@ -502,6 +502,9 @@ const ShelterModule: React.FC = () => { + @@ -512,7 +515,7 @@ const ShelterModule: React.FC = () => {
- {viewMode === '3d' ? ( + {viewMode === '3d' || viewMode === 'airflow' ? ( { const [barType, setBarType] = React.useState('flat'); const [baseWidth, setBaseWidth] = React.useState(3); const [height, setHeight] = React.useState(30); - const [panels, setPanels] = React.useState(6); - const [phi, setPhi] = React.useState(0.3); + const [panels, setPanels] = React.useState(5); + const [phi, setPhi] = React.useState(0.2); const [alphaWind, setAlphaWind] = React.useState<0 | 45 | 90>(0); + const [viewMode, setViewMode] = useState<'solid' | 'airflow'>('solid'); const result = useMemo(() => { const aFace = baseWidth * height; @@ -178,6 +180,24 @@ const TowerModule: React.FC = () => { {section === 'square' ? 'Quadrada' : 'Triangular'} α = {alphaWind}° +
+
+ + +
+
@@ -190,6 +210,7 @@ const TowerModule: React.FC = () => { phi={phi} alphaWind={alphaWind} forceKN={result.forceKN} + viewMode={viewMode} />