504 lines
28 KiB
TypeScript
504 lines
28 KiB
TypeScript
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₁ < 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;
|