@operato/scene-storage 10.0.0-beta.50 → 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.
- package/CHANGELOG.md +20 -0
- package/dist/box.js +2 -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 +2 -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 +2 -1
- package/src/pallet.ts +2 -1
- package/src/parcel.ts +2 -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 +6 -1
- package/translations/ja.json +6 -1
- package/translations/ko.json +6 -1
- package/translations/ms.json +6 -1
- package/translations/zh.json +6 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/box.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import type { State, Material3D, PickupFrame, PoseSerialized } from '@hatiolab/things-scene'
|
|
14
14
|
import {
|
|
15
15
|
Carriable,
|
|
16
|
+
Identifiable,
|
|
16
17
|
Legendable,
|
|
17
18
|
Placeable,
|
|
18
19
|
type Alignment,
|
|
@@ -81,7 +82,7 @@ const NATURE: ComponentNature = {
|
|
|
81
82
|
* scene-tree). If a future use case needs nested boxes, extend Container.
|
|
82
83
|
*/
|
|
83
84
|
@sceneComponent('box')
|
|
84
|
-
export default class Box extends Carriable(Legendable(Placeable(RectPath(Shape)))) {
|
|
85
|
+
export default class Box extends Identifiable(Carriable(Legendable(Placeable(RectPath(Shape))))) {
|
|
85
86
|
declare state: BoxState
|
|
86
87
|
|
|
87
88
|
static legends: Record<string, LegendBinding> = {
|
package/src/pallet.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import type { State, Material3D, PickupFrame, PoseSerialized } from '@hatiolab/things-scene'
|
|
16
16
|
import {
|
|
17
17
|
Carriable,
|
|
18
|
+
Identifiable,
|
|
18
19
|
Legendable,
|
|
19
20
|
Placeable,
|
|
20
21
|
type Alignment,
|
|
@@ -122,7 +123,7 @@ const NATURE: ComponentNature = {
|
|
|
122
123
|
* detection.
|
|
123
124
|
*/
|
|
124
125
|
@sceneComponent('pallet')
|
|
125
|
-
export default class Pallet extends Carriable(Legendable(Placeable(ContainerAbstract))) {
|
|
126
|
+
export default class Pallet extends Identifiable(Carriable(Legendable(Placeable(ContainerAbstract)))) {
|
|
126
127
|
declare state: PalletState
|
|
127
128
|
|
|
128
129
|
static legends: Record<string, LegendBinding> = {
|
package/src/parcel.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import type { State, Material3D, PickupFrame, PoseSerialized } from '@hatiolab/things-scene'
|
|
14
14
|
import {
|
|
15
15
|
Carriable,
|
|
16
|
+
Identifiable,
|
|
16
17
|
Placeable,
|
|
17
18
|
type Alignment,
|
|
18
19
|
type PlacementArchetype
|
|
@@ -62,7 +63,7 @@ const NATURE: ComponentNature = {
|
|
|
62
63
|
* inspected indicators would add a status legend then.
|
|
63
64
|
*/
|
|
64
65
|
@sceneComponent('parcel')
|
|
65
|
-
export default class Parcel extends Carriable(Placeable(RectPath(Shape))) {
|
|
66
|
+
export default class Parcel extends Identifiable(Carriable(Placeable(RectPath(Shape)))) {
|
|
66
67
|
declare state: ParcelState
|
|
67
68
|
|
|
68
69
|
static placement: PlacementArchetype = 'operation'
|
package/src/picking-station.ts
CHANGED
|
@@ -14,11 +14,12 @@ import type { State, Material3D } from '@hatiolab/things-scene'
|
|
|
14
14
|
import {
|
|
15
15
|
CarrierHolder,
|
|
16
16
|
Placeable,
|
|
17
|
-
|
|
17
|
+
SingleSlotHolder,
|
|
18
18
|
type AttachFrame,
|
|
19
19
|
type Alignment,
|
|
20
20
|
type Heights,
|
|
21
|
-
type PlacementArchetype
|
|
21
|
+
type PlacementArchetype,
|
|
22
|
+
type ProcessStep
|
|
22
23
|
} from '@operato/scene-base'
|
|
23
24
|
|
|
24
25
|
import { PickingStation3D } from './picking-station-3d.js'
|
|
@@ -52,7 +53,10 @@ const NATURE: ComponentNature = {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
@sceneComponent('picking-station')
|
|
55
|
-
export default class PickingStation
|
|
56
|
+
export default class PickingStation
|
|
57
|
+
extends SingleSlotHolder()(CarrierHolder(Placeable(ContainerAbstract)))
|
|
58
|
+
implements ProcessStep
|
|
59
|
+
{
|
|
56
60
|
declare state: PickingStationState
|
|
57
61
|
declare _realObject?: PickingStation3D
|
|
58
62
|
|
|
@@ -63,57 +67,30 @@ export default class PickingStation extends CarrierHolder(Placeable(ContainerAbs
|
|
|
63
67
|
get nature(): ComponentNature { return NATURE }
|
|
64
68
|
get anchors() { return [] }
|
|
65
69
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
hasCarrierAt(slotId: string): boolean {
|
|
70
|
-
return slotId === SLOT_ID &&
|
|
71
|
-
((this as any).components ?? []).some((c: any) => c?.isCarriable)
|
|
72
|
-
}
|
|
73
|
-
canReceiveAt(slotId: string, _carrier?: Component): boolean {
|
|
74
|
-
return slotId === SLOT_ID && !this.hasCarrierAt(slotId)
|
|
75
|
-
}
|
|
76
|
-
occupiedSlotIds(): ReadonlyArray<string> {
|
|
77
|
-
return this.hasCarrierAt(SLOT_ID) ? [SLOT_ID] : []
|
|
78
|
-
}
|
|
79
|
-
emptySlotIds(): ReadonlyArray<string> {
|
|
80
|
-
return this.hasCarrierAt(SLOT_ID) ? [] : [SLOT_ID]
|
|
81
|
-
}
|
|
82
|
-
obtainCarrier(slotId: string): Component | null {
|
|
83
|
-
if (slotId !== SLOT_ID) return null
|
|
84
|
-
return ((this as any).components ?? []).find((c: any) => c?.isCarriable) ?? null
|
|
85
|
-
}
|
|
70
|
+
// SingleSlotHolder hook overrides ───────────────────────────────────────────
|
|
71
|
+
_singleSlotId() { return SLOT_ID }
|
|
86
72
|
|
|
87
73
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
74
|
+
* SlottedHolder duck (slotIds / hasCarrierAt / canReceiveAt / occupiedSlotIds
|
|
75
|
+
* / emptySlotIds / obtainCarrier / receiveAt / accept / receive / slotTargetAt
|
|
76
|
+
* / getSlotAttachObject3d) — SingleSlotHolder mixin 제공.
|
|
90
77
|
*
|
|
91
|
-
*
|
|
78
|
+
* receiveAt 후 부가 dwell 동작은 `_onCarrierReceived` hook 으로 위임.
|
|
92
79
|
*/
|
|
93
|
-
|
|
94
|
-
;(this as any).reparent?.(carrier, { ...(options ?? {}), animated: false })
|
|
80
|
+
_onCarrierReceived(_carrier: Component, _options?: any) {
|
|
95
81
|
const procMs = this.state.processingTimeMs ?? 0
|
|
96
|
-
if (procMs
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
async receive(carrier: Component, options?: any): Promise<void> {
|
|
110
|
-
return this.receiveAt(SLOT_ID, carrier, options)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
slotTargetAt(slotId: string): SlotTarget { return new SlotTarget(this as any, slotId) }
|
|
114
|
-
getSlotAttachObject3d(_slotId: string): any {
|
|
115
|
-
return (this as any)._realObject?.getAttachFrame?.()
|
|
116
|
-
}
|
|
82
|
+
if (procMs <= 0) return
|
|
83
|
+
this.setState({ status: 'processing' as PickingStationStatus })
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
if ((this.state.status as PickingStationStatus) === 'processing') {
|
|
86
|
+
this.setState({ status: 'idle' as PickingStationStatus })
|
|
87
|
+
}
|
|
88
|
+
}, procMs)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Phase ZT-V PR-6 ProcessStep ─────────────────────────────────────────────
|
|
92
|
+
readonly isProcessStep = true as const
|
|
93
|
+
get processingTimeMs() { return this.state.processingTimeMs ?? 0 }
|
|
117
94
|
|
|
118
95
|
/** carrier 를 작업대(table) 상단에 안착. */
|
|
119
96
|
attachPointFor(carrier: Component): AttachFrame | null {
|
package/src/rack-grid.ts
CHANGED
|
@@ -38,6 +38,7 @@ import * as THREE from 'three'
|
|
|
38
38
|
import {
|
|
39
39
|
CarrierHolder,
|
|
40
40
|
Placeable,
|
|
41
|
+
RecordStorage,
|
|
41
42
|
SlotTarget,
|
|
42
43
|
componentBoundingBox,
|
|
43
44
|
type AttachFrame,
|
|
@@ -268,11 +269,25 @@ export interface RackGridState extends State {
|
|
|
268
269
|
|
|
269
270
|
@sceneComponent('rack-grid')
|
|
270
271
|
export default class RackGrid
|
|
271
|
-
extends
|
|
272
|
+
extends RecordStorage<{ cellId: string; [key: string]: any }>()(
|
|
273
|
+
CarrierHolder(Placeable(ContainerAbstract))
|
|
274
|
+
)
|
|
272
275
|
implements SlottedHolder
|
|
273
276
|
{
|
|
274
277
|
declare state: RackGridState
|
|
275
278
|
|
|
279
|
+
// RecordStorage mixin hook overrides.
|
|
280
|
+
// record.cellId 가 slotId (3-segment '{col}-{row}-{shelf}' format).
|
|
281
|
+
_recordToSlotId(record: { cellId: string }): string {
|
|
282
|
+
return record.cellId ?? ''
|
|
283
|
+
}
|
|
284
|
+
// 3D rebuild — rebuildStockMesh 우선, 없으면 mixin default.
|
|
285
|
+
_rebuildVisual(): void {
|
|
286
|
+
const ro = (this as any)._realObject
|
|
287
|
+
if (ro?.rebuildStockMesh) ro.rebuildStockMesh()
|
|
288
|
+
else if (ro?.update) ro.update()
|
|
289
|
+
}
|
|
290
|
+
|
|
276
291
|
// Phase Auto-Nav (AN-PR-2) — Obstacle 자격. AMR / mover 의 자동 path planning
|
|
277
292
|
// 시 회피 대상. `state.isObstacle` 명시 false 시 override (예외 처리).
|
|
278
293
|
get isObstacle(): boolean {
|
|
@@ -417,63 +432,8 @@ export default class RackGrid
|
|
|
417
432
|
;(this._realObject as any)?.rebuildStockMesh?.()
|
|
418
433
|
}
|
|
419
434
|
|
|
420
|
-
|
|
421
|
-
get records(): Array<{ cellId: string; [key: string]: any }> {
|
|
422
|
-
return (this.state.data as any) ?? []
|
|
423
|
-
}
|
|
435
|
+
// records / legendTarget / _onLegendChanged / resolveLegendColor — RecordStorage mixin 제공.
|
|
424
436
|
|
|
425
|
-
// ── Legend integration ──────────────────────────────────
|
|
426
|
-
|
|
427
|
-
private _legendTarget?: Component
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Legend 컴포넌트 lookup. 우선순위:
|
|
431
|
-
* 1) state.legendTarget id 명시
|
|
432
|
-
* 2) scene 전체 의 type='legend' 첫 번째 컴포넌트 (자동 발견)
|
|
433
|
-
*/
|
|
434
|
-
get legendTarget(): Component | undefined {
|
|
435
|
-
if (this._legendTarget) return this._legendTarget
|
|
436
|
-
|
|
437
|
-
const id = this.state.legendTarget
|
|
438
|
-
if (id) {
|
|
439
|
-
const found = (this.root as any)?.findById?.(id) as Component | undefined
|
|
440
|
-
if (found) {
|
|
441
|
-
this._legendTarget = found
|
|
442
|
-
;(found as any).on?.('change', this._onLegendChanged, this)
|
|
443
|
-
return found
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const visit = (node: any): Component | undefined => {
|
|
448
|
-
if (!node) return undefined
|
|
449
|
-
if (node.state?.type === 'legend') return node as Component
|
|
450
|
-
const children = node.components as Component[] | undefined
|
|
451
|
-
if (children) {
|
|
452
|
-
for (const c of children) {
|
|
453
|
-
const r = visit(c)
|
|
454
|
-
if (r) return r
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
return undefined
|
|
458
|
-
}
|
|
459
|
-
const found = visit(this.root)
|
|
460
|
-
if (found) {
|
|
461
|
-
this._legendTarget = found
|
|
462
|
-
;(found as any).on?.('change', this._onLegendChanged, this)
|
|
463
|
-
}
|
|
464
|
-
return found
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
private _onLegendChanged = (): void => {
|
|
468
|
-
;(this._realObject as any)?.rebuildStockMesh?.()
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* record 의 legend.field 값을 ranges 와 매칭해 색상 해석.
|
|
473
|
-
* - `range.value === recordValue` (카테고리 일치)
|
|
474
|
-
* - `range.min ≤ Number(v) < range.max` (수치 범위)
|
|
475
|
-
* - 매칭 없으면 `defaultColor` 또는 undefined
|
|
476
|
-
*/
|
|
477
437
|
// ── View-mode click → rack-grid-cell-click event + popup ──
|
|
478
438
|
|
|
479
439
|
get eventMap() {
|
|
@@ -516,24 +476,13 @@ export default class RackGrid
|
|
|
516
476
|
|
|
517
477
|
const payload = { cellId, record, hitPoint: hit.point, instanceId, isStock }
|
|
518
478
|
this.trigger('rack-grid-cell-click', payload)
|
|
519
|
-
this._invokePopup(cellId, record)
|
|
479
|
+
if (cellId) this._invokePopup(cellId, record ?? { cellId })
|
|
520
480
|
|
|
521
481
|
// popup 외부 click 으로 인식되어 자동 close 되는 회귀 차단
|
|
522
482
|
mouseEvent.stopPropagation?.()
|
|
523
483
|
}
|
|
524
484
|
|
|
525
|
-
|
|
526
|
-
private _invokePopup(cellId: string | undefined, record: any): void {
|
|
527
|
-
const popupRefId = this.state.popupRef
|
|
528
|
-
if (!popupRefId || !cellId) return
|
|
529
|
-
const popupComp: any = (this.root as any)?.findById?.(popupRefId)
|
|
530
|
-
if (!popupComp || typeof popupComp.openPopup !== 'function') {
|
|
531
|
-
console.warn(`[rack-grid] popupRef="${popupRefId}" 가 가리키는 컴포넌트 없거나 openPopup 미지원`)
|
|
532
|
-
return
|
|
533
|
-
}
|
|
534
|
-
const anchor = this.slotTargetAt(cellId)
|
|
535
|
-
popupComp.openPopup(record ?? { cellId }, { anchor })
|
|
536
|
-
}
|
|
485
|
+
// _invokePopup — RecordStorage mixin 제공 (signature 동일: slotId=cellId, payload=record).
|
|
537
486
|
|
|
538
487
|
/** raycast → 우리 RackGrid 의 어떤 mesh 가 closest hit 인지. */
|
|
539
488
|
private _raycastHit(mouseEvent: MouseEvent): THREE.Intersection | undefined {
|
|
@@ -612,35 +561,7 @@ export default class RackGrid
|
|
|
612
561
|
return `${col}-${row}-${shelf}`
|
|
613
562
|
}
|
|
614
563
|
|
|
615
|
-
resolveLegendColor
|
|
616
|
-
const legend = this.legendTarget
|
|
617
|
-
if (!legend) return undefined
|
|
618
|
-
const status: any = (legend as any).getState?.('status') ?? (legend.state as any)?.status
|
|
619
|
-
if (!status) return undefined
|
|
620
|
-
|
|
621
|
-
const field = status.field as string | undefined
|
|
622
|
-
const ranges = status.ranges as any[] | undefined
|
|
623
|
-
if (!field || !Array.isArray(ranges)) return undefined
|
|
624
|
-
|
|
625
|
-
const value = record?.[field]
|
|
626
|
-
if (value === undefined || value === null) return status.defaultColor
|
|
627
|
-
|
|
628
|
-
for (const range of ranges) {
|
|
629
|
-
if (!range) continue
|
|
630
|
-
if (range.value !== undefined) {
|
|
631
|
-
if (range.value === value) return range.color
|
|
632
|
-
continue
|
|
633
|
-
}
|
|
634
|
-
const num = Number(value)
|
|
635
|
-
if (!Number.isFinite(num)) continue
|
|
636
|
-
const min = range.min !== undefined && range.min !== '' ? Number(range.min) : undefined
|
|
637
|
-
const max = range.max !== undefined && range.max !== '' ? Number(range.max) : undefined
|
|
638
|
-
const minOk = min === undefined || num >= min
|
|
639
|
-
const maxOk = max === undefined || num < max
|
|
640
|
-
if (minOk && maxOk) return range.color
|
|
641
|
-
}
|
|
642
|
-
return status.defaultColor as string | undefined
|
|
643
|
-
}
|
|
564
|
+
// resolveLegendColor — RecordStorage mixin 제공.
|
|
644
565
|
|
|
645
566
|
/**
|
|
646
567
|
* 새 (rows × columns) 에 맞춰 children 재구성. rack-table.buildCells 정확 클론.
|
|
@@ -1251,7 +1172,7 @@ export default class RackGrid
|
|
|
1251
1172
|
const cellId = this._resolveToCellId(slotIdOrLocation)
|
|
1252
1173
|
if (!cellId) return false
|
|
1253
1174
|
if (this._carrierChildAt(cellId)) return true
|
|
1254
|
-
return this.records.some(r => r.cellId === cellId)
|
|
1175
|
+
return this.records.some((r: any) => r.cellId === cellId)
|
|
1255
1176
|
}
|
|
1256
1177
|
|
|
1257
1178
|
/**
|
package/src/spot.ts
CHANGED
|
@@ -34,7 +34,7 @@ import type { State, Material3D } from '@hatiolab/things-scene'
|
|
|
34
34
|
import {
|
|
35
35
|
CarrierHolder,
|
|
36
36
|
Placeable,
|
|
37
|
-
|
|
37
|
+
SingleSlotHolder,
|
|
38
38
|
type AttachFrame,
|
|
39
39
|
type Alignment,
|
|
40
40
|
type Heights,
|
|
@@ -69,7 +69,11 @@ const NATURE: ComponentNature = {
|
|
|
69
69
|
// which forces `isHTMLElement(): true` and trips the 3D pipeline's
|
|
70
70
|
// addObject DOM-skip gate. Spot is purely 3D.
|
|
71
71
|
@sceneComponent('spot')
|
|
72
|
-
export default class Spot extends
|
|
72
|
+
export default class Spot extends SingleSlotHolder()(
|
|
73
|
+
CarrierHolder(Placeable(ContainerAbstract))
|
|
74
|
+
) {
|
|
75
|
+
// SingleSlotHolder hook override — Spot 의 slot id 'pad'.
|
|
76
|
+
_singleSlotId() { return SPOT_SLOT_ID }
|
|
73
77
|
declare state: SpotState
|
|
74
78
|
declare _realObject?: Spot3D
|
|
75
79
|
|
|
@@ -169,63 +173,11 @@ export default class Spot extends CarrierHolder(Placeable(ContainerAbstract)) {
|
|
|
169
173
|
}
|
|
170
174
|
}
|
|
171
175
|
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
// 안 함) — virtual location 의 handoff buffer 동작.
|
|
178
|
-
slotIds(): string[] {
|
|
179
|
-
return [SPOT_SLOT_ID]
|
|
180
|
-
}
|
|
181
|
-
hasCarrierAt(slotId: string): boolean {
|
|
182
|
-
return slotId === SPOT_SLOT_ID &&
|
|
183
|
-
((this as any).components ?? []).some((c: any) => c?.isCarriable)
|
|
184
|
-
}
|
|
185
|
-
canReceiveAt(slotId: string, _carrier?: Component): boolean {
|
|
186
|
-
return slotId === SPOT_SLOT_ID && !this.hasCarrierAt(slotId)
|
|
187
|
-
}
|
|
188
|
-
occupiedSlotIds(): ReadonlyArray<string> {
|
|
189
|
-
return this.hasCarrierAt(SPOT_SLOT_ID) ? [SPOT_SLOT_ID] : []
|
|
190
|
-
}
|
|
191
|
-
emptySlotIds(): ReadonlyArray<string> {
|
|
192
|
-
return this.hasCarrierAt(SPOT_SLOT_ID) ? [] : [SPOT_SLOT_ID]
|
|
193
|
-
}
|
|
194
|
-
obtainCarrier(slotId: string): Component | null {
|
|
195
|
-
if (slotId !== SPOT_SLOT_ID) return null
|
|
196
|
-
return ((this as any).components ?? []).find((c: any) => c?.isCarriable) ?? null
|
|
197
|
-
}
|
|
198
|
-
async receiveAt(_slotId: string, carrier: Component, options?: any): Promise<void> {
|
|
199
|
-
// Mover.place 의 dispatch → Transfer.handoff → SlotTarget.receive → 여기로 위임.
|
|
200
|
-
// carrier 를 논리 components + 3D 로 reparent 해야 다음 mover(crane 등)가
|
|
201
|
-
// obtainCarrier(components 조회)로 집어간다. dispose 하지 않고 상판에 유지
|
|
202
|
-
// (virtual location handoff buffer).
|
|
203
|
-
;(this as any).reparent?.(carrier, { ...(options ?? {}), animated: false })
|
|
204
|
-
}
|
|
205
|
-
getSlotAttachObject3d(_slotId: string): any {
|
|
206
|
-
return (this as any)._realObject?.getAttachFrame?.()
|
|
207
|
-
}
|
|
208
|
-
slotTargetAt(slotId: string): SlotTarget {
|
|
209
|
-
return new SlotTarget(this as any, slotId)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Mover.place 의 Case A(dispatch → holder.receive)에서 호출. 기본 receive 는
|
|
214
|
-
* carrier 를 3D(object3d)로만 attach 해서 논리 tree(components)엔 안 들어간다 →
|
|
215
|
-
* 다음 mover 가 obtainCarrier(components 조회)로 못 찾아 'no-carrier'. reparent
|
|
216
|
-
* (CarrierHolder.reparent: addComponent[논리] + attach[3D])로 위임해 carrier 가
|
|
217
|
-
* components 에 들어가도록 한다 — handoff 의 carrier 승계가 동작.
|
|
218
|
-
*/
|
|
219
|
-
// Transfer 의 handoff 는 `target.accept ?? target.receive` 순으로 호출한다 —
|
|
220
|
-
// accept 가 우선. Spot 은 carrier 를 논리 components + 3D 로 reparent 해야
|
|
221
|
-
// obtainCarrier 가 찾는다. accept/receive 둘 다 reparent 로 위임.
|
|
222
|
-
// 직접 dispatch(SlotTarget 미경유) 경로 대비 — accept/receive 도 reparent 로 위임.
|
|
223
|
-
async accept(carrier: Component, options?: any): Promise<void> {
|
|
224
|
-
;(this as any).reparent?.(carrier, { ...(options ?? {}), animated: false })
|
|
225
|
-
}
|
|
226
|
-
async receive(carrier: Component, options?: any): Promise<void> {
|
|
227
|
-
;(this as any).reparent?.(carrier, { ...(options ?? {}), animated: false })
|
|
228
|
-
}
|
|
176
|
+
// SlottedHolder duck (slotIds / hasCarrierAt / canReceiveAt / occupiedSlotIds /
|
|
177
|
+
// emptySlotIds / obtainCarrier / receiveAt / accept / receive / slotTargetAt /
|
|
178
|
+
// getSlotAttachObject3d) — SingleSlotHolder mixin 제공.
|
|
179
|
+
// virtual location handoff buffer 동작: receiveAt 가 reparent (components +
|
|
180
|
+
// 3D) 로 carrier 를 자식으로 유지 → 다음 mover 가 obtainCarrier 로 승계.
|
|
229
181
|
}
|
|
230
182
|
|
|
231
183
|
/** Spot 의 유일 virtual slot id. */
|
package/src/stockpile-grid.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type { State, Material3D } from '@hatiolab/things-scene'
|
|
|
22
22
|
import {
|
|
23
23
|
CarrierHolder,
|
|
24
24
|
Placeable,
|
|
25
|
+
RecordStorage,
|
|
25
26
|
SlotTarget,
|
|
26
27
|
type AttachFrame,
|
|
27
28
|
type Alignment,
|
|
@@ -96,7 +97,9 @@ const NATURE: ComponentNature = {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
@sceneComponent('stockpile-grid')
|
|
99
|
-
export default class StockpileGrid extends
|
|
100
|
+
export default class StockpileGrid extends RecordStorage<StockpileGridCell>()(
|
|
101
|
+
CarrierHolder(Placeable(ContainerAbstract))
|
|
102
|
+
) {
|
|
100
103
|
declare state: StockpileGridState
|
|
101
104
|
declare _realObject?: StockpileGrid3D
|
|
102
105
|
|
|
@@ -107,6 +110,14 @@ export default class StockpileGrid extends CarrierHolder(Placeable(ContainerAbst
|
|
|
107
110
|
get nature(): ComponentNature { return NATURE }
|
|
108
111
|
get anchors() { return [] }
|
|
109
112
|
|
|
113
|
+
// RecordStorage mixin 의 records / inventoryCount / addRecord / removeRecord 는
|
|
114
|
+
// cell-aware 시맨틱과 호환 안 됨. cell-nested data 라 *_recordsOf(col,row)_* 와
|
|
115
|
+
// _setCellRecords 를 사용. legend / popup / onLegendChanged 는 mixin 사용.
|
|
116
|
+
// _rebuildVisual hook 만 override.
|
|
117
|
+
_rebuildVisual(): void {
|
|
118
|
+
this._realObject?.update?.()
|
|
119
|
+
}
|
|
120
|
+
|
|
110
121
|
// ── grid 좌표 helpers ─────────────────────────────────────
|
|
111
122
|
get cols(): number { return Math.max(1, Math.floor(this.state.cols ?? 3)) }
|
|
112
123
|
get rows(): number { return Math.max(1, Math.floor(this.state.rows ?? 3)) }
|
|
@@ -269,63 +280,9 @@ export default class StockpileGrid extends CarrierHolder(Placeable(ContainerAbst
|
|
|
269
280
|
return carrier
|
|
270
281
|
}
|
|
271
282
|
|
|
272
|
-
//
|
|
273
|
-
private _legendTarget?: Component
|
|
274
|
-
get legendTarget(): Component | undefined {
|
|
275
|
-
if (this._legendTarget) return this._legendTarget
|
|
276
|
-
const id = this.state.legendTarget
|
|
277
|
-
if (id) {
|
|
278
|
-
const found = ((this as any).root)?.findById?.(id) as Component | undefined
|
|
279
|
-
if (found) {
|
|
280
|
-
this._legendTarget = found
|
|
281
|
-
;(found as any).on?.('change', this._onLegendChanged, this)
|
|
282
|
-
return found
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
const visit = (node: any): Component | undefined => {
|
|
286
|
-
if (!node) return undefined
|
|
287
|
-
if (node.state?.type === 'legend') return node as Component
|
|
288
|
-
const children = node.components as Component[] | undefined
|
|
289
|
-
if (children) for (const c of children) { const r = visit(c); if (r) return r }
|
|
290
|
-
return undefined
|
|
291
|
-
}
|
|
292
|
-
const found = visit((this as any).root)
|
|
293
|
-
if (found) {
|
|
294
|
-
this._legendTarget = found
|
|
295
|
-
;(found as any).on?.('change', this._onLegendChanged, this)
|
|
296
|
-
}
|
|
297
|
-
return found
|
|
298
|
-
}
|
|
299
|
-
private _onLegendChanged = (): void => { ;(this._realObject as any)?.update?.() }
|
|
300
|
-
|
|
301
|
-
resolveLegendColor(record: any): string | undefined {
|
|
302
|
-
const legend = this.legendTarget
|
|
303
|
-
if (!legend) return undefined
|
|
304
|
-
const status: any = (legend as any).getState?.('status') ?? (legend.state as any)?.status
|
|
305
|
-
if (!status) return undefined
|
|
306
|
-
const field = status.field as string | undefined
|
|
307
|
-
const ranges = status.ranges as any[] | undefined
|
|
308
|
-
if (!field || !Array.isArray(ranges)) return undefined
|
|
309
|
-
const value = record?.[field]
|
|
310
|
-
if (value === undefined || value === null) return status.defaultColor
|
|
311
|
-
for (const range of ranges) {
|
|
312
|
-
if (!range) continue
|
|
313
|
-
if (range.value !== undefined) {
|
|
314
|
-
if (range.value === value) return range.color
|
|
315
|
-
continue
|
|
316
|
-
}
|
|
317
|
-
const num = Number(value)
|
|
318
|
-
if (!Number.isFinite(num)) continue
|
|
319
|
-
const min = range.min !== undefined && range.min !== '' ? Number(range.min) : undefined
|
|
320
|
-
const max = range.max !== undefined && range.max !== '' ? Number(range.max) : undefined
|
|
321
|
-
const minOk = min === undefined || num >= min
|
|
322
|
-
const maxOk = max === undefined || num < max
|
|
323
|
-
if (minOk && maxOk) return range.color
|
|
324
|
-
}
|
|
325
|
-
return status.defaultColor as string | undefined
|
|
326
|
-
}
|
|
283
|
+
// legendTarget / _onLegendChanged / resolveLegendColor — RecordStorage mixin 제공.
|
|
327
284
|
|
|
328
|
-
// ── Popup + click
|
|
285
|
+
// ── Popup + click — mixin 의 _invokePopup 활용, cell payload 만 wrapper ──
|
|
329
286
|
get eventMap() {
|
|
330
287
|
return { '(self)': { '(self)': { click: this._onGridClick } } }
|
|
331
288
|
}
|
|
@@ -334,28 +291,23 @@ export default class StockpileGrid extends CarrierHolder(Placeable(ContainerAbst
|
|
|
334
291
|
const hit = this._raycastHit(mouseEvent)
|
|
335
292
|
if (!hit) return
|
|
336
293
|
const slotId = (hit.object?.userData?.slotId as string | undefined)
|
|
337
|
-
this.
|
|
294
|
+
this._dispatchCellPopup(slotId)
|
|
338
295
|
}
|
|
339
|
-
private
|
|
340
|
-
|
|
341
|
-
if (!popupRefId) return
|
|
342
|
-
const popupComp: any = (this as any).root?.findById?.(popupRefId)
|
|
343
|
-
if (!popupComp || typeof popupComp.openPopup !== 'function') return
|
|
296
|
+
private _dispatchCellPopup(slotId?: string): void {
|
|
297
|
+
if (!this.state.popupRef) return
|
|
344
298
|
if (slotId) {
|
|
345
299
|
const p = this.parseCellId(slotId)
|
|
346
|
-
|
|
347
|
-
popupComp.openPopup({
|
|
300
|
+
this._invokePopup(slotId, {
|
|
348
301
|
cellId: slotId,
|
|
349
302
|
col: p?.col, row: p?.row,
|
|
350
303
|
records: p ? this.recordsOf(p.col, p.row) : [],
|
|
351
304
|
capacity: p ? this.capacityOf(p.col, p.row) : undefined
|
|
352
|
-
}
|
|
305
|
+
})
|
|
353
306
|
} else {
|
|
354
|
-
|
|
355
|
-
popupComp.openPopup({
|
|
307
|
+
this._invokePopup(this.cellIdOf(0, 0), {
|
|
356
308
|
componentId: (this.state as any).id,
|
|
357
309
|
cols: this.cols, rows: this.rows
|
|
358
|
-
}
|
|
310
|
+
})
|
|
359
311
|
}
|
|
360
312
|
}
|
|
361
313
|
|