@operato/scene-storage 10.0.0-beta.48 → 10.0.0-beta.53

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 (78) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/box.js +2 -2
  3. package/dist/box.js.map +1 -1
  4. package/dist/index.d.ts +9 -0
  5. package/dist/index.js +6 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/pallet.js +2 -2
  8. package/dist/pallet.js.map +1 -1
  9. package/dist/parcel.js +2 -2
  10. package/dist/parcel.js.map +1 -1
  11. package/dist/picking-station-3d.d.ts +20 -0
  12. package/dist/picking-station-3d.js +162 -0
  13. package/dist/picking-station-3d.js.map +1 -0
  14. package/dist/picking-station.d.ts +50 -0
  15. package/dist/picking-station.js +186 -0
  16. package/dist/picking-station.js.map +1 -0
  17. package/dist/rack-capability.d.ts +11 -0
  18. package/dist/rack-capability.js +25 -0
  19. package/dist/rack-capability.js.map +1 -0
  20. package/dist/rack-grid.d.ts +4 -22
  21. package/dist/rack-grid.js +23 -115
  22. package/dist/rack-grid.js.map +1 -1
  23. package/dist/spot.d.ts +1 -0
  24. package/dist/spot.js +6 -2
  25. package/dist/spot.js.map +1 -1
  26. package/dist/stockpile-3d.d.ts +55 -0
  27. package/dist/stockpile-3d.js +387 -0
  28. package/dist/stockpile-3d.js.map +1 -0
  29. package/dist/stockpile-grid-3d.d.ts +30 -0
  30. package/dist/stockpile-grid-3d.js +301 -0
  31. package/dist/stockpile-grid-3d.js.map +1 -0
  32. package/dist/stockpile-grid.d.ts +85 -0
  33. package/dist/stockpile-grid.js +361 -0
  34. package/dist/stockpile-grid.js.map +1 -0
  35. package/dist/stockpile.d.ts +116 -0
  36. package/dist/stockpile.js +345 -0
  37. package/dist/stockpile.js.map +1 -0
  38. package/dist/storage-rack.d.ts +39 -44
  39. package/dist/storage-rack.js +71 -146
  40. package/dist/storage-rack.js.map +1 -1
  41. package/dist/templates/index.d.ts +80 -0
  42. package/dist/templates/index.js +7 -1
  43. package/dist/templates/index.js.map +1 -1
  44. package/dist/templates/picking-station.d.ts +20 -0
  45. package/dist/templates/picking-station.js +22 -0
  46. package/dist/templates/picking-station.js.map +1 -0
  47. package/dist/templates/stockpile-grid.d.ts +37 -0
  48. package/dist/templates/stockpile-grid.js +38 -0
  49. package/dist/templates/stockpile-grid.js.map +1 -0
  50. package/dist/templates/stockpile.d.ts +29 -0
  51. package/dist/templates/stockpile.js +31 -0
  52. package/dist/templates/stockpile.js.map +1 -0
  53. package/package.json +3 -3
  54. package/src/box.ts +2 -1
  55. package/src/index.ts +14 -0
  56. package/src/pallet.ts +2 -1
  57. package/src/parcel.ts +2 -1
  58. package/src/picking-station-3d.ts +164 -0
  59. package/src/picking-station.ts +220 -0
  60. package/src/rack-capability.ts +26 -0
  61. package/src/rack-grid.ts +24 -108
  62. package/src/spot.ts +15 -1
  63. package/src/stockpile-3d.ts +412 -0
  64. package/src/stockpile-grid-3d.ts +327 -0
  65. package/src/stockpile-grid.ts +408 -0
  66. package/src/stockpile.ts +427 -0
  67. package/src/storage-rack.ts +82 -137
  68. package/src/templates/index.ts +7 -1
  69. package/src/templates/picking-station.ts +23 -0
  70. package/src/templates/stockpile-grid.ts +39 -0
  71. package/src/templates/stockpile.ts +32 -0
  72. package/test/test-rack-capability.ts +51 -0
  73. package/translations/en.json +23 -6
  74. package/translations/ja.json +23 -6
  75. package/translations/ko.json +22 -5
  76. package/translations/ms.json +23 -6
  77. package/translations/zh.json +22 -5
  78. package/tsconfig.tsbuildinfo +1 -1
