@operato/scene-storage 10.0.0-beta.55 → 10.0.0-beta.57
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 +25 -0
- package/dist/rack-grid-cell.d.ts +4 -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.d.ts +9 -2
- package/dist/stockpile-grid.js +14 -5
- package/dist/stockpile-grid.js.map +1 -1
- package/dist/stockpile.d.ts +21 -3
- package/dist/stockpile.js +31 -4
- package/dist/stockpile.js.map +1 -1
- package/package.json +3 -3
- package/src/stockpile-3d.ts +76 -106
- package/src/stockpile-grid-3d.ts +5 -1
- package/src/stockpile-grid.ts +17 -6
- package/src/stockpile.ts +44 -6
- 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
|
@@ -27,7 +27,8 @@ import {
|
|
|
27
27
|
type AttachFrame,
|
|
28
28
|
type Alignment,
|
|
29
29
|
type Heights,
|
|
30
|
-
type PlacementArchetype
|
|
30
|
+
type PlacementArchetype,
|
|
31
|
+
type SlottedHolder
|
|
31
32
|
} from '@operato/scene-base'
|
|
32
33
|
|
|
33
34
|
import { StockpileGrid3D } from './stockpile-grid-3d.js'
|
|
@@ -99,7 +100,7 @@ const NATURE: ComponentNature = {
|
|
|
99
100
|
@sceneComponent('stockpile-grid')
|
|
100
101
|
export default class StockpileGrid extends RecordStorage<StockpileGridCell>()(
|
|
101
102
|
CarrierHolder(Placeable(ContainerAbstract))
|
|
102
|
-
) {
|
|
103
|
+
) implements SlottedHolder {
|
|
103
104
|
declare state: StockpileGridState
|
|
104
105
|
declare _realObject?: StockpileGrid3D
|
|
105
106
|
|
|
@@ -192,18 +193,28 @@ export default class StockpileGrid extends RecordStorage<StockpileGridCell>()(
|
|
|
192
193
|
return this._materializeCarrier(record, p.col, p.row)
|
|
193
194
|
}
|
|
194
195
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
/**
|
|
197
|
+
* carrier → StockpileRecord 추출. id 누락 시 `stkg-` prefix 자동 생성.
|
|
198
|
+
* SlottedHolder duck 의 표준 entry — receiveAt 안에서 호출. cell 정보 (col/row) 는
|
|
199
|
+
* slotId 의 parseCellId 로 receiveAt 가 처리 — record 자체에는 cell 좌표 미포함
|
|
200
|
+
* (cell array 가 그 컨텍스트 보유).
|
|
201
|
+
*/
|
|
202
|
+
recordFromCarrier(carrier: Component, _slotId: string): StockpileRecord {
|
|
198
203
|
const cstate: any = (carrier as any)?.state ?? {}
|
|
199
204
|
const cid = cstate.id ?? `stkg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`
|
|
200
|
-
|
|
205
|
+
return {
|
|
201
206
|
id: String(cid),
|
|
202
207
|
...(cstate.type ? { type: cstate.type } : {}),
|
|
203
208
|
...(typeof cstate.width === 'number' ? { width: cstate.width } : {}),
|
|
204
209
|
...(typeof cstate.height === 'number' ? { height: cstate.height } : {}),
|
|
205
210
|
...(typeof cstate.depth === 'number' ? { depth: cstate.depth } : {})
|
|
206
211
|
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async receiveAt(slotId: string, carrier: Component, _options?: any): Promise<void> {
|
|
215
|
+
const p = this.parseCellId(slotId)
|
|
216
|
+
if (!p) return
|
|
217
|
+
const record = this.recordFromCarrier(carrier, slotId)
|
|
207
218
|
const records = [...this.recordsOf(p.col, p.row), record]
|
|
208
219
|
this._setCellRecords(p.col, p.row, records)
|
|
209
220
|
;(carrier as any)?.dispose?.()
|
package/src/stockpile.ts
CHANGED
|
@@ -24,7 +24,8 @@ import {
|
|
|
24
24
|
type AttachFrame,
|
|
25
25
|
type Alignment,
|
|
26
26
|
type Heights,
|
|
27
|
-
type PlacementArchetype
|
|
27
|
+
type PlacementArchetype,
|
|
28
|
+
type SlottedHolder
|
|
28
29
|
} from '@operato/scene-base'
|
|
29
30
|
|
|
30
31
|
import { Stockpile3D } from './stockpile-3d.js'
|
|
@@ -62,6 +63,12 @@ export interface StockpileState extends State {
|
|
|
62
63
|
/** 어느 끝에서 빼나. default 'lifo'. */
|
|
63
64
|
pickPolicy?: PickPolicy
|
|
64
65
|
|
|
66
|
+
/**
|
|
67
|
+
* 라우팅 행선지 값 (RoutingTarget). sorter/router 가 carrier.destination 과 string
|
|
68
|
+
* 매칭해 이 StockPile 로 분배. 미설정 시 routingDestination() 가 state.id 로 fallback.
|
|
69
|
+
*/
|
|
70
|
+
destination?: string
|
|
71
|
+
|
|
65
72
|
/** click 시 invoke 할 Popup 컴포넌트 id (StorageRack / RackGrid 와 동일 패턴). */
|
|
66
73
|
popupRef?: string
|
|
67
74
|
|
|
@@ -94,6 +101,7 @@ const NATURE: ComponentNature = {
|
|
|
94
101
|
{ type: 'number', label: 'stack-height-limit', name: 'stackHeightLimit' },
|
|
95
102
|
{ type: 'select', label: 'pick-policy', name: 'pickPolicy',
|
|
96
103
|
property: { options: ['lifo', 'fifo'] } },
|
|
104
|
+
{ type: 'string', label: 'destination', name: 'destination' },
|
|
97
105
|
{ type: 'id-input', label: 'popup-ref', name: 'popupRef',
|
|
98
106
|
property: { component: 'popup' } },
|
|
99
107
|
{ type: 'id-input', label: 'legend-target', name: 'legendTarget',
|
|
@@ -106,7 +114,7 @@ const NATURE: ComponentNature = {
|
|
|
106
114
|
@sceneComponent('stockpile')
|
|
107
115
|
export default class Stockpile extends RecordStorage<StockpileRecord>()(
|
|
108
116
|
CarrierHolder(Placeable(ContainerAbstract))
|
|
109
|
-
) {
|
|
117
|
+
) implements SlottedHolder {
|
|
110
118
|
declare state: StockpileState
|
|
111
119
|
declare _realObject?: Stockpile3D
|
|
112
120
|
|
|
@@ -114,6 +122,22 @@ export default class Stockpile extends RecordStorage<StockpileRecord>()(
|
|
|
114
122
|
static align: Alignment = 'bottom'
|
|
115
123
|
static defaultDepth = (_h: Heights) => 5 // pad 두께
|
|
116
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
|
+
|
|
117
141
|
get nature(): ComponentNature { return NATURE }
|
|
118
142
|
get anchors() { return [] }
|
|
119
143
|
|
|
@@ -135,6 +159,11 @@ export default class Stockpile extends RecordStorage<StockpileRecord>()(
|
|
|
135
159
|
return true
|
|
136
160
|
}
|
|
137
161
|
|
|
162
|
+
/** Transferable contract — dispatch/Transfer planning 이 capacity 확인에 호출. 단일 slot 위임. */
|
|
163
|
+
canReceive(carrier?: Component): boolean {
|
|
164
|
+
return this.canReceiveAt(SLOT_ID, carrier)
|
|
165
|
+
}
|
|
166
|
+
|
|
138
167
|
occupiedSlotIds(): ReadonlyArray<string> {
|
|
139
168
|
return this.inventoryCount > 0 ? [SLOT_ID] : []
|
|
140
169
|
}
|
|
@@ -208,19 +237,28 @@ export default class Stockpile extends RecordStorage<StockpileRecord>()(
|
|
|
208
237
|
}
|
|
209
238
|
|
|
210
239
|
/**
|
|
211
|
-
* carrier
|
|
212
|
-
*
|
|
240
|
+
* carrier 의 state → StockpileRecord 추출. id 누락 시 `stk-` prefix 자동 생성.
|
|
241
|
+
* transform/refid 등 *_물리적 위치 필드_* 는 *_제외_* — record 는 *_논리적 식별_*
|
|
242
|
+
* 만. SlottedHolder duck 의 표준 entry point — receiveAt 안에서 호출.
|
|
213
243
|
*/
|
|
214
|
-
|
|
244
|
+
recordFromCarrier(carrier: Component, _slotId: string): StockpileRecord {
|
|
215
245
|
const cstate: any = (carrier as any)?.state ?? {}
|
|
216
246
|
const cid = cstate.id ?? `stk-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`
|
|
217
|
-
|
|
247
|
+
return {
|
|
218
248
|
id: String(cid),
|
|
219
249
|
...(cstate.type ? { type: cstate.type } : {}),
|
|
220
250
|
...(typeof cstate.width === 'number' ? { width: cstate.width } : {}),
|
|
221
251
|
...(typeof cstate.height === 'number' ? { height: cstate.height } : {}),
|
|
222
252
|
...(typeof cstate.depth === 'number' ? { depth: cstate.depth } : {})
|
|
223
253
|
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* carrier 를 받아 record 로 push, carrier 객체는 dispose (시각은 _realObject 가
|
|
258
|
+
* records 길이 기준 자동 갱신). capacity 초과 시도는 canReceiveAt 가 이미 차단.
|
|
259
|
+
*/
|
|
260
|
+
async receiveAt(slotId: string, carrier: Component, _options?: any): Promise<void> {
|
|
261
|
+
const record = this.recordFromCarrier(carrier, slotId)
|
|
224
262
|
const records = [...this.records, record]
|
|
225
263
|
this._setDataSilently(records)
|
|
226
264
|
;(carrier as any)?.dispose?.()
|