🚀 Auto-deploy: BrainWind atualizado em 28/07/2026 11:38:18
This commit is contained in:
@@ -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' && (
|
||||
|
||||
@@ -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 px = p.position.x;
|
||||
const py = p.position.y;
|
||||
const pz = p.position.z;
|
||||
|
||||
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 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 (isInsideBuilding && permeabilityCase === 'airtight') {
|
||||
// Push particle up to simulate wind going over the roof
|
||||
p.position.y += 0.2;
|
||||
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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user