diff --git a/PROGRESS.md b/PROGRESS.md index 2ae0c80..1d828fc 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -15,12 +15,12 @@ | Item | Valor | |------|-------| | 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 ✅ + M9.14 ✅ + M9.15 ✅ + M9.16 ✅ | +| 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 ✅ + M9.17 ✅ | | Cobertura NBR 6123:2023 | **100% das 11 seções + 3 anexos** | | Linhas de código TS/TSX | ~8.200 | | Arquivos `.ts`/`.tsx` no `src/` | **120** | -| Páginas (rotas) | **10** | -| Módulos Strategy | **7** | +| Páginas (rotas) | **11** | +| Módulos Strategy | **8** | | Tabelas da norma implementadas | **36 / 36** + 3 anexos (auditadas M9.1) | | Testes Vitest | **316 passando / 316 totais** | | Build de produção | ✅ passa (2645 módulos) | @@ -93,11 +93,12 @@ - `app/src/lib/nbr-tables/table-24-25.ts` — Coberturas isoladas (uma e duas águas) - `app/src/lib/nbr-tables/table-26.ts` — Cx, Cy barras faces planas - `app/src/lib/nbr-tables/table-27.ts` — Ca barras circulares (Re + regime) -- `app/src/lib/nbr-tables/table-28.ts` (via table-26/27) — Fator K +- `app/src/lib/nbr-tables/table-28.ts` (via table-26/27 e Piperack) — Fator η - `app/src/lib/nbr-tables/table-29.ts` — Cf fios/cabos - `app/src/lib/nbr-tables/table-30.ts` — Componentes em faces de torre - `app/src/lib/modules/truss.ts` — Reticulados isolados/múltiplos -- `app/src/pages/SignModule.tsx`, `IsolatedRoofModule.tsx`, `BarSelectorModule.tsx` +- `app/src/lib/modules/piperack.ts` — Módulo especial para Pipe-Racks Industriais +- `app/src/pages/SignModule.tsx`, `IsolatedRoofModule.tsx`, `BarSelectorModule.tsx`, `PiperackModule.tsx` ### Marco 6 — Torres + Pontes **Status:** ✅ Concluído @@ -765,7 +766,7 @@ gráficos SVG inline para variáveis CSS do tema, suportando dark mode. ## 🎯 Próximo passo sugerido -**M9.16 já está completo.** Para próximos passos, considerar: +**M9.17 (Módulo Pipe-Rack) já está completo.** Para próximos passos, considerar: **M9.11 — Testes E2E com Playwright** (~2 dias, médio impacto) — instalar `@playwright/test`, criar testes para fluxos completos (criar projeto galpão → salvar → exportar PDF), integrar ao CI. diff --git a/app/fix_lints.cjs b/app/fix_lints.cjs new file mode 100644 index 0000000..3d623f7 --- /dev/null +++ b/app/fix_lints.cjs @@ -0,0 +1,14 @@ +const fs = require('fs'); + +// Fix Piperack3D.tsx +let p3d = fs.readFileSync('src/components/three/Piperack3D.tsx', 'utf8'); +p3d = p3d.replace("import ExportMenu from '../components/ExportMenu';\n", ''); +p3d = p3d.replace("import FallbackDiagram from '../FallbackDiagram';\n", ''); +p3d = p3d.replace(/label="Vento \(Transversal\)"\s*\/>/g, '/>'); +p3d = p3d.replace('', '}>'); +fs.writeFileSync('src/components/three/Piperack3D.tsx', p3d); + +// Fix PiperackModule.tsx +let pmod = fs.readFileSync('src/pages/PiperackModule.tsx', 'utf8'); +pmod = pmod.replace("import { ExportMenu } from '../components/ExportMenu';", "import ExportMenu from '../components/ExportMenu';"); +fs.writeFileSync('src/pages/PiperackModule.tsx', pmod); diff --git a/app/fix_lints.js b/app/fix_lints.js new file mode 100644 index 0000000..3d623f7 --- /dev/null +++ b/app/fix_lints.js @@ -0,0 +1,14 @@ +const fs = require('fs'); + +// Fix Piperack3D.tsx +let p3d = fs.readFileSync('src/components/three/Piperack3D.tsx', 'utf8'); +p3d = p3d.replace("import ExportMenu from '../components/ExportMenu';\n", ''); +p3d = p3d.replace("import FallbackDiagram from '../FallbackDiagram';\n", ''); +p3d = p3d.replace(/label="Vento \(Transversal\)"\s*\/>/g, '/>'); +p3d = p3d.replace('', '}>'); +fs.writeFileSync('src/components/three/Piperack3D.tsx', p3d); + +// Fix PiperackModule.tsx +let pmod = fs.readFileSync('src/pages/PiperackModule.tsx', 'utf8'); +pmod = pmod.replace("import { ExportMenu } from '../components/ExportMenu';", "import ExportMenu from '../components/ExportMenu';"); +fs.writeFileSync('src/pages/PiperackModule.tsx', pmod); diff --git a/app/src/App.tsx b/app/src/App.tsx index 43b4578..7addc72 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -10,6 +10,7 @@ import BridgeModule from './pages/BridgeModule'; import DynamicsModule from './pages/DynamicsModule'; import SettingsModule from './pages/SettingsModule'; import TowerModule from './pages/TowerModule'; +import PiperackModule from './pages/PiperackModule'; import { Home, Settings, Menu, Cylinder, Church, CircleDot, Square, Layers, BarChart3, Activity, Warehouse, @@ -83,6 +84,7 @@ function AppLayout({ children }: { children: React.ReactNode }) { { path: '/barras', icon: , labelKey: 'nav_bar' as const }, { path: '/pontes', icon: , labelKey: 'nav_bridge' as const }, { path: '/torre', icon: , labelKey: 'nav_tower' as const }, + { path: '/piperack', icon: , labelKey: 'nav_piperack' as const }, { path: '/dinamica', icon: , labelKey: 'nav_dynamics' as const }, ]; @@ -276,6 +278,7 @@ function HomeMock() { { to: '/barras', icon: BarChart3, label: 'Barras Prismáticas', desc: 'Faces planas (Tab. 26) ou circulares (Tab. 27).' }, { to: '/pontes', icon: BridgeIcon, label: 'Pontes', desc: 'Pse, Cx/Cz do tabuleiro, flutter, galope (sec. 11).' }, { to: '/torre', icon: TowerIcon, label: 'Torres', desc: 'Torres treliçadas e mastros (sec. 8.4).' }, + { to: '/piperack', icon: Layers, label: 'Pipe-Racks Industriais', desc: 'Estruturas reticuladas múltiplas, tubulações, fator η (Tab. 28).' }, ]; return ( @@ -336,6 +339,7 @@ function App() { } /> } /> } /> + } /> } /> } /> diff --git a/app/src/components/SceneCanvas.tsx b/app/src/components/SceneCanvas.tsx index a3ae867..b554703 100644 --- a/app/src/components/SceneCanvas.tsx +++ b/app/src/components/SceneCanvas.tsx @@ -52,6 +52,8 @@ function CanvasInner({ fallback: _, shadows, ...canvasProps }: SceneCanvasProps) } export default function SceneCanvas({ fallback, style, className, moduleId, ...rest }: SceneCanvasProps) { + const { locks, toggleLock } = useViewerStore(); + if (!isWebGLSupported()) { return (
+ {/* Coluna Esquerda */} + + + + + {/* Coluna Direita */} + + + + + {/* Viga Transversal (Reticulado simplificado) */} + + + + + + ); + } + + // Gera as tubulações correndo longitudinalmente + const rackLength = spacing * Math.max(numFrames - 1, 0.5); // Garante comprimento visual mesmo com 1 pórtico + const pipeZCenter = -(spacing * (numFrames - 1)) / 2; + + const pipeMeshes = pipes.map((pipe, idx) => { + // Distribui os tubos ao longo da largura do pórtico + const xPos = -halfW + 0.5 + (idx % Math.max(pipes.length, 1)) * (width - 1) / Math.max(pipes.length - 1, 1) || 0; + const yPos = elevation + (idx % 2 === 0 ? 0.2 : -0.2); // Leve variação de altura + const color = pipeColors[idx % pipeColors.length]; + + return ( + + + + + ); + }); + + return ( + + {frames} + {pipeMeshes} + + {/* Vento batendo lateralmente (transversal ao Pipe-rack) */} + + + ); +} + +export function Piperack3D(props: Piperack3DInput) { + const isDark = useCanvasTheme() === 'dark'; + + return ( + }> + + + + + + + + + + ); +} diff --git a/app/src/lib/__tests__/audit-simulations.test.ts b/app/src/lib/__tests__/audit-simulations.test.ts index be138a5..d99b2f1 100644 --- a/app/src/lib/__tests__/audit-simulations.test.ts +++ b/app/src/lib/__tests__/audit-simulations.test.ts @@ -5,6 +5,7 @@ import { calculateVault } from '../modules/vault'; import { calculateDome } from '../modules/dome'; import { calculateTrussLattice } from '../modules/truss'; import { calculateBridgeDeckForces } from '../modules/bridge'; +import { calculatePiperack } from '../modules/piperack'; import { getWallCpeOfficial, getRoofCpeOfficial } from '../coefficients'; describe('Audit Simulations for NBR 6123:2023 Models', () => { @@ -175,4 +176,49 @@ describe('Audit Simulations for NBR 6123:2023 Models', () => { } expect(anomalies).toBe(0); }); -}); + }); + + it('Simulates Piperack model', () => { + // Cenário 1: Piperack vazio (sem tubos) com 3 pórticos + const res1 = calculatePiperack({ + width: 6, + height: 2, + elevation: 10, + spacing: 6, + numFrames: 3, + phiStruct: 0.2, + pipes: [], + q: 1, // kN/m² + }); + + expect(res1.phiTotal).toBe(0.2); // Sem tubos, mantém a solidez da estrutura + expect(res1.eta).toBe(1.0); // Tabela 28: phi=0.2 e e/h=(6/2)=3. Eta é 1.0. + + // Ca(0.2) = 2.4 + expect(res1.caFrontal).toBe(2.4); + + // Carga linear pórtico frontal = q * (phi * h) * Ca = 1 * (0.2 * 2) * 2.4 = 0.96 kN/m + expect(res1.linearLoadFront).toBeCloseTo(0.96, 2); + // Carga linear traseiros = 0.96 * 1.0 = 0.96 kN/m + expect(res1.linearLoadBack).toBeCloseTo(0.96, 2); + + // Cenário 2: Piperack super denso com tubulações + const res2 = calculatePiperack({ + width: 6, + height: 2, + elevation: 10, + spacing: 6, + numFrames: 3, + phiStruct: 0.2, + pipes: [{ id: 'p1', diameter: 1.0 }, { id: 'p2', diameter: 1.0 }], // total 2.0m de tubo num pórtico de 2.0m! + q: 1, + }); + + // phi pipes = 2.0 / 2.0 = 1.0 + // phi raw = 1.2, saturado em 1.0 + expect(res2.phiTotal).toBe(1.0); + // Ca = extrapolado: 1.6 - (1.0-0.6)*1.0 = 1.2 + expect(res2.caFrontal).toBeCloseTo(1.2, 2); + // Eta: para phi=1.0, e/h=3. Interpolado entre 2.0 e 4.0 (onde é 1.0). Então eta = 1.0. + expect(res2.eta).toBe(1.0); + }); diff --git a/app/src/lib/modules/bridge.ts b/app/src/lib/modules/bridge.ts index 6c4a233..cb2f4e0 100644 --- a/app/src/lib/modules/bridge.ts +++ b/app/src/lib/modules/bridge.ts @@ -47,7 +47,7 @@ export interface BridgeClassificationResult { * Pse = ρ · V_it² / (m · f_v²) */ export function classifyBridge(input: BridgeClassificationInput): BridgeClassificationResult { - const { lp, width, massPerLength, fv, v0, s1, deckHeight, category } = input; + const { massPerLength, fv, v0, s1, deckHeight, category } = input; const { b, p } = getBridgeParams(deckHeight, category); const vit = 0.65 * v0 * s1 * b * Math.pow(deckHeight / 10, p); diff --git a/app/src/lib/modules/piperack.ts b/app/src/lib/modules/piperack.ts new file mode 100644 index 0000000..282f20a --- /dev/null +++ b/app/src/lib/modules/piperack.ts @@ -0,0 +1,70 @@ +import { getEtaTable28 } from '../nbr-tables/table-28'; +import { linearInterp1D } from '../log-interp'; + +export interface PipeDef { + id: string; + diameter: number; // diâmetro ou altura do equipamento em metros +} + +export interface PiperackInput { + width: number; // b: largura do pórtico ortogonal ao vento (m) + height: number; // h: altura do pórtico/seção transversal (m) + elevation: number; // z: elevação média em relação ao solo (m) + spacing: number; // e: distância entre pórticos subsequentes (m) + numFrames: number; // n: número de pórticos paralelos na direção do vento + phiStruct: number; // φ_str: índice de solidez apenas da estrutura metálica + pipes: PipeDef[]; // Lista de tubulações correndo longitudinalmente + q: number; // q: Pressão dinâmica na elevação z (kN/m²) +} + +export interface PiperackResult { + phiTotal: number; + caFrontal: number; + eta: number; + linearLoadFront: number; // kN/m (carga no pórtico 1) + linearLoadBack: number; // kN/m (carga nos pórticos 2..n) + globalForce: number; // kN (força total na estrutura) +} + +/** Tabela de Ca genérica para reticulados planos (faces planas) baseada na solidez φ */ +const PHI_ARRAY = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]; +const CA_FLAT_ARRAY = [2.8, 2.4, 2.0, 1.8, 1.7, 1.6]; + +export function calculatePiperack(input: PiperackInput): PiperackResult { + const { width, height, spacing, numFrames, phiStruct, pipes, q } = input; + + // 1. Calcular o Índice de Solidez Total (φ) + const phiPipes = pipes.reduce((sum, pipe) => sum + pipe.diameter / height, 0); + const rawPhi = phiStruct + phiPipes; + const phiTotal = Math.min(Math.max(rawPhi, 0.1), 1.0); + + // 2. Coeficiente de Arrasto Frontal (Ca) para o primeiro pórtico + let caFrontal = 1.6; + if (phiTotal <= 0.6) { + caFrontal = linearInterp1D(PHI_ARRAY, CA_FLAT_ARRAY, phiTotal); + } else { + caFrontal = 1.6 - (phiTotal - 0.6) * 1.0; + } + + // 3. Fator de proteção (η) para pórticos a sotavento + const eh = spacing / height; + const eta = getEtaTable28(phiTotal, eh); + + // 4. Forças Lineares (kN/m) + const effectiveAreaPerMeter = phiTotal * height; + const linearLoadFront = q * effectiveAreaPerMeter * caFrontal; + const linearLoadBack = linearLoadFront * eta; + + // 5. Força Global (kN) + const totalLinearLoad = linearLoadFront + (numFrames > 1 ? linearLoadBack * (numFrames - 1) : 0); + const globalForce = totalLinearLoad * width; + + return { + phiTotal, + caFrontal, + eta, + linearLoadFront, + linearLoadBack, + globalForce + }; +} diff --git a/app/src/lib/nbr-tables/table-28.ts b/app/src/lib/nbr-tables/table-28.ts new file mode 100644 index 0000000..502dc1a --- /dev/null +++ b/app/src/lib/nbr-tables/table-28.ts @@ -0,0 +1,41 @@ +import { bilinearInterp } from '../bilinear-interp'; + +/** + * Tabela 28 — Fator de proteção η para estruturas reticuladas múltiplas + * (Pórticos paralelos). + * NBR 6123:2023 (correspondente à antiga Tabela 8). + * + * Linhas: Índice de solidez (φ) + * Colunas: Distância relativa (e/h ou e/b) + */ + +const PHI = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 1.0] as const; +const EH = [0.5, 1.0, 2.0, 4.0, 8.0] as const; + +const ETA_DATA: ReadonlyArray> = [ + // e/h = 0.5, 1.0, 2.0, 4.0, 8.0 + [ 1.00, 1.00, 1.00, 1.00, 1.00 ], // φ = 0.1 + [ 0.90, 1.00, 1.00, 1.00, 1.00 ], // φ = 0.2 + [ 0.80, 0.90, 1.00, 1.00, 1.00 ], // φ = 0.3 + [ 0.60, 0.80, 1.00, 1.00, 1.00 ], // φ = 0.4 + [ 0.45, 0.70, 1.00, 1.00, 1.00 ], // φ = 0.5 + [ 0.30, 0.60, 1.00, 1.00, 1.00 ], // φ = 0.6 + [ 0.30, 0.60, 1.00, 1.00, 1.00 ], // φ = 1.0 (mantido para saturação) +]; + +/** + * Retorna o fator de proteção η. + * @param phi Índice de solidez (0 a 1) + * @param eh Razão entre a distância e a menor dimensão ortogonal ao vento (e/h ou e/b) + */ +export function getEtaTable28(phi: number, eh: number): number { + return bilinearInterp( + { + xs: EH as unknown as number[], + ys: PHI as unknown as number[], + values: ETA_DATA as unknown as (readonly number[])[], + }, + eh, + phi + ); +} diff --git a/app/src/pages/PiperackModule.tsx b/app/src/pages/PiperackModule.tsx new file mode 100644 index 0000000..400a7db --- /dev/null +++ b/app/src/pages/PiperackModule.tsx @@ -0,0 +1,246 @@ +import React, { useMemo, useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Slider } from '@/components/ui/slider'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { useWindStore } from '@/store/appStore'; +import { calculatePiperack, type PipeDef } from '@/lib/modules/piperack'; +import { Piperack3D } from '@/components/three/Piperack3D'; +import SceneCapturePanel from '../components/SceneCapturePanel'; +import ExportMenu from '../components/ExportMenu'; +import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf'; +import { Plus, Trash2, Layers } from 'lucide-react'; +import { Input } from '@/components/ui/input'; + +const PiperackModule: React.FC = () => { + const { q } = useWindStore(); + const [width, setWidth] = useState(6); + const [height, setHeight] = useState(2.5); + const [elevation, setElevation] = useState(8); + const [spacing, setSpacing] = useState(6); + const [numFrames, setNumFrames] = useState(3); + const [phiStruct, setPhiStruct] = useState(0.2); + const [pipes, setPipes] = useState([]); + + const addPipe = () => { + setPipes([...pipes, { id: Math.random().toString(36).substr(2, 9), diameter: 0.5 }]); + }; + + const removePipe = (id: string) => { + setPipes(pipes.filter(p => p.id !== id)); + }; + + const updatePipe = (id: string, diam: number) => { + setPipes(pipes.map(p => p.id === id ? { ...p, diameter: diam } : p)); + }; + + const result = useMemo(() => { + return calculatePiperack({ + width, + height, + elevation, + spacing, + numFrames, + phiStruct, + pipes, + q, + }); + }, [width, height, elevation, spacing, numFrames, phiStruct, pipes, q]); + + const handleExportPDF = () => { + const sections: GenericPDFSection[] = [ + { + title: 'Geometria do Pipe-Rack', + type: 'grid', + gridItems: [ + { label: 'Largura do pórtico (b)', value: `${width} m` }, + { label: 'Altura do pórtico (h)', value: `${height} m` }, + { label: 'Elevação média (z)', value: `${elevation} m` }, + { label: 'Espaçamento (e)', value: `${spacing} m` }, + { label: 'Número de pórticos (n)', value: String(numFrames) }, + { label: 'φ (estrutural)', value: phiStruct.toFixed(2) }, + { label: 'Quantidade de Tubos', value: String(pipes.length) }, + ], + }, + { + title: 'Coeficientes Aerodinâmicos', + type: 'grid', + gridItems: [ + { label: 'φ total (estrutura + tubos)', value: result.phiTotal.toFixed(2) }, + { label: 'Ca frontal', value: result.caFrontal.toFixed(2) }, + { label: 'Fator proteção η', value: result.eta.toFixed(2) }, + ], + }, + { + title: 'Forças e Cargas', + type: 'grid', + gridItems: [ + { label: 'Carga Linear Pórtico Frontal', value: `${result.linearLoadFront.toFixed(2)} kN/m` }, + { label: 'Carga Linear Pórtico Traseiros (cada)', value: `${result.linearLoadBack.toFixed(2)} kN/m` }, + { label: 'Força Global Total', value: `${result.globalForce.toFixed(2)} kN` }, + ], + } + ]; + + exportGenericToPDF('Pipe-Racks Industriais', sections); + }; + + return ( +
+
+
+
+ +
+
+

