@operato/scene-storage 10.0.0-beta.43 → 10.0.0-beta.46
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 +37 -0
- package/dist/box-3d.d.ts +2 -0
- package/dist/box-3d.js +103 -64
- package/dist/box-3d.js.map +1 -1
- package/dist/crane-3d.d.ts +10 -0
- package/dist/crane-3d.js +34 -5
- package/dist/crane-3d.js.map +1 -1
- package/dist/crane.d.ts +136 -6
- package/dist/crane.js +567 -46
- package/dist/crane.js.map +1 -1
- package/dist/pallet-3d.d.ts +2 -0
- package/dist/pallet-3d.js +103 -53
- package/dist/pallet-3d.js.map +1 -1
- package/dist/parcel-3d.d.ts +1 -0
- package/dist/parcel-3d.js +18 -1
- package/dist/parcel-3d.js.map +1 -1
- package/dist/rack-grid-3d.js +26 -8
- package/dist/rack-grid-3d.js.map +1 -1
- package/dist/rack-grid.d.ts +94 -10
- package/dist/rack-grid.js +468 -86
- package/dist/rack-grid.js.map +1 -1
- package/dist/storage-rack-3d.js +1 -1
- package/dist/storage-rack-3d.js.map +1 -1
- package/dist/storage-rack.d.ts +31 -6
- package/dist/storage-rack.js +96 -14
- package/dist/storage-rack.js.map +1 -1
- package/package.json +3 -3
- package/src/box-3d.ts +121 -68
- package/src/crane-3d.ts +34 -4
- package/src/crane.ts +615 -55
- package/src/pallet-3d.ts +122 -55
- package/src/parcel-3d.ts +19 -1
- package/src/rack-grid-3d.ts +31 -8
- package/src/rack-grid.ts +488 -82
- package/src/storage-rack-3d.ts +1 -1
- package/src/storage-rack.ts +96 -14
- package/test/test-coord-alignment.ts +2 -2
- package/test/test-crane-bay-match.ts +130 -0
- package/test/test-crane-binding-resolve.ts +168 -0
- package/test/test-crane-duration.ts +90 -0
- package/test/test-crane-rotation-reach.ts +218 -0
- package/test/test-rack-grid-3d-alignment.ts +235 -0
- package/test/test-rack-grid-3d-attach-real.ts +375 -0
- package/test/test-rack-grid-cell.ts +2 -2
- package/test/test-rack-grid-location.ts +2 -2
- package/test/test-rack-grid-occupied-slots.ts +165 -0
- package/test/test-rack-grid-picking-position.ts +154 -0
- package/test/test-rack-grid-slot-api.ts +483 -0
- package/test/test-slot-ids-enumeration.ts +137 -0
- package/tsconfig.tsbuildinfo +1 -1
package/dist/rack-grid.js
CHANGED
|
@@ -30,9 +30,9 @@ import { __decorate } from "tslib";
|
|
|
30
30
|
* - cellId = `${col-1}-${row-1}-${shelf-1}` (0-based, 3 segments) — *grid + shelf*
|
|
31
31
|
* - 자식 StorageRack 내부에서는 `0-0-${shelf-1}` (자체 cellId 형식). RackGrid 가 변환.
|
|
32
32
|
*/
|
|
33
|
-
import { ContainerAbstract, Layout, Model, sceneComponent } from '@hatiolab/things-scene';
|
|
33
|
+
import { Component, ContainerAbstract, Layout, Model, sceneComponent } from '@hatiolab/things-scene';
|
|
34
34
|
import * as THREE from 'three';
|
|
35
|
-
import { Placeable, SlotTarget } from '@operato/scene-base';
|
|
35
|
+
import { CarrierHolder, Placeable, SlotTarget } from '@operato/scene-base';
|
|
36
36
|
import { RackGrid3D } from './rack-grid-3d.js';
|
|
37
37
|
// RackGridCell 의 refid 충돌 회피 — load 시 동적 생성되는 cell 의 refid 가 *부모
|
|
38
38
|
// RackGrid + 모델 안 다른 컴포넌트* refid 와 겹치지 않도록 *높은 시작값 + monotonic
|
|
@@ -41,6 +41,12 @@ let _cellRefidCounter = 100_000_000;
|
|
|
41
41
|
function _nextCellRefid() {
|
|
42
42
|
return _cellRefidCounter++;
|
|
43
43
|
}
|
|
44
|
+
// Transient carrier (obtainCarrier 가 materialize 한 컴포넌트) 용 refid. 모델 안
|
|
45
|
+
// 다른 컴포넌트와 겹치지 않도록 별도 대역.
|
|
46
|
+
let _carrierRefidCounter = 200_000_000;
|
|
47
|
+
function _nextCarrierRefid() {
|
|
48
|
+
return _carrierRefidCounter++;
|
|
49
|
+
}
|
|
44
50
|
function buildNewCell(app) {
|
|
45
51
|
const refid = _nextCellRefid();
|
|
46
52
|
const m = Model.compile({
|
|
@@ -60,6 +66,43 @@ function buildNewCell(app) {
|
|
|
60
66
|
return m;
|
|
61
67
|
}
|
|
62
68
|
const TABLE_LAYOUT = Layout.get('table');
|
|
69
|
+
/**
|
|
70
|
+
* RackGrid 의 *layout-aware children* — `rack-grid-cell` (= 데이타-proxy 시각
|
|
71
|
+
* 자식) 만 Table 분배 대상. obtainCarrier 가 만든 *실물 carrier* (Carriable —
|
|
72
|
+
* Parcel/Pallet 등) 는 *transient* 라 layout 분배 외. Table.reflow 가 *carrier
|
|
73
|
+
* 도 cells 다음 인덱스로 분배* 하면 `heights[rackRows] = undefined` → carrier.
|
|
74
|
+
* state.height = NaN → BoxGeometry NaN → invisible. 그 회귀 방지.
|
|
75
|
+
*/
|
|
76
|
+
const RACK_GRID_LAYOUT = {
|
|
77
|
+
reflow(container) {
|
|
78
|
+
const all = container.components ?? [];
|
|
79
|
+
const cellsOnly = all.filter((c) => c?.state?.type === 'rack-grid-cell');
|
|
80
|
+
if (cellsOnly.length === all.length) {
|
|
81
|
+
return TABLE_LAYOUT.reflow.call(TABLE_LAYOUT, container);
|
|
82
|
+
}
|
|
83
|
+
// Carrier 자식이 있는 경우 — container.components 를 *cells only* 로
|
|
84
|
+
// 임시 교체 후 base reflow. base 가 idx 기반 분배 시 carrier 가 *out of
|
|
85
|
+
// range* 인덱스에 안 오게.
|
|
86
|
+
const desc = Object.getOwnPropertyDescriptor(container, 'components');
|
|
87
|
+
Object.defineProperty(container, 'components', { value: cellsOnly, configurable: true, writable: true });
|
|
88
|
+
try {
|
|
89
|
+
TABLE_LAYOUT.reflow.call(TABLE_LAYOUT, container);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
if (desc)
|
|
93
|
+
Object.defineProperty(container, 'components', desc);
|
|
94
|
+
else
|
|
95
|
+
delete container.components;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
capturables(container) { return TABLE_LAYOUT.capturables(container); },
|
|
99
|
+
drawables(container) { return TABLE_LAYOUT.drawables(container); },
|
|
100
|
+
isStuck(component) { return TABLE_LAYOUT.isStuck?.(component) ?? true; },
|
|
101
|
+
keyNavigate(container, component, e) {
|
|
102
|
+
return TABLE_LAYOUT.keyNavigate?.(container, component, e);
|
|
103
|
+
},
|
|
104
|
+
joinType: TABLE_LAYOUT.joinType
|
|
105
|
+
};
|
|
63
106
|
function arrayOf(value, size) {
|
|
64
107
|
const arr = [];
|
|
65
108
|
for (let i = 0; i < size; i++)
|
|
@@ -113,7 +156,7 @@ const rowControlHandler = {
|
|
|
113
156
|
// handler 안 `this` 가 *해당 RackGrid 인스턴스* 를 가리키도록 (화살표 함수가
|
|
114
157
|
// getter 의 `this` 를 capture). rack-table-cell 의 동일 패턴.
|
|
115
158
|
// ─── RackGrid ─────────────────────────────────────────────────────────────────
|
|
116
|
-
let RackGrid = class RackGrid extends Placeable(ContainerAbstract) {
|
|
159
|
+
let RackGrid = class RackGrid extends CarrierHolder(Placeable(ContainerAbstract)) {
|
|
117
160
|
static placement = 'floor';
|
|
118
161
|
static align = 'bottom';
|
|
119
162
|
static defaultDepth = (h) => h.ceiling - h.floor;
|
|
@@ -545,12 +588,118 @@ let RackGrid = class RackGrid extends Placeable(ContainerAbstract) {
|
|
|
545
588
|
* truth, 없으면 cellOverrides[posKey].isEmpty fallback.
|
|
546
589
|
*/
|
|
547
590
|
isBayEmpty(col, row = 0) {
|
|
591
|
+
// cell.state.isEmpty 가 *명시 true* 일 때만 우선. false / undefined 면 fallback
|
|
592
|
+
// 으로 cellOverrides 확인. _buildCells 가 모든 bay 에 cell 자동 생성 + default
|
|
593
|
+
// isEmpty=false 라서, "cell 객체 존재 만으로 cellOverrides 차단" 시 사용자가
|
|
594
|
+
// cellOverrides 만 설정한 모델에서 isEmpty 효과 0 회귀.
|
|
548
595
|
const cell = this.cellAt(col, row);
|
|
549
|
-
if (cell)
|
|
550
|
-
return
|
|
596
|
+
if (cell?.state?.isEmpty === true)
|
|
597
|
+
return true;
|
|
551
598
|
const posKey = `${col}-${row}`;
|
|
552
599
|
return !!this.cellOverrides[posKey]?.isEmpty;
|
|
553
600
|
}
|
|
601
|
+
/**
|
|
602
|
+
* 모든 slot id 의 목록. columns × rackRows × shelves 조합. *isEmpty bay 는 제외*
|
|
603
|
+
* (물리적으로 없는 위치). SlottedHolder.slotIds — capability 기반 enumeration
|
|
604
|
+
* entry point.
|
|
605
|
+
*/
|
|
606
|
+
slotIds() {
|
|
607
|
+
const ids = [];
|
|
608
|
+
const cols = this.columns;
|
|
609
|
+
const rows = this.rackRows;
|
|
610
|
+
const shelves = this.shelves;
|
|
611
|
+
for (let col = 0; col < cols; col++) {
|
|
612
|
+
for (let row = 0; row < rows; row++) {
|
|
613
|
+
if (this.isBayEmpty(col, row))
|
|
614
|
+
continue; // 물리적 없음 — 제외
|
|
615
|
+
for (let shelf = 0; shelf < shelves; shelf++) {
|
|
616
|
+
ids.push(`${col}-${row}-${shelf}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return ids;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* 점유된 slot 의 id 목록 — state.data record + child *carrier* 둘 다.
|
|
624
|
+
*
|
|
625
|
+
* 중요: RackGridCell (시각 proxy) 의 *state.cellId 는 bayKey ("col-row")
|
|
626
|
+
* 형식* (`_syncChildCellIds` 가 set). slot 의 *full id ("col-row-shelf")* 와
|
|
627
|
+
* 다르다 — RackGridCell 은 *carrier 아닌 시각 proxy* 라 *occupied 카운트
|
|
628
|
+
* 대상 아님*. carriable child (= 실 carrier) 만 sweep.
|
|
629
|
+
*
|
|
630
|
+
* @param filter predicate — 통과하는 slotId 만. sweep 중 즉시 reject.
|
|
631
|
+
*/
|
|
632
|
+
occupiedSlotIds(filter) {
|
|
633
|
+
const set = new Set();
|
|
634
|
+
for (const r of this.records) {
|
|
635
|
+
const cid = r?.cellId;
|
|
636
|
+
if (typeof cid === 'string' && (!filter || filter(cid)))
|
|
637
|
+
set.add(cid);
|
|
638
|
+
}
|
|
639
|
+
for (const c of this.components ?? []) {
|
|
640
|
+
// *carriable 만* — RackGridCell (rack-grid-cell type) 의 bayKey 제외.
|
|
641
|
+
if (!c?.isCarriable)
|
|
642
|
+
continue;
|
|
643
|
+
const cid = c?.state?.cellId;
|
|
644
|
+
if (typeof cid === 'string' && (!filter || filter(cid)))
|
|
645
|
+
set.add(cid);
|
|
646
|
+
}
|
|
647
|
+
return Array.from(set);
|
|
648
|
+
}
|
|
649
|
+
/** 비어있는 slot 의 id 목록 — slotIds() - occupiedSlotIds(). isEmpty bay 자동 제외. */
|
|
650
|
+
emptySlotIds(filter) {
|
|
651
|
+
const occ = new Set(this.occupiedSlotIds());
|
|
652
|
+
const result = [];
|
|
653
|
+
for (const id of this.slotIds()) {
|
|
654
|
+
if (occ.has(id))
|
|
655
|
+
continue;
|
|
656
|
+
if (filter && !filter(id))
|
|
657
|
+
continue;
|
|
658
|
+
result.push(id);
|
|
659
|
+
}
|
|
660
|
+
return result;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* slot 의 *world position* — anchor object3d *생성 없이* 직접 계산. crane 의
|
|
664
|
+
* reach 검사 같은 *match 전 단계* 에서 *anchor 생성 0* 으로 사용 가능.
|
|
665
|
+
* match 후 *실제 attach* 가 필요할 때 `getSlotAttachObject3d` 가 lazy 생성.
|
|
666
|
+
*/
|
|
667
|
+
slotWorldPosition(slotId) {
|
|
668
|
+
const parsed = this.parseSlotId(slotId);
|
|
669
|
+
if (!parsed)
|
|
670
|
+
return undefined;
|
|
671
|
+
if (parsed.col < 0 || parsed.col >= this.columns)
|
|
672
|
+
return undefined;
|
|
673
|
+
if (parsed.row < 0 || parsed.row >= this.rackRows)
|
|
674
|
+
return undefined;
|
|
675
|
+
if (parsed.shelf < 0 || parsed.shelf >= this.shelves)
|
|
676
|
+
return undefined;
|
|
677
|
+
const ro = this._realObject;
|
|
678
|
+
if (!ro?.object3d)
|
|
679
|
+
return undefined;
|
|
680
|
+
const rs = this.state;
|
|
681
|
+
const width = rs?.width ?? 400;
|
|
682
|
+
const height = rs?.depth ?? 2000;
|
|
683
|
+
const depth = rs?.height ?? 200;
|
|
684
|
+
const shelfBase = Math.max(0, Math.min(rs?.shelfBaseHeight || 0, height * 0.9));
|
|
685
|
+
const cols = this.columns;
|
|
686
|
+
const rows = this.rackRows;
|
|
687
|
+
const shelves = this.shelves;
|
|
688
|
+
const shelfZone = height - shelfBase;
|
|
689
|
+
const bayW = width / cols;
|
|
690
|
+
const bayD = depth / rows;
|
|
691
|
+
const cellY = shelfZone / shelves;
|
|
692
|
+
const baseY = -height / 2;
|
|
693
|
+
const shelfBaseY = baseY + shelfBase;
|
|
694
|
+
const stockD = cellY * 0.7;
|
|
695
|
+
const cx = (parsed.col - cols / 2 + 0.5) * bayW;
|
|
696
|
+
const cellBottomY = shelfBaseY + parsed.shelf * cellY;
|
|
697
|
+
const cy = cellBottomY + stockD / 2;
|
|
698
|
+
const cz = (parsed.row - rows / 2 + 0.5) * bayD;
|
|
699
|
+
const v = new THREE.Vector3(cx, cy, cz);
|
|
700
|
+
ro.object3d.localToWorld(v);
|
|
701
|
+
return { x: v.x, y: v.y, z: v.z };
|
|
702
|
+
}
|
|
554
703
|
buildRealObject() {
|
|
555
704
|
return new RackGrid3D(this);
|
|
556
705
|
}
|
|
@@ -721,13 +870,16 @@ let RackGrid = class RackGrid extends Placeable(ContainerAbstract) {
|
|
|
721
870
|
}
|
|
722
871
|
// ── Grid layout ─────────────────────────────────────────
|
|
723
872
|
get columns() {
|
|
724
|
-
|
|
873
|
+
const v = this.state.columns;
|
|
874
|
+
return Math.max(1, Math.floor(typeof v === 'number' && Number.isFinite(v) ? v : 5));
|
|
725
875
|
}
|
|
726
876
|
get rackRows() {
|
|
727
|
-
|
|
877
|
+
const v = this.state.rows;
|
|
878
|
+
return Math.max(1, Math.floor(typeof v === 'number' && Number.isFinite(v) ? v : 1));
|
|
728
879
|
}
|
|
729
880
|
get shelves() {
|
|
730
|
-
|
|
881
|
+
const v = this.state.shelves;
|
|
882
|
+
return Math.max(1, Math.floor(typeof v === 'number' && Number.isFinite(v) ? v : 4));
|
|
731
883
|
}
|
|
732
884
|
// ── cellId / posKey 변환 ────────────────────────────────
|
|
733
885
|
/** posKey = `${col-1}-${row-1}` (0-based, 2 segments). 1-based input. */
|
|
@@ -735,10 +887,10 @@ let RackGrid = class RackGrid extends Placeable(ContainerAbstract) {
|
|
|
735
887
|
return `${col - 1}-${row - 1}`;
|
|
736
888
|
}
|
|
737
889
|
/** cellId = `${col-1}-${row-1}-${shelf-1}` (0-based, 3 segments). 1-based input. */
|
|
738
|
-
|
|
890
|
+
slotIdOf(col, row = 1, shelf = 1) {
|
|
739
891
|
return `${col - 1}-${row - 1}-${shelf - 1}`;
|
|
740
892
|
}
|
|
741
|
-
|
|
893
|
+
parseSlotId(cellId) {
|
|
742
894
|
const parts = cellId.split('-');
|
|
743
895
|
if (parts.length !== 3)
|
|
744
896
|
return null;
|
|
@@ -789,7 +941,7 @@ let RackGrid = class RackGrid extends Placeable(ContainerAbstract) {
|
|
|
789
941
|
* section/unit 둘 다 있고 isEmpty=false 일 때만 부여. 미부여 시 null.
|
|
790
942
|
*/
|
|
791
943
|
locationOf(cellId) {
|
|
792
|
-
const parsed = this.
|
|
944
|
+
const parsed = this.parseSlotId(cellId);
|
|
793
945
|
if (!parsed)
|
|
794
946
|
return null;
|
|
795
947
|
const posKey = `${parsed.col}-${parsed.row}`;
|
|
@@ -913,99 +1065,312 @@ let RackGrid = class RackGrid extends Placeable(ContainerAbstract) {
|
|
|
913
1065
|
* 자식의 grid 위치 식별: 자식의 state.column / state.row (1-based) 명시. RackGrid 가
|
|
914
1066
|
* 자식 추가 시 자동 할당 또는 사용자가 명시.
|
|
915
1067
|
*/
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1068
|
+
/**
|
|
1069
|
+
* cellId 매칭되는 RackGrid 의 직접 자식 carrier (operation archetype).
|
|
1070
|
+
* RackGrid 의 자식 중 rack-grid-cell (시각 proxy) 외에 obtainCarrier 가 add 한
|
|
1071
|
+
* transient carrier 들이 섞임 — 그 중 cellId / placement='operation' 매칭.
|
|
1072
|
+
* storage-rack 의 동일 패턴.
|
|
1073
|
+
*/
|
|
1074
|
+
_carrierChildAt(cellId) {
|
|
920
1075
|
const children = this.components ?? [];
|
|
921
|
-
|
|
922
|
-
const
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
const childCol = (c.state?.column ?? 1) - 1;
|
|
926
|
-
const childRow = (c.state?.row ?? 1) - 1;
|
|
927
|
-
if (childCol === parsed.col && childRow === parsed.row) {
|
|
928
|
-
return c;
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
return null;
|
|
1076
|
+
return children.find(c => {
|
|
1077
|
+
const placement = c.constructor.placement;
|
|
1078
|
+
return placement === 'operation' && c.state?.cellId === cellId;
|
|
1079
|
+
});
|
|
932
1080
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1081
|
+
/**
|
|
1082
|
+
* state.data 의 internal 갱신 — obtainCarrier / receiveAt 가 사용.
|
|
1083
|
+
* setState 와 달리 'change' / onchangeData / mapping cascade 우회 — 사용자 script
|
|
1084
|
+
* 의 자기-자신 호출 회귀 차단. 시각 갱신은 직접 RealObject.rebuildStockMesh 호출.
|
|
1085
|
+
* storage-rack 의 동일 패턴.
|
|
1086
|
+
*/
|
|
1087
|
+
_setDataSilently(newData) {
|
|
1088
|
+
const self = this;
|
|
1089
|
+
if (!self._state)
|
|
1090
|
+
self._state = {};
|
|
1091
|
+
self._state.data = newData;
|
|
1092
|
+
self._cachedState = null;
|
|
1093
|
+
this._realObject?.rebuildStockMesh?.();
|
|
1094
|
+
}
|
|
1095
|
+
// ── SlottedHolder 컨트랙 — RackGrid 자체의 records 에서 직접 처리 ────
|
|
1096
|
+
//
|
|
1097
|
+
// Plan A 정신: stock 데이터는 RackGrid 의 state.data 에 sparse 로 통합 저장.
|
|
1098
|
+
// 자식 컴포넌트는 시각용 rack-grid-cell (light proxy) 뿐 carrier 보유 X.
|
|
1099
|
+
// storage-rack 의 본체 로직 (transient materialize / silent setData) 을 차용,
|
|
1100
|
+
// 좌표 / 사이즈만 RackGrid 의 cellAt(col,row).bounds + getSlotSize 로 적응.
|
|
1101
|
+
/** state.data record 또는 이미 transient materialize 된 carrier child 가 있는가. */
|
|
1102
|
+
hasCarrierAt(slotIdOrLocation) {
|
|
1103
|
+
const cellId = this._resolveToCellId(slotIdOrLocation);
|
|
1104
|
+
if (!cellId)
|
|
937
1105
|
return false;
|
|
938
|
-
|
|
939
|
-
|
|
1106
|
+
if (this._carrierChildAt(cellId))
|
|
1107
|
+
return true;
|
|
1108
|
+
return this.records.some(r => r.cellId === cellId);
|
|
940
1109
|
}
|
|
1110
|
+
/**
|
|
1111
|
+
* carrier 를 obtain — 이미 child 면 그대로, 아니면 state.data record 로 transient
|
|
1112
|
+
* materialize 후 RackGrid 의 직접 자식으로 add 하고 state.data 에서 그 record 제거.
|
|
1113
|
+
* record 도 child 도 없으면 null.
|
|
1114
|
+
*/
|
|
941
1115
|
obtainCarrier(slotIdOrLocation) {
|
|
942
|
-
const
|
|
943
|
-
if (!
|
|
1116
|
+
const cellId = this._resolveToCellId(slotIdOrLocation);
|
|
1117
|
+
if (!cellId)
|
|
1118
|
+
return null;
|
|
1119
|
+
const existing = this._carrierChildAt(cellId);
|
|
1120
|
+
if (existing)
|
|
1121
|
+
return existing;
|
|
1122
|
+
const records = this.records;
|
|
1123
|
+
const idx = records.findIndex(r => r?.cellId === cellId);
|
|
1124
|
+
if (idx === -1)
|
|
1125
|
+
return null;
|
|
1126
|
+
const record = records[idx];
|
|
1127
|
+
const carrierType = record.type || 'parcel';
|
|
1128
|
+
const CarrierClass = Component.register(carrierType);
|
|
1129
|
+
if (!CarrierClass) {
|
|
1130
|
+
console.warn(`[rack-grid] obtainCarrier("${cellId}"): carrier type "${carrierType}" 미등록`);
|
|
944
1131
|
return null;
|
|
945
|
-
|
|
1132
|
+
}
|
|
1133
|
+
// RackGrid-inner 좌표 — anchor (getSlotAttachObject3d) / stock InstancedMesh 와
|
|
1134
|
+
// *동일 식* 사용: 균등 분할 + center origin → top-left origin 변환.
|
|
1135
|
+
// 이전엔 widths/heights *비례 분할* 을 썼는데 anchor/stock 은 *균등 분할* 이라
|
|
1136
|
+
// 어긋났음. update loop 이 carrier.state.left/top 기반으로 위치 재계산해도
|
|
1137
|
+
// anchor 와 일치하도록 *처음부터 같은 식* 으로 박는다.
|
|
1138
|
+
const parsed = this.parseSlotId(cellId);
|
|
946
1139
|
if (!parsed)
|
|
947
1140
|
return null;
|
|
948
|
-
const
|
|
949
|
-
|
|
1141
|
+
const rackWidth = this.state.width ?? 400;
|
|
1142
|
+
const rackDepthY = this.state.height ?? 200; // 2D height = 3D Z = inner Y
|
|
1143
|
+
const stateDepth = this.state.depth ?? 2000; // 3D Y (vertical)
|
|
1144
|
+
const bayW = rackWidth / this.columns;
|
|
1145
|
+
const bayD = rackDepthY / this.rackRows;
|
|
1146
|
+
// anchor X(center) = (col - cols/2 + 0.5) * bayW → 2D top-left center = anchor.X + rackWidth/2
|
|
1147
|
+
const cellCenterInnerX = (parsed.col + 0.5) * bayW;
|
|
1148
|
+
const cellCenterInnerY = (parsed.row + 0.5) * bayD;
|
|
1149
|
+
// shelf level Y — anchor (getSlotAttachObject3d) 와 동일 식.
|
|
1150
|
+
const shelfBase = Math.max(0, Math.min(this.state.shelfBaseHeight || 0, stateDepth * 0.9));
|
|
1151
|
+
const shelfZone = stateDepth - shelfBase;
|
|
1152
|
+
const cellYHeight = shelfZone / this.shelves;
|
|
1153
|
+
const shelfBaseY = -stateDepth / 2 + shelfBase;
|
|
1154
|
+
const cellBottomY = shelfBaseY + parsed.shelf * cellYHeight;
|
|
1155
|
+
const slotSize = this.getSlotSize(cellId) ?? { width: 50, height: 50, depth: 50 };
|
|
1156
|
+
// record 의 사이즈가 *0* 또는 *non-finite (NaN/Infinity)* 이면 slotSize fallback —
|
|
1157
|
+
// nullish 만으로는 NaN/0 통과해 BoxGeometry NaN bug 유발.
|
|
1158
|
+
const pickSize = (v, fb) => typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : fb;
|
|
1159
|
+
const carrierW = pickSize(record.width, slotSize.width);
|
|
1160
|
+
const carrierH = pickSize(record.height, slotSize.height);
|
|
1161
|
+
const carrierD = pickSize(record.depth, slotSize.depth);
|
|
1162
|
+
// 차원 / 좌표 finite 검증 — 어느 단계에서 NaN 이 들어왔는지 명시 진단.
|
|
1163
|
+
if (!Number.isFinite(carrierW) || !Number.isFinite(carrierH) || !Number.isFinite(carrierD) ||
|
|
1164
|
+
!Number.isFinite(cellCenterInnerX) || !Number.isFinite(cellCenterInnerY)) {
|
|
1165
|
+
console.error('[rack-grid] obtainCarrier: non-finite dim/pos — carrier 생성 차단', {
|
|
1166
|
+
cellId,
|
|
1167
|
+
carrierW, carrierH, carrierD,
|
|
1168
|
+
cellCenterInnerX, cellCenterInnerY,
|
|
1169
|
+
slotSize,
|
|
1170
|
+
recordSize: { w: record.width, h: record.height, d: record.depth },
|
|
1171
|
+
gridDims: {
|
|
1172
|
+
columns: this.columns, rackRows: this.rackRows, shelves: this.shelves,
|
|
1173
|
+
stateW: this.state?.width, stateH: this.state?.height,
|
|
1174
|
+
stateD: this.state?.depth
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
// record 에서 id / refid / transform 류는 *제외* — id 가 들어가면 scene 안 기존
|
|
1180
|
+
// component 와 충돌해 parent 가 잘못 잡힘.
|
|
1181
|
+
const { id: _id, refid: _refid, transform: _tf, ...recordCopy } = record;
|
|
1182
|
+
const carrierState = {
|
|
1183
|
+
...recordCopy,
|
|
1184
|
+
type: carrierType,
|
|
1185
|
+
cellId, // 슬롯 주소
|
|
1186
|
+
refid: _nextCarrierRefid(), // refid 충돌 회피
|
|
1187
|
+
width: carrierW,
|
|
1188
|
+
height: carrierH,
|
|
1189
|
+
depth: carrierD,
|
|
1190
|
+
left: cellCenterInnerX - carrierW / 2,
|
|
1191
|
+
top: cellCenterInnerY - carrierH / 2,
|
|
1192
|
+
// zPos = carrier 의 *parent-local 3D Y bottom*. things-scene 의 RealObject 가
|
|
1193
|
+
// 3D Y center = zPos + depth/2 로 계산. 미명시 시 NaN — carrier.matrixWorld NaN
|
|
1194
|
+
// → fork 가 NaN target 따라가서 빈 포크질. anchor 식 (cellBottomY + stockD/2)
|
|
1195
|
+
// 와 일치하도록 zPos = cellBottomY 명시.
|
|
1196
|
+
zPos: cellBottomY
|
|
1197
|
+
};
|
|
1198
|
+
const carrier = new CarrierClass(carrierState, this._app);
|
|
1199
|
+
this.addComponent(carrier, { silent: true });
|
|
1200
|
+
// 3D 강제 빌드 + holder attach + manual placement/anchor.
|
|
1201
|
+
void carrier.realObject;
|
|
1202
|
+
carrier.applyHolderAttachPoint?.();
|
|
1203
|
+
carrier.realObject?.setTransientPlacement?.({ policy: 'carried' });
|
|
1204
|
+
const anchor = this.getSlotAttachObject3d(cellId);
|
|
1205
|
+
const carrierObj3d = carrier.realObject?.object3d;
|
|
1206
|
+
if (anchor && carrierObj3d) {
|
|
1207
|
+
anchor.add(carrierObj3d);
|
|
1208
|
+
carrierObj3d.position.set(0, 0, 0);
|
|
1209
|
+
carrierObj3d.updateMatrixWorld(true);
|
|
1210
|
+
}
|
|
1211
|
+
// record 제거 — silent (mapping cascade 회피). 동일 cellId 의 *모든* record 정리.
|
|
1212
|
+
this._setDataSilently(records.filter(r => r?.cellId !== cellId));
|
|
1213
|
+
return carrier;
|
|
950
1214
|
}
|
|
1215
|
+
/**
|
|
1216
|
+
* cell 이 carrier 를 받을 수 있는가.
|
|
1217
|
+
* - isEmpty 위치 는 거부 (modeling 차원 location-less)
|
|
1218
|
+
* - state.data 에 record 있으면 점유 → false
|
|
1219
|
+
* - 자기 자신 carrier 가 child 면 idempotent true (자기 자리 복귀)
|
|
1220
|
+
*/
|
|
951
1221
|
canReceiveAt(slotIdOrLocation, carrier) {
|
|
952
|
-
const
|
|
953
|
-
if (!
|
|
1222
|
+
const cellId = this._resolveToCellId(slotIdOrLocation);
|
|
1223
|
+
if (!cellId)
|
|
954
1224
|
return false;
|
|
955
|
-
const parsed = this.
|
|
1225
|
+
const parsed = this.parseSlotId(cellId);
|
|
956
1226
|
if (!parsed)
|
|
957
1227
|
return false;
|
|
958
|
-
// isEmpty 위치 는 carrier 못 받음 (location-less, modeling 차원)
|
|
959
1228
|
const override = this.cellOverrides[`${parsed.col}-${parsed.row}`];
|
|
960
1229
|
if (override?.isEmpty)
|
|
961
1230
|
return false;
|
|
962
|
-
const
|
|
963
|
-
|
|
1231
|
+
const records = this.records;
|
|
1232
|
+
if (records.some(r => r?.cellId === cellId))
|
|
1233
|
+
return false;
|
|
1234
|
+
const existingChild = this._carrierChildAt(cellId);
|
|
1235
|
+
if (existingChild && existingChild !== carrier)
|
|
1236
|
+
return false;
|
|
1237
|
+
return true;
|
|
964
1238
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1239
|
+
/**
|
|
1240
|
+
* Carrier 가 RackGrid 의 slot 으로 들어옴 — 즉시 dispose + state.data 에 record 로
|
|
1241
|
+
* 환원. 결과: stock InstancedMesh 가 그 자리에 instance 표시, RackGrid 의 자식 트리는
|
|
1242
|
+
* 깨끗 (rack-grid-cell 만 남음).
|
|
1243
|
+
*/
|
|
1244
|
+
async receiveAt(slotIdOrLocation, carrier, _options) {
|
|
1245
|
+
const cellId = this._resolveToCellId(slotIdOrLocation);
|
|
1246
|
+
if (!cellId)
|
|
968
1247
|
throw new Error(`RackGrid.receiveAt: cannot resolve "${slotIdOrLocation}"`);
|
|
969
|
-
|
|
970
|
-
if (
|
|
971
|
-
throw new Error(`RackGrid.receiveAt
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1248
|
+
// disposed carrier 의 재처리 차단
|
|
1249
|
+
if (carrier?._disposed) {
|
|
1250
|
+
throw new Error(`RackGrid.receiveAt("${cellId}"): carrier is already disposed. ` +
|
|
1251
|
+
'After a successful pickAndPlace the carrier becomes a state.data record — ' +
|
|
1252
|
+
'use rack.obtainCarrier(cellId) to get a fresh transient carrier instead.');
|
|
1253
|
+
}
|
|
1254
|
+
if (!this.canReceiveAt(cellId, carrier)) {
|
|
1255
|
+
;
|
|
1256
|
+
this.trigger?.('transfer-rejected', {
|
|
1257
|
+
type: 'transfer-rejected',
|
|
1258
|
+
component: carrier, container: this, reason: 'slot-occupied', cellId
|
|
1259
|
+
});
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
const record = this.recordFromCarrier(carrier, cellId);
|
|
1263
|
+
// Carrier 의 parent 에서 떼고 dispose. parent 는 보통 mover (crane) 이거나 RackGrid 자신.
|
|
1264
|
+
const carrierParent = carrier.parent;
|
|
1265
|
+
if (carrierParent && typeof carrierParent.removeComponent === 'function') {
|
|
1266
|
+
carrierParent.removeComponent(carrier);
|
|
975
1267
|
}
|
|
976
|
-
|
|
1268
|
+
// 명시적 Three.js detach — RealObject.dispose() 의 clear() 는 object3d 자체를
|
|
1269
|
+
// scene graph 의 parent 에서 떼지 않으므로 ghost 방지 위해 명시 제거.
|
|
1270
|
+
const carrierObj3d = carrier._realObject?.object3d;
|
|
1271
|
+
if (carrierObj3d?.parent && typeof carrierObj3d.parent.remove === 'function') {
|
|
1272
|
+
carrierObj3d.parent.remove(carrierObj3d);
|
|
1273
|
+
}
|
|
1274
|
+
;
|
|
1275
|
+
carrier.dispose?.();
|
|
1276
|
+
// state.data 에 push — silent (mapping cascade 회피). 동일 cellId 중복 방어.
|
|
1277
|
+
const currentRecords = this.records.filter(r => r?.cellId !== cellId);
|
|
1278
|
+
this._setDataSilently([...currentRecords, record]);
|
|
1279
|
+
this.trigger?.('transfer-received', {
|
|
1280
|
+
type: 'transfer-received',
|
|
1281
|
+
component: carrier, container: this, slotId: cellId, record
|
|
1282
|
+
});
|
|
977
1283
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1284
|
+
/**
|
|
1285
|
+
* Carrier 의 state 를 state.data record 로 추출. transform/position 관련은 record
|
|
1286
|
+
* 와 무관해 skip. (storage-rack 의 동일 패턴.)
|
|
1287
|
+
*/
|
|
1288
|
+
recordFromCarrier(carrier, cellId) {
|
|
1289
|
+
const state = carrier.state ?? {};
|
|
1290
|
+
const SKIP_KEYS = new Set([
|
|
1291
|
+
'left', 'top', 'zPos',
|
|
1292
|
+
'transform', 'rotation', 'scale',
|
|
1293
|
+
'_transferSlotId',
|
|
1294
|
+
'cellId',
|
|
1295
|
+
'id',
|
|
1296
|
+
'refid'
|
|
1297
|
+
]);
|
|
1298
|
+
const record = { cellId, type: state.type };
|
|
1299
|
+
for (const key of Object.keys(state)) {
|
|
1300
|
+
if (SKIP_KEYS.has(key))
|
|
1301
|
+
continue;
|
|
1302
|
+
record[key] = state[key];
|
|
1303
|
+
}
|
|
1304
|
+
return record;
|
|
1305
|
+
}
|
|
1306
|
+
// ── CarrierHolder — attach frame for direct carrier children ─────────────
|
|
1307
|
+
/**
|
|
1308
|
+
* children gate — CarrierHolder.containable 의 *carriable-only* 제한을 완화.
|
|
1309
|
+
* RackGrid 의 자식은 두 종류:
|
|
1310
|
+
* - RackGridCell : modeling-time visual proxy (carriable 아님)
|
|
1311
|
+
* - carrier : Plan A 의 obtainCarrier 가 materialize (carriable)
|
|
1312
|
+
* 둘 다 허용해야 정상 동작.
|
|
1313
|
+
*/
|
|
1314
|
+
containable(component) {
|
|
1315
|
+
if (!component)
|
|
1316
|
+
return false;
|
|
1317
|
+
if (component?.state?.type === 'rack-grid-cell')
|
|
1318
|
+
return true;
|
|
1319
|
+
return component.isCarriable === true;
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Attach frame for direct-child carrier — `applyHolderAttachPoint` 가 이 결과의
|
|
1323
|
+
* `attach` 에 carrier obj3d 를 attach 하고 `localPosition` 을 set. cell anchor
|
|
1324
|
+
* 반환 + carried placement 와 함께 동작 — carrier 가 cell 위치에 고정.
|
|
1325
|
+
*/
|
|
1326
|
+
attachPointFor(carrier) {
|
|
1327
|
+
const cellId = carrier?.state?.cellId;
|
|
1328
|
+
if (cellId) {
|
|
1329
|
+
const obj = this.getSlotAttachObject3d(cellId);
|
|
1330
|
+
if (obj)
|
|
1331
|
+
return { attach: obj, localPosition: { x: 0, y: 0, z: 0 } };
|
|
1332
|
+
}
|
|
1333
|
+
const root = this._realObject?.object3d;
|
|
1334
|
+
if (!root)
|
|
1335
|
+
return null;
|
|
1336
|
+
return { attach: root, localPosition: { x: 0, y: 0, z: 0 } };
|
|
981
1337
|
}
|
|
982
1338
|
/**
|
|
983
1339
|
* Slot 의 attach object3d — Stock InstancedMesh 의 instance 와 *같은 world 위치* 에
|
|
984
|
-
* 위치한 invisible Object3D. popup tether
|
|
985
|
-
*
|
|
1340
|
+
* 위치한 invisible Object3D. popup tether 등이 이 object3d 의 matrixWorld 를
|
|
1341
|
+
* 사용. lazy 생성 + cache.
|
|
986
1342
|
*/
|
|
987
|
-
|
|
1343
|
+
_attachAnchorBySlot = new Map();
|
|
1344
|
+
/**
|
|
1345
|
+
* Anchor position cache 의 signature — rack 의 차원 state 변경 시 무효화 trigger.
|
|
1346
|
+
* 동일 signature 인 동안 *anchor.position.set 재계산 skip*. crane 의 첫
|
|
1347
|
+
* findAdjacentSlots 가 *수많은 anchor 의 position.set 매 호출* — *동일 차원
|
|
1348
|
+
* state 일 때* 의 *중복 계산 차단*.
|
|
1349
|
+
*/
|
|
1350
|
+
_attachAnchorSig;
|
|
988
1351
|
getSlotAttachObject3d(slotId) {
|
|
989
|
-
const parsed = this.
|
|
990
|
-
if (!parsed)
|
|
1352
|
+
const parsed = this.parseSlotId(slotId);
|
|
1353
|
+
if (!parsed) {
|
|
1354
|
+
console.warn(`[rack-grid] getSlotAttachObject3d: invalid cellId format "${slotId}"`);
|
|
991
1355
|
return undefined;
|
|
992
|
-
|
|
1356
|
+
}
|
|
1357
|
+
if (parsed.col < 0 || parsed.col >= this.columns) {
|
|
1358
|
+
console.warn(`[rack-grid] getSlotAttachObject3d: col=${parsed.col} out of range [0, ${this.columns})`);
|
|
993
1359
|
return undefined;
|
|
994
|
-
|
|
1360
|
+
}
|
|
1361
|
+
if (parsed.row < 0 || parsed.row >= this.rackRows) {
|
|
1362
|
+
console.warn(`[rack-grid] getSlotAttachObject3d: row=${parsed.row} out of range [0, ${this.rackRows})`);
|
|
995
1363
|
return undefined;
|
|
996
|
-
|
|
1364
|
+
}
|
|
1365
|
+
if (parsed.shelf < 0 || parsed.shelf >= this.shelves) {
|
|
1366
|
+
console.warn(`[rack-grid] getSlotAttachObject3d: shelf=${parsed.shelf} out of range [0, ${this.shelves}) — cellId "${slotId}" — fork 가 fallback 위치 (한 층 아래 등) 로 갈 수 있음`);
|
|
997
1367
|
return undefined;
|
|
1368
|
+
}
|
|
998
1369
|
const ro = this._realObject;
|
|
999
1370
|
if (!ro?.object3d)
|
|
1000
1371
|
return undefined;
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
obj = new THREE.Object3D();
|
|
1004
|
-
obj.name = `rack-grid-anchor:${slotId}`;
|
|
1005
|
-
ro.object3d.add(obj);
|
|
1006
|
-
this._attachAnchorByCell.set(slotId, obj);
|
|
1007
|
-
}
|
|
1008
|
-
// 위치 갱신 — Stock InstancedMesh 의 instance 위치 와 동일 공식
|
|
1372
|
+
// signature 검사 — 차원 state 변경 시 *모든 anchor invalidate* 후 새로 생성.
|
|
1373
|
+
// 동일 sig 일 때 *기존 anchor 그대로 사용 (position.set skip)*.
|
|
1009
1374
|
const rs = this.state;
|
|
1010
1375
|
const cols = this.columns;
|
|
1011
1376
|
const rows = this.rackRows;
|
|
@@ -1014,25 +1379,42 @@ let RackGrid = class RackGrid extends Placeable(ContainerAbstract) {
|
|
|
1014
1379
|
const height = rs?.depth ?? 2000;
|
|
1015
1380
|
const depth = rs?.height ?? 200;
|
|
1016
1381
|
const shelfBase = Math.max(0, Math.min(rs?.shelfBaseHeight || 0, height * 0.9));
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1382
|
+
const sig = `${cols}-${rows}-${shelves}-${width}-${height}-${depth}-${shelfBase}`;
|
|
1383
|
+
if (this._attachAnchorSig !== sig) {
|
|
1384
|
+
// 차원 변경 — 모든 기존 anchor 폐기 후 sig 갱신.
|
|
1385
|
+
for (const oldObj of this._attachAnchorBySlot.values()) {
|
|
1386
|
+
ro.object3d.remove(oldObj);
|
|
1387
|
+
}
|
|
1388
|
+
this._attachAnchorBySlot.clear();
|
|
1389
|
+
this._attachAnchorSig = sig;
|
|
1390
|
+
}
|
|
1391
|
+
let obj = this._attachAnchorBySlot.get(slotId);
|
|
1392
|
+
if (!obj) {
|
|
1393
|
+
obj = new THREE.Object3D();
|
|
1394
|
+
obj.name = `rack-grid-anchor:${slotId}`;
|
|
1395
|
+
ro.object3d.add(obj);
|
|
1396
|
+
this._attachAnchorBySlot.set(slotId, obj);
|
|
1397
|
+
// 신규 anchor — position 계산 + set (1 회 만, 이후 호출은 cache hit).
|
|
1398
|
+
const shelfZone = height - shelfBase;
|
|
1399
|
+
const bayW = width / cols;
|
|
1400
|
+
const bayD = depth / rows;
|
|
1401
|
+
const cellY = shelfZone / shelves;
|
|
1402
|
+
const baseY = -height / 2;
|
|
1403
|
+
const shelfBaseY = baseY + shelfBase;
|
|
1404
|
+
const stockD = cellY * 0.7;
|
|
1405
|
+
const cx = (parsed.col - cols / 2 + 0.5) * bayW;
|
|
1406
|
+
const cellBottomY = shelfBaseY + parsed.shelf * cellY;
|
|
1407
|
+
const cy = cellBottomY + stockD / 2;
|
|
1408
|
+
const cz = (parsed.row - rows / 2 + 0.5) * bayD;
|
|
1409
|
+
obj.position.set(cx, cy, cz);
|
|
1410
|
+
}
|
|
1029
1411
|
// *parent 의 matrixWorld 가 dirty 면 자식 matrixWorld 도 dirty* — 강제 갱신.
|
|
1030
1412
|
ro.object3d.updateMatrixWorld(true);
|
|
1031
1413
|
obj.updateMatrixWorld(true);
|
|
1032
1414
|
return obj;
|
|
1033
1415
|
}
|
|
1034
1416
|
getSlotSize(slotId) {
|
|
1035
|
-
const parsed = this.
|
|
1417
|
+
const parsed = this.parseSlotId(slotId);
|
|
1036
1418
|
if (!parsed)
|
|
1037
1419
|
return undefined;
|
|
1038
1420
|
const rs = this.state;
|
|
@@ -1049,7 +1431,7 @@ let RackGrid = class RackGrid extends Placeable(ContainerAbstract) {
|
|
|
1049
1431
|
};
|
|
1050
1432
|
}
|
|
1051
1433
|
cellCenter2D(slotId) {
|
|
1052
|
-
const parsed = this.
|
|
1434
|
+
const parsed = this.parseSlotId(slotId);
|
|
1053
1435
|
if (!parsed)
|
|
1054
1436
|
return null;
|
|
1055
1437
|
const rs = this.state;
|