@fmsim/machine 1.0.87 → 2.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/agv-line.js.map +1 -1
  2. package/dist/agv.js.map +1 -1
  3. package/dist/buffer.js.map +1 -1
  4. package/dist/carrier.js +1 -0
  5. package/dist/carrier.js.map +1 -1
  6. package/dist/conveyor-join.js.map +1 -1
  7. package/dist/conveyor.js +1 -1
  8. package/dist/conveyor.js.map +1 -1
  9. package/dist/crane-rail.js +53 -0
  10. package/dist/crane-rail.js.map +1 -0
  11. package/dist/crane.js.map +1 -1
  12. package/dist/equipment.js.map +1 -1
  13. package/dist/factories/agv-3d.js +121 -0
  14. package/dist/factories/agv-3d.js.map +1 -0
  15. package/dist/factories/conveyor-3d.js +258 -0
  16. package/dist/factories/conveyor-3d.js.map +1 -0
  17. package/dist/factories/crane-3d.js +150 -0
  18. package/dist/factories/crane-3d.js.map +1 -0
  19. package/dist/factories/crane-rail-3d.js +95 -0
  20. package/dist/factories/crane-rail-3d.js.map +1 -0
  21. package/dist/factories/machine-3d.js +288 -0
  22. package/dist/factories/machine-3d.js.map +1 -0
  23. package/dist/factories/oht-3d.js +151 -0
  24. package/dist/factories/oht-3d.js.map +1 -0
  25. package/dist/factories/shuttle-3d.js +136 -0
  26. package/dist/factories/shuttle-3d.js.map +1 -0
  27. package/dist/index.js +38 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/mcs-carrier-holder.js +6 -1
  30. package/dist/mcs-carrier-holder.js.map +1 -1
  31. package/dist/mcs-machine.js.map +1 -1
  32. package/dist/mcs-unit.js.map +1 -1
  33. package/dist/mcs-vehicle.js +2 -1
  34. package/dist/mcs-vehicle.js.map +1 -1
  35. package/dist/oht-line.js.map +1 -1
  36. package/dist/oht.js.map +1 -1
  37. package/dist/port-flow.js.map +1 -1
  38. package/dist/port.js.map +1 -1
  39. package/dist/scene/root-container-override.js.map +1 -1
  40. package/dist/shelf.js.map +1 -1
  41. package/dist/shuttle.js.map +1 -1
  42. package/dist/stocker-abnormal-bar.js +180 -0
  43. package/dist/stocker-abnormal-bar.js.map +1 -0
  44. package/dist/stocker-capacity-bar.js.map +1 -1
  45. package/dist/stocker-carrier-bar.js +194 -0
  46. package/dist/stocker-carrier-bar.js.map +1 -0
  47. package/dist/stocker.js +1 -1
  48. package/dist/stocker.js.map +1 -1
  49. package/dist/tsconfig.tsbuildinfo +1 -1
  50. package/dist/zone-capacity-bar.js.map +1 -1
  51. package/package.json +6 -6
  52. package/src/agv-line.ts +1 -1
  53. package/src/agv.ts +3 -3
  54. package/src/buffer.ts +1 -1
  55. package/src/carrier.ts +8 -8
  56. package/src/conveyor-join.ts +1 -1
  57. package/src/conveyor.ts +2 -2
  58. package/src/crane-rail.ts +58 -0
  59. package/src/crane.ts +1 -1
  60. package/src/equipment.ts +1 -1
  61. package/src/factories/agv-3d.ts +135 -0
  62. package/src/factories/conveyor-3d.ts +311 -0
  63. package/src/factories/crane-3d.ts +176 -0
  64. package/src/factories/crane-rail-3d.ts +114 -0
  65. package/src/factories/machine-3d.ts +361 -0
  66. package/src/factories/oht-3d.ts +172 -0
  67. package/src/factories/shuttle-3d.ts +159 -0
  68. package/src/index.ts +40 -0
  69. package/src/mcs-carrier-holder.ts +8 -2
  70. package/src/mcs-machine.ts +1 -1
  71. package/src/mcs-unit.ts +1 -1
  72. package/src/mcs-vehicle.ts +3 -3
  73. package/src/oht-line.ts +1 -1
  74. package/src/oht.ts +1 -1
  75. package/src/port-flow.ts +1 -1
  76. package/src/port.ts +1 -1
  77. package/src/scene/root-container-override.ts +2 -2
  78. package/src/shelf.ts +1 -1
  79. package/src/shuttle.ts +1 -1
  80. package/src/stocker-abnormal-bar.ts +242 -0
  81. package/src/stocker-capacity-bar.ts +3 -3
  82. package/src/stocker-carrier-bar.ts +257 -0
  83. package/src/stocker.ts +3 -3
  84. package/src/zone-capacity-bar.ts +2 -2
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Conveyor 3D Factory
3
+ *
4
+ * 'conveyor' type (default): side rails + cylindrical rollers + 4 legs + cross-bracing
5
+ * 'belt' type: side rails + two end drums + belt wrapping + 4 legs + cross-bracing
6
+ * 'rail' type: side rails + two parallel rails + 4 legs + cross-bracing
7
+ *
8
+ * Based on operato-scene conveyor-3d.ts, adapted for fmsim:
9
+ * - conveyorType is string ('', 'conveyor', 'belt', 'rail') instead of numeric
10
+ * - statusColor from MCSStatusMixin instead of value-based lookup
11
+ * - rollWidth auto-calculated: Math.max(10, Math.min(width, height) / 2)
12
+ */
13
+
14
+ import * as THREE from 'three'
15
+ import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
16
+ import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene'
17
+ import type { Component } from '@hatiolab/things-scene'
18
+ import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'
19
+
20
+ const FRAME_COLOR = 0x888899
21
+ const ROLLER_COLOR = 0xaaaabc
22
+ const DEFAULT_COLOR = 0x999999
23
+
24
+ export class Conveyor3D extends RealObjectGroup {
25
+ get effectiveDepth(): number {
26
+ const { depth } = this.component.state
27
+ return depth || TRANSPORT_SURFACE_HEIGHT
28
+ }
29
+
30
+ // group origin = 바닥 (y=0). MCSRealObject3D와 동일 컨벤션.
31
+ // 기하는 build()에서 y=0(바닥)~y=depth(상단) 범위로 배치.
32
+ protected get syncZPosOffset(): number {
33
+ return 0
34
+ }
35
+
36
+ private resolveColor(): number {
37
+ const comp = this.component as any
38
+
39
+ // 1) MCSStatusMixin.statusColor (legend 기반 상태 색상)
40
+ // '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
41
+ try {
42
+ const sc = comp.statusColor
43
+ if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()
44
+ } catch {
45
+ /* statusColor 미구현 시 무시 */
46
+ }
47
+
48
+ // 2) state.fillStyle fallback
49
+ const { fillStyle } = this.component.state
50
+ if (fillStyle && typeof fillStyle === 'string') {
51
+ try {
52
+ return new THREE.Color(fillStyle).getHex()
53
+ } catch {
54
+ /* 파싱 실패 시 무시 */
55
+ }
56
+ }
57
+
58
+ return DEFAULT_COLOR
59
+ }
60
+
61
+ build() {
62
+ super.build()
63
+
64
+ const { width, height, conveyorType } = this.component.state
65
+ const depth = this.effectiveDepth
66
+ if (!width || !height) return
67
+
68
+ // 2D와 동일 로직으로 rollWidth 산출
69
+ const rollWidth = Math.max(10, Math.min(width, height) / 2)
70
+ const rollerDiameter = Math.max(rollWidth, 2)
71
+ const frameH = Math.min(rollerDiameter, depth)
72
+ const legH = Math.max(depth - frameH, 0)
73
+ const railW = Math.max(height * 0.06, 2)
74
+ const legThickness = Math.max(railW * 0.8, 2)
75
+
76
+ const frameMaterial = new THREE.MeshStandardMaterial({
77
+ color: FRAME_COLOR,
78
+ metalness: 0.85,
79
+ roughness: 0.35,
80
+ })
81
+
82
+ // --- Frame: side rails + legs + cross-bracing ---
83
+ // Y 좌표: y=0(바닥) ~ y=depth(상단). 레일 상단이 depth에 위치.
84
+ const frameGeometries: THREE.BufferGeometry[] = []
85
+ const railY = depth - frameH / 2
86
+
87
+ // Side rails (along X)
88
+ for (const zSign of [-1, 1]) {
89
+ const rail = new THREE.BoxGeometry(width, frameH, railW)
90
+ rail.translate(0, railY, zSign * (height / 2 - railW / 2))
91
+ frameGeometries.push(rail)
92
+ }
93
+
94
+ // 4 legs at corners
95
+ if (legH > 0) {
96
+ const legTopY = depth - frameH
97
+ const legCenterY = legTopY - legH / 2
98
+
99
+ for (const xSign of [-1, 1]) {
100
+ for (const zSign of [-1, 1]) {
101
+ const lx = xSign * (width / 2 - legThickness / 2)
102
+ const lz = zSign * (height / 2 - railW / 2)
103
+ const leg = new THREE.BoxGeometry(legThickness, legH, legThickness)
104
+ leg.translate(lx, legCenterY, lz)
105
+ frameGeometries.push(leg)
106
+ }
107
+ }
108
+
109
+ // Cross-bracing: 앞뒤 다리 연결 (Z 방향)
110
+ const braceH = legThickness * 0.6
111
+ const braceW = legThickness * 0.6
112
+ const braceLen = height - railW
113
+ const braceY = legTopY - legH * 0.35
114
+
115
+ for (const xSign of [-1, 1]) {
116
+ const brace = new THREE.BoxGeometry(braceW, braceH, braceLen)
117
+ brace.translate(xSign * (width / 2 - legThickness / 2), braceY, 0)
118
+ frameGeometries.push(brace)
119
+ }
120
+
121
+ // Cross-bracing: 좌우 다리 연결 (X 방향)
122
+ const braceLenX = width - legThickness
123
+ for (const zSign of [-1, 1]) {
124
+ const brace = new THREE.BoxGeometry(braceLenX, braceH, braceW)
125
+ brace.translate(0, braceY, zSign * (height / 2 - railW / 2))
126
+ frameGeometries.push(brace)
127
+ }
128
+ }
129
+
130
+ const frameMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(frameGeometries), frameMaterial)
131
+ frameMesh.castShadow = true
132
+ frameMesh.receiveShadow = true
133
+ this.object3d.add(frameMesh)
134
+
135
+ // --- Rollers, Belt, or Rails ---
136
+ if (conveyorType === 'rail') {
137
+ this.buildRails(width, height, depth, frameH, railW)
138
+ } else if (conveyorType === 'belt') {
139
+ this.buildBelt(width, height, depth, frameH, railW)
140
+ } else {
141
+ this.buildRollers(width, height, depth, frameH, railW, rollWidth)
142
+ }
143
+ }
144
+
145
+ private buildRollers(
146
+ width: number,
147
+ height: number,
148
+ depth: number,
149
+ frameH: number,
150
+ railW: number,
151
+ rollWidth: number
152
+ ) {
153
+ const rollerRadius = Math.max(rollWidth / 8, 1)
154
+ const rollerLength = height - railW * 2 - 0.5
155
+ const diameter = rollerRadius * 2
156
+ // 롤러 간 최소 간격 (z-fighting 방지용 미세 갭만)
157
+ const step = diameter * 1.05
158
+ // 롤러 상단 = 이송 표면 (y=depth)
159
+ const rollerY = depth - rollerRadius
160
+ const color = this.resolveColor()
161
+
162
+ const rollerGeometries: THREE.BufferGeometry[] = []
163
+ const count = Math.max(1, Math.floor(width / step))
164
+ const totalSpan = (count - 1) * step
165
+ const startX = -totalSpan / 2
166
+
167
+ for (let i = 0; i < count; i++) {
168
+ const x = startX + i * step
169
+ const roller = new THREE.CylinderGeometry(rollerRadius, rollerRadius, rollerLength, 16)
170
+ roller.rotateX(Math.PI / 2)
171
+ roller.translate(x, rollerY, 0)
172
+ rollerGeometries.push(roller)
173
+ }
174
+
175
+ if (rollerGeometries.length > 0) {
176
+ const rollerMesh = new THREE.Mesh(
177
+ BufferGeometryUtils.mergeGeometries(rollerGeometries),
178
+ new THREE.MeshStandardMaterial({ color, metalness: 0.9, roughness: 0.2 })
179
+ )
180
+ rollerMesh.castShadow = true
181
+ this.object3d.add(rollerMesh)
182
+ }
183
+ }
184
+
185
+ private buildBelt(width: number, height: number, depth: number, frameH: number, railW: number) {
186
+ const drumRadius = frameH * 0.38
187
+ const drumLength = height - railW * 2
188
+ const beltY = depth - drumRadius
189
+ const color = this.resolveColor()
190
+
191
+ // Two end drums
192
+ const drumGeometries: THREE.BufferGeometry[] = []
193
+ const drumInset = drumRadius * 1.5
194
+ const leftDrumX = -width / 2 + drumInset
195
+ const rightDrumX = width / 2 - drumInset
196
+
197
+ for (const dx of [leftDrumX, rightDrumX]) {
198
+ const drum = new THREE.CylinderGeometry(drumRadius, drumRadius, drumLength, 16)
199
+ drum.rotateX(Math.PI / 2)
200
+ drum.translate(dx, beltY, 0)
201
+ drumGeometries.push(drum)
202
+ }
203
+
204
+ const drumMesh = new THREE.Mesh(
205
+ BufferGeometryUtils.mergeGeometries(drumGeometries),
206
+ new THREE.MeshStandardMaterial({ color: ROLLER_COLOR, metalness: 0.9, roughness: 0.2 })
207
+ )
208
+ drumMesh.castShadow = true
209
+ this.object3d.add(drumMesh)
210
+
211
+ // Belt surface
212
+ const beltThickness = drumRadius * 0.12
213
+ const beltMaterial = new THREE.MeshStandardMaterial({
214
+ color: color,
215
+ metalness: 0.0,
216
+ roughness: 0.9,
217
+ })
218
+
219
+ // Flat belt top surface spanning between drums
220
+ const flatLen = rightDrumX - leftDrumX
221
+ const flatGeo = new THREE.BoxGeometry(flatLen, beltThickness, drumLength)
222
+ const flatMesh = new THREE.Mesh(flatGeo, beltMaterial)
223
+ flatMesh.position.set(0, beltY + drumRadius, 0)
224
+ flatMesh.castShadow = true
225
+ this.object3d.add(flatMesh)
226
+
227
+ // Belt wrap around drums (segmented half-cylinder)
228
+ const wrapSegments = 12
229
+ for (const [dx, angleStart] of [
230
+ [leftDrumX, Math.PI / 2],
231
+ [rightDrumX, -Math.PI / 2],
232
+ ] as [number, number][]) {
233
+ const wrapGeometries: THREE.BufferGeometry[] = []
234
+ for (let i = 0; i < wrapSegments; i++) {
235
+ const a0 = angleStart + (Math.PI * i) / wrapSegments
236
+ const a1 = angleStart + (Math.PI * (i + 1)) / wrapSegments
237
+ const r = drumRadius + beltThickness / 2
238
+
239
+ const x0 = dx + Math.cos(a0) * r
240
+ const y0 = beltY + Math.sin(a0) * r
241
+ const x1 = dx + Math.cos(a1) * r
242
+ const y1 = beltY + Math.sin(a1) * r
243
+
244
+ const segLen = Math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2) * 1.05
245
+ const segAngle = Math.atan2(y1 - y0, x1 - x0)
246
+
247
+ const seg = new THREE.BoxGeometry(segLen, beltThickness, drumLength)
248
+ seg.rotateZ(segAngle)
249
+ seg.translate((x0 + x1) / 2, (y0 + y1) / 2, 0)
250
+ wrapGeometries.push(seg)
251
+ }
252
+ const wrapMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(wrapGeometries), beltMaterial)
253
+ wrapMesh.castShadow = true
254
+ this.object3d.add(wrapMesh)
255
+ }
256
+
257
+ // Flat belt bottom (return path)
258
+ const bottomGeo = new THREE.BoxGeometry(flatLen, beltThickness, drumLength * 0.85)
259
+ const bottomMesh = new THREE.Mesh(bottomGeo, beltMaterial)
260
+ bottomMesh.position.set(0, beltY - drumRadius, 0)
261
+ this.object3d.add(bottomMesh)
262
+ }
263
+
264
+ private buildRails(width: number, height: number, depth: number, frameH: number, railW: number) {
265
+ const color = this.resolveColor()
266
+ const railHeight = frameH * 0.3
267
+ const railY = depth - railHeight / 2
268
+ const innerWidth = height - railW * 2
269
+ // 두 레일 간격: 내부 폭의 60%
270
+ const railSpacing = innerWidth * 0.6
271
+
272
+ const railMaterial = new THREE.MeshStandardMaterial({
273
+ color,
274
+ metalness: 0.7,
275
+ roughness: 0.3,
276
+ })
277
+
278
+ const railGeometries: THREE.BufferGeometry[] = []
279
+
280
+ // Two parallel rails running along X axis
281
+ for (const zSign of [-1, 1]) {
282
+ const rail = new THREE.BoxGeometry(width - railW * 2, railHeight, railW * 0.8)
283
+ rail.translate(0, railY, zSign * (railSpacing / 2))
284
+ railGeometries.push(rail)
285
+ }
286
+
287
+ const railMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(railGeometries), railMaterial)
288
+ railMesh.castShadow = true
289
+ this.object3d.add(railMesh)
290
+ }
291
+
292
+ updateDimension() {}
293
+ updateAlpha() {}
294
+
295
+ onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
296
+ if (
297
+ 'conveyorType' in after ||
298
+ 'width' in after ||
299
+ 'height' in after ||
300
+ 'depth' in after ||
301
+ 'fillStyle' in after ||
302
+ 'strokeStyle' in after
303
+ ) {
304
+ this.update()
305
+ return
306
+ }
307
+ super.onchange(after, before)
308
+ }
309
+ }
310
+
311
+ registerRealObjectFactory('Conveyor', (component: Component) => new Conveyor3D(component))
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Crane (Stacker Crane) 3D Factory
3
+ *
4
+ * 스토커 내부의 스태커 크레인 — CraneRail 위를 이동하는 차체.
5
+ * 레일 인프라는 CraneRail이 담당하므로 크레인은 이동체만 표현.
6
+ *
7
+ * 구조 (바닥 원점, y=0~depth):
8
+ * - 하단 바퀴/클램프 (4%, 바닥 레일에 물리는 부분)
9
+ * - 수직 마스트 (좌우 기둥 88%, statusColor 적용)
10
+ * - 캐리지 포크 (마스트 40% 높이, 양쪽 포크 암)
11
+ * - 상단 클램프 (4%, 천장 레일에 물리는 부분)
12
+ */
13
+
14
+ import * as THREE from 'three'
15
+ import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
16
+ import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene'
17
+ import type { Component } from '@hatiolab/things-scene'
18
+ import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'
19
+
20
+ const CLAMP_COLOR = 0x444444
21
+ const FORK_COLOR = 0x999999
22
+ const DEFAULT_BODY_COLOR = 0xcc8844
23
+
24
+ export class Crane3D extends RealObjectGroup {
25
+ get effectiveDepth(): number {
26
+ const { depth } = this.component.state
27
+ return depth || 5 * TRANSPORT_SURFACE_HEIGHT - 2
28
+ }
29
+
30
+ protected get syncZPosOffset(): number {
31
+ return 0
32
+ }
33
+
34
+ private resolveColor(): number {
35
+ const comp = this.component as any
36
+
37
+ try {
38
+ const sc = comp.statusColor
39
+ if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()
40
+ } catch {
41
+ /* statusColor 미구현 시 무시 */
42
+ }
43
+
44
+ const { fillStyle } = this.component.state
45
+ if (fillStyle && typeof fillStyle === 'string') {
46
+ try {
47
+ return new THREE.Color(fillStyle).getHex()
48
+ } catch {
49
+ /* 파싱 실패 시 무시 */
50
+ }
51
+ }
52
+
53
+ return DEFAULT_BODY_COLOR
54
+ }
55
+
56
+ build() {
57
+ super.build()
58
+
59
+ const { width = 10, height = 10 } = this.component.state
60
+ const w = Math.abs(width)
61
+ const h = Math.abs(height)
62
+ const d = this.effectiveDepth
63
+ if (!w || !h) return
64
+
65
+ // 높이 배분: 하단 클램프 4%, 마스트 92%, 상단 클램프 4%
66
+ const clampH = d * 0.04
67
+ const mastH = d * 0.92
68
+
69
+ const pillarThick = Math.min(w, h) * 0.12
70
+
71
+ const clampMat = new THREE.MeshStandardMaterial({
72
+ color: CLAMP_COLOR,
73
+ roughness: 0.5,
74
+ metalness: 0.5,
75
+ })
76
+
77
+ // ── 1. 하단 클램프 (바닥 레일에 물리는 바퀴부) ──
78
+ const clampW = w * 0.7
79
+ const clampD = h * 0.5
80
+ const bottomClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD)
81
+ const bottomClamp = new THREE.Mesh(bottomClampGeo, clampMat)
82
+ bottomClamp.position.y = clampH / 2
83
+ bottomClamp.castShadow = true
84
+ this.object3d.add(bottomClamp)
85
+
86
+ // ── 2. 수직 마스트 (좌우 기둥 + 상·하 가로 연결 바) ──
87
+ const mastBaseY = clampH
88
+ const bodyColor = this.resolveColor()
89
+ const mastMat = new THREE.MeshStandardMaterial({
90
+ color: bodyColor,
91
+ roughness: 0.3,
92
+ metalness: 0.6,
93
+ })
94
+
95
+ const mastGeometries: THREE.BufferGeometry[] = []
96
+
97
+ // 좌우 수직 기둥
98
+ for (const xSign of [-1, 1]) {
99
+ const pillar = new THREE.BoxGeometry(pillarThick, mastH, pillarThick)
100
+ pillar.translate(xSign * (w / 2 - pillarThick / 2), mastBaseY + mastH / 2, 0)
101
+ mastGeometries.push(pillar)
102
+ }
103
+
104
+ // 하단 가로 연결 바
105
+ const crossW = w - pillarThick
106
+ const crossH = pillarThick * 0.5
107
+ const lowerCross = new THREE.BoxGeometry(crossW, crossH, pillarThick)
108
+ lowerCross.translate(0, mastBaseY + crossH / 2, 0)
109
+ mastGeometries.push(lowerCross)
110
+
111
+ // 상단 가로 연결 바
112
+ const upperCross = new THREE.BoxGeometry(crossW, crossH, pillarThick)
113
+ upperCross.translate(0, mastBaseY + mastH - crossH / 2, 0)
114
+ mastGeometries.push(upperCross)
115
+
116
+ const mastMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(mastGeometries), mastMat)
117
+ mastMesh.castShadow = true
118
+ this.object3d.add(mastMesh)
119
+
120
+ // ── 3. 캐리지 포크 (마스트 40% 높이, 양쪽으로 뻗는 포크 암) ──
121
+ const forkY = mastBaseY + mastH * 0.4
122
+ const forkMat = new THREE.MeshStandardMaterial({
123
+ color: FORK_COLOR,
124
+ roughness: 0.4,
125
+ metalness: 0.5,
126
+ })
127
+
128
+ const forkGeometries: THREE.BufferGeometry[] = []
129
+
130
+ // 캐리지 중심 플레이트 (마스트에 부착, 양쪽 포크의 베이스)
131
+ const plateW = w * 0.9
132
+ const plateH = d * 0.08
133
+ const plateD = pillarThick * 0.8
134
+ const centerPlate = new THREE.BoxGeometry(plateW, plateH, plateD)
135
+ centerPlate.translate(0, forkY + plateH / 2, 0)
136
+ forkGeometries.push(centerPlate)
137
+
138
+ // 양쪽 포크 암 (좌/우 선반 방향으로 뻗음)
139
+ const armW = pillarThick * 0.6
140
+ const armH = plateH * 0.4
141
+ const armD = h * 0.6
142
+ const armSpacing = w * 0.5
143
+
144
+ for (const zSign of [-1, 1]) {
145
+ for (const xSign of [-1, 1]) {
146
+ const arm = new THREE.BoxGeometry(armW, armH, armD)
147
+ arm.translate(xSign * (armSpacing / 2), forkY + armH / 2, zSign * (plateD / 2 + armD / 2))
148
+ forkGeometries.push(arm)
149
+ }
150
+ }
151
+
152
+ const forkMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(forkGeometries), forkMat)
153
+ forkMesh.castShadow = true
154
+ this.object3d.add(forkMesh)
155
+
156
+ // ── 4. 상단 클램프 (천장 레일에 물리는 가이드부) ──
157
+ const topClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD)
158
+ const topClamp = new THREE.Mesh(topClampGeo, clampMat)
159
+ topClamp.position.y = mastBaseY + mastH + clampH / 2
160
+ topClamp.castShadow = true
161
+ this.object3d.add(topClamp)
162
+ }
163
+
164
+ updateDimension() {}
165
+ updateAlpha() {}
166
+
167
+ onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
168
+ if ('width' in after || 'height' in after || 'depth' in after || 'fillStyle' in after || 'strokeStyle' in after) {
169
+ this.update()
170
+ return
171
+ }
172
+ super.onchange(after, before)
173
+ }
174
+ }
175
+
176
+ registerRealObjectFactory('Crane', (component: Component) => new Crane3D(component))
@@ -0,0 +1,114 @@
1
+ /**
2
+ * CraneRail 3D Factory
3
+ *
4
+ * 스토커 내부 스태커 크레인의 주행 레일.
5
+ * 바닥 레일 + 천장 가이드 레일로 크레인의 2축 이동(수평 주행 + 수직 승강)을 지원.
6
+ *
7
+ * 구조 (바닥 원점, y=0~depth):
8
+ * - 바닥 레일: 2개의 평행 트랙 (크레인 바퀴 주행)
9
+ * - 천장 가이드 레일: 2개의 평행 트랙 (마스트 상단 안내)
10
+ * - 수직 지지대: 양 끝단 기둥 (바닥~천장 연결)
11
+ */
12
+
13
+ import * as THREE from 'three'
14
+ import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
15
+ import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene'
16
+ import type { Component } from '@hatiolab/things-scene'
17
+ import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'
18
+
19
+ const RAIL_COLOR = 0x666677
20
+ const SUPPORT_COLOR = 0x555566
21
+
22
+ /** 크레인 레일 기본 높이: 스토커와 동일 */
23
+ const DEFAULT_RAIL_DEPTH = 5 * TRANSPORT_SURFACE_HEIGHT
24
+
25
+ export class CraneRail3D extends RealObjectGroup {
26
+ get effectiveDepth(): number {
27
+ const { depth } = this.component.state
28
+ return depth || DEFAULT_RAIL_DEPTH
29
+ }
30
+
31
+ protected get syncZPosOffset(): number {
32
+ return 0
33
+ }
34
+
35
+ build() {
36
+ super.build()
37
+
38
+ const { width = 10, height = 10 } = this.component.state
39
+ const w = Math.abs(width)
40
+ const h = Math.abs(height)
41
+ const d = this.effectiveDepth
42
+ if (!w || !h) return
43
+
44
+ const railH = Math.max(d * 0.02, 2) // 레일 높이 (얇은 트랙)
45
+ const railW = Math.max(h * 0.06, 2) // 레일 폭
46
+ const supportThick = Math.max(Math.min(w, h) * 0.05, 2)
47
+
48
+ const railMat = new THREE.MeshStandardMaterial({
49
+ color: RAIL_COLOR,
50
+ roughness: 0.3,
51
+ metalness: 0.7,
52
+ })
53
+
54
+ const supportMat = new THREE.MeshStandardMaterial({
55
+ color: SUPPORT_COLOR,
56
+ roughness: 0.5,
57
+ metalness: 0.5,
58
+ })
59
+
60
+ // ── 바닥 레일 (2개 평행 트랙) ──
61
+ const bottomGeometries: THREE.BufferGeometry[] = []
62
+ for (const zSign of [-1, 1]) {
63
+ const rail = new THREE.BoxGeometry(w, railH, railW)
64
+ rail.translate(0, railH / 2, zSign * (h / 2 - railW / 2))
65
+ bottomGeometries.push(rail)
66
+ }
67
+ const bottomMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(bottomGeometries), railMat)
68
+ bottomMesh.castShadow = true
69
+ bottomMesh.receiveShadow = true
70
+ this.object3d.add(bottomMesh)
71
+
72
+ // ── 천장 가이드 레일 (2개 평행 트랙) ──
73
+ const topGeometries: THREE.BufferGeometry[] = []
74
+ for (const zSign of [-1, 1]) {
75
+ const rail = new THREE.BoxGeometry(w, railH, railW)
76
+ rail.translate(0, d - railH / 2, zSign * (h / 2 - railW / 2))
77
+ topGeometries.push(rail)
78
+ }
79
+ const topMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(topGeometries), railMat)
80
+ topMesh.castShadow = true
81
+ this.object3d.add(topMesh)
82
+
83
+ // ── 양 끝단 수직 지지대 (바닥~천장 연결) ──
84
+ const supportH = d - railH * 2
85
+ const supportGeometries: THREE.BufferGeometry[] = []
86
+ for (const xSign of [-1, 1]) {
87
+ for (const zSign of [-1, 1]) {
88
+ const support = new THREE.BoxGeometry(supportThick, supportH, supportThick)
89
+ support.translate(
90
+ xSign * (w / 2 - supportThick / 2),
91
+ railH + supportH / 2,
92
+ zSign * (h / 2 - railW / 2)
93
+ )
94
+ supportGeometries.push(support)
95
+ }
96
+ }
97
+ const supportMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(supportGeometries), supportMat)
98
+ supportMesh.castShadow = true
99
+ this.object3d.add(supportMesh)
100
+ }
101
+
102
+ updateDimension() {}
103
+ updateAlpha() {}
104
+
105
+ onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
106
+ if ('width' in after || 'height' in after || 'depth' in after || 'fillStyle' in after || 'strokeStyle' in after) {
107
+ this.update()
108
+ return
109
+ }
110
+ super.onchange(after, before)
111
+ }
112
+ }
113
+
114
+ registerRealObjectFactory('CraneRail', (component: Component) => new CraneRail3D(component))