🚀 Auto-deploy: BrainWind atualizado em 13/07/2026 20:57:51
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
<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 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), pipe.elevationOffset)}
|
||||
step={0.1}
|
||||
min={0.1}
|
||||
className="h-8 text-xs font-mono"
|
||||
/>
|
||||
</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">
|
||||
|
||||
Reference in New Issue
Block a user