@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
@@ -26,6 +26,19 @@ export default class MCSMachine extends MCSStatusMixin(ContainerAbstract) {
26
26
  return false
27
27
  }
28
28
 
29
+ /**
30
+ * id 후보를 조회할 때 백엔드에 보낼 타입명. 값이 있으면 검색(lookup) 대상.
31
+ * 기본은 자기 타입. 자기 타입과 다른 타입으로 조회해야 하는 특수 케이스는 override.
32
+ */
33
+ get idLookupType(): string | undefined {
34
+ return this.state.type
35
+ }
36
+
37
+ /** MCS 머신 — parentObject(부모 머신) 후보 수집 시 머신으로 식별. */
38
+ get isMcsMachine(): boolean {
39
+ return true
40
+ }
41
+
29
42
  containable(component: Component) {
30
43
  return ['rect', 'ellipse'].includes(component.state.type)
31
44
  }
@@ -70,6 +83,35 @@ export default class MCSMachine extends MCSStatusMixin(ContainerAbstract) {
70
83
  return LEGEND_MACHINE
71
84
  }
72
85
 
86
+ /**
87
+ * In 2D, ignore `state.alpha`; preserve everything else.
88
+ *
89
+ * things-scene's base prerender (component-drawing.ts:31, 44) does:
90
+ * const fade = (alpha || 1) - this._delta.fade
91
+ * if (fade) context.globalAlpha = fade
92
+ *
93
+ * It *overwrites* `globalAlpha` (not multiplies), so the inherited
94
+ * parent alpha is also lost — that's a base-layer quirk we match the
95
+ * recovery for. Then at line 48 it applies editMode+hidden dimming
96
+ * with a *= 0.5.
97
+ *
98
+ * Our recovery:
99
+ * 1. Capture the inherited `globalAlpha` *before* super runs.
100
+ * 2. Let super do its work (transforms / shadow / dim flags).
101
+ * 3. Restore globalAlpha to inherited × editMode-hidden dim factor —
102
+ * i.e., what super would have produced if our state.alpha were 1.
103
+ *
104
+ * 3D path is unaffected: 3D factories read `state.alpha` directly via
105
+ * their `updateAlpha()` implementations and continue to honor it (with
106
+ * per-config opacity fallback).
107
+ */
108
+ prerender(ctx: any) {
109
+ const inherited = ctx.globalAlpha
110
+ super.prerender(ctx)
111
+ const dim = this.app.isEditMode && this.hidden ? 0.5 : 1
112
+ ctx.globalAlpha = inherited * dim
113
+ }
114
+
73
115
  render(ctx) {
74
116
  var { round = 0, lineWidth } = this.state
75
117
  var { left, top, width, height } = this.bounds
package/src/mcs-unit.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Component, ContainerAbstract } from '@hatiolab/things-scene'
1
+ import { ContainerAbstract } from '@hatiolab/things-scene'
2
2
  import { getPopupData } from '@fmsim/api'
3
3
 
4
4
  import { MCSStatusMixin, MCSStatusMixinProperties } from './features/mcs-status-mixin'
