@operato/scene-storage 10.0.0-beta.56 → 10.0.0-beta.58
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 +33 -0
- package/dist/crane.js +13 -7
- package/dist/crane.js.map +1 -1
- package/dist/rack-grid-cell.d.ts +4 -1
- package/dist/rack-grid.js +3 -3
- package/dist/rack-grid.js.map +1 -1
- package/dist/spot.js +2 -0
- package/dist/spot.js.map +1 -1
- package/dist/stockpile-3d.d.ts +0 -5
- package/dist/stockpile-3d.js +89 -93
- package/dist/stockpile-3d.js.map +1 -1
- package/dist/stockpile-grid-3d.js +5 -1
- package/dist/stockpile-grid-3d.js.map +1 -1
- package/dist/stockpile-grid.js +3 -3
- package/dist/stockpile-grid.js.map +1 -1
- package/dist/stockpile.d.ts +12 -0
- package/dist/stockpile.js +22 -3
- package/dist/stockpile.js.map +1 -1
- package/dist/storage-rack.js +3 -3
- package/dist/storage-rack.js.map +1 -1
- package/package.json +3 -3
- package/src/crane.ts +14 -7
- package/src/rack-grid.ts +3 -3
- package/src/spot.ts +2 -0
- package/src/stockpile-3d.ts +76 -106
- package/src/stockpile-grid-3d.ts +5 -1
- package/src/stockpile-grid.ts +3 -3
- package/src/stockpile.ts +35 -4
- package/src/storage-rack.ts +3 -3
- package/tsconfig.tsbuildinfo +1 -1
package/src/stockpile-3d.ts
CHANGED
|
@@ -13,12 +13,79 @@
|
|
|
13
13
|
|
|
14
14
|
import * as THREE from 'three'
|
|
15
15
|
import { RealObjectGroup } from '@hatiolab/things-scene'
|
|
16
|
+
import { computeStackPositions } from '@operato/scene-base'
|
|
16
17
|
|
|
17
18
|
import type Stockpile from './stockpile.js'
|
|
18
19
|
import type { StackPattern, CarrierPreset } from './stockpile.js'
|
|
19
20
|
|
|
20
21
|
const PAD_DEPTH = 2
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* 그래프(root.__flowGraph)에서 이 stockpile 로 *들어오는* 노드(입구)를 찾아, 적치를 *입구
|
|
25
|
+
* 맞은편부터* 하도록 reverseX/Z 를 산출한다. storage 가 conveyance 를 import 하지 않고
|
|
26
|
+
* 캐시 속성만 일반적으로 읽음(토폴로지 viz 와 동일 방식). 입구/그래프 없으면 undefined →
|
|
27
|
+
* 기본(-x/-z 모서리부터). 입구 방향은 stockpile local frame 기준(회전 반영).
|
|
28
|
+
*/
|
|
29
|
+
function inflowFillOpts(stockpile: any): { advanceAlongX?: boolean; reverse?: boolean } | undefined {
|
|
30
|
+
const root = stockpile?.root
|
|
31
|
+
// 로드 직후(sim 전)엔 그래프 캐시가 없을 수 있다 — conveyance 가 globalThis 에 심은
|
|
32
|
+
// 빌더로 즉석 build(토폴로지 viz 와 동일). 없으면(컨베이어 미로드) 기본값.
|
|
33
|
+
if (root && !root.__flowGraph) {
|
|
34
|
+
try { (globalThis as any).__operatoBuildFlowGraph?.(root) } catch { /* ignore */ }
|
|
35
|
+
}
|
|
36
|
+
const cache = root?.__flowGraph
|
|
37
|
+
const out = cache?.graph?.out as Map<string, Array<string | null>> | undefined
|
|
38
|
+
const comps = cache?.components as Map<string, any> | undefined
|
|
39
|
+
// 그래프 빌더(flow-graph-coordinator collect)와 *동일* idOf — refid 는 set('refid') 로
|
|
40
|
+
// state.refid 에 저장되므로 c.refid 와 state.refid 를 둘 다 확인해야 한다. (c.refid 만 보면
|
|
41
|
+
// 명시 id 없는 stockpile 이 전부 undefined → bail → 기본값으로 떨어져 입구 도출 실패.)
|
|
42
|
+
const idOf = (c: any): string | undefined => {
|
|
43
|
+
const raw = c?.state?.id
|
|
44
|
+
if (typeof raw === 'string' && raw.length > 0) return raw
|
|
45
|
+
const refid = c?.refid ?? c?.state?.refid
|
|
46
|
+
return refid != null ? String(refid) : undefined
|
|
47
|
+
}
|
|
48
|
+
const myId = idOf(stockpile)
|
|
49
|
+
if (!out || !comps || !myId) return undefined
|
|
50
|
+
let feeder: any
|
|
51
|
+
out.forEach((targets, srcId) => {
|
|
52
|
+
if (!feeder && Array.isArray(targets) && targets.includes(myId)) feeder = comps.get(srcId)
|
|
53
|
+
})
|
|
54
|
+
const ss = stockpile?.state
|
|
55
|
+
// feeder(입구) 못 찾으면 기본값(undefined) — 마주한 슈트 없는 stockpile.
|
|
56
|
+
if (!feeder || !ss) return undefined
|
|
57
|
+
// 입구 흐름 = *소스 링크의 방향* — feeder(슈트)의 배출 anchor(이 stockpile 로 향하는) direction.
|
|
58
|
+
// (중심-중심 벡터가 아니라 링크 방향면을 써야 정확. 긴 슈트의 중심은 실제 유입면과 다를 수 있음.)
|
|
59
|
+
let dir: { x: number; z: number } | undefined
|
|
60
|
+
try {
|
|
61
|
+
const outs = feeder.getSegmentTopology?.()?.outbounds as Array<{ worldPos?: { x: number; z: number }; direction?: { x: number; z: number } }> | undefined
|
|
62
|
+
if (Array.isArray(outs) && outs.length) {
|
|
63
|
+
const sx = (ss.left ?? 0) + (ss.width ?? 0) / 2
|
|
64
|
+
const sz = (ss.top ?? 0) + (ss.height ?? 0) / 2
|
|
65
|
+
let best = outs[0]
|
|
66
|
+
let bestD = Infinity
|
|
67
|
+
for (const a of outs) {
|
|
68
|
+
if (!a?.worldPos || !a.direction) continue
|
|
69
|
+
const d = (a.worldPos.x - sx) ** 2 + (a.worldPos.z - sz) ** 2
|
|
70
|
+
if (d < bestD) { bestD = d; best = a } // stockpile 에 가장 가까운 배출 anchor
|
|
71
|
+
}
|
|
72
|
+
if (best?.direction) dir = best.direction
|
|
73
|
+
}
|
|
74
|
+
} catch { /* topology 못 얻으면 fallback 없음 → 기본 */ }
|
|
75
|
+
if (!dir) return undefined
|
|
76
|
+
// 흐름 방향(world)을 stockpile local frame 으로 (회전 -rot).
|
|
77
|
+
const rot = typeof ss.rotation === 'number' ? ss.rotation : 0
|
|
78
|
+
const cos = Math.cos(-rot)
|
|
79
|
+
const sin = Math.sin(-rot)
|
|
80
|
+
const lx = dir.x * cos - dir.z * sin
|
|
81
|
+
const lz = dir.x * sin + dir.z * cos
|
|
82
|
+
// 흐름이 x 우세면 x축 진행. items 는 흐름 방향 끝(먼저 닿는 곳)부터 쌓여 입구쪽으로 →
|
|
83
|
+
// 흐름이 + 면 + 끝부터(reverse).
|
|
84
|
+
const advanceAlongX = Math.abs(lx) >= Math.abs(lz)
|
|
85
|
+
const reverse = advanceAlongX ? lx > 0 : lz > 0
|
|
86
|
+
return { advanceAlongX, reverse }
|
|
87
|
+
}
|
|
88
|
+
|
|
22
89
|
/** carrierPreset 별 default 색 (state.carrierWidth/Height/Depth 미지정 시 크기와 함께). */
|
|
23
90
|
const PRESET_COLOR: Record<CarrierPreset, number> = {
|
|
24
91
|
box: 0xb87333, // kraft brown
|
|
@@ -186,7 +253,15 @@ export class Stockpile3D extends RealObjectGroup {
|
|
|
186
253
|
return presetDefaultColor
|
|
187
254
|
}
|
|
188
255
|
|
|
189
|
-
|
|
256
|
+
// stackHeightLimit = *총 적치 높이*(scene unit) — 단(layer) 개수가 아니다. 아이템
|
|
257
|
+
// 한 단 높이(cd)로 나눠 최대 단 수로 환산해 전달. 예: 한도 60 + 아이템높이 10 → 6단.
|
|
258
|
+
const heightLimit = state.stackHeightLimit
|
|
259
|
+
const maxLayers = typeof heightLimit === 'number' && heightLimit > 0 && cd > 0
|
|
260
|
+
? Math.max(1, Math.floor(heightLimit / cd))
|
|
261
|
+
: undefined
|
|
262
|
+
// 입구 맞은편부터 채우기 — 그래프에서 이 stockpile 로 들어오는 노드(입구)를 도출.
|
|
263
|
+
const fillOpts = inflowFillOpts(stockpile)
|
|
264
|
+
const positions = computeStackPositions(pattern, count, { w: areaW, h: areaH }, { w: cw, h: ch, d: cd }, gap, maxLayers, fillOpts)
|
|
190
265
|
this._meshByRecordId.clear()
|
|
191
266
|
const records = stockpile.records
|
|
192
267
|
for (let i = 0; i < positions.length; i++) {
|
|
@@ -213,111 +288,6 @@ export class Stockpile3D extends RealObjectGroup {
|
|
|
213
288
|
return this._meshByRecordId.get(recordId)
|
|
214
289
|
}
|
|
215
290
|
|
|
216
|
-
/**
|
|
217
|
-
* stackPattern 별 각 carrier 의 local 좌표(영역 중심 기준). y 는 pad 위에 쌓이는
|
|
218
|
-
* 높이(cd 단위), x/z 는 영역 평면 위 분포.
|
|
219
|
-
*/
|
|
220
|
-
private _computeStackPositions(
|
|
221
|
-
pattern: StackPattern,
|
|
222
|
-
count: number,
|
|
223
|
-
areaW: number,
|
|
224
|
-
areaH: number,
|
|
225
|
-
cw: number,
|
|
226
|
-
ch: number,
|
|
227
|
-
cd: number,
|
|
228
|
-
gap: number,
|
|
229
|
-
heightLimit?: number
|
|
230
|
-
): Array<{ x: number; y: number; z: number }> {
|
|
231
|
-
const out: Array<{ x: number; y: number; z: number }> = []
|
|
232
|
-
|
|
233
|
-
// column — 한 자리에 수직 적층
|
|
234
|
-
if (pattern === 'column') {
|
|
235
|
-
const layers = heightLimit ?? count
|
|
236
|
-
for (let i = 0; i < count; i++) {
|
|
237
|
-
const layer = Math.min(i, layers - 1)
|
|
238
|
-
out.push({ x: 0, y: layer * cd + cd / 2, z: 0 })
|
|
239
|
-
}
|
|
240
|
-
return out
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// 격자 cell — 행/열 수. cell stride = carrier + gap (인접 carrier 사이 분리).
|
|
244
|
-
const stridX = cw + gap
|
|
245
|
-
const stridZ = ch + gap
|
|
246
|
-
const cols = Math.max(1, Math.floor((areaW + gap) / stridX))
|
|
247
|
-
const rows = Math.max(1, Math.floor((areaH + gap) / stridZ))
|
|
248
|
-
const cellsPerLayer = cols * rows
|
|
249
|
-
|
|
250
|
-
// pyramid — 위로 갈수록 행/열 -1 씩 (사다리꼴)
|
|
251
|
-
if (pattern === 'pyramid') {
|
|
252
|
-
let remaining = count
|
|
253
|
-
let layer = 0
|
|
254
|
-
while (remaining > 0) {
|
|
255
|
-
const lcols = Math.max(1, cols - layer)
|
|
256
|
-
const lrows = Math.max(1, rows - layer)
|
|
257
|
-
const cellsThisLayer = lcols * lrows
|
|
258
|
-
const take = Math.min(remaining, cellsThisLayer)
|
|
259
|
-
for (let i = 0; i < take; i++) {
|
|
260
|
-
const c = i % lcols
|
|
261
|
-
const r = Math.floor(i / lcols)
|
|
262
|
-
// 가운데 정렬 — 줄어든 만큼 offset
|
|
263
|
-
const offsetCol = (cols - lcols) / 2
|
|
264
|
-
const offsetRow = (rows - lrows) / 2
|
|
265
|
-
const x = -areaW / 2 + (c + offsetCol + 0.5) * stridX
|
|
266
|
-
const z = -areaH / 2 + (r + offsetRow + 0.5) * stridZ
|
|
267
|
-
out.push({ x, y: layer * cd + cd / 2, z })
|
|
268
|
-
}
|
|
269
|
-
remaining -= take
|
|
270
|
-
layer++
|
|
271
|
-
if (lcols === 1 && lrows === 1) {
|
|
272
|
-
// 정상에 도달 — 남은 건 column 처럼 위로
|
|
273
|
-
for (let i = 0; i < remaining; i++) {
|
|
274
|
-
out.push({ x: 0, y: (layer + i) * cd + cd / 2, z: 0 })
|
|
275
|
-
}
|
|
276
|
-
break
|
|
277
|
-
}
|
|
278
|
-
if (heightLimit && layer >= heightLimit) break
|
|
279
|
-
}
|
|
280
|
-
return out
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// pile — 자연 더미. row 와 비슷하되 가장자리는 약간 옆으로 흘러내림 효과
|
|
284
|
-
// (간단 구현 — 단마다 약간 random offset).
|
|
285
|
-
if (pattern === 'pile') {
|
|
286
|
-
// pseudo-random (인덱스 기반, deterministic)
|
|
287
|
-
const rng = (i: number) => ((Math.sin(i * 12.9898) * 43758.5453) % 1 + 1) % 1
|
|
288
|
-
for (let i = 0; i < count; i++) {
|
|
289
|
-
const layer = Math.floor(i / cellsPerLayer)
|
|
290
|
-
const idx = i % cellsPerLayer
|
|
291
|
-
const c = idx % cols
|
|
292
|
-
const r = Math.floor(idx / cols)
|
|
293
|
-
const jitterX = (rng(i * 2) - 0.5) * cw * 0.2
|
|
294
|
-
const jitterZ = (rng(i * 2 + 1) - 0.5) * ch * 0.2
|
|
295
|
-
const x = -areaW / 2 + (c + 0.5) * stridX + jitterX
|
|
296
|
-
const z = -areaH / 2 + (r + 0.5) * stridZ + jitterZ
|
|
297
|
-
out.push({ x, y: layer * cd + cd / 2, z })
|
|
298
|
-
if (heightLimit && layer >= heightLimit - 1 && idx === cellsPerLayer - 1) break
|
|
299
|
-
}
|
|
300
|
-
return out
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// row (default) / staggered — 격자 적층
|
|
304
|
-
for (let i = 0; i < count; i++) {
|
|
305
|
-
const layer = Math.floor(i / cellsPerLayer)
|
|
306
|
-
if (heightLimit && layer >= heightLimit) break
|
|
307
|
-
const idx = i % cellsPerLayer
|
|
308
|
-
const c = idx % cols
|
|
309
|
-
const r = Math.floor(idx / cols)
|
|
310
|
-
let x = -areaW / 2 + (c + 0.5) * stridX
|
|
311
|
-
const z = -areaH / 2 + (r + 0.5) * stridZ
|
|
312
|
-
// staggered — 홀수 layer 는 1/2 cell offset (벽돌식)
|
|
313
|
-
if (pattern === 'staggered' && layer % 2 === 1) {
|
|
314
|
-
x += stridX / 2
|
|
315
|
-
}
|
|
316
|
-
out.push({ x, y: layer * cd + cd / 2, z })
|
|
317
|
-
}
|
|
318
|
-
return out
|
|
319
|
-
}
|
|
320
|
-
|
|
321
291
|
/** carrierPreset 별 geometry. drum 은 cylinder, 나머지는 box. */
|
|
322
292
|
private _carrierGeometry(preset: CarrierPreset, w: number, h: number, d: number): THREE.BufferGeometry {
|
|
323
293
|
if (preset === 'drum') {
|
package/src/stockpile-grid-3d.ts
CHANGED
|
@@ -162,7 +162,11 @@ export class StockpileGrid3D extends RealObjectGroup {
|
|
|
162
162
|
const ch = state.carrierHeight ?? def.h
|
|
163
163
|
const cd = state.carrierDepth ?? def.d
|
|
164
164
|
const gap = state.carrierGap ?? 10
|
|
165
|
-
|
|
165
|
+
// stackHeightLimit = *총 적치 높이*(scene unit) → 단(layer) 수로 환산 (아이템 한 단 = cd).
|
|
166
|
+
const rawHeightLimit = state.stackHeightLimit as number | undefined
|
|
167
|
+
const heightLimit = typeof rawHeightLimit === 'number' && rawHeightLimit > 0 && cd > 0
|
|
168
|
+
? Math.max(1, Math.floor(rawHeightLimit / cd))
|
|
169
|
+
: undefined
|
|
166
170
|
|
|
167
171
|
const cellW = grid.cellW
|
|
168
172
|
const cellH = grid.cellH
|
package/src/stockpile-grid.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import * as THREE from 'three'
|
|
20
|
-
import { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'
|
|
20
|
+
import { Component, ComponentNature, ContainerAbstract, Holdable, RealObject, sceneComponent } from '@hatiolab/things-scene'
|
|
21
21
|
import type { State, Material3D } from '@hatiolab/things-scene'
|
|
22
22
|
import {
|
|
23
23
|
CarrierHolder,
|
|
@@ -99,7 +99,7 @@ const NATURE: ComponentNature = {
|
|
|
99
99
|
|
|
100
100
|
@sceneComponent('stockpile-grid')
|
|
101
101
|
export default class StockpileGrid extends RecordStorage<StockpileGridCell>()(
|
|
102
|
-
CarrierHolder(Placeable(ContainerAbstract))
|
|
102
|
+
Holdable(CarrierHolder(Placeable(ContainerAbstract)))
|
|
103
103
|
) implements SlottedHolder {
|
|
104
104
|
declare state: StockpileGridState
|
|
105
105
|
declare _realObject?: StockpileGrid3D
|
|
@@ -285,7 +285,7 @@ export default class StockpileGrid extends RecordStorage<StockpileGridCell>()(
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
const carrier = new CarrierClass(carrierState, (this as any)._app)
|
|
288
|
-
;(this as any).
|
|
288
|
+
;(this as any).addHolding(carrier) // transient carrier → _holdings (§1.2)
|
|
289
289
|
void (carrier as any).realObject
|
|
290
290
|
;(carrier as any).applyHolderAttachPoint?.()
|
|
291
291
|
return carrier
|
package/src/stockpile.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import * as THREE from 'three'
|
|
17
|
-
import { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'
|
|
17
|
+
import { Component, ComponentNature, ContainerAbstract, Holdable, RealObject, sceneComponent } from '@hatiolab/things-scene'
|
|
18
18
|
import type { State, Material3D } from '@hatiolab/things-scene'
|
|
19
19
|
import {
|
|
20
20
|
CarrierHolder,
|
|
@@ -63,6 +63,12 @@ export interface StockpileState extends State {
|
|
|
63
63
|
/** 어느 끝에서 빼나. default 'lifo'. */
|
|
64
64
|
pickPolicy?: PickPolicy
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* 라우팅 행선지 값 (RoutingTarget). sorter/router 가 carrier.destination 과 string
|
|
68
|
+
* 매칭해 이 StockPile 로 분배. 미설정 시 routingDestination() 가 state.id 로 fallback.
|
|
69
|
+
*/
|
|
70
|
+
destination?: string
|
|
71
|
+
|
|
66
72
|
/** click 시 invoke 할 Popup 컴포넌트 id (StorageRack / RackGrid 와 동일 패턴). */
|
|
67
73
|
popupRef?: string
|
|
68
74
|
|
|
@@ -95,6 +101,7 @@ const NATURE: ComponentNature = {
|
|
|
95
101
|
{ type: 'number', label: 'stack-height-limit', name: 'stackHeightLimit' },
|
|
96
102
|
{ type: 'select', label: 'pick-policy', name: 'pickPolicy',
|
|
97
103
|
property: { options: ['lifo', 'fifo'] } },
|
|
104
|
+
{ type: 'string', label: 'destination', name: 'destination' },
|
|
98
105
|
{ type: 'id-input', label: 'popup-ref', name: 'popupRef',
|
|
99
106
|
property: { component: 'popup' } },
|
|
100
107
|
{ type: 'id-input', label: 'legend-target', name: 'legendTarget',
|
|
@@ -106,7 +113,7 @@ const NATURE: ComponentNature = {
|
|
|
106
113
|
|
|
107
114
|
@sceneComponent('stockpile')
|
|
108
115
|
export default class Stockpile extends RecordStorage<StockpileRecord>()(
|
|
109
|
-
CarrierHolder(Placeable(ContainerAbstract))
|
|
116
|
+
Holdable(CarrierHolder(Placeable(ContainerAbstract)))
|
|
110
117
|
) implements SlottedHolder {
|
|
111
118
|
declare state: StockpileState
|
|
112
119
|
declare _realObject?: Stockpile3D
|
|
@@ -115,6 +122,22 @@ export default class Stockpile extends RecordStorage<StockpileRecord>()(
|
|
|
115
122
|
static align: Alignment = 'bottom'
|
|
116
123
|
static defaultDepth = (_h: Heights) => 5 // pad 두께
|
|
117
124
|
|
|
125
|
+
/**
|
|
126
|
+
* RoutingTarget — 자기 행선지 값 publish. sorter 가 carrier.destination 과 매칭.
|
|
127
|
+
* 미설정 시 자기 id 로 (단일 sink 직접 지정 fallback). chute 와 동일 규약.
|
|
128
|
+
*/
|
|
129
|
+
routingDestination(): string {
|
|
130
|
+
return (this.state.destination as string | undefined) ?? (this.state.id as string)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── 그래프 참여 ─────────────────────────────────────────────────────────────
|
|
134
|
+
// StockPile 은 *방향 없는 받는 footprint*. 별도 면 anchor 를 publish 하지 않는다 —
|
|
135
|
+
// 어느 면으로 받는지는 의미 없고, 슈트/컨베이어의 *출구 anchor 가 자기 footprint 에
|
|
136
|
+
// 닿으면*(matchOutbounds 의 deliversInto, bounds 기반) 자동 연결된다. (사용자 명시
|
|
137
|
+
// 2026-06-14: "스톡파일은 슈트의 출구와 면한 부분이 있으면 그곳을 링크".) RoutingTarget
|
|
138
|
+
// (routingDestination) 만으로 그래프 노드로 수집되고, bounds 는 coordinator 가 채운다.
|
|
139
|
+
// 멀리 떨어진 타겟은 명시 링크(nextRef/RoutingLinks)로 연결.
|
|
140
|
+
|
|
118
141
|
get nature(): ComponentNature { return NATURE }
|
|
119
142
|
get anchors() { return [] }
|
|
120
143
|
|
|
@@ -136,6 +159,11 @@ export default class Stockpile extends RecordStorage<StockpileRecord>()(
|
|
|
136
159
|
return true
|
|
137
160
|
}
|
|
138
161
|
|
|
162
|
+
/** Transferable contract — dispatch/Transfer planning 이 capacity 확인에 호출. 단일 slot 위임. */
|
|
163
|
+
canReceive(carrier?: Component): boolean {
|
|
164
|
+
return this.canReceiveAt(SLOT_ID, carrier)
|
|
165
|
+
}
|
|
166
|
+
|
|
139
167
|
occupiedSlotIds(): ReadonlyArray<string> {
|
|
140
168
|
return this.inventoryCount > 0 ? [SLOT_ID] : []
|
|
141
169
|
}
|
|
@@ -201,8 +229,11 @@ export default class Stockpile extends RecordStorage<StockpileRecord>()(
|
|
|
201
229
|
}
|
|
202
230
|
|
|
203
231
|
const carrier = new CarrierClass(carrierState, (this as any)._app)
|
|
204
|
-
//
|
|
205
|
-
|
|
232
|
+
// transient carrier → _holdings (모델링 자식 `_components` 아님). Mover.pick
|
|
233
|
+
// 이 reparent 호출 시 ContainerAbstract.addComponent 가 oldContainer._holdings
|
|
234
|
+
// 도 자동 정리 (stale 방지, §1.2 직교). board JSON 직렬화 / Inspector 표시
|
|
235
|
+
// 자동 차단 — Spot 시범 패턴 (2026-06-16).
|
|
236
|
+
;(this as any).addHolding(carrier)
|
|
206
237
|
void (carrier as any).realObject
|
|
207
238
|
;(carrier as any).applyHolderAttachPoint?.()
|
|
208
239
|
return carrier
|
package/src/storage-rack.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
-
import { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'
|
|
4
|
+
import { Component, ComponentNature, ContainerAbstract, Holdable, RealObject, sceneComponent } from '@hatiolab/things-scene'
|
|
5
5
|
import type { State, Material3D } from '@hatiolab/things-scene'
|
|
6
6
|
import * as THREE from 'three'
|
|
7
7
|
import {
|
|
@@ -168,7 +168,7 @@ function _nextCarrierRefid(): number {
|
|
|
168
168
|
@sceneComponent('storage-rack')
|
|
169
169
|
export default class Rack
|
|
170
170
|
extends RecordStorage<{ cellId: string; [key: string]: any }>()(
|
|
171
|
-
CellContainer(CarrierHolder(Placeable(ContainerAbstract)))
|
|
171
|
+
CellContainer(Holdable(CarrierHolder(Placeable(ContainerAbstract))))
|
|
172
172
|
)
|
|
173
173
|
implements SlottedHolder
|
|
174
174
|
{
|
|
@@ -516,7 +516,7 @@ export default class Rack
|
|
|
516
516
|
// 만든 게 아니라 *내부적으로 잠시 빌리는 것*. 새 컴포넌트 추가 → root.onadded
|
|
517
517
|
// → refreshMappings (1s debounce) → 모든 컴포넌트 매핑 재실행 의 cascade 차단.
|
|
518
518
|
// (silent 모드: _silentAdd 플래그 set → refreshMappings skip)
|
|
519
|
-
;(this as any).
|
|
519
|
+
;(this as any).addHolding(carrier) // transient carrier → _holdings (§1.2)
|
|
520
520
|
|
|
521
521
|
// 3D 강제 빌드 + attach (pipeline tick 없이도 즉시 표시 — Mover 가 곧 pick).
|
|
522
522
|
void (carrier as any).realObject
|