Compare commits
10 Commits
9fece3f174
...
d40b3b0c9c
| Author | SHA1 | Date | |
|---|---|---|---|
| d40b3b0c9c | |||
| dc3721e12b | |||
| 9726f3c503 | |||
| 55221b3214 | |||
| c00a3b861a | |||
| ff771b5ac8 | |||
| f79d87a0c2 | |||
| dc7bebd339 | |||
| 59ac5c1827 | |||
| 40e73e734a |
+71
-11
@@ -14,15 +14,15 @@
|
|||||||
|
|
||||||
| Item | Valor |
|
| Item | Valor |
|
||||||
|------|-------|
|
|------|-------|
|
||||||
| Data da última atualização | 2026-07-07 |
|
| Data da última atualização | 2026-07-10 |
|
||||||
| Marcos concluídos | **8 / 8** (plano completo) + M9.1 ✅ + M9.2 ✅ + M9.3 ✅ + M9.4 ✅ + M9.5 ✅ + M9.6 ✅ + M9.7 ✅ + M9.8 ✅ + M9.9 ✅ + M9.10 ✅ + M9.13 ✅ |
|
| Marcos concluídos | **8 / 8** (plano completo) + M9.1 ✅ + M9.2 ✅ + M9.3 ✅ + M9.4 ✅ + M9.5 ✅ + M9.6 ✅ + M9.7 ✅ + M9.8 ✅ + M9.9 ✅ + M9.10 ✅ + M9.13 ✅ + M9.14 ✅ + M9.15 ✅ + M9.16 ✅ |
|
||||||
| Cobertura NBR 6123:2023 | **100% das 11 seções + 3 anexos** |
|
| Cobertura NBR 6123:2023 | **100% das 11 seções + 3 anexos** |
|
||||||
| Linhas de código TS/TSX | ~7.400 |
|
| Linhas de código TS/TSX | ~8.200 |
|
||||||
| Arquivos `.ts`/`.tsx` no `src/` | **110** |
|
| Arquivos `.ts`/`.tsx` no `src/` | **120** |
|
||||||
| Páginas (rotas) | **10** |
|
| Páginas (rotas) | **10** |
|
||||||
| Módulos Strategy | **7** |
|
| Módulos Strategy | **7** |
|
||||||
| Tabelas da norma implementadas | **36 / 36** + 3 anexos (auditadas M9.1) |
|
| Tabelas da norma implementadas | **36 / 36** + 3 anexos (auditadas M9.1) |
|
||||||
| Testes Vitest | **310 passando / 310 totais** |
|
| Testes Vitest | **316 passando / 316 totais** |
|
||||||
| Build de produção | ✅ passa (2645 módulos) |
|
| Build de produção | ✅ passa (2645 módulos) |
|
||||||
| Lint (oxlint) | ✅ 0 erros |
|
| Lint (oxlint) | ✅ 0 erros |
|
||||||
| Persistência local | ✅ IndexedDB (Dexie-style) |
|
| Persistência local | ✅ IndexedDB (Dexie-style) |
|
||||||
@@ -311,10 +311,10 @@ cd /root/Apps/windapp/app
|
|||||||
# 1. Compilação TS (deve passar sem erros)
|
# 1. Compilação TS (deve passar sem erros)
|
||||||
./node_modules/.bin/tsc -b
|
./node_modules/.bin/tsc -b
|
||||||
|
|
||||||
# 2. Testes (deve mostrar 38/38 passing)
|
# 2. Testes (deve mostrar 316/316 passing)
|
||||||
./node_modules/.bin/vitest run
|
./node_modules/.bin/vitest run
|
||||||
|
|
||||||
# 3. Lint (deve mostrar 0 errors, ~5 warnings cosméticos)
|
# 3. Lint (deve mostrar 0 errors, ~10 warnings cosméticos)
|
||||||
./node_modules/.bin/oxlint
|
./node_modules/.bin/oxlint
|
||||||
|
|
||||||
# 4. Build de produção
|
# 4. Build de produção
|
||||||
@@ -326,9 +326,9 @@ npm run dev
|
|||||||
|
|
||||||
**Resultado esperado:**
|
**Resultado esperado:**
|
||||||
- ✅ tsc: silent (sem output)
|
- ✅ tsc: silent (sem output)
|
||||||
- ✅ vitest: `Test Files 5 passed (5) | Tests 38 passed (38)`
|
- ✅ vitest: `Test Files 16 passed (16) | Tests 316 passed (316)`
|
||||||
- ✅ oxlint: `Found N warnings and 0 errors`
|
- ✅ oxlint: `Found 10 warnings and 0 errors`
|
||||||
- ✅ vite: `✓ built in ~2s` com `dist/assets/index-*.js ~2.9 MB`
|
- ✅ vite: `✓ built in ~2.5s` com `dist/assets/index-*.js ~3.2 MB`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -634,6 +634,66 @@ gráficos SVG inline para variáveis CSS do tema, suportando dark mode.
|
|||||||
- Refatoração de `ExportMenu.tsx` para receber callbacks customizados, permitindo a exportação de PDF com suporte à visualização 3D para qualquer módulo de engenharia suportado pela NBR 6123.
|
- Refatoração de `ExportMenu.tsx` para receber callbacks customizados, permitindo a exportação de PDF com suporte à visualização 3D para qualquer módulo de engenharia suportado pela NBR 6123.
|
||||||
- Testes de build (`tsc -b` e `vite build`) passando com sucesso e mantendo a integridade (310/310 testes unitários sem regressões).
|
- Testes de build (`tsc -b` e `vite build`) passando com sucesso e mantendo a integridade (310/310 testes unitários sem regressões).
|
||||||
|
|
||||||
|
### M9.16 — Ferramenta de Auditoria LLM (Settings → Testes)
|
||||||
|
**Status:** ✅ Concluído (2026-07-10)
|
||||||
|
**Escopo:** Criar ferramenta temporária (dentro da página de Configurações, aba "Testes") que valida os cálculos do VentoApp contra a NBR 6123:2023 usando LLMs sequenciais. 110+ cenários de teste cobrindo todos os 10 módulos de estrutura + edge cases + variantes de Cpi.
|
||||||
|
**Entregas:**
|
||||||
|
- `app/src/lib/audit/types.ts` — Tipos centrais: `AuditScenario`, `AuditConfig`, `AuditReport`, `ScenarioResult`, `CheckStatus`, `LLMProvider`, `RouteModuleMap`
|
||||||
|
- `app/src/lib/audit/norm-reference.ts` — Dados extraídos das tabelas existentes (S₂, Cpe paredes/telhados, cilindros, pontes, fórmulas dinâmicas) para servir como ground-truth ao LLM
|
||||||
|
- `app/src/lib/audit/invariantes.ts` — 35+ regras da NBR 6123 (limites de Cpi, fórmula q, ranges de S₂, etc.)
|
||||||
|
- `app/src/lib/audit/scenarios.ts` — Gerador de 110+ cenários: Blessmann (10), varredura de geometrias (galpão, cilindro, ponte, placa, torre), variantes de Cpi (6 casos), edge cases (z mínimo, saturação, V₀, classe), cross-module, dinâmica (conforto)
|
||||||
|
- `app/src/lib/audit/serializer.ts` — Serialização de cenário para texto (formato legível por LLM)
|
||||||
|
- `app/src/lib/audit/prompt-builder.ts` — Montagem do prompt: system (norma + invariantes) + user (cenários serializados)
|
||||||
|
- `app/src/lib/audit/runner.ts` — Chamada a 4 provedores (OpenAI, Anthropic, MiniMax, OpenRouter) + Ollama local com retry, timeout 180s, `runAudit()` orquestrador
|
||||||
|
- `app/src/lib/audit/parser.ts` — Parse da resposta JSON do LLM + `buildAuditReport()` que agrupa por módulo
|
||||||
|
- `app/src/lib/audit/export-audit-pdf.tsx` — PDF do relatório via `@react-pdf/renderer`
|
||||||
|
- `app/src/components/AuditPanel.tsx` — Painel principal: config (provider/model/key), executar, barra de progresso, tabela de resultados por módulo
|
||||||
|
- `app/src/components/AuditDetailPopup.tsx` — Dialog com FallbackDiagram SVG + tabela de checks + inputs JSON + botões Exportar PDF / Copiar para módulo
|
||||||
|
- `app/src/pages/SettingsModule.tsx` — Adicionada aba "Testes" via `Tabs` (Configurações + Testes)
|
||||||
|
- `app/src/lib/i18n.ts` — 30 chaves de tradução para auditoria (pt-BR + en-US)
|
||||||
|
- **Total:** 316/316 testes passando (era 310/310).
|
||||||
|
|
||||||
|
**Provedores suportados:**
|
||||||
|
| Provedor | Modelo padrão | API Base |
|
||||||
|
|----------|---------------|----------|
|
||||||
|
| OpenAI | `gpt-4o` | `api.openai.com/v1` |
|
||||||
|
| Anthropic | `claude-3-5-sonnet-latest` | `api.anthropic.com/v1` |
|
||||||
|
| MiniMax | `minimax-m3.0` (ou m2.7) | `api.minimax.chat/v1` |
|
||||||
|
| OpenRouter | `openai/gpt-4o` (qualquer model) | `openrouter.ai/api/v1` |
|
||||||
|
| Ollama | `deepseek-r1` (local) | `localhost:11434` |
|
||||||
|
|
||||||
|
**Arquitetura:**
|
||||||
|
- Cálculo puro offline: `scenarios.ts` gera 110+ cenários com inputs/referências/faixas esperadas
|
||||||
|
- Prompt enviado ao LLM com a norma como contexto (invariantes + tabelas)
|
||||||
|
- LLM retorna JSON por cenário: `{ scenarioId, verdict, checks[], summary }`
|
||||||
|
- `buildAuditReport()` agrega por módulo (galpão, cilindro, etc.)
|
||||||
|
- Interface em Settings → Testes: escolhe provedor, executa, vê resultado em tabela
|
||||||
|
- Detalhes em popup com diagrama SVG 2D + checks individuais + inputs
|
||||||
|
- PDF exportável com resumo + todos os checks por módulo
|
||||||
|
- Botão "Copiar para o módulo" navega para rota correspondente
|
||||||
|
|
||||||
|
**Inputs validados:** V₀, S₁, S₂, S₃, Cpi (6 casos), Cpe paredes (4 faces), Cpe telhados (6 zonas), drag, atrito, coeficientes de cilindros (Re, h/d, perfil), classificação de pontes (Pse, Vit), Cf de placas, conforto humano (ISO 10137), parâmetros de torres.
|
||||||
|
|
||||||
|
#### M9.14 — Manual Didático Interativo com Animações SVG
|
||||||
|
**Status:** ✅ Concluído (2026-07-10)
|
||||||
|
**Escopo:** Criar um super manual colorido, ilustrativo e acessível com explicações interpretativas da norma NBR 6123, incluindo fluxos de vento animados em SVG para todos os módulos (Cilindros, Galpões, Cúpulas, Abóbadas, Muros, Pontes, Barras, Coberturas Isoladas).
|
||||||
|
**Entregas:**
|
||||||
|
- `app/src/components/WindFlowGrid2D.tsx` robusto e inteligente, renderizando variações didáticas automáticas para cada módulo (ex: vento a 0° vs 90° em galpões, e forças +Fz de arrancamento na borda de coberturas).
|
||||||
|
- Animações fluídas em CSS: `wind-line-slow` (fluxo laminar) e `wind-line-fast` (fluxo acelerado/sucção, em roxo para fácil distinção).
|
||||||
|
- Zonas de esteira turbulenta e setas vetoriais (Cpi, Sustentação/Downforce) acopladas ao SVG com legendas dinâmicas perfeitamente combinadas à exibição.
|
||||||
|
- Suporte estrito e bonito ao modo Dark / Light, fugindo de cores agressivas (usando tailwind `dark:bg-*`).
|
||||||
|
- Componente em barra lateral `EducationalManual.tsx` disponível universalmente em todas as views.
|
||||||
|
|
||||||
|
#### M9.15 — Rebranding e Oficialização: BrainWind v1.0
|
||||||
|
**Status:** ✅ Concluído (2026-07-10)
|
||||||
|
**Escopo:** Oficializar o nome e versão do produto alterando referências legadas ("VentoApp", "appvento") para **BrainWind v1.0**.
|
||||||
|
**Entregas:**
|
||||||
|
- Modificação no `index.html` (SEO, `<title>` e meta description).
|
||||||
|
- Atualização em massa dos dicionários globais (`i18n.ts`) refletindo nos headers da UI principal.
|
||||||
|
- Adequação do rodapé dos manuais didáticos, bem como de todos os geradores de memórias de cálculo em PDF (`export-pdf.tsx`, `export-generic-pdf.tsx`).
|
||||||
|
- Correção nos scripts geradores de inputs para o software estrutural Ftool (`export-ftool.ts`).
|
||||||
|
- Atualização e refatoração nas asserções das suítes de testes (`i18n.test.ts`, `export-ftool.test.ts`) para suportar a validação do novo nome.
|
||||||
|
|
||||||
#### M9.11 — Persistência opcional em servidor (~especulativo)
|
#### M9.11 — Persistência opcional em servidor (~especulativo)
|
||||||
**Problema:** Tudo é local (IndexedDB).
|
**Problema:** Tudo é local (IndexedDB).
|
||||||
**Tarefa:**
|
**Tarefa:**
|
||||||
@@ -705,7 +765,7 @@ gráficos SVG inline para variáveis CSS do tema, suportando dark mode.
|
|||||||
|
|
||||||
## 🎯 Próximo passo sugerido
|
## 🎯 Próximo passo sugerido
|
||||||
|
|
||||||
**M9.7 já está completo.** Para próximos passos, considerar:
|
**M9.16 já está completo.** Para próximos passos, considerar:
|
||||||
**M9.11 — Testes E2E com Playwright** (~2 dias, médio impacto) —
|
**M9.11 — Testes E2E com Playwright** (~2 dias, médio impacto) —
|
||||||
instalar `@playwright/test`, criar testes para fluxos completos
|
instalar `@playwright/test`, criar testes para fluxos completos
|
||||||
(criar projeto galpão → salvar → exportar PDF), integrar ao CI.
|
(criar projeto galpão → salvar → exportar PDF), integrar ao CI.
|
||||||
|
|||||||
+2
-2
@@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="theme-color" content="#6b21a8" />
|
<meta name="theme-color" content="#6b21a8" />
|
||||||
<meta name="description" content="VentoApp — Cálculo de cargas de vento conforme NBR 6123:2023. Galpões, cilindros, abóbadas, cúpulas, muros, barras, pontes, dinâmica." />
|
<meta name="description" content="BrainWind v1.0 — Cálculo de cargas de vento conforme NBR 6123:2023. Galpões, cilindros, abóbadas, cúpulas, muros, barras, pontes, dinâmica." />
|
||||||
<title>VentoApp — NBR 6123:2023</title>
|
<title>BrainWind v1.0 — NBR 6123:2023</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogClose,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { FileDown, Copy, X } from 'lucide-react';
|
||||||
|
import type { AuditScenario, ScenarioResult } from '@/lib/audit/types';
|
||||||
|
import FallbackDiagram from '@/components/FallbackDiagram';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
import type { RouteModuleMap } from '@/lib/audit/types';
|
||||||
|
|
||||||
|
const MODULE_ROUTES: RouteModuleMap = {
|
||||||
|
galpao: '/galpao',
|
||||||
|
cilindro: '/cilindro',
|
||||||
|
vault: '/abobada',
|
||||||
|
dome: '/cupula',
|
||||||
|
sign: '/muros',
|
||||||
|
'isolated-roof': '/cobertura-isolada',
|
||||||
|
bar: '/barras',
|
||||||
|
bridge: '/pontes',
|
||||||
|
tower: '/torres',
|
||||||
|
dynamics: '/dinamica',
|
||||||
|
'cross-module': '/galpao',
|
||||||
|
'edge-case': '/galpao',
|
||||||
|
'cpi-variant': '/galpao',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AuditDetailPopupProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
scenario: AuditScenario | null;
|
||||||
|
result: ScenarioResult | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AuditDetailPopup({ open, onOpenChange, scenario, result }: AuditDetailPopupProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
if (!scenario) return null;
|
||||||
|
|
||||||
|
const handleCopyToModule = () => {
|
||||||
|
const route = MODULE_ROUTES[scenario.module];
|
||||||
|
if (route) {
|
||||||
|
onOpenChange(false);
|
||||||
|
navigate(route);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExportPDF = () => {
|
||||||
|
if (!result) return;
|
||||||
|
import('@/lib/audit/export-audit-pdf').then((mod) => {
|
||||||
|
const report = {
|
||||||
|
id: 'popup-export',
|
||||||
|
provider: 'audit',
|
||||||
|
model: 'manual',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
totalScenarios: 1,
|
||||||
|
totalPassed: result.verdict === 'PASS' ? 1 : 0,
|
||||||
|
totalWarnings: result.verdict === 'WARN' ? 1 : 0,
|
||||||
|
totalFailed: result.verdict === 'FAIL' ? 1 : 0,
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
module: scenario.module,
|
||||||
|
moduleLabel: scenario.moduleLabel,
|
||||||
|
totalScenarios: 1,
|
||||||
|
passed: result.verdict === 'PASS' ? 1 : 0,
|
||||||
|
warnings: result.verdict === 'WARN' ? 1 : 0,
|
||||||
|
failed: result.verdict === 'FAIL' ? 1 : 0,
|
||||||
|
scenarios: [result],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rawResponse: undefined,
|
||||||
|
};
|
||||||
|
mod.exportAuditReportToPDF(report);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusIcon = (v: string) => {
|
||||||
|
switch (v) {
|
||||||
|
case 'PASS': return <span className="text-emerald-600 font-bold">✓</span>;
|
||||||
|
case 'WARN': return <span className="text-amber-600 font-bold">⚠</span>;
|
||||||
|
case 'FAIL': return <span className="text-red-600 font-bold">✗</span>;
|
||||||
|
default: return <span className="text-muted-foreground">?</span>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[600px] max-h-[85vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2 text-base">
|
||||||
|
{scenario.id}
|
||||||
|
{result && <span className="text-sm font-normal">{statusIcon(result.verdict)} {result.verdict}</span>}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{scenario.description} — {scenario.moduleLabel} · {scenario.nbrSection}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="w-full h-48 rounded-md overflow-hidden border bg-muted/10 mb-3">
|
||||||
|
<FallbackDiagram type={scenario.diagramType as never} props={scenario.diagramProps as never} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-xs mb-3">
|
||||||
|
<div className="rounded-md border bg-muted/30 p-2">
|
||||||
|
<span className="text-muted-foreground">Módulo:</span>{' '}
|
||||||
|
<span className="font-medium">{scenario.moduleLabel}</span>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border bg-muted/30 p-2">
|
||||||
|
<span className="text-muted-foreground">NBR:</span>{' '}
|
||||||
|
<span className="font-medium">{scenario.nbrSection}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{result && result.checks.length > 0 && (
|
||||||
|
<div className="space-y-1 mb-3">
|
||||||
|
<h4 className="text-sm font-semibold">Verificações</h4>
|
||||||
|
<div className="rounded-md border overflow-hidden">
|
||||||
|
<table className="w-full text-xs">
|
||||||
|
<thead className="bg-muted/50">
|
||||||
|
<tr>
|
||||||
|
<th className="p-2 text-left w-10">Status</th>
|
||||||
|
<th className="p-2 text-left">Check</th>
|
||||||
|
<th className="p-2 text-left">Esperado</th>
|
||||||
|
<th className="p-2 text-left">Obtido</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{result.checks.map((chk, i) => (
|
||||||
|
<tr key={i} className="border-t border-border">
|
||||||
|
<td className="p-2">{statusIcon(chk.status)}</td>
|
||||||
|
<td className="p-2 font-medium">{chk.name}</td>
|
||||||
|
<td className="p-2 text-muted-foreground">{chk.expected}</td>
|
||||||
|
<td className="p-2">{chk.actual}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{result.summary && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">{result.summary}</p>
|
||||||
|
)}
|
||||||
|
{result.llmNotes && (
|
||||||
|
<p className="text-xs italic text-muted-foreground mt-1">LLM: {result.llmNotes}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-1 mb-3">
|
||||||
|
<h4 className="text-sm font-semibold">Entradas</h4>
|
||||||
|
<div className="rounded-md border bg-muted/10 p-2 text-xs font-mono max-h-32 overflow-y-auto">
|
||||||
|
<pre className="whitespace-pre-wrap">{JSON.stringify(scenario.inputs, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
<Button variant="outline" size="sm" onClick={handleExportPDF} disabled={!result}>
|
||||||
|
<FileDown className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
Exportar PDF
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" onClick={handleCopyToModule}>
|
||||||
|
<Copy className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
Copiar para o módulo
|
||||||
|
</Button>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="ghost" size="sm">
|
||||||
|
<X className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
Fechar
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
import { useState, useCallback, useRef } from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { Play, FileDown, Loader2, AlertCircle } from 'lucide-react';
|
||||||
|
import type { AuditConfig, AuditScenario, ScenarioResult, AuditReport } from '@/lib/audit/types';
|
||||||
|
import { generateAllScenarios } from '@/lib/audit/scenarios';
|
||||||
|
import AuditDetailPopup from './AuditDetailPopup';
|
||||||
|
|
||||||
|
const PROVIDERS: { value: string; label: string }[] = [
|
||||||
|
{ value: 'openai', label: 'OpenAI' },
|
||||||
|
{ value: 'anthropic', label: 'Anthropic' },
|
||||||
|
{ value: 'minimax', label: 'MiniMax' },
|
||||||
|
{ value: 'openrouter', label: 'OpenRouter' },
|
||||||
|
{ value: 'ollama', label: 'Ollama' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEFAULT_MODELS: Record<string, string> = {
|
||||||
|
openai: 'gpt-4o',
|
||||||
|
anthropic: 'claude-3-5-sonnet-latest',
|
||||||
|
minimax: 'minimax-m3.0',
|
||||||
|
openrouter: 'openai/gpt-4o',
|
||||||
|
ollama: 'deepseek-r1',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AuditPanel() {
|
||||||
|
|
||||||
|
const [config, setConfig] = useState<AuditConfig>({
|
||||||
|
provider: 'openai',
|
||||||
|
model: 'gpt-4o',
|
||||||
|
apiKey: '',
|
||||||
|
baseUrl: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [status, setStatus] = useState<'idle' | 'generating' | 'running' | 'done' | 'error'>('idle');
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [report, setReport] = useState<AuditReport | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const abortRef = useRef(false);
|
||||||
|
|
||||||
|
const [popupOpen, setPopupOpen] = useState(false);
|
||||||
|
const [selectedScenario, setSelectedScenario] = useState<AuditScenario | null>(null);
|
||||||
|
const [selectedResult, setSelectedResult] = useState<ScenarioResult | null>(null);
|
||||||
|
|
||||||
|
const handleProviderChange = (val: string) => {
|
||||||
|
setConfig((c) => ({
|
||||||
|
...c,
|
||||||
|
provider: val as AuditConfig['provider'],
|
||||||
|
model: DEFAULT_MODELS[val] ?? c.model,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRun = useCallback(async () => {
|
||||||
|
if (!config.apiKey && config.provider !== 'ollama') {
|
||||||
|
setError('Informe uma API key para continuar.');
|
||||||
|
setStatus('error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
abortRef.current = false;
|
||||||
|
setError(null);
|
||||||
|
setReport(null);
|
||||||
|
setProgress(0);
|
||||||
|
|
||||||
|
setStatus('generating');
|
||||||
|
const scenarios = generateAllScenarios();
|
||||||
|
setTotal(scenarios.length);
|
||||||
|
|
||||||
|
setStatus('running');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { runAudit } = await import('@/lib/audit/runner');
|
||||||
|
const reportResult = await runAudit(
|
||||||
|
config,
|
||||||
|
scenarios,
|
||||||
|
(p) => {
|
||||||
|
setProgress(p);
|
||||||
|
if (abortRef.current) throw new Error('Abortado pelo usuário');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setReport(reportResult);
|
||||||
|
setStatus('done');
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.message === 'Abortado pelo usuário') {
|
||||||
|
setStatus('idle');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setError(e instanceof Error ? e.message : 'Erro desconhecido');
|
||||||
|
setStatus('error');
|
||||||
|
}
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
const handleExportPDF = () => {
|
||||||
|
if (!report) return;
|
||||||
|
import('@/lib/audit/export-audit-pdf').then((mod) => mod.exportAuditReportToPDF(report));
|
||||||
|
};
|
||||||
|
|
||||||
|
const grouped = report && report.modules
|
||||||
|
? report.modules.map((m: { module: string; moduleLabel: string; totalScenarios: number; passed: number; failed: number; warnings: number }) => ({
|
||||||
|
module: m.module,
|
||||||
|
label: m.moduleLabel,
|
||||||
|
total: m.totalScenarios,
|
||||||
|
passed: m.passed,
|
||||||
|
failed: m.failed,
|
||||||
|
warnings: m.warnings,
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Auditoria LLM</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Validação dos cálculos de vento usando LLMs sequenciais. 100+ cenários cobrindo todos os módulos.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-sm font-medium">Provedor</label>
|
||||||
|
<Select value={config.provider} onValueChange={handleProviderChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{PROVIDERS.map((p) => (
|
||||||
|
<SelectItem key={p.value} value={p.value}>{p.label}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-sm font-medium">Modelo</label>
|
||||||
|
<Input
|
||||||
|
value={config.model}
|
||||||
|
onChange={(e) => setConfig((c) => ({ ...c, model: e.target.value }))}
|
||||||
|
placeholder="gpt-4o, claude-3-5-sonnet-..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-sm font-medium">API Key</label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={config.apiKey}
|
||||||
|
onChange={(e) => setConfig((c) => ({ ...c, apiKey: e.target.value }))}
|
||||||
|
placeholder="sk-..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-sm font-medium">Base URL (opcional)</label>
|
||||||
|
<Input
|
||||||
|
value={config.baseUrl ?? ''}
|
||||||
|
onChange={(e) => setConfig((c) => ({ ...c, baseUrl: e.target.value || undefined }))}
|
||||||
|
placeholder="https://api.openai.com/v1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<Button
|
||||||
|
onClick={handleRun}
|
||||||
|
disabled={status === 'running' || status === 'generating'}
|
||||||
|
>
|
||||||
|
{status === 'running' || status === 'generating' ? (
|
||||||
|
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Play className="w-4 h-4 mr-1.5" />
|
||||||
|
)}
|
||||||
|
Executar Auditoria
|
||||||
|
</Button>
|
||||||
|
{(status === 'running' || status === 'generating') && (
|
||||||
|
<Button variant="outline" size="sm" onClick={() => { abortRef.current = true; }}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{status === 'done' && report && (
|
||||||
|
<Button variant="outline" size="sm" onClick={handleExportPDF}>
|
||||||
|
<FileDown className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
Exportar PDF
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(status === 'running' || status === 'generating') && total > 0 && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex justify-between text-xs text-muted-foreground">
|
||||||
|
<span>{status === 'generating' ? 'Gerando cenários...' : `Processando cenários...`}</span>
|
||||||
|
<span>{progress} / {total}</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-muted rounded-full h-2 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="bg-purple-600 h-full rounded-full transition-all duration-300"
|
||||||
|
style={{ width: `${(progress / total) * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === 'error' && error && (
|
||||||
|
<div className="rounded-md border border-red-200 bg-red-50 dark:bg-red-950 dark:border-red-800 p-3 text-sm text-red-700 dark:text-red-300 flex items-start gap-2">
|
||||||
|
<AlertCircle className="w-4 h-4 mt-0.5 shrink-0" />
|
||||||
|
<span>{error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{status === 'done' && report && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg flex items-center justify-between">
|
||||||
|
<span>Resultados</span>
|
||||||
|
<span className="text-sm font-normal text-muted-foreground">
|
||||||
|
{report.totalPassed}/{report.totalScenarios} PASS —
|
||||||
|
W:{report.totalWarnings} F:{report.totalFailed}
|
||||||
|
</span>
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Provedor: {report.provider} ({report.model}) — {new Date(report.timestamp).toLocaleString('pt-BR')}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="rounded-md border overflow-hidden">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead className="bg-muted/50">
|
||||||
|
<tr>
|
||||||
|
<th className="p-2 text-left">Módulo</th>
|
||||||
|
<th className="p-2 text-center">Total</th>
|
||||||
|
<th className="p-2 text-center">PASS</th>
|
||||||
|
<th className="p-2 text-center">WARN</th>
|
||||||
|
<th className="p-2 text-center">FAIL</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{grouped.map((g: { module: string; label: string; total: number; passed: number; failed: number; warnings: number }) => (
|
||||||
|
<tr key={g.module} className="border-t border-border cursor-pointer hover:bg-muted/30"
|
||||||
|
onClick={() => {
|
||||||
|
const mod = report.modules.find((m) => m.module === g.module);
|
||||||
|
if (!mod) return;
|
||||||
|
const modScenarios = generateAllScenarios().filter((s) => s.module === g.module);
|
||||||
|
const dialog = mod.scenarios[0];
|
||||||
|
if (modScenarios.length > 0 && dialog) {
|
||||||
|
setSelectedScenario(modScenarios[0]);
|
||||||
|
setSelectedResult(dialog);
|
||||||
|
setPopupOpen(true);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<td className="p-2 font-medium">{g.label}</td>
|
||||||
|
<td className="p-2 text-center">{g.total}</td>
|
||||||
|
<td className="p-2 text-center"><Badge className="bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-300">{g.passed}</Badge></td>
|
||||||
|
<td className="p-2 text-center"><Badge className="bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-300">{g.warnings}</Badge></td>
|
||||||
|
<td className="p-2 text-center"><Badge className="bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300">{g.failed}</Badge></td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
|
Clique em um módulo para ver detalhes dos cenários e verificações.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AuditDetailPopup
|
||||||
|
open={popupOpen}
|
||||||
|
onOpenChange={setPopupOpen}
|
||||||
|
scenario={selectedScenario}
|
||||||
|
result={selectedResult}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { HelpCircle, Info, Settings2 } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from '@/components/ui/sheet';
|
||||||
|
import { manuals } from '@/data/manuals';
|
||||||
|
import { WindFlowGrid2D } from './WindFlowGrid2D';
|
||||||
|
|
||||||
|
interface EducationalManualProps {
|
||||||
|
type: string;
|
||||||
|
params: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EducationalManual({ type, params }: EducationalManualProps) {
|
||||||
|
const manual = manuals[type];
|
||||||
|
|
||||||
|
if (!manual) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gera explicações interpretativas dinâmicas com base nas opções selecionadas no painel
|
||||||
|
const renderConfigurationExplanation = () => {
|
||||||
|
const explanations: React.ReactNode[] = [];
|
||||||
|
|
||||||
|
// --- Abóbadas (vault) ---
|
||||||
|
if (type === 'vault') {
|
||||||
|
const regime = params.regime || 'laminar-rough';
|
||||||
|
const cpi = Number(params.localCpi || 0);
|
||||||
|
|
||||||
|
// Regime de escoamento
|
||||||
|
if (regime === 'laminar-rough') {
|
||||||
|
explanations.push(
|
||||||
|
<div key="vault-regime" className="text-xs border-l-2 border-sky-400 pl-3 py-1 space-y-1">
|
||||||
|
<span className="font-bold text-foreground block">Escoamento: Baixa Turbulência (Tabelas 15-17)</span>
|
||||||
|
<p>Representa vento uniforme sobre terreno desobstruído. O escoamento flui de forma limpa sobre o arco, gerando os valores tradicionais de sucção no topo.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
explanations.push(
|
||||||
|
<div key="vault-regime" className="text-xs border-l-2 border-red-500 pl-3 py-1 space-y-1">
|
||||||
|
<span className="font-bold text-foreground block">Escoamento: Regime Turbulento (Tabelas 18-20)</span>
|
||||||
|
<p>O vento apresenta alta turbulência devido a obstáculos no terreno. As correntes de vento ondulantes (exibidas no simulador) induzem picos de pressões dinâmicas flutuantes.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permeabilidade (Cpi)
|
||||||
|
if (cpi === 0) {
|
||||||
|
explanations.push(
|
||||||
|
<div key="vault-cpi" className="text-xs border-l-2 border-slate-400 pl-3 py-1 space-y-1">
|
||||||
|
<span className="font-bold text-foreground block">Pressão Interna Neutralizada (Cpi = 0.0)</span>
|
||||||
|
<p>As aberturas são inexistentes ou estão distribuídas igualmente nas faces. A força resultante no arco depende exclusivamente da sucção externa.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (cpi > 0) {
|
||||||
|
explanations.push(
|
||||||
|
<div key="vault-cpi" className="text-xs border-l-2 border-orange-500 pl-3 py-1 space-y-1">
|
||||||
|
<span className="font-bold text-orange-500 block">Abóbada Inflada (Cpi = +{cpi})</span>
|
||||||
|
<p>Uma abertura dominante na face de barlavento permite a entrada do ar, "inflando" a abóbada de dentro para fora (indicado pelas setas laranjas). Essa pressão interna empurra a cobertura para cima, somando-se à sucção externa do vento e aumentando de forma crítica a força de arrancamento do telhado!</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
explanations.push(
|
||||||
|
<div key="vault-cpi" className="text-xs border-l-2 border-blue-500 pl-3 py-1 space-y-1">
|
||||||
|
<span className="font-bold text-blue-500 block">Abóbada Sugada / Vácuo (Cpi = {cpi})</span>
|
||||||
|
<p>Com aberturas dominantes nas faces protegidas (laterais ou sotavento), o ar interno é sugado para fora, criando um vácuo interno (setas azuis). Esse efeito de vácuo ajuda a compensar e aliviar a sucção externa no topo, mas puxa as paredes para dentro.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Cilindros (cylinder) ---
|
||||||
|
if (type === 'cylinder') {
|
||||||
|
const isSmooth = params.surface === 'smooth';
|
||||||
|
const cpi = Number(params.cpi || 0);
|
||||||
|
|
||||||
|
explanations.push(
|
||||||
|
<div key="cyl-surface" className="text-xs border-l-2 border-sky-400 pl-3 py-1 space-y-1">
|
||||||
|
<span className="font-bold text-foreground block">Superfície: {isSmooth ? 'Lisa' : 'Rugosa'}</span>
|
||||||
|
<p>{isSmooth
|
||||||
|
? 'Superfície de baixo atrito. No regime supercrítico, o descolamento da camada limite é atrasado significativamente, fazendo o coeficiente de arrasto Ca cair para valores muito baixos (~0.4).'
|
||||||
|
: 'Casca áspera. A rugosidade gera microturbulências na superfície do cilindro que impedem a esteira de estreitar excessivamente, mantendo o arrasto Ca em níveis elevados no regime crítico/supercrítico.'
|
||||||
|
}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cpi > 0) {
|
||||||
|
explanations.push(
|
||||||
|
<div key="cyl-cpi" className="text-xs border-l-2 border-orange-500 pl-3 py-1 space-y-1">
|
||||||
|
<span className="font-bold text-orange-500 block">Reservatório Inflado (Cpi positivo)</span>
|
||||||
|
<p>O ar interno pressiona as paredes do silo/chaminé de dentro para fora, somando-se às sucções externas nas laterais.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Muros e Placas (sign) ---
|
||||||
|
if (type === 'sign') {
|
||||||
|
const hasEndPlates = params.hasEndPlates === true;
|
||||||
|
const alpha = Number(params.alpha || 90);
|
||||||
|
|
||||||
|
explanations.push(
|
||||||
|
<div key="sign-alpha" className="text-xs border-l-2 border-sky-400 pl-3 py-1 space-y-1">
|
||||||
|
<span className="font-bold text-foreground block">Incidência do Vento: {alpha}º</span>
|
||||||
|
<p>{alpha === 90
|
||||||
|
? 'Vento Perpendicular: o arrasto atua uniformemente, concentrando a força resultante exatamente no centro da placa (sem torque de torção nas fundações).'
|
||||||
|
: 'Vento Oblíquo: o fluxo se concentra na borda esquerda de ataque. Isso gera uma excentricidade horizontal na força (e = 0.25 L), introduzindo um momento torsor crítico que deve ser suportado pelos pilares.'
|
||||||
|
}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
explanations.push(
|
||||||
|
<div key="sign-plates" className="text-xs border-l-2 border-red-500 pl-3 py-1 space-y-1">
|
||||||
|
<span className="font-bold text-foreground block">Abas Laterais (End Plates): {hasEndPlates ? 'Com abas' : 'Sem abas'}</span>
|
||||||
|
<p>{hasEndPlates
|
||||||
|
? 'As abas vermelhas mostradas no simulador impedem o ar de escapar pelas laterais do muro, gerando um escoamento bidimensional plano que eleva a força de arrasto Cf ao máximo.'
|
||||||
|
: 'O vento escoa tridimensionalmente pelas bordas laterais livres, criando um efeito de alívio aerodinâmico que reduz a força resultante nas pontas do muro.'
|
||||||
|
}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Galpões (warehouse) ---
|
||||||
|
if (type === 'warehouse') {
|
||||||
|
const cpi = Number(params.cpi || 0);
|
||||||
|
|
||||||
|
if (cpi > 0) {
|
||||||
|
explanations.push(
|
||||||
|
<div key="wh-cpi" className="text-xs border-l-2 border-orange-500 pl-3 py-1 space-y-1">
|
||||||
|
<span className="font-bold text-orange-500 block">Galpão Inflado (Cpi = +{cpi})</span>
|
||||||
|
<p>Ocorre quando há aberturas dominantes na parede de barlavento (frente). O ar "sopra" para dentro e expande o galpão de dentro para fora, somando-se à sucção externa do telhado e aumentando de forma gravíssima o risco de arrancamento da cobertura!</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (cpi < 0) {
|
||||||
|
explanations.push(
|
||||||
|
<div key="wh-cpi" className="text-xs border-l-2 border-blue-500 pl-3 py-1 space-y-1">
|
||||||
|
<span className="font-bold text-blue-500 block">Galpão Desinflado / Vácuo (Cpi = {cpi})</span>
|
||||||
|
<p>Ocorre com aberturas nas paredes laterais ou de trás (sotavento). O ar interno é sugado, puxando as chapas do telhado para dentro e ajudando a contrapor a sucção do vento externo.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (explanations.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3 bg-slate-900/10 p-4 border border-border/60 rounded-xl my-4">
|
||||||
|
<div className="flex items-center gap-2 text-primary border-b border-border/40 pb-1.5">
|
||||||
|
<Settings2 className="size-4" />
|
||||||
|
<h4 className="text-xs font-bold uppercase tracking-wider">Análise das Suas Configurações</h4>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{explanations}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="size-10 bg-background/80 backdrop-blur-xs border border-border shadow-md hover:bg-accent/80 transition-all rounded-full flex items-center justify-center"
|
||||||
|
title="Manual Didático da Norma NBR 6123"
|
||||||
|
>
|
||||||
|
<Info className="size-5 text-primary animate-pulse" />
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent className="overflow-y-auto w-full sm:max-w-xl p-6 flex flex-col gap-6 border-l border-border bg-card">
|
||||||
|
<SheetHeader className="border-b border-border pb-4 flex flex-col gap-1.5">
|
||||||
|
<div className="flex items-center gap-2 text-primary">
|
||||||
|
<HelpCircle className="size-5" />
|
||||||
|
<SheetTitle className="text-xl font-bold tracking-tight">{manual.title}</SheetTitle>
|
||||||
|
</div>
|
||||||
|
<SheetDescription className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
{manual.intro}
|
||||||
|
</SheetDescription>
|
||||||
|
</SheetHeader>
|
||||||
|
|
||||||
|
{/* Simulador Interativo 2D no topo do manual */}
|
||||||
|
<WindFlowGrid2D type={type} params={params} />
|
||||||
|
|
||||||
|
{/* Quadro Dinâmico explicativo dos seletores */}
|
||||||
|
{renderConfigurationExplanation()}
|
||||||
|
|
||||||
|
{/* Seções de Texto e Fórmulas */}
|
||||||
|
<div className="flex flex-col gap-6 text-foreground leading-relaxed">
|
||||||
|
{manual.sections.map((section, idx) => (
|
||||||
|
<div key={idx} className="flex flex-col gap-3 bg-muted/20 p-4 rounded-xl border border-border/40">
|
||||||
|
<h3 className="text-base font-bold text-primary flex items-center gap-2 border-b border-border/60 pb-1.5">
|
||||||
|
{section.title}
|
||||||
|
</h3>
|
||||||
|
<div className="text-sm prose prose-neutral dark:prose-invert max-w-none text-muted-foreground">
|
||||||
|
{section.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-auto border-t border-border pt-4 text-center">
|
||||||
|
<p className="text-[10px] text-muted-foreground font-mono">
|
||||||
|
BrainWind v1.0 • Referência: ABNT NBR 6123:2023
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -403,6 +403,7 @@ interface IsolatedRoofProps {
|
|||||||
type: 'shed' | 'gable';
|
type: 'shed' | 'gable';
|
||||||
theta: number;
|
theta: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
width: number;
|
||||||
depth: number;
|
depth: number;
|
||||||
cpeWindward: number;
|
cpeWindward: number;
|
||||||
cpeLeeward: number;
|
cpeLeeward: number;
|
||||||
@@ -410,7 +411,7 @@ interface IsolatedRoofProps {
|
|||||||
forceKN: number;
|
forceKN: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function IsolatedRoofDiagram({ type, theta, height, depth, cpeWindward, cpeLeeward, cpeTop, forceKN }: IsolatedRoofProps) {
|
function IsolatedRoofDiagram({ type, theta, height, width, depth, cpeWindward, cpeLeeward, cpeTop, forceKN }: IsolatedRoofProps) {
|
||||||
const vw = 400, vh = 300;
|
const vw = 400, vh = 300;
|
||||||
const s = Math.min((vw - 100) / depth, (vh - 80) / (height + depth * Math.tan((theta * Math.PI) / 180)));
|
const s = Math.min((vw - 100) / depth, (vh - 80) / (height + depth * Math.tan((theta * Math.PI) / 180)));
|
||||||
const cx = vw / 2, ground = vh - 40;
|
const cx = vw / 2, ground = vh - 40;
|
||||||
@@ -451,7 +452,7 @@ function IsolatedRoofDiagram({ type, theta, height, depth, cpeWindward, cpeLeewa
|
|||||||
<text x={cx + 12} y={ground - h - rise - 12 - forceLen(forceKN) / 2} fontSize={8} fill="#ef4444">{forceKN.toFixed(1)} kN</text>
|
<text x={cx + 12} y={ground - h - rise - 12 - forceLen(forceKN) / 2} fontSize={8} fill="#ef4444">{forceKN.toFixed(1)} kN</text>
|
||||||
</g>
|
</g>
|
||||||
)}
|
)}
|
||||||
<text x={cx} y={ground + 18} textAnchor="middle" fontSize={9} fill="#475569">θ = {theta}° | h = {height}m | prof. = {depth}m</text>
|
<text x={cx} y={ground + 18} textAnchor="middle" fontSize={9} fill="#475569">θ = {theta}° | h = {height}m | b = {width}m | l = {depth}m</text>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,15 +8,35 @@ interface SceneCanvasProps extends CanvasProps {
|
|||||||
fallback: ReactNode;
|
fallback: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CanvasInner({ fallback: _, ...canvasProps }: SceneCanvasProps) {
|
function CanvasInner({ fallback: _, shadows, ...canvasProps }: SceneCanvasProps) {
|
||||||
const registerCanvas = useCaptureStore((s) => s.registerCanvas);
|
const registerCanvas = useCaptureStore((s) => s.registerCanvas);
|
||||||
const unregisterCanvas = useCaptureStore((s) => s.unregisterCanvas);
|
const unregisterCanvas = useCaptureStore((s) => s.unregisterCanvas);
|
||||||
|
|
||||||
useEffect(() => () => unregisterCanvas(), [unregisterCanvas]);
|
useEffect(() => () => unregisterCanvas(), [unregisterCanvas]);
|
||||||
|
|
||||||
|
// Suprimir avisos de depreciação conhecidos do Three.js (r185) que o R3F ainda aciona
|
||||||
|
useEffect(() => {
|
||||||
|
const originalWarn = console.warn;
|
||||||
|
console.warn = (...args) => {
|
||||||
|
if (typeof args[0] === 'string' && (args[0].includes('THREE.Clock') || args[0].includes('THREE.WebGLShadowMap'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
originalWarn(...args);
|
||||||
|
};
|
||||||
|
return () => {
|
||||||
|
console.warn = originalWarn;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// O R3F v9 usa PCFSoftShadowMap por padrão, que está deprecado no Three r185.
|
||||||
|
// Mudar para PCFShadowMap (1) via propriedade shadows.
|
||||||
|
const shadowConfig = shadows === true ? { type: 1 as any } : shadows;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Canvas
|
<Canvas
|
||||||
|
frameloop="demand"
|
||||||
{...canvasProps}
|
{...canvasProps}
|
||||||
|
shadows={shadowConfig}
|
||||||
onCreated={(state) => {
|
onCreated={(state) => {
|
||||||
registerCanvas(state.gl.domElement);
|
registerCanvas(state.gl.domElement);
|
||||||
canvasProps.onCreated?.(state);
|
canvasProps.onCreated?.(state);
|
||||||
|
|||||||
@@ -32,20 +32,11 @@ function PressureArrow({
|
|||||||
}) {
|
}) {
|
||||||
const { q } = useWindStore();
|
const { q } = useWindStore();
|
||||||
const force = p * q; // kN/m2
|
const force = p * q; // kN/m2
|
||||||
if (Math.abs(force) < 0.05) return null;
|
|
||||||
|
|
||||||
const length = Math.max(0.6, Math.min(3.0, Math.abs(force) * 1.5));
|
|
||||||
const isPressure = p > 0;
|
|
||||||
const color = isPressure ? '#3b82f6' : '#ef4444';
|
|
||||||
|
|
||||||
const normVec = useMemo(() => new THREE.Vector3(...normal).normalize(), [normal]);
|
const normVec = useMemo(() => new THREE.Vector3(...normal).normalize(), [normal]);
|
||||||
const centerVec = useMemo(() => new THREE.Vector3(...center), [center]);
|
const centerVec = useMemo(() => new THREE.Vector3(...center), [center]);
|
||||||
|
const isPressure = p > 0;
|
||||||
const dir = isPressure ? normVec.clone().negate() : normVec.clone();
|
const dir = useMemo(() => isPressure ? normVec.clone().negate() : normVec.clone(), [isPressure, normVec]);
|
||||||
|
|
||||||
const start = isPressure ? centerVec.clone().sub(dir.clone().multiplyScalar(length)) : centerVec;
|
|
||||||
const end = isPressure ? centerVec : centerVec.clone().add(dir.clone().multiplyScalar(length));
|
|
||||||
const mid = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5);
|
|
||||||
|
|
||||||
const quat = useMemo(() => {
|
const quat = useMemo(() => {
|
||||||
const q = new THREE.Quaternion();
|
const q = new THREE.Quaternion();
|
||||||
@@ -53,6 +44,15 @@ function PressureArrow({
|
|||||||
return new THREE.Euler().setFromQuaternion(q);
|
return new THREE.Euler().setFromQuaternion(q);
|
||||||
}, [dir]);
|
}, [dir]);
|
||||||
|
|
||||||
|
if (Math.abs(force) < 0.05) return null;
|
||||||
|
|
||||||
|
const length = Math.max(0.6, Math.min(3.0, Math.abs(force) * 1.5));
|
||||||
|
const color = isPressure ? '#3b82f6' : '#ef4444';
|
||||||
|
|
||||||
|
const start = isPressure ? centerVec.clone().sub(dir.clone().multiplyScalar(length)) : centerVec;
|
||||||
|
const end = isPressure ? centerVec : centerVec.clone().add(dir.clone().multiplyScalar(length));
|
||||||
|
const mid = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5);
|
||||||
|
|
||||||
const headLen = Math.min(0.4, length * 0.4);
|
const headLen = Math.min(0.4, length * 0.4);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,782 @@
|
|||||||
|
interface WindFlowGrid2DProps {
|
||||||
|
type: string;
|
||||||
|
params?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) {
|
||||||
|
// Estilos de animação e keyframes para os SVGs
|
||||||
|
const styleBlock = (
|
||||||
|
<style>{`
|
||||||
|
@keyframes windFlow {
|
||||||
|
0% { stroke-dashoffset: 36; }
|
||||||
|
100% { stroke-dashoffset: 0; }
|
||||||
|
}
|
||||||
|
@keyframes vortexRotate {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
@keyframes vibration {
|
||||||
|
0%, 100% { transform: translateY(0px); }
|
||||||
|
50% { transform: translateY(-6px); }
|
||||||
|
}
|
||||||
|
.wind-line {
|
||||||
|
stroke: #3b82f6;
|
||||||
|
stroke-width: 1.8;
|
||||||
|
stroke-dasharray: 8 16;
|
||||||
|
animation: windFlow 1.8s linear infinite;
|
||||||
|
fill: none;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
.wind-line-fast {
|
||||||
|
stroke: #a855f7;
|
||||||
|
stroke-width: 2.2;
|
||||||
|
stroke-dasharray: 6 12;
|
||||||
|
animation: windFlow 0.9s linear infinite;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
.wind-line-slow {
|
||||||
|
stroke: #93c5fd;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
stroke-dasharray: 10 20;
|
||||||
|
animation: windFlow 3s linear infinite;
|
||||||
|
fill: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.vortex-line {
|
||||||
|
stroke: #ef4444;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
stroke-dasharray: 4 8;
|
||||||
|
animation: windFlow 2.5s linear infinite;
|
||||||
|
fill: none;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
.vortex-spin-cw {
|
||||||
|
transform-origin: center;
|
||||||
|
animation: vortexRotate 2.5s linear infinite;
|
||||||
|
}
|
||||||
|
.vortex-spin-ccw {
|
||||||
|
transform-origin: center;
|
||||||
|
animation: vortexRotate 2.5s linear infinite reverse;
|
||||||
|
}
|
||||||
|
.vibe-cyl {
|
||||||
|
animation: vibration 1.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.cpi-arrow-pos {
|
||||||
|
stroke: #f97316;
|
||||||
|
stroke-width: 2;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
.cpi-arrow-neg {
|
||||||
|
stroke: #3b82f6;
|
||||||
|
stroke-width: 2;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Define quais itens de legenda devem ser exibidos para cada tipo de módulo
|
||||||
|
const getVisibleLegendItems = () => {
|
||||||
|
const hasCpi = ['warehouse'].includes(type);
|
||||||
|
const hasVortex = ['isolated-roof', 'bar', 'tower', 'vault', 'cylinder', 'sign', 'dynamics'].includes(type);
|
||||||
|
const hasLift = ['bridge', 'isolated-roof'].includes(type);
|
||||||
|
|
||||||
|
return {
|
||||||
|
wind: true,
|
||||||
|
fast: true,
|
||||||
|
vortex: hasVortex,
|
||||||
|
vortexSpin: ['cylinder', 'dynamics', 'sign'].includes(type),
|
||||||
|
cpiPos: hasCpi,
|
||||||
|
cpiNeg: hasCpi,
|
||||||
|
lift: hasLift,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getViewLabel = () => {
|
||||||
|
switch (type) {
|
||||||
|
case 'bridge':
|
||||||
|
case 'isolated-roof':
|
||||||
|
case 'vault':
|
||||||
|
case 'dome':
|
||||||
|
case 'warehouse':
|
||||||
|
return 'Vista Lateral (Corte Transversal)';
|
||||||
|
case 'cylinder':
|
||||||
|
case 'sign':
|
||||||
|
case 'bar':
|
||||||
|
case 'dynamics':
|
||||||
|
case 'tower':
|
||||||
|
return 'Vista Superior (Planta) / Aérea';
|
||||||
|
default:
|
||||||
|
return 'Vista Esquemática';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Renderizador do grid didático de cenários
|
||||||
|
const renderScenarios = () => {
|
||||||
|
const legendDef = (
|
||||||
|
<defs>
|
||||||
|
<marker id="arrow-green" 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="#10b981" />
|
||||||
|
</marker>
|
||||||
|
<marker id="arrow-red" 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="#ef4444" />
|
||||||
|
</marker>
|
||||||
|
<marker id="cpi-pos" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="5" markerHeight="5" orient="auto">
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#f97316" />
|
||||||
|
</marker>
|
||||||
|
<marker id="cpi-neg" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="5" markerHeight="5" orient="auto">
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#3b82f6" />
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'bridge':
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
{/* Caso 1 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-sky-400">Caso A: Vento Frontal Horizontal (0°)</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
<path d="M -20,20 L 320,20" className="wind-line-slow" />
|
||||||
|
<path d="M -20,50 Q 110,40 150,40 Q 190,40 320,50" className="wind-line-fast" />
|
||||||
|
<rect x="110" y="65" width="80" height="20" rx="2" fill="#475569" stroke="#64748b" strokeWidth="1.5" />
|
||||||
|
<path d="M -20,100 Q 110,110 150,110 Q 190,110 320,100" className="wind-line-fast" />
|
||||||
|
<path d="M -20,130 L 320,130" className="wind-line-slow" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">Escoamento simétrico ao redor do tabuleiro. Arrasto máximo horizontal, sustentação nula.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Caso 2 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-emerald-400">Caso B: Vento Ascendente (+5°)</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
{legendDef}
|
||||||
|
<g transform="rotate(15, 150, 75)">
|
||||||
|
<path d="M -20,15 L 320,15" className="wind-line-slow" />
|
||||||
|
<path d="M -20,45 Q 150,30 320,45" className="wind-line-fast" />
|
||||||
|
<path d="M -20,105 Q 150,120 320,105" className="wind-line-fast" />
|
||||||
|
<path d="M -20,135 L 320,135" className="wind-line-slow" />
|
||||||
|
</g>
|
||||||
|
<rect x="110" y="65" width="80" height="20" rx="2" fill="#475569" stroke="#64748b" strokeWidth="1.5" />
|
||||||
|
{/* Força de sustentação positiva */}
|
||||||
|
<path d="M 150,55 L 150,20" stroke="#10b981" strokeWidth="2.5" fill="none" markerEnd="url(#arrow-green)" />
|
||||||
|
<text x="160" y="30" fill="#10b981" fontSize="14" fontWeight="bold" fontFamily="monospace">+Fz (Lift)</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">O vento atinge o tabuleiro de baixo para cima, defletindo o fluxo e gerando força de sustentação vertical positiva.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Caso 3 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-rose-400">Caso C: Vento Descendente (-5°)</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
{legendDef}
|
||||||
|
<g transform="rotate(-15, 150, 75)">
|
||||||
|
<path d="M -20,15 L 320,15" className="wind-line-slow" />
|
||||||
|
<path d="M -20,45 Q 150,30 320,45" className="wind-line-fast" />
|
||||||
|
<path d="M -20,105 Q 150,120 320,105" className="wind-line-fast" />
|
||||||
|
<path d="M -20,135 L 320,135" className="wind-line-slow" />
|
||||||
|
</g>
|
||||||
|
<rect x="110" y="65" width="80" height="20" rx="2" fill="#475569" stroke="#64748b" strokeWidth="1.5" />
|
||||||
|
{/* Força de sustentação negativa */}
|
||||||
|
<path d="M 150,95 L 150,130" stroke="#ef4444" strokeWidth="2.5" fill="none" markerEnd="url(#arrow-red)" />
|
||||||
|
<text x="160" y="125" fill="#ef4444" fontSize="14" fontWeight="bold" fontFamily="monospace">-Fz (Downforce)</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">Vento de cima para baixo empurra o tabuleiro para baixo, aumentando a carga vertical sobre as vigas principais.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'isolated-roof':
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Caso 1 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-sky-400">Caso A: Escoamento Livre por Baixo</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
{legendDef}
|
||||||
|
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
||||||
|
<line x1="120" y1="65" x2="120" y2="130" stroke="#64748b" strokeWidth="2" />
|
||||||
|
<line x1="180" y1="65" x2="180" y2="130" stroke="#64748b" strokeWidth="2" />
|
||||||
|
{/* Telhado inclinado */}
|
||||||
|
<line x1="90" y1="55" x2="210" y2="65" stroke="#f43f5e" strokeWidth="4" strokeLinecap="round" />
|
||||||
|
|
||||||
|
{/* Linhas de vento desviando suavemente */}
|
||||||
|
<path d="M -20,20 Q 150,15 320,25" className="wind-line-slow" />
|
||||||
|
<path d="M -20,45 C 100,45 120,35 150,35 C 180,35 200,50 320,55" className="wind-line-fast" />
|
||||||
|
<path d="M -20,80 Q 150,85 320,80" className="wind-line" />
|
||||||
|
<path d="M -20,110 L 320,110" className="wind-line-slow" />
|
||||||
|
|
||||||
|
{/* Força de sustentação (Lift) leve */}
|
||||||
|
<path d="M 150,50 L 150,25" stroke="#10b981" strokeWidth="2" fill="none" markerEnd="url(#arrow-green)" />
|
||||||
|
<text x="155" y="35" fill="#10b981" fontSize="12" fontWeight="bold" fontFamily="monospace">+Fz</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">Como a estrutura não possui paredes, o ar flui sob a cobertura, gerando sucções menores no intradorso.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Caso 2 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-rose-400">Caso B: Efeito de Sucção no Bordo de Ataque</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
{legendDef}
|
||||||
|
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
||||||
|
<line x1="120" y1="65" x2="120" y2="130" stroke="#64748b" strokeWidth="2" />
|
||||||
|
<line x1="180" y1="65" x2="180" y2="130" stroke="#64748b" strokeWidth="2" />
|
||||||
|
<line x1="90" y1="50" x2="210" y2="70" stroke="#f43f5e" strokeWidth="4" strokeLinecap="round" />
|
||||||
|
|
||||||
|
{/* Vento inclinado batendo na borda de ataque superior */}
|
||||||
|
<g transform="rotate(10, 150, 60)">
|
||||||
|
<path d="M -20,20 Q 120,5 320,25" className="wind-line-fast" />
|
||||||
|
<path d="M -20,50 Q 100,20 150,20 C 190,20 220,55 320,60" className="vortex-line" />
|
||||||
|
<path d="M -20,95 Q 150,95 320,95" className="wind-line" />
|
||||||
|
</g>
|
||||||
|
{/* Pequena zona vermelha de turbulência no início do telhado */}
|
||||||
|
<path d="M 95,35 Q 125,15 155,30" fill="none" stroke="#ef4444" strokeWidth="1.5" strokeDasharray="3 6" />
|
||||||
|
|
||||||
|
{/* Força de arrancamento forte na borda */}
|
||||||
|
<path d="M 110,40 L 110,10" stroke="#10b981" strokeWidth="2.5" fill="none" markerEnd="url(#arrow-green)" />
|
||||||
|
<text x="115" y="25" fill="#10b981" fontSize="12" fontWeight="bold" fontFamily="monospace">+Fz (Arrancamento)</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">Vento inclinado gera descolamento brusco na borda esquerda elevada. Isso gera um pico severo de sucção (arrancamento).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'bar':
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Caso 1 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-sky-400">Caso A: Fluxo Frontal (0°)</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
<path d="M -20,20 L 320,20" className="wind-line-slow" />
|
||||||
|
<path d="M -20,45 C 100,40 120,25 150,25 C 180,25 200,40 320,45" className="wind-line" />
|
||||||
|
<path d="M -20,105 C 100,110 120,125 150,125 C 180,125 200,110 320,105" className="wind-line" />
|
||||||
|
<path d="M -20,130 L 320,130" className="wind-line-slow" />
|
||||||
|
<path d="M 160,50 Q 230,45 320,65" className="vortex-line" />
|
||||||
|
<path d="M 160,100 Q 230,105 320,85" className="vortex-line" />
|
||||||
|
|
||||||
|
{/* Perfil L frontal */}
|
||||||
|
<g transform="translate(135, 55)">
|
||||||
|
<path d="M 0,0 L 10,0 L 10,25 L 30,25 L 30,35 L 0,35 Z" fill="#475569" stroke="#64748b" strokeWidth="1.5" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">Vento atinge perpendicularmente a face. O ar acumula no interior da cantoneira, gerando altíssimo arrasto ($C_a \approx 2.0$).</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Caso 2 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-rose-400">Caso B: Escoamento Oblíquo (45°)</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
<path d="M -20,20 L 320,20" className="wind-line-slow" />
|
||||||
|
<path d="M -20,45 C 100,35 120,20 160,20 C 200,20 220,50 320,60" className="wind-line-fast" />
|
||||||
|
<path d="M -20,105 C 100,105 130,110 160,110 C 190,110 210,125 320,120" className="wind-line" />
|
||||||
|
<path d="M -20,130 L 320,130" className="wind-line-slow" />
|
||||||
|
<path d="M 175,70 Q 240,65 320,90" className="vortex-line" />
|
||||||
|
|
||||||
|
{/* Perfil L rotacionado */}
|
||||||
|
<g transform="translate(150, 75) rotate(45)">
|
||||||
|
<path d="M -15,-17 L -5,-17 L -5,8 L 15,8 L 15,18 L -15,18 Z" fill="#475569" stroke="#64748b" strokeWidth="1.5" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">O vento flui inclinado. O ar contorna a quina da cantoneira, aliviando o empuxo direto e gerando turbulência assimétrica.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'tower':
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Caso 1 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-sky-400">Caso A: Vento Frontal a 0°</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
<path d="M -20,20 L 320,20" className="wind-line-slow" />
|
||||||
|
<path d="M -20,50 Q 110,40 140,40 Q 170,40 320,50" className="wind-line" />
|
||||||
|
<path d="M -20,100 Q 110,110 140,110 Q 170,110 320,100" className="wind-line" />
|
||||||
|
<path d="M -20,130 L 320,130" className="wind-line-slow" />
|
||||||
|
{/* Pequenas linhas de turbulência passando por dentro da treliça */}
|
||||||
|
<path d="M 170,75 L 320,75" className="vortex-line" />
|
||||||
|
|
||||||
|
{/* Seção quadrada da torre */}
|
||||||
|
<g transform="translate(135, 60)">
|
||||||
|
<rect x="0" y="0" width="30" height="30" fill="none" stroke="#64748b" strokeWidth="2.5" />
|
||||||
|
<line x1="0" y1="0" x2="30" y2="30" stroke="#64748b" strokeWidth="1" />
|
||||||
|
<line x1="30" y1="0" x2="0" y2="30" stroke="#64748b" strokeWidth="1" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">Vento passa através dos vazios das barras. Cada cantoneira gera sua própria microesteira de turbulência.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Caso 2 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-rose-400">Caso B: Vento Diagonal a 45°</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
<path d="M -20,15 L 320,15" className="wind-line-slow" />
|
||||||
|
<path d="M -20,45 C 100,35 130,20 150,20 C 180,20 200,45 320,55" className="wind-line" />
|
||||||
|
<path d="M -20,105 C 100,115 130,130 150,130 C 180,130 200,105 320,95" className="wind-line" />
|
||||||
|
<path d="M -20,135 L 320,135" className="wind-line-slow" />
|
||||||
|
<path d="M 170,75 L 320,75" className="vortex-line" />
|
||||||
|
|
||||||
|
{/* Seção quadrada rotacionada */}
|
||||||
|
<g transform="translate(150, 75) rotate(45)">
|
||||||
|
<rect x="-15" y="-15" width="30" height="30" fill="none" stroke="#64748b" strokeWidth="2.5" />
|
||||||
|
<line x1="-15" y1="-15" x2="15" y2="15" stroke="#64748b" strokeWidth="1" />
|
||||||
|
<line x1="15" y1="-15" x2="-15" y2="15" stroke="#64748b" strokeWidth="1" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">Vento incide na diagonal da torre. O fator de incidência aumenta o esforço nas fundações de canto.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'vault':
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Caso 1 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-sky-400">Caso A: Regime de Baixa Turbulência</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
||||||
|
|
||||||
|
{/* Linhas de vento contornando perfeitamente */}
|
||||||
|
<path d="M -20,20 L 320,20" className="wind-line-slow" />
|
||||||
|
<path d="M -20,60 C 100,50 110,25 150,25 C 190,25 200,60 320,60" className="wind-line-fast" />
|
||||||
|
<path d="M -20,100 C 90,90 100,65 150,65 C 200,65 210,100 320,100" className="wind-line" />
|
||||||
|
|
||||||
|
{/* Abóbada */}
|
||||||
|
<path d="M 100,130 A 50,50 0 0 1 200,130 Z" fill="#475569" stroke="#64748b" strokeWidth="2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">O escoamento contorna colado à casca, gerando sucção simétrica com valor máximo de Cpe no topo do arco.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Caso 2 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-rose-400">Caso B: Regime Turbulento (Série 51/52)</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
||||||
|
|
||||||
|
{/* Linhas de vento ondulando e descolando antes */}
|
||||||
|
<path d="M -20,20 L 320,20" className="wind-line-slow" />
|
||||||
|
<path d="M -20,55 C 100,45 110,30 135,30 Q 160,20 185,55 Q 215,90 320,80" className="wind-line-fast" />
|
||||||
|
<path d="M -20,95 C 90,85 100,70 135,70 Q 150,60 175,90 Q 200,110 320,105" className="wind-line" />
|
||||||
|
<path d="M 175,80 Q 230,70 320,115" className="vortex-line" />
|
||||||
|
|
||||||
|
{/* Abóbada */}
|
||||||
|
<path d="M 100,130 A 50,50 0 0 1 200,130 Z" fill="#475569" stroke="#64748b" strokeWidth="2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">A turbulência do ar antecipa o descolamento da camada limite, gerando redemoinhos e pressões flutuantes a sotavento.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'dome':
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Caso 1 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-sky-400">Caso A: Cúpula Apoiada no Solo</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
||||||
|
|
||||||
|
{/* Vento contornando diretamente */}
|
||||||
|
<path d="M -20,20 L 320,20" className="wind-line-slow" />
|
||||||
|
<path d="M -20,60 C 100,50 110,25 150,25 C 190,25 200,60 320,60" className="wind-line-fast" />
|
||||||
|
<path d="M -20,100 C 90,90 100,65 150,65 C 200,65 210,100 320,100" className="wind-line" />
|
||||||
|
|
||||||
|
{/* Cúpula hemisférica */}
|
||||||
|
<path d="M 100,130 A 50,50 0 0 1 200,130 Z" fill="#475569" stroke="#64748b" strokeWidth="2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">Vento escoa diretamente da superfície do terreno, gerando pressões positivas na face esquerda inferior e sucção no topo.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Caso 2 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-rose-400">Caso B: Elevada sobre Parede Cilíndrica</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
||||||
|
|
||||||
|
{/* Vento desviando mais no barlavento devido à parede vertical */}
|
||||||
|
<path d="M -20,15 L 320,15" className="wind-line-slow" />
|
||||||
|
<path d="M -20,50 C 90,40 100,15 150,15 C 200,15 210,50 320,55" className="wind-line-fast" />
|
||||||
|
<path d="M -20,95 C 80,85 100,50 120,50 L 180,50 C 200,50 220,85 320,90" className="wind-line" />
|
||||||
|
|
||||||
|
{/* Parede + Cúpula */}
|
||||||
|
<rect x="110" y="90" width="80" height="40" fill="#2d3748" stroke="#475569" strokeWidth="1.5" />
|
||||||
|
<path d="M 110,90 A 40,40 0 0 1 190,90 Z" fill="#475569" stroke="#64748b" strokeWidth="2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">A parede de base deflete o fluxo bruscamente, gerando zonas de forte redemoinho sob a quina e alterando o Cpe do domo.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'warehouse':
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||||
|
{/* Caso 1 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-sky-400">Caso A: Vento Perpendicular (90°)</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
||||||
|
<path d="M -20,20 L 320,20" className="wind-line-slow" />
|
||||||
|
<path d="M -20,60 C 100,60 110,35 150,35 C 190,35 200,65 320,65" className="wind-line-fast" />
|
||||||
|
<path d="M -20,100 C 90,90 100,75 115,75 L 185,75 C 200,75 210,100 320,100" className="wind-line" />
|
||||||
|
|
||||||
|
{/* Galpão */}
|
||||||
|
<path d="M 110,130 L 110,85 L 150,55 L 190,85 L 190,130 Z" fill="#475569" stroke="#64748b" strokeWidth="2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">Vento incide a 90° (perpendicular à cumeeira). Pressão positiva na fachada de barlavento e sucção no telhado e sotavento.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Caso 2: Vento Paralelo */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-emerald-400">Caso B: Vento Paralelo (0°)</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
||||||
|
|
||||||
|
{/* Galpão visto de lado (retângulo) */}
|
||||||
|
<rect x="80" y="70" width="140" height="60" fill="#475569" stroke="#64748b" strokeWidth="2" />
|
||||||
|
|
||||||
|
{/* Linhas de vento (escoamento sobre o teto reto) */}
|
||||||
|
<path d="M -20,15 L 320,15" className="wind-line-slow" />
|
||||||
|
<path d="M -20,40 Q 80,35 150,40 T 320,45" className="wind-line-fast" />
|
||||||
|
<path d="M -20,85 L 80,85" className="wind-line" />
|
||||||
|
|
||||||
|
{/* Esteira turbulenta no telhado e atrás */}
|
||||||
|
<path d="M 80,70 Q 110,50 150,70" fill="none" stroke="#ef4444" strokeWidth="1.5" strokeDasharray="3 6" />
|
||||||
|
<path d="M 220,70 Q 250,90 280,110" fill="none" stroke="#ef4444" strokeWidth="1.5" strokeDasharray="3 6" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">Vento incide a 0° (paralelo à cumeeira). O fluxo contorna o oitão frontal, gerando forte sucção nas bordas longitudinais.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Caso 3 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-orange-400">Caso C: Pressão Interna (+Cpi)</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
{legendDef}
|
||||||
|
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
||||||
|
<path d="M 110,130 L 110,85 L 150,55 L 190,85 L 190,130 Z" fill="#475569" stroke="#64748b" strokeWidth="2" />
|
||||||
|
{/* Setas laranjas apontando para fora (inflado) */}
|
||||||
|
<path d="M 130,110 L 115,110" className="cpi-arrow-pos" markerEnd="url(#cpi-pos)" />
|
||||||
|
<path d="M 170,110 L 185,110" className="cpi-arrow-pos" markerEnd="url(#cpi-pos)" />
|
||||||
|
<path d="M 138,80 L 128,68" className="cpi-arrow-pos" markerEnd="url(#cpi-pos)" />
|
||||||
|
<path d="M 162,80 L 172,68" className="cpi-arrow-pos" markerEnd="url(#cpi-pos)" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">Com abertura na parede de ataque, a pressão interna "infla" a estrutura, empurrando paredes e teto para fora.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Caso 4 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-blue-400">Caso D: Sucção Interna (-Cpi)</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
{legendDef}
|
||||||
|
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
||||||
|
<path d="M 110,130 L 110,85 L 150,55 L 190,85 L 190,130 Z" fill="#475569" stroke="#64748b" strokeWidth="2" />
|
||||||
|
{/* Setas azuis apontando para dentro (sugado) */}
|
||||||
|
<path d="M 115,110 L 130,110" className="cpi-arrow-neg" markerEnd="url(#cpi-neg)" />
|
||||||
|
<path d="M 185,110 L 170,110" className="cpi-arrow-neg" markerEnd="url(#cpi-neg)" />
|
||||||
|
<path d="M 128,68 L 138,80" className="cpi-arrow-neg" markerEnd="url(#cpi-neg)" />
|
||||||
|
<path d="M 172,68 L 162,80" className="cpi-arrow-neg" markerEnd="url(#cpi-neg)" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">Com aberturas nas zonas de sucção externas, cria-se vácuo interno. O teto e paredes são puxados para dentro.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'cylinder':
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Caso 1 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-sky-400">Caso A: Escoamento Subcrítico (Superfície Lisa)</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
<path d="M -20,20 L 320,20" className="wind-line-slow" />
|
||||||
|
<path d="M -20,45 C 100,42 110,25 150,25 C 190,25 200,42 320,45" className="wind-line-fast" />
|
||||||
|
<path d="M -20,105 C 100,108 110,125 150,125 C 190,125 200,108 320,105" className="wind-line-fast" />
|
||||||
|
<path d="M -20,130 L 320,130" className="wind-line-slow" />
|
||||||
|
|
||||||
|
{/* Esteira de vórtices de Von Kármán larga */}
|
||||||
|
<g transform="translate(190, 75)">
|
||||||
|
<circle cx="20" cy="-20" r="8" fill="none" stroke="#ef4444" strokeWidth="1.2" strokeDasharray="2 4" className="vortex-spin-cw" />
|
||||||
|
<circle cx="50" cy="20" r="10" fill="none" stroke="#ef4444" strokeWidth="1.2" strokeDasharray="2 4" className="vortex-spin-ccw" />
|
||||||
|
<path d="M 0,0 Q 30,-25 60,0 Q 90,25 120,0" className="vortex-line" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<circle cx="150" cy="75" r="25" fill="#475569" stroke="#64748b" strokeWidth="1.5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">O fluxo descola precocemente (~80°), criando uma esteira de turbulência larga e resultando em arrasto elevado ($C_a \approx 1.2$).</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Caso 2 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-emerald-400">Caso B: Escoamento Supercrítico (Superfície Rugosa)</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
<path d="M -20,20 L 320,20" className="wind-line-slow" />
|
||||||
|
<path d="M -20,45 C 100,42 120,38 150,38 C 175,38 180,42 320,45" className="wind-line-fast" />
|
||||||
|
<path d="M -20,105 C 100,108 120,112 150,112 C 175,112 180,108 320,105" className="wind-line-fast" />
|
||||||
|
<path d="M -20,130 L 320,130" className="wind-line-slow" />
|
||||||
|
|
||||||
|
{/* Esteira mais estreita */}
|
||||||
|
<g transform="translate(195, 75)">
|
||||||
|
<circle cx="15" cy="-10" r="5" fill="none" stroke="#ef4444" strokeWidth="1" strokeDasharray="2 3" className="vortex-spin-cw" />
|
||||||
|
<circle cx="35" cy="10" r="6" fill="none" stroke="#ef4444" strokeWidth="1" strokeDasharray="2 3" className="vortex-spin-ccw" />
|
||||||
|
<path d="M 0,0 Q 20,-15 40,0 Q 60,15 80,0" className="vortex-line" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<circle cx="150" cy="75" r="25" fill="#475569" stroke="#e2e8f0" strokeWidth="3" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">A camada limite adere mais à casca (descolamento tardio ~120°). A esteira estreita gera menos resistência aerodinâmica ($C_a \approx 0.6$).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'sign':
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Caso 1 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-sky-400">Caso A: Muro sem Abas Laterais</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
||||||
|
<path d="M -20,20 L 320,20" className="wind-line-slow" />
|
||||||
|
<path d="M -20,55 C 100,50 120,30 150,30 C 180,30 200,60 320,65" className="wind-line" />
|
||||||
|
<path d="M -20,95 C 100,90 120,60 150,60 C 180,60 210,95 320,105" className="wind-line-fast" />
|
||||||
|
|
||||||
|
{/* Esteira turbulenta no sotavento */}
|
||||||
|
<circle cx="195" cy="100" r="10" fill="none" stroke="#ef4444" strokeWidth="1.2" strokeDasharray="2 4" className="vortex-spin-cw" />
|
||||||
|
<path d="M 160,110 Q 210,85 320,120" className="vortex-line" />
|
||||||
|
|
||||||
|
{/* Placa/Muro */}
|
||||||
|
<rect x="146" y="55" width="8" height="75" fill="#475569" stroke="#64748b" strokeWidth="1.5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">O vento contorna as bordas e escapa tridimensionalmente, resultando em um coeficiente de força aliviado.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Caso 2 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-rose-400">Caso B: Com Abas de Extremidade (End Plates)</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
<line x1="0" y1="130" x2="300" y2="130" stroke="#475569" strokeWidth="2" />
|
||||||
|
<path d="M -20,20 L 320,20" className="wind-line-slow" />
|
||||||
|
<path d="M -20,50 C 90,45 110,15 150,15 C 180,15 200,60 320,65" className="wind-line" />
|
||||||
|
<path d="M -20,95 C 100,90 120,40 150,40 C 180,40 220,105 320,115" className="wind-line-fast" />
|
||||||
|
|
||||||
|
{/* Esteira turbulenta muito maior */}
|
||||||
|
<circle cx="210" cy="100" r="18" fill="none" stroke="#ef4444" strokeWidth="1.2" strokeDasharray="2 4" className="vortex-spin-cw" />
|
||||||
|
<path d="M 160,110 Q 230,70 320,125" className="vortex-line" />
|
||||||
|
|
||||||
|
{/* Placa/Muro */}
|
||||||
|
<rect x="146" y="55" width="8" height="75" fill="#475569" stroke="#64748b" strokeWidth="1.5" />
|
||||||
|
{/* Abas vermelhas de extremidade */}
|
||||||
|
<line x1="140" y1="55" x2="160" y2="55" stroke="#ef4444" strokeWidth="3.5" />
|
||||||
|
<line x1="140" y1="130" x2="160" y2="130" stroke="#ef4444" strokeWidth="3.5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">As abas (barreiras vermelhas) bloqueiam a fuga lateral de ar. O fluxo é puramente 2D plano, forçando o empuxo ao valor limite superior ($C_f \approx 1.3 - 2.0$).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'dynamics':
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Caso 1 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-sky-400">Caso A: Abaixo da Velocidade Crítica (Sem Vibração)</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
<path d="M -20,20 L 320,20" className="wind-line-slow" />
|
||||||
|
<path d="M -20,50 Q 110,45 150,45 C 180,45 190,60 320,60" className="wind-line" />
|
||||||
|
<path d="M -20,100 Q 110,105 150,105 C 180,105 190,90 320,90" className="wind-line" />
|
||||||
|
<path d="M -20,130 L 320,130" className="wind-line-slow" />
|
||||||
|
|
||||||
|
{/* Pequenos vórtices desorganizados */}
|
||||||
|
<g transform="translate(185, 75)">
|
||||||
|
<circle cx="15" cy="-10" r="5" fill="none" stroke="#ef4444" strokeWidth="1" strokeDasharray="2 3" />
|
||||||
|
<circle cx="35" cy="10" r="5" fill="none" stroke="#ef4444" strokeWidth="1" strokeDasharray="2 3" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<circle cx="150" cy="75" r="22" fill="#475569" stroke="#64748b" strokeWidth="1.5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">Vórtices desprendem-se sem frequência uniforme. A estrutura permanece estável e as forças dinâmicas são negligenciáveis.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Caso 2 */}
|
||||||
|
<div className="flex flex-col gap-2 bg-white dark:bg-slate-950 border border-border/60 p-3 rounded-lg">
|
||||||
|
<span className="text-xs font-bold text-rose-400">Caso B: Velocidade Crítica (Ressonância / Lock-in)</span>
|
||||||
|
<div className="relative aspect-video bg-slate-100 dark:bg-slate-900/60 rounded-md overflow-hidden border border-border/30">
|
||||||
|
<svg viewBox="0 0 300 150" className="w-full h-full">
|
||||||
|
{styleBlock}
|
||||||
|
<path d="M -20,20 L 320,20" className="wind-line-slow" />
|
||||||
|
<path d="M -20,50 C 100,45 110,30 150,30 C 180,30 190,55 320,55" className="wind-line-fast" />
|
||||||
|
<path d="M -20,100 C 100,105 110,120 150,120 C 180,120 190,95 320,95" className="wind-line-fast" />
|
||||||
|
|
||||||
|
{/* Vórtices fortes e organizados de Von Kármán */}
|
||||||
|
<g transform="translate(185, 75)">
|
||||||
|
<circle cx="20" cy="-22" r="10" fill="none" stroke="#ef4444" strokeWidth="1.5" strokeDasharray="3 5" className="vortex-spin-cw" />
|
||||||
|
<circle cx="55" cy="22" r="11" fill="none" stroke="#ef4444" strokeWidth="1.5" strokeDasharray="3 5" className="vortex-spin-ccw" />
|
||||||
|
<path d="M 0,0 Q 35,-30 70,0 Q 105,30 140,0" className="vortex-line" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* Chaminé vibrando lateralmente */}
|
||||||
|
<g className="vibe-cyl">
|
||||||
|
<circle cx="150" cy="75" r="22" fill="#ef4444" stroke="#b91c1c" strokeWidth="2.5" />
|
||||||
|
<line x1="150" y1="75" x2="150" y2="25" stroke="#ef4444" strokeWidth="2" strokeDasharray="2 3" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-snug">Vórtices desprendem-se na frequência natural da estrutura, provocando oscilações transversais violentas (vibração de lock-in).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Caso padrão dinâmico geral (Módulos dinâmicos simples)
|
||||||
|
return (
|
||||||
|
<div className="bg-white dark:bg-slate-950 border border-border/60 p-4 rounded-lg flex items-center justify-center min-h-[160px]">
|
||||||
|
<svg viewBox="0 0 400 150" className="w-full max-w-[320px]">
|
||||||
|
{styleBlock}
|
||||||
|
<path d="M -20,30 L 420,30" className="wind-line-slow" />
|
||||||
|
<path d="M -20,75 L 420,75" className="wind-line" />
|
||||||
|
<path d="M -20,120 L 420,120" className="wind-line-slow" />
|
||||||
|
<circle cx="200" cy="75" r="20" fill="#475569" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const legendItems = getVisibleLegendItems();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 my-6 bg-slate-50 dark:bg-slate-900/40 p-4 border border-border/60 rounded-xl">
|
||||||
|
<div className="flex justify-between items-center border-b border-border/40 pb-2">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-foreground">Esquemas e Fluxos Comparativos NBR 6123</h3>
|
||||||
|
<span className="text-[10px] text-sky-600 dark:text-sky-400 font-medium font-mono">{getViewLabel()}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[9px] bg-sky-500/10 text-sky-600 dark:text-sky-400 font-bold border border-sky-500/20 px-2 py-0.5 rounded-full">SVG Animado</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Renderiza o grid comparativo */}
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{renderScenarios()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Caixa de Legenda Didática Inteligente (Filtra itens não utilizados no módulo ativo) */}
|
||||||
|
<div className="bg-white dark:bg-slate-950/60 p-3 rounded-lg border border-border/40 text-[11px] text-muted-foreground flex flex-col gap-2">
|
||||||
|
<span className="font-bold text-xs text-foreground uppercase tracking-wider">Legenda do Escoamento:</span>
|
||||||
|
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
|
||||||
|
{legendItems.wind && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="w-6 h-0.5 bg-blue-500 inline-block border-b border-dashed" />
|
||||||
|
<span>Linhas de Corrente de Vento</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{legendItems.fast && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="w-6 h-0.5 bg-purple-500 inline-block" />
|
||||||
|
<span>Fluxo Acelerado (Alta Vel. / Sucção)</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{legendItems.vortex && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="w-6 h-0.5 bg-red-500 inline-block border-b border-dotted" />
|
||||||
|
<span>Esteira Turbulenta / Recirculação</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{legendItems.vortexSpin && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="w-4 h-4 rounded-full border border-dashed border-red-500 inline-block animate-spin" style={{ animationDuration: '4s' }} />
|
||||||
|
<span>Vórtices de Desprendimento</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{legendItems.cpiPos && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="text-orange-500 text-xs font-bold font-sans">→</span>
|
||||||
|
<span>Seta Laranja: Pressão Interna (+Cpi)</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{legendItems.cpiNeg && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="text-blue-500 text-xs font-bold font-sans">←</span>
|
||||||
|
<span>Seta Azul Escura: Sucção Interna (-Cpi)</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{legendItems.lift && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="flex gap-1 items-center font-bold text-base">
|
||||||
|
<span className="text-emerald-500">↑</span>
|
||||||
|
<span className="text-red-500">↓</span>
|
||||||
|
</div>
|
||||||
|
<span>Forças Verticais (Sustentação/Downforce)</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -25,11 +25,6 @@ export interface Bar3DInput {
|
|||||||
cx: number;
|
cx: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Converte kN para um comprimento visual proporcional no eixo 3D.
|
|
||||||
*/
|
|
||||||
const forceToLength = (kN: number): number => Math.min(Math.max(Math.abs(kN) * 0.3, 0.3), 4);
|
|
||||||
|
|
||||||
function BarModel({
|
function BarModel({
|
||||||
barType,
|
barType,
|
||||||
section,
|
section,
|
||||||
@@ -37,94 +32,60 @@ function BarModel({
|
|||||||
width,
|
width,
|
||||||
length,
|
length,
|
||||||
alpha,
|
alpha,
|
||||||
fxKN,
|
|
||||||
fyKN,
|
|
||||||
cx,
|
cx,
|
||||||
}: Bar3DInput) {
|
}: Bar3DInput) {
|
||||||
const barRadius = barType === 'circular' ? (diameter ?? 0.05) / 2 : Math.min(width ?? 0.1, 0.08) / 2;
|
const barRadius = barType === 'circular' ? (diameter ?? 0.05) / 2 : Math.min(width ?? 0.1, 0.08) / 2;
|
||||||
const barThickness = barType === 'circular' ? barRadius : barRadius * 0.5;
|
const barThickness = barType === 'circular' ? barRadius : barRadius * 0.5;
|
||||||
|
|
||||||
// Cor baseada em Cx
|
|
||||||
const barColor = useMemo(() => {
|
const barColor = useMemo(() => {
|
||||||
const intensity = Math.min(1, Math.abs(cx) / 2.5);
|
const intensity = Math.min(1, Math.abs(cx) / 2.5);
|
||||||
const hue = 215 - intensity * 215;
|
const hue = 215 - intensity * 215;
|
||||||
return new THREE.Color(`hsl(${hue}, ${65 + intensity * 25}%, ${45 - intensity * 10}%)`);
|
return new THREE.Color(`hsl(${hue}, ${65 + intensity * 25}%, ${45 - intensity * 10}%)`);
|
||||||
}, [cx]);
|
}, [cx]);
|
||||||
|
|
||||||
// Rotação da barra em torno do eixo Y (alinhada com eixo X inicialmente)
|
// Ação do vento deve rotacionar a barra em seu próprio eixo longitudinal (roll).
|
||||||
// Direção do vento é +X; α é o ângulo da face da barra em relação ao vento
|
// O eixo longitudinal na nossa geometria é o X.
|
||||||
const alphaRad = (alpha * Math.PI) / 180;
|
const alphaRad = (alpha * Math.PI) / 180;
|
||||||
const barRotation = -alphaRad; // rotação em torno do eixo Y para alinhar a face
|
// A rotação deve ser negativa para que o vento (vindo de -Z) atinja as faces corretas conforme a NBR.
|
||||||
|
const barRotationX = -alphaRad;
|
||||||
// Direção do vetor de força resultante (na direção da força calculada)
|
|
||||||
const forceMag = Math.sqrt(fxKN * fxKN + fyKN * fyKN);
|
|
||||||
const forceAngle = Math.atan2(fyKN, fxKN);
|
|
||||||
const arrowLen = forceToLength(forceMag);
|
|
||||||
|
|
||||||
// Centro da barra (origem)
|
|
||||||
const center = new THREE.Vector3(0, 0, 0);
|
|
||||||
|
|
||||||
// Posição da ponta da seta
|
|
||||||
const arrowEnd = useMemo(
|
|
||||||
() => new THREE.Vector3(
|
|
||||||
Math.cos(forceAngle) * arrowLen,
|
|
||||||
Math.sin(forceAngle) * arrowLen,
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
[forceAngle, arrowLen],
|
|
||||||
);
|
|
||||||
const arrowMid = useMemo(
|
|
||||||
() => new THREE.Vector3(arrowEnd.x / 2, arrowEnd.y / 2, 0),
|
|
||||||
[arrowEnd],
|
|
||||||
);
|
|
||||||
const quat = useMemo(() => {
|
|
||||||
const dir = arrowEnd.clone().normalize();
|
|
||||||
const q = new THREE.Quaternion();
|
|
||||||
q.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
|
||||||
return new THREE.Euler().setFromQuaternion(q);
|
|
||||||
}, [arrowEnd]);
|
|
||||||
|
|
||||||
const headLen = 0.25;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group rotation={[0, barRotation, 0]}>
|
<group>
|
||||||
{/* Eixo principal da barra ao longo do eixo X */}
|
{/* Seta do Vento (Fixa Globalmente, soprando em +Z) */}
|
||||||
{barType === 'circular' ? (
|
<group position={[0, 0, barRadius + 0.5]}>
|
||||||
<mesh position={[0, 0, 0]} rotation={[0, 0, Math.PI / 2]} castShadow>
|
<mesh position={[0, 0, 0.4]} rotation={[Math.PI / 2, 0, 0]}>
|
||||||
<cylinderGeometry args={[barRadius, barRadius, length, 16]} />
|
<cylinderGeometry args={[0.02, 0.02, 0.8, 8]} />
|
||||||
<meshStandardMaterial color={barColor} roughness={0.4} metalness={0.3} />
|
<meshStandardMaterial color="#22c55e" />
|
||||||
</mesh>
|
</mesh>
|
||||||
) : (
|
<mesh position={[0, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}>
|
||||||
<SectionShape section={section ?? 'placa'} width={width ?? 0.1} length={length} color={barColor} thickness={barThickness} />
|
<coneGeometry args={[0.08, 0.2, 8]} />
|
||||||
)}
|
<meshStandardMaterial color="#22c55e" />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
|
||||||
{/* Eixos de referência */}
|
{/* Barra Rotacionada (Roll) */}
|
||||||
<axesHelper args={[length * 0.5]} />
|
<group rotation={[barRotationX, 0, 0]}>
|
||||||
|
{/* Eixo principal da barra ao longo do eixo X */}
|
||||||
{/* Vetor de força (resultante) */}
|
{barType === 'circular' ? (
|
||||||
{forceMag > 0.01 && (
|
<mesh position={[0, 0, 0]} rotation={[0, 0, Math.PI / 2]} castShadow>
|
||||||
<group>
|
<cylinderGeometry args={[barRadius, barRadius, length, 16]} />
|
||||||
{arrowLen - headLen > 0.01 && (
|
<meshStandardMaterial color={barColor} roughness={0.4} metalness={0.3} />
|
||||||
<mesh position={arrowMid.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
|
||||||
<cylinderGeometry args={[0.04, 0.04, arrowLen - headLen, 10]} />
|
|
||||||
<meshStandardMaterial color="#ef4444" />
|
|
||||||
</mesh>
|
|
||||||
)}
|
|
||||||
<mesh position={arrowEnd.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
|
||||||
<coneGeometry args={[0.1, headLen, 10]} />
|
|
||||||
<meshStandardMaterial color="#ef4444" />
|
|
||||||
</mesh>
|
</mesh>
|
||||||
</group>
|
) : (
|
||||||
)}
|
<SectionShape section={section ?? 'placa'} width={width ?? 0.1} length={length} color={barColor} thickness={barThickness} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Eixos de referência locais da barra */}
|
||||||
|
<axesHelper args={[length * 0.5]} />
|
||||||
|
</group>
|
||||||
|
|
||||||
{/* Marca de origem */}
|
{/* Marca de origem */}
|
||||||
<mesh position={[0, 0, 0]} castShadow>
|
<mesh position={[0, 0, 0]} castShadow>
|
||||||
<sphereGeometry args={[0.06, 12, 12]} />
|
<sphereGeometry args={[0.06, 12, 12]} />
|
||||||
<meshStandardMaterial color="#fbbf24" emissive="#fbbf24" emissiveIntensity={0.4} />
|
<meshStandardMaterial color="#fbbf24" emissive="#fbbf24" emissiveIntensity={0.4} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<axesHelper args={[length * 0.3]} />
|
|
||||||
<Text
|
<Text
|
||||||
position={[0, -barRadius * 2 - 0.3, 0]}
|
position={[0, -barRadius * 2 - 0.4, 0]}
|
||||||
fontSize={0.3}
|
fontSize={0.3}
|
||||||
color="#1e40af"
|
color="#1e40af"
|
||||||
anchorX="center"
|
anchorX="center"
|
||||||
@@ -132,7 +93,6 @@ function BarModel({
|
|||||||
>
|
>
|
||||||
α={alpha}° | Cx={cx.toFixed(2)}
|
α={alpha}° | Cx={cx.toFixed(2)}
|
||||||
</Text>
|
</Text>
|
||||||
{center && null}
|
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import SceneCanvas from '../SceneCanvas';
|
import SceneCanvas from '../SceneCanvas';
|
||||||
import FallbackDiagram from '../FallbackDiagram';
|
import FallbackDiagram from '../FallbackDiagram';
|
||||||
|
import { WindArrow } from './WindArrow';
|
||||||
|
|
||||||
export interface Bridge3DInput {
|
export interface Bridge3DInput {
|
||||||
/** Maior vão Lₚ (m) */
|
/** Maior vão Lₚ (m) */
|
||||||
@@ -21,6 +22,8 @@ export interface Bridge3DInput {
|
|||||||
fxPerLength: number;
|
fxPerLength: number;
|
||||||
/** Força de sustentação por unidade de comprimento Fz (kN/m) */
|
/** Força de sustentação por unidade de comprimento Fz (kN/m) */
|
||||||
fzPerLength: number;
|
fzPerLength: number;
|
||||||
|
/** Ângulo de ataque do vento (graus) */
|
||||||
|
alpha: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BridgeModel({
|
function BridgeModel({
|
||||||
@@ -29,8 +32,7 @@ function BridgeModel({
|
|||||||
deckHeight,
|
deckHeight,
|
||||||
heg,
|
heg,
|
||||||
cx,
|
cx,
|
||||||
fxPerLength,
|
alpha,
|
||||||
fzPerLength,
|
|
||||||
}: Bridge3DInput) {
|
}: Bridge3DInput) {
|
||||||
const halfL = lp / 2;
|
const halfL = lp / 2;
|
||||||
const halfW = width / 2;
|
const halfW = width / 2;
|
||||||
@@ -47,13 +49,6 @@ function BridgeModel({
|
|||||||
// Pilar heights: posicionar 3 pilares ao longo do vão
|
// Pilar heights: posicionar 3 pilares ao longo do vão
|
||||||
const pillarHeights = useMemo(() => [deckY - 0.5, deckY - 0.5, deckY - 0.5], [deckY]);
|
const pillarHeights = useMemo(() => [deckY - 0.5, deckY - 0.5, deckY - 0.5], [deckY]);
|
||||||
|
|
||||||
// Vetor de força (Fx horizontal)
|
|
||||||
const fxLen = Math.min(Math.max(Math.abs(fxPerLength) * 0.5, 0.3), 4);
|
|
||||||
const fxDir = fxPerLength >= 0 ? 1 : -1;
|
|
||||||
// Vetor de força (Fz vertical)
|
|
||||||
const fzLen = Math.min(Math.max(Math.abs(fzPerLength) * 0.5, 0.3), 4);
|
|
||||||
const fzDir = fzPerLength >= 0 ? 1 : -1;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
{/* Tabuleiro (deck) */}
|
{/* Tabuleiro (deck) */}
|
||||||
@@ -89,32 +84,15 @@ function BridgeModel({
|
|||||||
<meshStandardMaterial color="#60a5fa" opacity={0.4} transparent roughness={0.3} />
|
<meshStandardMaterial color="#60a5fa" opacity={0.4} transparent roughness={0.3} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Vetor Cx (horizontal) */}
|
{/* Seta de Vento (bate no meio do tabuleiro) */}
|
||||||
<ForceArrow
|
<WindArrow
|
||||||
start={[-halfL * 0.6, deckY + deckThickness + 0.3, halfW + 0.5]}
|
target={[0, deckY + deckThickness / 2, 0]}
|
||||||
direction={[fxDir, 0, 0]}
|
direction={[0, Math.sin((alpha * Math.PI) / 180), Math.cos((alpha * Math.PI) / 180)]}
|
||||||
length={fxLen}
|
scale={4}
|
||||||
color="#ef4444"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Vetor Cz (vertical) */}
|
|
||||||
<ForceArrow
|
|
||||||
start={[halfL * 0.6, deckY + deckThickness + 0.3, halfW + 0.5]}
|
|
||||||
direction={[0, fzDir, 0]}
|
|
||||||
length={fzLen}
|
|
||||||
color="#3b82f6"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Vetor no centro também para destacar */}
|
|
||||||
<ForceArrow
|
|
||||||
start={[0, deckY + deckThickness + 0.3, 0]}
|
|
||||||
direction={[fxDir, 0, 0]}
|
|
||||||
length={fxLen * 0.7}
|
|
||||||
color="#ef4444"
|
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
position={[0, deckY + deckThickness + 1.0, 0]}
|
position={[0, deckY + deckThickness + 1.5, 0]}
|
||||||
fontSize={0.6}
|
fontSize={1.2}
|
||||||
color="#1e40af"
|
color="#1e40af"
|
||||||
anchorX="center"
|
anchorX="center"
|
||||||
anchorY="bottom"
|
anchorY="bottom"
|
||||||
@@ -125,54 +103,7 @@ function BridgeModel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ForceArrow({
|
|
||||||
start,
|
|
||||||
direction,
|
|
||||||
length,
|
|
||||||
color,
|
|
||||||
}: {
|
|
||||||
start: [number, number, number];
|
|
||||||
direction: [number, number, number];
|
|
||||||
length: number;
|
|
||||||
color: string;
|
|
||||||
}) {
|
|
||||||
const startVec = useMemo(() => new THREE.Vector3(...start), [start]);
|
|
||||||
const dirVec = useMemo(() => new THREE.Vector3(...direction), [direction]);
|
|
||||||
const end = useMemo(
|
|
||||||
() => new THREE.Vector3(
|
|
||||||
startVec.x + dirVec.x * length,
|
|
||||||
startVec.y + dirVec.y * length,
|
|
||||||
startVec.z + dirVec.z * length,
|
|
||||||
),
|
|
||||||
[startVec, dirVec, length],
|
|
||||||
);
|
|
||||||
const mid = useMemo(
|
|
||||||
() => new THREE.Vector3().addVectors(startVec, end).multiplyScalar(0.5),
|
|
||||||
[startVec, end],
|
|
||||||
);
|
|
||||||
const quat = useMemo(() => {
|
|
||||||
const dir = new THREE.Vector3().subVectors(end, startVec).normalize();
|
|
||||||
const q = new THREE.Quaternion();
|
|
||||||
q.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
|
||||||
return new THREE.Euler().setFromQuaternion(q);
|
|
||||||
}, [startVec, end]);
|
|
||||||
const headLen = 0.3;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group>
|
|
||||||
{length - headLen > 0.01 && (
|
|
||||||
<mesh position={mid.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
|
||||||
<cylinderGeometry args={[0.06, 0.06, length - headLen, 10]} />
|
|
||||||
<meshStandardMaterial color={color} />
|
|
||||||
</mesh>
|
|
||||||
)}
|
|
||||||
<mesh position={end.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
|
||||||
<coneGeometry args={[0.15, headLen, 10]} />
|
|
||||||
<meshStandardMaterial color={color} />
|
|
||||||
</mesh>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Bridge3DViewer(input: Bridge3DInput) {
|
export default function Bridge3DViewer(input: Bridge3DInput) {
|
||||||
const { lp, deckHeight, width } = input;
|
const { lp, deckHeight, width } = input;
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ export default function Dynamics3DViewer(props: Dynamics3DInput) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SceneCanvas
|
<SceneCanvas
|
||||||
|
frameloop="always"
|
||||||
shadows
|
shadows
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [cameraDistance, cameraDistance * 0.5, cameraDistance], fov: 45 }}
|
camera={{ position: [cameraDistance, cameraDistance * 0.5, cameraDistance], fov: 45 }}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import SceneCanvas from '../SceneCanvas';
|
import SceneCanvas from '../SceneCanvas';
|
||||||
import FallbackDiagram from '../FallbackDiagram';
|
import FallbackDiagram from '../FallbackDiagram';
|
||||||
|
import { WindArrow } from './WindArrow';
|
||||||
|
|
||||||
export interface IsolatedRoof3DInput {
|
export interface IsolatedRoof3DInput {
|
||||||
/** Tipo de cobertura: 'shed' (uma água) ou 'gable' (duas águas) */
|
/** Tipo de cobertura: 'shed' (uma água) ou 'gable' (duas águas) */
|
||||||
@@ -11,7 +12,9 @@ export interface IsolatedRoof3DInput {
|
|||||||
theta: number;
|
theta: number;
|
||||||
/** Altura livre dos suportes (m) */
|
/** Altura livre dos suportes (m) */
|
||||||
height: number;
|
height: number;
|
||||||
/** Profundidade da cobertura (m) — dimensão perpendicular à seção */
|
/** Largura da cobertura (m) — dimensão transversal (b) */
|
||||||
|
width: number;
|
||||||
|
/** Profundidade da cobertura (m) — dimensão longitudinal (l) */
|
||||||
depth: number;
|
depth: number;
|
||||||
/** Cpe barlavento (sobre a face exposta ao vento) */
|
/** Cpe barlavento (sobre a face exposta ao vento) */
|
||||||
cpeWindward: number;
|
cpeWindward: number;
|
||||||
@@ -23,8 +26,6 @@ export interface IsolatedRoof3DInput {
|
|||||||
forceKN: number;
|
forceKN: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const forceToLength = (kN: number): number => Math.min(Math.max(Math.abs(kN) * 0.15, 0.5), 6);
|
|
||||||
|
|
||||||
function pressureColor(cpe: number): THREE.Color {
|
function pressureColor(cpe: number): THREE.Color {
|
||||||
const clamped = Math.max(-2.5, Math.min(1.5, cpe));
|
const clamped = Math.max(-2.5, Math.min(1.5, cpe));
|
||||||
const t = (clamped + 2.5) / 4.0;
|
const t = (clamped + 2.5) / 4.0;
|
||||||
@@ -36,6 +37,7 @@ function IsolatedRoofModel({
|
|||||||
type,
|
type,
|
||||||
theta,
|
theta,
|
||||||
height,
|
height,
|
||||||
|
width,
|
||||||
depth,
|
depth,
|
||||||
cpeWindward,
|
cpeWindward,
|
||||||
cpeLeeward,
|
cpeLeeward,
|
||||||
@@ -43,12 +45,11 @@ function IsolatedRoofModel({
|
|||||||
}: IsolatedRoof3DInput) {
|
}: IsolatedRoof3DInput) {
|
||||||
const thetaRad = (theta * Math.PI) / 180;
|
const thetaRad = (theta * Math.PI) / 180;
|
||||||
const halfDepth = depth / 2;
|
const halfDepth = depth / 2;
|
||||||
|
const halfWidth = width / 2;
|
||||||
|
|
||||||
const windwardColor = useMemo(() => pressureColor(cpeWindward), [cpeWindward]);
|
const windwardColor = useMemo(() => pressureColor(cpeWindward), [cpeWindward]);
|
||||||
const leewardColor = useMemo(() => pressureColor(cpeLeeward), [cpeLeeward]);
|
const leewardColor = useMemo(() => pressureColor(cpeLeeward), [cpeLeeward]);
|
||||||
|
|
||||||
const arrowLen = forceToLength(forceKN);
|
|
||||||
|
|
||||||
const h_diff = depth * Math.tan(thetaRad);
|
const h_diff = depth * Math.tan(thetaRad);
|
||||||
const h_half = (depth / 2) * Math.tan(thetaRad);
|
const h_half = (depth / 2) * Math.tan(thetaRad);
|
||||||
|
|
||||||
@@ -60,29 +61,29 @@ function IsolatedRoofModel({
|
|||||||
const list: { pos: [number, number, number]; h: number }[] = [];
|
const list: { pos: [number, number, number]; h: number }[] = [];
|
||||||
if (type === 'shed') {
|
if (type === 'shed') {
|
||||||
list.push(
|
list.push(
|
||||||
{ pos: [-halfDepth, height / 2, -halfDepth], h: height },
|
{ pos: [-halfWidth, height / 2, -halfDepth], h: height },
|
||||||
{ pos: [halfDepth, height / 2, -halfDepth], h: height },
|
{ pos: [halfWidth, height / 2, -halfDepth], h: height },
|
||||||
{ pos: [-halfDepth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
{ pos: [-halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
||||||
{ pos: [halfDepth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
{ pos: [halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
list.push(
|
list.push(
|
||||||
{ pos: [-halfDepth, height / 2, -halfDepth], h: height },
|
{ pos: [-halfWidth, height / 2, -halfDepth], h: height },
|
||||||
{ pos: [halfDepth, height / 2, -halfDepth], h: height },
|
{ pos: [halfWidth, height / 2, -halfDepth], h: height },
|
||||||
{ pos: [-halfDepth, height / 2, halfDepth], h: height },
|
{ pos: [-halfWidth, height / 2, halfDepth], h: height },
|
||||||
{ pos: [halfDepth, height / 2, halfDepth], h: height },
|
{ pos: [halfWidth, height / 2, halfDepth], h: height },
|
||||||
{ pos: [-halfDepth, (height + h_half) / 2, 0], h: height + h_half },
|
{ pos: [-halfWidth, (height + h_half) / 2, 0], h: height + h_half },
|
||||||
{ pos: [halfDepth, (height + h_half) / 2, 0], h: height + h_half },
|
{ pos: [halfWidth, (height + h_half) / 2, 0], h: height + h_half },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
}, [type, depth, height, h_diff, h_half, halfDepth]);
|
}, [type, depth, width, height, h_diff, h_half, halfDepth, halfWidth]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
{/* Solo translúcido */}
|
{/* Solo translúcido */}
|
||||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.01, 0]} receiveShadow>
|
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.01, 0]} receiveShadow>
|
||||||
<planeGeometry args={[depth * 2, depth * 2]} />
|
<planeGeometry args={[width * 1.5, depth * 1.5]} />
|
||||||
<meshStandardMaterial color="#94a3b8" transparent opacity={0.15} />
|
<meshStandardMaterial color="#94a3b8" transparent opacity={0.15} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ function IsolatedRoofModel({
|
|||||||
castShadow
|
castShadow
|
||||||
receiveShadow
|
receiveShadow
|
||||||
>
|
>
|
||||||
<boxGeometry args={[depth, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||||
<meshStandardMaterial color={windwardColor} opacity={0.9} transparent roughness={0.4} />
|
<meshStandardMaterial color={windwardColor} opacity={0.9} transparent roughness={0.4} />
|
||||||
</mesh>
|
</mesh>
|
||||||
{/* Metade Sotavento (Z > 0) */}
|
{/* Metade Sotavento (Z > 0) */}
|
||||||
@@ -107,7 +108,7 @@ function IsolatedRoofModel({
|
|||||||
castShadow
|
castShadow
|
||||||
receiveShadow
|
receiveShadow
|
||||||
>
|
>
|
||||||
<boxGeometry args={[depth, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||||
<meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} />
|
<meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} />
|
||||||
</mesh>
|
</mesh>
|
||||||
</group>
|
</group>
|
||||||
@@ -121,7 +122,7 @@ function IsolatedRoofModel({
|
|||||||
castShadow
|
castShadow
|
||||||
receiveShadow
|
receiveShadow
|
||||||
>
|
>
|
||||||
<boxGeometry args={[depth, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||||
<meshStandardMaterial color={windwardColor} opacity={0.9} transparent roughness={0.4} />
|
<meshStandardMaterial color={windwardColor} opacity={0.9} transparent roughness={0.4} />
|
||||||
</mesh>
|
</mesh>
|
||||||
{/* Água Direita / Sotavento (Z > 0) */}
|
{/* Água Direita / Sotavento (Z > 0) */}
|
||||||
@@ -131,7 +132,7 @@ function IsolatedRoofModel({
|
|||||||
castShadow
|
castShadow
|
||||||
receiveShadow
|
receiveShadow
|
||||||
>
|
>
|
||||||
<boxGeometry args={[depth, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||||
<meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} />
|
<meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} />
|
||||||
</mesh>
|
</mesh>
|
||||||
</group>
|
</group>
|
||||||
@@ -145,12 +146,10 @@ function IsolatedRoofModel({
|
|||||||
</mesh>
|
</mesh>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Seta de força resultante (sucção para cima) */}
|
{/* Vetor de vento (Seta verde/azul batendo no elemento) */}
|
||||||
<ForceArrow
|
<WindArrow
|
||||||
start={new THREE.Vector3(0, centerY, 0)}
|
target={new THREE.Vector3(0, centerY, 0)}
|
||||||
direction={new THREE.Vector3(0, 1, 0)}
|
direction={new THREE.Vector3(0, 0, 1)}
|
||||||
length={arrowLen}
|
|
||||||
color="#ef4444"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* === LINHAS DE COTA (CAD-Style) === */}
|
{/* === LINHAS DE COTA (CAD-Style) === */}
|
||||||
@@ -208,8 +207,7 @@ function IsolatedRoofModel({
|
|||||||
|
|
||||||
{/* Rótulo Superior */}
|
{/* Rótulo Superior */}
|
||||||
<Text
|
<Text
|
||||||
position={[0, centerY + arrowLen + 0.8, 0]}
|
position={[0, centerY + 3, 0]}
|
||||||
fontSize={0.4}
|
|
||||||
color="#1a202c"
|
color="#1a202c"
|
||||||
anchorX="center"
|
anchorX="center"
|
||||||
anchorY="bottom"
|
anchorY="bottom"
|
||||||
@@ -220,75 +218,23 @@ function IsolatedRoofModel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ForceArrow({
|
|
||||||
start,
|
|
||||||
direction,
|
|
||||||
length,
|
|
||||||
color,
|
|
||||||
}: {
|
|
||||||
start: THREE.Vector3;
|
|
||||||
direction: THREE.Vector3;
|
|
||||||
length: number;
|
|
||||||
color: string;
|
|
||||||
}) {
|
|
||||||
const end = useMemo(
|
|
||||||
() => new THREE.Vector3(start.x + direction.x * length, start.y + direction.y * length, start.z + direction.z * length),
|
|
||||||
[start, direction, length],
|
|
||||||
);
|
|
||||||
const headLen = 0.3;
|
|
||||||
const headRadius = 0.1;
|
|
||||||
const shaftRadius = 0.04;
|
|
||||||
|
|
||||||
const midPoint = useMemo(
|
|
||||||
() => new THREE.Vector3((start.x + end.x) / 2, (start.y + end.y) / 2, (start.z + end.z) / 2),
|
|
||||||
[start, end],
|
|
||||||
);
|
|
||||||
const shaftLength = length - headLen;
|
|
||||||
|
|
||||||
const rotation = useMemo(() => {
|
|
||||||
const dir = new THREE.Vector3().subVectors(end, start).normalize();
|
|
||||||
const quat = new THREE.Quaternion();
|
|
||||||
quat.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
|
||||||
const euler = new THREE.Euler().setFromQuaternion(quat);
|
|
||||||
return [euler.x, euler.y, euler.z] as [number, number, number];
|
|
||||||
}, [start, end]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group>
|
|
||||||
{shaftLength > 0 && (
|
|
||||||
<mesh position={midPoint.toArray()} rotation={rotation} castShadow>
|
|
||||||
<cylinderGeometry args={[shaftRadius, shaftRadius, shaftLength, 12]} />
|
|
||||||
<meshStandardMaterial color={color} />
|
|
||||||
</mesh>
|
|
||||||
)}
|
|
||||||
<mesh
|
|
||||||
position={[end.x, end.y, end.z]}
|
|
||||||
rotation={rotation}
|
|
||||||
castShadow
|
|
||||||
>
|
|
||||||
<coneGeometry args={[headRadius, headLen, 12]} />
|
|
||||||
<meshStandardMaterial color={color} />
|
|
||||||
</mesh>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function IsolatedRoof3DViewer({
|
export default function IsolatedRoof3DViewer({
|
||||||
type,
|
type,
|
||||||
theta,
|
theta,
|
||||||
height,
|
height,
|
||||||
|
width,
|
||||||
depth,
|
depth,
|
||||||
cpeWindward,
|
cpeWindward,
|
||||||
cpeLeeward,
|
cpeLeeward,
|
||||||
cpeTop,
|
cpeTop,
|
||||||
forceKN,
|
forceKN,
|
||||||
}: IsolatedRoof3DInput) {
|
}: IsolatedRoof3DInput) {
|
||||||
const cameraDistance = Math.max(depth * 1.3, height * 1.5, 8);
|
const cameraDistance = Math.max(Math.max(width, depth) * 1.3, height * 1.5, 8);
|
||||||
|
|
||||||
const fallback = (
|
const fallback = (
|
||||||
<FallbackDiagram
|
<FallbackDiagram
|
||||||
type="isolatedRoof"
|
type="isolatedRoof"
|
||||||
props={{ type, theta, height, depth, cpeWindward, cpeLeeward, cpeTop, forceKN }}
|
props={{ type, theta, height, width, depth, cpeWindward, cpeLeeward, cpeTop, forceKN }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -311,6 +257,7 @@ export default function IsolatedRoof3DViewer({
|
|||||||
type={type}
|
type={type}
|
||||||
theta={theta}
|
theta={theta}
|
||||||
height={height}
|
height={height}
|
||||||
|
width={width}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
cpeWindward={cpeWindward}
|
cpeWindward={cpeWindward}
|
||||||
cpeLeeward={cpeLeeward}
|
cpeLeeward={cpeLeeward}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import SceneCanvas from '../SceneCanvas';
|
import SceneCanvas from '../SceneCanvas';
|
||||||
import FallbackDiagram from '../FallbackDiagram';
|
import FallbackDiagram from '../FallbackDiagram';
|
||||||
|
import { WindArrow } from './WindArrow';
|
||||||
|
|
||||||
export interface Sign3DInput {
|
export interface Sign3DInput {
|
||||||
/** Comprimento ℓ (m) */
|
/** Comprimento ℓ (m) */
|
||||||
@@ -22,10 +23,8 @@ export interface Sign3DInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converte kN para um comprimento visual proporcional no eixo 3D.
|
* Módulo 3D para visualização de Muros e Placas Isoladas.
|
||||||
* 1 kN = 0.25 m de seta (escala calibrada para visualização).
|
|
||||||
*/
|
*/
|
||||||
const forceToLength = (kN: number): number => Math.min(Math.max(kN * 0.25, 0.5), 8);
|
|
||||||
|
|
||||||
function SignModel({
|
function SignModel({
|
||||||
length,
|
length,
|
||||||
@@ -33,13 +32,11 @@ function SignModel({
|
|||||||
groundClearance,
|
groundClearance,
|
||||||
alpha,
|
alpha,
|
||||||
cf,
|
cf,
|
||||||
forceKN,
|
|
||||||
applicationPoint,
|
applicationPoint,
|
||||||
}: Sign3DInput) {
|
}: Sign3DInput) {
|
||||||
const baseY = groundClearance;
|
const baseY = groundClearance;
|
||||||
const topY = baseY + height;
|
const topY = baseY + height;
|
||||||
const halfL = length / 2;
|
const halfL = length / 2;
|
||||||
const arrowLen = forceToLength(forceKN);
|
|
||||||
|
|
||||||
// Direção da seta no plano XZ (α é o ângulo de incidência do vento relativo à superfície)
|
// Direção da seta no plano XZ (α é o ângulo de incidência do vento relativo à superfície)
|
||||||
// O ângulo em relação à normal da placa (eixo X) é 90 - α
|
// O ângulo em relação à normal da placa (eixo X) é 90 - α
|
||||||
@@ -114,12 +111,10 @@ function SignModel({
|
|||||||
Resultante (e = {applicationPoint.toFixed(2)}m)
|
Resultante (e = {applicationPoint.toFixed(2)}m)
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Vetor de força resultante */}
|
{/* Vetor de vento (Seta verde/azul batendo no elemento) */}
|
||||||
<ForceArrow
|
<WindArrow
|
||||||
start={arrowStart}
|
target={arrowStart}
|
||||||
direction={arrowDir}
|
direction={arrowDir}
|
||||||
length={arrowLen}
|
|
||||||
color={forceKN >= 0 ? '#ef4444' : '#3b82f6'}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* === LINHAS DE COTA (CAD-Style Dimensions) === */}
|
{/* === LINHAS DE COTA (CAD-Style Dimensions) === */}
|
||||||
@@ -154,7 +149,7 @@ function SignModel({
|
|||||||
</group>
|
</group>
|
||||||
|
|
||||||
{/* Cota de Comprimento (l) */}
|
{/* Cota de Comprimento (l) */}
|
||||||
<group position={[0.4, baseY + height / 2, 0]}>
|
<group position={[-0.4, baseY + height / 2, 0]}>
|
||||||
{/* Linha horizontal longitudinal */}
|
{/* Linha horizontal longitudinal */}
|
||||||
<mesh position={[0, 0, 0]}>
|
<mesh position={[0, 0, 0]}>
|
||||||
<boxGeometry args={[0.015, 0.015, length]} />
|
<boxGeometry args={[0.015, 0.015, length]} />
|
||||||
@@ -172,8 +167,8 @@ function SignModel({
|
|||||||
</mesh>
|
</mesh>
|
||||||
{/* Texto do comprimento */}
|
{/* Texto do comprimento */}
|
||||||
<Text
|
<Text
|
||||||
position={[0.15, 0, 0]}
|
position={[-0.15, 0, 0]}
|
||||||
rotation={[0, Math.PI / 2, 0]}
|
rotation={[0, -Math.PI / 2, 0]}
|
||||||
fontSize={0.25}
|
fontSize={0.25}
|
||||||
color="#475569"
|
color="#475569"
|
||||||
anchorX="center"
|
anchorX="center"
|
||||||
@@ -197,61 +192,7 @@ function SignModel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ForceArrow({
|
|
||||||
start,
|
|
||||||
direction,
|
|
||||||
length,
|
|
||||||
color,
|
|
||||||
}: {
|
|
||||||
start: THREE.Vector3;
|
|
||||||
direction: THREE.Vector3;
|
|
||||||
length: number;
|
|
||||||
color: string;
|
|
||||||
}) {
|
|
||||||
const end = useMemo(
|
|
||||||
() => new THREE.Vector3(start.x + direction.x * length, start.y, start.z + direction.z * length),
|
|
||||||
[start, direction, length],
|
|
||||||
);
|
|
||||||
const headLen = 0.3;
|
|
||||||
const headRadius = 0.1;
|
|
||||||
const shaftRadius = 0.04;
|
|
||||||
|
|
||||||
// Cilindro principal (haste)
|
|
||||||
const midPoint = useMemo(
|
|
||||||
() => new THREE.Vector3((start.x + end.x) / 2, (start.y + end.y) / 2, (start.z + end.z) / 2),
|
|
||||||
[start, end],
|
|
||||||
);
|
|
||||||
const shaftLength = length - headLen;
|
|
||||||
|
|
||||||
// Rotação do cilindro (apontar de start para end)
|
|
||||||
const rotation = useMemo(() => {
|
|
||||||
const dir = new THREE.Vector3().subVectors(end, start).normalize();
|
|
||||||
const quat = new THREE.Quaternion();
|
|
||||||
quat.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
|
||||||
const euler = new THREE.Euler().setFromQuaternion(quat);
|
|
||||||
return [euler.x, euler.y, euler.z] as [number, number, number];
|
|
||||||
}, [start, end]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group>
|
|
||||||
{shaftLength > 0 && (
|
|
||||||
<mesh position={midPoint.toArray()} rotation={rotation} castShadow>
|
|
||||||
<cylinderGeometry args={[shaftRadius, shaftRadius, shaftLength, 12]} />
|
|
||||||
<meshStandardMaterial color={color} />
|
|
||||||
</mesh>
|
|
||||||
)}
|
|
||||||
{/* Ponta da seta (cone) */}
|
|
||||||
<mesh
|
|
||||||
position={[end.x, end.y, end.z]}
|
|
||||||
rotation={rotation}
|
|
||||||
castShadow
|
|
||||||
>
|
|
||||||
<coneGeometry args={[headRadius, headLen, 12]} />
|
|
||||||
<meshStandardMaterial color={color} />
|
|
||||||
</mesh>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Sign3DViewer({
|
export default function Sign3DViewer({
|
||||||
length,
|
length,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
|||||||
import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
|
import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import SceneCanvas from '../SceneCanvas';
|
import SceneCanvas from '../SceneCanvas';
|
||||||
|
import { WindArrow } from './WindArrow';
|
||||||
import FallbackDiagram from '../FallbackDiagram';
|
import FallbackDiagram from '../FallbackDiagram';
|
||||||
|
|
||||||
export interface Tower3DInput {
|
export interface Tower3DInput {
|
||||||
@@ -190,31 +191,11 @@ function TowerModel({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Vetores de força distribuídos pelos tramos (meio de cada tramo) */}
|
{/* Seta de Vento única atingindo o meio da torre */}
|
||||||
{Array.from({ length: panels }).map((_, i) => {
|
<WindArrow
|
||||||
const pHeight = height / panels;
|
target={[0, height / 2, 0]}
|
||||||
const startY = (i + 0.5) * pHeight; // Altura no meio do tramo
|
direction={forceDir}
|
||||||
const pForce = forceKN / panels; // Força por tramo
|
/>
|
||||||
|
|
||||||
// Escala da seta menor para ficar visualmente agradável
|
|
||||||
const pArrowLen = Math.min(Math.max(pForce * 0.1, 0.5), 2.5);
|
|
||||||
|
|
||||||
// Calcular o ponto inicial para que a ponta da seta encoste na face (baseWidth / 2)
|
|
||||||
const endX = -forceDir.x * (baseWidth / 2);
|
|
||||||
const endZ = -forceDir.z * (baseWidth / 2);
|
|
||||||
const startX = endX - forceDir.x * pArrowLen;
|
|
||||||
const startZ = endZ - forceDir.z * pArrowLen;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ForceArrow
|
|
||||||
key={`force-${i}`}
|
|
||||||
start={[startX, startY, startZ]}
|
|
||||||
direction={forceDir}
|
|
||||||
length={pArrowLen}
|
|
||||||
color="#ef4444"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Labels indicativos */}
|
{/* Labels indicativos */}
|
||||||
<mesh position={[baseWidth / 2 + 0.5, 0.5, 0]}>
|
<mesh position={[baseWidth / 2 + 0.5, 0.5, 0]}>
|
||||||
@@ -234,50 +215,6 @@ function TowerModel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ForceArrow({
|
|
||||||
start,
|
|
||||||
direction,
|
|
||||||
length,
|
|
||||||
color,
|
|
||||||
}: {
|
|
||||||
start: [number, number, number];
|
|
||||||
direction: THREE.Vector3;
|
|
||||||
length: number;
|
|
||||||
color: string;
|
|
||||||
}) {
|
|
||||||
const startVec = useMemo(() => new THREE.Vector3(...start), [start]);
|
|
||||||
const end = useMemo(
|
|
||||||
() => new THREE.Vector3(startVec.x + direction.x * length, startVec.y, startVec.z + direction.z * length),
|
|
||||||
[startVec, direction, length],
|
|
||||||
);
|
|
||||||
const mid = useMemo(
|
|
||||||
() => new THREE.Vector3((startVec.x + end.x) / 2, (startVec.y + end.y) / 2, (startVec.z + end.z) / 2),
|
|
||||||
[startVec, end],
|
|
||||||
);
|
|
||||||
const quat = useMemo(() => {
|
|
||||||
const dir = new THREE.Vector3().subVectors(end, startVec).normalize();
|
|
||||||
const q = new THREE.Quaternion();
|
|
||||||
q.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
|
||||||
return new THREE.Euler().setFromQuaternion(q);
|
|
||||||
}, [startVec, end]);
|
|
||||||
const headLen = 0.3;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group>
|
|
||||||
{length - headLen > 0 && (
|
|
||||||
<mesh position={mid.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
|
||||||
<cylinderGeometry args={[0.05, 0.05, length - headLen, 10]} />
|
|
||||||
<meshStandardMaterial color={color} />
|
|
||||||
</mesh>
|
|
||||||
)}
|
|
||||||
<mesh position={end.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
|
||||||
<coneGeometry args={[0.12, headLen, 10]} />
|
|
||||||
<meshStandardMaterial color={color} />
|
|
||||||
</mesh>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Tower3DViewer(input: Tower3DInput) {
|
export default function Tower3DViewer(input: Tower3DInput) {
|
||||||
const { baseWidth, height } = input;
|
const { baseWidth, height } = input;
|
||||||
const dist = Math.max(baseWidth * 3, height * 1.2);
|
const dist = Math.max(baseWidth * 3, height * 1.2);
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
export interface WindArrowProps {
|
||||||
|
/** Ponto onde a ponta da seta do vento toca (ex: centro geométrico da estrutura) */
|
||||||
|
target: THREE.Vector3 | [number, number, number];
|
||||||
|
/** Direção de onde o vento vem (e para onde ele vai). Vetor normalizado. */
|
||||||
|
direction: THREE.Vector3 | [number, number, number];
|
||||||
|
/** Fator de escala da seta (default: 1) */
|
||||||
|
scale?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindArrow({ target, direction, scale = 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);
|
||||||
|
|
||||||
|
const end = targetVec;
|
||||||
|
const base = useMemo(
|
||||||
|
() => new THREE.Vector3(end.x - dirVec.x * length, end.y - dirVec.y * length, end.z - dirVec.z * length),
|
||||||
|
[end, dirVec, length],
|
||||||
|
);
|
||||||
|
|
||||||
|
const headLen = 0.3 * scale;
|
||||||
|
const headRadius = 0.1 * scale;
|
||||||
|
const shaftRadius = 0.04 * scale;
|
||||||
|
|
||||||
|
const midPoint = useMemo(
|
||||||
|
() => new THREE.Vector3((base.x + end.x) / 2, (base.y + end.y) / 2, (base.z + end.z) / 2),
|
||||||
|
[base, end],
|
||||||
|
);
|
||||||
|
const shaftLength = length - headLen;
|
||||||
|
|
||||||
|
const rotation = useMemo(() => {
|
||||||
|
const dir = new THREE.Vector3().subVectors(end, base).normalize();
|
||||||
|
const quat = new THREE.Quaternion();
|
||||||
|
quat.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
||||||
|
const euler = new THREE.Euler().setFromQuaternion(quat);
|
||||||
|
return [euler.x, euler.y, euler.z] as [number, number, number];
|
||||||
|
}, [base, end]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{shaftLength > 0 && (
|
||||||
|
<mesh position={midPoint.toArray()} rotation={rotation} castShadow>
|
||||||
|
<cylinderGeometry args={[shaftRadius, shaftRadius, shaftLength, 12]} />
|
||||||
|
<meshStandardMaterial color={color} />
|
||||||
|
</mesh>
|
||||||
|
)}
|
||||||
|
{/* Ponta da seta (cone) */}
|
||||||
|
<mesh position={[end.x, end.y, end.z]} rotation={rotation} castShadow>
|
||||||
|
<coneGeometry args={[headRadius, headLen, 12]} />
|
||||||
|
<meshStandardMaterial color={color} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
|
import { Dialog as SheetPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||||
|
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||||
|
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||||
|
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||||
|
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Overlay
|
||||||
|
data-slot="sheet-overlay"
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
side = "right",
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||||
|
side?: "top" | "right" | "bottom" | "left"
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SheetPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.Content
|
||||||
|
data-slot="sheet-content"
|
||||||
|
className={cn(
|
||||||
|
"fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
|
||||||
|
side === "right" &&
|
||||||
|
"inset-y-0 right-0 h-full w-full border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-md",
|
||||||
|
side === "left" &&
|
||||||
|
"inset-y-0 left-0 h-full w-full border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-md",
|
||||||
|
side === "top" &&
|
||||||
|
"inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||||
|
side === "bottom" &&
|
||||||
|
"inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||||
|
<XIcon className="size-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</SheetPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</SheetPrimitive.Content>
|
||||||
|
</SheetPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-header"
|
||||||
|
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-footer"
|
||||||
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Title
|
||||||
|
data-slot="sheet-title"
|
||||||
|
className={cn("font-semibold text-foreground text-lg", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Description
|
||||||
|
data-slot="sheet-description"
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sheet,
|
||||||
|
SheetTrigger,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetFooter,
|
||||||
|
SheetTitle,
|
||||||
|
SheetDescription,
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export const barManual = {
|
||||||
|
title: 'Barras Estruturais Individuais (Seção 8.2)',
|
||||||
|
intro: 'Coeficientes de arrasto para perfis estruturais isolados (perfis I, U, L, tubulares), abordando forças por unidade de comprimento.',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '1. Equação do Esforço',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
A força de arrasto exercida sobre barras individuais é dada por metro linear de elemento:
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
|
||||||
|
F = q · Ca · d
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Onde <strong>d</strong> é a dimensão característica da seção transversal do perfil (altura exposta frontalmente) e <strong>Ca</strong> é o coeficiente de arrasto da seção estudada.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '2. Formas Geométricas dos Perfis (Tabelas 26 a 28)',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
O coeficiente Ca varia drasticamente com o perfil devido a cantos vivos ou aerodinâmicos:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1">
|
||||||
|
<li><strong>Perfis de Faces Planas (L, U, I):</strong> Possuem cantos vivos que forçam o descolamento imediato do vento, gerando altos valores de arrasto (Ca em torno de 1.4 a 2.0).</li>
|
||||||
|
<li><strong>Perfis Circulares/Tubulares:</strong> O vento consegue contornar a forma circular, reduzindo o arrasto consideravelmente (Ca em torno de 0.6 a 1.2, dependendo de Reynolds).</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export const bridgeManual = {
|
||||||
|
title: 'Tabuleiro de Ponte (Seção 11.2 / 11.3)',
|
||||||
|
intro: 'Explicação detalhada sobre a ação estática do vento em pontes, cobrindo forças de arrasto (horizontais) e forças de sustentação (verticais).',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '1. Forças Atuantes',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
A ação do vento em tabuleiros de pontes é dividida em duas componentes principais:
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm space-y-2 border border-border">
|
||||||
|
<div>
|
||||||
|
<strong>Força de Arrasto (Fx):</strong> Fx = q · Cx · Heg · Lp
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Força de Sustentação (Fz):</strong> Fz = q · Cz · B · Lp
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Onde <strong>q</strong> é a pressão dinâmica do vento, <strong>Lp</strong> é o comprimento do vão exposto, <strong>Heg</strong> é a altura equivalente do tabuleiro (soma das áreas de projeção das vigas, guarda-corpos e barreiras) e <strong>B</strong> é a largura da ponte.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '2. Coeficientes Aerodinâmicos (Cx e Cz)',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
Os coeficientes são determinados com base na relação geométrica entre a largura e a altura do tabuleiro (B / Heg), bem como o ângulo de ataque do vento (α):
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1">
|
||||||
|
<li>
|
||||||
|
<strong>Cx (Arrasto):</strong> Representa a resistência que a ponte oferece ao fluxo horizontal de vento. Valores maiores de Cx significam maior força empurrando a ponte lateralmente.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Cz (Sustentação/Lift):</strong> Representa a força vertical (para cima ou para baixo) induzida pela inclinação do vento (ângulo de ataque α).
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Se o vento incide com um ângulo positivo (vindo ligeiramente de baixo, α > 0), ele tende a "levantar" a ponte (Cz positivo). Se incide de cima (α < 0), empurra a ponte para baixo (Cz negativo).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '3. Glossário de Parâmetros',
|
||||||
|
content: (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-border text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b">
|
||||||
|
<th className="text-left py-2 font-semibold">Símbolo</th>
|
||||||
|
<th className="text-left py-2 font-semibold">Parâmetro</th>
|
||||||
|
<th className="text-left py-2 font-semibold">Descrição</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-border">
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 font-mono font-semibold">Lp</td>
|
||||||
|
<td className="py-2">Maior Vão (m)</td>
|
||||||
|
<td className="py-2">O comprimento do vão livre da ponte sob análise.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 font-mono font-semibold">B</td>
|
||||||
|
<td className="py-2">Largura (m)</td>
|
||||||
|
<td className="py-2">Largura total do tabuleiro da ponte (perpendicular ao fluxo principal).</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 font-mono font-semibold">Heg</td>
|
||||||
|
<td className="py-2">Altura Equivalente (m)</td>
|
||||||
|
<td className="py-2">Soma da projeção vertical de todos os elementos expostos ao vento.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 font-mono font-semibold">α (alpha)</td>
|
||||||
|
<td className="py-2">Ângulo de Ataque (º)</td>
|
||||||
|
<td className="py-2">Inclinação do vetor do vento em relação ao plano horizontal (varia de -5º a +5º).</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 font-mono font-semibold">q</td>
|
||||||
|
<td className="py-2">Pressão Dinâmica (kN/m²)</td>
|
||||||
|
<td className="py-2">Pressão do vento calculada a partir da velocidade de projeto.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export const cylinderManual = {
|
||||||
|
title: 'Cilindros e Chaminés (Tabela 13 - Seção 6.1)',
|
||||||
|
intro: 'Ação do vento em cilindros circulares (chaminés, silos e reservatórios), detalhando a influência do número de Reynolds e da rugosidade superficial no arrasto.',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '1. Equação do Arrasto Global',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
A força de arrasto exercida por unidade de comprimento de um cilindro circular é calculada por:
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
|
||||||
|
F = q · Ca · D
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Onde <strong>D</strong> é o diâmetro externo do cilindro e <strong>Ca</strong> é o coeficiente de arrasto corrigido pela esbeltez.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '2. Regimes de Fluxo e Número de Reynolds (Re)',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
O comportamento aerodinâmico em cilindros circulares é governado pelo <strong>Número de Reynolds (Re)</strong>, um parâmetro adimensional que indica a relação entre as forças de inércia e de viscosidade do fluxo:
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted/50 p-3 rounded-lg font-mono text-sm border border-border">
|
||||||
|
Re = 70.000 · Vk · D
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
O fluxo é classificado em três regimes principais na NBR 6123:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1">
|
||||||
|
<li>
|
||||||
|
<strong>Subcrítico (Re < 1.5 · 10⁵):</strong> A camada limite é laminar. O descolamento do fluxo ocorre cedo (cerca de 80º em relação ao ponto de estagnação). A esteira de turbulência atrás do cilindro é larga, resultando em um arrasto elevado (Ca ≈ 1.2).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Crítico / Supercrítico (Re > 4 · 10⁵):</strong> A camada limite torna-se turbulenta. Ela consegue "colar" mais tempo na parede do cilindro, atrasando o descolamento para cerca de 120º. A esteira traseira estreita significativamente, derrubando o arrasto de forma abrupta (Ca pode cair para 0.4 a 0.7).
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '3. Influência da Rugosidade Superficial',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
Para altos números de Reynolds, a rugosidade superficial do cilindro (rugosidade relativa <strong>d/k</strong>) impede que o arrasto caia excessivamente.
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1">
|
||||||
|
<li>
|
||||||
|
<strong>Superfícies lisas (d/k > 10⁶):</strong> Exibem o menor arrasto possível no regime supercrítico (Ca ≈ 0.4 - 0.5).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Superfícies rugosas (d/k reduzido, ex: concreto bruto ou chapa ondulada):</strong> Aceleram a transição para fluxo turbulento, mas a rugosidade gera turbulência local que impede a esteira de estreitar tanto, mantendo o arrasto em patamares mais elevados (Ca ≈ 0.7 - 0.9).
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export const domeManual = {
|
||||||
|
title: 'Cúpulas Esféricas (Tabela 21 e 22)',
|
||||||
|
intro: 'Ação do vento em cúpulas e coberturas hemisféricas, analisando coeficientes de forças globais e pressões locais.',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '1. Aerodinâmica Tridimensional',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
Cúpulas esféricas possuem comportamento aerodinâmico tridimensional muito favorável. O ar contorna a cúpula não apenas por cima, mas também pelos lados, reduzindo a turbulência geral comparado com coberturas planas ou inclinadas.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '2. Coeficientes de Pressão Externa',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
O vento acelerado sobre a cúpula cria três zonas de pressões marcantes:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1">
|
||||||
|
<li><strong>Barlavento (frente):</strong> Sobrepressão sutil perto da base, que rapidamente vira sucção à medida que sobe no arco esférico.</li>
|
||||||
|
<li><strong>Topo:</strong> Ponto de máxima velocidade do vento. Sofre uma sucção crítica gerada pelo escoamento aerodinâmico acelerado.</li>
|
||||||
|
<li><strong>Sotavento (traseira):</strong> Zona de vácuo aerodinâmico com sucção homogênea constante e desprendimento de esteira tridimensional.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export const dynamicsManual = {
|
||||||
|
title: 'Efeitos Dinâmicos e Ressonância (Seção 9 e Anexo G)',
|
||||||
|
intro: 'Explicação detalhada dos fenômenos dinâmicos induzidos pelo vento, com foco especial no desprendimento de vórtices (Vortex Shedding).',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '1. Desprendimento de Vórtices (Vortex Shedding)',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
Quando o vento sopra perpendicularmente a um obstáculo cilíndrico (como uma chaminé ou torre), o fluxo de ar se divide e cria redemoinhos alternados na traseira da estrutura.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Estes redemoinhos, conhecidos como <strong>Vórtices de Von Kármán</strong>, geram forças de sucção alternadas nas laterais perpendiculares à direção do vento. Como consequência, o cilindro sofre forças dinâmicas oscilantes transversais.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '2. Velocidade Crítica de Ressonância (Vcr)',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
A frequência na qual os vórtices se desprendem é dada por:
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
|
||||||
|
f = St · V / D
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Onde <strong>St</strong> é o número de Strouhal (adimensional, geralmente 0.2 para cilindros), <strong>V</strong> é a velocidade do vento e <strong>D</strong> é o diâmetro.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Se a frequência de descolamento de vórtices se igualar a uma das frequências naturais da estrutura (<strong>fn</strong>), ocorre o fenômeno de **ressonância**, gerando grandes amplitudes de oscilação. A velocidade do vento que ativa a ressonância é chamada de **Velocidade Crítica (Vcr)**:
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
|
||||||
|
Vcr = fn · D / St
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '3. Parâmetros Chave',
|
||||||
|
content: (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-border text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b">
|
||||||
|
<th className="text-left py-2 font-semibold">Parâmetro</th>
|
||||||
|
<th className="text-left py-2 font-semibold">Descrição</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-border">
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 font-mono font-semibold">St (Strouhal)</td>
|
||||||
|
<td className="py-2">Número adimensional que depende da geometria da seção. Para cilindro liso, St = 0.2.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 font-mono font-semibold">fn (Frequência)</td>
|
||||||
|
<td className="py-2">Frequência natural de vibração da estrutura (em Hz). Estruturas mais esbeltas têm fn menor.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 font-mono font-semibold">Vcr (Vel. Crítica)</td>
|
||||||
|
<td className="py-2">Velocidade em que os esforços alternados dos vórtices entram em ressonância com a estrutura.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { bridgeManual } from './bridge';
|
||||||
|
import { towerManual } from './tower';
|
||||||
|
import { signManual } from './sign';
|
||||||
|
import { warehouseManual } from './warehouse';
|
||||||
|
import { cylinderManual } from './cylinder';
|
||||||
|
import { vaultManual } from './vault';
|
||||||
|
import { domeManual } from './dome';
|
||||||
|
import { barManual } from './bar';
|
||||||
|
import { dynamicsManual } from './dynamics';
|
||||||
|
import { isolatedRoofManual } from './isolated-roof';
|
||||||
|
|
||||||
|
export const manuals: Record<string, typeof bridgeManual> = {
|
||||||
|
bridge: bridgeManual,
|
||||||
|
tower: towerManual,
|
||||||
|
sign: signManual,
|
||||||
|
warehouse: warehouseManual,
|
||||||
|
cylinder: cylinderManual,
|
||||||
|
vault: vaultManual,
|
||||||
|
dome: domeManual,
|
||||||
|
bar: barManual,
|
||||||
|
dynamics: dynamicsManual,
|
||||||
|
'isolated-roof': isolatedRoofManual,
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export const isolatedRoofManual = {
|
||||||
|
title: 'Coberturas Isoladas (Tabelas 24 e 25)',
|
||||||
|
intro: 'Ação do vento em coberturas sem paredes (estruturas abertas, hangares abertos, postos de gasolina), abordando coeficientes de pressão líquida e forças resultantes.',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '1. Coeficiente de Pressão Líquida (Cp)',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
Como as coberturas isoladas não possuem paredes para vedação, o vento escoa livremente tanto por cima quanto por baixo do telhado.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Por isso, a norma utiliza o coeficiente de **pressão líquida (Cp)**, que já representa a diferença líquida de pressão entre a face superior e inferior da chapa do telhado:
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
|
||||||
|
F = q · Cp · A
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Dependendo da direção e do ângulo de inclinação do telhado, a chapa pode sofrer sucção (força para cima) ou sobrepressão (força para baixo).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '2. Efeito da Inclinação (θ)',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
O ângulo de inclinação do telhado (<strong>θ</strong>) define se o escoamento é suave ou gera turbulência pesada:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1">
|
||||||
|
<li><strong>Ângulos pequenos (θ < 5º):</strong> O escoamento gera sucção quase uniforme em toda a chapa devido ao efeito Venturi do vento passando sob a cobertura.</li>
|
||||||
|
<li><strong>Ângulos elevados (θ > 15º):</strong> A cobertura atua como uma asa de avião com alto ângulo de ataque. O lado de barlavento sofre forte pressão para baixo, enquanto o lado de sotavento sofre sucção severa.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export const signManual = {
|
||||||
|
title: 'Muros e Placas Isoladas (Tabela 11 e Seção 6.1)',
|
||||||
|
intro: 'Ação do vento em muros, painéis e placas isoladas livres no solo, detalhando arrasto, excentricidades e efeitos de placas de extremidade.',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '1. Coeficiente de Força (Cf)',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
Para muros e placas isoladas, a NBR 6123 define o coeficiente de força (<strong>Cf</strong>) com base na relação de aspecto e folga do solo:
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
|
||||||
|
F = q · Cf · A
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Onde <strong>A</strong> é a área total exposta do muro (Comprimento · Altura). O coeficiente Cf varia conforme o alongamento do muro. Muros muito longos e rente ao solo têm Cf maior porque o fluxo de ar não consegue escapar pelas pontas, gerando maior retenção de energia do vento.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '2. Excentricidade da Força (Torsão)',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
O vento raramente atinge o muro de forma perfeitamente homogênea. Quando o vento incide com um ângulo oblíquo (ex: α = 50º), ele se concentra mais próximo a uma das bordas.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Por este motivo, a norma NBR 6123 exige a aplicação da força resultante com uma <strong>excentricidade horizontal (e)</strong> em relação ao centro geométrico da placa:
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted/50 p-3 rounded-lg font-mono text-sm border border-border">
|
||||||
|
e = 0.25 · L (comprimento do muro)
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Esta excentricidade gera um momento torsor nas fundações e suportes estruturais do muro, sendo um fator crítico no dimensionamento de placas e outdoors.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '3. Placas de Extremidade (End Plates)',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
Placas de extremidade são abas físicas verticais instaladas nas pontas do muro para fins aerodinâmicos ou construtivos:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1">
|
||||||
|
<li>
|
||||||
|
<strong>Sem abas:</strong> O vento bate na frente e escoa com facilidade pelas pontas (efeito de borda tridimensional), o que reduz ligeiramente o Cf local próximo às pontas.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Com abas:</strong> As abas forçam o vento a subir apenas pelo topo, gerando um padrão bidimensional de escoamento. Isso aumenta significativamente a pressão diferencial entre a face da frente e de trás do muro, elevando o valor de Cf para patamares elevados (de 1.3 até 2.0).
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export const towerManual = {
|
||||||
|
title: 'Torre Reticulada (Seção 8.5)',
|
||||||
|
intro: 'Ação do vento em torres de seção quadrada ou triangular equilátera, cobrindo o índice de solidez, coeficientes de arrasto e fatores de incidência.',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '1. Força de Arrasto Total',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
A força de arrasto exercida sobre cada tramo de uma torre reticulada é dada por:
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
|
||||||
|
F = q · Ca · Ae
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Onde <strong>q</strong> é a pressão de vento de projeto, <strong>Ca</strong> é o coeficiente de arrasto e <strong>Ae</strong> é a área projetada efetiva (soma da projeção das barras de barlavento e sotavento, ajustada pelo coeficiente de proteção de tramos).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '2. Índice de Solidez (φ)',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
O índice de solidez (<strong>φ</strong>) é a relação entre a área real exposta pelas barras e a área total envolvente delimitada pelo contorno da face da torre:
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted/50 p-3 rounded-lg font-mono text-sm border border-border">
|
||||||
|
φ = Área das Barras / (Largura da Base · Altura do Tramo)
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Uma solidez baixa (φ próximo a 0.05) indica uma torre muito vazada (vento passa direto). Uma solidez alta (φ próximo a 1) indica uma torre quase sólida. O coeficiente de arrasto Ca diminui à medida que a solidez aumenta porque as barras de sotavento ficam mais protegidas do vento.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '3. Ação Direcional (Kα) - Quadrada vs Triangular',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
O vento pode atingir a torre de diferentes direções. A norma aborda isso aplicando um fator direcional <strong>Kα</strong>:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1">
|
||||||
|
<li>
|
||||||
|
<strong>Torres Quadradas:</strong> O vento incidindo na diagonal (45º) causa maior arrasto global do que nas faces (0º). Portanto, Kα é maior para vento a 45º.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Torres Triangulares:</strong> Devido à simetria geométrica de um triângulo equilátero, a NBR 6123 assume que o arrasto total é constante independentemente da direção. Assim, <strong>Kα é sempre 1.0</strong> para qualquer direção do vento.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '4. Glossário de Parâmetros',
|
||||||
|
content: (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-border text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b">
|
||||||
|
<th className="text-left py-2 font-semibold">Símbolo</th>
|
||||||
|
<th className="text-left py-2 font-semibold">Parâmetro</th>
|
||||||
|
<th className="text-left py-2 font-semibold">Descrição</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-border">
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 font-mono font-semibold">φ (phi)</td>
|
||||||
|
<td className="py-2">Índice de Solidez</td>
|
||||||
|
<td className="py-2">Razão entre a área das barras e a área total envolvente da face.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 font-mono font-semibold">Kα</td>
|
||||||
|
<td className="py-2">Fator de Direção</td>
|
||||||
|
<td className="py-2">Multiplicador que ajusta a força com base no ângulo de incidência do vento.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 font-mono font-semibold">Ca</td>
|
||||||
|
<td className="py-2">Coeficiente de Arrasto</td>
|
||||||
|
<td className="py-2">Coeficiente adimensional que mede a resistência aerodinâmica da estrutura.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export const vaultManual = {
|
||||||
|
title: 'Abóbadas Cilíndricas (Tabela 15 - Seção 6.2)',
|
||||||
|
intro: 'Coeficientes de pressão externa para coberturas cilíndricas convexas (abóbadas) assentadas diretamente no solo ou elevadas.',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '1. Geometria da Abóbada',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
O fluxo de vento sobre abóbadas é fortemente influenciado pelas relações geométricas:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1">
|
||||||
|
<li><strong>f (flecha):</strong> Altura do arco da abóbada.</li>
|
||||||
|
<li><strong>d (vão):</strong> Largura da projeção horizontal da abóbada.</li>
|
||||||
|
<li><strong>h (altura da parede de suporte):</strong> Se a abóbada estiver assentada diretamente no solo, h = 0. Se estiver elevada, h representa a altura da parede.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '2. Distribuição de Pressões (Cpe)',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
O perfil curvo da abóbada atua como um aerofólio (perfil de asa), gerando aceleração acentuada do ar no topo:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1">
|
||||||
|
<li>
|
||||||
|
<strong>Barlavento (início do arco):</strong> Pode sofrer pressão positiva se a parede frontal for alta, ou sucção leve caso o vento deslize suavemente pela curvatura suave.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Topo (Zona Central):</strong> É a região de maior aceleração do fluxo de vento. Sofre uma sucção fortíssima (Cpe muito negativo), tendendo a puxar a cobertura para cima.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Sotavento (fim do arco):</strong> O fluxo descola da abóbada, gerando uma zona de recirculação com sucção constante, embora de menor magnitude que no topo.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export const warehouseManual = {
|
||||||
|
title: 'Galpão Retangular (Tabelas 4 e 5 - Seção 6)',
|
||||||
|
intro: 'Ação do vento em galpões de base retangular e telhados a duas águas. Abrange os coeficientes de pressão externa (Cpe) e interna (Cpi).',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '1. Pressão Resultante (p)',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
A pressão efetiva do vento em qualquer ponto da superfície externa do galpão é a diferença entre a pressão externa e a interna:
|
||||||
|
</p>
|
||||||
|
<div className="bg-muted/50 p-4 rounded-lg font-mono text-sm border border-border">
|
||||||
|
p = q · (Cpe - Cpi)
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Onde:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1">
|
||||||
|
<li><strong>q:</strong> Pressão dinâmica do vento na altura do topo do telhado.</li>
|
||||||
|
<li><strong>Cpe:</strong> Coeficiente de pressão externa (depende da zona do galpão).</li>
|
||||||
|
<li><strong>Cpi:</strong> Coeficiente de pressão interna (depende de aberturas nas paredes).</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '2. Zonas de Pressão Externa (Cpe)',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
Devido à complexidade aerodinâmica, a NBR 6123 divide o galpão em várias zonas de pressão localizadas:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1">
|
||||||
|
<li>
|
||||||
|
<strong>Paredes (Zonas A, B, C, D, E):</strong> Geralmente, a parede de barlavento sofre sobrepressão (Cpe > 0, Zona D), enquanto as paredes laterais (Zonas A, B, C) e sotavento (Zona E) sofrem sucção (Cpe < 0) devido ao fluxo descolado.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Telhado (Zonas F, G, H, I, J):</strong> O fluxo de ar descola bruscamente no beiral de barlavento, criando sucções locais fortíssimas nas bordas (Zonas F e G). No meio do telhado (Zona H) e na água de sotavento (Zonas I, J), a sucção tende a diminuir levemente.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '3. Pressão Interna (Cpi)',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
A pressão interna surge da passagem do ar através de frestas, janelas, portões e aberturas:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc pl-5 space-y-1">
|
||||||
|
<li>
|
||||||
|
<strong>Abertura dominante em barlavento (frente):</strong> O vento entra e "infla" o galpão por dentro. Isso gera uma sobrepressão interna positiva (Cpi até +0.9). Esta pressão positiva empurra as paredes e o telhado de dentro para fora, somando-se à sucção externa e aumentando o risco de colapso do telhado.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Abertura dominante em sotavento ou laterais:</strong> O ar é "sugado" de dentro do galpão, criando vácuo/sucção interna (Cpi até -0.9). Isso ajuda a estabilizar o telhado contra a sucção externa, mas sobrecarrega a parede de barlavento.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -136,8 +136,8 @@ describe('M9.4 — serializeFtool (texto exportado)', () => {
|
|||||||
txt = serializeFtool(model);
|
txt = serializeFtool(model);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Contém cabeçalho VentoApp', () => {
|
it('Contém cabeçalho BrainWind', () => {
|
||||||
expect(txt).toContain('VentoApp');
|
expect(txt).toContain('BrainWind v1.0');
|
||||||
expect(txt).toContain('NBR 6123:2023');
|
expect(txt).toContain('NBR 6123:2023');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,11 @@ describe('M9.8 — Dicionário de traduções', () => {
|
|||||||
expect(t('nav_warehouse', 'pt-BR')).not.toBe(t('nav_warehouse', 'en-US'));
|
expect(t('nav_warehouse', 'pt-BR')).not.toBe(t('nav_warehouse', 'en-US'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Chaves "neutras" (marca) são iguais em pt-BR e en-US', () => {
|
describe('Traduções básicas', () => {
|
||||||
expect(t('app_title', 'pt-BR')).toBe('VentoApp');
|
it('app_title', () => {
|
||||||
expect(t('app_title', 'en-US')).toBe('VentoApp');
|
expect(t('app_title', 'pt-BR')).toBe('BrainWind v1.0');
|
||||||
|
expect(t('app_title', 'en-US')).toBe('BrainWind v1.0');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Fallback para pt-BR quando locale é inválido', () => {
|
it('Fallback para pt-BR quando locale é inválido', () => {
|
||||||
@@ -151,7 +153,7 @@ describe('M9.8 — Persistência localStorage (via polyfill)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('M9.8 — Chaves principais em pt-BR', () => {
|
describe('M9.8 — Chaves principais em pt-BR', () => {
|
||||||
it('app_title = VentoApp', () => expect(t('app_title', 'pt-BR')).toBe('VentoApp'));
|
it('app_title = BrainWind v1.0', () => expect(t('app_title', 'pt-BR')).toBe('BrainWind v1.0'));
|
||||||
it('nav_home = Início', () => expect(t('nav_home', 'pt-BR')).toBe('Início'));
|
it('nav_home = Início', () => expect(t('nav_home', 'pt-BR')).toBe('Início'));
|
||||||
it('nav_warehouse = Galpão', () => expect(t('nav_warehouse', 'pt-BR')).toBe('Galpão'));
|
it('nav_warehouse = Galpão', () => expect(t('nav_warehouse', 'pt-BR')).toBe('Galpão'));
|
||||||
it('nav_cylinder = Cilindro', () => expect(t('nav_cylinder', 'pt-BR')).toBe('Cilindro'));
|
it('nav_cylinder = Cilindro', () => expect(t('nav_cylinder', 'pt-BR')).toBe('Cilindro'));
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { Document, Page, Text, View, StyleSheet, pdf } from '@react-pdf/renderer';
|
||||||
|
import type { AuditReport, ModuleAuditResult, ScenarioResult, CheckStatus } from './types';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
page: {
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: 40,
|
||||||
|
fontSize: 10,
|
||||||
|
fontFamily: 'Helvetica',
|
||||||
|
color: '#333',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
marginBottom: 20,
|
||||||
|
borderBottom: '2pt solid #6b21a8',
|
||||||
|
paddingBottom: 10,
|
||||||
|
},
|
||||||
|
title: { fontSize: 20, fontWeight: 'bold', color: '#6b21a8' },
|
||||||
|
subtitle: { fontSize: 10, color: '#666', marginTop: 4 },
|
||||||
|
section: { marginTop: 15, marginBottom: 10 },
|
||||||
|
sectionTitle: { fontSize: 14, fontWeight: 'bold', marginBottom: 8, color: '#111' },
|
||||||
|
summaryRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
summaryLabel: { fontWeight: 'bold', width: 200 },
|
||||||
|
summaryValue: { flex: 1 },
|
||||||
|
moduleCard: {
|
||||||
|
marginTop: 12,
|
||||||
|
padding: 10,
|
||||||
|
border: '1pt solid #ddd',
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
moduleTitle: { fontSize: 12, fontWeight: 'bold', marginBottom: 6, color: '#6b21a8' },
|
||||||
|
moduleStats: { fontSize: 9, color: '#555', marginBottom: 4 },
|
||||||
|
scenarioItem: {
|
||||||
|
marginTop: 6,
|
||||||
|
padding: 6,
|
||||||
|
backgroundColor: '#f9fafb',
|
||||||
|
borderRadius: 3,
|
||||||
|
},
|
||||||
|
scenarioId: { fontSize: 9, fontWeight: 'bold', marginBottom: 3, color: '#111' },
|
||||||
|
checkRow: { flexDirection: 'row', marginBottom: 2, fontSize: 8 },
|
||||||
|
statusBadge: { width: 40, fontWeight: 'bold' },
|
||||||
|
checkLabel: { width: 120 },
|
||||||
|
checkValue: { flex: 1 },
|
||||||
|
footer: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 30,
|
||||||
|
left: 40,
|
||||||
|
right: 40,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#999',
|
||||||
|
fontSize: 8,
|
||||||
|
borderTop: '1pt solid #eaeaea',
|
||||||
|
paddingTop: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function statusColor(s: CheckStatus): string {
|
||||||
|
switch (s) {
|
||||||
|
case 'PASS': return '#16a34a';
|
||||||
|
case 'WARN': return '#d97706';
|
||||||
|
case 'FAIL': return '#dc2626';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReportPDFProps {
|
||||||
|
report: AuditReport;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuditReportDocument({ report }: ReportPDFProps) {
|
||||||
|
return (
|
||||||
|
<Document>
|
||||||
|
<Page size="A4" style={styles.page}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.title}>Relatório de Auditoria — BrainWind</Text>
|
||||||
|
<Text style={styles.subtitle}>
|
||||||
|
Provedor: {report.provider} ({report.model}) | {new Date(report.timestamp).toLocaleString('pt-BR')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>Resumo</Text>
|
||||||
|
<View style={styles.summaryRow}>
|
||||||
|
<Text style={styles.summaryLabel}>Total de Cenários:</Text>
|
||||||
|
<Text style={styles.summaryValue}>{report.totalScenarios}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.summaryRow}>
|
||||||
|
<Text style={[styles.summaryLabel, { color: '#16a34a' }]}>Pass:</Text>
|
||||||
|
<Text style={styles.summaryValue}>{report.totalPassed}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.summaryRow}>
|
||||||
|
<Text style={[styles.summaryLabel, { color: '#d97706' }]}>Warn:</Text>
|
||||||
|
<Text style={styles.summaryValue}>{report.totalWarnings}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.summaryRow}>
|
||||||
|
<Text style={[styles.summaryLabel, { color: '#dc2626' }]}>Fail:</Text>
|
||||||
|
<Text style={styles.summaryValue}>{report.totalFailed}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{report.modules.map((mod: ModuleAuditResult) => (
|
||||||
|
<View key={mod.module} wrap={false} style={styles.moduleCard}>
|
||||||
|
<Text style={styles.moduleTitle}>
|
||||||
|
{mod.moduleLabel} — {mod.passed}/{mod.totalScenarios} (W:{mod.warnings} F:{mod.failed})
|
||||||
|
</Text>
|
||||||
|
{mod.scenarios.map((sc: ScenarioResult) => (
|
||||||
|
<View key={sc.scenarioId} style={styles.scenarioItem}>
|
||||||
|
<Text style={styles.scenarioId}>
|
||||||
|
{sc.scenarioId} — {sc.verdict === 'PASS' ? '✓' : sc.verdict === 'WARN' ? '⚠' : '✗'} {sc.summary}
|
||||||
|
</Text>
|
||||||
|
{sc.checks.map((chk, i) => (
|
||||||
|
<View key={i} style={styles.checkRow}>
|
||||||
|
<Text style={[styles.statusBadge, { color: statusColor(chk.status) }]}>{chk.status}</Text>
|
||||||
|
<Text style={styles.checkLabel}>{chk.name}</Text>
|
||||||
|
<Text style={styles.checkValue}>{chk.actual} ({chk.expected})</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Text style={styles.footer}>
|
||||||
|
Gerado por BrainWind v1.0 — Ferramenta de Auditoria NBR 6123:2023
|
||||||
|
</Text>
|
||||||
|
</Page>
|
||||||
|
</Document>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportAuditReportToPDF(report: AuditReport): Promise<void> {
|
||||||
|
const blob = await pdf(<AuditReportDocument report={report} />).toBlob();
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.setAttribute('download', `auditoria-${report.id}.pdf`);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
export const INVARIANTES_NBR6123 = `
|
||||||
|
## Checklist de Verificação — NBR 6123:2023
|
||||||
|
|
||||||
|
Use esta lista para auditar os resultados. Cada regra é OBRIGATÓRIA.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GLOBAIS
|
||||||
|
|
||||||
|
1. **q = 0.613 × Vk² / 1000** — fórmula exata, sem aproximação
|
||||||
|
- Calc: q_N = 0.613 × Vk²; q_kN = q_N / 1000
|
||||||
|
- Tolerância: ±0.0001 kN/m²
|
||||||
|
|
||||||
|
2. **Vk = V0 × S1 × S2 × S3** — fórmula exata
|
||||||
|
- Verificar: Vk / (V0 × S1 × S2 × S3) ≈ 1.0
|
||||||
|
- Tolerância: ±0.01 m/s no arredondamento
|
||||||
|
|
||||||
|
3. **S2 é não-decrescente com z** — monotonicidade
|
||||||
|
- Para z1 < z2: S2(z1) <= S2(z2)
|
||||||
|
- Exceto se z1 < 5m (nesse caso S2(5m) é o piso)
|
||||||
|
|
||||||
|
4. **S2 saturado em z >= z_g**
|
||||||
|
- Cat I: z_g=250m, Cat II: z_g=300m, Cat III: z_g=350m
|
||||||
|
- Cat IV: z_g=420m, Cat V: z_g=500m
|
||||||
|
|
||||||
|
5. **Cpi ∈ [-0.9, +0.9]** — limite normativo SEMPRE
|
||||||
|
- Qualquer valor fora deste intervalo = FAIL
|
||||||
|
|
||||||
|
6. **Classe estrutural correta**
|
||||||
|
- dim <= 20 → Classe A
|
||||||
|
- 20 < dim <= 50 → Classe B
|
||||||
|
- dim > 50 → Classe C
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GALPÃO (paredes e telhados)
|
||||||
|
|
||||||
|
7. **Pressão em cada zona = q × (Cpe − Cpi)**
|
||||||
|
- Verificar: p(Cpe) = q × (Cpe − Cpi)
|
||||||
|
- Cpe vem da Tabela 6 (paredes) ou Tabela 7 (telhados)
|
||||||
|
|
||||||
|
8. **Sinal da zona C (sotavento)** — tipicamente positiva
|
||||||
|
- Cpe zona C > 0 (pressão) quando α=0°
|
||||||
|
- Exceção: quando Cpi é suficientemente negativo
|
||||||
|
|
||||||
|
9. **Sinal das zonas E/F (barlavento do telhado)** — tipicamente negativas
|
||||||
|
- Cpe zona E < 0 (sucção) na maioria dos casos
|
||||||
|
- Verificar que não é > 0 sem motivo
|
||||||
|
|
||||||
|
10. **Força de atrito — aplica SOMENTE quando l0/h > 4 OU l0/b > 4**
|
||||||
|
- Se condição falsa: força de atrito deve ser ZERO
|
||||||
|
- Cf: smooth=0.01, ribbed=0.02, rectangular=0.04
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CILINDRO
|
||||||
|
|
||||||
|
11. **Re = 70.000 × Vk × d** — fórmula exata
|
||||||
|
- Verificar que o Reynolds está correto para o Vk e d fornecidos
|
||||||
|
|
||||||
|
12. **Cpe(0°) > 0 (barlavento)** — pressão sempre positiva em θ=0°
|
||||||
|
- exceto cilindro muito curto (h/d < 0.5) pode variar
|
||||||
|
|
||||||
|
13. **Cpe(90°) < 0 (sucção)** — zona de separação
|
||||||
|
- Qualquer valor positivo em 90° é suspeito
|
||||||
|
|
||||||
|
14. **Cpi para topo aberto: -0.8 se h/d >= 0.3, -0.5 caso contrário**
|
||||||
|
- h/d = altura / diâmetro
|
||||||
|
|
||||||
|
15. **Regime supercrítico: Re > 400.000**
|
||||||
|
- Se Re < 400.000: tabela de subcrítico deve ser usada
|
||||||
|
- A Tabela 13 é válida para Re > 400.000
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ABÓBADA
|
||||||
|
|
||||||
|
16. **f/l determina regime correto**
|
||||||
|
- Regime laminar (baixa turbulência):Tabelas 15-17
|
||||||
|
- Regime turbulento (alta turbulência): Tabelas 18-20
|
||||||
|
|
||||||
|
17. **Cpe negativo na maioria das zonas de sucção**
|
||||||
|
- Abóbadas tipicamente geram sucção na superfície superior
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CÚPULA
|
||||||
|
|
||||||
|
18. **f/d razão limitada**
|
||||||
|
- Cúpulas com f/d muito alto (>0.5) são raras
|
||||||
|
- Verificar que f/d está em faixa coberta pelas tabelas
|
||||||
|
|
||||||
|
19. **Força de sustentação (lift) existe para cúpulas no solo**
|
||||||
|
- Cs (coeficiente de sustentação) deve ser calculado
|
||||||
|
- Cúpulas em parede cilíndrica têm Csi = 0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### MUROS E PLACAS
|
||||||
|
|
||||||
|
20. **Cf entre 0.8 e 2.0 para placas planas**
|
||||||
|
- Fc = Cf × q × A (força total)
|
||||||
|
- Verificar: Cf está na faixa válida da Figura 7
|
||||||
|
|
||||||
|
21. **Força aplicada no centro de pressão**
|
||||||
|
- Altura do ponto de aplicação conforme tipo de placa
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### COBERTURAS ISOLADAS
|
||||||
|
|
||||||
|
22. **Cpe para telhado isolado em shed/gable**
|
||||||
|
- Diferente de telhado conectado a paredes
|
||||||
|
- Verificar que usa Tabela 24 ou 25, não Tabela 7
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BARRAS
|
||||||
|
|
||||||
|
23. **Ca decresce com phi crescente (barras planas)**
|
||||||
|
- phi=0.05 → Ca≈3.6
|
||||||
|
- phi=1.0 → Ca≈1.4
|
||||||
|
|
||||||
|
24. **Barras circulares: Ca depende do regime de Reynolds**
|
||||||
|
- Subcrítico: Ca≈1.2
|
||||||
|
- Supercrítico: Ca≈0.6
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### PONTES
|
||||||
|
|
||||||
|
25. **Pse < 0.04 → Classe 1; 0.04-1.0 → Classe 2; >1.0 → Classe 3**
|
||||||
|
- Pse = ρ × V_it² / (m × fv²) — forma simplificada
|
||||||
|
- ρ = 1.226 kg/m³
|
||||||
|
|
||||||
|
26. **V_it = 0.65 × V0 × S1 × b × (z/10)^p**
|
||||||
|
- b e p vêm da Tabela 35 por categoria
|
||||||
|
|
||||||
|
27. **Cx diminui com B/Heg crescente**
|
||||||
|
- Cx = 0.21 + 1.83 × (B/Heg)^(-1.13) para 1 <= B/Heg <= 27
|
||||||
|
- Cx máximo = 2.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TORRES
|
||||||
|
|
||||||
|
28. **Ca para torre quadrada de barras planas**
|
||||||
|
- Ca diminui com phi aumentando (mais sólido = menos arrasto por shielding)
|
||||||
|
- K_alpha: 1.0 para α<=12.5°, sobe para ~1.16 em α=45°
|
||||||
|
|
||||||
|
29. **Ca para torre triangular**
|
||||||
|
- K_alpha = 1.0 sempre (não varia com ângulo)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### DINÂMICA
|
||||||
|
|
||||||
|
30. **Strouhal: St ≈ 0.20 para circular, St ≈ 0.11 para quadrado**
|
||||||
|
- Usado para calcular velocidade crítica Vcr = f × D / St
|
||||||
|
|
||||||
|
31. **Scruton: Sc = 2 × m × δs / (ρ × D²)**
|
||||||
|
- Verificar que está acima do limite crítico para supressão de vórtices
|
||||||
|
|
||||||
|
32. **Conforto: a_lim = 0.01 × ka × f^1.124**
|
||||||
|
- ka: 6.12 (comercial), 4.06 (residencial)
|
||||||
|
- Válido SOMENTE para f ∈ [0.06, 1.00] Hz
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### SINAL CONVENCIONAL
|
||||||
|
|
||||||
|
33. **p > 0 → pressão (empurrando a superfície para dentro)**
|
||||||
|
34. **p < 0 → sucção (puxando a superfície para fora)**
|
||||||
|
35. **Nunca omitir Cpi no cálculo de pressão**
|
||||||
|
- p = q × (Cpe − Cpi) é a fórmula completa
|
||||||
|
`;
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
export const NORM_REFERENCE = `
|
||||||
|
# NBR 6123:2023 — Referência Completa para Auditoria
|
||||||
|
|
||||||
|
Este documento é uma extração autoritativa dos dados da norma NBR 6123:2023,
|
||||||
|
implementados no código-fonte do VentoApp. Use-o como referência para verificar
|
||||||
|
se os cálculos do aplicativo estão corretos.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Seção 4 — Velocidade Básica e Pressão Dinâmica
|
||||||
|
|
||||||
|
### Fórmulas fundamentais
|
||||||
|
|
||||||
|
- **Velocidade característica**: Vk = V0 × S1 × S2 × S3 (m/s)
|
||||||
|
- **Pressão dinâmica**: q = 0.613 × Vk² / 1000 (kN/m²)
|
||||||
|
- **Massa específica do ar**: ρ = 1.226 kg/m³
|
||||||
|
|
||||||
|
### Símbolos
|
||||||
|
|
||||||
|
| Símbolo | Significado |
|
||||||
|
|---------|-------------|
|
||||||
|
| V0 | Velocidade básica do vento (m/s) — Figura 1 ou Anexo C |
|
||||||
|
| S1 | Fator topográfico (Seção 5.2) |
|
||||||
|
| S2 | Fator de rugosidade do terreno, tamanho da edificação e altura (Seção 5.3) |
|
||||||
|
| S3 | Fator estatístico (Seção 5.4) |
|
||||||
|
| Vk | Velocidade característica do vento (m/s) |
|
||||||
|
| q | Pressão dinâmica (kN/m²) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Seção 5.3 — Fator S2 (Tabela 3)
|
||||||
|
|
||||||
|
### z_g — Altura da camada limite atmosférica (m)
|
||||||
|
|
||||||
|
| Categoria | I | II | III | IV | V |
|
||||||
|
|-----------|-----|-----|------|-----|-----|
|
||||||
|
| z_g (m) | 250 | 300 | 350 | 420 | 500 |
|
||||||
|
|
||||||
|
### Tabela 1 — Parâmetros b, p, Fr
|
||||||
|
|
||||||
|
| Categoria | Classe A | Classe B | Classe C |
|
||||||
|
|-----------|----------|----------|----------|
|
||||||
|
| **I** | b=1.10, p=0.06, Fr=1.00 | b=1.11, p=0.065, Fr=0.98 | b=1.12, p=0.07, Fr=0.95 |
|
||||||
|
| **II** | b=1.00, p=0.085, Fr=1.00 | b=1.00, p=0.09, Fr=0.98 | b=1.00, p=0.10, Fr=0.95 |
|
||||||
|
| **III** | b=0.94, p=0.10, Fr=1.00 | b=0.94, p=0.105, Fr=0.98 | b=0.93, p=0.115, Fr=0.95 |
|
||||||
|
| **IV** | b=0.86, p=0.12, Fr=1.00 | b=0.85, p=0.125, Fr=0.98 | b=0.84, p=0.135, Fr=0.95 |
|
||||||
|
| **V** | b=0.74, p=0.15, Fr=1.00 | b=0.73, p=0.16, Fr=0.98 | b=0.71, p=0.175, Fr=0.95 |
|
||||||
|
|
||||||
|
### Tabela 3 — Fator S2 (extrato das alturas mais importantes)
|
||||||
|
|
||||||
|
| z(m) | I-A | I-B | I-C | II-A | II-B | II-C | III-A | III-B | III-C | IV-A | IV-B | IV-C | V-A | V-B | V-C |
|
||||||
|
|-------|------|------|------|-------|-------|-------|--------|--------|--------|-------|-------|-------|------|------|------|
|
||||||
|
| 5 | 1.06 | 1.04 | 1.01 | 0.94 | 0.92 | 0.89 | 0.83 | 0.81 | 0.78 | 0.74 | 0.72 | 0.69 | 0.63 | 0.61 | 0.58 |
|
||||||
|
| 10 | 1.10 | 1.09 | 1.06 | 1.00 | 0.98 | 0.95 | 0.90 | 0.88 | 0.85 | 0.82 | 0.80 | 0.77 | 0.71 | 0.69 | 0.66 |
|
||||||
|
| 15 | 1.13 | 1.12 | 1.10 | 1.04 | 1.02 | 0.99 | 0.94 | 0.93 | 0.90 | 0.87 | 0.85 | 0.82 | 0.77 | 0.75 | 0.72 |
|
||||||
|
| 20 | 1.15 | 1.14 | 1.12 | 1.06 | 1.04 | 1.01 | 0.98 | 0.96 | 0.93 | 0.91 | 0.89 | 0.86 | 0.81 | 0.79 | 0.76 |
|
||||||
|
| 30 | 1.17 | 1.17 | 1.15 | 1.10 | 1.08 | 1.05 | 1.03 | 1.01 | 0.99 | 0.96 | 0.94 | 0.92 | 0.87 | 0.85 | 0.82 |
|
||||||
|
| 40 | 1.20 | 1.20 | 1.18 | 1.13 | 1.11 | 1.08 | 1.06 | 1.05 | 1.02 | 1.00 | 0.98 | 0.96 | 0.91 | 0.89 | 0.87 |
|
||||||
|
| 50 | 1.21 | 1.21 | 1.20 | 1.15 | 1.13 | 1.10 | 1.09 | 1.08 | 1.05 | 1.03 | 1.01 | 0.99 | 0.94 | 0.92 | 0.90 |
|
||||||
|
| 60 | 1.22 | 1.22 | 1.21 | 1.16 | 1.14 | 1.12 | 1.12 | 1.10 | 1.08 | 1.06 | 1.04 | 1.02 | 0.97 | 0.95 | 0.93 |
|
||||||
|
| 80 | 1.25 | 1.24 | 1.23 | 1.19 | 1.17 | 1.15 | 1.16 | 1.14 | 1.12 | 1.10 | 1.08 | 1.06 | 1.01 | 0.99 | 0.97 |
|
||||||
|
| 100 | 1.26 | 1.26 | 1.25 | 1.22 | 1.20 | 1.18 | 1.19 | 1.17 | 1.15 | 1.13 | 1.11 | 1.09 | 1.04 | 1.03 | 1.01 |
|
||||||
|
| 120 | 1.28 | 1.28 | 1.27 | 1.23 | 1.21 | 1.19 | 1.21 | 1.20 | 1.18 | 1.15 | 1.14 | 1.12 | 1.07 | 1.06 | 1.04 |
|
||||||
|
| 140 | 1.29 | 1.29 | 1.28 | 1.24 | 1.22 | 1.20 | 1.22 | 1.21 | 1.19 | 1.17 | 1.16 | 1.14 | 1.09 | 1.08 | 1.06 |
|
||||||
|
| 160 | 1.30 | 1.30 | 1.29 | 1.25 | 1.23 | 1.21 | 1.23 | 1.22 | 1.20 | 1.18 | 1.17 | 1.15 | 1.11 | 1.10 | 1.08 |
|
||||||
|
| 180 | 1.31 | 1.31 | 1.30 | 1.26 | 1.24 | 1.22 | 1.24 | 1.23 | 1.21 | 1.19 | 1.18 | 1.16 | 1.12 | 1.11 | 1.09 |
|
||||||
|
| 200 | 1.32 | 1.32 | 1.31 | 1.27 | 1.25 | 1.23 | 1.25 | 1.24 | 1.22 | 1.20 | 1.19 | 1.17 | 1.14 | 1.13 | 1.11 |
|
||||||
|
| 250 | 1.34 | 1.34 | 1.33 | 1.29 | 1.27 | 1.25 | 1.28 | 1.27 | 1.25 | 1.23 | 1.22 | 1.20 | 1.17 | 1.16 | 1.14 |
|
||||||
|
| 300 | 1.34 | 1.34 | 1.33 | 1.30 | 1.28 | 1.26 | 1.30 | 1.29 | 1.27 | 1.25 | 1.24 | 1.22 | 1.20 | 1.19 | 1.17 |
|
||||||
|
| 350 | 1.34 | 1.34 | 1.33 | 1.31 | 1.29 | 1.27 | 1.31 | 1.30 | 1.28 | 1.27 | 1.26 | 1.24 | 1.22 | 1.21 | 1.19 |
|
||||||
|
| 400 | 1.34 | 1.34 | 1.34 | 1.32 | 1.30 | 1.28 | 1.32 | 1.31 | 1.29 | 1.28 | 1.27 | 1.25 | 1.24 | 1.23 | 1.21 |
|
||||||
|
| 420 | 1.35 | 1.35 | 1.35 | 1.32 | 1.30 | 1.28 | 1.33 | 1.32 | 1.30 | 1.29 | 1.28 | 1.26 | 1.25 | 1.24 | 1.22 |
|
||||||
|
| 450 | 1.35 | 1.35 | 1.35 | 1.33 | 1.31 | 1.29 | 1.34 | 1.33 | 1.31 | 1.30 | 1.29 | 1.27 | 1.26 | 1.25 | 1.23 |
|
||||||
|
| 500 | 1.35 | 1.35 | 1.35 | 1.34 | 1.32 | 1.30 | 1.35 | 1.34 | 1.32 | 1.32 | 1.31 | 1.29 | 1.28 | 1.27 | 1.25 |
|
||||||
|
|
||||||
|
**Nota**: z mínimo = 5 m (piso normativo). S2 é saturado para z >= z_g.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Seção 5.4 — Fator S3 (Tabela 4)
|
||||||
|
|
||||||
|
| Grupo | S3 | Descrição |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| 1 | 1.11 | Hospitais, quartéis de bombeiro, centrais de controle, pontes, substâncias perigosas |
|
||||||
|
| 2 | 1.06 | Edificações com risco à vida (aglomerações >200 pessoas) |
|
||||||
|
| 3 | 1.00 | Edificações residences, hotéis, comércio, indústrias (padrão) |
|
||||||
|
| 4 | 0.95 | Depósitos, silos, sem circulação de pessoas |
|
||||||
|
| 5 | 0.83 | Construções temporárias (máx 2 anos) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Seção 5.3.2 — Classe Estrutural
|
||||||
|
|
||||||
|
| Maior dimensão | Classe |
|
||||||
|
|----------------|--------|
|
||||||
|
| dim <= 20 m | A |
|
||||||
|
| 20 < dim <= 50 m | B |
|
||||||
|
| dim > 50 m | C |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Seção 6.1.1 — Coeficientes de Pressão Externa em Paredes (Tabela 6)
|
||||||
|
|
||||||
|
### α = 0° (vento perpendicular à largura b)
|
||||||
|
|
||||||
|
| h/b | a/b | A1/B1 | A2/B2 | C (sotavento) | D |
|
||||||
|
|------|------|--------|--------|----------------|----|
|
||||||
|
| <=0.5 | 1.0–1.5 | -0.8 | -0.5 | +0.7 | -0.4 |
|
||||||
|
| <=0.5 | >=2.0 | -0.8 | -0.4 | +0.7 | -0.3 |
|
||||||
|
| 0.5–1.5 | 1.0–1.5 | -0.9 | -0.5 | +0.7 | -0.5 |
|
||||||
|
| 0.5–1.5 | >=2.0 | -0.9 | -0.4 | +0.7 | -0.3 |
|
||||||
|
| >1.5 | 1.0–1.5 | -1.0 | -0.6 | +0.8 | -0.6 |
|
||||||
|
| >1.5 | >=2.0 | -1.0 | -0.5 | +0.8 | -0.3 |
|
||||||
|
|
||||||
|
### α = 90° (vento perpendicular ao comprimento a)
|
||||||
|
|
||||||
|
| h/b | a/b | A (barlavento) | B (lateral) | C1/D1 | C2/D2 |
|
||||||
|
|------|------|--------|--------|----------------|----|
|
||||||
|
| <=0.5 | 1.0–1.5 | +0.7 | -0.4 | -0.8 | -0.4 |
|
||||||
|
| <=0.5 | >=2.0 | +0.7 | -0.5 | -0.9 | -0.5 |
|
||||||
|
| 0.5–1.5 | 1.0–1.5 | +0.7 | -0.5 | -0.9 | -0.5 |
|
||||||
|
| 0.5–1.5 | >=2.0 | +0.7 | -0.6 | -0.9 | -0.5 |
|
||||||
|
| >1.5 | 1.0–1.5 | +0.8 | -0.6 | -1.0 | -0.6 |
|
||||||
|
| >1.5 | >=2.0 | +0.8 | -0.6 | -1.0 | -0.6 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Seção 6.2.1 — Cilindros de Seção Circular (Tabela 13)
|
||||||
|
|
||||||
|
### Reynolds: Re = 70.000 × Vk × d
|
||||||
|
|
||||||
|
### Tabela 13 — Cpe por ângulo, h/d e superfície
|
||||||
|
|
||||||
|
Ângulos: 0°, 10°, 20°, 30°, 40°, 50°, 60°, 70°, 80°, 90°, 100°, 120°, 140°, 160°, 180°
|
||||||
|
|
||||||
|
**Superfície RUUGOSA (h/d = 10):**
|
||||||
|
0°=+1.0, 10°=+0.9, 20°=+0.7, 30°=+0.4, 40°=0, 50°=-0.5, 60°=-0.95, 70°=-1.25, 80°=-1.2, 90°=-1.0, 100°=-0.8, 120°=-0.5, 140°=-0.4, 160°=-0.4, 180°=-0.4
|
||||||
|
|
||||||
|
**Superfície RUUGOSA (h/d ≤ 2.5):**
|
||||||
|
0°=+1.0, 10°=+0.9, 20°=+0.7, 30°=+0.4, 40°=0, 50°=-0.4, 60°=-0.8, 70°=-1.1, 80°=-1.05, 90°=-0.85, 100°=-0.65, 120°=-0.35, 140°=-0.3, 160°=-0.3, 180°=-0.3
|
||||||
|
|
||||||
|
**Superfície LISSA (h/d = 10):**
|
||||||
|
0°=+1.0, 10°=+0.9, 20°=+0.7, 30°=+0.35, 40°=0, 50°=-0.7, 60°=-1.2, 70°=-1.4, 80°=-1.45, 90°=-1.4, 100°=-1.1, 120°=-0.6, 140°=-0.35, 160°=-0.35, 180°=-0.35
|
||||||
|
|
||||||
|
**Superfície LISSA (h/d ≤ 2.5):**
|
||||||
|
0°=+1.0, 10°=+0.9, 20°=+0.7, 30°=+0.35, 40°=0, 50°=-0.5, 60°=-1.05, 70°=-1.25, 80°=-1.3, 90°=-1.2, 100°=-0.85, 120°=-0.4, 140°=-0.25, 160°=-0.25, 180°=-0.25
|
||||||
|
|
||||||
|
### Regime Supercrítico: Re > 400.000
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Seção 6.3 — Pressão Interna (Cpi)
|
||||||
|
|
||||||
|
### Casos simplificados (Tabela)
|
||||||
|
|
||||||
|
| Caso de permeabilidade | Cpi |
|
||||||
|
|------------------------|------|
|
||||||
|
| Duas faces opostas igualmente permeáveis, outras duas impermeáveis (α=0°) | +0.2 |
|
||||||
|
| Duas faces opostas igualmente permeáveis, outras duas impermeáveis (α=90°) | -0.3 |
|
||||||
|
| Quatro faces igualmente permeáveis | 0.0 |
|
||||||
|
| Permeabilidade dominante na face de barlavento | +0.1 a +0.8 (conforme razão de áreas) |
|
||||||
|
| Permeabilidade dominante na face de sotavento/lateral | -0.3 a -0.8 |
|
||||||
|
| Faces estanques (sem aberturas) | -0.2 ou +0.2 |
|
||||||
|
|
||||||
|
### Cilindro com topo aberto
|
||||||
|
|
||||||
|
| h/d | Cpi |
|
||||||
|
|------|------|
|
||||||
|
| h/d < 0.3 | -0.5 |
|
||||||
|
| h/d >= 0.3 | -0.8 |
|
||||||
|
|
||||||
|
### Limite normativo: Cpi ∈ [-0.9, +0.9]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Seção 6.1.2 — Coeficiente de Arrasto (Ca) para Edificações
|
||||||
|
|
||||||
|
Para edificações com faces retangulares, o Ca é determinado pela Figura 4 (baixa turbulência) ou Figura 5 (alta turbulência), em função de h/l1 e l1/l2.
|
||||||
|
|
||||||
|
- h/l1 = altura / menor dimensão em planta
|
||||||
|
- l1/l2 = menor dimensão / maior dimensão em planta
|
||||||
|
|
||||||
|
### Faixas típicas de Ca (baixa turbulência):
|
||||||
|
|
||||||
|
| h/l1 | l1/l2=0.4 | l1/l2=0.7 | l1/l2=1.0 |
|
||||||
|
|-------|-----------|-----------|-----------|
|
||||||
|
| 0.25 | ~1.1 | ~1.15 | ~1.2 |
|
||||||
|
| 1.0 | ~1.2 | ~1.3 | ~1.4 |
|
||||||
|
| 2.0 | ~1.3 | ~1.4 | ~1.5 |
|
||||||
|
| 4.0 | ~1.4 | ~1.55 | ~1.65 |
|
||||||
|
| 8.0 | ~1.5 | ~1.65 | ~1.75 |
|
||||||
|
|
||||||
|
### Alta turbulência (requer: profundidade/largura > 1/3 E altura <= 2 × altura média do entorno)
|
||||||
|
|
||||||
|
Ca é menor que na baixa turbulência para a mesma geometria.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Seção 6.1.5 — Força de Atrito
|
||||||
|
|
||||||
|
Aplica-se quando l0/h > 4 OU l0/b > 4.
|
||||||
|
|
||||||
|
| Tipo de superfície | Cf |
|
||||||
|
|---------------------|------|
|
||||||
|
| Lisa (metal, vidro) | 0.01 |
|
||||||
|
| Nervurada/ribbed (ondulações) | 0.02 |
|
||||||
|
| Nervuras retangulares | 0.04 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Seção 8.1-8.2 — Barras Prismáticas (Tabela 26-27)
|
||||||
|
|
||||||
|
### Barras de face plana (Tabela 26) — Ca como função de phi (solidez)
|
||||||
|
|
||||||
|
| phi (φ) | Ca |
|
||||||
|
|----------|------|
|
||||||
|
| 0.05 | ~3.6 |
|
||||||
|
| 0.10 | ~3.0 |
|
||||||
|
| 0.20 | ~2.4 |
|
||||||
|
| 0.30 | ~2.0 |
|
||||||
|
| 0.40 | ~1.8 |
|
||||||
|
| 0.50 | ~1.7 |
|
||||||
|
| 0.60 | ~1.6 |
|
||||||
|
| 0.70 | ~1.5 |
|
||||||
|
| 0.80 | ~1.45 |
|
||||||
|
| 0.90 | ~1.4 |
|
||||||
|
| 1.00 | ~1.4 |
|
||||||
|
|
||||||
|
### Barras circulares (Tabela 27)
|
||||||
|
|
||||||
|
- Subcrítico (Re < 3×10⁵): Ca ≈ 1.2
|
||||||
|
- Transicional: Ca ≈ 0.7
|
||||||
|
- Supercrítico (Re > 3×10⁵): Ca ≈ 0.6
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Seção 8.5 — Torres Treliçadas (Tabela 30)
|
||||||
|
|
||||||
|
- Torre de seção quadrada: Kα = 1.0 para α<=12.5°, 1.08 para α<=45°
|
||||||
|
- Torre de seção triangular: Kα = 1.0 sempre
|
||||||
|
- Força: F = Ca × Kα × q × A_face × φ
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Seção 11 — Pontes
|
||||||
|
|
||||||
|
### Tabela 35 — Parâmetros b, p para V_it = 0.65 × V0 × S1 × b × (z/10)^p
|
||||||
|
|
||||||
|
| Categoria | b | p |
|
||||||
|
|-----------|---|------|
|
||||||
|
| I | 1.25 | 0.10 |
|
||||||
|
| II | 1.00 | 0.16 |
|
||||||
|
| III | 0.85 | 0.20 |
|
||||||
|
| IV | 0.68 | 0.25 |
|
||||||
|
| V | 0.44 | 0.35 |
|
||||||
|
|
||||||
|
### Classificação de pontes (Parâmetro Pse)
|
||||||
|
|
||||||
|
| Pse | Classe | Descrição |
|
||||||
|
|-----|--------|-----------|
|
||||||
|
| Pse < 0.04 | 1 | Efeitos dinâmicos podem ser desprezados |
|
||||||
|
| 0.04 <= Pse <= 1.0 | 2 | Efeitos dinâmicos devem ser avaliados |
|
||||||
|
| Pse > 1.0 | 3 | Análise aeroelástica obrigatória |
|
||||||
|
|
||||||
|
### Tabela 36 — Taxa de amortecimento ξ (%)
|
||||||
|
|
||||||
|
| Material | Detalhe | ξ (%) |
|
||||||
|
|----------|---------|-------|
|
||||||
|
| Aço | Chapas soldadas + pavimento asfáltico | 0.8 |
|
||||||
|
| Aço | Chapas soldadas + concreto | 1.0 |
|
||||||
|
| Aço | Parafusadas (ligação por atrito) | 1.5 |
|
||||||
|
| Aço | Rebitadas | 1.5 |
|
||||||
|
| Concreto armado | — | 2.5 |
|
||||||
|
| Concreto protendido | Protensão completa | 2.0 |
|
||||||
|
| Concreto protendido | Protensão parcial | 2.5 |
|
||||||
|
| Mistas aço-concreto | Vigas | 1.8 |
|
||||||
|
| Mistas aço-concreto | Vigas treliçadas | 2.0 |
|
||||||
|
| Alumínio | — | 2.0 |
|
||||||
|
| Madeira | — | 8.0 |
|
||||||
|
| Compósito | Matriz polimérica + fibras | 6.0 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Seção 9-10 — Dinâmica
|
||||||
|
|
||||||
|
### Strouhal Number (Tabela 33)
|
||||||
|
|
||||||
|
- Seção circular: St ≈ 0.20
|
||||||
|
- Seção quadrada: St ≈ 0.11
|
||||||
|
|
||||||
|
### Scruton Number
|
||||||
|
|
||||||
|
Sc = 2 × m × δs / (ρ × D²)
|
||||||
|
onde δs = amortecimento estrutural (log decrement = 2π × ξ)
|
||||||
|
|
||||||
|
### Velocidade crítica de vórtices
|
||||||
|
|
||||||
|
Vcr = f × D / St
|
||||||
|
|
||||||
|
### Conforto humano (ISO 10137)
|
||||||
|
|
||||||
|
a_lim = 0.01 × ka × f^1.124
|
||||||
|
|
||||||
|
| Uso | ka |
|
||||||
|
|-----|-----|
|
||||||
|
| Comercial/escritórios | 6.12 |
|
||||||
|
| Residencial | 4.06 |
|
||||||
|
|
||||||
|
Faixa de frequência válida: 0.06 Hz <= f <= 1.00 Hz
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Seção 7.2 — Coberturas Isoladas (Tabelas 24-25)
|
||||||
|
|
||||||
|
### Cobertura em shed (uma água)
|
||||||
|
|
||||||
|
- Cpe barlavento e sotavento conforme ângulo θ
|
||||||
|
- Força de sustentação conforme área projetada
|
||||||
|
|
||||||
|
### Cobertura em duas águas (gable)
|
||||||
|
|
||||||
|
- Cpe em barlavento e sotavento conforme θ
|
||||||
|
- Força de sustentação upwards quando configurado
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anexo A — S2 normalizado para intervalo de tempo
|
||||||
|
|
||||||
|
S2(t) = S2(3s) × (t/3)^(-0.17) para t em segundos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anexo B — Fator estatístico S3 por probabilidade e vida útil
|
||||||
|
|
||||||
|
S3 = 0.54 × (-ln(1 - Pm))^(-1/7) × m^(1/7)
|
||||||
|
|
||||||
|
onde:
|
||||||
|
- Pm = probabilidade de excedência no período de vida útil
|
||||||
|
- m = vida útil em anos
|
||||||
|
|
||||||
|
| Pm | -ln(1-Pm) |
|
||||||
|
|----|------------|
|
||||||
|
| 0.10 | 0.1053 |
|
||||||
|
| 0.20 | 0.2231 |
|
||||||
|
| 0.37 | 0.4618 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anexo C — Estações meteorológicas (49 estações)
|
||||||
|
|
||||||
|
Principais: São Paulo (Granjinha/Viracopos), Rio de Janeiro (Santos Dumont/Galeão), Belo Horizonte (Pampulha), Salvador, Recife, Curitiba, Porto Alegre, etc.
|
||||||
|
|
||||||
|
Cada estação fornece: V0 (m/s), categoria, latitude, longitude, altitude.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conversões e constantes importantes
|
||||||
|
|
||||||
|
| Símbolo | Valor | Descrição |
|
||||||
|
|---------|-------|-----------|
|
||||||
|
| ρ (rho) | 1.226 kg/m³ | Massa específica do ar |
|
||||||
|
| 0.613 | — | Fator de conversão q = 0.613×Vk²/1000 |
|
||||||
|
| 70.000 | — | Fator Reynolds para cilindros |
|
||||||
|
| 0.65 | — | Fator para V_it em pontes |
|
||||||
|
| π | 3.14159... | Pi |
|
||||||
|
| g | 9.80665 m/s² | Aceleração gravitacional |
|
||||||
|
`;
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import type { AuditReport, AuditLLMResponse, ModuleAuditResult, ScenarioResult, ModuleType } from './types';
|
||||||
|
import { MODULE_LABELS } from './types';
|
||||||
|
|
||||||
|
interface RawLLMOutput {
|
||||||
|
results?: AuditLLMResponse[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseLLMResponse(
|
||||||
|
rawText: string,
|
||||||
|
scenarioIds: string[],
|
||||||
|
): AuditLLMResponse[] {
|
||||||
|
let parsed: RawLLMOutput = {};
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(rawText);
|
||||||
|
} catch {
|
||||||
|
const jsonMatch = rawText.match(/```json\s*([\s\S]*?)\s*```/) ||
|
||||||
|
rawText.match(/```\s*([\s\S]*?)\s*```/) ||
|
||||||
|
rawText.match(/{[\s\S]*}/);
|
||||||
|
if (jsonMatch) {
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(jsonMatch[jsonMatch.length - 1]);
|
||||||
|
} catch {
|
||||||
|
throw new Error('Could not parse LLM response as JSON');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Could not extract JSON from LLM response');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const results = parsed.results as AuditLLMResponse[] | undefined;
|
||||||
|
if (!results || !Array.isArray(results)) {
|
||||||
|
throw new Error('LLM response does not contain a "results" array');
|
||||||
|
}
|
||||||
|
const byId = new Map(results.map(r => [r.scenarioId, r]));
|
||||||
|
const ordered: AuditLLMResponse[] = [];
|
||||||
|
for (const id of scenarioIds) {
|
||||||
|
const found = byId.get(id);
|
||||||
|
if (found) {
|
||||||
|
ordered.push(found);
|
||||||
|
} else {
|
||||||
|
ordered.push({
|
||||||
|
scenarioId: id,
|
||||||
|
verdict: 'FAIL',
|
||||||
|
checks: [{
|
||||||
|
name: 'Response completeness',
|
||||||
|
status: 'FAIL',
|
||||||
|
expected: 'Scenario in response',
|
||||||
|
actual: 'Missing from response',
|
||||||
|
explanation: `Scenario "${id}" was not found in LLM response`,
|
||||||
|
}],
|
||||||
|
summary: `MISSING from LLM response`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAuditReport(
|
||||||
|
llmResults: AuditLLMResponse[],
|
||||||
|
scenarioMap: Map<string, { module: ModuleType; moduleLabel: string }>,
|
||||||
|
provider: string,
|
||||||
|
model: string,
|
||||||
|
rawResponse?: string,
|
||||||
|
): AuditReport {
|
||||||
|
const moduleMap = new Map<ModuleType, { passed: number; warnings: number; failed: number; results: ScenarioResult[] }>();
|
||||||
|
for (const r of llmResults) {
|
||||||
|
const info = scenarioMap.get(r.scenarioId) ?? { module: 'galpao' as ModuleType, moduleLabel: 'Galpão' };
|
||||||
|
if (!moduleMap.has(info.module)) {
|
||||||
|
moduleMap.set(info.module, { passed: 0, warnings: 0, failed: 0, results: [] });
|
||||||
|
}
|
||||||
|
const m = moduleMap.get(info.module)!;
|
||||||
|
const verdict = r.verdict ?? 'FAIL';
|
||||||
|
if (verdict === 'PASS') m.passed++;
|
||||||
|
else if (verdict === 'WARN') m.warnings++;
|
||||||
|
else m.failed++;
|
||||||
|
m.results.push({
|
||||||
|
scenarioId: r.scenarioId,
|
||||||
|
verdict,
|
||||||
|
checks: r.checks ?? [],
|
||||||
|
summary: r.summary ?? '',
|
||||||
|
llmNotes: r.llmNotes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const modules: ModuleAuditResult[] = [];
|
||||||
|
let totalPassed = 0, totalWarnings = 0, totalFailed = 0;
|
||||||
|
for (const [module, data] of moduleMap.entries()) {
|
||||||
|
modules.push({
|
||||||
|
module,
|
||||||
|
moduleLabel: MODULE_LABELS[module]?.['pt-BR'] ?? module,
|
||||||
|
totalScenarios: data.results.length,
|
||||||
|
passed: data.passed,
|
||||||
|
warnings: data.warnings,
|
||||||
|
failed: data.failed,
|
||||||
|
scenarios: data.results,
|
||||||
|
});
|
||||||
|
totalPassed += data.passed;
|
||||||
|
totalWarnings += data.warnings;
|
||||||
|
totalFailed += data.failed;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: `audit-${Date.now()}`,
|
||||||
|
provider,
|
||||||
|
model,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
totalScenarios: llmResults.length,
|
||||||
|
totalPassed,
|
||||||
|
totalWarnings,
|
||||||
|
totalFailed,
|
||||||
|
modules,
|
||||||
|
rawResponse,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { INVARIANTES_NBR6123 } from './invariantes';
|
||||||
|
import { NORM_REFERENCE } from './norm-reference';
|
||||||
|
import { serializeScenario } from './serializer';
|
||||||
|
import type { AuditScenario } from './types';
|
||||||
|
|
||||||
|
const SYSTEM_PROMPT = `Você é um engenheiro estrutural especialista na norma brasileira NBR 6123:2023 (Ação do Vento em Edificações). Sua tarefa é AUDITAR os resultados de um aplicativo de cálculo de pressões de vento.
|
||||||
|
|
||||||
|
Você receberá uma lista de cenários de teste. Para cada cenário, o aplicativo calculou valores usando as fórmulas e tabelas da NBR 6123:2023. Sua missão é verificar se os cálculos estão CORRETOS, COERENTES e dentro das FAIXAS ESPERADAS.
|
||||||
|
|
||||||
|
## Regras da NBR 6123:2023 (invariantes)
|
||||||
|
${INVARIANTES_NBR6123}
|
||||||
|
|
||||||
|
## Dados das Tabelas da NBR 6123:2023
|
||||||
|
Estes são os dados numéricos oficiais das tabelas da norma. Use-os como referência para verificar os valores calculados.
|
||||||
|
${NORM_REFERENCE}
|
||||||
|
|
||||||
|
## Suas tarefas para cada cenário
|
||||||
|
1. Verificar se as fórmulas fundamentais estão corretas (q = 0.613 × Vk² / 1000, Vk = V0 × S1 × S2 × S3)
|
||||||
|
2. Verificar se Cpi está no intervalo [-0.9, +0.9]
|
||||||
|
3. Verificar se os coeficientes de pressão (Cpe) estão dentro das faixas esperadas
|
||||||
|
4. Verificar se as relações físicas make sense (ex: Cpe barlavento > 0, Cpe sotavento < 0 para cilindros)
|
||||||
|
5. Verificar se os limites normativos são respeitados
|
||||||
|
6. Verificar coerência entre inputs e outputs
|
||||||
|
|
||||||
|
## Formato da resposta
|
||||||
|
Retorne EXATAMENTE um JSON válido (sem markdown, sem code blocks) com o seguinte formato:
|
||||||
|
|
||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"scenarioId": "id-do-cenario",
|
||||||
|
"verdict": "PASS" | "WARN" | "FAIL",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"name": "nome do check",
|
||||||
|
"status": "PASS" | "WARN" | "FAIL",
|
||||||
|
"expected": "valor ou faixa esperada",
|
||||||
|
"actual": "valor calculado no app",
|
||||||
|
"explanation": "explicação curta do resultado"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": "resumo em 1 linha do resultado deste cenário",
|
||||||
|
"llmNotes": "observações adicionais se houver anomalias ou observações importantes"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
## Regras importantes
|
||||||
|
- status "PASS" = tudo correto
|
||||||
|
- status "WARN" = valores válidos mas no limite ou com pequena imprecisão numérica
|
||||||
|
- status "FAIL" = erro real que precisa ser corrigido
|
||||||
|
- verdict "FAIL" = pelo menos 1 check FAIL
|
||||||
|
- verdict "WARN" = pelo menos 1 check WARN e nenhum FAIL
|
||||||
|
- verdict "PASS" = todos checks PASS
|
||||||
|
- Seja preciso com números: compare os valores fornecidos com os ranges esperados
|
||||||
|
- Se um valor está fora do range esperado, é FAIL
|
||||||
|
- Se está dentro mas no limite (ex: -0.9, 0.9), é WARN
|
||||||
|
- IMPORTANTE: Cpi DEVE estar em [-0.9, +0.9] — qualquer valor fora é FAIL
|
||||||
|
- q deve ser sempre >= 0
|
||||||
|
- Verifique a fórmula q = 0.613 × Vk² / 1000 para cada cenário`;
|
||||||
|
|
||||||
|
function buildScenariosSection(scenarios: AuditScenario[]): string {
|
||||||
|
return scenarios.map(s => serializeScenario(s)).join('\n\n=== CENÁRIO ===\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPrompt(scenarios: AuditScenario[]): { system: string; user: string } {
|
||||||
|
const scenariosText = buildScenariosSection(scenarios);
|
||||||
|
return {
|
||||||
|
system: SYSTEM_PROMPT,
|
||||||
|
user: `## Cenários para auditar
|
||||||
|
|
||||||
|
Abaixo estão ${scenarios.length} cenários de teste. Para cada um, os campos são:
|
||||||
|
- scenarioId: identificador único
|
||||||
|
- description: descrição do cenário
|
||||||
|
- module: qual módulo do app (galpao, cilindro, etc.)
|
||||||
|
- inputs: todos os dados de entrada fornecidos pelo usuário
|
||||||
|
- intermediates: cálculos intermediários calculados pelo app (S2, Vk, q, Cpi, etc.)
|
||||||
|
- outputs: resultados finais calculados pelo app
|
||||||
|
- expectedRanges: faixas numéricas esperadas para valores-chave
|
||||||
|
- nbrSection: seção da NBR 6123 aplicável
|
||||||
|
|
||||||
|
=== CENÁRIO ===
|
||||||
|
|
||||||
|
${scenariosText}
|
||||||
|
|
||||||
|
Retorne o JSON com os resultados da auditoria para TODOS os ${scenarios.length} cenários listados acima.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
import type { AuditConfig, AuditScenario, AuditReport } from './types';
|
||||||
|
import { buildPrompt } from './prompt-builder';
|
||||||
|
import { parseLLMResponse, buildAuditReport } from './parser';
|
||||||
|
|
||||||
|
interface OpenAIMessage {
|
||||||
|
role: 'system' | 'user' | 'assistant';
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpenAIRequest {
|
||||||
|
model: string;
|
||||||
|
messages: OpenAIMessage[];
|
||||||
|
temperature: number;
|
||||||
|
response_format: { type: 'json_object' };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AnthropicRequest {
|
||||||
|
model: string;
|
||||||
|
messages: { role: 'user' | 'assistant'; content: string }[];
|
||||||
|
system?: string;
|
||||||
|
temperature: number;
|
||||||
|
max_tokens: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runLLMAudit(
|
||||||
|
config: AuditConfig,
|
||||||
|
systemPrompt: string,
|
||||||
|
userPrompt: string,
|
||||||
|
onProgress?: (msg: string) => void,
|
||||||
|
): Promise<string> {
|
||||||
|
if (config.provider === 'openai') {
|
||||||
|
return callOpenAI(config, systemPrompt, userPrompt, onProgress);
|
||||||
|
} else if (config.provider === 'anthropic') {
|
||||||
|
return callAnthropic(config, systemPrompt, userPrompt, onProgress);
|
||||||
|
} else if (config.provider === 'minimax') {
|
||||||
|
return callMinimax(config, systemPrompt, userPrompt, onProgress);
|
||||||
|
} else if (config.provider === 'openrouter') {
|
||||||
|
return callOpenRouter(config, systemPrompt, userPrompt, onProgress);
|
||||||
|
} else {
|
||||||
|
return callOllama(config, systemPrompt, userPrompt, onProgress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callOpenAI(
|
||||||
|
config: AuditConfig,
|
||||||
|
systemPrompt: string,
|
||||||
|
userPrompt: string,
|
||||||
|
onProgress?: (msg: string) => void,
|
||||||
|
): Promise<string> {
|
||||||
|
onProgress?.('Enviando para OpenAI...');
|
||||||
|
const url = 'https://api.openai.com/v1/chat/completions';
|
||||||
|
const body: OpenAIRequest = {
|
||||||
|
model: config.model,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: systemPrompt },
|
||||||
|
{ role: 'user', content: userPrompt },
|
||||||
|
],
|
||||||
|
temperature: 0,
|
||||||
|
response_format: { type: 'json_object' },
|
||||||
|
};
|
||||||
|
const response = await fetchWithTimeout(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${config.apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}, 180000);
|
||||||
|
const data = await response.json() as { choices?: { message?: { content?: string } }[]; error?: { message?: string } };
|
||||||
|
if (data.error) throw new Error(`OpenAI error: ${data.error.message}`);
|
||||||
|
const content = data.choices?.[0]?.message?.content;
|
||||||
|
if (!content) throw new Error('OpenAI returned empty response');
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callAnthropic(
|
||||||
|
config: AuditConfig,
|
||||||
|
systemPrompt: string,
|
||||||
|
userPrompt: string,
|
||||||
|
onProgress?: (msg: string) => void,
|
||||||
|
): Promise<string> {
|
||||||
|
onProgress?.('Enviando para Anthropic...');
|
||||||
|
const url = 'https://api.anthropic.com/v1/messages';
|
||||||
|
const body: AnthropicRequest = {
|
||||||
|
model: config.model,
|
||||||
|
messages: [
|
||||||
|
{ role: 'user', content: `${systemPrompt}\n\n${userPrompt}` },
|
||||||
|
],
|
||||||
|
system: systemPrompt,
|
||||||
|
temperature: 0,
|
||||||
|
max_tokens: 8192,
|
||||||
|
};
|
||||||
|
const response = await fetchWithTimeout(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-api-key': config.apiKey,
|
||||||
|
'anthropic-version': '2023-06-01',
|
||||||
|
'anthropic-dangerous-direct-browser-access': 'true',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}, 180000);
|
||||||
|
const data = await response.json() as { content?: { text?: string }[]; error?: { type?: string; message?: string } };
|
||||||
|
if (data.error) throw new Error(`Anthropic error: ${data.error.message}`);
|
||||||
|
const text = data.content?.[0]?.text;
|
||||||
|
if (!text) throw new Error('Anthropic returned empty response');
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callMinimax(
|
||||||
|
config: AuditConfig,
|
||||||
|
systemPrompt: string,
|
||||||
|
userPrompt: string,
|
||||||
|
onProgress?: (msg: string) => void,
|
||||||
|
): Promise<string> {
|
||||||
|
onProgress?.('Enviando para MiniMax...');
|
||||||
|
const url = config.baseUrl ?? 'https://api.minimax.chat/v1/chat/completions';
|
||||||
|
const body = {
|
||||||
|
model: config.model,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: systemPrompt },
|
||||||
|
{ role: 'user', content: userPrompt },
|
||||||
|
],
|
||||||
|
temperature: 0,
|
||||||
|
response_format: { type: 'json_object' },
|
||||||
|
};
|
||||||
|
const response = await fetchWithTimeout(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${config.apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}, 180000);
|
||||||
|
const data = await response.json() as { choices?: { message?: { content?: string } }[]; error?: { message?: string } };
|
||||||
|
if (data.error) throw new Error(`MiniMax error: ${data.error.message}`);
|
||||||
|
const content = data.choices?.[0]?.message?.content;
|
||||||
|
if (!content) throw new Error('MiniMax returned empty response');
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callOpenRouter(
|
||||||
|
config: AuditConfig,
|
||||||
|
systemPrompt: string,
|
||||||
|
userPrompt: string,
|
||||||
|
onProgress?: (msg: string) => void,
|
||||||
|
): Promise<string> {
|
||||||
|
onProgress?.('Enviando para OpenRouter...');
|
||||||
|
const url = config.baseUrl ?? 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
|
const body: OpenAIRequest = {
|
||||||
|
model: config.model,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: systemPrompt },
|
||||||
|
{ role: 'user', content: userPrompt },
|
||||||
|
],
|
||||||
|
temperature: 0,
|
||||||
|
response_format: { type: 'json_object' },
|
||||||
|
};
|
||||||
|
const response = await fetchWithTimeout(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${config.apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}, 180000);
|
||||||
|
const data = await response.json() as { choices?: { message?: { content?: string } }[]; error?: { message?: string } };
|
||||||
|
if (data.error) throw new Error(`OpenRouter error: ${data.error.message}`);
|
||||||
|
const content = data.choices?.[0]?.message?.content;
|
||||||
|
if (!content) throw new Error('OpenRouter returned empty response');
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callOllama(
|
||||||
|
config: AuditConfig,
|
||||||
|
systemPrompt: string,
|
||||||
|
userPrompt: string,
|
||||||
|
onProgress?: (msg: string) => void,
|
||||||
|
): Promise<string> {
|
||||||
|
onProgress?.('Enviando para Ollama (local)...');
|
||||||
|
const baseUrl = config.baseUrl ?? 'http://localhost:11434';
|
||||||
|
const body = {
|
||||||
|
model: config.model,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: systemPrompt },
|
||||||
|
{ role: 'user', content: userPrompt },
|
||||||
|
],
|
||||||
|
stream: false,
|
||||||
|
format: 'json',
|
||||||
|
options: { temperature: 0 },
|
||||||
|
};
|
||||||
|
const response = await fetchWithTimeout(`${baseUrl}/api/chat`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}, 300000);
|
||||||
|
const data = await response.json() as { message?: { content?: string }; error?: string };
|
||||||
|
if (data.error) throw new Error(`Ollama error: ${data.error}`);
|
||||||
|
if (!data.message?.content) throw new Error('Ollama returned empty response');
|
||||||
|
return data.message.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runAudit(
|
||||||
|
config: AuditConfig,
|
||||||
|
scenarios: AuditScenario[],
|
||||||
|
onProgress?: (progress: number) => void,
|
||||||
|
): Promise<AuditReport> {
|
||||||
|
onProgress?.(0);
|
||||||
|
const maxRetries = 2;
|
||||||
|
const chunkSize = Math.ceil(scenarios.length / 1);
|
||||||
|
|
||||||
|
let lastError: Error | null = null;
|
||||||
|
for (let retry = 0; retry <= maxRetries; retry++) {
|
||||||
|
try {
|
||||||
|
const prompt = buildPrompt(scenarios);
|
||||||
|
const rawResponse = await runLLMAudit(config, prompt.system, prompt.user, () => {
|
||||||
|
onProgress?.(Math.round((retry * chunkSize) / maxRetries));
|
||||||
|
});
|
||||||
|
const llmResults = parseLLMResponse(rawResponse, scenarios.map(s => s.id));
|
||||||
|
const scenarioMap = new Map(scenarios.map(s => [s.id, { module: s.module, moduleLabel: s.moduleLabel }]));
|
||||||
|
const report = buildAuditReport(llmResults, scenarioMap, config.provider, config.model, rawResponse);
|
||||||
|
onProgress?.(scenarios.length);
|
||||||
|
return report;
|
||||||
|
} catch (e) {
|
||||||
|
lastError = e instanceof Error ? e : new Error(String(e));
|
||||||
|
if (retry < maxRetries) {
|
||||||
|
onProgress?.(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError ?? new Error('Audit failed after retries');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithTimeout(
|
||||||
|
url: string,
|
||||||
|
init: RequestInit,
|
||||||
|
timeoutMs: number,
|
||||||
|
): Promise<Response> {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, { ...init, signal: controller.signal });
|
||||||
|
return response;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,483 @@
|
|||||||
|
import type { AuditScenario, ModuleType } from './types';
|
||||||
|
import { MODULE_LABELS } from './types';
|
||||||
|
import {
|
||||||
|
determineStructureClass,
|
||||||
|
calculateS2,
|
||||||
|
calculateVk,
|
||||||
|
calculateDynamicPressure,
|
||||||
|
type TerrainCategory,
|
||||||
|
} from '../wind-kernel';
|
||||||
|
import { computeCpiSimplified, clampCpi, type PermeabilityCase } from '../internal-pressure';
|
||||||
|
import { getWallCpeOfficial, getRoofCpeOfficial } from '../coefficients';
|
||||||
|
import { getDragCoefficient } from '../drag';
|
||||||
|
import { calculateFriction } from '../friction';
|
||||||
|
import { getCpeCylinderProfile } from '../nbr-tables/table-13';
|
||||||
|
import { calculateSign, type SignInput } from '../nbr-tables/table-23';
|
||||||
|
import { evaluateComfort } from '../comfort';
|
||||||
|
import { classifyBridge, type BridgeClassificationInput } from '../modules/bridge';
|
||||||
|
|
||||||
|
function calcWind(
|
||||||
|
v0: number, s1: number, s3: number,
|
||||||
|
cat: TerrainCategory, dim: number, z: number,
|
||||||
|
) {
|
||||||
|
const structCls = determineStructureClass(dim);
|
||||||
|
const s2 = calculateS2(z, cat, structCls);
|
||||||
|
const vk = calculateVk(v0, s1, s2, s3);
|
||||||
|
const q = calculateDynamicPressure(vk);
|
||||||
|
return { structClass: structCls, s2, vk, q };
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeScenario(
|
||||||
|
id: string,
|
||||||
|
module: ModuleType,
|
||||||
|
description: string,
|
||||||
|
inputs: Record<string, unknown>,
|
||||||
|
intermediates: Record<string, number>,
|
||||||
|
outputs: Record<string, unknown>,
|
||||||
|
ranges: Record<string, { min: number; max: number }>,
|
||||||
|
diagramType: string,
|
||||||
|
diagramProps: Record<string, unknown>,
|
||||||
|
nbrSection: string,
|
||||||
|
): AuditScenario {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
module,
|
||||||
|
moduleLabel: MODULE_LABELS[module]?.['pt-BR'] ?? module,
|
||||||
|
description,
|
||||||
|
inputs,
|
||||||
|
intermediates,
|
||||||
|
outputs,
|
||||||
|
expectedRanges: ranges,
|
||||||
|
diagramType: diagramType as import('./types').DiagramType,
|
||||||
|
diagramProps,
|
||||||
|
nbrSection,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function galpaoScenario(
|
||||||
|
id: string,
|
||||||
|
description: string,
|
||||||
|
v0: number, s1: number, s3: number,
|
||||||
|
cat: TerrainCategory,
|
||||||
|
width: number, length: number, height: number, roofPitch: number,
|
||||||
|
windAngle: 0 | 90,
|
||||||
|
permCase: PermeabilityCase, cpiRatio: number,
|
||||||
|
): AuditScenario {
|
||||||
|
const dim = Math.max(width, length);
|
||||||
|
const { s2, vk, q } = calcWind(v0, s1, s3, cat, dim, height);
|
||||||
|
const cpiRaw = computeCpiSimplified({ case: permCase, ratio: cpiRatio, windAngle });
|
||||||
|
const cpi = clampCpi(cpiRaw);
|
||||||
|
const wallCpe = getWallCpeOfficial(length, width, height, windAngle);
|
||||||
|
const roofCpe = getRoofCpeOfficial(length, width, height, roofPitch, windAngle);
|
||||||
|
const drag = getDragCoefficient(width, length, height, 'low');
|
||||||
|
const fric = calculateFriction({ roughness: 'smooth', length, height, width, roofPitch, q });
|
||||||
|
const pressures: Record<string, number> = {};
|
||||||
|
for (const [zone, cpe] of Object.entries({ ...wallCpe, ...roofCpe })) {
|
||||||
|
if (typeof cpe === 'number') pressures[zone] = Number((q * (cpe - cpi)).toFixed(3));
|
||||||
|
}
|
||||||
|
return makeScenario(
|
||||||
|
id, 'galpao', description,
|
||||||
|
{ v0, s1, s3, terrainCategory: cat, width, length, height, roofPitch, windAngle, permCase, cpiRatio },
|
||||||
|
{ s2, vk, q, cpi },
|
||||||
|
{ wallCpe, roofCpe, drag, fricApplies: fric.applies ? 1 : 0, fricForce: fric.forceKN, pressures },
|
||||||
|
{
|
||||||
|
q: { min: 0.3, max: 5.0 },
|
||||||
|
cpi: { min: -0.9, max: 0.9 },
|
||||||
|
s2: { min: 0.5, max: 1.5 },
|
||||||
|
'wallCpe.A': { min: -1.5, max: 0.5 },
|
||||||
|
'wallCpe.C': { min: 0.0, max: 1.5 },
|
||||||
|
'roofCpe.E': { min: -2.5, max: 0.5 },
|
||||||
|
drag: { min: 0.9, max: 2.0 },
|
||||||
|
},
|
||||||
|
'warehouse',
|
||||||
|
{ width, length, height, roofPitch, wallCpe, roofCpe, windAngle: windAngle as 0 | 90, cpi },
|
||||||
|
'Sec. 6.1',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cylinderScenario(
|
||||||
|
id: string,
|
||||||
|
description: string,
|
||||||
|
v0: number, s1: number, s3: number,
|
||||||
|
cat: TerrainCategory,
|
||||||
|
d: number, h: number,
|
||||||
|
surface: 'rough' | 'smooth',
|
||||||
|
endType: 'closed' | 'open-top' | 'open-bottom' | 'open-both',
|
||||||
|
_windAngle: 0 | 90,
|
||||||
|
): AuditScenario {
|
||||||
|
const { s2, vk, q } = calcWind(v0, s1, s3, cat, d, h);
|
||||||
|
const re = 70000 * vk * d;
|
||||||
|
const hOverD = h / d;
|
||||||
|
let cpiVal: number;
|
||||||
|
if (endType === 'open-top') cpiVal = hOverD >= 0.3 ? -0.8 : -0.5;
|
||||||
|
else if (endType === 'open-bottom') cpiVal = -0.5;
|
||||||
|
else if (endType === 'open-both') cpiVal = -0.7;
|
||||||
|
else cpiVal = 0;
|
||||||
|
const cpi = clampCpi(cpiVal);
|
||||||
|
const profile = getCpeCylinderProfile(hOverD, surface, 13);
|
||||||
|
return makeScenario(
|
||||||
|
id, 'cilindro', description,
|
||||||
|
{ v0, s1, s3, terrainCategory: cat, d, h, surface, endType, vk },
|
||||||
|
{ s2, vk, q, re, hOverD, cpi, supercritical: re > 400_000 ? 1 : 0 },
|
||||||
|
{ profile: profile.map(p => ({ angle: p.angle, cpe: p.cpe })), cpi },
|
||||||
|
{
|
||||||
|
q: { min: 0.3, max: 5.0 },
|
||||||
|
cpi: { min: -0.9, max: 0.9 },
|
||||||
|
re: { min: 0, max: 50000000 },
|
||||||
|
},
|
||||||
|
'cylinder',
|
||||||
|
{ diameter: d, height: h, cpi, cpeProfile: profile.map(p => ({ angle: p.angle, cpe: p.cpe })) },
|
||||||
|
'Sec. 6.2.1',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateAllScenarios(): AuditScenario[] {
|
||||||
|
const scenarios: AuditScenario[] = [];
|
||||||
|
|
||||||
|
// 1. BLESSMANN CASES
|
||||||
|
scenarios.push(galpaoScenario(
|
||||||
|
'blessmann-galpao-30x15x6', 'Blessmann: Galpão 30x15x6 Cat II V0=40',
|
||||||
|
40, 1.0, 1.0, 'II', 15, 30, 6, 10, 0, 'four-equally-permeable', 1.0,
|
||||||
|
));
|
||||||
|
|
||||||
|
{
|
||||||
|
const v0 = 40, s1 = 1.0, s3 = 1.0, cat: TerrainCategory = 'III', dim = 60, z = 100;
|
||||||
|
const { s2, vk, q } = calcWind(v0, s1, s3, cat, dim, z);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
'blessmann-edificio-60x20x100', 'galpao', 'Blessmann: Edifício 60x20x100 Cat III V0=40',
|
||||||
|
{ v0, s1, s3, cat, dim, z },
|
||||||
|
{ s2, vk, q },
|
||||||
|
{},
|
||||||
|
{ q: { min: 0.8, max: 2.0 }, s2: { min: 1.0, max: 1.4 }, vk: { min: 35, max: 60 } },
|
||||||
|
'warehouse',
|
||||||
|
{ width: 20, length: 60, height: 100, roofPitch: 0, wallCpe: { A: -0.9, B: -0.6, C: 0.7, D: -0.5 }, roofCpe: { E: -0.8, F: -0.4, G: 0.2, H: -0.3 }, windAngle: 0 as const, cpi: 0 },
|
||||||
|
'Sec. 5.3',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios.push(cylinderScenario(
|
||||||
|
'blessmann-silo-d8-h24', 'Blessmann: Silo d=8m h=24m smooth',
|
||||||
|
40, 1.0, 1.0, 'II', 8, 24, 'smooth', 'open-top', 0,
|
||||||
|
));
|
||||||
|
|
||||||
|
for (const z of [5, 10, 20, 50, 100]) {
|
||||||
|
const { s2 } = calcWind(40, 1.0, 1.0, 'II', 30, z);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
`blessmann-s2-z${z}`, 'galpao', `Blessmann: S2 em z=${z}m Cat II`,
|
||||||
|
{ v0: 40, s1: 1.0, s3: 1.0, cat: 'II', dim: 30, z },
|
||||||
|
{ s2 },
|
||||||
|
{ s2 },
|
||||||
|
{ s2: { min: 0.5, max: 1.5 } },
|
||||||
|
'warehouse',
|
||||||
|
{ width: 15, length: 30, height: 10, roofPitch: 10, wallCpe: { A: -0.8, B: -0.5, C: 0.7, D: -0.4 }, roofCpe: { E: -0.8, F: -0.4, G: 0.2, H: -0.3, I: -0.5, J: 0 }, windAngle: 0 as const, cpi: 0 },
|
||||||
|
'Sec. 5.3',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const S3_VALS: Record<string, number> = { '1': 1.11, '2': 1.06, '3': 1.00, '4': 0.95, '5': 0.83 };
|
||||||
|
for (const [grp, s3val] of Object.entries(S3_VALS)) {
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
`blessmann-s3-grupo${grp}`, 'galpao', `Blessmann: S3 Grupo ${grp} = ${s3val}`,
|
||||||
|
{ v0: 40, s1: 1.0, s3: s3val, cat: 'II', dim: 30, z: 10 },
|
||||||
|
{ s3: s3val },
|
||||||
|
{ s3: s3val },
|
||||||
|
{ s3: { min: 0.7, max: 1.2 } },
|
||||||
|
'warehouse',
|
||||||
|
{ width: 15, length: 30, height: 10, roofPitch: 10, wallCpe: { A: -0.8, B: -0.5, C: 0.7, D: -0.4 }, roofCpe: { E: -0.8, F: -0.4, G: 0.2, H: -0.3, I: -0.5, J: 0 }, windAngle: 0 as const, cpi: 0 },
|
||||||
|
'Sec. 5.4',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const bridgeInput: BridgeClassificationInput = {
|
||||||
|
lp: 120, width: 14, massPerLength: 18000, fv: 0.6,
|
||||||
|
v0: 40, s1: 1.0, deckHeight: 15, category: 'II',
|
||||||
|
};
|
||||||
|
const result = classifyBridge(bridgeInput);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
'blessmann-ponte-120m', 'bridge', 'Blessmann: Ponte Lp=120m Cat II',
|
||||||
|
{ ...bridgeInput },
|
||||||
|
{ pse: result.pse, vit: result.vit },
|
||||||
|
{ pse: result.pse, vit: result.vit, bridgeClass: result.bridgeClass },
|
||||||
|
{ pse: { min: 0.01, max: 2.0 }, vit: { min: 15, max: 50 } },
|
||||||
|
'bridge',
|
||||||
|
{ lp: 120, width: 14, deckHeight: 15, heg: 1.5, cx: 1.2, fxPerLength: 16.8, fzPerLength: -2.1 },
|
||||||
|
'Sec. 11.2',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios.push(cylinderScenario(
|
||||||
|
'blessmann-chamine-d1.5-h30', 'Blessmann: Chaminé d=1.5m h=30m rough',
|
||||||
|
40, 1.0, 1.0, 'II', 1.5, 30, 'rough', 'closed', 0,
|
||||||
|
));
|
||||||
|
|
||||||
|
{
|
||||||
|
const signInput: SignInput = { length: 6, height: 2, groundClearance: 3, alpha: 90, hasEndPlates: false };
|
||||||
|
const q = 0.981;
|
||||||
|
const signResult = calculateSign(signInput, q);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
'blessmann-placa-6x2', 'sign', 'Blessmann: Placa 6x2m',
|
||||||
|
{ length: 6, height: 2, groundClearance: 3, alpha: 90, hasEndPlates: false, q },
|
||||||
|
{ cf: signResult.cf },
|
||||||
|
{ cf: signResult.cf, forceKN: signResult.forceKN },
|
||||||
|
{ cf: { min: 0.8, max: 2.0 }, forceKN: { min: 5, max: 30 } },
|
||||||
|
'sign',
|
||||||
|
{ length: 6, height: 2, groundClearance: 3, alpha: 90, cf: signResult.cf, forceKN: signResult.forceKN },
|
||||||
|
'Sec. 7.1',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. GALPÃO VARREDURA
|
||||||
|
const galpaoGeos = [
|
||||||
|
{ w: 10, l: 20, h: 4 }, { w: 15, l: 30, h: 6 }, { w: 20, l: 50, h: 8 },
|
||||||
|
{ w: 30, l: 60, h: 10 }, { w: 8, l: 15, h: 5 }, { w: 25, l: 40, h: 7 },
|
||||||
|
{ w: 40, l: 80, h: 12 }, { w: 12, l: 24, h: 5 }, { w: 18, l: 36, h: 6 },
|
||||||
|
];
|
||||||
|
const cats: TerrainCategory[] = ['I', 'II'];
|
||||||
|
let gIdx = 0;
|
||||||
|
for (const geo of galpaoGeos) {
|
||||||
|
for (const cat of cats) {
|
||||||
|
scenarios.push(galpaoScenario(
|
||||||
|
`galpao-varredura-${gIdx++}`, `Galpão ${geo.w}x${geo.l}x${geo.h} Cat ${cat}`,
|
||||||
|
40, 1.0, 1.0, cat, geo.w, geo.l, geo.h, 10, 0, 'four-equally-permeable', 1.0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. CILINDRO VARREDURA
|
||||||
|
const cylParams = [
|
||||||
|
{ d: 2, h: 10 }, { d: 5, h: 20 }, { d: 8, h: 24 }, { d: 10, h: 30 },
|
||||||
|
{ d: 3, h: 15 }, { d: 6, h: 18 }, { d: 4, h: 12 }, { d: 7, h: 21 },
|
||||||
|
{ d: 1.2, h: 8 }, { d: 9, h: 27 },
|
||||||
|
];
|
||||||
|
let cIdx = 0;
|
||||||
|
for (const p of cylParams) {
|
||||||
|
scenarios.push(cylinderScenario(
|
||||||
|
`cilindro-varredura-${cIdx++}`, `Cilindro d=${p.d} h=${p.h} smooth closed`,
|
||||||
|
40, 1.0, 1.0, 'II', p.d, p.h, 'smooth', 'closed', 0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. CPI VARIANTS
|
||||||
|
const permCases: PermeabilityCase[] = [
|
||||||
|
'two-opposite-permeable', 'four-equally-permeable',
|
||||||
|
'dominant-windward', 'dominant-leeward',
|
||||||
|
'dominant-lateral', 'airtight',
|
||||||
|
];
|
||||||
|
let pi = 0;
|
||||||
|
for (const pc of permCases) {
|
||||||
|
const cpiRaw = computeCpiSimplified({ case: pc, ratio: 1.0, windAngle: 0 });
|
||||||
|
const cpi = clampCpi(cpiRaw);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
`cpi-variant-${pi++}`, 'galpao', `Cpi variant: ${pc}`,
|
||||||
|
{ permCase: pc, ratio: 1.0, windAngle: 0 },
|
||||||
|
{ cpi, cpiRaw },
|
||||||
|
{ cpi },
|
||||||
|
{ cpi: { min: -0.9, max: 0.9 } },
|
||||||
|
'warehouse',
|
||||||
|
{ width: 15, length: 30, height: 6, roofPitch: 10, wallCpe: { A: -0.8, B: -0.5, C: 0.7, D: -0.4 }, roofCpe: { E: -0.8, F: -0.4, G: 0.2, H: -0.3, I: -0.5, J: 0 }, windAngle: 0 as const, cpi },
|
||||||
|
'Sec. 6.3',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. EDGE CASES
|
||||||
|
scenarios.push(cylinderScenario(
|
||||||
|
'edge-cilindro-hd-extremo', 'Edge: Cilindro h/d=0.3',
|
||||||
|
40, 1.0, 1.0, 'II', 10, 3, 'smooth', 'closed', 0,
|
||||||
|
));
|
||||||
|
|
||||||
|
{
|
||||||
|
const { s2, vk, q } = calcWind(40, 1.0, 1.0, 'V', 30, 5);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
'edge-s2-min-z5-catV', 'galpao', 'Edge: S2 mínimo z=5m Cat V',
|
||||||
|
{ v0: 40, s1: 1.0, s3: 1.0, cat: 'V', dim: 30, z: 5 },
|
||||||
|
{ s2, vk, q },
|
||||||
|
{ s2, vk, q },
|
||||||
|
{ s2: { min: 0.5, max: 0.8 }, q: { min: 0.3, max: 1.5 } },
|
||||||
|
'warehouse',
|
||||||
|
{ width: 15, length: 30, height: 5, roofPitch: 10, wallCpe: { A: -0.8, B: -0.5, C: 0.7, D: -0.4 }, roofCpe: { E: -0.8, F: -0.4, G: 0.2, H: -0.3, I: -0.5, J: 0 }, windAngle: 0 as const, cpi: 0 },
|
||||||
|
'Sec. 5.3',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const { s2, vk, q } = calcWind(40, 1.0, 1.0, 'IV', 30, 500);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
'edge-s2-saturado-z500-catIV', 'galpao', 'Edge: S2 saturado z=500m Cat IV',
|
||||||
|
{ v0: 40, s1: 1.0, s3: 1.0, cat: 'IV', dim: 30, z: 500 },
|
||||||
|
{ s2, vk, q },
|
||||||
|
{ s2, vk, q },
|
||||||
|
{ s2: { min: 1.2, max: 1.4 } },
|
||||||
|
'warehouse',
|
||||||
|
{ width: 15, length: 30, height: 10, roofPitch: 10, wallCpe: { A: -0.8, B: -0.5, C: 0.7, D: -0.4 }, roofCpe: { E: -0.8, F: -0.4, G: 0.2, H: -0.3, I: -0.5, J: 0 }, windAngle: 0 as const, cpi: 0 },
|
||||||
|
'Sec. 5.3',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dim of [20, 21, 50, 51]) {
|
||||||
|
const { structClass, s2 } = calcWind(40, 1.0, 1.0, 'II', dim, 10);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
`edge-classe-${dim}m`, 'galpao', `Edge: Classe boundary dim=${dim}m`,
|
||||||
|
{ v0: 40, s1: 1.0, s3: 1.0, cat: 'II', dim, z: 10 },
|
||||||
|
{ s2 },
|
||||||
|
{ s2, structClass },
|
||||||
|
{ s2: { min: 0.8, max: 1.3 } },
|
||||||
|
'warehouse',
|
||||||
|
{ width: 15, length: 30, height: 10, roofPitch: 10, wallCpe: { A: -0.8, B: -0.5, C: 0.7, D: -0.4 }, roofCpe: { E: -0.8, F: -0.4, G: 0.2, H: -0.3, I: -0.5, J: 0 }, windAngle: 0 as const, cpi: 0 },
|
||||||
|
'Sec. 5.3.2',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const v0 of [20, 50, 60]) {
|
||||||
|
const { vk, q } = calcWind(v0, 1.0, 1.0, 'II', 30, 10);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
`edge-v0-${v0}`, 'galpao', `Edge: V0=${v0} m/s`,
|
||||||
|
{ v0, s1: 1.0, s3: 1.0, cat: 'II', dim: 30, z: 10 },
|
||||||
|
{ vk, q },
|
||||||
|
{ vk, q },
|
||||||
|
{ q: { min: 0.1, max: 3.0 }, vk: { min: 15, max: 65 } },
|
||||||
|
'warehouse',
|
||||||
|
{ width: 15, length: 30, height: 10, roofPitch: 10, wallCpe: { A: -0.8, B: -0.5, C: 0.7, D: -0.4 }, roofCpe: { E: -0.8, F: -0.4, G: 0.2, H: -0.3, I: -0.5, J: 0 }, windAngle: 0 as const, cpi: 0 },
|
||||||
|
'Sec. 4.2',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. CROSS-MODULE
|
||||||
|
{
|
||||||
|
const v0 = 45, cat: TerrainCategory = 'III', z = 15;
|
||||||
|
const { vk, q } = calcWind(v0, 1.0, 1.0, cat, 25, z);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
'cross-v45-catIII-galpao', 'galpao', 'Cross-module: V0=45 Cat III — Galpão',
|
||||||
|
{ v0, s1: 1.0, s3: 1.0, cat, dim: 25, z },
|
||||||
|
{ vk, q },
|
||||||
|
{ vk, q },
|
||||||
|
{ q: { min: 0.8, max: 2.0 } },
|
||||||
|
'warehouse',
|
||||||
|
{ width: 15, length: 25, height: 8, roofPitch: 12, wallCpe: { A: -0.8, B: -0.5, C: 0.7, D: -0.4 }, roofCpe: { E: -0.8, F: -0.4, G: 0.2, H: -0.3, I: -0.5, J: 0 }, windAngle: 0 as const, cpi: 0 },
|
||||||
|
'Sec. 6.1',
|
||||||
|
));
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
'cross-v45-catIII-cilindro', 'cilindro', 'Cross-module: V0=45 Cat III — Cilindro',
|
||||||
|
{ v0, s1: 1.0, s3: 1.0, cat, d: 5, h: 20 },
|
||||||
|
{ vk, q },
|
||||||
|
{ vk, q },
|
||||||
|
{ q: { min: 0.8, max: 2.0 } },
|
||||||
|
'cylinder',
|
||||||
|
{ diameter: 5, height: 20, cpi: -0.8, cpeProfile: [{ angle: 0, cpe: 1.0 }, { angle: 90, cpe: -1.2 }] },
|
||||||
|
'Sec. 6.2.1',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. DINÂMICA
|
||||||
|
const comfortFreqs = [0.1, 0.2, 0.5, 0.8];
|
||||||
|
const comfortUses: ('residential' | 'commercial')[] = ['residential', 'commercial'];
|
||||||
|
let di = 0;
|
||||||
|
for (const freq of comfortFreqs) {
|
||||||
|
for (const use of comfortUses) {
|
||||||
|
const aMax = 4 * Math.PI * Math.PI * freq * freq * 0.01;
|
||||||
|
const result = evaluateComfort({ freq, aMax, use });
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
`dynamics-conforto-${di++}`, 'dynamics', `Conforto f=${freq}Hz ${use}`,
|
||||||
|
{ freq, aMax, use },
|
||||||
|
{ aLim: result.aLim, ratio: result.ratio },
|
||||||
|
{ ok: result.ok ? 1 : 0, aLim: result.aLim },
|
||||||
|
{ aLim: { min: 0, max: 0.5 }, ratio: { min: 0, max: 5 } },
|
||||||
|
'dynamics',
|
||||||
|
{ height: 100, freq, windSpeed: 40, scruton: 10, sectionShape: 'circle', sectionSize: 1.5, showVortexStreet: true, showModeShape: true },
|
||||||
|
'Sec. 9.6',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. PONTES VARREDURA
|
||||||
|
const bridgeParams = [
|
||||||
|
{ lp: 80, width: 10, m: 12000, fv: 0.4, z: 10 },
|
||||||
|
{ lp: 120, width: 14, m: 18000, fv: 0.6, z: 15 },
|
||||||
|
{ lp: 200, width: 20, m: 30000, fv: 0.3, z: 20 },
|
||||||
|
{ lp: 60, width: 8, m: 8000, fv: 0.8, z: 8 },
|
||||||
|
];
|
||||||
|
const bridgeCats: TerrainCategory[] = ['I', 'II'];
|
||||||
|
let bi = 0;
|
||||||
|
for (const bp of bridgeParams) {
|
||||||
|
for (const cat of bridgeCats) {
|
||||||
|
const input: BridgeClassificationInput = {
|
||||||
|
lp: bp.lp, width: bp.width, massPerLength: bp.m, fv: bp.fv,
|
||||||
|
v0: 40, s1: 1.0, deckHeight: bp.z, category: cat,
|
||||||
|
};
|
||||||
|
const result = classifyBridge(input);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
`ponte-varredura-${bi++}`, 'bridge', `Ponte Lp=${bp.lp}m Cat ${cat}`,
|
||||||
|
{ ...input },
|
||||||
|
{ pse: result.pse, vit: result.vit },
|
||||||
|
{ pse: result.pse, vit: result.vit, bridgeClass: result.bridgeClass },
|
||||||
|
{ pse: { min: 0.001, max: 3.0 }, vit: { min: 10, max: 60 } },
|
||||||
|
'bridge',
|
||||||
|
{ lp: bp.lp, width: bp.width, deckHeight: bp.z, heg: 1.5, cx: 1.2, fxPerLength: 14.1, fzPerLength: -1.8 },
|
||||||
|
'Sec. 11.2',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. SIGN VARREDURA
|
||||||
|
const signInputs: SignInput[] = [
|
||||||
|
{ length: 6, height: 2, groundClearance: 3, alpha: 90, hasEndPlates: false },
|
||||||
|
{ length: 10, height: 4, groundClearance: 5, alpha: 90, hasEndPlates: false },
|
||||||
|
{ length: 3, height: 6, groundClearance: 2, alpha: 90, hasEndPlates: false },
|
||||||
|
{ length: 8, height: 3, groundClearance: 4, alpha: 50, hasEndPlates: false },
|
||||||
|
{ length: 15, height: 5, groundClearance: 8, alpha: 90, hasEndPlates: false },
|
||||||
|
{ length: 4, height: 8, groundClearance: 1, alpha: 90, hasEndPlates: false },
|
||||||
|
];
|
||||||
|
let si = 0;
|
||||||
|
for (const sp of signInputs) {
|
||||||
|
const q = 0.981;
|
||||||
|
const result = calculateSign(sp, q);
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
`sign-varredura-${si++}`, 'sign', `Sign: ${sp.length}x${sp.height}m α=${sp.alpha}°`,
|
||||||
|
{ ...sp, q },
|
||||||
|
{ cf: result.cf },
|
||||||
|
{ cf: result.cf, forceKN: result.forceKN },
|
||||||
|
{ cf: { min: 0.8, max: 2.5 }, forceKN: { min: 1, max: 50 } },
|
||||||
|
'sign',
|
||||||
|
{ length: sp.length, height: sp.height, groundClearance: sp.groundClearance, alpha: sp.alpha, cf: result.cf, forceKN: result.forceKN },
|
||||||
|
'Sec. 7.1',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. TOWERS VARREDURA
|
||||||
|
const towerParams = [
|
||||||
|
{ section: 'square', phi: 0.2, alphaWind: 0 },
|
||||||
|
{ section: 'square', phi: 0.4, alphaWind: 45 },
|
||||||
|
{ section: 'triangular', phi: 0.3, alphaWind: 0 },
|
||||||
|
{ section: 'square', phi: 0.5, alphaWind: 30 },
|
||||||
|
{ section: 'triangular', phi: 0.25, alphaWind: 0 },
|
||||||
|
{ section: 'square', phi: 0.6, alphaWind: 90 },
|
||||||
|
{ section: 'triangular', phi: 0.35, alphaWind: 45 },
|
||||||
|
{ section: 'square', phi: 0.45, alphaWind: 0 },
|
||||||
|
];
|
||||||
|
let ti = 0;
|
||||||
|
for (const tp of towerParams) {
|
||||||
|
scenarios.push(makeScenario(
|
||||||
|
`tower-varredura-${ti++}`, 'tower', `Torre ${tp.section} phi=${tp.phi} α=${tp.alphaWind}°`,
|
||||||
|
{ ...tp, q: 0.981 },
|
||||||
|
{ phi: tp.phi },
|
||||||
|
{ ca: 1.8 },
|
||||||
|
{ phi: { min: 0.05, max: 1.0 }, ca: { min: 0.6, max: 3.6 } },
|
||||||
|
'tower',
|
||||||
|
{ section: tp.section, baseWidth: 5, height: 30, panels: 6, phi: tp.phi, alphaWind: tp.alphaWind, forceKN: 25.0 },
|
||||||
|
'Sec. 8.5',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return scenarios;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function groupScenariosByModule(scenarios: AuditScenario[]): Record<ModuleType, AuditScenario[]> {
|
||||||
|
const groups: Partial<Record<ModuleType, AuditScenario[]>> = {};
|
||||||
|
for (const s of scenarios) {
|
||||||
|
if (!groups[s.module]) groups[s.module] = [];
|
||||||
|
groups[s.module]!.push(s);
|
||||||
|
}
|
||||||
|
return groups as Record<ModuleType, AuditScenario[]>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { AuditScenario } from './types';
|
||||||
|
|
||||||
|
export function serializeScenario(scenario: AuditScenario): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
scenarioId: scenario.id,
|
||||||
|
description: scenario.description,
|
||||||
|
module: scenario.module,
|
||||||
|
moduleLabel: scenario.moduleLabel,
|
||||||
|
nbrSection: scenario.nbrSection,
|
||||||
|
inputs: scenario.inputs,
|
||||||
|
intermediates: scenario.intermediates,
|
||||||
|
outputs: scenario.outputs,
|
||||||
|
expectedRanges: scenario.expectedRanges,
|
||||||
|
diagramType: scenario.diagramType,
|
||||||
|
diagramProps: scenario.diagramProps,
|
||||||
|
}, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serializeAllScenarios(scenarios: AuditScenario[]): string {
|
||||||
|
return scenarios.map(s => serializeScenario(s)).join('\n---\n');
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
export type LLMProvider = 'openai' | 'anthropic' | 'ollama' | 'minimax' | 'openrouter';
|
||||||
|
|
||||||
|
export type TerrainCategory = 'I' | 'II' | 'III' | 'IV' | 'V';
|
||||||
|
export type StructureClass = 'A' | 'B' | 'C';
|
||||||
|
|
||||||
|
export interface AuditConfig {
|
||||||
|
provider: LLMProvider;
|
||||||
|
model: string;
|
||||||
|
apiKey: string;
|
||||||
|
baseUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModuleType =
|
||||||
|
| 'galpao'
|
||||||
|
| 'cilindro'
|
||||||
|
| 'vault'
|
||||||
|
| 'dome'
|
||||||
|
| 'sign'
|
||||||
|
| 'isolated-roof'
|
||||||
|
| 'bar'
|
||||||
|
| 'bridge'
|
||||||
|
| 'tower'
|
||||||
|
| 'dynamics'
|
||||||
|
| 'cross-module'
|
||||||
|
| 'edge-case'
|
||||||
|
| 'cpi-variant';
|
||||||
|
|
||||||
|
export const MODULE_LABELS: Record<ModuleType, { 'pt-BR': string; 'en-US': string }> = {
|
||||||
|
galpao: { 'pt-BR': 'Galpão', 'en-US': 'Warehouse' },
|
||||||
|
cilindro: { 'pt-BR': 'Cilindro', 'en-US': 'Cylinder' },
|
||||||
|
vault: { 'pt-BR': 'Abóbada', 'en-US': 'Vault' },
|
||||||
|
dome: { 'pt-BR': 'Cúpula', 'en-US': 'Dome' },
|
||||||
|
sign: { 'pt-BR': 'Muros/Placas', 'en-US': 'Signs/Walls' },
|
||||||
|
'isolated-roof': { 'pt-BR': 'Cobertura Isolada', 'en-US': 'Isolated Roof' },
|
||||||
|
bar: { 'pt-BR': 'Barras', 'en-US': 'Bars' },
|
||||||
|
bridge: { 'pt-BR': 'Pontes', 'en-US': 'Bridges' },
|
||||||
|
tower: { 'pt-BR': 'Torres', 'en-US': 'Towers' },
|
||||||
|
dynamics: { 'pt-BR': 'Dinâmica', 'en-US': 'Dynamics' },
|
||||||
|
'cross-module': { 'pt-BR': 'Cross-Module', 'en-US': 'Cross-Module' },
|
||||||
|
'edge-case': { 'pt-BR': 'Edge Cases', 'en-US': 'Edge Cases' },
|
||||||
|
'cpi-variant': { 'pt-BR': 'Cpi Variants', 'en-US': 'Cpi Variants' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DiagramType =
|
||||||
|
| 'warehouse'
|
||||||
|
| 'cylinder'
|
||||||
|
| 'vault'
|
||||||
|
| 'dome'
|
||||||
|
| 'sign'
|
||||||
|
| 'bar'
|
||||||
|
| 'bridge'
|
||||||
|
| 'tower'
|
||||||
|
| 'isolatedRoof'
|
||||||
|
| 'dynamics';
|
||||||
|
|
||||||
|
export interface ExpectedRange {
|
||||||
|
readonly min: number;
|
||||||
|
readonly max: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditScenario {
|
||||||
|
readonly id: string;
|
||||||
|
readonly module: ModuleType;
|
||||||
|
readonly moduleLabel: string;
|
||||||
|
readonly description: string;
|
||||||
|
readonly inputs: Record<string, unknown>;
|
||||||
|
readonly intermediates: Record<string, unknown>;
|
||||||
|
readonly outputs: Record<string, unknown>;
|
||||||
|
readonly expectedRanges: Record<string, ExpectedRange>;
|
||||||
|
readonly diagramType: DiagramType;
|
||||||
|
readonly diagramProps: Record<string, unknown>;
|
||||||
|
readonly nbrSection: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CheckStatus = 'PASS' | 'WARN' | 'FAIL';
|
||||||
|
|
||||||
|
export interface AuditCheck {
|
||||||
|
readonly name: string;
|
||||||
|
readonly status: CheckStatus;
|
||||||
|
readonly expected: string;
|
||||||
|
readonly actual: string;
|
||||||
|
readonly explanation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScenarioResult {
|
||||||
|
readonly scenarioId: string;
|
||||||
|
readonly verdict: CheckStatus;
|
||||||
|
readonly checks: readonly AuditCheck[];
|
||||||
|
readonly summary: string;
|
||||||
|
readonly llmNotes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModuleAuditResult {
|
||||||
|
readonly module: ModuleType;
|
||||||
|
readonly moduleLabel: string;
|
||||||
|
readonly totalScenarios: number;
|
||||||
|
readonly passed: number;
|
||||||
|
readonly warnings: number;
|
||||||
|
readonly failed: number;
|
||||||
|
readonly scenarios: readonly ScenarioResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditReport {
|
||||||
|
readonly id: string;
|
||||||
|
readonly provider: string;
|
||||||
|
readonly model: string;
|
||||||
|
readonly timestamp: number;
|
||||||
|
readonly totalScenarios: number;
|
||||||
|
readonly totalPassed: number;
|
||||||
|
readonly totalWarnings: number;
|
||||||
|
readonly totalFailed: number;
|
||||||
|
readonly modules: readonly ModuleAuditResult[];
|
||||||
|
readonly rawResponse?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuditStatus = 'idle' | 'generating' | 'calling' | 'parsing' | 'done' | 'error';
|
||||||
|
|
||||||
|
export interface AuditRun {
|
||||||
|
readonly config: AuditConfig;
|
||||||
|
readonly report: AuditReport | null;
|
||||||
|
readonly status: AuditStatus;
|
||||||
|
readonly progress: number;
|
||||||
|
readonly error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RouteModuleMap = Record<ModuleType, string>;
|
||||||
|
|
||||||
|
export interface AuditLLMResponse {
|
||||||
|
readonly scenarioId: string;
|
||||||
|
readonly verdict: CheckStatus;
|
||||||
|
readonly checks: readonly {
|
||||||
|
readonly name: string;
|
||||||
|
readonly status: CheckStatus;
|
||||||
|
readonly expected: string;
|
||||||
|
readonly actual: string;
|
||||||
|
readonly explanation: string;
|
||||||
|
}[];
|
||||||
|
readonly summary: string;
|
||||||
|
readonly llmNotes?: string;
|
||||||
|
}
|
||||||
@@ -170,7 +170,7 @@ export function serializeFtool(model: FtoolModel): string {
|
|||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
|
|
||||||
lines.push('; =============================================');
|
lines.push('; =============================================');
|
||||||
lines.push('; VentoApp — Modelo Ftool');
|
lines.push('; BrainWind v1.0 — Modelo Ftool');
|
||||||
lines.push(`; Gerado em: ${new Date().toISOString()}`);
|
lines.push(`; Gerado em: ${new Date().toISOString()}`);
|
||||||
lines.push('; NBR 6123:2023 — Forças devidas ao vento');
|
lines.push('; NBR 6123:2023 — Forças devidas ao vento');
|
||||||
lines.push('; =============================================');
|
lines.push('; =============================================');
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const GenericReportDocument = ({ moduleName, sections, wind, sceneImage }: Gener
|
|||||||
<Document>
|
<Document>
|
||||||
<Page size="A4" style={styles.page}>
|
<Page size="A4" style={styles.page}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={styles.title}>VentoApp — Memória de Cálculo</Text>
|
<Text style={styles.title}>BrainWind v1.0 — Memória de Cálculo</Text>
|
||||||
<Text style={styles.subtitle}>Cargas de Vento: {moduleName} — NBR 6123:2023</Text>
|
<Text style={styles.subtitle}>Cargas de Vento: {moduleName} — NBR 6123:2023</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ const GenericReportDocument = ({ moduleName, sections, wind, sceneImage }: Gener
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<Text style={styles.footer}>
|
<Text style={styles.footer}>
|
||||||
Gerado por VentoApp — Ferramenta de Auxílio ao Cálculo Estrutural (NBR 6123:2023)
|
Gerado por BrainWind v1.0 — Ferramenta de Auxílio ao Cálculo Estrutural (NBR 6123:2023)
|
||||||
</Text>
|
</Text>
|
||||||
</Page>
|
</Page>
|
||||||
</Document>
|
</Document>
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
|
|||||||
<Document>
|
<Document>
|
||||||
<Page size="A4" style={styles.page}>
|
<Page size="A4" style={styles.page}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={styles.title}>VentoApp — Memória de Cálculo</Text>
|
<Text style={styles.title}>BrainWind v1.0 — Memória de Cálculo</Text>
|
||||||
<Text style={styles.subtitle}>Cargas de Vento em Galpão — NBR 6123:2023</Text>
|
<Text style={styles.subtitle}>Cargas de Vento em Galpão — NBR 6123:2023</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -282,7 +282,7 @@ const ReportDocument = ({ galpao, wind, sceneImage }: ReportProps) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Text style={styles.footer}>
|
<Text style={styles.footer}>
|
||||||
Gerado por VentoApp — Ferramenta de Auxílio ao Cálculo Estrutural (NBR 6123:2023)
|
Gerado por BrainWind v1.0 — Ferramenta de Auxílio ao Cálculo Estrutural (NBR 6123:2023)
|
||||||
</Text>
|
</Text>
|
||||||
</Page>
|
</Page>
|
||||||
</Document>
|
</Document>
|
||||||
|
|||||||
+29
-1
@@ -43,7 +43,7 @@ type Dict = Record<string, Record<Locale, string>>;
|
|||||||
|
|
||||||
const translations: Dict = {
|
const translations: Dict = {
|
||||||
// === Aplicação ===
|
// === Aplicação ===
|
||||||
app_title: { 'pt-BR': 'VentoApp', 'en-US': 'VentoApp' },
|
app_title: { 'pt-BR': 'BrainWind v1.0', 'en-US': 'BrainWind v1.0' },
|
||||||
app_subtitle: { 'pt-BR': 'Cálculo de cargas de vento — NBR 6123:2023', 'en-US': 'Wind load calculation — NBR 6123:2023' },
|
app_subtitle: { 'pt-BR': 'Cálculo de cargas de vento — NBR 6123:2023', 'en-US': 'Wind load calculation — NBR 6123:2023' },
|
||||||
app_loading: { 'pt-BR': 'Carregando...', 'en-US': 'Loading...' },
|
app_loading: { 'pt-BR': 'Carregando...', 'en-US': 'Loading...' },
|
||||||
|
|
||||||
@@ -177,6 +177,34 @@ const translations: Dict = {
|
|||||||
ftool_sign_convention: { 'pt-BR': 'Sinal de carga: positivo = na direção positiva do eixo Y (empuxo). Cargas de coluna em GlobalX (horizontal).', 'en-US': 'Load sign: positive = in the positive Y-axis direction (pressure). Column loads on GlobalX (horizontal).' },
|
ftool_sign_convention: { 'pt-BR': 'Sinal de carga: positivo = na direção positiva do eixo Y (empuxo). Cargas de coluna em GlobalX (horizontal).', 'en-US': 'Load sign: positive = in the positive Y-axis direction (pressure). Column loads on GlobalX (horizontal).' },
|
||||||
ftool_download: { 'pt-BR': 'Baixar galpao_ftool.txt', 'en-US': 'Download galpao_ftool.txt' },
|
ftool_download: { 'pt-BR': 'Baixar galpao_ftool.txt', 'en-US': 'Download galpao_ftool.txt' },
|
||||||
|
|
||||||
|
// === Auditoria LLM ===
|
||||||
|
audit_title: { 'pt-BR': 'Auditoria LLM', 'en-US': 'LLM Audit' },
|
||||||
|
audit_desc: { 'pt-BR': 'Validação dos cálculos de vento usando LLMs sequenciais. 100+ cenários cobrindo todos os módulos.', 'en-US': 'Wind calculation validation using sequential LLMs. 100+ scenarios covering all modules.' },
|
||||||
|
audit_run: { 'pt-BR': 'Executar Auditoria', 'en-US': 'Run Audit' },
|
||||||
|
audit_cancel: { 'pt-BR': 'Cancelar', 'en-US': 'Cancel' },
|
||||||
|
audit_export_pdf: { 'pt-BR': 'Exportar PDF', 'en-US': 'Export PDF' },
|
||||||
|
audit_provider: { 'pt-BR': 'Provedor', 'en-US': 'Provider' },
|
||||||
|
audit_model: { 'pt-BR': 'Modelo', 'en-US': 'Model' },
|
||||||
|
audit_apikey: { 'pt-BR': 'API Key', 'en-US': 'API Key' },
|
||||||
|
audit_baseurl: { 'pt-BR': 'Base URL (opcional)', 'en-US': 'Base URL (optional)' },
|
||||||
|
audit_generating: { 'pt-BR': 'Gerando cenários...', 'en-US': 'Generating scenarios...' },
|
||||||
|
audit_running: { 'pt-BR': 'Processando cenários...', 'en-US': 'Running scenarios...' },
|
||||||
|
audit_progress: { 'pt-BR': '{current} de {total}', 'en-US': '{current} of {total}' },
|
||||||
|
audit_no_apikey: { 'pt-BR': 'Informe uma API key para continuar.', 'en-US': 'Provide an API key to continue.' },
|
||||||
|
audit_results: { 'pt-BR': 'Resultados', 'en-US': 'Results' },
|
||||||
|
audit_summary: { 'pt-BR': '{passed}/{total} PASS — W:{warnings} F:{failed}', 'en-US': '{passed}/{total} PASS — W:{warnings} F:{failed}' },
|
||||||
|
audit_total: { 'pt-BR': 'Total', 'en-US': 'Total' },
|
||||||
|
audit_module: { 'pt-BR': 'Módulo', 'en-US': 'Module' },
|
||||||
|
audit_click_module: { 'pt-BR': 'Clique em um módulo para ver detalhes.', 'en-US': 'Click a module to view details.' },
|
||||||
|
audit_checks: { 'pt-BR': 'Verificações', 'en-US': 'Checks' },
|
||||||
|
audit_expected: { 'pt-BR': 'Esperado', 'en-US': 'Expected' },
|
||||||
|
audit_actual: { 'pt-BR': 'Obtido', 'en-US': 'Actual' },
|
||||||
|
audit_inputs: { 'pt-BR': 'Entradas', 'en-US': 'Inputs' },
|
||||||
|
audit_detail_title: { 'pt-BR': 'Detalhes do Cenário', 'en-US': 'Scenario Details' },
|
||||||
|
audit_copy_module: { 'pt-BR': 'Copiar para o módulo', 'en-US': 'Copy to module' },
|
||||||
|
audit_close: { 'pt-BR': 'Fechar', 'en-US': 'Close' },
|
||||||
|
audit_export_popup: { 'pt-BR': 'Exportar PDF deste cenário', 'en-US': 'Export PDF of this scenario' },
|
||||||
|
|
||||||
// === Home (App.tsx) ===
|
// === Home (App.tsx) ===
|
||||||
home_full_coverage: { 'pt-BR': 'Cobertura completa da norma', 'en-US': 'Full standard coverage' },
|
home_full_coverage: { 'pt-BR': 'Cobertura completa da norma', 'en-US': 'Full standard coverage' },
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { calculateCircleBarForce, reynoldsBar } from '@/lib/nbr-tables/table-27'
|
|||||||
import Bar3DViewer from '@/components/three/Bar3D';
|
import Bar3DViewer from '@/components/three/Bar3D';
|
||||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||||
import ExportMenu from '../components/ExportMenu';
|
import ExportMenu from '../components/ExportMenu';
|
||||||
|
import { EducationalManual } from '@/components/EducationalManual';
|
||||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||||
|
|
||||||
const BarSelectorModule: React.FC = () => {
|
const BarSelectorModule: React.FC = () => {
|
||||||
@@ -157,6 +158,9 @@ const BarSelectorModule: React.FC = () => {
|
|||||||
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
||||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">α = {alpha}°</Badge>
|
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">α = {alpha}°</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute top-4 right-4 z-10">
|
||||||
|
<EducationalManual type="bar" params={{ barType, section, alpha }} />
|
||||||
|
</div>
|
||||||
<Bar3DViewer
|
<Bar3DViewer
|
||||||
barType={barType}
|
barType={barType}
|
||||||
section={barType === 'flat' ? section : undefined}
|
section={barType === 'flat' ? section : undefined}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { classifyBridge, calculateBridgeDeckForces, flutterCheck, gallopingCheck
|
|||||||
import Bridge3DViewer from '@/components/three/Bridge3D';
|
import Bridge3DViewer from '@/components/three/Bridge3D';
|
||||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||||
import ExportMenu from '../components/ExportMenu';
|
import ExportMenu from '../components/ExportMenu';
|
||||||
|
import { EducationalManual } from '@/components/EducationalManual';
|
||||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||||
|
|
||||||
const BridgeModule: React.FC = () => {
|
const BridgeModule: React.FC = () => {
|
||||||
@@ -149,6 +150,9 @@ const BridgeModule: React.FC = () => {
|
|||||||
Classe {classification.bridgeClass}
|
Classe {classification.bridgeClass}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute top-4 right-4 z-10">
|
||||||
|
<EducationalManual type="bridge" params={{ alpha }} />
|
||||||
|
</div>
|
||||||
<Bridge3DViewer
|
<Bridge3DViewer
|
||||||
lp={lp}
|
lp={lp}
|
||||||
width={width}
|
width={width}
|
||||||
@@ -158,6 +162,7 @@ const BridgeModule: React.FC = () => {
|
|||||||
cz={forces.cz}
|
cz={forces.cz}
|
||||||
fxPerLength={forces.fxPerLength}
|
fxPerLength={forces.fxPerLength}
|
||||||
fzPerLength={forces.fzPerLength}
|
fzPerLength={forces.fzPerLength}
|
||||||
|
alpha={alpha}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { calculateCylinder, type CylinderEndType } from '@/lib/modules/cylinder'
|
|||||||
import Cylinder3DViewer from '@/components/three/Cylinder3D';
|
import Cylinder3DViewer from '@/components/three/Cylinder3D';
|
||||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||||
import ExportMenu from '../components/ExportMenu';
|
import ExportMenu from '../components/ExportMenu';
|
||||||
|
import { EducationalManual } from '@/components/EducationalManual';
|
||||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||||
|
|
||||||
const CylinderModule: React.FC = () => {
|
const CylinderModule: React.FC = () => {
|
||||||
@@ -140,6 +141,9 @@ const CylinderModule: React.FC = () => {
|
|||||||
Re = {result.re.toExponential(2)}
|
Re = {result.re.toExponential(2)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute top-4 right-4 z-10">
|
||||||
|
<EducationalManual type="cylinder" params={{ surface, endType, cpi: result.cpi }} />
|
||||||
|
</div>
|
||||||
<div className="w-full h-full bg-muted/20">
|
<div className="w-full h-full bg-muted/20">
|
||||||
<Cylinder3DViewer
|
<Cylinder3DViewer
|
||||||
diameter={diameter}
|
diameter={diameter}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { calculateDome, type DomeType } from '@/lib/modules/dome';
|
|||||||
import Dome3DViewer from '@/components/three/Dome3D';
|
import Dome3DViewer from '@/components/three/Dome3D';
|
||||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||||
import ExportMenu from '../components/ExportMenu';
|
import ExportMenu from '../components/ExportMenu';
|
||||||
|
import { EducationalManual } from '@/components/EducationalManual';
|
||||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||||
|
|
||||||
const DomeModule: React.FC = () => {
|
const DomeModule: React.FC = () => {
|
||||||
@@ -118,6 +119,9 @@ const DomeModule: React.FC = () => {
|
|||||||
Cpi = {cpi.toFixed(2)}
|
Cpi = {cpi.toFixed(2)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute top-4 right-4 z-10">
|
||||||
|
<EducationalManual type="dome" params={{ rise, diameter, cpi }} />
|
||||||
|
</div>
|
||||||
<div className="w-full h-full bg-muted/20">
|
<div className="w-full h-full bg-muted/20">
|
||||||
<Dome3DViewer
|
<Dome3DViewer
|
||||||
diameter={diameter}
|
diameter={diameter}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { TABLE_32 } from '@/lib/nbr-tables/table-32';
|
|||||||
import Dynamics3DViewer from '@/components/three/Dynamics3D';
|
import Dynamics3DViewer from '@/components/three/Dynamics3D';
|
||||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||||
import ExportMenu from '../components/ExportMenu';
|
import ExportMenu from '../components/ExportMenu';
|
||||||
|
import { EducationalManual } from '@/components/EducationalManual';
|
||||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||||
|
|
||||||
const DynamicsModule: React.FC = () => {
|
const DynamicsModule: React.FC = () => {
|
||||||
@@ -256,8 +257,9 @@ const DynamicsModule: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Card className="flex-1 min-h-[400px]">
|
<Card className="flex-1 min-h-[400px]">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3 flex flex-row items-center justify-between">
|
||||||
<CardTitle className="text-base">Visualização 3D</CardTitle>
|
<CardTitle className="text-base">Visualização 3D</CardTitle>
|
||||||
|
<EducationalManual type="dynamics" params={{ fn: freq }} />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="h-full">
|
<CardContent className="h-full">
|
||||||
<Dynamics3DViewer
|
<Dynamics3DViewer
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import ExportMenu from '../components/ExportMenu';
|
import ExportMenu from '../components/ExportMenu';
|
||||||
|
import { EducationalManual } from '@/components/EducationalManual';
|
||||||
|
|
||||||
const GalpaoModule: React.FC = () => {
|
const GalpaoModule: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
@@ -103,8 +104,9 @@ const GalpaoModule: React.FC = () => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute top-4 right-4 z-10">
|
<div className="absolute top-4 right-4 z-10 flex gap-2">
|
||||||
<ExportMenu />
|
<ExportMenu />
|
||||||
|
<EducationalManual type="warehouse" params={{ windAngle, cpi }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute bottom-4 left-4 z-10 flex gap-3 p-3 bg-background/90 backdrop-blur-md border border-border rounded-lg shadow-sm">
|
<div className="absolute bottom-4 left-4 z-10 flex gap-3 p-3 bg-background/90 backdrop-blur-md border border-border rounded-lg shadow-sm">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { calculateIsolatedShedRoof, calculateIsolatedGableRoof, frictionForceIso
|
|||||||
import IsolatedRoof3DViewer from '@/components/three/IsolatedRoof3D';
|
import IsolatedRoof3DViewer from '@/components/three/IsolatedRoof3D';
|
||||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||||
import ExportMenu from '../components/ExportMenu';
|
import ExportMenu from '../components/ExportMenu';
|
||||||
|
import { EducationalManual } from '@/components/EducationalManual';
|
||||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||||
|
|
||||||
const IsolatedRoofModule: React.FC = () => {
|
const IsolatedRoofModule: React.FC = () => {
|
||||||
@@ -164,10 +165,14 @@ const IsolatedRoofModule: React.FC = () => {
|
|||||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">θ = {theta}°</Badge>
|
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">θ = {theta}°</Badge>
|
||||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">{type === 'shed' ? 'Uma água' : 'Duas águas'}</Badge>
|
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">{type === 'shed' ? 'Uma água' : 'Duas águas'}</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute top-4 right-4 z-10">
|
||||||
|
<EducationalManual type="isolated-roof" params={{ theta, type }} />
|
||||||
|
</div>
|
||||||
<IsolatedRoof3DViewer
|
<IsolatedRoof3DViewer
|
||||||
type={type}
|
type={type}
|
||||||
theta={theta}
|
theta={theta}
|
||||||
height={height}
|
height={height}
|
||||||
|
width={width}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
cpeWindward={cpeWindward}
|
cpeWindward={cpeWindward}
|
||||||
cpeLeeward={cpeLeeward}
|
cpeLeeward={cpeLeeward}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Sun, Moon, Laptop, Save, Trash2, Upload, CheckCircle2, AlertCircle } from 'lucide-react';
|
import { Sun, Moon, Laptop, Save, Trash2, Upload, CheckCircle2, AlertCircle, FlaskConical } from 'lucide-react';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { useTheme } from '@/lib/theme';
|
import { useTheme } from '@/lib/theme';
|
||||||
import { useProjects } from '@/lib/hooks/useProjects';
|
import { useProjects } from '@/lib/hooks/useProjects';
|
||||||
import { useWindStore } from '@/store/appStore';
|
import { useWindStore } from '@/store/appStore';
|
||||||
@@ -12,6 +13,7 @@ import {
|
|||||||
readProjectFile,
|
readProjectFile,
|
||||||
type ImportResult,
|
type ImportResult,
|
||||||
} from '@/lib/import-project';
|
} from '@/lib/import-project';
|
||||||
|
import AuditPanel from '@/components/AuditPanel';
|
||||||
|
|
||||||
const SettingsModule: React.FC = () => {
|
const SettingsModule: React.FC = () => {
|
||||||
const { theme, setTheme, effectiveTheme } = useTheme();
|
const { theme, setTheme, effectiveTheme } = useTheme();
|
||||||
@@ -60,8 +62,19 @@ const SettingsModule: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-5xl mx-auto space-y-6 overflow-auto">
|
<div className="p-6 max-w-5xl mx-auto overflow-auto">
|
||||||
<h1 className="text-3xl font-bold">{t('nav_settings')}</h1>
|
<h1 className="text-3xl font-bold mb-6">{t('nav_settings')}</h1>
|
||||||
|
|
||||||
|
<Tabs defaultValue="config" className="w-full">
|
||||||
|
<TabsList className="grid w-full grid-cols-2 mb-6">
|
||||||
|
<TabsTrigger value="config">Configurações</TabsTrigger>
|
||||||
|
<TabsTrigger value="testes">
|
||||||
|
<FlaskConical className="w-4 h-4 mr-1.5" />
|
||||||
|
Testes
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="config" className="space-y-6">
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -207,6 +220,12 @@ const SettingsModule: React.FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="testes" className="space-y-6">
|
||||||
|
<AuditPanel />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { calculateSign } from '@/lib/nbr-tables/table-23';
|
|||||||
import Sign3DViewer from '@/components/three/Sign3D';
|
import Sign3DViewer from '@/components/three/Sign3D';
|
||||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||||
import ExportMenu from '../components/ExportMenu';
|
import ExportMenu from '../components/ExportMenu';
|
||||||
|
import { EducationalManual } from '@/components/EducationalManual';
|
||||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||||
|
|
||||||
const SignModule: React.FC = () => {
|
const SignModule: React.FC = () => {
|
||||||
@@ -118,6 +119,9 @@ const SignModule: React.FC = () => {
|
|||||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">F = {result.forceKN.toFixed(2)} kN</Badge>
|
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">F = {result.forceKN.toFixed(2)} kN</Badge>
|
||||||
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">Cf = {result.cf.toFixed(2)}</Badge>
|
<Badge variant="outline" className="bg-background/80 backdrop-blur-sm">Cf = {result.cf.toFixed(2)}</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute top-4 right-4 z-10">
|
||||||
|
<EducationalManual type="sign" params={{ length, height, alpha, hasEndPlates }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Sign3DViewer
|
<Sign3DViewer
|
||||||
length={length}
|
length={length}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { calculateTower, type TowerSection, type TowerBarType } from '@/lib/modu
|
|||||||
import Tower3DViewer from '@/components/three/Tower3D';
|
import Tower3DViewer from '@/components/three/Tower3D';
|
||||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||||
import ExportMenu from '../components/ExportMenu';
|
import ExportMenu from '../components/ExportMenu';
|
||||||
|
import { EducationalManual } from '@/components/EducationalManual';
|
||||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||||
|
|
||||||
const TowerModule: React.FC = () => {
|
const TowerModule: React.FC = () => {
|
||||||
@@ -141,6 +142,9 @@ const TowerModule: React.FC = () => {
|
|||||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">{section === 'square' ? 'Quadrada' : 'Triangular'}</Badge>
|
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">{section === 'square' ? 'Quadrada' : 'Triangular'}</Badge>
|
||||||
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">α = {alphaWind}°</Badge>
|
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">α = {alphaWind}°</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute top-4 right-4 z-10">
|
||||||
|
<EducationalManual type="tower" params={{ section, alphaWind }} />
|
||||||
|
</div>
|
||||||
<Tower3DViewer
|
<Tower3DViewer
|
||||||
section={section}
|
section={section}
|
||||||
barType={barType}
|
barType={barType}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { calculateVault, type VaultRegime } from '@/lib/modules/vault';
|
|||||||
import Vault3DViewer from '@/components/three/Vault3D';
|
import Vault3DViewer from '@/components/three/Vault3D';
|
||||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||||
import ExportMenu from '../components/ExportMenu';
|
import ExportMenu from '../components/ExportMenu';
|
||||||
|
import { EducationalManual } from '@/components/EducationalManual';
|
||||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||||
|
|
||||||
const VaultModule: React.FC = () => {
|
const VaultModule: React.FC = () => {
|
||||||
@@ -126,6 +127,9 @@ const VaultModule: React.FC = () => {
|
|||||||
Cpi = {localCpi > 0 ? `+${localCpi.toFixed(2)}` : localCpi.toFixed(2)}
|
Cpi = {localCpi > 0 ? `+${localCpi.toFixed(2)}` : localCpi.toFixed(2)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute top-4 right-4 z-10">
|
||||||
|
<EducationalManual type="vault" params={{ rise, span, regime, localCpi }} />
|
||||||
|
</div>
|
||||||
<div className="w-full h-full bg-muted/20">
|
<div className="w-full h-full bg-muted/20">
|
||||||
<Vault3DViewer span={span} length={length} rise={rise} cpi={localCpi} cpeProfile={{ ...result.windPerpendicular }} />
|
<Vault3DViewer span={span} length={length} rise={rise} cpi={localCpi} cpeProfile={{ ...result.windPerpendicular }} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user