🚀 Auto-deploy: BrainWind atualizado em 29/07/2026 10:27:37
This commit is contained in:
@@ -575,7 +575,7 @@ export function WarehouseModel() {
|
|||||||
{isParallel ? `Zona H (${roofCpe.H})` : `Zona H (${roofCpe.H})`}
|
{isParallel ? `Zona H (${roofCpe.H})` : `Zona H (${roofCpe.H})`}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
position={[widthSlope * 0.38, 0.35, 0]}
|
position={[widthSlope * 0.44, 0.35, 0]}
|
||||||
rotation={[-Math.PI / 2, 0, -Math.PI / 2]}
|
rotation={[-Math.PI / 2, 0, -Math.PI / 2]}
|
||||||
fontSize={zoneFontSize}
|
fontSize={zoneFontSize}
|
||||||
color="#000000"
|
color="#000000"
|
||||||
|
|||||||
@@ -61,15 +61,22 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) {
|
|||||||
.vibe-cyl {
|
.vibe-cyl {
|
||||||
animation: vibration 1.2s ease-in-out infinite;
|
animation: vibration 1.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
@keyframes cpiPulse {
|
||||||
|
0%, 100% { opacity: 0.4; stroke-width: 1.5; }
|
||||||
|
50% { opacity: 1; stroke-width: 2.5; }
|
||||||
|
}
|
||||||
.cpi-arrow-pos {
|
.cpi-arrow-pos {
|
||||||
stroke: #f97316;
|
stroke: #f97316;
|
||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
fill: none;
|
fill: none;
|
||||||
|
animation: cpiPulse 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
.cpi-arrow-neg {
|
.cpi-arrow-neg {
|
||||||
stroke: #3b82f6;
|
stroke: #3b82f6;
|
||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
fill: none;
|
fill: none;
|
||||||
|
animation: cpiPulse 1.5s ease-in-out infinite;
|
||||||
|
animation-delay: 0.75s;
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
);
|
);
|
||||||
@@ -488,8 +495,8 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) {
|
|||||||
<path d="M -20,85 L 80,85" className="wind-line" />
|
<path d="M -20,85 L 80,85" className="wind-line" />
|
||||||
|
|
||||||
{/* Esteira turbulenta no telhado e atrás */}
|
{/* Esteira turbulenta no telhado e atrás */}
|
||||||
<path d="M 80,70 Q 110,50 150,70" fill="none" stroke="#ef4444" strokeWidth="1.5" strokeDasharray="3 6" />
|
<path d="M 80,70 Q 110,50 150,70" className="vortex-line" />
|
||||||
<path d="M 220,70 Q 250,90 280,110" fill="none" stroke="#ef4444" strokeWidth="1.5" strokeDasharray="3 6" />
|
<path d="M 220,70 Q 250,90 280,110" className="vortex-line" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-muted-foreground leading-snug">Vento incide a 0° (paralelo à cumeeira). O fluxo contorna o oitão frontal, gerando forte sucção nas bordas longitudinais.</p>
|
<p className="text-[10px] text-muted-foreground leading-snug">Vento incide a 0° (paralelo à cumeeira). O fluxo contorna o oitão frontal, gerando forte sucção nas bordas longitudinais.</p>
|
||||||
@@ -503,6 +510,12 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) {
|
|||||||
{styleBlock}
|
{styleBlock}
|
||||||
{legendDef}
|
{legendDef}
|
||||||
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
||||||
|
|
||||||
|
{/* Fluxo entrando */}
|
||||||
|
<path d="M -20,20 L 320,20" className="wind-line-slow" />
|
||||||
|
<path d="M -20,60 C 100,60 110,35 150,35 C 190,35 200,65 320,65" className="wind-line-fast" />
|
||||||
|
<path d="M -20,100 L 110,100" className="wind-line" />
|
||||||
|
|
||||||
<path d="M 110,130 L 110,85 L 150,55 L 190,85 L 190,130 Z" fill="#475569" stroke="#64748b" strokeWidth="2" />
|
<path d="M 110,130 L 110,85 L 150,55 L 190,85 L 190,130 Z" fill="#475569" stroke="#64748b" strokeWidth="2" />
|
||||||
{/* Setas laranjas apontando para fora (inflado) */}
|
{/* Setas laranjas apontando para fora (inflado) */}
|
||||||
<path d="M 130,110 L 115,110" className="cpi-arrow-pos" markerEnd="url(#cpi-pos)" />
|
<path d="M 130,110 L 115,110" className="cpi-arrow-pos" markerEnd="url(#cpi-pos)" />
|
||||||
@@ -522,6 +535,12 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) {
|
|||||||
{styleBlock}
|
{styleBlock}
|
||||||
{legendDef}
|
{legendDef}
|
||||||
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
||||||
|
|
||||||
|
{/* Fluxo passando e sugando */}
|
||||||
|
<path d="M -20,20 L 320,20" className="wind-line-slow" />
|
||||||
|
<path d="M -20,60 C 100,60 110,35 150,35 C 190,35 200,65 320,65" className="wind-line-fast" />
|
||||||
|
<path d="M -20,100 C 90,90 100,75 115,75 L 185,75 C 200,75 210,100 320,100" className="wind-line" />
|
||||||
|
|
||||||
<path d="M 110,130 L 110,85 L 150,55 L 190,85 L 190,130 Z" fill="#475569" stroke="#64748b" strokeWidth="2" />
|
<path d="M 110,130 L 110,85 L 150,55 L 190,85 L 190,130 Z" fill="#475569" stroke="#64748b" strokeWidth="2" />
|
||||||
{/* Setas azuis apontando para dentro (sugado) */}
|
{/* Setas azuis apontando para dentro (sugado) */}
|
||||||
<path d="M 115,110 L 130,110" className="cpi-arrow-neg" markerEnd="url(#cpi-neg)" />
|
<path d="M 115,110 L 130,110" className="cpi-arrow-neg" markerEnd="url(#cpi-neg)" />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useRef, useMemo, useEffect } from 'react';
|
import { useRef, useMemo, useEffect } from 'react';
|
||||||
import { useFrame } from '@react-three/fiber';
|
import { useFrame } from '@react-three/fiber';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
|
import { useCanvasTheme } from '../../lib/theme';
|
||||||
|
|
||||||
interface WallCoefficients {
|
interface WallCoefficients {
|
||||||
A: number; B: number; C: number; D: number;
|
A: number; B: number; C: number; D: number;
|
||||||
@@ -21,6 +22,7 @@ interface AirflowSystemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AirflowSystem({ windAngle, width, length, height, permeabilityCase, wallCpe, roofCpe, cpi }: AirflowSystemProps) {
|
export function AirflowSystem({ windAngle, width, length, height, permeabilityCase, wallCpe, roofCpe, cpi }: AirflowSystemProps) {
|
||||||
|
const isDark = useCanvasTheme() === 'dark';
|
||||||
const count = 400; // Quantidade reduzida
|
const count = 400; // Quantidade reduzida
|
||||||
const meshRef = useRef<THREE.InstancedMesh>(null);
|
const meshRef = useRef<THREE.InstancedMesh>(null);
|
||||||
|
|
||||||
@@ -193,12 +195,12 @@ export function AirflowSystem({ windAngle, width, length, height, permeabilityCa
|
|||||||
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||||
|
|
||||||
if (p.currentCpe > 0) {
|
if (p.currentCpe > 0) {
|
||||||
colorObj.set('#ef4444').lerp(new THREE.Color('#fcd34d'), 1 - Math.min(1, p.currentCpe));
|
colorObj.set(isDark ? '#ef4444' : '#dc2626').lerp(new THREE.Color(isDark ? '#fcd34d' : '#f59e0b'), 1 - Math.min(1, p.currentCpe));
|
||||||
} else if (p.currentCpe < 0) {
|
} else if (p.currentCpe < 0) {
|
||||||
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
colorObj.set('#7dd3fc').lerp(new THREE.Color('#1e40af'), intensity);
|
colorObj.set(isDark ? '#7dd3fc' : '#38bdf8').lerp(new THREE.Color(isDark ? '#1e40af' : '#1d4ed8'), intensity);
|
||||||
} else {
|
} else {
|
||||||
colorObj.set('#f1f5f9');
|
colorObj.set(isDark ? '#f1f5f9' : '#64748b');
|
||||||
}
|
}
|
||||||
meshRef.current!.setColorAt(i, colorObj);
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
});
|
});
|
||||||
@@ -215,8 +217,8 @@ export function AirflowSystem({ windAngle, width, length, height, permeabilityCa
|
|||||||
<meshBasicMaterial
|
<meshBasicMaterial
|
||||||
color="#ffffff"
|
color="#ffffff"
|
||||||
transparent
|
transparent
|
||||||
opacity={0.6}
|
opacity={isDark ? 0.6 : 0.8}
|
||||||
blending={THREE.AdditiveBlending}
|
blending={isDark ? THREE.AdditiveBlending : THREE.NormalBlending}
|
||||||
depthWrite={false}
|
depthWrite={false}
|
||||||
/>
|
/>
|
||||||
</instancedMesh>
|
</instancedMesh>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import * as THREE from 'three';
|
|||||||
import SceneCanvas from '../SceneCanvas';
|
import SceneCanvas from '../SceneCanvas';
|
||||||
import FallbackDiagram from '../FallbackDiagram';
|
import FallbackDiagram from '../FallbackDiagram';
|
||||||
import { useCanvasTheme } from '@/lib/theme';
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
import { CylinderAirflowSystem } from './CylinderAirflowSystem';
|
||||||
|
|
||||||
export interface Cylinder3DInput {
|
export interface Cylinder3DInput {
|
||||||
diameter: number;
|
diameter: number;
|
||||||
@@ -12,6 +13,7 @@ export interface Cylinder3DInput {
|
|||||||
/** Cpe profile ao longo da circunferência (0° a 180°) */
|
/** Cpe profile ao longo da circunferência (0° a 180°) */
|
||||||
cpeProfile: { angle: number; cpe: number }[];
|
cpeProfile: { angle: number; cpe: number }[];
|
||||||
cpi: number;
|
cpi: number;
|
||||||
|
viewMode?: 'solid' | 'airflow';
|
||||||
}
|
}
|
||||||
|
|
||||||
function cylinderColor(cpe: number, cpi: number): THREE.Color {
|
function cylinderColor(cpe: number, cpi: number): THREE.Color {
|
||||||
@@ -23,9 +25,10 @@ function cylinderColor(cpe: number, cpi: number): THREE.Color {
|
|||||||
return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, 65 - intensity * 20)}%)`);
|
return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, 65 - intensity * 20)}%)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
|
function CylinderModel({ diameter, height, cpeProfile, cpi, viewMode = 'solid' }: Cylinder3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
|
const isAirflow = viewMode === 'airflow';
|
||||||
const segments = 64;
|
const segments = 64;
|
||||||
const radius = diameter / 2;
|
const radius = diameter / 2;
|
||||||
|
|
||||||
@@ -111,7 +114,13 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
|
|||||||
args={[normals, 3]}
|
args={[normals, 3]}
|
||||||
/>
|
/>
|
||||||
</bufferGeometry>
|
</bufferGeometry>
|
||||||
<meshStandardMaterial color={face.color} opacity={0.9} transparent roughness={0.4} side={THREE.DoubleSide} />
|
<meshStandardMaterial
|
||||||
|
color={face.color}
|
||||||
|
opacity={isAirflow ? 0.35 : 0.9}
|
||||||
|
transparent
|
||||||
|
roughness={0.4}
|
||||||
|
side={THREE.DoubleSide}
|
||||||
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -119,7 +128,13 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
|
|||||||
{/* Tampa superior sólida */}
|
{/* Tampa superior sólida */}
|
||||||
<mesh position={[0, height, 0]} rotation={[-Math.PI / 2, 0, 0]} castShadow receiveShadow>
|
<mesh position={[0, height, 0]} rotation={[-Math.PI / 2, 0, 0]} castShadow receiveShadow>
|
||||||
<circleGeometry args={[radius, segments]} />
|
<circleGeometry args={[radius, segments]} />
|
||||||
<meshStandardMaterial color={cylinderColor(cpeProfile[cpeProfile.length - 1].cpe, cpi)} opacity={0.8} transparent side={THREE.DoubleSide} roughness={0.4} />
|
<meshStandardMaterial
|
||||||
|
color={cylinderColor(cpeProfile[cpeProfile.length - 1].cpe, cpi)}
|
||||||
|
opacity={isAirflow ? 0.35 : 0.8}
|
||||||
|
transparent
|
||||||
|
side={THREE.DoubleSide}
|
||||||
|
roughness={0.4}
|
||||||
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Anéis de detalhe (bordas do cilindro) */}
|
{/* Anéis de detalhe (bordas do cilindro) */}
|
||||||
@@ -142,22 +157,24 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
|
|||||||
<cylinderGeometry args={[0.1, 0.1, 1.2, 16]} />
|
<cylinderGeometry args={[0.1, 0.1, 1.2, 16]} />
|
||||||
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<Text
|
|
||||||
position={[0, -1.5, 0]}
|
|
||||||
rotation={[Math.PI / 2, 0, 0]}
|
|
||||||
fontSize={0.4}
|
|
||||||
color="#3b82f6"
|
|
||||||
anchorX="center"
|
|
||||||
anchorY="middle"
|
|
||||||
>
|
|
||||||
Vento
|
|
||||||
</Text>
|
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
{/* Texto de Informação */}
|
{/* Texto Vento - fora do grupo para rotação horizontal compatível (0, 0, 0) e tamanho 50% maior (0.6) */}
|
||||||
|
<Text
|
||||||
|
position={[-radius - 4.2, height / 2, 0]}
|
||||||
|
rotation={[0, 0, 0]}
|
||||||
|
fontSize={0.6}
|
||||||
|
color="#3b82f6"
|
||||||
|
anchorX="center"
|
||||||
|
anchorY="middle"
|
||||||
|
>
|
||||||
|
Vento
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Texto de Informação - 50% maior */}
|
||||||
<Text
|
<Text
|
||||||
position={[0, height + 0.8, 0]}
|
position={[0, height + 0.8, 0]}
|
||||||
fontSize={Math.max(0.3, Math.min(0.6, diameter / 10))}
|
fontSize={Math.max(0.45, Math.min(0.9, (diameter / 10) * 1.5))}
|
||||||
color={isDark ? "#cbd5e1" : "#1a202c"}
|
color={isDark ? "#cbd5e1" : "#1a202c"}
|
||||||
anchorX="center"
|
anchorX="center"
|
||||||
anchorY="bottom"
|
anchorY="bottom"
|
||||||
@@ -168,7 +185,7 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Cylinder3DViewer({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
|
export default function Cylinder3DViewer({ diameter, height, cpeProfile, cpi, viewMode = 'solid' }: Cylinder3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
@@ -201,7 +218,10 @@ export default function Cylinder3DViewer({ diameter, height, cpeProfile, cpi }:
|
|||||||
shadow-camera-top={maxDim}
|
shadow-camera-top={maxDim}
|
||||||
shadow-camera-bottom={-maxDim}
|
shadow-camera-bottom={-maxDim}
|
||||||
/>
|
/>
|
||||||
<CylinderModel diameter={diameter} height={height} cpeProfile={cpeProfile} cpi={cpi} />
|
<CylinderModel diameter={diameter} height={height} cpeProfile={cpeProfile} cpi={cpi} viewMode={viewMode} />
|
||||||
|
{viewMode === 'airflow' && (
|
||||||
|
<CylinderAirflowSystem diameter={diameter} height={height} />
|
||||||
|
)}
|
||||||
<Grid
|
<Grid
|
||||||
infiniteGrid
|
infiniteGrid
|
||||||
fadeDistance={maxDim * 5}
|
fadeDistance={maxDim * 5}
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
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<THREE.InstancedMesh>(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 (sucção moderada)
|
||||||
|
} 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 ? '#ef4444' : '#dc2626')
|
||||||
|
.lerp(new THREE.Color(isDark ? '#fcd34d' : '#f59e0b'), 1 - Math.min(1, p.currentCpe));
|
||||||
|
} else if (p.currentCpe < 0) {
|
||||||
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
|
colorObj
|
||||||
|
.set(isDark ? '#7dd3fc' : '#38bdf8')
|
||||||
|
.lerp(new THREE.Color(isDark ? '#1e40af' : '#1d4ed8'), intensity);
|
||||||
|
} else {
|
||||||
|
colorObj.set(isDark ? '#f1f5f9' : '#64748b');
|
||||||
|
}
|
||||||
|
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]}>
|
||||||
|
<cylinderGeometry args={[0.01, 0.01, 1.5, 4]} />
|
||||||
|
<meshBasicMaterial
|
||||||
|
color="#ffffff"
|
||||||
|
transparent
|
||||||
|
opacity={isDark ? 0.6 : 0.8}
|
||||||
|
blending={isDark ? THREE.AdditiveBlending : THREE.NormalBlending}
|
||||||
|
depthWrite={false}
|
||||||
|
/>
|
||||||
|
</instancedMesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
|
|||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { Box, Wind } from 'lucide-react';
|
||||||
import { useWindStore } from '@/store/appStore';
|
import { useWindStore } from '@/store/appStore';
|
||||||
import { CpiSettingsCard } from '@/components/CpiSettingsCard';
|
import { CpiSettingsCard } from '@/components/CpiSettingsCard';
|
||||||
import { calculateCylinder, type CylinderEndType } from '@/lib/modules/cylinder';
|
import { calculateCylinder, type CylinderEndType } from '@/lib/modules/cylinder';
|
||||||
@@ -23,6 +25,7 @@ const CylinderModule: React.FC = () => {
|
|||||||
const [height, setHeight] = React.useState(20);
|
const [height, setHeight] = React.useState(20);
|
||||||
const [surface, setSurface] = React.useState<'rough' | 'smooth'>('rough');
|
const [surface, setSurface] = React.useState<'rough' | 'smooth'>('rough');
|
||||||
const [endType, setEndType] = React.useState<CylinderEndType>('closed');
|
const [endType, setEndType] = React.useState<CylinderEndType>('closed');
|
||||||
|
const [viewMode, setViewMode] = React.useState<'solid' | 'airflow'>('solid');
|
||||||
|
|
||||||
const result = useMemo(() => {
|
const result = useMemo(() => {
|
||||||
return calculateCylinder({
|
return calculateCylinder({
|
||||||
@@ -169,24 +172,47 @@ const CylinderModule: React.FC = () => {
|
|||||||
|
|
||||||
{/* Coluna Central: 3D */}
|
{/* Coluna Central: 3D */}
|
||||||
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
||||||
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
<div className="absolute top-4 left-4 right-4 z-10 flex flex-col sm:flex-row sm:justify-between gap-3 pointer-events-none">
|
||||||
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/50 backdrop-blur-sm shadow-sm">
|
<div className="flex flex-wrap gap-2 pointer-events-auto">
|
||||||
h/d = {result.hOverD.toFixed(2)}
|
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/50 backdrop-blur-sm shadow-sm">
|
||||||
</Badge>
|
h/d = {result.hOverD.toFixed(2)}
|
||||||
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">
|
</Badge>
|
||||||
Re = {result.re.toExponential(2)}
|
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">
|
||||||
</Badge>
|
Re = {result.re.toExponential(2)}
|
||||||
</div>
|
</Badge>
|
||||||
<div className="absolute top-4 right-4 z-10">
|
</div>
|
||||||
<EducationalManual type="cylinder" params={{ surface, endType, cpi: result.cpi }} />
|
<div className="flex bg-background/80 backdrop-blur-sm rounded-md p-1 shadow-sm border border-border pointer-events-auto ml-auto">
|
||||||
|
<Button
|
||||||
|
variant={viewMode === 'solid' ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-3 text-xs"
|
||||||
|
onClick={() => setViewMode('solid')}
|
||||||
|
>
|
||||||
|
<Box className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
Sólido
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={viewMode === 'airflow' ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-3 text-xs"
|
||||||
|
onClick={() => setViewMode('airflow')}
|
||||||
|
>
|
||||||
|
<Wind className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
Fluxo
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 pointer-events-auto justify-start sm:justify-end">
|
||||||
|
<EducationalManual type="cylinder" params={{ surface, endType, cpi: result.cpi }} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PressureLegend />
|
<PressureLegend />
|
||||||
<div className="w-full h-full bg-muted/20">
|
<div className="w-full h-full cursor-grab active:cursor-grabbing bg-muted/20">
|
||||||
<Cylinder3DViewer
|
<Cylinder3DViewer
|
||||||
diameter={diameter}
|
diameter={diameter}
|
||||||
height={height}
|
height={height}
|
||||||
cpeProfile={result.profile}
|
cpeProfile={result.profile}
|
||||||
cpi={result.cpi}
|
cpi={result.cpi}
|
||||||
|
viewMode={viewMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user