737 lines
39 KiB
TypeScript
737 lines
39 KiB
TypeScript
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 > 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;
|