feat: unified 0 and 90 degree PDF envelope and category descriptors
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
import React, { useMemo } 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 { useWindStore } from '@/store/appStore';
|
||||
import { calculateFlatBarForce, type FlatBarSection } from '@/lib/nbr-tables/table-26';
|
||||
import { calculateCircleBarForce, reynoldsBar } from '@/lib/nbr-tables/table-27';
|
||||
import Bar3DViewer from '@/components/three/Bar3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const BarSelectorModule: React.FC = () => {
|
||||
const { vk, q } = useWindStore();
|
||||
const [barType, setBarType] = React.useState<'flat' | 'circular'>('flat');
|
||||
const [section, setSection] = React.useState<FlatBarSection>('l');
|
||||
const [alpha, setAlpha] = React.useState(45);
|
||||
const [width, setWidth] = React.useState(0.1);
|
||||
const [length, setLength] = React.useState(5);
|
||||
const [diameter, setDiameter] = React.useState(0.05);
|
||||
|
||||
const flatResult = useMemo(() => {
|
||||
if (barType !== 'flat') return null;
|
||||
return calculateFlatBarForce({ section, alpha, width, length, q });
|
||||
}, [barType, section, alpha, width, length, q]);
|
||||
|
||||
const circleResult = useMemo(() => {
|
||||
if (barType !== 'circular') return null;
|
||||
return calculateCircleBarForce({ d: diameter, length, vk, q });
|
||||
}, [barType, diameter, length, vk, q]);
|
||||
|
||||
const reCircle = useMemo(() => reynoldsBar(vk, diameter), [vk, diameter]);
|
||||
|
||||
const handleExportPDF = () => {
|
||||
const sections: GenericPDFSection[] = [
|
||||
{
|
||||
title: 'Geometria da Barra',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Tipo', value: barType === 'flat' ? 'Faces planas' : 'Circular' },
|
||||
{ label: 'Comprimento (ℓ)', value: `${length} m` },
|
||||
...(barType === 'flat'
|
||||
? [
|
||||
{ label: 'Seção transversal', value: section },
|
||||
{ label: 'Ângulo (α)', value: `${alpha}°` },
|
||||
{ label: 'Largura (c)', value: `${width} m` },
|
||||
]
|
||||
: [
|
||||
{ label: 'Diâmetro (d)', value: `${diameter} m` },
|
||||
{ label: 'Reynolds (Re)', value: reCircle.toExponential(2) },
|
||||
{
|
||||
label: 'Regime',
|
||||
value: reCircle < 4.2e5 ? 'Subcrítico' : reCircle < 2.3e6 ? 'Crítico' : 'Supercrítico',
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Forças e Coeficientes',
|
||||
type: 'grid',
|
||||
gridItems:
|
||||
barType === 'flat' && flatResult
|
||||
? [
|
||||
{ label: 'Coeficiente Cx', value: flatResult.cx.toFixed(2) },
|
||||
{ label: 'Coeficiente Cy', value: flatResult.cy.toFixed(2) },
|
||||
{ label: 'Fator K', value: flatResult.kFactor.toFixed(2) },
|
||||
{ label: 'Força Fx', value: `${flatResult.fxKN.toFixed(3)} kN` },
|
||||
{ label: 'Força Fy', value: `${flatResult.fyKN.toFixed(3)} kN` },
|
||||
]
|
||||
: circleResult
|
||||
? [
|
||||
{ label: 'Coeficiente Ca', value: circleResult.ca.toFixed(2) },
|
||||
{ label: 'Regime (tabela)', value: circleResult.regime },
|
||||
{ label: 'Fator K', value: circleResult.kFactor.toFixed(2) },
|
||||
{ label: 'Força de Arrasto', value: `${circleResult.forceKN.toFixed(3)} kN` },
|
||||
]
|
||||
: [],
|
||||
},
|
||||
];
|
||||
exportGenericToPDF('Barra Prismática', sections);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Barra Prismática</CardTitle>
|
||||
<CardDescription>Sec. 8.1 — faces planas (Tab. 26) ou circulares (Tab. 27).</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Tipo de Barra</label>
|
||||
<Select value={barType} onValueChange={(v) => setBarType(v as 'flat' | 'circular')}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="flat">Faces planas</SelectItem>
|
||||
<SelectItem value="circular">Circular</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{barType === 'flat' && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Seção Transversal</label>
|
||||
<Select value={section} onValueChange={(v) => setSection(v as FlatBarSection)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="placa">Placa</SelectItem>
|
||||
<SelectItem value="l">Perfil L (cantoneira)</SelectItem>
|
||||
<SelectItem value="t">Perfil T</SelectItem>
|
||||
<SelectItem value="i">Perfil I</SelectItem>
|
||||
<SelectItem value="rectangle">Retângulo</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Ângulo α</label><span className="font-mono text-sm">{alpha}°</span></div>
|
||||
<Slider min={0} max={180} step={5} value={[alpha]} onValueChange={(v) => setAlpha(v[0])} />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Largura c</label><span className="font-mono text-sm">{width} m</span></div>
|
||||
<Slider min={0.05} max={1} step={0.01} value={[width]} onValueChange={(v) => setWidth(v[0])} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{barType === 'circular' && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Diâmetro d</label><span className="font-mono text-sm">{diameter} m</span></div>
|
||||
<Slider min={0.01} max={0.5} step={0.01} value={[diameter]} onValueChange={(v) => setDiameter(v[0])} />
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-xs">
|
||||
Re = {reCircle.toLocaleString('pt-BR')} · regime: {reCircle < 4.2e5 ? 'subcrítico' : reCircle < 2.3e6 ? 'crítico' : 'supercrítico'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Comprimento ℓ</label><span className="font-mono text-sm">{length} m</span></div>
|
||||
<Slider min={1} max={30} step={0.5} value={[length]} onValueChange={(v) => setLength(v[0])} />
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-1">
|
||||
<div className="flex justify-between"><span>Vₖ:</span><span className="font-mono">{vk.toFixed(2)} m/s</span></div>
|
||||
<div className="flex justify-between"><span>q:</span><span className="font-mono">{q.toFixed(4)} kN/m²</span></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Coluna Central: 3D */}
|
||||
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
||||
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">α = {alpha}°</Badge>
|
||||
</div>
|
||||
<Bar3DViewer
|
||||
barType={barType}
|
||||
section={barType === 'flat' ? section : undefined}
|
||||
diameter={barType === 'circular' ? diameter : undefined}
|
||||
width={barType === 'flat' ? width : undefined}
|
||||
length={length}
|
||||
alpha={alpha}
|
||||
fxKN={flatResult?.fxKN ?? 0}
|
||||
fyKN={flatResult?.fyKN ?? 0}
|
||||
cx={flatResult?.cx ?? 0}
|
||||
/>
|
||||
</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">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Forças Cx e Cy</CardTitle>
|
||||
<CardDescription>Com fator K (Tab. 28) aplicado</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{flatResult && (
|
||||
<>
|
||||
<div className="grid grid-cols-3 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Cx</div>
|
||||
<div className="font-mono font-medium">{flatResult.cx.toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Cy</div>
|
||||
<div className="font-mono font-medium">{flatResult.cy.toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">K</div>
|
||||
<div className="font-mono font-medium">{flatResult.kFactor.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Fx</div>
|
||||
<div className="font-mono font-medium">{flatResult.fxKN.toFixed(3)} kN</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Fy</div>
|
||||
<div className="font-mono font-medium">{flatResult.fyKN.toFixed(3)} kN</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{circleResult && (
|
||||
<>
|
||||
<div className="grid grid-cols-3 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Ca</div>
|
||||
<div className="font-mono font-medium">{circleResult.ca.toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Regime</div>
|
||||
<div className="font-mono font-medium text-xs">{circleResult.regime}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">K</div>
|
||||
<div className="font-mono font-medium">{circleResult.kFactor.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Força de arrasto</div>
|
||||
<div className="font-mono font-medium text-lg">{circleResult.forceKN.toFixed(3)} kN</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
</div>
|
||||
|
||||
<SceneCapturePanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BarSelectorModule;
|
||||
@@ -0,0 +1,253 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useWindStore } from '@/store/appStore';
|
||||
import { classifyBridge, calculateBridgeDeckForces, flutterCheck, gallopingCheck } from '@/lib/modules/bridge';
|
||||
import Bridge3DViewer from '@/components/three/Bridge3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const BridgeModule: React.FC = () => {
|
||||
const { v0, s1, terrainCategory, q } = useWindStore();
|
||||
const [lp, setLp] = React.useState(120);
|
||||
const [width, setWidth] = React.useState(14);
|
||||
const [deckHeight, setDeckHeight] = React.useState(15);
|
||||
const [heg, setHeg] = React.useState(2.5);
|
||||
const [mass, setMass] = React.useState(18000);
|
||||
const [fv, setFv] = React.useState(0.6);
|
||||
const [vk, setVk] = React.useState(45);
|
||||
const [alpha, setAlpha] = React.useState(0);
|
||||
const [vf, setVf] = React.useState(100); // Velocidade crítica de flutter da seção
|
||||
|
||||
const classification = useMemo(
|
||||
() => classifyBridge({ lp, width, massPerLength: mass, fv, v0, s1, deckHeight, category: terrainCategory }),
|
||||
[lp, width, mass, fv, v0, s1, deckHeight, terrainCategory],
|
||||
);
|
||||
|
||||
const forces = useMemo(() => calculateBridgeDeckForces({ width, heg: heg, vk, q, alpha }), [width, heg, vk, q, alpha]);
|
||||
|
||||
const flutter = useMemo(() => flutterCheck(vf, vk), [vf, vk]);
|
||||
const galope = useMemo(() => gallopingCheck(vf, vk), [vf, vk]);
|
||||
|
||||
const classColors: Record<1 | 2 | 3, string> = {
|
||||
1: 'bg-green-100 text-green-700 border-green-300',
|
||||
2: 'bg-yellow-100 text-yellow-700 border-yellow-300',
|
||||
3: 'bg-red-100 text-red-700 border-red-300',
|
||||
};
|
||||
|
||||
const handleExportPDF = () => {
|
||||
const sections: GenericPDFSection[] = [
|
||||
{
|
||||
title: 'Parâmetros da Ponte',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Maior vão (Lₚ)', value: `${lp} m` },
|
||||
{ label: 'Largura (B)', value: `${width} m` },
|
||||
{ label: 'Altura equivalente (Hₑg)', value: `${heg} m` },
|
||||
{ label: 'Altura do tabuleiro (z)', value: `${deckHeight} m` },
|
||||
{ label: 'Massa linear (m)', value: `${mass} kg/m` },
|
||||
{ label: 'Velocidade crítica (V_it)', value: `${classification.vit.toFixed(2)} m/s` },
|
||||
{ label: 'Ângulo de Ataque (α)', value: `${alpha}°` },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Classificação (Pₛₑ)',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Classe', value: `Classe ${classification.bridgeClass}` },
|
||||
{ label: 'Pₛₑ', value: classification.pse.toFixed(4) },
|
||||
{ label: 'Ação sugerida', value: classification.description },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Forças no Tabuleiro',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Cx / Cz / Cm', value: `${forces.cx.toFixed(2)} / ${forces.cz.toFixed(2)} / ${forces.cm.toFixed(2)}` },
|
||||
{ label: 'Fx (kN/m)', value: forces.fxPerLength.toFixed(3) },
|
||||
{ label: 'Fz (kN/m)', value: forces.fzPerLength.toFixed(3) },
|
||||
{ label: 'Mt (kNm/m)', value: forces.fmPerLength.toFixed(3) },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Verificações de Estabilidade',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Flutter (V_F vs 2Vk)', value: `${flutter.vf.toFixed(1)} > ${flutter.vkCrit.toFixed(1)} m/s → ${flutter.ok ? 'OK' : 'REVER'}` },
|
||||
{ label: 'Galope (V_F vs 1.25Vk)', value: `${galope.vf.toFixed(1)} > ${galope.vkCrit.toFixed(1)} m/s → ${galope.ok ? 'OK' : 'REVER'}` },
|
||||
],
|
||||
},
|
||||
];
|
||||
exportGenericToPDF('Tabuleiro de Ponte', sections);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Tabuleiro de Ponte</CardTitle>
|
||||
<CardDescription>Sec. 11.2 / 11.3 — análise estática.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Maior vão (Lₚ)</label><span className="font-mono text-sm">{lp} m</span></div>
|
||||
<Slider min={20} max={500} step={5} value={[lp]} onValueChange={(v) => setLp(v[0])} />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Largura (B)</label><span className="font-mono text-sm">{width} m</span></div>
|
||||
<Slider min={5} max={30} step={0.5} value={[width]} onValueChange={(v) => setWidth(v[0])} />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Altura equivalente (Hₑg)</label><span className="font-mono text-sm">{heg} m</span></div>
|
||||
<Slider min={0.5} max={5} step={0.1} value={[heg]} onValueChange={(v) => setHeg(v[0])} />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Altura do tabuleiro (z)</label><span className="font-mono text-sm">{deckHeight} m</span></div>
|
||||
<Slider min={5} max={80} step={1} value={[deckHeight]} onValueChange={(v) => setDeckHeight(v[0])} />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">m (kg/m)</label>
|
||||
<Input type="number" value={mass} onChange={(e) => setMass(Number(e.target.value))} />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">fᵥ (Hz)</label>
|
||||
<Input type="number" step="0.01" value={fv} onChange={(e) => setFv(Number(e.target.value))} />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">Vₖ(z) — projeto (m/s)</label>
|
||||
<Input type="number" value={vk} onChange={(e) => setVk(Number(e.target.value))} />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Ângulo de ataque (α)</label><span className="font-mono text-sm">{alpha}°</span></div>
|
||||
<Slider min={-5} max={5} step={0.5} value={[alpha]} onValueChange={(v) => setAlpha(v[0])} />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">Velocidade Crítica (V_F) aerodinâmica (m/s)</label>
|
||||
<Input type="number" value={vf} onChange={(e) => setVf(Number(e.target.value))} />
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-1">
|
||||
<div className="flex justify-between"><span>Categoria:</span><span className="font-mono">{terrainCategory}</span></div>
|
||||
<div className="flex justify-between"><span>V₀:</span><span className="font-mono">{v0} m/s</span></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Coluna Central: 3D */}
|
||||
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
||||
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">Pₛₑ = {classification.pse.toFixed(3)}</Badge>
|
||||
<Badge variant="outline" className={`bg-background/80 ${classColors[classification.bridgeClass]}`}>
|
||||
Classe {classification.bridgeClass}
|
||||
</Badge>
|
||||
</div>
|
||||
<Bridge3DViewer
|
||||
lp={lp}
|
||||
width={width}
|
||||
deckHeight={deckHeight}
|
||||
heg={heg}
|
||||
cx={forces.cx}
|
||||
cz={forces.cz}
|
||||
fxPerLength={forces.fxPerLength}
|
||||
fzPerLength={forces.fzPerLength}
|
||||
/>
|
||||
</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">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Classificação</CardTitle>
|
||||
<CardDescription>Parâmetro Pₛₑ (sec. 11.2.2)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className={`rounded-md border-2 p-3 text-center ${classColors[classification.bridgeClass]}`}>
|
||||
<div className="text-xs font-medium">Classe {classification.bridgeClass}</div>
|
||||
<div className="font-mono text-2xl font-bold">Pₛₑ = {classification.pse.toFixed(4)}</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{classification.description}</p>
|
||||
<Separator />
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">V_it (m/s)</div>
|
||||
<div className="font-mono font-medium">{classification.vit.toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">q (kN/m²)</div>
|
||||
<div className="font-mono font-medium">{q.toFixed(4)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="text-xs font-semibold uppercase text-muted-foreground mb-2">Forças e Momentos no Tabuleiro (sec. 11.3)</div>
|
||||
<div className="grid grid-cols-3 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Cx</div>
|
||||
<div className="font-mono font-medium">{forces.cx.toFixed(3)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Cz</div>
|
||||
<div className="font-mono font-medium">{forces.cz.toFixed(3)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Cm</div>
|
||||
<div className="font-mono font-medium">{forces.cm.toFixed(3)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 text-sm mt-2">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Fx (kN/m)</div>
|
||||
<div className="font-mono font-medium">{forces.fxPerLength.toFixed(3)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Fz (kN/m)</div>
|
||||
<div className="font-mono font-medium">{forces.fzPerLength.toFixed(3)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Mt (kNm/m)</div>
|
||||
<div className="font-mono font-medium">{forces.fmPerLength.toFixed(3)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="text-xs font-semibold uppercase text-muted-foreground mb-2">Verificações de estabilidade</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col gap-1 text-xs">
|
||||
<div className="flex justify-between items-center">
|
||||
<span>Flutter (11.5.4):</span>
|
||||
<Badge variant={flutter.ok ? 'default' : 'destructive'}>{flutter.ok ? 'OK' : 'REVER'}</Badge>
|
||||
</div>
|
||||
<div className="text-muted-foreground ml-1">
|
||||
V_F = {flutter.vf.toFixed(1)} {flutter.ok ? '>' : '<'} 2.0·Vₖ = {flutter.vkCrit.toFixed(1)} m/s
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 text-xs">
|
||||
<div className="flex justify-between items-center">
|
||||
<span>Galope (11.5.6):</span>
|
||||
<Badge variant={galope.ok ? 'default' : 'destructive'}>{galope.ok ? 'OK' : 'REVER'}</Badge>
|
||||
</div>
|
||||
<div className="text-muted-foreground ml-1">
|
||||
V_F = {galope.vf.toFixed(1)} {galope.ok ? '>' : '<'} 1.25·Vₖ = {galope.vkCrit.toFixed(1)} m/s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
</div>
|
||||
|
||||
<SceneCapturePanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BridgeModule;
|
||||
@@ -0,0 +1,199 @@
|
||||
import React, { useMemo } 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 { useWindStore } from '@/store/appStore';
|
||||
import { calculateCylinder, type CylinderEndType } from '@/lib/modules/cylinder';
|
||||
import Cylinder3DViewer from '@/components/three/Cylinder3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const CylinderModule: React.FC = () => {
|
||||
const { v0, terrainCategory, structureClass, s2, vk, q, cpi: globalCpi } = useWindStore();
|
||||
|
||||
const [diameter, setDiameter] = React.useState(8);
|
||||
const [height, setHeight] = React.useState(20);
|
||||
const [surface, setSurface] = React.useState<'rough' | 'smooth'>('rough');
|
||||
const [endType, setEndType] = React.useState<CylinderEndType>('closed');
|
||||
|
||||
const result = useMemo(() => {
|
||||
return calculateCylinder({
|
||||
d: diameter,
|
||||
h: height,
|
||||
vk,
|
||||
surface,
|
||||
endType,
|
||||
baseCpi: globalCpi,
|
||||
});
|
||||
}, [diameter, height, vk, surface, endType, globalCpi]);
|
||||
|
||||
const handleExportPDF = () => {
|
||||
const sections: GenericPDFSection[] = [
|
||||
{
|
||||
title: 'Geometria do Cilindro',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Diâmetro (d)', value: `${diameter} m` },
|
||||
{ label: 'Altura (h)', value: `${height} m` },
|
||||
{ label: 'h/d', value: result.hOverD.toFixed(2) },
|
||||
{ label: 'Tipo de Superfície', value: surface === 'rough' ? 'Rugosa' : 'Lisa' },
|
||||
{ label: 'Extremidades', value: endType },
|
||||
{ label: 'Reynolds (Re)', value: result.re.toExponential(2) },
|
||||
{ label: 'Regime', value: result.supercritical ? 'Supercrítico' : 'Subcrítico' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Pressões na Superfície',
|
||||
type: 'table',
|
||||
tableHeaders: ['Ângulo', 'Cpe', 'p (kN/m²)'],
|
||||
tableRows: result.profile.map((p) => [
|
||||
`${p.angle}°`,
|
||||
p.cpe.toFixed(2),
|
||||
p.pressureKN_m2.toFixed(3),
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: 'Força Resultante',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{
|
||||
label: 'Força horizontal por unidade de altura',
|
||||
value: `${result.forcePerHeightKN_m.toFixed(2)} kN/m`,
|
||||
},
|
||||
{ label: 'Nota Cpi', value: result.cpiNote },
|
||||
],
|
||||
},
|
||||
];
|
||||
exportGenericToPDF('Cilindros', sections);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Cilindro Vertical</CardTitle>
|
||||
<CardDescription>Sec. 6.2.1 — silos, reservatórios, chaminés.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<label className="text-sm font-medium">Diâmetro (d)</label>
|
||||
<span className="font-mono text-sm text-muted-foreground">{diameter} m</span>
|
||||
</div>
|
||||
<Slider min={2} max={30} step={0.5} value={[diameter]} onValueChange={(v) => setDiameter(v[0])} />
|
||||
</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 text-muted-foreground">{height} m</span>
|
||||
</div>
|
||||
<Slider min={4} max={80} step={0.5} value={[height]} onValueChange={(v) => setHeight(v[0])} />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Tipo de Superfície</label>
|
||||
<Select value={surface} onValueChange={(v) => setSurface(v as 'rough' | 'smooth')}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="rough">Rugosa (com saliências)</SelectItem>
|
||||
<SelectItem value="smooth">Lisa</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Extremidades</label>
|
||||
<Select value={endType} onValueChange={(v) => setEndType(v as CylinderEndType)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="closed">Fechado</SelectItem>
|
||||
<SelectItem value="open-top">Topo aberto</SelectItem>
|
||||
<SelectItem value="open-bottom">Base aberta</SelectItem>
|
||||
<SelectItem value="open-both">Ambos abertos</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-1">
|
||||
<div className="flex justify-between"><span>V₀:</span><span className="font-mono">{v0} m/s</span></div>
|
||||
<div className="flex justify-between"><span>S₂ (cat. {terrainCategory}, cl. {structureClass}):</span><span className="font-mono">{s2.toFixed(3)}</span></div>
|
||||
<div className="flex justify-between"><span>Vₖ:</span><span className="font-mono">{vk.toFixed(2)} m/s</span></div>
|
||||
<div className="flex justify-between"><span>q:</span><span className="font-mono">{q.toFixed(4)} kN/m²</span></div>
|
||||
<div className="flex justify-between"><span>Reynolds Re:</span><span className="font-mono">{result.re.toLocaleString('pt-BR')}</span></div>
|
||||
<div className="flex justify-between"><span>h/d:</span><span className="font-mono">{result.hOverD.toFixed(2)}</span></div>
|
||||
<div className="flex justify-between"><span>Regime:</span><span className="font-mono">{result.supercritical ? 'supercrítico' : 'subcrítico'}</span></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Coluna Central: 3D */}
|
||||
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
||||
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">
|
||||
h/d = {result.hOverD.toFixed(2)}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">
|
||||
Re = {result.re.toExponential(2)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="w-full h-full bg-muted/20">
|
||||
<Cylinder3DViewer
|
||||
diameter={diameter}
|
||||
height={height}
|
||||
cpeProfile={result.profile}
|
||||
cpi={result.cpi}
|
||||
/>
|
||||
</div>
|
||||
</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">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Pressões na Superfície</CardTitle>
|
||||
<CardDescription>{result.cpiNote}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-muted-foreground border-b">
|
||||
<th className="text-left py-2">Ângulo</th>
|
||||
<th className="text-right py-2">Cpe</th>
|
||||
<th className="text-right py-2">p (kN/m²)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{result.profile.map((p) => (
|
||||
<tr key={p.angle} className="border-b last:border-0">
|
||||
<td className="py-1 font-mono">{p.angle}°</td>
|
||||
<td className="text-right font-mono">{p.cpe.toFixed(2)}</td>
|
||||
<td className="text-right font-mono">{p.pressureKN_m2.toFixed(3)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Separator className="my-3" />
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Força horizontal por unidade de altura:</span>
|
||||
<Badge variant="default" className="font-mono">{result.forcePerHeightKN_m.toFixed(2)} kN/m</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
</div>
|
||||
|
||||
<SceneCapturePanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CylinderModule;
|
||||
@@ -0,0 +1,187 @@
|
||||
import React, { useMemo } 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 { useWindStore } from '@/store/appStore';
|
||||
import { calculateDome, type DomeType } from '@/lib/modules/dome';
|
||||
import Dome3DViewer from '@/components/three/Dome3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const DomeModule: React.FC = () => {
|
||||
const { vk, cpi } = useWindStore();
|
||||
const [diameter, setDiameter] = React.useState(20);
|
||||
const [rise, setRise] = React.useState(5);
|
||||
const [wallHeight, setWallHeight] = React.useState(8);
|
||||
const [type, setType] = React.useState<DomeType>('on-ground');
|
||||
|
||||
const result = useMemo(() => calculateDome({ d: diameter, f: rise, h: wallHeight, vk, type, cpi }), [diameter, rise, wallHeight, vk, type, cpi]);
|
||||
|
||||
const handleExportPDF = () => {
|
||||
const sections: GenericPDFSection[] = [
|
||||
{
|
||||
title: 'Geometria da Cúpula',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Tipo', value: type === 'on-ground' ? 'Sobre terreno' : 'Sobre parede cilíndrica' },
|
||||
{ label: 'Diâmetro (d)', value: `${diameter} m` },
|
||||
{ label: 'Flecha (f)', value: `${rise} m` },
|
||||
...(type === 'on-cylinder' ? [{ label: 'Altura da parede', value: `${wallHeight} m` }] : []),
|
||||
{ label: 'f/d', value: result.fOverD.toFixed(3) },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Coeficientes de Pressão (Cpe)',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Barlavento', value: result.cpeBarlavento.toFixed(2) },
|
||||
{ label: 'Topo', value: result.cpeTopo.toFixed(2) },
|
||||
{ label: 'Lateral', value: result.cpeLateral.toFixed(2) },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Pressões p (kN/m²)',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Barlavento', value: (result.q * (result.cpeBarlavento - cpi)).toFixed(3) },
|
||||
{ label: 'Topo', value: (result.q * (result.cpeTopo - cpi)).toFixed(3) },
|
||||
{ label: 'Lateral', value: (result.q * (result.cpeLateral - cpi)).toFixed(3) },
|
||||
],
|
||||
},
|
||||
...(result.liftCoefficient > 0 ? [{
|
||||
title: 'Força de Sustentação',
|
||||
type: 'grid' as const,
|
||||
gridItems: [
|
||||
{ label: 'Coeficiente', value: result.liftCoefficient.toFixed(2) },
|
||||
{ label: 'Força', value: `${result.liftForceKN.toFixed(2)} kN` },
|
||||
],
|
||||
}] : []),
|
||||
];
|
||||
exportGenericToPDF('Cúpula', sections);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Cúpula</CardTitle>
|
||||
<CardDescription>Sec. 6.2.4 — silos, reservatórios, ginásios.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Tipo</label>
|
||||
<Select value={type} onValueChange={(v) => setType(v as DomeType)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="on-ground">Sobre terreno (Tab. 21)</SelectItem>
|
||||
<SelectItem value="on-cylinder">Sobre parede cilíndrica (Tab. 22)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Diâmetro (d)</label><span className="font-mono text-sm">{diameter} m</span></div>
|
||||
<Slider min={5} max={80} step={1} value={[diameter]} onValueChange={(v) => setDiameter(v[0])} />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Flecha (f)</label><span className="font-mono text-sm">{rise} m</span></div>
|
||||
<Slider min={0.5} max={Math.min(diameter / 4, 25)} step={0.5} value={[rise]} onValueChange={(v) => setRise(v[0])} />
|
||||
</div>
|
||||
{type === 'on-cylinder' && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Altura da parede</label><span className="font-mono text-sm">{wallHeight} m</span></div>
|
||||
<Slider min={0} max={30} step={0.5} value={[wallHeight]} onValueChange={(v) => setWallHeight(v[0])} />
|
||||
</div>
|
||||
)}
|
||||
<Separator />
|
||||
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-1">
|
||||
<div className="flex justify-between"><span>f/d:</span><span className="font-mono">{result.fOverD.toFixed(3)}</span></div>
|
||||
<div className="flex justify-between"><span>Vₖ:</span><span className="font-mono">{vk.toFixed(2)} m/s</span></div>
|
||||
<div className="flex justify-between"><span>q:</span><span className="font-mono">{result.q.toFixed(4)} kN/m²</span></div>
|
||||
{result.liftCoefficient > 0 && (
|
||||
<div className="flex justify-between"><span>F sustentação:</span><span className="font-mono font-semibold">{result.liftForceKN.toFixed(2)} kN</span></div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Coluna Central: 3D */}
|
||||
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
||||
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">f/d = {result.fOverD.toFixed(2)}</Badge>
|
||||
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">
|
||||
Cpi = {cpi.toFixed(2)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="w-full h-full bg-muted/20">
|
||||
<Dome3DViewer
|
||||
diameter={diameter}
|
||||
rise={rise}
|
||||
wallHeight={wallHeight}
|
||||
cpi={cpi}
|
||||
cpeBarlavento={result.cpeBarlavento}
|
||||
cpeTopo={result.cpeTopo}
|
||||
cpeLateral={result.cpeLateral}
|
||||
/>
|
||||
</div>
|
||||
</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">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Coeficientes (Cpe)</CardTitle>
|
||||
<CardDescription>Por zona da cúpula</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-muted-foreground text-[10px]">Barlavento</div>
|
||||
<div className="font-mono font-medium text-lg">{result.cpeBarlavento.toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-muted-foreground text-[10px]">Topo</div>
|
||||
<div className="font-mono font-medium text-lg">{result.cpeTopo.toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-muted-foreground text-[10px]">Lateral</div>
|
||||
<div className="font-mono font-medium text-lg">{result.cpeLateral.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase text-muted-foreground mb-2">Pressões p (kN/m²)</h4>
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Barlavento</div>
|
||||
<div className="font-mono font-medium">{(result.q * (result.cpeBarlavento - cpi)).toFixed(3)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Topo</div>
|
||||
<div className="font-mono font-medium">{(result.q * (result.cpeTopo - cpi)).toFixed(3)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Lateral</div>
|
||||
<div className="font-mono font-medium">{(result.q * (result.cpeLateral - cpi)).toFixed(3)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
</div>
|
||||
|
||||
<SceneCapturePanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DomeModule;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,137 @@
|
||||
import React from 'react';
|
||||
import Warehouse3DViewer from '../components/Warehouse3D';
|
||||
import LinearLoadsTable from '../components/LinearLoadsTable';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import FtoolExportCard from '../components/FtoolExportCard';
|
||||
import { useGalpaoStore } from '../store/galpaoStore';
|
||||
import { useWindStore } from '../store/appStore';
|
||||
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 ExportMenu from '../components/ExportMenu';
|
||||
|
||||
const GalpaoModule: React.FC = () => {
|
||||
const {
|
||||
width,
|
||||
length,
|
||||
height,
|
||||
roofPitch,
|
||||
setWidth,
|
||||
setLength,
|
||||
setHeight,
|
||||
setRoofPitch,
|
||||
} = useGalpaoStore();
|
||||
const {
|
||||
q,
|
||||
cpi,
|
||||
windAngle,
|
||||
setWindAngle,
|
||||
} = useWindStore();
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Geometria</CardTitle>
|
||||
<CardDescription>Ajuste as dimensões do galpão.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-sm font-medium text-foreground">Largura (X)</label>
|
||||
<span className="text-sm text-muted-foreground font-mono">{width} m</span>
|
||||
</div>
|
||||
<Slider min={5} max={80} step={1} value={[width]} onValueChange={(vals) => setWidth(vals[0])} className="py-1 cursor-pointer" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-sm font-medium text-foreground">Comprimento (Z)</label>
|
||||
<span className="text-sm text-muted-foreground font-mono">{length} m</span>
|
||||
</div>
|
||||
<Slider min={5} max={150} step={1} value={[length]} onValueChange={(vals) => setLength(vals[0])} className="py-1 cursor-pointer" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-sm font-medium text-foreground">Altura (Y)</label>
|
||||
<span className="text-sm text-muted-foreground font-mono">{height} m</span>
|
||||
</div>
|
||||
<Slider min={3} max={40} step={0.5} value={[height]} onValueChange={(vals) => setHeight(vals[0])} className="py-1 cursor-pointer" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-sm font-medium text-foreground">Inclinação (θ)</label>
|
||||
<span className="text-sm text-muted-foreground font-mono">{roofPitch}°</span>
|
||||
</div>
|
||||
<Slider min={0} max={60} step={1} value={[roofPitch]} onValueChange={(vals) => setRoofPitch(vals[0])} className="py-1 cursor-pointer" />
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">Direção do Vento</label>
|
||||
<Select value={windAngle.toString()} onValueChange={(val) => setWindAngle(Number(val) as 0 | 90)}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Selecione a direção" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">0° (Perpendicular à largura)</SelectItem>
|
||||
<SelectItem value="90">90° (Paralelo à largura)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Coluna Direita: Viewer 3D */}
|
||||
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
||||
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm border-muted-foreground/20 text-foreground shadow-sm">
|
||||
Vento a {windAngle}°
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">
|
||||
Cpi = {cpi.toFixed(2)}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">
|
||||
q = {q.toFixed(3)} kN/m²
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<ExportMenu />
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-4 left-4 z-10 flex gap-3 p-3 bg-background/90 backdrop-blur-md border border-border rounded-lg shadow-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-blue-500 shadow-[0_0_8px_rgba(59,130,246,0.5)]" />
|
||||
<span className="text-xs font-medium text-foreground">Pressão (Empuxo)</span>
|
||||
</div>
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.5)]" />
|
||||
<span className="text-xs font-medium text-foreground">Sucção (Arrasto)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-full cursor-grab active:cursor-grabbing bg-muted/20">
|
||||
<Warehouse3DViewer />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Coluna Direita-Inferior: Cargas Lineares (M9.2) */}
|
||||
<div className="w-full lg:w-96 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<LinearLoadsTable />
|
||||
<SceneCapturePanel />
|
||||
<FtoolExportCard />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GalpaoModule;
|
||||
@@ -0,0 +1,278 @@
|
||||
import React, { useMemo } 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 { useWindStore } from '@/store/appStore';
|
||||
import { calculateIsolatedShedRoof, calculateIsolatedGableRoof, frictionForceIsolatedRoof } from '@/lib/nbr-tables/table-24-25';
|
||||
import IsolatedRoof3DViewer from '@/components/three/IsolatedRoof3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const IsolatedRoofModule: React.FC = () => {
|
||||
const { q } = useWindStore();
|
||||
const [type, setType] = React.useState<'shed' | 'gable'>('shed');
|
||||
const [theta, setTheta] = React.useState(15);
|
||||
const [height, setHeight] = React.useState(2);
|
||||
const [width, setWidth] = React.useState(10);
|
||||
const [depth, setDepth] = React.useState(10);
|
||||
|
||||
const result = useMemo(() => {
|
||||
if (type === 'shed') {
|
||||
const r = calculateIsolatedShedRoof({ theta, height, depth });
|
||||
return { ...r, cpb: { cpb1: 0, cpb2: 0 }, cpa: { cpa1: 0, cpa2: 0 } };
|
||||
}
|
||||
const g = calculateIsolatedGableRoof({ theta, height, depth });
|
||||
return { ...g, cph1: { high: 0, low: 0 }, cph2: { high: 0, low: 0 } };
|
||||
}, [type, theta, height, depth]);
|
||||
|
||||
const frictionForce = useMemo(() => frictionForceIsolatedRoof(q, depth, width), [q, depth, width]);
|
||||
|
||||
// Cálculo dos Cpe para o viewer 3D
|
||||
const cpeWindward = useMemo(() => {
|
||||
if (type === 'shed') return (result.cph1 as { high: number; low: number }).low;
|
||||
return (result.cpb as { cpb1: number; cpb2: number }).cpb1;
|
||||
}, [type, result]);
|
||||
|
||||
const cpeLeeward = useMemo(() => {
|
||||
if (type === 'shed') return (result.cph2 as { high: number; low: number }).low;
|
||||
return (result.cpa as { cpa1: number; cpa2: number }).cpa1;
|
||||
}, [type, result]);
|
||||
|
||||
const cpeTop = useMemo(() => {
|
||||
if (type === 'shed') return (result.cph1 as { high: number; low: number }).high;
|
||||
return (result.cpb as { cpb1: number; cpb2: number }).cpb1;
|
||||
}, [type, result]);
|
||||
|
||||
// Decomposição de forças globais (Carregamento 1 principal)
|
||||
const forces = useMemo(() => {
|
||||
const Ap = width * depth; // Plan area
|
||||
const tan = Math.tan((theta * Math.PI) / 180);
|
||||
|
||||
let fx = 0;
|
||||
let fy = 0; // +Y = sustentação/arrancamento para cima
|
||||
|
||||
if (type === 'shed') {
|
||||
const cp = (result.cph1 as { high: number; low: number }).high;
|
||||
fy = Ap * q * (-cp);
|
||||
fx = Ap * q * (cp * tan);
|
||||
} else {
|
||||
const cpb = (result.cpb as { cpb1: number; cpb2: number }).cpb1;
|
||||
const cpa = (result.cpa as { cpa1: number; cpa2: number }).cpa1;
|
||||
|
||||
fy = -q * (Ap / 2) * (cpb + cpa);
|
||||
fx = q * (Ap / 2) * tan * (cpb - cpa);
|
||||
}
|
||||
|
||||
return { fx, fy, magnitude: Math.sqrt(fx*fx + fy*fy) };
|
||||
}, [type, width, depth, theta, q, result]);
|
||||
|
||||
const handleExportPDF = () => {
|
||||
const sections: GenericPDFSection[] = [
|
||||
{
|
||||
title: 'Geometria da Cobertura Isolada',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Tipo', value: type === 'shed' ? 'Uma água' : 'Duas águas' },
|
||||
{ label: 'Inclinação (θ)', value: `${theta}°` },
|
||||
{ label: 'Largura (b)', value: `${width} m` },
|
||||
{ label: 'Profundidade (ℓ)', value: `${depth} m` },
|
||||
{ label: 'Altura livre (h)', value: `${height} m` },
|
||||
{ label: 'Aplicável à norma', value: result.applies ? 'Sim' : 'Não' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Coeficientes de Pressão (Cpe)',
|
||||
type: 'grid',
|
||||
gridItems: type === 'shed'
|
||||
? [
|
||||
{ label: 'Carregamento 1 (alta/baixa)', value: `${(result.cph1 as any).high.toFixed(2)} / ${(result.cph1 as any).low.toFixed(2)}` },
|
||||
{ label: 'Carregamento 2 (alta/baixa)', value: `${(result.cph2 as any).high.toFixed(2)} / ${(result.cph2 as any).low.toFixed(2)}` },
|
||||
]
|
||||
: [
|
||||
{ label: 'Barlavento (Cp_b1 / Cp_b2)', value: `${(result.cpb as any).cpb1.toFixed(2)} / ${(result.cpb as any).cpb2.toFixed(2)}` },
|
||||
{ label: 'Sotavento (Cp_a1 / Cp_a2)', value: `${(result.cpa as any).cpa1.toFixed(2)} / ${(result.cpa as any).cpa2.toFixed(2)}` },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Forças Globais (Carr. 1)',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Vertical (Fy - Arrancamento)', value: `${forces.fy.toFixed(2)} kN` },
|
||||
{ label: 'Horizontal (Fx - Arrasto)', value: `${forces.fx.toFixed(2)} kN` },
|
||||
{ label: 'Atrito (Ff)', value: `${frictionForce.toFixed(3)} kN` },
|
||||
],
|
||||
},
|
||||
];
|
||||
exportGenericToPDF('Cobertura Isolada', sections);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Cobertura Isolada</CardTitle>
|
||||
<CardDescription>Sec. 7.2 — sobre suportes de reduzidas dimensões.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Tipo de Cobertura</label>
|
||||
<Select value={type} onValueChange={(v) => setType(v as 'shed' | 'gable')}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="shed">Uma água (Tab. 24)</SelectItem>
|
||||
<SelectItem value="gable">Duas águas (Tab. 25)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Inclinação θ</label><span className="font-mono text-sm">{theta}°</span></div>
|
||||
<Slider min={0} max={30} step={1} value={[theta]} onValueChange={(v) => setTheta(v[0])} />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Largura (b)</label><span className="font-mono text-sm">{width} m</span></div>
|
||||
<Slider min={5} max={40} step={1} value={[width]} onValueChange={(v) => setWidth(v[0])} />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Profundidade (ℓ)</label><span className="font-mono text-sm">{depth} m</span></div>
|
||||
<Slider min={5} max={40} step={1} value={[depth]} onValueChange={(v) => setDepth(v[0])} />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Altura livre h</label><span className="font-mono text-sm">{height} m</span></div>
|
||||
<Slider min={0.5} max={10} step={0.5} value={[height]} onValueChange={(v) => setHeight(v[0])} />
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-1">
|
||||
<div className="flex justify-between"><span>q:</span><span className="font-mono">{q.toFixed(4)} kN/m²</span></div>
|
||||
<div className="flex justify-between"><span>Aplicável:</span><span className="font-mono">{result.applies ? 'sim' : 'não'}</span></div>
|
||||
</div>
|
||||
{!result.applies && (
|
||||
<div className="rounded-md border border-destructive/40 bg-destructive/5 p-2 text-xs text-destructive">
|
||||
Limites da Tabela {type === 'shed' ? '24' : '25'} não atendidos (h ≤ tg(θ)·b/2). Tratar como edificação fechada.
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Coluna Central: 3D */}
|
||||
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
||||
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">θ = {theta}°</Badge>
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">{type === 'shed' ? 'Uma água' : 'Duas águas'}</Badge>
|
||||
</div>
|
||||
<IsolatedRoof3DViewer
|
||||
type={type}
|
||||
theta={theta}
|
||||
height={height}
|
||||
depth={depth}
|
||||
cpeWindward={cpeWindward}
|
||||
cpeLeeward={cpeLeeward}
|
||||
cpeTop={cpeTop}
|
||||
forceKN={forces.magnitude}
|
||||
/>
|
||||
</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">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Coeficientes (Cpe)</CardTitle>
|
||||
<CardDescription>Dois carregamentos a verificar</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{type === 'shed' ? (
|
||||
<>
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase text-muted-foreground mb-2">Carregamento 1 (barlavento)</h4>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Alta (h)</div>
|
||||
<div className="font-mono font-medium">{(result.cph1 as { high: number; low: number }).high.toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Baixa (l)</div>
|
||||
<div className="font-mono font-medium">{(result.cph1 as { high: number; low: number }).low.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase text-muted-foreground mb-2">Carregamento 2 (invertido)</h4>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Alta (h)</div>
|
||||
<div className="font-mono font-medium">{(result.cph2 as { high: number; low: number }).high.toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Baixa (l)</div>
|
||||
<div className="font-mono font-medium">{(result.cph2 as { high: number; low: number }).low.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase text-muted-foreground mb-2">Zona barlavento (Cp_b)</h4>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Cp_b1</div>
|
||||
<div className="font-mono font-medium">{(result.cpb as { cpb1: number; cpb2: number }).cpb1.toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Cp_b2</div>
|
||||
<div className="font-mono font-medium">{(result.cpb as { cpb1: number; cpb2: number }).cpb2.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase text-muted-foreground mb-2">Zona sotavento (Cp_a)</h4>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Cp_a1</div>
|
||||
<div className="font-mono font-medium">{(result.cpa as { cpa1: number; cpa2: number }).cpa1.toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Cp_a2</div>
|
||||
<div className="font-mono font-medium">{(result.cpa as { cpa1: number; cpa2: number }).cpa2.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Separator />
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase text-muted-foreground mb-2">Forças Globais (Carr. 1)</h4>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Fx (Arrasto)</div>
|
||||
<div className="font-mono font-medium">{forces.fx.toFixed(2)} kN</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Fy (Arrancamento)</div>
|
||||
<div className="font-mono font-medium">{forces.fy.toFixed(2)} kN</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Força de atrito (vento ∥ geratriz):</span>
|
||||
<Badge variant="default" className="font-mono">{frictionForce.toFixed(3)} kN</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
</div>
|
||||
|
||||
<SceneCapturePanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IsolatedRoofModule;
|
||||
@@ -0,0 +1,214 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sun, Moon, Laptop, Save, Trash2, Upload, CheckCircle2, AlertCircle } from 'lucide-react';
|
||||
import { useTheme } from '@/lib/theme';
|
||||
import { useProjects } from '@/lib/hooks/useProjects';
|
||||
import { useWindStore } from '@/store/appStore';
|
||||
import { useI18n } from '@/store/i18nStore';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
importProjectFromText,
|
||||
readProjectFile,
|
||||
type ImportResult,
|
||||
} from '@/lib/import-project';
|
||||
|
||||
const SettingsModule: React.FC = () => {
|
||||
const { theme, setTheme, effectiveTheme } = useTheme();
|
||||
const { projects, loading, error, remove } = useProjects();
|
||||
const { v0, terrainCategory, structureClass, s3Group } = useWindStore();
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [importResult, setImportResult] = useState<ImportResult | null>(null);
|
||||
const [importing, setImporting] = useState(false);
|
||||
|
||||
const exportSnapshot = () => {
|
||||
const state = useWindStore.getState();
|
||||
const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', 'ventoapp-state.json');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
const handleImportClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleFileSelected = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
setImporting(true);
|
||||
setImportResult(null);
|
||||
try {
|
||||
const text = await readProjectFile(file);
|
||||
const result = importProjectFromText(text);
|
||||
setImportResult(result);
|
||||
} catch (e) {
|
||||
setImportResult({
|
||||
ok: false,
|
||||
error: e instanceof Error ? e.message : 'Falha ao ler arquivo',
|
||||
});
|
||||
} finally {
|
||||
setImporting(false);
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-5xl mx-auto space-y-6 overflow-auto">
|
||||
<h1 className="text-3xl font-bold">{t('nav_settings')}</h1>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('settings_appearance')}</CardTitle>
|
||||
<CardDescription>{t('settings_appearance_desc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex gap-2">
|
||||
<Button variant={theme === 'light' ? 'default' : 'outline'} onClick={() => setTheme('light')}>
|
||||
<Sun className="w-4 h-4 mr-2" /> {t('settings_theme_light')}
|
||||
</Button>
|
||||
<Button variant={theme === 'dark' ? 'default' : 'outline'} onClick={() => setTheme('dark')}>
|
||||
<Moon className="w-4 h-4 mr-2" /> {t('settings_theme_dark')}
|
||||
</Button>
|
||||
<Button variant={theme === 'system' ? 'default' : 'outline'} onClick={() => setTheme('system')}>
|
||||
<Laptop className="w-4 h-4 mr-2" /> {t('settings_theme_system')}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-3">
|
||||
{t('settings_effective')}: <Badge variant="outline">{effectiveTheme}</Badge>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('settings_projects')}</CardTitle>
|
||||
<CardDescription>{t('settings_projects_desc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between mb-3 gap-2 flex-wrap">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('settings_projects_count', { count: projects.length })}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline" onClick={handleImportClick} disabled={importing}>
|
||||
<Upload className="w-4 h-4 mr-2" /> {importing ? t('settings_importing') : t('export_import')}
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
onChange={handleFileSelected}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button size="sm" variant="outline" onClick={exportSnapshot}>
|
||||
<Save className="w-4 h-4 mr-2" /> {t('export_snapshot')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{importResult && (
|
||||
<div
|
||||
className={`rounded-md border p-3 mb-3 text-sm flex items-start gap-2 ${
|
||||
importResult.ok
|
||||
? 'bg-emerald-50 border-emerald-200 text-emerald-700 dark:bg-emerald-950 dark:border-emerald-800 dark:text-emerald-300'
|
||||
: 'bg-red-50 border-red-200 text-red-700 dark:bg-red-950 dark:border-red-800 dark:text-red-300'
|
||||
}`}
|
||||
>
|
||||
{importResult.ok ? (
|
||||
<CheckCircle2 className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
) : (
|
||||
<AlertCircle className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium">
|
||||
{importResult.ok ? t('settings_import_success') : t('settings_import_error')}
|
||||
</div>
|
||||
{importResult.module && (
|
||||
<div className="text-xs mt-1">
|
||||
{t('settings_import_module')}: <Badge variant="outline">{importResult.module}</Badge>
|
||||
{importResult.projectName && (
|
||||
<span className="ml-2">{t('settings_import_project')}: {importResult.projectName}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{importResult.appliedFields && importResult.appliedFields.length > 0 && (
|
||||
<div className="text-xs mt-1">
|
||||
{t('settings_import_fields', { count: importResult.appliedFields.length })}: {importResult.appliedFields.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
{importResult.warnings && importResult.warnings.length > 0 && (
|
||||
<div className="text-xs mt-1 opacity-80">
|
||||
{t('settings_import_warnings')}: {importResult.warnings.join('; ')}
|
||||
</div>
|
||||
)}
|
||||
{importResult.error && (
|
||||
<div className="text-xs mt-1">{importResult.error}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && <p className="text-sm text-muted-foreground">{t('common_loading')}</p>}
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
{projects.length === 0 && !loading && (
|
||||
<p className="text-sm text-muted-foreground">{t('settings_no_projects')}</p>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{projects.map((p) => (
|
||||
<div key={p.id} className="flex items-center justify-between p-2 border rounded-md hover:bg-muted/40">
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{p.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{p.module} · {new Date(p.updatedAt).toLocaleString(locale === 'pt-BR' ? 'pt-BR' : 'en-US')}
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" onClick={() => p.id && remove(p.id)}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('settings_state')}</CardTitle>
|
||||
<CardDescription>{t('settings_state_desc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2"><span className="text-muted-foreground">V₀:</span> <span className="font-mono">{v0}</span></div>
|
||||
<div className="rounded-md border bg-muted/40 p-2"><span className="text-muted-foreground">Categoria:</span> <span className="font-mono">{terrainCategory}</span></div>
|
||||
<div className="rounded-md border bg-muted/40 p-2"><span className="text-muted-foreground">Classe:</span> <span className="font-mono">{structureClass}</span></div>
|
||||
<div className="rounded-md border bg-muted/40 p-2"><span className="text-muted-foreground">S₃ grupo:</span> <span className="font-mono">{s3Group}</span></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t('settings_about')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
<p><strong>{t('app_title')}</strong> — {t('settings_about_desc')}</p>
|
||||
<p className="text-muted-foreground">
|
||||
{t('settings_stack')}: React 19 + TypeScript + Vite + Tailwind v4 + shadcn/ui + Zustand + R3F + @react-pdf/renderer.
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
Cobertura: 11 seções normativas + 3 anexos. Todos os 8 marcos do plano de implementação concluídos.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsModule;
|
||||
@@ -0,0 +1,191 @@
|
||||
import React, { useMemo } 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 { useWindStore } from '@/store/appStore';
|
||||
import { calculateSign } from '@/lib/nbr-tables/table-23';
|
||||
import Sign3DViewer from '@/components/three/Sign3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const SignModule: React.FC = () => {
|
||||
const { q } = useWindStore();
|
||||
const [length, setLength] = React.useState(8);
|
||||
const [height, setHeight] = React.useState(3);
|
||||
const [alpha, setAlpha] = React.useState<90 | 50>(90);
|
||||
const [hasEndPlates, setHasEndPlates] = React.useState(true);
|
||||
const [groundClearance, setGroundClearance] = React.useState(0.5);
|
||||
|
||||
const result = useMemo(
|
||||
() =>
|
||||
calculateSign({ length, height, alpha, hasEndPlates, groundClearance }, q),
|
||||
[length, height, alpha, hasEndPlates, groundClearance, q],
|
||||
);
|
||||
|
||||
const handleExportPDF = () => {
|
||||
const sections: GenericPDFSection[] = [
|
||||
{
|
||||
title: 'Geometria do Muro/Placa',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Comprimento (ℓ)', value: `${length} m` },
|
||||
{ label: 'Altura (hₐ)', value: `${height} m` },
|
||||
{ label: 'Distância do solo', value: `${groundClearance} m` },
|
||||
{ label: 'ℓ/hₐ', value: result.lhRatio.toFixed(2) },
|
||||
{ label: 'Placas de extremidade', value: hasEndPlates ? 'Sim' : 'Não' },
|
||||
{ label: 'Ângulo de incidência (α)', value: `${alpha}°` },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Força Resultante',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Área Efetiva', value: `${result.areaEffective.toFixed(1)} m²` },
|
||||
{ label: 'Coeficiente de Força (Cf)', value: result.cf.toFixed(2) },
|
||||
{ label: 'Excentricidade (e)', value: `${result.applicationPoint.toFixed(2)} m` },
|
||||
{ label: 'Força F', value: `${result.forceKN.toFixed(2)} kN` },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Momento de Tombamento (Estimado)',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Em relação à base da placa', value: `${result.momentBaseKNm.toFixed(2)} kNm` },
|
||||
{ label: 'Em relação ao solo', value: `${result.momentGroundKNm.toFixed(2)} kNm` },
|
||||
],
|
||||
},
|
||||
];
|
||||
exportGenericToPDF('Muros e Placas', sections);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Muros e Placas</CardTitle>
|
||||
<CardDescription>Sec. 7.1 — vento perpendicular à face.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Comprimento (ℓ)</label><span className="font-mono text-sm">{length} m</span></div>
|
||||
<Slider min={2} max={30} step={0.5} value={[length]} onValueChange={(v) => setLength(v[0])} />
|
||||
</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={1} max={10} step={0.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">Distância do solo</label><span className="font-mono text-sm">{groundClearance} m</span></div>
|
||||
<Slider min={0} max={5} step={0.1} value={[groundClearance]} onValueChange={(v) => setGroundClearance(v[0])} />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Ângulo de Incidência</label>
|
||||
<Select value={alpha.toString()} onValueChange={(v) => setAlpha(Number(v) as 90 | 50)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="90">90° (perpendicular)</SelectItem>
|
||||
<SelectItem value="50">50° (oblíquo)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Placas de Extremidade</label>
|
||||
<Select value={hasEndPlates ? 'yes' : 'no'} onValueChange={(v) => setHasEndPlates(v === 'yes')}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="yes">Sim (ℓ/hₐ < 60)</SelectItem>
|
||||
<SelectItem value="no">Não (escoamento 2D)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-1">
|
||||
<div className="flex justify-between"><span>q:</span><span className="font-mono">{q.toFixed(4)} kN/m²</span></div>
|
||||
<div className="flex justify-between"><span>ℓ/hₐ:</span><span className="font-mono">{result.lhRatio.toFixed(2)}</span></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Coluna Central: 3D */}
|
||||
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
||||
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">F = {result.forceKN.toFixed(2)} kN</Badge>
|
||||
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">Cf = {result.cf.toFixed(2)}</Badge>
|
||||
</div>
|
||||
|
||||
<Sign3DViewer
|
||||
length={length}
|
||||
height={height}
|
||||
groundClearance={groundClearance}
|
||||
alpha={alpha}
|
||||
cf={result.cf}
|
||||
forceKN={result.forceKN}
|
||||
applicationPoint={result.applicationPoint}
|
||||
/>
|
||||
</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">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Força Resultante</CardTitle>
|
||||
<CardDescription>F = Cf · q · A</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-muted-foreground text-[10px]">Cf</div>
|
||||
<div className="font-mono font-medium text-lg">{result.cf.toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-muted-foreground text-[10px]">e (m)</div>
|
||||
<div className="font-mono font-medium text-lg">{result.applicationPoint.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-muted-foreground text-[10px]">Área efetiva</div>
|
||||
<div className="font-mono font-medium">{result.areaEffective.toFixed(1)} m²</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-muted-foreground text-[10px]">Força F</div>
|
||||
<div className="font-mono font-medium text-lg">{result.forceKN.toFixed(2)} kN</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<h4 className="text-xs font-semibold uppercase text-muted-foreground">Momento de Tombamento</h4>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-muted-foreground text-[10px]">M (base da placa)</div>
|
||||
<div className="font-mono font-medium">{result.momentBaseKNm.toFixed(2)} kNm</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-muted-foreground text-[10px]">M (solo)</div>
|
||||
<div className="font-mono font-medium text-destructive">{result.momentGroundKNm.toFixed(2)} kNm</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="text-xs text-muted-foreground">
|
||||
A força F atua perpendicularmente ao plano do muro ou placa, com excentricidade e medida a partir do centro geométrico.
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
</div>
|
||||
|
||||
<SceneCapturePanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignModule;
|
||||
@@ -0,0 +1,216 @@
|
||||
import React, { useMemo } 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 { useWindStore } from '@/store/appStore';
|
||||
import { calculateTower, type TowerSection, type TowerBarType } from '@/lib/modules/tower';
|
||||
import Tower3DViewer from '@/components/three/Tower3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const TowerModule: React.FC = () => {
|
||||
const { q } = useWindStore();
|
||||
const [section, setSection] = React.useState<TowerSection>('square');
|
||||
const [barType, setBarType] = React.useState<TowerBarType>('flat');
|
||||
const [baseWidth, setBaseWidth] = React.useState(3);
|
||||
const [height, setHeight] = React.useState(30);
|
||||
const [panels, setPanels] = React.useState(6);
|
||||
const [phi, setPhi] = React.useState(0.3);
|
||||
const [alphaWind, setAlphaWind] = React.useState<0 | 45 | 90>(0);
|
||||
|
||||
const result = useMemo(() => {
|
||||
const aFace = baseWidth * height;
|
||||
return calculateTower({
|
||||
section,
|
||||
barType,
|
||||
phi,
|
||||
aFace,
|
||||
alphaWind,
|
||||
q,
|
||||
});
|
||||
}, [section, barType, phi, baseWidth, height, alphaWind, q]);
|
||||
|
||||
const handleExportPDF = () => {
|
||||
const sections: GenericPDFSection[] = [
|
||||
{
|
||||
title: 'Geometria da Torre',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Seção', value: section === 'square' ? 'Quadrada' : 'Triangular' },
|
||||
{ label: 'Tipo de barra', value: barType === 'flat' ? 'Faces planas' : 'Circular' },
|
||||
{ label: 'Largura da base', value: `${baseWidth} m` },
|
||||
{ label: 'Altura', value: `${height} m` },
|
||||
{ label: 'Tramos', value: String(panels) },
|
||||
{ label: 'φ (área exposta)', value: phi.toFixed(2) },
|
||||
{ label: 'Ângulo do vento', value: `${alphaWind}°` },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Coeficientes',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Ca', value: result.ca.toFixed(2) },
|
||||
{ label: 'Ca efetivo', value: result.caEff.toFixed(2) },
|
||||
{ label: 'Kα', value: result.kAlpha.toFixed(2) },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Forças',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Força Total', value: `${result.forceKN.toFixed(2)} kN` },
|
||||
{ label: 'Face I', value: `${result.faceComponents.faceI.toFixed(2)} kN` },
|
||||
{ label: 'Face II', value: `${result.faceComponents.faceII.toFixed(2)} kN` },
|
||||
{ label: 'Face III', value: `${result.faceComponents.faceIII.toFixed(2)} kN` },
|
||||
{ label: 'Face IV', value: `${result.faceComponents.faceIV.toFixed(2)} kN` },
|
||||
],
|
||||
},
|
||||
];
|
||||
exportGenericToPDF('Torre Reticulada', sections);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Torre Reticulada</CardTitle>
|
||||
<CardDescription>Sec. 8.5 — faces planas ou circulares.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Seção</label>
|
||||
<Select value={section} onValueChange={(v) => setSection(v as TowerSection)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="square">Quadrada</SelectItem>
|
||||
<SelectItem value="triangular">Triangular equilátera</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Tipo de Barra</label>
|
||||
<Select value={barType} onValueChange={(v) => setBarType(v as TowerBarType)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="flat">Faces planas</SelectItem>
|
||||
<SelectItem value="circular">Circular</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Largura da base</label><span className="font-mono text-sm">{baseWidth} m</span></div>
|
||||
<Slider min={1} max={10} step={0.5} value={[baseWidth]} onValueChange={(v) => setBaseWidth(v[0])} />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Altura</label><span className="font-mono text-sm">{height} m</span></div>
|
||||
<Slider min={5} max={100} 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">Tramos</label><span className="font-mono text-sm">{panels}</span></div>
|
||||
<Slider min={2} max={20} step={1} value={[panels]} onValueChange={(v) => setPanels(v[0])} />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">φ (área exposta)</label><span className="font-mono text-sm">{phi.toFixed(2)}</span></div>
|
||||
<Slider min={0.05} max={1} step={0.05} value={[phi]} onValueChange={(v) => setPhi(v[0])} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Ângulo do vento</label>
|
||||
<Select value={String(alphaWind)} onValueChange={(v) => setAlphaWind(Number(v) as 0 | 45 | 90)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">0° (⊥ face)</SelectItem>
|
||||
<SelectItem value="45">45° (oblíquo)</SelectItem>
|
||||
<SelectItem value="90">90° (diagonal)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-1">
|
||||
<div className="flex justify-between"><span>q:</span><span className="font-mono">{q.toFixed(4)} kN/m²</span></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Coluna Central: 3D */}
|
||||
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
||||
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">{section === 'square' ? 'Quadrada' : 'Triangular'}</Badge>
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">α = {alphaWind}°</Badge>
|
||||
</div>
|
||||
<Tower3DViewer
|
||||
section={section}
|
||||
barType={barType}
|
||||
baseWidth={baseWidth}
|
||||
height={height}
|
||||
panels={panels}
|
||||
phi={phi}
|
||||
alphaWind={alphaWind}
|
||||
forceKN={result.forceKN}
|
||||
/>
|
||||
</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">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Resultados</CardTitle>
|
||||
<CardDescription>Coeficientes e forças na torre</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Ca</div>
|
||||
<div className="font-mono font-medium">{result.ca.toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Ca efetivo</div>
|
||||
<div className="font-mono font-medium">{result.caEff.toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Kα</div>
|
||||
<div className="font-mono font-medium">{result.kAlpha.toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Força total</div>
|
||||
<div className="font-mono font-medium">{result.forceKN.toFixed(2)} kN</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-semibold uppercase text-muted-foreground">Componentes por face</h4>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Face I</div>
|
||||
<div className="font-mono font-medium">{result.faceComponents.faceI.toFixed(2)} kN</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Face II</div>
|
||||
<div className="font-mono font-medium">{result.faceComponents.faceII.toFixed(2)} kN</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Face III</div>
|
||||
<div className="font-mono font-medium">{result.faceComponents.faceIII.toFixed(2)} kN</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-[10px] text-muted-foreground">Face IV</div>
|
||||
<div className="font-mono font-medium">{result.faceComponents.faceIV.toFixed(2)} kN</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
</div>
|
||||
|
||||
<SceneCapturePanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TowerModule;
|
||||
@@ -0,0 +1,190 @@
|
||||
import React, { useMemo } 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 { useWindStore } from '@/store/appStore';
|
||||
import { calculateVault, type VaultRegime } from '@/lib/modules/vault';
|
||||
import Vault3DViewer from '@/components/three/Vault3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const VaultModule: React.FC = () => {
|
||||
const { vk } = useWindStore();
|
||||
const [span, setSpan] = React.useState(20);
|
||||
const [length, setLength] = React.useState(30);
|
||||
const [rise, setRise] = React.useState(5);
|
||||
const [regime, setRegime] = React.useState<VaultRegime>('laminar-rough');
|
||||
const [localCpi, setLocalCpi] = React.useState<number>(0);
|
||||
|
||||
const result = useMemo(() => calculateVault({ f: rise, l: span, b: length, vk, regime, cpi: localCpi }), [span, length, rise, vk, regime, localCpi]);
|
||||
|
||||
const handleExportPDF = () => {
|
||||
const sections: GenericPDFSection[] = [
|
||||
{
|
||||
title: 'Geometria da Abóbada',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Vão (ℓ)', value: `${span} m` },
|
||||
{ label: 'Comprimento (b)', value: `${length} m` },
|
||||
{ label: 'Flecha (f)', value: `${rise} m` },
|
||||
{ label: 'f/ℓ', value: (rise / span).toFixed(2) },
|
||||
{ label: 'Regime', value: regime },
|
||||
{ label: 'Pressão Interna (Cpi)', value: localCpi > 0 ? `+${localCpi.toFixed(2)}` : localCpi.toFixed(2) },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Coeficientes de Pressão (Cpe)',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{
|
||||
label: 'Vento ⊥ geratriz',
|
||||
value: Object.entries(result.windPerpendicular).map(([k, v]) => `${k}: ${(v as number).toFixed(2)}`).join(' | '),
|
||||
},
|
||||
{
|
||||
label: 'Vento ∥ geratriz',
|
||||
value: Object.entries(result.windParallel).map(([k, v]) => `${k}: ${(v as number).toFixed(2)}`).join(' | '),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Pressões p (kN/m²)',
|
||||
type: 'grid',
|
||||
gridItems: Object.entries(result.pressures).map(([k, v]) => ({
|
||||
label: `Zona ${k}`,
|
||||
value: v.toFixed(3),
|
||||
})),
|
||||
},
|
||||
];
|
||||
exportGenericToPDF('Abóbada Cilíndrica', sections);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Abóbada Cilíndrica</CardTitle>
|
||||
<CardDescription>Sec. 6.2.3 — galpões curvos, igrejas.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Vão (ℓ)</label><span className="font-mono text-sm">{span} m</span></div>
|
||||
<Slider min={5} max={60} step={1} value={[span]} onValueChange={(v) => setSpan(v[0])} />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Comprimento (b)</label><span className="font-mono text-sm">{length} m</span></div>
|
||||
<Slider min={10} max={100} step={1} value={[length]} onValueChange={(v) => setLength(v[0])} />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Flecha (f)</label><span className="font-mono text-sm">{rise} m</span></div>
|
||||
<Slider min={1} max={Math.min(span / 2, 20)} step={0.5} value={[rise]} onValueChange={(v) => setRise(v[0])} />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Regime de Escoamento</label>
|
||||
<Select value={regime} onValueChange={(v) => setRegime(v as VaultRegime)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="laminar-rough">Baixa turbulência (Tab. 15-17)</SelectItem>
|
||||
<SelectItem value="turbulent-51">Turbulento — Série 51 (Tab. 18-20)</SelectItem>
|
||||
<SelectItem value="turbulent-52">Turbulento — Série 52 (Tab. 18-20)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Aberturas e Permeabilidade (Cpi)</label>
|
||||
<Select value={localCpi.toString()} onValueChange={(v) => setLocalCpi(Number(v))}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">Fechada / Igual (Cpi = 0,0)</SelectItem>
|
||||
<SelectItem value="0.2">Aberturas opostas — 0° (Cpi = +0,2)</SelectItem>
|
||||
<SelectItem value="-0.3">Aberturas opostas — 90° (Cpi = -0,3)</SelectItem>
|
||||
<SelectItem value="0.5">Abertura dominante a barlavento (Cpi = +0,5)</SelectItem>
|
||||
<SelectItem value="0.8">Abertura quase total a barlavento (Cpi = +0,8)</SelectItem>
|
||||
<SelectItem value="-0.5">Abertura dominante a sotavento (Cpi = -0,5)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-1">
|
||||
<div className="flex justify-between"><span>f/ℓ:</span><span className="font-mono">{(rise / span).toFixed(2)}</span></div>
|
||||
<div className="flex justify-between"><span>Vₖ:</span><span className="font-mono">{vk.toFixed(2)} m/s</span></div>
|
||||
<div className="flex justify-between"><span>q:</span><span className="font-mono">{result.q.toFixed(4)} kN/m²</span></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Coluna Central: Viewer 3D */}
|
||||
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
||||
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">f/ℓ = {(rise / span).toFixed(2)}</Badge>
|
||||
<Badge variant="outline" className={localCpi > 0 ? 'bg-destructive/10 text-destructive border-destructive/20 backdrop-blur-sm' : 'bg-background/80 backdrop-blur-sm'}>
|
||||
Cpi = {localCpi > 0 ? `+${localCpi.toFixed(2)}` : localCpi.toFixed(2)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="w-full h-full bg-muted/20">
|
||||
<Vault3DViewer span={span} length={length} rise={rise} cpi={localCpi} cpeProfile={{ ...result.windPerpendicular }} />
|
||||
</div>
|
||||
</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">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Coeficientes e Pressões</CardTitle>
|
||||
<CardDescription>Vento ⊥ geratriz / ∥ geratriz</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase text-muted-foreground mb-2">Vento ⊥ geratriz</h4>
|
||||
<div className="grid grid-cols-3 gap-1 text-xs">
|
||||
{Object.entries(result.windPerpendicular).map(([k, v]) => (
|
||||
<div key={k} className="rounded border bg-muted/40 p-1.5 text-center">
|
||||
<div className="text-muted-foreground text-[10px]">{k}</div>
|
||||
<div className="font-mono font-medium">{(v as number).toFixed(2)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase text-muted-foreground mb-2">Vento ∥ geratriz</h4>
|
||||
<div className="grid grid-cols-4 gap-1 text-xs">
|
||||
{Object.entries(result.windParallel).map(([k, v]) => (
|
||||
<div key={k} className="rounded border bg-muted/40 p-1.5 text-center">
|
||||
<div className="text-muted-foreground text-[10px]">{k}</div>
|
||||
<div className="font-mono font-medium">{(v as number).toFixed(2)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase text-muted-foreground mb-2">Pressões p (kN/m²)</h4>
|
||||
<div className="grid grid-cols-3 gap-1 text-xs">
|
||||
{Object.entries(result.pressures).map(([k, v]) => (
|
||||
<div key={k} className="rounded border bg-muted/40 p-1.5 text-center">
|
||||
<div className="text-muted-foreground text-[10px]">{k}</div>
|
||||
<div className="font-mono font-medium">{v.toFixed(3)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
</div>
|
||||
|
||||
<SceneCapturePanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VaultModule;
|
||||
Reference in New Issue
Block a user