@@ -9,7 +9,9 @@ import {
9
9
  CellMap,
10
10
  CarrierHolder,
11
11
  Placeable,
12
+ RecordStorage,
12
13
  SlotTarget,
14
+ componentBoundingBox,
13
15
  type AttachFrame,
14
16
  type Alignment,
15
17
  type Heights,
@@ -19,6 +21,7 @@ import {
19
21
  } from '@operato/scene-base'
20
22
 
21
23
  import { StorageRack3D } from './storage-rack-3d.js'
24
+ import { rackAcceptsMoverTool } from './rack-capability.js'
22
25
 
23
26
  /** Rack 컴포넌트 state */
24
27
  export interface StorageRackState extends State {
@@ -164,7 +167,9 @@ function _nextCarrierRefid(): number {
164
167
  */
165
168
  @sceneComponent('storage-rack')
166
169
  export default class Rack
167
- extends CellContainer(CarrierHolder(Placeable(ContainerAbstract)))
170
+ extends RecordStorage<{ cellId: string; [key: string]: any }>()(
171
+ CellContainer(CarrierHolder(Placeable(ContainerAbstract)))
172
+ )
168
173
  implements SlottedHolder
169
174
  {
170
175
  declare state: StorageRackState
@@ -173,19 +178,40 @@ export default class Rack
173
178
  static align: Alignment = 'bottom'
174
179
  static defaultDepth = (h: Heights) => h.ceiling - h.floor
175
180
 
181
+ // ── RecordStorage mixin hook overrides ────────────────────────────────
182
+ // record.cellId 가 slotId (mixin default 의 r.id 가 아닌 r.cellId).
183
+ _recordToSlotId(record: { cellId: string }): string {
184
+ return record.cellId ?? ''
185
+ }
186
+ // 3D rebuild 가 _realObject.rebuildStockMesh (mixin default 와 동일이지만 명시).
187
+ _rebuildVisual(): void {
188
+ ;(this._realObject as any)?.rebuildStockMesh?.()
189
+ }
190
+
176
191
  // Phase Auto-Nav (AN-PR-2) — Obstacle 자격.
177
192
  get isObstacle(): boolean {
178
193
  return (this.state as any)?.isObstacle !== false
179
194
  }
180
195
  obstacleBoundingBox(): { left: number; top: number; width: number; height: number; y?: number; zHeight?: number } | null {
181
- const s: any = this.state
182
- if (typeof s?.left !== 'number') return null
183
- return {
184
- left: s.left, top: s.top,
185
- width: s.width, height: s.height,
186
- y: typeof s.zPos === 'number' ? s.zPos : 0,
187
- zHeight: typeof s.depth === 'number' ? s.depth : 0
188
- }
196
+ // scene-base componentBoundingBox 위임 — rotation 적용된 AABB.
197
+ return componentBoundingBox(this)
198
+ }
199
+
200
+ /**
201
+ * rack *_놓으려는 mover_* 능력을 수용하는가. canAccept(carrier) 는 carrier
202
+ * 타입만 보지만, 이건 *_적재 mover 물리 능력_* 을 본다.
203
+ *
204
+ * rack 선반 적재는 *_높이 도달_* mover (crane / stacker / forklift) 의 몫이다.
205
+ * 평탄 데크 차량(agv-deck)은 바닥 운반 전용 — 선반에 직접 못 올린다. 따라서
206
+ * agv-deck 류는 거부 → transfer planner 가 자동으로 in-port 경유(환승)를 택한다.
207
+ *
208
+ * 거부 목록은 state.blockedTools 로 override (default ['agv-deck']). 향후 선반
209
+ * 높이(level) vs mover liftHeight 비교로 정교화 가능.
210
+ */
211
+ canAcceptFromMover(mover: any): boolean {
212
+ const toolType = mover?.toolType ?? mover?.state?.toolType
213
+ const blocked = (this.state as any)?.blockedTools ?? ['agv-deck']
214
+ return rackAcceptsMoverTool(toolType, blocked)
189
215
  }
190
216
 
191
217
  get nature() {
@@ -309,10 +335,7 @@ export default class Rack
309
335
  // - Place: await crane.place(c, destRack.slotTargetAt('B-0-0'))
310
336
  // SlotTarget.receive → destRack.receiveAt → c dispose + record push
311
337
 
312
- /** state.data record 목록 (읽기 전용 뷰). */
313
- get records(): ReadonlyArray<{ cellId: string; [key: string]: any }> {
314
- return (this.state.data as any) ?? []
315
- }
338
+ // `records` getter RecordStorage mixin 제공.
316
339
 
317
340
  /**
318
341
  * 1-based (bay, row, level) → 0-based cellId 문자열.
@@ -409,7 +432,7 @@ export default class Rack
409
432
  /** cellId 에 carrier 가 있는가 — child carrier 또는 state.data record 어느 쪽이든. */
410
433
  hasCarrierAt(cellId: string): boolean {
411
434
  if (this._carrierChildAt(cellId)) return true
412
- return this.records.some(r => r.cellId === cellId)
435
+ return this.records.some((r: any) => r.cellId === cellId)
413
436
  }
414
437
 
415
438
  /** cellId 매칭되는 rack 의 직접 자식 carrier (operation archetype). */
@@ -507,25 +530,8 @@ export default class Rack
507
530
  return carrier
508
531
  }
509
532
 
510
- /**
511
- * State.data *internal* 갱신 Plan A 의 obtainCarrier / receiveAt 가 사용.
512
- * `setState` 와 달리 *'change' 이벤트 / onchangeData / mapping cascade 를 우회*.
513
- *
514
- * 이유: mapping 시스템이 state.data 변경 시 *자동으로 script fire*. Plan A 의
515
- * setState 가 그 cascade 를 트리거하면 사용자 script 가 *재귀적으로 자기 자신을 호출*
516
- * 하는 회귀 (board 의 의도된 binding 일 수도, 우연일 수도) 발생.
517
- *
518
- * 대신 *직접 _state 갱신 + rebuildStockMesh 직접 호출* — 시각화는 갱신되지만 외부
519
- * mapping 은 fire 안 됨. External (WMS / application setState) 호출은 그대로 setState
520
- * 거치므로 그쪽 mapping 은 정상 동작.
521
- */
522
- private _setDataSilently(newData: any[]): void {
523
- const self = this as any
524
- if (!self._state) self._state = {}
525
- self._state.data = newData
526
- self._cachedState = null // state getter 가 다음 read 때 fresh build
527
- ;(this._realObject as any)?.rebuildStockMesh?.()
528
- }
533
+ // _setDataSilently — RecordStorage mixin 제공 (_rebuildVisual hook 통해
534
+ // rebuildStockMesh 호출). 동등 동작 + cachedState invalidate.
529
535
 
530
536
  /**
531
537
  * cell 이 carrier 를 받을 수 있는가.
@@ -802,88 +808,7 @@ export default class Rack
802
808
  ;(this._realObject as any)?.rebuildStockMesh?.()
803
809
  }
804
810
 
805
- // ── Legend record field 색상 매핑 ────────────────────────────────
806
-
807
- private _legendTarget?: Component
808
-
809
- /**
810
- * Legend 컴포넌트 lookup. 우선순위:
811
- * 1) state.legendTarget id 명시
812
- * 2) scene 전체에서 `type='legend'` 첫 번째 컴포넌트 (자동 발견)
813
- */
814
- get legendTarget(): Component | undefined {
815
- if (this._legendTarget) return this._legendTarget
816
-
817
- const id = this.state.legendTarget
818
- if (id) {
819
- const found = (this.root as any)?.findById?.(id) as Component | undefined
820
- if (found) {
821
- this._legendTarget = found
822
- ;(found as any).on?.('change', this._onLegendChanged, this)
823
- return found
824
- }
825
- }
826
-
827
- // scene-wide auto-discovery
828
- const visit = (node: any): Component | undefined => {
829
- if (!node) return undefined
830
- if (node.state?.type === 'legend') return node as Component
831
- const children = node.components as Component[] | undefined
832
- if (children) {
833
- for (const c of children) {
834
- const r = visit(c)
835
- if (r) return r
836
- }
837
- }
838
- return undefined
839
- }
840
- const found = visit(this.root)
841
- if (found) {
842
- this._legendTarget = found
843
- ;(found as any).on?.('change', this._onLegendChanged, this)
844
- }
845
- return found
846
- }
847
-
848
- private _onLegendChanged = (): void => {
849
- ;(this._realObject as any)?.rebuildStockMesh?.()
850
- }
851
-
852
- /**
853
- * record 의 legend.field 값을 ranges 와 매칭해 색상 해석.
854
- * - `range.value === recordValue` (카테고리 일치)
855
- * - `range.min ≤ Number(v) < range.max` (수치 범위)
856
- * - 매칭 없으면 `defaultColor` 또는 undefined
857
- */
858
- resolveLegendColor(record: any): string | undefined {
859
- const legend = this.legendTarget
860
- if (!legend) return undefined
861
- const status: any = (legend as any).getState?.('status') ?? (legend.state as any)?.status
862
- if (!status) return undefined
863
-
864
- const field = status.field as string | undefined
865
- const ranges = status.ranges as any[] | undefined
866
- if (!field || !Array.isArray(ranges)) return undefined
867
-
868
- const value = record?.[field]
869
- if (value === undefined || value === null) return status.defaultColor
870
-
871
- for (const range of ranges) {
872
- if (!range) continue
873
- if (range.value !== undefined) {
874
- if (range.value === value) return range.color
875
- continue
876
- }
877
- const num = Number(value)
878
- if (!Number.isFinite(num)) continue
879
- const min = range.min !== undefined && range.min !== '' ? Number(range.min) : undefined
880
- const max = range.max !== undefined && range.max !== '' ? Number(range.max) : undefined
881
- const minOk = min === undefined || num >= min
882
- const maxOk = max === undefined || num < max
883
- if (minOk && maxOk) return range.color
884
- }
885
- return status.defaultColor as string | undefined
886
- }
811
+ // legendTarget / _onLegendChanged / resolveLegendColor RecordStorage mixin 제공.
887
812
 
888
813
  // ── Click event — rack-cell-click 발사 ────────────────────────────────────
889
814
  //
@@ -951,30 +876,10 @@ export default class Rack
951
876
  this.trigger('rack-cell-click', payload)
952
877
 
953
878
  // Popup 호출 — 일반 mechanism (Popup 컴포넌트) 활용, anchor 만 클릭된 cell 로 override.
954
- this._invokePopup(cellId, record)
879
+ if (cellId) this._invokePopup(cellId, record ?? { cellId })
955
880
  }
956
881
 
957
- /**
958
- * state.popupRef 가 가리키는 Popup 컴포넌트를 invoke. anchor 를 SlotTarget 으로
959
- * 지정 — SlotTarget._realObject.object3d 가 cellId 위치의 anchor object3d 를
960
- * 가리켜 tether / projectToScreen 정확.
961
- *
962
- * - popupRef 미설정 → no-op (event 만 발사된 상태로 남음)
963
- * - 다른 cell 클릭 시 popup 이 새 anchor 로 "이동" (Popup 의 board 등 설정 유지)
964
- * - frame/empty 영역 클릭 시 호출 안 됨 → popup 그대로 유지
965
- * - 명시적 close 버튼은 popup 자체의 closable 옵션이 처리
966
- */
967
- private _invokePopup(cellId: string | undefined, record: any): void {
968
- const popupRefId = this.state.popupRef
969
- if (!popupRefId || !cellId) return
970
- const popupComp: any = (this.root as any)?.findById?.(popupRefId)
971
- if (!popupComp || typeof popupComp.openPopup !== 'function') {
972
- console.warn(`[storage-rack] popupRef="${popupRefId}" 가 가리키는 컴포넌트 없거나 openPopup 미지원`)
973
- return
974
- }
975
- const anchor = this.slotTargetAt(cellId)
976
- popupComp.openPopup(record ?? { cellId }, { anchor })
977
- }
882
+ // _invokePopup — RecordStorage mixin 제공 (cellId=slotId, record=payload).
978
883
 
979
884
  /**
980
885
  * 클릭 시 framework 의 mouse NDC (이미 InteractionManager 가 set 한 상태) 를 재사용해
@@ -1074,6 +979,46 @@ export default class Rack
1074
979
  return `${bayIdx}-${rowIdx}-${levelIdx}`
1075
980
  }
1076
981
 
982
+ /**
983
+ * Cell-precise approach point — mover 가 *_cell 정면 외부 stand-off_* 까지만.
984
+ *
985
+ * cellId format: `${bay - 1}-${row - 1}-${level - 1}` (0-based).
986
+ *
987
+ * **사용자 의도**: rack 은 *_전면_* 만 포크질 가능. front face = state.top 쪽
988
+ * (작은 z, 3D 의 -Z forward). back face 사용 금지 — forklift 가 rack 후면에
989
+ * 정차하는 동작은 잘못. navigation 의 obstacle avoidance 가 rack 우회 path 계산.
990
+ *
991
+ * forkLen — fork blade tip 부터 mover center 까지 거리. mover 가 그 거리만큼
992
+ * cell face 앞에서 정차 → 정확히 fork blade 가 cell face 정렬.
993
+ *
994
+ * (rotation 지원 V11 — 현재 axis-aligned rack 만, facing default = top.)
995
+ */
996
+ slotApproachWorldPosition(
997
+ slotId: string,
998
+ _fromPos?: { x: number; y: number; z: number }
999
+ ): { x: number; y: number; z: number } {
1000
+ const parts = slotId.split('-')
1001
+ const s: any = this.state
1002
+ const left = s?.left ?? 0
1003
+ const top = s?.top ?? 0
1004
+ const width = s?.width ?? 0
1005
+
1006
+ if (parts.length !== 3) {
1007
+ // fallback — front face center
1008
+ return { x: left + width / 2, y: 0, z: top - (s.depth ?? 100) * 0.5 }
1009
+ }
1010
+ const col = Number(parts[0])
1011
+ const bays = Math.max(1, Math.floor((s.bays as number) ?? 5))
1012
+
1013
+ const cellX = left + (col + 0.5) * (width / bays)
1014
+
1015
+ // fork 진입 거리 — rack.depth 의 절반 (mover 정차 점이 fork blade 가 cell 정면에
1016
+ // 정렬되는 거리). _fromPos 무시 — *_항상 front face_* (사용자 의도: 전면만 포크질).
1017
+ const forkLen = (s.depth ?? 100) * 0.5
1018
+
1019
+ return { x: cellX, y: 0, z: top - forkLen }
1020
+ }
1021
+
1077
1022
  // ── 3D ───────────────────────────────────────────────────────────────────
1078
1023
 
1079
1024
  buildRealObject(): RealObject | undefined {
@@ -10,6 +10,9 @@
10
10
  * - spot — virtual placement marker
11
11
  */
12
12
  import spot from './spot.js'
13
+ import stockpile from './stockpile.js'
14
+ import stockpileGrid from './stockpile-grid.js'
15
+ import pickingStation from './picking-station.js'
13
16
  const pallet = new URL('../../icons/pallet.png', import.meta.url).href
14
17
  const box = new URL('../../icons/box.png', import.meta.url).href
15
18
  const parcel = new URL('../../icons/parcel.png', import.meta.url).href
@@ -196,5 +199,8 @@ export default [
196
199
  ]
197
200
  }
198
201
  },
199
- spot
202
+ spot,
203
+ stockpile,
204
+ stockpileGrid,
205
+ pickingStation
200
206
  ]
@@ -0,0 +1,23 @@
1
+ // Reuse parcel.png as a placeholder icon until a dedicated picking-station icon is drawn.
2
+ const icon = new URL('../../icons/parcel.png', import.meta.url).href
3
+
4
+ export default {
5
+ type: 'picking-station',
6
+ description:
7
+ 'work station — carrier stays for processingTimeMs then becomes idle. anchors AMR↔human handoff',
8
+ group: 'storage',
9
+ icon,
10
+ model: {
11
+ type: 'picking-station',
12
+ top: 200,
13
+ left: 400,
14
+ width: 120,
15
+ height: 100,
16
+ depth: 60,
17
+ rotation: 0,
18
+ fillStyle: '#5a8ab8',
19
+ strokeStyle: '#3d6a8f',
20
+ processingTimeMs: 3000,
21
+ status: 'idle'
22
+ }
23
+ }
@@ -0,0 +1,39 @@
1
+ // Reuse parcel.png as a placeholder icon until a dedicated stockpile-grid icon is drawn.
2
+ const icon = new URL('../../icons/parcel.png', import.meta.url).href
3
+
4
+ export default {
5
+ type: 'stockpile-grid',
6
+ description:
7
+ 'grid of block/floor storage cells — each cell is a stockpile with its own records, capacity, and preset override',
8
+ group: 'storage',
9
+ icon,
10
+ model: {
11
+ type: 'stockpile-grid',
12
+ top: 200,
13
+ left: 400,
14
+ width: 300,
15
+ height: 200,
16
+ depth: 5,
17
+ rotation: 0,
18
+ fillStyle: '#c89c5c',
19
+ strokeStyle: '#7a5a2e',
20
+ cols: 3,
21
+ rows: 2,
22
+ cellWidth: 100,
23
+ cellHeight: 100,
24
+ stackPattern: 'row',
25
+ carrierPreset: 'box',
26
+ carrierWidth: 30,
27
+ carrierHeight: 30,
28
+ carrierDepth: 22,
29
+ carrierGap: 10,
30
+ capacity: 20,
31
+ pickPolicy: 'lifo',
32
+ // 데모 — 일부 cell 에 records 미리 채워서 끌어 놓자 마자 적치 보이게.
33
+ data: [
34
+ { col: 0, row: 0, data: [{ id: 'a1' }, { id: 'a2' }] },
35
+ { col: 1, row: 0, data: [{ id: 'b1' }, { id: 'b2' }, { id: 'b3' }] },
36
+ { col: 2, row: 1, data: [{ id: 'c1' }] }
37
+ ]
38
+ }
39
+ }
@@ -0,0 +1,32 @@
1
+ // Reuse parcel.png as a placeholder icon until a dedicated stockpile icon is drawn.
2
+ const icon = new URL('../../icons/parcel.png', import.meta.url).href
3
+
4
+ export default {
5
+ type: 'stockpile',
6
+ description:
7
+ 'block/floor storage — rectangular footprint with auto-stacked virtual carriers (stackPattern × carrierPreset) driven by state.data records',
8
+ group: 'storage' /* line|shape|textAndMedia|chartAndGauge|table|container|dataSource|3D|facility|storage|conveyance|transport|manufacturing|form|etc */,
9
+ icon,
10
+ model: {
11
+ type: 'stockpile',
12
+ top: 200,
13
+ left: 400,
14
+ width: 200,
15
+ height: 150,
16
+ depth: 5,
17
+ rotation: 0,
18
+ fillStyle: '#c89c5c',
19
+ // 바닥 페인트 라인 의도 — fillStyle 보다 진한 색으로 영역 마킹이 눈에 띄게.
20
+ strokeStyle: '#7a5a2e',
21
+ stackPattern: 'row',
22
+ carrierPreset: 'box',
23
+ carrierWidth: 30,
24
+ carrierHeight: 30,
25
+ carrierDepth: 22,
26
+ carrierGap: 10,
27
+ capacity: 30,
28
+ pickPolicy: 'lifo',
29
+ // 데모용 — 새로 끌어다 놓았을 때 적치된 모습이 즉시 보이도록 record 몇 개.
30
+ data: [{ id: 'p-1' }, { id: 'p-2' }, { id: 'p-3' }]
31
+ }
32
+ }
@@ -0,0 +1,51 @@
1
+ /*
2
+ * Rack 적재 mover 능력 판정 — rackAcceptsMoverTool 순수 로직 검증.
3
+ *
4
+ * storage-rack.canAcceptFromMover 가 이 함수에 위임. "AMR(agv-deck)은 rack 선반
5
+ * 직접 적재 불가 → 거부" 가 정확히 동작해야 transfer planner 가 in-port 경유를
6
+ * 자동 선택한다 (텔레포트 방지의 핵심).
7
+ */
8
+
9
+ import 'should'
10
+ import { rackAcceptsMoverTool } from '../src/rack-capability.js'
11
+
12
+ describe('rackAcceptsMoverTool — 적재 mover 능력 게이트', () => {
13
+ it('★ agv-deck (AMR) 거부 — 평탄데크는 선반 직접 적재 불가 ★', () => {
14
+ rackAcceptsMoverTool('agv-deck').should.be.false()
15
+ })
16
+
17
+ it('gripper (crane) 허용', () => {
18
+ rackAcceptsMoverTool('gripper').should.be.true()
19
+ })
20
+
21
+ it('forklift-fork (지게차) 허용', () => {
22
+ rackAcceptsMoverTool('forklift-fork').should.be.true()
23
+ })
24
+
25
+ it('stacker / shuttle (미니로드) 허용', () => {
26
+ rackAcceptsMoverTool('stacker').should.be.true()
27
+ rackAcceptsMoverTool('shuttle').should.be.true()
28
+ })
29
+
30
+ it('toolType 없음 (능력 미상) → 허용 (보수적 통과)', () => {
31
+ rackAcceptsMoverTool(undefined).should.be.true()
32
+ rackAcceptsMoverTool(null).should.be.true()
33
+ })
34
+
35
+ it('blockedTools override — 빈 배열이면 모두 허용', () => {
36
+ rackAcceptsMoverTool('agv-deck', []).should.be.true()
37
+ })
38
+
39
+ it('blockedTools 명시 — gripper 도 거부 가능', () => {
40
+ rackAcceptsMoverTool('gripper', ['gripper', 'agv-deck']).should.be.false()
41
+ })
42
+
43
+ it('blockedTools 가 배열 아님 (잘못된 값) → 허용', () => {
44
+ rackAcceptsMoverTool('agv-deck', 'invalid' as any).should.be.true()
45
+ })
46
+
47
+ it('default blockedTools 는 agv-deck 만 — 다른 평탄류 영향 없음', () => {
48
+ // 'agv' 같은 변형은 default 에 없으므로 허용 (사용자가 명시해야 거부)
49
+ rackAcceptsMoverTool('agv').should.be.true()
50
+ })
51
+ })
@@ -10,12 +10,10 @@
10
10
  "component.mobile-storage-rack": "mobile storage rack",
11
11
  "component.spot": "spot",
12
12
  "component.generic-container": "container",
13
-
14
13
  "label.tracking-id": "tracking id",
15
14
  "label.material": "material",
16
15
  "label.depth": "depth",
17
16
  "label.fill": "fill",
18
-
19
17
  "label.bays": "bays",
20
18
  "label.levels": "levels",
21
19
  "label.rows": "rows",
@@ -39,8 +37,7 @@
39
37
  "label.stock-scale": "stock scale",
40
38
  "label.legend-target": "legend target",
41
39
  "label.shelf-base-height": "shelf base height",
42
-
43
- "label.status": "status",
40
+ "label.status": "Status",
44
41
  "label.simulate": "simulate",
45
42
  "label.speed": "speed",
46
43
  "label.bound-holders": "bound holders",
@@ -51,7 +48,6 @@
51
48
  "label.fork-extension": "fork extension",
52
49
  "label.fork-lift": "fork lift",
53
50
  "label.pocket-depth": "pocket depth",
54
-
55
51
  "option.idle": "idle",
56
52
  "option.moving": "moving",
57
53
  "option.loading": "loading",
@@ -64,5 +60,26 @@
64
60
  "option.multi": "multi",
65
61
  "option.bulk": "bulk",
66
62
  "option.wood": "wood",
67
- "option.plastic": "plastic"
63
+ "option.plastic": "plastic",
64
+ "component.stockpile": "Stockpile",
65
+ "component.stockpile-grid": "Stockpile Grid",
66
+ "label.stack-pattern": "Stack Pattern",
67
+ "label.carrier-preset": "Carrier Preset",
68
+ "label.carrier-width": "Carrier Width",
69
+ "label.carrier-height": "Carrier Height",
70
+ "label.carrier-depth": "Carrier Depth",
71
+ "label.carrier-gap": "Carrier Gap",
72
+ "label.capacity": "Capacity",
73
+ "label.stack-height-limit": "Stack Height Limit",
74
+ "label.pick-policy": "Pick Policy",
75
+ "label.cols": "Columns",
76
+ "label.cell-width": "Cell Width",
77
+ "label.cell-height": "Cell Height",
78
+ "component.picking-station": "Picking Station",
79
+ "label.processing-time-ms": "Processing Time (ms)",
80
+ "component.container": "container",
81
+ "component.wood pallet": "wood pallet",
82
+ "component.plastic pallet": "plastic pallet",
83
+ "component.wood box": "wood box",
84
+ "component.plastic box": "plastic box"
68
85
  }
