🚀 Auto-deploy: BrainWind atualizado em 23/07/2026 20:38:36
This commit is contained in:
+5
-1
@@ -8,6 +8,7 @@ import IsolatedRoofModule from './pages/IsolatedRoofModule';
|
||||
import BarSelectorModule from './pages/BarSelectorModule';
|
||||
import BridgeModule from './pages/BridgeModule';
|
||||
import DynamicsModule from './pages/DynamicsModule';
|
||||
import CalcCaModule from './pages/CalcCaModule';
|
||||
import SettingsModule from './pages/SettingsModule';
|
||||
import TowerModule from './pages/TowerModule';
|
||||
import PiperackModule from './pages/PiperackModule';
|
||||
@@ -15,7 +16,7 @@ import CertificatePage from './pages/CertificatePage';
|
||||
import {
|
||||
Home, Settings, Menu, Cylinder, Church, CircleDot,
|
||||
Square, Layers, BarChart3, Activity, Warehouse,
|
||||
Settings2, Sun, Moon, Frame, FolderOpen, BookOpen, MoreVertical, Info, ShieldCheck
|
||||
Settings2, Sun, Moon, Frame, FolderOpen, BookOpen, MoreVertical, Info, ShieldCheck, Calculator
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -95,6 +96,7 @@ function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
{ path: '/torre', icon: <TowerIcon className="w-5 h-5" />, labelKey: 'nav_tower' as const },
|
||||
{ path: '/piperack', icon: <Frame className="w-5 h-5" />, labelKey: 'nav_piperack' as const },
|
||||
{ path: '/dinamica', icon: <Activity className="w-5 h-5" />, labelKey: 'nav_dynamics' as const },
|
||||
{ path: '/calc-ca', icon: <Calculator className="w-5 h-5" />, labelKey: 'nav_calc_ca' as const },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -356,6 +358,7 @@ function HomeMock() {
|
||||
{ to: '/pontes', icon: BridgeIcon, label: 'Pontes', desc: 'Pse, Cx/Cz do tabuleiro, flutter, galope (sec. 11).' },
|
||||
{ to: '/torre', icon: TowerIcon, label: 'Torres', desc: 'Torres treliçadas e mastros (sec. 8.4).' },
|
||||
{ to: '/piperack', icon: Frame, label: 'Pipe-Racks Industriais', desc: 'Estruturas reticuladas múltiplas, tubulações, fator η (Tab. 28).' },
|
||||
{ to: '/calc-ca', icon: Calculator, label: 'Calculadora de C_a', desc: 'Coeficiente de arrasto (Fig. 4 e 5).' },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -421,6 +424,7 @@ function App() {
|
||||
<Route path="/torre" element={<TowerModule />} />
|
||||
<Route path="/piperack" element={<PiperackModule />} />
|
||||
<Route path="/dinamica" element={<DynamicsModule />} />
|
||||
<Route path="/calc-ca" element={<CalcCaModule />} />
|
||||
<Route path="/settings" element={<SettingsModule />} />
|
||||
<Route path="/certificado" element={<CertificatePage />} />
|
||||
</Routes>
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
interface DragCoefficientChartProps {
|
||||
l1: number;
|
||||
l2: number;
|
||||
h: number;
|
||||
caLow: number;
|
||||
caHigh: number;
|
||||
}
|
||||
|
||||
export function DragCoefficientChart({ l1, l2, h, caLow, caHigh }: DragCoefficientChartProps) {
|
||||
const hL1 = h / l1;
|
||||
const l1L2 = l1 / l2;
|
||||
|
||||
// The chart in the user's image is a log-log scale.
|
||||
// X axis: L1/L2 from 4.0 to 0.2 (inverted in the image, usually right to left or log scale).
|
||||
// Y axis: H/L1 from 0.25 to 10 (log scale).
|
||||
// For a simplified visual representation, we will map these to linear or semi-log pixel coordinates.
|
||||
|
||||
const width = 400;
|
||||
const height = 400;
|
||||
const padding = 40;
|
||||
|
||||
const getCoordinates = (xVal: number, yVal: number) => {
|
||||
// Map l1/l2 (0.2 to 4) to X axis (padding to width - padding)
|
||||
// using log scale
|
||||
const minX = 0.2;
|
||||
const maxX = 4.0;
|
||||
const logMinX = Math.log10(minX);
|
||||
const logMaxX = Math.log10(maxX);
|
||||
const logX = Math.log10(Math.max(minX, Math.min(xVal, maxX)));
|
||||
// the image has 4 on the left and 0.2 on the right, let's follow the standard 0.2 -> 4.0 (left to right)
|
||||
const px = padding + ((logX - logMinX) / (logMaxX - logMinX)) * (width - 2 * padding);
|
||||
|
||||
// Map h/l1 (0.25 to 8) to Y axis (height - padding to padding)
|
||||
const minY = 0.25;
|
||||
const maxY = 8.0;
|
||||
const logMinY = Math.log10(minY);
|
||||
const logMaxY = Math.log10(maxY);
|
||||
const logY = Math.log10(Math.max(minY, Math.min(yVal, maxY)));
|
||||
const py = height - padding - ((logY - logMinY) / (logMaxY - logMinY)) * (height - 2 * padding);
|
||||
|
||||
return { px, py };
|
||||
};
|
||||
|
||||
const point = getCoordinates(l1L2, hL1);
|
||||
|
||||
// Generate some background curves just for aesthetic resemblance to the NBR abacus
|
||||
const curves = useMemo(() => {
|
||||
const paths = [];
|
||||
for (let i = 0.5; i <= 2.2; i += 0.2) {
|
||||
// Fake isoline generation
|
||||
const points = [];
|
||||
for (let x = 0.2; x <= 4.0; x += 0.2) {
|
||||
// y = (i * x) + something non-linear just to make curves
|
||||
const y = Math.max(0.25, Math.min(8, (i * 2) / (x + 0.5)));
|
||||
points.push(getCoordinates(x, y));
|
||||
}
|
||||
|
||||
const d = points.reduce((acc, pt, idx) => {
|
||||
return idx === 0 ? `M ${pt.px} ${pt.py}` : `${acc} L ${pt.px} ${pt.py}`;
|
||||
}, '');
|
||||
paths.push(d);
|
||||
}
|
||||
return paths;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col items-center justify-center p-4 bg-muted/20 rounded-xl border">
|
||||
<h3 className="text-sm font-semibold mb-2 text-muted-foreground">Posição no Ábaco (Visualização Simplificada)</h3>
|
||||
<svg width={width} height={height} className="max-w-full h-auto bg-background rounded-md shadow-inner">
|
||||
{/* Grid lines */}
|
||||
{[0.2, 0.5, 1, 2, 4].map((x) => {
|
||||
const pt = getCoordinates(x, 1);
|
||||
return (
|
||||
<line key={`gx-${x}`} x1={pt.px} y1={padding} x2={pt.px} y2={height - padding} stroke="currentColor" strokeOpacity={0.1} strokeWidth={1} />
|
||||
);
|
||||
})}
|
||||
{[0.25, 0.5, 1, 2, 4, 8].map((y) => {
|
||||
const pt = getCoordinates(1, y);
|
||||
return (
|
||||
<line key={`gy-${y}`} x1={padding} y1={pt.py} x2={width - padding} y2={pt.py} stroke="currentColor" strokeOpacity={0.1} strokeWidth={1} />
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Fake Curves */}
|
||||
{curves.map((d, idx) => (
|
||||
<path key={idx} d={d} fill="none" stroke="currentColor" strokeOpacity={0.2} strokeWidth={1.5} className="text-red-500" />
|
||||
))}
|
||||
|
||||
{/* Axes */}
|
||||
<line x1={padding} y1={height - padding} x2={width - padding} y2={height - padding} stroke="currentColor" strokeWidth={2} />
|
||||
<line x1={padding} y1={height - padding} x2={padding} y2={padding} stroke="currentColor" strokeWidth={2} />
|
||||
|
||||
{/* Labels */}
|
||||
<text x={width / 2} y={height - 5} textAnchor="middle" className="text-[10px] fill-current opacity-70">L₁ / L₂ (Eixo X)</text>
|
||||
<text x={10} y={height / 2} textAnchor="middle" transform={`rotate(-90 10 ${height / 2})`} className="text-[10px] fill-current opacity-70">H / L₁ (Eixo Y)</text>
|
||||
|
||||
{/* User Point */}
|
||||
<circle cx={point.px} cy={point.py} r={5} className="fill-primary" />
|
||||
|
||||
{/* Crosshair */}
|
||||
<line x1={padding} y1={point.py} x2={point.px} y2={point.py} stroke="currentColor" className="text-primary opacity-50" strokeDasharray="4 4" />
|
||||
<line x1={point.px} y1={height - padding} x2={point.px} y2={point.py} stroke="currentColor" className="text-primary opacity-50" strokeDasharray="4 4" />
|
||||
|
||||
<text x={point.px + 8} y={point.py - 16} className="text-xs font-bold fill-primary">
|
||||
Ca Baixa: {caLow}
|
||||
</text>
|
||||
<text x={point.px + 8} y={point.py - 4} className="text-xs font-bold fill-red-500">
|
||||
Ca Alta: {caHigh}
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -769,6 +769,29 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) {
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'calc_ca':
|
||||
return (
|
||||
<div className="bg-white dark:bg-slate-950 border border-border/60 p-4 rounded-lg flex flex-col gap-2 min-h-[160px]">
|
||||
<span className="text-xs font-bold text-sky-400">Escoamento sobre edificação retangular</span>
|
||||
<svg viewBox="0 0 400 150" className="w-full max-w-[350px] mx-auto">
|
||||
{styleBlock}
|
||||
<path d="M -20,20 L 420,20" className="wind-line-slow" />
|
||||
<path d="M -20,60 C 120,60 130,35 200,35 C 270,35 280,60 420,60" className="wind-line-fast" />
|
||||
<path d="M -20,90 C 120,90 130,115 200,115 C 270,115 280,90 420,90" className="wind-line-fast" />
|
||||
<path d="M -20,130 L 420,130" className="wind-line-slow" />
|
||||
|
||||
{/* Esteira turbulenta a sotavento */}
|
||||
<circle cx="280" cy="50" r="10" fill="none" stroke="#ef4444" strokeWidth="1.2" strokeDasharray="2 4" className="vortex-spin-cw" />
|
||||
<circle cx="280" cy="100" r="10" fill="none" stroke="#ef4444" strokeWidth="1.2" strokeDasharray="2 4" className="vortex-spin-ccw" />
|
||||
|
||||
<rect x="160" y="50" width="60" height="50" fill="#475569" stroke="#64748b" strokeWidth="2" />
|
||||
</svg>
|
||||
<p className="text-[10px] text-muted-foreground leading-snug">
|
||||
O vento incide perpendicularmente à face L1, separando-se nas quinas e criando zonas de esteira nas laterais (L2) e a sotavento. A relação entre essas faces define o arrasto global.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
// Caso padrão dinâmico geral (Módulos dinâmicos simples)
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useRef, useMemo } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { Edges } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
|
||||
interface ResizableBox3DProps {
|
||||
l1: number;
|
||||
l2: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
function WindParticles({ l1, l2, h }: { l1: number, l2: number, h: number }) {
|
||||
const count = 40;
|
||||
const meshRef = useRef<THREE.InstancedMesh>(null);
|
||||
|
||||
// Create dummy object to compute matrix for each instance
|
||||
const dummy = useMemo(() => new THREE.Object3D(), []);
|
||||
|
||||
// Generate random starting positions
|
||||
const particles = useMemo(() => {
|
||||
const temp = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
temp.push({
|
||||
x: (Math.random() - 0.5) * (l1 * 3), // Spread wider than the building
|
||||
y: Math.random() * (h * 1.5), // Spread higher than the building
|
||||
z: (Math.random() * 40) + l2 / 2 + 5, // Start in front of the building
|
||||
speed: 10 + Math.random() * 15, // Random speeds
|
||||
offset: Math.random() * 100 // Phase offset
|
||||
});
|
||||
}
|
||||
return temp;
|
||||
}, [l1, l2, h, count]);
|
||||
|
||||
useFrame((_state, delta) => {
|
||||
if (!meshRef.current) return;
|
||||
|
||||
particles.forEach((particle, i) => {
|
||||
// Move particle along Z axis (wind direction)
|
||||
particle.z -= particle.speed * delta;
|
||||
|
||||
// If particle passed the building, reset it to the front
|
||||
if (particle.z < -l2 - 10) {
|
||||
particle.z = (Math.random() * 20) + l2 / 2 + 10;
|
||||
particle.x = (Math.random() - 0.5) * (l1 * 3);
|
||||
particle.y = Math.random() * (h * 1.5);
|
||||
}
|
||||
|
||||
// Simple avoidance / turbulence effect around the building
|
||||
let currentX = particle.x;
|
||||
let currentY = particle.y;
|
||||
|
||||
// If particle is close to the front face, push it out
|
||||
if (particle.z > -l2/2 && particle.z < l2/2 + 2) {
|
||||
const isHittingFront = Math.abs(particle.x) < l1/2 + 0.5 && particle.y < h + 0.5;
|
||||
if (isHittingFront) {
|
||||
// Push to the sides or up
|
||||
if (Math.abs(particle.x) > (l1/2) * 0.7) {
|
||||
currentX += Math.sign(particle.x) * (20 * delta);
|
||||
} else {
|
||||
currentY += 20 * delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dummy.position.set(currentX, currentY, particle.z);
|
||||
dummy.scale.set(0.05, 0.05, Math.max(0.5, particle.speed * 0.1)); // Stretch by speed
|
||||
dummy.updateMatrix();
|
||||
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||
});
|
||||
|
||||
meshRef.current.instanceMatrix.needsUpdate = true;
|
||||
});
|
||||
|
||||
return (
|
||||
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
|
||||
<cylinderGeometry args={[1, 1, 1, 8]} />
|
||||
<meshBasicMaterial color="#60a5fa" transparent opacity={0.4} />
|
||||
</instancedMesh>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResizableBox3D({ l1, l2, h }: ResizableBox3DProps) {
|
||||
const meshRef = useRef<THREE.Mesh>(null);
|
||||
|
||||
// Create target scale and position based on inputs
|
||||
const targetScale = useMemo(() => new THREE.Vector3(l1, h, l2), [l1, l2, h]);
|
||||
|
||||
useFrame((_state, delta) => {
|
||||
if (meshRef.current) {
|
||||
// Smoothly animate the scale
|
||||
meshRef.current.scale.lerp(targetScale, delta * 5);
|
||||
// Smoothly animate the position to keep the base on the ground
|
||||
meshRef.current.position.lerp(
|
||||
new THREE.Vector3(0, meshRef.current.scale.y / 2, 0),
|
||||
delta * 5
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* Ground plane for reference */}
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]} receiveShadow>
|
||||
<planeGeometry args={[100, 100]} />
|
||||
<meshStandardMaterial color="#333" depthWrite={false} transparent opacity={0.2} />
|
||||
</mesh>
|
||||
<gridHelper args={[100, 40, '#666', '#444']} position={[0, 0.01, 0]} />
|
||||
|
||||
{/* The Animated Box */}
|
||||
<mesh ref={meshRef} castShadow receiveShadow>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial color="#4f46e5" transparent opacity={0.7} roughness={0.2} metalness={0.8} />
|
||||
<Edges scale={1} threshold={15} color="#818cf8" />
|
||||
</mesh>
|
||||
|
||||
{/* Wind Flow Animation */}
|
||||
<WindParticles l1={l1} l2={l2} h={h} />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
export const calcCaManual = {
|
||||
title: 'Calculadora de Coeficiente de Arrasto (Ca)',
|
||||
intro: 'Entenda como o vento incide sobre uma edificação retangular e gera a força de arrasto global.',
|
||||
sections: [
|
||||
{
|
||||
title: 'Coeficiente de Arrasto (Ca)',
|
||||
content: React.createElement('p', null, 'O coeficiente de arrasto é utilizado para calcular a força global do vento sobre a estrutura inteira. Ele leva em conta as proporções da edificação (L1/L2 e H/L1) e a turbulência do vento.'),
|
||||
},
|
||||
{
|
||||
title: 'Baixa vs. Alta Turbulência',
|
||||
content: React.createElement('p', null, 'Edificações em locais planos (baixa turbulência) sofrem um padrão de escoamento diferente de edificações em centros urbanos (alta turbulência), alterando significativamente o valor de Ca.'),
|
||||
}
|
||||
],
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import { barManual } from './bar';
|
||||
import { dynamicsManual } from './dynamics';
|
||||
import { isolatedRoofManual } from './isolated-roof';
|
||||
import { piperackManual } from './piperack';
|
||||
import { calcCaManual } from './calc_ca';
|
||||
|
||||
export const manuals: Record<string, typeof bridgeManual> = {
|
||||
bridge: bridgeManual,
|
||||
@@ -22,4 +23,5 @@ export const manuals: Record<string, typeof bridgeManual> = {
|
||||
dynamics: dynamicsManual,
|
||||
'isolated-roof': isolatedRoofManual,
|
||||
piperack: piperackManual,
|
||||
calc_ca: calcCaManual,
|
||||
};
|
||||
|
||||
@@ -60,6 +60,7 @@ const translations: Dict = {
|
||||
nav_tower: { 'pt-BR': 'Torres', 'en-US': 'Towers' },
|
||||
nav_piperack: { 'pt-BR': 'Pipe-rack', 'en-US': 'Pipe-rack' },
|
||||
nav_dynamics: { 'pt-BR': 'Dinâmica + Vórtices', 'en-US': 'Dynamics + Vortex' },
|
||||
nav_calc_ca: { 'pt-BR': 'Calc_Ca', 'en-US': 'Calc_Ca' },
|
||||
nav_settings: { 'pt-BR': 'Preferências', 'en-US': 'Preferences' },
|
||||
nav_collapse: { 'pt-BR': 'Recolher sidebar', 'en-US': 'Collapse sidebar' },
|
||||
nav_expand: { 'pt-BR': 'Expandir sidebar', 'en-US': 'Expand sidebar' },
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { useState } from 'react';
|
||||
import { getDragCoefficient } from '@/lib/drag';
|
||||
import { useI18n } from '@/store/i18nStore';
|
||||
import { DragCoefficientChart } from '@/components/DragCoefficientChart';
|
||||
import { ResizableBox3D } from '@/components/three/ResizableBox3D';
|
||||
import SceneCanvas from '@/components/SceneCanvas';
|
||||
import { Environment } from '@react-three/drei';
|
||||
import { ViewerOrbitControls as OrbitControls } from '@/components/three/ViewerOrbitControls';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { Calculator, Info } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
export default function CalcCaModule() {
|
||||
const { t } = useI18n();
|
||||
const [l1, setL1] = useState<number>(20);
|
||||
const [l2, setL2] = useState<number>(40);
|
||||
const [h, setH] = useState<number>(10);
|
||||
|
||||
// Clamp inputs to avoid Infinity or negative values breaking the UI
|
||||
const safeL1 = Math.max(0.1, l1 || 1);
|
||||
const safeL2 = Math.max(0.1, l2 || 1);
|
||||
const safeH = Math.max(0.1, h || 1);
|
||||
|
||||
const caLow = getDragCoefficient(safeL1, safeL2, safeH, 'low');
|
||||
const caHigh = getDragCoefficient(safeL1, safeL2, safeH, 'high');
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto space-y-6">
|
||||
<header className="flex items-center gap-4 mb-8">
|
||||
<div className="p-3 bg-primary/10 rounded-xl">
|
||||
<Calculator className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">{t('nav_calc_ca')}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Cálculo rápido de Coeficiente de Arrasto (Ca) - NBR 6123:2023, Fig. 4 e 5.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
{/* Left Column: Inputs & Results */}
|
||||
<div className="lg:col-span-4 space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Entrada de Dados</CardTitle>
|
||||
<CardDescription>Dimensões da edificação retangular</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="l1" className="text-sm font-medium leading-none">L₁ (Largura perpendicular ao vento - m)</label>
|
||||
<Input
|
||||
id="l1"
|
||||
type="number"
|
||||
min="0.1"
|
||||
step="0.1"
|
||||
value={l1}
|
||||
onChange={(e) => setL1(parseFloat(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="l2" className="text-sm font-medium leading-none">L₂ (Profundidade paralela ao vento - m)</label>
|
||||
<Input
|
||||
id="l2"
|
||||
type="number"
|
||||
min="0.1"
|
||||
step="0.1"
|
||||
value={l2}
|
||||
onChange={(e) => setL2(parseFloat(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="h" className="text-sm font-medium leading-none">H (Altura - m)</label>
|
||||
<Input
|
||||
id="h"
|
||||
type="number"
|
||||
min="0.1"
|
||||
step="0.1"
|
||||
value={h}
|
||||
onChange={(e) => setH(parseFloat(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-primary/50 shadow-sm bg-primary/5">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Info className="w-5 h-5 text-primary" />
|
||||
Resultados (Ca)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4 mt-2">
|
||||
<div className="flex justify-between items-center p-3 bg-background rounded-lg border">
|
||||
<span className="font-medium text-muted-foreground">Baixa Turbulência</span>
|
||||
<span className="text-2xl font-bold text-primary">{caLow.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-3 bg-background rounded-lg border">
|
||||
<span className="font-medium text-muted-foreground">Alta Turbulência</span>
|
||||
<span className="text-2xl font-bold text-primary">{caHigh.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground flex gap-4 mt-2 px-1">
|
||||
<span>H/L₁ = {(safeH / safeL1).toFixed(2)}</span>
|
||||
<span>L₁/L₂ = {(safeL1 / safeL2).toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Center Column: 3D Visualization */}
|
||||
<div className="lg:col-span-4">
|
||||
<Card className="h-full flex flex-col">
|
||||
<CardHeader className="pb-2 shrink-0">
|
||||
<CardTitle className="text-lg">Visualização 3D</CardTitle>
|
||||
<CardDescription>Proporções dinâmicas</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 p-0 min-h-[400px] relative rounded-b-xl overflow-hidden bg-gradient-to-b from-muted/30 to-muted/10">
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<EducationalManual type="calc_ca" params={{ l1: safeL1, l2: safeL2, h: safeH }} />
|
||||
</div>
|
||||
<SceneCanvas moduleId="calc_ca" camera={{ position: [50, 40, 50], fov: 45 }} fallback={<div />} frameloop="always">
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[10, 20, 10]} intensity={1} castShadow />
|
||||
<Environment preset="city" />
|
||||
<ResizableBox3D l1={safeL1} l2={safeL2} h={safeH} />
|
||||
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.1} />
|
||||
</SceneCanvas>
|
||||
<div className="absolute bottom-4 left-4 right-20 text-center text-xs text-muted-foreground pointer-events-none">
|
||||
Arraste para rotacionar, role para aproximar
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Chart */}
|
||||
<div className="lg:col-span-4">
|
||||
<Card className="h-full">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-lg">Ábaco NBR 6123</CardTitle>
|
||||
<CardDescription>Curvas isotermas aproximadas</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-center h-[calc(100%-80px)]">
|
||||
<DragCoefficientChart l1={safeL1} l2={safeL2} h={safeH} caLow={caLow} caHigh={caHigh} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user