@fmsim/machine 1.0.88 → 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 (165) hide show
  1. package/dist/agv-line.js.map +1 -1
  2. package/dist/agv.js +6 -2
  3. package/dist/agv.js.map +1 -1
  4. package/dist/buffer.js.map +1 -1
  5. package/dist/carrier-manager.js +183 -0
  6. package/dist/carrier-manager.js.map +1 -0
  7. package/dist/carrier.js +105 -14
  8. package/dist/carrier.js.map +1 -1
  9. package/dist/conveyor-join.js +8 -2
  10. package/dist/conveyor-join.js.map +1 -1
  11. package/dist/conveyor.js +9 -1
  12. package/dist/conveyor.js.map +1 -1
  13. package/dist/crane-rail.js +53 -0
  14. package/dist/crane-rail.js.map +1 -0
  15. package/dist/crane.js +144 -1
  16. package/dist/crane.js.map +1 -1
  17. package/dist/equipment.js.map +1 -1
  18. package/dist/factories/agv-3d.js +147 -0
  19. package/dist/factories/agv-3d.js.map +1 -0
  20. package/dist/factories/conveyor-3d.js +345 -0
  21. package/dist/factories/conveyor-3d.js.map +1 -0
  22. package/dist/factories/crane-3d.js +186 -0
  23. package/dist/factories/crane-3d.js.map +1 -0
  24. package/dist/factories/crane-rail-3d.js +95 -0
  25. package/dist/factories/crane-rail-3d.js.map +1 -0
  26. package/dist/factories/machine-3d.js +973 -0
  27. package/dist/factories/machine-3d.js.map +1 -0
  28. package/dist/factories/oht-3d.js +177 -0
  29. package/dist/factories/oht-3d.js.map +1 -0
  30. package/dist/factories/shuttle-3d.js +159 -0
  31. package/dist/factories/shuttle-3d.js.map +1 -0
  32. package/dist/factories/z-priority-defaults.js +33 -0
  33. package/dist/factories/z-priority-defaults.js.map +1 -0
  34. package/dist/factories/z-priority.js +54 -0
  35. package/dist/factories/z-priority.js.map +1 -0
  36. package/dist/features/conveyor-mixin.js.map +1 -1
  37. package/dist/features/mcs-status-default.js +15 -1
  38. package/dist/features/mcs-status-default.js.map +1 -1
  39. package/dist/features/mcs-status-mixin.js +22 -0
  40. package/dist/features/mcs-status-mixin.js.map +1 -1
  41. package/dist/features/mcs-zone-mixin.js.map +1 -1
  42. package/dist/features/parent-object-mixin.js +15 -7
  43. package/dist/features/parent-object-mixin.js.map +1 -1
  44. package/dist/floating-container.js +5 -0
  45. package/dist/floating-container.js.map +1 -1
  46. package/dist/index.js +58 -2
  47. package/dist/index.js.map +1 -1
  48. package/dist/mcs-carrier-holder.js +56 -17
  49. package/dist/mcs-carrier-holder.js.map +1 -1
  50. package/dist/mcs-machine.js +39 -0
  51. package/dist/mcs-machine.js.map +1 -1
  52. package/dist/mcs-unit.js +20 -0
  53. package/dist/mcs-unit.js.map +1 -1
  54. package/dist/mcs-vehicle.js +10 -4
  55. package/dist/mcs-vehicle.js.map +1 -1
  56. package/dist/node.js +10 -3
  57. package/dist/node.js.map +1 -1
  58. package/dist/oht-line.js.map +1 -1
  59. package/dist/oht.js +4 -0
  60. package/dist/oht.js.map +1 -1
  61. package/dist/port-flow.js +4 -0
  62. package/dist/port-flow.js.map +1 -1
  63. package/dist/port.js +4 -0
  64. package/dist/port.js.map +1 -1
  65. package/dist/scanner.js +30 -0
  66. package/dist/scanner.js.map +1 -0
  67. package/dist/scene/root-container-override.js +160 -96
  68. package/dist/scene/root-container-override.js.map +1 -1
  69. package/dist/shelf.js +4 -0
  70. package/dist/shelf.js.map +1 -1
  71. package/dist/shuttle.js.map +1 -1
  72. package/dist/static-container.js +5 -0
  73. package/dist/static-container.js.map +1 -1
  74. package/dist/stocker-abnormal-bar.js +184 -0
  75. package/dist/stocker-abnormal-bar.js.map +1 -0
  76. package/dist/stocker-capacity-bar.js +253 -6
  77. package/dist/stocker-capacity-bar.js.map +1 -1
  78. package/dist/stocker-carrier-bar.js +198 -0
  79. package/dist/stocker-carrier-bar.js.map +1 -0
  80. package/dist/stocker.js +7 -1
  81. package/dist/stocker.js.map +1 -1
  82. package/dist/templates/backdrop.js +30 -0
  83. package/dist/templates/backdrop.js.map +1 -0
  84. package/dist/templates/decal.js +21 -0
  85. package/dist/templates/decal.js.map +1 -0
  86. package/dist/templates/index.js +7 -1
  87. package/dist/templates/index.js.map +1 -1
  88. package/dist/templates/scanner.js +20 -0
  89. package/dist/templates/scanner.js.map +1 -0
  90. package/dist/tsconfig.tsbuildinfo +1 -1
  91. package/dist/utils/crane-position.js +83 -0
  92. package/dist/utils/crane-position.js.map +1 -0
  93. package/dist/utils/data-value.js +21 -0
  94. package/dist/utils/data-value.js.map +1 -0
  95. package/dist/utils/fit-gauge-scale.js +28 -0
  96. package/dist/utils/fit-gauge-scale.js.map +1 -0
  97. package/dist/utils/get-value-on-ranges.js +2 -4
  98. package/dist/utils/get-value-on-ranges.js.map +1 -1
  99. package/dist/utils/stocker-gauge.js +78 -0
  100. package/dist/utils/stocker-gauge.js.map +1 -0
  101. package/dist/zone-capacity-bar.js +3 -3
  102. package/dist/zone-capacity-bar.js.map +1 -1
  103. package/icons/backdrop.png +0 -0
  104. package/icons/decal.png +0 -0
  105. package/jest.config.js +10 -0
  106. package/package.json +7 -6
  107. package/src/agv-line.ts +1 -1
  108. package/src/agv.ts +10 -5
  109. package/src/buffer.ts +1 -1
  110. package/src/carrier-manager.ts +223 -0
  111. package/src/carrier.ts +111 -18
  112. package/src/conveyor-join.ts +6 -3
  113. package/src/conveyor.ts +10 -2
  114. package/src/crane-rail.ts +58 -0
  115. package/src/crane.ts +182 -2
  116. package/src/equipment.ts +1 -1
  117. package/src/factories/agv-3d.ts +160 -0
  118. package/src/factories/conveyor-3d.ts +398 -0
  119. package/src/factories/crane-3d.ts +217 -0
  120. package/src/factories/crane-rail-3d.ts +115 -0
  121. package/src/factories/machine-3d.ts +1192 -0
  122. package/src/factories/oht-3d.ts +196 -0
  123. package/src/factories/shuttle-3d.ts +179 -0
  124. package/src/factories/z-priority-defaults.ts +38 -0
  125. package/src/factories/z-priority.ts +60 -0
  126. package/src/features/conveyor-mixin.ts +1 -1
  127. package/src/features/mcs-status-default.ts +15 -1
  128. package/src/features/mcs-status-mixin.ts +26 -4
  129. package/src/features/mcs-zone-mixin.ts +1 -1
  130. package/src/features/parent-object-mixin.ts +17 -8
  131. package/src/floating-container.ts +5 -0
  132. package/src/index.ts +66 -2
  133. package/src/mcs-carrier-holder.ts +58 -29
  134. package/src/mcs-machine.ts +43 -1
  135. package/src/mcs-unit.ts +25 -2
  136. package/src/mcs-vehicle.ts +12 -9
  137. package/src/node.ts +9 -3
  138. package/src/oht-line.ts +1 -1
  139. package/src/oht.ts +7 -2
  140. package/src/port-flow.ts +6 -1
  141. package/src/port.ts +6 -1
  142. package/src/scanner.ts +29 -0
  143. package/src/scene/root-container-override.ts +156 -105
  144. package/src/shelf.ts +6 -1
  145. package/src/shuttle.ts +1 -1
  146. package/src/static-container.ts +5 -0
  147. package/src/stocker-abnormal-bar.ts +247 -0
  148. package/src/stocker-capacity-bar.ts +301 -10
  149. package/src/stocker-carrier-bar.ts +262 -0
  150. package/src/stocker.ts +10 -3
  151. package/src/templates/backdrop.ts +30 -0
  152. package/src/templates/decal.ts +21 -0
  153. package/src/templates/index.ts +7 -1
  154. package/src/templates/scanner.ts +20 -0
  155. package/src/utils/crane-position.ts +105 -0
  156. package/src/utils/data-value.ts +19 -0
  157. package/src/utils/fit-gauge-scale.ts +35 -0
  158. package/src/utils/get-value-on-ranges.ts +2 -4
  159. package/src/utils/stocker-gauge.ts +111 -0
  160. package/src/zone-capacity-bar.ts +5 -5
  161. package/translations/en.json +26 -1
  162. package/translations/ja.json +26 -1
  163. package/translations/ko.json +26 -1
  164. package/translations/ms.json +26 -1
  165. package/translations/zh.json +26 -1