@@ -10,12 +10,10 @@
10
10
  "component.mobile-storage-rack": "移動式ラック",
11
11
  "component.spot": "スポット",
12
12
  "component.generic-container": "コンテナ",
13
-
14
13
  "label.tracking-id": "追跡ID",
15
14
  "label.material": "材質",
16
15
  "label.depth": "深さ",
17
16
  "label.fill": "塗りつぶし",
18
-
19
17
  "label.bays": "ベイ数",
20
18
  "label.levels": "段数",
21
19
  "label.rows": "行",
@@ -39,8 +37,7 @@
39
37
  "label.stock-scale": "在庫スケール",
40
38
  "label.legend-target": "凡例対象",
41
39
  "label.shelf-base-height": "棚の基準高さ",
42
-
43
- "label.status": "状態",
40
+ "label.status": "ステータス",
44
41
  "label.simulate": "シミュレーション",
45
42
  "label.speed": "速度",
46
43
  "label.bound-holders": "バインドホルダー",
@@ -51,7 +48,6 @@
51
48
  "label.fork-extension": "フォーク伸縮",
52
49
  "label.fork-lift": "フォーク昇降",
53
50
  "label.pocket-depth": "ポケットの深さ",
54
-
55
51
  "option.idle": "待機",
56
52
  "option.moving": "移動中",
