feat: aprimoramento didatico dos SVGs, rotulagem de vistas, legendas e explicacao contextualizada

This commit is contained in:
2026-07-10 00:05:29 +00:00
parent 40e73e734a
commit 59ac5c1827
5 changed files with 673 additions and 276 deletions
+144 -2
View File
@@ -1,4 +1,5 @@
import { BookOpen, HelpCircle } from 'lucide-react';
import React from 'react';
import { HelpCircle, Info, Settings2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Sheet,
@@ -23,6 +24,144 @@ export function EducationalManual({ type, params }: EducationalManualProps) {
return null;
}
// Gera explicações interpretativas dinâmicas com base nas opções selecionadas no painel
const renderConfigurationExplanation = () => {
const explanations: React.ReactNode[] = [];
// --- Abóbadas (vault) ---
if (type === 'vault') {
const regime = params.regime || 'laminar-rough';
const cpi = Number(params.localCpi || 0);
// Regime de escoamento
if (regime === 'laminar-rough') {
explanations.push(
<div key="vault-regime" className="text-xs border-l-2 border-sky-400 pl-3 py-1 space-y-1">
<span className="font-bold text-foreground block">Escoamento: Baixa Turbulência (Tabelas 15-17)</span>
<p>Representa vento uniforme sobre terreno desobstruído. O escoamento flui de forma limpa sobre o arco, gerando os valores tradicionais de sucção no topo.</p>
</div>
);
} else {
explanations.push(
<div key="vault-regime" className="text-xs border-l-2 border-red-500 pl-3 py-1 space-y-1">
<span className="font-bold text-foreground block">Escoamento: Regime Turbulento (Tabelas 18-20)</span>
<p>O vento apresenta alta turbulência devido a obstáculos no terreno. As correntes de vento ondulantes (exibidas no simulador) induzem picos de pressões dinâmicas flutuantes.</p>
</div>
);
}
// Permeabilidade (Cpi)
if (cpi === 0) {
explanations.push(
<div key="vault-cpi" className="text-xs border-l-2 border-slate-400 pl-3 py-1 space-y-1">
<span className="font-bold text-foreground block">Pressão Interna Neutralizada (Cpi = 0.0)</span>
<p>As aberturas são inexistentes ou estão distribuídas igualmente nas faces. A força resultante no arco depende exclusivamente da sucção externa.</p>
</div>
);
} else if (cpi > 0) {
explanations.push(
<div key="vault-cpi" className="text-xs border-l-2 border-orange-500 pl-3 py-1 space-y-1">
<span className="font-bold text-orange-500 block">Abóbada Inflada (Cpi = +{cpi})</span>
<p>Uma abertura dominante na face de barlavento permite a entrada do ar, "inflando" a abóbada de dentro para fora (indicado pelas setas laranjas). Essa pressão interna empurra a cobertura para cima, somando-se à sucção externa do vento e aumentando de forma crítica a força de arrancamento do telhado!</p>
</div>
);
} else {
explanations.push(
<div key="vault-cpi" className="text-xs border-l-2 border-blue-500 pl-3 py-1 space-y-1">
<span className="font-bold text-blue-500 block">Abóbada Sugada / Vácuo (Cpi = {cpi})</span>
<p>Com aberturas dominantes nas faces protegidas (laterais ou sotavento), o ar interno é sugado para fora, criando um vácuo interno (setas azuis). Esse efeito de vácuo ajuda a compensar e aliviar a sucção externa no topo, mas puxa as paredes para dentro.</p>
</div>
);
}
}
// --- Cilindros (cylinder) ---
if (type === 'cylinder') {
const isSmooth = params.surface === 'smooth';
const cpi = Number(params.cpi || 0);
explanations.push(
<div key="cyl-surface" className="text-xs border-l-2 border-sky-400 pl-3 py-1 space-y-1">
<span className="font-bold text-foreground block">Superfície: {isSmooth ? 'Lisa' : 'Rugosa'}</span>
<p>{isSmooth
? 'Superfície de baixo atrito. No regime supercrítico, o descolamento da camada limite é atrasado significativamente, fazendo o coeficiente de arrasto Ca cair para valores muito baixos (~0.4).'
: 'Casca áspera. A rugosidade gera microturbulências na superfície do cilindro que impedem a esteira de estreitar excessivamente, mantendo o arrasto Ca em níveis elevados no regime crítico/supercrítico.'
}</p>
</div>
);
if (cpi > 0) {
explanations.push(
<div key="cyl-cpi" className="text-xs border-l-2 border-orange-500 pl-3 py-1 space-y-1">
<span className="font-bold text-orange-500 block">Reservatório Inflado (Cpi positivo)</span>
<p>O ar interno pressiona as paredes do silo/chaminé de dentro para fora, somando-se às sucções externas nas laterais.</p>
</div>
);
}
}
// --- Muros e Placas (sign) ---
if (type === 'sign') {
const hasEndPlates = params.hasEndPlates === true;
const alpha = Number(params.alpha || 90);
explanations.push(
<div key="sign-alpha" className="text-xs border-l-2 border-sky-400 pl-3 py-1 space-y-1">
<span className="font-bold text-foreground block">Incidência do Vento: {alpha}º</span>
<p>{alpha === 90
? 'Vento Perpendicular: o arrasto atua uniformemente, concentrando a força resultante exatamente no centro da placa (sem torque de torção nas fundações).'
: 'Vento Oblíquo: o fluxo se concentra na borda esquerda de ataque. Isso gera uma excentricidade horizontal na força (e = 0.25 L), introduzindo um momento torsor crítico que deve ser suportado pelos pilares.'
}</p>
</div>
);
explanations.push(
<div key="sign-plates" className="text-xs border-l-2 border-red-500 pl-3 py-1 space-y-1">
<span className="font-bold text-foreground block">Abas Laterais (End Plates): {hasEndPlates ? 'Com abas' : 'Sem abas'}</span>
<p>{hasEndPlates
? 'As abas vermelhas mostradas no simulador impedem o ar de escapar pelas laterais do muro, gerando um escoamento bidimensional plano que eleva a força de arrasto Cf ao máximo.'
: 'O vento escoa tridimensionalmente pelas bordas laterais livres, criando um efeito de alívio aerodinâmico que reduz a força resultante nas pontas do muro.'
}</p>
</div>
);
}
// --- Galpões (warehouse) ---
if (type === 'warehouse') {
const cpi = Number(params.cpi || 0);
if (cpi > 0) {
explanations.push(
<div key="wh-cpi" className="text-xs border-l-2 border-orange-500 pl-3 py-1 space-y-1">
<span className="font-bold text-orange-500 block">Galpão Inflado (Cpi = +{cpi})</span>
<p>Ocorre quando aberturas dominantes na parede de barlavento (frente). O ar "sopra" para dentro e expande o galpão de dentro para fora, somando-se à sucção externa do telhado e aumentando de forma gravíssima o risco de arrancamento da cobertura!</p>
</div>
);
} else if (cpi < 0) {
explanations.push(
<div key="wh-cpi" className="text-xs border-l-2 border-blue-500 pl-3 py-1 space-y-1">
<span className="font-bold text-blue-500 block">Galpão Desinflado / Vácuo (Cpi = {cpi})</span>
<p>Ocorre com aberturas nas paredes laterais ou de trás (sotavento). O ar interno é sugado, puxando as chapas do telhado para dentro e ajudando a contrapor a sucção do vento externo.</p>
</div>
);
}
}
if (explanations.length === 0) return null;
return (
<div className="flex flex-col gap-3 bg-slate-900/10 p-4 border border-border/60 rounded-xl my-4">
<div className="flex items-center gap-2 text-primary border-b border-border/40 pb-1.5">
<Settings2 className="size-4" />
<h4 className="text-xs font-bold uppercase tracking-wider">Análise das Suas Configurações</h4>
</div>
<div className="flex flex-col gap-3">
{explanations}
</div>
</div>
);
};
return (
<Sheet>
<SheetTrigger asChild>
@@ -32,7 +171,7 @@ export function EducationalManual({ type, params }: EducationalManualProps) {
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" />
<Info 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">
@@ -49,6 +188,9 @@ export function EducationalManual({ type, params }: EducationalManualProps) {
{/* Simulador Interativo 2D no topo do manual */}
<WindFlowGrid2D type={type} params={params} />
{/* Quadro Dinâmico explicativo dos seletores */}
{renderConfigurationExplanation()}
{/* Seções de Texto e Fórmulas */}
<div className="flex flex-col gap-6 text-foreground leading-relaxed">
{manual.sections.map((section, idx) => (
+526 -271
View File
@@ -1,12 +1,11 @@
interface WindFlowGrid2DProps {
type: string;
params: Record<string, any>;
}
export function WindFlowGrid2D({ type, params }: WindFlowGrid2DProps) {
// Estilo global de animação inserido no próprio SVG
// Estilos globais e animações integrados no SVG
const styleBlock = (
<style>{`
@keyframes windFlow {
@@ -22,7 +21,7 @@ export function WindFlowGrid2D({ type, params }: WindFlowGrid2DProps) {
50% { transform: translateY(-8px); }
}
.wind-line {
stroke: #60a5fa;
stroke: #3b82f6;
stroke-width: 1.8;
stroke-dasharray: 8 16;
animation: windFlow 1.8s linear infinite;
@@ -30,7 +29,7 @@ export function WindFlowGrid2D({ type, params }: WindFlowGrid2DProps) {
opacity: 0.85;
}
.wind-line-fast {
stroke: #38bdf8;
stroke: #0ea5e9;
stroke-width: 2.2;
stroke-dasharray: 6 12;
animation: windFlow 0.9s linear infinite;
@@ -45,11 +44,12 @@ export function WindFlowGrid2D({ type, params }: WindFlowGrid2DProps) {
opacity: 0.5;
}
.vortex-line {
stroke: #f87171;
stroke: #ef4444;
stroke-width: 1.5;
stroke-dasharray: 4 8;
animation: windFlow 2.5s linear infinite;
fill: none;
opacity: 0.8;
}
.vortex-spin-cw {
transform-origin: center;
@@ -62,71 +62,185 @@ export function WindFlowGrid2D({ type, params }: WindFlowGrid2DProps) {
.vibe-cyl {
animation: vibration 1.5s ease-in-out infinite;
}
.cpi-arrow-pos {
stroke: #f97316;
stroke-width: 2;
fill: none;
}
.cpi-arrow-neg {
stroke: #2563eb;
stroke-width: 2;
fill: none;
}
`}</style>
);
// Renderizadores específicos de fluxo ativo com base nos sliders da página
const getViewLabel = () => {
switch (type) {
case 'tower':
case 'cylinder':
case 'bar':
return 'Vista Aérea (Superior / Planta Baixa)';
case 'bridge':
case 'sign':
case 'warehouse':
case 'vault':
case 'dome':
case 'dynamics':
case 'isolated-roof':
return 'Vista Lateral (Corte Transversal)';
default:
return 'Vista Esquemática';
}
};
// Desenha os vetores de Cpi (pressão interna) dentro do perfil do galpão/cilindro/abóbada
const renderCpiVectors = (cpi: number, shape: 'rect' | 'arch' | 'circle' | 'dome') => {
if (cpi === 0) return null;
const isPositive = cpi > 0;
const marker = isPositive ? 'url(#cpi-pos)' : 'url(#cpi-neg)';
const strokeClass = isPositive ? 'cpi-arrow-pos' : 'cpi-arrow-neg';
if (shape === 'arch') {
// Abóbada: 3 vetores radiais
return (
<g>
{isPositive ? (
<>
<path d="M 190,140 L 165,115" className={strokeClass} markerEnd={marker} />
<path d="M 200,135 L 200,105" className={strokeClass} markerEnd={marker} />
<path d="M 210,140 L 235,115" className={strokeClass} markerEnd={marker} />
</>
) : (
<>
<path d="M 165,115 L 190,140" className={strokeClass} markerEnd={marker} />
<path d="M 200,105 L 200,135" className={strokeClass} markerEnd={marker} />
<path d="M 235,115 L 210,140" className={strokeClass} markerEnd={marker} />
</>
)}
</g>
);
}
if (shape === 'rect') {
// Galpão: paredes e telhado
return (
<g>
{isPositive ? (
<>
{/* Paredes */}
<path d="M 170,145 L 145,145" className={strokeClass} markerEnd={marker} />
<path d="M 230,145 L 255,145" className={strokeClass} markerEnd={marker} />
{/* Telhado */}
<path d="M 180,105 L 165,85" className={strokeClass} markerEnd={marker} />
<path d="M 220,105 L 235,85" className={strokeClass} markerEnd={marker} />
</>
) : (
<>
<path d="M 145,145 L 170,145" className={strokeClass} markerEnd={marker} />
<path d="M 255,145 L 230,145" className={strokeClass} markerEnd={marker} />
<path d="M 165,85 L 180,105" className={strokeClass} markerEnd={marker} />
<path d="M 235,85 L 220,105" className={strokeClass} markerEnd={marker} />
</>
)}
</g>
);
}
if (shape === 'circle') {
// Cilindro: 4 direções
return (
<g>
{isPositive ? (
<>
<path d="M 190,100 L 170,100" className={strokeClass} markerEnd={marker} />
<path d="M 210,100 L 230,100" className={strokeClass} markerEnd={marker} />
<path d="M 200,90 L 200,70" className={strokeClass} markerEnd={marker} />
<path d="M 200,110 L 200,130" className={strokeClass} markerEnd={marker} />
</>
) : (
<>
<path d="M 170,100 L 190,100" className={strokeClass} markerEnd={marker} />
<path d="M 230,100 L 210,100" className={strokeClass} markerEnd={marker} />
<path d="M 200,70 L 200,90" className={strokeClass} markerEnd={marker} />
<path d="M 200,130 L 200,110" className={strokeClass} markerEnd={marker} />
</>
)}
</g>
);
}
if (shape === 'dome') {
// Cúpula
return (
<g>
{isPositive ? (
<>
<path d="M 170,145 L 155,145" className={strokeClass} markerEnd={marker} />
<path d="M 230,145 L 245,145" className={strokeClass} markerEnd={marker} />
<path d="M 200,115 L 200,85" className={strokeClass} markerEnd={marker} />
<path d="M 185,100 L 165,80" className={strokeClass} markerEnd={marker} />
<path d="M 215,100 L 235,80" className={strokeClass} markerEnd={marker} />
</>
) : (
<>
<path d="M 155,145 L 170,145" className={strokeClass} markerEnd={marker} />
<path d="M 245,145 L 230,145" className={strokeClass} markerEnd={marker} />
<path d="M 200,85 L 200,115" className={strokeClass} markerEnd={marker} />
<path d="M 165,80 L 185,100" className={strokeClass} markerEnd={marker} />
<path d="M 235,80 L 215,100" className={strokeClass} markerEnd={marker} />
</>
)}
</g>
);
}
return null;
};
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
const rotationAngle = alpha * 3;
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" />
<svg viewBox="0 0 400 200" className="w-full h-full">
{styleBlock}
<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" />
<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>
{/* 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>
<g transform="translate(140, 85)">
<rect x="0" y="0" width="120" height="30" rx="3" fill="#334155" stroke="#475569" strokeWidth="2" />
<rect x="5" y="-6" width="8" height="6" fill="#1e293b" />
<rect x="107" y="-6" width="8" height="6" fill="#1e293b" />
</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>
{alpha !== 0 && (
<g>
<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="215" y={alpha > 0 ? 45 : 160} fill={alpha > 0 ? "#10b981" : "#ef4444"} className="text-xs font-bold font-mono">
{alpha > 0 ? '+Fz (Sustentação)' : '-Fz (Abaixamento)'}
</text>
</g>
)}
<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>
);
}
@@ -134,243 +248,384 @@ export function WindFlowGrid2D({ type, params }: WindFlowGrid2DProps) {
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}
<svg viewBox="0 0 400 200" className="w-full h-full">
{styleBlock}
<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" />
<path d="M 230,85 Q 310,70 420,85" className="vortex-line" />
<path d="M 230,115 Q 310,130 420,115" className="vortex-line" />
</g>
{/* 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>
<g transform={`translate(200, 100) rotate(${alphaWind})`}>
{section === 'square' ? (
<>
<rect x="-25" y="-25" width="50" height="50" fill="none" stroke="#64748b" strokeWidth="3" />
<line x1="-25" y1="-25" x2="25" y2="25" stroke="#64748b" strokeWidth="1.5" />
<line x1="25" y1="-25" x2="-25" y2="25" stroke="#64748b" strokeWidth="1.5" />
</>
) : (
<>
<polygon points="0,-28 25,15 -25,15" fill="none" stroke="#64748b" strokeWidth="3" />
<line x1="0" y1="-28" x2="0" y2="15" stroke="#64748b" strokeWidth="1.5" />
<line x1="-25" y1="15" x2="12.5" y2="-6.5" stroke="#64748b" strokeWidth="1.5" />
<line x1="25" y1="15" x2="-12.5" y2="-6.5" stroke="#64748b" strokeWidth="1.5" />
</>
)}
</g>
</svg>
);
}
case 'cylinder': {
const isSmooth = params.roughness === 'smooth' || params.d_over_k > 1e5;
const isSmooth = params.surface === 'smooth';
const cpi = Number(params.cpi || 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}
<svg viewBox="0 0 400 200" className="w-full h-full">
{styleBlock}
{/* Marcadores de setas Cpi */}
<defs>
<marker id="cpi-pos" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#f97316" />
</marker>
<marker id="cpi-neg" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb" />
</marker>
</defs>
{/* 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>
<path d="M -20,20 L 420,20" className="wind-line-slow" />
<path d="M -20,60 C 150,55 160,30 200,30 C 240,30 250,55 420,55" className="wind-line-fast" />
<path d="M -20,140 C 150,145 160,170 200,170 C 240,170 250,145 420,145" className="wind-line-fast" />
<path d="M -20,180 L 420,180" className="wind-line-slow" />
</g>
<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" />
<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"} />
{/* Pressão Interna */}
{renderCpiVectors(cpi, 'circle')}
</svg>
);
}
case 'sign': {
const hasEndPlates = params.hasEndPlates === true;
return (
<svg viewBox="0 0 400 200" className="w-full h-full">
{styleBlock}
<line x1="0" y1="170" x2="400" y2="170" stroke="#475569" strokeWidth="3" />
<g>
<path d="M -20,30 L 420,30" className="wind-line-slow" />
{/* O vento passa reto ou desvia mais dependendo das abas laterais */}
<path
d={hasEndPlates
? "M -20,70 C 130,60 160,20 200,20 C 230,20 260,80 420,80"
: "M -20,80 C 150,70 170,40 210,40 C 260,40 300,80 420,80"
}
className="wind-line"
/>
<path d="M -20,130 C 140,120 160,50 200,50 C 250,50 310,120 420,120" className="wind-line-fast" />
{/* Maior turbulência se houver placas de extremidade */}
<circle cx="260" cy="130" r={hasEndPlates ? "22" : "15"} fill="none" stroke="#ef4444" strokeWidth="1.5" strokeDasharray="2 4" className="vortex-spin-cw" />
<path d="M 210,140 Q 280,90 420,155" className="vortex-line" />
</g>
{/* Muro */}
<rect x="195" y="70" width="10" height="100" fill="#334155" stroke="#475569" strokeWidth="2" />
{/* Placas de extremidade (abas de borda) */}
{hasEndPlates && (
<g>
<line x1="190" y1="70" x2="210" y2="70" stroke="#ef4444" strokeWidth="4" />
<line x1="190" y1="170" x2="210" y2="170" stroke="#ef4444" strokeWidth="4" />
</g>
)}
</svg>
);
}
case 'warehouse': {
const cpi = Number(params.cpi || 0);
return (
<svg viewBox="0 0 400 200" className="w-full h-full">
{styleBlock}
<defs>
<marker id="cpi-pos" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#f97316" />
</marker>
<marker id="cpi-neg" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb" />
</marker>
</defs>
<line x1="0" y1="170" x2="400" y2="170" stroke="#475569" strokeWidth="3" />
<g>
<path d="M -20,30 L 420,30" className="wind-line-slow" />
<path d="M -20,80 C 130,80 140,40 200,40 C 260,40 270,90 420,90" className="wind-line-fast" />
<path d="M -20,130 C 120,120 130,95 150,95 L 250,95 C 270,95 280,130 420,130" className="wind-line" />
<path d="M 250,110 Q 300,100 420,140" className="vortex-line" />
</g>
{/* Galpão */}
<path
d="M 140,170 L 140,110 L 200,70 L 260,110 L 260,170 Z"
fill="#334155"
stroke="#475569"
strokeWidth="2.5"
/>
{/* Pressão Interna */}
{renderCpiVectors(cpi, 'rect')}
</svg>
);
}
case 'vault': {
const regime = params.regime || 'laminar-rough';
const cpi = Number(params.localCpi || 0);
const isTurbulent = regime.startsWith('turbulent');
return (
<svg viewBox="0 0 400 200" className="w-full h-full">
{styleBlock}
<defs>
<marker id="cpi-pos" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#f97316" />
</marker>
<marker id="cpi-neg" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb" />
</marker>
</defs>
<line x1="0" y1="170" x2="400" y2="170" stroke="#475569" strokeWidth="3" />
<g>
<path d="M -20,30 L 420,30" className="wind-line-slow" />
{/* Linha de vento ondulante / caótica se for turbulento */}
<path
d={isTurbulent
? "M -20,75 C 130,65 140,45 180,45 Q 210,30 240,75 Q 275,120 420,110"
: "M -20,80 C 130,70 140,40 200,40 C 260,40 270,90 420,90"
}
className="wind-line-fast"
/>
<path
d={isTurbulent
? "M -20,125 C 120,115 130,95 180,95 Q 200,80 230,115 Q 260,140 420,135"
: "M -20,130 C 120,120 130,85 200,85 C 270,85 280,130 420,130"
}
className="wind-line"
/>
{isTurbulent && (
<path d="M 230,105 Q 300,90 420,145" className="vortex-line" />
)}
</g>
{/* Abóbada */}
<path
d="M 130,170 A 70,70 0 0 1 270,170 Z"
fill="#334155"
stroke="#475569"
strokeWidth="2.5"
/>
{/* Pressão Interna */}
{renderCpiVectors(cpi, 'arch')}
</svg>
);
}
case 'dome': {
const cpi = Number(params.cpi || 0);
return (
<svg viewBox="0 0 400 200" className="w-full h-full">
{styleBlock}
<defs>
<marker id="cpi-pos" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#f97316" />
</marker>
<marker id="cpi-neg" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb" />
</marker>
</defs>
<line x1="0" y1="170" x2="400" y2="170" stroke="#475569" strokeWidth="3" />
<g>
<path d="M -20,30 L 420,30" className="wind-line-slow" />
<path d="M -20,70 C 120,60 140,30 200,30 C 260,30 280,75 420,75" className="wind-line-fast" />
<path d="M -20,120 C 120,110 140,80 200,80 C 260,80 280,125 420,125" className="wind-line" />
</g>
{/* Cúpula */}
<g>
<rect x="150" y="120" width="100" height="50" fill="#1e293b" stroke="#334155" strokeWidth="2" />
<path
d="M 150,120 A 50,50 0 0 1 250,120 Z"
fill="#334155"
stroke="#475569"
strokeWidth="2.5"
/>
</g>
{/* Pressão Interna */}
{renderCpiVectors(cpi, 'dome')}
</svg>
);
}
case 'isolated-roof': {
const typeRoof = params.type || 'shed';
return (
<svg viewBox="0 0 400 200" className="w-full h-full">
{styleBlock}
<line x1="0" y1="170" x2="400" y2="170" stroke="#475569" strokeWidth="3" />
<g>
<path d="M -20,30 Q 200,20 420,30" className="wind-line-slow" />
<path d="M -20,70 Q 200,60 420,70" className="wind-line-fast" />
<path d="M -20,110 Q 200,105 420,110" className="wind-line" />
<path d="M -20,145 L 420,145" className="wind-line-slow" />
</g>
<g>
<line x1="160" y1="85" x2="160" y2="170" stroke="#475569" strokeWidth="3" />
<line x1="240" y1="85" x2="240" y2="170" stroke="#475569" strokeWidth="3" />
{typeRoof === 'shed' ? (
<line x1="130" y1="75" x2="270" y2="90" stroke="#f43f5e" strokeWidth="5" strokeLinecap="round" />
) : (
<path d="M 130,90 L 200,70 L 270,90" fill="none" stroke="#f43f5e" strokeWidth="5" strokeLinecap="round" />
)}
</g>
</svg>
);
}
case 'bar': {
const barType = params.barType || 'flat';
const section = params.section || 'l';
const alpha = Number(params.alpha || 0);
return (
<svg viewBox="0 0 400 200" className="w-full h-full">
{styleBlock}
<g transform={`rotate(${alpha}, 200, 100)`}>
<path d="M -20,30 L 420,30" className="wind-line-slow" />
<path d="M -20,70 C 150,60 170,40 230,40 Q 320,70 420,70" className="wind-line" />
<path d="M -20,130 C 150,140 170,160 230,160 Q 320,130 420,130" className="wind-line" />
<path d="M -20,170 L 420,170" className="wind-line-slow" />
</g>
<g transform="translate(200, 100)">
{barType === 'circular' ? (
<circle cx="0" cy="0" r="22" fill="#334155" stroke="#475569" strokeWidth="2.5" />
) : (
<g transform="translate(-20, -20)">
{section === 'l' && (
<path d="M 0,0 L 12,0 L 12,28 L 40,28 L 40,40 L 0,40 Z" fill="#334155" stroke="#475569" strokeWidth="2" />
)}
{section === 'u' && (
<path d="M 0,0 L 12,0 L 12,28 L 28,28 L 28,0 L 40,0 L 40,40 L 0,40 Z" fill="#334155" stroke="#475569" strokeWidth="2" />
)}
{section === 'i' && (
<path d="M 0,0 L 40,0 L 40,10 L 26,10 L 26,30 L 40,30 L 40,40 L 0,40 L 0,30 L 14,30 L 14,10 L 0,10 Z" fill="#334155" stroke="#475569" strokeWidth="2" />
)}
</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>
)}
</g>
</svg>
);
}
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}
<svg viewBox="0 0 400 200" className="w-full h-full">
{styleBlock}
<g>
<path d="M -20,25 L 420,25" className="wind-line-slow" />
<path d="M -20,60 C 150,55 160,35 200,35 C 240,35 250,60 420,60" className="wind-line" />
<path d="M -20,140 C 150,145 160,165 200,165 C 240,165 250,140 420,140" className="wind-line" />
<path d="M -20,175 L 420,175" className="wind-line-slow" />
</g>
{/* 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>
<g transform="translate(240, 100)">
<circle cx="20" cy="-25" r="9" fill="none" stroke="#ef4444" strokeWidth="1.5" strokeDasharray="3 6" className="vortex-spin-cw" />
<circle cx="60" cy="25" r="11" fill="none" stroke="#ef4444" strokeWidth="1.5" strokeDasharray="3 6" className="vortex-spin-ccw" />
</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>
<g className="vibe-cyl">
<circle cx="200" cy="100" r="28" fill="#ef4444" stroke="#b91c1c" strokeWidth="2.5" />
<line x1="200" y1="100" x2="200" y2="40" stroke="#ef4444" strokeWidth="2.5" strokeDasharray="2 4" />
</g>
</svg>
);
}
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>
<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,100 L 420,100" className="wind-line" />
<path d="M -20,170 L 420,170" className="wind-line-slow" />
<circle cx="200" cy="100" r="25" fill="#334155" />
</svg>
);
}
};
// 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 className="flex flex-col gap-4 my-6 bg-slate-900/40 p-4 border border-border/60 rounded-xl">
<div className="flex justify-between items-center border-b border-border/40 pb-2">
<div>
<h3 className="text-sm font-semibold">Túnel de Vento Virtual 2D</h3>
<span className="text-[10px] text-sky-400 font-medium font-mono">{getViewLabel()}</span>
</div>
<span className="text-[9px] bg-sky-500/10 text-sky-400 font-bold border border-sky-500/20 px-2 py-0.5 rounded-full">SVG Animado</span>
</div>
<div className="relative aspect-video w-full bg-slate-950 border border-border/80 rounded-lg overflow-hidden shadow-inner">
{renderActiveSimulation()}
</div>
{/* Caixa de Legenda Didática Expandida */}
<div className="bg-slate-950/60 p-3 rounded-lg border border-border/40 text-[11px] text-muted-foreground flex flex-col gap-2">
<span className="font-bold text-xs text-foreground uppercase tracking-wider">Legenda do Escoamento:</span>
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
<div className="flex items-center gap-2">
<span className="w-6 h-0.5 bg-blue-500 inline-block border-b border-dashed" />
<span>Linhas de Corrente de Vento</span>
</div>
<div className="flex items-center gap-2">
<span className="w-6 h-0.5 bg-sky-400 inline-block" />
<span>Fluxo Acelerado (Alta Vel. / Sucção)</span>
</div>
<div className="flex items-center gap-2">
<span className="w-6 h-0.5 bg-red-500 inline-block border-b border-dotted" />
<span>Esteira Turbulenta / Recirculação</span>
</div>
<div className="flex items-center gap-2">
<span className="w-4 h-4 rounded-full border border-dashed border-red-500 inline-block" />
<span>Vórtices de Desprendimento</span>
</div>
{/* Legenda de Pressão Interna */}
<div className="flex items-center gap-2">
<span className="text-orange-500 text-xs font-bold font-sans"></span>
<span>Seta Laranja: Pressão Interna (+Cpi)</span>
</div>
<div className="flex items-center gap-2">
<span className="text-blue-500 text-xs font-bold font-sans"></span>
<span>Seta Azul Escura: Sucção Interna (-Cpi)</span>
</div>
</div>
</div>
{renderActiveSimulation()}
{renderComparisonGrid()}
</div>
);
}
+1 -1
View File
@@ -142,7 +142,7 @@ const CylinderModule: React.FC = () => {
</Badge>
</div>
<div className="absolute top-4 right-4 z-10">
<EducationalManual type="cylinder" params={{ roughness: surface }} />
<EducationalManual type="cylinder" params={{ surface, endType, cpi: result.cpi }} />
</div>
<div className="w-full h-full bg-muted/20">
<Cylinder3DViewer
+1 -1
View File
@@ -120,7 +120,7 @@ const DomeModule: React.FC = () => {
</Badge>
</div>
<div className="absolute top-4 right-4 z-10">
<EducationalManual type="dome" params={{ rise, diameter }} />
<EducationalManual type="dome" params={{ rise, diameter, cpi }} />
</div>
<div className="w-full h-full bg-muted/20">
<Dome3DViewer
+1 -1
View File
@@ -128,7 +128,7 @@ const VaultModule: React.FC = () => {
</Badge>
</div>
<div className="absolute top-4 right-4 z-10">
<EducationalManual type="vault" params={{ rise, span }} />
<EducationalManual type="vault" params={{ rise, span, regime, localCpi }} />
</div>
<div className="w-full h-full bg-muted/20">
<Vault3DViewer span={span} length={length} rise={rise} cpi={localCpi} cpeProfile={{ ...result.windPerpendicular }} />