Files
BrainWind/app/src/pages/IsolatedRoofModule.tsx
T

286 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { EducationalManual } from '@/components/EducationalManual';
import { PressureLegend } from '@/components/PressureLegend';
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 text-foreground border border-border/80 backdrop-blur-sm shadow-sm">θ = {theta}°</Badge>
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/80 backdrop-blur-sm shadow-sm">{type === 'shed' ? 'Uma água' : 'Duas águas'}</Badge>
</div>
<div className="absolute top-4 right-4 z-10">
<EducationalManual type="isolated-roof" params={{ theta, type }} />
</div>
<PressureLegend />
<IsolatedRoof3DViewer
type={type}
theta={theta}
height={height}
width={width}
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;