@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
@@ -1,7 +1,22 @@
1
- import { Model, Properties } from '@hatiolab/things-scene'
2
1
  import { themesColorMap, themesAnimationMap } from '@fmsim/api'
3
2
  import MCSUnit from './mcs-unit.js'
4
- import Carrier from './carrier.js'
3
+ import { parseIfJsonString } from './utils/data-value'
4
+
5
+ /**
6
+ * Holder.data 변경 감지 시 carrierManager 에 전달할 4 키 묶음 + multiCarriers 배열.
7
+ * 비교 결과 단 하나라도 다르면 carrierManager.update 호출.
8
+ * (데이터 키는 모두 대문자 전제 — 케이스 정규화 없이 직접 읽는다)
9
+ */
10
+ const CARRIER_DATA_KEYS = ['CARRIERNAME', 'CARRIERTYPE', 'EMPTYTYPE', 'CARRIERSTATUS'] as const
11
+
12
+ function multiCarriersChanged(a: any, b: any): boolean {
13
+ // MULTICARRIERS 값이 JSON 문자열로 올 수 있어 파싱 후 비교. shallow JSON 비교로 충분 —
14
+ // 각 항목이 4 키 primitive 만 가짐. 빈/누락은 같은 '[]' 직렬화로 동치 취급.
15
+ return (
16
+ JSON.stringify(parseIfJsonString(a?.MULTICARRIERS) ?? []) !==
17
+ JSON.stringify(parseIfJsonString(b?.MULTICARRIERS) ?? [])
18
+ )
19
+ }
5
20
 
6
21
  export default class MCSCarrierHolder extends MCSUnit {
7
22
  static get properties(): any {
@@ -28,55 +43,63 @@ export default class MCSCarrierHolder extends MCSUnit {
28
43
  label: 'show-carrier-text',
29
44
  name: 'showCarrierText'
30
45
  }
46
+ // 적층 두께는 carrier 측 (Carrier.added) 에서 자기 default depth 를 결정.
47
+ // holder 측에는 별도 carrierDepth 속성을 두지 않는다.
31
48
  ]
32
49
  }
33
50
 
34
- carrier?: Carrier | null
35
-
36
- async ready() {
37
- super.ready()
51
+ get showOverflow() {
52
+ return true
53
+ }
38
54
 
39
- for (let component of this.components) {
40
- this.removeComponent(component)
55
+ get hierarchy() {
56
+ return {
57
+ ...super.hierarchy,
58
+ components: []
41
59
  }
42
-
43
- const { width, height } = this.state
44
-
45
- this.carrier = Model.compile(
46
- {
47
- type: 'Carrier',
48
- left: 0,
49
- top: 0,
50
- width,
51
- height,
52
- showText: this.getState('showCarrierText') || false
53
- },
54
- this.root.app
55
- ) as any
56
-
57
- this.addComponent(this.carrier!)
58
-
59
- this.carrier!.data = this.data
60
60
  }
61
61
 
62
62
  dispose(): void {
63
- this.carrier = null
64
-
63
+ ;(this.root as any)?.carrierManager?.update(this, {}, this.data)
65
64
  super.dispose()
66
65
  }
67
66
 
68
- get hierarchy() {
69
- return {
70
- ...super.hierarchy,
71
- components: []
67
+ async ready() {
68
+ super.ready()
69
+
70
+ if (this._hasCarrierData(this.data)) {
71
+ ;(this.root as any).carrierManager.update(this, this.data || {}, undefined)
72
72
  }
73
73
  }
74
74
 
75
75
  onchangeData(after: Record<string, any>, before: Record<string, any>): void {
76
- if (this.carrier) {
77
- this.carrier.data = this.data
76
+ const afterData = after.data || {}
77
+ const beforeData = before.data || {}
78
+
79
+ let changed = false
80
+ for (const k of CARRIER_DATA_KEYS) {
81
+ if (afterData[k] !== beforeData[k]) {
82
+ changed = true
83
+ break
84
+ }
85
+ }
86
+ if (!changed) {
87
+ changed = multiCarriersChanged(afterData, beforeData)
88
+ }
89
+
90
+ if (changed) {
91
+ ;(this.root as any).carrierManager.update(this, afterData, beforeData)
78
92
  }
79
93
 
80
94
  super.onchangeData(after, before)
81
95
  }
96
+
97
+ /** data 안에 carrier 정보 (대표 또는 multiCarriers) 가 하나라도 있는지. */
98
+ private _hasCarrierData(data: any): boolean {
99
+ if (!data) return false
100
+ if (data.CARRIERNAME) return true
101
+ const list = parseIfJsonString(data.MULTICARRIERS)
102
+ if (Array.isArray(list) && list.some((a: any) => a?.CARRIERNAME)) return true
103
+ return false
104
+ }
82
105
  }
@@ -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,16 @@ export default class MCSMachine extends MCSStatusMixin(ContainerAbstract) {
70
83
  return LEGEND_MACHINE
71
84
  }
72
85
 
86
+ /**
87
+ * 2D 에서는 `state.alpha`(3D material 전용)를 무시한다. hidden/모델링 dim 흐림은 **코어(draw)가
88
+ * 일괄 처리**하므로 여기서 손대지 않는다 (component-drawing.ts draw 가 prerender 직후 faint 적용).
89
+ * fade 애니메이션은 보존. 3D 는 factory `updateAlpha()` 가 state.alpha 를 별도로 존중.
90
+ */
91
+ prerender(ctx: any) {
92
+ super.prerender(ctx)
93
+ ctx.globalAlpha = 1 - this._delta.fade
94
+ }
95
+
73
96
  render(ctx) {
74
97
  var { round = 0, lineWidth } = this.state
75
98
  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,14 @@ export default class MCSUnit extends MCSStatusMixin(ParentObjectMixin(ContainerA
49
61
  return LEGEND_UNIT
50
62
  }
51
63
 
64
+ /**
65
+ * 2D 에서는 state.alpha 무시. hidden/모델링 dim 흐림은 코어(draw)가 일괄 처리 (MCSMachine 동일).
66
+ */
67
+ prerender(ctx: any) {
68
+ super.prerender(ctx)
69
+ ctx.globalAlpha = 1 - this._delta.fade
70
+ }
71
+
52
72
  contains(x, y) {
53
73
  var { left, top, width, height, lineWidth = 0 } = this.state
54
74
  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
  }