From d83a8321380690a2390cbdec830567d4f4693bbe Mon Sep 17 00:00:00 2001 From: Marcos Date: Tue, 28 Jul 2026 11:08:28 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Auto-deploy:=20BrainWind=20atual?= =?UTF-8?q?izado=20em=2028/07/2026=2011:08:28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/components/Warehouse3D.tsx | 99 ++++++++++--------- app/src/components/three/AirflowSystem.tsx | 105 +++++++++++++++++++++ app/src/pages/GalpaoModule.tsx | 25 ++++- app/src/store/galpaoStore.ts | 4 + fix_warehouse.js | 36 +++++++ 5 files changed, 221 insertions(+), 48 deletions(-) create mode 100644 app/src/components/three/AirflowSystem.tsx create mode 100644 fix_warehouse.js diff --git a/app/src/components/Warehouse3D.tsx b/app/src/components/Warehouse3D.tsx index 22ea0d2..7b86ab4 100644 --- a/app/src/components/Warehouse3D.tsx +++ b/app/src/components/Warehouse3D.tsx @@ -4,6 +4,7 @@ import { Grid, Environment, Text } from '@react-three/drei'; import { ViewerOrbitControls as OrbitControls } from './three/ViewerOrbitControls'; import * as THREE from 'three'; import { useGalpaoStore } from '../store/galpaoStore'; +import { AirflowSystem } from './three/AirflowSystem'; import { useWindStore } from '../store/appStore'; import SceneCanvas from './SceneCanvas'; import FallbackDiagram from './FallbackDiagram'; @@ -34,6 +35,8 @@ function PressureArrow({ p: number; }) { const { q } = useWindStore(); + const { viewMode } = useGalpaoStore(); + if (viewMode === 'airflow') return null; const force = p * q; // kN/m2 const normVec = useMemo(() => new THREE.Vector3(...normal).normalize(), [normal]); @@ -63,12 +66,12 @@ function PressureArrow({ {length - headLen > 0 && ( - + )} - + ); @@ -131,7 +134,7 @@ export function WarehouseModel() { const { invalidate } = useThree(); const theme = useCanvasTheme(); const isDark = theme === 'dark'; - const { width, length, height, roofPitch, wallCpe, roofCpe, template, extraHeight, skirtHeight } = useGalpaoStore(); + const { width, length, height, roofPitch, wallCpe, roofCpe, template, extraHeight, skirtHeight, viewMode } = useGalpaoStore(); const { windAngle, cpi, permeabilityCase } = useWindStore(); useEffect(() => { @@ -181,26 +184,26 @@ export function WarehouseModel() { return ( {/* === PAREDES === */} - {showMainWalls && ( + {showMainWalls && viewMode === 'solid' && ( {/* Lateral Esquerda (X = -width/2, Parede C) */} - + - + {hasOpening('C', permeabilityCase, windAngle) && ( - + )} @@ -209,20 +212,20 @@ export function WarehouseModel() { - + - + {hasOpening('D', permeabilityCase, windAngle) && ( - + )} @@ -231,20 +234,20 @@ export function WarehouseModel() { - + - + {hasOpening('A', permeabilityCase, windAngle) && ( - + )} @@ -253,20 +256,20 @@ export function WarehouseModel() { - + - + {hasOpening('B', permeabilityCase, windAngle) && ( - + )} @@ -279,22 +282,22 @@ export function WarehouseModel() { {/* Platibanda Traseira */} - + {/* Platibanda Frontal */} - + {/* Platibanda Esquerda */} - + {/* Platibanda Direita */} - + )} @@ -305,22 +308,22 @@ export function WarehouseModel() { {/* Saia Traseira */} - + {/* Saia Frontal */} - + {/* Saia Esquerda */} - + {/* Saia Direita */} - + )} @@ -330,11 +333,11 @@ export function WarehouseModel() { - + - + @@ -342,11 +345,11 @@ export function WarehouseModel() { - + - + @@ -356,47 +359,47 @@ export function WarehouseModel() { {/* Seg 1 (Traseiro-Fim) */} - + {/* Seg 2 (Traseiro-Meio) */} - + {/* Seg 3 (Frontal-Meio) */} - + {/* Seg 4 (Frontal-Fim) */} - + {/* Linhas de Divisão das Zonas no Telhado */} - + - + - + - + {/* Rótulo e Título da Água */} @@ -487,47 +490,47 @@ export function WarehouseModel() { {/* Seg 1 (Traseiro-Fim) */} - + {/* Seg 2 (Traseiro-Meio) */} - + {/* Seg 3 (Frontal-Meio) */} - + {/* Seg 4 (Frontal-Fim) */} - + {/* Linhas de Divisão das Zonas no Telhado */} - + - + - + - + {/* Rótulo e Título da Água */} @@ -616,19 +619,21 @@ export function WarehouseModel() { {/* Cumeeira */} - + {/* Bordas Laterais do Telhado (Beirais) */} - + - + + {viewMode === 'airflow' && } + {/* === RÓTULOS 3D === */} {showMainWalls && ( diff --git a/app/src/components/three/AirflowSystem.tsx b/app/src/components/three/AirflowSystem.tsx new file mode 100644 index 0000000..034273f --- /dev/null +++ b/app/src/components/three/AirflowSystem.tsx @@ -0,0 +1,105 @@ +import { useRef, useMemo } from 'react'; +import { useFrame } from '@react-three/fiber'; +import * as THREE from 'three'; + +interface AirflowSystemProps { + windAngle: number; + width: number; + length: number; + height: number; + permeabilityCase: string; +} + +export function AirflowSystem({ windAngle, width, length, height, permeabilityCase }: AirflowSystemProps) { + const count = 300; + const meshRef = useRef(null); + const particles = useMemo(() => { + const temp = []; + for (let i = 0; i < count; i++) { + temp.push({ + position: new THREE.Vector3( + (Math.random() - 0.5) * (width * 3), + Math.random() * height * 2, + (Math.random() - 0.5) * (length * 3) + ), + speed: 0.1 + Math.random() * 0.2, + wobbleSpeed: Math.random() * 0.05, + wobbleOffset: Math.random() * Math.PI * 2, + }); + } + return temp; + }, [count, width, length, height]); + + const dummy = useMemo(() => new THREE.Object3D(), []); + + useFrame((state) => { + if (!meshRef.current) return; + const time = state.clock.elapsedTime; + const isParallel = windAngle === 90; + + // Wind direction vector + const dir = isParallel ? new THREE.Vector3(0, 0, 1) : new THREE.Vector3(1, 0, 0); + + // Bounds + const halfW = width / 2; + const halfL = length / 2; + + particles.forEach((p, i) => { + // Move particle + p.position.addScaledVector(dir, p.speed); + + // Add slight turbulence + p.position.y += Math.sin(time * p.wobbleSpeed * 10 + p.wobbleOffset) * 0.02; + + const isInsideX = p.position.x > -halfW && p.position.x < halfW; + const isInsideZ = p.position.z > -halfL && p.position.z < halfL; + const isInsideY = p.position.y > 0 && p.position.y < height; + const isInsideBuilding = isInsideX && isInsideZ && isInsideY; + + if (isInsideBuilding && permeabilityCase === 'airtight') { + // Push particle up to simulate wind going over the roof + p.position.y += 0.2; + } + + // Reset if it goes too far + if (isParallel) { + if (p.position.z > halfL + 20) { + p.position.z = -halfL - 20; + p.position.y = Math.random() * height * 1.5; + p.position.x = (Math.random() - 0.5) * (width * 2); + } + } else { + if (p.position.x > halfW + 20) { + p.position.x = -halfW - 20; + p.position.y = Math.random() * height * 1.5; + p.position.z = (Math.random() - 0.5) * (length * 2); + } + } + + dummy.position.copy(p.position); + + // Orient the particle along the wind direction + dummy.lookAt(p.position.clone().add(dir)); + dummy.rotateX(Math.PI / 2); // Cylinder is aligned along Y by default, rotate to face direction + + dummy.updateMatrix(); + meshRef.current!.setMatrixAt(i, dummy.matrix); + }); + + meshRef.current.instanceMatrix.needsUpdate = true; + }); + + return ( + + {/* A simple arrow-like or dashed line geometry */} + + + + ); +} diff --git a/app/src/pages/GalpaoModule.tsx b/app/src/pages/GalpaoModule.tsx index 6060a71..7cdee01 100644 --- a/app/src/pages/GalpaoModule.tsx +++ b/app/src/pages/GalpaoModule.tsx @@ -13,7 +13,8 @@ import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { AlertCircle, Lightbulb } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { AlertCircle, Lightbulb, Box, Wind } from 'lucide-react'; import ExportMenu from '../components/ExportMenu'; import { EducationalManual } from '@/components/EducationalManual'; import { PressureLegend } from '@/components/PressureLegend'; @@ -37,6 +38,8 @@ const GalpaoModule: React.FC = () => { setLength, setHeight, setRoofPitch, + viewMode, + setViewMode, } = useGalpaoStore(); const { q, @@ -258,6 +261,26 @@ const GalpaoModule: React.FC = () => { q = {q.toFixed(3)} kN/m² +
+ + +
diff --git a/app/src/store/galpaoStore.ts b/app/src/store/galpaoStore.ts index 1aa8626..962fcc0 100644 --- a/app/src/store/galpaoStore.ts +++ b/app/src/store/galpaoStore.ts @@ -10,6 +10,7 @@ interface GalpaoState { template: GalpaoTemplate; extraHeight: number; // Altura da platibanda skirtHeight: number; // Altura da saia + viewMode: 'solid' | 'airflow'; // Modo de visualização 3D // Dimensões do Galpão Retangular width: number; @@ -25,6 +26,7 @@ interface GalpaoState { setTemplate: (val: GalpaoTemplate) => void; setExtraHeight: (val: number) => void; setSkirtHeight: (val: number) => void; + setViewMode: (val: 'solid' | 'airflow') => void; setWidth: (val: number) => void; setLength: (val: number) => void; setHeight: (val: number) => void; @@ -42,6 +44,7 @@ export const useGalpaoStore = create((set, get) => ({ template: 'standard', extraHeight: 1.5, skirtHeight: 4.5, + viewMode: 'solid', width: initWidth, length: initLength, height: initHeight, @@ -61,6 +64,7 @@ export const useGalpaoStore = create((set, get) => ({ setTemplate: (val) => set({ template: val }), setExtraHeight: (val) => set({ extraHeight: val }), setSkirtHeight: (val) => set({ skirtHeight: val }), + setViewMode: (val) => set({ viewMode: val }), setWidth: (val) => { set({ width: val }); diff --git a/fix_warehouse.js b/fix_warehouse.js new file mode 100644 index 0000000..9db81ad --- /dev/null +++ b/fix_warehouse.js @@ -0,0 +1,36 @@ +const fs = require('fs'); +let code = fs.readFileSync('app/src/components/Warehouse3D.tsx', 'utf8'); + +// Import AirflowSystem +if (!code.includes('AirflowSystem')) { + code = code.replace("import { useGalpaoStore } from '../store/galpaoStore';", "import { useGalpaoStore } from '../store/galpaoStore';\nimport { AirflowSystem } from './three/AirflowSystem';"); +} + +// Add viewMode to useGalpaoStore destructuring in WarehouseModel +if (!code.includes('viewMode = useGalpaoStore')) { + code = code.replace("const { width, length, height, roofPitch, wallCpe, roofCpe, template, extraHeight, skirtHeight } = useGalpaoStore();", "const { width, length, height, roofPitch, wallCpe, roofCpe, template, extraHeight, skirtHeight, viewMode } = useGalpaoStore();"); +} + +// Add viewMode to PressureArrow +if (!code.includes('const { viewMode } = useGalpaoStore();') && code.includes('function PressureArrow')) { + code = code.replace("const { q } = useWindStore();", "const { q } = useWindStore();\n const { viewMode } = useGalpaoStore();\n if (viewMode === 'airflow') return null;"); +} + +// Make meshStandardMaterial transparent +code = code.replace(/]+)>/g, (match, p1) => { + // If it already has transparent, skip + if (match.includes("transparent={viewMode")) return match; + return ``; +}); + +// Add AirflowSystem to the scene +if (!code.includes('}\n\n {/* === RÓTULOS 3D === */}"); +} + +// Rótulos 3D visibility +if (code.includes('{showMainWalls && (')) { + code = code.replace("{showMainWalls && (", "{showMainWalls && viewMode === 'solid' && ("); +} + +fs.writeFileSync('app/src/components/Warehouse3D.tsx', code);