feat: unified 0 and 90 degree PDF envelope and category descriptors
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user