Compare commits
22 Commits
b1cadac6ab
...
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 |
@@ -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": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"start": "npx --yes serve@latest -s dist",
|
||||
"lint": "oxlint",
|
||||
"preview": "vite preview",
|
||||
"test": "npm run test:engenharia && npm run test:sistema",
|
||||
@@ -35,6 +36,7 @@
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"three": "^0.185.1",
|
||||
"zundo": "^2.3.0",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -49,6 +51,7 @@
|
||||
"tailwindcss": "^4.3.2",
|
||||
"typescript": "~6.0.2",
|
||||
"vite": "^8.1.1",
|
||||
"vite-plugin-pwa": "^1.3.0",
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
+21
-75
@@ -1,19 +1,20 @@
|
||||
import { BrowserRouter, Routes, Route, Link, useLocation } from 'react-router-dom';
|
||||
import GalpaoModule from './pages/GalpaoModule';
|
||||
import CylinderModule from './pages/CylinderModule';
|
||||
import VaultModule from './pages/VaultModule';
|
||||
import DomeModule from './pages/DomeModule';
|
||||
import SignModule from './pages/SignModule';
|
||||
import IsolatedRoofModule from './pages/IsolatedRoofModule';
|
||||
import BarSelectorModule from './pages/BarSelectorModule';
|
||||
import BridgeModule from './pages/BridgeModule';
|
||||
import DynamicsModule from './pages/DynamicsModule';
|
||||
import CalcCaModule from './pages/CalcCaModule';
|
||||
import SettingsModule from './pages/SettingsModule';
|
||||
import TowerModule from './pages/TowerModule';
|
||||
import PiperackModule from './pages/PiperackModule';
|
||||
import ShelterModule from './pages/ShelterModule';
|
||||
import CertificatePage from './pages/CertificatePage';
|
||||
// Lazy load modules to improve performance and code splitting
|
||||
const GalpaoModule = lazy(() => import('./pages/GalpaoModule'));
|
||||
const CylinderModule = lazy(() => import('./pages/CylinderModule'));
|
||||
const VaultModule = lazy(() => import('./pages/VaultModule'));
|
||||
const DomeModule = lazy(() => import('./pages/DomeModule'));
|
||||
const SignModule = lazy(() => import('./pages/SignModule'));
|
||||
const IsolatedRoofModule = lazy(() => import('./pages/IsolatedRoofModule'));
|
||||
const BarSelectorModule = lazy(() => import('./pages/BarSelectorModule'));
|
||||
const BridgeModule = lazy(() => import('./pages/BridgeModule'));
|
||||
const DynamicsModule = lazy(() => import('./pages/DynamicsModule'));
|
||||
const CalcCaModule = lazy(() => import('./pages/CalcCaModule'));
|
||||
const SettingsModule = lazy(() => import('./pages/SettingsModule'));
|
||||
const TowerModule = lazy(() => import('./pages/TowerModule'));
|
||||
const PiperackModule = lazy(() => import('./pages/PiperackModule'));
|
||||
const ShelterModule = lazy(() => import('./pages/ShelterModule'));
|
||||
const CertificatePage = lazy(() => import('./pages/CertificatePage'));
|
||||
import {
|
||||
Home, Settings, Menu, Cylinder, Church, CircleDot,
|
||||
Square, Layers, BarChart3, Activity, Warehouse,
|
||||
@@ -26,7 +27,7 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useState } from 'react';
|
||||
import { useState, lazy, Suspense } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ThemeProvider, useTheme } from '@/lib/theme';
|
||||
@@ -74,6 +75,8 @@ const TowerIcon = (props: React.SVGProps<SVGSVGElement>) => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
import HomeMock from './pages/HomeMock';
|
||||
|
||||
function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
const location = useLocation();
|
||||
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() {
|
||||
return (
|
||||
@@ -414,6 +358,7 @@ function App() {
|
||||
<TooltipProvider>
|
||||
<BrowserRouter>
|
||||
<AppLayout>
|
||||
<Suspense fallback={<div className="flex h-full w-full items-center justify-center text-muted-foreground">Carregando módulo...</div>}>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomeMock />} />
|
||||
<Route path="/galpao" element={<GalpaoModule />} />
|
||||
@@ -432,6 +377,7 @@ function App() {
|
||||
<Route path="/settings" element={<SettingsModule />} />
|
||||
<Route path="/certificado" element={<CertificatePage />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</AppLayout>
|
||||
</BrowserRouter>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -85,7 +85,7 @@ export function DragCoefficientChart({ l1, l2, h, caLow, caHigh }: DragCoefficie
|
||||
|
||||
{/* Fake Curves */}
|
||||
{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 */}
|
||||
|
||||
@@ -400,7 +400,7 @@ function TowerDiagram({ baseWidth, height, panels, phi, forceKN }: TowerProps) {
|
||||
}
|
||||
|
||||
interface IsolatedRoofProps {
|
||||
type: 'shed' | 'gable';
|
||||
type: 'shed' | 'gable' | 'butterfly';
|
||||
theta: number;
|
||||
height: number;
|
||||
width: number;
|
||||
|
||||
@@ -42,6 +42,8 @@ export function GlobalWindSettingsModal({ trigger }: GlobalWindSettingsModalProp
|
||||
setTerrainCategory,
|
||||
locality,
|
||||
setLocality,
|
||||
fviz,
|
||||
setFviz,
|
||||
} = useWindStore();
|
||||
|
||||
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">
|
||||
<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="vizinhanca" className="text-xs py-1 px-1.5 flex-1">Vizinhança 3D</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="norma" className="space-y-4 min-w-0">
|
||||
@@ -215,6 +218,21 @@ export function GlobalWindSettingsModal({ trigger }: GlobalWindSettingsModalProp
|
||||
</p>
|
||||
</div>
|
||||
</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>
|
||||
</DialogContent>
|
||||
</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,
|
||||
getDragForce,
|
||||
} 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 { Input } from './ui/input';
|
||||
import { Badge } from './ui/badge';
|
||||
@@ -25,6 +27,10 @@ const LinearLoadsTable: React.FC = () => {
|
||||
|
||||
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(
|
||||
() => getColumnLinearLoads(cpi, q, wallCpe, frameSpacing, windAngle),
|
||||
[cpi, q, wallCpe, frameSpacing, windAngle],
|
||||
@@ -92,10 +98,11 @@ const LinearLoadsTable: React.FC = () => {
|
||||
<Separator />
|
||||
|
||||
<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="tercas">{t('linear_loads_tab_purlins')}</TabsTrigger>
|
||||
<TabsTrigger value="reacoes">{t('linear_loads_tab_reactions')}</TabsTrigger>
|
||||
<TabsTrigger value="combinacoes" className="text-xs">Combinações</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="pilares" className="space-y-3">
|
||||
@@ -202,6 +209,59 @@ const LinearLoadsTable: React.FC = () => {
|
||||
{t('linear_loads_warning_simplified')}
|
||||
</p>
|
||||
</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>
|
||||
|
||||
<Separator />
|
||||
|
||||
@@ -132,9 +132,9 @@ export function AirflowSystem({ windAngle, width, length, height, permeabilityCa
|
||||
if (inY && inX && inZ) {
|
||||
// Perto das paredes
|
||||
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.D;
|
||||
else if (Math.abs(px - (halfW)) < margin) localCpe = (windAngle === 0 ? 0.5 : wallCpe.D);
|
||||
else localCpe = cpi;
|
||||
|
||||
// Colisão dura (Impede entrar na parede sólida)
|
||||
@@ -195,12 +195,12 @@ export function AirflowSystem({ windAngle, width, length, height, permeabilityCa
|
||||
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||
|
||||
if (p.currentCpe > 0) {
|
||||
colorObj.set(isDark ? '#ef4444' : '#dc2626').lerp(new THREE.Color(isDark ? '#fcd34d' : '#f59e0b'), 1 - Math.min(1, p.currentCpe));
|
||||
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 ? '#7dd3fc' : '#38bdf8').lerp(new THREE.Color(isDark ? '#1e40af' : '#1d4ed8'), intensity);
|
||||
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção)
|
||||
} else {
|
||||
colorObj.set(isDark ? '#f1f5f9' : '#64748b');
|
||||
colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre)
|
||||
}
|
||||
meshRef.current!.setColorAt(i, colorObj);
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import SceneCanvas from '../SceneCanvas';
|
||||
import FallbackDiagram from '../FallbackDiagram';
|
||||
import { WindArrow } from './WindArrow';
|
||||
import { useCanvasTheme } from '@/lib/theme';
|
||||
import { BridgeAirflowSystem } from './BridgeAirflowSystem';
|
||||
|
||||
export interface Bridge3DInput {
|
||||
/** Maior vão Lₚ (m) */
|
||||
@@ -26,6 +27,8 @@ export interface Bridge3DInput {
|
||||
fzPerLength: number;
|
||||
/** Ângulo de ataque do vento (graus) */
|
||||
alpha: number;
|
||||
/** Modo de visualização */
|
||||
viewMode?: 'solid' | 'airflow';
|
||||
}
|
||||
|
||||
function BridgeModel({
|
||||
@@ -35,9 +38,11 @@ function BridgeModel({
|
||||
heg,
|
||||
cx,
|
||||
alpha,
|
||||
viewMode = 'solid',
|
||||
}: Bridge3DInput) {
|
||||
const theme = useCanvasTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const isAirflow = viewMode === 'airflow';
|
||||
|
||||
const halfL = lp / 2;
|
||||
const halfW = width / 2;
|
||||
@@ -46,33 +51,34 @@ function BridgeModel({
|
||||
|
||||
// Cor do tabuleiro baseada em Cx
|
||||
const deckColor = useMemo(() => {
|
||||
if (isAirflow) return new THREE.Color(isDark ? '#334155' : '#e2e8f0');
|
||||
const intensity = Math.min(1, Math.abs(cx) / 3);
|
||||
const hue = 200 - intensity * 60;
|
||||
const l = isDark ? (60 - intensity * 8) : (50 - intensity * 8);
|
||||
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
|
||||
const pillarHeights = useMemo(() => [deckY - 0.5, deckY - 0.5, deckY - 0.5], [deckY]);
|
||||
const barrierColor = isDark ? '#cbd5e1' : '#94a3b8';
|
||||
const pillarColor = isDark ? '#94a3b8' : '#64748b';
|
||||
const barrierColor = isDark ? (isAirflow ? '#475569' : '#cbd5e1') : (isAirflow ? '#cbd5e1' : '#94a3b8');
|
||||
const pillarColor = isDark ? (isAirflow ? '#334155' : '#94a3b8') : (isAirflow ? '#e2e8f0' : '#64748b');
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* Tabuleiro (deck) */}
|
||||
<mesh position={[0, deckY, 0]} castShadow receiveShadow>
|
||||
<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>
|
||||
|
||||
{/* Guarda-rodas/barreira lateral */}
|
||||
<mesh position={[0, deckY + deckThickness / 2 + 0.3, halfW - 0.15]} castShadow>
|
||||
<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 position={[0, deckY + deckThickness / 2 + 0.3, -halfW + 0.15]} castShadow>
|
||||
<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>
|
||||
|
||||
{/* Pilares (3 ao longo do comprimento) */}
|
||||
@@ -87,53 +93,68 @@ function BridgeModel({
|
||||
})}
|
||||
|
||||
{/* Solo / água */}
|
||||
{!isAirflow && (
|
||||
<mesh position={[0, -0.5, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
|
||||
<planeGeometry args={[lp * 1.6, width * 3]} />
|
||||
<meshStandardMaterial color="#60a5fa" opacity={0.4} transparent roughness={0.3} />
|
||||
</mesh>
|
||||
)}
|
||||
|
||||
{/* Seta de Vento (bate no meio do tabuleiro) */}
|
||||
{/* Seta de Vento */}
|
||||
{isAirflow ? (
|
||||
<group>
|
||||
<WindArrow
|
||||
target={[-halfL * 0.8, deckY, -halfW - 2]}
|
||||
direction={[0, 0, 1]}
|
||||
scale={4}
|
||||
/>
|
||||
<Text
|
||||
position={[-halfL * 0.8, deckY + 1.5, -halfW - 4]}
|
||||
rotation={[0, Math.PI, 0]}
|
||||
fontSize={1.2}
|
||||
color={isDark ? '#60a5fa' : '#3b82f6'}
|
||||
anchorX="center"
|
||||
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}
|
||||
/>
|
||||
<Text
|
||||
position={[0, deckY + deckThickness + 1.5, 0]}
|
||||
fontSize={1.2}
|
||||
color={isDark ? "#93c5fd" : "#1e40af"}
|
||||
anchorX="center"
|
||||
anchorY="bottom"
|
||||
>
|
||||
Lp={lp}m | B={width}m | Cx={cx.toFixed(2)}
|
||||
</Text>
|
||||
)}
|
||||
</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 isDark = theme === 'dark';
|
||||
const { lp, deckHeight, width } = input;
|
||||
const dist = Math.max(lp * 0.6, deckHeight * 2);
|
||||
|
||||
const fallback = (
|
||||
<FallbackDiagram
|
||||
type="bridge"
|
||||
props={input}
|
||||
props={{ lp, width, deckHeight, heg, cx, cz, fxPerLength, fzPerLength }}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<SceneCanvas moduleId="ponte"
|
||||
<SceneCanvas moduleId="pontes"
|
||||
shadows
|
||||
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||
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}
|
||||
>
|
||||
<ambientLight intensity={0.6} />
|
||||
<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
|
||||
infiniteGrid
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -159,14 +159,14 @@ function CylinderModel({ diameter, height, cpeProfile, cpi, viewMode = 'solid' }
|
||||
</mesh>
|
||||
</group>
|
||||
|
||||
{/* Texto Vento - fora do grupo para rotação horizontal compatível (0, 0, 0) e tamanho 50% maior (0.6) */}
|
||||
{/* Texto Vento - acima da seta com o dobro do tamanho (1.2) */}
|
||||
<Text
|
||||
position={[-radius - 4.2, height / 2, 0]}
|
||||
position={[-radius - 2.5, height / 2 + 1.0, 0]}
|
||||
rotation={[0, 0, 0]}
|
||||
fontSize={0.6}
|
||||
fontSize={1.2}
|
||||
color="#3b82f6"
|
||||
anchorX="center"
|
||||
anchorY="middle"
|
||||
anchorY="bottom"
|
||||
>
|
||||
Vento
|
||||
</Text>
|
||||
@@ -201,6 +201,7 @@ export default function Cylinder3DViewer({ diameter, height, cpeProfile, cpi, vi
|
||||
return (
|
||||
<SceneCanvas moduleId="cilindro"
|
||||
shadows
|
||||
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||
camera={{ position: [diameter * 1.5, height * 1.2, diameter * 1.5], fov: 40 }}
|
||||
fallback={fallback}
|
||||
|
||||
@@ -95,7 +95,7 @@ export function CylinderAirflowSystem({ diameter, height }: CylinderAirflowSyste
|
||||
} 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 (sucção moderada)
|
||||
p.currentCpe = 0.5; // Esteira (agora positivo para ficar vermelho)
|
||||
} else {
|
||||
p.currentCpe = 0;
|
||||
}
|
||||
@@ -123,16 +123,12 @@ export function CylinderAirflowSystem({ diameter, height }: CylinderAirflowSyste
|
||||
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||
|
||||
if (p.currentCpe > 0) {
|
||||
colorObj
|
||||
.set(isDark ? '#ef4444' : '#dc2626')
|
||||
.lerp(new THREE.Color(isDark ? '#fcd34d' : '#f59e0b'), 1 - Math.min(1, p.currentCpe));
|
||||
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 ? '#7dd3fc' : '#38bdf8')
|
||||
.lerp(new THREE.Color(isDark ? '#1e40af' : '#1d4ed8'), intensity);
|
||||
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção)
|
||||
} else {
|
||||
colorObj.set(isDark ? '#f1f5f9' : '#64748b');
|
||||
colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre)
|
||||
}
|
||||
meshRef.current!.setColorAt(i, colorObj);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as THREE from 'three';
|
||||
import SceneCanvas from '../SceneCanvas';
|
||||
import FallbackDiagram from '../FallbackDiagram';
|
||||
import { useCanvasTheme } from '@/lib/theme';
|
||||
import { DomeAirflowSystem } from './DomeAirflowSystem';
|
||||
|
||||
export interface Dome3DInput {
|
||||
diameter: number;
|
||||
@@ -14,6 +15,7 @@ export interface Dome3DInput {
|
||||
cpeBarlavento: number;
|
||||
cpeTopo: number;
|
||||
cpeLateral: number;
|
||||
viewMode?: 'solid' | 'airflow';
|
||||
}
|
||||
|
||||
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)}%)`);
|
||||
}
|
||||
|
||||
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 isDark = theme === 'dark';
|
||||
const isAirflow = viewMode === 'airflow';
|
||||
const radius = diameter / 2;
|
||||
const segments = 64;
|
||||
|
||||
@@ -64,7 +67,7 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
|
||||
{/* Parede cilíndrica inferior */}
|
||||
<mesh position={[0, wallHeight / 2, 0]} castShadow receiveShadow>
|
||||
<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>
|
||||
|
||||
{/* 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="index" args={[new Uint16Array(indices), 1]} />
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 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>
|
||||
<coneGeometry args={[0.3, 0.8, 16]} />
|
||||
<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]} />
|
||||
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
||||
</mesh>
|
||||
</group>
|
||||
|
||||
{/* Texto Vento */}
|
||||
<Text
|
||||
position={[0, -1.5, 0]}
|
||||
rotation={[Math.PI / 2, 0, 0]}
|
||||
fontSize={0.4}
|
||||
position={[-radius - 2.5, wallHeight / 2 + 1.2, 0]}
|
||||
rotation={[0, 0, 0]}
|
||||
fontSize={1.2}
|
||||
color="#3b82f6"
|
||||
anchorX="center"
|
||||
anchorY="middle"
|
||||
anchorY="bottom"
|
||||
>
|
||||
Vento
|
||||
</Text>
|
||||
</group>
|
||||
|
||||
{/* Texto informativo */}
|
||||
<Text
|
||||
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"}
|
||||
anchorX="center"
|
||||
anchorY="bottom"
|
||||
@@ -168,6 +173,7 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
|
||||
export default function Dome3DViewer(props: Dome3DInput) {
|
||||
const theme = useCanvasTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const { viewMode = 'solid' } = props;
|
||||
|
||||
const fallback = (
|
||||
<FallbackDiagram
|
||||
@@ -181,6 +187,7 @@ export default function Dome3DViewer(props: Dome3DInput) {
|
||||
return (
|
||||
<SceneCanvas moduleId="cupula"
|
||||
shadows
|
||||
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||
camera={{ position: [props.diameter * 1.5, (props.wallHeight + props.rise) * 1.5, props.diameter * 1.5], fov: 40 }}
|
||||
fallback={fallback}
|
||||
@@ -199,6 +206,9 @@ export default function Dome3DViewer(props: Dome3DInput) {
|
||||
shadow-camera-bottom={-maxDim}
|
||||
/>
|
||||
<DomeModel {...props} />
|
||||
{viewMode === 'airflow' && (
|
||||
<DomeAirflowSystem diameter={props.diameter} rise={props.rise} wallHeight={props.wallHeight} />
|
||||
)}
|
||||
<Grid
|
||||
infiniteGrid
|
||||
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 SceneCanvas from '../SceneCanvas';
|
||||
import FallbackDiagram from '../FallbackDiagram';
|
||||
import { WindArrow } from './WindArrow';
|
||||
import { useCanvasTheme } from '@/lib/theme';
|
||||
import { IsolatedRoofAirflowSystem } from './IsolatedRoofAirflowSystem';
|
||||
|
||||
export interface IsolatedRoof3DInput {
|
||||
/** Tipo de cobertura: 'shed' (uma água) ou 'gable' (duas águas) */
|
||||
type: 'shed' | 'gable';
|
||||
/** Tipo de cobertura: 'shed' (uma água), 'gable' (duas águas) ou 'butterfly' (borboleta) */
|
||||
type: 'shed' | 'gable' | 'butterfly';
|
||||
/** Inclinação θ (graus) */
|
||||
theta: number;
|
||||
/** Altura livre dos suportes (m) */
|
||||
@@ -26,6 +26,7 @@ export interface IsolatedRoof3DInput {
|
||||
cpeTop: number;
|
||||
/** Força resultante na cobertura (kN) */
|
||||
forceKN: number;
|
||||
viewMode?: 'solid' | 'airflow';
|
||||
}
|
||||
|
||||
function pressureColor(cpe: number, isDark: boolean): THREE.Color {
|
||||
@@ -50,9 +51,11 @@ function IsolatedRoofModel({
|
||||
cpeWindward,
|
||||
cpeLeeward,
|
||||
forceKN,
|
||||
viewMode = 'solid',
|
||||
}: IsolatedRoof3DInput) {
|
||||
const theme = useCanvasTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const isAirflow = viewMode === 'airflow';
|
||||
const labelColor = isDark ? '#cbd5e1' : '#475569';
|
||||
const textColor = isDark ? '#93c5fd' : '#1a202c';
|
||||
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 },
|
||||
);
|
||||
} else {
|
||||
} else if (type === 'gable') {
|
||||
list.push(
|
||||
{ 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 },
|
||||
);
|
||||
} 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;
|
||||
}, [type, depth, width, height, h_diff, h_half, halfDepth, halfWidth]);
|
||||
@@ -106,25 +118,9 @@ function IsolatedRoofModel({
|
||||
{type === 'shed' ? (
|
||||
// Uma água (Shed): dividida em metade barlavento e metade sotavento
|
||||
<group>
|
||||
{/* Metade Barlavento (Z < 0) */}
|
||||
<mesh
|
||||
position={[0, height + h_diff / 4, -depth / 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 position={[0, height + h_diff / 2, 0]} rotation={[-thetaRad, 0, 0]} castShadow receiveShadow>
|
||||
<boxGeometry args={[width, 0.08, depth / Math.cos(thetaRad)]} />
|
||||
<meshStandardMaterial color={windwardColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.4} />
|
||||
</mesh>
|
||||
</group>
|
||||
) : (
|
||||
@@ -133,22 +129,22 @@ function IsolatedRoofModel({
|
||||
{/* Água Esquerda / Barlavento (Z < 0) */}
|
||||
<mesh
|
||||
position={[0, height + h_half / 2, -depth / 4]}
|
||||
rotation={[-thetaRad, 0, 0]}
|
||||
rotation={[type === 'butterfly' ? thetaRad : -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} />
|
||||
<meshStandardMaterial color={windwardColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.4} />
|
||||
</mesh>
|
||||
{/* Água Direita / Sotavento (Z > 0) */}
|
||||
<mesh
|
||||
position={[0, height + h_half / 2, depth / 4]}
|
||||
rotation={[thetaRad, 0, 0]}
|
||||
rotation={[type === 'butterfly' ? -thetaRad : 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} />
|
||||
<meshStandardMaterial color={leewardColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.4} />
|
||||
</mesh>
|
||||
</group>
|
||||
)}
|
||||
@@ -161,11 +157,27 @@ function IsolatedRoofModel({
|
||||
</mesh>
|
||||
))}
|
||||
|
||||
{/* Vetor de vento (Seta verde/azul batendo no elemento) */}
|
||||
<WindArrow
|
||||
target={new THREE.Vector3(0, centerY, 0)}
|
||||
direction={new THREE.Vector3(0, 0, 1)}
|
||||
/>
|
||||
{/* Vetor de vento (Seta mais visível com texto) */}
|
||||
<group position={[0, centerY, -halfDepth - 3.5]} rotation={[Math.PI / 2, 0, 0]}>
|
||||
<mesh castShadow>
|
||||
<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) === */}
|
||||
{/* Cota de Altura (h) */}
|
||||
@@ -248,6 +260,8 @@ function IsolatedRoofModel({
|
||||
{/* Rótulo Superior */}
|
||||
<Text
|
||||
position={[0, centerY + 3, 0]}
|
||||
rotation={[0, Math.PI, 0]}
|
||||
fontSize={Math.max(0.4, width / 15)}
|
||||
color={textColor}
|
||||
anchorX="center"
|
||||
anchorY="bottom"
|
||||
@@ -268,6 +282,7 @@ export default function IsolatedRoof3DViewer({
|
||||
cpeLeeward,
|
||||
cpeTop,
|
||||
forceKN,
|
||||
viewMode = 'solid',
|
||||
}: IsolatedRoof3DInput) {
|
||||
const cameraDistance = Math.max(Math.max(width, depth) * 1.3, height * 1.5, 8);
|
||||
|
||||
@@ -281,8 +296,9 @@ export default function IsolatedRoof3DViewer({
|
||||
return (
|
||||
<SceneCanvas moduleId="cobertura_isolada"
|
||||
shadows
|
||||
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||
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}
|
||||
>
|
||||
<ambientLight intensity={0.7} />
|
||||
@@ -303,7 +319,19 @@ export default function IsolatedRoof3DViewer({
|
||||
cpeLeeward={cpeLeeward}
|
||||
cpeTop={cpeTop}
|
||||
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]} />
|
||||
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.05} />
|
||||
<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;
|
||||
l2: number;
|
||||
h: number;
|
||||
viewMode?: 'solid' | 'airflow';
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Create target scale and position based on inputs
|
||||
@@ -109,12 +110,12 @@ export function ResizableBox3D({ l1, l2, h }: ResizableBox3DProps) {
|
||||
{/* The Animated Box */}
|
||||
<mesh ref={meshRef} castShadow receiveShadow>
|
||||
<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" />
|
||||
</mesh>
|
||||
|
||||
{/* Wind Flow Animation */}
|
||||
<WindParticles l1={l1} l2={l2} h={h} />
|
||||
{viewMode === 'airflow' && <WindParticles l1={l1} l2={l2} h={h} />}
|
||||
</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 { WindArrow as GlobalWindArrow } from './WindArrow';
|
||||
import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls';
|
||||
import SceneCanvas from '../SceneCanvas';
|
||||
import { useCanvasTheme } from '@/lib/theme';
|
||||
import { ShelterAirflowSystem } from './ShelterAirflowSystem';
|
||||
import type { ShelterCondition, OpeningPosition, ShelterWindAngle } from '@/lib/nbr-tables/table-shelters';
|
||||
|
||||
export interface Shelter3DProps {
|
||||
@@ -18,44 +17,11 @@ export interface Shelter3DProps {
|
||||
rightClosure: number;
|
||||
openingPos: OpeningPosition;
|
||||
backRange?: [number, number];
|
||||
numColumns?: number;
|
||||
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({
|
||||
condition: _condition,
|
||||
depth,
|
||||
@@ -67,14 +33,18 @@ function ShelterModel({
|
||||
rightClosure,
|
||||
openingPos,
|
||||
backRange,
|
||||
numColumns = 2,
|
||||
windAngle = 0,
|
||||
viewMode,
|
||||
}: Shelter3DProps) {
|
||||
const isElevation = viewMode === 'elevation';
|
||||
const theme = useCanvasTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
// Geometria básica
|
||||
const halfW = width / 2;
|
||||
const halfD = depth / 2;
|
||||
const isAirflow = viewMode === 'airflow';
|
||||
const thetaRad = (theta * Math.PI) / 180;
|
||||
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 (
|
||||
<group position={[0, -height / 2, 0]}>
|
||||
{/* 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]}>
|
||||
{/* Pilar traseiro (lado direito na elevação) */}
|
||||
<mesh position={[0, height / 2, halfD - 0.25]}>
|
||||
@@ -161,50 +138,85 @@ function ShelterModel({
|
||||
{/* Parede Lateral Direita (X = +halfW) */}
|
||||
{renderWallWithGap(depth, height, rightClosure, 'distributed', Math.PI / 2, [halfW, 0, 0])}
|
||||
|
||||
{/* 4. Linhas de Corrente e Vetores de Vento (Ilustrativo e Didático) */}
|
||||
<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} />
|
||||
<WindArrow start={[-halfW * 0.5, height * 0.7, -halfD - 4]} end={[-halfW * 0.5, height * 0.7, -halfD + 1]} color="#38bdf8" speed={1.4} />
|
||||
<WindArrow start={[halfW * 0.5, height * 0.4, -halfD - 4]} end={[halfW * 0.5, height * 0.4, -halfD + 1]} color="#38bdf8" speed={1.3} />
|
||||
|
||||
{/* Linhas de corrente sobre o telhado */}
|
||||
<WindArrow start={[0, height + 1, -halfD - 3]} end={[0, height + slopeRise + 1.5, halfD + 2]} color="#60a5fa" speed={1.6} />
|
||||
|
||||
{/* Fresta na parede de fundo: se houver abertura no topo, mostrar vento escapando pela fresta superior */}
|
||||
{backClosure > 0 && backClosure < 100 && (openingPos === 'top' || (backRange && backRange[1] < 98)) && (
|
||||
<>
|
||||
<WindArrow start={[0, height * 0.85, 0]} end={[0, height * 0.95, halfD + 2.5]} color="#34d399" speed={1.8} />
|
||||
<Text position={[0, height + 0.35, halfD]} rotation={[0, Math.PI, 0]} fontSize={0.4} color="#34d399">
|
||||
Alívio (Fresta Topo)
|
||||
{/* Setas de Vento Global (0°, 45°, 90°) */}
|
||||
{[
|
||||
{
|
||||
angle: 0,
|
||||
label: 'Vento 0°',
|
||||
target: [0, height * 1.5 - 1.5, -halfD - 4] as [number, number, number],
|
||||
direction: [0, 0, 1] as [number, number, number],
|
||||
textPos: [0, height * 1.5, -halfD - 6] as [number, number, number],
|
||||
textRot: [0, Math.PI, 0] as [number, number, number],
|
||||
},
|
||||
{
|
||||
angle: 45,
|
||||
label: 'Vento 45°',
|
||||
target: [-halfW - 3, height * 1.5 - 1.5, -halfD - 3] as [number, number, number],
|
||||
direction: [0.707, 0, 0.707] as [number, number, number],
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Fresta na base */}
|
||||
{backClosure > 0 && backClosure < 100 && (openingPos === 'bottom' || (backRange && backRange[0] > 2)) && (
|
||||
<>
|
||||
<WindArrow start={[0, 0.4, 0]} end={[0, 0.4, halfD + 2.5]} color="#facc15" speed={1.8} />
|
||||
<Text position={[0, 1.2, halfD]} rotation={[0, Math.PI, 0]} fontSize={0.4} color="#facc15">
|
||||
Vão Livre Inferior
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Partículas de Ar */}
|
||||
{isAirflow && (
|
||||
<group>
|
||||
<ShelterAirflowSystem
|
||||
width={width}
|
||||
depth={depth}
|
||||
height={height}
|
||||
theta={theta}
|
||||
/>
|
||||
</group>
|
||||
)}
|
||||
|
||||
{/* 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 && (
|
||||
<>
|
||||
<Text position={[0, height + slopeRise + 1.4, 0]} rotation={[0, Math.PI, 0]} fontSize={0.45} color={isDark ? '#f8fafc' : '#0f172a'}>
|
||||
{isElevation ? 'CORTE A-A (Elevação)' : 'Abrigo / Pórtico (NBR 6123)'}
|
||||
</Text>
|
||||
<Text position={[0, 0.3, -halfD - 1.2]} rotation={[0, Math.PI, 0]} fontSize={0.35} color="#38bdf8">
|
||||
{`VENTO ${windAngle === 0 ? 'FRONTAL (0°)' : windAngle === 45 ? 'OBLÍQUO (45°)' : 'LATERAL (90°)'}`}
|
||||
</Text>
|
||||
<Text position={[0, height + slopeRise + 0.25, 0]} rotation={[0, Math.PI, 0]} fontSize={0.32} color="#f43f5e">
|
||||
<Text position={[0, height + slopeRise + 0.6, 0]} rotation={[0, Math.PI, 0]} fontSize={0.51} color={isDark ? '#ff4d6d' : '#b91c1c'}>
|
||||
Arrancamento [+Fz ↑]
|
||||
</Text>
|
||||
<Text position={[0, height * 0.5, halfD + 1.2]} rotation={[0, Math.PI, 0]} fontSize={0.32} color="#fb923c">
|
||||
<Text position={[0, height * 0.5, halfD + 1.2]} rotation={[0, Math.PI, 0]} fontSize={0.51} color={isDark ? '#ff9f43' : '#c2410c'}>
|
||||
Empuxo no Fundo [+Fx →]
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
@@ -212,13 +224,14 @@ function ShelterModel({
|
||||
export function Shelter3D(props: Shelter3DProps) {
|
||||
const theme = useCanvasTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const { width, depth, height } = props;
|
||||
const { width, depth, height, viewMode } = props;
|
||||
const dist = Math.max(width, depth) * 1.8 + 8;
|
||||
|
||||
return (
|
||||
<SceneCanvas
|
||||
moduleId="shelter"
|
||||
shadows
|
||||
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||
camera={{ position: [dist * 0.75, height * 1.1, -dist * 0.85], fov: 45 }}
|
||||
fallback={<div className="flex items-center justify-center h-full text-muted-foreground">3D indisponível</div>}
|
||||
|
||||
@@ -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 { WindArrow } from './WindArrow';
|
||||
import { useCanvasTheme } from '@/lib/theme';
|
||||
import { SignAirflowSystem } from './SignAirflowSystem';
|
||||
|
||||
export interface Sign3DInput {
|
||||
/** Comprimento ℓ (m) */
|
||||
@@ -22,6 +23,7 @@ export interface Sign3DInput {
|
||||
forceKN: number;
|
||||
/** Excentricidade e (m) */
|
||||
applicationPoint: number;
|
||||
viewMode?: 'solid' | 'airflow';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,9 +37,11 @@ function SignModel({
|
||||
alpha,
|
||||
cf,
|
||||
applicationPoint,
|
||||
viewMode = 'solid',
|
||||
}: Sign3DInput) {
|
||||
const theme = useCanvasTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const isAirflow = viewMode === 'airflow';
|
||||
const baseY = groundClearance;
|
||||
const topY = baseY + height;
|
||||
const halfL = length / 2;
|
||||
@@ -47,11 +51,13 @@ function SignModel({
|
||||
const angleToNormalRad = ((90 - alpha) * Math.PI) / 180;
|
||||
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)
|
||||
const arrowStart = useMemo(
|
||||
() => new THREE.Vector3(0, baseY + height / 2, applicationPoint),
|
||||
[applicationPoint, height, baseY],
|
||||
);
|
||||
// Posição alvo da seta no plano da placa (inicia no ponto de aplicação com a excentricidade ao longo de Z)
|
||||
// Afastada um pouco do muro para não encostar fisicamente
|
||||
const arrowStart = useMemo(() => {
|
||||
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
|
||||
const plateColor = useMemo(() => {
|
||||
@@ -67,7 +73,7 @@ function SignModel({
|
||||
{/* Placa principal */}
|
||||
<mesh position={[0, (baseY + topY) / 2, 0]} castShadow receiveShadow>
|
||||
<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>
|
||||
|
||||
{/* Placas de extremidade (retornos aerodinâmicos nas pontas) */}
|
||||
@@ -75,11 +81,11 @@ function SignModel({
|
||||
<>
|
||||
<mesh position={[0, (baseY + topY) / 2, halfL]} castShadow>
|
||||
<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 position={[0, (baseY + topY) / 2, -halfL]} castShadow>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
@@ -118,6 +124,7 @@ function SignModel({
|
||||
<WindArrow
|
||||
target={arrowStart}
|
||||
direction={arrowDir}
|
||||
scale={1.8}
|
||||
/>
|
||||
|
||||
{/* === LINHAS DE COTA (CAD-Style Dimensions) === */}
|
||||
@@ -206,6 +213,7 @@ export default function Sign3DViewer({
|
||||
cf,
|
||||
forceKN,
|
||||
applicationPoint,
|
||||
viewMode = 'solid',
|
||||
}: Sign3DInput) {
|
||||
const theme = useCanvasTheme();
|
||||
const isDark = theme === 'dark';
|
||||
@@ -222,6 +230,7 @@ export default function Sign3DViewer({
|
||||
return (
|
||||
<SceneCanvas moduleId="muro_placa"
|
||||
shadows
|
||||
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||
camera={{ position: [length * 1.3, height * 1.5, length * 1.3], fov: 40 }}
|
||||
fallback={fallback}
|
||||
@@ -236,7 +245,17 @@ export default function Sign3DViewer({
|
||||
cf={cf}
|
||||
forceKN={forceKN}
|
||||
applicationPoint={applicationPoint}
|
||||
viewMode={viewMode}
|
||||
/>
|
||||
{viewMode === 'airflow' && (
|
||||
<SignAirflowSystem
|
||||
length={length}
|
||||
height={height}
|
||||
groundClearance={groundClearance}
|
||||
alpha={alpha}
|
||||
cf={cf}
|
||||
/>
|
||||
)}
|
||||
<Grid
|
||||
infiniteGrid
|
||||
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 { WindArrow } from './WindArrow';
|
||||
import FallbackDiagram from '../FallbackDiagram';
|
||||
import { TowerAirflowSystem } from './TowerAirflowSystem';
|
||||
|
||||
export interface Tower3DInput {
|
||||
/** Forma da seção */
|
||||
@@ -23,6 +24,8 @@ export interface Tower3DInput {
|
||||
alphaWind: 0 | 45 | 90;
|
||||
/** Força total estimada na torre (kN) */
|
||||
forceKN: number;
|
||||
/** Modo de visualização */
|
||||
viewMode?: 'solid' | 'airflow';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,9 +139,11 @@ function TowerModel({
|
||||
phi,
|
||||
alphaWind,
|
||||
forceKN,
|
||||
viewMode = 'solid',
|
||||
}: Tower3DInput) {
|
||||
const theme = useCanvasTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const isAirflow = viewMode === 'airflow';
|
||||
|
||||
const geometry = useMemo(
|
||||
() => buildTowerGeometry(section, panels, baseWidth, height),
|
||||
@@ -201,11 +206,32 @@ function TowerModel({
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Seta de Vento única atingindo o meio da torre */}
|
||||
{/* Seta de Vento */}
|
||||
{isAirflow ? (
|
||||
<group>
|
||||
<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 */}
|
||||
<mesh position={[baseWidth / 2 + 0.5, 0.5, 0]}>
|
||||
@@ -214,6 +240,7 @@ function TowerModel({
|
||||
</mesh>
|
||||
<Text
|
||||
position={[0, height + 1.0, 0]}
|
||||
rotation={[0, Math.PI, 0]}
|
||||
fontSize={0.5}
|
||||
color={isDark ? "#93c5fd" : "#1e40af"}
|
||||
anchorX="center"
|
||||
@@ -228,7 +255,7 @@ function TowerModel({
|
||||
export default function Tower3DViewer(input: Tower3DInput) {
|
||||
const theme = useCanvasTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const { baseWidth, height } = input;
|
||||
const { baseWidth, height, viewMode = 'solid' } = input;
|
||||
const dist = Math.max(baseWidth * 3, height * 1.2);
|
||||
|
||||
const fallback = (
|
||||
@@ -241,13 +268,14 @@ export default function Tower3DViewer(input: Tower3DInput) {
|
||||
return (
|
||||
<SceneCanvas moduleId="torre"
|
||||
shadows
|
||||
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||
camera={{ position: [dist, height * 0.6, dist], fov: 45 }}
|
||||
fallback={fallback}
|
||||
>
|
||||
<ambientLight intensity={0.6} />
|
||||
<directionalLight position={[dist, height, dist]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
|
||||
<TowerModel {...input} />
|
||||
<TowerModel {...input} viewMode={viewMode} />
|
||||
<Grid
|
||||
infiniteGrid
|
||||
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 FallbackDiagram from '../FallbackDiagram';
|
||||
import { useCanvasTheme } from '@/lib/theme';
|
||||
import { VaultAirflowSystem } from './VaultAirflowSystem';
|
||||
|
||||
export interface Vault3DInput {
|
||||
span: number;
|
||||
@@ -14,6 +15,7 @@ export interface Vault3DInput {
|
||||
cpeProfile: Record<string, number>;
|
||||
cpeParallel?: Record<string, number>;
|
||||
viewDirection?: 'perpendicular' | 'parallel';
|
||||
viewMode?: 'solid' | 'airflow';
|
||||
}
|
||||
|
||||
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)}%)`);
|
||||
}
|
||||
|
||||
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 isDark = theme === 'dark';
|
||||
const isAirflow = viewMode === 'airflow';
|
||||
const segments = 64;
|
||||
|
||||
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="index" args={[new Uint16Array(indices), 1]} />
|
||||
</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>
|
||||
<Text
|
||||
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="index" args={[new Uint16Array(indices), 1]} />
|
||||
</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>
|
||||
<Text
|
||||
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) */}
|
||||
<mesh position={[0, 0, 0]} castShadow receiveShadow>
|
||||
<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>
|
||||
|
||||
{/* Tímpano Frontal (Z = length) */}
|
||||
<mesh position={[0, 0, length]} castShadow receiveShadow>
|
||||
<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>
|
||||
|
||||
{/* 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 */}
|
||||
<Text
|
||||
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 isDark = theme === 'dark';
|
||||
|
||||
@@ -202,6 +229,7 @@ export default function Vault3DViewer({ span, length, rise, cpi, cpeProfile, cpe
|
||||
return (
|
||||
<SceneCanvas moduleId="abobada"
|
||||
shadows
|
||||
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||
camera={{ position: [span * 1.2, rise * 1.5, length * 1.2], fov: 40 }}
|
||||
fallback={fallback}
|
||||
@@ -214,7 +242,19 @@ export default function Vault3DViewer({ span, length, rise, cpi, cpeProfile, cpe
|
||||
shadow-mapSize-width={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
|
||||
infiniteGrid
|
||||
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];
|
||||
/** Fator de escala da seta (default: 1) */
|
||||
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 color = '#3b82f6'; // Azul padrão do vento
|
||||
|
||||
const targetVec = target instanceof THREE.Vector3 ? target : new THREE.Vector3(...target);
|
||||
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 && (
|
||||
<mesh position={midPoint.toArray()} rotation={rotation} castShadow>
|
||||
<cylinderGeometry args={[shaftRadius, shaftRadius, shaftLength, 12]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
<meshStandardMaterial color={color} transparent={opacity < 1} opacity={opacity} />
|
||||
</mesh>
|
||||
)}
|
||||
{/* Ponta da seta (cone) */}
|
||||
<mesh position={[end.x, end.y, end.z]} rotation={rotation} castShadow>
|
||||
<coneGeometry args={[headRadius, headLen, 12]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
<meshStandardMaterial color={color} transparent={opacity < 1} opacity={opacity} />
|
||||
</mesh>
|
||||
</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 { calculateTower } from '../modules/tower';
|
||||
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(
|
||||
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
|
||||
const galpaoGeos = [
|
||||
{ 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) */
|
||||
export function frictionForceIsolatedRoof(q: number, a: number, b: number): number {
|
||||
return Number((0.05 * q * a * b).toFixed(3));
|
||||
|
||||
@@ -29,6 +29,8 @@ export interface ShelterInput {
|
||||
rightClosure: number;
|
||||
/** Posição da abertura na parede de fundo ('top' | 'bottom' | 'distributed') */
|
||||
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) */
|
||||
windAngle?: ShelterWindAngle;
|
||||
}
|
||||
@@ -38,10 +40,14 @@ export interface ShelterResult {
|
||||
windAngle: ShelterWindAngle;
|
||||
/** Coeficiente de pressão superior da cobertura (Ce superior) */
|
||||
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) */
|
||||
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;
|
||||
/** 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 */
|
||||
cfBack: number;
|
||||
/** 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) */
|
||||
lineLoads: {
|
||||
/** Carga horizontal em cada pilar (kN/m) */
|
||||
/** Carga horizontal no pilar do PÓRTICO CENTRAL (mais carregado) (kN/m) */
|
||||
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;
|
||||
/** 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 (%) */
|
||||
reliefPercentage: number;
|
||||
@@ -98,6 +112,7 @@ export function calculateShelterWind(input: ShelterInput, q: number): ShelterRes
|
||||
leftClosure,
|
||||
rightClosure,
|
||||
openingPos,
|
||||
numColumns = 2,
|
||||
} = input;
|
||||
|
||||
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)
|
||||
// Varia conforme o ângulo de incidência (0°, 45° ou 90°)
|
||||
let cpeTop = -1.0;
|
||||
let cpeEdgeTop = -2.0;
|
||||
if (windAngle === 0) {
|
||||
// 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;
|
||||
cpeEdgeTop = theta <= 5 ? -1.8 : theta <= 15 ? -2.0 : -2.4;
|
||||
} 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%)
|
||||
cpeTop = theta <= 5 ? -1.3 : theta <= 15 ? -1.4 : -1.6;
|
||||
cpeEdgeTop = theta <= 5 ? -2.2 : theta <= 15 ? -2.5 : -2.8;
|
||||
} else {
|
||||
// Lateral 90°: escoamento transversal sobre a cobertura, sucção constante por descolamento
|
||||
cpeTop = -1.1;
|
||||
cpeEdgeTop = -1.6;
|
||||
}
|
||||
|
||||
// 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)
|
||||
const cnfUp = cpeTop - Math.max(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
|
||||
let cfBack = 1.3 * bClose;
|
||||
@@ -210,20 +230,45 @@ export function calculateShelterWind(input: ShelterInput, q: number): ShelterRes
|
||||
const fySide = cfSide * q * (areaSide * 2);
|
||||
const fFriction = 0.04 * q * areaRoof;
|
||||
|
||||
// 6. Carga linear em pórticos (2 pórticos principais)
|
||||
const numPorticos = 2;
|
||||
let columnLoad = 0;
|
||||
// 6. Carga linear em pórticos (usando Áreas Tributárias)
|
||||
const numPorticos = Math.max(2, numColumns);
|
||||
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) {
|
||||
columnLoad = (fxBack / height / numPorticos) + (q * 1.8 * 0.15);
|
||||
columnLoadTotal = fxBack;
|
||||
} else if (windAngle === 45) {
|
||||
const combForce = Math.sqrt(fxBack * fxBack + fySide * fySide);
|
||||
columnLoad = (combForce / height / numPorticos) + (q * 1.8 * 0.15);
|
||||
columnLoadTotal = Math.sqrt(fxBack * fxBack + fySide * fySide);
|
||||
} else {
|
||||
// Em 90°, a carga transversal principal atua nas paredes laterais (Fy)
|
||||
columnLoad = (fySide / height / numPorticos) + (q * 1.8 * 0.15);
|
||||
columnLoadTotal = fySide;
|
||||
}
|
||||
|
||||
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
|
||||
let statusText = '';
|
||||
@@ -266,8 +311,10 @@ export function calculateShelterWind(input: ShelterInput, q: number): ShelterRes
|
||||
return {
|
||||
windAngle,
|
||||
cpeTop: Number(cpeTop.toFixed(2)),
|
||||
cpeEdgeTop: Number(cpeEdgeTop.toFixed(2)),
|
||||
cpiBottom: Number(effectiveCpi.toFixed(2)),
|
||||
cnfVertical: Number(cnfUp.toFixed(2)),
|
||||
cnfEdgeVertical: Number(cnfEdgeUp.toFixed(2)),
|
||||
cfBack: Number(cfBack.toFixed(2)),
|
||||
cfSide: Number(cfSide.toFixed(2)),
|
||||
forces: {
|
||||
@@ -279,7 +326,11 @@ export function calculateShelterWind(input: ShelterInput, q: number): ShelterRes
|
||||
},
|
||||
lineLoads: {
|
||||
columnLoad: Number(columnLoad.toFixed(2)),
|
||||
columnLoadEdge: Number(columnLoadEdge.toFixed(2)),
|
||||
beamLoad: Number(beamLoad.toFixed(2)),
|
||||
beamLoadEdge: Number(beamLoadEdge.toFixed(2)),
|
||||
beamMoment: Number(beamMoment.toFixed(2)),
|
||||
beamMomentEdge: Number(beamMomentEdge.toFixed(2)),
|
||||
},
|
||||
reliefPercentage,
|
||||
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 { useHydrationStore } from '@/store/hydrationStore';
|
||||
@@ -47,6 +48,7 @@ const BridgeModule: React.FC = () => {
|
||||
const [fv, setFv] = React.useState(0.6);
|
||||
const [vk, setVk] = React.useState(45);
|
||||
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 classification = useMemo(
|
||||
@@ -214,6 +216,24 @@ const BridgeModule: React.FC = () => {
|
||||
Classe {classification.bridgeClass}
|
||||
</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 className="absolute top-4 right-4 z-10">
|
||||
<EducationalManual type="bridge" params={{ alpha }} />
|
||||
</div>
|
||||
@@ -227,6 +247,7 @@ const BridgeModule: React.FC = () => {
|
||||
fxPerLength={forces.fxPerLength}
|
||||
fzPerLength={forces.fzPerLength}
|
||||
alpha={alpha}
|
||||
viewMode={viewMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import { useState } from 'react';
|
||||
import { usePersistentState } from '@/hooks/usePersistentState';
|
||||
import { getDragCoefficient } from '@/lib/drag';
|
||||
import { useI18n } from '@/store/i18nStore';
|
||||
import { DragCoefficientChart } from '@/components/DragCoefficientChart';
|
||||
import { ResizableBox3D } from '@/components/three/ResizableBox3D';
|
||||
import SceneCanvas from '@/components/SceneCanvas';
|
||||
import { Environment } from '@react-three/drei';
|
||||
import { ViewerOrbitControls as OrbitControls } from '@/components/three/ViewerOrbitControls';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { Calculator, Info } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Wind, Box } from 'lucide-react';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
export default function CalcCaModule() {
|
||||
const { t } = useI18n();
|
||||
const [l1, setL1] = useState<number>(20);
|
||||
const [l2, setL2] = useState<number>(40);
|
||||
const [h, setH] = useState<number>(10);
|
||||
const [l1, setL1] = usePersistentState<number>('CalcCaModule_l1', 20);
|
||||
const [l2, setL2] = usePersistentState<number>('CalcCaModule_l2', 40);
|
||||
const [h, setH] = usePersistentState<number>('CalcCaModule_h', 10);
|
||||
const [viewMode, setViewMode] = usePersistentState<'solid' | 'airflow'>('CalcCaModule_viewMode', 'solid');
|
||||
|
||||
// Clamp inputs to avoid Infinity or negative values breaking the UI
|
||||
const safeL1 = Math.max(0.1, l1 || 1);
|
||||
@@ -26,73 +25,94 @@ export default function CalcCaModule() {
|
||||
const caHigh = getDragCoefficient(safeL1, safeL2, safeH, 'high');
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto space-y-6">
|
||||
<header className="flex items-center gap-4 mb-8">
|
||||
<div className="p-3 bg-primary/10 rounded-xl">
|
||||
<Calculator className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<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 */}
|
||||
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">{t('nav_calc_ca')}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Cálculo rápido de Coeficiente de Arrasto (Ca) - NBR 6123:2023, Fig. 4 e 5.
|
||||
</p>
|
||||
<CardTitle className="text-lg">Calc_Ca</CardTitle>
|
||||
<CardDescription>Cálculo rápido de Coeficiente de Arrasto (Ca)</CardDescription>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
{/* Left Column: Inputs & Results */}
|
||||
<div className="lg:col-span-4 space-y-6">
|
||||
<Card>
|
||||
<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))}
|
||||
/>
|
||||
<CardContent className="space-y-6">
|
||||
<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>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="l2" className="text-sm font-medium leading-none">L₂ (Profundidade paralela ao vento - m)</label>
|
||||
<Input
|
||||
id="l2"
|
||||
type="number"
|
||||
min="0.1"
|
||||
step="0.1"
|
||||
value={l2}
|
||||
onChange={(e) => setL2(parseFloat(e.target.value))}
|
||||
/>
|
||||
<Slider min={1} max={100} step={1} value={[l1]} onValueChange={(v) => setL1(v[0])} />
|
||||
</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 className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<label className="text-sm font-medium">L₂ (Profundidade)</label>
|
||||
<span className="font-mono text-sm">{l2} m</span>
|
||||
</div>
|
||||
<Slider min={1} max={100} step={1} value={[l2]} onValueChange={(v) => setL2(v[0])} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<label className="text-sm font-medium">H (Altura)</label>
|
||||
<span className="font-mono text-sm">{h} m</span>
|
||||
</div>
|
||||
<Slider min={1} max={100} step={1} value={[h]} onValueChange={(v) => setH(v[0])} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="border-primary/50 shadow-sm bg-primary/5">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Info className="w-5 h-5 text-primary" />
|
||||
Resultados (Ca)
|
||||
</CardTitle>
|
||||
{/* 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>
|
||||
|
||||
<SceneCanvas
|
||||
moduleId="calc_ca"
|
||||
camera={{ position: [50, 40, 50], fov: 45 }}
|
||||
fallback={<div />}
|
||||
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||
>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[10, 20, 10]} intensity={1} castShadow />
|
||||
<Environment preset="city" />
|
||||
<ResizableBox3D l1={safeL1} l2={safeL2} h={safeH} viewMode={viewMode} />
|
||||
<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>
|
||||
</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>
|
||||
<div className="space-y-4 mt-2">
|
||||
<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>
|
||||
@@ -105,49 +125,20 @@ export default function CalcCaModule() {
|
||||
<span>H/L₁ = {(safeH / safeL1).toFixed(2)}</span>
|
||||
<span>L₁/L₂ = {(safeL1 / safeL2).toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Center Column: 3D Visualization */}
|
||||
<div className="lg:col-span-4">
|
||||
<Card className="h-full flex flex-col">
|
||||
<CardHeader className="pb-2 shrink-0">
|
||||
<CardTitle className="text-lg">Visualização 3D</CardTitle>
|
||||
<CardDescription>Proporções dinâmicas</CardDescription>
|
||||
</CardHeader>
|
||||
<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">
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<EducationalManual type="calc_ca" params={{ l1: safeL1, l2: safeL2, h: safeH }} />
|
||||
</div>
|
||||
<SceneCanvas moduleId="calc_ca" camera={{ position: [50, 40, 50], fov: 45 }} fallback={<div />} frameloop="always">
|
||||
<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="lg:col-span-4">
|
||||
<Card className="h-full">
|
||||
{/* Á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)]">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Slider } from '@/components/ui/slider';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Box, Wind } from 'lucide-react';
|
||||
import { useWindStore } from '@/store/appStore';
|
||||
import { CpiSettingsCard } from '@/components/CpiSettingsCard';
|
||||
import { calculateDome, type DomeType } from '@/lib/modules/dome';
|
||||
@@ -22,6 +24,7 @@ const DomeModule: React.FC = () => {
|
||||
const [rise, setRise] = React.useState(5);
|
||||
const [wallHeight, setWallHeight] = React.useState(8);
|
||||
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]);
|
||||
|
||||
@@ -158,7 +161,27 @@ const DomeModule: React.FC = () => {
|
||||
Cpi = {cpi.toFixed(2)}
|
||||
</Badge>
|
||||
</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 }} />
|
||||
</div>
|
||||
<PressureLegend />
|
||||
@@ -171,6 +194,7 @@ const DomeModule: React.FC = () => {
|
||||
cpeBarlavento={result.cpeBarlavento}
|
||||
cpeTopo={result.cpeTopo}
|
||||
cpeLateral={result.cpeLateral}
|
||||
viewMode={viewMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { usePersistentState } from '@/hooks/usePersistentState';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
@@ -34,7 +35,7 @@ import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||
|
||||
const DynamicsModule: React.FC = () => {
|
||||
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 [height, setHeight] = React.useState(40);
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import React from 'react';
|
||||
import Warehouse3DViewer from '../components/Warehouse3D';
|
||||
import Heatmap2D from '../components/Heatmap2D';
|
||||
import LinearLoadsTable from '../components/LinearLoadsTable';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import FtoolExportCard from '../components/FtoolExportCard';
|
||||
import { CpiSettingsCard } from '../components/CpiSettingsCard';
|
||||
import { useGalpaoStore } from '../store/galpaoStore';
|
||||
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 { Slider } from '@/components/ui/slider';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
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 { AlertCircle, Lightbulb, Box, Wind } from 'lucide-react';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
@@ -65,11 +67,18 @@ const GalpaoModule: React.FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
|
||||
{/* 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">
|
||||
<Tabs defaultValue="geometria" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 mb-4">
|
||||
<TabsTrigger value="geometria">1. Geometria</TabsTrigger>
|
||||
<TabsTrigger value="permeabilidade">2. Permeabilidade</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="geometria" className="space-y-4 m-0">
|
||||
<Card className="shadow-sm border-border">
|
||||
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
|
||||
<div>
|
||||
<CardTitle className="text-lg">Geometria</CardTitle>
|
||||
<CardTitle className="text-lg">Geometria e Vento</CardTitle>
|
||||
<CardDescription>Tabelas 4 e 5 — NBR 6123.</CardDescription>
|
||||
</div>
|
||||
<SaveModuleDialog
|
||||
@@ -98,7 +107,7 @@ const GalpaoModule: React.FC = () => {
|
||||
<div className="flex gap-2">
|
||||
<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">
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -243,12 +252,17 @@ const GalpaoModule: React.FC = () => {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="permeabilidade" className="space-y-4 m-0">
|
||||
<CpiSettingsCard />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</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">
|
||||
<Tabs defaultValue="3d" className="w-full h-full flex flex-col">
|
||||
<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">
|
||||
<div className="flex flex-wrap gap-2 pointer-events-auto">
|
||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm border-muted-foreground/20 text-foreground shadow-sm">
|
||||
@@ -261,7 +275,22 @@ const GalpaoModule: React.FC = () => {
|
||||
q = {q.toFixed(3)} kN/m²
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex bg-background/80 backdrop-blur-sm rounded-md p-1 shadow-sm border border-border pointer-events-auto ml-auto">
|
||||
|
||||
<div className="flex pointer-events-auto bg-background/80 backdrop-blur-sm rounded-md p-1 shadow-sm border border-border">
|
||||
<TabsList className="h-7 px-1">
|
||||
<TabsTrigger value="3d" className="text-xs h-6 px-3">Modelo 3D</TabsTrigger>
|
||||
<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>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 pointer-events-auto justify-start sm:justify-end ml-auto">
|
||||
<EducationalManual type="warehouse" params={{ windAngle, cpi }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabsContent value="3d" className="flex-1 m-0 h-full w-full relative">
|
||||
<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">
|
||||
<Button
|
||||
variant={viewMode === 'solid' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
@@ -282,16 +311,31 @@ const GalpaoModule: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 pointer-events-auto justify-start sm:justify-end">
|
||||
<EducationalManual type="warehouse" params={{ windAngle, cpi }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PressureLegend />
|
||||
|
||||
<div className="w-full h-full cursor-grab active:cursor-grabbing bg-muted/20">
|
||||
<Warehouse3DViewer />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="heatmap" className="flex-1 m-0 h-full w-full bg-muted/10 overflow-auto flex items-center justify-center">
|
||||
<Heatmap2D />
|
||||
</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>
|
||||
|
||||
{/* 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 { Slider } from '@/components/ui/slider';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
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 SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
@@ -17,17 +19,22 @@ import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generi
|
||||
|
||||
const IsolatedRoofModule: React.FC = () => {
|
||||
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 [height, setHeight] = React.useState(2);
|
||||
const [width, setWidth] = React.useState(10);
|
||||
const [depth, setDepth] = React.useState(10);
|
||||
const [width, setWidth] = usePersistentState('IsolatedRoofModule_width', 10);
|
||||
const [depth, setDepth] = usePersistentState('IsolatedRoofModule_depth', 10);
|
||||
const [viewMode, setViewMode] = usePersistentState<'solid' | 'airflow'>('IsolatedRoofModule_viewMode', 'solid');
|
||||
|
||||
const result = useMemo(() => {
|
||||
if (type === 'shed') {
|
||||
const r = calculateIsolatedShedRoof({ theta, height, depth });
|
||||
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 });
|
||||
return { ...g, cph1: { high: 0, low: 0 }, cph2: { high: 0, low: 0 } };
|
||||
}, [type, theta, height, depth]);
|
||||
@@ -79,12 +86,15 @@ const IsolatedRoofModule: React.FC = () => {
|
||||
title: 'Geometria da Cobertura Isolada',
|
||||
type: 'grid',
|
||||
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: 'Largura (b)', value: `${width} m` },
|
||||
{ label: 'Profundidade (ℓ)', value: `${depth} 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',
|
||||
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'})`,
|
||||
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 />
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Tipo de Cobertura</label>
|
||||
<Select value={type} onValueChange={(v) => setType(v as 'shed' | 'gable')}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<Select value={type} onValueChange={(v: any) => setType(v)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Tipo de Cobertura" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="shed">Uma água (Tab. 24)</SelectItem>
|
||||
<SelectItem value="gable">Duas águas (Tab. 25)</SelectItem>
|
||||
<SelectItem value="shed">Uma Água</SelectItem>
|
||||
<SelectItem value="gable">Duas Águas</SelectItem>
|
||||
<SelectItem value="butterfly">Telhado Borboleta (Em V)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</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="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">{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 className="absolute top-4 right-4 z-10">
|
||||
<EducationalManual type="isolated-roof" params={{ theta, type }} />
|
||||
@@ -216,6 +247,7 @@ const IsolatedRoofModule: React.FC = () => {
|
||||
cpeLeeward={cpeLeeward}
|
||||
cpeTop={cpeTop}
|
||||
forceKN={forces.magnitude}
|
||||
viewMode={viewMode}
|
||||
/>
|
||||
</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 { useHydrationStore } from '@/store/hydrationStore';
|
||||
@@ -8,25 +9,27 @@ import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useWindStore } from '@/store/appStore';
|
||||
import { calculatePiperack, type PipeDef } from '@/lib/modules/piperack';
|
||||
import { isResonantRisk } from '@/lib/dynamics';
|
||||
import { Piperack3D } from '@/components/three/Piperack3D';
|
||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||
import ExportMenu from '../components/ExportMenu';
|
||||
import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
||||
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 { Input } from '@/components/ui/input';
|
||||
|
||||
const PiperackModule: React.FC = () => {
|
||||
const { q } = useWindStore();
|
||||
const [width, setWidth] = useState(17);
|
||||
const [height, setHeight] = useState(2);
|
||||
const [elevation, setElevation] = useState(6);
|
||||
const [spacing, setSpacing] = useState(2);
|
||||
const [numFrames, setNumFrames] = useState(2);
|
||||
const [phiStruct, setPhiStruct] = useState(0.5);
|
||||
const [pipes, setPipes] = useState<PipeDef[]>([]);
|
||||
const [width, setWidth] = usePersistentState('PiperackModule_width', 17);
|
||||
const [height, setHeight] = usePersistentState('PiperackModule_height', 2);
|
||||
const [elevation, setElevation] = usePersistentState('PiperackModule_elevation', 6);
|
||||
const [spacing, setSpacing] = usePersistentState('PiperackModule_spacing', 2);
|
||||
const [numFrames, setNumFrames] = usePersistentState('PiperackModule_numFrames', 2);
|
||||
const [phiStruct, setPhiStruct] = usePersistentState('PiperackModule_phiStruct', 0.5);
|
||||
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 clearPendingLoad = useHydrationStore((s) => s.clearPendingLoad);
|
||||
@@ -41,6 +44,7 @@ const PiperackModule: React.FC = () => {
|
||||
if (p.numFrames !== undefined) setNumFrames(p.numFrames);
|
||||
if (p.pipes !== undefined) setPipes(p.pipes);
|
||||
if (p.phiStruct !== undefined) setPhiStruct(p.phiStruct);
|
||||
if (p.f1 !== undefined) setF1(p.f1);
|
||||
clearPendingLoad();
|
||||
}
|
||||
}, [pendingLoad, clearPendingLoad]);
|
||||
@@ -157,7 +161,7 @@ const PiperackModule: React.FC = () => {
|
||||
</div>
|
||||
<SaveModuleDialog
|
||||
moduleType="piperack"
|
||||
inputs={{ elevation, width, height, spacing, numFrames, pipes, phiStruct }}
|
||||
inputs={{ elevation, width, height, spacing, numFrames, pipes, phiStruct, f1 }}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
@@ -219,6 +223,33 @@ const PiperackModule: React.FC = () => {
|
||||
</div>
|
||||
<Slider value={[phiStruct]} min={0.1} max={1.0} step={0.05} onValueChange={v => setPhiStruct(v[0])} />
|
||||
</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>
|
||||
|
||||
|
||||
@@ -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 { Button } from '@/components/ui/button';
|
||||
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 logoInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [importing, setImporting] = usePersistentState('SettingsModule_importing', false);
|
||||
const { companyLogo, setCompanyLogo } = useWindStore();
|
||||
|
||||
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 { Slider } from '@/components/ui/slider';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useWindStore } from '@/store/appStore';
|
||||
import { useHydrationStore } from '@/store/hydrationStore';
|
||||
import {
|
||||
calculateShelterWind,
|
||||
calculateShelterWindAllAngles,
|
||||
@@ -19,7 +21,7 @@ import { EducationalManual } from '@/components/EducationalManual';
|
||||
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||
import { Eye, Box, Layers, ArrowUpRight, CheckCircle2 } from 'lucide-react';
|
||||
import { Eye, Box, Layers, ArrowUpRight, CheckCircle2, Wind, AlertTriangle } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const conditionLabels: Record<ShelterCondition, string> = {
|
||||
@@ -31,17 +33,40 @@ const conditionLabels: Record<ShelterCondition, string> = {
|
||||
|
||||
const ShelterModule: React.FC = () => {
|
||||
const { q } = useWindStore();
|
||||
const [condition, setCondition] = useState<ShelterCondition>('cond1');
|
||||
const [windAngle, setWindAngle] = useState<ShelterWindAngle>(0);
|
||||
const [depth, setDepth] = useState(6);
|
||||
const [width, setWidth] = useState(10);
|
||||
const [height, setHeight] = useState(4.8);
|
||||
const [theta, setTheta] = useState(5);
|
||||
const [backClosure, setBackClosure] = useState(75);
|
||||
const [backRange, setBackRange] = useState<[number, number]>([0, 75]);
|
||||
const [leftClosure, setLeftClosure] = useState(100);
|
||||
const [rightClosure, setRightClosure] = useState(100);
|
||||
const [viewMode, setViewMode] = useState<'3d' | 'elevation'>('3d');
|
||||
const [condition, setCondition] = usePersistentState<ShelterCondition>('ShelterModule_condition', 'cond1');
|
||||
const [windAngle, setWindAngle] = usePersistentState<ShelterWindAngle>('ShelterModule_windAngle', 0);
|
||||
const [depth, setDepth] = usePersistentState('ShelterModule_depth', 6);
|
||||
const [width, setWidth] = usePersistentState('ShelterModule_width', 10);
|
||||
const [height, setHeight] = usePersistentState('ShelterModule_height', 4.8);
|
||||
const [theta, setTheta] = usePersistentState('ShelterModule_theta', 5);
|
||||
const [numColumns, setNumColumns] = usePersistentState('ShelterModule_numColumns', 2);
|
||||
const [backClosure, setBackClosure] = usePersistentState('ShelterModule_backClosure', 75);
|
||||
const [backRange, setBackRange] = usePersistentState<[number, number]>('ShelterModule_backRange', [0, 75]);
|
||||
const [leftClosure, setLeftClosure] = usePersistentState('ShelterModule_leftClosure', 100);
|
||||
const [rightClosure, setRightClosure] = usePersistentState('ShelterModule_rightClosure', 100);
|
||||
const [surfaceMass, setSurfaceMass] = usePersistentState('ShelterModule_surfaceMass', 15); // kg/m² para lona/telha leve
|
||||
const [viewMode, setViewMode] = usePersistentState<'3d' | 'elevation' | 'airflow'>('ShelterModule_viewMode', '3d');
|
||||
|
||||
const pendingLoad = useHydrationStore((s) => s.pendingLoad);
|
||||
const clearPendingLoad = useHydrationStore((s) => s.clearPendingLoad);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (pendingLoad && pendingLoad.module === 'shelter') {
|
||||
const p = pendingLoad.inputs as any;
|
||||
if (p.condition !== undefined) setCondition(p.condition);
|
||||
if (p.windAngle !== undefined) setWindAngle(p.windAngle);
|
||||
if (p.depth !== undefined) setDepth(p.depth);
|
||||
if (p.width !== undefined) setWidth(p.width);
|
||||
if (p.height !== undefined) setHeight(p.height);
|
||||
if (p.theta !== undefined) setTheta(p.theta);
|
||||
if (p.numColumns !== undefined) setNumColumns(p.numColumns);
|
||||
if (p.backClosure !== undefined) setBackClosure(p.backClosure);
|
||||
if (p.leftClosure !== undefined) setLeftClosure(p.leftClosure);
|
||||
if (p.rightClosure !== undefined) setRightClosure(p.rightClosure);
|
||||
if (p.surfaceMass !== undefined) setSurfaceMass(p.surfaceMass);
|
||||
clearPendingLoad();
|
||||
}
|
||||
}, [pendingLoad, clearPendingLoad, setCondition, setWindAngle, setDepth, setWidth, setHeight, setTheta, setNumColumns, setBackClosure, setLeftClosure, setRightClosure, setSurfaceMass]);
|
||||
|
||||
// Determinação automática da posição da fresta/vão a partir dos cursores (base e topo)
|
||||
const bottomGap = backRange[0];
|
||||
@@ -90,17 +115,21 @@ const ShelterModule: React.FC = () => {
|
||||
leftClosure,
|
||||
rightClosure,
|
||||
openingPos,
|
||||
numColumns,
|
||||
};
|
||||
return {
|
||||
result: calculateShelterWind({ ...input, windAngle }, q),
|
||||
envelope: calculateShelterWindAllAngles(input, q),
|
||||
};
|
||||
}, [condition, q, depth, width, height, theta, backClosure, backRange, leftClosure, rightClosure, openingPos, windAngle]);
|
||||
}, [condition, q, depth, width, height, theta, backClosure, backRange, leftClosure, rightClosure, openingPos, windAngle, numColumns]);
|
||||
|
||||
const effAreaRoof = depth * width;
|
||||
const effAreaBack = (width * height) * (backClosure / 100);
|
||||
const effAreaSideTotal = (depth * height) * ((leftClosure + rightClosure) / 100);
|
||||
|
||||
const gravityForce = (surfaceMass * effAreaRoof * 9.81) / 1000; // in kN
|
||||
const isUpliftCritical = envelope.criticalUp.value > gravityForce;
|
||||
|
||||
const handleExportPDF = () => {
|
||||
const angleLabel = windAngle === 0 ? '0° (Frontal)' : windAngle === 45 ? '45° (Oblíquo)' : '90° (Lateral)';
|
||||
const sections: GenericPDFSection[] = [
|
||||
@@ -122,12 +151,13 @@ const ShelterModule: React.FC = () => {
|
||||
title: `Coeficientes Aerodinâmicos e Pressões Líquidas - Direção ${angleLabel}`,
|
||||
type: 'grid',
|
||||
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: '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 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)}`
|
||||
],
|
||||
[
|
||||
'Cnf Vertical Líquido',
|
||||
String(envelope.res0.cnfVertical),
|
||||
String(envelope.res45.cnfVertical),
|
||||
String(envelope.res90.cnfVertical),
|
||||
`Crítico: ${Math.min(envelope.res0.cnfVertical, envelope.res45.cnfVertical, envelope.res90.cnfVertical)}`
|
||||
'Cnf Vertical Médio | Borda',
|
||||
`${envelope.res0.cnfVertical} | ${envelope.res0.cnfEdgeVertical}`,
|
||||
`${envelope.res45.cnfVertical} | ${envelope.res45.cnfEdgeVertical}`,
|
||||
`${envelope.res90.cnfVertical} | ${envelope.res90.cnfEdgeVertical}`,
|
||||
`Crítico Borda: ${Math.min(envelope.res0.cnfEdgeVertical, envelope.res45.cnfEdgeVertical, envelope.res90.cnfEdgeVertical)}`
|
||||
],
|
||||
[
|
||||
'Força Vertical Telhado [Fz ↑]',
|
||||
@@ -282,6 +312,13 @@ const ShelterModule: React.FC = () => {
|
||||
`${envelope.res90.lineLoads.beamLoad} kN/m`,
|
||||
`${Math.max(envelope.res0.lineLoads.beamLoad, envelope.res45.lineLoads.beamLoad, envelope.res90.lineLoads.beamLoad).toFixed(2)} kN/m`
|
||||
],
|
||||
[
|
||||
'Momento na Raiz da Viga',
|
||||
`${envelope.res0.lineLoads.beamMoment} kN.m`,
|
||||
`${envelope.res45.lineLoads.beamMoment} kN.m`,
|
||||
`${envelope.res90.lineLoads.beamMoment} kN.m`,
|
||||
`${Math.max(envelope.res0.lineLoads.beamMoment, envelope.res45.lineLoads.beamMoment, envelope.res90.lineLoads.beamMoment).toFixed(2)} kN.m`
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -298,14 +335,19 @@ const ShelterModule: React.FC = () => {
|
||||
title: `Cargas Lineares para Dimensionamento (${angleLabel}) (kN/m)`,
|
||||
type: 'grid',
|
||||
gridItems: [
|
||||
{ label: 'Carga Horizontal no Pilar Traseiro', value: `${result.lineLoads.columnLoad} kN/m` },
|
||||
{ label: 'Carga Vertical na Viga em Balanço', value: `${result.lineLoads.beamLoad} kN/m` },
|
||||
{ label: 'Carga Horiz. Pórtico Central', value: `${result.lineLoads.columnLoad} kN/m` },
|
||||
{ label: 'Carga Horiz. Extremidade', value: `${result.lineLoads.columnLoadEdge} kN/m` },
|
||||
{ label: 'Carga Vert. Média Viga Central', value: `${result.lineLoads.beamLoad} kN/m` },
|
||||
{ label: 'Carga Vert. Média Extremidade', value: `${result.lineLoads.beamLoadEdge} kN/m` },
|
||||
{ label: 'Momento Engaste Viga Central', value: `${result.lineLoads.beamMoment} kN.m` },
|
||||
{ label: 'Momento Engaste Extremidade', value: `${result.lineLoads.beamMomentEdge} kN.m` },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Parecer Técnico do Regime Topológico e Aerodinâmico',
|
||||
title: 'Parecer Técnico e Alertas Aerodinâmicos (Zonas de Borda)',
|
||||
type: 'text',
|
||||
content: `${result.statusText} Permeabilidade líquida da parede de fundo: ${100 - backClosure}%. Alívio de arrancamento obtido pela fresta superior: ${result.reliefPercentage}%. 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',
|
||||
@@ -351,7 +393,7 @@ const ShelterModule: React.FC = () => {
|
||||
</div>
|
||||
<SaveModuleDialog
|
||||
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>
|
||||
<CardContent className="space-y-5">
|
||||
@@ -476,6 +518,28 @@ const ShelterModule: React.FC = () => {
|
||||
</div>
|
||||
<Slider min={0} max={30} step={1} value={[theta]} onValueChange={(v) => setTheta(v[0])} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span>Número de Pórticos (Pilares):</span>
|
||||
<span className="font-mono font-medium">{numColumns} pórticos</span>
|
||||
</div>
|
||||
<Slider min={2} max={20} step={1} value={[numColumns]} onValueChange={(v) => setNumColumns(v[0])} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 pt-2 border-t">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span>Massa Superficial (kg/m²):</span>
|
||||
<span className="font-mono font-medium">{surfaceMass} kg/m²</span>
|
||||
</div>
|
||||
<Slider min={2} max={100} step={1} value={[surfaceMass]} onValueChange={(v) => setSurfaceMass(v[0])} />
|
||||
<p className="text-[10px] text-muted-foreground mt-1">Lonas ~3kg/m², Telhas Metálicas ~10-15kg/m²</p>
|
||||
{isUpliftCritical && (
|
||||
<div className="mt-2 p-2 bg-destructive/10 border border-destructive/20 rounded text-xs text-destructive flex gap-2 items-start">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5" />
|
||||
<p>Alerta de Arrancamento: Força (↑) {envelope.criticalUp.value.toFixed(1)} kN > Peso ({gravityForce.toFixed(1)} kN). Exige ancoragem/estaiamento.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -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')}>
|
||||
<Box className="size-3 mr-1" /> Vista 3D
|
||||
</Button>
|
||||
<Button size="sm" variant={viewMode === 'airflow' ? 'default' : 'ghost'} className="h-6 px-2.5 text-xs rounded-full" onClick={() => setViewMode('airflow')}>
|
||||
<Wind className="size-3 mr-1" /> Fluxo
|
||||
</Button>
|
||||
<Button size="sm" variant={viewMode === 'elevation' ? 'default' : 'ghost'} className="h-6 px-2.5 text-xs rounded-full" onClick={() => setViewMode('elevation')}>
|
||||
<Eye className="size-3 mr-1" /> Corte A-A
|
||||
</Button>
|
||||
@@ -512,7 +579,7 @@ const ShelterModule: React.FC = () => {
|
||||
<EducationalManual type="shelter" params={{ condition, backClosure, openingPos, theta, windAngle }} />
|
||||
</div>
|
||||
|
||||
{viewMode === '3d' ? (
|
||||
{viewMode === '3d' || viewMode === 'airflow' ? (
|
||||
<Shelter3D
|
||||
condition={condition}
|
||||
depth={depth}
|
||||
@@ -525,6 +592,7 @@ const ShelterModule: React.FC = () => {
|
||||
rightClosure={rightClosure}
|
||||
openingPos={openingPos}
|
||||
windAngle={windAngle}
|
||||
numColumns={numColumns}
|
||||
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>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-xs text-muted-foreground">
|
||||
<span>Coeficiente Cnf</span>
|
||||
<span className="font-mono">{result.cnfVertical}</span>
|
||||
<span>Cnf Médio | Cnf Borda</span>
|
||||
<span className="font-mono">{result.cnfVertical} | <span className="text-destructive font-bold">{result.cnfEdgeVertical}</span></span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-[11px] text-muted-foreground/80">
|
||||
<span>Pressão (/m²)</span>
|
||||
@@ -592,13 +660,17 @@ const ShelterModule: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center pb-2 border-b">
|
||||
<span className="text-primary font-medium">Carga no Pilar (Pórtico)</span>
|
||||
<span className="text-primary font-medium">Carga no Pilar (Central)</span>
|
||||
<span className="font-mono font-bold text-emerald-600 dark:text-emerald-400">{result.lineLoads.columnLoad.toFixed(2)} kN/m</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center pb-3 border-b">
|
||||
<span className="text-primary font-medium">Carga na Viga (Balanço)</span>
|
||||
<div className="flex justify-between items-center pt-2">
|
||||
<span className="text-primary font-medium">Carga Vertical Viga (Central)</span>
|
||||
<span className="font-mono font-bold text-sky-600 dark:text-sky-400">{result.lineLoads.beamLoad.toFixed(2)} kN/m</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center pb-3 border-b text-xs text-muted-foreground">
|
||||
<span>Momento Engaste (Asa)</span>
|
||||
<span className="font-mono text-destructive font-bold">{result.lineLoads.beamMoment.toFixed(2)} kN.m</span>
|
||||
</div>
|
||||
|
||||
<div className="pt-1 space-y-2">
|
||||
<div className="font-bold text-xs text-primary flex items-center justify-between">
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Slider } from '@/components/ui/slider';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Box, Wind } from 'lucide-react';
|
||||
import { useWindStore } from '@/store/appStore';
|
||||
import { calculateSign } from '@/lib/nbr-tables/table-23';
|
||||
import Sign3DViewer from '@/components/three/Sign3D';
|
||||
@@ -22,6 +24,7 @@ const SignModule: React.FC = () => {
|
||||
const [alpha, setAlpha] = React.useState<90 | 50>(90);
|
||||
const [hasEndPlates, setHasEndPlates] = React.useState(true);
|
||||
const [groundClearance, setGroundClearance] = React.useState(0.5);
|
||||
const [viewMode, setViewMode] = React.useState<'solid' | 'airflow'>('solid');
|
||||
|
||||
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="outline" className="bg-background/80 backdrop-blur-sm text-foreground border border-border/80 shadow-sm">Cf = {result.cf.toFixed(2)}</Badge>
|
||||
</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 }} />
|
||||
</div>
|
||||
<PressureLegend />
|
||||
@@ -169,6 +192,7 @@ const SignModule: React.FC = () => {
|
||||
cf={result.cf}
|
||||
forceKN={result.forceKN}
|
||||
applicationPoint={result.applicationPoint}
|
||||
viewMode={viewMode}
|
||||
/>
|
||||
</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 { Slider } from '@/components/ui/slider';
|
||||
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 [baseWidth, setBaseWidth] = React.useState(3);
|
||||
const [height, setHeight] = React.useState(30);
|
||||
const [panels, setPanels] = React.useState(6);
|
||||
const [phi, setPhi] = React.useState(0.3);
|
||||
const [panels, setPanels] = React.useState(5);
|
||||
const [phi, setPhi] = React.useState(0.2);
|
||||
const [alphaWind, setAlphaWind] = React.useState<0 | 45 | 90>(0);
|
||||
const [viewMode, setViewMode] = usePersistentState<'solid' | 'airflow'>('TowerModule_viewMode', 'solid');
|
||||
|
||||
const result = useMemo(() => {
|
||||
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">α = {alphaWind}°</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 className="absolute top-4 right-4 z-10">
|
||||
<EducationalManual type="tower" params={{ section, alphaWind }} />
|
||||
</div>
|
||||
@@ -190,6 +210,7 @@ const TowerModule: React.FC = () => {
|
||||
phi={phi}
|
||||
alphaWind={alphaWind}
|
||||
forceKN={result.forceKN}
|
||||
viewMode={viewMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
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 { calculateVault, type VaultRegime } from '@/lib/modules/vault';
|
||||
import Vault3DViewer from '@/components/three/Vault3D';
|
||||
@@ -24,6 +26,7 @@ const VaultModule: React.FC = () => {
|
||||
const [regime, setRegime] = React.useState<VaultRegime>('laminar-rough');
|
||||
const [localCpi, setLocalCpi] = React.useState<number>(0);
|
||||
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]);
|
||||
|
||||
@@ -167,7 +170,27 @@ const VaultModule: React.FC = () => {
|
||||
Cpi = {localCpi > 0 ? `+${localCpi.toFixed(2)}` : localCpi.toFixed(2)}
|
||||
</Badge>
|
||||
</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">
|
||||
<TabsList className="h-8 p-1">
|
||||
<TabsTrigger value="perpendicular" className="text-xs px-3">Vento ⊥</TabsTrigger>
|
||||
@@ -186,6 +209,7 @@ const VaultModule: React.FC = () => {
|
||||
cpeProfile={{ ...result.windPerpendicular }}
|
||||
cpeParallel={{ ...result.windParallel }}
|
||||
viewDirection={viewDirection}
|
||||
viewMode={viewMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface GlobalWindState {
|
||||
largestDimension: number;
|
||||
heightZ: number;
|
||||
locality: string | null;
|
||||
fviz: number; // Fator de vizinhança 3D (0.7 a 1.3)
|
||||
|
||||
// Saídas (Calculados)
|
||||
structureClass: StructureClass;
|
||||
@@ -42,6 +43,7 @@ export interface GlobalWindState {
|
||||
setPermeabilityCase: (c: PermeabilityCase) => void;
|
||||
setCpiRatio: (r: number) => void;
|
||||
setCpiManual: (c: number) => void;
|
||||
setFviz: (f: number) => void;
|
||||
companyLogo: string | null;
|
||||
setCompanyLogo: (logo: string | null) => void;
|
||||
|
||||
@@ -81,7 +83,7 @@ export const useWindStore = create<GlobalWindState>((set, get) => {
|
||||
structureClass: calc.structClass,
|
||||
s2: calc.s2,
|
||||
vk: calc.vk,
|
||||
q: calc.q,
|
||||
q: calc.q * (state.fviz ?? 1.0),
|
||||
cpi: calcCpi(state.permeabilityCase, state.cpiRatio, state.windAngle),
|
||||
};
|
||||
};
|
||||
@@ -95,6 +97,7 @@ export const useWindStore = create<GlobalWindState>((set, get) => {
|
||||
largestDimension: 30,
|
||||
heightZ: 10,
|
||||
locality: null,
|
||||
fviz: 1.0,
|
||||
|
||||
structureClass: 'B',
|
||||
s2: 1.06,
|
||||
@@ -169,9 +172,10 @@ export const useWindStore = create<GlobalWindState>((set, get) => {
|
||||
},
|
||||
setCpiManual: (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' ?
|
||||
// Wait, there is no 'custom'. We just set the value.
|
||||
},
|
||||
setFviz: (f) => {
|
||||
set({ fviz: f });
|
||||
get().updateCalculations();
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { create } from 'zustand';
|
||||
import { temporal } from 'zundo';
|
||||
import { getWallCpeOfficial as getWallCpe, getRoofCpeOfficial as getRoofCpe } from '../lib/coefficients';
|
||||
import { useWindStore } from './appStore';
|
||||
import type { WallCoefficients, RoofCoefficients } from '../lib/coefficients';
|
||||
@@ -40,7 +41,9 @@ const initHeight = 6;
|
||||
const initPitch = 10;
|
||||
const initAngle: 0 | 90 = 0;
|
||||
|
||||
export const useGalpaoStore = create<GalpaoState>((set, get) => ({
|
||||
export const useGalpaoStore = create<GalpaoState>()(
|
||||
temporal(
|
||||
(set, get) => ({
|
||||
template: 'standard',
|
||||
extraHeight: 1.5,
|
||||
skirtHeight: 4.5,
|
||||
@@ -85,4 +88,4 @@ export const useGalpaoStore = create<GalpaoState>((set, get) => ({
|
||||
set({ roofPitch: val });
|
||||
get().updateCoefficients();
|
||||
},
|
||||
}));
|
||||
})));
|
||||
+34
-1
@@ -1,6 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
@@ -24,7 +25,39 @@ try {
|
||||
|
||||
// https://vite.dev/config/
|
||||
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: {
|
||||
alias: {
|
||||
"@": 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