Files
BrainWind/app/src/pages/DynamicsModule.tsx
T

436 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useMemo, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Slider } from '@/components/ui/slider';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { Input } from '@/components/ui/input';
import { useWindStore } from '@/store/appStore';
import {
getDynamicParams,
estimateFundamentalFrequency,
calculateVp,
dynamicFactor,
getStrouhalNumber,
criticalVelocity,
vortexDispenseCheck,
scrutonNumber,
isVortexSusceptible,
evaluateComfort,
maxAcceleration,
type StructureDynamicType,
type SectionShape,
} from '@/lib/modules/dynamics';
import { calculateS2 } from '@/lib/wind-kernel';
import { TABLE_32 } from '@/lib/nbr-tables/table-32';
import Dynamics3DViewer from '@/components/three/Dynamics3D';
import SceneCapturePanel from '../components/SceneCapturePanel';
import ExportMenu from '../components/ExportMenu';
import { EducationalManual } from '@/components/EducationalManual';
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
import { WindParametersSummary } from '@/components/WindParametersSummary';
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
import { usePersistentState } from '@/hooks/usePersistentState';
const DynamicsModule: React.FC = () => {
const { v0, s1, s3, terrainCategory, structureClass, q } = useWindStore();
const [activeTab, setActiveTab] = usePersistentState('DynamicsModule_activeTab', 'edificio');
const [structureType, setStructureType] = React.useState<StructureDynamicType>('concrete-shearwall');
const [height, setHeight] = React.useState(40);
const [freq, setFreq] = React.useState(0.5);
const [uMax, setUMax] = React.useState(0.01);
const [useType, setUseType] = React.useState<'residential' | 'commercial'>('commercial');
const [sectionShape, setSectionShape] = React.useState<SectionShape>('rectangle-b-a-1-2');
const [sectionRatio, setSectionRatio] = React.useState(1);
const [sectionSize, setSectionSize] = React.useState(0.5);
const [massEq, setMassEq] = React.useState(2000);
const dynParams = useMemo(() => getDynamicParams(structureType), [structureType]);
const estimatedFreq = useMemo(() => estimateFundamentalFrequency(structureType, height), [structureType, height]);
const vp = useMemo(() => calculateVp(v0, s3), [v0, s3]);
const s2 = useMemo(() => calculateS2(height, terrainCategory, structureClass), [height, terrainCategory, structureClass]);
const dynFactor = useMemo(
() =>
dynamicFactor({
category: terrainCategory,
vp,
freq,
height,
xi: dynParams.xiPercent,
}),
[terrainCategory, vp, freq, height, dynParams.xiPercent],
);
const st = useMemo(() => getStrouhalNumber(sectionShape, sectionRatio), [sectionShape, sectionRatio]);
const vcr = useMemo(() => criticalVelocity(freq, sectionSize, st), [freq, sectionSize, st]);
const scrutin = useMemo(() => scrutonNumber(dynParams.xiPercent / 100, massEq, 1.226, sectionSize), [dynParams.xiPercent, massEq, sectionSize]);
const okVortex = useMemo(() => vortexDispenseCheck(vcr, v0, s1, s2, s3), [vcr, v0, s1, s2, s3]);
const aMax = useMemo(() => maxAcceleration(freq, uMax), [freq, uMax]);
const comfort = useMemo(() => evaluateComfort({ freq, aMax, use: useType }), [freq, aMax, useType]);
const handleExportPDF = () => {
const sections: GenericPDFSection[] = [
{
title: 'Geometria e Parâmetros Base (Dinâmica)',
type: 'grid',
gridItems: [
{ label: 'Tipo', value: structureType },
{ label: 'Altura (h)', value: `${height} m` },
{ label: 'Frequência (f)', value: `${freq.toFixed(2)} Hz` },
{ label: 'Freq. Estimada (Tab. 31)', value: `${estimatedFreq.toFixed(2)} Hz` },
{ label: 'γ (Gama)', value: dynParams.gamma.toString() },
{ label: 'ξ (Amortecimento)', value: `${dynParams.xiPercent}%` },
],
},
{
title: 'Resposta Dinâmica (Sec. 9.3)',
type: 'grid',
gridItems: [
{ label: 'Vₚ', value: `${vp.toFixed(2)} m/s` },
{ label: 'Fator ζ', value: dynFactor.toFixed(3) },
{ label: `S₂(z=${height}m)`, value: s2.toFixed(3) },
{ label: 'q₀', value: `${q.toFixed(4)} kN/m²` },
],
},
{
title: 'Desprendimento de Vórtices (Sec. 10)',
type: 'grid',
gridItems: [
{ label: 'Forma da seção', value: sectionShape },
{ label: 'Strouhal (St)', value: st.toFixed(3) },
{ label: 'Vcr (Crítica)', value: `${vcr.toFixed(2)} m/s` },
{ label: 'Scruton (Sc)', value: scrutin.toFixed(1) },
{ label: 'Susceptível', value: isVortexSusceptible(scrutin) ? 'Sim' : 'Não' },
{ label: 'Dispensa (Sec 10.2)', value: okVortex ? 'Sim (OK)' : 'Não (REVER)' },
],
},
{
title: 'Conforto Humano (ISO 10137)',
type: 'grid',
gridItems: [
{ label: 'Uso', value: useType === 'residential' ? 'Residencial' : 'Comercial' },
{ label: 'Desloc. uₘₐₓ', value: `${(uMax * 1000).toFixed(2)} mm` },
{ label: 'a_max (Pico)', value: `${aMax.toFixed(4)} m/s²` },
{ label: 'a_lim (Limite)', value: `${comfort.aLim.toFixed(4)} m/s²` },
{ label: 'Status', value: comfort.ok ? 'OK' : 'REVER' },
],
},
{
title: 'Demonstração dos Passos Matemáticos — Análise Dinâmica e Vórtices',
type: 'math-card',
mathSteps: [
{
title: '1. Fator de Amplificação Dinâmica (ξ) ou Coeficiente de Rajada',
formula: 'ξ = f(f₁, T₀, β, amortecimento) (NBR 6123 Sec. 9 / Anexo A)',
calculation: `Para frequência f₁ = ${freq} Hz e amortecimento ζ = ${dynParams.xiPercent}%, fator de resposta dinâmica calculado = ${dynFactor.toFixed(3)}`,
result: `ξ = ${dynFactor.toFixed(3)}`,
note: 'Amplifica as cargas estáticas para levar em conta a ressonância gerada pela turbulência atmosférica.',
},
{
title: '2. Velocidade Crítica de Desprendimento de Vórtices (Vcr)',
formula: 'Vcr = (f₁ · L) / St [m/s] (Sec. 10 / Tabela 31 NBR 6123)',
calculation: `(${freq} Hz · ${sectionSize} m) / ${st.toFixed(3)} (Strouhal) = ${vcr.toFixed(2)} m/s`,
result: `Vcr = ${vcr.toFixed(2)} m/s`,
note: 'Velocidade de vento na qual a frequência de desprendimento de vórtices coincide com a frequência própria da estrutura.',
},
{
title: '3. Número de Scruton (Sc) e Suscetibilidade Aerodinâmica',
formula: 'Sc = (2 · me · δ) / (ρ · L²) (Análise de amortecimento aerodinâmico)',
calculation: `Com massa equivalente ${massEq} kg/m e amortecimento, Scruton calculado = ${scrutin.toFixed(1)}`,
result: `Sc = ${scrutin.toFixed(1)}`,
note: 'Estruturas com baixo Scruton (geralmente < 15) são suscetíveis a fortes vibrações transversais por vórtices.',
},
{
title: '4. Aceleração de Pico no Topo (a_max) vs Conforto Humano',
formula: 'a_max = (2π · f₁)² · u_max [m/s²] (ISO 10137 / NBR 6123 Anexo A)',
calculation: `Aceleração de pico = ${aMax.toFixed(4)} m/s² vs Limite aceitável = ${comfort.aLim.toFixed(4)} m/s²`,
result: `a_max = ${aMax.toFixed(4)} m/s² (${comfort.ok ? 'OK' : 'REVER'})`,
note: 'Verificação do bem-estar dos ocupantes em edifícios altos submetidos a vibrações induzidas pelo vento.',
},
],
},
];
exportGenericToPDF('Análise Dinâmica', sections);
};
return (
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30 overflow-y-auto">
{/* Coluna Esquerda: Controles */}
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-3 mb-4">
<TabsTrigger value="edificio">Edifício</TabsTrigger>
<TabsTrigger value="vortex">Vórtices</TabsTrigger>
<TabsTrigger value="conforto">Conforto</TabsTrigger>
</TabsList>
<TabsContent value="edificio" className="space-y-4">
<Card>
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
<div>
<CardTitle className="text-lg">Análise Dinâmica Contínua</CardTitle>
<CardDescription>Sec. 9.3 / Tab. 31 e 32</CardDescription>
</div>
<SaveModuleDialog
moduleType="dynamics"
inputs={{ structureType, height, freq, uMax, useType, sectionShape, sectionRatio, sectionSize, massEq }}
/>
</CardHeader>
<CardContent className="space-y-4">
<WindParametersSummary />
<div className="space-y-2">
<label className="text-sm font-medium">Tipo de Estrutura</label>
<Select value={structureType} onValueChange={(v) => setStructureType(v as StructureDynamicType)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="portal-concrete">Aporticada concreto (γ=1,2; ξ=2%)</SelectItem>
<SelectItem value="concrete-shearwall">Concreto c/ cortina (γ=1,0; ξ=2%)</SelectItem>
<SelectItem value="concrete-tower-variable">Torre concreto variável (γ=2,7; ξ=1,5%)</SelectItem>
<SelectItem value="concrete-tower-uniform">Torre concreto uniforme (γ=1,7; ξ=1%)</SelectItem>
<SelectItem value="steel-welded">Aço soldada (γ=1,2; ξ=1%)</SelectItem>
<SelectItem value="steel-tower-uniform">Torre aço uniforme (γ=1,7; ξ=0,8%)</SelectItem>
<SelectItem value="wood">Madeira (γ=; ξ=3%)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-3">
<div className="flex justify-between"><label className="text-sm font-medium">Altura h</label><span className="font-mono text-sm">{height} m</span></div>
<Slider min={10} max={200} step={1} value={[height]} onValueChange={(v) => setHeight(v[0])} />
</div>
<div className="space-y-3">
<div className="flex justify-between"><label className="text-sm font-medium">Frequência f</label><span className="font-mono text-sm">{freq.toFixed(2)} Hz</span></div>
<Slider min={0.1} max={2} step={0.01} value={[freq]} onValueChange={(v) => setFreq(v[0])} />
</div>
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-1">
<div className="flex justify-between"><span>f estimado (Tab. 31):</span><span className="font-mono">{estimatedFreq.toFixed(2)} Hz</span></div>
<div className="flex justify-between"><span>γ:</span><span className="font-mono">{dynParams.gamma}</span></div>
<div className="flex justify-between"><span>ξ:</span><span className="font-mono">{dynParams.xiPercent}%</span></div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="vortex" className="space-y-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg">Desprendimento de Vórtices</CardTitle>
<CardDescription>Sec. 10 Vcr, Scruton, St</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">Forma da Seção</label>
<Select value={sectionShape} onValueChange={(v) => setSectionShape(v as SectionShape)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="circle">Circular (St=0,20)</SelectItem>
<SelectItem value="rectangle-b-a-1-3">Retângulo b/a=1/3</SelectItem>
<SelectItem value="rectangle-b-a-1-2">Retângulo b/a=1/2</SelectItem>
<SelectItem value="rectangle-b-a-1-1">Retângulo b/a=1</SelectItem>
<SelectItem value="rectangle-b-a-1-0-5">Retângulo b/a=2</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-3">
<div className="flex justify-between"><label className="text-sm font-medium">b/a</label><span className="font-mono text-sm">{sectionRatio.toFixed(2)}</span></div>
<Slider min={0.1} max={5} step={0.05} value={[sectionRatio]} onValueChange={(v) => setSectionRatio(v[0])} />
</div>
<div className="space-y-3">
<div className="flex justify-between"><label className="text-sm font-medium">Dim. L</label><span className="font-mono text-sm">{sectionSize} m</span></div>
<Slider min={0.1} max={5} step={0.1} value={[sectionSize]} onValueChange={(v) => setSectionSize(v[0])} />
</div>
<div className="space-y-3">
<label className="text-sm font-medium">m_eq (kg/m)</label>
<Input type="number" value={massEq} onChange={(e) => setMassEq(Number(e.target.value))} />
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="conforto" className="space-y-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg">Conforto Humano</CardTitle>
<CardDescription>Sec. 9.6 ISO 10137</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">Tipo de Uso</label>
<Select value={useType} onValueChange={(v) => setUseType(v as 'residential' | 'commercial')}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="commercial">Comercial / Escritórios</SelectItem>
<SelectItem value="residential">Residencial</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-3">
<div className="flex justify-between"><label className="text-sm font-medium">Desloc. uₘₐₓ</label><span className="font-mono text-sm">{(uMax * 1000).toFixed(2)} mm</span></div>
<Slider min={0.001} max={0.1} step={0.001} value={[uMax]} onValueChange={(v) => setUMax(v[0])} />
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
{/* Coluna Central: Gráficos e 3D */}
<div className="flex-1 flex flex-col gap-4">
{activeTab === 'edificio' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">Perfil de Velocidade</CardTitle>
</CardHeader>
<CardContent>
<DynamicProfileChart q={q} height={height} category={terrainCategory} />
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">Fator Dinâmico ζ</CardTitle>
</CardHeader>
<CardContent>
<DynamicFactorChart category={terrainCategory} height={height} vp={vp} freq={freq} />
</CardContent>
</Card>
</div>
)}
<Card className="flex-1 min-h-[400px]">
<CardHeader className="pb-3 flex flex-row items-center justify-between">
<CardTitle className="text-base">Visualização 3D</CardTitle>
<EducationalManual type="dynamics" params={{ fn: freq }} />
</CardHeader>
<CardContent className="h-full">
<Dynamics3DViewer
height={height}
freq={freq}
windSpeed={v0 * s1 * s2 * s3}
scruton={scrutin}
sectionShape={sectionShape}
sectionSize={sectionSize}
showVortexStreet={activeTab === 'vortex'}
showModeShape={true}
/>
</CardContent>
</Card>
</div>
{/* Coluna Direita: Resultados */}
<div className="w-full lg:w-96 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
<Tabs value={activeTab} className="w-full">
<TabsList className="hidden" />
<TabsContent value="edificio">
<Card className="shadow-sm border-border">
<CardHeader className="pb-3">
<CardTitle className="text-lg">Parâmetros Dinâmicos</CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-2">
<div className="flex justify-between"><span>Vₚ:</span><span className="font-mono">{vp.toFixed(2)} m/s</span></div>
<div className="flex justify-between"><span>ζ (fator):</span><span className="font-mono">{dynFactor.toFixed(3)}</span></div>
<div className="flex justify-between"><span>S(z={height}m):</span><span className="font-mono">{s2.toFixed(3)}</span></div>
<div className="flex justify-between"><span>q:</span><span className="font-mono">{q.toFixed(4)} kN/m²</span></div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="vortex">
<Card className="shadow-sm border-border">
<CardHeader className="pb-3">
<CardTitle className="text-lg">Análise de Vórtices</CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-2">
<div className="flex justify-between"><span>Strouhal (St):</span><span className="font-mono">{st.toFixed(3)}</span></div>
<div className="flex justify-between"><span>Vcr (Crítica):</span><span className="font-mono font-semibold">{vcr.toFixed(2)} m/s</span></div>
<div className="flex justify-between"><span>Scruton (Sc):</span><span className="font-mono">{scrutin.toFixed(1)}</span></div>
<div className="flex justify-between"><span>Susceptível:</span><span className="font-mono">{isVortexSusceptible(scrutin) ? 'sim' : 'não'}</span></div>
<Separator className="my-2" />
<div className="flex justify-between items-center">
<span>Dispensa (sec 10.2):</span>
<Badge variant={okVortex ? 'default' : 'destructive'}>{okVortex ? 'OK' : 'REVER'}</Badge>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="conforto">
<Card className="shadow-sm border-border">
<CardHeader className="pb-3">
<CardTitle className="text-lg">Verificação de Conforto</CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-2">
<div className="flex justify-between"><span>a_max (Pico):</span><span className="font-mono">{aMax.toFixed(4)} m/s²</span></div>
<div className="flex justify-between"><span>a_lim (Limite):</span><span className="font-mono">{comfort.aLim.toFixed(4)} m/s²</span></div>
<div className="flex justify-between"><span>a / a_lim:</span><span className="font-mono">{comfort.ratio.toFixed(2)}</span></div>
<Separator className="my-2" />
<div className="flex justify-between items-center">
<span>Status:</span>
<Badge variant={comfort.ok ? 'default' : 'destructive'}>{comfort.ok ? 'OK' : 'REVER'}</Badge>
</div>
<p className="text-muted-foreground mt-2">{comfort.description}</p>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
<ExportMenu onExportPDF={handleExportPDF} />
</div>
<SceneCapturePanel />
</div>
</div>
);
};
const DynamicProfileChart: React.FC<{ q: number; height: number; category: 'I' | 'II' | 'III' | 'IV' | 'V' }> = ({ q, height, category }) => {
const points = [];
const { bm, p } = TABLE_32[category];
for (let z = 0; z <= height; z += height / 30) {
const factor = bm * Math.pow(z / height, p);
points.push({ z, value: q * factor });
}
const maxValue = Math.max(...points.map((p) => p.value));
const pathData = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${(p.z / height) * 300},${160 - (p.value / maxValue) * 140}`).join(' ');
return (
<svg viewBox="0 0 320 200" className="w-full h-48">
<line x1="0" y1="160" x2="300" y2="160" stroke="var(--color-border)" strokeWidth="1" />
<line x1="0" y1="0" x2="0" y2="160" stroke="var(--color-border)" strokeWidth="1" />
<path d={pathData} fill="none" stroke="var(--color-primary)" strokeWidth="2" />
<text x="150" y="190" textAnchor="middle" fontSize="11" fill="var(--color-muted-foreground)">z (m) {height.toFixed(0)} m</text>
<text x="10" y="20" fontSize="11" fill="var(--color-muted-foreground)">q(z) (kN/m²)</text>
</svg>
);
};
const DynamicFactorChart: React.FC<{ category: 'I' | 'II' | 'III' | 'IV' | 'V'; height: number; vp: number; freq: number }> = ({ category, height, vp, freq }) => {
const points = [];
for (let xi = 0.5; xi <= 5; xi += 0.5) {
const factor = dynamicFactor({ category, vp, freq, height, xi });
points.push({ xi, factor });
}
const maxFactor = Math.max(...points.map((p) => p.factor), 5);
const pathData = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${((p.xi - 0.5) / 4.5) * 300},${160 - (p.factor / maxFactor) * 140}`).join(' ');
return (
<svg viewBox="0 0 320 200" className="w-full h-48">
<line x1="0" y1="160" x2="300" y2="160" stroke="var(--color-border)" strokeWidth="1" />
<line x1="1" y1="0" x2="1" y2="160" stroke="var(--color-border)" strokeWidth="1" />
<line x1="0" y1={160 - 140 * 1 / maxFactor} x2="300" y2={160 - 140 * 1 / maxFactor} stroke="var(--color-border)" strokeDasharray="4" opacity="0.5" />
<path d={pathData} fill="none" stroke="var(--color-primary)" strokeWidth="2" />
<text x="150" y="190" textAnchor="middle" fontSize="11" fill="var(--color-muted-foreground)">ξ (%)</text>
<text x="10" y="20" fontSize="11" fill="var(--color-muted-foreground)">ζ</text>
</svg>
);
};
export default DynamicsModule;