From fde885ca6024247e2b7e2f2f58cba6c3f67b5fcf Mon Sep 17 00:00:00 2001 From: Marcos Date: Wed, 29 Jul 2026 10:44:07 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Auto-deploy:=20BrainWind=20atual?= =?UTF-8?q?izado=20em=2029/07/2026=2010:44:07?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/components/three/Dome3D.tsx | 38 ++-- .../components/three/DomeAirflowSystem.tsx | 186 ++++++++++++++++ app/src/components/three/Sign3D.tsx | 22 +- .../components/three/SignAirflowSystem.tsx | 187 ++++++++++++++++ app/src/components/three/Vault3D.tsx | 30 ++- .../components/three/VaultAirflowSystem.tsx | 205 ++++++++++++++++++ app/src/pages/DomeModule.tsx | 26 ++- app/src/pages/SignModule.tsx | 26 ++- app/src/pages/VaultModule.tsx | 26 ++- 9 files changed, 719 insertions(+), 27 deletions(-) create mode 100644 app/src/components/three/DomeAirflowSystem.tsx create mode 100644 app/src/components/three/SignAirflowSystem.tsx create mode 100644 app/src/components/three/VaultAirflowSystem.tsx diff --git a/app/src/components/three/Dome3D.tsx b/app/src/components/three/Dome3D.tsx index 0066bc2..f205171 100644 --- a/app/src/components/three/Dome3D.tsx +++ b/app/src/components/three/Dome3D.tsx @@ -5,6 +5,7 @@ import * as THREE from 'three'; import SceneCanvas from '../SceneCanvas'; import FallbackDiagram from '../FallbackDiagram'; import { useCanvasTheme } from '@/lib/theme'; +import { DomeAirflowSystem } from './DomeAirflowSystem'; export interface Dome3DInput { diameter: number; @@ -14,6 +15,7 @@ export interface Dome3DInput { cpeBarlavento: number; cpeTopo: number; cpeLateral: number; + viewMode?: 'solid' | 'airflow'; } function domeColor(cpe: number, cpi: number, isDark: boolean): THREE.Color { @@ -26,9 +28,10 @@ function domeColor(cpe: number, cpi: number, isDark: boolean): THREE.Color { return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, l + 5)}%)`); } -function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cpeLateral }: Dome3DInput) { +function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cpeLateral, viewMode = 'solid' }: Dome3DInput) { const theme = useCanvasTheme(); const isDark = theme === 'dark'; + const isAirflow = viewMode === 'airflow'; const radius = diameter / 2; const segments = 64; @@ -64,7 +67,7 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp {/* Parede cilíndrica inferior */} - + {/* Detalhes de anéis metálicos nas bordas */} @@ -124,7 +127,7 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp - + ); })} @@ -139,22 +142,24 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp - - Vento - + {/* Texto Vento */} + + Vento + + {/* Texto informativo */} + {viewMode === 'airflow' && ( + + )} (null); + const R = diameter / 2; + const H = wallHeight + rise; + + const particles = useMemo(() => { + const temp = []; + const spanX = Math.max(15, R * 5); + const spanZ = Math.max(12, R * 4); + for (let i = 0; i < count; i++) { + temp.push({ + position: new THREE.Vector3( + (Math.random() - 0.5) * spanX * 2, + Math.random() * (H * 1.4), + (Math.random() - 0.5) * spanZ * 2 + ), + baseSpeed: 0.12 + Math.random() * 0.08, + wobbleSpeed: Math.random() * 0.05, + wobbleOffset: Math.random() * Math.PI * 2, + currentCpe: 0, + }); + } + return temp; + }, [count, R, H]); + + const dummy = useMemo(() => new THREE.Object3D(), []); + const colorObj = useMemo(() => new THREE.Color(), []); + + useEffect(() => { + if (meshRef.current) { + for (let i = 0; i < count; i++) { + meshRef.current.setColorAt(i, new THREE.Color('#ffffff')); + } + if (meshRef.current.instanceColor) { + meshRef.current.instanceColor.needsUpdate = true; + } + } + }, [count]); + + useFrame((state) => { + if (!meshRef.current) return; + const time = state.clock.elapsedTime; + const spanX = Math.max(15, R * 5); + const spanZ = Math.max(12, R * 4); + + // Parâmetros da esfera da calota + const rSphere = (R * R + rise * rise) / (2 * rise); + const centerY = wallHeight - (rSphere - rise); + + particles.forEach((p, i) => { + let deflectX = 1.0; + let deflectY = 0.0; + let deflectZ = 0.0; + + const px = p.position.x; + const py = p.position.y; + const pz = p.position.z; + + const r = Math.sqrt(px * px + pz * pz); + const safeR = Math.max(R + 0.3, r); + + // Colisão / Deflexão com a parede cilíndrica inferior + if (py <= wallHeight) { + if (r < R + 0.25) { + const angle = Math.atan2(pz, px); + p.position.x = Math.cos(angle) * (R + 0.25); + p.position.z = Math.sin(angle) * (R + 0.25); + } + + if (r < R * 3.0) { + const r2 = safeR * safeR; + const R2 = R * R; + deflectX = 1 - (R2 * (px * px - pz * pz)) / (r2 * r2); + deflectZ = -(2 * R2 * px * pz) / (r2 * r2); + } + } + // Colisão / Deflexão com a calota esférica (Cúpula) + else if (py > wallHeight && py <= H + 2.0) { + // Distância até o centro virtual da calota + const dySphere = py - centerY; + const distToSphere = Math.sqrt(px * px + dySphere * dySphere + pz * pz); + + if (distToSphere < rSphere + 0.25) { + // Normal da esfera + const nx = px / distToSphere; + const ny = dySphere / distToSphere; + const nz = pz / distToSphere; + + // Reposiciona na casca externa + p.position.x = nx * (rSphere + 0.25); + p.position.y = centerY + ny * (rSphere + 0.25); + p.position.z = nz * (rSphere + 0.25); + + // Direciona o vento para subir + deflectY = ny * 1.2; + deflectX = nx * 1.2; + deflectZ = nz * 0.5; + } + } + + // Esteira de sucção sotavento + if (px > R * 0.8 && Math.abs(pz) < R * 1.5 && py <= H) { + deflectZ += Math.sin(time * 5 + px * 0.8 + p.wobbleOffset) * 0.25; + deflectY -= 0.1; + } + + // Mapear Coeficiente Cpe local para cor + if (py <= H + 0.5 && r < R * 2.2) { + if (px < -R * 0.3) { + p.currentCpe = 0.6; // Barlavento + } else if (Math.abs(px) <= R * 0.4 && py > wallHeight) { + p.currentCpe = -1.0; // Topo da cúpula (forte sucção) + } else { + p.currentCpe = -0.5; // Lateral / Sotavento + } + } else { + p.currentCpe = 0; + } + + const moveVec = new THREE.Vector3(deflectX, deflectY, deflectZ).normalize(); + const currentSpeed = p.baseSpeed * Math.max(0.3, Math.min(1.5, deflectX)); + + p.position.addScaledVector(moveVec, currentSpeed); + + // Reposição quando sai do volume + if (p.position.x > spanX) { + p.position.x = -spanX; + p.position.y = Math.random() * (H * 1.4); + p.position.z = (Math.random() - 0.5) * spanZ * 2; + p.currentCpe = 0; + } + + dummy.position.copy(p.position); + dummy.lookAt(p.position.clone().add(moveVec)); + 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 ? '#ef4444' : '#dc2626') + .lerp(new THREE.Color(isDark ? '#fcd34d' : '#f59e0b'), 1 - Math.min(1, p.currentCpe)); + } 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); + } else { + colorObj.set(isDark ? '#f1f5f9' : '#64748b'); + } + meshRef.current!.setColorAt(i, colorObj); + }); + + meshRef.current.instanceMatrix.needsUpdate = true; + if (meshRef.current.instanceColor) { + meshRef.current.instanceColor.needsUpdate = true; + } + }); + + return ( + + + + + ); +} diff --git a/app/src/components/three/Sign3D.tsx b/app/src/components/three/Sign3D.tsx index 99dd955..ac1b184 100644 --- a/app/src/components/three/Sign3D.tsx +++ b/app/src/components/three/Sign3D.tsx @@ -6,6 +6,7 @@ import SceneCanvas from '../SceneCanvas'; import FallbackDiagram from '../FallbackDiagram'; import { WindArrow } from './WindArrow'; import { useCanvasTheme } from '@/lib/theme'; +import { SignAirflowSystem } from './SignAirflowSystem'; export interface Sign3DInput { /** Comprimento ℓ (m) */ @@ -22,6 +23,7 @@ export interface Sign3DInput { forceKN: number; /** Excentricidade e (m) */ applicationPoint: number; + viewMode?: 'solid' | 'airflow'; } /** @@ -35,9 +37,11 @@ function SignModel({ alpha, cf, applicationPoint, + viewMode = 'solid', }: Sign3DInput) { const theme = useCanvasTheme(); const isDark = theme === 'dark'; + const isAirflow = viewMode === 'airflow'; const baseY = groundClearance; const topY = baseY + height; const halfL = length / 2; @@ -67,7 +71,7 @@ function SignModel({ {/* Placa principal */} - + {/* Placas de extremidade (retornos aerodinâmicos nas pontas) */} @@ -75,11 +79,11 @@ function SignModel({ <> - + - + )} @@ -206,6 +210,7 @@ export default function Sign3DViewer({ cf, forceKN, applicationPoint, + viewMode = 'solid', }: Sign3DInput) { const theme = useCanvasTheme(); const isDark = theme === 'dark'; @@ -222,6 +227,7 @@ export default function Sign3DViewer({ return ( + {viewMode === 'airflow' && ( + + )} (null); + const baseY = groundClearance; + const topY = baseY + height; + const halfL = length / 2; + + const rad = ((90 - alpha) * Math.PI) / 180; + const windDir = useMemo(() => new THREE.Vector3(Math.cos(rad), 0, Math.sin(rad)).normalize(), [rad]); + + const particles = useMemo(() => { + const temp = []; + const limitX = Math.max(15, length * 1.5); + const limitZ = Math.max(15, length * 1.5); + for (let i = 0; i < count; i++) { + // Spawn no plano do vento + temp.push({ + position: new THREE.Vector3( + (Math.random() - 0.5) * limitX * 2, + Math.random() * (topY * 1.6), + (Math.random() - 0.5) * limitZ * 2 + ), + baseSpeed: 0.12 + Math.random() * 0.08, + wobbleSpeed: Math.random() * 0.05, + wobbleOffset: Math.random() * Math.PI * 2, + currentCpe: 0, + }); + } + return temp; + }, [count, length, topY]); + + const dummy = useMemo(() => new THREE.Object3D(), []); + const colorObj = useMemo(() => new THREE.Color(), []); + + useEffect(() => { + if (meshRef.current) { + for (let i = 0; i < count; i++) { + meshRef.current.setColorAt(i, new THREE.Color('#ffffff')); + } + if (meshRef.current.instanceColor) { + meshRef.current.instanceColor.needsUpdate = true; + } + } + }, [count]); + + useFrame((state) => { + if (!meshRef.current) return; + const time = state.clock.elapsedTime; + const limitX = Math.max(15, length * 1.5); + const limitZ = Math.max(15, length * 1.5); + + particles.forEach((p, i) => { + let deflectX = windDir.x; + let deflectY = 0.0; + let deflectZ = windDir.z; + + const px = p.position.x; + const py = p.position.y; + const pz = p.position.z; + + // Verifica se a partícula está prestes a colidir com a placa (X=0) + const isInsideY = py >= baseY && py <= topY; + const isInsideZ = pz >= -halfL && pz <= halfL; + + if (Math.abs(px) < 1.8 && isInsideY && isInsideZ) { + // Distância e fator de proximidade + const dist = Math.abs(px); + const force = Math.max(0, 1.8 - dist); // maior quanto mais perto de X = 0 + + // Se está vindo a barlavento, desvia para as bordas + if (px < 0) { + // Desvio em Y (por cima do topo ou por baixo do solo) + const distToTop = Math.abs(py - topY); + const distToBottom = Math.abs(py - baseY); + if (distToTop < distToBottom || baseY < 0.3) { + deflectY += force * 0.25; + } else { + deflectY -= force * 0.25; + } + + // Desvio em Z (pelas laterais da placa) + if (pz > 0) { + deflectZ += force * 0.3; + } else { + deflectZ -= force * 0.3; + } + + // Reduz a velocidade frontal (X) para simular desaceleração do ponto de estagnação + deflectX *= Math.max(0.2, 1 - force * 0.4); + } + + // Colisão dura para não atravessar fisicamente a placa a X=0 + if (Math.abs(px) < 0.15) { + p.position.x = px < 0 ? -0.15 : 0.15; + } + } + + // Zona de esteira turbulenta a sotavento (atrás do muro X > 0) + if (px > 0.1 && px < length && isInsideZ) { + // Redução de velocidade na esteira + deflectX *= 0.6; + // Turbulência oscilatória + deflectZ += Math.sin(time * 6 + px * 0.8 + p.wobbleOffset) * 0.15; + deflectY += Math.cos(time * 4 + px * 0.8 + p.wobbleOffset) * 0.1; + } + + // Definir cor das partículas (Cpe/Cf equivalente para coloração) + if (Math.abs(px) < length && isInsideZ && py < topY + 1.0) { + if (px < 0) { + p.currentCpe = cf * 0.6; // Pressão positiva + } else { + p.currentCpe = -cf * 0.6; // Sucção + } + } else { + p.currentCpe = 0; + } + + const moveVec = new THREE.Vector3(deflectX, deflectY, deflectZ).normalize(); + const currentSpeed = p.baseSpeed * (px > 0 && px < length ? 0.6 : 1.0); + + p.position.addScaledVector(moveVec, currentSpeed); + + // Reposição quando sai dos limites + // Como o vento tem direção inclinada, reposicionamos quando sai de um cilindro ou caixa + if (p.position.x > limitX || p.position.z > limitZ) { + // Reinicia a barlavento extremo + p.position.x = -limitX; + p.position.z = (Math.random() - 0.5) * limitZ * 2; + p.position.y = Math.random() * (topY * 1.6); + p.currentCpe = 0; + } + + dummy.position.copy(p.position); + dummy.lookAt(p.position.clone().add(moveVec)); + 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 ? '#ef4444' : '#dc2626') + .lerp(new THREE.Color(isDark ? '#fcd34d' : '#f59e0b'), 1 - Math.min(1, p.currentCpe)); + } 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); + } else { + colorObj.set(isDark ? '#f1f5f9' : '#64748b'); + } + meshRef.current!.setColorAt(i, colorObj); + }); + + meshRef.current.instanceMatrix.needsUpdate = true; + if (meshRef.current.instanceColor) { + meshRef.current.instanceColor.needsUpdate = true; + } + }); + + return ( + + + + + ); +} diff --git a/app/src/components/three/Vault3D.tsx b/app/src/components/three/Vault3D.tsx index 2641c61..1c52ba0 100644 --- a/app/src/components/three/Vault3D.tsx +++ b/app/src/components/three/Vault3D.tsx @@ -5,6 +5,7 @@ import * as THREE from 'three'; import SceneCanvas from '../SceneCanvas'; import FallbackDiagram from '../FallbackDiagram'; import { useCanvasTheme } from '@/lib/theme'; +import { VaultAirflowSystem } from './VaultAirflowSystem'; export interface Vault3DInput { span: number; @@ -14,6 +15,7 @@ export interface Vault3DInput { cpeProfile: Record; cpeParallel?: Record; viewDirection?: 'perpendicular' | 'parallel'; + viewMode?: 'solid' | 'airflow'; } function vaultColor(cpe: number, cpi: number, isDark: boolean): THREE.Color { @@ -26,9 +28,10 @@ function vaultColor(cpe: number, cpi: number, isDark: boolean): THREE.Color { return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, l + 5)}%)`); } -function VaultModel({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDirection = 'perpendicular' }: Vault3DInput) { +function VaultModel({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDirection = 'perpendicular', viewMode = 'solid' }: Vault3DInput) { const theme = useCanvasTheme(); const isDark = theme === 'dark'; + const isAirflow = viewMode === 'airflow'; const segments = 64; const points = useMemo(() => { @@ -92,7 +95,7 @@ function VaultModel({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDire - + - + - + {/* Tímpano Frontal (Z = length) */} - + {/* Rótulo de dimensões */} @@ -186,7 +189,7 @@ function VaultModel({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDire ); } -export default function Vault3DViewer({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDirection = 'perpendicular' }: Vault3DInput) { +export default function Vault3DViewer({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDirection = 'perpendicular', viewMode = 'solid' }: Vault3DInput) { const theme = useCanvasTheme(); const isDark = theme === 'dark'; @@ -202,6 +205,7 @@ export default function Vault3DViewer({ span, length, rise, cpi, cpeProfile, cpe return ( - + + {viewMode === 'airflow' && ( + + )} (null); + + const particles = useMemo(() => { + const temp = []; + const limitX = Math.max(20, span * 2.5); + const limitZ = Math.max(20, length * 1.5); + for (let i = 0; i < count; i++) { + temp.push({ + position: new THREE.Vector3( + (Math.random() - 0.5) * limitX * 2, + Math.random() * (rise * 2.5), + (Math.random() - 0.5) * limitZ * 2 + ), + baseSpeed: 0.12 + Math.random() * 0.08, + wobbleSpeed: Math.random() * 0.05, + wobbleOffset: Math.random() * Math.PI * 2, + currentCpe: 0, + }); + } + return temp; + }, [count, span, length, rise]); + + const dummy = useMemo(() => new THREE.Object3D(), []); + const colorObj = useMemo(() => new THREE.Color(), []); + + useEffect(() => { + if (meshRef.current) { + for (let i = 0; i < count; i++) { + meshRef.current.setColorAt(i, new THREE.Color('#ffffff')); + } + if (meshRef.current.instanceColor) { + meshRef.current.instanceColor.needsUpdate = true; + } + } + }, [count]); + + useFrame((state) => { + if (!meshRef.current) return; + const time = state.clock.elapsedTime; + const limitX = Math.max(20, span * 2.5); + const limitZ = Math.max(20, length * 1.5); + + const isPerp = viewDirection === 'perpendicular'; + + particles.forEach((p, i) => { + let deflectX = isPerp ? 1.0 : 0.0; + let deflectY = 0.0; + let deflectZ = isPerp ? 0.0 : 1.0; + + const px = p.position.x; + const py = p.position.y; + const pz = p.position.z; + + // Altura teórica do arco da abóboda na coordenada X do ponto + const inSpan = px >= -span / 2 && px <= span / 2; + const inLength = pz >= 0 && pz <= length; // Lembrar que no Vault3D o modelo é desenhado de Z=0 a Z=length + const archY = inSpan ? rise * Math.sin(((px + span / 2) / span) * Math.PI) : 0; + + if (isPerp) { + // Vento perpendicular (ao longo do eixo X) + if (inLength) { + if (px >= -span / 2 - 2 && px <= span / 2 + 2) { + // Deflexão ao se aproximar e passar pela abóboda + const theta = ((Math.max(-span / 2, Math.min(span / 2, px)) + span / 2) / span) * Math.PI; + const slope = (rise / span) * Math.PI * Math.cos(theta); + + // A velocidade vertical induzida acompanha a inclinação do arco + if (py < archY + 3.0) { + deflectY = slope * 0.8; + } + + // Evita colisão dura (ficar abaixo do arco) + if (py < archY + 0.3) { + p.position.y = archY + 0.3; + } + } + + // Zona de turbulência/esteira atrás da abóboda (X > span/2) + if (px > span / 2 && px < span / 2 + 10) { + deflectY -= 0.15; + deflectZ += Math.sin(time * 5 + px * 0.5 + p.wobbleOffset) * 0.2; + } + } + + // Mapear cor com base na posição + if (inLength && inSpan && py < archY + 2.0) { + if (px < -span * 0.15) { + p.currentCpe = 0.7; // Compressão a barlavento + } else if (px >= -span * 0.15 && px <= span * 0.15) { + p.currentCpe = -0.9; // Forte sucção no topo + } else { + p.currentCpe = -0.5; // Sucção a sotavento + } + } else { + p.currentCpe = 0; + } + + } else { + // Vento paralelo (ao longo do eixo Z) + if (inSpan) { + // Se estiver muito baixo (abaixo da abóboda), empurra para cima da casca + if (py < archY + 0.3) { + p.position.y = archY + 0.3; + } + + // Deflexão nas bordas frontais/traseiras (Z=0 e Z=length) + if (pz < -2 && pz > -8) { + const dist = (-pz); + const lift = Math.max(0, archY - py) / (dist + 1); + deflectY += lift * 0.4; + } + } + + // Mapear cor para vento paralelo (normalmente sucção em toda a extensão) + if (inSpan && inLength && py < archY + 2.0) { + // Zonas A, B, C, D longitudinais + const zRatio = pz / length; + if (zRatio < 0.25) p.currentCpe = -0.8; // Entrada + else if (zRatio < 0.5) p.currentCpe = -0.6; + else p.currentCpe = -0.4; + } else { + p.currentCpe = 0; + } + } + + // Suaviza a velocidade + const moveVec = new THREE.Vector3(deflectX, deflectY, deflectZ).normalize(); + const currentSpeed = p.baseSpeed * (isPerp ? Math.max(0.4, 1 - Math.abs(deflectY) * 0.2) : 1.0); + + p.position.addScaledVector(moveVec, currentSpeed); + + // Reposicionar partículas fora do limite + if (isPerp) { + if (p.position.x > limitX) { + p.position.x = -limitX; + p.position.y = Math.random() * (rise * 2.5); + p.position.z = (Math.random() - 0.5) * limitZ * 2; + p.currentCpe = 0; + } + } else { + if (p.position.z > limitZ) { + p.position.z = -limitZ; + p.position.y = Math.random() * (rise * 2.5); + p.position.x = (Math.random() - 0.5) * limitX * 2; + p.currentCpe = 0; + } + } + + dummy.position.copy(p.position); + dummy.lookAt(p.position.clone().add(moveVec)); + dummy.rotateX(Math.PI / 2); + dummy.scale.set(1, 1 + currentSpeed * 6, 1); + dummy.updateMatrix(); + meshRef.current!.setMatrixAt(i, dummy.matrix); + + // Colorir + if (p.currentCpe > 0) { + colorObj + .set(isDark ? '#ef4444' : '#dc2626') + .lerp(new THREE.Color(isDark ? '#fcd34d' : '#f59e0b'), 1 - Math.min(1, p.currentCpe)); + } 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); + } else { + colorObj.set(isDark ? '#f1f5f9' : '#64748b'); + } + meshRef.current!.setColorAt(i, colorObj); + }); + + meshRef.current.instanceMatrix.needsUpdate = true; + if (meshRef.current.instanceColor) { + meshRef.current.instanceColor.needsUpdate = true; + } + }); + + return ( + + + + + ); +} diff --git a/app/src/pages/DomeModule.tsx b/app/src/pages/DomeModule.tsx index 43c35c4..06ec19f 100644 --- a/app/src/pages/DomeModule.tsx +++ b/app/src/pages/DomeModule.tsx @@ -4,6 +4,8 @@ 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 { Button } from '@/components/ui/button'; +import { Box, Wind } from 'lucide-react'; import { useWindStore } from '@/store/appStore'; import { CpiSettingsCard } from '@/components/CpiSettingsCard'; import { calculateDome, type DomeType } from '@/lib/modules/dome'; @@ -22,6 +24,7 @@ const DomeModule: React.FC = () => { const [rise, setRise] = React.useState(5); const [wallHeight, setWallHeight] = React.useState(8); const [type, setType] = React.useState('on-ground'); + const [viewMode, setViewMode] = React.useState<'solid' | 'airflow'>('solid'); const result = useMemo(() => calculateDome({ d: diameter, f: rise, h: wallHeight, vk, type, cpi }), [diameter, rise, wallHeight, vk, type, cpi]); @@ -158,7 +161,27 @@ const DomeModule: React.FC = () => { Cpi = {cpi.toFixed(2)} -
+
+
+ + +
@@ -171,6 +194,7 @@ const DomeModule: React.FC = () => { cpeBarlavento={result.cpeBarlavento} cpeTopo={result.cpeTopo} cpeLateral={result.cpeLateral} + viewMode={viewMode} />
diff --git a/app/src/pages/SignModule.tsx b/app/src/pages/SignModule.tsx index 720c76f..8b08236 100644 --- a/app/src/pages/SignModule.tsx +++ b/app/src/pages/SignModule.tsx @@ -4,6 +4,8 @@ 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 { Button } from '@/components/ui/button'; +import { Box, Wind } from 'lucide-react'; import { useWindStore } from '@/store/appStore'; import { calculateSign } from '@/lib/nbr-tables/table-23'; import Sign3DViewer from '@/components/three/Sign3D'; @@ -22,6 +24,7 @@ const SignModule: React.FC = () => { const [alpha, setAlpha] = React.useState<90 | 50>(90); const [hasEndPlates, setHasEndPlates] = React.useState(true); const [groundClearance, setGroundClearance] = React.useState(0.5); + const [viewMode, setViewMode] = React.useState<'solid' | 'airflow'>('solid'); const result = useMemo( () => @@ -156,7 +159,27 @@ const SignModule: React.FC = () => { F = {result.forceKN.toFixed(2)} kN Cf = {result.cf.toFixed(2)} -
+
+
+ + +
@@ -169,6 +192,7 @@ const SignModule: React.FC = () => { cf={result.cf} forceKN={result.forceKN} applicationPoint={result.applicationPoint} + viewMode={viewMode} />
diff --git a/app/src/pages/VaultModule.tsx b/app/src/pages/VaultModule.tsx index 7b8535e..69ec345 100644 --- a/app/src/pages/VaultModule.tsx +++ b/app/src/pages/VaultModule.tsx @@ -5,6 +5,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Button } from '@/components/ui/button'; +import { Box, Wind } from 'lucide-react'; import { useWindStore } from '@/store/appStore'; import { calculateVault, type VaultRegime } from '@/lib/modules/vault'; import Vault3DViewer from '@/components/three/Vault3D'; @@ -24,6 +26,7 @@ const VaultModule: React.FC = () => { const [regime, setRegime] = React.useState('laminar-rough'); const [localCpi, setLocalCpi] = React.useState(0); const [viewDirection, setViewDirection] = React.useState<'perpendicular' | 'parallel'>('perpendicular'); + const [viewMode, setViewMode] = React.useState<'solid' | 'airflow'>('solid'); const result = useMemo(() => calculateVault({ f: rise, l: span, b: length, vk, regime, cpi: localCpi }), [span, length, rise, vk, regime, localCpi]); @@ -167,7 +170,27 @@ const VaultModule: React.FC = () => { Cpi = {localCpi > 0 ? `+${localCpi.toFixed(2)}` : localCpi.toFixed(2)} -
+
+
+ + +
setViewDirection(v as 'perpendicular' | 'parallel')} className="bg-background/80 backdrop-blur-sm rounded-md shadow-sm border border-border"> Vento ⊥ @@ -186,6 +209,7 @@ const VaultModule: React.FC = () => { cpeProfile={{ ...result.windPerpendicular }} cpeParallel={{ ...result.windParallel }} viewDirection={viewDirection} + viewMode={viewMode} />