🚀 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 BarSelectorModule from './pages/BarSelectorModule';
|
||||||
import BridgeModule from './pages/BridgeModule';
|
import BridgeModule from './pages/BridgeModule';
|
||||||
import DynamicsModule from './pages/DynamicsModule';
|
import DynamicsModule from './pages/DynamicsModule';
|
||||||
|
import CalcCaModule from './pages/CalcCaModule';
|
||||||
import SettingsModule from './pages/SettingsModule';
|
import SettingsModule from './pages/SettingsModule';
|
||||||
import TowerModule from './pages/TowerModule';
|
import TowerModule from './pages/TowerModule';
|
||||||
import PiperackModule from './pages/PiperackModule';
|
import PiperackModule from './pages/PiperackModule';
|
||||||
@@ -15,7 +16,7 @@ import CertificatePage from './pages/CertificatePage';
|
|||||||
import {
|
import {
|
||||||
Home, Settings, Menu, Cylinder, Church, CircleDot,
|
Home, Settings, Menu, Cylinder, Church, CircleDot,
|
||||||
Square, Layers, BarChart3, Activity, Warehouse,
|
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';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
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: '/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: '/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: '/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 (
|
return (
|
||||||
@@ -356,6 +358,7 @@ function HomeMock() {
|
|||||||
{ to: '/pontes', icon: BridgeIcon, label: 'Pontes', desc: 'Pse, Cx/Cz do tabuleiro, flutter, galope (sec. 11).' },
|
{ 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: '/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: '/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 (
|
return (
|
||||||
@@ -421,6 +424,7 @@ function App() {
|
|||||||
<Route path="/torre" element={<TowerModule />} />
|
<Route path="/torre" element={<TowerModule />} />
|
||||||
<Route path="/piperack" element={<PiperackModule />} />
|
<Route path="/piperack" element={<PiperackModule />} />
|
||||||
<Route path="/dinamica" element={<DynamicsModule />} />
|
<Route path="/dinamica" element={<DynamicsModule />} />
|
||||||
|
<Route path="/calc-ca" element={<CalcCaModule />} />
|
||||||
<Route path="/settings" element={<SettingsModule />} />
|
<Route path="/settings" element={<SettingsModule />} />
|
||||||
<Route path="/certificado" element={<CertificatePage />} />
|
<Route path="/certificado" element={<CertificatePage />} />
|
||||||
</Routes>
|
</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>
|
</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:
|
default:
|
||||||
// Caso padrão dinâmico geral (Módulos dinâmicos simples)
|
// Caso padrão dinâmico geral (Módulos dinâmicos simples)
|
||||||
return (
|
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 { dynamicsManual } from './dynamics';
|
||||||
import { isolatedRoofManual } from './isolated-roof';
|
import { isolatedRoofManual } from './isolated-roof';
|
||||||
import { piperackManual } from './piperack';
|
import { piperackManual } from './piperack';
|
||||||
|
import { calcCaManual } from './calc_ca';
|
||||||
|
|
||||||
export const manuals: Record<string, typeof bridgeManual> = {
|
export const manuals: Record<string, typeof bridgeManual> = {
|
||||||
bridge: bridgeManual,
|
bridge: bridgeManual,
|
||||||
@@ -22,4 +23,5 @@ export const manuals: Record<string, typeof bridgeManual> = {
|
|||||||
dynamics: dynamicsManual,
|
dynamics: dynamicsManual,
|
||||||
'isolated-roof': isolatedRoofManual,
|
'isolated-roof': isolatedRoofManual,
|
||||||
piperack: piperackManual,
|
piperack: piperackManual,
|
||||||
|
calc_ca: calcCaManual,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ const translations: Dict = {
|
|||||||
nav_tower: { 'pt-BR': 'Torres', 'en-US': 'Towers' },
|
nav_tower: { 'pt-BR': 'Torres', 'en-US': 'Towers' },
|
||||||
nav_piperack: { 'pt-BR': 'Pipe-rack', 'en-US': 'Pipe-rack' },
|
nav_piperack: { 'pt-BR': 'Pipe-rack', 'en-US': 'Pipe-rack' },
|
||||||
nav_dynamics: { 'pt-BR': 'Dinâmica + Vórtices', 'en-US': 'Dynamics + Vortex' },
|
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_settings: { 'pt-BR': 'Preferências', 'en-US': 'Preferences' },
|
||||||
nav_collapse: { 'pt-BR': 'Recolher sidebar', 'en-US': 'Collapse sidebar' },
|
nav_collapse: { 'pt-BR': 'Recolher sidebar', 'en-US': 'Collapse sidebar' },
|
||||||
nav_expand: { 'pt-BR': 'Expandir sidebar', 'en-US': 'Expand 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