@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
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@fmsim/machine",
3
3
  "description": "Layout View를 구성하는 설비(machine)들과 유닛을 구현한 모듈입니다.",
4
4
  "author": "heartyoh",
5
- "version": "3.0.0-beta.4",
5
+ "version": "3.0.0-beta.6",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
8
8
  "things-scene": true,
@@ -27,10 +27,11 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@fmsim/api": "^3.0.0-beta.2",
30
- "@fmsim/figure-capabilities": "^3.0.0-beta.4",
30
+ "@fmsim/figure-capabilities": "^3.0.0-beta.5",
31
31
  "@hatiolab/figure-model": "^0.12.0",
32
32
  "@hatiolab/things-scene": "^10.1.90",
33
- "@operato/slot-layout-editor": "^10.29.33"
33
+ "@operato/slot-layout-editor": "^10.29.33",
34
+ "tinycolor2": "^1.6.0"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@hatiolab/prettier-config": "^1.0.0",
@@ -58,5 +59,5 @@
58
59
  "prettier --write"
59
60
  ]
60
61
  },
61
- "gitHead": "95b704a73e326b7be3bbd1ef3db5ee31f7ef7184"
62
+ "gitHead": "d3de9f6ded33cac773a583a2308883a8bed2fabc"
62
63
  }
package/src/agv-line.ts CHANGED
@@ -22,8 +22,8 @@ export default class AGVLine extends MCSTransport {
22
22
  return '#333'
23
23
  }
24
24
 
