@operato/scene-storage 10.0.0-beta.41 → 10.0.0-beta.43
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/MIGRATION-plan-a-slot-api.md +266 -0
- package/PLAN-A-rack-as-slot-holder.md +164 -0
- package/dist/crane.js +1 -1
- package/dist/crane.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/parcel-3d.js +42 -9
- package/dist/parcel-3d.js.map +1 -1
- package/dist/rack-grid-3d.d.ts +18 -7
- package/dist/rack-grid-3d.js +372 -69
- package/dist/rack-grid-3d.js.map +1 -1
- package/dist/rack-grid-cell.d.ts +21 -72
- package/dist/rack-grid-cell.js +147 -243
- package/dist/rack-grid-cell.js.map +1 -1
- package/dist/rack-grid.d.ts +277 -56
- package/dist/rack-grid.js +1230 -695
- package/dist/rack-grid.js.map +1 -1
- package/dist/rack-materials.d.ts +9 -0
- package/dist/rack-materials.js +55 -0
- package/dist/rack-materials.js.map +1 -0
- package/dist/storage-rack-3d.d.ts +15 -0
- package/dist/storage-rack-3d.js +131 -30
- package/dist/storage-rack-3d.js.map +1 -1
- package/dist/storage-rack.d.ts +242 -45
- package/dist/storage-rack.js +684 -106
- package/dist/storage-rack.js.map +1 -1
- package/package.json +3 -3
- package/src/crane.ts +1 -1
- package/src/index.ts +3 -4
- package/src/parcel-3d.ts +41 -9
- package/src/rack-grid-3d.ts +383 -80
- package/src/rack-grid-cell.ts +161 -305
- package/src/rack-grid.ts +1263 -762
- package/src/rack-materials.ts +61 -0
- package/src/storage-rack-3d.ts +144 -30
- package/src/storage-rack.ts +763 -111
- package/test/test-carrier-lifecycle.ts +361 -0
- package/test/test-coord-alignment.ts +201 -0
- package/test/test-external-to-rack.ts +461 -0
- package/test/test-mover-concurrent-bug.ts +304 -0
- package/test/test-mover-rollback.ts +290 -0
- package/test/test-r19-place-absorb.ts +174 -0
- package/test/test-rack-3d-attach-real.ts +301 -0
- package/test/test-rack-concurrent.ts +254 -0
- package/test/test-rack-edge-cases.ts +323 -0
- package/test/test-rack-grid-cell.ts +318 -0
- package/test/test-rack-grid-location.ts +657 -0
- package/test/test-real-3d-positioning.ts +158 -0
- package/test/test-slot-center-convention.ts +116 -0
- package/test/test-slot-target.ts +189 -0
- package/test/test-storage-rack-batched.ts +606 -0
- package/test/test-storage-rack-click.ts +329 -0
- package/test/test-storage-rack-slot-api.ts +357 -0
- package/test/test-toscene-convention.ts +162 -0
- package/test/test-user-scenario-sequential.ts +334 -0
- package/translations/en.json +2 -0
- package/translations/ja.json +2 -0
- package/translations/ko.json +2 -0
- package/translations/ms.json +2 -0
- package/translations/zh.json +2 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/rack-column.d.ts +0 -35
- package/dist/rack-column.js +0 -258
- package/dist/rack-column.js.map +0 -1
- package/dist/rack-grid-helpers.d.ts +0 -28
- package/dist/rack-grid-helpers.js +0 -71
- package/dist/rack-grid-helpers.js.map +0 -1
- package/dist/rack-grid-location.d.ts +0 -37
- package/dist/rack-grid-location.js +0 -227
- package/dist/rack-grid-location.js.map +0 -1
- package/dist/storage-cell-3d.d.ts +0 -25
- package/dist/storage-cell-3d.js +0 -88
- package/dist/storage-cell-3d.js.map +0 -1
- package/dist/storage-cell.d.ts +0 -73
- package/dist/storage-cell.js +0 -215
- package/dist/storage-cell.js.map +0 -1
- package/src/rack-column.ts +0 -340
- package/src/rack-grid-helpers.ts +0 -77
- package/src/rack-grid-location.ts +0 -286
- package/src/storage-cell-3d.ts +0 -101
- package/src/storage-cell.ts +0 -267
- package/test/test-cell-position.ts +0 -105
- package/test/test-rack-grid.ts +0 -77
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 실제 THREE.js scene graph 에서 anchor 와 stock 의 *world position 일치* 검증.
|
|
3
|
+
*
|
|
4
|
+
* 단순 공식 비교 (test-coord-alignment.ts) 가 통과해도, 실제 Three.js 환경에선 rack 의
|
|
5
|
+
* matrixWorld / 부모 chain 변환에 따라 어긋날 수 있음. 이 테스트는:
|
|
6
|
+
* 1. 실 THREE.Group (rack.object3d) 만들기
|
|
7
|
+
* 2. anchor 와 stock 을 각각 그 자식으로 추가
|
|
8
|
+
* 3. 위치 set 후 *matrixWorld 비교*
|
|
9
|
+
* 둘이 동일 world 위치라면 framework 통과 시도 정확. 어긋나면 framework 측 추가 변환 의심.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import 'should'
|
|
13
|
+
import * as THREE from 'three'
|
|
14
|
+
|
|
15
|
+
function computeAnchorPos(
|
|
16
|
+
cell: { localPosition: { x: number; y: number; z: number } },
|
|
17
|
+
rp: { width: number; depth: number; height: number; bays: number; levels: number; shelfBase: number }
|
|
18
|
+
) {
|
|
19
|
+
const sz = rp.depth - rp.shelfBase
|
|
20
|
+
const bw = rp.width / rp.bays
|
|
21
|
+
const lh = sz / rp.levels
|
|
22
|
+
const sd = lh * 0.7
|
|
23
|
+
return {
|
|
24
|
+
x: cell.localPosition.x + bw / 2 - rp.width / 2,
|
|
25
|
+
y: cell.localPosition.y - rp.depth / 2 + sd / 2,
|
|
26
|
+
z: cell.localPosition.z + rp.height / 2 - rp.height / 2
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function computeStockPos(
|
|
31
|
+
cell: { localPosition: { x: number; y: number; z: number } },
|
|
32
|
+
rp: { width: number; depth: number; height: number; bays: number; levels: number; shelfBase: number }
|
|
33
|
+
) {
|
|
34
|
+
const sz = rp.depth - rp.shelfBase
|
|
35
|
+
const bw = rp.width / rp.bays
|
|
36
|
+
const lh = sz / rp.levels
|
|
37
|
+
const sd = lh * 0.7
|
|
38
|
+
const cellCenterY = cell.localPosition.y + lh / 2 - rp.depth / 2
|
|
39
|
+
return {
|
|
40
|
+
x: cell.localPosition.x + bw / 2 - rp.width / 2,
|
|
41
|
+
y: cellCenterY - lh / 2 + sd / 2,
|
|
42
|
+
z: cell.localPosition.z + rp.height / 2 - rp.height / 2
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeCell(bay: number, row: number, level: number, p: any) {
|
|
47
|
+
return {
|
|
48
|
+
localPosition: {
|
|
49
|
+
x: (bay - 1) * p.bayWidth,
|
|
50
|
+
y: p.shelfBase + (level - 1) * p.levelHeight,
|
|
51
|
+
z: (row - 1) * p.rowDepth
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('Real Three.js: anchor / stock world position 일치', () => {
|
|
57
|
+
const rp = { width: 1000, depth: 3000, height: 600, bays: 5, levels: 4, shelfBase: 0 }
|
|
58
|
+
const bw = rp.width / rp.bays
|
|
59
|
+
const lh = (rp.depth - rp.shelfBase) / rp.levels
|
|
60
|
+
const rd = rp.height
|
|
61
|
+
|
|
62
|
+
it('rack 가 origin 에 있을 때 — anchor.matrixWorld === stock.matrixWorld', () => {
|
|
63
|
+
const rackObj = new THREE.Group()
|
|
64
|
+
rackObj.position.set(0, 0, 0)
|
|
65
|
+
rackObj.updateMatrixWorld(true)
|
|
66
|
+
|
|
67
|
+
const cell = makeCell(2, 1, 2, { bayWidth: bw, rowDepth: rd, levelHeight: lh, shelfBase: 0 })
|
|
68
|
+
const anchorPos = computeAnchorPos(cell, rp)
|
|
69
|
+
const stockPos = computeStockPos(cell, rp)
|
|
70
|
+
|
|
71
|
+
const anchor = new THREE.Object3D()
|
|
72
|
+
anchor.position.set(anchorPos.x, anchorPos.y, anchorPos.z)
|
|
73
|
+
rackObj.add(anchor)
|
|
74
|
+
anchor.updateMatrixWorld(true)
|
|
75
|
+
|
|
76
|
+
const stock = new THREE.Object3D()
|
|
77
|
+
stock.position.set(stockPos.x, stockPos.y, stockPos.z)
|
|
78
|
+
rackObj.add(stock)
|
|
79
|
+
stock.updateMatrixWorld(true)
|
|
80
|
+
|
|
81
|
+
const anchorWorld = new THREE.Vector3()
|
|
82
|
+
anchor.getWorldPosition(anchorWorld)
|
|
83
|
+
const stockWorld = new THREE.Vector3()
|
|
84
|
+
stock.getWorldPosition(stockWorld)
|
|
85
|
+
|
|
86
|
+
anchorWorld.x.should.be.approximately(stockWorld.x, 0.0001)
|
|
87
|
+
anchorWorld.y.should.be.approximately(stockWorld.y, 0.0001)
|
|
88
|
+
anchorWorld.z.should.be.approximately(stockWorld.z, 0.0001)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('rack 가 translated (1000, 500, 200) 일 때 — anchor === stock', () => {
|
|
92
|
+
const rackObj = new THREE.Group()
|
|
93
|
+
rackObj.position.set(1000, 500, 200)
|
|
94
|
+
rackObj.updateMatrixWorld(true)
|
|
95
|
+
|
|
96
|
+
const cell = makeCell(3, 1, 3, { bayWidth: bw, rowDepth: rd, levelHeight: lh, shelfBase: 0 })
|
|
97
|
+
const anchorPos = computeAnchorPos(cell, rp)
|
|
98
|
+
const stockPos = computeStockPos(cell, rp)
|
|
99
|
+
|
|
100
|
+
const anchor = new THREE.Object3D(); anchor.position.set(anchorPos.x, anchorPos.y, anchorPos.z)
|
|
101
|
+
rackObj.add(anchor); anchor.updateMatrixWorld(true)
|
|
102
|
+
const stock = new THREE.Object3D(); stock.position.set(stockPos.x, stockPos.y, stockPos.z)
|
|
103
|
+
rackObj.add(stock); stock.updateMatrixWorld(true)
|
|
104
|
+
|
|
105
|
+
const aw = new THREE.Vector3(); anchor.getWorldPosition(aw)
|
|
106
|
+
const sw = new THREE.Vector3(); stock.getWorldPosition(sw)
|
|
107
|
+
aw.x.should.be.approximately(sw.x, 0.0001)
|
|
108
|
+
aw.y.should.be.approximately(sw.y, 0.0001)
|
|
109
|
+
aw.z.should.be.approximately(sw.z, 0.0001)
|
|
110
|
+
// 또한 rack 의 translation 이 정확히 반영되어야 함
|
|
111
|
+
aw.x.should.be.approximately(anchorPos.x + 1000, 0.0001)
|
|
112
|
+
aw.y.should.be.approximately(anchorPos.y + 500, 0.0001)
|
|
113
|
+
aw.z.should.be.approximately(anchorPos.z + 200, 0.0001)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('rack 가 회전 (Y 축 45°) 일 때 — anchor === stock', () => {
|
|
117
|
+
const rackObj = new THREE.Group()
|
|
118
|
+
rackObj.rotation.y = Math.PI / 4
|
|
119
|
+
rackObj.updateMatrixWorld(true)
|
|
120
|
+
|
|
121
|
+
const cell = makeCell(1, 1, 1, { bayWidth: bw, rowDepth: rd, levelHeight: lh, shelfBase: 0 })
|
|
122
|
+
const anchorPos = computeAnchorPos(cell, rp)
|
|
123
|
+
const stockPos = computeStockPos(cell, rp)
|
|
124
|
+
|
|
125
|
+
const anchor = new THREE.Object3D(); anchor.position.set(anchorPos.x, anchorPos.y, anchorPos.z)
|
|
126
|
+
rackObj.add(anchor); anchor.updateMatrixWorld(true)
|
|
127
|
+
const stock = new THREE.Object3D(); stock.position.set(stockPos.x, stockPos.y, stockPos.z)
|
|
128
|
+
rackObj.add(stock); stock.updateMatrixWorld(true)
|
|
129
|
+
|
|
130
|
+
const aw = new THREE.Vector3(); anchor.getWorldPosition(aw)
|
|
131
|
+
const sw = new THREE.Vector3(); stock.getWorldPosition(sw)
|
|
132
|
+
aw.x.should.be.approximately(sw.x, 0.0001)
|
|
133
|
+
aw.y.should.be.approximately(sw.y, 0.0001)
|
|
134
|
+
aw.z.should.be.approximately(sw.z, 0.0001)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('5×4 grid 모든 cell — anchor === stock', () => {
|
|
138
|
+
const rackObj = new THREE.Group()
|
|
139
|
+
rackObj.position.set(100, 200, 300)
|
|
140
|
+
rackObj.rotation.set(0, Math.PI / 6, 0)
|
|
141
|
+
rackObj.updateMatrixWorld(true)
|
|
142
|
+
|
|
143
|
+
for (let b = 1; b <= 5; b++) {
|
|
144
|
+
for (let l = 1; l <= 4; l++) {
|
|
145
|
+
const cell = makeCell(b, 1, l, { bayWidth: bw, rowDepth: rd, levelHeight: lh, shelfBase: 0 })
|
|
146
|
+
const aPos = computeAnchorPos(cell, rp)
|
|
147
|
+
const sPos = computeStockPos(cell, rp)
|
|
148
|
+
const a = new THREE.Object3D(); a.position.set(aPos.x, aPos.y, aPos.z); rackObj.add(a); a.updateMatrixWorld(true)
|
|
149
|
+
const s = new THREE.Object3D(); s.position.set(sPos.x, sPos.y, sPos.z); rackObj.add(s); s.updateMatrixWorld(true)
|
|
150
|
+
const aw = new THREE.Vector3(); a.getWorldPosition(aw)
|
|
151
|
+
const sw = new THREE.Vector3(); s.getWorldPosition(sw)
|
|
152
|
+
aw.x.should.be.approximately(sw.x, 0.0001)
|
|
153
|
+
aw.y.should.be.approximately(sw.y, 0.0001)
|
|
154
|
+
aw.z.should.be.approximately(sw.z, 0.0001)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SlotTarget.center 의 좌표 컨벤션 검증.
|
|
3
|
+
*
|
|
4
|
+
* 핵심 가설: things-scene 의 모든 컴포넌트의 `.center` 는 *parent 좌표계 의 center*
|
|
5
|
+
* (= `bounds.left + width/2`). Crane.moveTo 가 *target.center → target.toScene* 으로
|
|
6
|
+
* X 도달 위치를 결정. 이때 *pick target (예: parcel)* 와 *place target (SlotTarget)*
|
|
7
|
+
* 의 .center 가 *같은 좌표계 의 값* 이어야 동일한 변환 체인이 정확히 작동.
|
|
8
|
+
*
|
|
9
|
+
* 결함 시나리오: SlotTarget.center 가 *rack-local-inner 좌표* (rack.left/top 미포함)
|
|
10
|
+
* 를 반환하면, rack.toScene 이 같은 컨벤션 하에 처리해도 *layer 좌표* 의 점과 어긋남.
|
|
11
|
+
* → place 만 X 위치 어긋남, pick 은 정상 (parcel.center 는 layer 좌표).
|
|
12
|
+
*
|
|
13
|
+
* 이 테스트가 *pickup vs place 의 좌표 일관성* 을 검증. 사용자가 본 결함:
|
|
14
|
+
* "pick 은 비교적 잘 위치를 잡는데, place 는 완전히 bay 위치를 못 잡는다" — 의 직접 원인.
|
|
15
|
+
*/
|
|
16
|
+
import 'should'
|
|
17
|
+
|
|
18
|
+
// ── things-scene 의 get_center 시뮬 — Component 의 .center 는 parent 좌표 의 center
|
|
19
|
+
function componentCenter(bounds: { left: number; top: number; width: number; height: number }) {
|
|
20
|
+
return {
|
|
21
|
+
x: bounds.left + bounds.width / 2,
|
|
22
|
+
y: bounds.top + bounds.height / 2
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── 새 cellCenter2D 공식 (수정 후) — rack.left + bayCenter
|
|
27
|
+
function cellCenter2D_NEW(
|
|
28
|
+
cellId: string,
|
|
29
|
+
rack: { left: number; top: number; width: number; height: number; bays: number }
|
|
30
|
+
) {
|
|
31
|
+
const bayIdx = Number(cellId.split('-')[0] ?? 0)
|
|
32
|
+
const bayWidth = rack.width / rack.bays
|
|
33
|
+
return {
|
|
34
|
+
x: rack.left + bayIdx * bayWidth + bayWidth / 2,
|
|
35
|
+
y: rack.top + rack.height / 2
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── 이전 (결함) cellCenter2D 공식 — rack.left/top 미포함
|
|
40
|
+
function cellCenter2D_OLD(
|
|
41
|
+
cellId: string,
|
|
42
|
+
rack: { left: number; top: number; width: number; height: number; bays: number }
|
|
43
|
+
) {
|
|
44
|
+
const bayIdx = Number(cellId.split('-')[0] ?? 0)
|
|
45
|
+
const bayWidth = rack.width / rack.bays
|
|
46
|
+
return {
|
|
47
|
+
x: bayIdx * bayWidth + bayWidth / 2,
|
|
48
|
+
y: rack.height / 2
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('SlotTarget.center 의 좌표 컨벤션 — pickup vs place 의 일관성', () => {
|
|
53
|
+
|
|
54
|
+
// 사용자의 실 환경 값 — rackState left=365.17 top=478.6 W=800 D=800 H=50 bays=20
|
|
55
|
+
const rack = { left: 365.17, top: 478.6, width: 800, height: 50, bays: 20 }
|
|
56
|
+
|
|
57
|
+
it('parcel.center === bay 0 의 hypothetical "real cell" .center (둘 다 layer 좌표)', () => {
|
|
58
|
+
// 가상의 *진짜 cell 컴포넌트* 가 있다고 가정 — 그 bounds 는 rack.left + bayLeft, rack.top
|
|
59
|
+
// bay 0 의 bounds = { left: rack.left + 0*bayWidth, top: rack.top, width: bayWidth, height: rack.height }
|
|
60
|
+
const bayWidth = rack.width / rack.bays
|
|
61
|
+
const realCellBounds = { left: rack.left + 0 * bayWidth, top: rack.top, width: bayWidth, height: rack.height }
|
|
62
|
+
const realCellCenter = componentCenter(realCellBounds)
|
|
63
|
+
|
|
64
|
+
// 이게 SlotTarget.center 가 *반환해야 할 값* — 진짜 cell 컴포넌트 와 동일.
|
|
65
|
+
realCellCenter.x.should.equal(rack.left + bayWidth / 2) // = 365.17 + 20 = 385.17
|
|
66
|
+
realCellCenter.y.should.equal(rack.top + rack.height / 2) // = 478.6 + 25 = 503.6
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('NEW cellCenter2D === hypothetical real cell .center (일관성)', () => {
|
|
70
|
+
const bayWidth = rack.width / rack.bays
|
|
71
|
+
const realCellCenter = componentCenter({
|
|
72
|
+
left: rack.left + 0 * bayWidth, top: rack.top, width: bayWidth, height: rack.height
|
|
73
|
+
})
|
|
74
|
+
const slotCenter = cellCenter2D_NEW('0-0-0', rack)
|
|
75
|
+
|
|
76
|
+
slotCenter.x.should.equal(realCellCenter.x)
|
|
77
|
+
slotCenter.y.should.equal(realCellCenter.y)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('OLD cellCenter2D ≠ hypothetical real cell .center (결함 재현)', () => {
|
|
81
|
+
const bayWidth = rack.width / rack.bays
|
|
82
|
+
const realCellCenter = componentCenter({
|
|
83
|
+
left: rack.left + 0 * bayWidth, top: rack.top, width: bayWidth, height: rack.height
|
|
84
|
+
})
|
|
85
|
+
const slotCenter = cellCenter2D_OLD('0-0-0', rack)
|
|
86
|
+
|
|
87
|
+
// OLD 는 rack.left, rack.top 미포함이므로 *rack.left, rack.top 만큼* 차이.
|
|
88
|
+
slotCenter.x.should.not.equal(realCellCenter.x)
|
|
89
|
+
slotCenter.y.should.not.equal(realCellCenter.y)
|
|
90
|
+
Math.abs(slotCenter.x - realCellCenter.x).should.be.approximately(rack.left, 0.001)
|
|
91
|
+
Math.abs(slotCenter.y - realCellCenter.y).should.be.approximately(rack.top, 0.001)
|
|
92
|
+
// 차이가 *정확히 rack 의 left/top* — 이것이 사용자가 본 "완전히 어긋남" 의 크기 (≈ 365)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('NEW cellCenter2D: 다양한 bay/level 에 대해 layer 좌표 일관성', () => {
|
|
96
|
+
for (let b = 0; b < rack.bays; b++) {
|
|
97
|
+
const bayWidth = rack.width / rack.bays
|
|
98
|
+
const realCenter = componentCenter({
|
|
99
|
+
left: rack.left + b * bayWidth, top: rack.top, width: bayWidth, height: rack.height
|
|
100
|
+
})
|
|
101
|
+
const slotCenter = cellCenter2D_NEW(`${b}-0-0`, rack)
|
|
102
|
+
slotCenter.x.should.equal(realCenter.x)
|
|
103
|
+
slotCenter.y.should.equal(realCenter.y)
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('어긋남의 크기 = rack.left (= 365.17) — pickup 정확, place ≈ -365 X 어긋남', () => {
|
|
108
|
+
// 사용자 환경에서 OLD 가 만든 어긋남 크기.
|
|
109
|
+
const oldCenter = cellCenter2D_OLD('0-0-0', rack)
|
|
110
|
+
const newCenter = cellCenter2D_NEW('0-0-0', rack)
|
|
111
|
+
const xShift = newCenter.x - oldCenter.x // 365.17
|
|
112
|
+
xShift.should.be.approximately(rack.left, 0.001)
|
|
113
|
+
// 즉 OLD 는 *bay 0 의 시각 위치 (= 385.17)* 가 아닌 *(20, 25) — rack 의 왼쪽 위쪽 가까운 점*
|
|
114
|
+
// 으로 Crane 을 보냄. fork 가 rack 자체에서 벗어남.
|
|
115
|
+
})
|
|
116
|
+
})
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Plan A — SlotTarget 의 Mover/Crane 인터페이스 호환성 검증.
|
|
3
|
+
*
|
|
4
|
+
* SlotTarget 은 Mover.pickAndPlace 의 dest 로 들어가며, framework 가 다음 surface 를
|
|
5
|
+
* 호출함:
|
|
6
|
+
* - target.state — Crane.engage (cellId 식별)
|
|
7
|
+
* - target.center — Mover.moveTo 의 2D path
|
|
8
|
+
* - target._realObject.object3d — getWorldPose / projectToScreen
|
|
9
|
+
* - target.parent — legacy 경로
|
|
10
|
+
* - target.canReceive — Mover.place dispatch
|
|
11
|
+
* - target.receive — Mover.place dispatch
|
|
12
|
+
* - target.attachPointFor — Carriable.applyHolderAttachPoint
|
|
13
|
+
*
|
|
14
|
+
* SlotTarget 이 holder 의 SlottedHolder API 에 정확히 위임하는지를 격리 검증.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import 'should'
|
|
18
|
+
import * as THREE from 'three'
|
|
19
|
+
import { SlotTarget } from '../../scene-base/src/slotted-holder.js'
|
|
20
|
+
|
|
21
|
+
// ── Mock holder — SlottedHolder 컨트랙 구현 ─────────────────────────────────
|
|
22
|
+
|
|
23
|
+
class MockHolder {
|
|
24
|
+
parent: any = { tag: 'parent-scene' }
|
|
25
|
+
center = { x: 100, y: 200 }
|
|
26
|
+
state: any = { type: 'mock-holder' }
|
|
27
|
+
|
|
28
|
+
_slotAnchors = new Map<string, THREE.Object3D>()
|
|
29
|
+
_receivedCalls: any[] = []
|
|
30
|
+
_canReceiveResult = true
|
|
31
|
+
_attachCenters = new Map<string, { x: number; y: number }>()
|
|
32
|
+
|
|
33
|
+
hasCarrierAt(_slotId: string): boolean { return false }
|
|
34
|
+
obtainCarrier(_slotId: string): any { return null }
|
|
35
|
+
canReceiveAt(_slotId: string, _carrier?: any): boolean { return this._canReceiveResult }
|
|
36
|
+
async receiveAt(slotId: string, carrier: any, options?: any): Promise<void> {
|
|
37
|
+
this._receivedCalls.push({ slotId, carrier, options })
|
|
38
|
+
}
|
|
39
|
+
recordFromCarrier(_carrier: any, slotId: string): any {
|
|
40
|
+
return { cellId: slotId }
|
|
41
|
+
}
|
|
42
|
+
getSlotAttachObject3d(slotId: string): THREE.Object3D | undefined {
|
|
43
|
+
let obj = this._slotAnchors.get(slotId)
|
|
44
|
+
if (!obj) {
|
|
45
|
+
obj = new THREE.Object3D()
|
|
46
|
+
obj.name = `mock-slot:${slotId}`
|
|
47
|
+
this._slotAnchors.set(slotId, obj)
|
|
48
|
+
}
|
|
49
|
+
return obj
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
cellCenter2D(slotId: string): { x: number; y: number } | null {
|
|
53
|
+
return this._attachCenters.get(slotId) ?? null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
slotTargetAt(slotId: string): SlotTarget {
|
|
57
|
+
return new SlotTarget(this as any, slotId)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Group 1: state ───────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
describe('SlotTarget: state — cellId/slotId 노출', () => {
|
|
64
|
+
it('state.cellId 와 state.slotId 둘 다 slotId 와 일치', () => {
|
|
65
|
+
const h = new MockHolder()
|
|
66
|
+
const st = h.slotTargetAt('5-0-7')
|
|
67
|
+
st.state.cellId.should.equal('5-0-7')
|
|
68
|
+
st.state.slotId.should.equal('5-0-7')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('state.type 은 slot-target — 다른 컴포넌트 type 과 구분', () => {
|
|
72
|
+
const h = new MockHolder()
|
|
73
|
+
const st = h.slotTargetAt('A')
|
|
74
|
+
st.state.type.should.equal('slot-target')
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// ── Group 2: center — 2D path planning ──────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
describe('SlotTarget: center — holder.cellCenter2D 로 위임', () => {
|
|
81
|
+
it('holder.cellCenter2D 가 값 반환 → SlotTarget.center 가 그걸 그대로 반환', () => {
|
|
82
|
+
const h = new MockHolder()
|
|
83
|
+
h._attachCenters.set('A', { x: 555, y: 777 })
|
|
84
|
+
const st = h.slotTargetAt('A')
|
|
85
|
+
st.center.should.deepEqual({ x: 555, y: 777 })
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('cellCenter2D 가 null → holder.center 로 fallback', () => {
|
|
89
|
+
const h = new MockHolder()
|
|
90
|
+
h.center = { x: 42, y: 84 }
|
|
91
|
+
// cellCenter2D 에 등록 안 함 → null 반환
|
|
92
|
+
const st = h.slotTargetAt('未-등록')
|
|
93
|
+
st.center.should.deepEqual({ x: 42, y: 84 })
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// ── Group 3: _realObject.object3d — getWorldPose 호환 ───────────────────────
|
|
98
|
+
|
|
99
|
+
describe('SlotTarget: _realObject.object3d — getSlotAttachObject3d 위임', () => {
|
|
100
|
+
it('object3d 반환은 holder.getSlotAttachObject3d 와 동일 인스턴스', () => {
|
|
101
|
+
const h = new MockHolder()
|
|
102
|
+
const st = h.slotTargetAt('A')
|
|
103
|
+
const obj = st._realObject.object3d
|
|
104
|
+
obj!.should.equal(h.getSlotAttachObject3d('A'))
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('singleton — 같은 slotId 두 번 호출도 같은 object3d', () => {
|
|
108
|
+
const h = new MockHolder()
|
|
109
|
+
const st1 = h.slotTargetAt('A')
|
|
110
|
+
const st2 = h.slotTargetAt('A')
|
|
111
|
+
st1._realObject.object3d!.should.equal(st2._realObject.object3d!)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('서로 다른 slotId → 서로 다른 object3d', () => {
|
|
115
|
+
const h = new MockHolder()
|
|
116
|
+
const a = h.slotTargetAt('A')._realObject.object3d
|
|
117
|
+
const b = h.slotTargetAt('B')._realObject.object3d
|
|
118
|
+
;(a !== b).should.be.true()
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// ── Group 4: canReceive / receive — Transferable 인터페이스 ──────────────────
|
|
123
|
+
|
|
124
|
+
describe('SlotTarget: canReceive / receive — holder 에 위임', () => {
|
|
125
|
+
it('canReceive 가 holder.canReceiveAt 그대로 반환', () => {
|
|
126
|
+
const h = new MockHolder()
|
|
127
|
+
h._canReceiveResult = true
|
|
128
|
+
h.slotTargetAt('A').canReceive({} as any).should.be.true()
|
|
129
|
+
h._canReceiveResult = false
|
|
130
|
+
h.slotTargetAt('A').canReceive({} as any).should.be.false()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('receive 가 holder.receiveAt 으로 위임 (slotId 와 carrier 전달)', async () => {
|
|
134
|
+
const h = new MockHolder()
|
|
135
|
+
const carrier = { tag: 'carrier-X' }
|
|
136
|
+
await h.slotTargetAt('C-0-0').receive(carrier as any, { dur: 200 })
|
|
137
|
+
h._receivedCalls.length.should.equal(1)
|
|
138
|
+
h._receivedCalls[0].slotId.should.equal('C-0-0')
|
|
139
|
+
h._receivedCalls[0].carrier.should.equal(carrier)
|
|
140
|
+
h._receivedCalls[0].options.dur.should.equal(200)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// ── Group 5: attachPointFor — Carriable.applyHolderAttachPoint 호환 ───────────
|
|
145
|
+
|
|
146
|
+
describe('SlotTarget: attachPointFor — slot anchor object3d 가 attach frame', () => {
|
|
147
|
+
it('attachPointFor 반환의 .attach 가 _realObject.object3d 와 동일', () => {
|
|
148
|
+
const h = new MockHolder()
|
|
149
|
+
const st = h.slotTargetAt('A')
|
|
150
|
+
const frame = st.attachPointFor({} as any)
|
|
151
|
+
frame!.attach.should.equal(st._realObject.object3d)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('holder 의 getSlotAttachObject3d 가 undefined → attachPointFor null', () => {
|
|
155
|
+
const h: any = new MockHolder()
|
|
156
|
+
h.getSlotAttachObject3d = () => undefined
|
|
157
|
+
const st = (h as MockHolder).slotTargetAt('?')
|
|
158
|
+
;(st.attachPointFor({} as any) === null).should.be.true()
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// ── Group 6: parent — legacy 경로 ────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
describe('SlotTarget: parent — holder.parent 그대로 노출', () => {
|
|
165
|
+
it('parent 는 holder.parent 와 동일', () => {
|
|
166
|
+
const h = new MockHolder()
|
|
167
|
+
const st = h.slotTargetAt('A')
|
|
168
|
+
st.parent.should.equal(h.parent)
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// ── Group 7: 동일 holder 의 여러 SlotTarget 끼리 독립성 ─────────────────────
|
|
173
|
+
|
|
174
|
+
describe('SlotTarget: 여러 인스턴스 사이의 독립성', () => {
|
|
175
|
+
it('서로 다른 slotId → 서로 다른 state/center/object3d', () => {
|
|
176
|
+
const h = new MockHolder()
|
|
177
|
+
h._attachCenters.set('A', { x: 1, y: 1 })
|
|
178
|
+
h._attachCenters.set('B', { x: 99, y: 99 })
|
|
179
|
+
|
|
180
|
+
const sA = h.slotTargetAt('A')
|
|
181
|
+
const sB = h.slotTargetAt('B')
|
|
182
|
+
|
|
183
|
+
sA.state.cellId.should.equal('A')
|
|
184
|
+
sB.state.cellId.should.equal('B')
|
|
185
|
+
sA.center.should.deepEqual({ x: 1, y: 1 })
|
|
186
|
+
sB.center.should.deepEqual({ x: 99, y: 99 })
|
|
187
|
+
;(sA._realObject.object3d !== sB._realObject.object3d).should.be.true()
|
|
188
|
+
})
|
|
189
|
+
})
|