@operato/scene-storage 10.0.0-beta.48 → 10.0.0-beta.53
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 +36 -0
- package/dist/box.js +2 -2
- package/dist/box.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/pallet.js +2 -2
- package/dist/pallet.js.map +1 -1
- package/dist/parcel.js +2 -2
- package/dist/parcel.js.map +1 -1
- package/dist/picking-station-3d.d.ts +20 -0
- package/dist/picking-station-3d.js +162 -0
- package/dist/picking-station-3d.js.map +1 -0
- package/dist/picking-station.d.ts +50 -0
- package/dist/picking-station.js +186 -0
- package/dist/picking-station.js.map +1 -0
- package/dist/rack-capability.d.ts +11 -0
- package/dist/rack-capability.js +25 -0
- package/dist/rack-capability.js.map +1 -0
- package/dist/rack-grid.d.ts +4 -22
- package/dist/rack-grid.js +23 -115
- package/dist/rack-grid.js.map +1 -1
- package/dist/spot.d.ts +1 -0
- package/dist/spot.js +6 -2
- package/dist/spot.js.map +1 -1
- package/dist/stockpile-3d.d.ts +55 -0
- package/dist/stockpile-3d.js +387 -0
- package/dist/stockpile-3d.js.map +1 -0
- package/dist/stockpile-grid-3d.d.ts +30 -0
- package/dist/stockpile-grid-3d.js +301 -0
- package/dist/stockpile-grid-3d.js.map +1 -0
- package/dist/stockpile-grid.d.ts +85 -0
- package/dist/stockpile-grid.js +361 -0
- package/dist/stockpile-grid.js.map +1 -0
- package/dist/stockpile.d.ts +116 -0
- package/dist/stockpile.js +345 -0
- package/dist/stockpile.js.map +1 -0
- package/dist/storage-rack.d.ts +39 -44
- package/dist/storage-rack.js +71 -146
- package/dist/storage-rack.js.map +1 -1
- package/dist/templates/index.d.ts +80 -0
- package/dist/templates/index.js +7 -1
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/picking-station.d.ts +20 -0
- package/dist/templates/picking-station.js +22 -0
- package/dist/templates/picking-station.js.map +1 -0
- package/dist/templates/stockpile-grid.d.ts +37 -0
- package/dist/templates/stockpile-grid.js +38 -0
- package/dist/templates/stockpile-grid.js.map +1 -0
- package/dist/templates/stockpile.d.ts +29 -0
- package/dist/templates/stockpile.js +31 -0
- package/dist/templates/stockpile.js.map +1 -0
- package/package.json +3 -3
- package/src/box.ts +2 -1
- package/src/index.ts +14 -0
- package/src/pallet.ts +2 -1
- package/src/parcel.ts +2 -1
- package/src/picking-station-3d.ts +164 -0
- package/src/picking-station.ts +220 -0
- package/src/rack-capability.ts +26 -0
- package/src/rack-grid.ts +24 -108
- package/src/spot.ts +15 -1
- package/src/stockpile-3d.ts +412 -0
- package/src/stockpile-grid-3d.ts +327 -0
- package/src/stockpile-grid.ts +408 -0
- package/src/stockpile.ts +427 -0
- package/src/storage-rack.ts +82 -137
- package/src/templates/index.ts +7 -1
- package/src/templates/picking-station.ts +23 -0
- package/src/templates/stockpile-grid.ts +39 -0
- package/src/templates/stockpile.ts +32 -0
- package/test/test-rack-capability.ts +51 -0
- package/translations/en.json +23 -6
- package/translations/ja.json +23 -6
- package/translations/ko.json +22 -5
- package/translations/ms.json +23 -6
- package/translations/zh.json +22 -5
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* StockpileGrid3D — cell × cell 의 평치 영역 3D 시각화. cell 마다 pad + records 기반
|
|
5
|
+
* 자동 적치 carrier mesh. Stockpile3D 와 같은 idiom 의 cell 별 적용.
|
|
6
|
+
*
|
|
7
|
+
* cell pad mesh 에 userData.slotId 를 심어 click raycast 가 cell 을 식별, popup
|
|
8
|
+
* tether anchor 로 동작 (storage-rack 패턴).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as THREE from 'three'
|
|
12
|
+
import { RealObjectGroup } from '@hatiolab/things-scene'
|
|
13
|
+
|
|
14
|
+
import type StockpileGrid from './stockpile-grid.js'
|
|
15
|
+
import type { StackPattern, CarrierPreset } from './stockpile.js'
|
|
16
|
+
|
|
17
|
+
const PAD_DEPTH = 2
|
|
18
|
+
|
|
19
|
+
const PRESET_COLOR: Record<CarrierPreset, number> = {
|
|
20
|
+
box: 0xb87333,
|
|
21
|
+
pallet: 0x8b6f47,
|
|
22
|
+
drum: 0x556679,
|
|
23
|
+
sack: 0xd4c89a,
|
|
24
|
+
crate: 0x9a7548,
|
|
25
|
+
bale: 0xa89968
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const PRESET_DEFAULT_SIZE: Record<CarrierPreset, { w: number; h: number; d: number }> = {
|
|
29
|
+
box: { w: 26, h: 26, d: 20 },
|
|
30
|
+
pallet: { w: 40, h: 30, d: 15 },
|
|
31
|
+
drum: { w: 24, h: 24, d: 30 },
|
|
32
|
+
sack: { w: 30, h: 28, d: 18 },
|
|
33
|
+
crate: { w: 30, h: 30, d: 22 },
|
|
34
|
+
bale: { w: 32, h: 30, d: 18 }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class StockpileGrid3D extends RealObjectGroup {
|
|
38
|
+
/** cellSlotId → pad mesh (popup tether anchor 용). */
|
|
39
|
+
private _padByCellId: Map<string, THREE.Mesh> = new Map()
|
|
40
|
+
/** cellSlotId → carriers group. */
|
|
41
|
+
private _carriersByCellId: Map<string, THREE.Group> = new Map()
|
|
42
|
+
/** material 캐시 (color → material) — 같은 색 carrier 공유. */
|
|
43
|
+
private _materialByColor: Map<number, THREE.MeshStandardMaterial> = new Map()
|
|
44
|
+
/** grid 전체를 감싸는 outer group (보드 보정 등). */
|
|
45
|
+
private _gridGroup?: THREE.Group
|
|
46
|
+
|
|
47
|
+
build() {
|
|
48
|
+
super.build()
|
|
49
|
+
this._buildGrid()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
update() {
|
|
53
|
+
super.update()
|
|
54
|
+
this._rebuildAll()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** carrier attach anchor — slotId 가 cellId 면 그 cell pad 반환, 아니면 grid group. */
|
|
58
|
+
getAttachFrame(slotId?: string): THREE.Object3D | undefined {
|
|
59
|
+
if (slotId) {
|
|
60
|
+
const pad = this._padByCellId.get(slotId)
|
|
61
|
+
if (pad) return pad
|
|
62
|
+
}
|
|
63
|
+
return this._gridGroup
|
|
64
|
+
}
|
|
65
|
+
/** slotTargetAt(cellId).attachObject3d = cell pad — Stockpile.getSlotAttachObject3d 가 호출. */
|
|
66
|
+
getCellPadMesh(slotId: string): THREE.Mesh | undefined {
|
|
67
|
+
return this._padByCellId.get(slotId)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private _disposeAll(): void {
|
|
71
|
+
if (this._gridGroup) {
|
|
72
|
+
this._disposeGroupChildren(this._gridGroup)
|
|
73
|
+
this.object3d.remove(this._gridGroup)
|
|
74
|
+
this._gridGroup = undefined
|
|
75
|
+
}
|
|
76
|
+
this._padByCellId.clear()
|
|
77
|
+
this._carriersByCellId.clear()
|
|
78
|
+
for (const m of this._materialByColor.values()) m.dispose()
|
|
79
|
+
this._materialByColor.clear()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private _disposeGroupChildren(group: THREE.Object3D): void {
|
|
83
|
+
while (group.children.length > 0) {
|
|
84
|
+
const child = group.children[0]
|
|
85
|
+
this._disposeGroupChildren(child)
|
|
86
|
+
group.remove(child)
|
|
87
|
+
if ((child as any).geometry?.dispose) (child as any).geometry.dispose()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private _rebuildAll(): void {
|
|
92
|
+
this._disposeAll()
|
|
93
|
+
this._buildGrid()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private _buildGrid(): void {
|
|
97
|
+
const grid = this.component as unknown as StockpileGrid
|
|
98
|
+
const cols = grid.cols
|
|
99
|
+
const rows = grid.rows
|
|
100
|
+
const cellW = grid.cellW
|
|
101
|
+
const cellH = grid.cellH
|
|
102
|
+
const state = grid.state as any
|
|
103
|
+
|
|
104
|
+
const outline = new THREE.Color((state.strokeStyle as string) || '#7a5a2e')
|
|
105
|
+
const fillColor = new THREE.Color((state.fillStyle as string) || '#c89c5c')
|
|
106
|
+
|
|
107
|
+
this._gridGroup = new THREE.Group()
|
|
108
|
+
// grid pad 원점은 컴포넌트 중심 (left+totalW/2). state.left/top 기준 origin 0
|
|
109
|
+
// 인 사각형 좌상단 — grid local 좌표에서 cell 의 인덱스 (col, row) → cell center
|
|
110
|
+
// = (col+0.5)*cellW - totalW/2, (row+0.5)*cellH - totalH/2 (3D world 의 컴포넌트
|
|
111
|
+
// 중심 기준 local).
|
|
112
|
+
const totalW = cellW * cols
|
|
113
|
+
const totalH = cellH * rows
|
|
114
|
+
this.object3d.add(this._gridGroup)
|
|
115
|
+
|
|
116
|
+
for (let r = 0; r < rows; r++) {
|
|
117
|
+
for (let c = 0; c < cols; c++) {
|
|
118
|
+
const cellId = grid.cellIdOf(c, r)
|
|
119
|
+
const cellFill = fillColor // cell 별 색 override 없음 — 모두 grid 공통
|
|
120
|
+
|
|
121
|
+
// ── cell pad ─────────────────────────────────────────
|
|
122
|
+
const padGeom = new THREE.BoxGeometry(cellW * 0.96, PAD_DEPTH, cellH * 0.96)
|
|
123
|
+
const padMat = new THREE.MeshStandardMaterial({
|
|
124
|
+
color: cellFill, roughness: 0.85, transparent: true, opacity: 0.55
|
|
125
|
+
})
|
|
126
|
+
const pad = new THREE.Mesh(padGeom, padMat)
|
|
127
|
+
const cx = (c + 0.5) * cellW - totalW / 2
|
|
128
|
+
const cz = (r + 0.5) * cellH - totalH / 2
|
|
129
|
+
pad.position.set(cx, PAD_DEPTH / 2, cz)
|
|
130
|
+
pad.receiveShadow = true
|
|
131
|
+
pad.userData.slotId = cellId
|
|
132
|
+
this._gridGroup.add(pad)
|
|
133
|
+
this._padByCellId.set(cellId, pad)
|
|
134
|
+
|
|
135
|
+
// cell outline — 페인트 라인
|
|
136
|
+
const padEdges = new THREE.EdgesGeometry(padGeom)
|
|
137
|
+
const padLine = new THREE.LineSegments(padEdges, new THREE.LineBasicMaterial({ color: outline }))
|
|
138
|
+
padLine.position.copy(pad.position)
|
|
139
|
+
this._gridGroup.add(padLine)
|
|
140
|
+
|
|
141
|
+
// ── cell carriers ────────────────────────────────────
|
|
142
|
+
const cg = new THREE.Group()
|
|
143
|
+
cg.position.set(cx, PAD_DEPTH, cz)
|
|
144
|
+
this._gridGroup.add(cg)
|
|
145
|
+
this._carriersByCellId.set(cellId, cg)
|
|
146
|
+
this._populateCellCarriers(c, r, cg)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** cell 의 records 를 stackPattern / carrierPreset 으로 배치. */
|
|
152
|
+
private _populateCellCarriers(col: number, row: number, group: THREE.Group): void {
|
|
153
|
+
const grid = this.component as unknown as StockpileGrid
|
|
154
|
+
const records = grid.recordsOf(col, row)
|
|
155
|
+
if (records.length === 0) return
|
|
156
|
+
|
|
157
|
+
const state = grid.state as any
|
|
158
|
+
const preset = (state.carrierPreset ?? 'box') as CarrierPreset
|
|
159
|
+
const pattern = (state.stackPattern ?? 'row') as StackPattern
|
|
160
|
+
const def = PRESET_DEFAULT_SIZE[preset] ?? PRESET_DEFAULT_SIZE.box
|
|
161
|
+
const cw = state.carrierWidth ?? def.w
|
|
162
|
+
const ch = state.carrierHeight ?? def.h
|
|
163
|
+
const cd = state.carrierDepth ?? def.d
|
|
164
|
+
const gap = state.carrierGap ?? 10
|
|
165
|
+
const heightLimit = state.stackHeightLimit as number | undefined
|
|
166
|
+
|
|
167
|
+
const cellW = grid.cellW
|
|
168
|
+
const cellH = grid.cellH
|
|
169
|
+
const geom = this._carrierGeometry(preset, cw, ch, cd)
|
|
170
|
+
|
|
171
|
+
const positions = this._computeStackPositions(pattern, records.length, cellW, cellH, cw, ch, cd, gap, heightLimit)
|
|
172
|
+
const presetDefault = PRESET_COLOR[preset] ?? PRESET_COLOR.box
|
|
173
|
+
for (let i = 0; i < positions.length; i++) {
|
|
174
|
+
const p = positions[i]
|
|
175
|
+
const record = records[i]
|
|
176
|
+
const legendColor = (grid as any).resolveLegendColor?.(record)
|
|
177
|
+
let colorHex = presetDefault
|
|
178
|
+
if (typeof legendColor === 'string' && legendColor.length > 0) {
|
|
179
|
+
try { colorHex = new THREE.Color(legendColor).getHex() } catch { /* ignore */ }
|
|
180
|
+
}
|
|
181
|
+
const mat = this._getMaterial(colorHex)
|
|
182
|
+
const mesh = new THREE.Mesh(geom, mat)
|
|
183
|
+
mesh.position.set(p.x, p.y, p.z)
|
|
184
|
+
mesh.castShadow = true
|
|
185
|
+
mesh.receiveShadow = true
|
|
186
|
+
mesh.userData.recordId = record.id
|
|
187
|
+
mesh.userData.cellId = grid.cellIdOf(col, row)
|
|
188
|
+
group.add(mesh)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private _computeStackPositions(
|
|
193
|
+
pattern: StackPattern,
|
|
194
|
+
count: number,
|
|
195
|
+
areaW: number, areaH: number,
|
|
196
|
+
cw: number, ch: number, cd: number,
|
|
197
|
+
gap: number,
|
|
198
|
+
heightLimit?: number
|
|
199
|
+
): Array<{ x: number; y: number; z: number }> {
|
|
200
|
+
const out: Array<{ x: number; y: number; z: number }> = []
|
|
201
|
+
if (pattern === 'column') {
|
|
202
|
+
const layers = heightLimit ?? count
|
|
203
|
+
for (let i = 0; i < count; i++) {
|
|
204
|
+
const layer = Math.min(i, layers - 1)
|
|
205
|
+
out.push({ x: 0, y: layer * cd + cd / 2, z: 0 })
|
|
206
|
+
}
|
|
207
|
+
return out
|
|
208
|
+
}
|
|
209
|
+
const stridX = cw + gap
|
|
210
|
+
const stridZ = ch + gap
|
|
211
|
+
const cols = Math.max(1, Math.floor((areaW + gap) / stridX))
|
|
212
|
+
const rows = Math.max(1, Math.floor((areaH + gap) / stridZ))
|
|
213
|
+
const cellsPerLayer = cols * rows
|
|
214
|
+
if (pattern === 'pyramid') {
|
|
215
|
+
let remaining = count
|
|
216
|
+
let layer = 0
|
|
217
|
+
while (remaining > 0) {
|
|
218
|
+
const lcols = Math.max(1, cols - layer)
|
|
219
|
+
const lrows = Math.max(1, rows - layer)
|
|
220
|
+
const cellsThisLayer = lcols * lrows
|
|
221
|
+
const take = Math.min(remaining, cellsThisLayer)
|
|
222
|
+
for (let i = 0; i < take; i++) {
|
|
223
|
+
const c = i % lcols
|
|
224
|
+
const r = Math.floor(i / lcols)
|
|
225
|
+
const offC = (cols - lcols) / 2
|
|
226
|
+
const offR = (rows - lrows) / 2
|
|
227
|
+
const x = -areaW / 2 + (c + offC + 0.5) * stridX
|
|
228
|
+
const z = -areaH / 2 + (r + offR + 0.5) * stridZ
|
|
229
|
+
out.push({ x, y: layer * cd + cd / 2, z })
|
|
230
|
+
}
|
|
231
|
+
remaining -= take
|
|
232
|
+
layer++
|
|
233
|
+
if (lcols === 1 && lrows === 1) {
|
|
234
|
+
for (let i = 0; i < remaining; i++) {
|
|
235
|
+
out.push({ x: 0, y: (layer + i) * cd + cd / 2, z: 0 })
|
|
236
|
+
}
|
|
237
|
+
break
|
|
238
|
+
}
|
|
239
|
+
if (heightLimit && layer >= heightLimit) break
|
|
240
|
+
}
|
|
241
|
+
return out
|
|
242
|
+
}
|
|
243
|
+
if (pattern === 'pile') {
|
|
244
|
+
const rng = (i: number) => ((Math.sin(i * 12.9898) * 43758.5453) % 1 + 1) % 1
|
|
245
|
+
for (let i = 0; i < count; i++) {
|
|
246
|
+
const layer = Math.floor(i / cellsPerLayer)
|
|
247
|
+
const idx = i % cellsPerLayer
|
|
248
|
+
const c = idx % cols
|
|
249
|
+
const r = Math.floor(idx / cols)
|
|
250
|
+
const jX = (rng(i * 2) - 0.5) * cw * 0.2
|
|
251
|
+
const jZ = (rng(i * 2 + 1) - 0.5) * ch * 0.2
|
|
252
|
+
const x = -areaW / 2 + (c + 0.5) * stridX + jX
|
|
253
|
+
const z = -areaH / 2 + (r + 0.5) * stridZ + jZ
|
|
254
|
+
out.push({ x, y: layer * cd + cd / 2, z })
|
|
255
|
+
if (heightLimit && layer >= heightLimit - 1 && idx === cellsPerLayer - 1) break
|
|
256
|
+
}
|
|
257
|
+
return out
|
|
258
|
+
}
|
|
259
|
+
// row / staggered
|
|
260
|
+
for (let i = 0; i < count; i++) {
|
|
261
|
+
const layer = Math.floor(i / cellsPerLayer)
|
|
262
|
+
if (heightLimit && layer >= heightLimit) break
|
|
263
|
+
const idx = i % cellsPerLayer
|
|
264
|
+
const c = idx % cols
|
|
265
|
+
const r = Math.floor(idx / cols)
|
|
266
|
+
let x = -areaW / 2 + (c + 0.5) * stridX
|
|
267
|
+
const z = -areaH / 2 + (r + 0.5) * stridZ
|
|
268
|
+
if (pattern === 'staggered' && layer % 2 === 1) x += stridX / 2
|
|
269
|
+
out.push({ x, y: layer * cd + cd / 2, z })
|
|
270
|
+
}
|
|
271
|
+
return out
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private _carrierGeometry(preset: CarrierPreset, w: number, h: number, d: number): THREE.BufferGeometry {
|
|
275
|
+
if (preset === 'drum') {
|
|
276
|
+
const radius = Math.min(w, h) / 2
|
|
277
|
+
return new THREE.CylinderGeometry(radius, radius, d, 16)
|
|
278
|
+
}
|
|
279
|
+
if (preset === 'sack' || preset === 'bale') {
|
|
280
|
+
return new THREE.BoxGeometry(w * 0.92, d * 0.92, h * 0.92)
|
|
281
|
+
}
|
|
282
|
+
return new THREE.BoxGeometry(w, d, h)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private _getMaterial(colorHex: number): THREE.MeshStandardMaterial {
|
|
286
|
+
let m = this._materialByColor.get(colorHex)
|
|
287
|
+
if (!m) {
|
|
288
|
+
m = new THREE.MeshStandardMaterial({ color: colorHex, roughness: 0.7 })
|
|
289
|
+
this._materialByColor.set(colorHex, m)
|
|
290
|
+
}
|
|
291
|
+
return m
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>): void {
|
|
295
|
+
// 거의 모든 속성 변경이 grid 재구성 — 단순화.
|
|
296
|
+
const keys = [
|
|
297
|
+
'cols', 'rows', 'cellWidth', 'cellHeight',
|
|
298
|
+
'cells', 'data',
|
|
299
|
+
'stackPattern', 'carrierPreset',
|
|
300
|
+
'carrierWidth', 'carrierHeight', 'carrierDepth', 'carrierGap',
|
|
301
|
+
'capacity', 'stackHeightLimit', 'pickPolicy',
|
|
302
|
+
'fillStyle', 'strokeStyle',
|
|
303
|
+
'width', 'height'
|
|
304
|
+
]
|
|
305
|
+
if (keys.some(k => k in after)) {
|
|
306
|
+
this._rebuildAll()
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
super.onchange?.(after, before)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
updateDimension(): void {
|
|
313
|
+
this._rebuildAll()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
updateAlpha(): void {
|
|
317
|
+
const alpha = typeof (this.component.state as any).alpha === 'number'
|
|
318
|
+
? (this.component.state as any).alpha : 1
|
|
319
|
+
// pad 들의 opacity 만 (carriers 는 alpha 그대로)
|
|
320
|
+
for (const pad of this._padByCellId.values()) {
|
|
321
|
+
const m = pad.material as THREE.MeshStandardMaterial
|
|
322
|
+
m.opacity = 0.55 * alpha
|
|
323
|
+
m.transparent = m.opacity < 1
|
|
324
|
+
m.needsUpdate = true
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|