🚀 Auto-deploy: BrainWind atualizado em 13/07/2026 15:00:47

This commit is contained in:
2026-07-13 15:00:47 +00:00
parent 314c9b7490
commit ab8f89e807
11 changed files with 567 additions and 10 deletions
+7 -6
View File
@@ -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.
+14
View File
@@ -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);
+14
View File
@@ -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);
+4
View File
@@ -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: <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: '/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 },
];
@@ -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() {
<Route path="/barras" element={<BarSelectorModule />} />
<Route path="/pontes" element={<BridgeModule />} />
<Route path="/torre" element={<TowerModule />} />
<Route path="/piperack" element={<PiperackModule />} />
<Route path="/dinamica" element={<DynamicsModule />} />
<Route path="/settings" element={<SettingsModule />} />
</Routes>
+2 -2
View File
@@ -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 (
<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
const isLocked = moduleId ? !!locks[moduleId] : false;
+121
View File
@@ -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 { 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);
});
+1 -1
View File
@@ -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);
+70
View File
@@ -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
};
}
+41
View File
@@ -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
);
}
+246
View File
@@ -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;