diff --git a/app/src/components/Warehouse3D.tsx b/app/src/components/Warehouse3D.tsx index 762631e..395f7cc 100644 --- a/app/src/components/Warehouse3D.tsx +++ b/app/src/components/Warehouse3D.tsx @@ -631,7 +631,7 @@ export function WarehouseModel() { - {(viewMode as string) === 'airflow' && } + {(viewMode as string) === 'airflow' && } {/* === RÓTULOS 3D === */} {showMainWalls && viewMode === 'solid' && ( diff --git a/app/src/components/three/AirflowSystem.tsx b/app/src/components/three/AirflowSystem.tsx index 034273f..69ab7b6 100644 --- a/app/src/components/three/AirflowSystem.tsx +++ b/app/src/components/three/AirflowSystem.tsx @@ -1,102 +1,191 @@ -import { useRef, useMemo } from 'react'; +import { useRef, useMemo, useEffect } from 'react'; import { useFrame } from '@react-three/fiber'; import * as THREE from 'three'; +interface WallCoefficients { + A: number; B: number; C: number; D: number; +} +interface RoofCoefficients { + E: number; F: number; G: number; H: number; I: number; J: number; +} + interface AirflowSystemProps { - windAngle: number; + windAngle: 0 | 90; width: number; length: number; height: number; permeabilityCase: string; + wallCpe: WallCoefficients; + roofCpe: RoofCoefficients; + cpi: number; } -export function AirflowSystem({ windAngle, width, length, height, permeabilityCase }: AirflowSystemProps) { - const count = 300; +export function AirflowSystem({ windAngle, width, length, height, permeabilityCase, wallCpe, roofCpe, cpi }: AirflowSystemProps) { + const count = 1000; 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) + (Math.random() - 0.5) * (width * 4), + Math.random() * (height * 3), + (Math.random() - 0.5) * (length * 4) ), - speed: 0.1 + Math.random() * 0.2, + baseSpeed: 0.15 + Math.random() * 0.2, wobbleSpeed: Math.random() * 0.05, wobbleOffset: Math.random() * Math.PI * 2, + currentCpe: 0, }); } return temp; }, [count, width, length, height]); const dummy = useMemo(() => new THREE.Object3D(), []); + const colorObj = useMemo(() => new THREE.Color(), []); + + // Inicializa cores + 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 isParallel = windAngle === 90; - // Wind direction vector - const dir = isParallel ? new THREE.Vector3(0, 0, 1) : new THREE.Vector3(1, 0, 0); + // Vetor direção do vento + const windDir = isParallel ? new THREE.Vector3(0, 0, 1) : new THREE.Vector3(1, 0, 0); - // Bounds const halfW = width / 2; const halfL = length / 2; + const margin = 1.0; + const roofAvgCpe = (roofCpe.E + roofCpe.F + roofCpe.G + roofCpe.H + roofCpe.I + roofCpe.J) / 6; + particles.forEach((p, i) => { - // Move particle - p.position.addScaledVector(dir, p.speed); + // Determinar a zona e o Cpe local + let localCpe = 0; + let deflectY = 0; + let deflectX = 0; + let deflectZ = 0; - // 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; + const px = p.position.x; + const py = p.position.y; + const pz = p.position.z; - if (isInsideBuilding && permeabilityCase === 'airtight') { - // Push particle up to simulate wind going over the roof - p.position.y += 0.2; + const inX = px >= -halfW - margin && px <= halfW + margin; + const inZ = pz >= -halfL - margin && pz <= halfL + margin; + const inY = py >= 0 && py <= height; + const aboveRoof = py > height && py <= height + 3 && inX && inZ; + + 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(px - (-halfW)) < margin) localCpe = wallCpe.C; + else if (Math.abs(px - (halfW)) < margin) localCpe = wallCpe.D; + else localCpe = cpi; // Dentro do prédio + + // Se bateu na parede a barlavento, desvia o fluxo (Aerodinâmica) + const isAirtight = permeabilityCase !== 'four-equally-permeable'; + if (isAirtight) { + if (windAngle === 0 && px < -halfW + margin) { + deflectY = 0.1; // Sobe + deflectZ = pz > 0 ? 0.05 : -0.05; // Vai pros lados + } else if (windAngle === 90 && pz < -halfL + margin) { + deflectY = 0.1; // Sobe + deflectX = px > 0 ? 0.05 : -0.05; // Vai pros lados + } + } + } else if (aboveRoof) { + localCpe = roofAvgCpe; + // Vórtice no telhado + p.position.y += Math.sin(time * 5 + p.wobbleOffset) * 0.03; } - // Reset if it goes too far + // Suaviza a transição de Cpe para a cor não piscar + p.currentCpe += (localCpe - p.currentCpe) * 0.1; + + // Modifica velocidade com base no Cpe (Pressão > 0 diminui vel, Sucção < 0 aumenta vel) + const speedModifier = Math.max(0.2, 1 - (p.currentCpe * 0.5)); + const currentSpeed = p.baseSpeed * speedModifier; + + // Move a partícula + p.position.x += windDir.x * currentSpeed + deflectX; + p.position.y += deflectY; + p.position.z += windDir.z * currentSpeed + deflectZ; + + // Turbulência natural + p.position.y += Math.sin(time * p.wobbleSpeed * 10 + p.wobbleOffset) * 0.01; + + // Recicla partículas que saem da tela 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); + if (p.position.z > halfL + 30) { + p.position.z = -halfL - 30; + p.position.y = Math.random() * (height * 2); + p.position.x = (Math.random() - 0.5) * (width * 3); + p.currentCpe = 0; } } 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); + if (p.position.x > halfW + 30) { + p.position.x = -halfW - 30; + p.position.y = Math.random() * (height * 2); + p.position.z = (Math.random() - 0.5) * (length * 3); + p.currentCpe = 0; } } + // Atualiza posição e escala na matriz 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 + // Aponta para a direção do movimento real + const moveVec = new THREE.Vector3(windDir.x + deflectX, deflectY, windDir.z + deflectZ).normalize(); + dummy.lookAt(p.position.clone().add(moveVec)); + dummy.rotateX(Math.PI / 2); + + // Estica a barrinha dependendo da velocidade (efeito blur) + dummy.scale.set(1, 1 + currentSpeed * 5, 1); dummy.updateMatrix(); meshRef.current!.setMatrixAt(i, dummy.matrix); + + // Atualiza Cor + if (p.currentCpe > 0) { + // Pressão (Quente: Vermelho -> Laranja) + colorObj.set('#ef4444').lerp(new THREE.Color('#fcd34d'), 1 - Math.min(1, p.currentCpe)); + } else if (p.currentCpe < 0) { + // Sucção (Frio: Azul claro -> Azul escuro) + const intensity = Math.min(1, Math.abs(p.currentCpe)); + colorObj.set('#38bdf8').lerp(new THREE.Color('#1e3a8a'), intensity); + } else { + // Neutro + colorObj.set('#cbd5e1'); + } + meshRef.current!.setColorAt(i, colorObj); }); meshRef.current.instanceMatrix.needsUpdate = true; + if (meshRef.current.instanceColor) { + meshRef.current.instanceColor.needsUpdate = true; + } }); return ( - {/* A simple arrow-like or dashed line geometry */} - +