import { useRef, useMemo, useEffect } from 'react'; import { useFrame } from '@react-three/fiber'; import * as THREE from 'three'; import { useCanvasTheme } from '../../lib/theme'; export interface CylinderAirflowSystemProps { diameter: number; height: number; } export function CylinderAirflowSystem({ diameter, height }: CylinderAirflowSystemProps) { const isDark = useCanvasTheme() === 'dark'; const count = 450; const meshRef = useRef(null); const R = diameter / 2; const particles = useMemo(() => { const temp = []; const spanX = Math.max(15, R * 5); const spanZ = Math.max(10, R * 3.5); for (let i = 0; i < count; i++) { temp.push({ position: new THREE.Vector3( (Math.random() - 0.5) * spanX * 2, Math.random() * (height * 1.3), (Math.random() - 0.5) * spanZ * 2 ), baseSpeed: 0.12 + Math.random() * 0.08, wobbleSpeed: Math.random() * 0.05, wobbleOffset: Math.random() * Math.PI * 2, currentCpe: 0, }); } return temp; }, [count, R, height]); const dummy = useMemo(() => new THREE.Object3D(), []); const colorObj = useMemo(() => new THREE.Color(), []); 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 spanX = Math.max(15, R * 5); const spanZ = Math.max(10, R * 3.5); particles.forEach((p, i) => { let deflectX = 1.0; let deflectY = 0; let deflectZ = 0; const px = p.position.x; const py = p.position.y; const pz = p.position.z; const r = Math.sqrt(px * px + pz * pz); const safeR = Math.max(R + 0.3, r); // Proteção para não entrar no cilindro if (r < R + 0.25 && py <= height) { const angle = Math.atan2(pz, px); p.position.x = Math.cos(angle) * (R + 0.25); p.position.z = Math.sin(angle) * (R + 0.25); } // Cálculo aerodinâmico (escoamento potencial ao redor de cilindro) if (py <= height * 1.05 && r < R * 3.0) { const r2 = safeR * safeR; const R2 = R * R; // Velocidades de escoamento potencial em 2D ao redor do cilindro deflectX = 1 - (R2 * (px * px - pz * pz)) / (r2 * r2); deflectZ = -(2 * R2 * px * pz) / (r2 * r2); // Esteira turbulenta na região à jusante (+X) if (px > 0 && Math.abs(pz) < R * 1.3) { deflectZ += Math.sin(time * 5 + px * 0.8 + p.wobbleOffset) * 0.35; deflectX = Math.max(0.4, deflectX); } } // Determina coeficiente para cor da partícula if (py <= height && r < R * 2.2) { if (px < -R * 0.3 && Math.abs(pz) < R * 0.8) { p.currentCpe = 0.8; // Barlavento (pressão positiva) } else if (Math.abs(px) <= R * 0.6 && r < R * 1.5) { p.currentCpe = -1.2; // Laterais (forte sucção) } else if (px > R * 0.3 && Math.abs(pz) < R * 1.2) { p.currentCpe = 0.5; // Esteira (agora positivo para ficar vermelho) } else { p.currentCpe = 0; } } else { p.currentCpe = 0; } const moveVec = new THREE.Vector3(deflectX, deflectY, deflectZ).normalize(); const currentSpeed = p.baseSpeed * Math.max(0.3, Math.min(1.5, deflectX)); p.position.addScaledVector(moveVec, currentSpeed); // Reposição quando sai do volume if (p.position.x > spanX) { p.position.x = -spanX; p.position.y = Math.random() * (height * 1.3); p.position.z = (Math.random() - 0.5) * spanZ * 2; } dummy.position.copy(p.position); dummy.lookAt(p.position.clone().add(moveVec)); dummy.rotateX(Math.PI / 2); dummy.scale.set(1, 1 + currentSpeed * 6, 1); dummy.updateMatrix(); meshRef.current!.setMatrixAt(i, dummy.matrix); if (p.currentCpe > 0) { colorObj.set(isDark ? '#f87171' : '#ef4444'); // Vermelho (Turbulento/Pressão) } else if (p.currentCpe < 0) { const intensity = Math.min(1, Math.abs(p.currentCpe)); colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção) } else { colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre) } meshRef.current!.setColorAt(i, colorObj); }); meshRef.current.instanceMatrix.needsUpdate = true; if (meshRef.current.instanceColor) { meshRef.current.instanceColor.needsUpdate = true; } }); return ( ); }