feat: unified 0 and 90 degree PDF envelope and category descriptors
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { useWindStore } from '@/store/appStore';
|
||||
import { calculateSign } from '@/lib/nbr-tables/table-23';
|
||||
import Sign3DViewer from '@/components/three/Sign3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
|
||||
const SignModule: React.FC = () => {
|
||||
const { q } = useWindStore();
|
||||
const [length, setLength] = React.useState(8);
|
||||
const [height, setHeight] = React.useState(3);
|
||||
const [alpha, setAlpha] = React.useState<90 | 50>(90);
|
||||
const [hasEndPlates, setHasEndPlates] = React.useState(true);
|
||||
const [groundClearance, setGroundClearance] = React.useState(0.5);
|
||||
|
||||
const result = useMemo(
|
||||
() =>
|
||||
calculateSign({ length, height, alpha, hasEndPlates, groundClearance }, q),
|
||||
[length, height, alpha, hasEndPlates, groundClearance, q],
|
||||
);
|
||||
|
||||
const handleExportPDF = () => {
|
||||
const sections: GenericPDFSection[] = [
|
||||
{
|
||||
title: 'Geometria do Muro/Placa',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Comprimento (ℓ)', value: `${length} m` },
|
||||
{ label: 'Altura (hₐ)', value: `${height} m` },
|
||||
{ label: 'Distância do solo', value: `${groundClearance} m` },
|
||||
{ label: 'ℓ/hₐ', value: result.lhRatio.toFixed(2) },
|
||||
{ label: 'Placas de extremidade', value: hasEndPlates ? 'Sim' : 'Não' },
|
||||
{ label: 'Ângulo de incidência (α)', value: `${alpha}°` },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Força Resultante',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Área Efetiva', value: `${result.areaEffective.toFixed(1)} m²` },
|
||||
{ label: 'Coeficiente de Força (Cf)', value: result.cf.toFixed(2) },
|
||||
{ label: 'Excentricidade (e)', value: `${result.applicationPoint.toFixed(2)} m` },
|
||||
{ label: 'Força F', value: `${result.forceKN.toFixed(2)} kN` },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Momento de Tombamento (Estimado)',
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Em relação à base da placa', value: `${result.momentBaseKNm.toFixed(2)} kNm` },
|
||||
{ label: 'Em relação ao solo', value: `${result.momentGroundKNm.toFixed(2)} kNm` },
|
||||
],
|
||||
},
|
||||
];
|
||||
exportGenericToPDF('Muros e Placas', sections);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
|
||||
{/* Coluna Esquerda: Controles */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Muros e Placas</CardTitle>
|
||||
<CardDescription>Sec. 7.1 — vento perpendicular à face.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Comprimento (ℓ)</label><span className="font-mono text-sm">{length} m</span></div>
|
||||
<Slider min={2} max={30} step={0.5} value={[length]} onValueChange={(v) => setLength(v[0])} />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Altura (hₐ)</label><span className="font-mono text-sm">{height} m</span></div>
|
||||
<Slider min={1} max={10} step={0.1} value={[height]} onValueChange={(v) => setHeight(v[0])} />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between"><label className="text-sm font-medium">Distância do solo</label><span className="font-mono text-sm">{groundClearance} m</span></div>
|
||||
<Slider min={0} max={5} step={0.1} value={[groundClearance]} onValueChange={(v) => setGroundClearance(v[0])} />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Ângulo de Incidência</label>
|
||||
<Select value={alpha.toString()} onValueChange={(v) => setAlpha(Number(v) as 90 | 50)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="90">90° (perpendicular)</SelectItem>
|
||||
<SelectItem value="50">50° (oblíquo)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Placas de Extremidade</label>
|
||||
<Select value={hasEndPlates ? 'yes' : 'no'} onValueChange={(v) => setHasEndPlates(v === 'yes')}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="yes">Sim (ℓ/hₐ < 60)</SelectItem>
|
||||
<SelectItem value="no">Não (escoamento 2D)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-3 text-xs space-y-1">
|
||||
<div className="flex justify-between"><span>q:</span><span className="font-mono">{q.toFixed(4)} kN/m²</span></div>
|
||||
<div className="flex justify-between"><span>ℓ/hₐ:</span><span className="font-mono">{result.lhRatio.toFixed(2)}</span></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Coluna Central: 3D */}
|
||||
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
||||
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">F = {result.forceKN.toFixed(2)} kN</Badge>
|
||||
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">Cf = {result.cf.toFixed(2)}</Badge>
|
||||
</div>
|
||||
|
||||
<Sign3DViewer
|
||||
length={length}
|
||||
height={height}
|
||||
groundClearance={groundClearance}
|
||||
alpha={alpha}
|
||||
cf={result.cf}
|
||||
forceKN={result.forceKN}
|
||||
applicationPoint={result.applicationPoint}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Coluna Direita: Resultados */}
|
||||
<div className="w-full lg:w-96 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Força Resultante</CardTitle>
|
||||
<CardDescription>F = Cf · q · A</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-muted-foreground text-[10px]">Cf</div>
|
||||
<div className="font-mono font-medium text-lg">{result.cf.toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-muted-foreground text-[10px]">e (m)</div>
|
||||
<div className="font-mono font-medium text-lg">{result.applicationPoint.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-muted-foreground text-[10px]">Área efetiva</div>
|
||||
<div className="font-mono font-medium">{result.areaEffective.toFixed(1)} m²</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-muted-foreground text-[10px]">Força F</div>
|
||||
<div className="font-mono font-medium text-lg">{result.forceKN.toFixed(2)} kN</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<h4 className="text-xs font-semibold uppercase text-muted-foreground">Momento de Tombamento</h4>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-muted-foreground text-[10px]">M (base da placa)</div>
|
||||
<div className="font-mono font-medium">{result.momentBaseKNm.toFixed(2)} kNm</div>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/40 p-2 text-center">
|
||||
<div className="text-muted-foreground text-[10px]">M (solo)</div>
|
||||
<div className="font-mono font-medium text-destructive">{result.momentGroundKNm.toFixed(2)} kNm</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="text-xs text-muted-foreground">
|
||||
A força F atua perpendicularmente ao plano do muro ou placa, com excentricidade e medida a partir do centro geométrico.
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
|
||||
<ExportMenu onExportPDF={handleExportPDF} />
|
||||
</div>
|
||||
|
||||
<SceneCapturePanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignModule;
|
||||
Reference in New Issue
Block a user