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

This commit is contained in:
2026-07-08 19:52:34 +00:00
commit 9fece3f174
170 changed files with 27177 additions and 0 deletions
+246
View File
@@ -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;
+253
View File
@@ -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;
+199
View File
@@ -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;
+187
View File
@@ -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;
+390
View File
@@ -0,0 +1,390 @@
import React, { useMemo, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Slider } from '@/components/ui/slider';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { Input } from '@/components/ui/input';
import { useWindStore } from '@/store/appStore';
import {
getDynamicParams,
estimateFundamentalFrequency,
calculateVp,
dynamicFactor,
getStrouhalNumber,
criticalVelocity,
vortexDispenseCheck,
scrutonNumber,
isVortexSusceptible,
evaluateComfort,
maxAcceleration,
type StructureDynamicType,
type SectionShape,
} from '@/lib/modules/dynamics';
import { calculateS2 } from '@/lib/wind-kernel';
import { TABLE_32 } from '@/lib/nbr-tables/table-32';
import Dynamics3DViewer from '@/components/three/Dynamics3D';
import SceneCapturePanel from '../components/SceneCapturePanel';
import ExportMenu from '../components/ExportMenu';
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
const DynamicsModule: React.FC = () => {
const { v0, s1, s3, terrainCategory, structureClass, q } = useWindStore();
const [activeTab, setActiveTab] = useState('edificio');
const [structureType, setStructureType] = React.useState<StructureDynamicType>('concrete-shearwall');
const [height, setHeight] = React.useState(40);
const [freq, setFreq] = React.useState(0.5);
const [uMax, setUMax] = React.useState(0.01);
const [useType, setUseType] = React.useState<'residential' | 'commercial'>('commercial');
const [sectionShape, setSectionShape] = React.useState<SectionShape>('rectangle-b-a-1-2');
const [sectionRatio, setSectionRatio] = React.useState(1);
const [sectionSize, setSectionSize] = React.useState(0.5);
const [massEq, setMassEq] = React.useState(2000);
const dynParams = useMemo(() => getDynamicParams(structureType), [structureType]);
const estimatedFreq = useMemo(() => estimateFundamentalFrequency(structureType, height), [structureType, height]);
const vp = useMemo(() => calculateVp(v0, s3), [v0, s3]);
const s2 = useMemo(() => calculateS2(height, terrainCategory, structureClass), [height, terrainCategory, structureClass]);
const dynFactor = useMemo(
() =>
dynamicFactor({
category: terrainCategory,
vp,
freq,
height,
xi: dynParams.xiPercent,
}),
[terrainCategory, vp, freq, height, dynParams.xiPercent],
);
const st = useMemo(() => getStrouhalNumber(sectionShape, sectionRatio), [sectionShape, sectionRatio]);
const vcr = useMemo(() => criticalVelocity(freq, sectionSize, st), [freq, sectionSize, st]);
const scrutin = useMemo(() => scrutonNumber(dynParams.xiPercent / 100, massEq, 1.226, sectionSize), [dynParams.xiPercent, massEq, sectionSize]);
const okVortex = useMemo(() => vortexDispenseCheck(vcr, v0, s1, s2, s3), [vcr, v0, s1, s2, s3]);
const aMax = useMemo(() => maxAcceleration(freq, uMax), [freq, uMax]);
const comfort = useMemo(() => evaluateComfort({ freq, aMax, use: useType }), [freq, aMax, useType]);
const handleExportPDF = () => {
const sections: GenericPDFSection[] = [
{
title: 'Geometria e Parâmetros Base (Dinâmica)',
type: 'grid',
gridItems: [
{ label: 'Tipo', value: structureType },
{ label: 'Altura (h)', value: `${height} m` },
{ label: 'Frequência (f)', value: `${freq.toFixed(2)} Hz` },
{ label: 'Freq. Estimada (Tab. 31)', value: `${estimatedFreq.toFixed(2)} Hz` },
{ label: 'γ (Gama)', value: dynParams.gamma.toString() },
{ label: 'ξ (Amortecimento)', value: `${dynParams.xiPercent}%` },
],
},
{
title: 'Resposta Dinâmica (Sec. 9.3)',
type: 'grid',
gridItems: [
{ label: 'Vₚ', value: `${vp.toFixed(2)} m/s` },
{ label: 'Fator ζ', value: dynFactor.toFixed(3) },
{ label: `S₂(z=${height}m)`, value: s2.toFixed(3) },
{ label: 'q₀', value: `${q.toFixed(4)} kN/m²` },
],
},
{
title: 'Desprendimento de Vórtices (Sec. 10)',
type: 'grid',
gridItems: [
{ label: 'Forma da seção', value: sectionShape },
{ label: 'Strouhal (St)', value: st.toFixed(3) },
{ label: 'Vcr (Crítica)', value: `${vcr.toFixed(2)} m/s` },
{ label: 'Scruton (Sc)', value: scrutin.toFixed(1) },
{ label: 'Susceptível', value: isVortexSusceptible(scrutin) ? 'Sim' : 'Não' },
{ label: 'Dispensa (Sec 10.2)', value: okVortex ? 'Sim (OK)' : 'Não (REVER)' },
],
},
{
title: 'Conforto Humano (ISO 10137)',
type: 'grid',
gridItems: [
{ label: 'Uso', value: useType === 'residential' ? 'Residencial' : 'Comercial' },
{ label: 'Desloc. uₘₐₓ', value: `${(uMax * 1000).toFixed(2)} mm` },
{ label: 'a_max (Pico)', value: `${aMax.toFixed(4)} m/s²` },
{ label: 'a_lim (Limite)', value: `${comfort.aLim.toFixed(4)} m/s²` },
{ label: 'Status', value: comfort.ok ? 'OK' : 'REVER' },
],
},
];
exportGenericToPDF('Análise Dinâmica', sections);
};
return (
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30 overflow-y-auto">
{/* Coluna Esquerda: Controles */}
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-3 mb-4">
<TabsTrigger value="edificio">Edifício</TabsTrigger>
<TabsTrigger value="vortex">Vórtices</TabsTrigger>
<TabsTrigger value="conforto">Conforto</TabsTrigger>
</TabsList>
<TabsContent value="edificio" className="space-y-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg">Resposta Dinâmica</CardTitle>
<CardDescription>Sec. 9.3 / Tab. 31 e 32</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">Tipo de Estrutura</label>
<Select value={structureType} onValueChange={(v) => setStructureType(v as StructureDynamicType)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="portal-concrete">Aporticada concreto (γ=1,2; ξ=2%)</SelectItem>
<SelectItem value="concrete-shearwall">Concreto c/ cortina (γ=1,0; ξ=2%)</SelectItem>
<SelectItem value="concrete-tower-variable">Torre concreto variável (γ=2,7; ξ=1,5%)</SelectItem>
<SelectItem value="concrete-tower-uniform">Torre concreto uniforme (γ=1,7; ξ=1%)</SelectItem>
<SelectItem value="steel-welded">Aço soldada (γ=1,2; ξ=1%)</SelectItem>
<SelectItem value="steel-tower-uniform">Torre aço uniforme (γ=1,7; ξ=0,8%)</SelectItem>
<SelectItem value="wood">Madeira (γ=; ξ=3%)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-3">
<div className="flex justify-between"><label className="text-sm font-medium">Altura h</label><span className="font-mono text-sm">{height} m</span></div>
<Slider min={10} max={200} step={1} value={[height]} onValueChange={(v) => setHeight(v[0])} />
</div>
<div className="space-y-3">
<div className="flex justify-between"><label className="text-sm font-medium">Frequência f</label><span className="font-mono text-sm">{freq.toFixed(2)} Hz</span></div>
<Slider min={0.1} max={2} step={0.01} value={[freq]} onValueChange={(v) => setFreq(v[0])} />
</div>
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-1">
<div className="flex justify-between"><span>f estimado (Tab. 31):</span><span className="font-mono">{estimatedFreq.toFixed(2)} Hz</span></div>
<div className="flex justify-between"><span>γ:</span><span className="font-mono">{dynParams.gamma}</span></div>
<div className="flex justify-between"><span>ξ:</span><span className="font-mono">{dynParams.xiPercent}%</span></div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="vortex" className="space-y-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg">Desprendimento de Vórtices</CardTitle>
<CardDescription>Sec. 10 Vcr, Scruton, St</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">Forma da Seção</label>
<Select value={sectionShape} onValueChange={(v) => setSectionShape(v as SectionShape)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="circle">Circular (St=0,20)</SelectItem>
<SelectItem value="rectangle-b-a-1-3">Retângulo b/a=1/3</SelectItem>
<SelectItem value="rectangle-b-a-1-2">Retângulo b/a=1/2</SelectItem>
<SelectItem value="rectangle-b-a-1-1">Retângulo b/a=1</SelectItem>
<SelectItem value="rectangle-b-a-1-0-5">Retângulo b/a=2</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-3">
<div className="flex justify-between"><label className="text-sm font-medium">b/a</label><span className="font-mono text-sm">{sectionRatio.toFixed(2)}</span></div>
<Slider min={0.1} max={5} step={0.05} value={[sectionRatio]} onValueChange={(v) => setSectionRatio(v[0])} />
</div>
<div className="space-y-3">
<div className="flex justify-between"><label className="text-sm font-medium">Dim. L</label><span className="font-mono text-sm">{sectionSize} m</span></div>
<Slider min={0.1} max={5} step={0.1} value={[sectionSize]} onValueChange={(v) => setSectionSize(v[0])} />
</div>
<div className="space-y-3">
<label className="text-sm font-medium">m_eq (kg/m)</label>
<Input type="number" value={massEq} onChange={(e) => setMassEq(Number(e.target.value))} />
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="conforto" className="space-y-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg">Conforto Humano</CardTitle>
<CardDescription>Sec. 9.6 ISO 10137</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">Tipo de Uso</label>
<Select value={useType} onValueChange={(v) => setUseType(v as 'residential' | 'commercial')}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="commercial">Comercial / Escritórios</SelectItem>
<SelectItem value="residential">Residencial</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-3">
<div className="flex justify-between"><label className="text-sm font-medium">Desloc. uₘₐₓ</label><span className="font-mono text-sm">{(uMax * 1000).toFixed(2)} mm</span></div>
<Slider min={0.001} max={0.1} step={0.001} value={[uMax]} onValueChange={(v) => setUMax(v[0])} />
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
{/* Coluna Central: Gráficos e 3D */}
<div className="flex-1 flex flex-col gap-4">
{activeTab === 'edificio' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">Perfil de Velocidade</CardTitle>
</CardHeader>
<CardContent>
<DynamicProfileChart q={q} height={height} category={terrainCategory} />
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">Fator Dinâmico ζ</CardTitle>
</CardHeader>
<CardContent>
<DynamicFactorChart category={terrainCategory} height={height} vp={vp} freq={freq} />
</CardContent>
</Card>
</div>
)}
<Card className="flex-1 min-h-[400px]">
<CardHeader className="pb-3">
<CardTitle className="text-base">Visualização 3D</CardTitle>
</CardHeader>
<CardContent className="h-full">
<Dynamics3DViewer
height={height}
freq={freq}
windSpeed={v0 * s1 * s2 * s3}
scruton={scrutin}
sectionShape={sectionShape}
sectionSize={sectionSize}
showVortexStreet={activeTab === 'vortex'}
showModeShape={true}
/>
</CardContent>
</Card>
</div>
{/* Coluna Direita: Resultados */}
<div className="w-full lg:w-96 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
<Tabs value={activeTab} className="w-full">
<TabsList className="hidden" />
<TabsContent value="edificio">
<Card className="shadow-sm border-border">
<CardHeader className="pb-3">
<CardTitle className="text-lg">Parâmetros Dinâmicos</CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-2">
<div className="flex justify-between"><span>Vₚ:</span><span className="font-mono">{vp.toFixed(2)} m/s</span></div>
<div className="flex justify-between"><span>ζ (fator):</span><span className="font-mono">{dynFactor.toFixed(3)}</span></div>
<div className="flex justify-between"><span>S(z={height}m):</span><span className="font-mono">{s2.toFixed(3)}</span></div>
<div className="flex justify-between"><span>q:</span><span className="font-mono">{q.toFixed(4)} kN/m²</span></div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="vortex">
<Card className="shadow-sm border-border">
<CardHeader className="pb-3">
<CardTitle className="text-lg">Análise de Vórtices</CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-2">
<div className="flex justify-between"><span>Strouhal (St):</span><span className="font-mono">{st.toFixed(3)}</span></div>
<div className="flex justify-between"><span>Vcr (Crítica):</span><span className="font-mono font-semibold">{vcr.toFixed(2)} m/s</span></div>
<div className="flex justify-between"><span>Scruton (Sc):</span><span className="font-mono">{scrutin.toFixed(1)}</span></div>
<div className="flex justify-between"><span>Susceptível:</span><span className="font-mono">{isVortexSusceptible(scrutin) ? 'sim' : 'não'}</span></div>
<Separator className="my-2" />
<div className="flex justify-between items-center">
<span>Dispensa (sec 10.2):</span>
<Badge variant={okVortex ? 'default' : 'destructive'}>{okVortex ? 'OK' : 'REVER'}</Badge>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="conforto">
<Card className="shadow-sm border-border">
<CardHeader className="pb-3">
<CardTitle className="text-lg">Verificação de Conforto</CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-2">
<div className="flex justify-between"><span>a_max (Pico):</span><span className="font-mono">{aMax.toFixed(4)} m/s²</span></div>
<div className="flex justify-between"><span>a_lim (Limite):</span><span className="font-mono">{comfort.aLim.toFixed(4)} m/s²</span></div>
<div className="flex justify-between"><span>a / a_lim:</span><span className="font-mono">{comfort.ratio.toFixed(2)}</span></div>
<Separator className="my-2" />
<div className="flex justify-between items-center">
<span>Status:</span>
<Badge variant={comfort.ok ? 'default' : 'destructive'}>{comfort.ok ? 'OK' : 'REVER'}</Badge>
</div>
<p className="text-muted-foreground mt-2">{comfort.description}</p>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
<ExportMenu onExportPDF={handleExportPDF} />
</div>
<SceneCapturePanel />
</div>
</div>
);
};
const DynamicProfileChart: React.FC<{ q: number; height: number; category: 'I' | 'II' | 'III' | 'IV' | 'V' }> = ({ q, height, category }) => {
const points = [];
const { bm, p } = TABLE_32[category];
for (let z = 0; z <= height; z += height / 30) {
const factor = bm * Math.pow(z / height, p);
points.push({ z, value: q * factor });
}
const maxValue = Math.max(...points.map((p) => p.value));
const pathData = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${(p.z / height) * 300},${160 - (p.value / maxValue) * 140}`).join(' ');
return (
<svg viewBox="0 0 320 200" className="w-full h-48">
<line x1="0" y1="160" x2="300" y2="160" stroke="var(--color-border)" strokeWidth="1" />
<line x1="0" y1="0" x2="0" y2="160" stroke="var(--color-border)" strokeWidth="1" />
<path d={pathData} fill="none" stroke="var(--color-primary)" strokeWidth="2" />
<text x="150" y="190" textAnchor="middle" fontSize="11" fill="var(--color-muted-foreground)">z (m) {height.toFixed(0)} m</text>
<text x="10" y="20" fontSize="11" fill="var(--color-muted-foreground)">q(z) (kN/m²)</text>
</svg>
);
};
const DynamicFactorChart: React.FC<{ category: 'I' | 'II' | 'III' | 'IV' | 'V'; height: number; vp: number; freq: number }> = ({ category, height, vp, freq }) => {
const points = [];
for (let xi = 0.5; xi <= 5; xi += 0.5) {
const factor = dynamicFactor({ category, vp, freq, height, xi });
points.push({ xi, factor });
}
const maxFactor = Math.max(...points.map((p) => p.factor), 5);
const pathData = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${((p.xi - 0.5) / 4.5) * 300},${160 - (p.factor / maxFactor) * 140}`).join(' ');
return (
<svg viewBox="0 0 320 200" className="w-full h-48">
<line x1="0" y1="160" x2="300" y2="160" stroke="var(--color-border)" strokeWidth="1" />
<line x1="1" y1="0" x2="1" y2="160" stroke="var(--color-border)" strokeWidth="1" />
<line x1="0" y1={160 - 140 * 1 / maxFactor} x2="300" y2={160 - 140 * 1 / maxFactor} stroke="var(--color-border)" strokeDasharray="4" opacity="0.5" />
<path d={pathData} fill="none" stroke="var(--color-primary)" strokeWidth="2" />
<text x="150" y="190" textAnchor="middle" fontSize="11" fill="var(--color-muted-foreground)">ξ (%)</text>
<text x="10" y="20" fontSize="11" fill="var(--color-muted-foreground)">ζ</text>
</svg>
);
};
export default DynamicsModule;
+137
View File
@@ -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;
+278
View File
@@ -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;
+214
View File
@@ -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;
+191
View File
@@ -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)}` },
{ 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ₐ &lt; 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;
+216
View File
@@ -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;
+190
View File
@@ -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;