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

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 +17 -36
  14. package/dist/factories/agv-3d.js.map +1 -1
  15. package/dist/factories/conveyor-3d.js +179 -100
  16. package/dist/factories/conveyor-3d.js.map +1 -1
  17. package/dist/factories/crane-3d.js +56 -29
  18. package/dist/factories/crane-3d.js.map +1 -1
  19. package/dist/factories/crane-rail-3d.js +14 -18
  20. package/dist/factories/crane-rail-3d.js.map +1 -1
  21. package/dist/factories/machine-3d.js +533 -197
  22. package/dist/factories/machine-3d.js.map +1 -1
  23. package/dist/factories/oht-3d.js +29 -47
  24. package/dist/factories/oht-3d.js.map +1 -1
  25. package/dist/factories/shuttle-3d.js +13 -32
  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 +20 -0
  46. package/dist/mcs-machine.js.map +1 -1
  47. package/dist/mcs-unit.js +17 -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 +19 -36
  107. package/src/factories/conveyor-3d.ts +183 -103
  108. package/src/factories/crane-3d.ts +62 -29
  109. package/src/factories/crane-rail-3d.ts +17 -20
  110. package/src/factories/machine-3d.ts +631 -205
  111. package/src/factories/oht-3d.ts +30 -46
  112. package/src/factories/shuttle-3d.ts +14 -32
  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 +23 -0
  124. package/src/mcs-unit.ts +21 -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,12 +10,12 @@
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'
17
17
  import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js'
18
- import { RealObjectGroup, registerRealObjectFactory, disposeObject3D } from '@hatiolab/things-scene'
18
+ import { RealObjectGroup, registerRealObjectFactory, disposeObject3D, MaterialBank } from '@hatiolab/things-scene'
19
19
  import type { Component } from '@hatiolab/things-scene'
20
20
  import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'
21
21
 