@@ -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' {
@@ -22,14 +23,31 @@ declare module '@hatiolab/things-scene' {
22
23
  craneLegendTheme?: any
23
24
  shelfLegendTheme?: any
24
25
  equipmentLegendTheme?: any
25
- style: { [key: string]: any | Legend }
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
 
32
- const ModelLayer = Component.register('model-layer') as any
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
+
50
+ const ModelLayer = (Component as any).register('model-layer') as any
33
51
 
34
52
  Object.defineProperty(ModelLayer.prototype, 'nature', {
35
53
  get: function () {
@@ -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,8 +19,13 @@ 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
- return this.root.shelfLegendTheme || super.getLegendFallback()
28
+ return (this.root as any).shelfLegendTheme || super.getLegendFallback()
24
29
  }
25
30
 
26
31
  render(ctx: CanvasRenderingContext2D) {
package/src/shuttle.ts CHANGED
@@ -20,7 +20,7 @@ export default class Shuttle extends MCSVehicle {
20
20
  }
21
21
 
22
22
  getLegendFallback() {
23
- return this.root.shuttleLegendTheme || super.getLegendFallback()
23
+ return (this.root as any).shuttleLegendTheme || super.getLegendFallback()
24
24
  }
25
25
 
26
26
  render(ctx: CanvasRenderingContext2D) {
@@ -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
  }
@@ -0,0 +1,247 @@
1
+ import {
2
+ Component,
3
+ ComponentNature,
4
+ POSITION,
5
+ RectPath,
6
+ Shape,
7
+ sceneComponent
8
+ } from '@hatiolab/things-scene'
9
+ import { safeRound } from './utils/safe-round'
10
+ import { ParentObjectMixin, ParentObjectMixinProperties } from './features/parent-object-mixin'
11
+
12
+ /**
13
+ * 캐리어 ID 정상/비정상 비율 게이지 (Normal / Abnormal)
14
+ *
15
+ * 스토커에 보관된 캐리어들의 정상/비정상 비율을 세그먼트 게이지로 표시한다.
16
+ * Portrait(세로) 모드에서는 아래→위, Landscape(가로) 모드에서는 왼→오 방향으로 채운다.
17
+ */
18
+
19
+ interface Segment {
20
+ label: string
21
+ count: number
22
+ ratio: number
23
+ color: string
24
+ }
25
+
26
+ const COLOR_NORMAL = '#8BDA5B'
27
+ const COLOR_ABNORMAL = '#F4B4B4'
28
+
29
+ const NATURE: ComponentNature = {
30
+ mutable: false,
31
+ resizable: true,
32
+ rotatable: true,
33
+ properties: [
34
+ ...ParentObjectMixinProperties,
35
+ {
36
+ type: 'number',
37
+ name: 'carrierNormal',
38
+ label: 'carrier-normal'
39
+ },
40
+ {
41
+ type: 'number',
42
+ name: 'carrierAbnormal',
43
+ label: 'carrier-abnormal'
44
+ },
45
+ {
46
+ type: 'number',
47
+ label: 'round',
48
+ name: 'round',
49
+ property: {
50
+ min: 0
51
+ }
52
+ }
53
+ ]
54
+ }
55
+
56
+ var controlHandler = {
57
+ ondragmove: function (point: POSITION, index: number, component: Component) {
58
+ var { left, top, width, height } = component.model
59
+
60
+ var transcoorded = component.transcoordP2S(point.x, point.y)
61
+ var round = ((transcoorded.x - left) / (width / 2)) * 100
62
+
63
+ round = safeRound(round, width, height)
64
+
65
+ component.set({ round })
66
+ }
67
+ }
68
+
69
+ @sceneComponent('StockerAbnormalBar')
70
+ export default class StockerAbnormalBar extends ParentObjectMixin(RectPath(Shape)) {
71
+ static get nature() {
72
+ return NATURE
73
+ }
74
+
75
+ /** StockerAbnormalBar 의 parentObject 는 STOCKER. */
76
+ get parentObjectTypes(): string[] {
77
+ return ['STOCKER']
78
+ }
79
+
80
+ get controls() {
81
+ var { left, top, width, round, height } = this.state
82
+ round = round == undefined ? 0 : safeRound(round, width, height)
83
+
84
+ return [
85
+ {
86
+ x: left + (width / 2) * (round / 100),
87
+ y: top,
88
+ handler: controlHandler
89
+ }
90
+ ]
91
+ }
92
+
93
+ isIdentifiable() {
94
+ return false
95
+ }
96
+
97
+ get segments(): Segment[] {
98
+ const { carrierNormal = 0, carrierAbnormal = 0 } = this.state
99
+ const total = carrierNormal + carrierAbnormal
100
+
101
+ if (total === 0) {
102
+ return [
103
+ { label: 'Normal', count: 0, ratio: 0, color: COLOR_NORMAL },
104
+ { label: 'Abnormal', count: 0, ratio: 0, color: COLOR_ABNORMAL }
105
+ ]
106
+ }
107
+
108
+ return [
109
+ { label: 'Normal', count: carrierNormal, ratio: carrierNormal / total, color: COLOR_NORMAL },
110
+ { label: 'Abnormal', count: carrierAbnormal, ratio: carrierAbnormal / total, color: COLOR_ABNORMAL }
111
+ ]
112
+ }
113
+
114
+ render(context: CanvasRenderingContext2D) {
115
+ const { width, height } = this.bounds
116
+
117
+ if (width >= height) {
118
+ this.renderLandscape(context)
119
+ } else {
120
+ this.renderPortrait(context)
121
+ }
122
+ }
123
+
124
+ renderPortrait(context: CanvasRenderingContext2D) {
125
+ const { left, top, width, height } = this.bounds
126
+ const { lineWidth, strokeStyle, round, fillStyle = 'white' } = this.state
127
+ const segments = this.segments
128
+
129
+ context.save()
130
+ context.translate(left, top)
131
+
132
+ // background
133
+ context.beginPath()
134
+ context.fillStyle = fillStyle
135
+ context.roundRect(0, 0, width, height, round)
136
+ context.fill()
137
+ context.clip()
138
+
139
+ // segments (아래→위: Abnormal이 아래, Normal이 위)
140
+ let y = height
141
+ for (const seg of [...segments].reverse()) {
142
+ const segHeight = height * seg.ratio
143
+ if (segHeight <= 0) continue
144
+
145
+ y -= segHeight
146
+
147
+ context.beginPath()
148
+ context.fillStyle = seg.color
149
+ context.rect(0, y, width, segHeight)
150
+ context.fill()
151
+
152
+ this._drawSegmentLabel(context, seg, 0, y, width, segHeight)
153
+ }
154
+
155
+ // outline
156
+ context.beginPath()
157
+ context.setLineDash([])
158
+ context.strokeStyle = strokeStyle
159
+ context.lineWidth = lineWidth
160
+ context.roundRect(0, 0, width, height, round)
161
+ context.stroke()
162
+
163
+ context.translate(-left, -top)
164
+ context.restore()
165
+ }
166
+
167
+ renderLandscape(context: CanvasRenderingContext2D) {
168
+ const { left, top, width, height } = this.bounds
169
+ const { lineWidth, strokeStyle, round, fillStyle = 'white' } = this.state
170
+ const segments = this.segments
171
+
172
+ context.save()
173
+ context.translate(left, top)
174
+
175
+ // background
176
+ context.beginPath()
177
+ context.fillStyle = fillStyle
178
+ context.roundRect(0, 0, width, height, round)
179
+ context.fill()
180
+ context.clip()
181
+
182
+ // segments (왼→오)
183
+ let x = 0
184
+ for (const seg of segments) {
185
+ const segWidth = width * seg.ratio
186
+ if (segWidth <= 0) continue
187
+
188
+ context.beginPath()
189
+ context.fillStyle = seg.color
190
+ context.rect(x, 0, segWidth, height)
191
+ context.fill()
192
+
193
+ this._drawSegmentLabel(context, seg, x, 0, segWidth, height)
194
+
195
+ x += segWidth
196
+ }
197
+
198
+ // outline
199
+ context.beginPath()
200
+ context.setLineDash([])
201
+ context.strokeStyle = strokeStyle
202
+ context.lineWidth = lineWidth
203
+ context.roundRect(0, 0, width, height, round)
204
+ context.stroke()
205
+
206
+ context.translate(-left, -top)
207
+ context.restore()
208
+ }
209
+
210
+ private _drawSegmentLabel(
211
+ context: CanvasRenderingContext2D,
212
+ seg: Segment,
213
+ x: number,
214
+ y: number,
215
+ w: number,
216
+ h: number
217
+ ) {
218
+ const percent = Math.round(seg.ratio * 100)
219
+ const lines = [seg.label, `${percent}%`]
220
+
221
+ const fontSize = Math.max(9, Math.min(14, Math.min(w, h) * 0.22))
222
+ context.font = `bold ${fontSize}px sans-serif`
223
+ context.fillStyle = '#333'
224
+ context.textAlign = 'center'
225
+ context.textBaseline = 'middle'
226
+
227
+ const lineHeight = fontSize * 1.2
228
+ const totalHeight = lines.length * lineHeight
229
+ const startY = y + (h - totalHeight) / 2 + lineHeight / 2
230
+
231
+ for (let i = 0; i < lines.length; i++) {
232
+ context.fillText(lines[i], x + w / 2, startY + i * lineHeight)
233
+ }
234
+ }
235
+
236
+ onchangeData(after: Record<string, any>, before: Record<string, any>): void {
237
+ const {
238
+ CARRIER_NORMAL = 0,
239
+ CARRIER_ABNORMAL = 0
240
+ } = this.data && typeof this.data == 'object' ? this.data : {}
241
+
242
+ this.setState({
243
+ carrierNormal: Number(CARRIER_NORMAL) || 0,
244
+ carrierAbnormal: Number(CARRIER_ABNORMAL) || 0
245
+ })
246
+ }
247
+ }