@operato/scene-storage 10.0.0-beta.50 → 10.0.0-beta.54

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.
package/src/stockpile.ts CHANGED
@@ -19,6 +19,7 @@ import type { State, Material3D } from '@hatiolab/things-scene'
19
19
  import {
20
20
  CarrierHolder,
21
21
  Placeable,
22
+ RecordStorage,
22
23
  SlotTarget,
23
24
  type AttachFrame,
24
25
  type Alignment,
@@ -103,7 +104,9 @@ const NATURE: ComponentNature = {
103
104
  }
104
105
 
105
106
  @sceneComponent('stockpile')
106
- export default class Stockpile extends CarrierHolder(Placeable(ContainerAbstract)) {
107
+ export default class Stockpile extends RecordStorage<StockpileRecord>()(
108
+ CarrierHolder(Placeable(ContainerAbstract))
109
+ ) {
107
110
  declare state: StockpileState
108
111
  declare _realObject?: Stockpile3D
109
112
 
@@ -114,15 +117,11 @@ export default class Stockpile extends CarrierHolder(Placeable(ContainerAbstract
114
117
  get nature(): ComponentNature { return NATURE }
115
118
  get anchors() { return [] }
116
119
 
117
- // ── records (state.data 읽기 전용 뷰, storage-rack 패턴) ─────────────
118
- get records(): ReadonlyArray<StockpileRecord> {
119
- return (this.state.data as StockpileRecord[]) ?? []
120
- }
121
- get inventoryCount(): number {
122
- return this.records.length
123
- }
120
+ // ── records / inventoryCount: RecordStorage mixin 제공.
124
121
 
125
- // ── SlottedHolder duck-type — 단일 slot ('pile') ────────────────────────
122
+ // ── SlottedHolder duck-type override — 단일 slot ('pile') ───────────────
123
+ // mixin default 는 record 마다 slotId. Stockpile 은 *_단일 슬롯_* 시맨틱이라
124
+ // 자기 구현 유지.
126
125
  slotIds(): ReadonlyArray<string> { return [SLOT_ID] }
127
126
 
128
127
  hasCarrierAt(slotId: string): boolean {
@@ -260,9 +259,9 @@ export default class Stockpile extends CarrierHolder(Placeable(ContainerAbstract
260
259
  return (this as any)._realObject?.getAttachFrame?.(slotId)
261
260
  }
262
261
 
263
- /** state.data 갱신 + 3D 즉시 재배치. */
264
- private _setDataSilently(records: StockpileRecord[]): void {
265
- ;(this.state as any).data = records
262
+ // _setDataSilently RecordStorage mixin 제공 다만 mixin 은 _rebuildVisual
263
+ // 호출. Stockpile 의 3D 갱신은 update() 라서 hook override.
264
+ _rebuildVisual(): void {
266
265
  this._realObject?.update?.()
267
266
  }
268
267
 
@@ -329,38 +328,32 @@ export default class Stockpile extends CarrierHolder(Placeable(ContainerAbstract
329
328
  // hit.object 가 carrier mesh 면 userData.recordId 보유 → 그 stock 의 popup.
330
329
  // pad / 기타면 stockpile 전체 popup.
331
330
  const recordId = hit.object?.userData?.recordId as string | undefined
332
- this._invokePopup(typeof recordId === 'string' ? recordId : undefined)
331
+ this._dispatchStockpilePopup(typeof recordId === 'string' ? recordId : undefined)
333
332
  }
334
333
 
335
334
  /**
336
335
  * state.popupRef 가 가리키는 Popup 컴포넌트를 invoke.
337
336
  * - recordId 명시 → 그 record 의 anchor = mesh. payload = 해당 record.
338
337
  * - 미명시 (pad 클릭) → 'pile' anchor (pad). payload = 전체 inventory.
339
- * anchor 는 SlotTarget — Popup 이 anchor.holder.getSlotAttachObject3d(anchor.slotId)
340
- * tether 위치 잡음 (storage-rack 동일 패턴).
338
+ *
339
+ * RecordStorage mixin `_invokePopup(slotId, payload)` 활용. 단일 slot
340
+ * 시맨틱이 record/pile 두 모드라 mixin 위 wrapper 로 dispatch.
341
341
  */
342
- private _invokePopup(recordId?: string): void {
343
- const popupRefId = this.state.popupRef
344
- if (!popupRefId) return
345
- const popupComp: any = (this as any).root?.findById?.(popupRefId)
346
- if (!popupComp || typeof popupComp.openPopup !== 'function') {
347
- console.warn(`[stockpile] popupRef="${popupRefId}" 가 가리키는 컴포넌트 없거나 openPopup 미지원`)
348
- return
349
- }
342
+ private _dispatchStockpilePopup(recordId?: string): void {
343
+ if (!this.state.popupRef) return
350
344
  if (recordId) {
351
- const record = this.records.find(r => r.id === recordId) ?? { id: recordId }
352
- const anchor = this.slotTargetAt(recordId)
353
- popupComp.openPopup(record, { anchor })
345
+ const record = (this.records as ReadonlyArray<StockpileRecord>)
346
+ .find(r => r.id === recordId) ?? { id: recordId }
347
+ this._invokePopup(recordId, record)
354
348
  } else {
355
- const anchor = this.slotTargetAt(SLOT_ID)
356
- popupComp.openPopup({
349
+ this._invokePopup(SLOT_ID, {
357
350
  componentId: (this.state as any).id,
358
351
  records: this.records,
359
352
  inventoryCount: this.inventoryCount,
360
353
  capacity: this.state.capacity,
361
354
  carrierPreset: this.state.carrierPreset,
362
355
  stackPattern: this.state.stackPattern
363
- }, { anchor })
356
+ })
364
357
  }
365
358
  }
366
359
 
@@ -412,81 +405,7 @@ export default class Stockpile extends CarrierHolder(Placeable(ContainerAbstract
412
405
  return undefined
413
406
  }
414
407
 
415
- // ── Legend record field 색상 매핑 (StorageRack 동일 패턴) ───────
416
- private _legendTarget?: Component
417
-
418
- /**
419
- * Legend 컴포넌트 lookup. 우선:
420
- * 1) state.legendTarget id 명시
421
- * 2) scene 전체에서 type='legend' 첫 번째 (자동 발견)
422
- */
423
- get legendTarget(): Component | undefined {
424
- if (this._legendTarget) return this._legendTarget
425
- const id = this.state.legendTarget
426
- if (id) {
427
- const found = ((this as any).root)?.findById?.(id) as Component | undefined
428
- if (found) {
429
- this._legendTarget = found
430
- ;(found as any).on?.('change', this._onLegendChanged, this)
431
- return found
432
- }
433
- }
434
- const visit = (node: any): Component | undefined => {
435
- if (!node) return undefined
436
- if (node.state?.type === 'legend') return node as Component
437
- const children = node.components as Component[] | undefined
438
- if (children) for (const c of children) {
439
- const r = visit(c)
440
- if (r) return r
441
- }
442
- return undefined
443
- }
444
- const found = visit((this as any).root)
445
- if (found) {
446
- this._legendTarget = found
447
- ;(found as any).on?.('change', this._onLegendChanged, this)
448
- }
449
- return found
450
- }
451
-
452
- private _onLegendChanged = (): void => {
453
- ;(this._realObject as any)?.update?.()
454
- }
455
-
456
- /**
457
- * record 의 legend.field 값을 ranges 와 매칭해 색상 해석.
458
- * - range.value === recordValue (카테고리)
459
- * - range.min ≤ Number(v) < range.max (수치)
460
- * - 매칭 없으면 defaultColor
461
- */
462
- resolveLegendColor(record: any): string | undefined {
463
- const legend = this.legendTarget
464
- if (!legend) return undefined
465
- const status: any = (legend as any).getState?.('status') ?? (legend.state as any)?.status
466
- if (!status) return undefined
467
- const field = status.field as string | undefined
468
- const ranges = status.ranges as any[] | undefined
469
- if (!field || !Array.isArray(ranges)) return undefined
470
-
471
- const value = record?.[field]
472
- if (value === undefined || value === null) return status.defaultColor
473
-
474
- for (const range of ranges) {
475
- if (!range) continue
476
- if (range.value !== undefined) {
477
- if (range.value === value) return range.color
478
- continue
479
- }
480
- const num = Number(value)
481
- if (!Number.isFinite(num)) continue
482
- const min = range.min !== undefined && range.min !== '' ? Number(range.min) : undefined
483
- const max = range.max !== undefined && range.max !== '' ? Number(range.max) : undefined
484
- const minOk = min === undefined || num >= min
485
- const maxOk = max === undefined || num < max
486
- if (minOk && maxOk) return range.color
487
- }
488
- return status.defaultColor as string | undefined
489
- }
408
+ // legendTarget / _onLegendChanged / resolveLegendColor RecordStorage mixin 제공.
490
409
 
491
410
  buildRealObject(): RealObject | undefined {
492
411
  return new Stockpile3D(this)
@@ -9,6 +9,7 @@ import {
9
9
  CellMap,
10
10
  CarrierHolder,
11
11
  Placeable,
12
+ RecordStorage,
12
13
  SlotTarget,
13
14
  componentBoundingBox,
14
15
  type AttachFrame,
@@ -166,7 +167,9 @@ function _nextCarrierRefid(): number {
166
167
  */
167
168
  @sceneComponent('storage-rack')
168
169
  export default class Rack
169
- extends CellContainer(CarrierHolder(Placeable(ContainerAbstract)))
170
+ extends RecordStorage<{ cellId: string; [key: string]: any }>()(
171
+ CellContainer(CarrierHolder(Placeable(ContainerAbstract)))
172
+ )
170
173
  implements SlottedHolder
171
174
  {
172
175
  declare state: StorageRackState
@@ -175,6 +178,16 @@ export default class Rack
175
178
  static align: Alignment = 'bottom'
176
179
  static defaultDepth = (h: Heights) => h.ceiling - h.floor
177
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
+
178
191
  // Phase Auto-Nav (AN-PR-2) — Obstacle 자격.
179
192
  get isObstacle(): boolean {
180
193
  return (this.state as any)?.isObstacle !== false
@@ -322,10 +335,7 @@ export default class Rack
322
335
  // - Place: await crane.place(c, destRack.slotTargetAt('B-0-0'))
323
336
  // SlotTarget.receive → destRack.receiveAt → c dispose + record push
324
337
 
325
- /** state.data record 목록 (읽기 전용 뷰). */
326
- get records(): ReadonlyArray<{ cellId: string; [key: string]: any }> {
327
- return (this.state.data as any) ?? []
328
- }
338
+ // `records` getter RecordStorage mixin 제공.
329
339
 
330
340
  /**
331
341
  * 1-based (bay, row, level) → 0-based cellId 문자열.
@@ -422,7 +432,7 @@ export default class Rack
422
432
  /** cellId 에 carrier 가 있는가 — child carrier 또는 state.data record 어느 쪽이든. */
423
433
  hasCarrierAt(cellId: string): boolean {
424
434
  if (this._carrierChildAt(cellId)) return true
425
- return this.records.some(r => r.cellId === cellId)
435
+ return this.records.some((r: any) => r.cellId === cellId)
426
436
  }
427
437
 
428
438
  /** cellId 매칭되는 rack 의 직접 자식 carrier (operation archetype). */
@@ -520,25 +530,8 @@ export default class Rack
520
530
  return carrier
521
531
  }
522
532
 
523
- /**
524
- * State.data *internal* 갱신 Plan A 의 obtainCarrier / receiveAt 가 사용.
525
- * `setState` 와 달리 *'change' 이벤트 / onchangeData / mapping cascade 를 우회*.
526
- *
527
- * 이유: mapping 시스템이 state.data 변경 시 *자동으로 script fire*. Plan A 의
528
- * setState 가 그 cascade 를 트리거하면 사용자 script 가 *재귀적으로 자기 자신을 호출*
529
- * 하는 회귀 (board 의 의도된 binding 일 수도, 우연일 수도) 발생.
530
- *
531
- * 대신 *직접 _state 갱신 + rebuildStockMesh 직접 호출* — 시각화는 갱신되지만 외부
532
- * mapping 은 fire 안 됨. External (WMS / application setState) 호출은 그대로 setState
533
- * 거치므로 그쪽 mapping 은 정상 동작.
534
- */
535
- private _setDataSilently(newData: any[]): void {
536
- const self = this as any
537
- if (!self._state) self._state = {}
538
- self._state.data = newData
539
- self._cachedState = null // state getter 가 다음 read 때 fresh build
540
- ;(this._realObject as any)?.rebuildStockMesh?.()
541
- }
533
+ // _setDataSilently — RecordStorage mixin 제공 (_rebuildVisual hook 통해
534
+ // rebuildStockMesh 호출). 동등 동작 + cachedState invalidate.
542
535
 
543
536
  /**
544
537
  * cell 이 carrier 를 받을 수 있는가.
@@ -815,88 +808,7 @@ export default class Rack
815
808
  ;(this._realObject as any)?.rebuildStockMesh?.()
816
809
  }
817
810
 
818
- // ── Legend record field 색상 매핑 ────────────────────────────────
819
-
820
- private _legendTarget?: Component
821
-
822
- /**
823
- * Legend 컴포넌트 lookup. 우선순위:
824
- * 1) state.legendTarget id 명시
825
- * 2) scene 전체에서 `type='legend'` 첫 번째 컴포넌트 (자동 발견)
826
- */
827
- get legendTarget(): Component | undefined {
828
- if (this._legendTarget) return this._legendTarget
829
-
830
- const id = this.state.legendTarget
831
- if (id) {
832
- const found = (this.root as any)?.findById?.(id) as Component | undefined
833
- if (found) {
834
- this._legendTarget = found
835
- ;(found as any).on?.('change', this._onLegendChanged, this)
836
- return found
837
- }
838
- }
839
-
840
- // scene-wide auto-discovery
841
- const visit = (node: any): Component | undefined => {
842
- if (!node) return undefined
843
- if (node.state?.type === 'legend') return node as Component
844
- const children = node.components as Component[] | undefined
845
- if (children) {
846
- for (const c of children) {
847
- const r = visit(c)
848
- if (r) return r
849
- }
850
- }
851
- return undefined
852
- }
853
- const found = visit(this.root)
854
- if (found) {
855
- this._legendTarget = found
856
- ;(found as any).on?.('change', this._onLegendChanged, this)
857
- }
858
- return found
859
- }
860
-
861
- private _onLegendChanged = (): void => {
862
- ;(this._realObject as any)?.rebuildStockMesh?.()
863
- }
864
-
865
- /**
866
- * record 의 legend.field 값을 ranges 와 매칭해 색상 해석.
867
- * - `range.value === recordValue` (카테고리 일치)
868
- * - `range.min ≤ Number(v) < range.max` (수치 범위)
869
- * - 매칭 없으면 `defaultColor` 또는 undefined
870
- */
871
- resolveLegendColor(record: any): string | undefined {
872
- const legend = this.legendTarget
873
- if (!legend) return undefined
874
- const status: any = (legend as any).getState?.('status') ?? (legend.state as any)?.status
875
- if (!status) return undefined
876
-
877
- const field = status.field as string | undefined
878
- const ranges = status.ranges as any[] | undefined
879
- if (!field || !Array.isArray(ranges)) return undefined
880
-
881
- const value = record?.[field]
882
- if (value === undefined || value === null) return status.defaultColor
883
-
884
- for (const range of ranges) {
885
- if (!range) continue
886
- if (range.value !== undefined) {
887
- if (range.value === value) return range.color
888
- continue
889
- }
890
- const num = Number(value)
891
- if (!Number.isFinite(num)) continue
892
- const min = range.min !== undefined && range.min !== '' ? Number(range.min) : undefined
893
- const max = range.max !== undefined && range.max !== '' ? Number(range.max) : undefined
894
- const minOk = min === undefined || num >= min
895
- const maxOk = max === undefined || num < max
896
- if (minOk && maxOk) return range.color
897
- }
898
- return status.defaultColor as string | undefined
899
- }
811
+ // legendTarget / _onLegendChanged / resolveLegendColor RecordStorage mixin 제공.
900
812
 
901
813
  // ── Click event — rack-cell-click 발사 ────────────────────────────────────
902
814
  //
@@ -964,30 +876,10 @@ export default class Rack
964
876
  this.trigger('rack-cell-click', payload)
965
877
 
966
878
  // Popup 호출 — 일반 mechanism (Popup 컴포넌트) 활용, anchor 만 클릭된 cell 로 override.
967
- this._invokePopup(cellId, record)
879
+ if (cellId) this._invokePopup(cellId, record ?? { cellId })
968
880
  }
969
881
 
970
- /**
971
- * state.popupRef 가 가리키는 Popup 컴포넌트를 invoke. anchor 를 SlotTarget 으로
972
- * 지정 — SlotTarget._realObject.object3d 가 cellId 위치의 anchor object3d 를
973
- * 가리켜 tether / projectToScreen 정확.
974
- *
975
- * - popupRef 미설정 → no-op (event 만 발사된 상태로 남음)
976
- * - 다른 cell 클릭 시 popup 이 새 anchor 로 "이동" (Popup 의 board 등 설정 유지)
977
- * - frame/empty 영역 클릭 시 호출 안 됨 → popup 그대로 유지
978
- * - 명시적 close 버튼은 popup 자체의 closable 옵션이 처리
979
- */
980
- private _invokePopup(cellId: string | undefined, record: any): void {
981
- const popupRefId = this.state.popupRef
982
- if (!popupRefId || !cellId) return
983
- const popupComp: any = (this.root as any)?.findById?.(popupRefId)
984
- if (!popupComp || typeof popupComp.openPopup !== 'function') {
985
- console.warn(`[storage-rack] popupRef="${popupRefId}" 가 가리키는 컴포넌트 없거나 openPopup 미지원`)
986
- return
987
- }
988
- const anchor = this.slotTargetAt(cellId)
989
- popupComp.openPopup(record ?? { cellId }, { anchor })
990
- }
882
+ // _invokePopup — RecordStorage mixin 제공 (cellId=slotId, record=payload).
991
883
 
992
884
  /**
993
885
  * 클릭 시 framework 의 mouse NDC (이미 InteractionManager 가 set 한 상태) 를 재사용해
@@ -1087,6 +979,46 @@ export default class Rack
1087
979
  return `${bayIdx}-${rowIdx}-${levelIdx}`
1088
980
  }
1089
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
+
1090
1022
  // ── 3D ───────────────────────────────────────────────────────────────────
1091
1023
 
1092
1024
  buildRealObject(): RealObject | undefined {
@@ -10,6 +10,7 @@
10
10
  "component.mobile-storage-rack": "mobile storage rack",
11
11
  "component.spot": "spot",
12
12
  "component.generic-container": "container",
13
+ "label.data": "Data (JSON)",
13
14
  "label.tracking-id": "tracking id",
14
15
  "label.material": "material",
15
16
  "label.depth": "depth",
@@ -76,5 +77,10 @@
76
77
  "label.cell-width": "Cell Width",
77
78
  "label.cell-height": "Cell Height",
78
79
  "component.picking-station": "Picking Station",
79
- "label.processing-time-ms": "Processing Time (ms)"
80
+ "label.processing-time-ms": "Processing Time (ms)",
81
+ "component.container": "container",
82
+ "component.wood pallet": "wood pallet",
83
+ "component.plastic pallet": "plastic pallet",
84
+ "component.wood box": "wood box",
85
+ "component.plastic box": "plastic box"
80
86
  }
@@ -10,6 +10,7 @@
10
10
  "component.mobile-storage-rack": "移動式ラック",
11
11
  "component.spot": "スポット",
12
12
  "component.generic-container": "コンテナ",
13
+ "label.data": "データ (JSON)",
13
14
  "label.tracking-id": "追跡ID",
14
15
  "label.material": "材質",
15
16
  "label.depth": "深さ",
@@ -76,5 +77,10 @@
76
77
  "label.cell-width": "セル幅",
77
78
  "label.cell-height": "セル高さ",
78
79
  "component.picking-station": "ピッキングステーション",
79
- "label.processing-time-ms": "処理時間 (ms)"
80
+ "label.processing-time-ms": "処理時間 (ms)",
81
+ "component.container": "コンテナ",
82
+ "component.wood pallet": "木製パレット",
83
+ "component.plastic pallet": "プラスチックパレット",
84
+ "component.wood box": "木製ボックス",
85
+ "component.plastic box": "プラスチックボックス"
80
86
  }
@@ -10,6 +10,7 @@
10
10
  "component.mobile-storage-rack": "이동형 저장 랙",
11
11
  "component.spot": "스팟",
12
12
  "component.generic-container": "컨테이너",
13
+ "label.data": "데이터 (JSON)",
13
14
  "label.tracking-id": "추적 ID",
14
15
  "label.material": "재질",
15
16
  "label.depth": "깊이",
@@ -76,5 +77,10 @@
76
77
  "label.cell-width": "셀 너비",
77
78
  "label.cell-height": "셀 높이",
78
79
  "component.picking-station": "피킹 스테이션",
79
- "label.processing-time-ms": "처리 시간 (ms)"
80
+ "label.processing-time-ms": "처리 시간 (ms)",
81
+ "component.container": "컨테이너",
82
+ "component.wood pallet": "목재 팔레트",
83
+ "component.plastic pallet": "플라스틱 팔레트",
84
+ "component.wood box": "목재 박스",
85
+ "component.plastic box": "플라스틱 박스"
80
86
  }
@@ -10,6 +10,7 @@
10
10
  "component.mobile-storage-rack": "rak mudah alih",
11
11
  "component.spot": "titik",
12
12
  "component.generic-container": "bekas",
13
+ "label.data": "Data (JSON)",
13
14
  "label.tracking-id": "ID penjejakan",
14
15
  "label.material": "bahan",
15
16
  "label.depth": "kedalaman",
@@ -76,5 +77,10 @@
76
77
  "label.cell-width": "Lebar Sel",
77
78
  "label.cell-height": "Tinggi Sel",
78
79
  "component.picking-station": "Stesen Pengambilan",
79
- "label.processing-time-ms": "Masa Proses (ms)"
80
+ "label.processing-time-ms": "Masa Proses (ms)",
81
+ "component.container": "kontena",
82
+ "component.wood pallet": "palet kayu",
83
+ "component.plastic pallet": "palet plastik",
84
+ "component.wood box": "kotak kayu",
85
+ "component.plastic box": "kotak plastik"
80
86
  }
@@ -10,6 +10,7 @@
10
10
  "component.mobile-storage-rack": "移动货架",
11
11
  "component.spot": "位点",
12
12
  "component.generic-container": "容器",
13
+ "label.data": "数据 (JSON)",
13
14
  "label.tracking-id": "追踪ID",
14
15
  "label.material": "材质",
15
16
  "label.depth": "深度",
@@ -76,5 +77,10 @@
76
77
  "label.cell-width": "单元宽度",
77
78
  "label.cell-height": "单元高度",
78
79
  "component.picking-station": "拣选工作站",
79
- "label.processing-time-ms": "处理时间 (ms)"
80
+ "label.processing-time-ms": "处理时间 (ms)",
81
+ "component.container": "容器",
82
+ "component.wood pallet": "木质托盘",
83
+ "component.plastic pallet": "塑料托盘",
84
+ "component.wood box": "木质箱",
85
+ "component.plastic box": "塑料箱"
80
86
  }