diff --git a/PLANO_NORMAS_INTERNACIONAIS.md b/PLANO_NORMAS_INTERNACIONAIS.md new file mode 100644 index 0000000..614769f --- /dev/null +++ b/PLANO_NORMAS_INTERNACIONAIS.md @@ -0,0 +1,159 @@ +# 🌍 PLANO ARQUITETURAL DE EXPANSÃO: NORMAS INTERNACIONAIS DE VENTO & BENCHMARKING MULTI-NORMA + +> **Documento de Visão Técnica, Segurança Arquitetural e Roadmap Futuro** +> **Projeto:** VentoApp / BrainWind — Engenharia de Vento Avançada +> **Estado Atual (Julho/2026):** Domínio completo da NBR 6123:2023 (100% coberta, 36 tabelas, testes unitários travados, visualizador 3D interativo, cálculo puro determinístico). + +--- + +## 1. A Visão e O Sonho Técnico + +Transformar o **BrainWind / VentoApp** em mais do que o melhor software brasileiro de cálculo de ações do vento: elevá-lo ao status de **Plataforma de Auditoria e Benchmarking Internacional de Engenharia de Vento**. + +O objetivo é permitir que engenheiros estruturais, peritos e pesquisadores possam: +1. Projetar estruturas segundo normas internacionais (**Eurocode 1 — EN 1991-1-4**, **ASCE 7-22 — EUA**, **AS/NZS 1170.2 — Austrália/Nova Zelândia**). +2. Confrontar em **tempo real e lado a lado** os esforços gerados pela **NBR 6123:2023** contra 2, 3 ou 4 normas mundiais simultaneamente para a mesma geometria. +3. Entender com clareza aerodinâmica e visual onde cada norma é mais conservadora ou mais econômica. + +--- + +## 2. A Premissa Sagrada: Proteção do "Coração Intocável" (NBR 6123:2023) + +O maior risco de softwares estruturais em expansão é a **regressão funcional** — quebrar o que já está estável, testado e confiável para acomodar novos requisitos. Para que esse risco seja **ZERO**, estabelecemos o seguinte pacto arquitetural: + +### 2.1. Princípio Aberto-Fechado (SOLID — *Open-Closed Principle*) +* O código atual de cálculo da NBR 6123 (`src/lib/nbr-tables/`, `src/lib/modules/`, `src/lib/wind-kernel.ts`) permanece **estritamente fechado para modificações**. +* Nenhuma rotina, tabela ou constante existente da NBR 6123 precisará ou deverá ser editada para acomodar uma norma estrangeira. +* Toda nova norma será implementada como um **pacote independente e isolado** em diretórios dedicados (`src/lib/eurocode-tables/`, `src/lib/asce-tables/`, `src/lib/standards-drivers/`). + +### 2.2. NBR 6123 como Padrão Intocável (*Default Root*) +* Ao iniciar o aplicativo ou carregar um projeto brasileiro, o motor rodará exclusivamente no fluxo nativo da NBR 6123, preservando a experiência de uso de 100% dos engenheiros brasileiros sem latência ou burocracia adicional de parâmetros internacionais. + +--- + +## 3. Arquitetura de "Drivers Normativos" (`StandardStrategy`) + +Para desacoplar a geometria do modelo físico de cálculo, elevamos a arquitetura para o padrão **Strategy Driver**: + +``` + ┌──────────────────────────────────────────────┐ + │ MODELO GEOMÉTRICO CANÔNICO (CORE) │ + │ (b, d, h, θ, Permeabilidades, Topografia) │ + └──────────────────────┬───────────────────────┘ + │ + ┌───────────────────────┴───────────────────────┐ + ▼ ▼ ▼ + ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ + │ Driver NBR 6123 │ │ Driver Eurocode 1 │ │ Driver ASCE 7-22 │ + │ (NBR6123Driver) │ │ (EN1991_1_4Driver) │ │ (ASCE7_22Driver) │ + └──────────┬──────────┘ └──────────┬──────────┘ └──────────┬──────────┘ + │ │ │ + ▼ ▼ ▼ + Tabelas NBR (36) Tabelas EN 1991-1-4 Tabelas ASCE 7-22 + (Rajada de 3 seg) (Média de 10 min) (Rajada + Fator Kz) +``` + +### 3.1. Conciliação Físico-Atmosférica (Fator de Durst & Tempos de Base) +As normas mundiais diferem no tempo de média da velocidade do vento de referência: +* **NBR 6123 e ASCE 7-22:** Baseiam-se na velocidade de **rajada de 3 segundos**. +* **Eurocode 1 (EN 1991-1-4):** Baseia-se na velocidade **média de 10 minutos**. +* **Solução:** O motor canônico integrará conversores espectrais normatizados (ex.: **Fator de Durst / Curvas de Cook**) que permitem transacionar velocidades de 10 minutos para rajadas de 3 segundos com precisão matemática, colocando todas as normas na **mesma base física real de energia cinética do fluxo**. + +--- + +## 4. O Segredo de UX: Mapeamento Inteligente e "Tradução Normativa" + +Para que o usuário **não precise preencher 4 cadastros inteiros** só para ver um comparativo, o sistema dividirá os parâmetros de entrada em duas camadas: + +### 4.1. Camada 1: Parâmetros Universais (Canônicos) +Comuns a 100% das estruturas no planeta: +* **Geometria:** Largura ($b$), Vão/Comprimento ($d$), Altura dos pilares ($h$), Inclinação da cobertura ($\theta$). +* **Física:** Percentual de fechamento de paredes ($0\%$ a $100\%$), velocidade base de projeto ($m/s$). + +### 4.2. Camada 2: Tradutor Automático de Padrões (*Default Mapping Adaptor*) +Quando o engenheiro projeta um galpão com dados brasileiros, o **Tradutor Normativo** infere automaticamente as propriedades correspondentes nas outras normas em segundo plano: +* *Categoria de Terreno IV (Brasil) $\longrightarrow$ Terrain Category III (Eurocode 1) $\longrightarrow$ Exposure Category C (ASCE 7).* +* *Classe de Edificação B (Brasil) $\longrightarrow$ Fator de Pico / Altura equivalente.* + +### 4.3. Gaveta de Ajustes Normativos Específicos (*Overrides*) +Para engenheiros peritos que desejam refinar parâmetros exclusivos de uma norma estrangeira sem alterar a geometria global, a interface disponibilizará um painel retrátil **"Ajustes Normativos Específicos"**: +* **Exemplo ASCE 7:** Permite alterar manualmente a *Enclosure Classification* (`Enclosed`, `Partially Enclosed`, `Partially Open`, `Open`) e o fator de altitude acima do nível do mar ($K_e$). +* **Exemplo Eurocode 1:** Permite ajustar a densidade do ar em função de temperatura e altitude em locais específicos da Europa. + +--- + +## 5. Isolamento e Blindagem de Arquivos Salvos (`Project Schema`) + +Para evitar mistura de referências ou corrupção de dados: +1. **Atributo `standard` Obrigatório no Schema JSON:** + Todo arquivo salvo conterá explicitamente a norma de origem: + ```json + { + "id": "galpao-logistico-01", + "standard": "NBR6123_2023", + "version": "2.0.0", + "geometry": { "width": 20, "depth": 40, "height": 8, "theta": 15 }, + "parameters": { "v0": 45, "s1": 1.0, "s2Category": 4, "s2Class": "B", "s3": 1.0 } + } + ``` +2. **Guarda de Carregamento (*Schema Guard*):** + Ao abrir um arquivo, o app identifica a chave `"standard"`. Se um arquivo do Eurocode for aberto por engano, a interface avisará o usuário, trocando o contexto visual para as tabelas e símbolos correspondentes sem sobrescrever o projeto brasileiro. + +--- + +## 6. O Módulo de "Benchmarking Lado a Lado" (Comparativo em Tempo Real) + +Como cada cálculo estrutural de vento executado em TypeScript puro roda em **menos de 2 milissegundos**, o aplicativo é capaz de executar **4 motores em paralelo em tempo real** sempre que o usuário mover um slider. + +### 6.1. Tabela de Confronto Direto (`kN/m²` e `kN`) +Uma tela dedicada que coloca frente a frente: +* **Pressão Efetiva Crítica e Força Total:** Por zona do telhado, paredes frontais/traseiras e esforços na base do pilar. +* **Mapa de Conservadorismo (Cores Críticas):** Destacando automaticamente em **Vermelho** a norma que exige a maior carga de projeto (mais conservadora) e em **Verde** a mais branda para cada elemento estrutural. + +### 6.2. Gráficos de Delta Percentual ($\Delta\%$) +Gráficos de barra em SVG/Recharts mostrando os deltas em tempo real: +* *Exemplo de insight automático:* "Para esta inclinação de $15^\circ$, a **ASCE 7-22** prescreve forças de sucção nos cantos do telhado **+14,2% maiores** que a **NBR 6123**, enquanto o **Eurocode 1** fica **-5,8% menor**." + +### 6.3. Modelo 3D Interativo Multi-Norma (*Split-Screen / Toggle*) +* Opção de **Abas Instantâneas no topo do 3D:** (`[ NBR 6123 ] [ Eurocode 1 ] [ ASCE 7-22 ]`). Clicar na aba altera na hora o mapa de cores de pressão na geometria 3D. +* **Modo Tela Dividida (*Split-Screen*):** Corta o modelo 3D ao meio, mostrando do lado esquerdo o gradiente de pressões da NBR 6123 e do lado direito o gradiente da norma comparada. + +--- + +## 7. O "Selo de Garantia": Bateria de Testes contra a Literatura Estrangeira + +Para que qualquer norma estrangeira receba o selo de confiança técnica do **BrainWind**, ela deverá ser aprovada em uma suíte de testes unitários (`Vitest` em `src/lib/__tests__/standards/`) travada contra exercícios de referência consagrados: + +| Norma | Fonte Literária Oficial para Travamento de Testes Unitários | +| :--- | :--- | +| **ASCE 7-22 (EUA)** | • *Guide to the Wind Load Provisions of ASCE 7-22* (ASCE / SEAOC)
• *SEAOC Structural/Seismic Design Manual — Wind Examples* | +| **Eurocode 1 (EN 1991-1-4)** | • *ECCS — Designers' Guide to EN 1991-1-4 (Eurocode 1)*
• *JRC Technical Reports (European Commission) — Steel & Wind Design* | +| **AS/NZS 1170.2 (Aus/NZ)** | • *Australian Guide to the Wind Loading Standard AS/NZS 1170.2* (Engineers Australia) | + +### 7.1. Testes de Invariância Aerodinâmica +Além do teste dos exemplos numéricos, a suíte rodará **testes de invariância relacional**: +* Em qualquer das 4 normas, a face de barlavento **sempre** deve resultar em sobrepressão positiva. +* O topo da cumeeira de telhados em duas águas **sempre** deve registrar pico de sucção na separação de escoamento. +* O alívio aerodinâmico em abrigos com abertura superior **sempre** deve reduzir o arrancamento vertical. + +--- + +## 8. Roadmap de Execução Recomendado + +``` +[ FASE ATUAL ] ──► CONSOLIDAÇÃO TOTAL NBR 6123:2023 (100% Norma, 100% Testes, UI/3D Impecável) + │ + ▼ +[ FASE FUTURA 1 ] ──► Criação da Camada "StandardStrategy" (Isolada, sem mexer no Core NBR) + │ + ▼ +[ FASE FUTURA 2 ] ──► Driver Eurocode 1 (EN 1991-1-4) + Testes de Bancada ECCS / JRC + │ + ▼ +[ FASE FUTURA 3 ] ──► Driver ASCE 7-22 (EUA) + Testes de Bancada ASCE Guide / SEAOC + │ + ▼ +[ FASE FUTURA 4 ] ──► Lançamento da Tela "Benchmarking Lado a Lado & Auditoria Multi-Norma" +``` + +> **Conclusão:** Seguindo este plano, mantemos nossa estabilidade 100% blindada e criamos uma das mais avançadas e respeitadas ferramentas de engenharia de vento da América Latina e do mundo. diff --git a/app/src/components/SaveModuleDialog.tsx b/app/src/components/SaveModuleDialog.tsx index 17339c4..557b5e8 100644 --- a/app/src/components/SaveModuleDialog.tsx +++ b/app/src/components/SaveModuleDialog.tsx @@ -15,7 +15,7 @@ import { useProjects } from '../lib/hooks/useProjects'; import { useWindStore } from '../store/appStore'; interface SaveModuleDialogProps { - moduleType: 'galpao' | 'cilindro' | 'vault' | 'dome' | 'sign' | 'isolated-roof' | 'bar' | 'bridge' | 'dynamics' | 'piperack' | 'tower'; + moduleType: 'galpao' | 'cilindro' | 'vault' | 'dome' | 'sign' | 'isolated-roof' | 'bar' | 'bridge' | 'dynamics' | 'piperack' | 'tower' | 'shelter'; inputs: Record; triggerButton?: React.ReactNode; } diff --git a/app/src/components/WindFlowGrid2D.tsx b/app/src/components/WindFlowGrid2D.tsx index bc07b13..c846a38 100644 --- a/app/src/components/WindFlowGrid2D.tsx +++ b/app/src/components/WindFlowGrid2D.tsx @@ -76,15 +76,15 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) { // Define quais itens de legenda devem ser exibidos para cada tipo de módulo const getVisibleLegendItems = () => { - const hasCpi = ['warehouse'].includes(type); - const hasVortex = ['isolated-roof', 'bar', 'tower', 'vault', 'cylinder', 'sign', 'dynamics', 'piperack'].includes(type); - const hasLift = ['bridge', 'isolated-roof'].includes(type); + const hasCpi = ['warehouse', 'shelter'].includes(type); + const hasVortex = ['isolated-roof', 'bar', 'tower', 'vault', 'cylinder', 'sign', 'dynamics', 'piperack', 'shelter'].includes(type); + const hasLift = ['bridge', 'isolated-roof', 'shelter'].includes(type); return { wind: true, fast: true, vortex: hasVortex, - vortexSpin: ['cylinder', 'dynamics', 'sign', 'piperack'].includes(type), + vortexSpin: ['cylinder', 'dynamics', 'sign', 'piperack', 'shelter'].includes(type), cpiPos: hasCpi, cpiNeg: hasCpi, lift: hasLift, @@ -99,7 +99,8 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) { case 'dome': case 'warehouse': case 'piperack': - return 'Vista Lateral (Corte Transversal)'; + case 'shelter': + return 'Vista Lateral (Corte Transversal / Elevação A-A)'; case 'cylinder': case 'sign': case 'bar': @@ -769,6 +770,91 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) { ); + case 'shelter': + return ( +
+ {/* Caso A: Abrigo Estanque - Condição 1 */} +
+ Caso A: Abrigo Estanque em 3 Lados (Condição 1 - Armadilha) +
+ + {legendDef} + {styleBlock} + {/* Piso e Sapata */} + + + + {/* Linhas de vento incidentes e superiores */} + + + + + + {/* Geometria do Abrigo/Pórtico Retangular em Balanço */} + + + + + + + {/* Armadilha Aerodinâmica (+Cpi elevado) */} + + + + +Cpi até +0,8 (Empuxo) + + {/* Seta Vermelha Vertical de Arrancamento Máximo Fz */} + + Fz máx (+Cpi) + + {/* Esteira Turbulenta a Sotavento */} + + + + +
+

