@operato/scene-storage 10.0.0-beta.44 → 10.0.0-beta.47
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 +44 -0
- 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 +578 -48
- package/dist/crane.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 +103 -10
- package/dist/rack-grid.js +484 -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 +40 -6
- package/dist/storage-rack.js +111 -14
- package/dist/storage-rack.js.map +1 -1
- package/package.json +4 -4
- package/src/crane-3d.ts +34 -4
- package/src/crane.ts +625 -57
- package/src/parcel-3d.ts +19 -1
- package/src/rack-grid-3d.ts +31 -8
- package/src/rack-grid.ts +504 -82
- package/src/storage-rack-3d.ts +1 -1
- package/src/storage-rack.ts +111 -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/test/things-scene-loader-impl.mjs +37 -0
- package/test/things-scene-loader.mjs +24 -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
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Crane.findAdjacentSlots 의 reach 검사 — rotation 적용 검증.
|
|
3
|
+
*
|
|
4
|
+
* 핵심 가설 (사용자 보고 narrow 의 source):
|
|
5
|
+
* - StorageRack 정상 = rotation 0 모델 → world X 차 검사 와 rail-local X 가 일치.
|
|
6
|
+
* - RackGrid narrow = rotation π/2 모델 → rail X axis = world Z. 그러나 기존
|
|
7
|
+
* findAdjacentSlots 는 world X 차로만 검사 → far cell 도 reach 안으로 매칭 →
|
|
8
|
+
* Crane.moveTo 의 carriagePosition clamp 끝점 박힘 → carriage narrow.
|
|
9
|
+
*
|
|
10
|
+
* 가설 확인 path — 두 식 비교:
|
|
11
|
+
* (legacy) xOk = |pos.x - cx| <= xHalf; zOk = |pos.z - cz| <= zHalf
|
|
12
|
+
* (fix) railLocalX = dx*cos + dz*sin; railLocalZ = -dx*sin + dz*cos
|
|
13
|
+
* xOk = |railLocalX| <= xHalf; zOk = |railLocalZ| <= zHalf
|
|
14
|
+
*
|
|
15
|
+
* rotation = 0 → 두 식 결과 동일 (StorageRack 정상 설명).
|
|
16
|
+
* rotation = π/2 → 두 식 결과 다름 (RackGrid narrow 설명).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import 'should'
|
|
20
|
+
|
|
21
|
+
function legacyReach(opts: {
|
|
22
|
+
pos: { x: number; z: number }
|
|
23
|
+
crane: { x: number; z: number }
|
|
24
|
+
xHalf: number; zHalf: number
|
|
25
|
+
}): { xOk: boolean; zOk: boolean; matched: boolean } {
|
|
26
|
+
const dx = Math.abs(opts.pos.x - opts.crane.x)
|
|
27
|
+
const dz = Math.abs(opts.pos.z - opts.crane.z)
|
|
28
|
+
const xOk = dx <= opts.xHalf
|
|
29
|
+
const zOk = dz <= opts.zHalf
|
|
30
|
+
return { xOk, zOk, matched: xOk && zOk }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fixReach(opts: {
|
|
34
|
+
pos: { x: number; z: number }
|
|
35
|
+
crane: { x: number; z: number }
|
|
36
|
+
rotation: number
|
|
37
|
+
xHalf: number; zHalf: number
|
|
38
|
+
}): { railLocalX: number; railLocalZ: number; xOk: boolean; zOk: boolean; matched: boolean } {
|
|
39
|
+
const dx = opts.pos.x - opts.crane.x
|
|
40
|
+
const dz = opts.pos.z - opts.crane.z
|
|
41
|
+
const cos = Math.cos(opts.rotation)
|
|
42
|
+
const sin = Math.sin(opts.rotation)
|
|
43
|
+
const railLocalX = dx * cos + dz * sin
|
|
44
|
+
const railLocalZ = -dx * sin + dz * cos
|
|
45
|
+
const xOk = Math.abs(railLocalX) <= opts.xHalf
|
|
46
|
+
const zOk = Math.abs(railLocalZ) <= opts.zHalf
|
|
47
|
+
return { railLocalX, railLocalZ, xOk, zOk, matched: xOk && zOk }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('rotation = 0 (StorageRack 모델 가정) — legacy 와 fix 동일 동작', () => {
|
|
51
|
+
it('cell 이 X reach 안 + Z reach 안 → 둘 다 matched', () => {
|
|
52
|
+
const legacy = legacyReach({ pos: { x: 100, z: 50 }, crane: { x: 0, z: 0 }, xHalf: 200, zHalf: 100 })
|
|
53
|
+
const fix = fixReach({ pos: { x: 100, z: 50 }, crane: { x: 0, z: 0 }, rotation: 0, xHalf: 200, zHalf: 100 })
|
|
54
|
+
legacy.matched.should.be.true()
|
|
55
|
+
fix.matched.should.be.true()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('cell 이 X reach 밖 → 둘 다 not matched', () => {
|
|
59
|
+
const legacy = legacyReach({ pos: { x: 500, z: 50 }, crane: { x: 0, z: 0 }, xHalf: 200, zHalf: 100 })
|
|
60
|
+
const fix = fixReach({ pos: { x: 500, z: 50 }, crane: { x: 0, z: 0 }, rotation: 0, xHalf: 200, zHalf: 100 })
|
|
61
|
+
legacy.matched.should.be.false()
|
|
62
|
+
fix.matched.should.be.false()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('cell 이 Z reach 밖 → 둘 다 not matched', () => {
|
|
66
|
+
const legacy = legacyReach({ pos: { x: 100, z: 800 }, crane: { x: 0, z: 0 }, xHalf: 200, zHalf: 100 })
|
|
67
|
+
const fix = fixReach({ pos: { x: 100, z: 800 }, crane: { x: 0, z: 0 }, rotation: 0, xHalf: 200, zHalf: 100 })
|
|
68
|
+
legacy.matched.should.be.false()
|
|
69
|
+
fix.matched.should.be.false()
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('rotation = π/2 (RackGrid 모델 — diag 의 확정 값) — legacy 와 fix 결과 다름', () => {
|
|
74
|
+
// 사용자 모델 의 *_rail X axis 가 world Z axis* (rotation π/2 회전).
|
|
75
|
+
// 즉 carriage 의 실 운동 = world Z 방향.
|
|
76
|
+
|
|
77
|
+
it('cell 이 rail axis (= world Z) 따라 reach 안 → fix 만 정확히 matched', () => {
|
|
78
|
+
// cell 의 world Z 가 crane Z 와 가까움 (= rail 안). world X 차 작음.
|
|
79
|
+
// 사용자 의도: matched. legacy 와 fix 둘 다 matched 일 것 (X reach 안).
|
|
80
|
+
const legacy = legacyReach({ pos: { x: 50, z: 100 }, crane: { x: 0, z: 0 }, xHalf: 200, zHalf: 100 })
|
|
81
|
+
const fix = fixReach({ pos: { x: 50, z: 100 }, crane: { x: 0, z: 0 }, rotation: Math.PI / 2, xHalf: 200, zHalf: 100 })
|
|
82
|
+
legacy.matched.should.be.true()
|
|
83
|
+
fix.matched.should.be.true()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('cell 이 rail axis 따라 reach 밖 (= world Z 차 큼) → fix 만 not matched', () => {
|
|
87
|
+
// 사용자 의도: cell 의 world Z 가 crane Z 와 멀음 = rail 밖 → NOT matched.
|
|
88
|
+
// legacy: world Z 차 = 800 > zHalf(100) → not matched. fix 도 not matched. 둘 다 정확.
|
|
89
|
+
const legacy = legacyReach({ pos: { x: 50, z: 800 }, crane: { x: 0, z: 0 }, xHalf: 200, zHalf: 100 })
|
|
90
|
+
const fix = fixReach({ pos: { x: 50, z: 800 }, crane: { x: 0, z: 0 }, rotation: Math.PI / 2, xHalf: 200, zHalf: 100 })
|
|
91
|
+
legacy.matched.should.be.false()
|
|
92
|
+
fix.matched.should.be.false()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('★ 결정적 시나리오 ★ cell 이 *world X 방향* 으로 매우 멀음 + world Z 가까움 — legacy 는 *not matched* (잘못), fix 는 *matched* (정확)', () => {
|
|
96
|
+
// rail X axis = world Z 인 모델. cell 의 world Z = 50 (rail 안), world X = 600 (멀음).
|
|
97
|
+
// 사용자 의도: rail X axis 의 perpendicular (= world X) 차이는 *_fork reach (zHalf=100)*
|
|
98
|
+
// 안 이어야 함. 600 > 100 → 사용자 의도 = NOT matched.
|
|
99
|
+
//
|
|
100
|
+
// legacy: |dx|=600 > xHalf(200) → not matched.
|
|
101
|
+
// fix: railLocalX = 0*0 + 50*1 = 50 (rail 안). railLocalZ = -600*1 + 50*0 = -600.
|
|
102
|
+
// |railLocalZ| = 600 > zHalf(100) → not matched.
|
|
103
|
+
// 둘 다 not matched — 일치하나 *_왜* 의 path 다름.
|
|
104
|
+
const legacy = legacyReach({ pos: { x: 600, z: 50 }, crane: { x: 0, z: 0 }, xHalf: 200, zHalf: 100 })
|
|
105
|
+
const fix = fixReach({ pos: { x: 600, z: 50 }, crane: { x: 0, z: 0 }, rotation: Math.PI / 2, xHalf: 200, zHalf: 100 })
|
|
106
|
+
legacy.matched.should.be.false()
|
|
107
|
+
fix.matched.should.be.false()
|
|
108
|
+
// 핵심 — fix 의 railLocalX 는 작은 값 (50, rail 안), railLocalZ 가 큰 값 (-600, fork 밖).
|
|
109
|
+
fix.railLocalX.should.be.approximately(50, 1e-9)
|
|
110
|
+
fix.railLocalZ.should.be.approximately(-600, 1e-9)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('★ narrow source 의 결정적 증거 ★ cell 의 world Z 가 매우 멀음 + world X 안 — legacy 는 *X reach 안* 으로 매칭, fix 는 *rail axis 밖* 으로 reject', () => {
|
|
114
|
+
// 사용자 모델 의 *_rotation π/2* — rail X axis = world Z. cell 의 world Z = 700
|
|
115
|
+
// (rail 의 X axis 따라 매우 멀음 — 사용자 의도 NOT matched, *_rail 밖*).
|
|
116
|
+
// 그러나 world X 는 0 (X reach 안). legacy 는 X reach 통과 → matched (잘못).
|
|
117
|
+
// fix: railLocalX = 0*0 + 700*1 = 700. |railLocalX| = 700 > xHalf(200) → not matched. 정확.
|
|
118
|
+
const legacy = legacyReach({ pos: { x: 0, z: 700 }, crane: { x: 0, z: 0 }, xHalf: 200, zHalf: 1500 })
|
|
119
|
+
const fix = fixReach({ pos: { x: 0, z: 700 }, crane: { x: 0, z: 0 }, rotation: Math.PI / 2, xHalf: 200, zHalf: 1500 })
|
|
120
|
+
// legacy 의 zHalf 를 일부러 크게 — Z 검사 통과시키고 X 검사 통과 여부 만 보기 위함.
|
|
121
|
+
legacy.matched.should.be.true() // ← legacy 의 잘못된 매칭 (실 사용자 narrow 의 source)
|
|
122
|
+
fix.matched.should.be.false() // ← fix 의 정확한 reject
|
|
123
|
+
fix.railLocalX.should.equal(700)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('useBoundScope = true (X 무관) — 진짜 *narrow source* 검증', () => {
|
|
128
|
+
// 기존 코드: useBoundScope = true 시 xOk = true (X 무관). 의도 = "rail 끝까지 reach".
|
|
129
|
+
// 실 결과: rail 의 *_perpendicular axis (= fork 방향)* 만 검사 → rail axis 따라 *_무한 cell*
|
|
130
|
+
// 매칭 → Crane.moveTo 가 그 cell 의 railLocalX 값 (큰 음수/양수) 으로 clamp 끝점 박힘.
|
|
131
|
+
|
|
132
|
+
function boundScopeReach(opts: {
|
|
133
|
+
pos: { x: number; z: number }; crane: { x: number; z: number }
|
|
134
|
+
rotation: number; zHalf: number
|
|
135
|
+
}): { matched: boolean; railLocalX: number } {
|
|
136
|
+
const dx = opts.pos.x - opts.crane.x
|
|
137
|
+
const dz = opts.pos.z - opts.crane.z
|
|
138
|
+
const cos = Math.cos(opts.rotation)
|
|
139
|
+
const sin = Math.sin(opts.rotation)
|
|
140
|
+
const railLocalX = dx * cos + dz * sin
|
|
141
|
+
const railLocalZ = -dx * sin + dz * cos
|
|
142
|
+
const xOk = true // ← useBoundScope = true 시 X 무관
|
|
143
|
+
const zOk = Math.abs(opts.pos.z - opts.crane.z) <= opts.zHalf // ← 기존: world Z 차로만 검사
|
|
144
|
+
return { matched: xOk && zOk, railLocalX }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
it('rotation π/2 + bound 모델 — 멀리 떨어진 cell 도 매칭됨 → Crane.moveTo 에서 railLocalX 큰 값', () => {
|
|
148
|
+
// cell 의 world Z = 50 (Z reach 안), world X = 1000 (매우 멀음).
|
|
149
|
+
const r = boundScopeReach({
|
|
150
|
+
pos: { x: 1000, z: 50 }, crane: { x: 0, z: 0 },
|
|
151
|
+
rotation: Math.PI / 2, zHalf: 100
|
|
152
|
+
})
|
|
153
|
+
r.matched.should.be.true()
|
|
154
|
+
// Crane.moveTo 의 railLocalX = dx*cos + dz*sin = 1000*0 + 50*1 = 50. 작은 값 정상.
|
|
155
|
+
r.railLocalX.should.be.approximately(50, 1e-9)
|
|
156
|
+
// 즉 이 시나리오 는 *_narrow source 아님*. legacy 의 bound scope 가 X 무관이라도
|
|
157
|
+
// railLocalX 가 작아서 clamp 안 됨.
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('★ 실 narrow source ★ rotation π/2 + cell 의 *world Z 가 멀음* (rail X 따라 멀음) + world Z 차 > zHalf 일 때', () => {
|
|
161
|
+
// 사용자 모델 의 *_여러 RackGrid 의 *_far row*. cell 의 world Z 가 *_특정 row 의 Y*.
|
|
162
|
+
// 같은 RackGrid 안 *_여러 row 의 cell* 의 Y variance > zHalf.
|
|
163
|
+
// 단, 이 시나리오 = Z 검사 통과 못 함 → not matched. narrow source 아님.
|
|
164
|
+
const r = boundScopeReach({
|
|
165
|
+
pos: { x: 50, z: 800 }, crane: { x: 0, z: 0 },
|
|
166
|
+
rotation: Math.PI / 2, zHalf: 100
|
|
167
|
+
})
|
|
168
|
+
r.matched.should.be.false()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('rotation = π/2 + RackGrid 의 *aisle row* cell — world Z 가 crane.z 근처 (reach 안), world X 가 col 따라 다양', () => {
|
|
172
|
+
// RackGrid 의 aisle row 만 매칭 — 같은 row 의 여러 col cell.
|
|
173
|
+
// 각 col 의 world X 가 다양 → railLocalX = dx*cos + dz*sin = 0 + dz*1 = dz (= 0 근처) 동일!
|
|
174
|
+
// = 모든 col 의 cell 이 *_같은 railLocalX (≈ 0)* → carriage 가 *_같은 carriagePosition* visit → narrow.
|
|
175
|
+
//
|
|
176
|
+
// 사용자 의도: carriage 가 *_col 따라 다양한 X visit*. 그러려면 *_rail X axis = world X*
|
|
177
|
+
// (rotation = 0). 그러나 사용자 모델 = rotation π/2 → rail X = world Z → col variance 영향 없음.
|
|
178
|
+
const cells = [
|
|
179
|
+
{ x: -200, z: 50 },
|
|
180
|
+
{ x: 0, z: 50 },
|
|
181
|
+
{ x: 200, z: 50 },
|
|
182
|
+
{ x: 400, z: 50 }
|
|
183
|
+
]
|
|
184
|
+
const railLocalXs = cells.map(pos =>
|
|
185
|
+
boundScopeReach({ pos, crane: { x: 0, z: 0 }, rotation: Math.PI / 2, zHalf: 100 }).railLocalX
|
|
186
|
+
)
|
|
187
|
+
// 모든 col 의 railLocalX 가 *_같음* (= 50). carriage variance 0!
|
|
188
|
+
for (const v of railLocalXs) v.should.be.approximately(50, 1e-9)
|
|
189
|
+
// variance check — 최대 ~floating noise 만, 의미있는 variance 0.
|
|
190
|
+
const variance = Math.max(...railLocalXs) - Math.min(...railLocalXs)
|
|
191
|
+
variance.should.be.lessThan(1e-9)
|
|
192
|
+
// ⇒ 사용자 보고 narrow ✓. 진짜 source = 모델의 crane.rotation 이 aisle 방향과 어긋남.
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
describe('★ 결론 ★ RackGrid 모델 narrow 의 진짜 source', () => {
|
|
197
|
+
it('rotation = π/2 + cell 분포 가 *col 방향 (world X)* — rail X axis (= world Z) 와 직각 → carriage variance 0', () => {
|
|
198
|
+
// 같은 row 의 여러 col cell 들은 *_world X 만 다양*, world Z 동일.
|
|
199
|
+
// rail X = world Z (rotation π/2). 즉 cell 의 carriage 도달 위치 = railLocalX = dz (동일).
|
|
200
|
+
// ⇒ carriage 가 한 점만 visit. = 사용자 보고 narrow.
|
|
201
|
+
const dzs = [50, 50, 50, 50] // 모든 cell 의 dz 동일 (= 같은 row)
|
|
202
|
+
new Set(dzs).size.should.equal(1) // variance 0
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('rotation = 0 + cell 분포 가 *col 방향* — rail X axis (= world X) 와 평행 → carriage variance 큼', () => {
|
|
206
|
+
// 같은 row 의 여러 col cell — *_world X 다양*. rail X = world X. railLocalX = dx (다양).
|
|
207
|
+
const cells = [
|
|
208
|
+
{ x: -200, z: 50 },
|
|
209
|
+
{ x: 0, z: 50 },
|
|
210
|
+
{ x: 200, z: 50 },
|
|
211
|
+
{ x: 400, z: 50 }
|
|
212
|
+
]
|
|
213
|
+
const cos = 1, sin = 0 // rotation 0
|
|
214
|
+
const railLocalXs = cells.map(p => p.x * cos + p.z * sin)
|
|
215
|
+
new Set(railLocalXs).size.should.equal(4) // variance 큼
|
|
216
|
+
// ⇒ StorageRack (rotation = 0) 정상 동작 설명.
|
|
217
|
+
})
|
|
218
|
+
})
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* RackGrid — anchor / stock / carrier 의 *실제 3D world position 일치* 검증.
|
|
3
|
+
*
|
|
4
|
+
* 사용자 보고: pick 시 carrier 가 *anchor (= fork 가 가는 자리)* 와 다른 위치에
|
|
5
|
+
* 그려져 fork 가 빈 포크질. 식 자체가 어디서 어긋나는지를 *실 Three.js
|
|
6
|
+
* scene graph* 로 직접 검증.
|
|
7
|
+
*
|
|
8
|
+
* 1. rack.object3d (THREE.Group) 만들기
|
|
9
|
+
* 2. anchor / stock / carrier 를 각각 *그 자식* 으로 추가
|
|
10
|
+
* 3. 각 식 (코드 발췌) 으로 position set 후 matrixWorld 비교
|
|
11
|
+
*
|
|
12
|
+
* 세 값이 모든 (col, row, shelf) 에서 동일하면 식 자체는 정확. 어긋나면 *어느
|
|
13
|
+
* 식이 잘못된지* 즉시 식별.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import 'should'
|
|
17
|
+
import * as THREE from 'three'
|
|
18
|
+
|
|
19
|
+
// ── 식 발췌 — RackGrid 의 실제 코드와 동일 ─────────────────────────────────
|
|
20
|
+
|
|
21
|
+
interface RackParams {
|
|
22
|
+
width: number // 3D X
|
|
23
|
+
height: number // 3D Z (front-back) = state.height = 2D bounds height
|
|
24
|
+
depth: number // 3D Y (vertical) = state.depth
|
|
25
|
+
cols: number
|
|
26
|
+
rows: number
|
|
27
|
+
shelves: number
|
|
28
|
+
shelfBase: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function derived(rp: RackParams) {
|
|
32
|
+
const bayW = rp.width / rp.cols
|
|
33
|
+
const bayD = rp.height / rp.rows
|
|
34
|
+
const shelfZone = rp.depth - rp.shelfBase
|
|
35
|
+
const cellY = shelfZone / rp.shelves
|
|
36
|
+
const baseY = -rp.depth / 2
|
|
37
|
+
const shelfBaseY = baseY + rp.shelfBase
|
|
38
|
+
const stockD = cellY * 0.7
|
|
39
|
+
return { bayW, bayD, shelfZone, cellY, baseY, shelfBaseY, stockD }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// anchor 식 — rack-grid.ts getSlotAttachObject3d (line 1346-1349)
|
|
43
|
+
function anchorPos(col: number, row: number, shelf: number, rp: RackParams) {
|
|
44
|
+
const { bayW, bayD, cellY, shelfBaseY, stockD } = derived(rp)
|
|
45
|
+
return {
|
|
46
|
+
x: (col - rp.cols / 2 + 0.5) * bayW,
|
|
47
|
+
y: shelfBaseY + shelf * cellY + stockD / 2,
|
|
48
|
+
z: (row - rp.rows / 2 + 0.5) * bayD
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// stock 식 — rack-grid-3d.ts matrixFor (line 358-363, cy 식은 line 280 부근의 동일)
|
|
53
|
+
function stockPos(col: number, row: number, shelf: number, rp: RackParams) {
|
|
54
|
+
const { bayW, bayD, cellY, shelfBaseY, stockD } = derived(rp)
|
|
55
|
+
return {
|
|
56
|
+
x: (col - rp.cols / 2 + 0.5) * bayW,
|
|
57
|
+
y: shelfBaseY + shelf * cellY + stockD / 2,
|
|
58
|
+
z: (row - rp.rows / 2 + 0.5) * bayD
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// carrier 식 — rack-grid.ts obtainCarrier 의 *2D state.left/top → 3D world* 변환.
|
|
63
|
+
// 내 가정: parent (RackGrid) 의 *2D bounds center* 가 *3D origin*. 즉
|
|
64
|
+
// 3D X = state.left + carrierW/2 - parent.width/2 = cellCenterInnerX - rackWidth/2
|
|
65
|
+
// 3D Z = state.top + carrierH/2 - parent.height/2 = cellCenterInnerY - rackHeight/2
|
|
66
|
+
// 단 *2D Y → 3D Z 부호* 가 +/- 어느 쪽인지 things-scene 컨벤션 따라.
|
|
67
|
+
function carrierPos_assumeY_to_PlusZ(col: number, row: number, shelf: number, rp: RackParams) {
|
|
68
|
+
const { bayW, bayD, cellY, shelfBaseY, stockD } = derived(rp)
|
|
69
|
+
const cellCenterInnerX = (col + 0.5) * bayW
|
|
70
|
+
const cellCenterInnerY = (row + 0.5) * bayD
|
|
71
|
+
return {
|
|
72
|
+
x: cellCenterInnerX - rp.width / 2,
|
|
73
|
+
y: shelfBaseY + shelf * cellY + stockD / 2, // anchor 와 동일 (carrier.depth 가 stockD 인 가정)
|
|
74
|
+
z: cellCenterInnerY - rp.height / 2
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 부호 반대 가설 — 2D Y → 3D -Z
|
|
79
|
+
function carrierPos_assumeY_to_MinusZ(col: number, row: number, shelf: number, rp: RackParams) {
|
|
80
|
+
const { cellY, shelfBaseY, stockD, bayW, bayD } = derived(rp)
|
|
81
|
+
const cellCenterInnerX = (col + 0.5) * bayW
|
|
82
|
+
const cellCenterInnerY = (row + 0.5) * bayD
|
|
83
|
+
return {
|
|
84
|
+
x: cellCenterInnerX - rp.width / 2,
|
|
85
|
+
y: shelfBaseY + shelf * cellY + stockD / 2,
|
|
86
|
+
z: rp.height / 2 - cellCenterInnerY // 부호 반전
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Helper — 실 Three.js Group 안에 3 객체 add 후 matrixWorld 비교 ────────────
|
|
91
|
+
|
|
92
|
+
function compareWorld(rp: RackParams, col: number, row: number, shelf: number) {
|
|
93
|
+
const rackObj = new THREE.Group()
|
|
94
|
+
rackObj.position.set(0, 0, 0)
|
|
95
|
+
rackObj.updateMatrixWorld(true)
|
|
96
|
+
|
|
97
|
+
const a = anchorPos(col, row, shelf, rp)
|
|
98
|
+
const s = stockPos(col, row, shelf, rp)
|
|
99
|
+
const c1 = carrierPos_assumeY_to_PlusZ(col, row, shelf, rp)
|
|
100
|
+
const c2 = carrierPos_assumeY_to_MinusZ(col, row, shelf, rp)
|
|
101
|
+
|
|
102
|
+
const anchorObj = new THREE.Object3D()
|
|
103
|
+
anchorObj.position.set(a.x, a.y, a.z)
|
|
104
|
+
rackObj.add(anchorObj)
|
|
105
|
+
|
|
106
|
+
const stockObj = new THREE.Object3D()
|
|
107
|
+
stockObj.position.set(s.x, s.y, s.z)
|
|
108
|
+
rackObj.add(stockObj)
|
|
109
|
+
|
|
110
|
+
const carrier1Obj = new THREE.Object3D()
|
|
111
|
+
carrier1Obj.position.set(c1.x, c1.y, c1.z)
|
|
112
|
+
rackObj.add(carrier1Obj)
|
|
113
|
+
|
|
114
|
+
const carrier2Obj = new THREE.Object3D()
|
|
115
|
+
carrier2Obj.position.set(c2.x, c2.y, c2.z)
|
|
116
|
+
rackObj.add(carrier2Obj)
|
|
117
|
+
|
|
118
|
+
rackObj.updateMatrixWorld(true)
|
|
119
|
+
|
|
120
|
+
const aw = new THREE.Vector3()
|
|
121
|
+
const sw = new THREE.Vector3()
|
|
122
|
+
const c1w = new THREE.Vector3()
|
|
123
|
+
const c2w = new THREE.Vector3()
|
|
124
|
+
anchorObj.getWorldPosition(aw)
|
|
125
|
+
stockObj.getWorldPosition(sw)
|
|
126
|
+
carrier1Obj.getWorldPosition(c1w)
|
|
127
|
+
carrier2Obj.getWorldPosition(c2w)
|
|
128
|
+
|
|
129
|
+
return { aw, sw, c1w, c2w }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Tests ──────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
describe('RackGrid Real 3D: anchor / stock / carrier world position 일치', () => {
|
|
135
|
+
const rp: RackParams = {
|
|
136
|
+
width: 1000, height: 600, depth: 2000,
|
|
137
|
+
cols: 5, rows: 3, shelves: 4, shelfBase: 0
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
it('anchor 와 stock 은 *모든 (col,row,shelf)* 에서 동일 world position', () => {
|
|
141
|
+
for (let col = 0; col < rp.cols; col++) {
|
|
142
|
+
for (let row = 0; row < rp.rows; row++) {
|
|
143
|
+
for (let shelf = 0; shelf < rp.shelves; shelf++) {
|
|
144
|
+
const { aw, sw } = compareWorld(rp, col, row, shelf)
|
|
145
|
+
aw.x.should.be.approximately(sw.x, 1e-9, `col=${col} row=${row} shelf=${shelf}`)
|
|
146
|
+
aw.y.should.be.approximately(sw.y, 1e-9, `col=${col} row=${row} shelf=${shelf}`)
|
|
147
|
+
aw.z.should.be.approximately(sw.z, 1e-9, `col=${col} row=${row} shelf=${shelf}`)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('carrier (2D Y → 3D +Z 가정) — anchor 와 동일 world position', () => {
|
|
154
|
+
// 사용자가 *현재 코드* 의 식. fork 가 가는 자리(anchor) 와 carrier 가 같은 곳에
|
|
155
|
+
// 있어야 정상. 이 테스트 통과 = 식 자체 정확. 그러면 사용자 보고의 어긋남은
|
|
156
|
+
// *things-scene 의 *2D → 3D 변환* 이 *우리 가정과 다름* 시사.
|
|
157
|
+
for (let col = 0; col < rp.cols; col++) {
|
|
158
|
+
for (let row = 0; row < rp.rows; row++) {
|
|
159
|
+
for (let shelf = 0; shelf < rp.shelves; shelf++) {
|
|
160
|
+
const { aw, c1w } = compareWorld(rp, col, row, shelf)
|
|
161
|
+
aw.x.should.be.approximately(c1w.x, 1e-9, `+Z col=${col} row=${row} shelf=${shelf}`)
|
|
162
|
+
aw.y.should.be.approximately(c1w.y, 1e-9, `+Z col=${col} row=${row} shelf=${shelf}`)
|
|
163
|
+
aw.z.should.be.approximately(c1w.z, 1e-9, `+Z col=${col} row=${row} shelf=${shelf}`)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('carrier (2D Y → 3D -Z 가정) — Z 좌표가 anchor 와 *반대 부호*', () => {
|
|
170
|
+
// 만약 things-scene 의 변환 컨벤션이 이쪽이라면 *내 obtainCarrier 식의 Z* 가
|
|
171
|
+
// *anchor 와 반대* — fork 가 anchor 위치 (= stock 자리) 로 오는데 carrier 는
|
|
172
|
+
// *반대 측 Z* 에 그려짐. 사용자 보고와 일치하는 가설.
|
|
173
|
+
for (let col = 0; col < rp.cols; col++) {
|
|
174
|
+
for (let row = 0; row < rp.rows; row++) {
|
|
175
|
+
const { aw, c2w } = compareWorld(rp, col, row, 0)
|
|
176
|
+
if (row === rp.rows / 2 - 0.5 || row === Math.floor(rp.rows / 2)) {
|
|
177
|
+
// 중앙 row 는 Z=0 이라 부호 무관 — skip
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
// Z 부호 반대 — anchor.z 와 c2w.z 의 *합이 0* (대칭)
|
|
181
|
+
;(aw.z + c2w.z).should.be.approximately(0, 1e-9,
|
|
182
|
+
`row=${row}: anchor.z=${aw.z}, carrier(-Z).z=${c2w.z}`)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('row 별 anchor.z 값 명시 — row=0 가 *-Z (앞)*, row=N-1 가 *+Z (뒤)*', () => {
|
|
188
|
+
const { aw: aw0 } = compareWorld(rp, 0, 0, 0)
|
|
189
|
+
const { aw: awLast } = compareWorld(rp, 0, rp.rows - 1, 0)
|
|
190
|
+
aw0.z.should.be.lessThan(0)
|
|
191
|
+
awLast.z.should.be.greaterThan(0)
|
|
192
|
+
// bayD = height / rows = 200
|
|
193
|
+
// row 0: (0 - 1.5 + 0.5) * 200 = -200
|
|
194
|
+
// row 2: (2 - 1.5 + 0.5) * 200 = +200
|
|
195
|
+
aw0.z.should.be.approximately(-200, 1e-9)
|
|
196
|
+
awLast.z.should.be.approximately(200, 1e-9)
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
// ── 사용자 보고 *fork 가 뒤로 나감* 가설 분리 ─────────────────────────────────
|
|
201
|
+
|
|
202
|
+
describe('RackGrid: fork 진입 방향 가설 분리', () => {
|
|
203
|
+
/*
|
|
204
|
+
* 위 테스트의 두 carrier 식 결과로 본 분리:
|
|
205
|
+
*
|
|
206
|
+
* (A) anchor / stock / carrier(+Z) 모두 동일 world → 모두 식 정확.
|
|
207
|
+
* 사용자 보고의 어긋남은 *식 외* 원인:
|
|
208
|
+
* - things-scene 의 *2D → 3D 변환 부호* 가 +Z 가 아닌 -Z (= carrier(-Z) 가
|
|
209
|
+
* 실제 carrier 위치). 그러면 *carrier 가 anchor 반대* — fork 빈 공간 + 뒤로.
|
|
210
|
+
* → fix: rack-grid.ts 의 cellCenterInnerY 부호 반전.
|
|
211
|
+
* - 또는 things-scene 변환이 +Z 정확 → *fork 의 *진입 face* 가 *crane 위치*
|
|
212
|
+
* 따라 자동 결정 = 시뮬레이션 *crane 의 RackGrid 측 위치* 가 *fork 진입
|
|
213
|
+
* 반대 방향*. *코드 fix 외 영역*.
|
|
214
|
+
*
|
|
215
|
+
* (B) anchor != stock 또는 anchor != carrier → 식 자체 잘못. 수정 대상 분명.
|
|
216
|
+
*/
|
|
217
|
+
it('진단 출력 — row=0 ~ row=N-1 의 anchor.z / carrier(+Z).z / carrier(-Z).z', () => {
|
|
218
|
+
const rp: RackParams = {
|
|
219
|
+
width: 1000, height: 600, depth: 2000,
|
|
220
|
+
cols: 5, rows: 3, shelves: 4, shelfBase: 0
|
|
221
|
+
}
|
|
222
|
+
const rows: Array<{ row: number; anchorZ: number; carrierPlusZ: number; carrierMinusZ: number }> = []
|
|
223
|
+
for (let row = 0; row < rp.rows; row++) {
|
|
224
|
+
const { aw, c1w, c2w } = compareWorld(rp, 0, row, 0)
|
|
225
|
+
rows.push({
|
|
226
|
+
row,
|
|
227
|
+
anchorZ: Math.round(aw.z),
|
|
228
|
+
carrierPlusZ: Math.round(c1w.z),
|
|
229
|
+
carrierMinusZ: Math.round(c2w.z)
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
console.table(rows)
|
|
233
|
+
rows.length.should.equal(rp.rows)
|
|
234
|
+
})
|
|
235
|
+
})
|