25
- getLegendFallback() {
26
- return (this.root as any).agvlineLegendTheme || super.getLegendFallback()
25
+ get statusLegendRow(): string {
26
+ return 'agvline'
27
27
  }
28
28
 
29
29
  render(context: CanvasRenderingContext2D) {
package/src/agv.ts CHANGED
@@ -37,8 +37,13 @@ export default class AGV extends MCSVehicle {
37
37
  return ['AGVLine']
38
38
  }
39
39
 
40
- getLegendFallback() {
41
- return (this.root as any).agvLegendTheme || super.getLegendFallback()
40
+ get statusLegendRow(): string {
41
+ return 'agv'
42
+ }
43
+
44
+ /** Its STATUS row and its battery's (the battery is the AGV's own: only a board with AGVs shows the row) */
45
+ get legendRowsUsed(): string[] {
46
+ return [...super.legendRowsUsed, 'battery-rate']
42
47
  }
43
48
 
44
49
  getBatteryLegendFallback() {
package/src/buffer.ts CHANGED
@@ -14,8 +14,8 @@ export default class Buffer extends MCSMachine {
14
14
  return NATURE
15
15
  }
16
16
 
17
- getLegendFallback() {
18
- return (this.root as any).bufferLegendTheme || super.getLegendFallback()
17
+ get statusLegendRow(): string {
18
+ return 'buffer'
19
19
  }
20
20
 
21
21
  containable(component: Component) {
@@ -98,8 +98,8 @@ export default class ConveyorJoin extends ConveyorMixin(MCSTransport) {
98
98
  return NATURE
99
99
  }
100
100
 
101
- getLegendFallback() {
102
- return (this.root as any).conveyorLegendTheme || super.getLegendFallback()
101
+ get statusLegendRow(): string {
102
+ return 'conveyor'
103
103
  }
104
104
 
105
105
  containable(component: Component) {
package/src/conveyor.ts CHANGED
@@ -36,8 +36,8 @@ export default class Conveyor extends MCSTransport {
36
36
  return '#777'
37
37
  }
38
38
 
39
- getLegendFallback() {
40
- return (this.root as any).conveyorLegendTheme || super.getLegendFallback()
39
+ get statusLegendRow(): string {
40
+ return 'conveyor'
41
41
  }
42
42
 
43
43
  containable(component: Component) {
package/src/crane.ts CHANGED
@@ -92,8 +92,8 @@ export default class Crane extends LoadLayoutHolder(MCSVehicle) {
92
92
  return ['STOCKER']
93
93
  }
94
94
 
95
- getLegendFallback() {
96
- return (this.root as any).craneLegendTheme || super.getLegendFallback()
95
+ get statusLegendRow(): string {
96
+ return 'crane'
97
97
  }
98
98
 
99
99
  private _posAnimator?: CranePositionAnimator
package/src/equipment.ts CHANGED
@@ -14,8 +14,8 @@ export default class Equipment extends MCSMachine {
14
14
  return NATURE
15
15
  }
16
16
 
17
- getLegendFallback() {
18
- return (this.root as any).equipmentLegendTheme || super.getLegendFallback()
17
+ get statusLegendRow(): string {
18
+ return 'equipment'
19
19
  }
20
20
 
21
21
  containable(component: Component) {
@@ -15,7 +15,7 @@ import * as THREE from 'three'
15
15
  import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
16
16
  import { RealObjectGroup, registerRealObjectFactory, MaterialBank } from '@hatiolab/things-scene'
17
17
  import type { Component } from '@hatiolab/things-scene'
18
- import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'
18
+ import { MACHINE_HEIGHTS } from '../features/lv-heights.js'
19
19
  import { Z_FMSIM } from './z-priority'
20
20
 
21
21
  const FRAME_COLOR = 0x888899
@@ -27,7 +27,7 @@ export class Conveyor3D extends RealObjectGroup {
27
27
 
28
28
  get effectiveDepth(): number {
29
29
  const { depth } = this.component.state
30
- return depth || TRANSPORT_SURFACE_HEIGHT
30
+ return depth || MACHINE_HEIGHTS.conveyor
31
31
  }
32
32
 
33
33
  private resolveColor(): number {
@@ -14,6 +14,8 @@ import { RealObjectGroup, registerRealObjectFactory, disposeObject3D, MaterialBa
14
14
  import type { Component } from '@hatiolab/things-scene'
15
15
  import { LEGEND_CAPACITY, legendColor, legendRangeEntryOf, resolveCarrierLegend } from '../features/mcs-status-default'
16
16
  import { fitGaugeScale } from '../utils/fit-gauge-scale'
17
+ import { railTopOf } from '../features/oht-rail'
18
+ import { MACHINE_HEIGHTS, MACHINE_OPACITY, TRANSPORT_SURFACE_HEIGHT } from '../features/lv-heights'
17
19
  import {
18
20
  carrierSegmentsFromData,
19
21
  abnormalSegmentsFromData,
@@ -59,12 +61,8 @@ interface MCS3DConfig {
59
61
  zPriority?: number
60
62
  }
61
63
 
62
- /**
63
- * 포트 기본 높이 (절대값).
64
- * 모든 이송장치의 상단 높이 = 이 값.
65
- * 캐리어가 이송장치 간 높이 차 없이 이동할 수 있는 기준 높이.
66
- */
67
- export const TRANSPORT_SURFACE_HEIGHT = 30
64
+ /* 포트 기본 높이와 설비 타입별 높이는 한 곳(features/lv-heights) — 피규어 fmsim.Machine 도 같은 표를 읽는다 */
65
+ export { TRANSPORT_SURFACE_HEIGHT } from '../features/lv-heights'
68
66
 
69
67
  const DEFAULTS: MCS3DConfig = {
70
68
  depthRatio: 0.5,
@@ -502,8 +500,8 @@ function register(type: string, overrides: Partial<MCS3DConfig> = {}) {
502
500
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
503
501
 
504
502
  register('Equipment', {
505
- defaultDepth: 3 * TRANSPORT_SURFACE_HEIGHT,
506
- opacity: 0.75,
503
+ defaultDepth: MACHINE_HEIGHTS.equipment,
504
+ opacity: MACHINE_OPACITY.equipment,
507
505
  metalness: 0.4,
508
506
  roughness: 0.6,
509
507
  defaultColor: 0x8899aa,
@@ -511,8 +509,8 @@ register('Equipment', {
511
509
  })
512
510
 
513
511
  register('Buffer', {
514
- defaultDepth: TRANSPORT_SURFACE_HEIGHT,
515
- opacity: 1.0,
512
+ defaultDepth: MACHINE_HEIGHTS.buffer,
513
+ opacity: MACHINE_OPACITY.buffer,
516
514
  metalness: 0.3,
517
515
  roughness: 0.7,
518
516
  defaultColor: 0x99aa88,
@@ -534,14 +532,14 @@ register('AGVLine', {
534
532
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
535
533
 
536
534
  const OHT_LINE_RAIL_COLOR = 0xcccccc
537
- const OHT_LINE_HEIGHT = 7 * TRANSPORT_SURFACE_HEIGHT // OHT 상단과 일치
538
535
 
539
536
  class OHTLineRealObject3D extends RealObjectGroup {
540
537
  private _mesh?: THREE.Mesh
541
538
 
542
539
  get zPriority(): number { return Z_FMSIM.OHT_LINE }
543
540
  get effectiveDepth(): number { return 0 }
544
- protected get syncZPosOffset(): number { return OHT_LINE_HEIGHT }
541
+ /** 레일 윗면 = 작업환경 천장 − 라인의 매달림 길이(railHang). OHT 상단이 여기에 매달린다 */
542
+ protected get syncZPosOffset(): number { return railTopOf(this.component) }
545
543
 
546
544
  build() {
547
545
  super.build()
@@ -616,6 +614,7 @@ class OHTLineRealObject3D extends RealObjectGroup {
616
614
  this.update()
617
615
  return
618
616
  }
617
+ if ('railHang' in after) this.updateTransform()
619
618
  super.onchange(after, before)
620
619
  }
621
620
 
@@ -15,14 +15,12 @@ import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeom
15
15
  import { RealObjectGroup, registerRealObjectFactory, disposeObject3D, MaterialBank } from '@hatiolab/things-scene'
16
16
  import type { Component } from '@hatiolab/things-scene'
17
17
  import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'
18
+ import { ohtRailTop } from '../features/oht-rail.js'
18
19
 
19
20
  const TROLLEY_COLOR = 0x222222
20
21
  const FRAME_COLOR = 0x444444
21
22
  const DEFAULT_BODY_COLOR = 0x6688cc
22
23
 
23
- /** OHT 레일 높이: 스토커(5x) 위 */
24
- const OHT_RAIL_HEIGHT = 6 * TRANSPORT_SURFACE_HEIGHT
25
-
26
24
  export class OHT3D extends RealObjectGroup {
27
25
  private _ownMeshes: THREE.Mesh[] = []
28
26
  get effectiveDepth(): number {
@@ -30,9 +28,9 @@ export class OHT3D extends RealObjectGroup {
30
28
  return depth || TRANSPORT_SURFACE_HEIGHT
31
29
  }
32
30
 
33
- /** 천장 레일 높이에 배치 (center origin 기준: 레일 높이 + 볼륨 중심 반깊이) */
31
+ /** 자기 라인의 레일에 상단을 매단다 (center origin 기준: 레일 윗면 − 볼륨 중심 반깊이) */
34
32
  protected get syncZPosOffset(): number {
35
- return OHT_RAIL_HEIGHT + this.effectiveDepth / 2
33
+ return ohtRailTop(this.component) - this.effectiveDepth / 2
36
34
  }
37
35
 
38
36
  private resolveColor(): number {
@@ -0,0 +1,31 @@
1
+ import { themesColorMap, themesColorRange, themesAnimationMap } from '@fmsim/api'
2
+ import { legendRows, type LegendRow } from '@fmsim/figure-capabilities'
3
+ import { legendRowsOnBoard } from './legend-rows'
4
+
5
+ /** The options a row's legend select offers: the board's legends of its kind. */
6
+ const LEGEND_OPTIONS: Record<LegendRow['kind'], unknown> = {
7
+ 'color-map': themesColorMap,
8
+ 'color-ranges': themesColorRange,
9
+ 'animation-map': themesAnimationMap
10
+ }
11
+
12
+ const legendSelect = (row: LegendRow) => ({ type: 'select', label: row.label, name: row.property, property: { options: LEGEND_OPTIONS[row.kind] } })
13
+ const legendHeader = (name: string, label: string) => ({ type: 'legend', label: '', name, property: { label } })
14
+
15
+ /**
16
+ * The board's LEGENDS panel, drawn from the registered rows (ADR-0167 decision 20): the rows the board's components
17
+ * follow first, then the others; the stocker's rows apart, as they were.
18
+ */
19
+ export function legendPanel(board: any): any[] {
20
+ const used = legendRowsOnBoard(board)
21
+ const rows = legendRows().filter(row => row.section !== 'stocker')
22
+ const here = rows.filter(row => used.has(row.name))
23
+ const others = rows.filter(row => !used.has(row.name))
24
+ const stocker = legendRows().filter(row => row.section === 'stocker')
25
+ return [
26
+ legendHeader('LEGENDS', 'LEGENDS'),
27
+ ...here.map(legendSelect),
28
+ ...(others.length ? [legendHeader('LEGENDS-OTHERS', 'LEGENDS · NOT ON THIS BOARD'), ...others.map(legendSelect)] : []),
29
+ ...(stocker.length ? [legendHeader('LEGENDS-STOCKER', 'LEGENDS · STOCKER'), ...stocker.map(legendSelect)] : [])
30
+ ]
31
+ }
@@ -0,0 +1,86 @@
1
+ import JSON5 from 'json5'
2
+ import tinycolor from 'tinycolor2'
3
+ import { legendRowOf } from '@fmsim/figure-capabilities'
4
+ import type { Legend } from './mcs-status-default'
5
+
6
+ /*
7
+ * A board's legend rows (ADR-0167 decision 20): the legend a row chose is read here, by the row's board property, for
8
+ * fmsim's own components and its figures alike. Kept in the board properties they have always had.
9
+ */
10
+
11
+ // rootModel.state.style[name] 에 저장된 legend/animation 값은 보드 직렬화 형태에 따라 (a) 정상 object 또는 (b) JSON 문자열
12
+ // 둘 다로 나타난다 (2026-06-09 진단). WeakMap<state, Map<rawString, parsed>> 으로 *raw string 자체를 key* — 보드 편집으로 raw 가
13
+ // 바뀌면 자연 무효화, 같은 raw 의 반복 lookup 은 O(1). root 가 GC 되면 cache 도 같이 GC.
14
+ const _legendParseCache = new WeakMap<object, Map<string, any>>()
15
+
16
+ export function parseLegendIfString(state: any, raw: any): any {
17
+ if (typeof raw !== 'string') return raw
18
+ let cache = _legendParseCache.get(state)
19
+ if (!cache) {
20
+ cache = new Map()
21
+ _legendParseCache.set(state, cache)
22
+ }
23
+ if (cache.has(raw)) return cache.get(raw)
24
+ let parsed: any
25
+ try {
26
+ parsed = JSON.parse(raw)
27
+ } catch {
28
+ parsed = raw // parse 실패 시 raw 보존
29
+ }
30
+ cache.set(raw, parsed)
31
+ return parsed
32
+ }
33
+
34
+ /** The legend a board property names, read from the board's style: none when the board chose none. */
35
+ export function boardLegendOf(root: any, property: string): Legend | undefined {
36
+ const name = root?.rootModel?.state?.[property]
37
+ if (!name || !root?.state?.style) return undefined
38
+ return parseLegendIfString(root.state, root.state.style[name]) || undefined
39
+ }
40
+
41
+ /** The legend the board chose for a row, for a component on it: none when the row is not registered or not chosen. */
42
+ export function boardLegendOfRow(component: any, row: unknown): Legend | undefined {
43
+ const registered = legendRowOf(row)
44
+ return registered ? boardLegendOf(component?.root, registered.property) : undefined
45
+ }
46
+
47
+ /**
48
+ * A legend colour as a figure takes it: `#RRGGBB`. fmsim's own components draw any CSS colour a legend gives (`navy`,
49
+ * `rgb(33, 148, 33)`, `#f00`); a figure's table takes `#RRGGBB` only (ADR-0166), so an opaque colour is written so. A
50
+ * colour that is not opaque (`transparent`) has no `#RRGGBB` and stays as it is -- the figure says it did not take it.
51
+ */
52
+ export function figureColourOf(value: unknown): unknown {
53
+ if (typeof value !== 'string' || /^#[0-9a-f]{6}$/i.test(value)) return value
54
+ const colour = tinycolor(value.trim())
55
+ return colour.isValid() && colour.getAlpha() === 1 ? colour.toHexString() : value
56
+ }
57
+
58
+ /** A legend as a figure takes it from fmsim: each entry's colour, written `#RRGGBB`, and effect; an entry's JSON5 text read. */
59
+ export function colourAndEffect(legend: Record<string, unknown>): Record<string, unknown> {
60
+ return Object.fromEntries(
61
+ Object.entries(legend).map(([code, raw]) => {
62
+ let entry = raw
63
+ if (typeof entry === 'string' && entry.trim().startsWith('{')) {
64
+ try {
65
+ entry = JSON5.parse(entry)
66
+ } catch {
67
+ return [code, raw]
68
+ }
69
+ }
70
+ if (!entry || typeof entry !== 'object') return [code, figureColourOf(entry)]
71
+ const { color, effect } = entry as { color?: unknown; effect?: unknown }
72
+ return [code, { ...(color !== undefined ? { color: figureColourOf(color) } : {}), ...(effect !== undefined ? { effect } : {}) }]
73
+ })
74
+ )
75
+ }
76
+
77
+ /** The rows the components of a board follow, each once: what its LEGENDS panel shows first. */
78
+ export function legendRowsOnBoard(container: any): Set<string> {
79
+ const used = new Set<string>()
80
+ const walk = (c: any) => {
81
+ for (const row of c?.legendRowsUsed ?? []) if (row) used.add(row)
82
+ for (const child of c?.components ?? []) walk(child)
83
+ }
84
+ walk(container)
85
+ return used
86
+ }
@@ -0,0 +1,151 @@
1
+ import { figureLegendSourcesChanged, registerFigureCapability, registerFigureLegendResolver } from '@hatiolab/things-scene'
2
+ import { CARRIER_HOLDER, MACHINE, STATUS, STATUS_LEGEND_ROLE, UNIT, VEHICLE, carrierHolderProperties, statusProperties } from '@fmsim/figure-capabilities'
3
+ import { themesAnimationMap, themesColorMap } from '@fmsim/api'
4
+ import { boardLegendOfRow, colourAndEffect, parseLegendIfString } from './legend-rows'
5
+ import { MACHINE_HEIGHTS, TRANSPORT_SURFACE_HEIGHT } from './lv-heights'
6
+ import { VehicleTravel } from './vehicle-travel'
7
+ import { CarrierHolder } from '../mcs-carrier-holder'
8
+
9
+ /*
10
+ * fmsim's figure capabilities (ADR-0167 decision 22): what a figure is in LV -- fmsim.Machine, fmsim.Unit or
11
+ * fmsim.Vehicle, one at most -- and the features worn with any of them, fmsim.Status and fmsim.CarrierHolder. A kind
12
+ * says its type (`lvType`), the board row a status follows; the features are the mixins fmsim's own wear.
13
+ */
14
+
15
+ /**
16
+ * STATUS as the figure takes it: a code, so a number (MCS sends 4001) is given as its text ('4001'). The status table reads
17
+ * STATUS as a text and a figure refuses data whose value is not of the declared kind; the component's own data is not changed.
18
+ */
19
+ export function statusAsCode(data: any): any {
20
+ return data && typeof data === 'object' && typeof data.STATUS === 'number' ? { ...data, STATUS: String(data.STATUS) } : data
21
+ }
22
+
23
+ /**
24
+ * `fmsim.Status`: the figure follows the board row of its kind's type, a change of its legend has its tables chosen
25
+ * again, and its STATUS reaches the figure as a code. The colour is its capability's table, drawn by things-scene.
26
+ */
27
+ export function StatusFigure<T extends new (...args: any[]) => any>(Base: T) {
28
+ return class extends Base {
29
+ _statusInputs?: unknown
30
+
31
+ /** The figure's inputs, given the data's STATUS as a code when they are made (a new release starts them again) */
32
+ get figureInputs(): any {
33
+ const inputs = super.figureInputs
34
+ if (inputs && inputs !== this._statusInputs) {
35
+ this._statusInputs = inputs
36
+ if (typeof (this as any).state?.data?.STATUS === 'number') inputs.setData(statusAsCode((this as any).state.data))
37
+ }
38
+ return inputs
39
+ }
40
+
41
+ /** The board row its status follows: its kind's type, none without a kind */
42
+ get statusLegendRow(): string | undefined {
43
+ return (this as any).lvType
44
+ }
45
+
46
+ get legendRowsUsed(): string[] {
47
+ const row = this.statusLegendRow
48
+ return [...((super.legendRowsUsed as string[] | undefined) ?? []), ...(row ? [row] : [])]
49
+ }
50
+
51
+ onchange(after: Record<string, any>, before: Record<string, any>) {
52
+ super.onchange?.(after && 'data' in after ? { ...after, data: statusAsCode(after.data) } : after, before)
53
+ if (after && 'legendName' in after) figureLegendSourcesChanged(this)
54
+ }
55
+ }
56
+ }
57
+
58
+ /** An LV kind: its type, chosen or its default, is `lvType`; another type has the figure's legends chosen again. */
59
+ function Kind<T extends new (...args: any[]) => any>(Base: T, property: string, defaultOf: (figure: any) => string) {
60
+ return class extends Base {
61
+ get lvType(): string {
62
+ const chosen = (this as any).state?.[property]
63
+ return typeof chosen === 'string' && chosen ? chosen : defaultOf(this)
64
+ }
65
+
66
+ onchange(after: Record<string, any>, before: Record<string, any>) {
67
+ super.onchange?.(after, before)
68
+ if (after && property in after) figureLegendSourcesChanged(this)
69
+ }
70
+ }
71
+ }
72
+
73
+ /**
74
+ * `fmsim.Machine`: an LV machine of a type, equipment unless chosen -- it stands as tall as fmsim's own machine of that
75
+ * type when the board writes no height (things-scene asks the component, `effectiveDepth`).
76
+ */
77
+ export function LVMachine<T extends new (...args: any[]) => any>(Base: T) {
78
+ return class extends Kind(Base, 'machineType', () => 'equipment') {
79
+ get effectiveDepth(): number | undefined {
80
+ return MACHINE_HEIGHTS[this.lvType]
81
+ }
82
+
83
+ onchange(after: Record<string, any>, before: Record<string, any>) {
84
+ super.onchange(after, before)
85
+ /* Another type stands at another height: the 3D object takes it */
86
+ if (after && 'machineType' in after) {
87
+ const real = (this as any)._realObject
88
+ real?.updateDimension?.()
89
+ real?.updateTransform?.()
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ /** `fmsim.Unit`: an LV unit of a type, a port unless chosen -- a tag LV handles it by */
96
+ export function LVUnit<T extends new (...args: any[]) => any>(Base: T) {
97
+ return Kind(Base, 'unitType', () => 'port')
98
+ }
99
+
100
+ /** `fmsim.Vehicle`: an LV vehicle -- it travels between nodes; its type, unless chosen, an AGV on the floor and an OHT on the ceiling */
101
+ export function LVVehicle<T extends new (...args: any[]) => any>(Base: T) {
102
+ return Kind(VehicleTravel(Base), 'vehicleType', figure => (figure?.constructor?.placement === 'ceiling' ? 'oht' : 'agv'))
103
+ }
104
+
105
+ /**
106
+ * `fmsim.CarrierHolder` on a figure: its carriers stand in its places -- a gripper, a lift plate, a fork's tiers -- and
107
+ * go with them; one without places keeps them in a pile on its top. Carriers change hands at the transport surface, so a
108
+ * holder that takes them on its
109
+ * top stands that tall when nothing else says -- as fmsim's own AGV and shuttle do. A machine's height wins (whichever is
110
+ * worn over the other); a holder that hangs them or hangs itself from the ceiling (an OHT) keeps its own.
111
+ */
112
+ export function FigureCarrierHolder<T extends new (...args: any[]) => any>(Base: T) {
113
+ return class extends CarrierHolder(Base) {
114
+ /** Its carriers stand in its own places, which go with the parts that carry them (ADR-0167 decision 23) */
115
+ get fillsPlaces(): boolean {
116
+ return true
117
+ }
118
+
119
+ get effectiveDepth(): number | undefined {
120
+ const given = super.effectiveDepth
121
+ if (given !== undefined) return given
122
+ const hung = (this as any).state?.carrierMount === 'HANG' || (this as any).constructor?.placement === 'ceiling'
123
+ return hung ? undefined : TRANSPORT_SURFACE_HEIGHT
124
+ }
125
+ }
126
+ }
127
+
128
+ /**
129
+ * The legend a status figure shows its STATUS by: its own choice (`legendName`, as fmsim's own have), then the board row
130
+ * of its kind's type; none chosen, no answer (its capability's table, fmsim's default legends).
131
+ */
132
+ export function statusLegend({ component, role }: { component: any; role: string }): unknown {
133
+ if (role !== STATUS_LEGEND_ROLE) return undefined
134
+ const own = component?.state?.legendName
135
+ const root = component?.root
136
+ const row = component?.statusLegendRow
137
+ const legend =
138
+ (typeof own === 'string' && own && root?.state?.style ? parseLegendIfString(root.state, root.state.style[own]) : undefined) ||
139
+ (row ? boardLegendOfRow(component, row) : undefined)
140
+ return legend && typeof legend === 'object' ? colourAndEffect(legend) : undefined
141
+ }
142
+
143
+ registerFigureLegendResolver(statusLegend)
144
+ registerFigureCapability(MACHINE, base => LVMachine(base) as never)
145
+ registerFigureCapability(UNIT, base => LVUnit(base) as never)
146
+ registerFigureCapability(VEHICLE, base => LVVehicle(base) as never)
147
+ registerFigureCapability({ ...STATUS, properties: statusProperties({ legends: themesColorMap }) as never }, base => StatusFigure(base) as never)
148
+ registerFigureCapability(
149
+ { ...CARRIER_HOLDER, properties: carrierHolderProperties({ legends: themesColorMap, animations: themesAnimationMap, mount: true }) as never },
150
+ base => FigureCarrierHolder(base) as never
151
+ )
@@ -0,0 +1,22 @@
1
+ /**
2
+ * LV's heights (ADR-0167 decision 21): fmsim gives no height on the board -- a machine stands as tall as its type, in
3
+ * transport surfaces, the height every port and transport stands at so carriers move between them level. fmsim's own
4
+ * machines and a figure wearing fmsim.Machine read the one table.
5
+ */
6
+
7
+ /** 포트 기본 높이 (절대값). 모든 이송장치의 상단 높이 = 이 값 — 캐리어가 이송장치 간 높이 차 없이 이동하는 기준 */
8
+ export const TRANSPORT_SURFACE_HEIGHT = 30
9
+
10
+ /** The height a machine of each type stands at, by its legend row name */
11
+ export const MACHINE_HEIGHTS: Record<string, number> = {
12
+ equipment: 3 * TRANSPORT_SURFACE_HEIGHT,
13
+ buffer: TRANSPORT_SURFACE_HEIGHT,
14
+ conveyor: TRANSPORT_SURFACE_HEIGHT
15
+ }
16
+
17
+ /** How opaque fmsim's own machine of each type is drawn: equipment is seen through, so the ports and carriers in it show */
18
+ export const MACHINE_OPACITY: Record<string, number> = {
19
+ equipment: 0.75,
20
+ buffer: 1,
21
+ conveyor: 1
22
+ }
@@ -1,8 +1,8 @@
1
1
  import { resolveLegend, resolveLegendEntries, type LegendEntry as RangeEntry } from '@hatiolab/things-scene'
2
- import { LEGEND_CARRIER, legendColor, type Legend, type LegendEntry } from '@fmsim/figure-capabilities'
2
+ import { LEGEND_CARRIER, LEGEND_CRANE, LEGEND_MACHINE, LEGEND_UNIT, LEGEND_VEHICLE, legendColor, type Legend, type LegendEntry } from '@fmsim/figure-capabilities'
3
3
 
4
- /* 범례 항목 모양과 캐리어 기본 범례는 피규어 능력과 한 곳(@fmsim/figure-capabilities, ADR-0167) */
5
- export { LEGEND_CARRIER, legendColor, type Legend, type LegendEntry }
4
+ /* 범례 항목 모양과 캐리어 · 상태 기본 범례는 피규어 능력과 한 곳(@fmsim/figure-capabilities, ADR-0167) */
5
+ export { LEGEND_CARRIER, LEGEND_CRANE, LEGEND_MACHINE, LEGEND_UNIT, LEGEND_VEHICLE, legendColor, type Legend, type LegendEntry }
6
6
 
7
7
  /**
8
8
  * 코드의 범례 항목: 그 코드의 항목, 없으면 `default` (ADR-0166 판정 6). things-scene 의 한 조회(resolveLegend)로 읽는다 — 피규어의 표와
@@ -30,56 +30,6 @@ export function legendRangeEntryOf(value: unknown, legend: Legend | null | undef
30
30
 
31
31
  export const LEGEND_NULL: Legend = { default: '#F0F0F0' }
32
32
 
33
- export const LEGEND_MACHINE: Legend = {
34
- '1000': '#707070', // DISCONNECTED
35
- '1001': '#8BDA5B', // UP
36
- '1002': '#EB3245', // OUTOFSERVICE
37
- '1003': '#5630D0', // PM
38
- '1004': '#FFFFFF', // NOTONLINE
39
- '1005': '#404040', // DOWN
40
- '1006': '#F4F226', // NOTAUTO
41
- '1007': '#FD9B00', // ALARM
42
- '1008': '#4AA7FE', // FULL
43
- default: '#F0F0F0'
44
- }
45
-
46
- export const LEGEND_VEHICLE: Legend = {
47
- '4000': '#EB3245', // OUTOFSERVICE
48
- '4001': '#8BDA5B', // ASSIGNED
49
- // '4002': '#cbf266', // ASSIGNED_ACQUIRING
50
- // '4003': '#cbf266', // ASSIGNED_DEPOSITING
51
- // '4004': '#cbf266', // ASSIGNED_ENROUTE
52
- // '4005': '#cbf266', // ASSIGNED_PARKED
53
- '4006': '#FD9B00', // NOTASSIGNED
54
- '4007': '#32D1C4', // INSTALLED
55
- '4008': '#9790EC', // CHARGING
56
- '4009': '#4AA7FE', // CHARGED
57
- '4010': '#404040', // REMOVED
58
- default: '#F0F0F0'
59
- }
60
-
61
- export const LEGEND_UNIT: Legend = {
62
- '2000': '#FD9B00', // BANNED
63
- '2001': '#404040', // DOWN
64
- '2002': '#8BDA5B', // EMPTY
65
- '2003': '#FFFFFF', // EMPTY_RESERVED
66
- '2004': '#32D1C4', // FULL_RESERVED
67
- '2005': '#4AA7FE', // FULL
68
- '2006': '#EB3245', // NOTINSERVICE
69
- default: '#F0F0F0'
70
- }
71
-
72
- export const LEGEND_CRANE: Legend = {
73
- '3000': '#EB3245', // OUTOFSERVICE
74
- // '3001': '#404040', // DOWN
75
- // '3002': '#8BDA5B', // EMPTY
76
- '3003': '#FFFFFF', // IDLE
77
- '3004': '#32D1C4', // ACTIVE
78
- // '3005': '#4AA7FE', // FULL
79
- // '3006': '#EB3245', // NOTINSERVICE
80
- default: '#F0F0F0'
81
- }
82
-
83
33
  /**
84
34
  * carrier legend(테마) 해석 — carrier 컴포넌트(carrier.ts get legend)와 동일 로직 단일 소스.
85
35
  * parent 에 carrierLegendName 이 지정되면 그 style 을, 아니면 board 전역 carrierLegendTheme,
@@ -0,0 +1,31 @@
1
+ import { drawingScaleFor, sceneHeightsOf } from '@hatiolab/things-scene'
2
+
3
+ /*
4
+ * Where an OHT runs: on its line's rail, hung from the work environment's ceiling. The rail's top is the ceiling less the
5
+ * line's hang (`railHang`, mm); an OHT -- fmsim's own or a figure wearing fmsim.Vehicle -- hangs its top from the rail of
6
+ * the line it runs on. Nothing here is a height of its own: each follows the ceiling of the room the board draws.
7
+ */
8
+
9
+ /** The top of an OHT line's rail, in drawing units: the scene's ceiling less the line's hang. */
10
+ export function railTopOf(line: any): number {
11
+ const ceiling = sceneHeightsOf(line).ceiling
12
+ const hang = Number(line?.state?.railHang) || 0
13
+ return ceiling - hang / drawingScaleFor(line).mmPerUnit
14
+ }
15
+
16
+ /** The OHT line a vehicle runs on: the one its data names (NODEMACHINE), its parent object, or the line holding it. */
17
+ export function ohtLineOf(vehicle: any): any {
18
+ const isLine = (c: any) => c?.state?.type === 'OHTLine'
19
+ for (const id of [vehicle?.data?.NODEMACHINE, vehicle?.state?.parentObjectId]) {
20
+ if (!id) continue
21
+ const line = (vehicle.root?.findAllById?.(id) || []).find(isLine)
22
+ if (line) return line
23
+ }
24
+ return isLine(vehicle?.parent) ? vehicle.parent : undefined
25
+ }
26
+
27
+ /** The height an OHT hangs its top from, in drawing units: its line's rail, or the ceiling when it runs on none. */
28
+ export function ohtRailTop(vehicle: any): number {
29
+ const line = ohtLineOf(vehicle)
30
+ return line ? railTopOf(line) : sceneHeightsOf(vehicle).ceiling
31
+ }