57
53
  "option.loading": "積込中",
@@ -64,5 +60,26 @@
64
60
  "option.multi": "マルチ",
65
61
  "option.bulk": "バルク",
66
62
  "option.wood": "木製",
67
- "option.plastic": "プラスチック"
63
+ "option.plastic": "プラスチック",
64
+ "component.stockpile": "平地保管",
65
+ "component.stockpile-grid": "平地保管グリッド",
66
+ "label.stack-pattern": "積み方",
67
+ "label.carrier-preset": "キャリア種類",
68
+ "label.carrier-width": "キャリア幅",
69
+ "label.carrier-height": "キャリア高さ",
70
+ "label.carrier-depth": "キャリア奥行",
71
+ "label.carrier-gap": "キャリア間隔",
72
+ "label.capacity": "収容量",
73
+ "label.stack-height-limit": "段数制限",
74
+ "label.pick-policy": "取出方針",
75
+ "label.cols": "列数",
76
+ "label.cell-width": "セル幅",
77
+ "label.cell-height": "セル高さ",
78
+ "component.picking-station": "ピッキングステーション",
79
+ "label.processing-time-ms": "処理時間 (ms)",
80
+ "component.container": "コンテナ",
81
+ "component.wood pallet": "木製パレット",
82
+ "component.plastic pallet": "プラスチックパレット",
83
+ "component.wood box": "木製ボックス",
84
+ "component.plastic box": "プラスチックボックス"
68
85
  }
