feat: adicionar super manual didatico com fluxos 2d e sheet panel

This commit is contained in:
2026-07-09 23:40:06 +00:00
parent 9fece3f174
commit 40e73e734a
34 changed files with 1443 additions and 390 deletions
+74
View File
@@ -0,0 +1,74 @@
import { BookOpen, HelpCircle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import { manuals } from '@/data/manuals';
import { WindFlowGrid2D } from './WindFlowGrid2D';
interface EducationalManualProps {
type: string;
params: Record<string, any>;
}
export function EducationalManual({ type, params }: EducationalManualProps) {
const manual = manuals[type];
if (!manual) {
return null;
}
return (
<Sheet>
<SheetTrigger asChild>
<Button
variant="outline"
size="icon"
className="size-10 bg-background/80 backdrop-blur-xs border border-border shadow-md hover:bg-accent/80 transition-all rounded-full flex items-center justify-center"
title="Manual Didático da Norma NBR 6123"
>
<BookOpen className="size-5 text-primary animate-pulse" />
</Button>
</SheetTrigger>
<SheetContent className="overflow-y-auto w-full sm:max-w-xl p-6 flex flex-col gap-6 border-l border-border bg-card">
<SheetHeader className="border-b border-border pb-4 flex flex-col gap-1.5">
<div className="flex items-center gap-2 text-primary">
<HelpCircle className="size-5" />
<SheetTitle className="text-xl font-bold tracking-tight">{manual.title}</SheetTitle>
</div>
<SheetDescription className="text-sm text-muted-foreground leading-relaxed">
{manual.intro}
</SheetDescription>
</SheetHeader>
{/* Simulador Interativo 2D no topo do manual */}
<WindFlowGrid2D type={type} params={params} />
{/* Seções de Texto e Fórmulas */}
<div className="flex flex-col gap-6 text-foreground leading-relaxed">
{manual.sections.map((section, idx) => (
<div key={idx} className="flex flex-col gap-3 bg-muted/20 p-4 rounded-xl border border-border/40">
<h3 className="text-base font-bold text-primary flex items-center gap-2 border-b border-border/60 pb-1.5">
{section.title}
</h3>
<div className="text-sm prose prose-neutral dark:prose-invert max-w-none text-muted-foreground">
{section.content}
</div>
</div>
))}
</div>
<div className="mt-auto border-t border-border pt-4 text-center">
<p className="text-[10px] text-muted-foreground font-mono">
VentoApp Referência: ABNT NBR 6123:2023
</p>
</div>
</SheetContent>
</Sheet>
);
}
+3 -2
View File
@@ -403,6 +403,7 @@ interface IsolatedRoofProps {
type: 'shed' | 'gable'; type: 'shed' | 'gable';
theta: number; theta: number;
height: number; height: number;
width: number;
depth: number; depth: number;
cpeWindward: number; cpeWindward: number;
cpeLeeward: number; cpeLeeward: number;
@@ -410,7 +411,7 @@ interface IsolatedRoofProps {
forceKN: number; forceKN: number;
} }
function IsolatedRoofDiagram({ type, theta, height, depth, cpeWindward, cpeLeeward, cpeTop, forceKN }: IsolatedRoofProps) { function IsolatedRoofDiagram({ type, theta, height, width, depth, cpeWindward, cpeLeeward, cpeTop, forceKN }: IsolatedRoofProps) {
const vw = 400, vh = 300; const vw = 400, vh = 300;
const s = Math.min((vw - 100) / depth, (vh - 80) / (height + depth * Math.tan((theta * Math.PI) / 180))); const s = Math.min((vw - 100) / depth, (vh - 80) / (height + depth * Math.tan((theta * Math.PI) / 180)));
const cx = vw / 2, ground = vh - 40; const cx = vw / 2, ground = vh - 40;
@@ -451,7 +452,7 @@ function IsolatedRoofDiagram({ type, theta, height, depth, cpeWindward, cpeLeewa
<text x={cx + 12} y={ground - h - rise - 12 - forceLen(forceKN) / 2} fontSize={8} fill="#ef4444">{forceKN.toFixed(1)} kN</text> <text x={cx + 12} y={ground - h - rise - 12 - forceLen(forceKN) / 2} fontSize={8} fill="#ef4444">{forceKN.toFixed(1)} kN</text>
</g> </g>
)} )}
<text x={cx} y={ground + 18} textAnchor="middle" fontSize={9} fill="#475569">θ = {theta}° | h = {height}m | prof. = {depth}m</text> <text x={cx} y={ground + 18} textAnchor="middle" fontSize={9} fill="#475569">θ = {theta}° | h = {height}m | b = {width}m | l = {depth}m</text>
</svg> </svg>
); );
} }
+21 -1
View File
@@ -8,15 +8,35 @@ interface SceneCanvasProps extends CanvasProps {
fallback: ReactNode; fallback: ReactNode;
} }
function CanvasInner({ fallback: _, ...canvasProps }: SceneCanvasProps) { function CanvasInner({ fallback: _, shadows, ...canvasProps }: SceneCanvasProps) {
const registerCanvas = useCaptureStore((s) => s.registerCanvas); const registerCanvas = useCaptureStore((s) => s.registerCanvas);
const unregisterCanvas = useCaptureStore((s) => s.unregisterCanvas); const unregisterCanvas = useCaptureStore((s) => s.unregisterCanvas);
useEffect(() => () => unregisterCanvas(), [unregisterCanvas]); useEffect(() => () => unregisterCanvas(), [unregisterCanvas]);
// Suprimir avisos de depreciação conhecidos do Three.js (r185) que o R3F ainda aciona
useEffect(() => {
const originalWarn = console.warn;
console.warn = (...args) => {
if (typeof args[0] === 'string' && (args[0].includes('THREE.Clock') || args[0].includes('THREE.WebGLShadowMap'))) {
return;
}
originalWarn(...args);
};
return () => {
console.warn = originalWarn;
};
}, []);
// O R3F v9 usa PCFSoftShadowMap por padrão, que está deprecado no Three r185.
// Mudar para PCFShadowMap (1) via propriedade shadows.
const shadowConfig = shadows === true ? { type: 1 as any } : shadows;
return ( return (
<Canvas <Canvas
frameloop="demand"
{...canvasProps} {...canvasProps}
shadows={shadowConfig}
onCreated={(state) => { onCreated={(state) => {
registerCanvas(state.gl.domElement); registerCanvas(state.gl.domElement);
canvasProps.onCreated?.(state); canvasProps.onCreated?.(state);
+11 -11
View File
@@ -32,20 +32,11 @@ function PressureArrow({
}) { }) {
const { q } = useWindStore(); const { q } = useWindStore();
const force = p * q; // kN/m2 const force = p * q; // kN/m2
if (Math.abs(force) < 0.05) return null;
const length = Math.max(0.6, Math.min(3.0, Math.abs(force) * 1.5));
const isPressure = p > 0;
const color = isPressure ? '#3b82f6' : '#ef4444';
const normVec = useMemo(() => new THREE.Vector3(...normal).normalize(), [normal]); const normVec = useMemo(() => new THREE.Vector3(...normal).normalize(), [normal]);
const centerVec = useMemo(() => new THREE.Vector3(...center), [center]); const centerVec = useMemo(() => new THREE.Vector3(...center), [center]);
const isPressure = p > 0;
const dir = isPressure ? normVec.clone().negate() : normVec.clone(); const dir = useMemo(() => isPressure ? normVec.clone().negate() : normVec.clone(), [isPressure, normVec]);
const start = isPressure ? centerVec.clone().sub(dir.clone().multiplyScalar(length)) : centerVec;
const end = isPressure ? centerVec : centerVec.clone().add(dir.clone().multiplyScalar(length));
const mid = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5);
const quat = useMemo(() => { const quat = useMemo(() => {
const q = new THREE.Quaternion(); const q = new THREE.Quaternion();
@@ -53,6 +44,15 @@ function PressureArrow({
return new THREE.Euler().setFromQuaternion(q); return new THREE.Euler().setFromQuaternion(q);
}, [dir]); }, [dir]);
if (Math.abs(force) < 0.05) return null;
const length = Math.max(0.6, Math.min(3.0, Math.abs(force) * 1.5));
const color = isPressure ? '#3b82f6' : '#ef4444';
const start = isPressure ? centerVec.clone().sub(dir.clone().multiplyScalar(length)) : centerVec;
const end = isPressure ? centerVec : centerVec.clone().add(dir.clone().multiplyScalar(length));
const mid = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5);
const headLen = Math.min(0.4, length * 0.4); const headLen = Math.min(0.4, length * 0.4);
return ( return (
+376
View File
@@ -0,0 +1,376 @@
interface WindFlowGrid2DProps {
type: string;
params: Record<string, any>;
}
export function WindFlowGrid2D({ type, params }: WindFlowGrid2DProps) {
// Estilo global de animação inserido no próprio SVG
const styleBlock = (
<style>{`
@keyframes windFlow {
0% { stroke-dashoffset: 36; }
100% { stroke-dashoffset: 0; }
}
@keyframes vortexRotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes vibration {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-8px); }
}
.wind-line {
stroke: #60a5fa;
stroke-width: 1.8;
stroke-dasharray: 8 16;
animation: windFlow 1.8s linear infinite;
fill: none;
opacity: 0.85;
}
.wind-line-fast {
stroke: #38bdf8;
stroke-width: 2.2;
stroke-dasharray: 6 12;
animation: windFlow 0.9s linear infinite;
fill: none;
}
.wind-line-slow {
stroke: #93c5fd;
stroke-width: 1.5;
stroke-dasharray: 10 20;
animation: windFlow 3s linear infinite;
fill: none;
opacity: 0.5;
}
.vortex-line {
stroke: #f87171;
stroke-width: 1.5;
stroke-dasharray: 4 8;
animation: windFlow 2.5s linear infinite;
fill: none;
}
.vortex-spin-cw {
transform-origin: center;
animation: vortexRotate 3s linear infinite;
}
.vortex-spin-ccw {
transform-origin: center;
animation: vortexRotate 3s linear infinite reverse;
}
.vibe-cyl {
animation: vibration 1.5s ease-in-out infinite;
}
`}</style>
);
// Renderizadores específicos de fluxo ativo com base nos sliders da página
const renderActiveSimulation = () => {
switch (type) {
case 'bridge': {
const alpha = Number(params.alpha || 0);
// O vento muda de ângulo
const rotationAngle = alpha * 3; // amplifica o ângulo para ficar visível
return (
<div className="flex flex-col gap-2">
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Simulação Ativa (com suas variáveis)</h4>
<div className="relative aspect-video w-full bg-slate-950 border border-border rounded-lg overflow-hidden">
<svg viewBox="0 0 400 200" className="w-full h-full">
{styleBlock}
{/* Linhas de vento de fundo */}
<g transform={`rotate(${rotationAngle}, 200, 100)`}>
<path d="M -50,30 L 450,30" className="wind-line-slow" />
<path d="M -50,70 Q 150,50 450,70" className="wind-line" />
<path d="M -50,130 Q 150,150 450,130" className="wind-line" />
<path d="M -50,170 L 450,170" className="wind-line-slow" />
{/* Linhas aceleradas acima e abaixo */}
<path d="M -50,90 Q 200,60 450,90" className="wind-line-fast" />
<path d="M -50,110 Q 200,140 450,110" className="wind-line-fast" />
</g>
{/* Tabuleiro da ponte */}
<g transform="translate(150, 85)">
<rect x="0" y="0" width="100" height="30" rx="4" fill="#1e293b" stroke="#334155" strokeWidth="2" />
{/* Barreiras de proteção */}
<rect x="5" y="-6" width="6" height="6" fill="#64748b" />
<rect x="89" y="-6" width="6" height="6" fill="#64748b" />
</g>
{/* Vetores de Força baseados no ângulo */}
{alpha !== 0 && (
<g>
{/* Vetor de sustentação (Fz) */}
<path
d={alpha > 0 ? "M 200,75 L 200,30" : "M 200,125 L 200,170"}
stroke={alpha > 0 ? "#10b981" : "#ef4444"}
strokeWidth="3"
fill="none"
markerEnd="url(#arrow)"
/>
<text x="210" y={alpha > 0 ? 45 : 160} fill={alpha > 0 ? "#10b981" : "#ef4444"} className="text-xs font-bold font-mono">
{alpha > 0 ? 'Sustentação (+Fz)' : 'Downforce (-Fz)'}
</text>
</g>
)}
{/* Marcadores SVG de setas */}
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="context-stroke" />
</marker>
</defs>
</svg>
<div className="absolute bottom-2 right-2 bg-slate-900/80 backdrop-blur px-2 py-1 rounded text-[10px] text-sky-400 font-mono">
Ângulo: {alpha}º
</div>
</div>
</div>
);
}
case 'tower': {
const section = params.section || 'square';
const alphaWind = Number(params.alphaWind || 0);
return (
<div className="flex flex-col gap-2">
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Simulação Ativa (com suas variáveis)</h4>
<div className="relative aspect-video w-full bg-slate-950 border border-border rounded-lg overflow-hidden">
<svg viewBox="0 0 400 200" className="w-full h-full">
{styleBlock}
{/* Linhas de vento fluindo */}
<g>
<path d="M -20,20 L 420,20" className="wind-line-slow" />
<path d="M -20,60 C 150,60 170,40 230,40 C 270,40 320,60 420,60" className="wind-line" />
<path d="M -20,140 C 150,140 170,160 230,160 C 270,160 320,140 420,140" className="wind-line" />
<path d="M -20,180 L 420,180" className="wind-line-slow" />
{/* Turbulência traseira (esteira) */}
<path d="M 230,90 Q 300,75 420,90" className="vortex-line" />
<path d="M 230,110 Q 300,125 420,110" className="vortex-line" />
</g>
{/* Perfil da Seção da Torre (Centro) */}
<g transform={`translate(200, 100) rotate(${alphaWind})`}>
{section === 'square' ? (
<rect x="-25" y="-25" width="50" height="50" fill="none" stroke="#64748b" strokeWidth="4" />
) : (
<polygon points="0,-28 25,15 -25,15" fill="none" stroke="#64748b" strokeWidth="4" />
)}
{/* Barras internas treliçadas */}
{section === 'square' ? (
<>
<line x1="-25" y1="-25" x2="25" y2="25" stroke="#475569" strokeWidth="1.5" />
<line x1="25" y1="-25" x2="-25" y2="25" stroke="#475569" strokeWidth="1.5" />
</>
) : (
<>
<line x1="0" y1="-28" x2="0" y2="15" stroke="#475569" strokeWidth="1.5" />
<line x1="-25" y1="15" x2="12.5" y2="-6.5" stroke="#475569" strokeWidth="1.5" />
<line x1="25" y1="15" x2="-12.5" y2="-6.5" stroke="#475569" strokeWidth="1.5" />
</>
)}
</g>
</svg>
<div className="absolute bottom-2 right-2 bg-slate-900/80 backdrop-blur px-2 py-1 rounded text-[10px] text-sky-400 font-mono">
{section === 'square' ? 'Quadrada' : 'Triangular'} | {alphaWind}º
</div>
</div>
</div>
);
}
case 'cylinder': {
const isSmooth = params.roughness === 'smooth' || params.d_over_k > 1e5;
return (
<div className="flex flex-col gap-2">
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Simulação Ativa (com suas variáveis)</h4>
<div className="relative aspect-video w-full bg-slate-950 border border-border rounded-lg overflow-hidden">
<svg viewBox="0 0 400 200" className="w-full h-full">
{styleBlock}
{/* Fluxo contornando cilindro */}
<g>
<path d="M -20,20 L 420,20" className="wind-line-slow" />
<path d="M -20,60 C 150,55 160,35 200,35 C 240,35 250,55 420,55" className="wind-line-fast" />
<path d="M -20,140 C 150,145 160,165 200,165 C 240,165 250,145 420,145" className="wind-line-fast" />
<path d="M -20,180 L 420,180" className="wind-line-slow" />
</g>
{/* Esteira de Vórtices de Von Kármán */}
<g transform="translate(250, 100)">
<circle cx="20" cy="-25" r="10" fill="none" stroke="#ef4444" strokeWidth="1.5" strokeDasharray="3 6" className="vortex-spin-cw" />
<circle cx="60" cy="25" r="12" fill="none" stroke="#ef4444" strokeWidth="1.5" strokeDasharray="3 6" className="vortex-spin-ccw" />
<circle cx="100" cy="-20" r="14" fill="none" stroke="#ef4444" strokeWidth="1.5" strokeDasharray="3 6" className="vortex-spin-cw" />
<path d="M 0,0 Q 40,-35 80,0 Q 120,35 160,0" className="vortex-line" />
</g>
{/* Cilindro */}
<circle cx="200" cy="100" r="35" fill="#334155" stroke="#475569" strokeWidth={isSmooth ? "2" : "5"} />
</svg>
<div className="absolute bottom-2 right-2 bg-slate-900/80 backdrop-blur px-2 py-1 rounded text-[10px] text-sky-400 font-mono">
Superfície: {isSmooth ? 'Lisa (Menor Arrasto)' : 'Rugosa (Maior Arrasto)'}
</div>
</div>
</div>
);
}
case 'dynamics': {
const fn = Number(params.fn || 0.5);
// Frequência regula a velocidade da vibração visual
const vibeDuration = Math.max(0.5, Math.min(2.5, 1 / fn));
return (
<div className="flex flex-col gap-2">
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Simulação Ativa (com suas variáveis)</h4>
<div className="relative aspect-video w-full bg-slate-950 border border-border rounded-lg overflow-hidden">
<svg viewBox="0 0 400 200" className="w-full h-full">
{styleBlock}
{/* Linhas de vento */}
<g>
<path d="M -20,20 L 420,20" className="wind-line-slow" />
<path d="M -20,60 C 150,55 160,35 200,35 C 240,35 250,55 420,55" className="wind-line" />
<path d="M -20,140 C 150,145 160,165 200,165 C 240,165 250,145 420,145" className="wind-line" />
<path d="M -20,180 L 420,180" className="wind-line-slow" />
</g>
{/* Vórtices alternados desalinhados de Von Kármán */}
<g transform="translate(250, 100)">
<path d="M 0,0 Q 40,-25 80,10 Q 120,-15 160,5" className="vortex-line" />
</g>
{/* Cilindro que VIBRA verticalmente na ressonância */}
<g className="vibe-cyl" style={{ animationDuration: `${vibeDuration}s` }}>
<circle cx="200" cy="100" r="30" fill="#ef4444" stroke="#f87171" strokeWidth="2" opacity="0.9" />
<line x1="200" y1="100" x2="200" y2="40" stroke="#f87171" strokeWidth="2" strokeDasharray="2 4" />
</g>
</svg>
<div className="absolute bottom-2 right-2 bg-slate-900/80 backdrop-blur px-2 py-1 rounded text-[10px] text-sky-400 font-mono">
fn: {fn} Hz | Oscilação Ativa
</div>
</div>
</div>
);
}
default:
// Padrão genérico de fluxo para os demais módulos
return (
<div className="flex flex-col gap-2">
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Visualização do Escoamento</h4>
<div className="relative aspect-video w-full bg-slate-950 border border-border rounded-lg overflow-hidden">
<svg viewBox="0 0 400 200" className="w-full h-full">
{styleBlock}
<path d="M -20,30 L 420,30" className="wind-line-slow" />
<path d="M -20,70 Q 200,40 420,70" className="wind-line" />
<path d="M -20,130 Q 200,160 420,130" className="wind-line" />
<path d="M -20,170 L 420,170" className="wind-line-slow" />
{/* Elemento genérico no centro */}
<rect x="180" y="80" width="40" height="40" rx="3" fill="#334155" stroke="#475569" strokeWidth="2" />
</svg>
</div>
</div>
);
}
};
// Comparativos estáticos baseados em grids como sugerido pelo usuário
const renderComparisonGrid = () => {
if (type === 'bridge') {
return (
<div className="grid grid-cols-2 gap-4 mt-4">
<div className="flex flex-col gap-1">
<span className="text-[10px] font-bold text-muted-foreground uppercase">Incidência Descendente (α = -5º)</span>
<div className="aspect-video w-full bg-slate-950 border border-border rounded-lg overflow-hidden">
<svg viewBox="0 0 400 200" className="w-full h-full opacity-90">
{styleBlock}
<g transform="rotate(-15, 200, 100)">
<path d="M -50,50 L 450,50" className="wind-line-slow" />
<path d="M -50,100 L 450,100" className="wind-line" />
<path d="M -50,150 L 450,150" className="wind-line-slow" />
</g>
<rect x="150" y="85" width="100" height="30" rx="4" fill="#334155" />
{/* Seta para baixo */}
<path d="M 200,125 L 200,170" stroke="#ef4444" strokeWidth="3" markerEnd="url(#arrow)" />
</svg>
</div>
<p className="text-[10px] text-muted-foreground">Vento empurra o tabuleiro para baixo, aumentando os esforços de compressão nos pilares.</p>
</div>
<div className="flex flex-col gap-1">
<span className="text-[10px] font-bold text-muted-foreground uppercase">Incidência Ascendente (α = +5º)</span>
<div className="aspect-video w-full bg-slate-950 border border-border rounded-lg overflow-hidden">
<svg viewBox="0 0 400 200" className="w-full h-full opacity-90">
{styleBlock}
<g transform="rotate(15, 200, 100)">
<path d="M -50,50 L 450,50" className="wind-line-slow" />
<path d="M -50,100 L 450,100" className="wind-line" />
<path d="M -50,150 L 450,150" className="wind-line-slow" />
</g>
<rect x="150" y="85" width="100" height="30" rx="4" fill="#334155" />
{/* Seta para cima */}
<path d="M 200,75 L 200,30" stroke="#10b981" strokeWidth="3" markerEnd="url(#arrow)" />
</svg>
</div>
<p className="text-[10px] text-muted-foreground">Efeito aerofólio levanta a ponte. Crítico para pontes estaiadas ou suspensas.</p>
</div>
</div>
);
}
if (type === 'tower') {
return (
<div className="grid grid-cols-2 gap-4 mt-4">
<div className="flex flex-col gap-1">
<span className="text-[10px] font-bold text-muted-foreground uppercase">Vento Normal (0º)</span>
<div className="aspect-video w-full bg-slate-950 border border-border rounded-lg overflow-hidden">
<svg viewBox="0 0 400 200" className="w-full h-full">
{styleBlock}
<path d="M -20,60 C 150,60 170,45 230,45 Q 320,60 420,60" className="wind-line" />
<path d="M -20,140 C 150,140 170,155 230,155 Q 320,140 420,140" className="wind-line" />
<g transform="translate(180, 80)">
<rect x="0" y="0" width="40" height="40" fill="none" stroke="#64748b" strokeWidth="3" />
<line x1="0" y1="0" x2="40" y2="40" stroke="#475569" />
<line x1="40" y1="0" x2="0" y2="40" stroke="#475569" />
</g>
</svg>
</div>
<p className="text-[10px] text-muted-foreground">Vento incidindo perpendicularmente à face frontal. Menor área exposta efetiva.</p>
</div>
<div className="flex flex-col gap-1">
<span className="text-[10px] font-bold text-muted-foreground uppercase">Vento na Diagonal (45º)</span>
<div className="aspect-video w-full bg-slate-950 border border-border rounded-lg overflow-hidden">
<svg viewBox="0 0 400 200" className="w-full h-full">
{styleBlock}
<path d="M -20,40 C 150,40 170,30 230,30 Q 320,40 420,40" className="wind-line" />
<path d="M -20,160 C 150,160 170,170 230,170 Q 320,160 420,160" className="wind-line" />
<g transform="translate(200, 100) rotate(45)">
<rect x="-20" y="-20" width="40" height="40" fill="none" stroke="#64748b" strokeWidth="3" />
<line x1="-20" y1="-20" x2="20" y2="20" stroke="#475569" />
</g>
</svg>
</div>
<p className="text-[10px] text-muted-foreground">Aumenta a esteira e o coeficiente de arrasto Ca nas torres de seção quadrada.</p>
</div>
</div>
);
}
// Grid padrão se não houver um caso comparativo customizado
return null;
};
return (
<div className="flex flex-col gap-4 my-6 bg-slate-900/40 p-4 border rounded-xl">
<div className="flex justify-between items-center">
<h3 className="text-sm font-semibold">Túnel de Vento Virtual (Escoamento 2D)</h3>
<span className="text-[10px] bg-sky-500/10 text-sky-400 font-bold border border-sky-500/20 px-2 py-0.5 rounded-full">SVG Interativo</span>
</div>
{renderActiveSimulation()}
{renderComparisonGrid()}
</div>
);
}
+30 -70
View File
@@ -25,11 +25,6 @@ export interface Bar3DInput {
cx: number; cx: number;
} }
/**
* Converte kN para um comprimento visual proporcional no eixo 3D.
*/
const forceToLength = (kN: number): number => Math.min(Math.max(Math.abs(kN) * 0.3, 0.3), 4);
function BarModel({ function BarModel({
barType, barType,
section, section,
@@ -37,94 +32,60 @@ function BarModel({
width, width,
length, length,
alpha, alpha,
fxKN,
fyKN,
cx, cx,
}: Bar3DInput) { }: Bar3DInput) {
const barRadius = barType === 'circular' ? (diameter ?? 0.05) / 2 : Math.min(width ?? 0.1, 0.08) / 2; const barRadius = barType === 'circular' ? (diameter ?? 0.05) / 2 : Math.min(width ?? 0.1, 0.08) / 2;
const barThickness = barType === 'circular' ? barRadius : barRadius * 0.5; const barThickness = barType === 'circular' ? barRadius : barRadius * 0.5;
// Cor baseada em Cx
const barColor = useMemo(() => { const barColor = useMemo(() => {
const intensity = Math.min(1, Math.abs(cx) / 2.5); const intensity = Math.min(1, Math.abs(cx) / 2.5);
const hue = 215 - intensity * 215; const hue = 215 - intensity * 215;
return new THREE.Color(`hsl(${hue}, ${65 + intensity * 25}%, ${45 - intensity * 10}%)`); return new THREE.Color(`hsl(${hue}, ${65 + intensity * 25}%, ${45 - intensity * 10}%)`);
}, [cx]); }, [cx]);
// Rotação da barra em torno do eixo Y (alinhada com eixo X inicialmente) // Ação do vento deve rotacionar a barra em seu próprio eixo longitudinal (roll).
// Direção do vento é +X; α é o ângulo da face da barra em relação ao vento // O eixo longitudinal na nossa geometria é o X.
const alphaRad = (alpha * Math.PI) / 180; const alphaRad = (alpha * Math.PI) / 180;
const barRotation = -alphaRad; // rotação em torno do eixo Y para alinhar a face // A rotação deve ser negativa para que o vento (vindo de -Z) atinja as faces corretas conforme a NBR.
const barRotationX = -alphaRad;
// Direção do vetor de força resultante (na direção da força calculada)
const forceMag = Math.sqrt(fxKN * fxKN + fyKN * fyKN);
const forceAngle = Math.atan2(fyKN, fxKN);
const arrowLen = forceToLength(forceMag);
// Centro da barra (origem)
const center = new THREE.Vector3(0, 0, 0);
// Posição da ponta da seta
const arrowEnd = useMemo(
() => new THREE.Vector3(
Math.cos(forceAngle) * arrowLen,
Math.sin(forceAngle) * arrowLen,
0,
),
[forceAngle, arrowLen],
);
const arrowMid = useMemo(
() => new THREE.Vector3(arrowEnd.x / 2, arrowEnd.y / 2, 0),
[arrowEnd],
);
const quat = useMemo(() => {
const dir = arrowEnd.clone().normalize();
const q = new THREE.Quaternion();
q.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
return new THREE.Euler().setFromQuaternion(q);
}, [arrowEnd]);
const headLen = 0.25;
return ( return (
<group rotation={[0, barRotation, 0]}> <group>
{/* Eixo principal da barra ao longo do eixo X */} {/* Seta do Vento (Fixa Globalmente, soprando em +Z) */}
{barType === 'circular' ? ( <group position={[0, 0, barRadius + 0.5]}>
<mesh position={[0, 0, 0]} rotation={[0, 0, Math.PI / 2]} castShadow> <mesh position={[0, 0, 0.4]} rotation={[Math.PI / 2, 0, 0]}>
<cylinderGeometry args={[barRadius, barRadius, length, 16]} /> <cylinderGeometry args={[0.02, 0.02, 0.8, 8]} />
<meshStandardMaterial color={barColor} roughness={0.4} metalness={0.3} /> <meshStandardMaterial color="#22c55e" />
</mesh> </mesh>
) : ( <mesh position={[0, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<SectionShape section={section ?? 'placa'} width={width ?? 0.1} length={length} color={barColor} thickness={barThickness} /> <coneGeometry args={[0.08, 0.2, 8]} />
)} <meshStandardMaterial color="#22c55e" />
</mesh>
</group>
{/* Eixos de referência */} {/* Barra Rotacionada (Roll) */}
<axesHelper args={[length * 0.5]} /> <group rotation={[barRotationX, 0, 0]}>
{/* Eixo principal da barra ao longo do eixo X */}
{/* Vetor de força (resultante) */} {barType === 'circular' ? (
{forceMag > 0.01 && ( <mesh position={[0, 0, 0]} rotation={[0, 0, Math.PI / 2]} castShadow>
<group> <cylinderGeometry args={[barRadius, barRadius, length, 16]} />
{arrowLen - headLen > 0.01 && ( <meshStandardMaterial color={barColor} roughness={0.4} metalness={0.3} />
<mesh position={arrowMid.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
<cylinderGeometry args={[0.04, 0.04, arrowLen - headLen, 10]} />
<meshStandardMaterial color="#ef4444" />
</mesh>
)}
<mesh position={arrowEnd.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
<coneGeometry args={[0.1, headLen, 10]} />
<meshStandardMaterial color="#ef4444" />
</mesh> </mesh>
</group> ) : (
)} <SectionShape section={section ?? 'placa'} width={width ?? 0.1} length={length} color={barColor} thickness={barThickness} />
)}
{/* Eixos de referência locais da barra */}
<axesHelper args={[length * 0.5]} />
</group>
{/* Marca de origem */} {/* Marca de origem */}
<mesh position={[0, 0, 0]} castShadow> <mesh position={[0, 0, 0]} castShadow>
<sphereGeometry args={[0.06, 12, 12]} /> <sphereGeometry args={[0.06, 12, 12]} />
<meshStandardMaterial color="#fbbf24" emissive="#fbbf24" emissiveIntensity={0.4} /> <meshStandardMaterial color="#fbbf24" emissive="#fbbf24" emissiveIntensity={0.4} />
</mesh> </mesh>
<axesHelper args={[length * 0.3]} />
<Text <Text
position={[0, -barRadius * 2 - 0.3, 0]} position={[0, -barRadius * 2 - 0.4, 0]}
fontSize={0.3} fontSize={0.3}
color="#1e40af" color="#1e40af"
anchorX="center" anchorX="center"
@@ -132,7 +93,6 @@ function BarModel({
> >
α={alpha}° | Cx={cx.toFixed(2)} α={alpha}° | Cx={cx.toFixed(2)}
</Text> </Text>
{center && null}
</group> </group>
); );
} }
+11 -80
View File
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
import * as THREE from 'three'; import * as THREE from 'three';
import SceneCanvas from '../SceneCanvas'; import SceneCanvas from '../SceneCanvas';
import FallbackDiagram from '../FallbackDiagram'; import FallbackDiagram from '../FallbackDiagram';
import { WindArrow } from './WindArrow';
export interface Bridge3DInput { export interface Bridge3DInput {
/** Maior vão Lₚ (m) */ /** Maior vão Lₚ (m) */
@@ -21,6 +22,8 @@ export interface Bridge3DInput {
fxPerLength: number; fxPerLength: number;
/** Força de sustentação por unidade de comprimento Fz (kN/m) */ /** Força de sustentação por unidade de comprimento Fz (kN/m) */
fzPerLength: number; fzPerLength: number;
/** Ângulo de ataque do vento (graus) */
alpha: number;
} }
function BridgeModel({ function BridgeModel({
@@ -29,8 +32,7 @@ function BridgeModel({
deckHeight, deckHeight,
heg, heg,
cx, cx,
fxPerLength, alpha,
fzPerLength,
}: Bridge3DInput) { }: Bridge3DInput) {
const halfL = lp / 2; const halfL = lp / 2;
const halfW = width / 2; const halfW = width / 2;
@@ -47,13 +49,6 @@ function BridgeModel({
// Pilar heights: posicionar 3 pilares ao longo do vão // Pilar heights: posicionar 3 pilares ao longo do vão
const pillarHeights = useMemo(() => [deckY - 0.5, deckY - 0.5, deckY - 0.5], [deckY]); const pillarHeights = useMemo(() => [deckY - 0.5, deckY - 0.5, deckY - 0.5], [deckY]);
// Vetor de força (Fx horizontal)
const fxLen = Math.min(Math.max(Math.abs(fxPerLength) * 0.5, 0.3), 4);
const fxDir = fxPerLength >= 0 ? 1 : -1;
// Vetor de força (Fz vertical)
const fzLen = Math.min(Math.max(Math.abs(fzPerLength) * 0.5, 0.3), 4);
const fzDir = fzPerLength >= 0 ? 1 : -1;
return ( return (
<group> <group>
{/* Tabuleiro (deck) */} {/* Tabuleiro (deck) */}
@@ -89,32 +84,15 @@ function BridgeModel({
<meshStandardMaterial color="#60a5fa" opacity={0.4} transparent roughness={0.3} /> <meshStandardMaterial color="#60a5fa" opacity={0.4} transparent roughness={0.3} />
</mesh> </mesh>
{/* Vetor Cx (horizontal) */} {/* Seta de Vento (bate no meio do tabuleiro) */}
<ForceArrow <WindArrow
start={[-halfL * 0.6, deckY + deckThickness + 0.3, halfW + 0.5]} target={[0, deckY + deckThickness / 2, 0]}
direction={[fxDir, 0, 0]} direction={[0, Math.sin((alpha * Math.PI) / 180), Math.cos((alpha * Math.PI) / 180)]}
length={fxLen} scale={4}
color="#ef4444"
/>
{/* Vetor Cz (vertical) */}
<ForceArrow
start={[halfL * 0.6, deckY + deckThickness + 0.3, halfW + 0.5]}
direction={[0, fzDir, 0]}
length={fzLen}
color="#3b82f6"
/>
{/* Vetor no centro também para destacar */}
<ForceArrow
start={[0, deckY + deckThickness + 0.3, 0]}
direction={[fxDir, 0, 0]}
length={fxLen * 0.7}
color="#ef4444"
/> />
<Text <Text
position={[0, deckY + deckThickness + 1.0, 0]} position={[0, deckY + deckThickness + 1.5, 0]}
fontSize={0.6} fontSize={1.2}
color="#1e40af" color="#1e40af"
anchorX="center" anchorX="center"
anchorY="bottom" anchorY="bottom"
@@ -125,54 +103,7 @@ function BridgeModel({
); );
} }
function ForceArrow({
start,
direction,
length,
color,
}: {
start: [number, number, number];
direction: [number, number, number];
length: number;
color: string;
}) {
const startVec = useMemo(() => new THREE.Vector3(...start), [start]);
const dirVec = useMemo(() => new THREE.Vector3(...direction), [direction]);
const end = useMemo(
() => new THREE.Vector3(
startVec.x + dirVec.x * length,
startVec.y + dirVec.y * length,
startVec.z + dirVec.z * length,
),
[startVec, dirVec, length],
);
const mid = useMemo(
() => new THREE.Vector3().addVectors(startVec, end).multiplyScalar(0.5),
[startVec, end],
);
const quat = useMemo(() => {
const dir = new THREE.Vector3().subVectors(end, startVec).normalize();
const q = new THREE.Quaternion();
q.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
return new THREE.Euler().setFromQuaternion(q);
}, [startVec, end]);
const headLen = 0.3;
return (
<group>
{length - headLen > 0.01 && (
<mesh position={mid.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
<cylinderGeometry args={[0.06, 0.06, length - headLen, 10]} />
<meshStandardMaterial color={color} />
</mesh>
)}
<mesh position={end.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
<coneGeometry args={[0.15, headLen, 10]} />
<meshStandardMaterial color={color} />
</mesh>
</group>
);
}
export default function Bridge3DViewer(input: Bridge3DInput) { export default function Bridge3DViewer(input: Bridge3DInput) {
const { lp, deckHeight, width } = input; const { lp, deckHeight, width } = input;
+1
View File
@@ -186,6 +186,7 @@ export default function Dynamics3DViewer(props: Dynamics3DInput) {
return ( return (
<SceneCanvas <SceneCanvas
frameloop="always"
shadows shadows
gl={{ preserveDrawingBuffer: true, antialias: true }} gl={{ preserveDrawingBuffer: true, antialias: true }}
camera={{ position: [cameraDistance, cameraDistance * 0.5, cameraDistance], fov: 45 }} camera={{ position: [cameraDistance, cameraDistance * 0.5, cameraDistance], fov: 45 }}
+31 -84
View File
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
import * as THREE from 'three'; import * as THREE from 'three';
import SceneCanvas from '../SceneCanvas'; import SceneCanvas from '../SceneCanvas';
import FallbackDiagram from '../FallbackDiagram'; import FallbackDiagram from '../FallbackDiagram';
import { WindArrow } from './WindArrow';
export interface IsolatedRoof3DInput { export interface IsolatedRoof3DInput {
/** Tipo de cobertura: 'shed' (uma água) ou 'gable' (duas águas) */ /** Tipo de cobertura: 'shed' (uma água) ou 'gable' (duas águas) */
@@ -11,7 +12,9 @@ export interface IsolatedRoof3DInput {
theta: number; theta: number;
/** Altura livre dos suportes (m) */ /** Altura livre dos suportes (m) */
height: number; height: number;
/** Profundidade da cobertura (m) — dimensão perpendicular à seção */ /** Largura da cobertura (m) — dimensão transversal (b) */
width: number;
/** Profundidade da cobertura (m) — dimensão longitudinal (l) */
depth: number; depth: number;
/** Cpe barlavento (sobre a face exposta ao vento) */ /** Cpe barlavento (sobre a face exposta ao vento) */
cpeWindward: number; cpeWindward: number;
@@ -23,8 +26,6 @@ export interface IsolatedRoof3DInput {
forceKN: number; forceKN: number;
} }
const forceToLength = (kN: number): number => Math.min(Math.max(Math.abs(kN) * 0.15, 0.5), 6);
function pressureColor(cpe: number): THREE.Color { function pressureColor(cpe: number): THREE.Color {
const clamped = Math.max(-2.5, Math.min(1.5, cpe)); const clamped = Math.max(-2.5, Math.min(1.5, cpe));
const t = (clamped + 2.5) / 4.0; const t = (clamped + 2.5) / 4.0;
@@ -36,6 +37,7 @@ function IsolatedRoofModel({
type, type,
theta, theta,
height, height,
width,
depth, depth,
cpeWindward, cpeWindward,
cpeLeeward, cpeLeeward,
@@ -43,12 +45,11 @@ function IsolatedRoofModel({
}: IsolatedRoof3DInput) { }: IsolatedRoof3DInput) {
const thetaRad = (theta * Math.PI) / 180; const thetaRad = (theta * Math.PI) / 180;
const halfDepth = depth / 2; const halfDepth = depth / 2;
const halfWidth = width / 2;
const windwardColor = useMemo(() => pressureColor(cpeWindward), [cpeWindward]); const windwardColor = useMemo(() => pressureColor(cpeWindward), [cpeWindward]);
const leewardColor = useMemo(() => pressureColor(cpeLeeward), [cpeLeeward]); const leewardColor = useMemo(() => pressureColor(cpeLeeward), [cpeLeeward]);
const arrowLen = forceToLength(forceKN);
const h_diff = depth * Math.tan(thetaRad); const h_diff = depth * Math.tan(thetaRad);
const h_half = (depth / 2) * Math.tan(thetaRad); const h_half = (depth / 2) * Math.tan(thetaRad);
@@ -60,29 +61,29 @@ function IsolatedRoofModel({
const list: { pos: [number, number, number]; h: number }[] = []; const list: { pos: [number, number, number]; h: number }[] = [];
if (type === 'shed') { if (type === 'shed') {
list.push( list.push(
{ pos: [-halfDepth, height / 2, -halfDepth], h: height }, { pos: [-halfWidth, height / 2, -halfDepth], h: height },
{ pos: [halfDepth, height / 2, -halfDepth], h: height }, { pos: [halfWidth, height / 2, -halfDepth], h: height },
{ pos: [-halfDepth, (height + h_diff) / 2, halfDepth], h: height + h_diff }, { pos: [-halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
{ pos: [halfDepth, (height + h_diff) / 2, halfDepth], h: height + h_diff }, { pos: [halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
); );
} else { } else {
list.push( list.push(
{ pos: [-halfDepth, height / 2, -halfDepth], h: height }, { pos: [-halfWidth, height / 2, -halfDepth], h: height },
{ pos: [halfDepth, height / 2, -halfDepth], h: height }, { pos: [halfWidth, height / 2, -halfDepth], h: height },
{ pos: [-halfDepth, height / 2, halfDepth], h: height }, { pos: [-halfWidth, height / 2, halfDepth], h: height },
{ pos: [halfDepth, height / 2, halfDepth], h: height }, { pos: [halfWidth, height / 2, halfDepth], h: height },
{ pos: [-halfDepth, (height + h_half) / 2, 0], h: height + h_half }, { pos: [-halfWidth, (height + h_half) / 2, 0], h: height + h_half },
{ pos: [halfDepth, (height + h_half) / 2, 0], h: height + h_half }, { pos: [halfWidth, (height + h_half) / 2, 0], h: height + h_half },
); );
} }
return list; return list;
}, [type, depth, height, h_diff, h_half, halfDepth]); }, [type, depth, width, height, h_diff, h_half, halfDepth, halfWidth]);
return ( return (
<group> <group>
{/* Solo translúcido */} {/* Solo translúcido */}
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.01, 0]} receiveShadow> <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.01, 0]} receiveShadow>
<planeGeometry args={[depth * 2, depth * 2]} /> <planeGeometry args={[width * 1.5, depth * 1.5]} />
<meshStandardMaterial color="#94a3b8" transparent opacity={0.15} /> <meshStandardMaterial color="#94a3b8" transparent opacity={0.15} />
</mesh> </mesh>
@@ -97,7 +98,7 @@ function IsolatedRoofModel({
castShadow castShadow
receiveShadow receiveShadow
> >
<boxGeometry args={[depth, 0.08, depth / (2 * Math.cos(thetaRad))]} /> <boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
<meshStandardMaterial color={windwardColor} opacity={0.9} transparent roughness={0.4} /> <meshStandardMaterial color={windwardColor} opacity={0.9} transparent roughness={0.4} />
</mesh> </mesh>
{/* Metade Sotavento (Z > 0) */} {/* Metade Sotavento (Z > 0) */}
@@ -107,7 +108,7 @@ function IsolatedRoofModel({
castShadow castShadow
receiveShadow receiveShadow
> >
<boxGeometry args={[depth, 0.08, depth / (2 * Math.cos(thetaRad))]} /> <boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
<meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} /> <meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} />
</mesh> </mesh>
</group> </group>
@@ -121,7 +122,7 @@ function IsolatedRoofModel({
castShadow castShadow
receiveShadow receiveShadow
> >
<boxGeometry args={[depth, 0.08, depth / (2 * Math.cos(thetaRad))]} /> <boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
<meshStandardMaterial color={windwardColor} opacity={0.9} transparent roughness={0.4} /> <meshStandardMaterial color={windwardColor} opacity={0.9} transparent roughness={0.4} />
</mesh> </mesh>
{/* Água Direita / Sotavento (Z > 0) */} {/* Água Direita / Sotavento (Z > 0) */}
@@ -131,7 +132,7 @@ function IsolatedRoofModel({
castShadow castShadow
receiveShadow receiveShadow
> >
<boxGeometry args={[depth, 0.08, depth / (2 * Math.cos(thetaRad))]} /> <boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
<meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} /> <meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} />
</mesh> </mesh>
</group> </group>
@@ -145,12 +146,10 @@ function IsolatedRoofModel({
</mesh> </mesh>
))} ))}
{/* Seta de força resultante (sucção para cima) */} {/* Vetor de vento (Seta verde/azul batendo no elemento) */}
<ForceArrow <WindArrow
start={new THREE.Vector3(0, centerY, 0)} target={new THREE.Vector3(0, centerY, 0)}
direction={new THREE.Vector3(0, 1, 0)} direction={new THREE.Vector3(0, 0, 1)}
length={arrowLen}
color="#ef4444"
/> />
{/* === LINHAS DE COTA (CAD-Style) === */} {/* === LINHAS DE COTA (CAD-Style) === */}
@@ -208,8 +207,7 @@ function IsolatedRoofModel({
{/* Rótulo Superior */} {/* Rótulo Superior */}
<Text <Text
position={[0, centerY + arrowLen + 0.8, 0]} position={[0, centerY + 3, 0]}
fontSize={0.4}
color="#1a202c" color="#1a202c"
anchorX="center" anchorX="center"
anchorY="bottom" anchorY="bottom"
@@ -220,75 +218,23 @@ function IsolatedRoofModel({
); );
} }
function ForceArrow({
start,
direction,
length,
color,
}: {
start: THREE.Vector3;
direction: THREE.Vector3;
length: number;
color: string;
}) {
const end = useMemo(
() => new THREE.Vector3(start.x + direction.x * length, start.y + direction.y * length, start.z + direction.z * length),
[start, direction, length],
);
const headLen = 0.3;
const headRadius = 0.1;
const shaftRadius = 0.04;
const midPoint = useMemo(
() => new THREE.Vector3((start.x + end.x) / 2, (start.y + end.y) / 2, (start.z + end.z) / 2),
[start, end],
);
const shaftLength = length - headLen;
const rotation = useMemo(() => {
const dir = new THREE.Vector3().subVectors(end, start).normalize();
const quat = new THREE.Quaternion();
quat.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
const euler = new THREE.Euler().setFromQuaternion(quat);
return [euler.x, euler.y, euler.z] as [number, number, number];
}, [start, end]);
return (
<group>
{shaftLength > 0 && (
<mesh position={midPoint.toArray()} rotation={rotation} castShadow>
<cylinderGeometry args={[shaftRadius, shaftRadius, shaftLength, 12]} />
<meshStandardMaterial color={color} />
</mesh>
)}
<mesh
position={[end.x, end.y, end.z]}
rotation={rotation}
castShadow
>
<coneGeometry args={[headRadius, headLen, 12]} />
<meshStandardMaterial color={color} />
</mesh>
</group>
);
}
export default function IsolatedRoof3DViewer({ export default function IsolatedRoof3DViewer({
type, type,
theta, theta,
height, height,
width,
depth, depth,
cpeWindward, cpeWindward,
cpeLeeward, cpeLeeward,
cpeTop, cpeTop,
forceKN, forceKN,
}: IsolatedRoof3DInput) { }: IsolatedRoof3DInput) {
const cameraDistance = Math.max(depth * 1.3, height * 1.5, 8); const cameraDistance = Math.max(Math.max(width, depth) * 1.3, height * 1.5, 8);
const fallback = ( const fallback = (
<FallbackDiagram <FallbackDiagram
type="isolatedRoof" type="isolatedRoof"
props={{ type, theta, height, depth, cpeWindward, cpeLeeward, cpeTop, forceKN }} props={{ type, theta, height, width, depth, cpeWindward, cpeLeeward, cpeTop, forceKN }}
/> />
); );
@@ -311,6 +257,7 @@ export default function IsolatedRoof3DViewer({
type={type} type={type}
theta={theta} theta={theta}
height={height} height={height}
width={width}
depth={depth} depth={depth}
cpeWindward={cpeWindward} cpeWindward={cpeWindward}
cpeLeeward={cpeLeeward} cpeLeeward={cpeLeeward}
+8 -67
View File
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
import * as THREE from 'three'; import * as THREE from 'three';
import SceneCanvas from '../SceneCanvas'; import SceneCanvas from '../SceneCanvas';
import FallbackDiagram from '../FallbackDiagram'; import FallbackDiagram from '../FallbackDiagram';
import { WindArrow } from './WindArrow';
export interface Sign3DInput { export interface Sign3DInput {
/** Comprimento (m) */ /** Comprimento (m) */
@@ -22,10 +23,8 @@ export interface Sign3DInput {
} }
/** /**
* Converte kN para um comprimento visual proporcional no eixo 3D. * Módulo 3D para visualização de Muros e Placas Isoladas.
* 1 kN = 0.25 m de seta (escala calibrada para visualização).
*/ */
const forceToLength = (kN: number): number => Math.min(Math.max(kN * 0.25, 0.5), 8);
function SignModel({ function SignModel({
length, length,
@@ -33,13 +32,11 @@ function SignModel({
groundClearance, groundClearance,
alpha, alpha,
cf, cf,
forceKN,
applicationPoint, applicationPoint,
}: Sign3DInput) { }: Sign3DInput) {
const baseY = groundClearance; const baseY = groundClearance;
const topY = baseY + height; const topY = baseY + height;
const halfL = length / 2; const halfL = length / 2;
const arrowLen = forceToLength(forceKN);
// Direção da seta no plano XZ (α é o ângulo de incidência do vento relativo à superfície) // Direção da seta no plano XZ (α é o ângulo de incidência do vento relativo à superfície)
// O ângulo em relação à normal da placa (eixo X) é 90 - α // O ângulo em relação à normal da placa (eixo X) é 90 - α
@@ -114,12 +111,10 @@ function SignModel({
Resultante (e = {applicationPoint.toFixed(2)}m) Resultante (e = {applicationPoint.toFixed(2)}m)
</Text> </Text>
{/* Vetor de força resultante */} {/* Vetor de vento (Seta verde/azul batendo no elemento) */}
<ForceArrow <WindArrow
start={arrowStart} target={arrowStart}
direction={arrowDir} direction={arrowDir}
length={arrowLen}
color={forceKN >= 0 ? '#ef4444' : '#3b82f6'}
/> />
{/* === LINHAS DE COTA (CAD-Style Dimensions) === */} {/* === LINHAS DE COTA (CAD-Style Dimensions) === */}
@@ -154,7 +149,7 @@ function SignModel({
</group> </group>
{/* Cota de Comprimento (l) */} {/* Cota de Comprimento (l) */}
<group position={[0.4, baseY + height / 2, 0]}> <group position={[-0.4, baseY + height / 2, 0]}>
{/* Linha horizontal longitudinal */} {/* Linha horizontal longitudinal */}
<mesh position={[0, 0, 0]}> <mesh position={[0, 0, 0]}>
<boxGeometry args={[0.015, 0.015, length]} /> <boxGeometry args={[0.015, 0.015, length]} />
@@ -172,8 +167,8 @@ function SignModel({
</mesh> </mesh>
{/* Texto do comprimento */} {/* Texto do comprimento */}
<Text <Text
position={[0.15, 0, 0]} position={[-0.15, 0, 0]}
rotation={[0, Math.PI / 2, 0]} rotation={[0, -Math.PI / 2, 0]}
fontSize={0.25} fontSize={0.25}
color="#475569" color="#475569"
anchorX="center" anchorX="center"
@@ -197,61 +192,7 @@ function SignModel({
); );
} }
function ForceArrow({
start,
direction,
length,
color,
}: {
start: THREE.Vector3;
direction: THREE.Vector3;
length: number;
color: string;
}) {
const end = useMemo(
() => new THREE.Vector3(start.x + direction.x * length, start.y, start.z + direction.z * length),
[start, direction, length],
);
const headLen = 0.3;
const headRadius = 0.1;
const shaftRadius = 0.04;
// Cilindro principal (haste)
const midPoint = useMemo(
() => new THREE.Vector3((start.x + end.x) / 2, (start.y + end.y) / 2, (start.z + end.z) / 2),
[start, end],
);
const shaftLength = length - headLen;
// Rotação do cilindro (apontar de start para end)
const rotation = useMemo(() => {
const dir = new THREE.Vector3().subVectors(end, start).normalize();
const quat = new THREE.Quaternion();
quat.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
const euler = new THREE.Euler().setFromQuaternion(quat);
return [euler.x, euler.y, euler.z] as [number, number, number];
}, [start, end]);
return (
<group>
{shaftLength > 0 && (
<mesh position={midPoint.toArray()} rotation={rotation} castShadow>
<cylinderGeometry args={[shaftRadius, shaftRadius, shaftLength, 12]} />
<meshStandardMaterial color={color} />
</mesh>
)}
{/* Ponta da seta (cone) */}
<mesh
position={[end.x, end.y, end.z]}
rotation={rotation}
castShadow
>
<coneGeometry args={[headRadius, headLen, 12]} />
<meshStandardMaterial color={color} />
</mesh>
</group>
);
}
export default function Sign3DViewer({ export default function Sign3DViewer({
length, length,
+6 -69
View File
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
import { OrbitControls, Grid, Environment, Text } from '@react-three/drei'; import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
import * as THREE from 'three'; import * as THREE from 'three';
import SceneCanvas from '../SceneCanvas'; import SceneCanvas from '../SceneCanvas';
import { WindArrow } from './WindArrow';
import FallbackDiagram from '../FallbackDiagram'; import FallbackDiagram from '../FallbackDiagram';
export interface Tower3DInput { export interface Tower3DInput {
@@ -190,31 +191,11 @@ function TowerModel({
); );
})} })}
{/* Vetores de força distribuídos pelos tramos (meio de cada tramo) */} {/* Seta de Vento única atingindo o meio da torre */}
{Array.from({ length: panels }).map((_, i) => { <WindArrow
const pHeight = height / panels; target={[0, height / 2, 0]}
const startY = (i + 0.5) * pHeight; // Altura no meio do tramo direction={forceDir}
const pForce = forceKN / panels; // Força por tramo />
// Escala da seta menor para ficar visualmente agradável
const pArrowLen = Math.min(Math.max(pForce * 0.1, 0.5), 2.5);
// Calcular o ponto inicial para que a ponta da seta encoste na face (baseWidth / 2)
const endX = -forceDir.x * (baseWidth / 2);
const endZ = -forceDir.z * (baseWidth / 2);
const startX = endX - forceDir.x * pArrowLen;
const startZ = endZ - forceDir.z * pArrowLen;
return (
<ForceArrow
key={`force-${i}`}
start={[startX, startY, startZ]}
direction={forceDir}
length={pArrowLen}
color="#ef4444"
/>
);
})}
{/* Labels indicativos */} {/* Labels indicativos */}
<mesh position={[baseWidth / 2 + 0.5, 0.5, 0]}> <mesh position={[baseWidth / 2 + 0.5, 0.5, 0]}>
@@ -234,50 +215,6 @@ function TowerModel({
); );
} }
function ForceArrow({
start,
direction,
length,
color,
}: {
start: [number, number, number];
direction: THREE.Vector3;
length: number;
color: string;
}) {
const startVec = useMemo(() => new THREE.Vector3(...start), [start]);
const end = useMemo(
() => new THREE.Vector3(startVec.x + direction.x * length, startVec.y, startVec.z + direction.z * length),
[startVec, direction, length],
);
const mid = useMemo(
() => new THREE.Vector3((startVec.x + end.x) / 2, (startVec.y + end.y) / 2, (startVec.z + end.z) / 2),
[startVec, end],
);
const quat = useMemo(() => {
const dir = new THREE.Vector3().subVectors(end, startVec).normalize();
const q = new THREE.Quaternion();
q.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
return new THREE.Euler().setFromQuaternion(q);
}, [startVec, end]);
const headLen = 0.3;
return (
<group>
{length - headLen > 0 && (
<mesh position={mid.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
<cylinderGeometry args={[0.05, 0.05, length - headLen, 10]} />
<meshStandardMaterial color={color} />
</mesh>
)}
<mesh position={end.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
<coneGeometry args={[0.12, headLen, 10]} />
<meshStandardMaterial color={color} />
</mesh>
</group>
);
}
export default function Tower3DViewer(input: Tower3DInput) { export default function Tower3DViewer(input: Tower3DInput) {
const { baseWidth, height } = input; const { baseWidth, height } = input;
const dist = Math.max(baseWidth * 3, height * 1.2); const dist = Math.max(baseWidth * 3, height * 1.2);
+59
View File
@@ -0,0 +1,59 @@
import { useMemo } from 'react';
import * as THREE from 'three';
export interface WindArrowProps {
/** Ponto onde a ponta da seta do vento toca (ex: centro geométrico da estrutura) */
target: THREE.Vector3 | [number, number, number];
/** Direção de onde o vento vem (e para onde ele vai). Vetor normalizado. */
direction: THREE.Vector3 | [number, number, number];
/** Fator de escala da seta (default: 1) */
scale?: number;
}
export function WindArrow({ target, direction, scale = 1 }: WindArrowProps) {
const length = 2.5 * scale; // Tamanho visual fixo
const color = '#3b82f6'; // Azul padrão do vento
const targetVec = target instanceof THREE.Vector3 ? target : new THREE.Vector3(...target);
const dirVec = direction instanceof THREE.Vector3 ? direction : new THREE.Vector3(...direction);
const end = targetVec;
const base = useMemo(
() => new THREE.Vector3(end.x - dirVec.x * length, end.y - dirVec.y * length, end.z - dirVec.z * length),
[end, dirVec, length],
);
const headLen = 0.3 * scale;
const headRadius = 0.1 * scale;
const shaftRadius = 0.04 * scale;
const midPoint = useMemo(
() => new THREE.Vector3((base.x + end.x) / 2, (base.y + end.y) / 2, (base.z + end.z) / 2),
[base, end],
);
const shaftLength = length - headLen;
const rotation = useMemo(() => {
const dir = new THREE.Vector3().subVectors(end, base).normalize();
const quat = new THREE.Quaternion();
quat.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
const euler = new THREE.Euler().setFromQuaternion(quat);
return [euler.x, euler.y, euler.z] as [number, number, number];
}, [base, end]);
return (
<group>
{shaftLength > 0 && (
<mesh position={midPoint.toArray()} rotation={rotation} castShadow>
<cylinderGeometry args={[shaftRadius, shaftRadius, shaftLength, 12]} />
<meshStandardMaterial color={color} />
</mesh>
)}
{/* Ponta da seta (cone) */}
<mesh position={[end.x, end.y, end.z]} rotation={rotation} castShadow>
<coneGeometry args={[headRadius, headLen, 12]} />
<meshStandardMaterial color={color} />
</mesh>
</group>
);
}
+141
View File
@@ -0,0 +1,141 @@
import * as React from "react"
import { XIcon } from "lucide-react"
import { Dialog as SheetPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
showCloseButton = true,
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
showCloseButton?: boolean
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
side === "right" &&
"inset-y-0 right-0 h-full w-full border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-md",
side === "left" &&
"inset-y-0 left-0 h-full w-full border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-md",
side === "top" &&
"inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
side === "bottom" &&
"inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
className
)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Content>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("font-semibold text-foreground text-lg", className)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}
+38
View File
@@ -0,0 +1,38 @@
export const barManual = {
title: 'Barras Estruturais Individuais (Seção 8.2)',
intro: 'Coeficientes de arrasto para perfis estruturais isolados (perfis I, U, L, tubulares), abordando forças por unidade de comprimento.',
sections: [
{
title: '1. Equação do Esforço',
content: (
<div className="flex flex-col gap-3">
<p>
A força de arrasto exercida sobre barras individuais é dada por metro linear de elemento:
</p>
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
F = q · Ca · d
</div>
<p>
Onde <strong>d</strong> é a dimensão característica da seção transversal do perfil (altura exposta frontalmente) e <strong>Ca</strong> é o coeficiente de arrasto da seção estudada.
</p>
</div>
),
},
{
title: '2. Formas Geométricas dos Perfis (Tabelas 26 a 28)',
content: (
<div className="flex flex-col gap-3">
<p>
O coeficiente Ca varia drasticamente com o perfil devido a cantos vivos ou aerodinâmicos:
</p>
<ul className="list-disc pl-5 space-y-1">
<li><strong>Perfis de Faces Planas (L, U, I):</strong> Possuem cantos vivos que forçam o descolamento imediato do vento, gerando altos valores de arrasto (Ca em torno de 1.4 a 2.0).</li>
<li><strong>Perfis Circulares/Tubulares:</strong> O vento consegue contornar a forma circular, reduzindo o arrasto consideravelmente (Ca em torno de 0.6 a 1.2, dependendo de Reynolds).</li>
</ul>
</div>
),
},
],
};
+93
View File
@@ -0,0 +1,93 @@
export const bridgeManual = {
title: 'Tabuleiro de Ponte (Seção 11.2 / 11.3)',
intro: 'Explicação detalhada sobre a ação estática do vento em pontes, cobrindo forças de arrasto (horizontais) e forças de sustentação (verticais).',
sections: [
{
title: '1. Forças Atuantes',
content: (
<div className="flex flex-col gap-3">
<p>
A ação do vento em tabuleiros de pontes é dividida em duas componentes principais:
</p>
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm space-y-2 border border-border">
<div>
<strong>Força de Arrasto (Fx):</strong> Fx = q · Cx · Heg · Lp
</div>
<div>
<strong>Força de Sustentação (Fz):</strong> Fz = q · Cz · B · Lp
</div>
</div>
<p>
Onde <strong>q</strong> é a pressão dinâmica do vento, <strong>Lp</strong> é o comprimento do vão exposto, <strong>Heg</strong> é a altura equivalente do tabuleiro (soma das áreas de projeção das vigas, guarda-corpos e barreiras) e <strong>B</strong> é a largura da ponte.
</p>
</div>
),
},
{
title: '2. Coeficientes Aerodinâmicos (Cx e Cz)',
content: (
<div className="flex flex-col gap-3">
<p>
Os coeficientes são determinados com base na relação geométrica entre a largura e a altura do tabuleiro (B / Heg), bem como o ângulo de ataque do vento (α):
</p>
<ul className="list-disc pl-5 space-y-1">
<li>
<strong>Cx (Arrasto):</strong> Representa a resistência que a ponte oferece ao fluxo horizontal de vento. Valores maiores de Cx significam maior força empurrando a ponte lateralmente.
</li>
<li>
<strong>Cz (Sustentação/Lift):</strong> Representa a força vertical (para cima ou para baixo) induzida pela inclinação do vento (ângulo de ataque α).
</li>
</ul>
<p>
Se o vento incide com um ângulo positivo (vindo ligeiramente de baixo, α &gt; 0), ele tende a "levantar" a ponte (Cz positivo). Se incide de cima (α &lt; 0), empurra a ponte para baixo (Cz negativo).
</p>
</div>
),
},
{
title: '3. Glossário de Parâmetros',
content: (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-border text-sm">
<thead>
<tr className="border-b">
<th className="text-left py-2 font-semibold">Símbolo</th>
<th className="text-left py-2 font-semibold">Parâmetro</th>
<th className="text-left py-2 font-semibold">Descrição</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
<tr>
<td className="py-2 font-mono font-semibold">Lp</td>
<td className="py-2">Maior Vão (m)</td>
<td className="py-2">O comprimento do vão livre da ponte sob análise.</td>
</tr>
<tr>
<td className="py-2 font-mono font-semibold">B</td>
<td className="py-2">Largura (m)</td>
<td className="py-2">Largura total do tabuleiro da ponte (perpendicular ao fluxo principal).</td>
</tr>
<tr>
<td className="py-2 font-mono font-semibold">Heg</td>
<td className="py-2">Altura Equivalente (m)</td>
<td className="py-2">Soma da projeção vertical de todos os elementos expostos ao vento.</td>
</tr>
<tr>
<td className="py-2 font-mono font-semibold">α (alpha)</td>
<td className="py-2">Ângulo de Ataque (º)</td>
<td className="py-2">Inclinação do vetor do vento em relação ao plano horizontal (varia de -5º a +5º).</td>
</tr>
<tr>
<td className="py-2 font-mono font-semibold">q</td>
<td className="py-2">Pressão Dinâmica (kN/m²)</td>
<td className="py-2">Pressão do vento calculada a partir da velocidade de projeto.</td>
</tr>
</tbody>
</table>
</div>
),
},
],
};
+66
View File
@@ -0,0 +1,66 @@
export const cylinderManual = {
title: 'Cilindros e Chaminés (Tabela 13 - Seção 6.1)',
intro: 'Ação do vento em cilindros circulares (chaminés, silos e reservatórios), detalhando a influência do número de Reynolds e da rugosidade superficial no arrasto.',
sections: [
{
title: '1. Equação do Arrasto Global',
content: (
<div className="flex flex-col gap-3">
<p>
A força de arrasto exercida por unidade de comprimento de um cilindro circular é calculada por:
</p>
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
F = q · Ca · D
</div>
<p>
Onde <strong>D</strong> é o diâmetro externo do cilindro e <strong>Ca</strong> é o coeficiente de arrasto corrigido pela esbeltez.
</p>
</div>
),
},
{
title: '2. Regimes de Fluxo e Número de Reynolds (Re)',
content: (
<div className="flex flex-col gap-3">
<p>
O comportamento aerodinâmico em cilindros circulares é governado pelo <strong>Número de Reynolds (Re)</strong>, um parâmetro adimensional que indica a relação entre as forças de inércia e de viscosidade do fluxo:
</p>
<div className="bg-muted/50 p-3 rounded-lg font-mono text-sm border border-border">
Re = 70.000 · Vk · D
</div>
<p>
O fluxo é classificado em três regimes principais na NBR 6123:
</p>
<ul className="list-disc pl-5 space-y-1">
<li>
<strong>Subcrítico (Re &lt; 1.5 · 10):</strong> A camada limite é laminar. O descolamento do fluxo ocorre cedo (cerca de 80º em relação ao ponto de estagnação). A esteira de turbulência atrás do cilindro é larga, resultando em um arrasto elevado (Ca 1.2).
</li>
<li>
<strong>Crítico / Supercrítico (Re &gt; 4 · 10):</strong> A camada limite torna-se turbulenta. Ela consegue "colar" mais tempo na parede do cilindro, atrasando o descolamento para cerca de 120º. A esteira traseira estreita significativamente, derrubando o arrasto de forma abrupta (Ca pode cair para 0.4 a 0.7).
</li>
</ul>
</div>
),
},
{
title: '3. Influência da Rugosidade Superficial',
content: (
<div className="flex flex-col gap-3">
<p>
Para altos números de Reynolds, a rugosidade superficial do cilindro (rugosidade relativa <strong>d/k</strong>) impede que o arrasto caia excessivamente.
</p>
<ul className="list-disc pl-5 space-y-1">
<li>
<strong>Superfícies lisas (d/k &gt; 10):</strong> Exibem o menor arrasto possível no regime supercrítico (Ca 0.4 - 0.5).
</li>
<li>
<strong>Superfícies rugosas (d/k reduzido, ex: concreto bruto ou chapa ondulada):</strong> Aceleram a transição para fluxo turbulento, mas a rugosidade gera turbulência local que impede a esteira de estreitar tanto, mantendo o arrasto em patamares mais elevados (Ca 0.7 - 0.9).
</li>
</ul>
</div>
),
},
],
};
+33
View File
@@ -0,0 +1,33 @@
export const domeManual = {
title: 'Cúpulas Esféricas (Tabela 21 e 22)',
intro: 'Ação do vento em cúpulas e coberturas hemisféricas, analisando coeficientes de forças globais e pressões locais.',
sections: [
{
title: '1. Aerodinâmica Tridimensional',
content: (
<div className="flex flex-col gap-3">
<p>
Cúpulas esféricas possuem comportamento aerodinâmico tridimensional muito favorável. O ar contorna a cúpula não apenas por cima, mas também pelos lados, reduzindo a turbulência geral comparado com coberturas planas ou inclinadas.
</p>
</div>
),
},
{
title: '2. Coeficientes de Pressão Externa',
content: (
<div className="flex flex-col gap-3">
<p>
O vento acelerado sobre a cúpula cria três zonas de pressões marcantes:
</p>
<ul className="list-disc pl-5 space-y-1">
<li><strong>Barlavento (frente):</strong> Sobrepressão sutil perto da base, que rapidamente vira sucção à medida que sobe no arco esférico.</li>
<li><strong>Topo:</strong> Ponto de máxima velocidade do vento. Sofre uma sucção crítica gerada pelo escoamento aerodinâmico acelerado.</li>
<li><strong>Sotavento (traseira):</strong> Zona de vácuo aerodinâmico com sucção homogênea constante e desprendimento de esteira tridimensional.</li>
</ul>
</div>
),
},
],
};
+72
View File
@@ -0,0 +1,72 @@
export const dynamicsManual = {
title: 'Efeitos Dinâmicos e Ressonância (Seção 9 e Anexo G)',
intro: 'Explicação detalhada dos fenômenos dinâmicos induzidos pelo vento, com foco especial no desprendimento de vórtices (Vortex Shedding).',
sections: [
{
title: '1. Desprendimento de Vórtices (Vortex Shedding)',
content: (
<div className="flex flex-col gap-3">
<p>
Quando o vento sopra perpendicularmente a um obstáculo cilíndrico (como uma chaminé ou torre), o fluxo de ar se divide e cria redemoinhos alternados na traseira da estrutura.
</p>
<p>
Estes redemoinhos, conhecidos como <strong>Vórtices de Von Kármán</strong>, geram forças de sucção alternadas nas laterais perpendiculares à direção do vento. Como consequência, o cilindro sofre forças dinâmicas oscilantes transversais.
</p>
</div>
),
},
{
title: '2. Velocidade Crítica de Ressonância (Vcr)',
content: (
<div className="flex flex-col gap-3">
<p>
A frequência na qual os vórtices se desprendem é dada por:
</p>
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
f = St · V / D
</div>
<p>
Onde <strong>St</strong> é o número de Strouhal (adimensional, geralmente 0.2 para cilindros), <strong>V</strong> é a velocidade do vento e <strong>D</strong> é o diâmetro.
</p>
<p>
Se a frequência de descolamento de vórtices se igualar a uma das frequências naturais da estrutura (<strong>fn</strong>), ocorre o fenômeno de **ressonância**, gerando grandes amplitudes de oscilação. A velocidade do vento que ativa a ressonância é chamada de **Velocidade Crítica (Vcr)**:
</p>
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
Vcr = fn · D / St
</div>
</div>
),
},
{
title: '3. Parâmetros Chave',
content: (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-border text-sm">
<thead>
<tr className="border-b">
<th className="text-left py-2 font-semibold">Parâmetro</th>
<th className="text-left py-2 font-semibold">Descrição</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
<tr>
<td className="py-2 font-mono font-semibold">St (Strouhal)</td>
<td className="py-2">Número adimensional que depende da geometria da seção. Para cilindro liso, St = 0.2.</td>
</tr>
<tr>
<td className="py-2 font-mono font-semibold">fn (Frequência)</td>
<td className="py-2">Frequência natural de vibração da estrutura (em Hz). Estruturas mais esbeltas têm fn menor.</td>
</tr>
<tr>
<td className="py-2 font-mono font-semibold">Vcr (Vel. Crítica)</td>
<td className="py-2">Velocidade em que os esforços alternados dos vórtices entram em ressonância com a estrutura.</td>
</tr>
</tbody>
</table>
</div>
),
},
],
};
+23
View File
@@ -0,0 +1,23 @@
import { bridgeManual } from './bridge';
import { towerManual } from './tower';
import { signManual } from './sign';
import { warehouseManual } from './warehouse';
import { cylinderManual } from './cylinder';
import { vaultManual } from './vault';
import { domeManual } from './dome';
import { barManual } from './bar';
import { dynamicsManual } from './dynamics';
import { isolatedRoofManual } from './isolated-roof';
export const manuals: Record<string, typeof bridgeManual> = {
bridge: bridgeManual,
tower: towerManual,
sign: signManual,
warehouse: warehouseManual,
cylinder: cylinderManual,
vault: vaultManual,
dome: domeManual,
bar: barManual,
dynamics: dynamicsManual,
'isolated-roof': isolatedRoofManual,
};
+41
View File
@@ -0,0 +1,41 @@
export const isolatedRoofManual = {
title: 'Coberturas Isoladas (Tabelas 24 e 25)',
intro: 'Ação do vento em coberturas sem paredes (estruturas abertas, hangares abertos, postos de gasolina), abordando coeficientes de pressão líquida e forças resultantes.',
sections: [
{
title: '1. Coeficiente de Pressão Líquida (Cp)',
content: (
<div className="flex flex-col gap-3">
<p>
Como as coberturas isoladas não possuem paredes para vedação, o vento escoa livremente tanto por cima quanto por baixo do telhado.
</p>
<p>
Por isso, a norma utiliza o coeficiente de **pressão líquida (Cp)**, que representa a diferença líquida de pressão entre a face superior e inferior da chapa do telhado:
</p>
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
F = q · Cp · A
</div>
<p>
Dependendo da direção e do ângulo de inclinação do telhado, a chapa pode sofrer sucção (força para cima) ou sobrepressão (força para baixo).
</p>
</div>
),
},
{
title: '2. Efeito da Inclinação (θ)',
content: (
<div className="flex flex-col gap-3">
<p>
O ângulo de inclinação do telhado (<strong>θ</strong>) define se o escoamento é suave ou gera turbulência pesada:
</p>
<ul className="list-disc pl-5 space-y-1">
<li><strong>Ângulos pequenos (θ &lt; 5º):</strong> O escoamento gera sucção quase uniforme em toda a chapa devido ao efeito Venturi do vento passando sob a cobertura.</li>
<li><strong>Ângulos elevados (θ &gt; 15º):</strong> A cobertura atua como uma asa de avião com alto ângulo de ataque. O lado de barlavento sofre forte pressão para baixo, enquanto o lado de sotavento sofre sucção severa.</li>
</ul>
</div>
),
},
],
};
+61
View File
@@ -0,0 +1,61 @@
export const signManual = {
title: 'Muros e Placas Isoladas (Tabela 11 e Seção 6.1)',
intro: 'Ação do vento em muros, painéis e placas isoladas livres no solo, detalhando arrasto, excentricidades e efeitos de placas de extremidade.',
sections: [
{
title: '1. Coeficiente de Força (Cf)',
content: (
<div className="flex flex-col gap-3">
<p>
Para muros e placas isoladas, a NBR 6123 define o coeficiente de força (<strong>Cf</strong>) com base na relação de aspecto e folga do solo:
</p>
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
F = q · Cf · A
</div>
<p>
Onde <strong>A</strong> é a área total exposta do muro (Comprimento · Altura). O coeficiente Cf varia conforme o alongamento do muro. Muros muito longos e rente ao solo têm Cf maior porque o fluxo de ar não consegue escapar pelas pontas, gerando maior retenção de energia do vento.
</p>
</div>
),
},
{
title: '2. Excentricidade da Força (Torsão)',
content: (
<div className="flex flex-col gap-3">
<p>
O vento raramente atinge o muro de forma perfeitamente homogênea. Quando o vento incide com um ângulo oblíquo (ex: α = 50º), ele se concentra mais próximo a uma das bordas.
</p>
<p>
Por este motivo, a norma NBR 6123 exige a aplicação da força resultante com uma <strong>excentricidade horizontal (e)</strong> em relação ao centro geométrico da placa:
</p>
<div className="bg-muted/50 p-3 rounded-lg font-mono text-sm border border-border">
e = 0.25 · L (comprimento do muro)
</div>
<p>
Esta excentricidade gera um momento torsor nas fundações e suportes estruturais do muro, sendo um fator crítico no dimensionamento de placas e outdoors.
</p>
</div>
),
},
{
title: '3. Placas de Extremidade (End Plates)',
content: (
<div className="flex flex-col gap-3">
<p>
Placas de extremidade são abas físicas verticais instaladas nas pontas do muro para fins aerodinâmicos ou construtivos:
</p>
<ul className="list-disc pl-5 space-y-1">
<li>
<strong>Sem abas:</strong> O vento bate na frente e escoa com facilidade pelas pontas (efeito de borda tridimensional), o que reduz ligeiramente o Cf local próximo às pontas.
</li>
<li>
<strong>Com abas:</strong> As abas forçam o vento a subir apenas pelo topo, gerando um padrão bidimensional de escoamento. Isso aumenta significativamente a pressão diferencial entre a face da frente e de trás do muro, elevando o valor de Cf para patamares elevados (de 1.3 até 2.0).
</li>
</ul>
</div>
),
},
],
};
+91
View File
@@ -0,0 +1,91 @@
export const towerManual = {
title: 'Torre Reticulada (Seção 8.5)',
intro: 'Ação do vento em torres de seção quadrada ou triangular equilátera, cobrindo o índice de solidez, coeficientes de arrasto e fatores de incidência.',
sections: [
{
title: '1. Força de Arrasto Total',
content: (
<div className="flex flex-col gap-3">
<p>
A força de arrasto exercida sobre cada tramo de uma torre reticulada é dada por:
</p>
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
F = q · Ca · Ae
</div>
<p>
Onde <strong>q</strong> é a pressão de vento de projeto, <strong>Ca</strong> é o coeficiente de arrasto e <strong>Ae</strong> é a área projetada efetiva (soma da projeção das barras de barlavento e sotavento, ajustada pelo coeficiente de proteção de tramos).
</p>
</div>
),
},
{
title: '2. Índice de Solidez (φ)',
content: (
<div className="flex flex-col gap-3">
<p>
O índice de solidez (<strong>φ</strong>) é a relação entre a área real exposta pelas barras e a área total envolvente delimitada pelo contorno da face da torre:
</p>
<div className="bg-muted/50 p-3 rounded-lg font-mono text-sm border border-border">
φ = Área das Barras / (Largura da Base · Altura do Tramo)
</div>
<p>
Uma solidez baixa (φ próximo a 0.05) indica uma torre muito vazada (vento passa direto). Uma solidez alta (φ próximo a 1) indica uma torre quase sólida. O coeficiente de arrasto Ca diminui à medida que a solidez aumenta porque as barras de sotavento ficam mais protegidas do vento.
</p>
</div>
),
},
{
title: '3. Ação Direcional (Kα) - Quadrada vs Triangular',
content: (
<div className="flex flex-col gap-3">
<p>
O vento pode atingir a torre de diferentes direções. A norma aborda isso aplicando um fator direcional <strong>Kα</strong>:
</p>
<ul className="list-disc pl-5 space-y-1">
<li>
<strong>Torres Quadradas:</strong> O vento incidindo na diagonal (45º) causa maior arrasto global do que nas faces (0º). Portanto, Kα é maior para vento a 45º.
</li>
<li>
<strong>Torres Triangulares:</strong> Devido à simetria geométrica de um triângulo equilátero, a NBR 6123 assume que o arrasto total é constante independentemente da direção. Assim, <strong>Kα é sempre 1.0</strong> para qualquer direção do vento.
</li>
</ul>
</div>
),
},
{
title: '4. Glossário de Parâmetros',
content: (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-border text-sm">
<thead>
<tr className="border-b">
<th className="text-left py-2 font-semibold">Símbolo</th>
<th className="text-left py-2 font-semibold">Parâmetro</th>
<th className="text-left py-2 font-semibold">Descrição</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
<tr>
<td className="py-2 font-mono font-semibold">φ (phi)</td>
<td className="py-2">Índice de Solidez</td>
<td className="py-2">Razão entre a área das barras e a área total envolvente da face.</td>
</tr>
<tr>
<td className="py-2 font-mono font-semibold">Kα</td>
<td className="py-2">Fator de Direção</td>
<td className="py-2">Multiplicador que ajusta a força com base no ângulo de incidência do vento.</td>
</tr>
<tr>
<td className="py-2 font-mono font-semibold">Ca</td>
<td className="py-2">Coeficiente de Arrasto</td>
<td className="py-2">Coeficiente adimensional que mede a resistência aerodinâmica da estrutura.</td>
</tr>
</tbody>
</table>
</div>
),
},
],
};
+44
View File
@@ -0,0 +1,44 @@
export const vaultManual = {
title: 'Abóbadas Cilíndricas (Tabela 15 - Seção 6.2)',
intro: 'Coeficientes de pressão externa para coberturas cilíndricas convexas (abóbadas) assentadas diretamente no solo ou elevadas.',
sections: [
{
title: '1. Geometria da Abóbada',
content: (
<div className="flex flex-col gap-3">
<p>
O fluxo de vento sobre abóbadas é fortemente influenciado pelas relações geométricas:
</p>
<ul className="list-disc pl-5 space-y-1">
<li><strong>f (flecha):</strong> Altura do arco da abóbada.</li>
<li><strong>d (vão):</strong> Largura da projeção horizontal da abóbada.</li>
<li><strong>h (altura da parede de suporte):</strong> Se a abóbada estiver assentada diretamente no solo, h = 0. Se estiver elevada, h representa a altura da parede.</li>
</ul>
</div>
),
},
{
title: '2. Distribuição de Pressões (Cpe)',
content: (
<div className="flex flex-col gap-3">
<p>
O perfil curvo da abóbada atua como um aerofólio (perfil de asa), gerando aceleração acentuada do ar no topo:
</p>
<ul className="list-disc pl-5 space-y-1">
<li>
<strong>Barlavento (início do arco):</strong> Pode sofrer pressão positiva se a parede frontal for alta, ou sucção leve caso o vento deslize suavemente pela curvatura suave.
</li>
<li>
<strong>Topo (Zona Central):</strong> É a região de maior aceleração do fluxo de vento. Sofre uma sucção fortíssima (Cpe muito negativo), tendendo a puxar a cobertura para cima.
</li>
<li>
<strong>Sotavento (fim do arco):</strong> O fluxo descola da abóbada, gerando uma zona de recirculação com sucção constante, embora de menor magnitude que no topo.
</li>
</ul>
</div>
),
},
],
};
+65
View File
@@ -0,0 +1,65 @@
export const warehouseManual = {
title: 'Galpão Retangular (Tabelas 4 e 5 - Seção 6)',
intro: 'Ação do vento em galpões de base retangular e telhados a duas águas. Abrange os coeficientes de pressão externa (Cpe) e interna (Cpi).',
sections: [
{
title: '1. Pressão Resultante (p)',
content: (
<div className="flex flex-col gap-3">
<p>
A pressão efetiva do vento em qualquer ponto da superfície externa do galpão é a diferença entre a pressão externa e a interna:
</p>
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
p = q · (Cpe - Cpi)
</div>
<p>
Onde:
</p>
<ul className="list-disc pl-5 space-y-1">
<li><strong>q:</strong> Pressão dinâmica do vento na altura do topo do telhado.</li>
<li><strong>Cpe:</strong> Coeficiente de pressão externa (depende da zona do galpão).</li>
<li><strong>Cpi:</strong> Coeficiente de pressão interna (depende de aberturas nas paredes).</li>
</ul>
</div>
),
},
{
title: '2. Zonas de Pressão Externa (Cpe)',
content: (
<div className="flex flex-col gap-3">
<p>
Devido à complexidade aerodinâmica, a NBR 6123 divide o galpão em várias zonas de pressão localizadas:
</p>
<ul className="list-disc pl-5 space-y-1">
<li>
<strong>Paredes (Zonas A, B, C, D, E):</strong> Geralmente, a parede de barlavento sofre sobrepressão (Cpe &gt; 0, Zona D), enquanto as paredes laterais (Zonas A, B, C) e sotavento (Zona E) sofrem sucção (Cpe &lt; 0) devido ao fluxo descolado.
</li>
<li>
<strong>Telhado (Zonas F, G, H, I, J):</strong> O fluxo de ar descola bruscamente no beiral de barlavento, criando sucções locais fortíssimas nas bordas (Zonas F e G). No meio do telhado (Zona H) e na água de sotavento (Zonas I, J), a sucção tende a diminuir levemente.
</li>
</ul>
</div>
),
},
{
title: '3. Pressão Interna (Cpi)',
content: (
<div className="flex flex-col gap-3">
<p>
A pressão interna surge da passagem do ar através de frestas, janelas, portões e aberturas:
</p>
<ul className="list-disc pl-5 space-y-1">
<li>
<strong>Abertura dominante em barlavento (frente):</strong> O vento entra e "infla" o galpão por dentro. Isso gera uma sobrepressão interna positiva (Cpi até +0.9). Esta pressão positiva empurra as paredes e o telhado de dentro para fora, somando-se à sucção externa e aumentando o risco de colapso do telhado.
</li>
<li>
<strong>Abertura dominante em sotavento ou laterais:</strong> O ar é "sugado" de dentro do galpão, criando vácuo/sucção interna (Cpi até -0.9). Isso ajuda a estabilizar o telhado contra a sucção externa, mas sobrecarrega a parede de barlavento.
</li>
</ul>
</div>
),
},
],
};
+4
View File
@@ -10,6 +10,7 @@ import { calculateCircleBarForce, reynoldsBar } from '@/lib/nbr-tables/table-27'
import Bar3DViewer from '@/components/three/Bar3D'; import Bar3DViewer from '@/components/three/Bar3D';
import SceneCapturePanel from '../components/SceneCapturePanel'; import SceneCapturePanel from '../components/SceneCapturePanel';
import ExportMenu from '../components/ExportMenu'; import ExportMenu from '../components/ExportMenu';
import { EducationalManual } from '@/components/EducationalManual';
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf'; import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
const BarSelectorModule: React.FC = () => { const BarSelectorModule: React.FC = () => {
@@ -157,6 +158,9 @@ const BarSelectorModule: React.FC = () => {
<div className="absolute top-4 left-4 z-10 flex gap-2"> <div className="absolute top-4 left-4 z-10 flex gap-2">
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">α = {alpha}°</Badge> <Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">α = {alpha}°</Badge>
</div> </div>
<div className="absolute top-4 right-4 z-10">
<EducationalManual type="bar" params={{ barType, section, alpha }} />
</div>
<Bar3DViewer <Bar3DViewer
barType={barType} barType={barType}
section={barType === 'flat' ? section : undefined} section={barType === 'flat' ? section : undefined}
+5
View File
@@ -9,6 +9,7 @@ import { classifyBridge, calculateBridgeDeckForces, flutterCheck, gallopingCheck
import Bridge3DViewer from '@/components/three/Bridge3D'; import Bridge3DViewer from '@/components/three/Bridge3D';
import SceneCapturePanel from '../components/SceneCapturePanel'; import SceneCapturePanel from '../components/SceneCapturePanel';
import ExportMenu from '../components/ExportMenu'; import ExportMenu from '../components/ExportMenu';
import { EducationalManual } from '@/components/EducationalManual';
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf'; import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
const BridgeModule: React.FC = () => { const BridgeModule: React.FC = () => {
@@ -149,6 +150,9 @@ const BridgeModule: React.FC = () => {
Classe {classification.bridgeClass} Classe {classification.bridgeClass}
</Badge> </Badge>
</div> </div>
<div className="absolute top-4 right-4 z-10">
<EducationalManual type="bridge" params={{ alpha }} />
</div>
<Bridge3DViewer <Bridge3DViewer
lp={lp} lp={lp}
width={width} width={width}
@@ -158,6 +162,7 @@ const BridgeModule: React.FC = () => {
cz={forces.cz} cz={forces.cz}
fxPerLength={forces.fxPerLength} fxPerLength={forces.fxPerLength}
fzPerLength={forces.fzPerLength} fzPerLength={forces.fzPerLength}
alpha={alpha}
/> />
</div> </div>
+4
View File
@@ -9,6 +9,7 @@ import { calculateCylinder, type CylinderEndType } from '@/lib/modules/cylinder'
import Cylinder3DViewer from '@/components/three/Cylinder3D'; import Cylinder3DViewer from '@/components/three/Cylinder3D';
import SceneCapturePanel from '../components/SceneCapturePanel'; import SceneCapturePanel from '../components/SceneCapturePanel';
import ExportMenu from '../components/ExportMenu'; import ExportMenu from '../components/ExportMenu';
import { EducationalManual } from '@/components/EducationalManual';
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf'; import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
const CylinderModule: React.FC = () => { const CylinderModule: React.FC = () => {
@@ -140,6 +141,9 @@ const CylinderModule: React.FC = () => {
Re = {result.re.toExponential(2)} Re = {result.re.toExponential(2)}
</Badge> </Badge>
</div> </div>
<div className="absolute top-4 right-4 z-10">
<EducationalManual type="cylinder" params={{ roughness: surface }} />
</div>
<div className="w-full h-full bg-muted/20"> <div className="w-full h-full bg-muted/20">
<Cylinder3DViewer <Cylinder3DViewer
diameter={diameter} diameter={diameter}
+4
View File
@@ -9,6 +9,7 @@ import { calculateDome, type DomeType } from '@/lib/modules/dome';
import Dome3DViewer from '@/components/three/Dome3D'; import Dome3DViewer from '@/components/three/Dome3D';
import SceneCapturePanel from '../components/SceneCapturePanel'; import SceneCapturePanel from '../components/SceneCapturePanel';
import ExportMenu from '../components/ExportMenu'; import ExportMenu from '../components/ExportMenu';
import { EducationalManual } from '@/components/EducationalManual';
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf'; import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
const DomeModule: React.FC = () => { const DomeModule: React.FC = () => {
@@ -118,6 +119,9 @@ const DomeModule: React.FC = () => {
Cpi = {cpi.toFixed(2)} Cpi = {cpi.toFixed(2)}
</Badge> </Badge>
</div> </div>
<div className="absolute top-4 right-4 z-10">
<EducationalManual type="dome" params={{ rise, diameter }} />
</div>
<div className="w-full h-full bg-muted/20"> <div className="w-full h-full bg-muted/20">
<Dome3DViewer <Dome3DViewer
diameter={diameter} diameter={diameter}
+3 -1
View File
@@ -27,6 +27,7 @@ import { TABLE_32 } from '@/lib/nbr-tables/table-32';
import Dynamics3DViewer from '@/components/three/Dynamics3D'; import Dynamics3DViewer from '@/components/three/Dynamics3D';
import SceneCapturePanel from '../components/SceneCapturePanel'; import SceneCapturePanel from '../components/SceneCapturePanel';
import ExportMenu from '../components/ExportMenu'; import ExportMenu from '../components/ExportMenu';
import { EducationalManual } from '@/components/EducationalManual';
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf'; import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
const DynamicsModule: React.FC = () => { const DynamicsModule: React.FC = () => {
@@ -256,8 +257,9 @@ const DynamicsModule: React.FC = () => {
</div> </div>
)} )}
<Card className="flex-1 min-h-[400px]"> <Card className="flex-1 min-h-[400px]">
<CardHeader className="pb-3"> <CardHeader className="pb-3 flex flex-row items-center justify-between">
<CardTitle className="text-base">Visualização 3D</CardTitle> <CardTitle className="text-base">Visualização 3D</CardTitle>
<EducationalManual type="dynamics" params={{ fn: freq }} />
</CardHeader> </CardHeader>
<CardContent className="h-full"> <CardContent className="h-full">
<Dynamics3DViewer <Dynamics3DViewer
+3 -1
View File
@@ -11,6 +11,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import ExportMenu from '../components/ExportMenu'; import ExportMenu from '../components/ExportMenu';
import { EducationalManual } from '@/components/EducationalManual';
const GalpaoModule: React.FC = () => { const GalpaoModule: React.FC = () => {
const { const {
@@ -103,8 +104,9 @@ const GalpaoModule: React.FC = () => {
</Badge> </Badge>
</div> </div>
<div className="absolute top-4 right-4 z-10"> <div className="absolute top-4 right-4 z-10 flex gap-2">
<ExportMenu /> <ExportMenu />
<EducationalManual type="warehouse" params={{ windAngle, cpi }} />
</div> </div>
<div className="absolute bottom-4 left-4 z-10 flex gap-3 p-3 bg-background/90 backdrop-blur-md border border-border rounded-lg shadow-sm"> <div className="absolute bottom-4 left-4 z-10 flex gap-3 p-3 bg-background/90 backdrop-blur-md border border-border rounded-lg shadow-sm">
+5
View File
@@ -9,6 +9,7 @@ import { calculateIsolatedShedRoof, calculateIsolatedGableRoof, frictionForceIso
import IsolatedRoof3DViewer from '@/components/three/IsolatedRoof3D'; import IsolatedRoof3DViewer from '@/components/three/IsolatedRoof3D';
import SceneCapturePanel from '../components/SceneCapturePanel'; import SceneCapturePanel from '../components/SceneCapturePanel';
import ExportMenu from '../components/ExportMenu'; import ExportMenu from '../components/ExportMenu';
import { EducationalManual } from '@/components/EducationalManual';
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf'; import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
const IsolatedRoofModule: React.FC = () => { const IsolatedRoofModule: React.FC = () => {
@@ -164,10 +165,14 @@ const IsolatedRoofModule: React.FC = () => {
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">θ = {theta}°</Badge> <Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">θ = {theta}°</Badge>
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">{type === 'shed' ? 'Uma água' : 'Duas águas'}</Badge> <Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">{type === 'shed' ? 'Uma água' : 'Duas águas'}</Badge>
</div> </div>
<div className="absolute top-4 right-4 z-10">
<EducationalManual type="isolated-roof" params={{ theta, type }} />
</div>
<IsolatedRoof3DViewer <IsolatedRoof3DViewer
type={type} type={type}
theta={theta} theta={theta}
height={height} height={height}
width={width}
depth={depth} depth={depth}
cpeWindward={cpeWindward} cpeWindward={cpeWindward}
cpeLeeward={cpeLeeward} cpeLeeward={cpeLeeward}
+4
View File
@@ -9,6 +9,7 @@ import { calculateSign } from '@/lib/nbr-tables/table-23';
import Sign3DViewer from '@/components/three/Sign3D'; import Sign3DViewer from '@/components/three/Sign3D';
import SceneCapturePanel from '../components/SceneCapturePanel'; import SceneCapturePanel from '../components/SceneCapturePanel';
import ExportMenu from '../components/ExportMenu'; import ExportMenu from '../components/ExportMenu';
import { EducationalManual } from '@/components/EducationalManual';
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf'; import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
const SignModule: React.FC = () => { const SignModule: React.FC = () => {
@@ -118,6 +119,9 @@ const SignModule: React.FC = () => {
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">F = {result.forceKN.toFixed(2)} kN</Badge> <Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">F = {result.forceKN.toFixed(2)} kN</Badge>
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">Cf = {result.cf.toFixed(2)}</Badge> <Badge variant="outline" className="bg-background/80 backdrop-blur-sm">Cf = {result.cf.toFixed(2)}</Badge>
</div> </div>
<div className="absolute top-4 right-4 z-10">
<EducationalManual type="sign" params={{ length, height, alpha, hasEndPlates }} />
</div>
<Sign3DViewer <Sign3DViewer
length={length} length={length}
+4
View File
@@ -8,6 +8,7 @@ import { calculateTower, type TowerSection, type TowerBarType } from '@/lib/modu
import Tower3DViewer from '@/components/three/Tower3D'; import Tower3DViewer from '@/components/three/Tower3D';
import SceneCapturePanel from '../components/SceneCapturePanel'; import SceneCapturePanel from '../components/SceneCapturePanel';
import ExportMenu from '../components/ExportMenu'; import ExportMenu from '../components/ExportMenu';
import { EducationalManual } from '@/components/EducationalManual';
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf'; import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
const TowerModule: React.FC = () => { const TowerModule: React.FC = () => {
@@ -141,6 +142,9 @@ const TowerModule: React.FC = () => {
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">{section === 'square' ? 'Quadrada' : 'Triangular'}</Badge> <Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">{section === 'square' ? 'Quadrada' : 'Triangular'}</Badge>
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">α = {alphaWind}°</Badge> <Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">α = {alphaWind}°</Badge>
</div> </div>
<div className="absolute top-4 right-4 z-10">
<EducationalManual type="tower" params={{ section, alphaWind }} />
</div>
<Tower3DViewer <Tower3DViewer
section={section} section={section}
barType={barType} barType={barType}
+4
View File
@@ -9,6 +9,7 @@ import { calculateVault, type VaultRegime } from '@/lib/modules/vault';
import Vault3DViewer from '@/components/three/Vault3D'; import Vault3DViewer from '@/components/three/Vault3D';
import SceneCapturePanel from '../components/SceneCapturePanel'; import SceneCapturePanel from '../components/SceneCapturePanel';
import ExportMenu from '../components/ExportMenu'; import ExportMenu from '../components/ExportMenu';
import { EducationalManual } from '@/components/EducationalManual';
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf'; import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
const VaultModule: React.FC = () => { const VaultModule: React.FC = () => {
@@ -126,6 +127,9 @@ const VaultModule: React.FC = () => {
Cpi = {localCpi > 0 ? `+${localCpi.toFixed(2)}` : localCpi.toFixed(2)} Cpi = {localCpi > 0 ? `+${localCpi.toFixed(2)}` : localCpi.toFixed(2)}
</Badge> </Badge>
</div> </div>
<div className="absolute top-4 right-4 z-10">
<EducationalManual type="vault" params={{ rise, span }} />
</div>
<div className="w-full h-full bg-muted/20"> <div className="w-full h-full bg-muted/20">
<Vault3DViewer span={span} length={length} rise={rise} cpi={localCpi} cpeProfile={{ ...result.windPerpendicular }} /> <Vault3DViewer span={span} length={length} rise={rise} cpi={localCpi} cpeProfile={{ ...result.windPerpendicular }} />
</div> </div>