Compare commits
25 Commits
ce793d2213
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b6c96ada9c | |||
| 3219011b33 | |||
| f66ae1a3aa | |||
| 91cb5a78c5 | |||
| 7b179aaeea | |||
| 02811b09a4 | |||
| fd71215958 | |||
| f787e8c002 | |||
| b842d6009f | |||
| ddcb56d78b | |||
| f1cbfc1604 | |||
| 6b6ff35bbd | |||
| 495dace14c | |||
| 2ee2632556 | |||
| 5f9580436c | |||
| 0a4dc9b57a | |||
| ff01f68941 | |||
| 8d5f70e94a | |||
| dc90b289c5 | |||
| a30125c7ce | |||
| fde885ca60 | |||
| d4d4905798 | |||
| b1cadac6ab | |||
| 6141b9a41c | |||
| dff84c78ba |
@@ -0,0 +1,74 @@
|
|||||||
|
# 🌪️ BrainWind: Dossiê Institucional & Técnico
|
||||||
|
|
||||||
|
> **A plataforma definitiva de engenharia para cálculo aerodinâmico e ações do vento em conformidade integral com a ABNT NBR 6123:2023.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. O que é o BrainWind?
|
||||||
|
|
||||||
|
O **BrainWind** é um software de engenharia estrutural de última geração, desenvolvido para calcular, simular e validar com máxima precisão e interatividade visual as **forças e pressões do vento** incidentes sobre as mais diversas tipologias estruturais.
|
||||||
|
|
||||||
|
Diferente de planilhas eletrônicas frágeis ou módulos estáticos e burocráticos presentes em softwares tradicionais, o BrainWind transforma formulações matemáticas densas da **NBR 6123:2023** em uma experiência visual em tempo real (3D e 2D), didática e instantaneamente acionável para o projetista estrutural.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Diferenciais Competitivos no Mercado
|
||||||
|
|
||||||
|
| Aspecto | Softwares Tradicionais / Planilhas | **BrainWind** |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Norma Técnica** | Fórmulas legadas ou parciais (NBR 6123:1988) | **100% de Cobertura da NBR 6123:2023** (todas as 36 tabelas e anexos oficiais) |
|
||||||
|
| **Visualização** | Gráficos 2D estáticos ou tabelas em preto e branco | **Modelos 3D Interativos (Three.js)** + Linhas de Fluxo de Vento (*Airflow*) + Heatmaps 2D em tempo real |
|
||||||
|
| **Comparação de Cenários** | Necessidade de rodar cálculos separados | **Modo A/B Simultâneo** (vento a 0° vs 90° lado a lado na mesma tela) |
|
||||||
|
| **Validação e Confiabilidade** | "Caixa-preta" sem comprovação de gabarito | **TesteBib**: Validação em tempo real contra casos clássicos da literatura (Blessmann, Pfeil, Rebello) |
|
||||||
|
| **Integração com Análise** | Digitação manual de cargas nó a nó | **Exportação nativa para Ftool (.txt)**, PDFs com memorial didático e CSVs |
|
||||||
|
| **Auditoria por Inteligência Artificial** | Inexistente | **Módulo de Auditoria Automatizada por LLM** que audita os cálculos e consistência |
|
||||||
|
| **Acessibilidade** | Exige instalação pesada de desktop | **PWA Moderno (Web/Mobile)**, ultra-rápido, funciona offline com persistência IndexedDB |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Principais Highlights que Surpreendem
|
||||||
|
|
||||||
|
### 📐 1. Abrangência Estrutural sem Paralelo (14 Módulos)
|
||||||
|
O BrainWind cobre desde a edificação mais comum até estruturas industriais e especiais complexas:
|
||||||
|
- **Galpões e Edifícios Paralelepipédicos:** Zonas de pressão externa (A, B, C, D nas paredes; E, F, G, H, I, J no telhado), coeficientes $C_{pe}$ e pressões internas $C_{pi}$.
|
||||||
|
- **Silos, Tanques e Chaminés Cilíndricas:** Número de Reynolds ($Re$), regimes sub/supercrítico e perfil completo da Tabela 13.
|
||||||
|
- **Abóbadas Cilíndricas & Cúpulas Semiesféricas:** Sustentação aerodinâmica, empuxo ascendente e regimes laminares/turbulentos.
|
||||||
|
- **Muros, Placas e Platibandas:** Forças de arrasto em placas com ou sem folga em relação ao solo (*ground clearance*).
|
||||||
|
- **Coberturas Isoladas (Hangares e Marquises):** A uma água, duas águas simétricas e formato em "V" (borboleta).
|
||||||
|
- **Barras Prismáticas & Cantoneiras:** Coeficientes de força $C_x$ e $C_y$ para qualquer ângulo $\alpha$ e fator de redução por comprimento finito ($K$).
|
||||||
|
- **Pontes Rodoviárias/Ferroviárias:** Classificação estática/dinâmica e forças de sustentação e arrasto no tabuleiro.
|
||||||
|
- **Torres Treliçadas de Telecomunicação:** Seções quadradas e triangulares com índice de plenitude $\varphi$.
|
||||||
|
- **Pipe-Racks Industriais:** Quadros estruturais múltiplos com bandejas e tubulações, calculando sombreamento ($\eta$) e arrasto global.
|
||||||
|
- **Abrigos Leves & Tendas Tensionadas:** Checagem de arrancamento do vento (*uplift*) vs massa superficial gravitacional.
|
||||||
|
- **Efeitos Dinâmicos e Conforto Humano:** Desprendimento de vórtices (Von Kármán, Strouhal, Scruton) e critérios de aceleração máxima conforme a ISO 10137.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔬 2. Validação Bibliográfica Transparente (TesteBib)
|
||||||
|
O BrainWind não esconde cálculos. Na aba **TesteBib**, o projetista tem acesso a dezenas de problemas resolvidos passo a passo extraídos dos principais manuais brasileiros de engenharia:
|
||||||
|
- Prof. **Joaquim Blessmann** (*O Vento na Engenharia Estrutural*, *Ação do Vento em Torres*, *Ação do Vento em Coberturas Curvas*).
|
||||||
|
- Prof. **Walter Pfeil & Michèle Pfeil** (*Estruturas de Aço*).
|
||||||
|
- Prof. **Yopanan Rebello** (*A Concepção Estrutural e a Arquitetura*).
|
||||||
|
|
||||||
|
Cada cenário compara os resultados em tempo real do motor de cálculo (*Kernel Local*) com o gabarito oficial da literatura, comprovando tolerância zero a desvios.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🎨 3. UI/UX de Alta Engenharia com "Modo A/B"
|
||||||
|
- **Visualizador 3D com Linhas de Corrente:** Permite alternar entre o modo *Sólido* (com mapeamento de cor para sucção em vermelho e sobrepressão em azul) e o modo *Fluxo* (partículas animadas que mostram o desvio do vento).
|
||||||
|
- **Heatmap 2D Proporcional:** Planta baixa detalhada das zonas de pressão com indicação de vento e cotas dinâmicas.
|
||||||
|
- **Modo A/B Integrado:** Visualize no mesmo painel a planta do galpão sofrendo vento a 0° e vento a 90°, permitindo identificar instantaneamente o pior caso para o dimensionamento dos pilares e terças.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ⚡ 4. Produtividade Máxima no Fluxo de Projeto
|
||||||
|
- **Geração de Cargas Lineares:** Transforma pressões superficiais ($\text{kN/m}^2$) em cargas lineares distribuídas ($\text{kN/m}$) para os pórticos principais e terças de cobertura.
|
||||||
|
- **Combinações de Ações (NBR 8681 / NBR 6118):** Combinação automática em ELU e ELS ($1.4\,PP + 1.4\,Vento$).
|
||||||
|
- **Exportação Ftool (.txt):** Exporta diretamente a geometria do pórtico e as cargas de vento prontas para serem abertas e calculadas no Ftool.
|
||||||
|
- **Certificado de Fidelidade Normativa:** Emissão de certificado digital de conformidade com a NBR 6123:2023.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Conclusão
|
||||||
|
|
||||||
|
O **BrainWind** posiciona-se não apenas como uma calculadora, mas como uma **suíte completa de inteligência aerodinâmica para a engenharia estrutural**. Ele une o rigor matemático absoluto da ABNT ao que há de mais avançado em tecnologia web e computação gráfica, garantindo projetos mais seguros, econômicos, auditáveis e profissionais.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[phases.setup]
|
||||||
|
nixPkgs = ["nodejs_23"]
|
||||||
|
|
||||||
|
[start]
|
||||||
|
cmd = "npx --yes serve@latest -s dist"
|
||||||
Generated
+5144
-6
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
|
"start": "npx --yes serve@latest -s dist",
|
||||||
"lint": "oxlint",
|
"lint": "oxlint",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "npm run test:engenharia && npm run test:sistema",
|
"test": "npm run test:engenharia && npm run test:sistema",
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"three": "^0.185.1",
|
"three": "^0.185.1",
|
||||||
|
"zundo": "^2.3.0",
|
||||||
"zustand": "^5.0.14"
|
"zustand": "^5.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -49,6 +51,7 @@
|
|||||||
"tailwindcss": "^4.3.2",
|
"tailwindcss": "^4.3.2",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"vite": "^8.1.1",
|
"vite": "^8.1.1",
|
||||||
|
"vite-plugin-pwa": "^1.3.0",
|
||||||
"vitest": "^4.1.10"
|
"vitest": "^4.1.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-93
@@ -1,19 +1,20 @@
|
|||||||
import { BrowserRouter, Routes, Route, Link, useLocation } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, Link, useLocation } from 'react-router-dom';
|
||||||
import GalpaoModule from './pages/GalpaoModule';
|
// Lazy load modules to improve performance and code splitting
|
||||||
import CylinderModule from './pages/CylinderModule';
|
const GalpaoModule = lazy(() => import('./pages/GalpaoModule'));
|
||||||
import VaultModule from './pages/VaultModule';
|
const CylinderModule = lazy(() => import('./pages/CylinderModule'));
|
||||||
import DomeModule from './pages/DomeModule';
|
const VaultModule = lazy(() => import('./pages/VaultModule'));
|
||||||
import SignModule from './pages/SignModule';
|
const DomeModule = lazy(() => import('./pages/DomeModule'));
|
||||||
import IsolatedRoofModule from './pages/IsolatedRoofModule';
|
const SignModule = lazy(() => import('./pages/SignModule'));
|
||||||
import BarSelectorModule from './pages/BarSelectorModule';
|
const IsolatedRoofModule = lazy(() => import('./pages/IsolatedRoofModule'));
|
||||||
import BridgeModule from './pages/BridgeModule';
|
const BarSelectorModule = lazy(() => import('./pages/BarSelectorModule'));
|
||||||
import DynamicsModule from './pages/DynamicsModule';
|
const BridgeModule = lazy(() => import('./pages/BridgeModule'));
|
||||||
import CalcCaModule from './pages/CalcCaModule';
|
const DynamicsModule = lazy(() => import('./pages/DynamicsModule'));
|
||||||
import SettingsModule from './pages/SettingsModule';
|
const CalcCaModule = lazy(() => import('./pages/CalcCaModule'));
|
||||||
import TowerModule from './pages/TowerModule';
|
const SettingsModule = lazy(() => import('./pages/SettingsModule'));
|
||||||
import PiperackModule from './pages/PiperackModule';
|
const TowerModule = lazy(() => import('./pages/TowerModule'));
|
||||||
import ShelterModule from './pages/ShelterModule';
|
const PiperackModule = lazy(() => import('./pages/PiperackModule'));
|
||||||
import CertificatePage from './pages/CertificatePage';
|
const ShelterModule = lazy(() => import('./pages/ShelterModule'));
|
||||||
|
const CertificatePage = lazy(() => import('./pages/CertificatePage'));
|
||||||
import {
|
import {
|
||||||
Home, Settings, Menu, Cylinder, Church, CircleDot,
|
Home, Settings, Menu, Cylinder, Church, CircleDot,
|
||||||
Square, Layers, BarChart3, Activity, Warehouse,
|
Square, Layers, BarChart3, Activity, Warehouse,
|
||||||
@@ -26,7 +27,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { useState } from 'react';
|
import { useState, lazy, Suspense } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ThemeProvider, useTheme } from '@/lib/theme';
|
import { ThemeProvider, useTheme } from '@/lib/theme';
|
||||||
@@ -74,6 +75,8 @@ const TowerIcon = (props: React.SVGProps<SVGSVGElement>) => (
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
import HomeMock from './pages/HomeMock';
|
||||||
|
|
||||||
function AppLayout({ children }: { children: React.ReactNode }) {
|
function AppLayout({ children }: { children: React.ReactNode }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
@@ -347,66 +350,7 @@ function AppLayout({ children }: { children: React.ReactNode }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function HomeMock() {
|
|
||||||
const { t } = useI18n();
|
|
||||||
const modules = [
|
|
||||||
{ to: '/galpao', icon: Warehouse, label: 'Galpão Retangular', desc: 'Paredes A/B/C/D, telhados E-J, excentricidade, atrito, alta turbulência.' },
|
|
||||||
{ to: '/cilindro', icon: Cylinder, label: 'Cilindro Vertical', desc: 'Silos, reservatórios, chaminés. Cpe por ângulo (Tab. 13), Reynolds.' },
|
|
||||||
{ to: '/abobada', icon: Church, label: 'Abóbada Cilíndrica', desc: 'Coberturas curvas em arco. Tab. 15-20, 6 zonas.' },
|
|
||||||
{ to: '/cupula', icon: CircleDot, label: 'Cúpula', desc: 'Sobre terreno (Tab. 21) ou parede cilíndrica (Tab. 22).' },
|
|
||||||
{ to: '/muros', icon: Square, label: 'Muros e Placas', desc: 'Cf para vento perpendicular e oblíquo (Tab. 23).' },
|
|
||||||
{ to: '/cobertura-isolada', icon: Layers, label: 'Coberturas Isoladas', desc: 'Uma ou duas águas, abas perpendiculares (Tab. 24-25).' },
|
|
||||||
{ to: '/barras', icon: BarChart3, label: 'Barras Prismáticas', desc: 'Faces planas (Tab. 26) ou circulares (Tab. 27).' },
|
|
||||||
{ to: '/pontes', icon: BridgeIcon, label: 'Pontes', desc: 'Pse, Cx/Cz do tabuleiro, flutter, galope (sec. 11).' },
|
|
||||||
{ to: '/torre', icon: TowerIcon, label: 'Torres', desc: 'Torres treliçadas e mastros (sec. 8.4).' },
|
|
||||||
{ to: '/piperack', icon: Frame, label: 'Pipe-Racks Industriais', desc: 'Estruturas reticuladas múltiplas, tubulações, fator η (Tab. 28).' },
|
|
||||||
{ to: '/abrigo', icon: Tent, label: 'Abrigos e Pórticos Fechados', desc: 'Marquises, abrigos e pórticos. 4 condições topológicas, frestas de alívio, corte A-A (Tab. 23-25, sec. 6.3).' },
|
|
||||||
{ to: '/calc-ca', icon: Calculator, label: 'Calculadora de C_a', desc: 'Coeficiente de arrasto (Fig. 4 e 5).' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-8 max-w-6xl mx-auto space-y-8">
|
|
||||||
<header className="text-center space-y-4 flex flex-col items-center mt-4 mb-6">
|
|
||||||
<div className="relative inline-block">
|
|
||||||
<img src="/logo_brainwind.png" alt="BrainWind" className="h-20 md:h-28 w-auto object-contain dark:drop-shadow-[0_0_15px_rgba(255,255,255,0.4)]" />
|
|
||||||
<span className="absolute -bottom-1 -right-2 md:-right-6 bg-primary text-primary-foreground text-[10px] md:text-xs font-bold px-2 py-0.5 rounded-full shadow-sm border border-primary-foreground/20">v1.0</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-muted-foreground text-lg">
|
|
||||||
{t('app_subtitle')}
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
{modules.map((m) => (
|
|
||||||
<Link key={m.to} to={m.to} className="block p-6 rounded-xl border bg-card hover:shadow-lg hover:border-primary transition-all">
|
|
||||||
<m.icon className="w-8 h-8 text-primary mb-3" />
|
|
||||||
<h2 className="font-semibold text-lg mb-1">{m.label}</h2>
|
|
||||||
<p className="text-sm text-muted-foreground">{m.desc}</p>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-lg border bg-muted/40 p-4 text-sm text-muted-foreground">
|
|
||||||
<p className="font-medium text-foreground mb-2">{t('home_full_coverage')}</p>
|
|
||||||
<ul className="grid grid-cols-2 md:grid-cols-4 gap-1 text-xs">
|
|
||||||
<li>✓ Sec. 5 — V₀, S₁, S₂, S₃</li>
|
|
||||||
<li>✓ Sec. 6.1 — Paralelepipédicas</li>
|
|
||||||
<li>✓ Sec. 6.2 — Cilindros, abóbadas, cúpulas</li>
|
|
||||||
<li>✓ Sec. 6.3 — Pressão interna (Cpi)</li>
|
|
||||||
<li>✓ Sec. 6.4 — Vizinhança</li>
|
|
||||||
<li>✓ Sec. 7 — Muros, coberturas isoladas</li>
|
|
||||||
<li>✓ Sec. 8 — Barras e reticulados</li>
|
|
||||||
<li>✓ Sec. 9 — Efeitos dinâmicos</li>
|
|
||||||
<li>✓ Sec. 10 — Vórtices</li>
|
|
||||||
<li>✓ Sec. 11 — Pontes</li>
|
|
||||||
<li>✓ Anexo A — S₂(qualquer t)</li>
|
|
||||||
<li>✓ Anexo B — S₃(Pₘ, vida útil)</li>
|
|
||||||
<li>✓ Anexo C — 49 estações meteorológicas</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -414,24 +358,26 @@ function App() {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<Routes>
|
<Suspense fallback={<div className="flex h-full w-full items-center justify-center text-muted-foreground">Carregando módulo...</div>}>
|
||||||
<Route path="/" element={<HomeMock />} />
|
<Routes>
|
||||||
<Route path="/galpao" element={<GalpaoModule />} />
|
<Route path="/" element={<HomeMock />} />
|
||||||
<Route path="/cilindro" element={<CylinderModule />} />
|
<Route path="/galpao" element={<GalpaoModule />} />
|
||||||
<Route path="/abobada" element={<VaultModule />} />
|
<Route path="/cilindro" element={<CylinderModule />} />
|
||||||
<Route path="/cupula" element={<DomeModule />} />
|
<Route path="/abobada" element={<VaultModule />} />
|
||||||
<Route path="/muros" element={<SignModule />} />
|
<Route path="/cupula" element={<DomeModule />} />
|
||||||
<Route path="/cobertura-isolada" element={<IsolatedRoofModule />} />
|
<Route path="/muros" element={<SignModule />} />
|
||||||
<Route path="/barras" element={<BarSelectorModule />} />
|
<Route path="/cobertura-isolada" element={<IsolatedRoofModule />} />
|
||||||
<Route path="/pontes" element={<BridgeModule />} />
|
<Route path="/barras" element={<BarSelectorModule />} />
|
||||||
<Route path="/torre" element={<TowerModule />} />
|
<Route path="/pontes" element={<BridgeModule />} />
|
||||||
<Route path="/piperack" element={<PiperackModule />} />
|
<Route path="/torre" element={<TowerModule />} />
|
||||||
<Route path="/abrigo" element={<ShelterModule />} />
|
<Route path="/piperack" element={<PiperackModule />} />
|
||||||
<Route path="/dinamica" element={<DynamicsModule />} />
|
<Route path="/abrigo" element={<ShelterModule />} />
|
||||||
<Route path="/calc-ca" element={<CalcCaModule />} />
|
<Route path="/dinamica" element={<DynamicsModule />} />
|
||||||
<Route path="/settings" element={<SettingsModule />} />
|
<Route path="/calc-ca" element={<CalcCaModule />} />
|
||||||
<Route path="/certificado" element={<CertificatePage />} />
|
<Route path="/settings" element={<SettingsModule />} />
|
||||||
</Routes>
|
<Route path="/certificado" element={<CertificatePage />} />
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export function DragCoefficientChart({ l1, l2, h, caLow, caHigh }: DragCoefficie
|
|||||||
|
|
||||||
{/* Fake Curves */}
|
{/* Fake Curves */}
|
||||||
{curves.map((d, idx) => (
|
{curves.map((d, idx) => (
|
||||||
<path key={idx} d={d} fill="none" stroke="currentColor" strokeOpacity={0.2} strokeWidth={1.5} className="text-red-500" />
|
<path key={idx} d={d} fill="none" strokeWidth={1.5} className="stroke-amber-500/40 dark:stroke-amber-400/60" />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Axes */}
|
{/* Axes */}
|
||||||
|
|||||||
@@ -400,7 +400,7 @@ function TowerDiagram({ baseWidth, height, panels, phi, forceKN }: TowerProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IsolatedRoofProps {
|
interface IsolatedRoofProps {
|
||||||
type: 'shed' | 'gable';
|
type: 'shed' | 'gable' | 'butterfly';
|
||||||
theta: number;
|
theta: number;
|
||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ export function GlobalWindSettingsModal({ trigger }: GlobalWindSettingsModalProp
|
|||||||
setTerrainCategory,
|
setTerrainCategory,
|
||||||
locality,
|
locality,
|
||||||
setLocality,
|
setLocality,
|
||||||
|
fviz,
|
||||||
|
setFviz,
|
||||||
} = useWindStore();
|
} = useWindStore();
|
||||||
|
|
||||||
const [stationQuery, setStationQuery] = useState('');
|
const [stationQuery, setStationQuery] = useState('');
|
||||||
@@ -77,6 +79,7 @@ export function GlobalWindSettingsModal({ trigger }: GlobalWindSettingsModalProp
|
|||||||
<TabsList className="flex w-full mb-4 bg-muted p-1 text-xs min-w-0">
|
<TabsList className="flex w-full mb-4 bg-muted p-1 text-xs min-w-0">
|
||||||
<TabsTrigger value="norma" className="text-xs py-1 px-1.5 flex-1">NBR 6123</TabsTrigger>
|
<TabsTrigger value="norma" className="text-xs py-1 px-1.5 flex-1">NBR 6123</TabsTrigger>
|
||||||
<TabsTrigger value="local" className="text-xs py-1 px-1.5 flex-1">Localidade</TabsTrigger>
|
<TabsTrigger value="local" className="text-xs py-1 px-1.5 flex-1">Localidade</TabsTrigger>
|
||||||
|
<TabsTrigger value="vizinhanca" className="text-xs py-1 px-1.5 flex-1">Vizinhança 3D</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="norma" className="space-y-4 min-w-0">
|
<TabsContent value="norma" className="space-y-4 min-w-0">
|
||||||
@@ -215,6 +218,21 @@ export function GlobalWindSettingsModal({ trigger }: GlobalWindSettingsModalProp
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="vizinhanca" className="space-y-4 min-w-0">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<label className="text-xs font-medium text-foreground">Fator de Vizinhança (Fviz)</label>
|
||||||
|
<span className="text-xs text-muted-foreground font-mono">{fviz.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<Slider min={0.7} max={1.3} step={0.01} value={[fviz]} onValueChange={(vals) => setFviz(vals[0])} className="py-1 cursor-pointer" />
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
Modifica diretamente a pressão dinâmica (q).<br/>
|
||||||
|
• <b>< 1.0</b>: Efeito de sombreamento (edifícios ao redor protegem a estrutura).<br/>
|
||||||
|
• <b>> 1.0</b>: Efeito funil ou fenda (edifícios ao redor aceleram o vento).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useGalpaoStore } from '../store/galpaoStore';
|
||||||
|
import { useWindStore } from '../store/appStore';
|
||||||
|
import type { WallCoefficients, RoofCoefficients } from '../lib/coefficients';
|
||||||
|
|
||||||
|
interface Heatmap2DProps {
|
||||||
|
overrideAngle?: 0 | 90;
|
||||||
|
overrideWallCpe?: WallCoefficients;
|
||||||
|
overrideRoofCpe?: RoofCoefficients;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapeia o valor da pressão (Cpe - Cpi) para uma cor.
|
||||||
|
* Sucção (negativo) = Tons de vermelho/laranja.
|
||||||
|
* Pressão (positivo) = Tons de azul.
|
||||||
|
*/
|
||||||
|
const getPressureColor = (cpe: number, cpi: number): string => {
|
||||||
|
const p = cpe - cpi;
|
||||||
|
if (p < 0) {
|
||||||
|
const intensity = Math.min(Math.abs(p) / 1.5, 1); // Normaliza sucção até -1.5
|
||||||
|
return `rgba(239, 68, 68, ${0.2 + intensity * 0.7})`; // Vermelho
|
||||||
|
} else if (p > 0) {
|
||||||
|
const intensity = Math.min(p / 1.0, 1); // Normaliza pressão até +1.0
|
||||||
|
return `rgba(59, 130, 246, ${0.2 + intensity * 0.7})`; // Azul
|
||||||
|
}
|
||||||
|
return 'rgba(200, 200, 200, 0.3)'; // Neutro
|
||||||
|
};
|
||||||
|
|
||||||
|
const Heatmap2D: React.FC<Heatmap2DProps> = ({ overrideAngle, overrideWallCpe, overrideRoofCpe }) => {
|
||||||
|
const storeGalpao = useGalpaoStore();
|
||||||
|
const storeWind = useWindStore();
|
||||||
|
|
||||||
|
const width = storeGalpao.width;
|
||||||
|
const length = storeGalpao.length;
|
||||||
|
const wallCpe = overrideWallCpe || storeGalpao.wallCpe;
|
||||||
|
const roofCpe = overrideRoofCpe || storeGalpao.roofCpe;
|
||||||
|
const windAngle = overrideAngle !== undefined ? overrideAngle : storeWind.windAngle;
|
||||||
|
const cpi = storeWind.cpi;
|
||||||
|
|
||||||
|
// Dimensões base do SVG
|
||||||
|
const svgW = 600;
|
||||||
|
const svgH = 600;
|
||||||
|
const margin = 100;
|
||||||
|
|
||||||
|
// Fator de escala para caber a planta (width x length) na tela
|
||||||
|
const maxDim = Math.max(width, length);
|
||||||
|
const scale = (svgW - margin * 2) / maxDim;
|
||||||
|
|
||||||
|
// Tamanhos desenhados
|
||||||
|
const drawW = width * scale;
|
||||||
|
const drawL = length * scale;
|
||||||
|
|
||||||
|
// Centro do desenho
|
||||||
|
const cx = svgW / 2;
|
||||||
|
const cy = svgH / 2;
|
||||||
|
const x0 = cx - drawW / 2;
|
||||||
|
const y0 = cy - drawL / 2;
|
||||||
|
|
||||||
|
// Determinar larguras das zonas do telhado (baseado na NBR 6123, onde as bordas têm distâncias específicas)
|
||||||
|
// Para simplificar graficamente o Heatmap, usaremos proporções visuais para E, F, G, H
|
||||||
|
const zoneSizeX = Math.min(drawW * 0.2, 40); // Representação genérica da zona E/F
|
||||||
|
const zoneSizeY = Math.min(drawL * 0.2, 40);
|
||||||
|
|
||||||
|
// A NBR 6123 rotaciona as zonas dependendo de windAngle (0° vs 90°)
|
||||||
|
// Vento 0° = perpendicular ao comprimento (Z). A=Frente, B=Fundo, C=Lado, D=Lado
|
||||||
|
// Vento 90° = perpendicular à largura (X). A=Lado, B=Lado, C=Frente, D=Fundo
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex flex-col items-center justify-center p-4">
|
||||||
|
<div className="bg-background rounded-xl p-4 shadow-sm border border-border w-full max-w-2xl flex flex-col max-h-full">
|
||||||
|
<h3 className="text-center font-semibold mb-2 shrink-0">Planta Baixa - Telhado e Paredes</h3>
|
||||||
|
<p className="text-xs text-center text-muted-foreground mb-4 shrink-0">Vento incidindo a {windAngle}°</p>
|
||||||
|
|
||||||
|
<div className="flex-1 min-h-0 relative flex items-center justify-center w-full">
|
||||||
|
<svg width="100%" height="100%" viewBox={`0 0 ${svgW} ${svgH}`} className="max-w-[400px] max-h-[40vh] mx-auto overflow-visible" style={{ maxHeight: 'min(400px, 40vh)' }}>
|
||||||
|
<defs>
|
||||||
|
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" className="text-foreground" />
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{/* Vento (Setas Dinâmicas) */}
|
||||||
|
{windAngle === 90 && (
|
||||||
|
<g className="text-blue-500" stroke="currentColor" strokeWidth="3" markerEnd="url(#arrow)">
|
||||||
|
<line x1={x0 - 100} y1={cy} x2={x0 - 60} y2={cy} />
|
||||||
|
<line x1={x0 - 100} y1={cy - 40} x2={x0 - 60} y2={cy - 40} />
|
||||||
|
<line x1={x0 - 100} y1={cy + 40} x2={x0 - 60} y2={cy + 40} />
|
||||||
|
<text x={x0 - 110} y={cy} textAnchor="end" dominantBaseline="middle" className="text-2xl font-bold fill-blue-500" stroke="none">Vento (90°)</text>
|
||||||
|
</g>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{windAngle === 0 && (
|
||||||
|
<g className="text-blue-500" stroke="currentColor" strokeWidth="3" markerEnd="url(#arrow)">
|
||||||
|
<line x1={cx} y1={y0 - 100} x2={cx} y2={y0 - 60} />
|
||||||
|
<line x1={cx - 40} y1={y0 - 100} x2={cx - 40} y2={y0 - 60} />
|
||||||
|
<line x1={cx + 40} y1={y0 - 100} x2={cx + 40} y2={y0 - 60} />
|
||||||
|
<text x={cx} y={y0 - 115} textAnchor="middle" className="text-2xl font-bold fill-blue-500" stroke="none">Vento (0°)</text>
|
||||||
|
</g>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Área Total do Telhado (Fundo) */}
|
||||||
|
<rect x={x0} y={y0} width={drawW} height={drawL} fill="white" stroke="#ccc" strokeWidth="2" />
|
||||||
|
|
||||||
|
{/* Zonas do Telhado (Simplificadas para Vento a 0 ou 90) */}
|
||||||
|
{windAngle === 90 ? (
|
||||||
|
// Vento 90° -> Zonas E, G na esquerda, F, H na direita etc. (Esquema simplificado de visualização)
|
||||||
|
<>
|
||||||
|
{/* Metade Barlavento (Esquerda) */}
|
||||||
|
<rect x={x0} y={y0} width={drawW/2} height={drawL} fill={getPressureColor(roofCpe.E || 0, cpi)} stroke="#666" />
|
||||||
|
{/* Metade Sotavento (Direita) */}
|
||||||
|
<rect x={x0 + drawW/2} y={y0} width={drawW/2} height={drawL} fill={getPressureColor(roofCpe.F || 0, cpi)} stroke="#666" />
|
||||||
|
{/* Zonas Críticas */}
|
||||||
|
<rect x={x0} y={y0} width={zoneSizeX} height={zoneSizeY} fill={getPressureColor(roofCpe.G || 0, cpi)} stroke="#333" />
|
||||||
|
<rect x={x0} y={y0 + drawL - zoneSizeY} width={zoneSizeX} height={zoneSizeY} fill={getPressureColor(roofCpe.G || 0, cpi)} stroke="#333" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
// Vento 0°
|
||||||
|
<>
|
||||||
|
{/* Metade Barlavento (Topo) */}
|
||||||
|
<rect x={x0} y={y0} width={drawW} height={drawL/2} fill={getPressureColor(roofCpe.E || 0, cpi)} stroke="#666" />
|
||||||
|
{/* Metade Sotavento (Baixo) */}
|
||||||
|
<rect x={x0} y={y0 + drawL/2} width={drawW} height={drawL/2} fill={getPressureColor(roofCpe.F || 0, cpi)} stroke="#666" />
|
||||||
|
{/* Zonas Críticas */}
|
||||||
|
<rect x={x0} y={y0} width={zoneSizeX} height={zoneSizeY} fill={getPressureColor(roofCpe.G || 0, cpi)} stroke="#333" />
|
||||||
|
<rect x={x0 + drawW - zoneSizeX} y={y0} width={zoneSizeX} height={zoneSizeY} fill={getPressureColor(roofCpe.G || 0, cpi)} stroke="#333" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Labels do Telhado (Simplificados) */}
|
||||||
|
<text x={cx} y={cy} textAnchor="middle" dominantBaseline="middle" className="text-sm font-bold fill-foreground" style={{ mixBlendMode: 'difference' }}>
|
||||||
|
TELHADO
|
||||||
|
</text>
|
||||||
|
|
||||||
|
{/* PAREDES (Desenhadas como bordas expandidas) */}
|
||||||
|
{/* Parede A (Frente em 0°, Lado Esq em 90°) */}
|
||||||
|
{windAngle === 0 ? (
|
||||||
|
<rect x={x0} y={y0 - 15} width={drawW} height={15} fill={getPressureColor(wallCpe.A, cpi)} stroke="#111" />
|
||||||
|
) : (
|
||||||
|
<rect x={x0 - 15} y={y0} width={15} height={drawL} fill={getPressureColor(wallCpe.A, cpi)} stroke="#111" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Parede B (Fundo em 0°, Lado Dir em 90°) */}
|
||||||
|
{windAngle === 0 ? (
|
||||||
|
<rect x={x0} y={y0 + drawL} width={drawW} height={15} fill={getPressureColor(wallCpe.B, cpi)} stroke="#111" />
|
||||||
|
) : (
|
||||||
|
<rect x={x0 + drawW} y={y0} width={15} height={drawL} fill={getPressureColor(wallCpe.B, cpi)} stroke="#111" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Parede C (Lateral Dir em 0°, Frente em 90°) */}
|
||||||
|
{windAngle === 0 ? (
|
||||||
|
<rect x={x0 + drawW} y={y0} width={15} height={drawL} fill={getPressureColor(wallCpe.C, cpi)} stroke="#111" />
|
||||||
|
) : (
|
||||||
|
<rect x={x0} y={y0 + drawL} width={drawW} height={15} fill={getPressureColor(wallCpe.C, cpi)} stroke="#111" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Parede D (Lateral Esq em 0°, Fundo em 90°) */}
|
||||||
|
{windAngle === 0 ? (
|
||||||
|
<rect x={x0 - 15} y={y0} width={15} height={drawL} fill={getPressureColor(wallCpe.D, cpi)} stroke="#111" />
|
||||||
|
) : (
|
||||||
|
<rect x={x0} y={y0 - 15} width={drawW} height={15} fill={getPressureColor(wallCpe.D, cpi)} stroke="#111" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Indicadores de Paredes */}
|
||||||
|
{windAngle === 0 ? (
|
||||||
|
<>
|
||||||
|
<text x={cx} y={y0 - 25} textAnchor="middle" className="text-2xl font-bold fill-foreground">A</text>
|
||||||
|
<text x={cx} y={y0 + drawL + 25} textAnchor="middle" dominantBaseline="hanging" className="text-2xl font-bold fill-foreground">B</text>
|
||||||
|
<text x={x0 - 25} y={cy} textAnchor="end" dominantBaseline="middle" className="text-2xl font-bold fill-foreground">D</text>
|
||||||
|
<text x={x0 + drawW + 25} y={cy} textAnchor="start" dominantBaseline="middle" className="text-2xl font-bold fill-foreground">C</text>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<text x={x0 - 25} y={cy} textAnchor="end" dominantBaseline="middle" className="text-2xl font-bold fill-foreground">A</text>
|
||||||
|
<text x={x0 + drawW + 25} y={cy} textAnchor="start" dominantBaseline="middle" className="text-2xl font-bold fill-foreground">B</text>
|
||||||
|
<text x={cx} y={y0 + drawL + 25} textAnchor="middle" dominantBaseline="hanging" className="text-2xl font-bold fill-foreground">C</text>
|
||||||
|
<text x={cx} y={y0 - 25} textAnchor="middle" className="text-2xl font-bold fill-foreground">D</text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 shrink-0 flex flex-wrap justify-center gap-4 text-xs sm:text-sm">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 rounded" style={{ backgroundColor: getPressureColor(-1.0, cpi) }}></div>
|
||||||
|
<span>Sucção Máxima</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 rounded border border-border" style={{ backgroundColor: getPressureColor(0, cpi) }}></div>
|
||||||
|
<span>Neutro (0)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 rounded" style={{ backgroundColor: getPressureColor(1.0, cpi) }}></div>
|
||||||
|
<span>Sobrepressão Máxima</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Heatmap2D;
|
||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
getPillarBaseMoment,
|
getPillarBaseMoment,
|
||||||
getDragForce,
|
getDragForce,
|
||||||
} from '../lib/line-loads';
|
} from '../lib/line-loads';
|
||||||
|
import { calcELU, calcELS } from '../lib/combinations';
|
||||||
|
import type { LoadInputs } from '../lib/combinations';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from './ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from './ui/card';
|
||||||
import { Input } from './ui/input';
|
import { Input } from './ui/input';
|
||||||
import { Badge } from './ui/badge';
|
import { Badge } from './ui/badge';
|
||||||
@@ -25,6 +27,10 @@ const LinearLoadsTable: React.FC = () => {
|
|||||||
|
|
||||||
const [purlinSpacing, setPurlinSpacing] = useState<number>(1.5);
|
const [purlinSpacing, setPurlinSpacing] = useState<number>(1.5);
|
||||||
|
|
||||||
|
// Gravitational Loads (kN/m²)
|
||||||
|
const [pp, setPp] = useState<number>(0.15); // Peso Próprio típico
|
||||||
|
const [sc, setSc] = useState<number>(0.25); // Sobrecarga típica de cobertura
|
||||||
|
|
||||||
const columnLoads = useMemo(
|
const columnLoads = useMemo(
|
||||||
() => getColumnLinearLoads(cpi, q, wallCpe, frameSpacing, windAngle),
|
() => getColumnLinearLoads(cpi, q, wallCpe, frameSpacing, windAngle),
|
||||||
[cpi, q, wallCpe, frameSpacing, windAngle],
|
[cpi, q, wallCpe, frameSpacing, windAngle],
|
||||||
@@ -92,10 +98,11 @@ const LinearLoadsTable: React.FC = () => {
|
|||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<Tabs defaultValue="pilares" className="w-full">
|
<Tabs defaultValue="pilares" className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-3">
|
<TabsList className="grid w-full grid-cols-4">
|
||||||
<TabsTrigger value="pilares">{t('linear_loads_tab_pillars')}</TabsTrigger>
|
<TabsTrigger value="pilares">{t('linear_loads_tab_pillars')}</TabsTrigger>
|
||||||
<TabsTrigger value="tercas">{t('linear_loads_tab_purlins')}</TabsTrigger>
|
<TabsTrigger value="tercas">{t('linear_loads_tab_purlins')}</TabsTrigger>
|
||||||
<TabsTrigger value="reacoes">{t('linear_loads_tab_reactions')}</TabsTrigger>
|
<TabsTrigger value="reacoes">{t('linear_loads_tab_reactions')}</TabsTrigger>
|
||||||
|
<TabsTrigger value="combinacoes" className="text-xs">Combinações</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="pilares" className="space-y-3">
|
<TabsContent value="pilares" className="space-y-3">
|
||||||
@@ -202,6 +209,59 @@ const LinearLoadsTable: React.FC = () => {
|
|||||||
{t('linear_loads_warning_simplified')}
|
{t('linear_loads_warning_simplified')}
|
||||||
</p>
|
</p>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="combinacoes" className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4 bg-muted/20 p-3 rounded-md border">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-xs font-medium text-foreground">Peso Próprio (kN/m²)</label>
|
||||||
|
<Input type="number" step={0.05} value={pp} onChange={e => setPp(Number(e.target.value))} className="h-8" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-xs font-medium text-foreground">Sobrecarga (kN/m²)</label>
|
||||||
|
<Input type="number" step={0.05} value={sc} onChange={e => setSc(Number(e.target.value))} className="h-8" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-md border overflow-hidden">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead className="bg-muted/40">
|
||||||
|
<tr>
|
||||||
|
<th className="px-3 py-2 text-left font-medium">Elemento (Têrça)</th>
|
||||||
|
<th className="px-3 py-2 text-right font-medium">Vento (kN/m)</th>
|
||||||
|
<th className="px-3 py-2 text-right font-medium">ELU (kN/m)</th>
|
||||||
|
<th className="px-3 py-2 text-right font-medium text-muted-foreground">ELS (kN/m)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{ zona: 'E', w: roofLoads.E },
|
||||||
|
{ zona: 'F', w: roofLoads.F },
|
||||||
|
{ zona: 'G', w: roofLoads.G },
|
||||||
|
{ zona: 'H', w: roofLoads.H },
|
||||||
|
{ zona: 'I', w: roofLoads.I },
|
||||||
|
{ zona: 'J', w: roofLoads.J },
|
||||||
|
].map((row) => {
|
||||||
|
const inputs: LoadInputs = {
|
||||||
|
pp: pp * purlinSpacing, // linearizando a carga de área
|
||||||
|
sc: sc * purlinSpacing,
|
||||||
|
w: row.w
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<tr key={row.zona} className="border-t">
|
||||||
|
<td className="px-3 py-2 font-mono font-semibold">Zona {row.zona}</td>
|
||||||
|
<td className="px-3 py-2 text-right font-mono">{fmtSigned(row.w)}</td>
|
||||||
|
<td className="px-3 py-2 text-right font-mono font-bold text-primary">{fmtSigned(calcELU(inputs))}</td>
|
||||||
|
<td className="px-3 py-2 text-right font-mono text-muted-foreground">{fmtSigned(calcELS(inputs))}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-muted-foreground leading-relaxed">
|
||||||
|
* Combinações simplificadas para NBR 8681. Assumido vento principal ou sobrecarga principal (pega o pior caso ELU Normal).
|
||||||
|
</p>
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|||||||
@@ -401,25 +401,12 @@ export function WarehouseModel() {
|
|||||||
<meshStandardMaterial color="#0f172a" roughness={0.5} transparent={(viewMode as string) === 'airflow'} opacity={(viewMode as string) === 'airflow' ? 0.35 : 1} />
|
<meshStandardMaterial color="#0f172a" roughness={0.5} transparent={(viewMode as string) === 'airflow'} opacity={(viewMode as string) === 'airflow' ? 0.35 : 1} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Rótulo e Título da Água */}
|
|
||||||
<Text
|
|
||||||
position={[0, 0.95, 0]}
|
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
|
||||||
fontSize={titleFontSize}
|
|
||||||
color="#000000"
|
|
||||||
outlineWidth={0.04}
|
|
||||||
outlineColor="#ffffff"
|
|
||||||
fontWeight={700}
|
|
||||||
anchorX="center"
|
|
||||||
anchorY="middle"
|
|
||||||
>
|
|
||||||
{isParallel ? 'Telhado (Zonas E/F/G/H)' : 'Água Barlavento (Zonas E, F, J)'}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{/* Marcações Individuais das Zonas */}
|
{/* Marcações Individuais das Zonas */}
|
||||||
<Text
|
<Text
|
||||||
position={[0, 0.35, -3 * length / 8]}
|
position={[0, 0.35, -3 * length / 8]}
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
rotation={[-Math.PI / 2, 0, Math.PI]}
|
||||||
fontSize={zoneFontSize}
|
fontSize={zoneFontSize}
|
||||||
color="#000000"
|
color="#000000"
|
||||||
outlineWidth={0.035}
|
outlineWidth={0.035}
|
||||||
@@ -432,7 +419,7 @@ export function WarehouseModel() {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
position={[0, 0.35, -length / 8]}
|
position={[0, 0.35, -length / 8]}
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
rotation={[-Math.PI / 2, 0, Math.PI]}
|
||||||
fontSize={zoneFontSize}
|
fontSize={zoneFontSize}
|
||||||
color="#000000"
|
color="#000000"
|
||||||
outlineWidth={0.035}
|
outlineWidth={0.035}
|
||||||
@@ -445,7 +432,7 @@ export function WarehouseModel() {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
position={[0, 0.35, length / 8]}
|
position={[0, 0.35, length / 8]}
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
rotation={[-Math.PI / 2, 0, Math.PI]}
|
||||||
fontSize={zoneFontSize}
|
fontSize={zoneFontSize}
|
||||||
color="#000000"
|
color="#000000"
|
||||||
outlineWidth={0.035}
|
outlineWidth={0.035}
|
||||||
@@ -458,7 +445,7 @@ export function WarehouseModel() {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
position={[0, 0.35, 3 * length / 8]}
|
position={[0, 0.35, 3 * length / 8]}
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
rotation={[-Math.PI / 2, 0, Math.PI]}
|
||||||
fontSize={zoneFontSize}
|
fontSize={zoneFontSize}
|
||||||
color="#000000"
|
color="#000000"
|
||||||
outlineWidth={0.035}
|
outlineWidth={0.035}
|
||||||
@@ -471,7 +458,7 @@ export function WarehouseModel() {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
position={[-widthSlope * 0.38, 0.35, 0]}
|
position={[-widthSlope * 0.38, 0.35, 0]}
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
rotation={[-Math.PI / 2, 0, -Math.PI / 2]}
|
||||||
fontSize={zoneFontSize}
|
fontSize={zoneFontSize}
|
||||||
color="#000000"
|
color="#000000"
|
||||||
outlineWidth={0.035}
|
outlineWidth={0.035}
|
||||||
@@ -532,25 +519,12 @@ export function WarehouseModel() {
|
|||||||
<meshStandardMaterial color="#0f172a" roughness={0.5} transparent={(viewMode as string) === 'airflow'} opacity={(viewMode as string) === 'airflow' ? 0.35 : 1} />
|
<meshStandardMaterial color="#0f172a" roughness={0.5} transparent={(viewMode as string) === 'airflow'} opacity={(viewMode as string) === 'airflow' ? 0.35 : 1} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Rótulo e Título da Água */}
|
|
||||||
<Text
|
|
||||||
position={[0, 0.95, 0]}
|
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
|
||||||
fontSize={titleFontSize}
|
|
||||||
color="#000000"
|
|
||||||
outlineWidth={0.04}
|
|
||||||
outlineColor="#ffffff"
|
|
||||||
fontWeight={700}
|
|
||||||
anchorX="center"
|
|
||||||
anchorY="middle"
|
|
||||||
>
|
|
||||||
{isParallel ? 'Telhado (Zonas E/F/G/H)' : 'Água Sotavento (Zonas G, H, I)'}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{/* Marcações Individuais das Zonas */}
|
{/* Marcações Individuais das Zonas */}
|
||||||
<Text
|
<Text
|
||||||
position={[0, 0.35, -3 * length / 8]}
|
position={[0, 0.35, -3 * length / 8]}
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
rotation={[-Math.PI / 2, 0, Math.PI]}
|
||||||
fontSize={zoneFontSize}
|
fontSize={zoneFontSize}
|
||||||
color="#000000"
|
color="#000000"
|
||||||
outlineWidth={0.035}
|
outlineWidth={0.035}
|
||||||
@@ -563,7 +537,7 @@ export function WarehouseModel() {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
position={[0, 0.35, -length / 8]}
|
position={[0, 0.35, -length / 8]}
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
rotation={[-Math.PI / 2, 0, Math.PI]}
|
||||||
fontSize={zoneFontSize}
|
fontSize={zoneFontSize}
|
||||||
color="#000000"
|
color="#000000"
|
||||||
outlineWidth={0.035}
|
outlineWidth={0.035}
|
||||||
@@ -576,7 +550,7 @@ export function WarehouseModel() {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
position={[0, 0.35, length / 8]}
|
position={[0, 0.35, length / 8]}
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
rotation={[-Math.PI / 2, 0, Math.PI]}
|
||||||
fontSize={zoneFontSize}
|
fontSize={zoneFontSize}
|
||||||
color="#000000"
|
color="#000000"
|
||||||
outlineWidth={0.035}
|
outlineWidth={0.035}
|
||||||
@@ -589,7 +563,7 @@ export function WarehouseModel() {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
position={[0, 0.35, 3 * length / 8]}
|
position={[0, 0.35, 3 * length / 8]}
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
rotation={[-Math.PI / 2, 0, Math.PI]}
|
||||||
fontSize={zoneFontSize}
|
fontSize={zoneFontSize}
|
||||||
color="#000000"
|
color="#000000"
|
||||||
outlineWidth={0.035}
|
outlineWidth={0.035}
|
||||||
@@ -601,8 +575,8 @@ export function WarehouseModel() {
|
|||||||
{isParallel ? `Zona H (${roofCpe.H})` : `Zona H (${roofCpe.H})`}
|
{isParallel ? `Zona H (${roofCpe.H})` : `Zona H (${roofCpe.H})`}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
position={[widthSlope * 0.38, 0.35, 0]}
|
position={[widthSlope * 0.44, 0.35, 0]}
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
rotation={[-Math.PI / 2, 0, -Math.PI / 2]}
|
||||||
fontSize={zoneFontSize}
|
fontSize={zoneFontSize}
|
||||||
color="#000000"
|
color="#000000"
|
||||||
outlineWidth={0.035}
|
outlineWidth={0.035}
|
||||||
|
|||||||
@@ -61,15 +61,22 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) {
|
|||||||
.vibe-cyl {
|
.vibe-cyl {
|
||||||
animation: vibration 1.2s ease-in-out infinite;
|
animation: vibration 1.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
@keyframes cpiPulse {
|
||||||
|
0%, 100% { opacity: 0.4; stroke-width: 1.5; }
|
||||||
|
50% { opacity: 1; stroke-width: 2.5; }
|
||||||
|
}
|
||||||
.cpi-arrow-pos {
|
.cpi-arrow-pos {
|
||||||
stroke: #f97316;
|
stroke: #f97316;
|
||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
fill: none;
|
fill: none;
|
||||||
|
animation: cpiPulse 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
.cpi-arrow-neg {
|
.cpi-arrow-neg {
|
||||||
stroke: #3b82f6;
|
stroke: #3b82f6;
|
||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
fill: none;
|
fill: none;
|
||||||
|
animation: cpiPulse 1.5s ease-in-out infinite;
|
||||||
|
animation-delay: 0.75s;
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
);
|
);
|
||||||
@@ -488,8 +495,8 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) {
|
|||||||
<path d="M -20,85 L 80,85" className="wind-line" />
|
<path d="M -20,85 L 80,85" className="wind-line" />
|
||||||
|
|
||||||
{/* Esteira turbulenta no telhado e atrás */}
|
{/* Esteira turbulenta no telhado e atrás */}
|
||||||
<path d="M 80,70 Q 110,50 150,70" fill="none" stroke="#ef4444" strokeWidth="1.5" strokeDasharray="3 6" />
|
<path d="M 80,70 Q 110,50 150,70" className="vortex-line" />
|
||||||
<path d="M 220,70 Q 250,90 280,110" fill="none" stroke="#ef4444" strokeWidth="1.5" strokeDasharray="3 6" />
|
<path d="M 220,70 Q 250,90 280,110" className="vortex-line" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-muted-foreground leading-snug">Vento incide a 0° (paralelo à cumeeira). O fluxo contorna o oitão frontal, gerando forte sucção nas bordas longitudinais.</p>
|
<p className="text-[10px] text-muted-foreground leading-snug">Vento incide a 0° (paralelo à cumeeira). O fluxo contorna o oitão frontal, gerando forte sucção nas bordas longitudinais.</p>
|
||||||
@@ -503,6 +510,12 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) {
|
|||||||
{styleBlock}
|
{styleBlock}
|
||||||
{legendDef}
|
{legendDef}
|
||||||
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
||||||
|
|
||||||
|
{/* Fluxo entrando */}
|
||||||
|
<path d="M -20,20 L 320,20" className="wind-line-slow" />
|
||||||
|
<path d="M -20,60 C 100,60 110,35 150,35 C 190,35 200,65 320,65" className="wind-line-fast" />
|
||||||
|
<path d="M -20,100 L 110,100" className="wind-line" />
|
||||||
|
|
||||||
<path d="M 110,130 L 110,85 L 150,55 L 190,85 L 190,130 Z" fill="#475569" stroke="#64748b" strokeWidth="2" />
|
<path d="M 110,130 L 110,85 L 150,55 L 190,85 L 190,130 Z" fill="#475569" stroke="#64748b" strokeWidth="2" />
|
||||||
{/* Setas laranjas apontando para fora (inflado) */}
|
{/* Setas laranjas apontando para fora (inflado) */}
|
||||||
<path d="M 130,110 L 115,110" className="cpi-arrow-pos" markerEnd="url(#cpi-pos)" />
|
<path d="M 130,110 L 115,110" className="cpi-arrow-pos" markerEnd="url(#cpi-pos)" />
|
||||||
@@ -522,6 +535,12 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) {
|
|||||||
{styleBlock}
|
{styleBlock}
|
||||||
{legendDef}
|
{legendDef}
|
||||||
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
||||||
|
|
||||||
|
{/* Fluxo passando e sugando */}
|
||||||
|
<path d="M -20,20 L 320,20" className="wind-line-slow" />
|
||||||
|
<path d="M -20,60 C 100,60 110,35 150,35 C 190,35 200,65 320,65" className="wind-line-fast" />
|
||||||
|
<path d="M -20,100 C 90,90 100,75 115,75 L 185,75 C 200,75 210,100 320,100" className="wind-line" />
|
||||||
|
|
||||||
<path d="M 110,130 L 110,85 L 150,55 L 190,85 L 190,130 Z" fill="#475569" stroke="#64748b" strokeWidth="2" />
|
<path d="M 110,130 L 110,85 L 150,55 L 190,85 L 190,130 Z" fill="#475569" stroke="#64748b" strokeWidth="2" />
|
||||||
{/* Setas azuis apontando para dentro (sugado) */}
|
{/* Setas azuis apontando para dentro (sugado) */}
|
||||||
<path d="M 115,110 L 130,110" className="cpi-arrow-neg" markerEnd="url(#cpi-neg)" />
|
<path d="M 115,110 L 130,110" className="cpi-arrow-neg" markerEnd="url(#cpi-neg)" />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useRef, useMemo, useEffect } from 'react';
|
import { useRef, useMemo, useEffect } from 'react';
|
||||||
import { useFrame } from '@react-three/fiber';
|
import { useFrame } from '@react-three/fiber';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
|
import { useCanvasTheme } from '../../lib/theme';
|
||||||
|
|
||||||
interface WallCoefficients {
|
interface WallCoefficients {
|
||||||
A: number; B: number; C: number; D: number;
|
A: number; B: number; C: number; D: number;
|
||||||
@@ -21,6 +22,7 @@ interface AirflowSystemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AirflowSystem({ windAngle, width, length, height, permeabilityCase, wallCpe, roofCpe, cpi }: AirflowSystemProps) {
|
export function AirflowSystem({ windAngle, width, length, height, permeabilityCase, wallCpe, roofCpe, cpi }: AirflowSystemProps) {
|
||||||
|
const isDark = useCanvasTheme() === 'dark';
|
||||||
const count = 400; // Quantidade reduzida
|
const count = 400; // Quantidade reduzida
|
||||||
const meshRef = useRef<THREE.InstancedMesh>(null);
|
const meshRef = useRef<THREE.InstancedMesh>(null);
|
||||||
|
|
||||||
@@ -130,9 +132,9 @@ export function AirflowSystem({ windAngle, width, length, height, permeabilityCa
|
|||||||
if (inY && inX && inZ) {
|
if (inY && inX && inZ) {
|
||||||
// Perto das paredes
|
// Perto das paredes
|
||||||
if (Math.abs(pz - (-halfL)) < margin) localCpe = wallCpe.A;
|
if (Math.abs(pz - (-halfL)) < margin) localCpe = wallCpe.A;
|
||||||
else if (Math.abs(pz - (halfL)) < margin) localCpe = wallCpe.B;
|
else if (Math.abs(pz - (halfL)) < margin) localCpe = (windAngle === 90 ? 0.5 : wallCpe.B);
|
||||||
else if (Math.abs(px - (-halfW)) < margin) localCpe = wallCpe.C;
|
else if (Math.abs(px - (-halfW)) < margin) localCpe = wallCpe.C;
|
||||||
else if (Math.abs(px - (halfW)) < margin) localCpe = wallCpe.D;
|
else if (Math.abs(px - (halfW)) < margin) localCpe = (windAngle === 0 ? 0.5 : wallCpe.D);
|
||||||
else localCpe = cpi;
|
else localCpe = cpi;
|
||||||
|
|
||||||
// Colisão dura (Impede entrar na parede sólida)
|
// Colisão dura (Impede entrar na parede sólida)
|
||||||
@@ -193,12 +195,12 @@ export function AirflowSystem({ windAngle, width, length, height, permeabilityCa
|
|||||||
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||||
|
|
||||||
if (p.currentCpe > 0) {
|
if (p.currentCpe > 0) {
|
||||||
colorObj.set('#ef4444').lerp(new THREE.Color('#fcd34d'), 1 - Math.min(1, p.currentCpe));
|
colorObj.set(isDark ? '#f87171' : '#ef4444'); // Vermelho (Turbulento/Pressão)
|
||||||
} else if (p.currentCpe < 0) {
|
} else if (p.currentCpe < 0) {
|
||||||
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
colorObj.set('#7dd3fc').lerp(new THREE.Color('#1e40af'), intensity);
|
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção)
|
||||||
} else {
|
} else {
|
||||||
colorObj.set('#f1f5f9');
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre)
|
||||||
}
|
}
|
||||||
meshRef.current!.setColorAt(i, colorObj);
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
});
|
});
|
||||||
@@ -215,8 +217,8 @@ export function AirflowSystem({ windAngle, width, length, height, permeabilityCa
|
|||||||
<meshBasicMaterial
|
<meshBasicMaterial
|
||||||
color="#ffffff"
|
color="#ffffff"
|
||||||
transparent
|
transparent
|
||||||
opacity={0.6}
|
opacity={isDark ? 0.6 : 0.8}
|
||||||
blending={THREE.AdditiveBlending}
|
blending={isDark ? THREE.AdditiveBlending : THREE.NormalBlending}
|
||||||
depthWrite={false}
|
depthWrite={false}
|
||||||
/>
|
/>
|
||||||
</instancedMesh>
|
</instancedMesh>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import SceneCanvas from '../SceneCanvas';
|
|||||||
import FallbackDiagram from '../FallbackDiagram';
|
import FallbackDiagram from '../FallbackDiagram';
|
||||||
import { WindArrow } from './WindArrow';
|
import { WindArrow } from './WindArrow';
|
||||||
import { useCanvasTheme } from '@/lib/theme';
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
import { BridgeAirflowSystem } from './BridgeAirflowSystem';
|
||||||
|
|
||||||
export interface Bridge3DInput {
|
export interface Bridge3DInput {
|
||||||
/** Maior vão Lₚ (m) */
|
/** Maior vão Lₚ (m) */
|
||||||
@@ -26,6 +27,8 @@ export interface Bridge3DInput {
|
|||||||
fzPerLength: number;
|
fzPerLength: number;
|
||||||
/** Ângulo de ataque do vento (graus) */
|
/** Ângulo de ataque do vento (graus) */
|
||||||
alpha: number;
|
alpha: number;
|
||||||
|
/** Modo de visualização */
|
||||||
|
viewMode?: 'solid' | 'airflow';
|
||||||
}
|
}
|
||||||
|
|
||||||
function BridgeModel({
|
function BridgeModel({
|
||||||
@@ -35,9 +38,11 @@ function BridgeModel({
|
|||||||
heg,
|
heg,
|
||||||
cx,
|
cx,
|
||||||
alpha,
|
alpha,
|
||||||
|
viewMode = 'solid',
|
||||||
}: Bridge3DInput) {
|
}: Bridge3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
|
const isAirflow = viewMode === 'airflow';
|
||||||
|
|
||||||
const halfL = lp / 2;
|
const halfL = lp / 2;
|
||||||
const halfW = width / 2;
|
const halfW = width / 2;
|
||||||
@@ -46,33 +51,34 @@ function BridgeModel({
|
|||||||
|
|
||||||
// Cor do tabuleiro baseada em Cx
|
// Cor do tabuleiro baseada em Cx
|
||||||
const deckColor = useMemo(() => {
|
const deckColor = useMemo(() => {
|
||||||
|
if (isAirflow) return new THREE.Color(isDark ? '#334155' : '#e2e8f0');
|
||||||
const intensity = Math.min(1, Math.abs(cx) / 3);
|
const intensity = Math.min(1, Math.abs(cx) / 3);
|
||||||
const hue = 200 - intensity * 60;
|
const hue = 200 - intensity * 60;
|
||||||
const l = isDark ? (60 - intensity * 8) : (50 - intensity * 8);
|
const l = isDark ? (60 - intensity * 8) : (50 - intensity * 8);
|
||||||
return new THREE.Color(`hsl(${hue}, ${55 + intensity * 30}%, ${l}%)`);
|
return new THREE.Color(`hsl(${hue}, ${55 + intensity * 30}%, ${l}%)`);
|
||||||
}, [cx, isDark]);
|
}, [cx, isDark, isAirflow]);
|
||||||
|
|
||||||
// Pilar heights: posicionar 3 pilares ao longo do vão
|
// Pilar heights: posicionar 3 pilares ao longo do vão
|
||||||
const pillarHeights = useMemo(() => [deckY - 0.5, deckY - 0.5, deckY - 0.5], [deckY]);
|
const pillarHeights = useMemo(() => [deckY - 0.5, deckY - 0.5, deckY - 0.5], [deckY]);
|
||||||
const barrierColor = isDark ? '#cbd5e1' : '#94a3b8';
|
const barrierColor = isDark ? (isAirflow ? '#475569' : '#cbd5e1') : (isAirflow ? '#cbd5e1' : '#94a3b8');
|
||||||
const pillarColor = isDark ? '#94a3b8' : '#64748b';
|
const pillarColor = isDark ? (isAirflow ? '#334155' : '#94a3b8') : (isAirflow ? '#e2e8f0' : '#64748b');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
{/* Tabuleiro (deck) */}
|
{/* Tabuleiro (deck) */}
|
||||||
<mesh position={[0, deckY, 0]} castShadow receiveShadow>
|
<mesh position={[0, deckY, 0]} castShadow receiveShadow>
|
||||||
<boxGeometry args={[lp, deckThickness, width]} />
|
<boxGeometry args={[lp, deckThickness, width]} />
|
||||||
<meshStandardMaterial color={deckColor} roughness={0.5} />
|
<meshStandardMaterial color={deckColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.5} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Guarda-rodas/barreira lateral */}
|
{/* Guarda-rodas/barreira lateral */}
|
||||||
<mesh position={[0, deckY + deckThickness / 2 + 0.3, halfW - 0.15]} castShadow>
|
<mesh position={[0, deckY + deckThickness / 2 + 0.3, halfW - 0.15]} castShadow>
|
||||||
<boxGeometry args={[lp, 0.5, 0.1]} />
|
<boxGeometry args={[lp, 0.5, 0.1]} />
|
||||||
<meshStandardMaterial color={barrierColor} roughness={0.7} />
|
<meshStandardMaterial color={barrierColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.7} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<mesh position={[0, deckY + deckThickness / 2 + 0.3, -halfW + 0.15]} castShadow>
|
<mesh position={[0, deckY + deckThickness / 2 + 0.3, -halfW + 0.15]} castShadow>
|
||||||
<boxGeometry args={[lp, 0.5, 0.1]} />
|
<boxGeometry args={[lp, 0.5, 0.1]} />
|
||||||
<meshStandardMaterial color={barrierColor} roughness={0.7} />
|
<meshStandardMaterial color={barrierColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.7} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Pilares (3 ao longo do comprimento) */}
|
{/* Pilares (3 ao longo do comprimento) */}
|
||||||
@@ -87,53 +93,68 @@ function BridgeModel({
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Solo / água */}
|
{/* Solo / água */}
|
||||||
<mesh position={[0, -0.5, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
|
{!isAirflow && (
|
||||||
<planeGeometry args={[lp * 1.6, width * 3]} />
|
<mesh position={[0, -0.5, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
|
||||||
<meshStandardMaterial color="#60a5fa" opacity={0.4} transparent roughness={0.3} />
|
<planeGeometry args={[lp * 1.6, width * 3]} />
|
||||||
</mesh>
|
<meshStandardMaterial color="#60a5fa" opacity={0.4} transparent roughness={0.3} />
|
||||||
|
</mesh>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Seta de Vento (bate no meio do tabuleiro) */}
|
{/* Seta de Vento */}
|
||||||
<WindArrow
|
{isAirflow ? (
|
||||||
target={[0, deckY + deckThickness / 2, 0]}
|
<group>
|
||||||
direction={[0, Math.sin((alpha * Math.PI) / 180), Math.cos((alpha * Math.PI) / 180)]}
|
<WindArrow
|
||||||
scale={4}
|
target={[-halfL * 0.8, deckY, -halfW - 2]}
|
||||||
/>
|
direction={[0, 0, 1]}
|
||||||
<Text
|
scale={4}
|
||||||
position={[0, deckY + deckThickness + 1.5, 0]}
|
/>
|
||||||
fontSize={1.2}
|
<Text
|
||||||
color={isDark ? "#93c5fd" : "#1e40af"}
|
position={[-halfL * 0.8, deckY + 1.5, -halfW - 4]}
|
||||||
anchorX="center"
|
rotation={[0, Math.PI, 0]}
|
||||||
anchorY="bottom"
|
fontSize={1.2}
|
||||||
>
|
color={isDark ? '#60a5fa' : '#3b82f6'}
|
||||||
Lp={lp}m | B={width}m | Cx={cx.toFixed(2)}
|
anchorX="center"
|
||||||
</Text>
|
anchorY="bottom"
|
||||||
|
>
|
||||||
|
Vento
|
||||||
|
</Text>
|
||||||
|
<BridgeAirflowSystem width={width} lp={lp} deckHeight={deckHeight} cx={cx} />
|
||||||
|
</group>
|
||||||
|
) : (
|
||||||
|
<WindArrow
|
||||||
|
target={[0, deckY + deckThickness / 2, 0]}
|
||||||
|
direction={[0, Math.sin((alpha * Math.PI) / 180), Math.cos((alpha * Math.PI) / 180)]}
|
||||||
|
scale={4}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Bridge3DViewer(input: Bridge3DInput) {
|
export default function Bridge3DViewer(props: Bridge3DInput) {
|
||||||
|
const { lp, width, deckHeight, heg, cx, cz, fxPerLength, fzPerLength, viewMode = 'solid' } = props;
|
||||||
|
const cameraDistance = Math.max(lp * 1.2, width * 2, deckHeight * 2, 15);
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
const { lp, deckHeight, width } = input;
|
|
||||||
const dist = Math.max(lp * 0.6, deckHeight * 2);
|
|
||||||
|
|
||||||
const fallback = (
|
const fallback = (
|
||||||
<FallbackDiagram
|
<FallbackDiagram
|
||||||
type="bridge"
|
type="bridge"
|
||||||
props={input}
|
props={{ lp, width, deckHeight, heg, cx, cz, fxPerLength, fzPerLength }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SceneCanvas moduleId="ponte"
|
<SceneCanvas moduleId="pontes"
|
||||||
shadows
|
shadows
|
||||||
|
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [dist * 0.8, deckHeight + width, dist], fov: 45 }}
|
camera={{ position: [-cameraDistance * 0.8, cameraDistance * 0.6, -cameraDistance], fov: 45 }}
|
||||||
fallback={fallback}
|
fallback={fallback}
|
||||||
>
|
>
|
||||||
<ambientLight intensity={0.6} />
|
<ambientLight intensity={0.6} />
|
||||||
<directionalLight position={[lp, deckHeight * 3, width * 3]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
|
<directionalLight position={[lp, deckHeight * 3, width * 3]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
|
||||||
<BridgeModel {...input} />
|
<BridgeModel {...props} viewMode={viewMode} />
|
||||||
<Grid
|
<Grid
|
||||||
infiniteGrid
|
infiniteGrid
|
||||||
fadeDistance={lp * 0.5}
|
fadeDistance={lp * 0.5}
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import React, { useRef, useMemo, useEffect } from 'react';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
|
||||||
|
interface Particle {
|
||||||
|
position: THREE.Vector3;
|
||||||
|
baseSpeed: number;
|
||||||
|
wobbleSpeed: number;
|
||||||
|
wobbleOffset: number;
|
||||||
|
currentCpe: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BridgeAirflowSystemProps {
|
||||||
|
width: number;
|
||||||
|
lp: number;
|
||||||
|
deckHeight: number;
|
||||||
|
cx: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BridgeAirflowSystem: React.FC<BridgeAirflowSystemProps> = ({
|
||||||
|
width,
|
||||||
|
lp,
|
||||||
|
deckHeight,
|
||||||
|
cx,
|
||||||
|
}) => {
|
||||||
|
const meshRef = useRef<THREE.InstancedMesh>(null);
|
||||||
|
const theme = useCanvasTheme();
|
||||||
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
|
const COUNT = 100;
|
||||||
|
const dummy = useMemo(() => new THREE.Object3D(), []);
|
||||||
|
const colorObj = useMemo(() => new THREE.Color(), []);
|
||||||
|
|
||||||
|
const halfWidth = width / 2;
|
||||||
|
const halfLp = lp / 2;
|
||||||
|
|
||||||
|
const particles = useMemo(() => {
|
||||||
|
const arr: Particle[] = [];
|
||||||
|
for (let i = 0; i < COUNT; i++) {
|
||||||
|
arr.push({
|
||||||
|
position: new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * lp,
|
||||||
|
Math.random() * (deckHeight + 10),
|
||||||
|
-halfWidth - 10 - Math.random() * 20
|
||||||
|
),
|
||||||
|
baseSpeed: 0.075 + Math.random() * 0.05,
|
||||||
|
wobbleSpeed: 0.5 + Math.random() * 1.5,
|
||||||
|
wobbleOffset: Math.random() * Math.PI * 2,
|
||||||
|
currentCpe: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, [width, lp, deckHeight]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (meshRef.current) {
|
||||||
|
for (let i = 0; i < COUNT; i++) {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
|
||||||
|
meshRef.current.setColorAt(i, colorObj);
|
||||||
|
}
|
||||||
|
meshRef.current.instanceColor!.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}, [isDark, colorObj]);
|
||||||
|
|
||||||
|
useFrame((state) => {
|
||||||
|
if (!meshRef.current) return;
|
||||||
|
const time = state.clock.elapsedTime;
|
||||||
|
|
||||||
|
particles.forEach((p, i) => {
|
||||||
|
const px = p.position.x;
|
||||||
|
const py = p.position.y;
|
||||||
|
const pz = p.position.z;
|
||||||
|
|
||||||
|
let currentSpeed = p.baseSpeed;
|
||||||
|
let localCpe = 0;
|
||||||
|
let deflectY = 0;
|
||||||
|
|
||||||
|
const inX = Math.abs(px) < halfLp + 1;
|
||||||
|
const inZ = Math.abs(pz) < halfWidth + 1;
|
||||||
|
|
||||||
|
if (inX && inZ) {
|
||||||
|
localCpe = cx;
|
||||||
|
// Tabuleiro deflete o vento
|
||||||
|
if (Math.abs(py - deckHeight) < 2) {
|
||||||
|
const dist = -pz;
|
||||||
|
const lift = Math.max(0, deckHeight + 1 - py) / (dist + 1);
|
||||||
|
deflectY += lift * currentSpeed * 0.6 * (py < deckHeight ? -1 : 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.currentCpe += (localCpe - p.currentCpe) * 0.1;
|
||||||
|
p.position.z += currentSpeed;
|
||||||
|
p.position.y += deflectY;
|
||||||
|
p.position.y += Math.sin(time * p.wobbleSpeed * 10 + p.wobbleOffset) * 0.01;
|
||||||
|
|
||||||
|
if (p.position.z > halfWidth + 10) {
|
||||||
|
p.position.z = -halfWidth - 10 - Math.random() * 5;
|
||||||
|
p.position.y = Math.random() * (deckHeight + 10);
|
||||||
|
p.position.x = (Math.random() - 0.5) * lp;
|
||||||
|
p.currentCpe = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dummy.position.copy(p.position);
|
||||||
|
dummy.rotation.set(0, 0, 0);
|
||||||
|
dummy.rotateX(Math.PI / 2);
|
||||||
|
dummy.scale.set(1, 1 + currentSpeed * 6, 1);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||||
|
|
||||||
|
if (p.currentCpe > 0) {
|
||||||
|
colorObj.set(isDark ? '#f87171' : '#ef4444');
|
||||||
|
} else if (p.currentCpe < 0) {
|
||||||
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
|
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity);
|
||||||
|
} else {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
|
||||||
|
}
|
||||||
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
meshRef.current.instanceMatrix.needsUpdate = true;
|
||||||
|
meshRef.current.instanceColor!.needsUpdate = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<instancedMesh ref={meshRef} args={[undefined, undefined, COUNT]}>
|
||||||
|
<cylinderGeometry args={[0.04, 0.04, 1.0, 4]} />
|
||||||
|
<meshBasicMaterial transparent opacity={0.6} depthWrite={false} />
|
||||||
|
</instancedMesh>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import * as THREE from 'three';
|
|||||||
import SceneCanvas from '../SceneCanvas';
|
import SceneCanvas from '../SceneCanvas';
|
||||||
import FallbackDiagram from '../FallbackDiagram';
|
import FallbackDiagram from '../FallbackDiagram';
|
||||||
import { useCanvasTheme } from '@/lib/theme';
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
import { CylinderAirflowSystem } from './CylinderAirflowSystem';
|
||||||
|
|
||||||
export interface Cylinder3DInput {
|
export interface Cylinder3DInput {
|
||||||
diameter: number;
|
diameter: number;
|
||||||
@@ -12,6 +13,7 @@ export interface Cylinder3DInput {
|
|||||||
/** Cpe profile ao longo da circunferência (0° a 180°) */
|
/** Cpe profile ao longo da circunferência (0° a 180°) */
|
||||||
cpeProfile: { angle: number; cpe: number }[];
|
cpeProfile: { angle: number; cpe: number }[];
|
||||||
cpi: number;
|
cpi: number;
|
||||||
|
viewMode?: 'solid' | 'airflow';
|
||||||
}
|
}
|
||||||
|
|
||||||
function cylinderColor(cpe: number, cpi: number): THREE.Color {
|
function cylinderColor(cpe: number, cpi: number): THREE.Color {
|
||||||
@@ -23,9 +25,10 @@ function cylinderColor(cpe: number, cpi: number): THREE.Color {
|
|||||||
return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, 65 - intensity * 20)}%)`);
|
return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, 65 - intensity * 20)}%)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
|
function CylinderModel({ diameter, height, cpeProfile, cpi, viewMode = 'solid' }: Cylinder3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
|
const isAirflow = viewMode === 'airflow';
|
||||||
const segments = 64;
|
const segments = 64;
|
||||||
const radius = diameter / 2;
|
const radius = diameter / 2;
|
||||||
|
|
||||||
@@ -111,7 +114,13 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
|
|||||||
args={[normals, 3]}
|
args={[normals, 3]}
|
||||||
/>
|
/>
|
||||||
</bufferGeometry>
|
</bufferGeometry>
|
||||||
<meshStandardMaterial color={face.color} opacity={0.9} transparent roughness={0.4} side={THREE.DoubleSide} />
|
<meshStandardMaterial
|
||||||
|
color={face.color}
|
||||||
|
opacity={isAirflow ? 0.35 : 0.9}
|
||||||
|
transparent
|
||||||
|
roughness={0.4}
|
||||||
|
side={THREE.DoubleSide}
|
||||||
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -119,7 +128,13 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
|
|||||||
{/* Tampa superior sólida */}
|
{/* Tampa superior sólida */}
|
||||||
<mesh position={[0, height, 0]} rotation={[-Math.PI / 2, 0, 0]} castShadow receiveShadow>
|
<mesh position={[0, height, 0]} rotation={[-Math.PI / 2, 0, 0]} castShadow receiveShadow>
|
||||||
<circleGeometry args={[radius, segments]} />
|
<circleGeometry args={[radius, segments]} />
|
||||||
<meshStandardMaterial color={cylinderColor(cpeProfile[cpeProfile.length - 1].cpe, cpi)} opacity={0.8} transparent side={THREE.DoubleSide} roughness={0.4} />
|
<meshStandardMaterial
|
||||||
|
color={cylinderColor(cpeProfile[cpeProfile.length - 1].cpe, cpi)}
|
||||||
|
opacity={isAirflow ? 0.35 : 0.8}
|
||||||
|
transparent
|
||||||
|
side={THREE.DoubleSide}
|
||||||
|
roughness={0.4}
|
||||||
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Anéis de detalhe (bordas do cilindro) */}
|
{/* Anéis de detalhe (bordas do cilindro) */}
|
||||||
@@ -142,22 +157,24 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
|
|||||||
<cylinderGeometry args={[0.1, 0.1, 1.2, 16]} />
|
<cylinderGeometry args={[0.1, 0.1, 1.2, 16]} />
|
||||||
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<Text
|
|
||||||
position={[0, -1.5, 0]}
|
|
||||||
rotation={[Math.PI / 2, 0, 0]}
|
|
||||||
fontSize={0.4}
|
|
||||||
color="#3b82f6"
|
|
||||||
anchorX="center"
|
|
||||||
anchorY="middle"
|
|
||||||
>
|
|
||||||
Vento
|
|
||||||
</Text>
|
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
{/* Texto de Informação */}
|
{/* Texto Vento - acima da seta com o dobro do tamanho (1.2) */}
|
||||||
|
<Text
|
||||||
|
position={[-radius - 2.5, height / 2 + 1.0, 0]}
|
||||||
|
rotation={[0, 0, 0]}
|
||||||
|
fontSize={1.2}
|
||||||
|
color="#3b82f6"
|
||||||
|
anchorX="center"
|
||||||
|
anchorY="bottom"
|
||||||
|
>
|
||||||
|
Vento
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Texto de Informação - 50% maior */}
|
||||||
<Text
|
<Text
|
||||||
position={[0, height + 0.8, 0]}
|
position={[0, height + 0.8, 0]}
|
||||||
fontSize={Math.max(0.3, Math.min(0.6, diameter / 10))}
|
fontSize={Math.max(0.45, Math.min(0.9, (diameter / 10) * 1.5))}
|
||||||
color={isDark ? "#cbd5e1" : "#1a202c"}
|
color={isDark ? "#cbd5e1" : "#1a202c"}
|
||||||
anchorX="center"
|
anchorX="center"
|
||||||
anchorY="bottom"
|
anchorY="bottom"
|
||||||
@@ -168,7 +185,7 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Cylinder3DViewer({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
|
export default function Cylinder3DViewer({ diameter, height, cpeProfile, cpi, viewMode = 'solid' }: Cylinder3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
@@ -184,6 +201,7 @@ export default function Cylinder3DViewer({ diameter, height, cpeProfile, cpi }:
|
|||||||
return (
|
return (
|
||||||
<SceneCanvas moduleId="cilindro"
|
<SceneCanvas moduleId="cilindro"
|
||||||
shadows
|
shadows
|
||||||
|
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [diameter * 1.5, height * 1.2, diameter * 1.5], fov: 40 }}
|
camera={{ position: [diameter * 1.5, height * 1.2, diameter * 1.5], fov: 40 }}
|
||||||
fallback={fallback}
|
fallback={fallback}
|
||||||
@@ -201,7 +219,10 @@ export default function Cylinder3DViewer({ diameter, height, cpeProfile, cpi }:
|
|||||||
shadow-camera-top={maxDim}
|
shadow-camera-top={maxDim}
|
||||||
shadow-camera-bottom={-maxDim}
|
shadow-camera-bottom={-maxDim}
|
||||||
/>
|
/>
|
||||||
<CylinderModel diameter={diameter} height={height} cpeProfile={cpeProfile} cpi={cpi} />
|
<CylinderModel diameter={diameter} height={height} cpeProfile={cpeProfile} cpi={cpi} viewMode={viewMode} />
|
||||||
|
{viewMode === 'airflow' && (
|
||||||
|
<CylinderAirflowSystem diameter={diameter} height={height} />
|
||||||
|
)}
|
||||||
<Grid
|
<Grid
|
||||||
infiniteGrid
|
infiniteGrid
|
||||||
fadeDistance={maxDim * 5}
|
fadeDistance={maxDim * 5}
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import { useRef, useMemo, useEffect } from 'react';
|
||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { useCanvasTheme } from '../../lib/theme';
|
||||||
|
|
||||||
|
export interface CylinderAirflowSystemProps {
|
||||||
|
diameter: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CylinderAirflowSystem({ diameter, height }: CylinderAirflowSystemProps) {
|
||||||
|
const isDark = useCanvasTheme() === 'dark';
|
||||||
|
const count = 450;
|
||||||
|
const meshRef = useRef<THREE.InstancedMesh>(null);
|
||||||
|
const R = diameter / 2;
|
||||||
|
|
||||||
|
const particles = useMemo(() => {
|
||||||
|
const temp = [];
|
||||||
|
const spanX = Math.max(15, R * 5);
|
||||||
|
const spanZ = Math.max(10, R * 3.5);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
temp.push({
|
||||||
|
position: new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * spanX * 2,
|
||||||
|
Math.random() * (height * 1.3),
|
||||||
|
(Math.random() - 0.5) * spanZ * 2
|
||||||
|
),
|
||||||
|
baseSpeed: 0.12 + Math.random() * 0.08,
|
||||||
|
wobbleSpeed: Math.random() * 0.05,
|
||||||
|
wobbleOffset: Math.random() * Math.PI * 2,
|
||||||
|
currentCpe: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return temp;
|
||||||
|
}, [count, R, height]);
|
||||||
|
|
||||||
|
const dummy = useMemo(() => new THREE.Object3D(), []);
|
||||||
|
const colorObj = useMemo(() => new THREE.Color(), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (meshRef.current) {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
meshRef.current.setColorAt(i, new THREE.Color('#ffffff'));
|
||||||
|
}
|
||||||
|
if (meshRef.current.instanceColor) {
|
||||||
|
meshRef.current.instanceColor.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [count]);
|
||||||
|
|
||||||
|
useFrame((state) => {
|
||||||
|
if (!meshRef.current) return;
|
||||||
|
const time = state.clock.elapsedTime;
|
||||||
|
const spanX = Math.max(15, R * 5);
|
||||||
|
const spanZ = Math.max(10, R * 3.5);
|
||||||
|
|
||||||
|
particles.forEach((p, i) => {
|
||||||
|
let deflectX = 1.0;
|
||||||
|
let deflectY = 0;
|
||||||
|
let deflectZ = 0;
|
||||||
|
|
||||||
|
const px = p.position.x;
|
||||||
|
const py = p.position.y;
|
||||||
|
const pz = p.position.z;
|
||||||
|
|
||||||
|
const r = Math.sqrt(px * px + pz * pz);
|
||||||
|
const safeR = Math.max(R + 0.3, r);
|
||||||
|
|
||||||
|
// Proteção para não entrar no cilindro
|
||||||
|
if (r < R + 0.25 && py <= height) {
|
||||||
|
const angle = Math.atan2(pz, px);
|
||||||
|
p.position.x = Math.cos(angle) * (R + 0.25);
|
||||||
|
p.position.z = Math.sin(angle) * (R + 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cálculo aerodinâmico (escoamento potencial ao redor de cilindro)
|
||||||
|
if (py <= height * 1.05 && r < R * 3.0) {
|
||||||
|
const r2 = safeR * safeR;
|
||||||
|
const R2 = R * R;
|
||||||
|
// Velocidades de escoamento potencial em 2D ao redor do cilindro
|
||||||
|
deflectX = 1 - (R2 * (px * px - pz * pz)) / (r2 * r2);
|
||||||
|
deflectZ = -(2 * R2 * px * pz) / (r2 * r2);
|
||||||
|
|
||||||
|
// Esteira turbulenta na região à jusante (+X)
|
||||||
|
if (px > 0 && Math.abs(pz) < R * 1.3) {
|
||||||
|
deflectZ += Math.sin(time * 5 + px * 0.8 + p.wobbleOffset) * 0.35;
|
||||||
|
deflectX = Math.max(0.4, deflectX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determina coeficiente para cor da partícula
|
||||||
|
if (py <= height && r < R * 2.2) {
|
||||||
|
if (px < -R * 0.3 && Math.abs(pz) < R * 0.8) {
|
||||||
|
p.currentCpe = 0.8; // Barlavento (pressão positiva)
|
||||||
|
} else if (Math.abs(px) <= R * 0.6 && r < R * 1.5) {
|
||||||
|
p.currentCpe = -1.2; // Laterais (forte sucção)
|
||||||
|
} else if (px > R * 0.3 && Math.abs(pz) < R * 1.2) {
|
||||||
|
p.currentCpe = 0.5; // Esteira (agora positivo para ficar vermelho)
|
||||||
|
} else {
|
||||||
|
p.currentCpe = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.currentCpe = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveVec = new THREE.Vector3(deflectX, deflectY, deflectZ).normalize();
|
||||||
|
const currentSpeed = p.baseSpeed * Math.max(0.3, Math.min(1.5, deflectX));
|
||||||
|
|
||||||
|
p.position.addScaledVector(moveVec, currentSpeed);
|
||||||
|
|
||||||
|
// Reposição quando sai do volume
|
||||||
|
if (p.position.x > spanX) {
|
||||||
|
p.position.x = -spanX;
|
||||||
|
p.position.y = Math.random() * (height * 1.3);
|
||||||
|
p.position.z = (Math.random() - 0.5) * spanZ * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
dummy.position.copy(p.position);
|
||||||
|
dummy.lookAt(p.position.clone().add(moveVec));
|
||||||
|
dummy.rotateX(Math.PI / 2);
|
||||||
|
dummy.scale.set(1, 1 + currentSpeed * 6, 1);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||||
|
|
||||||
|
if (p.currentCpe > 0) {
|
||||||
|
colorObj.set(isDark ? '#f87171' : '#ef4444'); // Vermelho (Turbulento/Pressão)
|
||||||
|
} else if (p.currentCpe < 0) {
|
||||||
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
|
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção)
|
||||||
|
} else {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre)
|
||||||
|
}
|
||||||
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
meshRef.current.instanceMatrix.needsUpdate = true;
|
||||||
|
if (meshRef.current.instanceColor) {
|
||||||
|
meshRef.current.instanceColor.needsUpdate = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
|
||||||
|
<cylinderGeometry args={[0.01, 0.01, 1.5, 4]} />
|
||||||
|
<meshBasicMaterial
|
||||||
|
color="#ffffff"
|
||||||
|
transparent
|
||||||
|
opacity={isDark ? 0.6 : 0.8}
|
||||||
|
blending={isDark ? THREE.AdditiveBlending : THREE.NormalBlending}
|
||||||
|
depthWrite={false}
|
||||||
|
/>
|
||||||
|
</instancedMesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import * as THREE from 'three';
|
|||||||
import SceneCanvas from '../SceneCanvas';
|
import SceneCanvas from '../SceneCanvas';
|
||||||
import FallbackDiagram from '../FallbackDiagram';
|
import FallbackDiagram from '../FallbackDiagram';
|
||||||
import { useCanvasTheme } from '@/lib/theme';
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
import { DomeAirflowSystem } from './DomeAirflowSystem';
|
||||||
|
|
||||||
export interface Dome3DInput {
|
export interface Dome3DInput {
|
||||||
diameter: number;
|
diameter: number;
|
||||||
@@ -14,6 +15,7 @@ export interface Dome3DInput {
|
|||||||
cpeBarlavento: number;
|
cpeBarlavento: number;
|
||||||
cpeTopo: number;
|
cpeTopo: number;
|
||||||
cpeLateral: number;
|
cpeLateral: number;
|
||||||
|
viewMode?: 'solid' | 'airflow';
|
||||||
}
|
}
|
||||||
|
|
||||||
function domeColor(cpe: number, cpi: number, isDark: boolean): THREE.Color {
|
function domeColor(cpe: number, cpi: number, isDark: boolean): THREE.Color {
|
||||||
@@ -26,9 +28,10 @@ function domeColor(cpe: number, cpi: number, isDark: boolean): THREE.Color {
|
|||||||
return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, l + 5)}%)`);
|
return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, l + 5)}%)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cpeLateral }: Dome3DInput) {
|
function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cpeLateral, viewMode = 'solid' }: Dome3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
|
const isAirflow = viewMode === 'airflow';
|
||||||
const radius = diameter / 2;
|
const radius = diameter / 2;
|
||||||
const segments = 64;
|
const segments = 64;
|
||||||
|
|
||||||
@@ -64,7 +67,7 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
|
|||||||
{/* Parede cilíndrica inferior */}
|
{/* Parede cilíndrica inferior */}
|
||||||
<mesh position={[0, wallHeight / 2, 0]} castShadow receiveShadow>
|
<mesh position={[0, wallHeight / 2, 0]} castShadow receiveShadow>
|
||||||
<cylinderGeometry args={[radius, radius, wallHeight, segments, 1, false]} />
|
<cylinderGeometry args={[radius, radius, wallHeight, segments, 1, false]} />
|
||||||
<meshStandardMaterial color="#cbd5e1" opacity={0.8} transparent roughness={0.4} />
|
<meshStandardMaterial color="#cbd5e1" opacity={isAirflow ? 0.25 : 0.8} transparent roughness={0.4} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Detalhes de anéis metálicos nas bordas */}
|
{/* Detalhes de anéis metálicos nas bordas */}
|
||||||
@@ -124,13 +127,13 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
|
|||||||
<bufferAttribute attach="attributes-normal" args={[new Float32Array(normals), 3]} />
|
<bufferAttribute attach="attributes-normal" args={[new Float32Array(normals), 3]} />
|
||||||
<bufferAttribute attach="index" args={[new Uint16Array(indices), 1]} />
|
<bufferAttribute attach="index" args={[new Uint16Array(indices), 1]} />
|
||||||
</bufferGeometry>
|
</bufferGeometry>
|
||||||
<meshStandardMaterial color={zone.color} opacity={0.92} transparent roughness={0.4} side={THREE.DoubleSide} />
|
<meshStandardMaterial color={zone.color} opacity={isAirflow ? 0.35 : 0.92} transparent roughness={0.4} side={THREE.DoubleSide} />
|
||||||
</mesh>
|
</mesh>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Seta indicativa de direção do vento */}
|
{/* Seta indicativa de direção do vento */}
|
||||||
<group position={[radius + 2.5, wallHeight / 2, 0]} rotation={[0, 0, Math.PI / 2]}>
|
<group position={[-radius - 2.5, wallHeight / 2, 0]} rotation={[0, 0, -Math.PI / 2]}>
|
||||||
<mesh castShadow>
|
<mesh castShadow>
|
||||||
<coneGeometry args={[0.3, 0.8, 16]} />
|
<coneGeometry args={[0.3, 0.8, 16]} />
|
||||||
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
||||||
@@ -139,22 +142,24 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
|
|||||||
<cylinderGeometry args={[0.1, 0.1, 1.2, 16]} />
|
<cylinderGeometry args={[0.1, 0.1, 1.2, 16]} />
|
||||||
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<Text
|
|
||||||
position={[0, -1.5, 0]}
|
|
||||||
rotation={[Math.PI / 2, 0, 0]}
|
|
||||||
fontSize={0.4}
|
|
||||||
color="#3b82f6"
|
|
||||||
anchorX="center"
|
|
||||||
anchorY="middle"
|
|
||||||
>
|
|
||||||
Vento
|
|
||||||
</Text>
|
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
|
{/* Texto Vento */}
|
||||||
|
<Text
|
||||||
|
position={[-radius - 2.5, wallHeight / 2 + 1.2, 0]}
|
||||||
|
rotation={[0, 0, 0]}
|
||||||
|
fontSize={1.2}
|
||||||
|
color="#3b82f6"
|
||||||
|
anchorX="center"
|
||||||
|
anchorY="bottom"
|
||||||
|
>
|
||||||
|
Vento
|
||||||
|
</Text>
|
||||||
|
|
||||||
{/* Texto informativo */}
|
{/* Texto informativo */}
|
||||||
<Text
|
<Text
|
||||||
position={[0, wallHeight + rise + 0.8, 0]}
|
position={[0, wallHeight + rise + 0.8, 0]}
|
||||||
fontSize={Math.max(0.3, Math.min(0.6, diameter / 12))}
|
fontSize={Math.max(0.35, Math.min(0.7, (diameter / 12) * 1.3))}
|
||||||
color={isDark ? "#cbd5e1" : "#1a202c"}
|
color={isDark ? "#cbd5e1" : "#1a202c"}
|
||||||
anchorX="center"
|
anchorX="center"
|
||||||
anchorY="bottom"
|
anchorY="bottom"
|
||||||
@@ -168,6 +173,7 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
|
|||||||
export default function Dome3DViewer(props: Dome3DInput) {
|
export default function Dome3DViewer(props: Dome3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
|
const { viewMode = 'solid' } = props;
|
||||||
|
|
||||||
const fallback = (
|
const fallback = (
|
||||||
<FallbackDiagram
|
<FallbackDiagram
|
||||||
@@ -181,6 +187,7 @@ export default function Dome3DViewer(props: Dome3DInput) {
|
|||||||
return (
|
return (
|
||||||
<SceneCanvas moduleId="cupula"
|
<SceneCanvas moduleId="cupula"
|
||||||
shadows
|
shadows
|
||||||
|
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [props.diameter * 1.5, (props.wallHeight + props.rise) * 1.5, props.diameter * 1.5], fov: 40 }}
|
camera={{ position: [props.diameter * 1.5, (props.wallHeight + props.rise) * 1.5, props.diameter * 1.5], fov: 40 }}
|
||||||
fallback={fallback}
|
fallback={fallback}
|
||||||
@@ -199,6 +206,9 @@ export default function Dome3DViewer(props: Dome3DInput) {
|
|||||||
shadow-camera-bottom={-maxDim}
|
shadow-camera-bottom={-maxDim}
|
||||||
/>
|
/>
|
||||||
<DomeModel {...props} />
|
<DomeModel {...props} />
|
||||||
|
{viewMode === 'airflow' && (
|
||||||
|
<DomeAirflowSystem diameter={props.diameter} rise={props.rise} wallHeight={props.wallHeight} />
|
||||||
|
)}
|
||||||
<Grid
|
<Grid
|
||||||
infiniteGrid
|
infiniteGrid
|
||||||
fadeDistance={maxDim * 5}
|
fadeDistance={maxDim * 5}
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import { useRef, useMemo, useEffect } from 'react';
|
||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { useCanvasTheme } from '../../lib/theme';
|
||||||
|
|
||||||
|
export interface DomeAirflowSystemProps {
|
||||||
|
diameter: number;
|
||||||
|
rise: number;
|
||||||
|
wallHeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DomeAirflowSystem({ diameter, rise, wallHeight }: DomeAirflowSystemProps) {
|
||||||
|
const isDark = useCanvasTheme() === 'dark';
|
||||||
|
const count = 400;
|
||||||
|
const meshRef = useRef<THREE.InstancedMesh>(null);
|
||||||
|
const R = diameter / 2;
|
||||||
|
const H = wallHeight + rise;
|
||||||
|
|
||||||
|
const particles = useMemo(() => {
|
||||||
|
const temp = [];
|
||||||
|
const spanX = Math.max(15, R * 5);
|
||||||
|
const spanZ = Math.max(12, R * 4);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
temp.push({
|
||||||
|
position: new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * spanX * 2,
|
||||||
|
Math.random() * (H * 1.4),
|
||||||
|
(Math.random() - 0.5) * spanZ * 2
|
||||||
|
),
|
||||||
|
baseSpeed: 0.12 + Math.random() * 0.08,
|
||||||
|
wobbleSpeed: Math.random() * 0.05,
|
||||||
|
wobbleOffset: Math.random() * Math.PI * 2,
|
||||||
|
currentCpe: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return temp;
|
||||||
|
}, [count, R, H]);
|
||||||
|
|
||||||
|
const dummy = useMemo(() => new THREE.Object3D(), []);
|
||||||
|
const colorObj = useMemo(() => new THREE.Color(), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (meshRef.current) {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
meshRef.current.setColorAt(i, new THREE.Color('#ffffff'));
|
||||||
|
}
|
||||||
|
if (meshRef.current.instanceColor) {
|
||||||
|
meshRef.current.instanceColor.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [count]);
|
||||||
|
|
||||||
|
useFrame((state) => {
|
||||||
|
if (!meshRef.current) return;
|
||||||
|
const time = state.clock.elapsedTime;
|
||||||
|
const spanX = Math.max(15, R * 5);
|
||||||
|
const spanZ = Math.max(12, R * 4);
|
||||||
|
|
||||||
|
// Parâmetros da esfera da calota
|
||||||
|
const rSphere = (R * R + rise * rise) / (2 * rise);
|
||||||
|
const centerY = wallHeight - (rSphere - rise);
|
||||||
|
|
||||||
|
particles.forEach((p, i) => {
|
||||||
|
let deflectX = 1.0;
|
||||||
|
let deflectY = 0.0;
|
||||||
|
let deflectZ = 0.0;
|
||||||
|
|
||||||
|
const px = p.position.x;
|
||||||
|
const py = p.position.y;
|
||||||
|
const pz = p.position.z;
|
||||||
|
|
||||||
|
const r = Math.sqrt(px * px + pz * pz);
|
||||||
|
const safeR = Math.max(R + 0.3, r);
|
||||||
|
|
||||||
|
// Colisão / Deflexão com a parede cilíndrica inferior
|
||||||
|
if (py <= wallHeight) {
|
||||||
|
if (r < R + 0.25) {
|
||||||
|
const angle = Math.atan2(pz, px);
|
||||||
|
p.position.x = Math.cos(angle) * (R + 0.25);
|
||||||
|
p.position.z = Math.sin(angle) * (R + 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r < R * 3.0) {
|
||||||
|
const r2 = safeR * safeR;
|
||||||
|
const R2 = R * R;
|
||||||
|
deflectX = 1 - (R2 * (px * px - pz * pz)) / (r2 * r2);
|
||||||
|
deflectZ = -(2 * R2 * px * pz) / (r2 * r2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Colisão / Deflexão com a calota esférica (Cúpula)
|
||||||
|
else if (py > wallHeight && py <= H + 2.0) {
|
||||||
|
// Distância até o centro virtual da calota
|
||||||
|
const dySphere = py - centerY;
|
||||||
|
const distToSphere = Math.sqrt(px * px + dySphere * dySphere + pz * pz);
|
||||||
|
|
||||||
|
if (distToSphere < rSphere + 0.25) {
|
||||||
|
// Normal da esfera
|
||||||
|
const nx = px / distToSphere;
|
||||||
|
const ny = dySphere / distToSphere;
|
||||||
|
const nz = pz / distToSphere;
|
||||||
|
|
||||||
|
// Reposiciona na casca externa
|
||||||
|
p.position.x = nx * (rSphere + 0.25);
|
||||||
|
p.position.y = centerY + ny * (rSphere + 0.25);
|
||||||
|
p.position.z = nz * (rSphere + 0.25);
|
||||||
|
|
||||||
|
// Direciona o vento para subir
|
||||||
|
deflectY = ny * 1.2;
|
||||||
|
deflectX = nx * 1.2;
|
||||||
|
deflectZ = nz * 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Esteira de sucção sotavento
|
||||||
|
if (px > R * 0.8 && Math.abs(pz) < R * 1.5 && py <= H) {
|
||||||
|
deflectZ += Math.sin(time * 5 + px * 0.8 + p.wobbleOffset) * 0.25;
|
||||||
|
deflectY -= 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapear Coeficiente Cpe local para cor
|
||||||
|
if (py <= H + 0.5 && r < R * 2.2) {
|
||||||
|
if (px < -R * 0.3) {
|
||||||
|
p.currentCpe = 0.6; // Barlavento
|
||||||
|
} else if (Math.abs(px) <= R * 0.4 && py > wallHeight) {
|
||||||
|
p.currentCpe = -1.0; // Topo da cúpula (forte sucção)
|
||||||
|
} else if (px > R * 0.2) {
|
||||||
|
p.currentCpe = 0.5; // Sotavento / Esteira (agora positivo para ficar vermelho)
|
||||||
|
} else {
|
||||||
|
p.currentCpe = -0.5; // Laterais / transição
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.currentCpe = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveVec = new THREE.Vector3(deflectX, deflectY, deflectZ).normalize();
|
||||||
|
const currentSpeed = p.baseSpeed * Math.max(0.3, Math.min(1.5, deflectX));
|
||||||
|
|
||||||
|
p.position.addScaledVector(moveVec, currentSpeed);
|
||||||
|
|
||||||
|
// Reposição quando sai do volume
|
||||||
|
if (p.position.x > spanX) {
|
||||||
|
p.position.x = -spanX;
|
||||||
|
p.position.y = Math.random() * (H * 1.4);
|
||||||
|
p.position.z = (Math.random() - 0.5) * spanZ * 2;
|
||||||
|
p.currentCpe = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dummy.position.copy(p.position);
|
||||||
|
dummy.lookAt(p.position.clone().add(moveVec));
|
||||||
|
dummy.rotateX(Math.PI / 2);
|
||||||
|
dummy.scale.set(1, 1 + currentSpeed * 6, 1);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||||
|
|
||||||
|
if (p.currentCpe > 0) {
|
||||||
|
colorObj.set(isDark ? '#f87171' : '#ef4444'); // Vermelho (Turbulento/Pressão)
|
||||||
|
} else if (p.currentCpe < 0) {
|
||||||
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
|
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção)
|
||||||
|
} else {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre)
|
||||||
|
}
|
||||||
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
meshRef.current.instanceMatrix.needsUpdate = true;
|
||||||
|
if (meshRef.current.instanceColor) {
|
||||||
|
meshRef.current.instanceColor.needsUpdate = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
|
||||||
|
<cylinderGeometry args={[0.01, 0.01, 1.5, 4]} />
|
||||||
|
<meshBasicMaterial
|
||||||
|
color="#ffffff"
|
||||||
|
transparent
|
||||||
|
opacity={isDark ? 0.6 : 0.8}
|
||||||
|
blending={isDark ? THREE.AdditiveBlending : THREE.NormalBlending}
|
||||||
|
depthWrite={false}
|
||||||
|
/>
|
||||||
|
</instancedMesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,12 +4,12 @@ import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls';
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import SceneCanvas from '../SceneCanvas';
|
import SceneCanvas from '../SceneCanvas';
|
||||||
import FallbackDiagram from '../FallbackDiagram';
|
import FallbackDiagram from '../FallbackDiagram';
|
||||||
import { WindArrow } from './WindArrow';
|
|
||||||
import { useCanvasTheme } from '@/lib/theme';
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
import { IsolatedRoofAirflowSystem } from './IsolatedRoofAirflowSystem';
|
||||||
|
|
||||||
export interface IsolatedRoof3DInput {
|
export interface IsolatedRoof3DInput {
|
||||||
/** Tipo de cobertura: 'shed' (uma água) ou 'gable' (duas águas) */
|
/** Tipo de cobertura: 'shed' (uma água), 'gable' (duas águas) ou 'butterfly' (borboleta) */
|
||||||
type: 'shed' | 'gable';
|
type: 'shed' | 'gable' | 'butterfly';
|
||||||
/** Inclinação θ (graus) */
|
/** Inclinação θ (graus) */
|
||||||
theta: number;
|
theta: number;
|
||||||
/** Altura livre dos suportes (m) */
|
/** Altura livre dos suportes (m) */
|
||||||
@@ -26,6 +26,7 @@ export interface IsolatedRoof3DInput {
|
|||||||
cpeTop: number;
|
cpeTop: number;
|
||||||
/** Força resultante na cobertura (kN) */
|
/** Força resultante na cobertura (kN) */
|
||||||
forceKN: number;
|
forceKN: number;
|
||||||
|
viewMode?: 'solid' | 'airflow';
|
||||||
}
|
}
|
||||||
|
|
||||||
function pressureColor(cpe: number, isDark: boolean): THREE.Color {
|
function pressureColor(cpe: number, isDark: boolean): THREE.Color {
|
||||||
@@ -50,9 +51,11 @@ function IsolatedRoofModel({
|
|||||||
cpeWindward,
|
cpeWindward,
|
||||||
cpeLeeward,
|
cpeLeeward,
|
||||||
forceKN,
|
forceKN,
|
||||||
|
viewMode = 'solid',
|
||||||
}: IsolatedRoof3DInput) {
|
}: IsolatedRoof3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
|
const isAirflow = viewMode === 'airflow';
|
||||||
const labelColor = isDark ? '#cbd5e1' : '#475569';
|
const labelColor = isDark ? '#cbd5e1' : '#475569';
|
||||||
const textColor = isDark ? '#93c5fd' : '#1a202c';
|
const textColor = isDark ? '#93c5fd' : '#1a202c';
|
||||||
const lineMaterialColor = isDark ? '#94a3b8' : '#64748b';
|
const lineMaterialColor = isDark ? '#94a3b8' : '#64748b';
|
||||||
@@ -81,7 +84,7 @@ function IsolatedRoofModel({
|
|||||||
{ pos: [-halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
{ pos: [-halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
||||||
{ pos: [halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
{ pos: [halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
||||||
);
|
);
|
||||||
} else {
|
} else if (type === 'gable') {
|
||||||
list.push(
|
list.push(
|
||||||
{ pos: [-halfWidth, height / 2, -halfDepth], h: height },
|
{ pos: [-halfWidth, height / 2, -halfDepth], h: height },
|
||||||
{ pos: [halfWidth, height / 2, -halfDepth], h: height },
|
{ pos: [halfWidth, height / 2, -halfDepth], h: height },
|
||||||
@@ -90,6 +93,15 @@ function IsolatedRoofModel({
|
|||||||
{ pos: [-halfWidth, (height + h_half) / 2, 0], h: height + h_half },
|
{ pos: [-halfWidth, (height + h_half) / 2, 0], h: height + h_half },
|
||||||
{ pos: [halfWidth, (height + h_half) / 2, 0], h: height + h_half },
|
{ pos: [halfWidth, (height + h_half) / 2, 0], h: height + h_half },
|
||||||
);
|
);
|
||||||
|
} else if (type === 'butterfly') {
|
||||||
|
list.push(
|
||||||
|
{ pos: [-halfWidth, (height + h_half) / 2, -halfDepth], h: height + h_half },
|
||||||
|
{ pos: [halfWidth, (height + h_half) / 2, -halfDepth], h: height + h_half },
|
||||||
|
{ pos: [-halfWidth, (height + h_half) / 2, halfDepth], h: height + h_half },
|
||||||
|
{ pos: [halfWidth, (height + h_half) / 2, halfDepth], h: height + h_half },
|
||||||
|
{ pos: [-halfWidth, height / 2, 0], h: height },
|
||||||
|
{ pos: [halfWidth, height / 2, 0], h: height },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
}, [type, depth, width, height, h_diff, h_half, halfDepth, halfWidth]);
|
}, [type, depth, width, height, h_diff, h_half, halfDepth, halfWidth]);
|
||||||
@@ -106,25 +118,9 @@ function IsolatedRoofModel({
|
|||||||
{type === 'shed' ? (
|
{type === 'shed' ? (
|
||||||
// Uma água (Shed): dividida em metade barlavento e metade sotavento
|
// Uma água (Shed): dividida em metade barlavento e metade sotavento
|
||||||
<group>
|
<group>
|
||||||
{/* Metade Barlavento (Z < 0) */}
|
<mesh position={[0, height + h_diff / 2, 0]} rotation={[-thetaRad, 0, 0]} castShadow receiveShadow>
|
||||||
<mesh
|
<boxGeometry args={[width, 0.08, depth / Math.cos(thetaRad)]} />
|
||||||
position={[0, height + h_diff / 4, -depth / 4]}
|
<meshStandardMaterial color={windwardColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.4} />
|
||||||
rotation={[-thetaRad, 0, 0]}
|
|
||||||
castShadow
|
|
||||||
receiveShadow
|
|
||||||
>
|
|
||||||
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
|
||||||
<meshStandardMaterial color={windwardColor} opacity={0.9} transparent roughness={0.4} />
|
|
||||||
</mesh>
|
|
||||||
{/* Metade Sotavento (Z > 0) */}
|
|
||||||
<mesh
|
|
||||||
position={[0, height + (3 * h_diff) / 4, depth / 4]}
|
|
||||||
rotation={[-thetaRad, 0, 0]}
|
|
||||||
castShadow
|
|
||||||
receiveShadow
|
|
||||||
>
|
|
||||||
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
|
||||||
<meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} />
|
|
||||||
</mesh>
|
</mesh>
|
||||||
</group>
|
</group>
|
||||||
) : (
|
) : (
|
||||||
@@ -133,22 +129,22 @@ function IsolatedRoofModel({
|
|||||||
{/* Água Esquerda / Barlavento (Z < 0) */}
|
{/* Água Esquerda / Barlavento (Z < 0) */}
|
||||||
<mesh
|
<mesh
|
||||||
position={[0, height + h_half / 2, -depth / 4]}
|
position={[0, height + h_half / 2, -depth / 4]}
|
||||||
rotation={[-thetaRad, 0, 0]}
|
rotation={[type === 'butterfly' ? thetaRad : -thetaRad, 0, 0]}
|
||||||
castShadow
|
castShadow
|
||||||
receiveShadow
|
receiveShadow
|
||||||
>
|
>
|
||||||
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||||
<meshStandardMaterial color={windwardColor} opacity={0.9} transparent roughness={0.4} />
|
<meshStandardMaterial color={windwardColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.4} />
|
||||||
</mesh>
|
</mesh>
|
||||||
{/* Água Direita / Sotavento (Z > 0) */}
|
{/* Água Direita / Sotavento (Z > 0) */}
|
||||||
<mesh
|
<mesh
|
||||||
position={[0, height + h_half / 2, depth / 4]}
|
position={[0, height + h_half / 2, depth / 4]}
|
||||||
rotation={[thetaRad, 0, 0]}
|
rotation={[type === 'butterfly' ? -thetaRad : thetaRad, 0, 0]}
|
||||||
castShadow
|
castShadow
|
||||||
receiveShadow
|
receiveShadow
|
||||||
>
|
>
|
||||||
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||||
<meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} />
|
<meshStandardMaterial color={leewardColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.4} />
|
||||||
</mesh>
|
</mesh>
|
||||||
</group>
|
</group>
|
||||||
)}
|
)}
|
||||||
@@ -161,11 +157,27 @@ function IsolatedRoofModel({
|
|||||||
</mesh>
|
</mesh>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Vetor de vento (Seta verde/azul batendo no elemento) */}
|
{/* Vetor de vento (Seta mais visível com texto) */}
|
||||||
<WindArrow
|
<group position={[0, centerY, -halfDepth - 3.5]} rotation={[Math.PI / 2, 0, 0]}>
|
||||||
target={new THREE.Vector3(0, centerY, 0)}
|
<mesh castShadow>
|
||||||
direction={new THREE.Vector3(0, 0, 1)}
|
<coneGeometry args={[0.4, 1.2, 16]} />
|
||||||
/>
|
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, -1.0, 0]} castShadow>
|
||||||
|
<cylinderGeometry args={[0.15, 0.15, 2.0, 16]} />
|
||||||
|
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
<Text
|
||||||
|
position={[0, centerY + 1.2, -halfDepth - 3.5]}
|
||||||
|
rotation={[0, Math.PI, 0]}
|
||||||
|
fontSize={1.2}
|
||||||
|
color="#3b82f6"
|
||||||
|
anchorX="center"
|
||||||
|
anchorY="bottom"
|
||||||
|
>
|
||||||
|
Vento
|
||||||
|
</Text>
|
||||||
|
|
||||||
{/* === LINHAS DE COTA (CAD-Style) === */}
|
{/* === LINHAS DE COTA (CAD-Style) === */}
|
||||||
{/* Cota de Altura (h) */}
|
{/* Cota de Altura (h) */}
|
||||||
@@ -248,6 +260,8 @@ function IsolatedRoofModel({
|
|||||||
{/* Rótulo Superior */}
|
{/* Rótulo Superior */}
|
||||||
<Text
|
<Text
|
||||||
position={[0, centerY + 3, 0]}
|
position={[0, centerY + 3, 0]}
|
||||||
|
rotation={[0, Math.PI, 0]}
|
||||||
|
fontSize={Math.max(0.4, width / 15)}
|
||||||
color={textColor}
|
color={textColor}
|
||||||
anchorX="center"
|
anchorX="center"
|
||||||
anchorY="bottom"
|
anchorY="bottom"
|
||||||
@@ -268,6 +282,7 @@ export default function IsolatedRoof3DViewer({
|
|||||||
cpeLeeward,
|
cpeLeeward,
|
||||||
cpeTop,
|
cpeTop,
|
||||||
forceKN,
|
forceKN,
|
||||||
|
viewMode = 'solid',
|
||||||
}: IsolatedRoof3DInput) {
|
}: IsolatedRoof3DInput) {
|
||||||
const cameraDistance = Math.max(Math.max(width, depth) * 1.3, height * 1.5, 8);
|
const cameraDistance = Math.max(Math.max(width, depth) * 1.3, height * 1.5, 8);
|
||||||
|
|
||||||
@@ -281,8 +296,9 @@ export default function IsolatedRoof3DViewer({
|
|||||||
return (
|
return (
|
||||||
<SceneCanvas moduleId="cobertura_isolada"
|
<SceneCanvas moduleId="cobertura_isolada"
|
||||||
shadows
|
shadows
|
||||||
|
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [cameraDistance, cameraDistance * 0.8, cameraDistance], fov: 40 }}
|
camera={{ position: [-cameraDistance * 0.8, cameraDistance * 0.8, -cameraDistance], fov: 40 }}
|
||||||
fallback={fallback}
|
fallback={fallback}
|
||||||
>
|
>
|
||||||
<ambientLight intensity={0.7} />
|
<ambientLight intensity={0.7} />
|
||||||
@@ -303,7 +319,19 @@ export default function IsolatedRoof3DViewer({
|
|||||||
cpeLeeward={cpeLeeward}
|
cpeLeeward={cpeLeeward}
|
||||||
cpeTop={cpeTop}
|
cpeTop={cpeTop}
|
||||||
forceKN={forceKN}
|
forceKN={forceKN}
|
||||||
|
viewMode={viewMode}
|
||||||
/>
|
/>
|
||||||
|
{viewMode === 'airflow' && (
|
||||||
|
<IsolatedRoofAirflowSystem
|
||||||
|
type={type}
|
||||||
|
theta={theta}
|
||||||
|
height={height}
|
||||||
|
width={width}
|
||||||
|
depth={depth}
|
||||||
|
cpeWindward={cpeWindward}
|
||||||
|
cpeLeeward={cpeLeeward}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Grid infiniteGrid fadeDistance={cameraDistance * 5} sectionColor="#94a3b8" cellColor="#cbd5e1" position={[0, -0.01, 0]} />
|
<Grid infiniteGrid fadeDistance={cameraDistance * 5} sectionColor="#94a3b8" cellColor="#cbd5e1" position={[0, -0.01, 0]} />
|
||||||
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.05} />
|
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.05} />
|
||||||
<Environment preset="city" />
|
<Environment preset="city" />
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import React, { useRef, useMemo, useEffect } from 'react';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
|
||||||
|
interface Particle {
|
||||||
|
position: THREE.Vector3;
|
||||||
|
baseSpeed: number;
|
||||||
|
wobbleSpeed: number;
|
||||||
|
wobbleOffset: number;
|
||||||
|
currentCpe: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IsolatedRoofAirflowSystemProps {
|
||||||
|
type: 'shed' | 'gable' | 'butterfly';
|
||||||
|
theta: number;
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
depth: number;
|
||||||
|
cpeWindward: number;
|
||||||
|
cpeLeeward: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IsolatedRoofAirflowSystem: React.FC<IsolatedRoofAirflowSystemProps> = ({
|
||||||
|
type,
|
||||||
|
theta,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
cpeWindward,
|
||||||
|
cpeLeeward,
|
||||||
|
}) => {
|
||||||
|
const meshRef = useRef<THREE.InstancedMesh>(null);
|
||||||
|
const theme = useCanvasTheme();
|
||||||
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
|
const COUNT = 100;
|
||||||
|
const dummy = useMemo(() => new THREE.Object3D(), []);
|
||||||
|
const colorObj = useMemo(() => new THREE.Color(), []);
|
||||||
|
|
||||||
|
const thetaRad = (theta * Math.PI) / 180;
|
||||||
|
const halfDepth = depth / 2;
|
||||||
|
const halfWidth = width / 2;
|
||||||
|
const h_diff = depth * Math.tan(thetaRad);
|
||||||
|
const h_half = halfDepth * Math.tan(thetaRad);
|
||||||
|
|
||||||
|
const particles = useMemo(() => {
|
||||||
|
const arr: Particle[] = [];
|
||||||
|
for (let i = 0; i < COUNT; i++) {
|
||||||
|
arr.push({
|
||||||
|
position: new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * width * 3.0,
|
||||||
|
Math.random() * (height + h_diff + 3),
|
||||||
|
-halfDepth - 5 - Math.random() * 20
|
||||||
|
),
|
||||||
|
baseSpeed: 0.075 + Math.random() * 0.05,
|
||||||
|
wobbleSpeed: 0.5 + Math.random() * 1.5,
|
||||||
|
wobbleOffset: Math.random() * Math.PI * 2,
|
||||||
|
currentCpe: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, [width, height, depth, h_diff]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (meshRef.current) {
|
||||||
|
for (let i = 0; i < COUNT; i++) {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
|
||||||
|
meshRef.current.setColorAt(i, colorObj);
|
||||||
|
}
|
||||||
|
meshRef.current.instanceColor!.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}, [isDark, colorObj]);
|
||||||
|
|
||||||
|
useFrame((state) => {
|
||||||
|
if (!meshRef.current) return;
|
||||||
|
const time = state.clock.elapsedTime;
|
||||||
|
|
||||||
|
particles.forEach((p, i) => {
|
||||||
|
const px = p.position.x;
|
||||||
|
const py = p.position.y;
|
||||||
|
const pz = p.position.z;
|
||||||
|
|
||||||
|
let currentSpeed = p.baseSpeed;
|
||||||
|
let localCpe = 0;
|
||||||
|
let deflectY = 0;
|
||||||
|
|
||||||
|
const inX = Math.abs(px) < halfWidth + 1;
|
||||||
|
const inZ = Math.abs(pz) < halfDepth + 2;
|
||||||
|
|
||||||
|
if (inX && inZ) {
|
||||||
|
if (type === 'butterfly') {
|
||||||
|
if (pz < 0) {
|
||||||
|
localCpe = cpeWindward;
|
||||||
|
// Vento acompanha a descida em direção ao vale
|
||||||
|
if (py > height + 0.2) {
|
||||||
|
deflectY -= currentSpeed * 0.3;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
localCpe = cpeLeeward;
|
||||||
|
// Vento bate na segunda água que sobe e é defletido para cima
|
||||||
|
if (py < height + h_half + 2) {
|
||||||
|
deflectY += currentSpeed * 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (pz < 0) {
|
||||||
|
localCpe = cpeWindward;
|
||||||
|
const maxH = type === 'shed' ? height + h_diff : height + h_half;
|
||||||
|
if (py < maxH + 1) {
|
||||||
|
const dist = -pz;
|
||||||
|
const lift = Math.max(0, maxH - py) / (dist + 1);
|
||||||
|
deflectY += lift * currentSpeed * 0.4;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
localCpe = cpeLeeward;
|
||||||
|
if (cpeLeeward < 0) {
|
||||||
|
deflectY -= currentSpeed * 0.15;
|
||||||
|
p.position.y += Math.sin(time * 5 + p.wobbleOffset) * 0.05;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pz > halfDepth && py < height + h_diff) {
|
||||||
|
localCpe = 0.5; // Esteira
|
||||||
|
deflectY -= currentSpeed * 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.currentCpe += (localCpe - p.currentCpe) * 0.1;
|
||||||
|
p.position.z += currentSpeed;
|
||||||
|
p.position.y += deflectY;
|
||||||
|
p.position.y += Math.sin(time * p.wobbleSpeed * 10 + p.wobbleOffset) * 0.01;
|
||||||
|
|
||||||
|
if (p.position.z > halfDepth + 10) {
|
||||||
|
p.position.z = -halfDepth - 10 - Math.random() * 5;
|
||||||
|
p.position.y = Math.random() * (height + h_diff + 3);
|
||||||
|
p.position.x = (Math.random() - 0.5) * width * 3.0;
|
||||||
|
p.currentCpe = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dummy.position.copy(p.position);
|
||||||
|
dummy.rotation.set(0, 0, 0);
|
||||||
|
dummy.rotateX(Math.PI / 2);
|
||||||
|
dummy.scale.set(1, 1 + currentSpeed * 6, 1);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||||
|
|
||||||
|
if (p.currentCpe > 0) {
|
||||||
|
colorObj.set(isDark ? '#f87171' : '#ef4444');
|
||||||
|
} else if (p.currentCpe < 0) {
|
||||||
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
|
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity);
|
||||||
|
} else {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
|
||||||
|
}
|
||||||
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
meshRef.current.instanceMatrix.needsUpdate = true;
|
||||||
|
meshRef.current.instanceColor!.needsUpdate = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<instancedMesh ref={meshRef} args={[undefined, undefined, COUNT]}>
|
||||||
|
<cylinderGeometry args={[0.04, 0.04, 1.0, 4]} />
|
||||||
|
<meshBasicMaterial transparent opacity={0.6} depthWrite={false} />
|
||||||
|
</instancedMesh>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ interface ResizableBox3DProps {
|
|||||||
l1: number;
|
l1: number;
|
||||||
l2: number;
|
l2: number;
|
||||||
h: number;
|
h: number;
|
||||||
|
viewMode?: 'solid' | 'airflow';
|
||||||
}
|
}
|
||||||
|
|
||||||
function WindParticles({ l1, l2, h }: { l1: number, l2: number, h: number }) {
|
function WindParticles({ l1, l2, h }: { l1: number, l2: number, h: number }) {
|
||||||
@@ -79,7 +80,7 @@ function WindParticles({ l1, l2, h }: { l1: number, l2: number, h: number }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ResizableBox3D({ l1, l2, h }: ResizableBox3DProps) {
|
export function ResizableBox3D({ l1, l2, h, viewMode = 'solid' }: ResizableBox3DProps) {
|
||||||
const meshRef = useRef<THREE.Mesh>(null);
|
const meshRef = useRef<THREE.Mesh>(null);
|
||||||
|
|
||||||
// Create target scale and position based on inputs
|
// Create target scale and position based on inputs
|
||||||
@@ -109,12 +110,12 @@ export function ResizableBox3D({ l1, l2, h }: ResizableBox3DProps) {
|
|||||||
{/* The Animated Box */}
|
{/* The Animated Box */}
|
||||||
<mesh ref={meshRef} castShadow receiveShadow>
|
<mesh ref={meshRef} castShadow receiveShadow>
|
||||||
<boxGeometry args={[1, 1, 1]} />
|
<boxGeometry args={[1, 1, 1]} />
|
||||||
<meshStandardMaterial color="#4f46e5" transparent opacity={0.7} roughness={0.2} metalness={0.8} />
|
<meshStandardMaterial color="#4f46e5" transparent opacity={viewMode === 'airflow' ? 0.3 : 0.7} roughness={0.2} metalness={0.8} />
|
||||||
<Edges scale={1} threshold={15} color="#818cf8" />
|
<Edges scale={1} threshold={15} color="#818cf8" />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Wind Flow Animation */}
|
{/* Wind Flow Animation */}
|
||||||
<WindParticles l1={l1} l2={l2} h={h} />
|
{viewMode === 'airflow' && <WindParticles l1={l1} l2={l2} h={h} />}
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import React, { useRef } from 'react';
|
|
||||||
import * as THREE from 'three';
|
|
||||||
import { useFrame } from '@react-three/fiber';
|
|
||||||
import { Text, Grid, Environment } from '@react-three/drei';
|
import { Text, Grid, Environment } from '@react-three/drei';
|
||||||
|
import { WindArrow as GlobalWindArrow } from './WindArrow';
|
||||||
import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls';
|
import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls';
|
||||||
import SceneCanvas from '../SceneCanvas';
|
import SceneCanvas from '../SceneCanvas';
|
||||||
import { useCanvasTheme } from '@/lib/theme';
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
import { ShelterAirflowSystem } from './ShelterAirflowSystem';
|
||||||
import type { ShelterCondition, OpeningPosition, ShelterWindAngle } from '@/lib/nbr-tables/table-shelters';
|
import type { ShelterCondition, OpeningPosition, ShelterWindAngle } from '@/lib/nbr-tables/table-shelters';
|
||||||
|
|
||||||
export interface Shelter3DProps {
|
export interface Shelter3DProps {
|
||||||
@@ -18,44 +17,11 @@ export interface Shelter3DProps {
|
|||||||
rightClosure: number;
|
rightClosure: number;
|
||||||
openingPos: OpeningPosition;
|
openingPos: OpeningPosition;
|
||||||
backRange?: [number, number];
|
backRange?: [number, number];
|
||||||
|
numColumns?: number;
|
||||||
windAngle?: ShelterWindAngle;
|
windAngle?: ShelterWindAngle;
|
||||||
viewMode: '3d' | 'elevation';
|
viewMode: '3d' | 'elevation' | 'airflow';
|
||||||
}
|
}
|
||||||
|
|
||||||
const WindArrow: React.FC<{
|
|
||||||
start: [number, number, number];
|
|
||||||
end: [number, number, number];
|
|
||||||
color?: string;
|
|
||||||
speed?: number;
|
|
||||||
}> = ({ start, end, color = '#38bdf8', speed = 1.5 }) => {
|
|
||||||
const arrowRef = useRef<THREE.Group>(null);
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group ref={arrowRef} quaternion={quaternion}>
|
|
||||||
<mesh>
|
|
||||||
<coneGeometry args={[0.25, 0.6, 8]} />
|
|
||||||
<meshStandardMaterial color={color} emissive={color} emissiveIntensity={0.6} />
|
|
||||||
</mesh>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function ShelterModel({
|
function ShelterModel({
|
||||||
condition: _condition,
|
condition: _condition,
|
||||||
depth,
|
depth,
|
||||||
@@ -67,14 +33,18 @@ function ShelterModel({
|
|||||||
rightClosure,
|
rightClosure,
|
||||||
openingPos,
|
openingPos,
|
||||||
backRange,
|
backRange,
|
||||||
|
numColumns = 2,
|
||||||
windAngle = 0,
|
windAngle = 0,
|
||||||
viewMode,
|
viewMode,
|
||||||
}: Shelter3DProps) {
|
}: Shelter3DProps) {
|
||||||
const isElevation = viewMode === 'elevation';
|
const isElevation = viewMode === 'elevation';
|
||||||
|
const theme = useCanvasTheme();
|
||||||
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
// Geometria básica
|
// Geometria básica
|
||||||
const halfW = width / 2;
|
const halfW = width / 2;
|
||||||
const halfD = depth / 2;
|
const halfD = depth / 2;
|
||||||
|
const isAirflow = viewMode === 'airflow';
|
||||||
const thetaRad = (theta * Math.PI) / 180;
|
const thetaRad = (theta * Math.PI) / 180;
|
||||||
const slopeRise = depth * Math.tan(thetaRad);
|
const slopeRise = depth * Math.tan(thetaRad);
|
||||||
|
|
||||||
@@ -123,10 +93,17 @@ function ShelterModel({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const overhang = 0.6;
|
||||||
|
const spanWidth = Math.max(0.1, width - 2 * overhang);
|
||||||
|
const spacing = spanWidth / Math.max(1, numColumns - 1);
|
||||||
|
const columnPositions = Array.from({ length: numColumns }).map(
|
||||||
|
(_, i) => -halfW + overhang + i * spacing
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group position={[0, -height / 2, 0]}>
|
<group position={[0, -height / 2, 0]}>
|
||||||
{/* 1. Estrutura Metálica Principal (Pilares e Vigas em Balanço - Tubos Quadrados) */}
|
{/* 1. Estrutura Metálica Principal (Pilares e Vigas em Balanço - Tubos Quadrados) */}
|
||||||
{[-halfW + 0.6, halfW - 0.6].map((xPos, idx) => (
|
{columnPositions.map((xPos, idx) => (
|
||||||
<group key={`frame-${idx}`} position={[xPos, 0, 0]}>
|
<group key={`frame-${idx}`} position={[xPos, 0, 0]}>
|
||||||
{/* Pilar traseiro (lado direito na elevação) */}
|
{/* Pilar traseiro (lado direito na elevação) */}
|
||||||
<mesh position={[0, height / 2, halfD - 0.25]}>
|
<mesh position={[0, height / 2, halfD - 0.25]}>
|
||||||
@@ -161,50 +138,85 @@ function ShelterModel({
|
|||||||
{/* Parede Lateral Direita (X = +halfW) */}
|
{/* Parede Lateral Direita (X = +halfW) */}
|
||||||
{renderWallWithGap(depth, height, rightClosure, 'distributed', Math.PI / 2, [halfW, 0, 0])}
|
{renderWallWithGap(depth, height, rightClosure, 'distributed', Math.PI / 2, [halfW, 0, 0])}
|
||||||
|
|
||||||
{/* 4. Linhas de Corrente e Vetores de Vento (Ilustrativo e Didático) */}
|
{/* Setas de Vento Global (0°, 45°, 90°) */}
|
||||||
<group position={[0, 0, 0]} rotation={[0, -((windAngle || 0) * Math.PI) / 180, 0]}>
|
{[
|
||||||
{/* Vento de entrada (Barlavento) entrando 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} />
|
angle: 0,
|
||||||
<WindArrow start={[-halfW * 0.5, height * 0.7, -halfD - 4]} end={[-halfW * 0.5, height * 0.7, -halfD + 1]} color="#38bdf8" speed={1.4} />
|
label: 'Vento 0°',
|
||||||
<WindArrow start={[halfW * 0.5, height * 0.4, -halfD - 4]} end={[halfW * 0.5, height * 0.4, -halfD + 1]} color="#38bdf8" speed={1.3} />
|
target: [0, height * 1.5 - 1.5, -halfD - 4] as [number, number, number],
|
||||||
|
direction: [0, 0, 1] as [number, number, number],
|
||||||
{/* Linhas de corrente sobre o telhado */}
|
textPos: [0, height * 1.5, -halfD - 6] as [number, number, number],
|
||||||
<WindArrow start={[0, height + 1, -halfD - 3]} end={[0, height + slopeRise + 1.5, halfD + 2]} color="#60a5fa" speed={1.6} />
|
textRot: [0, Math.PI, 0] as [number, number, number],
|
||||||
|
},
|
||||||
{/* 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)) && (
|
angle: 45,
|
||||||
<>
|
label: 'Vento 45°',
|
||||||
<WindArrow start={[0, height * 0.85, 0]} end={[0, height * 0.95, halfD + 2.5]} color="#34d399" speed={1.8} />
|
target: [-halfW - 3, height * 1.5 - 1.5, -halfD - 3] as [number, number, number],
|
||||||
<Text position={[0, height + 0.35, halfD]} rotation={[0, Math.PI, 0]} fontSize={0.4} color="#34d399">
|
direction: [0.707, 0, 0.707] as [number, number, number],
|
||||||
Alívio (Fresta Topo)
|
textPos: [-halfW - 5, height * 1.5, -halfD - 5] as [number, number, number],
|
||||||
|
textRot: [0, Math.PI * 0.75, 0] as [number, number, number],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
angle: 90,
|
||||||
|
label: 'Vento 90°',
|
||||||
|
target: [-halfW - 4, height * 1.5 - 1.5, 0] as [number, number, number],
|
||||||
|
direction: [1, 0, 0] as [number, number, number],
|
||||||
|
textPos: [-halfW - 6, height * 1.5, 0] as [number, number, number],
|
||||||
|
textRot: [0, Math.PI / 2, 0] as [number, number, number],
|
||||||
|
}
|
||||||
|
].map((arrow) => {
|
||||||
|
const isActive = windAngle === arrow.angle;
|
||||||
|
const arrowColor = isActive ? (isDark ? '#60a5fa' : '#2563eb') : (isDark ? '#475569' : '#94a3b8');
|
||||||
|
return (
|
||||||
|
<group key={`wind-${arrow.angle}`}>
|
||||||
|
<GlobalWindArrow
|
||||||
|
target={arrow.target}
|
||||||
|
direction={arrow.direction}
|
||||||
|
scale={isActive ? 3.2 : 2.0}
|
||||||
|
color={arrowColor}
|
||||||
|
opacity={isActive ? 1 : 0.3}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
position={arrow.textPos}
|
||||||
|
rotation={arrow.textRot}
|
||||||
|
fontSize={isActive ? 0.96 : 0.75}
|
||||||
|
color={arrowColor}
|
||||||
|
fillOpacity={isActive ? 1 : 0.5}
|
||||||
|
anchorX="center"
|
||||||
|
anchorY="bottom"
|
||||||
|
>
|
||||||
|
{arrow.label}
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</group>
|
||||||
)}
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{/* Fresta na base */}
|
{/* Partículas de Ar */}
|
||||||
{backClosure > 0 && backClosure < 100 && (openingPos === 'bottom' || (backRange && backRange[0] > 2)) && (
|
{isAirflow && (
|
||||||
<>
|
<group>
|
||||||
<WindArrow start={[0, 0.4, 0]} end={[0, 0.4, halfD + 2.5]} color="#facc15" speed={1.8} />
|
<ShelterAirflowSystem
|
||||||
<Text position={[0, 1.2, halfD]} rotation={[0, Math.PI, 0]} fontSize={0.4} color="#facc15">
|
width={width}
|
||||||
Vão Livre Inferior
|
depth={depth}
|
||||||
</Text>
|
height={height}
|
||||||
</>
|
theta={theta}
|
||||||
)}
|
/>
|
||||||
</group>
|
</group>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Textos Informativos 3D - Virados para quem olha da frente (-Z) */}
|
{/* 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">
|
{!isAirflow && (
|
||||||
{isElevation ? 'CORTE A-A (Elevação)' : 'Abrigo / Pórtico (NBR 6123)'}
|
<>
|
||||||
</Text>
|
<Text position={[0, height + slopeRise + 1.4, 0]} rotation={[0, Math.PI, 0]} fontSize={0.45} color={isDark ? '#f8fafc' : '#0f172a'}>
|
||||||
<Text position={[0, 0.3, -halfD - 1.2]} rotation={[0, Math.PI, 0]} fontSize={0.35} color="#38bdf8">
|
{isElevation ? 'CORTE A-A (Elevação)' : 'Abrigo / Pórtico (NBR 6123)'}
|
||||||
{`VENTO ${windAngle === 0 ? 'FRONTAL (0°)' : windAngle === 45 ? 'OBLÍQUO (45°)' : 'LATERAL (90°)'}`}
|
</Text>
|
||||||
</Text>
|
<Text position={[0, height + slopeRise + 0.6, 0]} rotation={[0, Math.PI, 0]} fontSize={0.51} color={isDark ? '#ff4d6d' : '#b91c1c'}>
|
||||||
<Text position={[0, height + slopeRise + 0.25, 0]} rotation={[0, Math.PI, 0]} fontSize={0.32} color="#f43f5e">
|
Arrancamento [+Fz ↑]
|
||||||
Arrancamento [+Fz ↑]
|
</Text>
|
||||||
</Text>
|
<Text position={[0, height * 0.5, halfD + 1.2]} rotation={[0, Math.PI, 0]} fontSize={0.51} color={isDark ? '#ff9f43' : '#c2410c'}>
|
||||||
<Text position={[0, height * 0.5, halfD + 1.2]} rotation={[0, Math.PI, 0]} fontSize={0.32} color="#fb923c">
|
Empuxo no Fundo [+Fx →]
|
||||||
Empuxo no Fundo [+Fx →]
|
</Text>
|
||||||
</Text>
|
</>
|
||||||
|
)}
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -212,13 +224,14 @@ function ShelterModel({
|
|||||||
export function Shelter3D(props: Shelter3DProps) {
|
export function Shelter3D(props: Shelter3DProps) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
const { width, depth, height } = props;
|
const { width, depth, height, viewMode } = props;
|
||||||
const dist = Math.max(width, depth) * 1.8 + 8;
|
const dist = Math.max(width, depth) * 1.8 + 8;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SceneCanvas
|
<SceneCanvas
|
||||||
moduleId="shelter"
|
moduleId="shelter"
|
||||||
shadows
|
shadows
|
||||||
|
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [dist * 0.75, height * 1.1, -dist * 0.85], fov: 45 }}
|
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>}
|
fallback={<div className="flex items-center justify-center h-full text-muted-foreground">3D indisponível</div>}
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import React, { useRef, useMemo, useEffect } from 'react';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
|
||||||
|
interface Particle {
|
||||||
|
position: THREE.Vector3;
|
||||||
|
baseSpeed: number;
|
||||||
|
wobbleSpeed: number;
|
||||||
|
wobbleOffset: number;
|
||||||
|
currentCpe: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShelterAirflowSystemProps {
|
||||||
|
width: number;
|
||||||
|
depth: number;
|
||||||
|
height: number;
|
||||||
|
theta: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShelterAirflowSystem: React.FC<ShelterAirflowSystemProps> = ({
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
height,
|
||||||
|
theta,
|
||||||
|
}) => {
|
||||||
|
const meshRef = useRef<THREE.InstancedMesh>(null);
|
||||||
|
const theme = useCanvasTheme();
|
||||||
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
|
const COUNT = 100;
|
||||||
|
const dummy = useMemo(() => new THREE.Object3D(), []);
|
||||||
|
const colorObj = useMemo(() => new THREE.Color(), []);
|
||||||
|
|
||||||
|
const halfWidth = width / 2;
|
||||||
|
const halfDepth = depth / 2;
|
||||||
|
const thetaRad = (theta * Math.PI) / 180;
|
||||||
|
const h_diff = depth * Math.tan(thetaRad);
|
||||||
|
|
||||||
|
const particles = useMemo(() => {
|
||||||
|
const arr: Particle[] = [];
|
||||||
|
for (let i = 0; i < COUNT; i++) {
|
||||||
|
arr.push({
|
||||||
|
position: new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * width * 3,
|
||||||
|
Math.random() * (height + h_diff + 5),
|
||||||
|
-halfDepth - 10 - Math.random() * 20
|
||||||
|
),
|
||||||
|
baseSpeed: 0.075 + Math.random() * 0.05,
|
||||||
|
wobbleSpeed: 0.5 + Math.random() * 1.5,
|
||||||
|
wobbleOffset: Math.random() * Math.PI * 2,
|
||||||
|
currentCpe: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, [width, depth, height, h_diff]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (meshRef.current) {
|
||||||
|
for (let i = 0; i < COUNT; i++) {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
|
||||||
|
meshRef.current.setColorAt(i, colorObj);
|
||||||
|
}
|
||||||
|
meshRef.current.instanceColor!.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}, [isDark, colorObj]);
|
||||||
|
|
||||||
|
useFrame((state) => {
|
||||||
|
if (!meshRef.current) return;
|
||||||
|
const time = state.clock.elapsedTime;
|
||||||
|
|
||||||
|
particles.forEach((p, i) => {
|
||||||
|
const px = p.position.x;
|
||||||
|
const py = p.position.y;
|
||||||
|
const pz = p.position.z;
|
||||||
|
|
||||||
|
let currentSpeed = p.baseSpeed;
|
||||||
|
let localCpe = 0;
|
||||||
|
let deflectY = 0;
|
||||||
|
|
||||||
|
const inX = Math.abs(px) < halfWidth + 1;
|
||||||
|
const inZ = Math.abs(pz) < halfDepth + 1;
|
||||||
|
|
||||||
|
if (inX && inZ) {
|
||||||
|
localCpe = 0.5;
|
||||||
|
// Teto inclinado deflete o vento
|
||||||
|
if (py < height + h_diff + 1 && py > height - 1) {
|
||||||
|
const dist = -pz;
|
||||||
|
const lift = Math.max(0, height + h_diff - py) / (dist + 1);
|
||||||
|
deflectY += lift * currentSpeed * 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.currentCpe += (localCpe - p.currentCpe) * 0.1;
|
||||||
|
p.position.z += currentSpeed;
|
||||||
|
p.position.y += deflectY;
|
||||||
|
p.position.y += Math.sin(time * p.wobbleSpeed * 10 + p.wobbleOffset) * 0.01;
|
||||||
|
|
||||||
|
if (p.position.z > halfDepth + 10) {
|
||||||
|
p.position.z = -halfDepth - 10 - Math.random() * 5;
|
||||||
|
p.position.y = Math.random() * (height + h_diff + 5);
|
||||||
|
p.position.x = (Math.random() - 0.5) * width * 3;
|
||||||
|
p.currentCpe = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dummy.position.copy(p.position);
|
||||||
|
dummy.rotation.set(0, 0, 0);
|
||||||
|
dummy.rotateX(Math.PI / 2);
|
||||||
|
dummy.scale.set(1, 1 + currentSpeed * 6, 1);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||||
|
|
||||||
|
if (p.currentCpe > 0) {
|
||||||
|
colorObj.set(isDark ? '#f87171' : '#ef4444');
|
||||||
|
} else if (p.currentCpe < 0) {
|
||||||
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
|
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity);
|
||||||
|
} else {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
|
||||||
|
}
|
||||||
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
meshRef.current.instanceMatrix.needsUpdate = true;
|
||||||
|
meshRef.current.instanceColor!.needsUpdate = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<instancedMesh ref={meshRef} args={[undefined, undefined, COUNT]}>
|
||||||
|
<cylinderGeometry args={[0.04, 0.04, 1.0, 4]} />
|
||||||
|
<meshBasicMaterial transparent opacity={0.6} depthWrite={false} />
|
||||||
|
</instancedMesh>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@ import SceneCanvas from '../SceneCanvas';
|
|||||||
import FallbackDiagram from '../FallbackDiagram';
|
import FallbackDiagram from '../FallbackDiagram';
|
||||||
import { WindArrow } from './WindArrow';
|
import { WindArrow } from './WindArrow';
|
||||||
import { useCanvasTheme } from '@/lib/theme';
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
import { SignAirflowSystem } from './SignAirflowSystem';
|
||||||
|
|
||||||
export interface Sign3DInput {
|
export interface Sign3DInput {
|
||||||
/** Comprimento ℓ (m) */
|
/** Comprimento ℓ (m) */
|
||||||
@@ -22,6 +23,7 @@ export interface Sign3DInput {
|
|||||||
forceKN: number;
|
forceKN: number;
|
||||||
/** Excentricidade e (m) */
|
/** Excentricidade e (m) */
|
||||||
applicationPoint: number;
|
applicationPoint: number;
|
||||||
|
viewMode?: 'solid' | 'airflow';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,9 +37,11 @@ function SignModel({
|
|||||||
alpha,
|
alpha,
|
||||||
cf,
|
cf,
|
||||||
applicationPoint,
|
applicationPoint,
|
||||||
|
viewMode = 'solid',
|
||||||
}: Sign3DInput) {
|
}: Sign3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
|
const isAirflow = viewMode === 'airflow';
|
||||||
const baseY = groundClearance;
|
const baseY = groundClearance;
|
||||||
const topY = baseY + height;
|
const topY = baseY + height;
|
||||||
const halfL = length / 2;
|
const halfL = length / 2;
|
||||||
@@ -47,11 +51,13 @@ function SignModel({
|
|||||||
const angleToNormalRad = ((90 - alpha) * Math.PI) / 180;
|
const angleToNormalRad = ((90 - alpha) * Math.PI) / 180;
|
||||||
const arrowDir = useMemo(() => new THREE.Vector3(Math.cos(angleToNormalRad), 0, Math.sin(angleToNormalRad)), [angleToNormalRad]);
|
const arrowDir = useMemo(() => new THREE.Vector3(Math.cos(angleToNormalRad), 0, Math.sin(angleToNormalRad)), [angleToNormalRad]);
|
||||||
|
|
||||||
// Posição da seta no plano da placa (inicia no ponto de aplicação com a excentricidade ao longo de Z)
|
// Posição alvo da seta no plano da placa (inicia no ponto de aplicação com a excentricidade ao longo de Z)
|
||||||
const arrowStart = useMemo(
|
// Afastada um pouco do muro para não encostar fisicamente
|
||||||
() => new THREE.Vector3(0, baseY + height / 2, applicationPoint),
|
const arrowStart = useMemo(() => {
|
||||||
[applicationPoint, height, baseY],
|
const targetPos = new THREE.Vector3(0, baseY + height / 2, applicationPoint);
|
||||||
);
|
const offset = arrowDir.clone().multiplyScalar(-0.8);
|
||||||
|
return targetPos.add(offset);
|
||||||
|
}, [applicationPoint, height, baseY, arrowDir]);
|
||||||
|
|
||||||
// Cor da placa: Azul (Pressão/Empuxo) com intensidade proporcional ao Cf
|
// Cor da placa: Azul (Pressão/Empuxo) com intensidade proporcional ao Cf
|
||||||
const plateColor = useMemo(() => {
|
const plateColor = useMemo(() => {
|
||||||
@@ -67,7 +73,7 @@ function SignModel({
|
|||||||
{/* Placa principal */}
|
{/* Placa principal */}
|
||||||
<mesh position={[0, (baseY + topY) / 2, 0]} castShadow receiveShadow>
|
<mesh position={[0, (baseY + topY) / 2, 0]} castShadow receiveShadow>
|
||||||
<boxGeometry args={[0.1, height, length]} />
|
<boxGeometry args={[0.1, height, length]} />
|
||||||
<meshStandardMaterial color={plateColor} opacity={0.8} transparent roughness={0.4} side={THREE.DoubleSide} />
|
<meshStandardMaterial color={plateColor} opacity={isAirflow ? 0.3 : 0.8} transparent roughness={0.4} side={THREE.DoubleSide} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Placas de extremidade (retornos aerodinâmicos nas pontas) */}
|
{/* Placas de extremidade (retornos aerodinâmicos nas pontas) */}
|
||||||
@@ -75,11 +81,11 @@ function SignModel({
|
|||||||
<>
|
<>
|
||||||
<mesh position={[0, (baseY + topY) / 2, halfL]} castShadow>
|
<mesh position={[0, (baseY + topY) / 2, halfL]} castShadow>
|
||||||
<boxGeometry args={[0.3, height * 0.95, 0.04]} />
|
<boxGeometry args={[0.3, height * 0.95, 0.04]} />
|
||||||
<meshStandardMaterial color={isDark ? "#475569" : "#64748b"} opacity={0.6} transparent side={THREE.DoubleSide} />
|
<meshStandardMaterial color={isDark ? "#475569" : "#64748b"} opacity={isAirflow ? 0.2 : 0.6} transparent side={THREE.DoubleSide} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<mesh position={[0, (baseY + topY) / 2, -halfL]} castShadow>
|
<mesh position={[0, (baseY + topY) / 2, -halfL]} castShadow>
|
||||||
<boxGeometry args={[0.3, height * 0.95, 0.04]} />
|
<boxGeometry args={[0.3, height * 0.95, 0.04]} />
|
||||||
<meshStandardMaterial color={isDark ? "#475569" : "#64748b"} opacity={0.6} transparent side={THREE.DoubleSide} />
|
<meshStandardMaterial color={isDark ? "#475569" : "#64748b"} opacity={isAirflow ? 0.2 : 0.6} transparent side={THREE.DoubleSide} />
|
||||||
</mesh>
|
</mesh>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -118,6 +124,7 @@ function SignModel({
|
|||||||
<WindArrow
|
<WindArrow
|
||||||
target={arrowStart}
|
target={arrowStart}
|
||||||
direction={arrowDir}
|
direction={arrowDir}
|
||||||
|
scale={1.8}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* === LINHAS DE COTA (CAD-Style Dimensions) === */}
|
{/* === LINHAS DE COTA (CAD-Style Dimensions) === */}
|
||||||
@@ -206,6 +213,7 @@ export default function Sign3DViewer({
|
|||||||
cf,
|
cf,
|
||||||
forceKN,
|
forceKN,
|
||||||
applicationPoint,
|
applicationPoint,
|
||||||
|
viewMode = 'solid',
|
||||||
}: Sign3DInput) {
|
}: Sign3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
@@ -222,6 +230,7 @@ export default function Sign3DViewer({
|
|||||||
return (
|
return (
|
||||||
<SceneCanvas moduleId="muro_placa"
|
<SceneCanvas moduleId="muro_placa"
|
||||||
shadows
|
shadows
|
||||||
|
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [length * 1.3, height * 1.5, length * 1.3], fov: 40 }}
|
camera={{ position: [length * 1.3, height * 1.5, length * 1.3], fov: 40 }}
|
||||||
fallback={fallback}
|
fallback={fallback}
|
||||||
@@ -236,7 +245,17 @@ export default function Sign3DViewer({
|
|||||||
cf={cf}
|
cf={cf}
|
||||||
forceKN={forceKN}
|
forceKN={forceKN}
|
||||||
applicationPoint={applicationPoint}
|
applicationPoint={applicationPoint}
|
||||||
|
viewMode={viewMode}
|
||||||
/>
|
/>
|
||||||
|
{viewMode === 'airflow' && (
|
||||||
|
<SignAirflowSystem
|
||||||
|
length={length}
|
||||||
|
height={height}
|
||||||
|
groundClearance={groundClearance}
|
||||||
|
alpha={alpha}
|
||||||
|
cf={cf}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Grid
|
<Grid
|
||||||
infiniteGrid
|
infiniteGrid
|
||||||
fadeDistance={maxDim * 5}
|
fadeDistance={maxDim * 5}
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import { useRef, useMemo, useEffect } from 'react';
|
||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { useCanvasTheme } from '../../lib/theme';
|
||||||
|
|
||||||
|
export interface SignAirflowSystemProps {
|
||||||
|
length: number;
|
||||||
|
height: number;
|
||||||
|
groundClearance: number;
|
||||||
|
alpha: 0 | 50 | 90;
|
||||||
|
cf: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SignAirflowSystem({ length, height, groundClearance, alpha, cf }: SignAirflowSystemProps) {
|
||||||
|
const isDark = useCanvasTheme() === 'dark';
|
||||||
|
const count = 175;
|
||||||
|
const meshRef = useRef<THREE.InstancedMesh>(null);
|
||||||
|
const baseY = groundClearance;
|
||||||
|
const topY = baseY + height;
|
||||||
|
const halfL = length / 2;
|
||||||
|
|
||||||
|
const rad = ((90 - alpha) * Math.PI) / 180;
|
||||||
|
const windDir = useMemo(() => new THREE.Vector3(Math.cos(rad), 0, Math.sin(rad)).normalize(), [rad]);
|
||||||
|
|
||||||
|
const particles = useMemo(() => {
|
||||||
|
const temp = [];
|
||||||
|
const limitX = Math.max(15, length * 1.5);
|
||||||
|
const limitZ = Math.max(15, length * 1.5);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
// Spawn no plano do vento
|
||||||
|
temp.push({
|
||||||
|
position: new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * limitX * 2,
|
||||||
|
Math.random() * (topY * 1.6),
|
||||||
|
(Math.random() - 0.5) * limitZ * 2
|
||||||
|
),
|
||||||
|
baseSpeed: 0.06 + Math.random() * 0.04,
|
||||||
|
wobbleSpeed: Math.random() * 0.05,
|
||||||
|
wobbleOffset: Math.random() * Math.PI * 2,
|
||||||
|
currentCpe: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return temp;
|
||||||
|
}, [count, length, topY]);
|
||||||
|
|
||||||
|
const dummy = useMemo(() => new THREE.Object3D(), []);
|
||||||
|
const colorObj = useMemo(() => new THREE.Color(), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (meshRef.current) {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
meshRef.current.setColorAt(i, new THREE.Color('#ffffff'));
|
||||||
|
}
|
||||||
|
if (meshRef.current.instanceColor) {
|
||||||
|
meshRef.current.instanceColor.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [count]);
|
||||||
|
|
||||||
|
useFrame((state) => {
|
||||||
|
if (!meshRef.current) return;
|
||||||
|
const time = state.clock.elapsedTime;
|
||||||
|
const limitX = Math.max(15, length * 1.5);
|
||||||
|
const limitZ = Math.max(15, length * 1.5);
|
||||||
|
|
||||||
|
particles.forEach((p, i) => {
|
||||||
|
let deflectX = windDir.x;
|
||||||
|
let deflectY = 0.0;
|
||||||
|
let deflectZ = windDir.z;
|
||||||
|
|
||||||
|
const px = p.position.x;
|
||||||
|
const py = p.position.y;
|
||||||
|
const pz = p.position.z;
|
||||||
|
|
||||||
|
// Verifica se a partícula está prestes a colidir com a placa (X=0)
|
||||||
|
const isInsideY = py >= baseY && py <= topY;
|
||||||
|
const isInsideZ = pz >= -halfL && pz <= halfL;
|
||||||
|
|
||||||
|
if (Math.abs(px) < 1.8 && isInsideY && isInsideZ) {
|
||||||
|
// Distância e fator de proximidade
|
||||||
|
const dist = Math.abs(px);
|
||||||
|
const force = Math.max(0, 1.8 - dist); // maior quanto mais perto de X = 0
|
||||||
|
|
||||||
|
// Se está vindo a barlavento, desvia para as bordas
|
||||||
|
if (px < 0) {
|
||||||
|
// Desvio em Y (por cima do topo ou por baixo do solo)
|
||||||
|
const distToTop = Math.abs(py - topY);
|
||||||
|
const distToBottom = Math.abs(py - baseY);
|
||||||
|
if (distToTop < distToBottom || baseY < 0.3) {
|
||||||
|
deflectY += force * 0.25;
|
||||||
|
} else {
|
||||||
|
deflectY -= force * 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Desvio em Z (pelas laterais da placa)
|
||||||
|
if (pz > 0) {
|
||||||
|
deflectZ += force * 0.3;
|
||||||
|
} else {
|
||||||
|
deflectZ -= force * 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduz a velocidade frontal (X) para simular desaceleração do ponto de estagnação
|
||||||
|
deflectX *= Math.max(0.2, 1 - force * 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colisão dura para não atravessar fisicamente a placa a X=0
|
||||||
|
if (Math.abs(px) < 0.15) {
|
||||||
|
p.position.x = px < 0 ? -0.15 : 0.15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zona de esteira turbulenta a sotavento (atrás do muro X > 0)
|
||||||
|
if (px > 0.1 && px < length && isInsideZ) {
|
||||||
|
// Redução de velocidade na esteira
|
||||||
|
deflectX *= 0.6;
|
||||||
|
// Turbulência oscilatória
|
||||||
|
deflectZ += Math.sin(time * 6 + px * 0.8 + p.wobbleOffset) * 0.15;
|
||||||
|
deflectY += Math.cos(time * 4 + px * 0.8 + p.wobbleOffset) * 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Definir cor das partículas (Cpe/Cf equivalente para coloração)
|
||||||
|
if (Math.abs(px) < length && isInsideZ && py < topY + 1.0) {
|
||||||
|
if (px < 0) {
|
||||||
|
p.currentCpe = cf * 0.6; // Pressão positiva
|
||||||
|
} else {
|
||||||
|
p.currentCpe = -cf * 0.6; // Sucção
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.currentCpe = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveVec = new THREE.Vector3(deflectX, deflectY, deflectZ).normalize();
|
||||||
|
const currentSpeed = p.baseSpeed * (px > 0 && px < length ? 0.6 : 1.0);
|
||||||
|
|
||||||
|
p.position.addScaledVector(moveVec, currentSpeed);
|
||||||
|
|
||||||
|
// Reposição quando sai dos limites
|
||||||
|
// Como o vento tem direção inclinada, reposicionamos quando sai de um cilindro ou caixa
|
||||||
|
if (p.position.x > limitX || p.position.z > limitZ) {
|
||||||
|
// Reinicia a barlavento extremo
|
||||||
|
p.position.x = -limitX;
|
||||||
|
p.position.z = (Math.random() - 0.5) * limitZ * 2;
|
||||||
|
p.position.y = Math.random() * (topY * 1.6);
|
||||||
|
p.currentCpe = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dummy.position.copy(p.position);
|
||||||
|
dummy.lookAt(p.position.clone().add(moveVec));
|
||||||
|
dummy.rotateX(Math.PI / 2);
|
||||||
|
dummy.scale.set(1, 1 + currentSpeed * 6, 1);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||||
|
|
||||||
|
if (p.currentCpe > 0) {
|
||||||
|
colorObj.set(isDark ? '#f87171' : '#ef4444'); // Vermelho (Turbulento/Pressão)
|
||||||
|
} else if (p.currentCpe < 0) {
|
||||||
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
|
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção)
|
||||||
|
} else {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre)
|
||||||
|
}
|
||||||
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
meshRef.current.instanceMatrix.needsUpdate = true;
|
||||||
|
if (meshRef.current.instanceColor) {
|
||||||
|
meshRef.current.instanceColor.needsUpdate = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
|
||||||
|
<cylinderGeometry args={[0.01, 0.01, 1.5, 4]} />
|
||||||
|
<meshBasicMaterial
|
||||||
|
color="#ffffff"
|
||||||
|
transparent
|
||||||
|
opacity={isDark ? 0.6 : 0.8}
|
||||||
|
blending={isDark ? THREE.AdditiveBlending : THREE.NormalBlending}
|
||||||
|
depthWrite={false}
|
||||||
|
/>
|
||||||
|
</instancedMesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import * as THREE from 'three';
|
|||||||
import SceneCanvas from '../SceneCanvas';
|
import SceneCanvas from '../SceneCanvas';
|
||||||
import { WindArrow } from './WindArrow';
|
import { WindArrow } from './WindArrow';
|
||||||
import FallbackDiagram from '../FallbackDiagram';
|
import FallbackDiagram from '../FallbackDiagram';
|
||||||
|
import { TowerAirflowSystem } from './TowerAirflowSystem';
|
||||||
|
|
||||||
export interface Tower3DInput {
|
export interface Tower3DInput {
|
||||||
/** Forma da seção */
|
/** Forma da seção */
|
||||||
@@ -23,6 +24,8 @@ export interface Tower3DInput {
|
|||||||
alphaWind: 0 | 45 | 90;
|
alphaWind: 0 | 45 | 90;
|
||||||
/** Força total estimada na torre (kN) */
|
/** Força total estimada na torre (kN) */
|
||||||
forceKN: number;
|
forceKN: number;
|
||||||
|
/** Modo de visualização */
|
||||||
|
viewMode?: 'solid' | 'airflow';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,9 +139,11 @@ function TowerModel({
|
|||||||
phi,
|
phi,
|
||||||
alphaWind,
|
alphaWind,
|
||||||
forceKN,
|
forceKN,
|
||||||
|
viewMode = 'solid',
|
||||||
}: Tower3DInput) {
|
}: Tower3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
|
const isAirflow = viewMode === 'airflow';
|
||||||
|
|
||||||
const geometry = useMemo(
|
const geometry = useMemo(
|
||||||
() => buildTowerGeometry(section, panels, baseWidth, height),
|
() => buildTowerGeometry(section, panels, baseWidth, height),
|
||||||
@@ -201,11 +206,32 @@ function TowerModel({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Seta de Vento única atingindo o meio da torre */}
|
{/* Seta de Vento */}
|
||||||
<WindArrow
|
{isAirflow ? (
|
||||||
target={[0, height / 2, 0]}
|
<group>
|
||||||
direction={forceDir}
|
<WindArrow
|
||||||
/>
|
target={[0, height / 2, -baseWidth * 1.5]}
|
||||||
|
direction={[0, 0, 1]}
|
||||||
|
scale={4}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
position={[0, height / 2 + 1.5, -baseWidth * 1.5 - 2]}
|
||||||
|
rotation={[0, Math.PI, 0]}
|
||||||
|
fontSize={1.2}
|
||||||
|
color={isDark ? '#60a5fa' : '#3b82f6'}
|
||||||
|
anchorX="center"
|
||||||
|
anchorY="bottom"
|
||||||
|
>
|
||||||
|
Vento
|
||||||
|
</Text>
|
||||||
|
<TowerAirflowSystem baseWidth={baseWidth} height={height} phi={phi} />
|
||||||
|
</group>
|
||||||
|
) : (
|
||||||
|
<WindArrow
|
||||||
|
target={[0, height / 2, 0]}
|
||||||
|
direction={forceDir}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Labels indicativos */}
|
{/* Labels indicativos */}
|
||||||
<mesh position={[baseWidth / 2 + 0.5, 0.5, 0]}>
|
<mesh position={[baseWidth / 2 + 0.5, 0.5, 0]}>
|
||||||
@@ -214,6 +240,7 @@ function TowerModel({
|
|||||||
</mesh>
|
</mesh>
|
||||||
<Text
|
<Text
|
||||||
position={[0, height + 1.0, 0]}
|
position={[0, height + 1.0, 0]}
|
||||||
|
rotation={[0, Math.PI, 0]}
|
||||||
fontSize={0.5}
|
fontSize={0.5}
|
||||||
color={isDark ? "#93c5fd" : "#1e40af"}
|
color={isDark ? "#93c5fd" : "#1e40af"}
|
||||||
anchorX="center"
|
anchorX="center"
|
||||||
@@ -228,7 +255,7 @@ function TowerModel({
|
|||||||
export default function Tower3DViewer(input: Tower3DInput) {
|
export default function Tower3DViewer(input: Tower3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
const { baseWidth, height } = input;
|
const { baseWidth, height, viewMode = 'solid' } = input;
|
||||||
const dist = Math.max(baseWidth * 3, height * 1.2);
|
const dist = Math.max(baseWidth * 3, height * 1.2);
|
||||||
|
|
||||||
const fallback = (
|
const fallback = (
|
||||||
@@ -241,13 +268,14 @@ export default function Tower3DViewer(input: Tower3DInput) {
|
|||||||
return (
|
return (
|
||||||
<SceneCanvas moduleId="torre"
|
<SceneCanvas moduleId="torre"
|
||||||
shadows
|
shadows
|
||||||
|
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [dist, height * 0.6, dist], fov: 45 }}
|
camera={{ position: [dist, height * 0.6, dist], fov: 45 }}
|
||||||
fallback={fallback}
|
fallback={fallback}
|
||||||
>
|
>
|
||||||
<ambientLight intensity={0.6} />
|
<ambientLight intensity={0.6} />
|
||||||
<directionalLight position={[dist, height, dist]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
|
<directionalLight position={[dist, height, dist]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
|
||||||
<TowerModel {...input} />
|
<TowerModel {...input} viewMode={viewMode} />
|
||||||
<Grid
|
<Grid
|
||||||
infiniteGrid
|
infiniteGrid
|
||||||
fadeDistance={height * 2}
|
fadeDistance={height * 2}
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import React, { useRef, useMemo, useEffect } from 'react';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
|
||||||
|
interface Particle {
|
||||||
|
position: THREE.Vector3;
|
||||||
|
baseSpeed: number;
|
||||||
|
wobbleSpeed: number;
|
||||||
|
wobbleOffset: number;
|
||||||
|
currentCpe: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TowerAirflowSystemProps {
|
||||||
|
baseWidth: number;
|
||||||
|
height: number;
|
||||||
|
phi: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TowerAirflowSystem: React.FC<TowerAirflowSystemProps> = ({
|
||||||
|
baseWidth,
|
||||||
|
height,
|
||||||
|
phi,
|
||||||
|
}) => {
|
||||||
|
const meshRef = useRef<THREE.InstancedMesh>(null);
|
||||||
|
const theme = useCanvasTheme();
|
||||||
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
|
const COUNT = 100;
|
||||||
|
const dummy = useMemo(() => new THREE.Object3D(), []);
|
||||||
|
const colorObj = useMemo(() => new THREE.Color(), []);
|
||||||
|
|
||||||
|
const halfWidth = baseWidth / 2;
|
||||||
|
|
||||||
|
const particles = useMemo(() => {
|
||||||
|
const arr: Particle[] = [];
|
||||||
|
for (let i = 0; i < COUNT; i++) {
|
||||||
|
arr.push({
|
||||||
|
position: new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * baseWidth * 3,
|
||||||
|
Math.random() * (height + 5),
|
||||||
|
-halfWidth - 10 - Math.random() * 20
|
||||||
|
),
|
||||||
|
baseSpeed: 0.075 + Math.random() * 0.05,
|
||||||
|
wobbleSpeed: 0.5 + Math.random() * 1.5,
|
||||||
|
wobbleOffset: Math.random() * Math.PI * 2,
|
||||||
|
currentCpe: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, [baseWidth, height]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (meshRef.current) {
|
||||||
|
for (let i = 0; i < COUNT; i++) {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
|
||||||
|
meshRef.current.setColorAt(i, colorObj);
|
||||||
|
}
|
||||||
|
meshRef.current.instanceColor!.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}, [isDark, colorObj]);
|
||||||
|
|
||||||
|
useFrame((state) => {
|
||||||
|
if (!meshRef.current) return;
|
||||||
|
const time = state.clock.elapsedTime;
|
||||||
|
|
||||||
|
particles.forEach((p, i) => {
|
||||||
|
const px = p.position.x;
|
||||||
|
const py = p.position.y;
|
||||||
|
const pz = p.position.z;
|
||||||
|
|
||||||
|
let currentSpeed = p.baseSpeed;
|
||||||
|
let localCpe = 0;
|
||||||
|
let deflectX = 0;
|
||||||
|
|
||||||
|
const inX = Math.abs(px) < halfWidth + 2;
|
||||||
|
const inZ = Math.abs(pz) < halfWidth + 2;
|
||||||
|
|
||||||
|
if (inX && inZ && py < height) {
|
||||||
|
localCpe = 0.8;
|
||||||
|
// Se a torre é densa (phi alto), o ar tenta desviar pelas laterais
|
||||||
|
if (pz < 0) {
|
||||||
|
const dist = halfWidth - Math.abs(px);
|
||||||
|
if (dist > 0) {
|
||||||
|
deflectX += (px > 0 ? 1 : -1) * currentSpeed * phi * 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.currentCpe += (localCpe - p.currentCpe) * 0.1;
|
||||||
|
p.position.z += currentSpeed;
|
||||||
|
p.position.x += deflectX;
|
||||||
|
p.position.y += Math.sin(time * p.wobbleSpeed * 10 + p.wobbleOffset) * 0.01;
|
||||||
|
|
||||||
|
if (p.position.z > halfWidth + 10) {
|
||||||
|
p.position.z = -halfWidth - 10 - Math.random() * 5;
|
||||||
|
p.position.y = Math.random() * (height + 5);
|
||||||
|
p.position.x = (Math.random() - 0.5) * baseWidth * 3;
|
||||||
|
p.currentCpe = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dummy.position.copy(p.position);
|
||||||
|
dummy.rotation.set(0, 0, 0);
|
||||||
|
dummy.rotateX(Math.PI / 2);
|
||||||
|
dummy.scale.set(1, 1 + currentSpeed * 6, 1);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||||
|
|
||||||
|
if (p.currentCpe > 0) {
|
||||||
|
colorObj.set(isDark ? '#f87171' : '#ef4444');
|
||||||
|
} else if (p.currentCpe < 0) {
|
||||||
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
|
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity);
|
||||||
|
} else {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
|
||||||
|
}
|
||||||
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
meshRef.current.instanceMatrix.needsUpdate = true;
|
||||||
|
meshRef.current.instanceColor!.needsUpdate = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<instancedMesh ref={meshRef} args={[undefined, undefined, COUNT]}>
|
||||||
|
<cylinderGeometry args={[0.04, 0.04, 1.0, 4]} />
|
||||||
|
<meshBasicMaterial transparent opacity={0.6} depthWrite={false} />
|
||||||
|
</instancedMesh>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import * as THREE from 'three';
|
|||||||
import SceneCanvas from '../SceneCanvas';
|
import SceneCanvas from '../SceneCanvas';
|
||||||
import FallbackDiagram from '../FallbackDiagram';
|
import FallbackDiagram from '../FallbackDiagram';
|
||||||
import { useCanvasTheme } from '@/lib/theme';
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
import { VaultAirflowSystem } from './VaultAirflowSystem';
|
||||||
|
|
||||||
export interface Vault3DInput {
|
export interface Vault3DInput {
|
||||||
span: number;
|
span: number;
|
||||||
@@ -14,6 +15,7 @@ export interface Vault3DInput {
|
|||||||
cpeProfile: Record<string, number>;
|
cpeProfile: Record<string, number>;
|
||||||
cpeParallel?: Record<string, number>;
|
cpeParallel?: Record<string, number>;
|
||||||
viewDirection?: 'perpendicular' | 'parallel';
|
viewDirection?: 'perpendicular' | 'parallel';
|
||||||
|
viewMode?: 'solid' | 'airflow';
|
||||||
}
|
}
|
||||||
|
|
||||||
function vaultColor(cpe: number, cpi: number, isDark: boolean): THREE.Color {
|
function vaultColor(cpe: number, cpi: number, isDark: boolean): THREE.Color {
|
||||||
@@ -26,9 +28,10 @@ function vaultColor(cpe: number, cpi: number, isDark: boolean): THREE.Color {
|
|||||||
return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, l + 5)}%)`);
|
return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, l + 5)}%)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function VaultModel({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDirection = 'perpendicular' }: Vault3DInput) {
|
function VaultModel({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDirection = 'perpendicular', viewMode = 'solid' }: Vault3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
|
const isAirflow = viewMode === 'airflow';
|
||||||
const segments = 64;
|
const segments = 64;
|
||||||
|
|
||||||
const points = useMemo(() => {
|
const points = useMemo(() => {
|
||||||
@@ -92,7 +95,7 @@ function VaultModel({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDire
|
|||||||
<bufferAttribute attach="attributes-position" args={[new Float32Array(verts), 3]} />
|
<bufferAttribute attach="attributes-position" args={[new Float32Array(verts), 3]} />
|
||||||
<bufferAttribute attach="index" args={[new Uint16Array(indices), 1]} />
|
<bufferAttribute attach="index" args={[new Uint16Array(indices), 1]} />
|
||||||
</bufferGeometry>
|
</bufferGeometry>
|
||||||
<meshStandardMaterial color={color} opacity={0.92} transparent side={THREE.DoubleSide} roughness={0.4} />
|
<meshStandardMaterial color={color} opacity={isAirflow ? 0.35 : 0.92} transparent side={THREE.DoubleSide} roughness={0.4} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<Text
|
<Text
|
||||||
position={[midPt.x, midPt.y + 0.5, length / 2]}
|
position={[midPt.x, midPt.y + 0.5, length / 2]}
|
||||||
@@ -139,7 +142,7 @@ function VaultModel({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDire
|
|||||||
<bufferAttribute attach="attributes-position" args={[new Float32Array(verts), 3]} />
|
<bufferAttribute attach="attributes-position" args={[new Float32Array(verts), 3]} />
|
||||||
<bufferAttribute attach="index" args={[new Uint16Array(indices), 1]} />
|
<bufferAttribute attach="index" args={[new Uint16Array(indices), 1]} />
|
||||||
</bufferGeometry>
|
</bufferGeometry>
|
||||||
<meshStandardMaterial color={color} opacity={0.92} transparent side={THREE.DoubleSide} roughness={0.4} />
|
<meshStandardMaterial color={color} opacity={isAirflow ? 0.35 : 0.92} transparent side={THREE.DoubleSide} roughness={0.4} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<Text
|
<Text
|
||||||
position={[0, rise + 0.5, (zStart + zEnd) / 2]}
|
position={[0, rise + 0.5, (zStart + zEnd) / 2]}
|
||||||
@@ -163,15 +166,39 @@ function VaultModel({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDire
|
|||||||
{/* Tímpano Traseiro (Z = 0) */}
|
{/* Tímpano Traseiro (Z = 0) */}
|
||||||
<mesh position={[0, 0, 0]} castShadow receiveShadow>
|
<mesh position={[0, 0, 0]} castShadow receiveShadow>
|
||||||
<shapeGeometry args={[archShape]} />
|
<shapeGeometry args={[archShape]} />
|
||||||
<meshStandardMaterial color={isDark ? "#334155" : "#cbd5e1"} opacity={0.7} transparent side={THREE.DoubleSide} roughness={0.5} />
|
<meshStandardMaterial color={isDark ? "#334155" : "#cbd5e1"} opacity={isAirflow ? 0.25 : 0.7} transparent side={THREE.DoubleSide} roughness={0.5} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Tímpano Frontal (Z = length) */}
|
{/* Tímpano Frontal (Z = length) */}
|
||||||
<mesh position={[0, 0, length]} castShadow receiveShadow>
|
<mesh position={[0, 0, length]} castShadow receiveShadow>
|
||||||
<shapeGeometry args={[archShape]} />
|
<shapeGeometry args={[archShape]} />
|
||||||
<meshStandardMaterial color={isDark ? "#334155" : "#cbd5e1"} opacity={0.7} transparent side={THREE.DoubleSide} roughness={0.5} />
|
<meshStandardMaterial color={isDark ? "#334155" : "#cbd5e1"} opacity={isAirflow ? 0.25 : 0.7} transparent side={THREE.DoubleSide} roughness={0.5} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
|
{/* Seta indicativa de direção do vento */}
|
||||||
|
<group position={[-span / 2 - 2.5, rise / 2, length / 2]} rotation={[0, 0, -Math.PI / 2]}>
|
||||||
|
<mesh castShadow>
|
||||||
|
<coneGeometry args={[0.3, 0.8, 16]} />
|
||||||
|
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, -0.6, 0]} castShadow>
|
||||||
|
<cylinderGeometry args={[0.1, 0.1, 1.2, 16]} />
|
||||||
|
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
{/* Texto Vento */}
|
||||||
|
<Text
|
||||||
|
position={[-span / 2 - 2.5, rise / 2 + 1.2, length / 2]}
|
||||||
|
rotation={[0, 0, 0]}
|
||||||
|
fontSize={1.2}
|
||||||
|
color="#3b82f6"
|
||||||
|
anchorX="center"
|
||||||
|
anchorY="bottom"
|
||||||
|
>
|
||||||
|
Vento
|
||||||
|
</Text>
|
||||||
|
|
||||||
{/* Rótulo de dimensões */}
|
{/* Rótulo de dimensões */}
|
||||||
<Text
|
<Text
|
||||||
position={[0, rise + 1.5, length / 2]}
|
position={[0, rise + 1.5, length / 2]}
|
||||||
@@ -186,7 +213,7 @@ function VaultModel({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDire
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Vault3DViewer({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDirection = 'perpendicular' }: Vault3DInput) {
|
export default function Vault3DViewer({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDirection = 'perpendicular', viewMode = 'solid' }: Vault3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
@@ -202,6 +229,7 @@ export default function Vault3DViewer({ span, length, rise, cpi, cpeProfile, cpe
|
|||||||
return (
|
return (
|
||||||
<SceneCanvas moduleId="abobada"
|
<SceneCanvas moduleId="abobada"
|
||||||
shadows
|
shadows
|
||||||
|
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [span * 1.2, rise * 1.5, length * 1.2], fov: 40 }}
|
camera={{ position: [span * 1.2, rise * 1.5, length * 1.2], fov: 40 }}
|
||||||
fallback={fallback}
|
fallback={fallback}
|
||||||
@@ -214,7 +242,19 @@ export default function Vault3DViewer({ span, length, rise, cpi, cpeProfile, cpe
|
|||||||
shadow-mapSize-width={1024}
|
shadow-mapSize-width={1024}
|
||||||
shadow-mapSize-height={1024}
|
shadow-mapSize-height={1024}
|
||||||
/>
|
/>
|
||||||
<VaultModel span={span} length={length} rise={rise} cpi={cpi} cpeProfile={cpeProfile} cpeParallel={cpeParallel} viewDirection={viewDirection} />
|
<VaultModel
|
||||||
|
span={span}
|
||||||
|
length={length}
|
||||||
|
rise={rise}
|
||||||
|
cpi={cpi}
|
||||||
|
cpeProfile={cpeProfile}
|
||||||
|
cpeParallel={cpeParallel}
|
||||||
|
viewDirection={viewDirection}
|
||||||
|
viewMode={viewMode}
|
||||||
|
/>
|
||||||
|
{viewMode === 'airflow' && (
|
||||||
|
<VaultAirflowSystem span={span} length={length} rise={rise} viewDirection={viewDirection} />
|
||||||
|
)}
|
||||||
<Grid
|
<Grid
|
||||||
infiniteGrid
|
infiniteGrid
|
||||||
fadeDistance={maxDimension * 5}
|
fadeDistance={maxDimension * 5}
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import { useRef, useMemo, useEffect } from 'react';
|
||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { useCanvasTheme } from '../../lib/theme';
|
||||||
|
|
||||||
|
export interface VaultAirflowSystemProps {
|
||||||
|
span: number;
|
||||||
|
length: number;
|
||||||
|
rise: number;
|
||||||
|
viewDirection?: 'perpendicular' | 'parallel';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VaultAirflowSystem({ span, length, rise, viewDirection = 'perpendicular' }: VaultAirflowSystemProps) {
|
||||||
|
const isDark = useCanvasTheme() === 'dark';
|
||||||
|
const count = 400;
|
||||||
|
const meshRef = useRef<THREE.InstancedMesh>(null);
|
||||||
|
|
||||||
|
const particles = useMemo(() => {
|
||||||
|
const temp = [];
|
||||||
|
const limitX = Math.max(20, span * 2.5);
|
||||||
|
const limitZ = Math.max(20, length * 1.5);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
temp.push({
|
||||||
|
position: new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * limitX * 2,
|
||||||
|
Math.random() * (rise * 2.5),
|
||||||
|
(Math.random() - 0.5) * limitZ * 2
|
||||||
|
),
|
||||||
|
baseSpeed: 0.12 + Math.random() * 0.08,
|
||||||
|
wobbleSpeed: Math.random() * 0.05,
|
||||||
|
wobbleOffset: Math.random() * Math.PI * 2,
|
||||||
|
currentCpe: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return temp;
|
||||||
|
}, [count, span, length, rise]);
|
||||||
|
|
||||||
|
const dummy = useMemo(() => new THREE.Object3D(), []);
|
||||||
|
const colorObj = useMemo(() => new THREE.Color(), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (meshRef.current) {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
meshRef.current.setColorAt(i, new THREE.Color('#ffffff'));
|
||||||
|
}
|
||||||
|
if (meshRef.current.instanceColor) {
|
||||||
|
meshRef.current.instanceColor.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [count]);
|
||||||
|
|
||||||
|
useFrame((state) => {
|
||||||
|
if (!meshRef.current) return;
|
||||||
|
const time = state.clock.elapsedTime;
|
||||||
|
const limitX = Math.max(20, span * 2.5);
|
||||||
|
const limitZ = Math.max(20, length * 1.5);
|
||||||
|
|
||||||
|
const isPerp = viewDirection === 'perpendicular';
|
||||||
|
|
||||||
|
particles.forEach((p, i) => {
|
||||||
|
let deflectX = isPerp ? 1.0 : 0.0;
|
||||||
|
let deflectY = 0.0;
|
||||||
|
let deflectZ = isPerp ? 0.0 : 1.0;
|
||||||
|
|
||||||
|
const px = p.position.x;
|
||||||
|
const py = p.position.y;
|
||||||
|
const pz = p.position.z;
|
||||||
|
|
||||||
|
// Altura teórica do arco da abóboda na coordenada X do ponto
|
||||||
|
const inSpan = px >= -span / 2 && px <= span / 2;
|
||||||
|
const inLength = pz >= 0 && pz <= length; // Lembrar que no Vault3D o modelo é desenhado de Z=0 a Z=length
|
||||||
|
const archY = inSpan ? rise * Math.sin(((px + span / 2) / span) * Math.PI) : 0;
|
||||||
|
|
||||||
|
if (isPerp) {
|
||||||
|
// Vento perpendicular (ao longo do eixo X)
|
||||||
|
if (inLength) {
|
||||||
|
if (px >= -span / 2 - 2 && px <= span / 2 + 2) {
|
||||||
|
// Deflexão ao se aproximar e passar pela abóboda
|
||||||
|
const theta = ((Math.max(-span / 2, Math.min(span / 2, px)) + span / 2) / span) * Math.PI;
|
||||||
|
const slope = (rise / span) * Math.PI * Math.cos(theta);
|
||||||
|
|
||||||
|
// A velocidade vertical induzida acompanha a inclinação do arco
|
||||||
|
if (py < archY + 3.0) {
|
||||||
|
deflectY = slope * 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evita colisão dura (ficar abaixo do arco)
|
||||||
|
if (py < archY + 0.3) {
|
||||||
|
p.position.y = archY + 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zona de turbulência/esteira atrás da abóboda (X > span/2)
|
||||||
|
if (px > span / 2 && px < span / 2 + 10) {
|
||||||
|
deflectY -= 0.15;
|
||||||
|
deflectZ += Math.sin(time * 5 + px * 0.5 + p.wobbleOffset) * 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapear cor com base na posição
|
||||||
|
if (inLength && inSpan && py < archY + 2.0) {
|
||||||
|
if (px < -span * 0.15) {
|
||||||
|
p.currentCpe = 0.7; // Compressão a barlavento
|
||||||
|
} else if (px >= -span * 0.15 && px <= span * 0.15) {
|
||||||
|
p.currentCpe = -0.9; // Forte sucção no topo
|
||||||
|
} else if (px > span / 4) {
|
||||||
|
p.currentCpe = 0.5; // Sotavento / Esteira (agora positivo para ficar vermelho)
|
||||||
|
} else {
|
||||||
|
p.currentCpe = -0.5; // Sucção a sotavento
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.currentCpe = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Vento paralelo (ao longo do eixo Z)
|
||||||
|
if (inSpan) {
|
||||||
|
// Se estiver muito baixo (abaixo da abóboda), empurra para cima da casca
|
||||||
|
if (py < archY + 0.3) {
|
||||||
|
p.position.y = archY + 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deflexão nas bordas frontais/traseiras (Z=0 e Z=length)
|
||||||
|
if (pz < -2 && pz > -8) {
|
||||||
|
const dist = (-pz);
|
||||||
|
const lift = Math.max(0, archY - py) / (dist + 1);
|
||||||
|
deflectY += lift * 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapear cor para vento paralelo (normalmente sucção em toda a extensão)
|
||||||
|
if (inSpan && inLength && py < archY + 2.0) {
|
||||||
|
// Zonas A, B, C, D longitudinais
|
||||||
|
const zRatio = pz / length;
|
||||||
|
if (zRatio < 0.25) p.currentCpe = -0.8; // Entrada
|
||||||
|
else if (zRatio < 0.5) p.currentCpe = -0.6;
|
||||||
|
else p.currentCpe = -0.4;
|
||||||
|
} else {
|
||||||
|
p.currentCpe = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suaviza a velocidade
|
||||||
|
const moveVec = new THREE.Vector3(deflectX, deflectY, deflectZ).normalize();
|
||||||
|
const currentSpeed = p.baseSpeed * (isPerp ? Math.max(0.4, 1 - Math.abs(deflectY) * 0.2) : 1.0);
|
||||||
|
|
||||||
|
p.position.addScaledVector(moveVec, currentSpeed);
|
||||||
|
|
||||||
|
// Reposicionar partículas fora do limite
|
||||||
|
if (isPerp) {
|
||||||
|
if (p.position.x > limitX) {
|
||||||
|
p.position.x = -limitX;
|
||||||
|
p.position.y = Math.random() * (rise * 2.5);
|
||||||
|
p.position.z = (Math.random() - 0.5) * limitZ * 2;
|
||||||
|
p.currentCpe = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (p.position.z > limitZ) {
|
||||||
|
p.position.z = -limitZ;
|
||||||
|
p.position.y = Math.random() * (rise * 2.5);
|
||||||
|
p.position.x = (Math.random() - 0.5) * limitX * 2;
|
||||||
|
p.currentCpe = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dummy.position.copy(p.position);
|
||||||
|
dummy.lookAt(p.position.clone().add(moveVec));
|
||||||
|
dummy.rotateX(Math.PI / 2);
|
||||||
|
dummy.scale.set(1, 1 + currentSpeed * 6, 1);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||||
|
|
||||||
|
// Colorir
|
||||||
|
if (p.currentCpe > 0) {
|
||||||
|
colorObj.set(isDark ? '#f87171' : '#ef4444'); // Vermelho (Turbulento/Pressão)
|
||||||
|
} else if (p.currentCpe < 0) {
|
||||||
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
|
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção)
|
||||||
|
} else {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre)
|
||||||
|
}
|
||||||
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
meshRef.current.instanceMatrix.needsUpdate = true;
|
||||||
|
if (meshRef.current.instanceColor) {
|
||||||
|
meshRef.current.instanceColor.needsUpdate = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
|
||||||
|
<cylinderGeometry args={[0.01, 0.01, 1.5, 4]} />
|
||||||
|
<meshBasicMaterial
|
||||||
|
color="#ffffff"
|
||||||
|
transparent
|
||||||
|
opacity={isDark ? 0.6 : 0.8}
|
||||||
|
blending={isDark ? THREE.AdditiveBlending : THREE.NormalBlending}
|
||||||
|
depthWrite={false}
|
||||||
|
/>
|
||||||
|
</instancedMesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,11 +8,14 @@ export interface WindArrowProps {
|
|||||||
direction: THREE.Vector3 | [number, number, number];
|
direction: THREE.Vector3 | [number, number, number];
|
||||||
/** Fator de escala da seta (default: 1) */
|
/** Fator de escala da seta (default: 1) */
|
||||||
scale?: number;
|
scale?: number;
|
||||||
|
/** Cor da seta */
|
||||||
|
color?: string;
|
||||||
|
/** Opacidade da seta (0 a 1) */
|
||||||
|
opacity?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WindArrow({ target, direction, scale = 1 }: WindArrowProps) {
|
export function WindArrow({ target, direction, scale = 1, color = '#3b82f6', opacity = 1 }: WindArrowProps) {
|
||||||
const length = 2.5 * scale; // Tamanho visual fixo
|
const length = 2.5 * scale; // Tamanho visual fixo
|
||||||
const color = '#3b82f6'; // Azul padrão do vento
|
|
||||||
|
|
||||||
const targetVec = target instanceof THREE.Vector3 ? target : new THREE.Vector3(...target);
|
const targetVec = target instanceof THREE.Vector3 ? target : new THREE.Vector3(...target);
|
||||||
const dirVec = direction instanceof THREE.Vector3 ? direction : new THREE.Vector3(...direction);
|
const dirVec = direction instanceof THREE.Vector3 ? direction : new THREE.Vector3(...direction);
|
||||||
@@ -46,13 +49,13 @@ export function WindArrow({ target, direction, scale = 1 }: WindArrowProps) {
|
|||||||
{shaftLength > 0 && (
|
{shaftLength > 0 && (
|
||||||
<mesh position={midPoint.toArray()} rotation={rotation} castShadow>
|
<mesh position={midPoint.toArray()} rotation={rotation} castShadow>
|
||||||
<cylinderGeometry args={[shaftRadius, shaftRadius, shaftLength, 12]} />
|
<cylinderGeometry args={[shaftRadius, shaftRadius, shaftLength, 12]} />
|
||||||
<meshStandardMaterial color={color} />
|
<meshStandardMaterial color={color} transparent={opacity < 1} opacity={opacity} />
|
||||||
</mesh>
|
</mesh>
|
||||||
)}
|
)}
|
||||||
{/* Ponta da seta (cone) */}
|
{/* Ponta da seta (cone) */}
|
||||||
<mesh position={[end.x, end.y, end.z]} rotation={rotation} castShadow>
|
<mesh position={[end.x, end.y, end.z]} rotation={rotation} castShadow>
|
||||||
<coneGeometry args={[headRadius, headLen, 12]} />
|
<coneGeometry args={[headRadius, headLen, 12]} />
|
||||||
<meshStandardMaterial color={color} />
|
<meshStandardMaterial color={color} transparent={opacity < 1} opacity={opacity} />
|
||||||
</mesh>
|
</mesh>
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface PersistentStore {
|
||||||
|
data: Record<string, any>;
|
||||||
|
setValue: (key: string, value: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePersistentStore = create<PersistentStore>((set) => ({
|
||||||
|
data: {},
|
||||||
|
setValue: (key, value) => set((state) => ({ data: { ...state.data, [key]: value } })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook para substituir o useState e manter o valor salvo globalmente na memória (Zustand)
|
||||||
|
* durante a navegação entre os componentes.
|
||||||
|
*/
|
||||||
|
export function usePersistentState<T>(key: string, initialValue: T): [T, (val: T | ((prev: T) => T)) => void] {
|
||||||
|
const storeValue = usePersistentStore((s) => s.data[key]);
|
||||||
|
const setValue = usePersistentStore((s) => s.setValue);
|
||||||
|
|
||||||
|
// Como o store não tem o valor inicial no momento da criação, precisamos lidar com isso.
|
||||||
|
// Se o valor estiver no store, usamos ele. Senão, usamos initialValue.
|
||||||
|
const value = storeValue !== undefined ? storeValue : initialValue;
|
||||||
|
|
||||||
|
const setter = (newVal: T | ((prev: T) => T)) => {
|
||||||
|
if (typeof newVal === 'function') {
|
||||||
|
setValue(key, (newVal as any)(value));
|
||||||
|
} else {
|
||||||
|
setValue(key, newVal);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return [value, setter];
|
||||||
|
}
|
||||||
@@ -17,6 +17,13 @@ import { evaluateComfort } from '../comfort';
|
|||||||
import { classifyBridge, type BridgeClassificationInput } from '../modules/bridge';
|
import { classifyBridge, type BridgeClassificationInput } from '../modules/bridge';
|
||||||
import { calculateTower } from '../modules/tower';
|
import { calculateTower } from '../modules/tower';
|
||||||
import { calculatePiperack, type PiperackInput } from '../modules/piperack';
|
import { calculatePiperack, type PiperackInput } from '../modules/piperack';
|
||||||
|
import { calculateVault, type VaultInput } from '../modules/vault';
|
||||||
|
import { calculateDome, type DomeInput } from '../modules/dome';
|
||||||
|
import { calculateIsolatedGableRoof, calculateIsolatedShedRoof } from '../nbr-tables/table-24-25';
|
||||||
|
import { calculateFlatBarForce, type FlatBarForceInput } from '../nbr-tables/table-26';
|
||||||
|
import { applyRoughnessChange } from '../wind-direction';
|
||||||
|
import { TABLE_1 } from '../nbr-tables/table-1';
|
||||||
|
import { getStrouhalNumber, criticalVelocity, scrutonNumber } from '../nbr-tables/table-33';
|
||||||
|
|
||||||
function calcWind(
|
function calcWind(
|
||||||
v0: number, s1: number, s3: number,
|
v0: number, s1: number, s3: number,
|
||||||
@@ -267,6 +274,209 @@ export function generateAllScenarios(): AuditScenario[] {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- NOVOS CENÁRIOS BIBLIOGRÁFICOS EXPANDIDOS (LITERATURA & NBR 6123) ---
|
||||||
|
|
||||||
|
// 1.1 Abóbada Cilíndrica (Blessmann / Tabela 15 NBR 6123)
|
||||||
|
{
|
||||||
|
const vaultIn: VaultInput = { f: 4, l: 20, b: 40, vk: 35, regime: 'laminar-rough', cpi: 0 };
|
||||||
|
const vaultRes = calculateVault(vaultIn);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
'blessmann-abobada-f4-l20', 'vault', 'Blessmann: Abóbada Cilíndrica f=4m l=20m b=40m',
|
||||||
|
{ ...vaultIn },
|
||||||
|
{ q: vaultRes.q, fOverL: 4 / 20 },
|
||||||
|
{ ...vaultRes.windPerpendicular, q: vaultRes.q },
|
||||||
|
{ q: { min: 0.5, max: 1.5 } },
|
||||||
|
'vault',
|
||||||
|
{ f: 4, l: 20, b: 40, cpeValues: vaultRes.windPerpendicular },
|
||||||
|
'Sec. 6.2.3 / Tabela 15',
|
||||||
|
true,
|
||||||
|
'Blessmann, J. Ação do Vento em Coberturas Curvas e Abóbadas, Exemplo 3.2 / NBR 6123:2023 Tabela 15',
|
||||||
|
'Abóbada cilíndrica com vão de 20m, flecha de 4m (f/l = 0,20) e comprimento de 40m sob vento perpendicular de 35 m/s. Avaliação da sucção no topo e pressões no bordo de ataque.'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.2 Cúpula Semiesférica apoiada no solo (Rebello / Tabela 21 NBR 6123)
|
||||||
|
{
|
||||||
|
const domeIn: DomeInput = { d: 30, f: 6, vk: 40, type: 'on-ground', cpi: 0 };
|
||||||
|
const domeRes = calculateDome(domeIn);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
'rebello-cupula-d30-f6', 'dome', 'Rebello: Cúpula no Solo d=30m f=6m',
|
||||||
|
{ ...domeIn },
|
||||||
|
{ q: domeRes.q, fOverD: domeRes.fOverD },
|
||||||
|
{ cpeBarlavento: domeRes.cpeBarlavento, cpeTopo: domeRes.cpeTopo, liftCoefficient: domeRes.liftCoefficient, liftForceKN: domeRes.liftForceKN },
|
||||||
|
{ q: { min: 0.5, max: 1.8 }, liftForceKN: { min: 50, max: 1000 } },
|
||||||
|
'dome',
|
||||||
|
{ d: 30, f: 6, cpeTopo: domeRes.cpeTopo, cpeBarlavento: domeRes.cpeBarlavento },
|
||||||
|
'Sec. 6.2.4 / Tabela 21',
|
||||||
|
true,
|
||||||
|
'Rebello, Y. A Concepção Estrutural e a Arquitetura / NBR 6123 Tabela 21 (Cúpulas sobre o solo)',
|
||||||
|
'Cúpula esférica implantada diretamente no terreno com diâmetro d=30m e flecha f=6m (f/d = 0,20). Determinação da força global de sustentação aerodinâmica (empuxo ascendente) para velocidade característica de 40 m/s.'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.3 Cobertura Isolada a Duas Águas - Hangar Aberto (Pfeil / Tabela 25 NBR 6123)
|
||||||
|
{
|
||||||
|
const isoIn = { theta: 10, height: 5, depth: 15 };
|
||||||
|
const isoRes = calculateIsolatedGableRoof(isoIn);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
'pfeil-cobertura-isolada-2aguas', 'isolated-roof', 'Pfeil: Cobertura Isolada 2 Águas θ=10°',
|
||||||
|
{ ...isoIn, q: 0.85 },
|
||||||
|
{ cpb1: isoRes.cpb.cpb1, cpa1: isoRes.cpa.cpa1 },
|
||||||
|
{ cpb1: isoRes.cpb.cpb1, cpb2: isoRes.cpb.cpb2, cpa1: isoRes.cpa.cpa1, cpa2: isoRes.cpa.cpa2, applies: isoRes.applies ? 1 : 0 },
|
||||||
|
{ cpb1: { min: 0.5, max: 2.0 }, cpa1: { min: -2.0, max: -0.5 } },
|
||||||
|
'isolatedRoof',
|
||||||
|
{ theta: 10, height: 5, depth: 15, cpa: isoRes.cpa, cpb: isoRes.cpb },
|
||||||
|
'Sec. 7.2 / Tabela 25',
|
||||||
|
true,
|
||||||
|
'Pfeil & Pfeil, Estruturas de Aço, 8ª ed., Cap. 5 (Galpões Abertos e Hangares Isolados) / NBR 6123 Tabela 25',
|
||||||
|
'Cobertura isolada simétrica de duas águas com inclinação de 10° e pé-direito livre h=5m (b=15m). Análise dos coeficientes de pressão líquida nos carregamentos extremos de sobrepressão e sucção.'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.4 Cobertura Isolada a Uma Água - Marquise / Abrigo (NBR 6123 Tabela 24)
|
||||||
|
{
|
||||||
|
const shedIn = { theta: 5, height: 3.5, depth: 10 };
|
||||||
|
const shedRes = calculateIsolatedShedRoof(shedIn);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
'nbr-marquise-isolada-1agua', 'isolated-roof', 'NBR 6123: Marquise Isolada a Uma Água θ=5°',
|
||||||
|
{ ...shedIn, q: 0.95 },
|
||||||
|
{ cph1High: shedRes.cph1.high, cph1Low: shedRes.cph1.low },
|
||||||
|
{ cph1High: shedRes.cph1.high, cph1Low: shedRes.cph1.low, applies: shedRes.applies ? 1 : 0 },
|
||||||
|
{ cph1High: { min: -1.5, max: 0.0 }, cph1Low: { min: -2.5, max: -0.5 } },
|
||||||
|
'isolatedRoof',
|
||||||
|
{ theta: 5, height: 3.5, depth: 10 },
|
||||||
|
'Sec. 7.2.1 / Tabela 24',
|
||||||
|
true,
|
||||||
|
'NBR 6123:2023, Tabela 24 — Coberturas isoladas a uma água plana / Exemplo Didático CBCA',
|
||||||
|
'Marquise/alpendre isolado a 1 água com inclinação suave de 5° e altura de 3.5m. Verificação dos coeficientes médios e limites de sucção no bordo de ataque.'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.5 Torre Treliçada Triangular de Telecomunicações (NBR 6123 Tabela 29/30 & Blessmann)
|
||||||
|
{
|
||||||
|
const towerIn = { section: 'triangular' as const, barType: 'flat' as const, phi: 0.25, aFace: 90, alphaWind: 0 as const, q: 1.15 };
|
||||||
|
const towerRes = calculateTower(towerIn);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
'blessmann-torre-triangular-phi025', 'tower', 'Blessmann: Torre Triangular φ=0.25',
|
||||||
|
{ ...towerIn },
|
||||||
|
{ caEff: towerRes.caEff },
|
||||||
|
{ caEff: towerRes.caEff, forceKN: towerRes.forceKN },
|
||||||
|
{ caEff: { min: 1.5, max: 3.5 }, forceKN: { min: 100, max: 500 } },
|
||||||
|
'tower',
|
||||||
|
{ section: 'triangular', baseWidth: 3, height: 30, panels: 6, phi: 0.25, alphaWind: 0, forceKN: towerRes.forceKN },
|
||||||
|
'Sec. 8.5 / Tabelas 29-30',
|
||||||
|
true,
|
||||||
|
'Blessmann, J. Ação do Vento em Torres e Estruturas Treliçadas, Cap. 7 / NBR 6123:2023 Sec. 8.5',
|
||||||
|
'Torre treliçada de seção triangular equilátera com índice de plenitude φ=0,25 e área projetada da face de 90 m² submetida a vento frontal (0°). Cálculo do coeficiente de arrasto efetivo e da força global na torre.'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.6 Barra Isolada Perfil L / Cantoneira de Torre (Pfeil & NBR 6123 Tabela 26)
|
||||||
|
{
|
||||||
|
const barIn: FlatBarForceInput = { section: 'l', alpha: 45, width: 0.15, length: 3.0, q: 1.05 };
|
||||||
|
const barRes = calculateFlatBarForce(barIn);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
'pfeil-cantoneira-l-alpha45', 'bar', 'Pfeil: Cantoneira L150x150 sob Vento a 45°',
|
||||||
|
{ ...barIn },
|
||||||
|
{ cx: barRes.cx, cy: barRes.cy, kFactor: barRes.kFactor },
|
||||||
|
{ fxKN: barRes.fxKN, fyKN: barRes.fyKN, cx: barRes.cx, cy: barRes.cy },
|
||||||
|
{ cx: { min: 1.0, max: 2.2 }, cy: { min: 1.0, max: 2.2 }, fxKN: { min: 0.5, max: 2.0 } },
|
||||||
|
'bar',
|
||||||
|
{ section: 'l', width: 0.15, length: 3.0, alpha: 45, fxKN: barRes.fxKN, fyKN: barRes.fyKN },
|
||||||
|
'Sec. 8.1 / Tabelas 26 e 28',
|
||||||
|
true,
|
||||||
|
'Pfeil, M. & Pfeil, W. Estruturas de Aço, Cap. Ações em Barras Isoladas / NBR 6123 Tabela 26',
|
||||||
|
'Barra prismática isolada de seção cantoneira de abas iguais (L 150x150 mm) com 3 metros de comprimento livre, sob incidência oblíqua do vento a 45°. Determinação das componentes de força Cx, Cy e redução por comprimento finito (Fator K).'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.7 Galpão Extenso — Força de Atrito Superficial (Blessmann Cap. 5 / NBR 6123 Sec. 6.1.5)
|
||||||
|
{
|
||||||
|
const fricIn = { roughness: 'rounded-ribs' as const, length: 80, width: 15, height: 6, roofPitch: 8, q: 1.0, windAngle: 0 as const };
|
||||||
|
const fricRes = calculateFriction(fricIn);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
'blessmann-galpao-extenso-atrito', 'galpao', 'Blessmann: Galpão Extenso 80x15m — Atrito Superficial',
|
||||||
|
{ ...fricIn },
|
||||||
|
{ applies: fricRes.applies ? 1 : 0, ratioLh: 80 / 6 },
|
||||||
|
{ applies: fricRes.applies ? 1 : 0, forceKN: fricRes.forceKN, roofArea: fricRes.roofArea, cf: fricRes.cf },
|
||||||
|
{ forceKN: { min: 10, max: 50 }, roofArea: { min: 1000, max: 1500 } },
|
||||||
|
'warehouse',
|
||||||
|
{ width: 15, length: 80, height: 6, roofPitch: 8, windAngle: 0, cpi: 0 },
|
||||||
|
'Sec. 6.1.5',
|
||||||
|
true,
|
||||||
|
'Blessmann, J. O Vento na Engenharia Estrutural, Cap. 5 (Forças de Atrito em Edificações Longas) / NBR 6123 Sec. 6.1.5',
|
||||||
|
'Galpão industrial longo de 80 metros de comprimento por 15m de largura e 6m de altura (l/h = 13,3 > 4). Telhas com nervuras onduladas/arredondadas (Cf = 0,02). Cálculo da força de arrasto por atrito superficial na cobertura.'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.8 Transição de Rugosidade de Terreno — Litoral para Centro Urbano (NBR 6123 Sec. 5.5)
|
||||||
|
{
|
||||||
|
const transIn = { near: 'IV' as TerrainCategory, far: 'I' as TerrainCategory, distance: 500, height: 30 };
|
||||||
|
const params = new Map<TerrainCategory, { b: number; p: number; fr: number }>();
|
||||||
|
(['I', 'II', 'III', 'IV', 'V'] as TerrainCategory[]).forEach(cat => {
|
||||||
|
params.set(cat, TABLE_1[cat].B);
|
||||||
|
});
|
||||||
|
const transRes = applyRoughnessChange(transIn, params);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
'nbr-transicao-rugosidade-catI-IV', 'galpao', 'NBR 6123: Transição de Rugosidade Cat I -> Cat IV (x=500m, z=30m)',
|
||||||
|
{ ...transIn },
|
||||||
|
{ z1: transRes.z1, z2: transRes.z2, s2: transRes.s2 },
|
||||||
|
{ s2: transRes.s2, z1: transRes.z1 },
|
||||||
|
{ s2: { min: 0.8, max: 1.3 } },
|
||||||
|
'warehouse',
|
||||||
|
{ width: 20, length: 40, height: 30, roofPitch: 0, windAngle: 0, cpi: 0 },
|
||||||
|
'Sec. 5.5',
|
||||||
|
true,
|
||||||
|
'NBR 6123:2023, Seção 5.5 (Mudança de Rugosidade do Terreno) / Notas de Aula Prof. Blessmann',
|
||||||
|
'Edificação de 30m de altura situada a 500 metros da linha de transição costeira (saída de Terreno Aberto Cat. I para Zona Urbana Densa Cat. IV). Determinação da altura da camada limite interna (z1) e do fator S2 interpolado.'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.9 Desprendimento de Vórtices em Chaminé Esbelta (Strouhal & Scruton — NBR 6123 Sec. 10)
|
||||||
|
{
|
||||||
|
const st = getStrouhalNumber('circle', 0); // 0.20
|
||||||
|
const fn = 1.2; // Hz
|
||||||
|
const d = 1.5; // m
|
||||||
|
const vcr = criticalVelocity(fn, d, st); // 1.2 * 1.5 / 0.20 = 9.0 m/s
|
||||||
|
const sc = scrutonNumber(0.02, 600, 1.225, 1.5); // 0.02 * 600 / (1.225 * 1.5) = 6.53
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
'nbr-vortices-chamine-strouhal', 'dynamics', 'NBR 6123: Vórtices de Von Kármán em Chaminé Cilíndrica',
|
||||||
|
{ shape: 'circle', diameter: d, naturalFreq: fn, massPerLength: 600, dampingRatio: 0.02 },
|
||||||
|
{ st, vcr, sc },
|
||||||
|
{ st, vcr, sc, isSusceptible: sc < 20 ? 1 : 0 },
|
||||||
|
{ vcr: { min: 5, max: 20 }, sc: { min: 2, max: 20 } },
|
||||||
|
'dynamics',
|
||||||
|
{ height: 45, freq: fn, windSpeed: vcr, scruton: sc, sectionShape: 'circle', sectionSize: d },
|
||||||
|
'Sec. 10 / Tabela 33',
|
||||||
|
true,
|
||||||
|
'NBR 6123:2023, Seção 10 (Efeitos Dinâmicos de Desprendimento de Vórtices) & Simiu & Scanlan',
|
||||||
|
'Chaminé metálica esbelta circular com d=1.5m, frequência natural fundamental fn=1.2 Hz, massa linear de 600 kg/m e amortecimento estrutural ξ=0,02. Cálculo do Número de Strouhal (St=0,20), velocidade crítica de ressonância (Vcr) e Número de Scruton (Sc).'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.10 Pipe-Rack Industrial com Múltiplos Planos (NBR 6123 Sec. 8.4 / Blessmann)
|
||||||
|
{
|
||||||
|
const pipeIn: PiperackInput = {
|
||||||
|
width: 12, height: 5, elevation: 8, spacing: 6, numFrames: 4, phiStruct: 0.20,
|
||||||
|
pipes: [{ id: 'p1', diameter: 0.6, elevationOffset: 5 }, { id: 'p2', diameter: 0.6, elevationOffset: 5 }],
|
||||||
|
q: 1.10
|
||||||
|
};
|
||||||
|
const pipeRes = calculatePiperack(pipeIn);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
'blessmann-piperack-4quadros', 'piperack', 'Blessmann: Pipe-Rack Industrial 4 Quadros com Tubulações',
|
||||||
|
{ ...pipeIn },
|
||||||
|
{ phiTotal: pipeRes.phiTotal, effectiveElevation: pipeRes.effectiveElevation },
|
||||||
|
{ caFrontal: pipeRes.caFrontal, eta: pipeRes.eta, globalForce: pipeRes.globalForce },
|
||||||
|
{ caFrontal: { min: 1.5, max: 3.0 }, eta: { min: 0.4, max: 1.0 }, globalForce: { min: 10, max: 300 } },
|
||||||
|
'warehouse',
|
||||||
|
{ width: 12, height: 5, length: 24 },
|
||||||
|
'Sec. 8.4',
|
||||||
|
true,
|
||||||
|
'Blessmann, J. Ação do Vento em Estruturas Industriais e Reticulados / NBR 6123:2023 Sec. 8.4',
|
||||||
|
'Pipe-rack industrial contendo 4 pórticos transversais espaçados de 6m com bandejas de cabos e 2 linhas de tubulação de 600mm de diâmetro. Cálculo do índice de plenitude total (φ), fator de sombreamento (η) e carga de arrasto global.'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// 2. GALPÃO VARREDURA
|
// 2. GALPÃO VARREDURA
|
||||||
const galpaoGeos = [
|
const galpaoGeos = [
|
||||||
{ w: 10, l: 20, h: 4 }, { w: 15, l: 30, h: 6 }, { w: 20, l: 50, h: 8 },
|
{ w: 10, l: 20, h: 4 }, { w: 15, l: 30, h: 6 }, { w: 20, l: 50, h: 8 },
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Módulo de Combinações de Ações (NBR 8681 / NBR 6118)
|
||||||
|
* Simplificado para uso prático com o vento.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface LoadInputs {
|
||||||
|
/** Peso Próprio (Permanente) em kN/m ou kN/m² */
|
||||||
|
pp: number;
|
||||||
|
/** Sobrecarga (Acidental) em kN/m ou kN/m² */
|
||||||
|
sc: number;
|
||||||
|
/** Carga de Vento (Acidental) em kN/m ou kN/m² */
|
||||||
|
w: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combinação ELU (Estado Limite Último) Normal
|
||||||
|
* Vento Principal: 1.4*PP + 1.4*W + 1.4*0.5*SC
|
||||||
|
* Sobrecarga Principal: 1.4*PP + 1.4*SC + 1.4*0.6*W
|
||||||
|
*/
|
||||||
|
export function calcELU(loads: LoadInputs): number {
|
||||||
|
const { pp, sc, w } = loads;
|
||||||
|
const g = 1.4 * pp;
|
||||||
|
|
||||||
|
// Vento como ação variável principal
|
||||||
|
const comb1 = g + (1.4 * w) + (1.4 * 0.5 * sc);
|
||||||
|
|
||||||
|
// Sobrecarga como ação variável principal
|
||||||
|
const comb2 = g + (1.4 * sc) + (1.4 * 0.6 * w);
|
||||||
|
|
||||||
|
return Math.max(Math.abs(comb1), Math.abs(comb2)) * Math.sign(w || pp || 1); // preserva direção da dominante
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combinação ELU Fiel (Arrancamento)
|
||||||
|
* Peso Próprio é favorável (reduz a chance de voar).
|
||||||
|
* Vento é desfavorável (puxa para cima, negativo).
|
||||||
|
* PP = 1.0, W = 1.4
|
||||||
|
*/
|
||||||
|
export function calcELUArrancamento(pp: number, w: number): number {
|
||||||
|
// pp é positivo para baixo. w é negativo para cima (sucção).
|
||||||
|
return (1.0 * pp) + (1.4 * w);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combinação ELS (Estado Limite de Serviço) - Frequente
|
||||||
|
* PP + 0.4*SC + 0.3*W
|
||||||
|
*/
|
||||||
|
export function calcELS(loads: LoadInputs): number {
|
||||||
|
const { pp, sc, w } = loads;
|
||||||
|
return pp + (0.4 * sc) + (0.3 * w);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Utilitários para Análise Dinâmica Simplificada (NBR 6123 - Anexo Dinâmico)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna verdadeiro se a frequência natural (Hz) indica que a estrutura
|
||||||
|
* é excessivamente flexível e exige cálculo pelo modelo dinâmico rigoroso.
|
||||||
|
* Uma regra de ouro da engenharia de vento brasileira é que f1 < 1Hz
|
||||||
|
* requer verificação atenciosa aos efeitos dinâmicos (rajadas e vórtices).
|
||||||
|
*/
|
||||||
|
export function isResonantRisk(f1: number): boolean {
|
||||||
|
return f1 > 0 && f1 < 1.0;
|
||||||
|
}
|
||||||
@@ -102,6 +102,22 @@ export function calculateIsolatedGableRoof(input: IsolatedGableRoofInput): Isola
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cobertura isolada em V (Borboleta).
|
||||||
|
* Extrapolação baseada na inversão geométrica das duas águas.
|
||||||
|
* A NBR 6123 não possui tabela explícita, portanto applies = false.
|
||||||
|
*/
|
||||||
|
export function calculateIsolatedButterflyRoof(input: IsolatedGableRoofInput): IsolatedGableRoofResult {
|
||||||
|
const base = calculateIsolatedGableRoof(input);
|
||||||
|
return {
|
||||||
|
// Barlavento (descendo): forte sucção no bordo de ataque
|
||||||
|
cpb: { cpb1: -Math.abs(base.cpa.cpa1), cpb2: -Math.abs(base.cpa.cpa2) },
|
||||||
|
// Sotavento (subindo): atua como uma barreira, sofrendo pressão
|
||||||
|
cpa: { cpa1: Math.abs(base.cpb.cpb1), cpa2: Math.abs(base.cpb.cpb2) },
|
||||||
|
applies: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Força de atrito na cobertura isolada: F = 0,05 · q · a · b (sec. 7.2.2) */
|
/** Força de atrito na cobertura isolada: F = 0,05 · q · a · b (sec. 7.2.2) */
|
||||||
export function frictionForceIsolatedRoof(q: number, a: number, b: number): number {
|
export function frictionForceIsolatedRoof(q: number, a: number, b: number): number {
|
||||||
return Number((0.05 * q * a * b).toFixed(3));
|
return Number((0.05 * q * a * b).toFixed(3));
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export interface ShelterInput {
|
|||||||
rightClosure: number;
|
rightClosure: number;
|
||||||
/** Posição da abertura na parede de fundo ('top' | 'bottom' | 'distributed') */
|
/** Posição da abertura na parede de fundo ('top' | 'bottom' | 'distributed') */
|
||||||
openingPos: OpeningPosition;
|
openingPos: OpeningPosition;
|
||||||
|
/** Número de pórticos principais/pilares contínuos (padrão: 2) */
|
||||||
|
numColumns?: number;
|
||||||
/** Ângulo de incidência do vento (0° = frontal, 45° = oblíquo, 90° = lateral) */
|
/** Ângulo de incidência do vento (0° = frontal, 45° = oblíquo, 90° = lateral) */
|
||||||
windAngle?: ShelterWindAngle;
|
windAngle?: ShelterWindAngle;
|
||||||
}
|
}
|
||||||
@@ -38,10 +40,14 @@ export interface ShelterResult {
|
|||||||
windAngle: ShelterWindAngle;
|
windAngle: ShelterWindAngle;
|
||||||
/** Coeficiente de pressão superior da cobertura (Ce superior) */
|
/** Coeficiente de pressão superior da cobertura (Ce superior) */
|
||||||
cpeTop: number;
|
cpeTop: number;
|
||||||
|
/** Coeficiente de pressão superior EXTREMO na zona de borda livre (Ce borda) */
|
||||||
|
cpeEdgeTop: number;
|
||||||
/** Coeficiente de pressão interna efetivo sob a cobertura (Cpi inferior) */
|
/** Coeficiente de pressão interna efetivo sob a cobertura (Cpi inferior) */
|
||||||
cpiBottom: number;
|
cpiBottom: number;
|
||||||
/** Coeficiente de força líquida vertical na cobertura (Cnf = cpeTop - cpiBottom) */
|
/** Coeficiente de força líquida vertical média na cobertura (Cnf = cpeTop - cpiBottom) */
|
||||||
cnfVertical: number;
|
cnfVertical: number;
|
||||||
|
/** Coeficiente de força líquida vertical de PICO na zona de borda livre */
|
||||||
|
cnfEdgeVertical: number;
|
||||||
/** Coeficiente de força horizontal na parede de fundo */
|
/** Coeficiente de força horizontal na parede de fundo */
|
||||||
cfBack: number;
|
cfBack: number;
|
||||||
/** Coeficiente de força horizontal nas paredes laterais */
|
/** Coeficiente de força horizontal nas paredes laterais */
|
||||||
@@ -61,10 +67,18 @@ export interface ShelterResult {
|
|||||||
};
|
};
|
||||||
/** Carga linear estimada nos pórticos metálicos principais (kN/m) (ex: Tubo 150x150) */
|
/** Carga linear estimada nos pórticos metálicos principais (kN/m) (ex: Tubo 150x150) */
|
||||||
lineLoads: {
|
lineLoads: {
|
||||||
/** Carga horizontal em cada pilar (kN/m) */
|
/** Carga horizontal no pilar do PÓRTICO CENTRAL (mais carregado) (kN/m) */
|
||||||
columnLoad: number;
|
columnLoad: number;
|
||||||
/** Carga vertical/horizontal na viga em balanço (kN/m) */
|
/** Carga horizontal no pilar do PÓRTICO DE EXTREMIDADE (kN/m) */
|
||||||
|
columnLoadEdge: number;
|
||||||
|
/** Carga vertical/horizontal média equivalente na viga do PÓRTICO CENTRAL (kN/m) */
|
||||||
beamLoad: number;
|
beamLoad: number;
|
||||||
|
/** Carga vertical/horizontal média equivalente na viga do PÓRTICO DE EXTREMIDADE (kN/m) */
|
||||||
|
beamLoadEdge: number;
|
||||||
|
/** Momento fletor na raiz/engaste do balanço CENTRAL (kN.m) considerando o Efeito Asa de Avião */
|
||||||
|
beamMoment: number;
|
||||||
|
/** Momento fletor na raiz/engaste do balanço na EXTREMIDADE (kN.m) */
|
||||||
|
beamMomentEdge: number;
|
||||||
};
|
};
|
||||||
/** Alívio percentual de arrancamento obtido pela posição da abertura (%) */
|
/** Alívio percentual de arrancamento obtido pela posição da abertura (%) */
|
||||||
reliefPercentage: number;
|
reliefPercentage: number;
|
||||||
@@ -98,6 +112,7 @@ export function calculateShelterWind(input: ShelterInput, q: number): ShelterRes
|
|||||||
leftClosure,
|
leftClosure,
|
||||||
rightClosure,
|
rightClosure,
|
||||||
openingPos,
|
openingPos,
|
||||||
|
numColumns = 2,
|
||||||
} = input;
|
} = input;
|
||||||
|
|
||||||
const windAngle: ShelterWindAngle = input.windAngle ?? 0;
|
const windAngle: ShelterWindAngle = input.windAngle ?? 0;
|
||||||
@@ -115,15 +130,19 @@ export function calculateShelterWind(input: ShelterInput, q: number): ShelterRes
|
|||||||
// 1. Coeficiente externo superior na cobertura (Ce superior)
|
// 1. Coeficiente externo superior na cobertura (Ce superior)
|
||||||
// Varia conforme o ângulo de incidência (0°, 45° ou 90°)
|
// Varia conforme o ângulo de incidência (0°, 45° ou 90°)
|
||||||
let cpeTop = -1.0;
|
let cpeTop = -1.0;
|
||||||
|
let cpeEdgeTop = -2.0;
|
||||||
if (windAngle === 0) {
|
if (windAngle === 0) {
|
||||||
// Frontal: -1.0 a -1.4 de sucção de barlavento, dependendo de theta
|
// Frontal: -1.0 a -1.4 de sucção de barlavento, dependendo de theta
|
||||||
cpeTop = theta <= 5 ? -1.0 : theta <= 15 ? -1.2 : -1.4;
|
cpeTop = theta <= 5 ? -1.0 : theta <= 15 ? -1.2 : -1.4;
|
||||||
|
cpeEdgeTop = theta <= 5 ? -1.8 : theta <= 15 ? -2.0 : -2.4;
|
||||||
} else if (windAngle === 45) {
|
} else if (windAngle === 45) {
|
||||||
// Oblíquo 45°: forte vórtice cônico de canto nas bordas de barlavento (aumento de sucção em ~20% a 30%)
|
// Oblíquo 45°: forte vórtice cônico de canto nas bordas de barlavento (aumento de sucção em ~20% a 30%)
|
||||||
cpeTop = theta <= 5 ? -1.3 : theta <= 15 ? -1.4 : -1.6;
|
cpeTop = theta <= 5 ? -1.3 : theta <= 15 ? -1.4 : -1.6;
|
||||||
|
cpeEdgeTop = theta <= 5 ? -2.2 : theta <= 15 ? -2.5 : -2.8;
|
||||||
} else {
|
} else {
|
||||||
// Lateral 90°: escoamento transversal sobre a cobertura, sucção constante por descolamento
|
// Lateral 90°: escoamento transversal sobre a cobertura, sucção constante por descolamento
|
||||||
cpeTop = -1.1;
|
cpeTop = -1.1;
|
||||||
|
cpeEdgeTop = -1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Cálculo do Cpi sob a cobertura (efeito estagnação da parede traseira / laterais)
|
// 2. Cálculo do Cpi sob a cobertura (efeito estagnação da parede traseira / laterais)
|
||||||
@@ -189,6 +208,7 @@ export function calculateShelterWind(input: ShelterInput, q: number): ShelterRes
|
|||||||
// Cnf vertical total na cobertura (arrancamento para cima = negativo)
|
// Cnf vertical total na cobertura (arrancamento para cima = negativo)
|
||||||
const cnfUp = cpeTop - Math.max(0, effectiveCpi);
|
const cnfUp = cpeTop - Math.max(0, effectiveCpi);
|
||||||
const cnfDown = 0.2 - Math.min(0, effectiveCpi);
|
const cnfDown = 0.2 - Math.min(0, effectiveCpi);
|
||||||
|
const cnfEdgeUp = cpeEdgeTop - Math.max(0, effectiveCpi);
|
||||||
|
|
||||||
// 4. Coeficientes horizontais em paredes de fundo e laterais
|
// 4. Coeficientes horizontais em paredes de fundo e laterais
|
||||||
let cfBack = 1.3 * bClose;
|
let cfBack = 1.3 * bClose;
|
||||||
@@ -210,20 +230,45 @@ export function calculateShelterWind(input: ShelterInput, q: number): ShelterRes
|
|||||||
const fySide = cfSide * q * (areaSide * 2);
|
const fySide = cfSide * q * (areaSide * 2);
|
||||||
const fFriction = 0.04 * q * areaRoof;
|
const fFriction = 0.04 * q * areaRoof;
|
||||||
|
|
||||||
// 6. Carga linear em pórticos (2 pórticos principais)
|
// 6. Carga linear em pórticos (usando Áreas Tributárias)
|
||||||
const numPorticos = 2;
|
const numPorticos = Math.max(2, numColumns);
|
||||||
let columnLoad = 0;
|
const numBays = numPorticos - 1;
|
||||||
|
const overhang = 0.6; // beiral genérico considerado no 3D
|
||||||
|
// Largura total efetiva entre o primeiro e o último pilar:
|
||||||
|
const spanWidth = Math.max(0.1, width - 2 * overhang);
|
||||||
|
const spacing = spanWidth / numBays;
|
||||||
|
|
||||||
|
// A área tributária de um pórtico central é o espaçamento 's'
|
||||||
|
// A área tributária de um pórtico de extremidade é 's/2' + overhang
|
||||||
|
const tributaryCentral = spacing;
|
||||||
|
const tributaryEdge = spacing / 2 + overhang;
|
||||||
|
|
||||||
|
// Fração da carga total que vai para o pórtico central vs extremidade
|
||||||
|
const fractionCentral = tributaryCentral / width;
|
||||||
|
const fractionEdge = tributaryEdge / width;
|
||||||
|
|
||||||
|
let columnLoadTotal = 0;
|
||||||
if (windAngle === 0) {
|
if (windAngle === 0) {
|
||||||
columnLoad = (fxBack / height / numPorticos) + (q * 1.8 * 0.15);
|
columnLoadTotal = fxBack;
|
||||||
} else if (windAngle === 45) {
|
} else if (windAngle === 45) {
|
||||||
const combForce = Math.sqrt(fxBack * fxBack + fySide * fySide);
|
columnLoadTotal = Math.sqrt(fxBack * fxBack + fySide * fySide);
|
||||||
columnLoad = (combForce / height / numPorticos) + (q * 1.8 * 0.15);
|
|
||||||
} else {
|
} else {
|
||||||
// Em 90°, a carga transversal principal atua nas paredes laterais (Fy)
|
columnLoadTotal = fySide;
|
||||||
columnLoad = (fySide / height / numPorticos) + (q * 1.8 * 0.15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const beamLoad = (fzUp / depth / numPorticos);
|
// Cargas Distribuídas Lineares no Pilar
|
||||||
|
const windBaseLoadColumn = q * 1.8 * 0.15; // arrasto base no próprio perfil do pilar
|
||||||
|
const columnLoad = (columnLoadTotal * fractionCentral / height) + windBaseLoadColumn;
|
||||||
|
const columnLoadEdge = (columnLoadTotal * fractionEdge / height) + windBaseLoadColumn;
|
||||||
|
|
||||||
|
// Cargas Distribuídas Lineares na Viga (Balanço)
|
||||||
|
const beamLoad = (fzUp * fractionCentral / depth);
|
||||||
|
const beamLoadEdge = (fzUp * fractionEdge / depth);
|
||||||
|
|
||||||
|
// Cálculo do Momento Fletor na raiz da viga em balanço considerando o centro de pressão deslocado (L/3)
|
||||||
|
const cpRootDist = (2 / 3) * depth;
|
||||||
|
const beamMoment = (fzUp * fractionCentral) * cpRootDist;
|
||||||
|
const beamMomentEdge = (fzUp * fractionEdge) * cpRootDist;
|
||||||
|
|
||||||
// Texto explicativo por condição e ângulo
|
// Texto explicativo por condição e ângulo
|
||||||
let statusText = '';
|
let statusText = '';
|
||||||
@@ -266,8 +311,10 @@ export function calculateShelterWind(input: ShelterInput, q: number): ShelterRes
|
|||||||
return {
|
return {
|
||||||
windAngle,
|
windAngle,
|
||||||
cpeTop: Number(cpeTop.toFixed(2)),
|
cpeTop: Number(cpeTop.toFixed(2)),
|
||||||
|
cpeEdgeTop: Number(cpeEdgeTop.toFixed(2)),
|
||||||
cpiBottom: Number(effectiveCpi.toFixed(2)),
|
cpiBottom: Number(effectiveCpi.toFixed(2)),
|
||||||
cnfVertical: Number(cnfUp.toFixed(2)),
|
cnfVertical: Number(cnfUp.toFixed(2)),
|
||||||
|
cnfEdgeVertical: Number(cnfEdgeUp.toFixed(2)),
|
||||||
cfBack: Number(cfBack.toFixed(2)),
|
cfBack: Number(cfBack.toFixed(2)),
|
||||||
cfSide: Number(cfSide.toFixed(2)),
|
cfSide: Number(cfSide.toFixed(2)),
|
||||||
forces: {
|
forces: {
|
||||||
@@ -279,7 +326,11 @@ export function calculateShelterWind(input: ShelterInput, q: number): ShelterRes
|
|||||||
},
|
},
|
||||||
lineLoads: {
|
lineLoads: {
|
||||||
columnLoad: Number(columnLoad.toFixed(2)),
|
columnLoad: Number(columnLoad.toFixed(2)),
|
||||||
|
columnLoadEdge: Number(columnLoadEdge.toFixed(2)),
|
||||||
beamLoad: Number(beamLoad.toFixed(2)),
|
beamLoad: Number(beamLoad.toFixed(2)),
|
||||||
|
beamLoadEdge: Number(beamLoadEdge.toFixed(2)),
|
||||||
|
beamMoment: Number(beamMoment.toFixed(2)),
|
||||||
|
beamMomentEdge: Number(beamMomentEdge.toFixed(2)),
|
||||||
},
|
},
|
||||||
reliefPercentage,
|
reliefPercentage,
|
||||||
statusText,
|
statusText,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { Wind, Box } from 'lucide-react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
|
|
||||||
import { useHydrationStore } from '@/store/hydrationStore';
|
import { useHydrationStore } from '@/store/hydrationStore';
|
||||||
@@ -47,6 +48,7 @@ const BridgeModule: React.FC = () => {
|
|||||||
const [fv, setFv] = React.useState(0.6);
|
const [fv, setFv] = React.useState(0.6);
|
||||||
const [vk, setVk] = React.useState(45);
|
const [vk, setVk] = React.useState(45);
|
||||||
const [alpha, setAlpha] = React.useState(0);
|
const [alpha, setAlpha] = React.useState(0);
|
||||||
|
const [viewMode, setViewMode] = usePersistentState<'solid' | 'airflow'>('BridgeModule_viewMode', 'solid');
|
||||||
const [vf, setVf] = React.useState(100); // Velocidade crítica de flutter da seção
|
const [vf, setVf] = React.useState(100); // Velocidade crítica de flutter da seção
|
||||||
|
|
||||||
const classification = useMemo(
|
const classification = useMemo(
|
||||||
@@ -214,6 +216,24 @@ const BridgeModule: React.FC = () => {
|
|||||||
Classe {classification.bridgeClass}
|
Classe {classification.bridgeClass}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute top-4 right-16 z-10">
|
||||||
|
<div className="flex bg-background/80 backdrop-blur-sm border border-border/80 rounded-md shadow-sm p-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('solid')}
|
||||||
|
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'solid' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
|
||||||
|
>
|
||||||
|
<Box className="w-3.5 h-3.5 inline-block mr-1.5" />
|
||||||
|
Sólido
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('airflow')}
|
||||||
|
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'airflow' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
|
||||||
|
>
|
||||||
|
<Wind className="w-3.5 h-3.5 inline-block mr-1.5" />
|
||||||
|
Fluxo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="absolute top-4 right-4 z-10">
|
<div className="absolute top-4 right-4 z-10">
|
||||||
<EducationalManual type="bridge" params={{ alpha }} />
|
<EducationalManual type="bridge" params={{ alpha }} />
|
||||||
</div>
|
</div>
|
||||||
@@ -227,6 +247,7 @@ const BridgeModule: React.FC = () => {
|
|||||||
fxPerLength={forces.fxPerLength}
|
fxPerLength={forces.fxPerLength}
|
||||||
fzPerLength={forces.fzPerLength}
|
fzPerLength={forces.fzPerLength}
|
||||||
alpha={alpha}
|
alpha={alpha}
|
||||||
|
viewMode={viewMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+112
-121
@@ -1,21 +1,20 @@
|
|||||||
import { useState } from 'react';
|
import { usePersistentState } from '@/hooks/usePersistentState';
|
||||||
import { getDragCoefficient } from '@/lib/drag';
|
import { getDragCoefficient } from '@/lib/drag';
|
||||||
import { useI18n } from '@/store/i18nStore';
|
|
||||||
import { DragCoefficientChart } from '@/components/DragCoefficientChart';
|
import { DragCoefficientChart } from '@/components/DragCoefficientChart';
|
||||||
import { ResizableBox3D } from '@/components/three/ResizableBox3D';
|
import { ResizableBox3D } from '@/components/three/ResizableBox3D';
|
||||||
import SceneCanvas from '@/components/SceneCanvas';
|
import SceneCanvas from '@/components/SceneCanvas';
|
||||||
import { Environment } from '@react-three/drei';
|
import { Environment } from '@react-three/drei';
|
||||||
import { ViewerOrbitControls as OrbitControls } from '@/components/three/ViewerOrbitControls';
|
import { ViewerOrbitControls as OrbitControls } from '@/components/three/ViewerOrbitControls';
|
||||||
import { EducationalManual } from '@/components/EducationalManual';
|
import { EducationalManual } from '@/components/EducationalManual';
|
||||||
import { Calculator, Info } from 'lucide-react';
|
import { Wind, Box } from 'lucide-react';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
|
||||||
export default function CalcCaModule() {
|
export default function CalcCaModule() {
|
||||||
const { t } = useI18n();
|
const [l1, setL1] = usePersistentState<number>('CalcCaModule_l1', 20);
|
||||||
const [l1, setL1] = useState<number>(20);
|
const [l2, setL2] = usePersistentState<number>('CalcCaModule_l2', 40);
|
||||||
const [l2, setL2] = useState<number>(40);
|
const [h, setH] = usePersistentState<number>('CalcCaModule_h', 10);
|
||||||
const [h, setH] = useState<number>(10);
|
const [viewMode, setViewMode] = usePersistentState<'solid' | 'airflow'>('CalcCaModule_viewMode', 'solid');
|
||||||
|
|
||||||
// Clamp inputs to avoid Infinity or negative values breaking the UI
|
// Clamp inputs to avoid Infinity or negative values breaking the UI
|
||||||
const safeL1 = Math.max(0.1, l1 || 1);
|
const safeL1 = Math.max(0.1, l1 || 1);
|
||||||
@@ -26,128 +25,120 @@ export default function CalcCaModule() {
|
|||||||
const caHigh = getDragCoefficient(safeL1, safeL2, safeH, 'high');
|
const caHigh = getDragCoefficient(safeL1, safeL2, safeH, 'high');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-7xl mx-auto space-y-6">
|
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
|
||||||
<header className="flex items-center gap-4 mb-8">
|
{/* Coluna Esquerda: Controles */}
|
||||||
<div className="p-3 bg-primary/10 rounded-xl">
|
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||||
<Calculator className="w-8 h-8 text-primary" />
|
<Card className="shadow-sm border-border">
|
||||||
</div>
|
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">{t('nav_calc_ca')}</h1>
|
<CardTitle className="text-lg">Calc_Ca</CardTitle>
|
||||||
<p className="text-muted-foreground">
|
<CardDescription>Cálculo rápido de Coeficiente de Arrasto (Ca)</CardDescription>
|
||||||
Cálculo rápido de Coeficiente de Arrasto (Ca) - NBR 6123:2023, Fig. 4 e 5.
|
</div>
|
||||||
</p>
|
</CardHeader>
|
||||||
</div>
|
<CardContent className="space-y-6">
|
||||||
</header>
|
<div className="space-y-3">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<label className="text-sm font-medium">L₁ (Largura)</label>
|
||||||
|
<span className="font-mono text-sm">{l1} m</span>
|
||||||
|
</div>
|
||||||
|
<Slider min={1} max={100} step={1} value={[l1]} onValueChange={(v) => setL1(v[0])} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
<div className="space-y-3">
|
||||||
{/* Left Column: Inputs & Results */}
|
<div className="flex justify-between">
|
||||||
<div className="lg:col-span-4 space-y-6">
|
<label className="text-sm font-medium">L₂ (Profundidade)</label>
|
||||||
<Card>
|
<span className="font-mono text-sm">{l2} m</span>
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-lg">Entrada de Dados</CardTitle>
|
|
||||||
<CardDescription>Dimensões da edificação retangular</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="l1" className="text-sm font-medium leading-none">L₁ (Largura perpendicular ao vento - m)</label>
|
|
||||||
<Input
|
|
||||||
id="l1"
|
|
||||||
type="number"
|
|
||||||
min="0.1"
|
|
||||||
step="0.1"
|
|
||||||
value={l1}
|
|
||||||
onChange={(e) => setL1(parseFloat(e.target.value))}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<Slider min={1} max={100} step={1} value={[l2]} onValueChange={(v) => setL2(v[0])} />
|
||||||
<label htmlFor="l2" className="text-sm font-medium leading-none">L₂ (Profundidade paralela ao vento - m)</label>
|
</div>
|
||||||
<Input
|
|
||||||
id="l2"
|
|
||||||
type="number"
|
|
||||||
min="0.1"
|
|
||||||
step="0.1"
|
|
||||||
value={l2}
|
|
||||||
onChange={(e) => setL2(parseFloat(e.target.value))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="h" className="text-sm font-medium leading-none">H (Altura - m)</label>
|
|
||||||
<Input
|
|
||||||
id="h"
|
|
||||||
type="number"
|
|
||||||
min="0.1"
|
|
||||||
step="0.1"
|
|
||||||
value={h}
|
|
||||||
onChange={(e) => setH(parseFloat(e.target.value))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="border-primary/50 shadow-sm bg-primary/5">
|
<div className="space-y-3">
|
||||||
<CardHeader className="pb-2">
|
<div className="flex justify-between">
|
||||||
<CardTitle className="text-lg flex items-center gap-2">
|
<label className="text-sm font-medium">H (Altura)</label>
|
||||||
<Info className="w-5 h-5 text-primary" />
|
<span className="font-mono text-sm">{h} m</span>
|
||||||
Resultados (Ca)
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4 mt-2">
|
|
||||||
<div className="flex justify-between items-center p-3 bg-background rounded-lg border">
|
|
||||||
<span className="font-medium text-muted-foreground">Baixa Turbulência</span>
|
|
||||||
<span className="text-2xl font-bold text-primary">{caLow.toFixed(2)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center p-3 bg-background rounded-lg border">
|
|
||||||
<span className="font-medium text-muted-foreground">Alta Turbulência</span>
|
|
||||||
<span className="text-2xl font-bold text-primary">{caHigh.toFixed(2)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted-foreground flex gap-4 mt-2 px-1">
|
|
||||||
<span>H/L₁ = {(safeH / safeL1).toFixed(2)}</span>
|
|
||||||
<span>L₁/L₂ = {(safeL1 / safeL2).toFixed(2)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
<Slider min={1} max={100} step={1} value={[h]} onValueChange={(v) => setH(v[0])} />
|
||||||
</Card>
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Coluna Central: 3D */}
|
||||||
|
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
||||||
|
<div className="absolute top-4 right-16 z-10">
|
||||||
|
<div className="flex bg-background/80 backdrop-blur-sm border border-border/80 rounded-md shadow-sm p-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('solid')}
|
||||||
|
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'solid' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
|
||||||
|
>
|
||||||
|
<Box className="w-3.5 h-3.5 inline-block mr-1.5" />
|
||||||
|
Sólido
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('airflow')}
|
||||||
|
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'airflow' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
|
||||||
|
>
|
||||||
|
<Wind className="w-3.5 h-3.5 inline-block mr-1.5" />
|
||||||
|
Fluxo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-4 right-4 z-10">
|
||||||
|
<EducationalManual type="calc_ca" params={{ l1: safeL1, l2: safeL2, h: safeH }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Center Column: 3D Visualization */}
|
<SceneCanvas
|
||||||
<div className="lg:col-span-4">
|
moduleId="calc_ca"
|
||||||
<Card className="h-full flex flex-col">
|
camera={{ position: [50, 40, 50], fov: 45 }}
|
||||||
<CardHeader className="pb-2 shrink-0">
|
fallback={<div />}
|
||||||
<CardTitle className="text-lg">Visualização 3D</CardTitle>
|
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||||
<CardDescription>Proporções dinâmicas</CardDescription>
|
>
|
||||||
</CardHeader>
|
<ambientLight intensity={0.5} />
|
||||||
<CardContent className="flex-1 p-0 min-h-[400px] relative rounded-b-xl overflow-hidden bg-gradient-to-b from-muted/30 to-muted/10">
|
<directionalLight position={[10, 20, 10]} intensity={1} castShadow />
|
||||||
<div className="absolute top-4 right-4 z-10">
|
<Environment preset="city" />
|
||||||
<EducationalManual type="calc_ca" params={{ l1: safeL1, l2: safeL2, h: safeH }} />
|
<ResizableBox3D l1={safeL1} l2={safeL2} h={safeH} viewMode={viewMode} />
|
||||||
</div>
|
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.1} />
|
||||||
<SceneCanvas moduleId="calc_ca" camera={{ position: [50, 40, 50], fov: 45 }} fallback={<div />} frameloop="always">
|
</SceneCanvas>
|
||||||
<ambientLight intensity={0.5} />
|
|
||||||
<directionalLight position={[10, 20, 10]} intensity={1} castShadow />
|
|
||||||
<Environment preset="city" />
|
|
||||||
<ResizableBox3D l1={safeL1} l2={safeL2} h={safeH} />
|
|
||||||
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.1} />
|
|
||||||
</SceneCanvas>
|
|
||||||
<div className="absolute bottom-4 left-4 right-20 text-center text-xs text-muted-foreground pointer-events-none">
|
|
||||||
Arraste para rotacionar, role para aproximar
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Column: Chart */}
|
<div className="absolute bottom-4 left-4 right-20 text-center text-xs text-muted-foreground pointer-events-none">
|
||||||
<div className="lg:col-span-4">
|
Arraste para rotacionar, role para aproximar
|
||||||
<Card className="h-full">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-lg">Ábaco NBR 6123</CardTitle>
|
|
||||||
<CardDescription>Curvas isotermas aproximadas</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex items-center justify-center h-[calc(100%-80px)]">
|
|
||||||
<DragCoefficientChart l1={safeL1} l2={safeL2} h={safeH} caLow={caLow} caHigh={caHigh} />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Coluna Direita: Resultados */}
|
||||||
|
<div className="w-full lg:w-96 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||||
|
<Card className="shadow-sm border-border">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-lg">Resultados (Ca)</CardTitle>
|
||||||
|
<CardDescription>Valores baseados nas tabelas 4 e 5</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center p-3 bg-background rounded-lg border">
|
||||||
|
<span className="font-medium text-muted-foreground">Baixa Turbulência</span>
|
||||||
|
<span className="text-2xl font-bold text-primary">{caLow.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center p-3 bg-background rounded-lg border">
|
||||||
|
<span className="font-medium text-muted-foreground">Alta Turbulência</span>
|
||||||
|
<span className="text-2xl font-bold text-primary">{caHigh.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground flex gap-4 mt-2 px-1">
|
||||||
|
<span>H/L₁ = {(safeH / safeL1).toFixed(2)}</span>
|
||||||
|
<span>L₁/L₂ = {(safeL1 / safeL2).toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Ábaco */}
|
||||||
|
<Card className="shadow-sm border-border flex-1 min-h-[300px]">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-lg">Ábaco NBR 6123</CardTitle>
|
||||||
|
<CardDescription>Curvas isotermas aproximadas</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex items-center justify-center h-[calc(100%-80px)] min-h-[250px]">
|
||||||
|
<DragCoefficientChart l1={safeL1} l2={safeL2} h={safeH} caLow={caLow} caHigh={caHigh} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
|
|||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { Box, Wind } from 'lucide-react';
|
||||||
import { useWindStore } from '@/store/appStore';
|
import { useWindStore } from '@/store/appStore';
|
||||||
import { CpiSettingsCard } from '@/components/CpiSettingsCard';
|
import { CpiSettingsCard } from '@/components/CpiSettingsCard';
|
||||||
import { calculateCylinder, type CylinderEndType } from '@/lib/modules/cylinder';
|
import { calculateCylinder, type CylinderEndType } from '@/lib/modules/cylinder';
|
||||||
@@ -23,6 +25,7 @@ const CylinderModule: React.FC = () => {
|
|||||||
const [height, setHeight] = React.useState(20);
|
const [height, setHeight] = React.useState(20);
|
||||||
const [surface, setSurface] = React.useState<'rough' | 'smooth'>('rough');
|
const [surface, setSurface] = React.useState<'rough' | 'smooth'>('rough');
|
||||||
const [endType, setEndType] = React.useState<CylinderEndType>('closed');
|
const [endType, setEndType] = React.useState<CylinderEndType>('closed');
|
||||||
|
const [viewMode, setViewMode] = React.useState<'solid' | 'airflow'>('solid');
|
||||||
|
|
||||||
const result = useMemo(() => {
|
const result = useMemo(() => {
|
||||||
return calculateCylinder({
|
return calculateCylinder({
|
||||||
@@ -169,24 +172,47 @@ const CylinderModule: React.FC = () => {
|
|||||||
|
|
||||||
{/* Coluna Central: 3D */}
|
{/* Coluna Central: 3D */}
|
||||||
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
||||||
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
<div className="absolute top-4 left-4 right-4 z-10 flex flex-col sm:flex-row sm:justify-between gap-3 pointer-events-none">
|
||||||
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/50 backdrop-blur-sm shadow-sm">
|
<div className="flex flex-wrap gap-2 pointer-events-auto">
|
||||||
h/d = {result.hOverD.toFixed(2)}
|
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/50 backdrop-blur-sm shadow-sm">
|
||||||
</Badge>
|
h/d = {result.hOverD.toFixed(2)}
|
||||||
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">
|
</Badge>
|
||||||
Re = {result.re.toExponential(2)}
|
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">
|
||||||
</Badge>
|
Re = {result.re.toExponential(2)}
|
||||||
</div>
|
</Badge>
|
||||||
<div className="absolute top-4 right-4 z-10">
|
</div>
|
||||||
<EducationalManual type="cylinder" params={{ surface, endType, cpi: result.cpi }} />
|
<div className="flex bg-background/80 backdrop-blur-sm rounded-md p-1 shadow-sm border border-border pointer-events-auto ml-auto">
|
||||||
|
<Button
|
||||||
|
variant={viewMode === 'solid' ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-3 text-xs"
|
||||||
|
onClick={() => setViewMode('solid')}
|
||||||
|
>
|
||||||
|
<Box className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
Sólido
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={viewMode === 'airflow' ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-3 text-xs"
|
||||||
|
onClick={() => setViewMode('airflow')}
|
||||||
|
>
|
||||||
|
<Wind className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
Fluxo
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 pointer-events-auto justify-start sm:justify-end">
|
||||||
|
<EducationalManual type="cylinder" params={{ surface, endType, cpi: result.cpi }} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PressureLegend />
|
<PressureLegend />
|
||||||
<div className="w-full h-full bg-muted/20">
|
<div className="w-full h-full cursor-grab active:cursor-grabbing bg-muted/20">
|
||||||
<Cylinder3DViewer
|
<Cylinder3DViewer
|
||||||
diameter={diameter}
|
diameter={diameter}
|
||||||
height={height}
|
height={height}
|
||||||
cpeProfile={result.profile}
|
cpeProfile={result.profile}
|
||||||
cpi={result.cpi}
|
cpi={result.cpi}
|
||||||
|
viewMode={viewMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { Slider } from '@/components/ui/slider';
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Box, Wind } from 'lucide-react';
|
||||||
import { useWindStore } from '@/store/appStore';
|
import { useWindStore } from '@/store/appStore';
|
||||||
import { CpiSettingsCard } from '@/components/CpiSettingsCard';
|
import { CpiSettingsCard } from '@/components/CpiSettingsCard';
|
||||||
import { calculateDome, type DomeType } from '@/lib/modules/dome';
|
import { calculateDome, type DomeType } from '@/lib/modules/dome';
|
||||||
@@ -22,6 +24,7 @@ const DomeModule: React.FC = () => {
|
|||||||
const [rise, setRise] = React.useState(5);
|
const [rise, setRise] = React.useState(5);
|
||||||
const [wallHeight, setWallHeight] = React.useState(8);
|
const [wallHeight, setWallHeight] = React.useState(8);
|
||||||
const [type, setType] = React.useState<DomeType>('on-ground');
|
const [type, setType] = React.useState<DomeType>('on-ground');
|
||||||
|
const [viewMode, setViewMode] = React.useState<'solid' | 'airflow'>('solid');
|
||||||
|
|
||||||
const result = useMemo(() => calculateDome({ d: diameter, f: rise, h: wallHeight, vk, type, cpi }), [diameter, rise, wallHeight, vk, type, cpi]);
|
const result = useMemo(() => calculateDome({ d: diameter, f: rise, h: wallHeight, vk, type, cpi }), [diameter, rise, wallHeight, vk, type, cpi]);
|
||||||
|
|
||||||
@@ -158,7 +161,27 @@ const DomeModule: React.FC = () => {
|
|||||||
Cpi = {cpi.toFixed(2)}
|
Cpi = {cpi.toFixed(2)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-4 right-4 z-10">
|
<div className="absolute top-4 right-4 z-10 flex flex-wrap gap-2 pointer-events-auto justify-end">
|
||||||
|
<div className="flex bg-background/80 backdrop-blur-sm rounded-md p-1 shadow-sm border border-border">
|
||||||
|
<Button
|
||||||
|
variant={viewMode === 'solid' ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-3 text-xs"
|
||||||
|
onClick={() => setViewMode('solid')}
|
||||||
|
>
|
||||||
|
<Box className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
Sólido
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={viewMode === 'airflow' ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-3 text-xs"
|
||||||
|
onClick={() => setViewMode('airflow')}
|
||||||
|
>
|
||||||
|
<Wind className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
Fluxo
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<EducationalManual type="dome" params={{ rise, diameter, cpi }} />
|
<EducationalManual type="dome" params={{ rise, diameter, cpi }} />
|
||||||
</div>
|
</div>
|
||||||
<PressureLegend />
|
<PressureLegend />
|
||||||
@@ -171,6 +194,7 @@ const DomeModule: React.FC = () => {
|
|||||||
cpeBarlavento={result.cpeBarlavento}
|
cpeBarlavento={result.cpeBarlavento}
|
||||||
cpeTopo={result.cpeTopo}
|
cpeTopo={result.cpeTopo}
|
||||||
cpeLateral={result.cpeLateral}
|
cpeLateral={result.cpeLateral}
|
||||||
|
viewMode={viewMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { usePersistentState } from '@/hooks/usePersistentState';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
@@ -34,7 +35,7 @@ import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
|||||||
|
|
||||||
const DynamicsModule: React.FC = () => {
|
const DynamicsModule: React.FC = () => {
|
||||||
const { v0, s1, s3, terrainCategory, structureClass, q } = useWindStore();
|
const { v0, s1, s3, terrainCategory, structureClass, q } = useWindStore();
|
||||||
const [activeTab, setActiveTab] = useState('edificio');
|
const [activeTab, setActiveTab] = usePersistentState('DynamicsModule_activeTab', 'edificio');
|
||||||
|
|
||||||
const [structureType, setStructureType] = React.useState<StructureDynamicType>('concrete-shearwall');
|
const [structureType, setStructureType] = React.useState<StructureDynamicType>('concrete-shearwall');
|
||||||
const [height, setHeight] = React.useState(40);
|
const [height, setHeight] = React.useState(40);
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Warehouse3DViewer from '../components/Warehouse3D';
|
import Warehouse3DViewer from '../components/Warehouse3D';
|
||||||
|
import Heatmap2D from '../components/Heatmap2D';
|
||||||
import LinearLoadsTable from '../components/LinearLoadsTable';
|
import LinearLoadsTable from '../components/LinearLoadsTable';
|
||||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||||
import FtoolExportCard from '../components/FtoolExportCard';
|
import FtoolExportCard from '../components/FtoolExportCard';
|
||||||
import { CpiSettingsCard } from '../components/CpiSettingsCard';
|
import { CpiSettingsCard } from '../components/CpiSettingsCard';
|
||||||
import { useGalpaoStore } from '../store/galpaoStore';
|
import { useGalpaoStore } from '../store/galpaoStore';
|
||||||
import { useWindStore } from '../store/appStore';
|
import { useWindStore } from '../store/appStore';
|
||||||
|
import { getWallCpeOfficial as getWallCpe, getRoofCpeOfficial as getRoofCpe } from '../lib/coefficients';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { AlertCircle, Lightbulb, Box, Wind } from 'lucide-react';
|
import { AlertCircle, Lightbulb, Box, Wind } from 'lucide-react';
|
||||||
import ExportMenu from '../components/ExportMenu';
|
import ExportMenu from '../components/ExportMenu';
|
||||||
@@ -65,18 +67,25 @@ const GalpaoModule: React.FC = () => {
|
|||||||
return (
|
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="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
|
||||||
{/* Coluna Esquerda: Controles */}
|
{/* Coluna Esquerda: Controles */}
|
||||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
<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">
|
<Tabs defaultValue="geometria" className="w-full">
|
||||||
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
|
<TabsList className="grid w-full grid-cols-2 mb-4">
|
||||||
<div>
|
<TabsTrigger value="geometria">1. Geometria</TabsTrigger>
|
||||||
<CardTitle className="text-lg">Geometria</CardTitle>
|
<TabsTrigger value="permeabilidade">2. Permeabilidade</TabsTrigger>
|
||||||
<CardDescription>Tabelas 4 e 5 — NBR 6123.</CardDescription>
|
</TabsList>
|
||||||
</div>
|
|
||||||
<SaveModuleDialog
|
<TabsContent value="geometria" className="space-y-4 m-0">
|
||||||
moduleType="galpao"
|
<Card className="shadow-sm border-border">
|
||||||
inputs={{ width, length, height, roofPitch }}
|
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
|
||||||
/>
|
<div>
|
||||||
</CardHeader>
|
<CardTitle className="text-lg">Geometria e Vento</CardTitle>
|
||||||
|
<CardDescription>Tabelas 4 e 5 — NBR 6123.</CardDescription>
|
||||||
|
</div>
|
||||||
|
<SaveModuleDialog
|
||||||
|
moduleType="galpao"
|
||||||
|
inputs={{ width, length, height, roofPitch }}
|
||||||
|
/>
|
||||||
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
<WindParametersSummary />
|
<WindParametersSummary />
|
||||||
|
|
||||||
@@ -98,7 +107,7 @@ const GalpaoModule: React.FC = () => {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<AlertCircle className="w-4 h-4 text-amber-500 shrink-0 mt-0.5" />
|
<AlertCircle className="w-4 h-4 text-amber-500 shrink-0 mt-0.5" />
|
||||||
<p className="text-xs text-amber-600 dark:text-amber-400">
|
<p className="text-xs text-amber-600 dark:text-amber-400">
|
||||||
A platibanda bloqueia o fluxo. Calcule os esforços sobre ela no módulo de <b>Muros/Placas</b>.
|
A platibanda bloqueia o fluxo. Calcule os esforços nela no módulo de <b>Muros/Placas</b>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -243,55 +252,90 @@ const GalpaoModule: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<CpiSettingsCard />
|
<TabsContent value="permeabilidade" className="space-y-4 m-0">
|
||||||
|
<CpiSettingsCard />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Coluna Direita: Viewer 3D */}
|
{/* Coluna Central: Viewer 3D e Heatmap (Tab) */}
|
||||||
<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="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 right-4 z-10 flex flex-col sm:flex-row sm:justify-between gap-3 pointer-events-none">
|
<Tabs defaultValue="3d" className="w-full h-full flex flex-col">
|
||||||
<div className="flex flex-wrap gap-2 pointer-events-auto">
|
<div className="absolute top-4 left-4 right-4 z-10 flex flex-col sm:flex-row sm:justify-between gap-3 pointer-events-none">
|
||||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm border-muted-foreground/20 text-foreground shadow-sm">
|
<div className="flex flex-wrap gap-2 pointer-events-auto">
|
||||||
Vento a {windAngle}°
|
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm border-muted-foreground/20 text-foreground shadow-sm">
|
||||||
</Badge>
|
Vento a {windAngle}°
|
||||||
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">
|
</Badge>
|
||||||
Cpi = {cpi.toFixed(2)}
|
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">
|
||||||
</Badge>
|
Cpi = {cpi.toFixed(2)}
|
||||||
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">
|
</Badge>
|
||||||
q = {q.toFixed(3)} kN/m²
|
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">
|
||||||
</Badge>
|
q = {q.toFixed(3)} kN/m²
|
||||||
</div>
|
</Badge>
|
||||||
<div className="flex bg-background/80 backdrop-blur-sm rounded-md p-1 shadow-sm border border-border pointer-events-auto ml-auto">
|
</div>
|
||||||
<Button
|
|
||||||
variant={viewMode === 'solid' ? 'secondary' : 'ghost'}
|
<div className="flex pointer-events-auto bg-background/80 backdrop-blur-sm rounded-md p-1 shadow-sm border border-border">
|
||||||
size="sm"
|
<TabsList className="h-7 px-1">
|
||||||
className="h-7 px-3 text-xs"
|
<TabsTrigger value="3d" className="text-xs h-6 px-3">Modelo 3D</TabsTrigger>
|
||||||
onClick={() => setViewMode('solid')}
|
<TabsTrigger value="heatmap" className="text-xs h-6 px-3">Heatmap 2D</TabsTrigger>
|
||||||
>
|
<TabsTrigger value="compare" className="text-xs h-6 px-3">Modo A/B</TabsTrigger>
|
||||||
<Box className="w-3.5 h-3.5 mr-1.5" />
|
</TabsList>
|
||||||
Sólido
|
</div>
|
||||||
</Button>
|
|
||||||
<Button
|
<div className="flex flex-wrap gap-2 pointer-events-auto justify-start sm:justify-end ml-auto">
|
||||||
variant={viewMode === 'airflow' ? 'secondary' : 'ghost'}
|
<EducationalManual type="warehouse" params={{ windAngle, cpi }} />
|
||||||
size="sm"
|
</div>
|
||||||
className="h-7 px-3 text-xs"
|
|
||||||
onClick={() => setViewMode('airflow')}
|
|
||||||
>
|
|
||||||
<Wind className="w-3.5 h-3.5 mr-1.5" />
|
|
||||||
Fluxo
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2 pointer-events-auto justify-start sm:justify-end">
|
<TabsContent value="3d" className="flex-1 m-0 h-full w-full relative">
|
||||||
<EducationalManual type="warehouse" params={{ windAngle, cpi }} />
|
<div className="absolute bottom-20 left-4 z-20 flex bg-background/80 backdrop-blur-sm rounded-md p-1 shadow-sm border border-border pointer-events-auto">
|
||||||
</div>
|
<Button
|
||||||
</div>
|
variant={viewMode === 'solid' ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-3 text-xs"
|
||||||
|
onClick={() => setViewMode('solid')}
|
||||||
|
>
|
||||||
|
<Box className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
Sólido
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={viewMode === 'airflow' ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-3 text-xs"
|
||||||
|
onClick={() => setViewMode('airflow')}
|
||||||
|
>
|
||||||
|
<Wind className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
Fluxo
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<PressureLegend />
|
<PressureLegend />
|
||||||
|
<div className="w-full h-full cursor-grab active:cursor-grabbing bg-muted/20">
|
||||||
|
<Warehouse3DViewer />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<div className="w-full h-full cursor-grab active:cursor-grabbing bg-muted/20">
|
<TabsContent value="heatmap" className="flex-1 m-0 h-full w-full bg-muted/10 overflow-auto flex items-center justify-center">
|
||||||
<Warehouse3DViewer />
|
<Heatmap2D />
|
||||||
</div>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="compare" className="flex-1 m-0 h-full w-full bg-muted/10 overflow-auto p-4 flex flex-col">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 flex-1">
|
||||||
|
<Heatmap2D
|
||||||
|
overrideAngle={0}
|
||||||
|
overrideWallCpe={getWallCpe(length, width, height, 0)}
|
||||||
|
overrideRoofCpe={getRoofCpe(length, width, height, roofPitch, 0)}
|
||||||
|
/>
|
||||||
|
<Heatmap2D
|
||||||
|
overrideAngle={90}
|
||||||
|
overrideWallCpe={getWallCpe(length, width, height, 90)}
|
||||||
|
overrideRoofCpe={getRoofCpe(length, width, height, roofPitch, 90)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Coluna Direita-Inferior: Cargas Lineares (M9.2) */}
|
{/* Coluna Direita-Inferior: Cargas Lineares (M9.2) */}
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Warehouse, Cylinder, Church, CircleDot, Square, Layers, BarChart3, Frame, Tent, Calculator } from 'lucide-react';
|
||||||
|
import { useI18n } from '../store/i18nStore';
|
||||||
|
import { useWindStore } from '../store/appStore';
|
||||||
|
|
||||||
|
// Mocked icons for bridge and tower to match App.tsx
|
||||||
|
const BridgeIcon = (props: React.SVGProps<SVGSVGElement>) => (
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
|
||||||
|
<path d="M3 18V6l3-1 6 3 6-3 3 1v12" />
|
||||||
|
<path d="M3 10h18" />
|
||||||
|
<path d="M8 10v8M16 10v8" />
|
||||||
|
<path d="M3 14c4-2 6-2 10 0s6 2 8 0" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const TowerIcon = (props: React.SVGProps<SVGSVGElement>) => (
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
|
||||||
|
<path d="M6 22L10 2M18 22L14 2" />
|
||||||
|
<path d="M10 2h4" />
|
||||||
|
<path d="M8.5 7h7" />
|
||||||
|
<path d="M7.5 12h9" />
|
||||||
|
<path d="M6.5 17h11" />
|
||||||
|
<path d="M6 22h12" />
|
||||||
|
<path d="M10 2l4 5M12 7l-4 5" />
|
||||||
|
<path d="M10 12l5 5M14 12l-5 5" />
|
||||||
|
<path d="M9.5 17l5.5 5M14.5 17l-5.5 5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function HomeMock() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { v0, s1, s2, s3, vk, q, locality } = useWindStore();
|
||||||
|
|
||||||
|
const modules = [
|
||||||
|
{ to: '/galpao', icon: Warehouse, label: 'Galpão Retangular', desc: 'Paredes A/B/C/D, telhados E-J, excentricidade, atrito, alta turbulência.' },
|
||||||
|
{ to: '/cilindro', icon: Cylinder, label: 'Cilindro Vertical', desc: 'Silos, reservatórios, chaminés. Cpe por ângulo (Tab. 13), Reynolds.' },
|
||||||
|
{ to: '/abobada', icon: Church, label: 'Abóbada Cilíndrica', desc: 'Coberturas curvas em arco. Tab. 15-20, 6 zonas.' },
|
||||||
|
{ to: '/cupula', icon: CircleDot, label: 'Cúpula', desc: 'Sobre terreno (Tab. 21) ou parede cilíndrica (Tab. 22).' },
|
||||||
|
{ to: '/muros', icon: Square, label: 'Muros e Placas', desc: 'Cf para vento perpendicular e oblíquo (Tab. 23).' },
|
||||||
|
{ to: '/cobertura-isolada', icon: Layers, label: 'Coberturas Isoladas', desc: 'Uma ou duas águas, abas perpendiculares (Tab. 24-25).' },
|
||||||
|
{ to: '/barras', icon: BarChart3, label: 'Barras Prismáticas', desc: 'Faces planas (Tab. 26) ou circulares (Tab. 27).' },
|
||||||
|
{ to: '/pontes', icon: BridgeIcon, label: 'Pontes', desc: 'Pse, Cx/Cz do tabuleiro, flutter, galope (sec. 11).' },
|
||||||
|
{ to: '/torre', icon: TowerIcon, label: 'Torres', desc: 'Torres treliçadas e mastros (sec. 8.4).' },
|
||||||
|
{ to: '/piperack', icon: Frame, label: 'Pipe-Racks Industriais', desc: 'Estruturas reticuladas múltiplas, tubulações, fator η (Tab. 28).' },
|
||||||
|
{ to: '/abrigo', icon: Tent, label: 'Abrigos e Pórticos Fechados', desc: 'Marquises, abrigos e pórticos. 4 condições topológicas, frestas de alívio, corte A-A (Tab. 23-25, sec. 6.3).' },
|
||||||
|
{ to: '/calc-ca', icon: Calculator, label: 'Calculadora de C_a', desc: 'Coeficiente de arrasto (Fig. 4 e 5).' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-8 max-w-6xl mx-auto space-y-8 animate-in fade-in duration-500">
|
||||||
|
<header className="text-center space-y-4 flex flex-col items-center mt-4 mb-6">
|
||||||
|
<div className="relative inline-block">
|
||||||
|
<img src="/logo_brainwind.png" alt="BrainWind" className="h-20 md:h-28 w-auto object-contain dark:drop-shadow-[0_0_15px_rgba(255,255,255,0.4)]" />
|
||||||
|
<span className="absolute -bottom-1 -right-2 md:-right-6 bg-primary text-primary-foreground text-[10px] md:text-xs font-bold px-2 py-0.5 rounded-full shadow-sm border border-primary-foreground/20">v1.1</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground text-lg">
|
||||||
|
{t('app_subtitle')}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Dashboard de Vento Básico */}
|
||||||
|
<div className="bg-card border rounded-xl p-6 shadow-sm">
|
||||||
|
<h3 className="font-semibold text-lg mb-4 text-card-foreground">Parâmetros Atuais do Vento {locality ? `(${locality})` : ''}</h3>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-6 gap-4">
|
||||||
|
<div className="flex flex-col items-center justify-center p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<span className="text-xs text-muted-foreground">V₀ (m/s)</span>
|
||||||
|
<span className="font-bold text-xl">{v0.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center justify-center p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<span className="text-xs text-muted-foreground">S₁</span>
|
||||||
|
<span className="font-bold text-xl">{s1.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center justify-center p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<span className="text-xs text-muted-foreground">S₂</span>
|
||||||
|
<span className="font-bold text-xl">{s2.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center justify-center p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<span className="text-xs text-muted-foreground">S₃</span>
|
||||||
|
<span className="font-bold text-xl">{s3.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center justify-center p-3 bg-muted/50 rounded-lg border">
|
||||||
|
<span className="text-xs text-muted-foreground">Vₖ (m/s)</span>
|
||||||
|
<span className="font-bold text-xl text-primary">{vk.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center justify-center p-3 bg-primary/10 rounded-lg border border-primary/20">
|
||||||
|
<span className="text-xs text-primary/80 font-medium">q (kN/m²)</span>
|
||||||
|
<span className="font-bold text-2xl text-primary">{q.toFixed(3)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{modules.map((m) => (
|
||||||
|
<Link key={m.to} to={m.to} className="block p-6 rounded-xl border bg-card hover:shadow-lg hover:border-primary transition-all">
|
||||||
|
<m.icon className="w-8 h-8 text-primary mb-3" />
|
||||||
|
<h2 className="font-semibold text-lg mb-1">{m.label}</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">{m.desc}</p>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border bg-muted/40 p-4 text-sm text-muted-foreground">
|
||||||
|
<p className="font-medium text-foreground mb-2">{t('home_full_coverage')}</p>
|
||||||
|
<ul className="grid grid-cols-2 md:grid-cols-4 gap-1 text-xs">
|
||||||
|
<li>✓ Sec. 5 — V₀, S₁, S₂, S₃</li>
|
||||||
|
<li>✓ Sec. 6.1 — Paralelepipédicas</li>
|
||||||
|
<li>✓ Sec. 6.2 — Cilindros, abóbadas, cúpulas</li>
|
||||||
|
<li>✓ Sec. 6.3 — Pressão interna (Cpi)</li>
|
||||||
|
<li>✓ Sec. 6.4 — Vizinhança</li>
|
||||||
|
<li>✓ Sec. 7 — Muros, coberturas isoladas</li>
|
||||||
|
<li>✓ Sec. 8 — Barras e reticulados</li>
|
||||||
|
<li>✓ Sec. 9 — Efeitos dinâmicos</li>
|
||||||
|
<li>✓ Sec. 10 — Vórtices</li>
|
||||||
|
<li>✓ Sec. 11 — Pontes</li>
|
||||||
|
<li>✓ Anexo A — S₂(qualquer t)</li>
|
||||||
|
<li>✓ Anexo B — S₃(Pₘ, vida útil)</li>
|
||||||
|
<li>✓ Anexo C — 49 estações</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import React, { useMemo } from 'react';
|
import { usePersistentState } from '@/hooks/usePersistentState';
|
||||||
|
import React, { useMemo, useState } from 'react';
|
||||||
|
import { Box, Wind } from 'lucide-react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { useWindStore } from '@/store/appStore';
|
import { useWindStore } from '@/store/appStore';
|
||||||
import { calculateIsolatedShedRoof, calculateIsolatedGableRoof, frictionForceIsolatedRoof } from '@/lib/nbr-tables/table-24-25';
|
import { calculateIsolatedShedRoof, calculateIsolatedGableRoof, calculateIsolatedButterflyRoof, frictionForceIsolatedRoof } from '@/lib/nbr-tables/table-24-25';
|
||||||
import IsolatedRoof3DViewer from '@/components/three/IsolatedRoof3D';
|
import IsolatedRoof3DViewer from '@/components/three/IsolatedRoof3D';
|
||||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||||
import ExportMenu from '../components/ExportMenu';
|
import ExportMenu from '../components/ExportMenu';
|
||||||
@@ -17,17 +19,22 @@ import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generi
|
|||||||
|
|
||||||
const IsolatedRoofModule: React.FC = () => {
|
const IsolatedRoofModule: React.FC = () => {
|
||||||
const { q } = useWindStore();
|
const { q } = useWindStore();
|
||||||
const [type, setType] = React.useState<'shed' | 'gable'>('shed');
|
const [type, setType] = React.useState<'shed' | 'gable' | 'butterfly'>('shed');
|
||||||
const [theta, setTheta] = React.useState(15);
|
const [theta, setTheta] = React.useState(15);
|
||||||
const [height, setHeight] = React.useState(2);
|
const [height, setHeight] = React.useState(2);
|
||||||
const [width, setWidth] = React.useState(10);
|
const [width, setWidth] = usePersistentState('IsolatedRoofModule_width', 10);
|
||||||
const [depth, setDepth] = React.useState(10);
|
const [depth, setDepth] = usePersistentState('IsolatedRoofModule_depth', 10);
|
||||||
|
const [viewMode, setViewMode] = usePersistentState<'solid' | 'airflow'>('IsolatedRoofModule_viewMode', 'solid');
|
||||||
|
|
||||||
const result = useMemo(() => {
|
const result = useMemo(() => {
|
||||||
if (type === 'shed') {
|
if (type === 'shed') {
|
||||||
const r = calculateIsolatedShedRoof({ theta, height, depth });
|
const r = calculateIsolatedShedRoof({ theta, height, depth });
|
||||||
return { ...r, cpb: { cpb1: 0, cpb2: 0 }, cpa: { cpa1: 0, cpa2: 0 } };
|
return { ...r, cpb: { cpb1: 0, cpb2: 0 }, cpa: { cpa1: 0, cpa2: 0 } };
|
||||||
}
|
}
|
||||||
|
if (type === 'butterfly') {
|
||||||
|
const b = calculateIsolatedButterflyRoof({ theta, height, depth });
|
||||||
|
return { ...b, cph1: { high: 0, low: 0 }, cph2: { high: 0, low: 0 } };
|
||||||
|
}
|
||||||
const g = calculateIsolatedGableRoof({ theta, height, depth });
|
const g = calculateIsolatedGableRoof({ theta, height, depth });
|
||||||
return { ...g, cph1: { high: 0, low: 0 }, cph2: { high: 0, low: 0 } };
|
return { ...g, cph1: { high: 0, low: 0 }, cph2: { high: 0, low: 0 } };
|
||||||
}, [type, theta, height, depth]);
|
}, [type, theta, height, depth]);
|
||||||
@@ -79,12 +86,15 @@ const IsolatedRoofModule: React.FC = () => {
|
|||||||
title: 'Geometria da Cobertura Isolada',
|
title: 'Geometria da Cobertura Isolada',
|
||||||
type: 'grid',
|
type: 'grid',
|
||||||
gridItems: [
|
gridItems: [
|
||||||
{ label: 'Tipo', value: type === 'shed' ? 'Uma água' : 'Duas águas' },
|
{ label: 'Tipo', value: type === 'shed' ? 'Uma água' : type === 'gable' ? 'Duas águas' : 'Borboleta (Extrapolação)' },
|
||||||
{ label: 'Inclinação (θ)', value: `${theta}°` },
|
{ label: 'Inclinação (θ)', value: `${theta}°` },
|
||||||
{ label: 'Largura (b)', value: `${width} m` },
|
{ label: 'Largura (b)', value: `${width} m` },
|
||||||
{ label: 'Profundidade (ℓ)', value: `${depth} m` },
|
{ label: 'Profundidade (ℓ)', value: `${depth} m` },
|
||||||
{ label: 'Altura livre (h)', value: `${height} m` },
|
{ label: 'Altura livre (h)', value: `${height} m` },
|
||||||
{ label: 'Aplicável à norma', value: result.applies ? 'Sim' : 'Não' },
|
{
|
||||||
|
label: 'Aplicável à norma',
|
||||||
|
value: result.applies ? 'Sim' : type === 'butterfly' ? 'Extrapolado (Não há tabela)' : 'Não'
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -116,7 +126,7 @@ const IsolatedRoofModule: React.FC = () => {
|
|||||||
{
|
{
|
||||||
title: '1. Coeficientes de Pressão Líquida (Cpn) e Carregamentos',
|
title: '1. Coeficientes de Pressão Líquida (Cpn) e Carregamentos',
|
||||||
formula: 'Cpn = Cp_superior − Cp_inferior (Tabelas 24 e 25 NBR 6123)',
|
formula: 'Cpn = Cp_superior − Cp_inferior (Tabelas 24 e 25 NBR 6123)',
|
||||||
calculation: `Para inclinação θ = ${theta}° (${type === 'shed' ? 'Uma água' : 'Duas águas'}), os coeficientes são definidos por zona e por carregamento máximo/mínimo.`,
|
calculation: `Para inclinação θ = ${theta}° (${type === 'shed' ? 'Uma água' : type === 'gable' ? 'Duas águas' : 'Borboleta'}), os coeficientes são definidos por zona e por carregamento máximo/mínimo.`,
|
||||||
result: `θ = ${theta}° (Tab. ${type === 'shed' ? '24' : '25'})`,
|
result: `θ = ${theta}° (Tab. ${type === 'shed' ? '24' : '25'})`,
|
||||||
note: 'Coberturas isoladas estão sujeitas a descolamento violento de fluxo, exigindo verificação para sucção (arrancamento) e compressão.',
|
note: 'Coberturas isoladas estão sujeitas a descolamento violento de fluxo, exigindo verificação para sucção (arrancamento) e compressão.',
|
||||||
},
|
},
|
||||||
@@ -159,11 +169,14 @@ const IsolatedRoofModule: React.FC = () => {
|
|||||||
<WindParametersSummary />
|
<WindParametersSummary />
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Tipo de Cobertura</label>
|
<label className="text-sm font-medium">Tipo de Cobertura</label>
|
||||||
<Select value={type} onValueChange={(v) => setType(v as 'shed' | 'gable')}>
|
<Select value={type} onValueChange={(v: any) => setType(v)}>
|
||||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Tipo de Cobertura" />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="shed">Uma água (Tab. 24)</SelectItem>
|
<SelectItem value="shed">Uma Água</SelectItem>
|
||||||
<SelectItem value="gable">Duas águas (Tab. 25)</SelectItem>
|
<SelectItem value="gable">Duas Águas</SelectItem>
|
||||||
|
<SelectItem value="butterfly">Telhado Borboleta (Em V)</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -200,7 +213,25 @@ const IsolatedRoofModule: React.FC = () => {
|
|||||||
<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="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
||||||
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
||||||
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/80 backdrop-blur-sm shadow-sm">θ = {theta}°</Badge>
|
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/80 backdrop-blur-sm shadow-sm">θ = {theta}°</Badge>
|
||||||
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/80 backdrop-blur-sm shadow-sm">{type === 'shed' ? 'Uma água' : 'Duas águas'}</Badge>
|
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/80 backdrop-blur-sm shadow-sm">{type === 'shed' ? 'Uma água' : type === 'gable' ? 'Duas águas' : 'Borboleta'}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-4 right-16 z-10">
|
||||||
|
<div className="flex bg-background/80 backdrop-blur-sm border border-border/80 rounded-md shadow-sm p-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('solid')}
|
||||||
|
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'solid' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
|
||||||
|
>
|
||||||
|
<Box className="w-3.5 h-3.5 inline-block mr-1.5" />
|
||||||
|
Sólido
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('airflow')}
|
||||||
|
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'airflow' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
|
||||||
|
>
|
||||||
|
<Wind className="w-3.5 h-3.5 inline-block mr-1.5" />
|
||||||
|
Fluxo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-4 right-4 z-10">
|
<div className="absolute top-4 right-4 z-10">
|
||||||
<EducationalManual type="isolated-roof" params={{ theta, type }} />
|
<EducationalManual type="isolated-roof" params={{ theta, type }} />
|
||||||
@@ -216,6 +247,7 @@ const IsolatedRoofModule: React.FC = () => {
|
|||||||
cpeLeeward={cpeLeeward}
|
cpeLeeward={cpeLeeward}
|
||||||
cpeTop={cpeTop}
|
cpeTop={cpeTop}
|
||||||
forceKN={forces.magnitude}
|
forceKN={forces.magnitude}
|
||||||
|
viewMode={viewMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import { usePersistentState } from '@/hooks/usePersistentState';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
|
|
||||||
import { useHydrationStore } from '@/store/hydrationStore';
|
import { useHydrationStore } from '@/store/hydrationStore';
|
||||||
@@ -8,25 +9,27 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { useWindStore } from '@/store/appStore';
|
import { useWindStore } from '@/store/appStore';
|
||||||
import { calculatePiperack, type PipeDef } from '@/lib/modules/piperack';
|
import { calculatePiperack, type PipeDef } from '@/lib/modules/piperack';
|
||||||
|
import { isResonantRisk } from '@/lib/dynamics';
|
||||||
import { Piperack3D } from '@/components/three/Piperack3D';
|
import { Piperack3D } from '@/components/three/Piperack3D';
|
||||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||||
import ExportMenu from '../components/ExportMenu';
|
import ExportMenu from '../components/ExportMenu';
|
||||||
import { EducationalManual } from '@/components/EducationalManual';
|
import { EducationalManual } from '@/components/EducationalManual';
|
||||||
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
||||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||||
import { Plus, Trash2, Info } from 'lucide-react';
|
import { Plus, Trash2, Info, AlertTriangle } from 'lucide-react';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/components/ui/tooltip';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
|
||||||
const PiperackModule: React.FC = () => {
|
const PiperackModule: React.FC = () => {
|
||||||
const { q } = useWindStore();
|
const { q } = useWindStore();
|
||||||
const [width, setWidth] = useState(17);
|
const [width, setWidth] = usePersistentState('PiperackModule_width', 17);
|
||||||
const [height, setHeight] = useState(2);
|
const [height, setHeight] = usePersistentState('PiperackModule_height', 2);
|
||||||
const [elevation, setElevation] = useState(6);
|
const [elevation, setElevation] = usePersistentState('PiperackModule_elevation', 6);
|
||||||
const [spacing, setSpacing] = useState(2);
|
const [spacing, setSpacing] = usePersistentState('PiperackModule_spacing', 2);
|
||||||
const [numFrames, setNumFrames] = useState(2);
|
const [numFrames, setNumFrames] = usePersistentState('PiperackModule_numFrames', 2);
|
||||||
const [phiStruct, setPhiStruct] = useState(0.5);
|
const [phiStruct, setPhiStruct] = usePersistentState('PiperackModule_phiStruct', 0.5);
|
||||||
const [pipes, setPipes] = useState<PipeDef[]>([]);
|
const [f1, setF1] = usePersistentState('PiperackModule_f1', 1.5); // Frequência natural (Hz)
|
||||||
|
const [pipes, setPipes] = usePersistentState<PipeDef[]>('PiperackModule_pipes', []);
|
||||||
|
|
||||||
const pendingLoad = useHydrationStore((s) => s.pendingLoad);
|
const pendingLoad = useHydrationStore((s) => s.pendingLoad);
|
||||||
const clearPendingLoad = useHydrationStore((s) => s.clearPendingLoad);
|
const clearPendingLoad = useHydrationStore((s) => s.clearPendingLoad);
|
||||||
@@ -41,6 +44,7 @@ const PiperackModule: React.FC = () => {
|
|||||||
if (p.numFrames !== undefined) setNumFrames(p.numFrames);
|
if (p.numFrames !== undefined) setNumFrames(p.numFrames);
|
||||||
if (p.pipes !== undefined) setPipes(p.pipes);
|
if (p.pipes !== undefined) setPipes(p.pipes);
|
||||||
if (p.phiStruct !== undefined) setPhiStruct(p.phiStruct);
|
if (p.phiStruct !== undefined) setPhiStruct(p.phiStruct);
|
||||||
|
if (p.f1 !== undefined) setF1(p.f1);
|
||||||
clearPendingLoad();
|
clearPendingLoad();
|
||||||
}
|
}
|
||||||
}, [pendingLoad, clearPendingLoad]);
|
}, [pendingLoad, clearPendingLoad]);
|
||||||
@@ -157,7 +161,7 @@ const PiperackModule: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<SaveModuleDialog
|
<SaveModuleDialog
|
||||||
moduleType="piperack"
|
moduleType="piperack"
|
||||||
inputs={{ elevation, width, height, spacing, numFrames, pipes, phiStruct }}
|
inputs={{ elevation, width, height, spacing, numFrames, pipes, phiStruct, f1 }}
|
||||||
/>
|
/>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
@@ -219,6 +223,33 @@ const PiperackModule: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Slider value={[phiStruct]} min={0.1} max={1.0} step={0.05} onValueChange={v => setPhiStruct(v[0])} />
|
<Slider value={[phiStruct]} min={0.1} max={1.0} step={0.05} onValueChange={v => setPhiStruct(v[0])} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium">Freq. Natural (f₁)</span>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="max-w-xs">
|
||||||
|
<p>Frequência fundamental no sentido do vento (Hz). Valores abaixo de 1 Hz indicam risco dinâmico elevado.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-muted-foreground">{f1.toFixed(1)} Hz</span>
|
||||||
|
</div>
|
||||||
|
<Slider value={[f1]} min={0.1} max={5.0} step={0.1} onValueChange={v => setF1(v[0])} />
|
||||||
|
|
||||||
|
{isResonantRisk(f1) && (
|
||||||
|
<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>Estrutura flexível (f₁ < 1 Hz). O vento produzirá amplificação dinâmica (C.A.D) considerável. É exigida análise pela NBR 6123 Capítulo 9.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useRef, useState } from 'react';
|
import { usePersistentState } from '@/hooks/usePersistentState';
|
||||||
|
import React, { useRef } from 'react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Sun, Moon, Laptop, Save, Trash2, Upload, FlaskConical, Download, FolderOpen, FileBox, FileJson, GraduationCap } from 'lucide-react';
|
import { Sun, Moon, Laptop, Save, Trash2, Upload, FlaskConical, Download, FolderOpen, FileBox, FileJson, GraduationCap } from 'lucide-react';
|
||||||
@@ -28,7 +29,7 @@ const SettingsModule: React.FC = () => {
|
|||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const logoInputRef = useRef<HTMLInputElement | null>(null);
|
const logoInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const [importing, setImporting] = useState(false);
|
const [importing, setImporting] = usePersistentState('SettingsModule_importing', false);
|
||||||
const { companyLogo, setCompanyLogo } = useWindStore();
|
const { companyLogo, setCompanyLogo } = useWindStore();
|
||||||
|
|
||||||
const exportSnapshot = () => {
|
const exportSnapshot = () => {
|
||||||
|
|||||||
+105
-33
@@ -1,9 +1,11 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import { usePersistentState } from '@/hooks/usePersistentState';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useWindStore } from '@/store/appStore';
|
import { useWindStore } from '@/store/appStore';
|
||||||
|
import { useHydrationStore } from '@/store/hydrationStore';
|
||||||
import {
|
import {
|
||||||
calculateShelterWind,
|
calculateShelterWind,
|
||||||
calculateShelterWindAllAngles,
|
calculateShelterWindAllAngles,
|
||||||
@@ -19,7 +21,7 @@ import { EducationalManual } from '@/components/EducationalManual';
|
|||||||
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
||||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||||
import { Eye, Box, Layers, ArrowUpRight, CheckCircle2 } from 'lucide-react';
|
import { Eye, Box, Layers, ArrowUpRight, CheckCircle2, Wind, AlertTriangle } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const conditionLabels: Record<ShelterCondition, string> = {
|
const conditionLabels: Record<ShelterCondition, string> = {
|
||||||
@@ -31,17 +33,40 @@ const conditionLabels: Record<ShelterCondition, string> = {
|
|||||||
|
|
||||||
const ShelterModule: React.FC = () => {
|
const ShelterModule: React.FC = () => {
|
||||||
const { q } = useWindStore();
|
const { q } = useWindStore();
|
||||||
const [condition, setCondition] = useState<ShelterCondition>('cond1');
|
const [condition, setCondition] = usePersistentState<ShelterCondition>('ShelterModule_condition', 'cond1');
|
||||||
const [windAngle, setWindAngle] = useState<ShelterWindAngle>(0);
|
const [windAngle, setWindAngle] = usePersistentState<ShelterWindAngle>('ShelterModule_windAngle', 0);
|
||||||
const [depth, setDepth] = useState(6);
|
const [depth, setDepth] = usePersistentState('ShelterModule_depth', 6);
|
||||||
const [width, setWidth] = useState(10);
|
const [width, setWidth] = usePersistentState('ShelterModule_width', 10);
|
||||||
const [height, setHeight] = useState(4.8);
|
const [height, setHeight] = usePersistentState('ShelterModule_height', 4.8);
|
||||||
const [theta, setTheta] = useState(5);
|
const [theta, setTheta] = usePersistentState('ShelterModule_theta', 5);
|
||||||
const [backClosure, setBackClosure] = useState(75);
|
const [numColumns, setNumColumns] = usePersistentState('ShelterModule_numColumns', 2);
|
||||||
const [backRange, setBackRange] = useState<[number, number]>([0, 75]);
|
const [backClosure, setBackClosure] = usePersistentState('ShelterModule_backClosure', 75);
|
||||||
const [leftClosure, setLeftClosure] = useState(100);
|
const [backRange, setBackRange] = usePersistentState<[number, number]>('ShelterModule_backRange', [0, 75]);
|
||||||
const [rightClosure, setRightClosure] = useState(100);
|
const [leftClosure, setLeftClosure] = usePersistentState('ShelterModule_leftClosure', 100);
|
||||||
const [viewMode, setViewMode] = useState<'3d' | 'elevation'>('3d');
|
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)
|
// Determinação automática da posição da fresta/vão a partir dos cursores (base e topo)
|
||||||
const bottomGap = backRange[0];
|
const bottomGap = backRange[0];
|
||||||
@@ -90,17 +115,21 @@ const ShelterModule: React.FC = () => {
|
|||||||
leftClosure,
|
leftClosure,
|
||||||
rightClosure,
|
rightClosure,
|
||||||
openingPos,
|
openingPos,
|
||||||
|
numColumns,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
result: calculateShelterWind({ ...input, windAngle }, q),
|
result: calculateShelterWind({ ...input, windAngle }, q),
|
||||||
envelope: calculateShelterWindAllAngles(input, q),
|
envelope: calculateShelterWindAllAngles(input, q),
|
||||||
};
|
};
|
||||||
}, [condition, q, depth, width, height, theta, backClosure, backRange, leftClosure, rightClosure, openingPos, windAngle]);
|
}, [condition, q, depth, width, height, theta, backClosure, backRange, leftClosure, rightClosure, openingPos, windAngle, numColumns]);
|
||||||
|
|
||||||
const effAreaRoof = depth * width;
|
const effAreaRoof = depth * width;
|
||||||
const effAreaBack = (width * height) * (backClosure / 100);
|
const effAreaBack = (width * height) * (backClosure / 100);
|
||||||
const effAreaSideTotal = (depth * height) * ((leftClosure + rightClosure) / 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 handleExportPDF = () => {
|
||||||
const angleLabel = windAngle === 0 ? '0° (Frontal)' : windAngle === 45 ? '45° (Oblíquo)' : '90° (Lateral)';
|
const angleLabel = windAngle === 0 ? '0° (Frontal)' : windAngle === 45 ? '45° (Oblíquo)' : '90° (Lateral)';
|
||||||
const sections: GenericPDFSection[] = [
|
const sections: GenericPDFSection[] = [
|
||||||
@@ -122,12 +151,13 @@ const ShelterModule: React.FC = () => {
|
|||||||
title: `Coeficientes Aerodinâmicos e Pressões Líquidas - Direção ${angleLabel}`,
|
title: `Coeficientes Aerodinâmicos e Pressões Líquidas - Direção ${angleLabel}`,
|
||||||
type: 'grid',
|
type: 'grid',
|
||||||
gridItems: [
|
gridItems: [
|
||||||
{ label: 'Ce Superior (Cobertura - Tab. 24)', value: result.cpeTop },
|
{ 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: 'Cpi Inferior (Sobrepressão sob Telhado)', value: result.cpiBottom },
|
||||||
{ label: 'Cnf Resultante (Ce - Cpi)', value: result.cnfVertical },
|
{ 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 Parede de Fundo (Tab. 23 / 6)', value: result.cfBack },
|
||||||
{ label: 'Cf Paredes Laterais', value: result.cfSide },
|
{ label: 'Cf Paredes Laterais', value: result.cfSide },
|
||||||
{ label: 'Alívio por Fresta Superior', value: `${result.reliefPercentage}%` },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -220,11 +250,11 @@ const ShelterModule: React.FC = () => {
|
|||||||
`Máx: +${Math.max(envelope.res0.cpiBottom, envelope.res45.cpiBottom, envelope.res90.cpiBottom).toFixed(2)}`
|
`Máx: +${Math.max(envelope.res0.cpiBottom, envelope.res45.cpiBottom, envelope.res90.cpiBottom).toFixed(2)}`
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Cnf Vertical Líquido',
|
'Cnf Vertical Médio | Borda',
|
||||||
String(envelope.res0.cnfVertical),
|
`${envelope.res0.cnfVertical} | ${envelope.res0.cnfEdgeVertical}`,
|
||||||
String(envelope.res45.cnfVertical),
|
`${envelope.res45.cnfVertical} | ${envelope.res45.cnfEdgeVertical}`,
|
||||||
String(envelope.res90.cnfVertical),
|
`${envelope.res90.cnfVertical} | ${envelope.res90.cnfEdgeVertical}`,
|
||||||
`Crítico: ${Math.min(envelope.res0.cnfVertical, envelope.res45.cnfVertical, envelope.res90.cnfVertical)}`
|
`Crítico Borda: ${Math.min(envelope.res0.cnfEdgeVertical, envelope.res45.cnfEdgeVertical, envelope.res90.cnfEdgeVertical)}`
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Força Vertical Telhado [Fz ↑]',
|
'Força Vertical Telhado [Fz ↑]',
|
||||||
@@ -282,6 +312,13 @@ const ShelterModule: React.FC = () => {
|
|||||||
`${envelope.res90.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`
|
`${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`
|
||||||
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -298,14 +335,19 @@ const ShelterModule: React.FC = () => {
|
|||||||
title: `Cargas Lineares para Dimensionamento (${angleLabel}) (kN/m)`,
|
title: `Cargas Lineares para Dimensionamento (${angleLabel}) (kN/m)`,
|
||||||
type: 'grid',
|
type: 'grid',
|
||||||
gridItems: [
|
gridItems: [
|
||||||
{ label: 'Carga Horizontal no Pilar Traseiro', value: `${result.lineLoads.columnLoad} kN/m` },
|
{ label: 'Carga Horiz. Pórtico Central', value: `${result.lineLoads.columnLoad} kN/m` },
|
||||||
{ label: 'Carga Vertical na Viga em Balanço', value: `${result.lineLoads.beamLoad} 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 do Regime Topológico e Aerodinâmico',
|
title: 'Parecer Técnico e Alertas Aerodinâmicos (Zonas de Borda)',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
content: `${result.statusText} Permeabilidade líquida da parede de fundo: ${100 - backClosure}%. Alívio de arrancamento obtido pela fresta superior: ${result.reliefPercentage}%. O relatório acima apresenta a tabela comparativa das 3 incidências de vento (0°, 45° e 90°) para garantir a verificação da envoltória mais desfavorável conforme a NBR 6123:2023. As cargas lineares calculadas (kN/m) podem ser aplicadas diretamente em softwares de pórtico plano como Ftool, SAP2000 ou CYPE 3D.`,
|
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',
|
title: 'Guia de Aplicação dos Eixos e Resultantes no Croqui',
|
||||||
@@ -351,7 +393,7 @@ const ShelterModule: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<SaveModuleDialog
|
<SaveModuleDialog
|
||||||
moduleType="shelter"
|
moduleType="shelter"
|
||||||
inputs={{ condition, depth, width, height, theta, backClosure, leftClosure, rightClosure, openingPos, windAngle }}
|
inputs={{ condition, depth, width, height, theta, backClosure, leftClosure, rightClosure, openingPos, windAngle, surfaceMass, numColumns }}
|
||||||
/>
|
/>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-5">
|
<CardContent className="space-y-5">
|
||||||
@@ -476,6 +518,28 @@ const ShelterModule: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Slider min={0} max={30} step={1} value={[theta]} onValueChange={(v) => setTheta(v[0])} />
|
<Slider min={0} max={30} step={1} value={[theta]} onValueChange={(v) => setTheta(v[0])} />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -502,6 +566,9 @@ const ShelterModule: React.FC = () => {
|
|||||||
<Button size="sm" variant={viewMode === '3d' ? 'default' : 'ghost'} className="h-6 px-2.5 text-xs rounded-full" onClick={() => setViewMode('3d')}>
|
<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
|
<Box className="size-3 mr-1" /> Vista 3D
|
||||||
</Button>
|
</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')}>
|
<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
|
<Eye className="size-3 mr-1" /> Corte A-A
|
||||||
</Button>
|
</Button>
|
||||||
@@ -512,7 +579,7 @@ const ShelterModule: React.FC = () => {
|
|||||||
<EducationalManual type="shelter" params={{ condition, backClosure, openingPos, theta, windAngle }} />
|
<EducationalManual type="shelter" params={{ condition, backClosure, openingPos, theta, windAngle }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{viewMode === '3d' ? (
|
{viewMode === '3d' || viewMode === 'airflow' ? (
|
||||||
<Shelter3D
|
<Shelter3D
|
||||||
condition={condition}
|
condition={condition}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
@@ -525,6 +592,7 @@ const ShelterModule: React.FC = () => {
|
|||||||
rightClosure={rightClosure}
|
rightClosure={rightClosure}
|
||||||
openingPos={openingPos}
|
openingPos={openingPos}
|
||||||
windAngle={windAngle}
|
windAngle={windAngle}
|
||||||
|
numColumns={numColumns}
|
||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -551,8 +619,8 @@ const ShelterModule: React.FC = () => {
|
|||||||
<span className="font-mono font-bold text-foreground">{result.forces.fzUp.toFixed(2)} kN</span>
|
<span className="font-mono font-bold text-foreground">{result.forces.fzUp.toFixed(2)} kN</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center text-xs text-muted-foreground">
|
<div className="flex justify-between items-center text-xs text-muted-foreground">
|
||||||
<span>Coeficiente Cnf</span>
|
<span>Cnf Médio | Cnf Borda</span>
|
||||||
<span className="font-mono">{result.cnfVertical}</span>
|
<span className="font-mono">{result.cnfVertical} | <span className="text-destructive font-bold">{result.cnfEdgeVertical}</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center text-[11px] text-muted-foreground/80">
|
<div className="flex justify-between items-center text-[11px] text-muted-foreground/80">
|
||||||
<span>Pressão (/m²)</span>
|
<span>Pressão (/m²)</span>
|
||||||
@@ -592,13 +660,17 @@ const ShelterModule: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center pb-2 border-b">
|
<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="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>
|
<span className="font-mono font-bold text-emerald-600 dark:text-emerald-400">{result.lineLoads.columnLoad.toFixed(2)} kN/m</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center pb-3 border-b">
|
<div className="flex justify-between items-center pt-2">
|
||||||
<span className="text-primary font-medium">Carga na Viga (Balanço)</span>
|
<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>
|
<span className="font-mono font-bold text-sky-600 dark:text-sky-400">{result.lineLoads.beamLoad.toFixed(2)} kN/m</span>
|
||||||
</div>
|
</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="pt-1 space-y-2">
|
||||||
<div className="font-bold text-xs text-primary flex items-center justify-between">
|
<div className="font-bold text-xs text-primary flex items-center justify-between">
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { Slider } from '@/components/ui/slider';
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Box, Wind } from 'lucide-react';
|
||||||
import { useWindStore } from '@/store/appStore';
|
import { useWindStore } from '@/store/appStore';
|
||||||
import { calculateSign } from '@/lib/nbr-tables/table-23';
|
import { calculateSign } from '@/lib/nbr-tables/table-23';
|
||||||
import Sign3DViewer from '@/components/three/Sign3D';
|
import Sign3DViewer from '@/components/three/Sign3D';
|
||||||
@@ -22,6 +24,7 @@ const SignModule: React.FC = () => {
|
|||||||
const [alpha, setAlpha] = React.useState<90 | 50>(90);
|
const [alpha, setAlpha] = React.useState<90 | 50>(90);
|
||||||
const [hasEndPlates, setHasEndPlates] = React.useState(true);
|
const [hasEndPlates, setHasEndPlates] = React.useState(true);
|
||||||
const [groundClearance, setGroundClearance] = React.useState(0.5);
|
const [groundClearance, setGroundClearance] = React.useState(0.5);
|
||||||
|
const [viewMode, setViewMode] = React.useState<'solid' | 'airflow'>('solid');
|
||||||
|
|
||||||
const result = useMemo(
|
const result = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -156,7 +159,27 @@ const SignModule: React.FC = () => {
|
|||||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm text-foreground border border-border/80 shadow-sm">F = {result.forceKN.toFixed(2)} kN</Badge>
|
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm text-foreground border border-border/80 shadow-sm">F = {result.forceKN.toFixed(2)} kN</Badge>
|
||||||
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm text-foreground border border-border/80 shadow-sm">Cf = {result.cf.toFixed(2)}</Badge>
|
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm text-foreground border border-border/80 shadow-sm">Cf = {result.cf.toFixed(2)}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-4 right-4 z-10">
|
<div className="absolute top-4 right-4 z-10 flex flex-wrap gap-2 pointer-events-auto justify-end">
|
||||||
|
<div className="flex bg-background/80 backdrop-blur-sm rounded-md p-1 shadow-sm border border-border">
|
||||||
|
<Button
|
||||||
|
variant={viewMode === 'solid' ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-3 text-xs"
|
||||||
|
onClick={() => setViewMode('solid')}
|
||||||
|
>
|
||||||
|
<Box className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
Sólido
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={viewMode === 'airflow' ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-3 text-xs"
|
||||||
|
onClick={() => setViewMode('airflow')}
|
||||||
|
>
|
||||||
|
<Wind className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
Fluxo
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<EducationalManual type="sign" params={{ length, height, alpha, hasEndPlates }} />
|
<EducationalManual type="sign" params={{ length, height, alpha, hasEndPlates }} />
|
||||||
</div>
|
</div>
|
||||||
<PressureLegend />
|
<PressureLegend />
|
||||||
@@ -169,6 +192,7 @@ const SignModule: React.FC = () => {
|
|||||||
cf={result.cf}
|
cf={result.cf}
|
||||||
forceKN={result.forceKN}
|
forceKN={result.forceKN}
|
||||||
applicationPoint={result.applicationPoint}
|
applicationPoint={result.applicationPoint}
|
||||||
|
viewMode={viewMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { Wind, Box } from 'lucide-react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
@@ -19,9 +20,10 @@ const TowerModule: React.FC = () => {
|
|||||||
const [barType, setBarType] = React.useState<TowerBarType>('flat');
|
const [barType, setBarType] = React.useState<TowerBarType>('flat');
|
||||||
const [baseWidth, setBaseWidth] = React.useState(3);
|
const [baseWidth, setBaseWidth] = React.useState(3);
|
||||||
const [height, setHeight] = React.useState(30);
|
const [height, setHeight] = React.useState(30);
|
||||||
const [panels, setPanels] = React.useState(6);
|
const [panels, setPanels] = React.useState(5);
|
||||||
const [phi, setPhi] = React.useState(0.3);
|
const [phi, setPhi] = React.useState(0.2);
|
||||||
const [alphaWind, setAlphaWind] = React.useState<0 | 45 | 90>(0);
|
const [alphaWind, setAlphaWind] = React.useState<0 | 45 | 90>(0);
|
||||||
|
const [viewMode, setViewMode] = usePersistentState<'solid' | 'airflow'>('TowerModule_viewMode', 'solid');
|
||||||
|
|
||||||
const result = useMemo(() => {
|
const result = useMemo(() => {
|
||||||
const aFace = baseWidth * height;
|
const aFace = baseWidth * height;
|
||||||
@@ -178,6 +180,24 @@ const TowerModule: React.FC = () => {
|
|||||||
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/50 backdrop-blur-sm shadow-sm">{section === 'square' ? 'Quadrada' : 'Triangular'}</Badge>
|
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/50 backdrop-blur-sm shadow-sm">{section === 'square' ? 'Quadrada' : 'Triangular'}</Badge>
|
||||||
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/50 backdrop-blur-sm shadow-sm">α = {alphaWind}°</Badge>
|
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/50 backdrop-blur-sm shadow-sm">α = {alphaWind}°</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute top-4 right-16 z-10">
|
||||||
|
<div className="flex bg-background/80 backdrop-blur-sm border border-border/80 rounded-md shadow-sm p-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('solid')}
|
||||||
|
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'solid' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
|
||||||
|
>
|
||||||
|
<Box className="w-3.5 h-3.5 inline-block mr-1.5" />
|
||||||
|
Sólido
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('airflow')}
|
||||||
|
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'airflow' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
|
||||||
|
>
|
||||||
|
<Wind className="w-3.5 h-3.5 inline-block mr-1.5" />
|
||||||
|
Fluxo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="absolute top-4 right-4 z-10">
|
<div className="absolute top-4 right-4 z-10">
|
||||||
<EducationalManual type="tower" params={{ section, alphaWind }} />
|
<EducationalManual type="tower" params={{ section, alphaWind }} />
|
||||||
</div>
|
</div>
|
||||||
@@ -190,6 +210,7 @@ const TowerModule: React.FC = () => {
|
|||||||
phi={phi}
|
phi={phi}
|
||||||
alphaWind={alphaWind}
|
alphaWind={alphaWind}
|
||||||
forceKN={result.forceKN}
|
forceKN={result.forceKN}
|
||||||
|
viewMode={viewMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Box, Wind } from 'lucide-react';
|
||||||
import { useWindStore } from '@/store/appStore';
|
import { useWindStore } from '@/store/appStore';
|
||||||
import { calculateVault, type VaultRegime } from '@/lib/modules/vault';
|
import { calculateVault, type VaultRegime } from '@/lib/modules/vault';
|
||||||
import Vault3DViewer from '@/components/three/Vault3D';
|
import Vault3DViewer from '@/components/three/Vault3D';
|
||||||
@@ -24,6 +26,7 @@ const VaultModule: React.FC = () => {
|
|||||||
const [regime, setRegime] = React.useState<VaultRegime>('laminar-rough');
|
const [regime, setRegime] = React.useState<VaultRegime>('laminar-rough');
|
||||||
const [localCpi, setLocalCpi] = React.useState<number>(0);
|
const [localCpi, setLocalCpi] = React.useState<number>(0);
|
||||||
const [viewDirection, setViewDirection] = React.useState<'perpendicular' | 'parallel'>('perpendicular');
|
const [viewDirection, setViewDirection] = React.useState<'perpendicular' | 'parallel'>('perpendicular');
|
||||||
|
const [viewMode, setViewMode] = React.useState<'solid' | 'airflow'>('solid');
|
||||||
|
|
||||||
const result = useMemo(() => calculateVault({ f: rise, l: span, b: length, vk, regime, cpi: localCpi }), [span, length, rise, vk, regime, localCpi]);
|
const result = useMemo(() => calculateVault({ f: rise, l: span, b: length, vk, regime, cpi: localCpi }), [span, length, rise, vk, regime, localCpi]);
|
||||||
|
|
||||||
@@ -167,7 +170,27 @@ const VaultModule: React.FC = () => {
|
|||||||
Cpi = {localCpi > 0 ? `+${localCpi.toFixed(2)}` : localCpi.toFixed(2)}
|
Cpi = {localCpi > 0 ? `+${localCpi.toFixed(2)}` : localCpi.toFixed(2)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-4 right-4 z-10 flex gap-2">
|
<div className="absolute top-4 right-4 z-10 flex flex-wrap gap-2 pointer-events-auto justify-end">
|
||||||
|
<div className="flex bg-background/80 backdrop-blur-sm rounded-md p-1 shadow-sm border border-border">
|
||||||
|
<Button
|
||||||
|
variant={viewMode === 'solid' ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-3 text-xs"
|
||||||
|
onClick={() => setViewMode('solid')}
|
||||||
|
>
|
||||||
|
<Box className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
Sólido
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={viewMode === 'airflow' ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-3 text-xs"
|
||||||
|
onClick={() => setViewMode('airflow')}
|
||||||
|
>
|
||||||
|
<Wind className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
Fluxo
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<Tabs value={viewDirection} onValueChange={(v) => setViewDirection(v as 'perpendicular' | 'parallel')} className="bg-background/80 backdrop-blur-sm rounded-md shadow-sm border border-border">
|
<Tabs value={viewDirection} onValueChange={(v) => setViewDirection(v as 'perpendicular' | 'parallel')} className="bg-background/80 backdrop-blur-sm rounded-md shadow-sm border border-border">
|
||||||
<TabsList className="h-8 p-1">
|
<TabsList className="h-8 p-1">
|
||||||
<TabsTrigger value="perpendicular" className="text-xs px-3">Vento ⊥</TabsTrigger>
|
<TabsTrigger value="perpendicular" className="text-xs px-3">Vento ⊥</TabsTrigger>
|
||||||
@@ -186,6 +209,7 @@ const VaultModule: React.FC = () => {
|
|||||||
cpeProfile={{ ...result.windPerpendicular }}
|
cpeProfile={{ ...result.windPerpendicular }}
|
||||||
cpeParallel={{ ...result.windParallel }}
|
cpeParallel={{ ...result.windParallel }}
|
||||||
viewDirection={viewDirection}
|
viewDirection={viewDirection}
|
||||||
|
viewMode={viewMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export interface GlobalWindState {
|
|||||||
largestDimension: number;
|
largestDimension: number;
|
||||||
heightZ: number;
|
heightZ: number;
|
||||||
locality: string | null;
|
locality: string | null;
|
||||||
|
fviz: number; // Fator de vizinhança 3D (0.7 a 1.3)
|
||||||
|
|
||||||
// Saídas (Calculados)
|
// Saídas (Calculados)
|
||||||
structureClass: StructureClass;
|
structureClass: StructureClass;
|
||||||
@@ -42,6 +43,7 @@ export interface GlobalWindState {
|
|||||||
setPermeabilityCase: (c: PermeabilityCase) => void;
|
setPermeabilityCase: (c: PermeabilityCase) => void;
|
||||||
setCpiRatio: (r: number) => void;
|
setCpiRatio: (r: number) => void;
|
||||||
setCpiManual: (c: number) => void;
|
setCpiManual: (c: number) => void;
|
||||||
|
setFviz: (f: number) => void;
|
||||||
companyLogo: string | null;
|
companyLogo: string | null;
|
||||||
setCompanyLogo: (logo: string | null) => void;
|
setCompanyLogo: (logo: string | null) => void;
|
||||||
|
|
||||||
@@ -81,7 +83,7 @@ export const useWindStore = create<GlobalWindState>((set, get) => {
|
|||||||
structureClass: calc.structClass,
|
structureClass: calc.structClass,
|
||||||
s2: calc.s2,
|
s2: calc.s2,
|
||||||
vk: calc.vk,
|
vk: calc.vk,
|
||||||
q: calc.q,
|
q: calc.q * (state.fviz ?? 1.0),
|
||||||
cpi: calcCpi(state.permeabilityCase, state.cpiRatio, state.windAngle),
|
cpi: calcCpi(state.permeabilityCase, state.cpiRatio, state.windAngle),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -95,6 +97,7 @@ export const useWindStore = create<GlobalWindState>((set, get) => {
|
|||||||
largestDimension: 30,
|
largestDimension: 30,
|
||||||
heightZ: 10,
|
heightZ: 10,
|
||||||
locality: null,
|
locality: null,
|
||||||
|
fviz: 1.0,
|
||||||
|
|
||||||
structureClass: 'B',
|
structureClass: 'B',
|
||||||
s2: 1.06,
|
s2: 1.06,
|
||||||
@@ -169,9 +172,10 @@ export const useWindStore = create<GlobalWindState>((set, get) => {
|
|||||||
},
|
},
|
||||||
setCpiManual: (c) => {
|
setCpiManual: (c) => {
|
||||||
set({ cpi: clampCpi(c) });
|
set({ cpi: clampCpi(c) });
|
||||||
// Not calling updateCalculations to avoid overriding it immediately if permeabilityCase applies,
|
},
|
||||||
// but if the user overrides manually we might want to switch permeabilityCase to 'custom' ?
|
setFviz: (f) => {
|
||||||
// Wait, there is no 'custom'. We just set the value.
|
set({ fviz: f });
|
||||||
|
get().updateCalculations();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
import { temporal } from 'zundo';
|
||||||
import { getWallCpeOfficial as getWallCpe, getRoofCpeOfficial as getRoofCpe } from '../lib/coefficients';
|
import { getWallCpeOfficial as getWallCpe, getRoofCpeOfficial as getRoofCpe } from '../lib/coefficients';
|
||||||
import { useWindStore } from './appStore';
|
import { useWindStore } from './appStore';
|
||||||
import type { WallCoefficients, RoofCoefficients } from '../lib/coefficients';
|
import type { WallCoefficients, RoofCoefficients } from '../lib/coefficients';
|
||||||
@@ -40,7 +41,9 @@ const initHeight = 6;
|
|||||||
const initPitch = 10;
|
const initPitch = 10;
|
||||||
const initAngle: 0 | 90 = 0;
|
const initAngle: 0 | 90 = 0;
|
||||||
|
|
||||||
export const useGalpaoStore = create<GalpaoState>((set, get) => ({
|
export const useGalpaoStore = create<GalpaoState>()(
|
||||||
|
temporal(
|
||||||
|
(set, get) => ({
|
||||||
template: 'standard',
|
template: 'standard',
|
||||||
extraHeight: 1.5,
|
extraHeight: 1.5,
|
||||||
skirtHeight: 4.5,
|
skirtHeight: 4.5,
|
||||||
@@ -85,4 +88,4 @@ export const useGalpaoStore = create<GalpaoState>((set, get) => ({
|
|||||||
set({ roofPitch: val });
|
set({ roofPitch: val });
|
||||||
get().updateCoefficients();
|
get().updateCoefficients();
|
||||||
},
|
},
|
||||||
}));
|
})));
|
||||||
+34
-1
@@ -1,6 +1,7 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
|
||||||
@@ -24,7 +25,39 @@ try {
|
|||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [
|
||||||
|
react(),
|
||||||
|
tailwindcss(),
|
||||||
|
VitePWA({
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'logo_brainwind.svg'],
|
||||||
|
manifest: {
|
||||||
|
name: 'VentoApp - BrainWind',
|
||||||
|
short_name: 'VentoApp',
|
||||||
|
description: 'Cálculo de ação do vento NBR 6123:2023',
|
||||||
|
theme_color: '#020617', // slate-950
|
||||||
|
background_color: '#ffffff',
|
||||||
|
display: 'standalone',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: 'logo_brainwind.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'logo_brainwind.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any maskable'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ['**/*.{js,css,html,ico,png,svg,json}'],
|
||||||
|
maximumFileSizeToCacheInBytes: 5000000 // 5MB limit for 3D assets
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[phases.setup]
|
||||||
|
nixPkgs = ["nodejs_23"]
|
||||||
|
|
||||||
|
[phases.install]
|
||||||
|
cmds = ["cd app && npm install"]
|
||||||
|
|
||||||
|
[phases.build]
|
||||||
|
cmds = ["cd app && npm run build"]
|
||||||
|
|
||||||
|
[start]
|
||||||
|
cmd = "cd app && npx --yes serve@latest -s dist"
|
||||||
Reference in New Issue
Block a user