@@ -10,12 +10,10 @@
10
10
  "component.mobile-storage-rack": "이동형 저장 랙",
11
11
  "component.spot": "스팟",
12
12
  "component.generic-container": "컨테이너",
13
-
14
13
  "label.tracking-id": "추적 ID",
15
14
  "label.material": "재질",
16
15
  "label.depth": "깊이",
17
16
  "label.fill": "채움",
18
-
19
17
  "label.bays": "베이 수",
20
18
  "label.levels": "단수",
21
19
  "label.rows": "행",
@@ -39,7 +37,6 @@
39
37
  "label.stock-scale": "재고 크기",
40
38
  "label.legend-target": "범례 대상",
41
39
  "label.shelf-base-height": "선반 시작 높이",
42
-
43
40
  "label.status": "상태",
44
41
  "label.simulate": "시뮬레이션",
45
42
  "label.speed": "속도",
@@ -51,7 +48,6 @@
51
48
  "label.fork-extension": "포크 신축",
52
49
  "label.fork-lift": "포크 들기",
53
50
  "label.pocket-depth": "포켓 깊이",
54
-
55
51
  "option.idle": "대기",
56
52
  "option.moving": "이동중",
57
53
  "option.loading": "적재중",
@@ -64,5 +60,26 @@
64
60
  "option.multi": "다중",
