From b1cadac6ab258a288c8e86195fc7d02f7206d4a2 Mon Sep 17 00:00:00 2001 From: Marcos Date: Wed, 29 Jul 2026 10:27:37 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Auto-deploy:=20BrainWind=20atual?= =?UTF-8?q?izado=20em=2029/07/2026=2010:27:37?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/components/Warehouse3D.tsx | 2 +- app/src/components/WindFlowGrid2D.tsx | 23 ++- app/src/components/three/AirflowSystem.tsx | 12 +- app/src/components/three/Cylinder3D.tsx | 54 ++++-- .../three/CylinderAirflowSystem.tsx | 158 ++++++++++++++++++ app/src/pages/CylinderModule.tsx | 48 ++++-- 6 files changed, 261 insertions(+), 36 deletions(-) create mode 100644 app/src/components/three/CylinderAirflowSystem.tsx diff --git a/app/src/components/Warehouse3D.tsx b/app/src/components/Warehouse3D.tsx index 2fb800f..4de3971 100644 --- a/app/src/components/Warehouse3D.tsx +++ b/app/src/components/Warehouse3D.tsx @@ -575,7 +575,7 @@ export function WarehouseModel() { {isParallel ? `Zona H (${roofCpe.H})` : `Zona H (${roofCpe.H})`} ); @@ -488,8 +495,8 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) { {/* Esteira turbulenta no telhado e atrás */} - - + +

Vento incide a 0° (paralelo à cumeeira). O fluxo contorna o oitão frontal, gerando forte sucção nas bordas longitudinais.

@@ -503,6 +510,12 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) { {styleBlock} {legendDef} + + {/* Fluxo entrando */} + + + + {/* Setas laranjas apontando para fora (inflado) */} @@ -522,6 +535,12 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) { {styleBlock} {legendDef} + + {/* Fluxo passando e sugando */} + + + + {/* Setas azuis apontando para dentro (sugado) */} diff --git a/app/src/components/three/AirflowSystem.tsx b/app/src/components/three/AirflowSystem.tsx index 4c31a6b..7f12fa3 100644 --- a/app/src/components/three/AirflowSystem.tsx +++ b/app/src/components/three/AirflowSystem.tsx @@ -1,6 +1,7 @@ import { useRef, useMemo, useEffect } from 'react'; import { useFrame } from '@react-three/fiber'; import * as THREE from 'three'; +import { useCanvasTheme } from '../../lib/theme'; interface WallCoefficients { A: number; B: number; C: number; D: number; @@ -21,6 +22,7 @@ interface AirflowSystemProps { } export function AirflowSystem({ windAngle, width, length, height, permeabilityCase, wallCpe, roofCpe, cpi }: AirflowSystemProps) { + const isDark = useCanvasTheme() === 'dark'; const count = 400; // Quantidade reduzida const meshRef = useRef(null); @@ -193,12 +195,12 @@ export function AirflowSystem({ windAngle, width, length, height, permeabilityCa meshRef.current!.setMatrixAt(i, dummy.matrix); if (p.currentCpe > 0) { - colorObj.set('#ef4444').lerp(new THREE.Color('#fcd34d'), 1 - Math.min(1, p.currentCpe)); + 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('#7dd3fc').lerp(new THREE.Color('#1e40af'), intensity); + colorObj.set(isDark ? '#7dd3fc' : '#38bdf8').lerp(new THREE.Color(isDark ? '#1e40af' : '#1d4ed8'), intensity); } else { - colorObj.set('#f1f5f9'); + colorObj.set(isDark ? '#f1f5f9' : '#64748b'); } meshRef.current!.setColorAt(i, colorObj); }); @@ -215,8 +217,8 @@ export function AirflowSystem({ windAngle, width, length, height, permeabilityCa diff --git a/app/src/components/three/Cylinder3D.tsx b/app/src/components/three/Cylinder3D.tsx index 09f2078..db9ed69 100644 --- a/app/src/components/three/Cylinder3D.tsx +++ b/app/src/components/three/Cylinder3D.tsx @@ -5,6 +5,7 @@ import * as THREE from 'three'; import SceneCanvas from '../SceneCanvas'; import FallbackDiagram from '../FallbackDiagram'; import { useCanvasTheme } from '@/lib/theme'; +import { CylinderAirflowSystem } from './CylinderAirflowSystem'; export interface Cylinder3DInput { diameter: number; @@ -12,6 +13,7 @@ export interface Cylinder3DInput { /** Cpe profile ao longo da circunferência (0° a 180°) */ cpeProfile: { angle: number; cpe: number }[]; cpi: number; + viewMode?: 'solid' | 'airflow'; } function cylinderColor(cpe: number, cpi: number): THREE.Color { @@ -23,9 +25,10 @@ function cylinderColor(cpe: number, cpi: number): THREE.Color { return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, 65 - intensity * 20)}%)`); } -function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) { +function CylinderModel({ diameter, height, cpeProfile, cpi, viewMode = 'solid' }: Cylinder3DInput) { const theme = useCanvasTheme(); const isDark = theme === 'dark'; + const isAirflow = viewMode === 'airflow'; const segments = 64; const radius = diameter / 2; @@ -111,7 +114,13 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) { args={[normals, 3]} /> - + ); })} @@ -119,7 +128,13 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) { {/* Tampa superior sólida */} - + {/* Anéis de detalhe (bordas do cilindro) */} @@ -142,22 +157,24 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) { - - Vento - - {/* Texto de Informação */} + {/* Texto Vento - fora do grupo para rotação horizontal compatível (0, 0, 0) e tamanho 50% maior (0.6) */} + + Vento + + + {/* Texto de Informação - 50% maior */} - + + {viewMode === 'airflow' && ( + + )} (null); + const R = diameter / 2; + + const particles = useMemo(() => { + const temp = []; + const spanX = Math.max(15, R * 5); + const spanZ = Math.max(10, R * 3.5); + for (let i = 0; i < count; i++) { + temp.push({ + position: new THREE.Vector3( + (Math.random() - 0.5) * spanX * 2, + Math.random() * (height * 1.3), + (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, height]); + + 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(10, R * 3.5); + + particles.forEach((p, i) => { + let deflectX = 1.0; + let deflectY = 0; + let deflectZ = 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); + + // Proteção para não entrar no cilindro + if (r < R + 0.25 && py <= height) { + const angle = Math.atan2(pz, px); + p.position.x = Math.cos(angle) * (R + 0.25); + p.position.z = Math.sin(angle) * (R + 0.25); + } + + // Cálculo aerodinâmico (escoamento potencial ao redor de cilindro) + if (py <= height * 1.05 && r < R * 3.0) { + const r2 = safeR * safeR; + const R2 = R * R; + // Velocidades de escoamento potencial em 2D ao redor do cilindro + deflectX = 1 - (R2 * (px * px - pz * pz)) / (r2 * r2); + deflectZ = -(2 * R2 * px * pz) / (r2 * r2); + + // Esteira turbulenta na região à jusante (+X) + if (px > 0 && Math.abs(pz) < R * 1.3) { + deflectZ += Math.sin(time * 5 + px * 0.8 + p.wobbleOffset) * 0.35; + deflectX = Math.max(0.4, deflectX); + } + } + + // Determina coeficiente para cor da partícula + if (py <= height && r < R * 2.2) { + if (px < -R * 0.3 && Math.abs(pz) < R * 0.8) { + p.currentCpe = 0.8; // Barlavento (pressão positiva) + } 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) + } else { + p.currentCpe = 0; + } + } 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() * (height * 1.3); + p.position.z = (Math.random() - 0.5) * spanZ * 2; + } + + 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/pages/CylinderModule.tsx b/app/src/pages/CylinderModule.tsx index da9e448..54673bc 100644 --- a/app/src/pages/CylinderModule.tsx +++ b/app/src/pages/CylinderModule.tsx @@ -3,7 +3,9 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com import { Slider } from '@/components/ui/slider'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; import { Separator } from '@/components/ui/separator'; +import { Box, Wind } from 'lucide-react'; import { useWindStore } from '@/store/appStore'; import { CpiSettingsCard } from '@/components/CpiSettingsCard'; import { calculateCylinder, type CylinderEndType } from '@/lib/modules/cylinder'; @@ -23,6 +25,7 @@ const CylinderModule: React.FC = () => { const [height, setHeight] = React.useState(20); const [surface, setSurface] = React.useState<'rough' | 'smooth'>('rough'); const [endType, setEndType] = React.useState('closed'); + const [viewMode, setViewMode] = React.useState<'solid' | 'airflow'>('solid'); const result = useMemo(() => { return calculateCylinder({ @@ -169,24 +172,47 @@ const CylinderModule: React.FC = () => { {/* Coluna Central: 3D */}
-
- - h/d = {result.hOverD.toFixed(2)} - - - Re = {result.re.toExponential(2)} - -
-
- +
+
+ + h/d = {result.hOverD.toFixed(2)} + + + Re = {result.re.toExponential(2)} + +
+
+ + +
+
+ +
-
+