@fmsim/machine 2.0.1 → 3.0.0-beta.2

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 (95) hide show
  1. package/dist/agv.js +2 -3
  2. package/dist/agv.js.map +1 -1
  3. package/dist/carrier.js +53 -43
  4. package/dist/carrier.js.map +1 -1
  5. package/dist/crane.js +37 -2
  6. package/dist/crane.js.map +1 -1
  7. package/dist/editors/index.js +2 -0
  8. package/dist/editors/index.js.map +1 -1
  9. package/dist/editors/load-layout-host.js +96 -0
  10. package/dist/editors/load-layout-host.js.map +1 -0
  11. package/dist/editors/ox-load-layout-panel.js +124 -0
  12. package/dist/editors/ox-load-layout-panel.js.map +1 -0
  13. package/dist/factories/crane-3d.js +77 -18
  14. package/dist/factories/crane-3d.js.map +1 -1
  15. package/dist/factories/machine-3d.js +29 -52
  16. package/dist/factories/machine-3d.js.map +1 -1
  17. package/dist/features/capacity-gauge-mixin.js +2 -3
  18. package/dist/features/capacity-gauge-mixin.js.map +1 -1
  19. package/dist/features/load-layout-holder.js +77 -0
  20. package/dist/features/load-layout-holder.js.map +1 -0
  21. package/dist/features/mcs-status-default.js +30 -7
  22. package/dist/features/mcs-status-default.js.map +1 -1
  23. package/dist/features/mcs-status-mixin.js +3 -3
  24. package/dist/features/mcs-status-mixin.js.map +1 -1
  25. package/dist/index.js +5 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/mcs-carrier-holder.js +287 -82
  28. package/dist/mcs-carrier-holder.js.map +1 -1
  29. package/dist/oht.js +4 -0
  30. package/dist/oht.js.map +1 -1
  31. package/dist/port.js +6 -1
  32. package/dist/port.js.map +1 -1
  33. package/dist/scene/root-container-override.js +5 -11
  34. package/dist/scene/root-container-override.js.map +1 -1
  35. package/dist/shelf.js +4 -0
  36. package/dist/shelf.js.map +1 -1
  37. package/dist/shuttle.js +4 -0
  38. package/dist/shuttle.js.map +1 -1
  39. package/dist/stocker-carrier-bar.js +4 -4
  40. package/dist/stocker-carrier-bar.js.map +1 -1
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/dist/utils/carrier-type-code.js +10 -0
  43. package/dist/utils/carrier-type-code.js.map +1 -0
  44. package/dist/utils/crane-forks.js +5 -0
  45. package/dist/utils/crane-forks.js.map +1 -0
  46. package/dist/utils/layout-issue-text.js +16 -0
  47. package/dist/utils/layout-issue-text.js.map +1 -0
  48. package/dist/utils/layout-issue.js +16 -0
  49. package/dist/utils/layout-issue.js.map +1 -0
  50. package/dist/utils/load-address.js +217 -0
  51. package/dist/utils/load-address.js.map +1 -0
  52. package/dist/utils/stocker-gauge.js +3 -2
  53. package/dist/utils/stocker-gauge.js.map +1 -1
  54. package/jest.config.js +9 -1
  55. package/package.json +9 -6
  56. package/src/agv.ts +2 -3
  57. package/src/carrier.ts +50 -41
  58. package/src/crane.ts +39 -2
  59. package/src/editors/index.ts +2 -0
  60. package/src/editors/load-layout-host.ts +121 -0
  61. package/src/editors/ox-load-layout-panel.ts +129 -0
  62. package/src/factories/crane-3d.ts +74 -20
  63. package/src/factories/machine-3d.ts +26 -49
  64. package/src/features/capacity-gauge-mixin.ts +2 -3
  65. package/src/features/load-layout-holder.ts +83 -0
  66. package/src/features/mcs-status-default.ts +29 -10
  67. package/src/features/mcs-status-mixin.ts +3 -7
  68. package/src/index.ts +7 -0
  69. package/src/mcs-carrier-holder.ts +319 -78
  70. package/src/oht.ts +5 -0
  71. package/src/port.ts +7 -1
  72. package/src/scene/root-container-override.ts +4 -14
  73. package/src/shelf.ts +5 -0
  74. package/src/shuttle.ts +5 -0
  75. package/src/stocker-carrier-bar.ts +4 -4
  76. package/src/utils/carrier-type-code.ts +9 -0
  77. package/src/utils/crane-forks.ts +4 -0
  78. package/src/utils/layout-issue-text.ts +18 -0
  79. package/src/utils/layout-issue.ts +41 -0
  80. package/src/utils/load-address.ts +270 -0
  81. package/src/utils/stocker-gauge.ts +3 -3
  82. package/translations/en.json +42 -1
  83. package/translations/ja.json +42 -1
  84. package/translations/ko.json +59 -18
  85. package/translations/ms.json +42 -1
  86. package/translations/zh.json +42 -1
  87. package/dist/carrier-manager.js +0 -191
  88. package/dist/carrier-manager.js.map +0 -1
  89. package/dist/features/animation-default.js +0 -6
  90. package/dist/features/animation-default.js.map +0 -1
  91. package/dist/utils/get-value-on-ranges.js +0 -21
  92. package/dist/utils/get-value-on-ranges.js.map +0 -1
  93. package/src/carrier-manager.ts +0 -231
  94. package/src/features/animation-default.ts +0 -13
  95. package/src/utils/get-value-on-ranges.ts +0 -23