65
61
  "option.bulk": "벌크",
66
62
  "option.wood": "나무",
67
- "option.plastic": "플라스틱"
63
+ "option.plastic": "플라스틱",
64
+ "component.stockpile": "평치",
65
+ "component.stockpile-grid": "평치 그리드",
66
+ "label.stack-pattern": "적치 패턴",
67
+ "label.carrier-preset": "캐리어 종류",
68
+ "label.carrier-width": "캐리어 너비",
69
+ "label.carrier-height": "캐리어 높이",
70
+ "label.carrier-depth": "캐리어 깊이",
71
+ "label.carrier-gap": "캐리어 간격",
72
+ "label.capacity": "수용량",
73
+ "label.stack-height-limit": "적치 단수 제한",
74
+ "label.pick-policy": "출고 정책",
75
+ "label.cols": "열 수",
76
+ "label.cell-width": "셀 너비",
77
+ "label.cell-height": "셀 높이",
78
+ "component.picking-station": "피킹 스테이션",
79
+ "label.processing-time-ms": "처리 시간 (ms)",
80
+ "component.container": "컨테이너",
81
+ "component.wood pallet": "목재 팔레트",
82
+ "component.plastic pallet": "플라스틱 팔레트",
83
+ "component.wood box": "목재 박스",
84
+ "component.plastic box": "플라스틱 박스"
68
85
  }