🚀 Auto-deploy: BrainWind atualizado em 13/07/2026 20:57:51

This commit is contained in:
2026-07-13 20:57:51 +00:00
parent d4e0abe260
commit c184b2211c
4 changed files with 84 additions and 28 deletions
+3 -1
View File
@@ -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 (
@@ -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,
});
+17 -4
View File
@@ -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,13 +33,23 @@ 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;
if (phiTotal <= 0.6) {
@@ -65,6 +77,7 @@ export function calculatePiperack(input: PiperackInput): PiperackResult {
eta,
linearLoadFront,
linearLoadBack,
globalForce
globalForce,
effectiveElevation
};
}
+57 -16
View File
@@ -25,15 +25,15 @@ const PiperackModule: React.FC = () => {
const [pipes, setPipes] = useState<PipeDef[]>([]);
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.
</div>
) : (
<div className="space-y-2">
<div className="space-y-4">
{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>
<div key={pipe.id} className="flex flex-col gap-2 p-3 rounded-md border bg-muted/20">
<div className="flex items-center justify-between">
<label className="text-xs font-semibold text-foreground">Tubo {index + 1}</label>
<Button variant="ghost" size="icon" className="h-6 w-6 text-destructive" onClick={() => removePipe(pipe.id)}>
<Trash2 className="w-3 h-3" />
</Button>
</div>
<div className="flex items-center gap-3">
<div className="flex-1 space-y-1">
<label className="text-[10px] text-muted-foreground uppercase tracking-wider">Diâmetro (m)</label>
<Input
type="number"
value={pipe.diameter}
onChange={(e) => updatePipe(pipe.id, Number(e.target.value))}
onChange={(e) => updatePipe(pipe.id, Number(e.target.value), pipe.elevationOffset)}
step={0.1}
min={0.1}
className="h-7 text-xs font-mono"
className="h-8 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 className="flex-1 space-y-1">
<label className="text-[10px] text-muted-foreground uppercase tracking-wider">Alt. Pórtico (0 a {height})</label>
<Input
type="number"
value={pipe.elevationOffset}
onChange={(e) => updatePipe(pipe.id, pipe.diameter, Number(e.target.value))}
step={0.1}
min={0}
max={height}
className="h-8 text-xs font-mono"
/>
</div>
</div>
</div>
))}
</div>
@@ -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 (
<circle key={p.id} cx={cx} cy={cy} r={r} fill="#3b82f6" fillOpacity="0.8" />
);
});
})()}
{/* Eixo indicativo de elevação Z_eff */}
<g opacity="0.6">
<line x1="20" y1={110 - Math.min(90, (result.effectiveElevation/elevation)*60)} x2="280" y2={110 - Math.min(90, (result.effectiveElevation/elevation)*60)} stroke="#f59e0b" strokeWidth="1" strokeDasharray="3 3" />
<text x="25" y={110 - Math.min(90, (result.effectiveElevation/elevation)*60) - 3} fontSize="8" fill="#d97706" fontWeight="bold">Z_eff</text>
</g>
<defs>
<marker id="arrowRight" markerWidth="6" markerHeight="6" refX="6" refY="3" orient="auto">
<path d="M0,0 L6,3 L0,6" fill="none" stroke="#10b981" strokeWidth="1.5" />
@@ -347,10 +387,11 @@ const PiperackModule: React.FC = () => {
<div className="space-y-1">
<div className="flex justify-between text-muted-foreground"><span>F. Frontal:</span> <span className="font-mono">{(result.linearLoadFront * width).toFixed(2)} kN</span></div>
{numFrames > 1 && <div className="flex justify-between text-muted-foreground"><span>F. Traseiros (Total):</span> <span className="font-mono">{((numFrames - 1) * result.linearLoadBack * width).toFixed(2)} kN</span></div>}
<div className="flex justify-between text-primary font-medium mt-1"><span>Z efetivo (Z_eff):</span> <span className="font-mono">{result.effectiveElevation.toFixed(2)} m</span></div>
</div>
<div className="space-y-1">
<div className="flex justify-between text-primary font-medium"><span>Momento Global de Tombamento:</span></div>
<div className="flex justify-end font-mono font-bold text-base">{(result.linearLoadFront * width * elevation + (numFrames > 1 ? (numFrames - 1) * result.linearLoadBack * width * elevation : 0)).toFixed(2)} kN·m</div>
<div className="flex justify-end font-mono font-bold text-base">{(result.globalForce * result.effectiveElevation).toFixed(2)} kN·m</div>
</div>
</div>
<p className="text-[10px] text-muted-foreground leading-tight text-center">