O vento frontal fica retido pela parede de fundo fechada, gerando sobrepressão interna (+Cpi) que empurra a cobertura retangular em balanço para cima.

+
+ + {/* Caso B: Alívio por Fresta Superior - Condição 2 */} +
+ Caso B: Alívio por Fresta Superior (Condição 2 + Lanternim) +
+ + {legendDef} + {styleBlock} + {/* Piso e Sapata */} + + + + {/* Linhas de vento incidentes e superiores */} + + + + {/* Fluxo escapando pela Fresta Superior (Alívio verde) */} + + + Escape do Ar + + {/* Geometria do Abrigo/Pórtico com FRESTA NO TOPO (vão de y=45 a y=72) */} + + + + + + {/* Força Vertical de Arrancamento Aliviada */} + + -25% Fz (Aliviado) + + {/* Esteira a Sotavento suave */} + + +
+

A fresta aberta no topo entre a parede e o telhado permite que a sobrepressão interna escape para sotavento, reduzindo drasticamente o arrancamento vertical da cobertura.

+
+
+ ); + case 'calc_ca': return (
diff --git a/app/src/components/three/Shelter3D.tsx b/app/src/components/three/Shelter3D.tsx index 074efe3..523f5d8 100644 --- a/app/src/components/three/Shelter3D.tsx +++ b/app/src/components/three/Shelter3D.tsx @@ -1,7 +1,10 @@ import React, { useRef } from 'react'; import * as THREE from 'three'; import { useFrame } from '@react-three/fiber'; -import { Text, Line } from '@react-three/drei'; +import { Text, Grid, Environment } from '@react-three/drei'; +import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls'; +import SceneCanvas from '../SceneCanvas'; +import { useCanvasTheme } from '@/lib/theme'; import type { ShelterCondition, OpeningPosition } from '@/lib/nbr-tables/table-shelters'; export interface Shelter3DProps { @@ -14,6 +17,7 @@ export interface Shelter3DProps { leftClosure: number; rightClosure: number; openingPos: OpeningPosition; + backRange?: [number, number]; viewMode: '3d' | 'elevation'; } @@ -24,23 +28,26 @@ const WindArrow: React.FC<{ speed?: number; }> = ({ start, end, color = '#38bdf8', speed = 1.5 }) => { const arrowRef = useRef(null); - useFrame(({ clock }) => { + + const quaternion = React.useMemo(() => { + const dir = new THREE.Vector3(...end).sub(new THREE.Vector3(...start)).normalize(); + return new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir); + }, [start, end]); + + useFrame(({ clock, invalidate }) => { if (arrowRef.current) { const t = (clock.getElapsedTime() * speed) % 1; const x = start[0] + (end[0] - start[0]) * t; const y = start[1] + (end[1] - start[1]) * t; const z = start[2] + (end[2] - start[2]) * t; arrowRef.current.position.set(x, y, z); + invalidate(); } }); - const dir = new THREE.Vector3(...end).sub(new THREE.Vector3(...start)); - const len = dir.length(); - const angleZ = Math.atan2(dir.y, dir.x); - return ( - - + + @@ -48,8 +55,8 @@ const WindArrow: React.FC<{ ); }; -export const Shelter3D: React.FC = ({ - condition, +function ShelterModel({ + condition: _condition, depth, width, height, @@ -58,8 +65,9 @@ export const Shelter3D: React.FC = ({ leftClosure, rightClosure, openingPos, + backRange, viewMode, -}) => { +}: Shelter3DProps) { const isElevation = viewMode === 'elevation'; // Geometria básica @@ -68,25 +76,32 @@ export const Shelter3D: React.FC = ({ const thetaRad = (theta * Math.PI) / 180; const slopeRise = depth * Math.tan(thetaRad); - // Paredes com abertura no topo ou na base + // Paredes com abertura no topo ou na base (sem caixas/linhas brancas deslocadas) const renderWallWithGap = ( w: number, h: number, closure: number, pos: OpeningPosition, rotY = 0, - position: [number, number, number] = [0, 0, 0] + position: [number, number, number] = [0, 0, 0], + customRange?: [number, number] ) => { if (closure <= 0) return null; - const op = closure / 100; - const wallH = h * op; + let op = closure / 100; + let wallH = h * op; let yPos = position[1] + wallH / 2; - if (pos === 'top' && op < 1) { + if (customRange && closure < 100) { + const bottomPct = Math.max(0, Math.min(100, customRange[0])) / 100; + const topPct = Math.max(0, Math.min(100, customRange[1])) / 100; + op = Math.max(0.01, topPct - bottomPct); + wallH = h * op; + yPos = position[1] + h * bottomPct + wallH / 2; + } else if (pos === 'top' && closure < 100) { // Parede na parte de baixo, fresta no topo yPos = position[1] + wallH / 2; - } else if (pos === 'bottom' && op < 1) { + } else if (pos === 'bottom' && closure < 100) { // Parede suspensa na parte superior, vão na base yPos = position[1] + h - wallH / 2; } @@ -102,24 +117,12 @@ export const Shelter3D: React.FC = ({ roughness={0.4} /> - {/* Linha de contorno */} - - - - ); }; return ( - {/* Plano do Solo */} - - - - - - {/* 1. Estrutura Metálica Principal (Pilares e Vigas em Balanço - Tubos Quadrados) */} {[-halfW + 0.6, halfW - 0.6].map((xPos, idx) => ( @@ -144,15 +147,11 @@ export const Shelter3D: React.FC = ({ - - - - {/* 3. Fechamentos de Fundo e Laterais (com suporte a fresta/abertura) */} {/* Parede de Fundo (Traseira Z = +halfD) */} - {renderWallWithGap(width, height, backClosure, openingPos, 0, [0, 0, halfD])} + {renderWallWithGap(width, height, backClosure, openingPos, 0, [0, 0, halfD], backRange)} {/* Parede Lateral Esquerda (X = -halfW) */} {renderWallWithGap(depth, height, leftClosure, 'distributed', Math.PI / 2, [-halfW, 0, 0])} @@ -162,44 +161,74 @@ export const Shelter3D: React.FC = ({ {/* 4. Linhas de Corrente e Vetores de Vento (Ilustrativo e Didático) */} - {/* Vento de entrada (Barlavento) */} - - - + {/* Vento de entrada (Barlavento) entrando pela frente em direção ao abrigo */} + + + {/* Linhas de corrente sobre o telhado */} - + - {/* Fresta na parede de fundo: se houver abertura no topo, mostrar vento escapando pela fresta superior! */} - {backClosure > 0 && backClosure < 100 && openingPos === 'top' && ( + {/* Fresta na parede de fundo: se houver abertura no topo, mostrar vento escapando pela fresta superior */} + {backClosure > 0 && backClosure < 100 && (openingPos === 'top' || (backRange && backRange[1] < 98)) && ( <> - + Alívio (Fresta Topo) )} {/* Fresta na base */} - {backClosure > 0 && backClosure < 100 && openingPos === 'bottom' && ( + {backClosure > 0 && backClosure < 100 && (openingPos === 'bottom' || (backRange && backRange[0] > 2)) && ( <> - + Vão Livre Inferior )} - {/* Textos Informativos 3D */} - + {/* Textos Informativos 3D - Virados para quem olha da frente (-Z) */} + {isElevation ? 'CORTE A-A (Elevação)' : 'Abrigo / Pórtico (NBR 6123)'} - + VENTO FRONTAL ); -}; +} + +export function Shelter3D(props: Shelter3DProps) { + const theme = useCanvasTheme(); + const isDark = theme === 'dark'; + const { width, depth, height } = props; + const dist = Math.max(width, depth) * 1.8 + 8; + + return ( + 3D indisponível
} + > + + + + + + + + ); +} export default Shelter3D; diff --git a/app/src/lib/import-project.ts b/app/src/lib/import-project.ts index d266283..fede3d1 100644 --- a/app/src/lib/import-project.ts +++ b/app/src/lib/import-project.ts @@ -39,6 +39,9 @@ const VALID_MODULES: readonly ModuleId[] = [ 'bar', 'bridge', 'dynamics', + 'piperack', + 'tower', + 'shelter', ]; const VALID_CATEGORIES: readonly TerrainCategory[] = ['I', 'II', 'III', 'IV', 'V']; diff --git a/app/src/lib/nbr-tables/table-shelters.ts b/app/src/lib/nbr-tables/table-shelters.ts index dc89987..215e9a6 100644 --- a/app/src/lib/nbr-tables/table-shelters.ts +++ b/app/src/lib/nbr-tables/table-shelters.ts @@ -20,6 +20,8 @@ export interface ShelterInput { theta: number; /** Percentual de fechamento da parede de fundo (0 a 100) */ backClosure: number; + /** Faixa de fechamento da parede de fundo [base%, topo%] (0 a 100) */ + backRange?: [number, number]; /** Percentual de fechamento da parede lateral esquerda (0 a 100) */ leftClosure: number; /** Percentual de fechamento da parede lateral direita (0 a 100) */ @@ -76,6 +78,7 @@ export function calculateShelterWind(input: ShelterInput, q: number): ShelterRes height, theta, backClosure, + backRange, leftClosure, rightClosure, openingPos, @@ -115,19 +118,26 @@ export function calculateShelterWind(input: ShelterInput, q: number): ShelterRes let reliefPercentage = 0; let effectiveCpi = baseCpi; - if (bClose > 0.1 && bClose < 0.98) { + if (backRange && bClose > 0.05 && bClose < 0.99) { + const topGapPct = Math.max(0, Math.min(100, 100 - backRange[1])) / 100; + // O alívio de arrancamento da cobertura depende diretamente da abertura no topo (fresta superior) + reliefPercentage = Math.round(35 * Math.min(1, topGapPct * 2.5)); + effectiveCpi = baseCpi * (1 - reliefPercentage / 100); + } else if (bClose > 0.05 && bClose < 0.99) { + const openRatio = 1 - bClose; // fração aberta da parede de fundo if (openingPos === 'top') { // Fresta superior permite o escape de sobrepressão debaixo do telhado - // Reduz o Cpi positivo em até 30%, gerando alívio de arrancamento na cobertura - reliefPercentage = Math.round(25 * (1 - bClose)); + // Reduz o Cpi positivo de forma proporcional ao tamanho da fresta no topo + reliefPercentage = Math.round(35 * openRatio); effectiveCpi = baseCpi * (1 - reliefPercentage / 100); } else if (openingPos === 'bottom') { - // Abertura inferior: o ar flui embaixo, mantendo pressão sob o telhado e aumentando momento na base + // Abertura inferior: o ar flui embaixo, mantendo pressão sob o telhado reliefPercentage = 0; effectiveCpi = baseCpi * 1.05; } else { - reliefPercentage = 5; - effectiveCpi = baseCpi * 0.95; + // Distribuída: alívio proporcional à permeabilidade aberta + reliefPercentage = Math.round(18 * openRatio); + effectiveCpi = baseCpi * (1 - reliefPercentage / 100); } } diff --git a/app/src/lib/storage.ts b/app/src/lib/storage.ts index 8d484af..25088eb 100644 --- a/app/src/lib/storage.ts +++ b/app/src/lib/storage.ts @@ -9,7 +9,7 @@ export interface SavedProject { id?: number; name: string; type: 'projeto' | 'template'; - module: 'galpao' | 'cilindro' | 'vault' | 'dome' | 'sign' | 'isolated-roof' | 'bar' | 'bridge' | 'dynamics' | 'piperack' | 'tower'; + module: 'galpao' | 'cilindro' | 'vault' | 'dome' | 'sign' | 'isolated-roof' | 'bar' | 'bridge' | 'dynamics' | 'piperack' | 'tower' | 'shelter'; inputs: Record; windData?: Record; createdAt: number; diff --git a/app/src/pages/ShelterModule.tsx b/app/src/pages/ShelterModule.tsx index 9f42308..a4fc645 100644 --- a/app/src/pages/ShelterModule.tsx +++ b/app/src/pages/ShelterModule.tsx @@ -1,7 +1,6 @@ import React, { useMemo, useState } 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 { Button } from '@/components/ui/button'; import { useWindStore } from '@/store/appStore'; @@ -11,14 +10,14 @@ import { type OpeningPosition, } from '@/lib/nbr-tables/table-shelters'; import Shelter3D from '@/components/three/Shelter3D'; -import SceneCanvas from '@/components/SceneCanvas'; +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, ShieldCheck, ArrowUpRight, CheckCircle2 } from 'lucide-react'; +import { Eye, Box, Layers, ArrowUpRight, CheckCircle2 } from 'lucide-react'; import { cn } from '@/lib/utils'; const conditionLabels: Record = { @@ -35,28 +34,41 @@ const ShelterModule: React.FC = () => { const [width, setWidth] = useState(10); const [height, setHeight] = useState(4.8); const [theta, setTheta] = useState(5); - const [backClosure, setBackClosure] = useState(85); + const [backClosure, setBackClosure] = useState(75); + const [backRange, setBackRange] = useState<[number, number]>([0, 75]); const [leftClosure, setLeftClosure] = useState(100); const [rightClosure, setRightClosure] = useState(100); - const [openingPos, setOpeningPos] = useState('top'); const [viewMode, setViewMode] = useState<'3d' | 'elevation'>('3d'); - // Ajusta percentuais ao trocar as 4 condições rápidas + // 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); @@ -64,65 +76,42 @@ const ShelterModule: React.FC = () => { }; const result = useMemo(() => { - return calculateShelterWind( - { - condition, - depth, - width, - height, - theta, - backClosure, - leftClosure, - rightClosure, - openingPos, - }, - q - ); - }, [condition, depth, width, height, theta, backClosure, leftClosure, rightClosure, openingPos, q]); + return calculateShelterWind({ + condition, + depth, + width, + height, + theta, + backClosure, + backRange, + leftClosure, + rightClosure, + openingPos, + }, q); + }, [condition, q, depth, width, height, theta, backClosure, backRange, leftClosure, rightClosure, openingPos]); const handleExportPDF = () => { const sections: GenericPDFSection[] = [ { - title: 'Geometria do Abrigo e Fechamentos', + title: 'Geometria e Fechamentos do Abrigo', type: 'grid', gridItems: [ - { label: 'Condição Topológica', value: conditionLabels[condition] }, { label: 'Comprimento (d / balanço)', value: `${depth} m` }, { label: 'Largura (b)', value: `${width} m` }, { label: 'Altura (h)', value: `${height} m` }, { label: 'Inclinação (θ)', value: `${theta}°` }, { label: 'Fechamento Fundo', value: `${backClosure}%` }, { label: 'Fechamento Lateral Esq / Dir', value: `${leftClosure}% / ${rightClosure}%` }, - { label: 'Posição da Abertura', value: openingPos === 'top' ? 'Fresta Topo (Alívio)' : openingPos === 'bottom' ? 'Vão Inferior' : 'Distribuída' }, - ], - }, - { - title: 'Coeficientes Aerodinâmicos (NBR 6123)', - type: 'grid', - gridItems: [ - { label: 'Ce Superior (Cobertura)', value: result.cpeTop.toString() }, - { label: 'Cpi Inferior (Efetivo)', value: result.cpiBottom.toString() }, - { label: 'Cnf Líquido (Arrancamento)', value: result.cnfVertical.toString() }, - { label: 'Cf Parede Fundo (Tab. 23)', value: result.cfBack.toString() }, - { label: 'Alívio por Fresta Superior', value: `${result.reliefPercentage}%` }, + { label: 'Posição da Abertura', value: openingPos }, ], }, { title: 'Forças Resultantes (kN)', type: 'grid', gridItems: [ - { label: 'Força Vertical (Fz - Arrancamento)', value: `${result.forces.fzUp} kN` }, - { label: 'Empuxo Frontal Fundo (Fx)', value: `${result.forces.fxBack} kN` }, - { label: 'Empuxo nas Laterais (Fy)', value: `${result.forces.fySide} kN` }, - { label: 'Atrito na Cobertura', value: `${result.forces.fFriction} kN` }, - ], - }, - { - title: 'Carga Linear nos Pórticos (kN/m)', - type: 'grid', - gridItems: [ - { label: 'Carga no Pilar (ex: Tubo 150x150)', value: `${result.lineLoads.columnLoad} kN/m` }, - { label: 'Carga na Viga em Balanço', value: `${result.lineLoads.beamLoad} kN/m` }, + { label: 'Força Vertical (Fz)', value: `${result.forces.fzUp} kN` }, + { label: 'Empuxo Frontal (Fx)', value: `${result.forces.fxBack} kN` }, + { label: 'Empuxo Laterais (Fy)', value: `${result.forces.fySide} kN` }, ], }, ]; @@ -130,9 +119,9 @@ const ShelterModule: React.FC = () => { }; return ( -
+
{/* Coluna Esquerda: Controles do Abrigo */} -
+
@@ -140,9 +129,7 @@ const ShelterModule: React.FC = () => { Abrigos e Pórticos - - Pórticos em balanço, marquises e garagens com fechamentos (Tab. 23-25 e sec. 6.3). - + Pórticos em balanço (Tab. 23-25 e sec. 6.3).
{ - {/* Seletor Rápido de 4 Condições Topológicas */}
-
+
{(['cond1', 'cond2', 'cond3', 'cond4'] as ShelterCondition[]).map((c) => ( ))}
- {/* Controle de Posição da Abertura (Topo / Base / Distribuída) */} -
- - -
- - {/* Sliders de Fechamento Parcial (%) */}
-
-
+
+
Parede de Fundo: - {backClosure}% + + {backClosure}% {backRange[0] > 0 || backRange[1] < 100 ? `(Base ${backRange[0]}% | Topo ${100 - backRange[1]}%)` : ''} +
- setBackClosure(v[0])} /> + { + if (Array.isArray(val) && val.length === 2) { + const [start, end] = val; + if (end >= start) { + setBackRange([start, end]); + setBackClosure(end - start); + } + } + }} + />
-
-
-
+
+
+
Lateral Esq: {leftClosure}%
- setLeftClosure(v[0])} /> + setLeftClosure(v[0])} />
-
-
+
+
Lateral Dir: {rightClosure}%
- setRightClosure(v[0])} /> + setRightClosure(v[0])} />
- {/* Sliders Geométricos */}
- {/* Coluna Direita: 3D, Corte A-A e Quadro de Ações */} -
- {/* Visualizador 3D com trava, manual (i) e corte em elevação */} -
-
- - θ = {theta}° +
+
+ + θ = {theta}° + + + Fundo: {backClosure}% ({openingPos === 'top' ? 'Fresta Topo' : openingPos === 'bottom' ? 'Vão Base' : 'Distrib.'}) + + {result.reliefPercentage > 0 && ( + + + Alívio Arrancamento: -{result.reliefPercentage}% - - Fundo: {backClosure}% ({openingPos === 'top' ? 'Fresta Topo' : openingPos === 'bottom' ? 'Vão Base' : 'Distrib.'}) - - {result.reliefPercentage > 0 && ( - - - Alívio Arrancamento: -{result.reliefPercentage}% - - )} + )} +
+ +
+
-
- {/* Botão de Corte em Elevação (Corte A-A) vs 3D */} -
- - +
+ +
+ + {viewMode === '3d' ? ( + + ) : ( +
+
+
- - {/* Ícone (i) Manual Didático NBR 6123 */} -
+ )} +
- 3D indisponível
} - > - - - - -
- - {/* Quadro Superior de Resultados na Base do 3D */} -
- - - F. Vertical (Fz - Arrancamento) -
- {result.forces.fzUp} kN - Cnf = {result.cnfVertical} -
- - {result.reliefPercentage > 0 ? `Com ${result.reliefPercentage}% de alívio por fresta` : 'Sem alívio por fresta superior'} - -
-
- - - - Empuxo no Fundo (Fx Traseira) -
- {result.forces.fxBack} kN - Cf = {result.cfBack} -
- - Parede com {backClosure}% de estanqueidade - -
-
- - - - Carga no Pilar (ex: Tubo 150x150) -
- - {result.lineLoads.columnLoad} kN/m - - h = {height} m -
- - Ação direta + reação horizontal por pilar - -
-
- - - - Carga na Viga em Balanço -
- - {result.lineLoads.beamLoad} kN/m - - d = {depth} m -
- - Sucção distribuída por braço principal - -
-
-
- - {/* Rodapé com Exportação e Captura */} -
- - +
+ + + + Forças Calculadas + {result.forces.fzUp.toFixed(1)} kN (Arrancamento) + + + +
+
+ F. Vertical (Fz - Cobertura) + {result.forces.fzUp.toFixed(2)} kN +
+
+ Coeficiente Cnf + {result.cnfVertical} +
+
+
+
+ Empuxo no Fundo (Fx) + {result.forces.fxBack.toFixed(2)} kN +
+
+ Cf / Estanqueidade + Cf = {result.cfBack} ({backClosure}%) +
+
+
+ Empuxo Lateral (Fy) + {result.forces.fySide.toFixed(2)} kN +
+
+ Carga no Pilar (Pórtico) + {result.lineLoads.columnLoad.toFixed(2)} kN/m +
+
+ Carga na Viga (Balanço) + {result.lineLoads.beamLoad.toFixed(2)} kN/m +
+
+
Parâmetros Adicionais (NBR 6123)
+
+
+ Permeabilidade (μ): + {100 - backClosure}% +
+
+ Alívio por Fresta Superior: + {result.reliefPercentage}% +
+
+ Força de Atrito (Ff): + {result.forces.fFriction.toFixed(2)} kN +
+
+

+ * Para a visualização didática dos esquemas aerodinâmicos e linhas de corrente, utilize a aba Corte A-A no painel central. +

+
+
+
+
+
+ +
+
);