feat: unified 0 and 90 degree PDF envelope and category descriptors
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user