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
+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;