@fmsim/machine 2.0.0-beta.7 → 2.0.0-beta.75

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 (119) hide show
  1. package/dist/agv.js +2 -2
  2. package/dist/agv.js.map +1 -1
  3. package/dist/carrier-manager.js +93 -0
  4. package/dist/carrier-manager.js.map +1 -0
  5. package/dist/carrier.js +81 -5
  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.map +1 -1
  12. package/dist/factories/agv-3d.js +8 -9
  13. package/dist/factories/agv-3d.js.map +1 -1
  14. package/dist/factories/conveyor-3d.js +166 -81
  15. package/dist/factories/conveyor-3d.js.map +1 -1
  16. package/dist/factories/crane-3d.js +5 -7
  17. package/dist/factories/crane-3d.js.map +1 -1
  18. package/dist/factories/crane-rail-3d.js +4 -6
  19. package/dist/factories/crane-rail-3d.js.map +1 -1
  20. package/dist/factories/machine-3d.js +384 -133
  21. package/dist/factories/machine-3d.js.map +1 -1
  22. package/dist/factories/oht-3d.js +20 -20
  23. package/dist/factories/oht-3d.js.map +1 -1
  24. package/dist/factories/shuttle-3d.js +2 -5
  25. package/dist/factories/shuttle-3d.js.map +1 -1
  26. package/dist/features/conveyor-mixin.js.map +1 -1
  27. package/dist/features/mcs-status-mixin.js +22 -0
  28. package/dist/features/mcs-status-mixin.js.map +1 -1
  29. package/dist/features/mcs-zone-mixin.js.map +1 -1
  30. package/dist/features/parent-object-mixin.js.map +1 -1
  31. package/dist/floating-container.js +5 -0
  32. package/dist/floating-container.js.map +1 -1
  33. package/dist/index.js +50 -34
  34. package/dist/index.js.map +1 -1
  35. package/dist/mcs-carrier-holder.js +39 -22
  36. package/dist/mcs-carrier-holder.js.map +1 -1
  37. package/dist/mcs-machine.js +28 -0
  38. package/dist/mcs-machine.js.map +1 -1
  39. package/dist/mcs-unit.js +10 -0
  40. package/dist/mcs-unit.js.map +1 -1
  41. package/dist/mcs-vehicle.js +8 -3
  42. package/dist/mcs-vehicle.js.map +1 -1
  43. package/dist/node.js +1 -1
  44. package/dist/node.js.map +1 -1
  45. package/dist/oht.js.map +1 -1
  46. package/dist/scanner.js +30 -0
  47. package/dist/scanner.js.map +1 -0
  48. package/dist/scene/root-container-override.js +9 -0
  49. package/dist/scene/root-container-override.js.map +1 -1
  50. package/dist/static-container.js +5 -0
  51. package/dist/static-container.js.map +1 -1
  52. package/dist/stocker-abnormal-bar.js +1 -1
  53. package/dist/stocker-abnormal-bar.js.map +1 -1
  54. package/dist/stocker-capacity-bar.js +3 -3
  55. package/dist/stocker-capacity-bar.js.map +1 -1
  56. package/dist/stocker-carrier-bar.js +1 -1
  57. package/dist/stocker-carrier-bar.js.map +1 -1
  58. package/dist/stocker.js +6 -0
  59. package/dist/stocker.js.map +1 -1
  60. package/dist/templates/backdrop.js +30 -0
  61. package/dist/templates/backdrop.js.map +1 -0
  62. package/dist/templates/decal.js +21 -0
  63. package/dist/templates/decal.js.map +1 -0
  64. package/dist/templates/index.js +7 -1
  65. package/dist/templates/index.js.map +1 -1
  66. package/dist/templates/scanner.js +20 -0
  67. package/dist/templates/scanner.js.map +1 -0
  68. package/dist/tsconfig.tsbuildinfo +1 -1
  69. package/dist/utils/get-value-on-ranges.js +2 -4
  70. package/dist/utils/get-value-on-ranges.js.map +1 -1
  71. package/dist/zone-capacity-bar.js +3 -3
  72. package/dist/zone-capacity-bar.js.map +1 -1
  73. package/icons/backdrop.png +0 -0
  74. package/icons/decal.png +0 -0
  75. package/jest.config.js +10 -0
  76. package/package.json +4 -3
  77. package/src/agv.ts +2 -2
  78. package/src/carrier-manager.ts +117 -0
  79. package/src/carrier.ts +80 -3
  80. package/src/conveyor-join.ts +5 -2
  81. package/src/conveyor.ts +8 -0
  82. package/src/crane.ts +1 -1
  83. package/src/factories/agv-3d.ts +9 -10
  84. package/src/factories/conveyor-3d.ts +168 -84
  85. package/src/factories/crane-3d.ts +6 -8
  86. package/src/factories/crane-rail-3d.ts +5 -7
  87. package/src/factories/machine-3d.ts +452 -145
  88. package/src/factories/oht-3d.ts +20 -20
  89. package/src/factories/shuttle-3d.ts +2 -6
  90. package/src/features/conveyor-mixin.ts +1 -1
  91. package/src/features/mcs-status-mixin.ts +26 -4
  92. package/src/features/mcs-zone-mixin.ts +1 -1
  93. package/src/features/parent-object-mixin.ts +1 -1
  94. package/src/floating-container.ts +5 -0
  95. package/src/index.ts +56 -34
  96. package/src/mcs-carrier-holder.ts +38 -34
  97. package/src/mcs-machine.ts +29 -0
  98. package/src/mcs-unit.ts +12 -1
  99. package/src/mcs-vehicle.ts +9 -6
  100. package/src/node.ts +1 -1
  101. package/src/oht.ts +1 -1
  102. package/src/scanner.ts +29 -0
  103. package/src/scene/root-container-override.ts +12 -0
  104. package/src/static-container.ts +5 -0
  105. package/src/stocker-abnormal-bar.ts +1 -1
  106. package/src/stocker-capacity-bar.ts +3 -3
  107. package/src/stocker-carrier-bar.ts +1 -1
  108. package/src/stocker.ts +7 -0
  109. package/src/templates/backdrop.ts +30 -0
  110. package/src/templates/decal.ts +21 -0
  111. package/src/templates/index.ts +7 -1
  112. package/src/templates/scanner.ts +20 -0
  113. package/src/utils/get-value-on-ranges.ts +2 -4
  114. package/src/zone-capacity-bar.ts +3 -3
  115. package/translations/en.json +16 -1
  116. package/translations/ja.json +16 -1
  117. package/translations/ko.json +16 -1
  118. package/translations/ms.json +16 -1
  119. package/translations/zh.json +16 -1
