🚀 Auto-deploy: BrainWind atualizado em 27/07/2026 18:39:28
This commit is contained in:
@@ -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)<br>• *SEAOC Structural/Seismic Design Manual — Wind Examples* |
|
||||
| **Eurocode 1 (EN 1991-1-4)** | • *ECCS — Designers' Guide to EN 1991-1-4 (Eurocode 1)*<br>• *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.
|
||||
@@ -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<string, unknown>;
|
||||
triggerButton?: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'shelter':
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Caso A: Abrigo Estanque - Condição 1 */}
|
||||
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||
<span className="text-xs font-bold text-orange-500 dark:text-orange-400">Caso A: Abrigo Estanque em 3 Lados (Condição 1 - Armadilha)</span>
|
||||
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||
<svg viewBox="0 0 320 160" className="w-full h-full">
|
||||
{legendDef}
|
||||
{styleBlock}
|
||||
{/* Piso e Sapata */}
|
||||
<line x1="-10" y1="135" x2="330" y2="135" stroke="#94a3b8" strokeWidth="2.5" />
|
||||
<rect x="194" y="135" width="22" height="6" fill="#64748b" />
|
||||
|
||||
{/* Linhas de vento incidentes e superiores */}
|
||||
<path d="M -10,35 Q 70,35 120,25 T 330,35" className="wind-line-fast" />
|
||||
<path d="M -10,20 Q 70,20 140,12 T 330,20" className="wind-line-fast" />
|
||||
<path d="M -10,75 L 75,75" className="wind-line" />
|
||||
<path d="M -10,105 L 75,105" className="wind-line" />
|
||||
|
||||
{/* Geometria do Abrigo/Pórtico Retangular em Balanço */}
|
||||
<rect x="200" y="45" width="12" height="90" fill="#475569" stroke="#94a3b8" strokeWidth="2" />
|
||||
<rect x="80" y="45" width="120" height="12" fill="#334155" stroke="#64748b" strokeWidth="2" />
|
||||
<rect x="80" y="39" width="4" height="24" fill="#ef4444" rx="1" />
|
||||
<line x1="120" y1="45" x2="200" y2="100" stroke="#64748b" strokeWidth="1.5" strokeDasharray="3 3" />
|
||||
<line x1="160" y1="45" x2="200" y2="75" stroke="#64748b" strokeWidth="1.5" strokeDasharray="3 3" />
|
||||
|
||||
{/* Armadilha Aerodinâmica (+Cpi elevado) */}
|
||||
<path d="M 125,115 Q 170,115 170,68" stroke="#f97316" strokeWidth="2.5" fill="none" markerEnd="url(#cpi-pos)" />
|
||||
<path d="M 95,115 Q 140,115 140,68" stroke="#f97316" strokeWidth="2.5" fill="none" markerEnd="url(#cpi-pos)" />
|
||||
<line x1="165" y1="95" x2="192" y2="95" stroke="#f97316" strokeWidth="2.5" markerEnd="url(#cpi-pos)" />
|
||||
<text x="110" y="125" fill="#f97316" fontSize="9" fontWeight="bold">+Cpi até +0,8 (Empuxo)</text>
|
||||
|
||||
{/* Seta Vermelha Vertical de Arrancamento Máximo Fz */}
|
||||
<line x1="145" y1="45" x2="145" y2="15" stroke="#ef4444" strokeWidth="3" markerEnd="url(#arrow-red)" />
|
||||
<text x="152" y="28" fill="#ef4444" fontSize="10" fontWeight="bold">Fz máx (+Cpi)</text>
|
||||
|
||||
{/* Esteira Turbulenta a Sotavento */}
|
||||
<path d="M 215,60 Q 260,50 330,65" className="vortex-line" />
|
||||
<circle cx="260" cy="75" r="16" fill="none" stroke="#ef4444" strokeWidth="1.2" strokeDasharray="2 4" className="vortex-spin-cw" />
|
||||
<circle cx="250" cy="110" r="12" fill="none" stroke="#ef4444" strokeWidth="1.2" strokeDasharray="2 4" className="vortex-spin-ccw" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground leading-snug">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.</p>
|
||||
</div>
|
||||
|
||||
{/* Caso B: Alívio por Fresta Superior - Condição 2 */}
|
||||
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||
<span className="text-xs font-bold text-emerald-500 dark:text-emerald-400">Caso B: Alívio por Fresta Superior (Condição 2 + Lanternim)</span>
|
||||
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||
<svg viewBox="0 0 320 160" className="w-full h-full">
|
||||
{legendDef}
|
||||
{styleBlock}
|
||||
{/* Piso e Sapata */}
|
||||
<line x1="-10" y1="135" x2="330" y2="135" stroke="#94a3b8" strokeWidth="2.5" />
|
||||
<rect x="194" y="135" width="22" height="6" fill="#64748b" />
|
||||
|
||||
{/* Linhas de vento incidentes e superiores */}
|
||||
<path d="M -10,35 Q 70,35 120,25 T 330,35" className="wind-line-fast" />
|
||||
<path d="M -10,75 L 75,75" className="wind-line" />
|
||||
|
||||
{/* Fluxo escapando pela Fresta Superior (Alívio verde) */}
|
||||
<path d="M -10,95 Q 90,95 180,60 L 330,60" stroke="#10b981" strokeWidth="2.5" strokeDasharray="6 6" fill="none" markerEnd="url(#arrow-green)" />
|
||||
<path d="M 50,115 Q 120,115 185,65 L 330,65" stroke="#10b981" strokeWidth="2" strokeDasharray="6 6" fill="none" opacity="0.8" />
|
||||
<text x="220" y="52" fill="#10b981" fontSize="10" fontWeight="bold">Escape do Ar</text>
|
||||
|
||||
{/* Geometria do Abrigo/Pórtico com FRESTA NO TOPO (vão de y=45 a y=72) */}
|
||||
<rect x="200" y="72" width="12" height="63" fill="#475569" stroke="#94a3b8" strokeWidth="2" />
|
||||
<rect x="80" y="45" width="120" height="12" fill="#334155" stroke="#64748b" strokeWidth="2" />
|
||||
<rect x="80" y="39" width="4" height="24" fill="#ef4444" rx="1" />
|
||||
<line x1="120" y1="45" x2="200" y2="100" stroke="#64748b" strokeWidth="1.5" strokeDasharray="3 3" />
|
||||
|
||||
{/* Força Vertical de Arrancamento Aliviada */}
|
||||
<line x1="145" y1="45" x2="145" y2="25" stroke="#10b981" strokeWidth="2.5" markerEnd="url(#arrow-green)" />
|
||||
<text x="152" y="32" fill="#10b981" fontSize="10" fontWeight="bold">-25% Fz (Aliviado)</text>
|
||||
|
||||
{/* Esteira a Sotavento suave */}
|
||||
<path d="M 225,90 Q 270,85 330,95" className="wind-line-slow" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground leading-snug">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.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'calc_ca':
|
||||
return (
|
||||
<div className="bg-white dark:bg-slate-950 border border-border/60 p-4 rounded-lg flex flex-col gap-2 min-h-[160px]">
|
||||
|
||||
@@ -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<THREE.Group>(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 (
|
||||
<group ref={arrowRef}>
|
||||
<mesh rotation={[0, 0, angleZ - Math.PI / 2]}>
|
||||
<group ref={arrowRef} quaternion={quaternion}>
|
||||
<mesh>
|
||||
<coneGeometry args={[0.25, 0.6, 8]} />
|
||||
<meshStandardMaterial color={color} emissive={color} emissiveIntensity={0.6} />
|
||||
</mesh>
|
||||
@@ -48,8 +55,8 @@ const WindArrow: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
export const Shelter3D: React.FC<Shelter3DProps> = ({
|
||||
condition,
|
||||
function ShelterModel({
|
||||
condition: _condition,
|
||||
depth,
|
||||
width,
|
||||
height,
|
||||
@@ -58,8 +65,9 @@ export const Shelter3D: React.FC<Shelter3DProps> = ({
|
||||
leftClosure,
|
||||
rightClosure,
|
||||
openingPos,
|
||||
backRange,
|
||||
viewMode,
|
||||
}) => {
|
||||
}: Shelter3DProps) {
|
||||
const isElevation = viewMode === 'elevation';
|
||||
|
||||
// Geometria básica
|
||||
@@ -68,25 +76,32 @@ export const Shelter3D: React.FC<Shelter3DProps> = ({
|
||||
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<Shelter3DProps> = ({
|
||||
roughness={0.4}
|
||||
/>
|
||||
</mesh>
|
||||
{/* Linha de contorno */}
|
||||
<lineSegments>
|
||||
<edgesGeometry args={[new THREE.BoxGeometry(w, wallH, 0.15)]} />
|
||||
<lineBasicMaterial color="#94a3b8" />
|
||||
</lineSegments>
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<group position={[0, -height / 2, 0]}>
|
||||
{/* Plano do Solo */}
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]} receiveShadow>
|
||||
<planeGeometry args={[width * 2, depth * 2]} />
|
||||
<meshStandardMaterial color="#1e293b" roughness={0.9} />
|
||||
</mesh>
|
||||
<gridHelper args={[Math.max(width, depth) * 2, 20, '#475569', '#334155']} position={[0, 0.01, 0]} />
|
||||
|
||||
{/* 1. Estrutura Metálica Principal (Pilares e Vigas em Balanço - Tubos Quadrados) */}
|
||||
{[-halfW + 0.6, halfW - 0.6].map((xPos, idx) => (
|
||||
<group key={`frame-${idx}`} position={[xPos, 0, 0]}>
|
||||
@@ -144,15 +147,11 @@ export const Shelter3D: React.FC<Shelter3DProps> = ({
|
||||
<boxGeometry args={[width + 0.6, 0.15, depth + 0.4]} />
|
||||
<meshStandardMaterial color="#3b82f6" roughness={0.3} metalness={0.4} />
|
||||
</mesh>
|
||||
<lineSegments position={[0, 0.15, 0]}>
|
||||
<edgesGeometry args={[new THREE.BoxGeometry(width + 0.6, 0.15, depth + 0.4)]} />
|
||||
<lineBasicMaterial color="#60a5fa" />
|
||||
</lineSegments>
|
||||
</group>
|
||||
|
||||
{/* 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<Shelter3DProps> = ({
|
||||
|
||||
{/* 4. Linhas de Corrente e Vetores de Vento (Ilustrativo e Didático) */}
|
||||
<group position={[0, 0, 0]}>
|
||||
{/* Vento de entrada (Barlavento) */}
|
||||
<WindArrow start={[0, height * 0.5, -halfD - 3]} end={[0, height * 0.5, -halfD + 1]} color="#38bdf8" speed={1.2} />
|
||||
<WindArrow start={[-halfW * 0.5, height * 0.7, -halfD - 3]} end={[-halfW * 0.5, height * 0.7, -halfD + 1]} color="#38bdf8" speed={1.4} />
|
||||
<WindArrow start={[halfW * 0.5, height * 0.4, -halfD - 3]} end={[halfW * 0.5, height * 0.4, -halfD + 1]} color="#38bdf8" speed={1.3} />
|
||||
{/* Vento de entrada (Barlavento) entrando pela frente em direção ao abrigo */}
|
||||
<WindArrow start={[0, height * 0.5, -halfD - 4]} end={[0, height * 0.5, -halfD + 1]} color="#38bdf8" speed={1.2} />
|
||||
<WindArrow start={[-halfW * 0.5, height * 0.7, -halfD - 4]} end={[-halfW * 0.5, height * 0.7, -halfD + 1]} color="#38bdf8" speed={1.4} />
|
||||
<WindArrow start={[halfW * 0.5, height * 0.4, -halfD - 4]} end={[halfW * 0.5, height * 0.4, -halfD + 1]} color="#38bdf8" speed={1.3} />
|
||||
|
||||
{/* Linhas de corrente sobre o telhado */}
|
||||
<WindArrow start={[0, height + 1, -halfD - 2]} end={[0, height + slopeRise + 1.5, halfD + 2]} color="#60a5fa" speed={1.6} />
|
||||
<WindArrow start={[0, height + 1, -halfD - 3]} end={[0, height + slopeRise + 1.5, halfD + 2]} color="#60a5fa" speed={1.6} />
|
||||
|
||||
{/* 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)) && (
|
||||
<>
|
||||
<WindArrow start={[0, height * 0.85, 0]} end={[0, height * 0.95, halfD + 2.5]} color="#34d399" speed={1.8} />
|
||||
<Text position={[0, height + 0.3, halfD]} fontSize={0.4} color="#34d399">
|
||||
<Text position={[0, height + 0.35, halfD]} rotation={[0, Math.PI, 0]} fontSize={0.4} color="#34d399">
|
||||
Alívio (Fresta Topo)
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Fresta na base */}
|
||||
{backClosure > 0 && backClosure < 100 && openingPos === 'bottom' && (
|
||||
{backClosure > 0 && backClosure < 100 && (openingPos === 'bottom' || (backRange && backRange[0] > 2)) && (
|
||||
<>
|
||||
<WindArrow start={[0, 0.4, 0]} end={[0, 0.4, halfD + 2.5]} color="#facc15" speed={1.8} />
|
||||
<Text position={[0, 1.2, halfD]} fontSize={0.4} color="#facc15">
|
||||
<Text position={[0, 1.2, halfD]} rotation={[0, Math.PI, 0]} fontSize={0.4} color="#facc15">
|
||||
Vão Livre Inferior
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</group>
|
||||
|
||||
{/* Textos Informativos 3D */}
|
||||
<Text position={[0, height + slopeRise + 0.8, 0]} fontSize={0.45} color="#e2e8f0">
|
||||
{/* Textos Informativos 3D - Virados para quem olha da frente (-Z) */}
|
||||
<Text position={[0, height + slopeRise + 0.8, 0]} rotation={[0, Math.PI, 0]} fontSize={0.45} color="#e2e8f0">
|
||||
{isElevation ? 'CORTE A-A (Elevação)' : 'Abrigo / Pórtico (NBR 6123)'}
|
||||
</Text>
|
||||
<Text position={[0, 0.3, -halfD - 1]} fontSize={0.35} color="#38bdf8">
|
||||
<Text position={[0, 0.3, -halfD - 1.2]} rotation={[0, Math.PI, 0]} fontSize={0.35} color="#38bdf8">
|
||||
VENTO FRONTAL
|
||||
</Text>
|
||||
</group>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
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 (
|
||||
<SceneCanvas
|
||||
moduleId="shelter"
|
||||
shadows
|
||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||
camera={{ position: [dist * 0.75, height * 1.1, -dist * 0.85], fov: 45 }}
|
||||
fallback={<div className="flex items-center justify-center h-full text-muted-foreground">3D indisponível</div>}
|
||||
>
|
||||
<ambientLight intensity={isDark ? 0.4 : 0.7} />
|
||||
<directionalLight position={[10, 20, 15]} intensity={isDark ? 1.0 : 1.3} castShadow />
|
||||
<ShelterModel {...props} />
|
||||
<Grid
|
||||
infiniteGrid
|
||||
fadeDistance={Math.max(width, depth) * 3}
|
||||
sectionColor={isDark ? '#475569' : '#94a3b8'}
|
||||
cellColor={isDark ? '#1e293b' : '#cbd5e1'}
|
||||
position={[0, -height / 2 - 0.01, 0]}
|
||||
/>
|
||||
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.05} target={[0, 0, 0]} />
|
||||
<Environment preset="city" />
|
||||
</SceneCanvas>
|
||||
);
|
||||
}
|
||||
|
||||
export default Shelter3D;
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
windData?: Record<string, unknown>;
|
||||
createdAt: number;
|
||||
|
||||
+148
-186
@@ -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<ShelterCondition, string> = {
|
||||
@@ -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<OpeningPosition>('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(
|
||||
{
|
||||
return calculateShelterWind({
|
||||
condition,
|
||||
depth,
|
||||
width,
|
||||
height,
|
||||
theta,
|
||||
backClosure,
|
||||
backRange,
|
||||
leftClosure,
|
||||
rightClosure,
|
||||
openingPos,
|
||||
},
|
||||
q
|
||||
);
|
||||
}, [condition, depth, width, height, theta, backClosure, leftClosure, rightClosure, openingPos, q]);
|
||||
}, 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 (
|
||||
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
|
||||
<div className="h-[calc(100vh-4rem)] flex flex-col lg:flex-row gap-4 p-4 overflow-hidden">
|
||||
{/* Coluna Esquerda: Controles do Abrigo */}
|
||||
<div className="w-full lg:w-96 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<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>
|
||||
@@ -140,9 +129,7 @@ const ShelterModule: React.FC = () => {
|
||||
<Layers className="size-5 text-primary" />
|
||||
Abrigos e Pórticos
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Pórticos em balanço, marquises e garagens com fechamentos (Tab. 23-25 e sec. 6.3).
|
||||
</CardDescription>
|
||||
<CardDescription>Pórticos em balanço (Tab. 23-25 e sec. 6.3).</CardDescription>
|
||||
</div>
|
||||
<SaveModuleDialog
|
||||
moduleType="shelter"
|
||||
@@ -152,76 +139,72 @@ const ShelterModule: React.FC = () => {
|
||||
<CardContent className="space-y-5">
|
||||
<WindParametersSummary />
|
||||
|
||||
{/* Seletor Rápido de 4 Condições Topológicas */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
Condições Topológicas
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
<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 text-xs text-left justify-start font-normal leading-tight"
|
||||
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-1.5 shrink-0", condition === c ? "text-primary-foreground" : "text-muted-foreground")} />
|
||||
<span>{conditionLabels[c].split(' (')[0]}</span>
|
||||
<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>
|
||||
|
||||
{/* Controle de Posição da Abertura (Topo / Base / Distribuída) */}
|
||||
<div className="space-y-2 pt-1 border-t">
|
||||
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
Posição da Abertura no Fundo
|
||||
</label>
|
||||
<Select value={openingPos} onValueChange={(v) => setOpeningPos(v as OpeningPosition)}>
|
||||
<SelectTrigger className="text-xs h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="top">Fresta no Topo (Alívio de Arrancamento)</SelectItem>
|
||||
<SelectItem value="bottom">Vão na Base (Comportamento Placa Suspensa)</SelectItem>
|
||||
<SelectItem value="distributed">Abertura Uniformemente Distribuída</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Sliders de Fechamento Parcial (%) */}
|
||||
<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-2">
|
||||
<div className="flex justify-between text-xs">
|
||||
<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}%</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={[backClosure]} onValueChange={(v) => setBackClosure(v[0])} />
|
||||
<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">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-xs">
|
||||
<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={5} value={[leftClosure]} onValueChange={(v) => setLeftClosure(v[0])} />
|
||||
<Slider min={0} max={100} step={10} value={[leftClosure]} onValueChange={(v) => setLeftClosure(v[0])} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-xs">
|
||||
<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={5} value={[rightClosure]} onValueChange={(v) => setRightClosure(v[0])} />
|
||||
<Slider min={0} max={100} step={10} value={[rightClosure]} onValueChange={(v) => setRightClosure(v[0])} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sliders Geométricos */}
|
||||
<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
|
||||
@@ -229,23 +212,23 @@ const ShelterModule: React.FC = () => {
|
||||
<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} m</span>
|
||||
<span className="font-mono font-medium">{depth.toFixed(1)} m</span>
|
||||
</div>
|
||||
<Slider min={3} max={20} step={0.5} value={[depth]} onValueChange={(v) => setDepth(v[0])} />
|
||||
<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} m</span>
|
||||
</div>
|
||||
<Slider min={2.5} max={12} step={0.1} value={[height]} onValueChange={(v) => setHeight(v[0])} />
|
||||
<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} m</span>
|
||||
</div>
|
||||
<Slider min={4} max={30} step={1} value={[width]} onValueChange={(v) => setWidth(v[0])} />
|
||||
<Slider min={4} max={40} step={1} value={[width]} onValueChange={(v) => setWidth(v[0])} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-xs">
|
||||
@@ -255,25 +238,13 @@ const ShelterModule: React.FC = () => {
|
||||
<Slider min={0} max={30} step={1} value={[theta]} onValueChange={(v) => setTheta(v[0])} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Normativo */}
|
||||
<div className="rounded-lg border bg-secondary/30 p-3 text-xs space-y-1.5">
|
||||
<div className="flex items-center gap-1.5 font-semibold text-foreground">
|
||||
<ShieldCheck className="size-4 text-emerald-500 shrink-0" />
|
||||
<span>Norma NBR 6123:2023 — Sec. 6.3 & Tab. 23-25</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground leading-relaxed">{result.statusText}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Coluna Direita: 3D, Corte A-A e Quadro de Ações */}
|
||||
<div className="flex-1 flex flex-col gap-4">
|
||||
{/* Visualizador 3D com trava, manual (i) e corte em elevação */}
|
||||
<div className="flex-1 flex flex-col min-h-[460px] bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
||||
<div className="absolute top-4 left-4 z-10 flex flex-wrap gap-2">
|
||||
<Badge variant="secondary" className="bg-background/85 text-foreground border border-border backdrop-blur-sm">
|
||||
<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-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">
|
||||
@@ -285,42 +256,21 @@ const ShelterModule: React.FC = () => {
|
||||
Alívio Arrancamento: -{result.reliefPercentage}%
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="absolute top-4 right-4 z-10 flex items-center gap-2">
|
||||
{/* Botão de Corte em Elevação (Corte A-A) vs 3D */}
|
||||
<div className="bg-background/90 border border-border rounded-full p-1 shadow-sm flex items-center">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={viewMode === '3d' ? 'default' : 'ghost'}
|
||||
className="h-7 px-3 text-xs rounded-full"
|
||||
onClick={() => setViewMode('3d')}
|
||||
>
|
||||
<Box className="size-3.5 mr-1" />
|
||||
Vista 3D
|
||||
<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 === 'elevation' ? 'default' : 'ghost'}
|
||||
className="h-7 px-3 text-xs rounded-full"
|
||||
onClick={() => setViewMode('elevation')}
|
||||
>
|
||||
<Eye className="size-3.5 mr-1" />
|
||||
Corte A-A (Frestas)
|
||||
<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>
|
||||
|
||||
{/* Ícone (i) Manual Didático NBR 6123 */}
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<EducationalManual type="shelter" params={{ condition, backClosure, openingPos, theta }} />
|
||||
</div>
|
||||
|
||||
<SceneCanvas
|
||||
moduleId="shelter"
|
||||
camera={{ position: [12, 6, 14], fov: 45 }}
|
||||
fallback={<div className="flex items-center justify-center h-full text-muted-foreground">3D indisponível</div>}
|
||||
>
|
||||
<ambientLight intensity={0.7} />
|
||||
<directionalLight position={[10, 20, 15]} intensity={1.2} />
|
||||
{viewMode === '3d' ? (
|
||||
<Shelter3D
|
||||
condition={condition}
|
||||
depth={depth}
|
||||
@@ -328,79 +278,91 @@ const ShelterModule: React.FC = () => {
|
||||
height={height}
|
||||
theta={theta}
|
||||
backClosure={backClosure}
|
||||
backRange={backRange}
|
||||
leftClosure={leftClosure}
|
||||
rightClosure={rightClosure}
|
||||
openingPos={openingPos}
|
||||
viewMode={viewMode}
|
||||
/>
|
||||
</SceneCanvas>
|
||||
) : (
|
||||
<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 }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quadro Superior de Resultados na Base do 3D */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<Card className="bg-card border-border">
|
||||
<CardContent className="p-4 flex flex-col justify-between h-full">
|
||||
<span className="text-xs text-muted-foreground font-medium">F. Vertical (Fz - Arrancamento)</span>
|
||||
<div className="flex items-baseline justify-between mt-2">
|
||||
<span className="text-xl font-bold font-mono text-primary">{result.forces.fzUp} kN</span>
|
||||
<span className="text-xs text-muted-foreground font-mono">Cnf = {result.cnfVertical}</span>
|
||||
<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 Calculadas
|
||||
<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>Coeficiente Cnf</span>
|
||||
<span className="font-mono">{result.cnfVertical}</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 no Fundo (Fx)</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>
|
||||
<div className="flex justify-between items-center pb-2 border-b">
|
||||
<span className="text-muted-foreground">Empuxo Lateral (Fy)</span>
|
||||
<span className="font-mono font-bold text-foreground">{result.forces.fySide.toFixed(2)} kN</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center pb-2 border-b">
|
||||
<span className="text-primary font-medium">Carga no Pilar (Pórtico)</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 pb-4 border-b">
|
||||
<span className="text-primary font-medium">Carga na Viga (Balanço)</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="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>
|
||||
<span className="text-[10px] text-muted-foreground mt-1">
|
||||
{result.reliefPercentage > 0 ? `Com ${result.reliefPercentage}% de alívio por fresta` : 'Sem alívio por fresta superior'}
|
||||
</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardContent className="p-4 flex flex-col justify-between h-full">
|
||||
<span className="text-xs text-muted-foreground font-medium">Empuxo no Fundo (Fx Traseira)</span>
|
||||
<div className="flex items-baseline justify-between mt-2">
|
||||
<span className="text-xl font-bold font-mono text-foreground">{result.forces.fxBack} kN</span>
|
||||
<span className="text-xs text-muted-foreground font-mono">Cf = {result.cfBack}</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground mt-1">
|
||||
Parede com {backClosure}% de estanqueidade
|
||||
</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardContent className="p-4 flex flex-col justify-between h-full">
|
||||
<span className="text-xs text-muted-foreground font-medium">Carga no Pilar (ex: Tubo 150x150)</span>
|
||||
<div className="flex items-baseline justify-between mt-2">
|
||||
<span className="text-xl font-bold font-mono text-emerald-600 dark:text-emerald-400">
|
||||
{result.lineLoads.columnLoad} kN/m
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground font-mono">h = {height} m</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground mt-1">
|
||||
Ação direta + reação horizontal por pilar
|
||||
</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card border-border">
|
||||
<CardContent className="p-4 flex flex-col justify-between h-full">
|
||||
<span className="text-xs text-muted-foreground font-medium">Carga na Viga em Balanço</span>
|
||||
<div className="flex items-baseline justify-between mt-2">
|
||||
<span className="text-xl font-bold font-mono text-sky-600 dark:text-sky-400">
|
||||
{result.lineLoads.beamLoad} kN/m
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground font-mono">d = {depth} m</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground mt-1">
|
||||
Sucção distribuída por braço principal
|
||||
</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Rodapé com Exportação e Captura */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 border-t pt-4">
|
||||
<SceneCapturePanel sceneId="shelter-module" />
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user