/** * CraneRail 3D Factory * * 스토커 내부 스태커 크레인의 주행 레일. * 바닥 레일 + 천장 가이드 레일로 크레인의 2축 이동(수평 주행 + 수직 승강)을 지원. * * 구조 (바닥 원점, y=0~depth): * - 바닥 레일: 2개의 평행 트랙 (크레인 바퀴 주행) * - 천장 가이드 레일: 2개의 평행 트랙 (마스트 상단 안내) * - 수직 지지대: 양 끝단 기둥 (바닥~천장 연결) */ import * as THREE from 'three' import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js' import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene' import type { Component } from '@hatiolab/things-scene' import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js' import { Z_FMSIM } from './z-priority' const RAIL_COLOR = 0x666677 const SUPPORT_COLOR = 0x555566 /** 크레인 레일 기본 높이: 스토커와 동일 */ const DEFAULT_RAIL_DEPTH = 5 * TRANSPORT_SURFACE_HEIGHT export class CraneRail3D extends RealObjectGroup { get zPriority(): number { return Z_FMSIM.CRANE_RAIL } get effectiveDepth(): number { const { depth } = this.component.state return depth || DEFAULT_RAIL_DEPTH } build() { super.build() const { width = 10, height = 10 } = this.component.state const w = Math.abs(width) const h = Math.abs(height) const d = this.effectiveDepth if (!w || !h) return const inner = this.components3D const railH = Math.max(d * 0.02, 2) // 레일 높이 (얇은 트랙) const railW = Math.max(h * 0.06, 2) // 레일 폭 const supportThick = Math.max(Math.min(w, h) * 0.05, 2) const railMat = new THREE.MeshStandardMaterial({ color: RAIL_COLOR, roughness: 0.3, metalness: 0.7, }) const supportMat = new THREE.MeshStandardMaterial({ color: SUPPORT_COLOR, roughness: 0.5, metalness: 0.5, }) // ── 바닥 레일 (2개 평행 트랙) ── const bottomGeometries: THREE.BufferGeometry[] = [] for (const zSign of [-1, 1]) { const rail = new THREE.BoxGeometry(w, railH, railW) rail.translate(0, railH / 2, zSign * (h / 2 - railW / 2)) bottomGeometries.push(rail) } const bottomMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(bottomGeometries), railMat) bottomMesh.castShadow = true bottomMesh.receiveShadow = true inner.add(bottomMesh) // ── 천장 가이드 레일 (2개 평행 트랙) ── const topGeometries: THREE.BufferGeometry[] = [] for (const zSign of [-1, 1]) { const rail = new THREE.BoxGeometry(w, railH, railW) rail.translate(0, d - railH / 2, zSign * (h / 2 - railW / 2)) topGeometries.push(rail) } const topMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(topGeometries), railMat) topMesh.castShadow = true inner.add(topMesh) // ── 양 끝단 수직 지지대 (바닥~천장 연결) ── const supportH = d - railH * 2 const supportGeometries: THREE.BufferGeometry[] = [] for (const xSign of [-1, 1]) { for (const zSign of [-1, 1]) { const support = new THREE.BoxGeometry(supportThick, supportH, supportThick) support.translate( xSign * (w / 2 - supportThick / 2), railH + supportH / 2, zSign * (h / 2 - railW / 2) ) supportGeometries.push(support) } } const supportMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(supportGeometries), supportMat) supportMesh.castShadow = true inner.add(supportMesh) } updateDimension() {} updateAlpha() {} onchange(after: Record, before: Record) { if ('width' in after || 'height' in after || 'depth' in after || 'fillStyle' in after || 'strokeStyle' in after) { this.update() return } super.onchange(after, before) } } registerRealObjectFactory('CraneRail', (component: Component) => new CraneRail3D(component))