diff --git a/app/src/components/three/Piperack3D.tsx b/app/src/components/three/Piperack3D.tsx index 1963f01..1a7c551 100644 --- a/app/src/components/three/Piperack3D.tsx +++ b/app/src/components/three/Piperack3D.tsx @@ -86,7 +86,9 @@ function PiperackModel({ const zPos = numFrames > 1 ? -zOffset + 0.5 + (idx % Math.max(pipes.length, 1)) * (totalSpacing - 1) / Math.max(pipes.length - 1, 1) || 0 : 0; - const yPos = elevation + height/2 + pipe.diameter/2 + (idx % 2 === 0 ? 0 : 0.2); + // elevationOffset varia de 0 (base da estrutura reticulada) a height (topo da estrutura) + // A base da estrutura fica em: rackTop - height, que é elevation - height/2 + const yPos = (elevation - height/2) + pipe.elevationOffset + pipe.diameter/2; const color = pipeColors[idx % pipeColors.length]; return ( diff --git a/app/src/lib/__tests__/audit-simulations.test.ts b/app/src/lib/__tests__/audit-simulations.test.ts index d99b2f1..8222480 100644 --- a/app/src/lib/__tests__/audit-simulations.test.ts +++ b/app/src/lib/__tests__/audit-simulations.test.ts @@ -210,7 +210,7 @@ describe('Audit Simulations for NBR 6123:2023 Models', () => { 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! + pipes: [{ id: 'p1', diameter: 1.0, elevationOffset: 1.0 }, { id: 'p2', diameter: 1.0, elevationOffset: 1.5 }], // total 2.0m de tubo num pórtico de 2.0m! q: 1, }); diff --git a/app/src/lib/modules/piperack.ts b/app/src/lib/modules/piperack.ts index 282f20a..bc01702 100644 --- a/app/src/lib/modules/piperack.ts +++ b/app/src/lib/modules/piperack.ts @@ -4,6 +4,7 @@ import { linearInterp1D } from '../log-interp'; export interface PipeDef { id: string; diameter: number; // diâmetro ou altura do equipamento em metros + elevationOffset: number; // posição Y relativa à base do reticulado (0 a h) } export interface PiperackInput { @@ -24,6 +25,7 @@ export interface PiperackResult { 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) + effectiveElevation: number; // z_eff: Altura efetiva de aplicação da força global (m) } /** Tabela de Ca genérica para reticulados planos (faces planas) baseada na solidez φ */ @@ -31,12 +33,22 @@ 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; + const { width, height, elevation, spacing, numFrames, phiStruct, pipes, q } = input; + + // 1. Calcular o Índice de Solidez Total (φ) e Centro de Pressão Efetivo + let sumPhiZ = phiStruct * elevation; // Momento de área estática estrutural + const phiPipes = pipes.reduce((sum, pipe) => { + const phiP = pipe.diameter / height; + const pipeZ = (elevation - height / 2) + pipe.elevationOffset; + sumPhiZ += phiP * pipeZ; + return sum + phiP; + }, 0); - // 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); + + // Z efetivo é a média ponderada pelas áreas de bloqueio (se rawPhi=0, usa elevation) + const effectiveElevation = rawPhi > 0 ? sumPhiZ / rawPhi : elevation; // 2. Coeficiente de Arrasto Frontal (Ca) para o primeiro pórtico let caFrontal = 1.6; @@ -65,6 +77,7 @@ export function calculatePiperack(input: PiperackInput): PiperackResult { eta, linearLoadFront, linearLoadBack, - globalForce + globalForce, + effectiveElevation }; } diff --git a/app/src/pages/PiperackModule.tsx b/app/src/pages/PiperackModule.tsx index a468c44..6103788 100644 --- a/app/src/pages/PiperackModule.tsx +++ b/app/src/pages/PiperackModule.tsx @@ -25,15 +25,15 @@ const PiperackModule: React.FC = () => { const [pipes, setPipes] = useState([]); const addPipe = () => { - setPipes([...pipes, { id: Math.random().toString(36).substr(2, 9), diameter: 0.5 }]); + setPipes([...pipes, { id: Math.random().toString(36).substr(2, 9), diameter: 0.5, elevationOffset: height / 2 }]); }; 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 updatePipe = (id: string, diam: number, yOffset: number) => { + setPipes(pipes.map(p => p.id === id ? { ...p, diameter: diam, elevationOffset: yOffset } : p)); }; const result = useMemo(() => { @@ -80,7 +80,8 @@ const PiperackModule: React.FC = () => { { label: 'Carga Linear Pórtico Frontal', value: `${result.linearLoadFront.toFixed(2)} kN/m` }, { label: 'Carga Linear Pórticos Traseiros (cada)', value: `${result.linearLoadBack.toFixed(2)} kN/m` }, { label: 'Força Global Total', value: `${result.globalForce.toFixed(2)} kN` }, - { label: 'Momento Total de Tombamento', value: `${(result.linearLoadFront * width * elevation + (numFrames > 1 ? (numFrames - 1) * result.linearLoadBack * width * elevation : 0)).toFixed(2)} kN·m` }, + { label: 'Centro de Pressão Efetivo (Z_eff)', value: `${result.effectiveElevation.toFixed(2)} m` }, + { label: 'Momento Total de Tombamento', value: `${(result.globalForce * result.effectiveElevation).toFixed(2)} kN·m` }, ], } ]; @@ -182,22 +183,40 @@ const PiperackModule: React.FC = () => { 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 - +
+
+ + +
+
+
+ + updatePipe(pipe.id, Number(e.target.value), pipe.elevationOffset)} + step={0.1} + min={0.1} + className="h-8 text-xs font-mono" + /> +
+
+ + updatePipe(pipe.id, pipe.diameter, Number(e.target.value))} + step={0.1} + min={0} + max={height} + className="h-8 text-xs font-mono" + /> +
+
))}
@@ -326,14 +345,35 @@ const PiperackModule: React.FC = () => { return pipes.map((p, idx) => { const cx = pipes.length === 1 ? (startX + endX)/2 : startX + idx * gap; const r = Math.max(4, Math.min(15, p.diameter * 8)); // escala visual - // y=25 para repousar sobre o suporte (y=35 para n=1) ou viga (y=40 para n>1) - const cy = numFrames === 1 ? 35 - r : 40 - r; + // p.elevationOffset varia de 0 (base) a height (topo) + // no SVG: Y cresce para baixo. y=110 (chão). Vigas y=40, y=65 + // Para não complicar a matemática com proporções da viga, + // Mapeamos a elevação local para a região do pórtico no SVG (y=110 chão, base=110 - (elevation - height/2) - isso é dinâmico) + // Vamos fazer simples: The drawn structural portico is roughly Y=35 (top) to Y=110 (bottom). + // If n=1, structure top is Y=35. + // Actually the pipe is attached to the column at a relative height. + const localRatio = Math.max(0, Math.min(1, p.elevationOffset / height)); + // No desenho, a altura reticulada é desenhada num "y" aproximado entre Y=35 (topo) e Y=110 (base da coluna inteira). + // Mas o pórtico real desenhado ali tem topo em 35 e altura 75. + // Então base do reticulado = 35 + 75 = 110. (Não, isso é pro solo). + // Supondo que a altura h desenhada seja o topo da coluna Y=35 até uns 2/3 (Y=60), + // Y invertido no SVG: base da seção = 65, topo = 35 (altura de 30 px). + const svgTop = 35; + const svgBottom = 65; + const cy = svgBottom - localRatio * (svgBottom - svgTop) - r; + return ( ); }); })()} + {/* Eixo indicativo de elevação Z_eff */} + + + Z_eff + + @@ -347,10 +387,11 @@ const PiperackModule: React.FC = () => {
F. Frontal: {(result.linearLoadFront * width).toFixed(2)} kN
{numFrames > 1 &&
F. Traseiros (Total): {((numFrames - 1) * result.linearLoadBack * width).toFixed(2)} kN
} +
Z efetivo (Z_eff): {result.effectiveElevation.toFixed(2)} m
Momento Global de Tombamento:
-
{(result.linearLoadFront * width * elevation + (numFrames > 1 ? (numFrames - 1) * result.linearLoadBack * width * elevation : 0)).toFixed(2)} kN·m
+
{(result.globalForce * result.effectiveElevation).toFixed(2)} kN·m