@fmsim/machine 2.0.0-beta.10 → 2.0.0-beta.100

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 (152) hide show
  1. package/dist/agv.js +6 -2
  2. package/dist/agv.js.map +1 -1
  3. package/dist/carrier-manager.js +183 -0
  4. package/dist/carrier-manager.js.map +1 -0
  5. package/dist/carrier.js +99 -9
  6. package/dist/carrier.js.map +1 -1
  7. package/dist/conveyor-join.js +8 -2
  8. package/dist/conveyor-join.js.map +1 -1
  9. package/dist/conveyor.js +8 -0
  10. package/dist/conveyor.js.map +1 -1
  11. package/dist/crane.js +144 -1
  12. package/dist/crane.js.map +1 -1
  13. package/dist/factories/agv-3d.js +8 -9
  14. package/dist/factories/agv-3d.js.map +1 -1
  15. package/dist/factories/conveyor-3d.js +168 -81
  16. package/dist/factories/conveyor-3d.js.map +1 -1
  17. package/dist/factories/crane-3d.js +48 -12
  18. package/dist/factories/crane-3d.js.map +1 -1
  19. package/dist/factories/crane-rail-3d.js +6 -6
  20. package/dist/factories/crane-rail-3d.js.map +1 -1
  21. package/dist/factories/machine-3d.js +527 -181
  22. package/dist/factories/machine-3d.js.map +1 -1
  23. package/dist/factories/oht-3d.js +20 -20
  24. package/dist/factories/oht-3d.js.map +1 -1
  25. package/dist/factories/shuttle-3d.js +2 -5
  26. package/dist/factories/shuttle-3d.js.map +1 -1
  27. package/dist/factories/z-priority-defaults.js +33 -0
  28. package/dist/factories/z-priority-defaults.js.map +1 -0
  29. package/dist/factories/z-priority.js +54 -0
  30. package/dist/factories/z-priority.js.map +1 -0
  31. package/dist/features/conveyor-mixin.js.map +1 -1
  32. package/dist/features/mcs-status-default.js +15 -1
  33. package/dist/features/mcs-status-default.js.map +1 -1
  34. package/dist/features/mcs-status-mixin.js +22 -0
  35. package/dist/features/mcs-status-mixin.js.map +1 -1
  36. package/dist/features/mcs-zone-mixin.js.map +1 -1
  37. package/dist/features/parent-object-mixin.js +15 -7
  38. package/dist/features/parent-object-mixin.js.map +1 -1
  39. package/dist/floating-container.js +5 -0
  40. package/dist/floating-container.js.map +1 -1
  41. package/dist/index.js +52 -34
  42. package/dist/index.js.map +1 -1
  43. package/dist/mcs-carrier-holder.js +56 -22
  44. package/dist/mcs-carrier-holder.js.map +1 -1
  45. package/dist/mcs-machine.js +39 -0
  46. package/dist/mcs-machine.js.map +1 -1
  47. package/dist/mcs-unit.js +20 -0
  48. package/dist/mcs-unit.js.map +1 -1
  49. package/dist/mcs-vehicle.js +8 -3
  50. package/dist/mcs-vehicle.js.map +1 -1
  51. package/dist/node.js +10 -3
  52. package/dist/node.js.map +1 -1
  53. package/dist/oht.js +4 -0
  54. package/dist/oht.js.map +1 -1
  55. package/dist/port-flow.js +4 -0
  56. package/dist/port-flow.js.map +1 -1
  57. package/dist/port.js +4 -0
  58. package/dist/port.js.map +1 -1
  59. package/dist/scanner.js +30 -0
  60. package/dist/scanner.js.map +1 -0
  61. package/dist/scene/root-container-override.js +160 -96
  62. package/dist/scene/root-container-override.js.map +1 -1
  63. package/dist/shelf.js +4 -0
  64. package/dist/shelf.js.map +1 -1
  65. package/dist/static-container.js +5 -0
  66. package/dist/static-container.js.map +1 -1
  67. package/dist/stocker-abnormal-bar.js +5 -1
  68. package/dist/stocker-abnormal-bar.js.map +1 -1
  69. package/dist/stocker-capacity-bar.js +253 -6
  70. package/dist/stocker-capacity-bar.js.map +1 -1
  71. package/dist/stocker-carrier-bar.js +5 -1
  72. package/dist/stocker-carrier-bar.js.map +1 -1
  73. package/dist/stocker.js +6 -0
  74. package/dist/stocker.js.map +1 -1
  75. package/dist/templates/backdrop.js +30 -0
  76. package/dist/templates/backdrop.js.map +1 -0
  77. package/dist/templates/decal.js +21 -0
  78. package/dist/templates/decal.js.map +1 -0
  79. package/dist/templates/index.js +7 -1
  80. package/dist/templates/index.js.map +1 -1
  81. package/dist/templates/scanner.js +20 -0
  82. package/dist/templates/scanner.js.map +1 -0
  83. package/dist/tsconfig.tsbuildinfo +1 -1
  84. package/dist/utils/crane-position.js +83 -0
  85. package/dist/utils/crane-position.js.map +1 -0
  86. package/dist/utils/data-value.js +21 -0
  87. package/dist/utils/data-value.js.map +1 -0
  88. package/dist/utils/fit-gauge-scale.js +28 -0
  89. package/dist/utils/fit-gauge-scale.js.map +1 -0
  90. package/dist/utils/get-value-on-ranges.js +2 -4
  91. package/dist/utils/get-value-on-ranges.js.map +1 -1
  92. package/dist/utils/stocker-gauge.js +78 -0
  93. package/dist/utils/stocker-gauge.js.map +1 -0
  94. package/dist/zone-capacity-bar.js +3 -3
  95. package/dist/zone-capacity-bar.js.map +1 -1
  96. package/icons/backdrop.png +0 -0
  97. package/icons/decal.png +0 -0
  98. package/jest.config.js +10 -0
  99. package/package.json +4 -3
  100. package/src/agv.ts +7 -2
  101. package/src/carrier-manager.ts +223 -0
  102. package/src/carrier.ts +103 -10
  103. package/src/conveyor-join.ts +5 -2
  104. package/src/conveyor.ts +8 -0
  105. package/src/crane.ts +181 -1
  106. package/src/factories/agv-3d.ts +9 -10
  107. package/src/factories/conveyor-3d.ts +171 -84
  108. package/src/factories/crane-3d.ts +53 -12
  109. package/src/factories/crane-rail-3d.ts +8 -7
  110. package/src/factories/machine-3d.ts +616 -191
  111. package/src/factories/oht-3d.ts +20 -20
  112. package/src/factories/shuttle-3d.ts +2 -6
  113. package/src/factories/z-priority-defaults.ts +38 -0
  114. package/src/factories/z-priority.ts +60 -0
  115. package/src/features/conveyor-mixin.ts +1 -1
  116. package/src/features/mcs-status-default.ts +15 -1
  117. package/src/features/mcs-status-mixin.ts +26 -4
  118. package/src/features/mcs-zone-mixin.ts +1 -1
  119. package/src/features/parent-object-mixin.ts +17 -8
  120. package/src/floating-container.ts +5 -0
  121. package/src/index.ts +58 -34
  122. package/src/mcs-carrier-holder.ts +57 -34
  123. package/src/mcs-machine.ts +42 -0
  124. package/src/mcs-unit.ts +24 -1
  125. package/src/mcs-vehicle.ts +9 -6
  126. package/src/node.ts +9 -3
  127. package/src/oht.ts +6 -1
  128. package/src/port-flow.ts +5 -0
  129. package/src/port.ts +5 -0
  130. package/src/scanner.ts +29 -0
  131. package/src/scene/root-container-override.ts +154 -103
  132. package/src/shelf.ts +5 -0
  133. package/src/static-container.ts +5 -0
  134. package/src/stocker-abnormal-bar.ts +6 -1
  135. package/src/stocker-capacity-bar.ts +298 -7
  136. package/src/stocker-carrier-bar.ts +6 -1
  137. package/src/stocker.ts +7 -0
  138. package/src/templates/backdrop.ts +30 -0
  139. package/src/templates/decal.ts +21 -0
  140. package/src/templates/index.ts +7 -1
  141. package/src/templates/scanner.ts +20 -0
  142. package/src/utils/crane-position.ts +105 -0
  143. package/src/utils/data-value.ts +19 -0
  144. package/src/utils/fit-gauge-scale.ts +35 -0
  145. package/src/utils/get-value-on-ranges.ts +2 -4
  146. package/src/utils/stocker-gauge.ts +111 -0
  147. package/src/zone-capacity-bar.ts +3 -3
  148. package/translations/en.json +26 -1
  149. package/translations/ja.json +26 -1
  150. package/translations/ko.json +26 -1
  151. package/translations/ms.json +26 -1
  152. package/translations/zh.json +26 -1
