🚀 Auto-deploy: BrainWind atualizado em 13/07/2026 15:00:47
This commit is contained in:
+7
-6
@@ -15,12 +15,12 @@
|
|||||||
| Item | Valor |
|
| Item | Valor |
|
||||||
|------|-------|
|
|------|-------|
|
||||||
| Data da última atualização | 2026-07-10 |
|
| 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** |
|
| Cobertura NBR 6123:2023 | **100% das 11 seções + 3 anexos** |
|
||||||
| Linhas de código TS/TSX | ~8.200 |
|
| Linhas de código TS/TSX | ~8.200 |
|
||||||
| Arquivos `.ts`/`.tsx` no `src/` | **120** |
|
| Arquivos `.ts`/`.tsx` no `src/` | **120** |
|
||||||
| Páginas (rotas) | **10** |
|
| Páginas (rotas) | **11** |
|
||||||
| Módulos Strategy | **7** |
|
| Módulos Strategy | **8** |
|
||||||
| Tabelas da norma implementadas | **36 / 36** + 3 anexos (auditadas M9.1) |
|
| Tabelas da norma implementadas | **36 / 36** + 3 anexos (auditadas M9.1) |
|
||||||
| Testes Vitest | **316 passando / 316 totais** |
|
| Testes Vitest | **316 passando / 316 totais** |
|
||||||
| Build de produção | ✅ passa (2645 módulos) |
|
| 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-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-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-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-29.ts` — Cf fios/cabos
|
||||||
- `app/src/lib/nbr-tables/table-30.ts` — Componentes em faces de torre
|
- `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/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
|
### Marco 6 — Torres + Pontes
|
||||||
**Status:** ✅ Concluído
|
**Status:** ✅ Concluído
|
||||||
@@ -765,7 +766,7 @@ gráficos SVG inline para variáveis CSS do tema, suportando dark mode.
|
|||||||
|
|
||||||
## 🎯 Próximo passo sugerido
|
## 🎯 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) —
|
**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.
|
||||||
|
|||||||
@@ -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('<SceneCanvas moduleId="piperack">', '<SceneCanvas moduleId="piperack" fallback={<div />}>');
|
||||||
|
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);
|
||||||
@@ -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('<SceneCanvas moduleId="piperack">', '<SceneCanvas moduleId="piperack" fallback={<div />}>');
|
||||||
|
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);
|
||||||
@@ -10,6 +10,7 @@ import BridgeModule from './pages/BridgeModule';
|
|||||||
import DynamicsModule from './pages/DynamicsModule';
|
import DynamicsModule from './pages/DynamicsModule';
|
||||||
import SettingsModule from './pages/SettingsModule';
|
import SettingsModule from './pages/SettingsModule';
|
||||||
import TowerModule from './pages/TowerModule';
|
import TowerModule from './pages/TowerModule';
|
||||||
|
import PiperackModule from './pages/PiperackModule';
|
||||||
import {
|
import {
|
||||||
Home, Settings, Menu, Cylinder, Church, CircleDot,
|
Home, Settings, Menu, Cylinder, Church, CircleDot,
|
||||||
Square, Layers, BarChart3, Activity, Warehouse,
|
Square, Layers, BarChart3, Activity, Warehouse,
|
||||||
@@ -83,6 +84,7 @@ function AppLayout({ children }: { children: React.ReactNode }) {
|
|||||||
{ path: '/barras', icon: <BarChart3 className="w-5 h-5" />, labelKey: 'nav_bar' as const },
|
{ path: '/barras', icon: <BarChart3 className="w-5 h-5" />, labelKey: 'nav_bar' as const },
|
||||||
{ path: '/pontes', icon: <BridgeIcon className="w-5 h-5" />, labelKey: 'nav_bridge' as const },
|
{ path: '/pontes', icon: <BridgeIcon className="w-5 h-5" />, labelKey: 'nav_bridge' as const },
|
||||||
{ path: '/torre', icon: <TowerIcon className="w-5 h-5" />, labelKey: 'nav_tower' as const },
|
{ path: '/torre', icon: <TowerIcon className="w-5 h-5" />, labelKey: 'nav_tower' as const },
|
||||||
|
{ path: '/piperack', icon: <Layers className="w-5 h-5" />, labelKey: 'nav_piperack' as const },
|
||||||
{ path: '/dinamica', icon: <Activity className="w-5 h-5" />, labelKey: 'nav_dynamics' as const },
|
{ path: '/dinamica', icon: <Activity className="w-5 h-5" />, 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: '/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: '/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: '/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 (
|
return (
|
||||||
@@ -336,6 +339,7 @@ function App() {
|
|||||||
<Route path="/barras" element={<BarSelectorModule />} />
|
<Route path="/barras" element={<BarSelectorModule />} />
|
||||||
<Route path="/pontes" element={<BridgeModule />} />
|
<Route path="/pontes" element={<BridgeModule />} />
|
||||||
<Route path="/torre" element={<TowerModule />} />
|
<Route path="/torre" element={<TowerModule />} />
|
||||||
|
<Route path="/piperack" element={<PiperackModule />} />
|
||||||
<Route path="/dinamica" element={<DynamicsModule />} />
|
<Route path="/dinamica" element={<DynamicsModule />} />
|
||||||
<Route path="/settings" element={<SettingsModule />} />
|
<Route path="/settings" element={<SettingsModule />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ function CanvasInner({ fallback: _, shadows, ...canvasProps }: SceneCanvasProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SceneCanvas({ fallback, style, className, moduleId, ...rest }: SceneCanvasProps) {
|
export default function SceneCanvas({ fallback, style, className, moduleId, ...rest }: SceneCanvasProps) {
|
||||||
|
const { locks, toggleLock } = useViewerStore();
|
||||||
|
|
||||||
if (!isWebGLSupported()) {
|
if (!isWebGLSupported()) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -63,8 +65,6 @@ export default function SceneCanvas({ fallback, style, className, moduleId, ...r
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { locks, toggleLock } = useViewerStore();
|
|
||||||
|
|
||||||
// If no moduleId is provided, use a default key or treat as unlocked globally
|
// If no moduleId is provided, use a default key or treat as unlocked globally
|
||||||
const isLocked = moduleId ? !!locks[moduleId] : false;
|
const isLocked = moduleId ? !!locks[moduleId] : false;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { Grid, Environment } from '@react-three/drei';
|
||||||
|
import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls';
|
||||||
|
import SceneCanvas from '../SceneCanvas';
|
||||||
|
import { WindArrow } from './WindArrow';
|
||||||
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
import type { PipeDef } from '../../lib/modules/piperack';
|
||||||
|
|
||||||
|
export interface Piperack3DInput {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
elevation: number;
|
||||||
|
spacing: number;
|
||||||
|
numFrames: number;
|
||||||
|
pipes: PipeDef[];
|
||||||
|
phiTotal: number;
|
||||||
|
eta: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PiperackModel({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
elevation,
|
||||||
|
spacing,
|
||||||
|
numFrames,
|
||||||
|
pipes,
|
||||||
|
phiTotal,
|
||||||
|
}: Piperack3DInput) {
|
||||||
|
const theme = useCanvasTheme();
|
||||||
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
|
const halfW = width / 2;
|
||||||
|
const rackTop = elevation + height / 2;
|
||||||
|
|
||||||
|
const steelColor = isDark ? '#94a3b8' : '#64748b';
|
||||||
|
const pipeColors = ['#ef4444', '#3b82f6', '#f59e0b', '#10b981', '#8b5cf6'];
|
||||||
|
|
||||||
|
// Gera os pórticos
|
||||||
|
const frames = [];
|
||||||
|
for (let i = 0; i < numFrames; i++) {
|
||||||
|
const z = -i * spacing;
|
||||||
|
frames.push(
|
||||||
|
<group key={`frame-${i}`} position={[0, 0, z]}>
|
||||||
|
{/* Coluna Esquerda */}
|
||||||
|
<mesh position={[-halfW, rackTop / 2, 0]} receiveShadow castShadow>
|
||||||
|
<boxGeometry args={[0.3, rackTop, 0.3]} />
|
||||||
|
<meshStandardMaterial color={steelColor} metalness={0.6} roughness={0.4} />
|
||||||
|
</mesh>
|
||||||
|
{/* Coluna Direita */}
|
||||||
|
<mesh position={[halfW, rackTop / 2, 0]} receiveShadow castShadow>
|
||||||
|
<boxGeometry args={[0.3, rackTop, 0.3]} />
|
||||||
|
<meshStandardMaterial color={steelColor} metalness={0.6} roughness={0.4} />
|
||||||
|
</mesh>
|
||||||
|
{/* Viga Transversal (Reticulado simplificado) */}
|
||||||
|
<mesh position={[0, elevation, 0]} receiveShadow castShadow>
|
||||||
|
<boxGeometry args={[width, height, 0.3]} />
|
||||||
|
<meshStandardMaterial color={steelColor} metalness={0.6} roughness={0.4} transparent opacity={phiTotal} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<mesh key={`pipe-${pipe.id}`} position={[xPos, yPos, pipeZCenter]} rotation={[Math.PI / 2, 0, 0]} receiveShadow castShadow>
|
||||||
|
<cylinderGeometry args={[pipe.diameter / 2, pipe.diameter / 2, rackLength + 1, 16]} />
|
||||||
|
<meshStandardMaterial color={color} metalness={0.3} roughness={0.2} />
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{frames}
|
||||||
|
{pipeMeshes}
|
||||||
|
|
||||||
|
{/* Vento batendo lateralmente (transversal ao Pipe-rack) */}
|
||||||
|
<WindArrow
|
||||||
|
direction={[0, 0, 1]}
|
||||||
|
target={[0, elevation, spacing * 0.5]}
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Piperack3D(props: Piperack3DInput) {
|
||||||
|
const isDark = useCanvasTheme() === 'dark';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SceneCanvas moduleId="piperack" fallback={<div />}>
|
||||||
|
<ambientLight intensity={isDark ? 0.3 : 0.6} />
|
||||||
|
<directionalLight position={[10, 15, 10]} intensity={isDark ? 1 : 1.5} castShadow shadow-bias={-0.001} />
|
||||||
|
<Environment preset="city" />
|
||||||
|
|
||||||
|
<PiperackModel {...props} />
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
args={[40, 40]}
|
||||||
|
position={[0, -0.01, 0]}
|
||||||
|
cellColor={isDark ? '#334155' : '#cbd5e1'}
|
||||||
|
sectionColor={isDark ? '#475569' : '#94a3b8'}
|
||||||
|
fadeDistance={30}
|
||||||
|
/>
|
||||||
|
<OrbitControls
|
||||||
|
minPolarAngle={0}
|
||||||
|
maxPolarAngle={Math.PI / 2 - 0.05}
|
||||||
|
minDistance={5}
|
||||||
|
maxDistance={50}
|
||||||
|
target={[0, props.elevation / 2, -(props.spacing * (props.numFrames - 1)) / 2]}
|
||||||
|
/>
|
||||||
|
</SceneCanvas>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { calculateVault } from '../modules/vault';
|
|||||||
import { calculateDome } from '../modules/dome';
|
import { calculateDome } from '../modules/dome';
|
||||||
import { calculateTrussLattice } from '../modules/truss';
|
import { calculateTrussLattice } from '../modules/truss';
|
||||||
import { calculateBridgeDeckForces } from '../modules/bridge';
|
import { calculateBridgeDeckForces } from '../modules/bridge';
|
||||||
|
import { calculatePiperack } from '../modules/piperack';
|
||||||
import { getWallCpeOfficial, getRoofCpeOfficial } from '../coefficients';
|
import { getWallCpeOfficial, getRoofCpeOfficial } from '../coefficients';
|
||||||
|
|
||||||
describe('Audit Simulations for NBR 6123:2023 Models', () => {
|
describe('Audit Simulations for NBR 6123:2023 Models', () => {
|
||||||
@@ -175,4 +176,49 @@ describe('Audit Simulations for NBR 6123:2023 Models', () => {
|
|||||||
}
|
}
|
||||||
expect(anomalies).toBe(0);
|
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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export interface BridgeClassificationResult {
|
|||||||
* Pse = ρ · V_it² / (m · f_v²)
|
* Pse = ρ · V_it² / (m · f_v²)
|
||||||
*/
|
*/
|
||||||
export function classifyBridge(input: BridgeClassificationInput): BridgeClassificationResult {
|
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 { b, p } = getBridgeParams(deckHeight, category);
|
||||||
const vit = 0.65 * v0 * s1 * b * Math.pow(deckHeight / 10, p);
|
const vit = 0.65 * v0 * s1 * b * Math.pow(deckHeight / 10, p);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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<ReadonlyArray<number>> = [
|
||||||
|
// 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
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<PipeDef[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="flex flex-col h-[calc(100vh-3.5rem)] md:h-screen bg-background">
|
||||||
|
<header className="shrink-0 flex items-center justify-between px-6 py-4 border-b bg-card">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 bg-primary/10 rounded-lg">
|
||||||
|
<Layers className="w-5 h-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-bold tracking-tight">Pipe-Racks Industriais</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">Vento transversal (Sec. 7 - NBR 6123)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<ExportMenu onExportPDF={handleExportPDF} />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden grid lg:grid-cols-[1fr_400px]">
|
||||||
|
{/* Painel Esquerdo: 3D */}
|
||||||
|
<div className="relative min-h-[400px] lg:min-h-0 bg-muted/30">
|
||||||
|
<Piperack3D
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
elevation={elevation}
|
||||||
|
spacing={spacing}
|
||||||
|
numFrames={numFrames}
|
||||||
|
pipes={pipes}
|
||||||
|
phiTotal={result.phiTotal}
|
||||||
|
eta={result.eta}
|
||||||
|
/>
|
||||||
|
<SceneCapturePanel />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Painel Direito: Controles */}
|
||||||
|
<div className="flex flex-col border-l bg-card overflow-y-auto">
|
||||||
|
<div className="p-6 space-y-8">
|
||||||
|
<Card className="shadow-none border-primary/20">
|
||||||
|
<CardHeader className="pb-4">
|
||||||
|
<CardTitle className="text-base flex items-center justify-between">
|
||||||
|
Forças Calculadas
|
||||||
|
<Badge variant="secondary" className="font-mono">{result.globalForce.toFixed(1)} kN (Global)</Badge>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4 text-sm">
|
||||||
|
<div className="flex justify-between items-center pb-2 border-b">
|
||||||
|
<span className="text-muted-foreground">φ efetivo</span>
|
||||||
|
<span className="font-mono font-medium">{result.phiTotal.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center pb-2 border-b">
|
||||||
|
<span className="text-muted-foreground">Ca frontal (Tab. 26 adaptada)</span>
|
||||||
|
<span className="font-mono font-medium">{result.caFrontal.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center pb-2 border-b">
|
||||||
|
<span className="text-muted-foreground">Fator η (Tab. 28)</span>
|
||||||
|
<span className="font-mono font-medium">{result.eta.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center pb-2 border-b">
|
||||||
|
<span className="text-muted-foreground text-primary font-medium">Carga Pórtico 1</span>
|
||||||
|
<span className="font-mono font-bold text-primary">{result.linearLoadFront.toFixed(2)} kN/m</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-muted-foreground text-primary font-medium">Carga Pórticos {numFrames > 1 ? `2 a ${numFrames}` : '-'}</span>
|
||||||
|
<span className="font-mono font-bold text-primary">{result.linearLoadBack.toFixed(2)} kN/m</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium mb-4">Geometria da Estrutura</h3>
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm font-medium">Largura (b)</span>
|
||||||
|
<span className="text-sm text-muted-foreground">{width} m</span>
|
||||||
|
</div>
|
||||||
|
<Slider value={[width]} min={2} max={15} step={0.5} onValueChange={v => setWidth(v[0])} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm font-medium">Altura do pórtico (h)</span>
|
||||||
|
<span className="text-sm text-muted-foreground">{height} m</span>
|
||||||
|
</div>
|
||||||
|
<Slider value={[height]} min={1} max={5} step={0.1} onValueChange={v => setHeight(v[0])} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm font-medium">Elevação (z)</span>
|
||||||
|
<span className="text-sm text-muted-foreground">{elevation} m</span>
|
||||||
|
</div>
|
||||||
|
<Slider value={[elevation]} min={2} max={25} step={1} onValueChange={v => setElevation(v[0])} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm font-medium">Espaçamento (e)</span>
|
||||||
|
<span className="text-sm text-muted-foreground">{spacing} m</span>
|
||||||
|
</div>
|
||||||
|
<Slider value={[spacing]} min={2} max={12} step={0.5} onValueChange={v => setSpacing(v[0])} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm font-medium">Número de Pórticos (n)</span>
|
||||||
|
<span className="text-sm text-muted-foreground">{numFrames}</span>
|
||||||
|
</div>
|
||||||
|
<Slider value={[numFrames]} min={1} max={10} step={1} onValueChange={v => setNumFrames(v[0])} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm font-medium">Índice de Solidez (φ str)</span>
|
||||||
|
<span className="text-sm text-muted-foreground">{phiStruct.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<Slider value={[phiStruct]} min={0.1} max={0.6} step={0.05} onValueChange={v => setPhiStruct(v[0])} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="font-medium">Tubulações (Bloqueio)</h3>
|
||||||
|
<Button variant="outline" size="sm" onClick={addPipe} className="h-7 text-xs">
|
||||||
|
<Plus className="w-3 h-3 mr-1" /> Adicionar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pipes.length === 0 ? (
|
||||||
|
<div className="text-center p-4 border border-dashed rounded-lg text-sm text-muted-foreground">
|
||||||
|
Nenhuma tubulação adicionada.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{pipes.map((pipe, index) => (
|
||||||
|
<div key={pipe.id} className="flex items-center gap-2 p-2 rounded-md border bg-muted/20">
|
||||||
|
<label className="w-20 text-xs text-muted-foreground shrink-0">Tubo {index + 1}</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={pipe.diameter}
|
||||||
|
onChange={(e) => updatePipe(pipe.id, Number(e.target.value))}
|
||||||
|
step={0.1}
|
||||||
|
min={0.1}
|
||||||
|
className="h-7 text-xs font-mono"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground">m</span>
|
||||||
|
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive shrink-0" onClick={() => removePipe(pipe.id)}>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PiperackModule;
|
||||||
Reference in New Issue
Block a user