🚀 Auto-deploy: BrainWind atualizado em 28/07/2026 11:38:18

This commit is contained in:
2026-07-28 11:38:18 +00:00
parent a77bd61dfd
commit 6384220731
2 changed files with 129 additions and 40 deletions
+1 -1
View File
@@ -631,7 +631,7 @@ export function WarehouseModel() {
<meshStandardMaterial color={isDark ? "#475569" : "#2d3748"} transparent={(viewMode as string) === 'airflow'} opacity={(viewMode as string) === 'airflow' ? 0.35 : 1} />
</mesh>
{(viewMode as string) === 'airflow' && <AirflowSystem windAngle={windAngle} width={width} length={length} height={height} permeabilityCase={permeabilityCase} />}
{(viewMode as string) === 'airflow' && <AirflowSystem windAngle={windAngle} width={width} length={length} height={height} permeabilityCase={permeabilityCase} wallCpe={wallCpe} roofCpe={roofCpe} cpi={cpi} />}
{/* === RÓTULOS 3D === */}
{showMainWalls && viewMode === 'solid' && (
+128 -39
View File
@@ -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<THREE.InstancedMesh>(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 (
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
{/* A simple arrow-like or dashed line geometry */}
<cylinderGeometry args={[0.02, 0.02, 1.5, 4]} />
<cylinderGeometry args={[0.04, 0.04, 1.5, 4]} />
<meshBasicMaterial
color="#a5f3fc"
color="#ffffff"
transparent
opacity={0.6}
opacity={0.8}
blending={THREE.AdditiveBlending}
depthWrite={false}
/>