@fmsim/machine 3.0.0-beta.4 → 3.0.0-beta.6

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 (94) hide show
  1. package/dist/agv-line.js +2 -2
  2. package/dist/agv-line.js.map +1 -1
  3. package/dist/agv.js +6 -2
  4. package/dist/agv.js.map +1 -1
  5. package/dist/buffer.js +2 -2
  6. package/dist/buffer.js.map +1 -1
  7. package/dist/conveyor-join.js +2 -2
  8. package/dist/conveyor-join.js.map +1 -1
  9. package/dist/conveyor.js +2 -2
  10. package/dist/conveyor.js.map +1 -1
  11. package/dist/crane.js +2 -2
  12. package/dist/crane.js.map +1 -1
  13. package/dist/equipment.js +2 -2
  14. package/dist/equipment.js.map +1 -1
  15. package/dist/factories/conveyor-3d.js +2 -2
  16. package/dist/factories/conveyor-3d.js.map +1 -1
  17. package/dist/factories/machine-3d.js +12 -12
  18. package/dist/factories/machine-3d.js.map +1 -1
  19. package/dist/factories/oht-3d.js +3 -4
  20. package/dist/factories/oht-3d.js.map +1 -1
  21. package/dist/features/legend-panel.js +29 -0
  22. package/dist/features/legend-panel.js.map +1 -0
  23. package/dist/features/legend-rows.js +88 -0
  24. package/dist/features/legend-rows.js.map +1 -0
  25. package/dist/features/lv-figures.js +143 -0
  26. package/dist/features/lv-figures.js.map +1 -0
  27. package/dist/features/lv-heights.js +20 -0
  28. package/dist/features/lv-heights.js.map +1 -0
  29. package/dist/features/mcs-status-default.js +3 -49
  30. package/dist/features/mcs-status-default.js.map +1 -1
  31. package/dist/features/oht-rail.js +32 -0
  32. package/dist/features/oht-rail.js.map +1 -0
  33. package/dist/features/vehicle-travel.js +18 -1
  34. package/dist/features/vehicle-travel.js.map +1 -1
  35. package/dist/index.js +4 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/mcs-carrier-holder.js +82 -33
  38. package/dist/mcs-carrier-holder.js.map +1 -1
  39. package/dist/mcs-machine.js +10 -1
  40. package/dist/mcs-machine.js.map +1 -1
  41. package/dist/mcs-unit.js +10 -1
  42. package/dist/mcs-unit.js.map +1 -1
  43. package/dist/mcs-vehicle.js +0 -4
  44. package/dist/mcs-vehicle.js.map +1 -1
  45. package/dist/oht-line.js +7 -3
  46. package/dist/oht-line.js.map +1 -1
  47. package/dist/oht.js +2 -2
  48. package/dist/oht.js.map +1 -1
  49. package/dist/port.js +2 -2
  50. package/dist/port.js.map +1 -1
  51. package/dist/scene/root-container-override.js +15 -210
  52. package/dist/scene/root-container-override.js.map +1 -1
  53. package/dist/shelf.js +2 -2
  54. package/dist/shelf.js.map +1 -1
  55. package/dist/shuttle.js +2 -2
  56. package/dist/shuttle.js.map +1 -1
  57. package/dist/stocker.js +2 -2
  58. package/dist/stocker.js.map +1 -1
  59. package/dist/tsconfig.tsbuildinfo +1 -1
  60. package/package.json +5 -4
  61. package/src/agv-line.ts +2 -2
  62. package/src/agv.ts +7 -2
  63. package/src/buffer.ts +2 -2
  64. package/src/conveyor-join.ts +2 -2
  65. package/src/conveyor.ts +2 -2
  66. package/src/crane.ts +2 -2
  67. package/src/equipment.ts +2 -2
  68. package/src/factories/conveyor-3d.ts +2 -2
  69. package/src/factories/machine-3d.ts +11 -12
  70. package/src/factories/oht-3d.ts +3 -5
  71. package/src/features/legend-panel.ts +31 -0
  72. package/src/features/legend-rows.ts +86 -0
  73. package/src/features/lv-figures.ts +151 -0
  74. package/src/features/lv-heights.ts +22 -0
  75. package/src/features/mcs-status-default.ts +3 -53
  76. package/src/features/oht-rail.ts +31 -0
  77. package/src/features/vehicle-travel.ts +16 -1
  78. package/src/index.ts +4 -0
  79. package/src/mcs-carrier-holder.ts +79 -36
  80. package/src/mcs-machine.ts +12 -1
  81. package/src/mcs-unit.ts +12 -1
  82. package/src/mcs-vehicle.ts +0 -4
  83. package/src/oht-line.ts +7 -3
  84. package/src/oht.ts +2 -2
  85. package/src/port.ts +2 -2
  86. package/src/scene/root-container-override.ts +15 -207
  87. package/src/shelf.ts +2 -2
  88. package/src/shuttle.ts +2 -2
  89. package/src/stocker.ts +2 -2
  90. package/translations/en.json +7 -1
  91. package/translations/ja.json +7 -1
  92. package/translations/ko.json +7 -1
  93. package/translations/ms.json +7 -1
  94. package/translations/zh.json +7 -1
