@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/CHANGELOG.md +29 -0
- package/dist/box.js +8 -2
- package/dist/box.js.map +1 -1
- package/dist/pallet.js +2 -2
- package/dist/pallet.js.map +1 -1
- package/dist/parcel.js +8 -2
- package/dist/parcel.js.map +1 -1
- package/dist/picking-station.d.ts +10 -16
- package/dist/picking-station.js +20 -46
- package/dist/picking-station.js.map +1 -1
- package/dist/rack-grid.d.ts +4 -22
- package/dist/rack-grid.js +21 -106
- package/dist/rack-grid.js.map +1 -1
- package/dist/spot.d.ts +2 -19
- package/dist/spot.js +4 -62
- package/dist/spot.js.map +1 -1
- package/dist/stockpile-grid.d.ts +2 -5
- package/dist/stockpile-grid.js +18 -86
- package/dist/stockpile-grid.js.map +1 -1
- package/dist/stockpile.d.ts +5 -22
- package/dist/stockpile.js +21 -115
- package/dist/stockpile.js.map +1 -1
- package/dist/storage-rack.d.ts +27 -44
- package/dist/storage-rack.js +52 -137
- package/dist/storage-rack.js.map +1 -1
- package/package.json +3 -3
- package/src/box.ts +8 -1
- package/src/pallet.ts +2 -1
- package/src/parcel.ts +8 -1
- package/src/picking-station.ts +26 -49
- package/src/rack-grid.ts +21 -100
- package/src/spot.ts +11 -59
- package/src/stockpile-grid.ts +21 -69
- package/src/stockpile.ts +23 -104
- package/src/storage-rack.ts +61 -129
- package/translations/en.json +7 -1
- package/translations/ja.json +7 -1
- package/translations/ko.json +7 -1
- package/translations/ms.json +7 -1
- package/translations/zh.json +7 -1
- package/tsconfig.tsbuildinfo +1 -1
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
|
|
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
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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.
|
|
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
|
-
*
|
|
340
|
-
*
|
|
338
|
+
*
|
|
339
|
+
* RecordStorage mixin 의 `_invokePopup(slotId, payload)` 를 활용. 단일 slot
|
|
340
|
+
* 시맨틱이 record/pile 두 모드라 mixin 위 wrapper 로 dispatch.
|
|
341
341
|
*/
|
|
342
|
-
private
|
|
343
|
-
|
|
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
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
//
|
|
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)
|
package/src/storage-rack.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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 {
|
package/translations/en.json
CHANGED
|
@@ -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
|
}
|
package/translations/ja.json
CHANGED
|
@@ -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
|
}
|
package/translations/ko.json
CHANGED
|
@@ -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
|
}
|
package/translations/ms.json
CHANGED
|
@@ -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
|
}
|
package/translations/zh.json
CHANGED
|
@@ -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
|
}
|