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 */}
-
+