Files
BrainWind/app/src/pages/PiperackModule.tsx
T

504 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useMemo, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { useHydrationStore } from '@/store/hydrationStore';
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
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 { isResonantRisk } from '@/lib/dynamics';
import { Piperack3D } from '@/components/three/Piperack3D';
import SceneCapturePanel from '../components/SceneCapturePanel';
import ExportMenu from '../components/ExportMenu';
import { EducationalManual } from '@/components/EducationalManual';
import { WindParametersSummary } from '@/components/WindParametersSummary';
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
import { Plus, Trash2, Info, AlertTriangle } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/components/ui/tooltip';
import { Input } from '@/components/ui/input';
import { usePersistentState } from '@/hooks/usePersistentState';
const PiperackModule: React.FC = () => {
const { q } = useWindStore();
const [width, setWidth] = usePersistentState('PiperackModule_width', 17);
const [height, setHeight] = usePersistentState('PiperackModule_height', 2);
const [elevation, setElevation] = usePersistentState('PiperackModule_elevation', 6);
const [spacing, setSpacing] = usePersistentState('PiperackModule_spacing', 2);
const [numFrames, setNumFrames] = usePersistentState('PiperackModule_numFrames', 2);
const [phiStruct, setPhiStruct] = usePersistentState('PiperackModule_phiStruct', 0.5);
const [f1, setF1] = usePersistentState('PiperackModule_f1', 1.5); // Frequência natural (Hz)
const [pipes, setPipes] = usePersistentState<PipeDef[]>('PiperackModule_pipes', []);
const pendingLoad = useHydrationStore((s) => s.pendingLoad);
const clearPendingLoad = useHydrationStore((s) => s.clearPendingLoad);
React.useEffect(() => {
if (pendingLoad && pendingLoad.module === 'piperack') {
const p = pendingLoad.inputs as any;
if (p.elevation !== undefined) setElevation(p.elevation);
if (p.width !== undefined) setWidth(p.width);
if (p.height !== undefined) setHeight(p.height);
if (p.spacing !== undefined) setSpacing(p.spacing);
if (p.numFrames !== undefined) setNumFrames(p.numFrames);
if (p.pipes !== undefined) setPipes(p.pipes);
if (p.phiStruct !== undefined) setPhiStruct(p.phiStruct);
if (p.f1 !== undefined) setF1(p.f1);
clearPendingLoad();
}
}, [pendingLoad, clearPendingLoad]);
const addPipe = () => {
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, yOffset: number) => {
setPipes(pipes.map(p => p.id === id ? { ...p, diameter: diam, elevationOffset: yOffset } : 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: 'Solidez estrutural', value: phiStruct.toFixed(2) },
{ label: 'Quantidade de Tubos', value: String(pipes.length) },
],
},
{
title: 'Coeficientes Aerodinâmicos',
type: 'grid',
gridItems: [
{ label: 'Solidez total (est. + tubos)', value: result.phiTotal.toFixed(2) },
{ label: 'Ca frontal', value: result.caFrontal.toFixed(2) },
{ label: 'Fator de proteção', value: result.eta.toFixed(2) },
],
},
{
title: 'Forças e Momentos Globais',
type: 'grid',
gridItems: [
{ 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: '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` },
],
},
{
title: 'Demonstração dos Passos Matemáticos — Pipe-Racks Industriais',
type: 'math-card',
mathSteps: [
{
title: '1. Solidez Combinada (φ_total) e Coeficiente Frontal (Ca)',
formula: 'φ_total = φ_estrutura + φ_tubulações -> Ca_frontal = f(φ_total)',
calculation: `φ_estrutural (${phiStruct.toFixed(2)}) + φ_tubos = ${result.phiTotal.toFixed(2)} -> Ca frontal = ${result.caFrontal.toFixed(2)}`,
result: `Ca_frontal = ${result.caFrontal.toFixed(2)}`,
note: 'Resistência aerodinâmica inicial considerando a obstrução conjunta das vigas, pilares e leito de tubulações.',
},
{
title: '2. Fator de Proteção Aerodinâmica (η) para Pórticos Sucessivos',
formula: 'η = f(e / h , φ_total) (efeito de sombreamento entre quadros sucessivos)',
calculation: `Espaçamento e/h = ${(spacing / height).toFixed(2)} e φ_total = ${result.phiTotal.toFixed(2)} -> Fator η = ${result.eta.toFixed(2)}`,
result: `η = ${result.eta.toFixed(2)}`,
note: 'Os pórticos posteriores ao frontal recebem vento atenuado pelo sombreamento do pórtico a barlavento.',
},
{
title: '3. Cargas Lineares nos Pórticos Frontal e Traseiros (q_linear)',
formula: 'q_front = q · Ca · h | q_back = q_front · η [kN/m]',
calculation: `${q.toFixed(2)} kN/m² · ${result.caFrontal.toFixed(2)} · (${height} m) = ${result.linearLoadFront.toFixed(2)} kN/m (Frontal) | Traseiro: ${result.linearLoadBack.toFixed(2)} kN/m`,
result: `q_front = ${result.linearLoadFront.toFixed(2)} kN/m`,
note: 'Carga linear distribuída horizontalmente no topo dos pórticos principais do pipe-rack.',
},
{
title: '4. Força Global Total e Momento de Tombamento na Base (M)',
formula: 'F_total = F_front + (n 1)·F_back | M_tombamento = F_total · Z_eff [kNm]',
calculation: `${result.globalForce.toFixed(2)} kN · (${result.effectiveElevation.toFixed(2)} m) = ${(result.globalForce * result.effectiveElevation).toFixed(2)} kNm`,
result: `M_total = ${(result.globalForce * result.effectiveElevation).toFixed(2)} kNm`,
note: 'Momento fletor global transferido para os pilares e fundações industriais.',
},
],
},
];
exportGenericToPDF('Pipe-Racks Industriais', sections);
};
return (
<div className="flex flex-col lg:flex-row h-full w-full gap-4 p-4 lg:p-6 bg-muted/30">
{/* Coluna Esquerda: Entradas */}
<div className="w-full lg:w-80 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
<Card className="shadow-sm border-border">
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0 pr-6">
<div>
<CardTitle className="text-lg">Pipe-Racks Industriais</CardTitle>
<CardDescription>Vento transversal (Sec. 7 - NBR 6123)</CardDescription>
</div>
<SaveModuleDialog
moduleType="piperack"
inputs={{ elevation, width, height, spacing, numFrames, pipes, phiStruct, f1 }}
/>
</CardHeader>
<CardContent className="space-y-6">
<WindParametersSummary />
<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={25} 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={0.5} 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={0.5} max={5} 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={5} step={1} onValueChange={v => setNumFrames(v[0])} />
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Índice de Solidez (φ str)</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
<p>O índice de solidez (φ) representa a relação entre a área efetivamente bloqueada pelos perfis do reticulado e a área total contornada pelo pórtico. Quanto mais "fechado" for o pórtico estrutural, maior o arrasto aerodinâmico (ref. Tabela 28 da NBR 6123).</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<span className="text-sm text-muted-foreground">{phiStruct.toFixed(2)}</span>
</div>
<Slider value={[phiStruct]} min={0.1} max={1.0} step={0.05} onValueChange={v => setPhiStruct(v[0])} />
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Freq. Natural (f)</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
<p>Frequência fundamental no sentido do vento (Hz). Valores abaixo de 1 Hz indicam risco dinâmico elevado.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<span className="text-sm text-muted-foreground">{f1.toFixed(1)} Hz</span>
</div>
<Slider value={[f1]} min={0.1} max={5.0} step={0.1} onValueChange={v => setF1(v[0])} />
{isResonantRisk(f1) && (
<div className="mt-2 p-2 bg-destructive/10 border border-destructive/20 rounded text-xs text-destructive flex gap-2 items-start">
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5" />
<p>Estrutura flexível (f &lt; 1 Hz). O vento produzirá amplificação dinâmica (C.A.D) considerável. É exigida análise pela NBR 6123 Capítulo 9.</p>
</div>
)}
</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-4">
{pipes.map((pipe, index) => (
<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>
)}
</div>
</CardContent>
</Card>
</div>
{/* Coluna Central: 3D */}
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
<div className="absolute top-4 right-4 z-10">
<EducationalManual type="piperack" params={{ numFrames, phiTotal: result.phiTotal }} />
</div>
<Piperack3D
width={width}
height={height}
elevation={elevation}
spacing={spacing}
numFrames={numFrames}
pipes={pipes}
phiTotal={result.phiTotal}
eta={result.eta}
/>
</div>
{/* Coluna Direita: Resultados */}
<div className="w-full lg:w-96 shrink-0 flex flex-col gap-4 overflow-y-auto pb-8 lg:pb-0">
<Card className="shadow-sm border-border">
<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 pb-4 border-b">
<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>
<div className="pt-2">
<h4 className="font-semibold text-sm mb-3">Distribuição do Vento (Vista Lateral)</h4>
<div className="bg-muted/30 p-3 rounded-md border text-xs space-y-4">
<svg width="100%" height="150" viewBox="0 0 300 150" className="bg-white rounded border">
{/* Linha do chão */}
<line x1="20" y1="110" x2="280" y2="110" stroke="#cbd5e1" strokeWidth="2" />
{/* Renderização Dinâmica baseada no numFrames */}
{(() => {
const startX = 60;
const endX = 240;
const step = numFrames > 1 ? (endX - startX) / (numFrames - 1) : 0;
return (
<>
{/* Vigas Longitudinais */}
{numFrames > 1 && (
<>
<line x1={startX + 4} y1="40" x2={endX + 4} y2="40" stroke="#94a3b8" strokeWidth="4" />
<line x1={startX + 4} y1="65" x2={endX + 4} y2="65" stroke="#94a3b8" strokeWidth="4" />
</>
)}
{/* Colunas */}
{Array.from({ length: numFrames }).map((_, i) => {
const colX = numFrames === 1 ? 150 : startX + i * step;
return (
<g key={`col-${i}`}>
<rect x={colX} y="35" width="8" height="75" fill="#64748b" />
<text x={colX + 4} y="125" textAnchor="middle" fontSize={numFrames > 3 ? "8" : "10"} fill="#475569">
{`C${i + 1}`}
</text>
</g>
);
})}
{/* Linha de cota para o espaçamento (e) entre C1 e C2 */}
{numFrames > 1 && (
<>
<line x1={startX + 4} y1="145" x2={startX + step + 4} y2="145" stroke="#64748b" strokeWidth="1" />
<line x1={startX + 4} y1="142" x2={startX + 4} y2="148" stroke="#64748b" strokeWidth="1" />
<line x1={startX + step + 4} y1="142" x2={startX + step + 4} y2="148" stroke="#64748b" strokeWidth="1" />
<text x={startX + step / 2 + 4} y="142" textAnchor="middle" fontSize="8" fill="#64748b">e = {spacing}m</text>
</>
)}
{/* Suportes para tubulação quando n=1 */}
{numFrames === 1 && pipes.length > 0 && (
<g>
<line x1="90" y1="35" x2="210" y2="35" stroke="#94a3b8" strokeWidth="3" />
</g>
)}
{/* Vento batendo na primeira coluna */}
<line x1={numFrames === 1 ? 100 : startX - 40} y1="50" x2={numFrames === 1 ? 145 : startX - 5} y2="50" stroke="#10b981" strokeWidth="2" markerEnd="url(#arrowRight)" />
<text x={numFrames === 1 ? 122 : startX - 22} y="45" textAnchor="middle" fill="#10b981" fontSize="10" fontWeight="bold">Vento (F)</text>
</>
);
})()}
{/* Tubulações transversais cortadas (Círculos azuis) */}
{(() => {
if (pipes.length === 0) return null;
const startX = numFrames === 1 ? 100 : 60;
const endX = numFrames === 1 ? 200 : 240;
const gap = pipes.length > 1 ? (endX - startX) / (pipes.length - 1) : 0;
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
// 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" />
</marker>
</defs>
</svg>
<div className="pt-2 space-y-2">
<div className="font-bold text-primary">Forças e Momentos Aerodinâmicos</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 bg-background p-3 rounded border">
<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.globalForce * result.effectiveElevation).toFixed(2)} kN·m</div>
</div>
</div>
<p className="text-[10px] text-muted-foreground leading-tight text-center">
* O Momento Global representa a ação aerodinâmica pura. As reações reais (esforços axiais e fletores) devem ser obtidas no modelo estrutural.
</p>
</div>
</div>
</div>
</CardContent>
</Card>
<div className="flex justify-center bg-background rounded-xl border border-border shadow-sm p-4">
<div className="flex flex-col items-end gap-2">
<ExportMenu onExportPDF={handleExportPDF} />
</div>
</div>
<SceneCapturePanel />
</div>
</div>
);
};
export default PiperackModule;