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

737 lines
39 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, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Slider } from '@/components/ui/slider';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { useWindStore } from '@/store/appStore';
import { useHydrationStore } from '@/store/hydrationStore';
import {
calculateShelterWind,
calculateShelterWindAllAngles,
type ShelterCondition,
type OpeningPosition,
type ShelterWindAngle,
} from '@/lib/nbr-tables/table-shelters';
import Shelter3D from '@/components/three/Shelter3D';
import { WindFlowGrid2D } from '@/components/WindFlowGrid2D';
import SceneCapturePanel from '../components/SceneCapturePanel';
import ExportMenu from '../components/ExportMenu';
import { EducationalManual } from '@/components/EducationalManual';
import { WindParametersSummary } from '@/components/WindParametersSummary';
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
import { Eye, Box, Layers, ArrowUpRight, CheckCircle2, Wind, AlertTriangle } from 'lucide-react';
import { cn } from '@/lib/utils';
import { usePersistentState } from '@/hooks/usePersistentState';
const conditionLabels: Record<ShelterCondition, string> = {
cond1: '1. Estanque 3 lados (Fundo + Lados)',
cond2: '2. Fundo Fechado + Lados Abertos',
cond3: '3. Fundo Aberto + Lados Fechados (Túnel)',
cond4: '4. Totalmente Livre (4 lados abertos)',
};
const ShelterModule: React.FC = () => {
const { q } = useWindStore();
const [condition, setCondition] = usePersistentState<ShelterCondition>('ShelterModule_condition', 'cond1');
const [windAngle, setWindAngle] = usePersistentState<ShelterWindAngle>('ShelterModule_windAngle', 0);
const [depth, setDepth] = usePersistentState('ShelterModule_depth', 6);
const [width, setWidth] = usePersistentState('ShelterModule_width', 10);
const [height, setHeight] = usePersistentState('ShelterModule_height', 4.8);
const [theta, setTheta] = usePersistentState('ShelterModule_theta', 5);
const [numColumns, setNumColumns] = usePersistentState('ShelterModule_numColumns', 2);
const [backClosure, setBackClosure] = usePersistentState('ShelterModule_backClosure', 75);
const [backRange, setBackRange] = usePersistentState<[number, number]>('ShelterModule_backRange', [0, 75]);
const [leftClosure, setLeftClosure] = usePersistentState('ShelterModule_leftClosure', 100);
const [rightClosure, setRightClosure] = usePersistentState('ShelterModule_rightClosure', 100);
const [surfaceMass, setSurfaceMass] = usePersistentState('ShelterModule_surfaceMass', 15); // kg/m² para lona/telha leve
const [viewMode, setViewMode] = usePersistentState<'3d' | 'elevation' | 'airflow'>('ShelterModule_viewMode', '3d');
const pendingLoad = useHydrationStore((s) => s.pendingLoad);
const clearPendingLoad = useHydrationStore((s) => s.clearPendingLoad);
React.useEffect(() => {
if (pendingLoad && pendingLoad.module === 'shelter') {
const p = pendingLoad.inputs as any;
if (p.condition !== undefined) setCondition(p.condition);
if (p.windAngle !== undefined) setWindAngle(p.windAngle);
if (p.depth !== undefined) setDepth(p.depth);
if (p.width !== undefined) setWidth(p.width);
if (p.height !== undefined) setHeight(p.height);
if (p.theta !== undefined) setTheta(p.theta);
if (p.numColumns !== undefined) setNumColumns(p.numColumns);
if (p.backClosure !== undefined) setBackClosure(p.backClosure);
if (p.leftClosure !== undefined) setLeftClosure(p.leftClosure);
if (p.rightClosure !== undefined) setRightClosure(p.rightClosure);
if (p.surfaceMass !== undefined) setSurfaceMass(p.surfaceMass);
clearPendingLoad();
}
}, [pendingLoad, clearPendingLoad, setCondition, setWindAngle, setDepth, setWidth, setHeight, setTheta, setNumColumns, setBackClosure, setLeftClosure, setRightClosure, setSurfaceMass]);
// Determinação automática da posição da fresta/vão a partir dos cursores (base e topo)
const bottomGap = backRange[0];
const topGap = 100 - backRange[1];
const openingPos: OpeningPosition =
topGap > 2 && topGap >= bottomGap
? 'top'
: bottomGap > 2 && bottomGap > topGap
? 'bottom'
: 'distributed';
const handleConditionChange = (cond: ShelterCondition) => {
setCondition(cond);
if (cond === 'cond1') {
setBackRange([0, 100]);
setBackClosure(100);
setLeftClosure(100);
setRightClosure(100);
} else if (cond === 'cond2') {
setBackRange([0, 100]);
setBackClosure(100);
setLeftClosure(0);
setRightClosure(0);
} else if (cond === 'cond3') {
setBackRange([0, 0]);
setBackClosure(0);
setLeftClosure(100);
setRightClosure(100);
} else if (cond === 'cond4') {
setBackRange([0, 0]);
setBackClosure(0);
setLeftClosure(0);
setRightClosure(0);
}
};
const { result, envelope } = useMemo(() => {
const input = {
condition,
depth,
width,
height,
theta,
backClosure,
backRange,
leftClosure,
rightClosure,
openingPos,
numColumns,
};
return {
result: calculateShelterWind({ ...input, windAngle }, q),
envelope: calculateShelterWindAllAngles(input, q),
};
}, [condition, q, depth, width, height, theta, backClosure, backRange, leftClosure, rightClosure, openingPos, windAngle, numColumns]);
const effAreaRoof = depth * width;
const effAreaBack = (width * height) * (backClosure / 100);
const effAreaSideTotal = (depth * height) * ((leftClosure + rightClosure) / 100);
const gravityForce = (surfaceMass * effAreaRoof * 9.81) / 1000; // in kN
const isUpliftCritical = envelope.criticalUp.value > gravityForce;
const handleExportPDF = () => {
const angleLabel = windAngle === 0 ? '0° (Frontal)' : windAngle === 45 ? '45° (Oblíquo)' : '90° (Lateral)';
const sections: GenericPDFSection[] = [
{
title: 'Geometria, Inclinação e Fechamentos do Abrigo',
type: 'grid',
gridItems: [
{ label: 'Comprimento (d / balanço)', value: `${depth.toFixed(1)} m` },
{ label: 'Largura (b)', value: `${width.toFixed(1)} m` },
{ label: 'Altura do Pilar (h)', value: `${height.toFixed(1)} m` },
{ label: 'Inclinação da Cobertura (θ)', value: `${theta}°` },
{ label: 'Condição NBR 6123 (Sec. 6.3)', value: condition },
{ label: 'Fechamento Parede de Fundo', value: `${backClosure}%` },
{ label: 'Fresta Parede de Fundo', value: backRange ? `${backRange[0]}% a ${backRange[1]}%` : 'Sem fresta' },
{ label: 'Fechamento Lateral Esq / Dir', value: `${leftClosure}% / ${rightClosure}%` },
],
},
{
title: `Coeficientes Aerodinâmicos e Pressões Líquidas - Direção ${angleLabel}`,
type: 'grid',
gridItems: [
{ label: 'Ce Médio (Cobertura)', value: result.cpeTop },
{ label: 'Ce Borda (Pico Aerodinâmico)', value: result.cpeEdgeTop },
{ label: 'Cpi Inferior (Sobrepressão sob Telhado)', value: result.cpiBottom },
{ label: 'Cnf Médio Resultante', value: result.cnfVertical },
{ label: 'Cnf Borda Resultante', value: result.cnfEdgeVertical },
{ label: 'Cf Parede de Fundo (Tab. 23 / 6)', value: result.cfBack },
{ label: 'Cf Paredes Laterais', value: result.cfSide },
],
},
{
title: `Pressões Distribuídas por Área de Incidência - Direção ${angleLabel} (kN/m² e kgf/m²)`,
type: 'grid',
gridItems: [
{
label: 'Pressão na Cobertura (Fz / A_telhado)',
value: `${(result.forces.fzUp / effAreaRoof).toFixed(2)} kN/m²`,
},
{
label: 'Pressão na Parede Traseira (Fx / A_fundo)',
value: effAreaBack > 0 ? `${(result.forces.fxBack / effAreaBack).toFixed(2)} kN/m²` : '0.00 kN/m² (0% fechado)',
},
{
label: 'Pressão nas Paredes Laterais (Fy / A_lat)',
value: effAreaSideTotal > 0 ? `${(result.forces.fySide / effAreaSideTotal).toFixed(2)} kN/m²` : '0.00 kN/m² (0% fechado)',
},
{
label: 'Pressão Dinâmica Base do Vento (q)',
value: `${q.toFixed(2)} kN/m²`,
},
{
label: 'Área da Cobertura (Telhado)',
value: `${effAreaRoof.toFixed(1)} m² (100%)`,
},
{
label: 'Áreas Efetivas Parede Fundo / Laterais',
value: `${effAreaBack.toFixed(1)} m² (${backClosure}%) / ${effAreaSideTotal.toFixed(1)} m² (Esq ${leftClosure}% + Dir ${rightClosure}%)`,
},
],
},
{
title: `Demonstração dos Passos Matemáticos — Pressões Distribuídas por m² (Direção ${angleLabel})`,
type: 'math-card',
mathSteps: [
{
title: '1. Pressão na Cobertura (Fz / A_telhado)',
formula: 'p_telhado = q · |Cnf_vertical| ou p_telhado = Fz / (d · b)',
calculation: `${q.toFixed(2)} kN/m² · |${result.cnfVertical}| = ${(result.forces.fzUp / effAreaRoof).toFixed(2)} kN/m² (onde A_telhado = ${depth.toFixed(1)}m × ${width.toFixed(1)}m = ${effAreaRoof.toFixed(1)}m²)`,
result: `${(result.forces.fzUp / effAreaRoof).toFixed(2)} kN/m²`,
note: 'A solicitação de arrancamento atua uniformemente distribuída na projeção horizontal da cobertura.',
},
{
title: '2. Pressão na Parede Traseira (Fx / A_efetiva,fundo)',
formula: 'p_fundo = q · Cf_parede ou p_fundo = Fx / [ (b · h) · (φ_fundo) ]',
calculation: effAreaBack > 0
? `${q.toFixed(2)} kN/m² · ${result.cfBack} = ${(result.forces.fxBack / effAreaBack).toFixed(2)} kN/m² (área de incidência sólida = ${effAreaBack.toFixed(1)}m² para ${backClosure}% fechado)`
: `Parede 0% fechada (Sem fechamento traseiro) -> A_efetiva = 0.0 m² e Fx = 0.00 kN`,
result: effAreaBack > 0 ? `${(result.forces.fxBack / effAreaBack).toFixed(2)} kN/m²` : '0.00 kN/m² (0% fechado)',
note: 'O coeficiente de força e a área são proporcionais à taxa de fechamento da parede de fundo.',
},
{
title: '3. Pressão nas Paredes Laterais (Fy / A_efetiva,laterais)',
formula: 'p_lateral = q · Cf_lateral ou p_lateral = Fy / [ 2 · (d · h) · (φ_médio) ]',
calculation: effAreaSideTotal > 0
? `${q.toFixed(2)} kN/m² · ${result.cfSide} = ${(result.forces.fySide / effAreaSideTotal).toFixed(2)} kN/m² (área efetiva = ${effAreaSideTotal.toFixed(1)}m²)`
: `Sem paredes laterais selecionadas (0% esq + 0% dir) -> A_efetiva = 0.0 m² e Fy = 0.00 kN`,
result: effAreaSideTotal > 0 ? `${(result.forces.fySide / effAreaSideTotal).toFixed(2)} kN/m²` : '0.00 kN/m² (0% fechado)',
note: effAreaSideTotal > 0
? 'A solicitação lateral é distribuída sobre a área efetiva sólida existente nas paredes esq/dir.'
: 'Como nenhuma parede lateral foi adicionada (0%), a área de incidência é nula (0,0 m²) e a pressão atuante é zero.',
},
{
title: '4. Pressão Dinâmica Base do Vento (q)',
formula: 'q = 0,613 · Vk² [N/m²] = 0,000613 · Vk² [kN/m²] (NBR 6123 Sec. 4.2)',
calculation: `0,613 · (${Math.sqrt((q * 1000) / 0.613).toFixed(1)} m/s)² / 1000 = ${q.toFixed(2)} kN/m²`,
result: `${q.toFixed(2)} kN/m²`,
note: 'Pressão dinâmica de referência calculada com base na velocidade característica Vk no nível superior do pilar.',
},
],
},
{
title: 'Envoltória e Comparativo Normativo — As 3 Incidências de Vento (0° • 45° • 90°)',
type: 'table',
tableHeaders: ['Parâmetro / Força', '0° (Frontal)', '45° (Oblíquo)', '90° (Lateral)', 'Envoltória / Crítico'],
tableRows: [
[
'Ce Superior (Cobertura)',
String(envelope.res0.cpeTop),
String(envelope.res45.cpeTop),
String(envelope.res90.cpeTop),
`Máx: ${Math.min(envelope.res0.cpeTop, envelope.res45.cpeTop, envelope.res90.cpeTop)} (Sucção)`
],
[
'Cpi Inferior (Sob Telhado)',
String(envelope.res0.cpiBottom),
String(envelope.res45.cpiBottom),
String(envelope.res90.cpiBottom),
`Máx: +${Math.max(envelope.res0.cpiBottom, envelope.res45.cpiBottom, envelope.res90.cpiBottom).toFixed(2)}`
],
[
'Cnf Vertical Médio | Borda',
`${envelope.res0.cnfVertical} | ${envelope.res0.cnfEdgeVertical}`,
`${envelope.res45.cnfVertical} | ${envelope.res45.cnfEdgeVertical}`,
`${envelope.res90.cnfVertical} | ${envelope.res90.cnfEdgeVertical}`,
`Crítico Borda: ${Math.min(envelope.res0.cnfEdgeVertical, envelope.res45.cnfEdgeVertical, envelope.res90.cnfEdgeVertical)}`
],
[
'Força Vertical Telhado [Fz ↑]',
`${envelope.res0.forces.fzUp} kN`,
`${envelope.res45.forces.fzUp} kN`,
`${envelope.res90.forces.fzUp} kN`,
`${envelope.criticalUp.value} kN (${envelope.criticalUp.angle}°)`
],
[
'Empuxo Parede Traseira [Fx →]',
`${envelope.res0.forces.fxBack} kN`,
`${envelope.res45.forces.fxBack} kN`,
`${envelope.res90.forces.fxBack} kN`,
`${envelope.criticalFx.value} kN (${envelope.criticalFx.angle}°)`
],
[
'Empuxo Paredes Laterais [Fy ↗]',
`${envelope.res0.forces.fySide} kN`,
`${envelope.res45.forces.fySide} kN`,
`${envelope.res90.forces.fySide} kN`,
`${envelope.criticalFy.value} kN (${envelope.criticalFy.angle}°)`
],
[
' ↳ Pressão Cobertura (kN/m²)',
`${(envelope.res0.forces.fzUp / effAreaRoof).toFixed(2)} kN/m²`,
`${(envelope.res45.forces.fzUp / effAreaRoof).toFixed(2)} kN/m²`,
`${(envelope.res90.forces.fzUp / effAreaRoof).toFixed(2)} kN/m²`,
`${(envelope.criticalUp.value / effAreaRoof).toFixed(2)} kN/m² (${envelope.criticalUp.angle}°)`
],
[
' ↳ Pressão Parede Fundo (kN/m²)',
effAreaBack > 0 ? `${(envelope.res0.forces.fxBack / effAreaBack).toFixed(2)} kN/m²` : '0.00 kN/m²',
effAreaBack > 0 ? `${(envelope.res45.forces.fxBack / effAreaBack).toFixed(2)} kN/m²` : '0.00 kN/m²',
effAreaBack > 0 ? `${(envelope.res90.forces.fxBack / effAreaBack).toFixed(2)} kN/m²` : '0.00 kN/m²',
effAreaBack > 0 ? `${(envelope.criticalFx.value / effAreaBack).toFixed(2)} kN/m² (${envelope.criticalFx.angle}°)` : '0.00 kN/m²'
],
[
' ↳ Pressão Paredes Lat. (kN/m²)',
effAreaSideTotal > 0 ? `${(envelope.res0.forces.fySide / effAreaSideTotal).toFixed(2)} kN/m²` : '0.00 kN/m²',
effAreaSideTotal > 0 ? `${(envelope.res45.forces.fySide / effAreaSideTotal).toFixed(2)} kN/m²` : '0.00 kN/m²',
effAreaSideTotal > 0 ? `${(envelope.res90.forces.fySide / effAreaSideTotal).toFixed(2)} kN/m²` : '0.00 kN/m²',
effAreaSideTotal > 0 ? `${(envelope.criticalFy.value / effAreaSideTotal).toFixed(2)} kN/m² (${envelope.criticalFy.angle}°)` : '0.00 kN/m²'
],
[
'Carga Horiz. no Pilar',
`${envelope.res0.lineLoads.columnLoad} kN/m`,
`${envelope.res45.lineLoads.columnLoad} kN/m`,
`${envelope.res90.lineLoads.columnLoad} kN/m`,
`${envelope.criticalColumnLoad.value} kN/m (${envelope.criticalColumnLoad.angle}°)`
],
[
'Carga Vert. na Viga',
`${envelope.res0.lineLoads.beamLoad} kN/m`,
`${envelope.res45.lineLoads.beamLoad} kN/m`,
`${envelope.res90.lineLoads.beamLoad} kN/m`,
`${Math.max(envelope.res0.lineLoads.beamLoad, envelope.res45.lineLoads.beamLoad, envelope.res90.lineLoads.beamLoad).toFixed(2)} kN/m`
],
[
'Momento na Raiz da Viga',
`${envelope.res0.lineLoads.beamMoment} kN.m`,
`${envelope.res45.lineLoads.beamMoment} kN.m`,
`${envelope.res90.lineLoads.beamMoment} kN.m`,
`${Math.max(envelope.res0.lineLoads.beamMoment, envelope.res45.lineLoads.beamMoment, envelope.res90.lineLoads.beamMoment).toFixed(2)} kN.m`
],
],
},
{
title: `Forças Resultantes no Pórtico (${angleLabel}) (kN)`,
type: 'grid',
gridItems: [
{ label: 'Força Vertical no Telhado [Fz ↑]', value: `${result.forces.fzUp} kN` },
{ label: 'Empuxo na Parede Traseira [Fx →]', value: `${result.forces.fxBack} kN` },
{ label: 'Empuxo nas Paredes Laterais [Fy ↗]', value: `${result.forces.fySide} kN` },
{ label: 'Força de Atrito no Telhado [F_atrito]', value: `${result.forces.fFriction} kN` },
],
},
{
title: `Cargas Lineares para Dimensionamento (${angleLabel}) (kN/m)`,
type: 'grid',
gridItems: [
{ label: 'Carga Horiz. Pórtico Central', value: `${result.lineLoads.columnLoad} kN/m` },
{ label: 'Carga Horiz. Extremidade', value: `${result.lineLoads.columnLoadEdge} kN/m` },
{ label: 'Carga Vert. Média Viga Central', value: `${result.lineLoads.beamLoad} kN/m` },
{ label: 'Carga Vert. Média Extremidade', value: `${result.lineLoads.beamLoadEdge} kN/m` },
{ label: 'Momento Engaste Viga Central', value: `${result.lineLoads.beamMoment} kN.m` },
{ label: 'Momento Engaste Extremidade', value: `${result.lineLoads.beamMomentEdge} kN.m` },
],
},
{
title: 'Parecer Técnico e Alertas Aerodinâmicos (Zonas de Borda)',
type: 'text',
content: `${result.statusText} Permeabilidade líquida da parede de fundo: ${100 - backClosure}%. Alívio de arrancamento obtido pela fresta superior: ${result.reliefPercentage}%.
⚠️ ATENÇÃO ESTRUTURAL (Efeito Asa de Avião): O coeficiente global médio subestima a força de arrancamento na ponta do balanço. Os cálculos acima consideram as zonas de borda com coeficientes extremos (Cnf Borda = ${result.cnfEdgeVertical}). O Momento Fletor na raiz da viga foi calculado considerando que o centro de pressão está deslocado para próximo à extremidade livre (L/3), o que resulta num momento de engaste substancialmente maior que o de uma carga uniformemente distribuída simples.`,
},
{
title: 'Guia de Aplicação dos Eixos e Resultantes no Croqui',
type: 'table',
tableHeaders: ['Eixo / Componente', 'Símbolo', 'Sentido e Aplicação no Croqui', 'Carga Distribuída'],
tableRows: [
[
'Eixo Z (Vertical)',
'Fz (↑)',
'Perpendicular ao telhado. Valor (+) indica sucção para cima (arrancamento); (-) indica carga descendente.',
`${result.lineLoads.beamLoad} kN/m na viga`
],
[
'Eixo X (Longitudinal)',
'Fx (→)',
'Na direção do vento (frente -> fundo). Atua como empuxo horizontal contra a parede traseira.',
`${result.lineLoads.columnLoad} kN/m no pilar`
],
[
'Eixo Y (Transversal)',
'Fy (↗)',
'Perpendicular ao vento. Atua horizontalmente contra o fechamento das paredes laterais.',
'—'
],
],
},
];
exportGenericToPDF('Abrigo / Pórtico Fechado', 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 do Abrigo */}
<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 flex flex-row items-start justify-between space-y-0 pr-6">
<div>
<CardTitle className="text-lg flex items-center gap-2">
<Layers className="size-5 text-primary" />
Abrigos e Pórticos
</CardTitle>
<CardDescription>Pórticos em balanço (Tab. 23-25 e sec. 6.3).</CardDescription>
</div>
<SaveModuleDialog
moduleType="shelter"
inputs={{ condition, depth, width, height, theta, backClosure, leftClosure, rightClosure, openingPos, windAngle, surfaceMass, numColumns }}
/>
</CardHeader>
<CardContent className="space-y-5">
<WindParametersSummary />
<div className="space-y-2">
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Condições Topológicas
</label>
<div className="flex flex-col gap-1.5">
{(['cond1', 'cond2', 'cond3', 'cond4'] as ShelterCondition[]).map((c) => (
<Button
key={c}
type="button"
variant={condition === c ? 'default' : 'outline'}
size="sm"
className="h-auto py-2 px-2.5 text-xs text-left justify-start font-normal leading-tight w-full"
onClick={() => handleConditionChange(c)}
>
<CheckCircle2 className={cn("size-3.5 mr-2 shrink-0", condition === c ? "text-primary-foreground" : "text-muted-foreground")} />
<span className="truncate">{conditionLabels[c]}</span>
</Button>
))}
</div>
</div>
<div className="space-y-2 pt-1 border-t">
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Incidência do Vento (Direção)
</label>
<div className="grid grid-cols-3 gap-1.5">
{([0, 45, 90] as ShelterWindAngle[]).map((ang) => (
<Button
key={ang}
type="button"
variant={windAngle === ang ? 'default' : 'outline'}
size="sm"
className="h-9 px-1.5 text-xs font-medium flex flex-col items-center justify-center leading-none"
onClick={() => setWindAngle(ang)}
>
<span className="font-bold">{ang}°</span>
<span className="text-[10px] opacity-80 mt-0.5">{ang === 0 ? 'Frontal' : ang === 45 ? 'Oblíquo' : 'Lateral'}</span>
</Button>
))}
</div>
</div>
<div className="space-y-3 pt-1 border-t">
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Permeabilidade / Fechamento (%)
</label>
<div className="space-y-1.5">
<div className="flex justify-between items-center text-xs">
<span>Parede de Fundo:</span>
<span className="font-mono font-medium">
{backClosure}% {backRange[0] > 0 || backRange[1] < 100 ? `(Base ${backRange[0]}% | Topo ${100 - backRange[1]}%)` : ''}
</span>
</div>
<Slider
min={0}
max={100}
step={5}
value={backRange}
onValueChange={(val) => {
if (Array.isArray(val) && val.length === 2) {
const [start, end] = val;
if (end >= start) {
setBackRange([start, end]);
setBackClosure(end - start);
}
}
}}
/>
</div>
<div className="grid grid-cols-2 gap-3 pt-1">
<div className="space-y-1">
<div className="flex justify-between text-[11px]">
<span>Lateral Esq:</span>
<span className="font-mono">{leftClosure}%</span>
</div>
<Slider min={0} max={100} step={10} value={[leftClosure]} onValueChange={(v) => setLeftClosure(v[0])} />
</div>
<div className="space-y-1">
<div className="flex justify-between text-[11px]">
<span>Lateral Dir:</span>
<span className="font-mono">{rightClosure}%</span>
</div>
<Slider min={0} max={100} step={10} value={[rightClosure]} onValueChange={(v) => setRightClosure(v[0])} />
</div>
</div>
</div>
<div className="space-y-3 pt-1 border-t">
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Dimensões e Inclinação
</label>
<div className="space-y-2">
<div className="flex justify-between text-xs">
<span>Vão do Balanço (d):</span>
<span className="font-mono font-medium">{depth.toFixed(1)} m</span>
</div>
<Slider min={2} max={15} step={0.1} value={[depth]} onValueChange={(v) => setDepth(v[0])} />
</div>
<div className="space-y-2">
<div className="flex justify-between text-xs">
<span>Altura do Pilar (h):</span>
<span className="font-mono font-medium">{height.toFixed(1)} m</span>
</div>
<Slider min={2.5} max={12} step={0.2} value={[height]} onValueChange={(v) => setHeight(v[0])} />
</div>
<div className="space-y-2">
<div className="flex justify-between text-xs">
<span>Largura do Abrigo (b):</span>
<span className="font-mono font-medium">{width.toFixed(1)} m</span>
</div>
<Slider min={4} max={40} step={0.2} value={[width]} onValueChange={(v) => setWidth(v[0])} />
</div>
<div className="space-y-2">
<div className="flex justify-between text-xs">
<span>Inclinação Cobertura (θ):</span>
<span className="font-mono font-medium">{theta}°</span>
</div>
<Slider min={0} max={30} step={1} value={[theta]} onValueChange={(v) => setTheta(v[0])} />
</div>
<div className="space-y-2">
<div className="flex justify-between text-xs">
<span>Número de Pórticos (Pilares):</span>
<span className="font-mono font-medium">{numColumns} pórticos</span>
</div>
<Slider min={2} max={20} step={1} value={[numColumns]} onValueChange={(v) => setNumColumns(v[0])} />
</div>
<div className="space-y-2 pt-2 border-t">
<div className="flex justify-between text-xs">
<span>Massa Superficial (kg/m²):</span>
<span className="font-mono font-medium">{surfaceMass} kg/m²</span>
</div>
<Slider min={2} max={100} step={1} value={[surfaceMass]} onValueChange={(v) => setSurfaceMass(v[0])} />
<p className="text-[10px] text-muted-foreground mt-1">Lonas ~3kg/m², Telhas Metálicas ~10-15kg/m²</p>
{isUpliftCritical && (
<div className="mt-2 p-2 bg-destructive/10 border border-destructive/20 rounded text-xs text-destructive flex gap-2 items-start">
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5" />
<p>Alerta de Arrancamento: Força () {envelope.criticalUp.value.toFixed(1)} kN &gt; Peso ({gravityForce.toFixed(1)} kN). Exige ancoragem/estaiamento.</p>
</div>
)}
</div>
</div>
</CardContent>
</Card>
</div>
<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 flex-wrap items-center gap-2">
<Badge variant="secondary" className="bg-primary/10 text-primary border border-primary/20 backdrop-blur-sm font-mono font-bold">
Vento: {windAngle}° ({windAngle === 0 ? 'Frontal' : windAngle === 45 ? 'Oblíquo' : 'Lateral'})
</Badge>
<Badge variant="secondary" className="bg-background/85 text-foreground border border-border backdrop-blur-sm font-mono">
θ = {theta}°
</Badge>
<Badge variant="secondary" className="bg-background/85 text-foreground border border-border backdrop-blur-sm">
Fundo: {backClosure}% ({openingPos === 'top' ? 'Fresta Topo' : openingPos === 'bottom' ? 'Vão Base' : 'Distrib.'})
</Badge>
{result.reliefPercentage > 0 && (
<Badge className="bg-emerald-600/90 hover:bg-emerald-600 text-white border-none shadow-sm flex items-center gap-1">
<ArrowUpRight className="size-3.5" />
Alívio Arrancamento: -{result.reliefPercentage}%
</Badge>
)}
<div className="bg-background/90 border border-border rounded-full p-0.5 shadow-sm flex items-center ml-1">
<Button size="sm" variant={viewMode === '3d' ? 'default' : 'ghost'} className="h-6 px-2.5 text-xs rounded-full" onClick={() => setViewMode('3d')}>
<Box className="size-3 mr-1" /> Vista 3D
</Button>
<Button size="sm" variant={viewMode === 'airflow' ? 'default' : 'ghost'} className="h-6 px-2.5 text-xs rounded-full" onClick={() => setViewMode('airflow')}>
<Wind className="size-3 mr-1" /> Fluxo
</Button>
<Button size="sm" variant={viewMode === 'elevation' ? 'default' : 'ghost'} className="h-6 px-2.5 text-xs rounded-full" onClick={() => setViewMode('elevation')}>
<Eye className="size-3 mr-1" /> Corte A-A
</Button>
</div>
</div>
<div className="absolute top-4 right-4 z-10">
<EducationalManual type="shelter" params={{ condition, backClosure, openingPos, theta, windAngle }} />
</div>
{viewMode === '3d' || viewMode === 'airflow' ? (
<Shelter3D
condition={condition}
depth={depth}
width={width}
height={height}
theta={theta}
backClosure={backClosure}
backRange={backRange}
leftClosure={leftClosure}
rightClosure={rightClosure}
openingPos={openingPos}
windAngle={windAngle}
numColumns={numColumns}
viewMode={viewMode}
/>
) : (
<div className="w-full h-full flex items-center justify-center p-6 bg-slate-950 overflow-auto">
<div className="max-w-4xl w-full">
<WindFlowGrid2D type="shelter" params={{ condition, backClosure, openingPos, theta, windAngle }} />
</div>
</div>
)}
</div>
<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-4">
<CardTitle className="text-base flex items-center justify-between">
Forças ({windAngle}°)
<Badge variant="secondary" className="font-mono">{result.forces.fzUp.toFixed(1)} kN (Arrancamento)</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4 text-sm">
<div className="space-y-2 border-b pb-3">
<div className="flex justify-between items-center">
<span className="text-muted-foreground">F. Vertical [Fz - Cobertura]</span>
<span className="font-mono font-bold text-foreground">{result.forces.fzUp.toFixed(2)} kN</span>
</div>
<div className="flex justify-between items-center text-xs text-muted-foreground">
<span>Cnf Médio | Cnf Borda</span>
<span className="font-mono">{result.cnfVertical} | <span className="text-destructive font-bold">{result.cnfEdgeVertical}</span></span>
</div>
<div className="flex justify-between items-center text-[11px] text-muted-foreground/80">
<span>Pressão (/m²)</span>
<span className="font-mono">{(result.forces.fzUp / effAreaRoof).toFixed(2)} kN/m² ({(result.forces.fzUp / effAreaRoof * 101.97).toFixed(1)} kgf/m²)</span>
</div>
</div>
<div className="space-y-2 border-b pb-3">
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Empuxo Traseiro [Fx - Parede]</span>
<span className="font-mono font-bold text-foreground">{result.forces.fxBack.toFixed(2)} kN</span>
</div>
<div className="flex justify-between items-center text-xs text-muted-foreground">
<span>Cf / Estanqueidade</span>
<span className="font-mono">Cf = {result.cfBack} ({backClosure}%)</span>
</div>
<div className="flex justify-between items-center text-[11px] text-muted-foreground/80">
<span>Pressão (/m²)</span>
<span className="font-mono">
{effAreaBack > 0
? `${(result.forces.fxBack / effAreaBack).toFixed(2)} kN/m² (${(result.forces.fxBack / effAreaBack * 101.97).toFixed(1)} kgf/m²)`
: '0.00 kN/m² (0% fechado)'}
</span>
</div>
</div>
<div className="space-y-1 border-b pb-2">
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Empuxo Lateral [Fy - Paredes]</span>
<span className="font-mono font-bold text-foreground">{result.forces.fySide.toFixed(2)} kN</span>
</div>
<div className="flex justify-between items-center text-[11px] text-muted-foreground/80">
<span>Pressão (/m²)</span>
<span className="font-mono">
{effAreaSideTotal > 0
? `${(result.forces.fySide / effAreaSideTotal).toFixed(2)} kN/m² (${(result.forces.fySide / effAreaSideTotal * 101.97).toFixed(1)} kgf/m²)`
: '0.00 kN/m² (0% fechado)'}
</span>
</div>
</div>
<div className="flex justify-between items-center pb-2 border-b">
<span className="text-primary font-medium">Carga no Pilar (Central)</span>
<span className="font-mono font-bold text-emerald-600 dark:text-emerald-400">{result.lineLoads.columnLoad.toFixed(2)} kN/m</span>
</div>
<div className="flex justify-between items-center pt-2">
<span className="text-primary font-medium">Carga Vertical Viga (Central)</span>
<span className="font-mono font-bold text-sky-600 dark:text-sky-400">{result.lineLoads.beamLoad.toFixed(2)} kN/m</span>
</div>
<div className="flex justify-between items-center pb-3 border-b text-xs text-muted-foreground">
<span>Momento Engaste (Asa)</span>
<span className="font-mono text-destructive font-bold">{result.lineLoads.beamMoment.toFixed(2)} kN.m</span>
</div>
<div className="pt-1 space-y-2">
<div className="font-bold text-xs text-primary flex items-center justify-between">
<span>Envoltória 3 Incidências (0° 45° 90°)</span>
<Badge variant="outline" className="text-[10px] font-mono">Crítico: {envelope.criticalUp.angle}°</Badge>
</div>
<div className="grid grid-cols-4 gap-1 text-[11px] bg-muted/40 p-2.5 rounded-md border">
<div className="font-semibold text-muted-foreground">Direção</div>
<div className="font-semibold text-right">Fz ()</div>
<div className="font-semibold text-right">Fx ()</div>
<div className="font-semibold text-right">Fy ()</div>
<div className={cn("font-mono", windAngle === 0 && "font-bold text-primary")}>0° Frontal</div>
<div className="font-mono text-right">{envelope.res0.forces.fzUp.toFixed(1)}</div>
<div className="font-mono text-right">{envelope.res0.forces.fxBack.toFixed(1)}</div>
<div className="font-mono text-right">{envelope.res0.forces.fySide.toFixed(1)}</div>
<div className={cn("font-mono", windAngle === 45 && "font-bold text-primary")}>45° Oblíquo</div>
<div className="font-mono text-right">{envelope.res45.forces.fzUp.toFixed(1)}</div>
<div className="font-mono text-right">{envelope.res45.forces.fxBack.toFixed(1)}</div>
<div className="font-mono text-right">{envelope.res45.forces.fySide.toFixed(1)}</div>
<div className={cn("font-mono", windAngle === 90 && "font-bold text-primary")}>90° Lateral</div>
<div className="font-mono text-right">{envelope.res90.forces.fzUp.toFixed(1)}</div>
<div className="font-mono text-right">{envelope.res90.forces.fxBack.toFixed(1)}</div>
<div className="font-mono text-right">{envelope.res90.forces.fySide.toFixed(1)}</div>
</div>
</div>
<div className="pt-2 space-y-2">
<div className="font-bold text-sm text-primary">Parâmetros Adicionais (NBR 6123)</div>
<div className="grid grid-cols-1 gap-2 bg-muted/30 p-3 rounded-md border text-xs">
<div className="flex justify-between text-muted-foreground">
<span>Permeabilidade (μ):</span>
<span className="font-mono font-medium text-foreground">{100 - backClosure}%</span>
</div>
<div className="flex justify-between text-muted-foreground">
<span>Alívio por Fresta Superior:</span>
<span className="font-mono font-medium text-emerald-600 dark:text-emerald-400">{result.reliefPercentage}%</span>
</div>
<div className="flex justify-between text-muted-foreground">
<span>Força de Atrito (Ff):</span>
<span className="font-mono font-medium text-foreground">{result.forces.fFriction.toFixed(2)} kN</span>
</div>
</div>
<p className="text-[11px] text-muted-foreground leading-tight">
* Para a visualização didática dos esquemas aerodinâmicos e linhas de corrente, utilize a aba <strong>Corte A-A</strong> no painel central.
</p>
</div>
</CardContent>
</Card>
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
<div className="flex flex-col items-end gap-2">
<ExportMenu onExportPDF={handleExportPDF} />
</div>
</div>
<SceneCapturePanel />
</div>
</div>
);
};
export default ShelterModule;