155 lines
4.9 KiB
TypeScript
155 lines
4.9 KiB
TypeScript
import { Grid, Environment } from '@react-three/drei';
|
|
import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls';
|
|
import SceneCanvas from '../SceneCanvas';
|
|
import { WindArrow } from './WindArrow';
|
|
import { useCanvasTheme } from '@/lib/theme';
|
|
import type { PipeDef } from '../../lib/modules/piperack';
|
|
import FallbackDiagram from '../FallbackDiagram';
|
|
|
|
export interface Piperack3DInput {
|
|
width: number;
|
|
height: number;
|
|
elevation: number;
|
|
spacing: number;
|
|
numFrames: number;
|
|
pipes: PipeDef[];
|
|
phiTotal: number;
|
|
eta: number;
|
|
}
|
|
|
|
function PiperackModel({
|
|
width,
|
|
height,
|
|
elevation,
|
|
spacing,
|
|
numFrames,
|
|
pipes,
|
|
phiTotal,
|
|
}: Piperack3DInput) {
|
|
const theme = useCanvasTheme();
|
|
const isDark = theme === 'dark';
|
|
|
|
const halfW = width / 2;
|
|
const rackTop = elevation + height / 2;
|
|
|
|
const steelColor = isDark ? '#94a3b8' : '#64748b';
|
|
const pipeColors = ['#ef4444', '#3b82f6', '#f59e0b', '#10b981', '#8b5cf6'];
|
|
|
|
// Gera os pórticos longitudinais (as fileiras de colunas)
|
|
const frames = [];
|
|
const totalSpacing = spacing * (numFrames > 1 ? numFrames - 1 : 0);
|
|
const zOffset = totalSpacing / 2;
|
|
|
|
for (let i = 0; i < numFrames; i++) {
|
|
const z = -zOffset + i * spacing;
|
|
|
|
// Desenha apenas 2 colunas nas extremidades da largura do pórtico (conforme solicitado pelo usuário)
|
|
const numCols = 2;
|
|
for (let c = 0; c < numCols; c++) {
|
|
const colX = -halfW + (c / (numCols - 1)) * width;
|
|
frames.push(
|
|
<mesh key={`col-${i}-${c}`} position={[colX, rackTop / 2, z]} receiveShadow castShadow>
|
|
<boxGeometry args={[0.3, rackTop, 0.3]} />
|
|
<meshStandardMaterial color={steelColor} metalness={0.6} roughness={0.4} />
|
|
</mesh>
|
|
);
|
|
}
|
|
|
|
// Viga Longitudinal da fileira (representando a face reticulada do pórtico)
|
|
frames.push(
|
|
<mesh key={`beam-${i}`} position={[0, elevation, z]} receiveShadow castShadow>
|
|
<boxGeometry args={[width, height, 0.3]} />
|
|
{/* Opacidade ajustada para atingir 90% quando a solidez for 1.0 */}
|
|
<meshStandardMaterial color={steelColor} metalness={0.6} roughness={0.4} transparent opacity={Math.max(0.1, Math.min(0.9, phiTotal * 0.9))} depthWrite={false} />
|
|
</mesh>
|
|
);
|
|
}
|
|
|
|
|
|
// Gera as tubulações correndo longitudinalmente (ao longo de X)
|
|
// Agrupa tubos pela mesma elevação para evitar colisões e distribuí-los lado a lado (ao longo de Z)
|
|
const pipeMeshes: any[] = [];
|
|
const groups: Record<number, {pipe: PipeDef, idx: number}[]> = {};
|
|
|
|
pipes.forEach((pipe, idx) => {
|
|
// Arredondamos a elevação para agrupar valores muito próximos
|
|
const key = Math.round(pipe.elevationOffset * 100) / 100;
|
|
if (!groups[key]) groups[key] = [];
|
|
groups[key].push({pipe, idx});
|
|
});
|
|
|
|
Object.values(groups).forEach(group => {
|
|
const gap = 0.2; // 20cm de espaçamento entre tubos na mesma elevação
|
|
const totalDiam = group.reduce((sum, item) => sum + item.pipe.diameter, 0);
|
|
const totalGroupWidth = totalDiam + gap * (group.length - 1);
|
|
|
|
// Inicia a distribuição centralizada em Z=0
|
|
let currentZ = -totalGroupWidth / 2;
|
|
|
|
group.forEach((item) => {
|
|
const { pipe, idx } = item;
|
|
const zPos = currentZ + pipe.diameter / 2;
|
|
currentZ += pipe.diameter + gap;
|
|
|
|
const yPos = (elevation - height/2) + pipe.elevationOffset + pipe.diameter/2;
|
|
const color = pipeColors[idx % pipeColors.length];
|
|
|
|
pipeMeshes.push(
|
|
<mesh key={`pipe-${pipe.id}`} position={[0, yPos, zPos]} rotation={[0, 0, Math.PI / 2]} receiveShadow castShadow>
|
|
<cylinderGeometry args={[pipe.diameter / 2, pipe.diameter / 2, width + 1, 16]} />
|
|
<meshStandardMaterial color={color} metalness={0.3} roughness={0.2} />
|
|
</mesh>
|
|
);
|
|
});
|
|
});
|
|
|
|
return (
|
|
<group>
|
|
{frames}
|
|
{pipeMeshes}
|
|
|
|
{/* Vento batendo lateralmente (transversal ao Pipe-rack) */}
|
|
<WindArrow
|
|
direction={[0, 0, 1]}
|
|
target={[0, elevation, zOffset + 2]}
|
|
/>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
export function Piperack3D(props: Piperack3DInput) {
|
|
const isDark = useCanvasTheme() === 'dark';
|
|
|
|
const fallback = (
|
|
<FallbackDiagram
|
|
type="piperack"
|
|
props={props}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<SceneCanvas moduleId="piperack" fallback={fallback}>
|
|
<ambientLight intensity={isDark ? 0.3 : 0.6} />
|
|
<directionalLight position={[10, 15, 10]} intensity={isDark ? 1 : 1.5} castShadow shadow-bias={-0.001} />
|
|
<Environment preset="city" />
|
|
|
|
<PiperackModel {...props} />
|
|
|
|
<Grid
|
|
args={[40, 40]}
|
|
position={[0, -0.01, 0]}
|
|
cellColor={isDark ? '#334155' : '#cbd5e1'}
|
|
sectionColor={isDark ? '#475569' : '#94a3b8'}
|
|
fadeDistance={30}
|
|
/>
|
|
<OrbitControls
|
|
minPolarAngle={0}
|
|
maxPolarAngle={Math.PI / 2 - 0.05}
|
|
minDistance={5}
|
|
maxDistance={80}
|
|
target={[0, props.elevation / 2, 0]}
|
|
/>
|
|
</SceneCanvas>
|
|
);
|
|
}
|