@@ -12,8 +12,7 @@ import * as THREE from 'three'
12
12
  import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
13
13
  import { RealObjectGroup, registerRealObjectFactory, disposeObject3D, MaterialBank } from '@hatiolab/things-scene'
14
14
  import type { Component } from '@hatiolab/things-scene'
15
- import { LEGEND_CAPACITY, resolveCarrierLegend } from '../features/mcs-status-default'
16
- import { getValueOnRanges } from '../utils/get-value-on-ranges'
15
+ import { LEGEND_CAPACITY, legendColor, legendRangeEntryOf, resolveCarrierLegend } from '../features/mcs-status-default'
17
16
  import { fitGaugeScale } from '../utils/fit-gauge-scale'
18
17
  import {
19
18
  carrierSegmentsFromData,
@@ -131,21 +130,10 @@ class MCSRealObject3D extends RealObjectGroup {
131
130
  if (this._config.defaultZPosFromParent) {
132
131
  return TRANSPORT_SURFACE_HEIGHT + halfD
133
132
  }
134
- // Carrier: 부모 유형에 따라 배치 위치 결정
135
- if (this._config.sizeRatio && this._config.sizeRatio < 1) {
136
- const parentType = this.component.parent?.state?.type
137
- // OHT: 그리퍼 아래에 매달림 — placement = -effectiveDepth(carrier top이 OHT bottom)
138
- if (parentType === 'OHT') {
139
- return -this.effectiveDepth + halfD
140
- }
141
- if (parentType === 'Port' || parentType === 'Shelf' || parentType === 'Shuttle') {
142
- return halfD
143
- }
144
- return TRANSPORT_SURFACE_HEIGHT + halfD
145
- }
146
133
  return halfD
147
134
  }
148
135
 
136
+
149
137
  // geometric 부분(center origin: depth/2)은 base 기본값 사용 — override 불필요
150
138
 
151
139
  // 컴포넌트의 상태 색상(statusColor) 또는 fillStyle → Three.js 색상 변환
@@ -799,7 +787,7 @@ class StockerRealObject3D extends MCSRealObject3D {
799
787
  const fullup = data.FULLUP === 'T'
800
788
  const usage = Math.round((cur / max) * 100)
801
789
  const legend = (this.component as any).root?.stockerCapacityLegendTheme || LEGEND_CAPACITY
802
- const color = fullup ? 'red' : (getValueOnRanges(usage, legend) || '#44aa44')
790
+ const color = fullup ? 'red' : (legendColor(legendRangeEntryOf(usage, legend)) || '#44aa44')
803
791
  return {
804
792
  segments: [{ ratio: usage / 100, color }],
805
793
  borderColor: color,
@@ -918,46 +906,35 @@ const CARRIER_CONFIG: MCS3DConfig = {
918
906
  }
919
907
 
920
908
  /**
921
- * 적층 시 다음 carrier 의 z-position 이 *_이전 carrier 의 effectiveDepth × N*
922
- * 만큼 위에 놓인다. N=1 이면 carrier 들이 맞붙어 적층 (기존), N<1 이면 부분
923
- * overlap (3D box 들이 겹쳐서 적층). 사용자 의도 — carrier 두께는 두꺼워도
924
- * 적층 시 *_compact 하게 보이도록* — N 을 작게.
909
+ * Carrier 의 3D. 홀더가 자리에 앉힌다 (공통 아이템 계약 3.4): 위치·회전은 홀더의 Anchor 노드 위 안착이고, 크기는 홀더가
910
+ * 보이는 치수(타입의 선언 치수, 모르면 자리의 표시용 상자)다. 자리 안에 들도록 줄이는 배율도 홀더가 준다.
925
911
  */
926
- const CARRIER_STACK_STEP_RATIO = 0.9
927
-
928
912
  class CarrierRealObject3D extends MCSRealObject3D {
929
913
  constructor(component: Component) {
930
914
  super(component, CARRIER_CONFIG)
931
915
  }
932
916
 
933
- /**
934
- * cz override — *carrier 자기 결정* 의 적층 위치.
935
- * parent.components 안 자기 index 까지의 형제 carrier 들의 effectiveDepth
936
- * 누적합을 base cz 위에 더한다. holder 측에 별도 layout 책임을 두지 않고,
937
- * carrier 가 자기 stack 위치를 알아서 결정.
938
- */
939
- override get cz(): number {
940
- const base = super.cz
941
- const parent = this.component.parent as any
942
- if (!parent?.components) return base
943
- const siblings = parent.components as any[]
944
- const idx = siblings.indexOf(this.component)
945
- if (idx <= 0) return base
946
- let offset = 0
947
- for (let i = 0; i < idx; i++) {
948
- const ro = siblings[i]?._realObject
949
- const eff = Number(ro?.effectiveDepth)
950
- const d = Number.isFinite(eff) && eff > 0 ? eff : 1
951
- offset += d * CARRIER_STACK_STEP_RATIO
952
- }
953
- // 적층 방향 — placement (= 부모 holder 타입) 에 따라 결정.
954
- // OHT 처럼 그리퍼 아래에 매달리는 holder 는 carrier 가 *_아래로_* 누적,
955
- // 그 외 (Port / Shelf / Shuttle / Vehicle) 는 위로 누적.
956
- // MCSRealObject3D.syncZPosOffset 의 OHT 분기 (-effectiveDepth + halfD) 와
957
- // 같은 방향성 일관 적용.
958
- const parentType = parent?.state?.type
959
- const dir = parentType === 'OHT' ? -1 : 1
960
- return base + dir * offset
917
+ /** 홀더가 보이는 치수 (도면 단위): 폭(X)·깊이(도면 Z)·높이. 모르면 null */
918
+ private get shown(): { w: number; d: number; h: number } | null {
919
+ const holder = this.component.parent as any
920
+ const dims = holder?.carrierShownDimsOf?.(this.component)
921
+ const k = holder?.mmPerSceneUnit
922
+ if (!dims || !(k > 0)) return null
923
+ return { w: dims.width / k, d: dims.depth / k, h: dims.height / k }
924
+ }
925
+
926
+ /** build 가 sizeRatio 를 곱하므로 미리 나눈다 */
927
+ protected override get absSize(): { w: number; h: number } {
928
+ const shown = this.shown
929
+ if (!shown) return super.absSize
930
+ const ratio = CARRIER_CONFIG.sizeRatio ?? 1
931
+ return { w: shown.w / ratio, h: shown.d / ratio }
932
+ }
933
+
934
+ override get effectiveDepth(): number {
935
+ const shown = this.shown
936
+ if (!shown) return super.effectiveDepth
937
+ return shown.h / (CARRIER_CONFIG.sizeRatio ?? 1)
961
938
  }
962
939
  }
963
940
 
@@ -1,8 +1,7 @@
1
1
  import { Component, POSITION } from '@hatiolab/things-scene'
2
2
  import { themesColorRange } from '@fmsim/api'
3
- import { LEGEND_CAPACITY, Legend, resolveCarrierLegend } from './mcs-status-default'
3
+ import { LEGEND_CAPACITY, Legend, legendColor, legendRangeEntryOf, resolveCarrierLegend } from './mcs-status-default'
4
4
  import { safeRound } from '../utils/safe-round'
5
- import { getValueOnRanges } from '../utils/get-value-on-ranges'
6
5
  import { carrierSegments, abnormalSegments, carrierColorsFromLegend, pickSegmentAt, GaugeSegment } from '../utils/stocker-gauge'
7
6
 
8
7
  type Constructor<T = {}> = new (...args: any[]) => T
@@ -158,7 +157,7 @@ export function CapacityGaugeMixin<TBase extends Constructor<Component>>(Base: T
158
157
 
159
158
  get statusColor(): string | undefined {
160
159
  const { fullup } = (this as any).state
161
- return fullup ? 'red' : getValueOnRanges(this.usage, this.legend)
160
+ return fullup ? 'red' : legendColor(legendRangeEntryOf(this.usage, this.legend))
162
161
  }
163
162
 
164
163
  get text() {
@@ -0,0 +1,83 @@
1
+ /*
2
+ * 적재 레이아웃 홀더 (설계 02번 3.2.1절, 3.6.1절, 3.7.1절)
3
+ * 홀더의 실효 레이아웃에서 Carrier 를 주소의 자리에 배정한다.
4
+ *
5
+ * 보유·자리·안착·판정은 공통 아이템 계약의 것이다(things-scene Capacity·Holdable, figure-model). 실효 레이아웃은
6
+ * Capacity.effectiveLayout 이 정한다: 도면 노드의 개별 설정(state.slotLayout)이 있으면 그것, 없으면 홀더의 기본값.
7
+ * 이 믹스인에 남는 것은 MCS 의 몫이다: 실시간 데이터의 주소 읽기와 점유 배정, 예외를 어느 밖 자리에 둘지.
8
+ *
9
+ * 유효한 실효 레이아웃이 있고 도면 축척을 알 때만 자리를 배정한다. 그 밖에는 MCSCarrierHolder 대로 모두 한 무더기(pile)다.
10
+ */
11
+
12
+ import { resolveSlot, slotAddresses, slotIdOf, type ItemDims, type ResolvedSlot, type SlotMount } from '@hatiolab/figure-model'
13
+ import { drawingScaleFor, type DrawingScale } from '@hatiolab/things-scene'
14
+ import { LoadLayout, OccupancyResult, incomingFromV2, resolveOccupancy } from '../utils/load-address'
15
+ import { EXCEPTION_SLOT, type CarrierRecord } from '../mcs-carrier-holder'
16
+
17
+ type Constructor<T = any> = new (...args: any[]) => T
18
+
19
+ export function LoadLayoutHolder<TBase extends Constructor>(Base: TBase) {
20
+ return class extends Base {
21
+ _loadOccupancy?: OccupancyResult
22
+
23
+ /** 기본 레이아웃의 Tier 가 서는 방식. 기종별 프리셋 배정은 확정 대기 (3.8절) */
24
+ get loadMount(): SlotMount {
25
+ return 'ON'
26
+ }
27
+
28
+ /**
29
+ * 도면 좌표 1단위당 mm 와 그것을 아는지. things-scene 이 도면 축척을 읽는 한 곳(drawingScaleFor, ADR-0144 2.2)에서
30
+ * 받는다. 축척이 없는 도면(known: false)에서는 자리를 배정하지 않는다.
31
+ */
32
+ get mmPerUnit(): DrawingScale {
33
+ return drawingScaleFor(this as any)
34
+ }
35
+
36
+ /** 자리를 배정하는 레이아웃. 유효한 실효 레이아웃이 없거나 축척을 모르면 undefined (한 무더기로 쌓는다) */
37
+ get _validLoadLayout(): LoadLayout | undefined {
38
+ const sel = (this as any).effectiveLayout
39
+ return sel.status === 'valid' && this.mmPerUnit.known ? (sel.layout as LoadLayout) : undefined
40
+ }
41
+
42
+ /**
43
+ * Carrier 마다 자리를 정한 레코드 (설계 02번 3.6.1절). 정상 자리는 자리마다 아래에서 위로, 예외는 그 뒤에 예외 영역에.
44
+ * 같은 이름이 두 번 오면 데이터 그대로 넘겨 공통 대조가 스냅샷째 거절하게 한다 (계약 Q2).
45
+ */
46
+ carrierRecords(data: Record<string, any> | undefined): CarrierRecord[] {
47
+ const layout = this._validLoadLayout
48
+ if (!layout) {
49
+ this._loadOccupancy = undefined
50
+ return super.carrierRecords(data)
51
+ }
52
+ const incoming = incomingFromV2(data)
53
+ const occupancy = resolveOccupancy(layout, incoming, this._loadOccupancy)
54
+ this._loadOccupancy = occupancy
55
+ const stateOf = new Map(incoming.map(c => [c.name, (this as any).carrierStateOf(c.record, c.record === data)] as const))
56
+ if (stateOf.size !== incoming.length) return incoming.map(c => ({ slotId: EXCEPTION_SLOT, itemId: c.name, itemState: stateOf.get(c.name)! }))
57
+ const records: CarrierRecord[] = []
58
+ for (const list of occupancy.slots.values()) for (const c of list) records.push({ slotId: slotIdOf(c.address), itemId: c.name, itemState: stateOf.get(c.name)! })
59
+ // 예외는 정상 자리와 나눈 예외 영역에 차례로 둔다. 다른 정상 자리를 대신 쓰지 않는다
60
+ for (const c of [...occupancy.addressUnknown, ...occupancy.addressError]) records.push({ slotId: EXCEPTION_SLOT, itemId: c.name, itemState: stateOf.get(c.name)! })
61
+ return records
62
+ }
63
+
64
+ /** 예외 영역의 치수 모르는 Carrier 는 첫 자리를 채우는 표시용 상자로 보인다 */
65
+ itemShownDimsOf(carrier: any, slot: ResolvedSlot, slotId: string): ItemDims | null {
66
+ if (slotId !== EXCEPTION_SLOT) return super.itemShownDimsOf(carrier, slot, slotId)
67
+ const first = this._firstSlot
68
+ return first ? super.itemShownDimsOf(carrier, first, slotIdOf({ tier: first.tier, slot: first.slotNo })) : null
69
+ }
70
+
71
+ /** 예외 영역의 Carrier 는 첫 자리 폭의 절반만큼 더 떨어져 선다 */
72
+ get exceptionHalfWidth(): number {
73
+ const first = this._firstSlot
74
+ return first && Number.isFinite(first.allowed.width) ? first.allowed.width / 2 : 0
75
+ }
76
+
77
+ get _firstSlot(): ResolvedSlot | null {
78
+ const layout = this._validLoadLayout
79
+ const address = layout ? [...slotAddresses(layout)][0] : undefined
80
+ return layout && address ? resolveSlot(layout, address) : null
81
+ }
82
+ }
83
+ }
@@ -1,5 +1,31 @@
1
- export interface Legend {
2
- [key: string]: string
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'
3
+
4
+ /* 범례 항목 모양과 캐리어 기본 범례는 피규어 능력과 한 곳(@fmsim/figure-capabilities, ADR-0167) */
5
+ export { LEGEND_CARRIER, legendColor, type Legend, type LegendEntry }
6
+
7
+ /**
8
+ * 코드의 범례 항목: 그 코드의 항목, 없으면 `default` (ADR-0166 판정 6). things-scene 의 한 조회(resolveLegend)로 읽는다 — 피규어의 표와
9
+ * 보드 매핑이 읽는 것과 같다.
10
+ */
11
+ export function legendEntryOf(code: unknown, legend: Legend | null | undefined): LegendEntry | undefined {
12
+ return resolveLegend(code, legend)
13
+ }
14
+
15
+ /**
16
+ * 수의 구간 범례 항목: 키 `from~to`(from 이상 to 미만, 한쪽 열림 가능)에서 그 값을 품은 항목, 없으면 `default`. 값이 없거나 수가 아니면
17
+ * `default` 다(ADR-0166 판정 6 — 0 으로 읽지 않는다). things-scene 의 한 조회(resolveLegendEntries)로 읽는다.
18
+ */
19
+ export function legendRangeEntryOf(value: unknown, legend: Legend | null | undefined): LegendEntry | undefined {
20
+ if (!legend) return undefined
21
+ const entries: RangeEntry<LegendEntry>[] = []
22
+ for (const [key, entry] of Object.entries(legend)) {
23
+ if (key === 'default') continue
24
+ const [from = '', to = ''] = key.split('~')
25
+ if (!key.includes('~')) continue
26
+ entries.push({ min: from === '' ? null : from, max: to === '' ? null : to, result: entry })
27
+ }
28
+ return resolveLegendEntries(value, entries, legend.default)
3
29
  }
4
30
 
5
31
  export const LEGEND_NULL: Legend = { default: '#F0F0F0' }
@@ -54,13 +80,6 @@ export const LEGEND_CRANE: Legend = {
54
80
  default: '#F0F0F0'
55
81
  }
56
82
 
57
- export const LEGEND_CARRIER: Legend = {
58
- FULL: '#4AA7FE',
59
- EMPTY: '#8BDA5B',
60
- EMPTYEMPTY: '#019D59',
61
- default: '#F0F0F0'
62
- }
63
-
64
83
  /**
65
84
  * carrier legend(테마) 해석 — carrier 컴포넌트(carrier.ts get legend)와 동일 로직 단일 소스.
66
85
  * parent 에 carrierLegendName 이 지정되면 그 style 을, 아니면 board 전역 carrierLegendTheme,
@@ -78,7 +97,7 @@ export const LEGEND_CAPACITY: Legend = {
78
97
  '0~20': '#5630D0',
79
98
  '20~80': '#8BDA5B',
80
99
  '80~': '#FD9B00', // 80% 이상 (= 100% 초과도 포함). 'from~' 형식이라
81
- // getValueOnRanges 가 value >= 80 매칭.
100
+ // legendRangeEntryOf 가 value >= 80 매칭.
82
101
  default: '#F0F0F0'
83
102
  }
84
103
 
@@ -1,13 +1,9 @@
1
1
  import { Component } from '@hatiolab/things-scene'
2
2
  import { themesColorMap } from '@fmsim/api'
3
- import { LEGEND_NULL } from './mcs-status-default'
3
+ import { LEGEND_NULL, Legend, legendColor, legendEntryOf } from './mcs-status-default'
4
4
 
5
5
  type Constructor<T = {}> = new (...args: any[]) => T
6
6
 
7
- interface Legend {
8
- [key: string]: string
9
- }
10
-
11
7
  export const MCSStatusMixinProperties = [
12
8
  {
13
9
  type: 'select',
@@ -45,14 +41,14 @@ export function MCSStatusMixin<TBase extends Constructor<Component>>(Base: TBase
45
41
  const status = this.status
46
42
  const legend = this.legend
47
43
 
48
- return (status && legend && (legend[status] || legend[status!] || legend.default)) || legend?.default || '#F0F0F0'
44
+ return (status && legend && legendColor(legendEntryOf(status, legend))) || legendColor(legend?.default) || '#F0F0F0'
49
45
  }
50
46
 
51
47
  get auxColor(): string | undefined {
52
48
  const status = this.auxStatus
53
49
  const legend = this.auxLegend
54
50
 
55
- return (status && legend && (legend[status] || legend[status!] || legend.default)) || legend?.default
51
+ return (status && legend && legendColor(legendEntryOf(status, legend))) || legendColor(legend?.default)
56
52
  }
57
53
 
58
54
  /**
package/src/index.ts CHANGED
@@ -54,6 +54,9 @@ import './clickable-popup'
54
54
  // ── groups ──
55
55
  export { default as MachineGroups } from './groups'
56
56
 
57
+ // ── legends ──
58
+ export { legendColor, legendEntryOf, legendRangeEntryOf, type Legend, type LegendEntry } from './features/mcs-status-default'
59
+
57
60
  // ── base classes ──
58
61
  export { default as MCSMachine } from './mcs-machine'
59
62
  export { default as MCSUnit } from './mcs-unit'
@@ -89,6 +92,7 @@ export { default as Port } from './port'
89
92
  export { default as Crane } from './crane'
90
93
  export { default as Shelf } from './shelf'
91
94
  export { default as Carrier } from './carrier'
95
+ export { CARRIER_HOLDER_PROPERTIES, CarrierHolder, carrierHolderLegend } from './mcs-carrier-holder'
92
96
 
93
97
  // ── topology ──
94
98
  export { default as Node } from './node'
@@ -98,3 +102,6 @@ export { default as NodePath } from './node-path'
98
102
  export { default as FloatingContainer } from './floating-container'
99
103
  export { default as StaticContainer } from './static-container'
100
104
  export { default as ClickablePopup } from './clickable-popup'
105
+
106
+ // ── CARRIERTYPE 을 아이템 타입 코드로 맞추는 규칙 (01번 6.8절, ADR-0155) ──
107
+ export { normalizeTypeCode } from './utils/carrier-type-code'