feat: unified 0 and 90 degree PDF envelope and category descriptors

This commit is contained in:
2026-07-08 19:52:34 +00:00
commit 9fece3f174
170 changed files with 27177 additions and 0 deletions
+390
View File
@@ -0,0 +1,390 @@
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 { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
const DynamicsModule: React.FC = () => {
const { v0, s1, s3, terrainCategory, structureClass, q } = useWindStore();
const [activeTab, setActiveTab] = useState('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' },
],
},
];
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">
<CardTitle className="text-lg">Resposta Dinâmica</CardTitle>
<CardDescription>Sec. 9.3 / Tab. 31 e 32</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<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">
<CardTitle className="text-base">Visualização 3D</CardTitle>
</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;