@@ -27,12 +27,6 @@ export class Conveyor3D extends RealObjectGroup {
27
27
  return depth || TRANSPORT_SURFACE_HEIGHT
28
28
  }
29
29
 
30
- // group origin = 바닥 (y=0). MCSRealObject3D와 동일 컨벤션.
31
- // 기하는 build()에서 y=0(바닥)~y=depth(상단) 범위로 배치.
32
- protected get syncZPosOffset(): number {
33
- return 0
34
- }
35
-
36
30
  private resolveColor(): number {
37
31
  const comp = this.component as any
38
32
 
@@ -40,14 +34,14 @@ export class Conveyor3D extends RealObjectGroup {
40
34
  // '#F0F0F0'은 상태 미설정 시 기본 fallback — 무시하고 defaultColor 사용
41
35
  try {
42
36
  const sc = comp.statusColor
43
- if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()
37
+ if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0' && sc !== 'transparent') return new THREE.Color(sc).getHex()
44
38
  } catch {
45
39
  /* statusColor 미구현 시 무시 */
46
40
  }
47
41
 
48
42
  // 2) state.fillStyle fallback
49
43
  const { fillStyle } = this.component.state
50
- if (fillStyle && typeof fillStyle === 'string') {
44
+ if (fillStyle && typeof fillStyle === 'string' && fillStyle !== 'transparent') {
51
45
  try {
52
46
  return new THREE.Color(fillStyle).getHex()
53
47
  } catch {
@@ -61,16 +55,31 @@ export class Conveyor3D extends RealObjectGroup {
61
55
  build() {
62
56
  super.build()
63
57
 
64
- const { width, height, conveyorType } = this.component.state
58
+ const { width: rawWidth, height: rawHeight, conveyorType, orientation } = this.component.state
65
59
  const depth = this.effectiveDepth
66
- if (!width || !height) return
60
+ if (!rawWidth || !rawHeight) return
61
+
62
+ // orientation: 'horizontal' = 롤러가 Z축(세로), 'vertical' = 롤러가 X축(가로)
63
+ // default = 긴 방향에 수직 (긴 축을 따라 이송)
64
+ const isVertical = orientation === 'vertical' ? true
65
+ : orientation === 'horizontal' ? false
66
+ : rawWidth < rawHeight // default: 세로가 길면 롤러는 가로(X축)
67
+ // isVertical=true → 롤러 축이 X(가로), 이송 방향이 Z(세로)
68
+ // isVertical=false → 롤러 축이 Z(세로), 이송 방향이 X(가로)
69
+ const width = rawWidth
70
+ const height = rawHeight
67
71
 
68
72
  // 2D와 동일 로직으로 rollWidth 산출
69
73
  const rollWidth = Math.max(10, Math.min(width, height) / 2)
70
74
  const rollerDiameter = Math.max(rollWidth, 2)
71
75
  const frameH = Math.min(rollerDiameter, depth)
72
76
  const legH = Math.max(depth - frameH, 0)
73
- const railW = Math.max(height * 0.06, 2)
77
+
78
+ // isVertical일 때 이송 방향이 Z축: 레일은 Z축을 따라, 벨트폭은 width
79
+ // !isVertical일 때 이송 방향이 X축: 레일은 X축을 따라, 벨트폭은 height
80
+ const beltWidth = isVertical ? width : height
81
+ const railLen = isVertical ? height : width
82
+ const railW = Math.max(beltWidth * 0.06, 2)
74
83
  const legThickness = Math.max(railW * 0.8, 2)
75
84
 
76
85
  const frameMaterial = new THREE.MeshStandardMaterial({
@@ -84,11 +93,21 @@ export class Conveyor3D extends RealObjectGroup {
84
93
  const frameGeometries: THREE.BufferGeometry[] = []
85
94
  const railY = depth - frameH / 2
86
95
 
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)
96
+ // Side rails — 이송 방향을 따라, 벨트 양측에 배치
97
+ if (isVertical) {
98
+ // 레일이 Z축(세로)을 따라, ±X에 배치
99
+ for (const xSign of [-1, 1]) {
100
+ const rail = new THREE.BoxGeometry(railW, frameH, height)
101
+ rail.translate(xSign * (width / 2 - railW / 2), railY, 0)
102
+ frameGeometries.push(rail)
103
+ }
104
+ } else {
105
+ // 레일이 X축(가로)을 따라, ±Z에 배치
106
+ for (const zSign of [-1, 1]) {
107
+ const rail = new THREE.BoxGeometry(width, frameH, railW)
108
+ rail.translate(0, railY, zSign * (height / 2 - railW / 2))
109
+ frameGeometries.push(rail)
110
+ }
92
111
  }
93
112
 
94
113
  // 4 legs at corners
@@ -96,49 +115,66 @@ export class Conveyor3D extends RealObjectGroup {
96
115
  const legTopY = depth - frameH
97
116
  const legCenterY = legTopY - legH / 2
98
117
 
118
+ // 다리 위치: 레일 안쪽에 정렬
119
+ const legXPos = isVertical ? (width / 2 - railW / 2) : (width / 2 - legThickness / 2)
120
+ const legZPos = isVertical ? (height / 2 - legThickness / 2) : (height / 2 - railW / 2)
121
+
99
122
  for (const xSign of [-1, 1]) {
100
123
  for (const zSign of [-1, 1]) {
101
- const lx = xSign * (width / 2 - legThickness / 2)
102
- const lz = zSign * (height / 2 - railW / 2)
103
124
  const leg = new THREE.BoxGeometry(legThickness, legH, legThickness)
104
- leg.translate(lx, legCenterY, lz)
125
+ leg.translate(xSign * legXPos, legCenterY, zSign * legZPos)
105
126
  frameGeometries.push(leg)
106
127
  }
107
128
  }
108
129
 
109
- // Cross-bracing: 앞뒤 다리 연결 (Z 방향)
130
+ // Cross-bracing
110
131
  const braceH = legThickness * 0.6
111
132
  const braceW = legThickness * 0.6
112
- const braceLen = height - railW
113
133
  const braceY = legTopY - legH * 0.35
114
134
 
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)
135
+ // 이송 방향 브레이스 (레일과 평행)
136
+ if (isVertical) {
137
+ for (const xSign of [-1, 1]) {
138
+ const brace = new THREE.BoxGeometry(braceW, braceH, height - legThickness)
139
+ brace.translate(xSign * legXPos, braceY, 0)
140
+ frameGeometries.push(brace)
141
+ }
142
+ } else {
143
+ for (const zSign of [-1, 1]) {
144
+ const brace = new THREE.BoxGeometry(width - legThickness, braceH, braceW)
145
+ brace.translate(0, braceY, zSign * legZPos)
146
+ frameGeometries.push(brace)
147
+ }
119
148
  }
120
149
 
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)
150
+ // 벨트폭 방향 브레이스 (레일과 수직)
151
+ if (isVertical) {
152
+ for (const zSign of [-1, 1]) {
153
+ const brace = new THREE.BoxGeometry(width - railW, braceH, braceW)
154
+ brace.translate(0, braceY, zSign * legZPos)
155
+ frameGeometries.push(brace)
156
+ }
157
+ } else {
158
+ for (const xSign of [-1, 1]) {
159
+ const brace = new THREE.BoxGeometry(braceW, braceH, height - railW)
160
+ brace.translate(xSign * legXPos, braceY, 0)
161
+ frameGeometries.push(brace)
162
+ }
127
163
  }
128
164
  }
129
165
 
130
166
  const frameMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(frameGeometries), frameMaterial)
131
167
  frameMesh.castShadow = true
132
168
  frameMesh.receiveShadow = true
133
- this.object3d.add(frameMesh)
169
+ this.components3D.add(frameMesh)
134
170
 
135
171
  // --- Rollers, Belt, or Rails ---
136
172
  if (conveyorType === 'rail') {
137
- this.buildRails(width, height, depth, frameH, railW)
173
+ this.buildRails(width, height, depth, frameH, railW, isVertical)
138
174
  } else if (conveyorType === 'belt') {
139
- this.buildBelt(width, height, depth, frameH, railW)
175
+ this.buildBelt(width, height, depth, frameH, railW, isVertical)
140
176
  } else {
141
- this.buildRollers(width, height, depth, frameH, railW, rollWidth)
177
+ this.buildRollers(width, height, depth, frameH, railW, rollWidth, isVertical)
142
178
  }
143
179
  }
144
180
 
@@ -148,28 +184,45 @@ export class Conveyor3D extends RealObjectGroup {
148
184
  depth: number,
149
185
  frameH: number,
150
186
  railW: number,
151
- rollWidth: number
187
+ rollWidth: number,
188
+ isVertical: boolean = false
152
189
  ) {
153
190
  const rollerRadius = Math.max(rollWidth / 8, 1)
154
- const rollerLength = height - railW * 2 - 0.5
155
191
  const diameter = rollerRadius * 2
156
- // 롤러 간 최소 간격 (z-fighting 방지용 미세 갭만)
157
192
  const step = diameter * 1.05
158
- // 롤러 상단 = 이송 표면 (y=depth)
159
193
  const rollerY = depth - rollerRadius
160
194
  const color = this.resolveColor()
161
195
 
162
196
  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)
197
+
198
+ if (isVertical) {
199
+ // 롤러 축 = X축(가로), 이송 방향 = Z축(세로)
200
+ const rollerLength = width - railW * 2 - 0.5
201
+ const count = Math.max(1, Math.floor(height / step))
202
+ const totalSpan = (count - 1) * step
203
+ const startZ = -totalSpan / 2
204
+
205
+ for (let i = 0; i < count; i++) {
206
+ const z = startZ + i * step
207
+ const roller = new THREE.CylinderGeometry(rollerRadius, rollerRadius, rollerLength, 16)
208
+ roller.rotateZ(Math.PI / 2)
209
+ roller.translate(0, rollerY, z)
210
+ rollerGeometries.push(roller)
211
+ }
212
+ } else {
213
+ // 롤러 축 = Z축(세로), 이송 방향 = X축(가로) — 기존 동작
214
+ const rollerLength = height - railW * 2 - 0.5
215
+ const count = Math.max(1, Math.floor(width / step))
216
+ const totalSpan = (count - 1) * step
217
+ const startX = -totalSpan / 2
218
+
219
+ for (let i = 0; i < count; i++) {
220
+ const x = startX + i * step
221
+ const roller = new THREE.CylinderGeometry(rollerRadius, rollerRadius, rollerLength, 16)
222
+ roller.rotateX(Math.PI / 2)
223
+ roller.translate(x, rollerY, 0)
224
+ rollerGeometries.push(roller)
225
+ }
173
226
  }
174
227
 
175
228
  if (rollerGeometries.length > 0) {
@@ -178,26 +231,35 @@ export class Conveyor3D extends RealObjectGroup {
178
231
  new THREE.MeshStandardMaterial({ color, metalness: 0.9, roughness: 0.2 })
179
232
  )
180
233
  rollerMesh.castShadow = true
181
- this.object3d.add(rollerMesh)
234
+ this.components3D.add(rollerMesh)
182
235
  }
183
236
  }
184
237
 
185
- private buildBelt(width: number, height: number, depth: number, frameH: number, railW: number) {
238
+ private buildBelt(width: number, height: number, depth: number, frameH: number, railW: number, isVertical: boolean = false) {
186
239
  const drumRadius = frameH * 0.38
187
- const drumLength = height - railW * 2
188
240
  const beltY = depth - drumRadius
189
241
  const color = this.resolveColor()
190
242
 
243
+ // 이송 방향 길이와 벨트폭
244
+ const transportLen = isVertical ? height : width
245
+ const beltWidth = isVertical ? width : height
246
+ const drumLength = beltWidth - railW * 2
247
+
191
248
  // Two end drums
192
249
  const drumGeometries: THREE.BufferGeometry[] = []
193
250
  const drumInset = drumRadius * 1.5
194
- const leftDrumX = -width / 2 + drumInset
195
- const rightDrumX = width / 2 - drumInset
251
+ const drum1 = -transportLen / 2 + drumInset
252
+ const drum2 = transportLen / 2 - drumInset
196
253
 
197
- for (const dx of [leftDrumX, rightDrumX]) {
254
+ for (const dp of [drum1, drum2]) {
198
255
  const drum = new THREE.CylinderGeometry(drumRadius, drumRadius, drumLength, 16)
199
- drum.rotateX(Math.PI / 2)
200
- drum.translate(dx, beltY, 0)
256
+ if (isVertical) {
257
+ drum.rotateZ(Math.PI / 2)
258
+ drum.translate(0, beltY, dp)
259
+ } else {
260
+ drum.rotateX(Math.PI / 2)
261
+ drum.translate(dp, beltY, 0)
262
+ }
201
263
  drumGeometries.push(drum)
202
264
  }
203
265
 
@@ -206,7 +268,7 @@ export class Conveyor3D extends RealObjectGroup {
206
268
  new THREE.MeshStandardMaterial({ color: ROLLER_COLOR, metalness: 0.9, roughness: 0.2 })
207
269
  )
208
270
  drumMesh.castShadow = true
209
- this.object3d.add(drumMesh)
271
+ this.components3D.add(drumMesh)
210
272
 
211
273
  // Belt surface
212
274
  const beltThickness = drumRadius * 0.12
@@ -217,18 +279,20 @@ export class Conveyor3D extends RealObjectGroup {
217
279
  })
218
280
 
219
281
  // Flat belt top surface spanning between drums
220
- const flatLen = rightDrumX - leftDrumX
221
- const flatGeo = new THREE.BoxGeometry(flatLen, beltThickness, drumLength)
282
+ const flatLen = drum2 - drum1
283
+ const flatGeo = isVertical
284
+ ? new THREE.BoxGeometry(drumLength, beltThickness, flatLen)
285
+ : new THREE.BoxGeometry(flatLen, beltThickness, drumLength)
222
286
  const flatMesh = new THREE.Mesh(flatGeo, beltMaterial)
223
287
  flatMesh.position.set(0, beltY + drumRadius, 0)
224
288
  flatMesh.castShadow = true
225
- this.object3d.add(flatMesh)
289
+ this.components3D.add(flatMesh)
226
290
 
227
291
  // Belt wrap around drums (segmented half-cylinder)
228
292
  const wrapSegments = 12
229
- for (const [dx, angleStart] of [
230
- [leftDrumX, Math.PI / 2],
231
- [rightDrumX, -Math.PI / 2],
293
+ for (const [dp, angleStart] of [
294
+ [drum1, Math.PI / 2],
295
+ [drum2, -Math.PI / 2],
232
296
  ] as [number, number][]) {
233
297
  const wrapGeometries: THREE.BufferGeometry[] = []
234
298
  for (let i = 0; i < wrapSegments; i++) {
@@ -236,36 +300,47 @@ export class Conveyor3D extends RealObjectGroup {
236
300
  const a1 = angleStart + (Math.PI * (i + 1)) / wrapSegments
237
301
  const r = drumRadius + beltThickness / 2
238
302
 
239
- const x0 = dx + Math.cos(a0) * r
303
+ const p0 = dp + Math.cos(a0) * r
240
304
  const y0 = beltY + Math.sin(a0) * r
241
- const x1 = dx + Math.cos(a1) * r
305
+ const p1 = dp + Math.cos(a1) * r
242
306
  const y1 = beltY + Math.sin(a1) * r
243
307
 
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)
308
+ const segLen = Math.sqrt((p1 - p0) ** 2 + (y1 - y0) ** 2) * 1.05
309
+ const segAngle = Math.atan2(y1 - y0, p1 - p0)
310
+
311
+ if (isVertical) {
312
+ const seg = new THREE.BoxGeometry(drumLength, beltThickness, segLen)
313
+ seg.rotateX(-segAngle)
314
+ seg.translate(0, (y0 + y1) / 2, (p0 + p1) / 2)
315
+ wrapGeometries.push(seg)
316
+ } else {
317
+ const seg = new THREE.BoxGeometry(segLen, beltThickness, drumLength)
318
+ seg.rotateZ(segAngle)
319
+ seg.translate((p0 + p1) / 2, (y0 + y1) / 2, 0)
320
+ wrapGeometries.push(seg)
321
+ }
251
322
  }
252
323
  const wrapMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(wrapGeometries), beltMaterial)
253
324
  wrapMesh.castShadow = true
254
- this.object3d.add(wrapMesh)
325
+ this.components3D.add(wrapMesh)
255
326
  }
256
327
 
257
328
  // Flat belt bottom (return path)
258
- const bottomGeo = new THREE.BoxGeometry(flatLen, beltThickness, drumLength * 0.85)
329
+ const bottomGeo = isVertical
330
+ ? new THREE.BoxGeometry(drumLength * 0.85, beltThickness, flatLen)
331
+ : new THREE.BoxGeometry(flatLen, beltThickness, drumLength * 0.85)
259
332
  const bottomMesh = new THREE.Mesh(bottomGeo, beltMaterial)
260
333
  bottomMesh.position.set(0, beltY - drumRadius, 0)
261
- this.object3d.add(bottomMesh)
334
+ this.components3D.add(bottomMesh)
262
335
  }
263
336
 
264
- private buildRails(width: number, height: number, depth: number, frameH: number, railW: number) {
337
+ private buildRails(width: number, height: number, depth: number, frameH: number, railW: number, isVertical: boolean = false) {
265
338
  const color = this.resolveColor()
266
339
  const railHeight = frameH * 0.3
267
340
  const railY = depth - railHeight / 2
268
- const innerWidth = height - railW * 2
341
+ const beltWidth = isVertical ? width : height
342
+ const transportLen = isVertical ? height : width
343
+ const innerWidth = beltWidth - railW * 2
269
344
  // 두 레일 간격: 내부 폭의 60%
270
345
  const railSpacing = innerWidth * 0.6
271
346
 
@@ -277,16 +352,24 @@ export class Conveyor3D extends RealObjectGroup {
277
352
 
278
353
  const railGeometries: THREE.BufferGeometry[] = []
279
354
 
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)
355
+ // Two parallel rails running along transport direction
356
+ if (isVertical) {
357
+ for (const xSign of [-1, 1]) {
358
+ const rail = new THREE.BoxGeometry(railW * 0.8, railHeight, transportLen - railW * 2)
359
+ rail.translate(xSign * (railSpacing / 2), railY, 0)
360
+ railGeometries.push(rail)
361
+ }
362
+ } else {
363
+ for (const zSign of [-1, 1]) {
364
+ const rail = new THREE.BoxGeometry(transportLen - railW * 2, railHeight, railW * 0.8)
365
+ rail.translate(0, railY, zSign * (railSpacing / 2))
366
+ railGeometries.push(rail)
367
+ }
285
368
  }
286
369
 
287
370
  const railMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(railGeometries), railMaterial)
288
371
  railMesh.castShadow = true
289
- this.object3d.add(railMesh)
372
+ this.components3D.add(railMesh)
290
373
  }
291
374
 
292
375
  updateDimension() {}
@@ -295,6 +378,7 @@ export class Conveyor3D extends RealObjectGroup {
295
378
  onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
296
379
  if (
297
380
  'conveyorType' in after ||
381
+ 'orientation' in after ||
298
382
  'width' in after ||
299
383
  'height' in after ||
300
384
  'depth' in after ||
@@ -27,10 +27,6 @@ export class Crane3D extends RealObjectGroup {
27
27
  return depth || 5 * TRANSPORT_SURFACE_HEIGHT - 2
28
28
  }
29
29
 
30
- protected get syncZPosOffset(): number {
31
- return 0
32
- }
33
-
34
30
  private resolveColor(): number {
35
31
  const comp = this.component as any
36
32
 
@@ -62,6 +58,8 @@ export class Crane3D extends RealObjectGroup {
62
58
  const d = this.effectiveDepth
63
59
  if (!w || !h) return
64
60
 
61
+ const inner = this.components3D
62
+
65
63
  // 높이 배분: 하단 클램프 4%, 마스트 92%, 상단 클램프 4%
66
64
  const clampH = d * 0.04
67
65
  const mastH = d * 0.92
@@ -81,7 +79,7 @@ export class Crane3D extends RealObjectGroup {
81
79
  const bottomClamp = new THREE.Mesh(bottomClampGeo, clampMat)
82
80
  bottomClamp.position.y = clampH / 2
83
81
  bottomClamp.castShadow = true
84
- this.object3d.add(bottomClamp)
82
+ inner.add(bottomClamp)
85
83
 
86
84
  // ── 2. 수직 마스트 (좌우 기둥 + 상·하 가로 연결 바) ──
87
85
  const mastBaseY = clampH
@@ -115,7 +113,7 @@ export class Crane3D extends RealObjectGroup {
115
113
 
116
114
  const mastMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(mastGeometries), mastMat)
117
115
  mastMesh.castShadow = true
118
- this.object3d.add(mastMesh)
116
+ inner.add(mastMesh)
119
117
 
120
118
  // ── 3. 캐리지 포크 (마스트 40% 높이, 양쪽으로 뻗는 포크 암) ──
121
119
  const forkY = mastBaseY + mastH * 0.4
@@ -151,14 +149,14 @@ export class Crane3D extends RealObjectGroup {
151
149
 
152
150
  const forkMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(forkGeometries), forkMat)
153
151
  forkMesh.castShadow = true
154
- this.object3d.add(forkMesh)
152
+ inner.add(forkMesh)
155
153
 
156
154
  // ── 4. 상단 클램프 (천장 레일에 물리는 가이드부) ──
157
155
  const topClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD)
158
156
  const topClamp = new THREE.Mesh(topClampGeo, clampMat)
159
157
  topClamp.position.y = mastBaseY + mastH + clampH / 2
160
158
  topClamp.castShadow = true
161
- this.object3d.add(topClamp)
159
+ inner.add(topClamp)
162
160
  }
163
161
 
164
162
  updateDimension() {}
@@ -28,10 +28,6 @@ export class CraneRail3D extends RealObjectGroup {
28
28
  return depth || DEFAULT_RAIL_DEPTH
29
29
  }
30
30
 
31
- protected get syncZPosOffset(): number {
32
- return 0
33
- }
34
-
35
31
  build() {
36
32
  super.build()
37
33
 
@@ -41,6 +37,8 @@ export class CraneRail3D extends RealObjectGroup {
41
37
  const d = this.effectiveDepth
42
38
  if (!w || !h) return
43
39
 
40
+ const inner = this.components3D
41
+
44
42
  const railH = Math.max(d * 0.02, 2) // 레일 높이 (얇은 트랙)
45
43
  const railW = Math.max(h * 0.06, 2) // 레일 폭
46
44
  const supportThick = Math.max(Math.min(w, h) * 0.05, 2)
@@ -67,7 +65,7 @@ export class CraneRail3D extends RealObjectGroup {
67
65
  const bottomMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(bottomGeometries), railMat)
68
66
  bottomMesh.castShadow = true
69
67
  bottomMesh.receiveShadow = true
70
- this.object3d.add(bottomMesh)
68
+ inner.add(bottomMesh)
71
69
 
72
70
  // ── 천장 가이드 레일 (2개 평행 트랙) ──
73
71
  const topGeometries: THREE.BufferGeometry[] = []
@@ -78,7 +76,7 @@ export class CraneRail3D extends RealObjectGroup {
78
76
  }
79
77
  const topMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(topGeometries), railMat)
80
78
  topMesh.castShadow = true
81
- this.object3d.add(topMesh)
79
+ inner.add(topMesh)
82
80
 
83
81
  // ── 양 끝단 수직 지지대 (바닥~천장 연결) ──
84
82
  const supportH = d - railH * 2
@@ -96,7 +94,7 @@ export class CraneRail3D extends RealObjectGroup {
96
94
  }
97
95
  const supportMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(supportGeometries), supportMat)
98
96
  supportMesh.castShadow = true
99
- this.object3d.add(supportMesh)
97
+ inner.add(supportMesh)
100
98
  }
101
99
 
102
100
  updateDimension() {}