@@ -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 {
@@ -80,11 +76,8 @@ export class AGV3D extends RealObjectGroup {
80
76
  const bumperPad = Math.min(w, h) * 0.03
81
77
  const bumperCornerR = Math.min(w + bumperPad * 2, h + bumperPad * 2, bumperH) * 0.2
82
78
  const bumperGeo = new RoundedBoxGeometry(w + bumperPad * 2, bumperH, h + bumperPad * 2, 4, bumperCornerR)
83
- const bumperMat = new THREE.MeshStandardMaterial({
84
- color: BUMPER_COLOR,
85
- roughness: 0.8,
86
- metalness: 0.1,
87
- })
79
+ // MaterialBank 공유 — dedup + faint 안전(코어 변형 스왑, disposeObject3D 가 __bankOwned 스킵).
80
+ const bumperMat = MaterialBank.material('agv|bumper', { roughness: 0.8, metalness: 0.1 }, BUMPER_COLOR)
88
81
  const bumper = new THREE.Mesh(bumperGeo, bumperMat)
89
82
  bumper.position.y = bumperH / 2
90
83
  bumper.castShadow = true
@@ -93,11 +86,7 @@ export class AGV3D extends RealObjectGroup {
93
86
  // ── 본체 (둥근 모서리, statusColor 적용) ──
94
87
  const bodyColor = this.resolveColor()
95
88
  const bodyGeo = new RoundedBoxGeometry(w, bodyH, h, 4, cornerR)
96
- const bodyMat = new THREE.MeshStandardMaterial({
97
- color: bodyColor,
98
- roughness: 0.3,
99
- metalness: 0.5,
100
- })
89
+ const bodyMat = MaterialBank.material('agv|body', { roughness: 0.3, metalness: 0.5 }, bodyColor)
101
90
  const body = new THREE.Mesh(bodyGeo, bodyMat)
102
91
  body.position.y = bumperH + bodyH / 2
103
92
  body.castShadow = true
@@ -107,18 +96,18 @@ export class AGV3D extends RealObjectGroup {
107
96
  const platInset = Math.min(w, h) * 0.04
108
97
  const platCornerR = Math.min(w - platInset * 2, h - platInset * 2, platH) * 0.15
109
98
  const platGeo = new RoundedBoxGeometry(w - platInset * 2, platH, h - platInset * 2, 4, platCornerR)
110
- const platMat = new THREE.MeshStandardMaterial({
111
- color: PLATFORM_COLOR,
112
- roughness: 0.5,
113
- metalness: 0.15,
114
- })
99
+ const platMat = MaterialBank.material('agv|platform', { roughness: 0.5, metalness: 0.15 }, PLATFORM_COLOR)
115
100
  const plat = new THREE.Mesh(platGeo, platMat)
116
101
  plat.position.y = bumperH + bodyH + gapH + platH / 2
117
102
  plat.castShadow = true
118
103
  plat.receiveShadow = true
119
104
 
120
105
  this._ownMeshes = [bumper, body, plat]
121
- for (const m of this._ownMeshes) this.object3d.add(m)
106
+ // ownGroup이 center-origin ↔ bottom-up 좌표계 변환을 자동 처리
107
+ for (const m of this._ownMeshes) this.components3D.add(m)
108
+
109
+ // mesh 는 super.build() 이후 생성 → 코어 faint 를 명시 적용(편집 흐림이 mesh 에 닿게).
110
+ this.updateAlpha()
122
111
  }
123
112
 
124
113
  updateDimension() {}
@@ -126,7 +115,7 @@ export class AGV3D extends RealObjectGroup {
126
115
  update() {
127
116
  // 자신의 mesh만 제거 (Carrier 등 자식 컴포넌트 3D 보존)
128
117
  for (const m of this._ownMeshes) {
129
- this.object3d.remove(m)
118
+ this.components3D.remove(m)
130
119
  disposeObject3D(m)
131
120
  }
132
121
  this._ownMeshes = []
@@ -137,23 +126,17 @@ export class AGV3D extends RealObjectGroup {
137
126
  this.updateHidden()
138
127
  }
139
128
 
140
- updateAlpha() {
141
- const { alpha = 1 } = this.component.state
142
- for (const m of this._ownMeshes) {
143
- const materials = Array.isArray(m.material) ? m.material : [m.material]
144
- for (const mat of materials) {
145
- ;(mat as THREE.MeshStandardMaterial).opacity = alpha
146
- ;(mat as THREE.MeshStandardMaterial).transparent = alpha < 1
147
- }
148
- }
149
- }
129
+ // updateAlpha override 제거 — 코어 RealObject.updateAlpha(=applyMaterialOpacity)로 수렴.
130
+ // canonical(bank base opacity 1) × state.alpha × faint. view 모드 불변, 편집 모드 faint 추가.
150
131
 
151
132
  onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
152
133
  if ('width' in after || 'height' in after || 'depth' in after ||
153
- 'fillStyle' in after || 'strokeStyle' in after || 'id' in after) {
134
+ 'fillStyle' in after || 'strokeStyle' in after || 'id' in after ||
135
+ 'EMPTYTYPE' in after || 'CARRIERSTATUS' in after) {
154
136
  this.update()
155
137
  return
156
138
  }
139
+
157
140
  super.onchange(after, before)
158
141
  }
159
142
  }
@@ -13,26 +13,23 @@
13
13
 
14
14
  import * as THREE from 'three'
15
15
  import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
16
- import { RealObjectGroup, registerRealObjectFactory } from '@hatiolab/things-scene'
16
+ import { RealObjectGroup, registerRealObjectFactory, MaterialBank } 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,34 +58,56 @@ 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
- const frameMaterial = new THREE.MeshStandardMaterial({
77
- color: FRAME_COLOR,
78
- metalness: 0.85,
79
- roughness: 0.35,
80
- })
88
+ // MaterialBank 공유(dedup). 자체 update() 없이 base 생명주기(guarded disposeObject3D) 상속 → 안전.
89
+ const frameMaterial = MaterialBank.material('conveyor|frame', { metalness: 0.85, roughness: 0.35 }, FRAME_COLOR)
81
90
 
82
91
  // --- Frame: side rails + legs + cross-bracing ---
83
92
  // Y 좌표: y=0(바닥) ~ y=depth(상단). 레일 상단이 depth에 위치.
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,50 +115,70 @@ 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
  }
179
+
180
+ // mesh 는 super.build() 이후 생성 → 코어 faint 를 명시 적용(편집 흐림이 mesh 에 닿게).
181
+ this.updateAlpha()
143
182
  }
144
183
 
145
184
  private buildRollers(
@@ -148,87 +187,111 @@ 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) {
176
232
  const rollerMesh = new THREE.Mesh(
177
233
  BufferGeometryUtils.mergeGeometries(rollerGeometries),
178
- new THREE.MeshStandardMaterial({ color, metalness: 0.9, roughness: 0.2 })
234
+ MaterialBank.material('conveyor|roller', { metalness: 0.9, roughness: 0.2 }, color)
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
 
204
269
  const drumMesh = new THREE.Mesh(
205
270
  BufferGeometryUtils.mergeGeometries(drumGeometries),
206
- new THREE.MeshStandardMaterial({ color: ROLLER_COLOR, metalness: 0.9, roughness: 0.2 })
271
+ MaterialBank.material('conveyor|drum', { metalness: 0.9, roughness: 0.2 }, ROLLER_COLOR)
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
213
- const beltMaterial = new THREE.MeshStandardMaterial({
214
- color: color,
215
- metalness: 0.0,
216
- roughness: 0.9,
217
- })
278
+ const beltMaterial = MaterialBank.material('conveyor|belt', { metalness: 0.0, roughness: 0.9 }, color)
218
279
 
219
280
  // Flat belt top surface spanning between drums
220
- const flatLen = rightDrumX - leftDrumX
221
- const flatGeo = new THREE.BoxGeometry(flatLen, beltThickness, drumLength)
281
+ const flatLen = drum2 - drum1
282
+ const flatGeo = isVertical
283
+ ? new THREE.BoxGeometry(drumLength, beltThickness, flatLen)
284
+ : new THREE.BoxGeometry(flatLen, beltThickness, drumLength)
222
285
  const flatMesh = new THREE.Mesh(flatGeo, beltMaterial)
223
286
  flatMesh.position.set(0, beltY + drumRadius, 0)
224
287
  flatMesh.castShadow = true
225
- this.object3d.add(flatMesh)
288
+ this.components3D.add(flatMesh)
226
289
 
227
290
  // Belt wrap around drums (segmented half-cylinder)
228
291
  const wrapSegments = 12
229
- for (const [dx, angleStart] of [
230
- [leftDrumX, Math.PI / 2],
231
- [rightDrumX, -Math.PI / 2],
292
+ for (const [dp, angleStart] of [
293
+ [drum1, Math.PI / 2],
294
+ [drum2, -Math.PI / 2],
232
295
  ] as [number, number][]) {
233
296
  const wrapGeometries: THREE.BufferGeometry[] = []
234
297
  for (let i = 0; i < wrapSegments; i++) {
@@ -236,65 +299,82 @@ export class Conveyor3D extends RealObjectGroup {
236
299
  const a1 = angleStart + (Math.PI * (i + 1)) / wrapSegments
237
300
  const r = drumRadius + beltThickness / 2
238
301
 
239
- const x0 = dx + Math.cos(a0) * r
302
+ const p0 = dp + Math.cos(a0) * r
240
303
  const y0 = beltY + Math.sin(a0) * r
241
- const x1 = dx + Math.cos(a1) * r
304
+ const p1 = dp + Math.cos(a1) * r
242
305
  const y1 = beltY + Math.sin(a1) * r
243
306
 
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)
307
+ const segLen = Math.sqrt((p1 - p0) ** 2 + (y1 - y0) ** 2) * 1.05
308
+ const segAngle = Math.atan2(y1 - y0, p1 - p0)
309
+
310
+ if (isVertical) {
311
+ const seg = new THREE.BoxGeometry(drumLength, beltThickness, segLen)
312
+ seg.rotateX(-segAngle)
313
+ seg.translate(0, (y0 + y1) / 2, (p0 + p1) / 2)
314
+ wrapGeometries.push(seg)
315
+ } else {
316
+ const seg = new THREE.BoxGeometry(segLen, beltThickness, drumLength)
317
+ seg.rotateZ(segAngle)
318
+ seg.translate((p0 + p1) / 2, (y0 + y1) / 2, 0)
319
+ wrapGeometries.push(seg)
320
+ }
251
321
  }
252
322
  const wrapMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(wrapGeometries), beltMaterial)
253
323
  wrapMesh.castShadow = true
254
- this.object3d.add(wrapMesh)
324
+ this.components3D.add(wrapMesh)
255
325
  }
256
326
 
257
327
  // Flat belt bottom (return path)
258
- const bottomGeo = new THREE.BoxGeometry(flatLen, beltThickness, drumLength * 0.85)
328
+ const bottomGeo = isVertical
329
+ ? new THREE.BoxGeometry(drumLength * 0.85, beltThickness, flatLen)
330
+ : new THREE.BoxGeometry(flatLen, beltThickness, drumLength * 0.85)
259
331
  const bottomMesh = new THREE.Mesh(bottomGeo, beltMaterial)
260
332
  bottomMesh.position.set(0, beltY - drumRadius, 0)
261
- this.object3d.add(bottomMesh)
333
+ this.components3D.add(bottomMesh)
262
334
  }
263
335
 
264
- private buildRails(width: number, height: number, depth: number, frameH: number, railW: number) {
336
+ private buildRails(width: number, height: number, depth: number, frameH: number, railW: number, isVertical: boolean = false) {
265
337
  const color = this.resolveColor()
266
338
  const railHeight = frameH * 0.3
267
339
  const railY = depth - railHeight / 2
268
- const innerWidth = height - railW * 2
340
+ const beltWidth = isVertical ? width : height
341
+ const transportLen = isVertical ? height : width
342
+ const innerWidth = beltWidth - railW * 2
269
343
  // 두 레일 간격: 내부 폭의 60%
270
344
  const railSpacing = innerWidth * 0.6
271
345
 
272
- const railMaterial = new THREE.MeshStandardMaterial({
273
- color,
274
- metalness: 0.7,
275
- roughness: 0.3,
276
- })
346
+ const railMaterial = MaterialBank.material('conveyor|rail', { metalness: 0.7, roughness: 0.3 }, color)
277
347
 
278
348
  const railGeometries: THREE.BufferGeometry[] = []
279
349
 
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)
350
+ // Two parallel rails running along transport direction
351
+ if (isVertical) {
352
+ for (const xSign of [-1, 1]) {
353
+ const rail = new THREE.BoxGeometry(railW * 0.8, railHeight, transportLen - railW * 2)
354
+ rail.translate(xSign * (railSpacing / 2), railY, 0)
355
+ railGeometries.push(rail)
356
+ }
357
+ } else {
358
+ for (const zSign of [-1, 1]) {
359
+ const rail = new THREE.BoxGeometry(transportLen - railW * 2, railHeight, railW * 0.8)
360
+ rail.translate(0, railY, zSign * (railSpacing / 2))
361
+ railGeometries.push(rail)
362
+ }
285
363
  }
286
364
 
287
365
  const railMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(railGeometries), railMaterial)
288
366
  railMesh.castShadow = true
289
- this.object3d.add(railMesh)
367
+ this.components3D.add(railMesh)
290
368
  }
291
369
 
292
370
  updateDimension() {}
293
- updateAlpha() {}
371
+ // updateAlpha override 제거 — 코어 RealObject.updateAlpha(=applyMaterialOpacity)로 수렴(faint).
372
+ // view 모드는 factor=1 → build-시 opacity 그대로(불변), 편집 모드만 faint 추가.
294
373
 
295
374
  onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
296
375
  if (
297
376
  'conveyorType' in after ||
377
+ 'orientation' in after ||
298
378
  'width' in after ||
299
379
  'height' in after ||
300
380
  'depth' in after ||