🚀 Auto-deploy: BrainWind atualizado em 29/07/2026 10:27:37

This commit is contained in:
2026-07-29 10:27:37 +00:00
parent 6141b9a41c
commit b1cadac6ab
6 changed files with 261 additions and 36 deletions
+1 -1
View File
@@ -575,7 +575,7 @@ export function WarehouseModel() {
{isParallel ? `Zona H (${roofCpe.H})` : `Zona H (${roofCpe.H})`}
</Text>
<Text
position={[widthSlope * 0.38, 0.35, 0]}
position={[widthSlope * 0.44, 0.35, 0]}
rotation={[-Math.PI / 2, 0, -Math.PI / 2]}
fontSize={zoneFontSize}
color="#000000"
+21 -2
View File
@@ -61,15 +61,22 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) {
.vibe-cyl {
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 {
stroke: #f97316;
stroke-width: 2;
fill: none;
animation: cpiPulse 1.5s ease-in-out infinite;
}
.cpi-arrow-neg {
stroke: #3b82f6;
stroke-width: 2;
fill: none;
animation: cpiPulse 1.5s ease-in-out infinite;
animation-delay: 0.75s;
}
`}</style>
);
@@ -488,8 +495,8 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) {
<path d="M -20,85 L 80,85" className="wind-line" />
{/* 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 220,70 Q 250,90 280,110" 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" className="vortex-line" />
</svg>
</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>
@@ -503,6 +510,12 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) {
{styleBlock}
{legendDef}
<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" />
{/* Setas laranjas apontando para fora (inflado) */}
<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}
{legendDef}
<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" />
{/* Setas azuis apontando para dentro (sugado) */}
<path d="M 115,110 L 130,110" className="cpi-arrow-neg" markerEnd="url(#cpi-neg)" />
+7 -5
View File
@@ -1,6 +1,7 @@
import { useRef, useMemo, useEffect } from 'react';
import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';
import { useCanvasTheme } from '../../lib/theme';
interface WallCoefficients {
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) {
const isDark = useCanvasTheme() === 'dark';
const count = 400; // Quantidade reduzida
const meshRef = useRef<THREE.InstancedMesh>(null);
@@ -193,12 +195,12 @@ export function AirflowSystem({ windAngle, width, length, height, permeabilityCa
meshRef.current!.setMatrixAt(i, dummy.matrix);
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) {
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 {
colorObj.set('#f1f5f9');
colorObj.set(isDark ? '#f1f5f9' : '#64748b');
}
meshRef.current!.setColorAt(i, colorObj);
});
@@ -215,8 +217,8 @@ export function AirflowSystem({ windAngle, width, length, height, permeabilityCa
<meshBasicMaterial
color="#ffffff"
transparent
opacity={0.6}
blending={THREE.AdditiveBlending}
opacity={isDark ? 0.6 : 0.8}
blending={isDark ? THREE.AdditiveBlending : THREE.NormalBlending}
depthWrite={false}
/>
</instancedMesh>
+37 -17
View File
@@ -5,6 +5,7 @@ import * as THREE from 'three';
import SceneCanvas from '../SceneCanvas';
import FallbackDiagram from '../FallbackDiagram';
import { useCanvasTheme } from '@/lib/theme';
import { CylinderAirflowSystem } from './CylinderAirflowSystem';
export interface Cylinder3DInput {
diameter: number;
@@ -12,6 +13,7 @@ export interface Cylinder3DInput {
/** Cpe profile ao longo da circunferência (0° a 180°) */
cpeProfile: { angle: number; cpe: number }[];
cpi: number;
viewMode?: 'solid' | 'airflow';
}
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)}%)`);
}
function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
function CylinderModel({ diameter, height, cpeProfile, cpi, viewMode = 'solid' }: Cylinder3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const isAirflow = viewMode === 'airflow';
const segments = 64;
const radius = diameter / 2;
@@ -111,7 +114,13 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
args={[normals, 3]}
/>
</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>
);
})}
@@ -119,7 +128,13 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
{/* Tampa superior sólida */}
<mesh position={[0, height, 0]} rotation={[-Math.PI / 2, 0, 0]} castShadow receiveShadow>
<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>
{/* 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]} />
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
</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>
{/* 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
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"}
anchorX="center"
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 isDark = theme === 'dark';
@@ -201,7 +218,10 @@ export default function Cylinder3DViewer({ diameter, height, cpeProfile, cpi }:
shadow-camera-top={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
infiniteGrid
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>
);
}
+37 -11
View File
@@ -3,7 +3,9 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
import { Slider } from '@/components/ui/slider';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { Box, Wind } from 'lucide-react';
import { useWindStore } from '@/store/appStore';
import { CpiSettingsCard } from '@/components/CpiSettingsCard';
import { calculateCylinder, type CylinderEndType } from '@/lib/modules/cylinder';
@@ -23,6 +25,7 @@ const CylinderModule: React.FC = () => {
const [height, setHeight] = React.useState(20);
const [surface, setSurface] = React.useState<'rough' | 'smooth'>('rough');
const [endType, setEndType] = React.useState<CylinderEndType>('closed');
const [viewMode, setViewMode] = React.useState<'solid' | 'airflow'>('solid');
const result = useMemo(() => {
return calculateCylinder({
@@ -169,24 +172,47 @@ const CylinderModule: React.FC = () => {
{/* 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="absolute top-4 left-4 z-10 flex gap-2">
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/50 backdrop-blur-sm shadow-sm">
h/d = {result.hOverD.toFixed(2)}
</Badge>
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">
Re = {result.re.toExponential(2)}
</Badge>
</div>
<div className="absolute top-4 right-4 z-10">
<EducationalManual type="cylinder" params={{ surface, endType, cpi: result.cpi }} />
<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">
<div className="flex flex-wrap gap-2 pointer-events-auto">
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/50 backdrop-blur-sm shadow-sm">
h/d = {result.hOverD.toFixed(2)}
</Badge>
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">
Re = {result.re.toExponential(2)}
</Badge>
</div>
<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>
<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
diameter={diameter}
height={height}
cpeProfile={result.profile}
cpi={result.cpi}
viewMode={viewMode}
/>
</div>
</div>