436 lines
23 KiB
TypeScript
436 lines
23 KiB
TypeScript
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; |