🚀 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})`}
|
||||
</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"
|
||||
|
||||
@@ -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)" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user