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