@@ -13,6 +13,18 @@ export default class MCSUnit extends MCSStatusMixin(ParentObjectMixin(ContainerA
13
13
  return [...MCSStatusMixinProperties, ...ParentObjectMixinProperties]
14
14
  }
15
15
 
16
+ get showMoveHandle() {
17
+ return false
18
+ }
19
+
20
+ /**
21
+ * id 후보를 조회할 때 백엔드에 보낼 타입명. 값이 있으면 검색(lookup) 대상.
22
+ * 기본은 자기 타입. 특수 케이스는 override.
23
+ */
24
+ get idLookupType(): string | undefined {
25
+ return this.state.type
26
+ }
27
+
16
28
  get hasTextProperty() {
17
29
  return true
18
30
  }
@@ -49,6 +61,17 @@ export default class MCSUnit extends MCSStatusMixin(ParentObjectMixin(ContainerA
49
61
  return LEGEND_UNIT
50
62
  }
51
63
 
64
+ /**
65
+ * In 2D, ignore `state.alpha`; preserve inherited alpha + editMode-hidden dim.
66
+ * See MCSMachine.prerender for the full rationale.
67
+ */
68
+ prerender(ctx: any) {
69
+ const inherited = ctx.globalAlpha
70
+ super.prerender(ctx)
71
+ const dim = this.app.isEditMode && this.hidden ? 0.5 : 1
72
+ ctx.globalAlpha = inherited * dim
73
+ }
74
+
52
75
  contains(x, y) {
53
76
  var { left, top, width, height, lineWidth = 0 } = this.state
54
77
  var extend = lineWidth / 2
@@ -1,4 +1,3 @@
1
- import { Properties } from '@hatiolab/things-scene'
2
1
  import { LEGEND_VEHICLE, Legend } from './features/mcs-status-default'
3
2
  import MCSCarrierHolder from './mcs-carrier-holder'
4
3
  import Node from './node'
@@ -124,22 +123,26 @@ export default class MCSVehicle extends MCSCarrierHolder {
124
123
 
125
124
  if ((this.root as any).vehicleAnimation) {
126
125
  this.trigger('waypoint', {
127
- from: beforeNode,
126
+ from: beforeNode || afterNode,
128
127
  to: afterNode,
129
128
  duration: 5000
130
129
  })
131
- } else {
132
- const fromCenter = beforeNode!.center
130
+ } else if (beforeNode) {
131
+ const fromCenter = beforeNode.center
133
132
  const toCenter = afterNode.center
134
133
 
135
- const { x: startX, y: startY } = beforeNode!.transcoordS2T(fromCenter.x, fromCenter.y)
134
+ const { x: startX, y: startY } = beforeNode.transcoordS2T(fromCenter.x, fromCenter.y)
136
135
  const { x: endX, y: endY } = afterNode.transcoordS2T(toCenter.x, toCenter.y)
137
136
 
138
137
  const theta = Math.atan2(endY - startY, endX - startX) + Math.PI / 2
139
138
 
140
- // 위치 설정
141
139
  this.location = { x: endX, y: endY }
142
140
  this.setState('rotation', theta)
141
+ } else {
142
+ // beforeNode 없음: afterNode 위치로 즉시 이동
143
+ const toCenter = afterNode.center
144
+ const { x, y } = afterNode.transcoordS2T(toCenter.x, toCenter.y)
145
+ this.location = { x, y }
143
146
  }
144
147
  }
145
148
  }
package/src/node.ts CHANGED
@@ -28,11 +28,17 @@ export default class Node extends Shape {
28
28
  static RADIUS = 8
29
29
 
30
30
  is3dish() {
31
- return false
31
+ return true
32
32
  }
33
33
 
34
- isDescendible() {
35
- return false
34
+ /**
35
+ * Node 는 기본적으로 컨테이너 드롭 입양 불가 — 라인 머신(AGVLine/OHTLine/CraneRail)만
36
+ * 자기 containable 화이트리스트로 Node 를 받는다. 단 FloorPlate 에는 들어갈 수 있게 허용.
37
+ * FloorPlate 는 things-scene 코어 컨테이너라 거기에 'Node' 를 명시할 수 없으므로
38
+ * Node 측에서 해결한다 (FloorPlate 의 기본 containable = component.isDescendible(this)).
39
+ */
40
+ isDescendible(container?: any) {
41
+ return container?.state?.type === 'floor-plate'
36
42
  }
37
43
 
38
44
  render(context) {
package/src/oht.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Component, ComponentNature, Properties, sceneComponent } from '@hatiolab/things-scene'
1
+ import { Component, ComponentNature, sceneComponent } from '@hatiolab/things-scene'
2
2
  import MCSVehicle from './mcs-vehicle'
3
3
 
4
4
  const NATURE: ComponentNature = {
@@ -20,6 +20,11 @@ export default class OHT extends MCSVehicle {
20
20
  return 'black'
21
21
  }
22
22
 
23
+ /** OHT 의 parentObject 는 OHTLine. */
24
+ get parentObjectTypes(): string[] {
25
+ return ['OHTLine']
26
+ }
27
+
23
28
  getLegendFallback() {
24
29
  return (this.root as any).ohtLegendTheme || super.getLegendFallback()
25
30
  }
package/src/port-flow.ts CHANGED
@@ -45,6 +45,11 @@ export default class PortFlow extends ParentObjectMixin(RectPath(Shape)) {
45
45
  return NATURE
46
46
  }
47
47
 
48
+ /** PortFlow 의 parentObject 는 Port (유닛). */
49
+ get parentObjectTypes(): string[] {
50
+ return ['Port']
51
+ }
52
+
48
53
  get hasTextProperty() {
49
54
  return this.getState('showText') || false
50
55
  }
package/src/port.ts CHANGED
@@ -23,6 +23,11 @@ export default class Port extends MCSZoneMixin(MCSCarrierHolder) {
23
23
  return (this.root as any).portLegendTheme || super.getLegendFallback()
24
24
  }
25
25
 
26
+ /** Port 는 후보가 매우 많아 부모 머신 없이는 id 리스트를 조회하지 않는다 (machine 필수). */
27
+ get idLookupRequiresMachine() {
28
+ return true
29
+ }
30
+
26
31
  render(ctx: CanvasRenderingContext2D) {
27
32
  var { left, top, width, height } = this.bounds
28
33
 
package/src/scanner.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { Component, ComponentNature, Triangle, sceneComponent } from '@hatiolab/things-scene'
2
+ import { MCSStatusMixin } from './features/mcs-status-mixin'
3
+
4
+ const NATURE: ComponentNature = {
5
+ mutable: true,
6
+ resizable: false,
7
+ rotatable: true,
8
+ properties: [
9
+ {
10
+ type: 'number',
11
+ label: 'round',
12
+ name: 'round',
13
+ property: { min: 0, max: 100, step: 1 }
14
+ }
15
+ ]
16
+ }
17
+
18
+ @sceneComponent('Scanner')
19
+ export default class Scanner extends MCSStatusMixin(Triangle) {
20
+ static get nature() {
21
+ return NATURE
22
+ }
23
+
24
+ is3dish() {
25
+ return true
26
+ }
27
+ }
28
+
29
+ Component.memoize(Scanner.prototype, 'path', false)
@@ -1,6 +1,7 @@
1
1
  import { Component, RootContainer } from '@hatiolab/things-scene'
2
2
  import { themesColorMap, themesColorRange, themesAnimationMap } from '@fmsim/api'
3
3
  import { Legend } from '../features/mcs-status-default'
4
+ import CarrierManager from '../carrier-manager'
4
5
 
5
6
  // RootContainer 클래스 타입 확장
6
7
  declare module '@hatiolab/things-scene' {
@@ -24,11 +25,28 @@ declare module '@hatiolab/things-scene' {
24
25
  equipmentLegendTheme?: any
25
26
  style: any
26
27
  vehicleAnimation?: boolean
28
+ // 스토커 게이지 표시 모드 — 런타임 전용 transient (모델 비저장, 항상 'capacity' 시작)
29
+ stockerGaugeMode: string
30
+ _stockerGaugeMode?: string
31
+ _refreshStockerGauges(): void
27
32
  // 캐시 관련 속성 추가
28
33
  _animationConfigCache?: Map<string, any>
34
+ _carrierManager?: CarrierManager
35
+ carrierManager: CarrierManager
29
36
  }
30
37
  }
31
38
 
39
+ /**
40
+ * 스토커 적재율 게이지 표시 모드 — 런타임 선택지.
41
+ * value 는 엔진 canonical(2D StockerCapacityBar / 3D buildGauge 공통), label 은 표시명.
42
+ * 런타임 UI(뷰어 컨트롤)가 이 목록으로 selector 를 구성한다.
43
+ */
44
+ export const STOCKER_GAUGE_MODES: { value: string; label: string }[] = [
45
+ { value: 'capacity', label: 'Capacity' },
46
+ { value: 'carrier', label: 'Carrier Status' },
47
+ { value: 'abnormal', label: 'Abnormal Carrier' }
48
+ ]
49
+
32
50
  const ModelLayer = (Component as any).register('model-layer') as any
33
51
 
34
52
  Object.defineProperty(ModelLayer.prototype, 'nature', {
@@ -194,6 +212,22 @@ Object.defineProperty(ModelLayer.prototype, 'nature', {
194
212
  property: {
195
213
  options: themesColorRange
196
214
  }
215
+ },
216
+ {
217
+ type: 'legend',
218
+ label: '',
219
+ name: 'MOOD',
220
+ property: {
221
+ label: 'MOOD'
222
+ }
223
+ },
224
+ {
225
+ type: 'select',
226
+ label: 'light-preset',
227
+ name: 'lightPreset',
228
+ property: {
229
+ options: ['dark', 'medium', 'bright']
230
+ }
197
231
  }
198
232
  ],
199
233
  'value-property': 'text'
@@ -201,128 +235,145 @@ Object.defineProperty(ModelLayer.prototype, 'nature', {
201
235
  }
202
236
  })
203
237
 
204
- Object.defineProperty(RootContainer.prototype, 'equipmentLegendTheme', {
205
- get: function () {
206
- const { equipmentLegendName } = this.rootModel.state
207
- return equipmentLegendName && this.state.style[equipmentLegendName]
208
- }
209
- })
210
-
211
- Object.defineProperty(RootContainer.prototype, 'bufferLegendTheme', {
212
- get: function () {
213
- const { bufferLegendName } = this.rootModel.state
214
- return bufferLegendName && this.state.style[bufferLegendName]
215
- }
216
- })
217
-
218
- Object.defineProperty(RootContainer.prototype, 'agvlineLegendTheme', {
219
- get: function () {
220
- const { agvlineLegendName } = this.rootModel.state
221
- return agvlineLegendName && this.state.style[agvlineLegendName]
222
- }
223
- })
224
-
225
- Object.defineProperty(RootContainer.prototype, 'ohtlineLegendTheme', {
226
- get: function () {
227
- const { ohtlineLegendName } = this.rootModel.state
228
- return ohtlineLegendName && this.state.style[ohtlineLegendName]
229
- }
230
- })
231
-
232
- Object.defineProperty(RootContainer.prototype, 'conveyorLegendTheme', {
233
- get: function () {
234
- const { conveyorLegendName } = this.rootModel.state
235
- return conveyorLegendName && this.state.style[conveyorLegendName]
236
- }
237
- })
238
-
239
- Object.defineProperty(RootContainer.prototype, 'stokerLegendTheme', {
240
- get: function () {
241
- const { stokerLegendName } = this.rootModel.state
242
- return stokerLegendName && this.state.style[stokerLegendName]
243
- }
244
- })
245
-
246
- Object.defineProperty(RootContainer.prototype, 'agvLegendTheme', {
247
- get: function () {
248
- const { agvLegendName } = this.rootModel.state
249
- return agvLegendName && this.state.style[agvLegendName]
250
- }
251
- })
252
-
253
- Object.defineProperty(RootContainer.prototype, 'ohtLegendTheme', {
254
- get: function () {
255
- const { ohtLegendName } = this.rootModel.state
256
- return ohtLegendName && this.state.style[ohtLegendName]
257
- }
258
- })
259
-
260
- Object.defineProperty(RootContainer.prototype, 'shuttleLegendTheme', {
261
- get: function () {
262
- const { shuttleLegendName } = this.rootModel.state
263
- return shuttleLegendName && this.state.style[shuttleLegendName]
264
- }
265
- })
238
+ /**
239
+ * 조명 프리셋 — 3 단계 밝기 토글.
240
+ *
241
+ * fmsim 은 디테일 조명 옵션 UI 를 노출하지 않으므로 단일 select 로 단순화.
242
+ * medium = things-scene 의 현재 default 값과 동일 (외관 변화 없음).
243
+ * 사용자가 select 변경 시 onchange 가 exposure + hemiIntensity + dirLightIntensity
244
+ * 를 일괄 set → three-capability 의 변경 감지 핸들러가 즉시 갱신.
245
+ */
246
+ const LIGHT_PRESETS: Record<string, { exposure: number; hemiIntensity: number; dirLightIntensity: number }> = {
247
+ dark: { exposure: 1.2, hemiIntensity: 4.0, dirLightIntensity: 0.6 },
248
+ medium: { exposure: 1.2, hemiIntensity: 4.0, dirLightIntensity: 2.0 },
249
+ bright: { exposure: 1.2, hemiIntensity: 4.0, dirLightIntensity: 5.0 }
250
+ }
266
251
 
267
- Object.defineProperty(RootContainer.prototype, 'craneLegendTheme', {
268
- get: function () {
269
- const { craneLegendName } = this.rootModel.state
270
- return craneLegendName && this.state.style[craneLegendName]
252
+ const _origModelLayerOnchange = ModelLayer.prototype.onchange
253
+ ModelLayer.prototype.onchange = function (after: any) {
254
+ if (after && Object.prototype.hasOwnProperty.call(after, 'lightPreset')) {
255
+ const preset = LIGHT_PRESETS[after.lightPreset as string]
256
+ if (preset) {
257
+ this.set({
258
+ exposure: preset.exposure,
259
+ hemiIntensity: preset.hemiIntensity,
260
+ dirLightIntensity: preset.dirLightIntensity
261
+ })
262
+ }
271
263
  }
272
- })
264
+ return _origModelLayerOnchange.call(this, after)
265
+ }
273
266
 
274
- Object.defineProperty(RootContainer.prototype, 'shelfLegendTheme', {
275
- get: function () {
276
- const { shelfLegendName } = this.rootModel.state
277
- return shelfLegendName && this.state.style[shelfLegendName]
278
- }
279
- })
267
+ // === LegendTheme / AnimationTheme value resolve ===
268
+ //
269
+ // rootModel.state.style[name] 에 저장된 legend/animation 값은 보드 직렬화
270
+ // 형태에 따라 (a) 정상 object 또는 (b) JSON 문자열 둘 다로 나타난다 (2026-06-09
271
+ // 진단 — consumer 인 carrier.ts:animationPreset 이 같은 이유로 typeof string
272
+ // 분기 + JSON5.parse 처리하는 선례 존재). LegendTheme getter 들이 이 분기를
273
+ // 두지 않아 string 통과 → getValueOnRanges 매칭 실패 → 3D gauge 의 색상이
274
+ // fallback 으로만 결정되던 증상.
275
+ //
276
+ // fix 는 getter 한 곳에서 lazy parse. WeakMap<state, Map<rawString, parsed>>
277
+ // 으로 *raw string 자체를 key* — 보드 편집으로 raw 가 바뀌면 자연 무효화,
278
+ // 같은 raw 의 반복 lookup 은 O(1). root 가 GC 되면 cache 도 같이 GC.
279
+ const _legendParseCache = new WeakMap<object, Map<string, any>>()
280
280
 
281
- Object.defineProperty(RootContainer.prototype, 'portLegendTheme', {
282
- get: function () {
283
- const { portLegendName } = this.rootModel.state
284
- return portLegendName && this.state.style[portLegendName]
281
+ function parseLegendIfString(state: any, raw: any): any {
282
+ if (typeof raw !== 'string') return raw
283
+ let cache = _legendParseCache.get(state)
284
+ if (!cache) {
285
+ cache = new Map()
286
+ _legendParseCache.set(state, cache)
285
287
  }
286
- })
288
+ if (cache.has(raw)) return cache.get(raw)
289
+ let parsed: any
290
+ try { parsed = JSON.parse(raw) } catch { parsed = raw } // parse 실패 시 raw 보존
291
+ cache.set(raw, parsed)
292
+ return parsed
293
+ }
287
294
 
288
- Object.defineProperty(RootContainer.prototype, 'carrierLegendTheme', {
289
- get: function () {
290
- const { carrierLegendName } = this.rootModel.state
291
- return carrierLegendName && this.state.style[carrierLegendName]
295
+ function legendThemeGetter(stateKey: string) {
296
+ return function (this: any) {
297
+ const name = this.rootModel.state[stateKey]
298
+ const raw = name && this.state.style[name]
299
+ return parseLegendIfString(this.state, raw)
292
300
  }
293
- })
301
+ }
294
302
 
295
- Object.defineProperty(RootContainer.prototype, 'carrierAnimationTheme', {
296
- get: function () {
297
- const { carrierAnimationName } = this.rootModel.state
298
- return carrierAnimationName && this.state.style[carrierAnimationName]
299
- }
300
- })
303
+ Object.defineProperty(RootContainer.prototype, 'equipmentLegendTheme', { get: legendThemeGetter('equipmentLegendName') })
304
+ Object.defineProperty(RootContainer.prototype, 'bufferLegendTheme', { get: legendThemeGetter('bufferLegendName') })
305
+ Object.defineProperty(RootContainer.prototype, 'agvlineLegendTheme', { get: legendThemeGetter('agvlineLegendName') })
306
+ Object.defineProperty(RootContainer.prototype, 'ohtlineLegendTheme', { get: legendThemeGetter('ohtlineLegendName') })
307
+ Object.defineProperty(RootContainer.prototype, 'conveyorLegendTheme', { get: legendThemeGetter('conveyorLegendName') })
308
+ Object.defineProperty(RootContainer.prototype, 'stokerLegendTheme', { get: legendThemeGetter('stokerLegendName') })
309
+ Object.defineProperty(RootContainer.prototype, 'agvLegendTheme', { get: legendThemeGetter('agvLegendName') })
310
+ Object.defineProperty(RootContainer.prototype, 'ohtLegendTheme', { get: legendThemeGetter('ohtLegendName') })
311
+ Object.defineProperty(RootContainer.prototype, 'shuttleLegendTheme', { get: legendThemeGetter('shuttleLegendName') })
312
+ Object.defineProperty(RootContainer.prototype, 'craneLegendTheme', { get: legendThemeGetter('craneLegendName') })
313
+ Object.defineProperty(RootContainer.prototype, 'shelfLegendTheme', { get: legendThemeGetter('shelfLegendName') })
314
+ Object.defineProperty(RootContainer.prototype, 'portLegendTheme', { get: legendThemeGetter('portLegendName') })
315
+ Object.defineProperty(RootContainer.prototype, 'carrierLegendTheme', { get: legendThemeGetter('carrierLegendName') })
316
+ Object.defineProperty(RootContainer.prototype, 'carrierAnimationTheme', { get: legendThemeGetter('carrierAnimationName') })
317
+ Object.defineProperty(RootContainer.prototype, 'stockerCapacityLegendTheme', { get: legendThemeGetter('stockerCapacityLegendName') })
318
+ Object.defineProperty(RootContainer.prototype, 'zoneCapacityLegendTheme', { get: legendThemeGetter('zoneCapacityLegendName') })
319
+ Object.defineProperty(RootContainer.prototype, 'batteryRateLegendTheme', { get: legendThemeGetter('batteryRateLegendName') })
301
320
 
302
- Object.defineProperty(RootContainer.prototype, 'stockerCapacityLegendTheme', {
321
+ Object.defineProperty(RootContainer.prototype, 'vehicleAnimation', {
303
322
  get: function () {
304
- const { stockerCapacityLegendName } = this.rootModel.state
305
- return stockerCapacityLegendName && this.state.style[stockerCapacityLegendName]
323
+ const { vehicleAnimation } = this.rootModel.state
324
+ return !!vehicleAnimation
306
325
  }
307
326
  })
308
327
 
309
- Object.defineProperty(RootContainer.prototype, 'zoneCapacityLegendTheme', {
328
+ Object.defineProperty(RootContainer.prototype, 'carrierManager', {
310
329
  get: function () {
311
- const { zoneCapacityLegendName } = this.rootModel.state
312
- return zoneCapacityLegendName && this.state.style[zoneCapacityLegendName]
330
+ if (!this._carrierManager) {
331
+ this._carrierManager = new CarrierManager(this)
332
+ }
333
+ return this._carrierManager
313
334
  }
314
335
  })
315
336
 
316
- Object.defineProperty(RootContainer.prototype, 'batteryRateLegendTheme', {
337
+ // === 스토커 게이지 표시 모드 (런타임 전용 transient) ===
338
+ //
339
+ // 모델(state)에 저장하지 않는다 — nature 속성이 아니므로 보드 직렬화에 포함되지 않고,
340
+ // 보드를 다시 열면 항상 기본값 'capacity' 로 시작한다. 실행 중 뷰어 컨트롤 등이
341
+ // `scene.rootContainer.stockerGaugeMode = 'carrier'` 로 동적 변경한다.
342
+ // (주의: scene.root 은 model_layer 라 override 가 없다. 반드시 rootContainer 에 설정.
343
+ // 컴포넌트의 this.root 는 RootContainer(isRoot)로 해석되어 이 값을 읽는다.)
344
+ //
345
+ // 2D(StockerCapacityBar.gaugeMode) 와 3D(machine-3d buildGauge) 가 모두 이 값을 읽어
346
+ // 게이지 표현이 항상 일치한다. 값이 바뀌면 setter 가 2D 리드로우 + 3D 게이지 재빌드를
347
+ // 즉시 트리거한다.
348
+ Object.defineProperty(RootContainer.prototype, 'stockerGaugeMode', {
317
349
  get: function () {
318
- const { batteryRateLegendName } = this.rootModel.state
319
- return batteryRateLegendName && this.state.style[batteryRateLegendName]
350
+ return this._stockerGaugeMode || 'capacity'
351
+ },
352
+ set: function (mode: string) {
353
+ const next = mode || 'capacity'
354
+ if (this._stockerGaugeMode === next) return
355
+ this._stockerGaugeMode = next
356
+ this._refreshStockerGauges()
320
357
  }
321
358
  })
322
359
 
323
- Object.defineProperty(RootContainer.prototype, 'vehicleAnimation', {
324
- get: function () {
325
- const { vehicleAnimation } = this.rootModel.state
326
- return !!vehicleAnimation
360
+ /** scene 하위 STOCKER(3D 게이지) + StockerCapacityBar(2D 게이지) 를 새 모드로 갱신. */
361
+ RootContainer.prototype._refreshStockerGauges = function () {
362
+ const walk = (container: any) => {
363
+ const kids = container?.components
364
+ if (!Array.isArray(kids)) return
365
+ for (const c of kids) {
366
+ const type = c?.state?.type
367
+ if (type === 'STOCKER') {
368
+ // 3D 게이지는 스토커 자신의 real-object 가 그린다 → 재빌드 (2D 모드면 _realObject 없음)
369
+ c._realObject?.update?.()
370
+ } else if (type === 'StockerCapacityBar' || type === 'MCSGaugeCapacityBar') {
371
+ // 2D 게이지는 자식 컴포넌트가 그린다 → 리드로우
372
+ c.invalidate?.()
373
+ }
374
+ walk(c)
375
+ }
327
376
  }
328
- })
377
+ walk(this)
378
+ this.invalidate?.()
379
+ }
package/src/shelf.ts CHANGED
@@ -19,6 +19,11 @@ export default class Shelf extends MCSZoneMixin(MCSCarrierHolder) {
19
19
  return 'black'
20
20
  }
21
21
 
22
+ /** Shelf 의 parentObject 는 STOCKER. */
23
+ get parentObjectTypes(): string[] {
24
+ return ['STOCKER']
25
+ }
26
+
22
27
  getLegendFallback() {
23
28
  return (this.root as any).shelfLegendTheme || super.getLegendFallback()
24
29
  }
@@ -13,6 +13,11 @@ const NATURE = {
13
13
 
14
14
  @sceneComponent('container')
15
15
  export default class StaticContainer extends Container {
16
+ // things-scene 의 `declare const Container_base` 패턴이 base 인스턴스 타입의 accessor
17
+ // 정보를 object-type property 로 평탄화하여 TS2611 발생. operato-scene 은 position
18
+ // override 케이스가 없어 미발현, fmsim 만 해당. things-scene 측 mixin 타입 패턴이
19
+ // 개선되면 제거.
20
+ // @ts-expect-error TS2611
16
21
  get position() {
17
22
  return 'static'
18
23
  }
@@ -72,6 +72,11 @@ export default class StockerAbnormalBar extends ParentObjectMixin(RectPath(Shape
72
72
  return NATURE
73
73
  }
74
74
 
75
+ /** StockerAbnormalBar 의 parentObject 는 STOCKER. */
76
+ get parentObjectTypes(): string[] {
77
+ return ['STOCKER']
78
+ }
79
+
75
80
  get controls() {
76
81
  var { left, top, width, round, height } = this.state
77
82
  round = round == undefined ? 0 : safeRound(round, width, height)
@@ -232,7 +237,7 @@ export default class StockerAbnormalBar extends ParentObjectMixin(RectPath(Shape
232
237
  const {
233
238
  CARRIER_NORMAL = 0,
234
239
  CARRIER_ABNORMAL = 0
235
- } = typeof this.data == 'object' ? this.data : {}
240
+ } = this.data && typeof this.data == 'object' ? this.data : {}
236
241
 
237
242
  this.setState({
238
243
  carrierNormal: Number(CARRIER_NORMAL) || 0,