Pipe-Racks Industriais

+

Vento transversal (Sec. 7 - NBR 6123)

+
+
+
+ +
+
+ +
+ {/* Painel Esquerdo: 3D */} +
+ + +
+ + {/* Painel Direito: Controles */} +
+
+ + + + Forças Calculadas + {result.globalForce.toFixed(1)} kN (Global) + + + +
+ φ efetivo + {result.phiTotal.toFixed(2)} +
+
+ Ca frontal (Tab. 26 adaptada) + {result.caFrontal.toFixed(2)} +
+
+ Fator η (Tab. 28) + {result.eta.toFixed(2)} +
+
+ Carga Pórtico 1 + {result.linearLoadFront.toFixed(2)} kN/m +
+
+ Carga Pórticos {numFrames > 1 ? `2 a ${numFrames}` : '-'} + {result.linearLoadBack.toFixed(2)} kN/m +
+
+
+ +
+
+

Geometria da Estrutura

+
+
+
+ Largura (b) + {width} m +
+ setWidth(v[0])} /> +
+
+
+ Altura do pórtico (h) + {height} m +
+ setHeight(v[0])} /> +
+
+
+ Elevação (z) + {elevation} m +
+ setElevation(v[0])} /> +
+
+
+ Espaçamento (e) + {spacing} m +
+ setSpacing(v[0])} /> +
+
+
+ Número de Pórticos (n) + {numFrames} +
+ setNumFrames(v[0])} /> +
+
+
+ Índice de Solidez (φ str) + {phiStruct.toFixed(2)} +
+ setPhiStruct(v[0])} /> +
+
+
+ +
+
+

Tubulações (Bloqueio)

+ +
+ + {pipes.length === 0 ? ( +
+ Nenhuma tubulação adicionada. +
+ ) : ( +
+ {pipes.map((pipe, index) => ( +
+ + updatePipe(pipe.id, Number(e.target.value))} + step={0.1} + min={0.1} + className="h-7 text-xs font-mono" + /> + m + +
+ ))} +
+ )} +
+
+
+
+
+
+ ); +}; + +export default PiperackModule;