@@ -1,4 +1,6 @@
1
+ import { ceilingZPos } from '@hatiolab/things-scene'
1
2
  import type Node from '../node'
3
+ import { ohtRailTop } from './oht-rail'
2
4
 
3
5
  /** The node a vehicle's data names: a `Node` of the board with that id and node machine. */
4
6
  function findNode(vehicle: any, nodeId: string, nodeMachine: string): Node | null {
@@ -21,6 +23,18 @@ export function VehicleTravel<T extends new (...args: any[]) => any>(Base: T) {
21
23
  return class extends Base {
22
24
  _lastWaypoint: { NodeId: string; NodeMachine: string } | null = null
23
25
 
26
+ /**
27
+ * Where a figure hung from the ceiling (an OHT) stands: its top at the rail of the line it runs on, as fmsim's own
28
+ * OHT -- not the ceiling. Things-scene takes it before the ceiling (a written zPos still wins); others say nothing.
29
+ */
30
+ get effectiveZPos(): number | undefined {
31
+ const own = super.effectiveZPos
32
+ if ((this as any).constructor?.placement !== 'ceiling') return own
33
+ const ro = (this as any)._realObject
34
+ if (!ro || !Number.isFinite(ro.effectiveDepth)) return own
35
+ return ceilingZPos(ro.placementMode, ohtRailTop(this), ro.effectiveDepth)
36
+ }
37
+
24
38
  /** The angle, in radians, the vehicle's front makes with the way it travels. */
25
39
  get travelHeading(): number {
26
40
  return ((Number((this as any).state?.vehicleHeading) || 0) * Math.PI) / 180
@@ -58,7 +72,8 @@ export function VehicleTravel<T extends new (...args: any[]) => any>(Base: T) {
58
72
  ;(this as any).trigger('waypoint', {
59
73
  from: beforeNode || afterNode,
60
74
  to: afterNode,
61
- duration: 5000
75
+ duration: 5000,
76
+ yawOffset: Math.PI / 2 + this.travelHeading
62
77
  })
63
78
  } else if (beforeNode) {
64
79
  const fromCenter = beforeNode.center
package/src/index.ts CHANGED
@@ -93,6 +93,10 @@ export { default as Crane } from './crane'
93
93
  export { default as Shelf } from './shelf'
94
94
  export { default as Carrier } from './carrier'
95
95
  export { CARRIER_HOLDER_PROPERTIES, CarrierHolder, carrierHolderLegend } from './mcs-carrier-holder'
96
+ export { LVMachine, LVUnit, LVVehicle, StatusFigure, statusAsCode, statusLegend } from './features/lv-figures'
97
+ export { MACHINE_HEIGHTS, TRANSPORT_SURFACE_HEIGHT } from './features/lv-heights'
98
+ export { boardLegendOf, boardLegendOfRow, legendRowsOnBoard } from './features/legend-rows'
99
+ export { legendPanel } from './features/legend-panel'
96
100
 
97
101
  // ── topology ──
98
102
  export { default as Node } from './node'
@@ -6,17 +6,18 @@ import {
6
6
  ItemsDataBinding,
7
7
  Model,
8
8
  TRANSFER_SLOT_KEY,
9
+ drawingScaleFor,
9
10
  figureLegendSourcesChanged,
10
11
  itemTypeEntryOf,
11
12
  itemTypeTableOf,
12
- registerFigureCapability,
13
13
  registerFigureLegendResolver,
14
14
  type ItemType
15
15
  } from '@hatiolab/things-scene'
16
+ import { colourAndEffect } from './features/legend-rows'
16
17
  import { HOLDER_ANCHOR, type ItemDims, type ResolvedSlot } from '@hatiolab/figure-model'
17
- import { CARRIER_HOLDER, carrierHolderProperties } from '@fmsim/figure-capabilities'
18
+ import { carrierHolderProperties } from '@fmsim/figure-capabilities'
18
19
  import MCSUnit from './mcs-unit.js'
19
- import { incomingFromV2 } from './utils/load-address'
20
+ import { incomingFromV2, resolveOccupancy, type IncomingCarrier, type LoadLayout, type OccupancyResult } from './utils/load-address'
20
21
 
21
22
  /**
22
23
  * A holder's slots outside its layout (common item contract 3.4): every carrier of a holder without a layout stands in
@@ -56,6 +57,11 @@ export function CarrierHolder<T extends new (...args: any[]) => any>(Base: T) {
56
57
  return true
57
58
  }
58
59
 
60
+ /** The board legend rows its carriers follow, with whatever else it follows (ADR-0167 decision 20) */
61
+ get legendRowsUsed(): string[] {
62
+ return [...((super.legendRowsUsed as string[] | undefined) ?? []), 'carrier', 'carrier-animation']
63
+ }
64
+
59
65
  /** The records the reconcile took last, as JSON: the same again is not reconciled */
60
66
  _carrierRecordsTaken?: string
61
67
 
@@ -94,15 +100,67 @@ export function CarrierHolder<T extends new (...args: any[]) => any>(Base: T) {
94
100
  }
95
101
 
96
102
  /**
97
- * One record per carrier the data names, bottom first: MULTICARRIERS in order, then the representative. Without a
98
- * layout they all stand in the pile. A carrier is told its own keys and whether to show its text.
103
+ * Whether carriers stand in the holder's own places (ADR-0167 decision 23): a figure holder's places -- a gripper, a
104
+ * lift plate, the tiers on a fork -- carry what stands on them as the figure moves. fmsim's own holders keep theirs: a
105
+ * pile, or the layout their data addresses (LoadLayoutHolder).
106
+ */
107
+ get fillsPlaces(): boolean {
108
+ return false
109
+ }
110
+
111
+ /** The holder's places as the last records took them: an addressed carrier keeps its slot while it is still there */
112
+ _placeOccupancy?: OccupancyResult
113
+
114
+ /**
115
+ * One record per carrier the data names, bottom first: MULTICARRIERS in order, then the representative. A carrier is
116
+ * told its own keys and whether to show its text. They stand in the pile, unless the holder fills its places.
99
117
  */
100
118
  carrierRecords(data: Record<string, any> | undefined): CarrierRecord[] {
101
- return incomingFromV2(data).map(c => ({
102
- slotId: PILE_SLOT,
119
+ const incoming = incomingFromV2(data)
120
+ const record = (c: IncomingCarrier, slotId: string): CarrierRecord => ({
121
+ slotId,
103
122
  itemId: c.name,
104
123
  itemState: this.carrierStateOf(c.record, c.record === data)
105
- }))
124
+ })
125
+ const places = this.fillsPlaces ? (((this as any).slots ?? []) as Array<{ id: string; maxCount?: number }>) : []
126
+ if (!places.length) return incoming.map(c => record(c, PILE_SLOT))
127
+
128
+ /* A carrier its data addresses (a layout's TIER/SLOT) stands at that address, as on a crane; a wrong address beside the holder. */
129
+ const layout = this._placeLayout
130
+ const occupancy = layout ? resolveOccupancy(layout, incoming, this._placeOccupancy) : undefined
131
+ this._placeOccupancy = occupancy
132
+ const slotOf = new Map<string, string>()
133
+ for (const [key, list] of occupancy?.slots ?? []) {
134
+ const [tier, slot] = key.split(':').map(Number)
135
+ for (const c of list) slotOf.set(c.name, `t${tier}_s${slot}`)
136
+ }
137
+ for (const c of occupancy?.addressError ?? []) slotOf.set(c.name, EXCEPTION_SLOT)
138
+
139
+ /* The others fill the places in their order, each up to what it takes; one still held keeps its place. What is left over stands beside the holder. */
140
+ const room = new Map(places.map(p => [p.id, p.maxCount ?? 1]))
141
+ for (const slotId of slotOf.values()) if (room.has(slotId)) room.set(slotId, room.get(slotId)! - 1)
142
+ const unplaced = incoming.filter(c => !slotOf.has(c.name))
143
+ const heldIn = new Map(((this as any)._items as any[]).map(item => [item.state?.id, item[TRANSFER_SLOT_KEY]] as const))
144
+ for (const c of unplaced) {
145
+ const kept = heldIn.get(c.name)
146
+ if (kept && (room.get(kept) ?? 0) > 0) {
147
+ slotOf.set(c.name, kept)
148
+ room.set(kept, room.get(kept)! - 1)
149
+ }
150
+ }
151
+ for (const c of unplaced) {
152
+ if (slotOf.has(c.name)) continue
153
+ const free = places.find(p => (room.get(p.id) ?? 0) > 0)
154
+ slotOf.set(c.name, free ? free.id : EXCEPTION_SLOT)
155
+ if (free) room.set(free.id, room.get(free.id)! - 1)
156
+ }
157
+ return incoming.map(c => record(c, slotOf.get(c.name)!))
158
+ }
159
+
160
+ /** The layout addresses are read by: the holder's, when it holds and the drawing's scale is known */
161
+ get _placeLayout(): LoadLayout | undefined {
162
+ const sel = (this as any).effectiveLayout
163
+ return sel?.status === 'valid' && drawingScaleFor(this as any).known ? (sel.layout as LoadLayout) : undefined
106
164
  }
107
165
 
108
166
  /** What a carrier is told: its own keys (the representative's are picked out of the holder data), and its text */
@@ -196,7 +254,9 @@ export function CarrierHolder<T extends new (...args: any[]) => any>(Base: T) {
196
254
  */
197
255
  itemShownDimsOf(_carrier: any, slot: ResolvedSlot, slotId: string): ItemDims | null {
198
256
  /* Elsewhere outside the layout, a holder that keeps carriers there says (the exception area: LoadLayoutHolder). */
199
- if (slotId === PILE_SLOT) {
257
+ /* A place that says no size (a gripper): the carrier is shown at its own (ADR-0167 decision 23). */
258
+ if (slotId !== PILE_SLOT && slotId !== EXCEPTION_SLOT && !slot?.allowed) return null
259
+ if (slotId === PILE_SLOT || slotId === EXCEPTION_SLOT) {
200
260
  const k = (this as any).mmPerSceneUnit
201
261
  const { width = 0, height = 0 } = (this as any).state
202
262
  const w = Math.abs(width) * PILE_PLAN_RATIO
@@ -225,12 +285,13 @@ export function CarrierHolder<T extends new (...args: any[]) => any>(Base: T) {
225
285
  // ── where carriers stand ──────────────────────────────────────────────────
226
286
 
227
287
  /**
228
- * Where a holder's carriers rest, in coordinates above the holder's bottom, and whether they stand on it or hang from
229
- * it: on the transport surface by default (fmsim v2). A Port, Shelf or Shuttle holds them at its bottom; an OHT hangs
230
- * them below. A figure holder says it by its `carrierMount` (ADR-0167 decision 18).
288
+ * Where a figure holder's carriers rest, in coordinates above the holder's bottom, and whether they stand on it or
289
+ * hang from it, as its `carrierMount` says (ADR-0167 decision 18): on top of its body, or below it. fmsim's own
290
+ * holders say their own (MCSCarrierHolder and those over it).
231
291
  */
232
292
  get carrierRest(): { height: number; mount: 'ON' | 'HANG' } {
233
- return (this as any).state?.carrierMount === 'HANG' ? { height: 0, mount: 'HANG' } : { height: 30, mount: 'ON' }
293
+ if ((this as any).state?.carrierMount === 'HANG') return { height: 0, mount: 'HANG' }
294
+ return { height: Number((this as any)._realObject?.effectiveDepth) || 0, mount: 'ON' }
234
295
  }
235
296
 
236
297
  /** The pile, and the exception area beside the holder's +X edge. Frames in mm on the holder's own anchor (its bottom) */
@@ -290,6 +351,11 @@ class MCSCarrierHolder extends CarrierHolder(ItemsDataBinding(Holdable(Capacity(
290
351
  static get properties(): any {
291
352
  return [...MCSUnit.properties, ...CARRIER_HOLDER_PROPERTIES]
292
353
  }
354
+
355
+ /** On the transport surface by default (fmsim v2). A Port, Shelf or Shuttle holds them at its bottom; an OHT hangs them below. */
356
+ get carrierRest(): { height: number; mount: 'ON' | 'HANG' } {
357
+ return { height: 30, mount: 'ON' }
358
+ }
293
359
  }
294
360
 
295
361
  export default MCSCarrierHolder
@@ -323,27 +389,4 @@ export function carrierHolderLegend({ component, role }: { component: any; role:
323
389
  return legend && typeof legend === 'object' ? colourAndEffect(legend) : undefined
324
390
  }
325
391
 
326
- /** A legend as a figure takes it from fmsim: each entry's colour and effect; a colour alone stays the colour; an entry's JSON5 text read. */
327
- function colourAndEffect(legend: Record<string, unknown>): Record<string, unknown> {
328
- return Object.fromEntries(
329
- Object.entries(legend).map(([code, raw]) => {
330
- let entry = raw
331
- if (typeof entry === 'string' && entry.trim().startsWith('{')) {
332
- try {
333
- entry = JSON5.parse(entry)
334
- } catch {
335
- return [code, raw]
336
- }
337
- }
338
- if (!entry || typeof entry !== 'object') return [code, entry]
339
- const { color, effect } = entry as { color?: unknown; effect?: unknown }
340
- return [code, { ...(color !== undefined ? { color } : {}), ...(effect !== undefined ? { effect } : {}) }]
341
- })
342
- )
343
- }
344
-
345
392
  registerFigureLegendResolver(carrierHolderLegend)
346
- registerFigureCapability(
347
- { ...CARRIER_HOLDER, properties: carrierHolderProperties({ legends: themesColorMap, animations: themesAnimationMap, mount: true }) as never },
348
- base => CarrierHolder(base) as never
349
- )
@@ -4,6 +4,7 @@ import { getPopupData } from '@fmsim/api'
4
4
  import { MCSStatusMixin, MCSStatusMixinProperties } from './features/mcs-status-mixin'
5
5
 
6
6
  import { LEGEND_MACHINE, Legend } from './features/mcs-status-default'
7
+ import { boardLegendOfRow } from './features/legend-rows'
7
8
 
8
9
  /**
9
10
  * MCS Machine의 공통 속성을 정의한 최상위 오브젝트
@@ -68,7 +69,17 @@ export default class MCSMachine extends MCSStatusMixin(ContainerAbstract) {
68
69
  }
69
70
 
70
71
  get legend(): Legend {
71
- return this.getTheme() || this.getLegendFallback()
72
+ return this.getTheme() || boardLegendOfRow(this, this.statusLegendRow) || this.getLegendFallback()
73
+ }
74
+
75
+ /** The board legend row its STATUS colour follows when it chose no legend of its own (ADR-0167 decision 20); none here */
76
+ get statusLegendRow(): string | undefined {
77
+ return undefined
78
+ }
79
+
80
+ /** The board legend rows it follows: what the board's LEGENDS panel shows first */
81
+ get legendRowsUsed(): string[] {
82
+ return this.statusLegendRow ? [this.statusLegendRow] : []
72
83
  }
73
84
 
74
85
  get auxLegend() {
package/src/mcs-unit.ts CHANGED
@@ -3,6 +3,7 @@ import { getPopupData } from '@fmsim/api'
3
3
 
4
4
  import { MCSStatusMixin, MCSStatusMixinProperties } from './features/mcs-status-mixin'
5
5
  import { LEGEND_UNIT, Legend } from './features/mcs-status-default'
6
+ import { boardLegendOfRow } from './features/legend-rows'
6
7
  import { ParentObjectMixin, ParentObjectMixinProperties } from './features/parent-object-mixin'
7
8
 
8
9
  /**
@@ -50,7 +51,17 @@ export default class MCSUnit extends MCSStatusMixin(ParentObjectMixin(ContainerA
50
51
  }
51
52
 
52
53
  get legend(): Legend {
53
- return this.getTheme() || this.getLegendFallback()
54
+ return this.getTheme() || boardLegendOfRow(this, this.statusLegendRow) || this.getLegendFallback()
55
+ }
56
+
57
+ /** The board legend row its STATUS colour follows when it chose no legend of its own (ADR-0167 decision 20); none here */
58
+ get statusLegendRow(): string | undefined {
59
+ return undefined
60
+ }
61
+
62
+ /** The board legend rows it follows: what the board's LEGENDS panel shows first */
63
+ get legendRowsUsed(): string[] {
64
+ return this.statusLegendRow ? [this.statusLegendRow] : []
54
65
  }
55
66
 
56
67
  get auxLegend() {
@@ -1,5 +1,3 @@
1
- import { registerFigureCapability } from '@hatiolab/things-scene'
2
- import { VEHICLE } from '@fmsim/figure-capabilities'
3
1
  import { LEGEND_VEHICLE, Legend } from './features/mcs-status-default'
4
2
  import { VehicleTravel } from './features/vehicle-travel'
5
3
  import MCSCarrierHolder from './mcs-carrier-holder'
@@ -82,5 +80,3 @@ export default class MCSVehicle extends VehicleTravel(MCSCarrierHolder) {
82
80
  }
83
81
  }
84
82
 
85
- /* fmsim.Vehicle (ADR-0167 18): a figure travels between nodes by the same mixin as fmsim's own AGV and OHT */
86
- registerFigureCapability(VEHICLE, base => VehicleTravel(base) as never)
package/src/oht-line.ts CHANGED
@@ -5,7 +5,11 @@ const NATURE: ComponentNature = {
5
5
  mutable: false,
6
6
  resizable: true,
7
7
  rotatable: true,
8
- properties: [...MCSTransport.properties]
8
+ properties: [
9
+ ...MCSTransport.properties,
10
+ /* 레일 윗면이 작업환경 천장에서 내려온 길이(mm). 레일과 그 위를 다니는 OHT 가 천장을 따라 움직인다 */
11
+ { type: 'number', label: 'rail-hang', name: 'railHang', property: { min: 0, step: 100 } }
12
+ ]
9
13
  }
10
14
 
11
15
  @sceneComponent('OHTLine')
@@ -18,8 +22,8 @@ export default class OHTLine extends MCSTransport {
18
22
  return '#333'
19
23
  }
20
24
 
21
- getLegendFallback() {
22
- return (this.root as any).ohtlineLegendTheme || super.getLegendFallback()
25
+ get statusLegendRow(): string {
26
+ return 'ohtline'
23
27
  }
24
28
 
25
29
  containable(component: Component) {
package/src/oht.ts CHANGED
@@ -30,8 +30,8 @@ export default class OHT extends MCSVehicle {
30
30
  return ['OHTLine']
31
31
  }
32
32
 
33
- getLegendFallback() {
34
- return (this.root as any).ohtLegendTheme || super.getLegendFallback()
33
+ get statusLegendRow(): string {
34
+ return 'oht'
35
35
  }
36
36
 
37
37
  render(ctx: CanvasRenderingContext2D) {
package/src/port.ts CHANGED
@@ -25,8 +25,8 @@ export default class Port extends LoadLayoutHolder(MCSZoneMixin(MCSCarrierHolder
25
25
  return 'black'
26
26
  }
27
27
 
28
- getLegendFallback() {
29
- return (this.root as any).portLegendTheme || super.getLegendFallback()
28
+ get statusLegendRow(): string {
29
+ return 'port'
30
30
  }
31
31
 
32
32
  /** Port 는 후보가 매우 많아 부모 머신 없이는 id 리스트를 조회하지 않는다 (machine 필수). */
@@ -1,6 +1,7 @@
1
1
  import { Component, RootContainer, figureLegendSourcesChanged } from '@hatiolab/things-scene'
2
- import { themesColorMap, themesColorRange, themesAnimationMap } from '@fmsim/api'
3
- import { Legend } from '../features/mcs-status-default'
2
+ import { FMSIM_LEGEND_ROWS, legendRows } from '@fmsim/figure-capabilities'
3
+ import { boardLegendOf } from '../features/legend-rows'
4
+ import { legendPanel } from '../features/legend-panel'
4
5
 
5
6
  // RootContainer 클래스 타입 확장
6
7
  declare module '@hatiolab/things-scene' {
@@ -58,158 +59,7 @@ Object.defineProperty(ModelLayer.prototype, 'nature', {
58
59
  label: 'vehicle-animation',
59
60
  name: 'vehicleAnimation'
60
61
  },
61
- {
62
- type: 'legend',
63
- label: '',
64
- name: 'LEGENDS',
65
- property: {
66
- label: 'LEGENDS'
67
- }
68
- },
69
- {
70
- type: 'select',
71
- label: 'equipment-legend-name',
72
- name: 'equipmentLegendName',
73
- property: {
74
- options: themesColorMap
75
- }
76
- },
77
- {
78
- type: 'select',
79
- label: 'buffer-legend-name',
80
- name: 'bufferLegendName',
81
- property: {
82
- options: themesColorMap
83
- }
84
- },
85
- {
86
- type: 'select',
87
- label: 'agvline-legend-name',
88
- name: 'agvlineLegendName',
89
- property: {
90
- options: themesColorMap
91
- }
92
- },
93
- {
94
- type: 'select',
95
- label: 'ohtline-legend-name',
96
- name: 'ohtlineLegendName',
97
- property: {
98
- options: themesColorMap
99
- }
100
- },
101
- {
102
- type: 'select',
103
- label: 'conveyor-legend-name',
104
- name: 'conveyorLegendName',
105
- property: {
106
- options: themesColorMap
107
- }
108
- },
109
- {
110
- type: 'select',
111
- label: 'stoker-legend-name',
112
- name: 'stokerLegendName',
113
- property: {
114
- options: themesColorMap
115
- }
116
- },
117
- {
118
- type: 'select',
119
- label: 'port-legend-name',
120
- name: 'portLegendName',
121
- property: {
122
- options: themesColorMap
123
- }
124
- },
125
- {
126
- type: 'select',
127
- label: 'agv-legend-name',
128
- name: 'agvLegendName',
129
- property: {
130
- options: themesColorMap
131
- }
132
- },
133
- {
134
- type: 'select',
135
- label: 'oht-legend-name',
136
- name: 'ohtLegendName',
137
- property: {
138
- options: themesColorMap
139
- }
140
- },
141
- {
142
- type: 'select',
143
- label: 'shuttle-legend-name',
144
- name: 'shuttleLegendName',
145
- property: {
146
- options: themesColorMap
147
- }
148
- },
149
- {
150
- type: 'select',
151
- label: 'crane-legend-name',
152
- name: 'craneLegendName',
153
- property: {
154
- options: themesColorMap
155
- }
156
- },
157
- {
158
- type: 'select',
159
- label: 'port-legend-name',
160
- name: 'portLegendName',
161
- property: {
162
- options: themesColorMap
163
- }
164
- },
165
- {
166
- type: 'select',
167
- label: 'shelf-legend-name',
168
- name: 'shelfLegendName',
169
- property: {
170
- options: themesColorMap
171
- }
172
- },
173
- {
174
- type: 'select',
175
- label: 'carrier-legend-name',
176
- name: 'carrierLegendName',
177
- property: {
178
- options: themesColorMap
179
- }
180
- },
181
- {
182
- type: 'select',
183
- label: 'carrier-animation-name',
184
- name: 'carrierAnimationName',
185
- property: {
186
- options: themesAnimationMap
187
- }
188
- },
189
- {
190
- type: 'select',
191
- label: 'stocker-capacity-legend-name',
192
- name: 'stockerCapacityLegendName',
193
- property: {
194
- options: themesColorRange
195
- }
196
- },
197
- {
198
- type: 'select',
199
- label: 'zone-capacity-legend-name',
200
- name: 'zoneCapacityLegendName',
201
- property: {
202
- options: themesColorRange
203
- }
204
- },
205
- {
206
- type: 'select',
207
- label: 'battery-rate-legend-name',
208
- name: 'batteryRateLegendName',
209
- property: {
210
- options: themesColorRange
211
- }
212
- },
62
+ ...legendPanel(this),
213
63
  {
214
64
  type: 'legend',
215
65
  label: '',
@@ -260,65 +110,23 @@ ModelLayer.prototype.onchange = function (after: any) {
260
110
  })
261
111
  }
262
112
  }
263
- /* 보드의 Carrier 범례가 바뀌었다: Carrier 피규어들이 범례를 다시 고른다 (ADR-0166 결정 12) */
264
- if (after && ['carrierLegendName', 'carrierAnimationName'].some(k => Object.prototype.hasOwnProperty.call(after, k))) figureLegendSourcesChanged()
113
+ /* 보드의 범례 줄이 바뀌었다: 피규어들이 범례를 다시 고른다 (ADR-0166 결정 12 · ADR-0167 결정 20) */
114
+ if (after && legendRows().some(row => Object.prototype.hasOwnProperty.call(after, row.property))) figureLegendSourcesChanged()
265
115
  return _origModelLayerOnchange.call(this, after)
266
116
  }
267
117
 
268
- // === LegendTheme / AnimationTheme value resolve ===
269
- //
270
- // rootModel.state.style[name] 에 저장된 legend/animation 값은 보드 직렬화
271
- // 형태에 따라 (a) 정상 object 또는 (b) JSON 문자열 둘 다로 나타난다 (2026-06-09
272
- // 진단 — consumer 인 carrier.ts:animationPreset 이 같은 이유로 typeof string
273
- // 분기 + JSON5.parse 처리하는 선례 존재). LegendTheme getter 들이 이 분기를
274
- // 두지 않아 string 통과 → 구간 조회 매칭 실패 → 3D gauge 의 색상이
275
- // fallback 으로만 결정되던 증상.
118
+ // === 범례 줄의 보드 속성 → 범례 (ADR-0167 결정 20) ===
276
119
  //
277
- // fix 는 getter 한 곳에서 lazy parse. WeakMap<state, Map<rawString, parsed>>
278
- // 으로 *raw string 자체를 key* — 보드 편집으로 raw 가 바뀌면 자연 무효화,
279
- // 같은 raw 의 반복 lookup 은 O(1). root 가 GC 되면 cache 도 같이 GC.
280
- const _legendParseCache = new WeakMap<object, Map<string, any>>()
281
-
282
- function parseLegendIfString(state: any, raw: any): any {
283
- if (typeof raw !== 'string') return raw
284
- let cache = _legendParseCache.get(state)
285
- if (!cache) {
286
- cache = new Map()
287
- _legendParseCache.set(state, cache)
288
- }
289
- if (cache.has(raw)) return cache.get(raw)
290
- let parsed: any
291
- try { parsed = JSON.parse(raw) } catch { parsed = raw } // parse 실패 시 raw 보존
292
- cache.set(raw, parsed)
293
- return parsed
294
- }
295
-
296
- function legendThemeGetter(stateKey: string) {
297
- return function (this: any) {
298
- const name = this.rootModel.state[stateKey]
299
- const raw = name && this.state.style[name]
300
- return parseLegendIfString(this.state, raw)
301
- }
120
+ // 줄마다 손으로 적던 getter 대신 등록된 줄에서 만든다: 보드 속성 `<x>Name` 의 범례를 `<x>Theme` 로 읽는다
121
+ // (equipmentLegendName → equipmentLegendTheme, carrierAnimationName → carrierAnimationTheme). 문자열 범례는 legend-rows 가 한 번 읽는다.
122
+ for (const row of FMSIM_LEGEND_ROWS) {
123
+ Object.defineProperty(RootContainer.prototype, row.property.replace(/Name$/, 'Theme'), {
124
+ get: function (this: any) {
125
+ return boardLegendOf(this, row.property)
126
+ }
127
+ })
302
128
  }
303
129
 
304
- Object.defineProperty(RootContainer.prototype, 'equipmentLegendTheme', { get: legendThemeGetter('equipmentLegendName') })
305
- Object.defineProperty(RootContainer.prototype, 'bufferLegendTheme', { get: legendThemeGetter('bufferLegendName') })
306
- Object.defineProperty(RootContainer.prototype, 'agvlineLegendTheme', { get: legendThemeGetter('agvlineLegendName') })
307
- Object.defineProperty(RootContainer.prototype, 'ohtlineLegendTheme', { get: legendThemeGetter('ohtlineLegendName') })
308
- Object.defineProperty(RootContainer.prototype, 'conveyorLegendTheme', { get: legendThemeGetter('conveyorLegendName') })
309
- Object.defineProperty(RootContainer.prototype, 'stokerLegendTheme', { get: legendThemeGetter('stokerLegendName') })
310
- Object.defineProperty(RootContainer.prototype, 'agvLegendTheme', { get: legendThemeGetter('agvLegendName') })
311
- Object.defineProperty(RootContainer.prototype, 'ohtLegendTheme', { get: legendThemeGetter('ohtLegendName') })
312
- Object.defineProperty(RootContainer.prototype, 'shuttleLegendTheme', { get: legendThemeGetter('shuttleLegendName') })
313
- Object.defineProperty(RootContainer.prototype, 'craneLegendTheme', { get: legendThemeGetter('craneLegendName') })
314
- Object.defineProperty(RootContainer.prototype, 'shelfLegendTheme', { get: legendThemeGetter('shelfLegendName') })
315
- Object.defineProperty(RootContainer.prototype, 'portLegendTheme', { get: legendThemeGetter('portLegendName') })
316
- Object.defineProperty(RootContainer.prototype, 'carrierLegendTheme', { get: legendThemeGetter('carrierLegendName') })
317
- Object.defineProperty(RootContainer.prototype, 'carrierAnimationTheme', { get: legendThemeGetter('carrierAnimationName') })
318
- Object.defineProperty(RootContainer.prototype, 'stockerCapacityLegendTheme', { get: legendThemeGetter('stockerCapacityLegendName') })
319
- Object.defineProperty(RootContainer.prototype, 'zoneCapacityLegendTheme', { get: legendThemeGetter('zoneCapacityLegendName') })
320
- Object.defineProperty(RootContainer.prototype, 'batteryRateLegendTheme', { get: legendThemeGetter('batteryRateLegendName') })
321
-
322
130
  Object.defineProperty(RootContainer.prototype, 'vehicleAnimation', {
323
131
  get: function () {
324
132
  const { vehicleAnimation } = this.rootModel.state
package/src/shelf.ts CHANGED
@@ -29,8 +29,8 @@ export default class Shelf extends MCSZoneMixin(MCSCarrierHolder) {
29
29
  return ['STOCKER']
30
30
  }
31
31
 
32
- getLegendFallback() {
33
- return (this.root as any).shelfLegendTheme || super.getLegendFallback()
32
+ get statusLegendRow(): string {
33
+ return 'shelf'
34
34
  }
35
35
 
36
36
  render(ctx: CanvasRenderingContext2D) {
package/src/shuttle.ts CHANGED
@@ -24,8 +24,8 @@ export default class Shuttle extends MCSVehicle {
24
24
  return 'black'
25
25
  }
26
26
 
27
- getLegendFallback() {
28
- return (this.root as any).shuttleLegendTheme || super.getLegendFallback()
27
+ get statusLegendRow(): string {
28
+ return 'shuttle'
29
29
  }
30
30
 
31
31
  render(ctx: CanvasRenderingContext2D) {
package/src/stocker.ts CHANGED
@@ -61,8 +61,8 @@ export default class Stocker extends MCSTransport {
61
61
  return NATURE
62
62
  }
63
63
 
64
- getLegendFallback() {
65
- return (this.root as any).stokerLegendTheme || super.getLegendFallback()
64
+ get statusLegendRow(): string {
65
+ return 'stocker'
66
66
  }
67
67
 
68
68
  containable(component: Component) {
@@ -115,5 +115,11 @@
115
115
  "label.load-layout.tier-field-empty": "Empty = tier 1",
116
116
  "label.load-layout.title": "Carrier load slots",
117
117
  "label.load-layout.unit-missing": "Drawing unit is not set (mmPerUnit). Physical placement and fit checks are unavailable.",
118
- "label.load-layout.unit-ok": "Drawing unit 1 = {mm} mm"
118
+ "label.load-layout.unit-ok": "Drawing unit 1 = {mm} mm",
119
+ "label.vehicle-heading": "vehicle heading",
120
+ "label.carrier-mount": "carrier mount",
121
+ "label.rail-hang": "rail hang (mm)",
122
+ "label.machine-type": "machine type",
123
+ "label.unit-type": "unit type",
124
+ "label.vehicle-type": "vehicle type"
119
125
  }
@@ -117,5 +117,11 @@
117
117
  "label.load-layout.tier-field-empty": "Empty = tier 1",
118
118
  "label.load-layout.title": "Carrier load slots",
119
119
  "label.load-layout.unit-missing": "Drawing unit is not set (mmPerUnit). Physical placement and fit checks are unavailable.",
120
- "label.load-layout.unit-ok": "Drawing unit 1 = {mm} mm"
120
+ "label.load-layout.unit-ok": "Drawing unit 1 = {mm} mm",
121
+ "label.vehicle-heading": "車両の向き",
122
+ "label.carrier-mount": "キャリアの載せ方",
123
+ "label.rail-hang": "レールの吊り下げ長さ(mm)",
124
+ "label.machine-type": "設備タイプ",
125
+ "label.unit-type": "ユニットタイプ",
126
+ "label.vehicle-type": "車両タイプ"
121
127
  }