🚀 Auto-deploy: BrainWind atualizado em 13/07/2026 15:00:47
This commit is contained in:
@@ -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 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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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