@@ -10,7 +10,7 @@
10
10
  * Based on operato-scene Vehicle3D, adapted for fmsim:
11
11
  * - extends RealObjectGroup (Conveyor3D와 동일 패턴)
12
12
  * - statusColor from MCSStatusMixin
13
- * - 바닥 원점 (y=0~depth)
13
+ * - 볼륨 중심 원점 (things-scene 기본 컨벤션) - mesh 로컬 [-d/2, +d/2]
14
14
  */
15
15
 
16
16
  import * as THREE from 'three'
@@ -30,10 +30,6 @@ export class AGV3D extends RealObjectGroup {
30
30
  return depth || TRANSPORT_SURFACE_HEIGHT
31
31
  }
32
32
 
33
- protected get syncZPosOffset(): number {
34
- return 0
35
- }
36
-
37
33
  private resolveColor(): number {
38
34
  const comp = this.component as any
39
35
 
@@ -41,14 +37,14 @@ export class AGV3D extends RealObjectGroup {
41
37
  // '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
42
38
  try {
43
39
  const sc = comp.statusColor
44
- if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()
40
+ if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent') return new THREE.Color(sc).getHex()
45
41
  } catch {
46
42
  /* statusColor 미구현 시 무시 */
47
43
  }
48
44
 
49
45
  // 2) state.fillStyle fallback
50
46
  const { fillStyle } = this.component.state
51
- if (fillStyle && typeof fillStyle === 'string') {
47
+ if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
52
48
  try {
53
49
  return new THREE.Color(fillStyle).getHex()
54
50
  } catch {
@@ -118,7 +114,8 @@ export class AGV3D extends RealObjectGroup {
118
114
  plat.receiveShadow = true
119
115
 
120
116
  this._ownMeshes = [bumper, body, plat]
121
- for (const m of this._ownMeshes) this.object3d.add(m)
117
+ // ownGroup이 center-origin ↔ bottom-up 좌표계 변환을 자동 처리
118
+ for (const m of this._ownMeshes) this.components3D.add(m)
122
119
  }
123
120
 
124
121
  updateDimension() {}
@@ -126,7 +123,7 @@ export class AGV3D extends RealObjectGroup {
126
123
  update() {
127
124
  // 자신의 mesh만 제거 (Carrier 등 자식 컴포넌트 3D 보존)
128
125
  for (const m of this._ownMeshes) {
129
- this.object3d.remove(m)
126
+ this.components3D.remove(m)
130
127
  disposeObject3D(m)
131
128
  }
132
129
  this._ownMeshes = []
@@ -150,10 +147,12 @@ export class AGV3D extends RealObjectGroup {
150
147
 
151
148
  onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
152
149
  if ('width' in after || 'height' in after || 'depth' in after ||
153
- 'fillStyle' in after || 'strokeStyle' in after || 'id' in after) {
150
+ 'fillStyle' in after || 'strokeStyle' in after || 'id' in after ||
151
+ 'EMPTYTYPE' in after || 'CARRIERSTATUS' in after) {
154
152
  this.update()
155
153
  return
156
154
  }
155
+
157
156
  super.onchange(after, before)
158
157
  }
159
158
  }
@@ -16,23 +16,20 @@ import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUti
16
16
  import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene'
17
17
  import type { Component } from '@hatiolab/things-scene'
18
18
  import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'
19
+ import { Z_FMSIM } from './z-priority'
19
20
 
20
21
  const FRAME_COLOR = 0x888899
21
22
  const ROLLER_COLOR = 0xaaaabc
22
23
  const DEFAULT_COLOR = 0x999999
23
24
 
24
25
  export class Conveyor3D extends RealObjectGroup {
26
+ get zPriority(): number { return Z_FMSIM.CONVEYOR }
27
+
25
28
  get effectiveDepth(): number {
26
29
  const { depth } = this.component.state
27
30
  return depth || TRANSPORT_SURFACE_HEIGHT
28
31
  }
29
32
 
30
- // group origin = 바닥 (y=0). MCSRealObject3D와 동일 컨벤션.
31
- // 기하는 build()에서 y=0(바닥)~y=depth(상단) 범위로 배치.
32
- protected get syncZPosOffset(): number {
33
- return 0
34
- }
35
-
36
33
  private resolveColor(): number {
37
34
  const comp = this.component as any
38
35
 
@@ -40,14 +37,14 @@ export class Conveyor3D extends RealObjectGroup {
40
37
  // '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
41
38
  try {
42
39
  const sc = comp.statusColor
43
- if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()
40
+ if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent') return new THREE.Color(sc).getHex()
44
41
  } catch {
45
42
  /* statusColor 미구현 시 무시 */
46
43
  }
47
44
 
48
45
  // 2) state.fillStyle fallback
49
46
  const { fillStyle } = this.component.state
50
- if (fillStyle && typeof fillStyle === 'string') {
47
+ if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
51
48
  try {
52
49
  return new THREE.Color(fillStyle).getHex()
53
50
  } catch {
@@ -61,16 +58,31 @@ export class Conveyor3D extends RealObjectGroup {
61
58
  build() {
62
59
  super.build()
63
60
 
64
- const { width, height, conveyorType } = this.component.state
61
+ const { width: rawWidth, height: rawHeight, conveyorType, orientation } = this.component.state
65
62
  const depth = this.effectiveDepth
66
- if (!width || !height) return
63
+ if (!rawWidth || !rawHeight) return
64
+
65
+ // orientation: 'horizontal' = 롤러가 Z축(세로), 'vertical' = 롤러가 X축(가로)
66
+ // default = 긴 방향에 수직 (긴 축을 따라 이송)
67
+ const isVertical = orientation === 'vertical' ? true
68
+ : orientation === 'horizontal' ? false
69
+ : rawWidth < rawHeight // default: 세로가 길면 롤러는 가로(X축)
70
+ // isVertical=true → 롤러 축이 X(가로), 이송 방향이 Z(세로)
71
+ // isVertical=false → 롤러 축이 Z(세로), 이송 방향이 X(가로)
72
+ const width = rawWidth
73
+ const height = rawHeight
67
74
 
68
75
  // 2D와 동일 로직으로 rollWidth 산출
69
76
  const rollWidth = Math.max(10, Math.min(width, height) / 2)
70
77
  const rollerDiameter = Math.max(rollWidth, 2)
71
78
  const frameH = Math.min(rollerDiameter, depth)
72
79
  const legH = Math.max(depth - frameH, 0)
73
- const railW = Math.max(height * 0.06, 2)
80
+
81
+ // isVertical일 때 이송 방향이 Z축: 레일은 Z축을 따라, 벨트폭은 width
82
+ // !isVertical일 때 이송 방향이 X축: 레일은 X축을 따라, 벨트폭은 height
83
+ const beltWidth = isVertical ? width : height
84
+ const railLen = isVertical ? height : width
85
+ const railW = Math.max(beltWidth * 0.06, 2)
74
86
  const legThickness = Math.max(railW * 0.8, 2)
75
87
 
76
88
  const frameMaterial = new THREE.MeshStandardMaterial({
@@ -84,11 +96,21 @@ export class Conveyor3D extends RealObjectGroup {
84
96
  const frameGeometries: THREE.BufferGeometry[] = []
85
97
  const railY = depth - frameH / 2
86
98
 
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)
99
+ // Side rails — 이송 방향을 따라, 벨트 양측에 배치
100
+ if (isVertical) {
101
+ // 레일이 Z축(세로)을 따라, ±X에 배치
102
+ for (const xSign of [-1, 1]) {
103
+ const rail = new THREE.BoxGeometry(railW, frameH, height)
104
+ rail.translate(xSign * (width / 2 - railW / 2), railY, 0)
105
+ frameGeometries.push(rail)
106
+ }
107
+ } else {
108
+ // 레일이 X축(가로)을 따라, ±Z에 배치
109
+ for (const zSign of [-1, 1]) {
110
+ const rail = new THREE.BoxGeometry(width, frameH, railW)
111
+ rail.translate(0, railY, zSign * (height / 2 - railW / 2))
112
+ frameGeometries.push(rail)
113
+ }
92
114
  }
93
115
 
94
116
  // 4 legs at corners
@@ -96,49 +118,66 @@ export class Conveyor3D extends RealObjectGroup {
96
118
  const legTopY = depth - frameH
97
119
  const legCenterY = legTopY - legH / 2
98
120
 
121
+ // 다리 위치: 레일 안쪽에 정렬
122
+ const legXPos = isVertical ? (width / 2 - railW / 2) : (width / 2 - legThickness / 2)
123
+ const legZPos = isVertical ? (height / 2 - legThickness / 2) : (height / 2 - railW / 2)
124
+
99
125
  for (const xSign of [-1, 1]) {
100
126
  for (const zSign of [-1, 1]) {
101
- const lx = xSign * (width / 2 - legThickness / 2)
102
- const lz = zSign * (height / 2 - railW / 2)
103
127
  const leg = new THREE.BoxGeometry(legThickness, legH, legThickness)
104
- leg.translate(lx, legCenterY, lz)
128
+ leg.translate(xSign * legXPos, legCenterY, zSign * legZPos)
105
129
  frameGeometries.push(leg)
106
130
  }
107
131
  }
108
132
 
109
- // Cross-bracing: 앞뒤 다리 연결 (Z 방향)
133
+ // Cross-bracing
110
134
  const braceH = legThickness * 0.6
111
135
  const braceW = legThickness * 0.6
112
- const braceLen = height - railW
113
136
  const braceY = legTopY - legH * 0.35
114
137
 
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)
138
+ // 이송 방향 브레이스 (레일과 평행)
139
+ if (isVertical) {
140
+ for (const xSign of [-1, 1]) {
141
+ const brace = new THREE.BoxGeometry(braceW, braceH, height - legThickness)
142
+ brace.translate(xSign * legXPos, braceY, 0)
143
+ frameGeometries.push(brace)
144
+ }
145
+ } else {
146
+ for (const zSign of [-1, 1]) {
147
+ const brace = new THREE.BoxGeometry(width - legThickness, braceH, braceW)
148
+ brace.translate(0, braceY, zSign * legZPos)
149
+ frameGeometries.push(brace)
150
+ }
119
151
  }
120
152
 
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)
153
+ // 벨트폭 방향 브레이스 (레일과 수직)
154
+ if (isVertical) {
155
+ for (const zSign of [-1, 1]) {
156
+ const brace = new THREE.BoxGeometry(width - railW, braceH, braceW)
157
+ brace.translate(0, braceY, zSign * legZPos)
158
+ frameGeometries.push(brace)
159
+ }
160
+ } else {
161
+ for (const xSign of [-1, 1]) {
162
+ const brace = new THREE.BoxGeometry(braceW, braceH, height - railW)
163
+ brace.translate(xSign * legXPos, braceY, 0)
164
+ frameGeometries.push(brace)
165
+ }
127
166
  }
128
167
  }
129
168
 
130
169
  const frameMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(frameGeometries), frameMaterial)
131
170
  frameMesh.castShadow = true
132
171
  frameMesh.receiveShadow = true
133
- this.object3d.add(frameMesh)
172
+ this.components3D.add(frameMesh)
134
173
 
135
174
  // --- Rollers, Belt, or Rails ---
136
175
  if (conveyorType === 'rail') {
137
- this.buildRails(width, height, depth, frameH, railW)
176
+ this.buildRails(width, height, depth, frameH, railW, isVertical)
138
177
  } else if (conveyorType === 'belt') {
139
- this.buildBelt(width, height, depth, frameH, railW)
178
+ this.buildBelt(width, height, depth, frameH, railW, isVertical)
140
179
  } else {
141
- this.buildRollers(width, height, depth, frameH, railW, rollWidth)
180
+ this.buildRollers(width, height, depth, frameH, railW, rollWidth, isVertical)
142
181
  }
143
182
  }
144
183
 
@@ -148,28 +187,45 @@ export class Conveyor3D extends RealObjectGroup {
148
187
  depth: number,
149
188
  frameH: number,
150
189
  railW: number,
151
- rollWidth: number
190
+ rollWidth: number,
191
+ isVertical: boolean = false
152
192
  ) {
153
193
  const rollerRadius = Math.max(rollWidth / 8, 1)
154
- const rollerLength = height - railW * 2 - 0.5
155
194
  const diameter = rollerRadius * 2
156
- // 롤러 간 최소 간격 (z-fighting 방지용 미세 갭만)
157
195
  const step = diameter * 1.05
158
- // 롤러 상단 = 이송 표면 (y=depth)
159
196
  const rollerY = depth - rollerRadius
160
197
  const color = this.resolveColor()
161
198
 
162
199
  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)
200
+
201
+ if (isVertical) {
202
+ // 롤러 축 = X축(가로), 이송 방향 = Z축(세로)
203
+ const rollerLength = width - railW * 2 - 0.5
204
+ const count = Math.max(1, Math.floor(height / step))
205
+ const totalSpan = (count - 1) * step
206
+ const startZ = -totalSpan / 2
207
+
208
+ for (let i = 0; i < count; i++) {
209
+ const z = startZ + i * step
210
+ const roller = new THREE.CylinderGeometry(rollerRadius, rollerRadius, rollerLength, 16)
211
+ roller.rotateZ(Math.PI / 2)
212
+ roller.translate(0, rollerY, z)
213
+ rollerGeometries.push(roller)
214
+ }
215
+ } else {
216
+ // 롤러 축 = Z축(세로), 이송 방향 = X축(가로) — 기존 동작
217
+ const rollerLength = height - railW * 2 - 0.5
218
+ const count = Math.max(1, Math.floor(width / step))
219
+ const totalSpan = (count - 1) * step
220
+ const startX = -totalSpan / 2
221
+
222
+ for (let i = 0; i < count; i++) {
223
+ const x = startX + i * step
224
+ const roller = new THREE.CylinderGeometry(rollerRadius, rollerRadius, rollerLength, 16)
225
+ roller.rotateX(Math.PI / 2)
226
+ roller.translate(x, rollerY, 0)
227
+ rollerGeometries.push(roller)
228
+ }
173
229
  }
174
230
 
175
231
  if (rollerGeometries.length > 0) {
@@ -178,26 +234,35 @@ export class Conveyor3D extends RealObjectGroup {
178
234
  new THREE.MeshStandardMaterial({ color, metalness: 0.9, roughness: 0.2 })
179
235
  )
180
236
  rollerMesh.castShadow = true
181
- this.object3d.add(rollerMesh)
237
+ this.components3D.add(rollerMesh)
182
238
  }
183
239
  }
184
240
 
185
- private buildBelt(width: number, height: number, depth: number, frameH: number, railW: number) {
241
+ private buildBelt(width: number, height: number, depth: number, frameH: number, railW: number, isVertical: boolean = false) {
186
242
  const drumRadius = frameH * 0.38
187
- const drumLength = height - railW * 2
188
243
  const beltY = depth - drumRadius
189
244
  const color = this.resolveColor()
190
245
 
246
+ // 이송 방향 길이와 벨트폭
247
+ const transportLen = isVertical ? height : width
248
+ const beltWidth = isVertical ? width : height
249
+ const drumLength = beltWidth - railW * 2
250
+
191
251
  // Two end drums
192
252
  const drumGeometries: THREE.BufferGeometry[] = []
193
253
  const drumInset = drumRadius * 1.5
194
- const leftDrumX = -width / 2 + drumInset
195
- const rightDrumX = width / 2 - drumInset
254
+ const drum1 = -transportLen / 2 + drumInset
255
+ const drum2 = transportLen / 2 - drumInset
196
256
 
197
- for (const dx of [leftDrumX, rightDrumX]) {
257
+ for (const dp of [drum1, drum2]) {
198
258
  const drum = new THREE.CylinderGeometry(drumRadius, drumRadius, drumLength, 16)
199
- drum.rotateX(Math.PI / 2)
200
- drum.translate(dx, beltY, 0)
259
+ if (isVertical) {
260
+ drum.rotateZ(Math.PI / 2)
261
+ drum.translate(0, beltY, dp)
262
+ } else {
263
+ drum.rotateX(Math.PI / 2)
264
+ drum.translate(dp, beltY, 0)
265
+ }
201
266
  drumGeometries.push(drum)
202
267
  }
203
268
 
@@ -206,7 +271,7 @@ export class Conveyor3D extends RealObjectGroup {
206
271
  new THREE.MeshStandardMaterial({ color: ROLLER_COLOR, metalness: 0.9, roughness: 0.2 })
207
272
  )
208
273
  drumMesh.castShadow = true
209
- this.object3d.add(drumMesh)
274
+ this.components3D.add(drumMesh)
210
275
 
211
276
  // Belt surface
212
277
  const beltThickness = drumRadius * 0.12
@@ -217,18 +282,20 @@ export class Conveyor3D extends RealObjectGroup {
217
282
  })
218
283
 
219
284
  // Flat belt top surface spanning between drums
220
- const flatLen = rightDrumX - leftDrumX
221
- const flatGeo = new THREE.BoxGeometry(flatLen, beltThickness, drumLength)
285
+ const flatLen = drum2 - drum1
286
+ const flatGeo = isVertical
287
+ ? new THREE.BoxGeometry(drumLength, beltThickness, flatLen)
288
+ : new THREE.BoxGeometry(flatLen, beltThickness, drumLength)
222
289
  const flatMesh = new THREE.Mesh(flatGeo, beltMaterial)
223
290
  flatMesh.position.set(0, beltY + drumRadius, 0)
224
291
  flatMesh.castShadow = true
225
- this.object3d.add(flatMesh)
292
+ this.components3D.add(flatMesh)
226
293
 
227
294
  // Belt wrap around drums (segmented half-cylinder)
228
295
  const wrapSegments = 12
229
- for (const [dx, angleStart] of [
230
- [leftDrumX, Math.PI / 2],
231
- [rightDrumX, -Math.PI / 2],
296
+ for (const [dp, angleStart] of [
297
+ [drum1, Math.PI / 2],
298
+ [drum2, -Math.PI / 2],
232
299
  ] as [number, number][]) {
233
300
  const wrapGeometries: THREE.BufferGeometry[] = []
234
301
  for (let i = 0; i < wrapSegments; i++) {
@@ -236,36 +303,47 @@ export class Conveyor3D extends RealObjectGroup {
236
303
  const a1 = angleStart + (Math.PI * (i + 1)) / wrapSegments
237
304
  const r = drumRadius + beltThickness / 2
238
305
 
239
- const x0 = dx + Math.cos(a0) * r
306
+ const p0 = dp + Math.cos(a0) * r
240
307
  const y0 = beltY + Math.sin(a0) * r
241
- const x1 = dx + Math.cos(a1) * r
308
+ const p1 = dp + Math.cos(a1) * r
242
309
  const y1 = beltY + Math.sin(a1) * r
243
310
 
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)
311
+ const segLen = Math.sqrt((p1 - p0) ** 2 + (y1 - y0) ** 2) * 1.05
312
+ const segAngle = Math.atan2(y1 - y0, p1 - p0)
313
+
314
+ if (isVertical) {
315
+ const seg = new THREE.BoxGeometry(drumLength, beltThickness, segLen)
316
+ seg.rotateX(-segAngle)
317
+ seg.translate(0, (y0 + y1) / 2, (p0 + p1) / 2)
318
+ wrapGeometries.push(seg)
319
+ } else {
320
+ const seg = new THREE.BoxGeometry(segLen, beltThickness, drumLength)
321
+ seg.rotateZ(segAngle)
322
+ seg.translate((p0 + p1) / 2, (y0 + y1) / 2, 0)
323
+ wrapGeometries.push(seg)
324
+ }
251
325
  }
252
326
  const wrapMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(wrapGeometries), beltMaterial)
253
327
  wrapMesh.castShadow = true
254
- this.object3d.add(wrapMesh)
328
+ this.components3D.add(wrapMesh)
255
329
  }
256
330
 
257
331
  // Flat belt bottom (return path)
258
- const bottomGeo = new THREE.BoxGeometry(flatLen, beltThickness, drumLength * 0.85)
332
+ const bottomGeo = isVertical
333
+ ? new THREE.BoxGeometry(drumLength * 0.85, beltThickness, flatLen)
334
+ : new THREE.BoxGeometry(flatLen, beltThickness, drumLength * 0.85)
259
335
  const bottomMesh = new THREE.Mesh(bottomGeo, beltMaterial)
260
336
  bottomMesh.position.set(0, beltY - drumRadius, 0)
261
- this.object3d.add(bottomMesh)
337
+ this.components3D.add(bottomMesh)
262
338
  }
263
339
 
264
- private buildRails(width: number, height: number, depth: number, frameH: number, railW: number) {
340
+ private buildRails(width: number, height: number, depth: number, frameH: number, railW: number, isVertical: boolean = false) {
265
341
  const color = this.resolveColor()
266
342
  const railHeight = frameH * 0.3
267
343
  const railY = depth - railHeight / 2
268
- const innerWidth = height - railW * 2
344
+ const beltWidth = isVertical ? width : height
345
+ const transportLen = isVertical ? height : width
346
+ const innerWidth = beltWidth - railW * 2
269
347
  // 두 레일 간격: 내부 폭의 60%
270
348
  const railSpacing = innerWidth * 0.6
271
349
 
@@ -277,16 +355,24 @@ export class Conveyor3D extends RealObjectGroup {
277
355
 
278
356
  const railGeometries: THREE.BufferGeometry[] = []
279
357
 
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)
358
+ // Two parallel rails running along transport direction
359
+ if (isVertical) {
360
+ for (const xSign of [-1, 1]) {
361
+ const rail = new THREE.BoxGeometry(railW * 0.8, railHeight, transportLen - railW * 2)
362
+ rail.translate(xSign * (railSpacing / 2), railY, 0)
363
+ railGeometries.push(rail)
364
+ }
365
+ } else {
366
+ for (const zSign of [-1, 1]) {
367
+ const rail = new THREE.BoxGeometry(transportLen - railW * 2, railHeight, railW * 0.8)
368
+ rail.translate(0, railY, zSign * (railSpacing / 2))
369
+ railGeometries.push(rail)
370
+ }
285
371
  }
286
372
 
287
373
  const railMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(railGeometries), railMaterial)
288
374
  railMesh.castShadow = true
289
- this.object3d.add(railMesh)
375
+ this.components3D.add(railMesh)
290
376
  }
291
377
 
292
378
  updateDimension() {}
@@ -295,6 +381,7 @@ export class Conveyor3D extends RealObjectGroup {
295
381
  onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
296
382
  if (
297
383
  'conveyorType' in after ||
384
+ 'orientation' in after ||
298
385
  'width' in after ||
299
386
  'height' in after ||
300
387
  'depth' in after ||
@@ -22,13 +22,27 @@ const FORK_COLOR = 0x999999
22
22
  const DEFAULT_BODY_COLOR = 0xcc8844
23
23
 
24
24
  export class Crane3D extends RealObjectGroup {
25
+ /** 캐리지 포크 group — Crane.state.forkZ (0~100) 가 변하면 position.y 만 lerp.
26
+ * build() 시 inner 에 add 되고, *_mastBaseY 와 mastH 를 함께 보관* 해서
27
+ * setForkZ 가 비율 계산에 사용. */
28
+ private forkGroup?: THREE.Group
29
+ private forkMastBaseY = 0
30
+ private forkMastH = 0
31
+
25
32
  get effectiveDepth(): number {
26
33
  const { depth } = this.component.state
27
34
  return depth || 5 * TRANSPORT_SURFACE_HEIGHT - 2
28
35
  }
29
36
 
30
- protected get syncZPosOffset(): number {
31
- return 0
37
+ /**
38
+ * 0~100 정규화 z (= state.forkZ) 를 mast 높이 비율로 변환해서 포크 group 의
39
+ * three-up y 위치 갱신. build() 가 안 끝났으면 noop — build 가 forkZ 를 다시
40
+ * 반영하면서 끝난다.
41
+ */
42
+ applyForkZ(z: number): void {
43
+ if (!this.forkGroup) return
44
+ const clamped = Math.max(0, Math.min(100, Number(z) || 0))
45
+ this.forkGroup.position.y = this.forkMastBaseY + (clamped / 100) * this.forkMastH
32
46
  }
33
47
 
34
48
  private resolveColor(): number {
@@ -62,6 +76,8 @@ export class Crane3D extends RealObjectGroup {
62
76
  const d = this.effectiveDepth
63
77
  if (!w || !h) return
64
78
 
79
+ const inner = this.components3D
80
+
65
81
  // 높이 배분: 하단 클램프 4%, 마스트 92%, 상단 클램프 4%
66
82
  const clampH = d * 0.04
67
83
  const mastH = d * 0.92
@@ -81,7 +97,7 @@ export class Crane3D extends RealObjectGroup {
81
97
  const bottomClamp = new THREE.Mesh(bottomClampGeo, clampMat)
82
98
  bottomClamp.position.y = clampH / 2
83
99
  bottomClamp.castShadow = true
84
- this.object3d.add(bottomClamp)
100
+ inner.add(bottomClamp)
85
101
 
86
102
  // ── 2. 수직 마스트 (좌우 기둥 + 상·하 가로 연결 바) ──
87
103
  const mastBaseY = clampH
@@ -115,10 +131,12 @@ export class Crane3D extends RealObjectGroup {
115
131
 
116
132
  const mastMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(mastGeometries), mastMat)
117
133
  mastMesh.castShadow = true
118
- this.object3d.add(mastMesh)
134
+ inner.add(mastMesh)
119
135
 
120
- // ── 3. 캐리지 포크 (마스트 40% 높이, 양쪽으로 뻗는 포크 암) ──
121
- const forkY = mastBaseY + mastH * 0.4
136
+ // ── 3. 캐리지 포크 (별도 group — state.forkZ 변경 시 group.position.y lerp) ──
137
+ // 포크 geometry 는 *_mastBaseY 기준 local 0 에 둠* — 즉 group.position.y = 0
138
+ // 일 때 mast 바닥 자리에. forkZ 0~100 에 따라 group.position.y 만 변경하면
139
+ // mast 따라 위/아래 이동. mesh 재구축 불필요.
122
140
  const forkMat = new THREE.MeshStandardMaterial({
123
141
  color: FORK_COLOR,
124
142
  roughness: 0.4,
@@ -127,15 +145,13 @@ export class Crane3D extends RealObjectGroup {
127
145
 
128
146
  const forkGeometries: THREE.BufferGeometry[] = []
129
147
 
130
- // 캐리지 중심 플레이트 (마스트에 부착, 양쪽 포크의 베이스)
131
148
  const plateW = w * 0.9
132
149
  const plateH = d * 0.08
133
150
  const plateD = pillarThick * 0.8
134
151
  const centerPlate = new THREE.BoxGeometry(plateW, plateH, plateD)
135
- centerPlate.translate(0, forkY + plateH / 2, 0)
152
+ centerPlate.translate(0, plateH / 2, 0) // local 0 = mast 바닥
136
153
  forkGeometries.push(centerPlate)
137
154
 
138
- // 양쪽 포크 암 (좌/우 선반 방향으로 뻗음)
139
155
  const armW = pillarThick * 0.6
140
156
  const armH = plateH * 0.4
141
157
  const armD = h * 0.6
@@ -144,31 +160,56 @@ export class Crane3D extends RealObjectGroup {
144
160
  for (const zSign of [-1, 1]) {
145
161
  for (const xSign of [-1, 1]) {
146
162
  const arm = new THREE.BoxGeometry(armW, armH, armD)
147
- arm.translate(xSign * (armSpacing / 2), forkY + armH / 2, zSign * (plateD / 2 + armD / 2))
163
+ arm.translate(xSign * (armSpacing / 2), armH / 2, zSign * (plateD / 2 + armD / 2))
148
164
  forkGeometries.push(arm)
149
165
  }
150
166
  }
151
167
 
152
168
  const forkMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(forkGeometries), forkMat)
153
169
  forkMesh.castShadow = true
154
- this.object3d.add(forkMesh)
170
+
171
+ const forkGroup = new THREE.Group()
172
+ forkGroup.add(forkMesh)
173
+ inner.add(forkGroup)
174
+ this.forkGroup = forkGroup
175
+ this.forkMastBaseY = mastBaseY
176
+ // forkZ=100 시점에 mast 의 *_위쪽 한계*. 포크 자체 두께 (plateH) 만큼은 마스트
177
+ // 안에 머무르도록 limit 보정 — 그렇지 않으면 forkZ=100 일 때 포크가 상단
178
+ // 클램프와 충돌. mastH * 0.92 로 약간 보수적 (= 마스트 본체 92% 까지만 이동).
179
+ this.forkMastH = mastH * 0.92
180
+
181
+ // 초기 forkZ 반영 — state.forkZ 가 이미 set 되어 있으면 그 위치에 배치
182
+ const initialForkZ = Number((this.component.state as any).forkZ)
183
+ this.applyForkZ(Number.isFinite(initialForkZ) ? initialForkZ : 0)
155
184
 
156
185
  // ── 4. 상단 클램프 (천장 레일에 물리는 가이드부) ──
157
186
  const topClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD)
158
187
  const topClamp = new THREE.Mesh(topClampGeo, clampMat)
159
188
  topClamp.position.y = mastBaseY + mastH + clampH / 2
160
189
  topClamp.castShadow = true
161
- this.object3d.add(topClamp)
190
+ inner.add(topClamp)
162
191
  }
163
192
 
164
193
  updateDimension() {}
165
194
  updateAlpha() {}
166
195
 
196
+ /** build() 직전 호출. forkGroup 참조를 깨끗이 비워둔다 — 새 build 가 다시 set. */
197
+ clear() {
198
+ this.forkGroup = undefined
199
+ super.clear()
200
+ }
201
+
167
202
  onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
168
203
  if ('width' in after || 'height' in after || 'depth' in after || 'fillStyle' in after || 'strokeStyle' in after) {
169
204
  this.update()
170
205
  return
171
206
  }
207
+ // forkZ 는 mesh 재구축 없이 group.position.y 만 갱신. 같은 onchange 에서
208
+ // left/top 도 함께 변경될 수 있으므로 super 도 반드시 호출해 본체 transform
209
+ // 갱신 path (state.left/top → obj3d.position.x/z) 가 동작하게 함.
210
+ if ('forkZ' in after) {
211
+ this.applyForkZ(Number(after.forkZ) || 0)
212
+ }
172
213
  super.onchange(after, before)
173
214
  }
174
215
  }