@operato/scene-storage 10.0.0-beta.47 → 10.0.0-beta.50

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/index.d.ts +9 -0
  3. package/dist/index.js +6 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/picking-station-3d.d.ts +20 -0
  6. package/dist/picking-station-3d.js +162 -0
  7. package/dist/picking-station-3d.js.map +1 -0
  8. package/dist/picking-station.d.ts +56 -0
  9. package/dist/picking-station.js +212 -0
  10. package/dist/picking-station.js.map +1 -0
  11. package/dist/rack-capability.d.ts +11 -0
  12. package/dist/rack-capability.js +25 -0
  13. package/dist/rack-capability.js.map +1 -0
  14. package/dist/rack-grid.js +3 -10
  15. package/dist/rack-grid.js.map +1 -1
  16. package/dist/spot.d.ts +19 -1
  17. package/dist/spot.js +63 -1
  18. package/dist/spot.js.map +1 -1
  19. package/dist/stockpile-3d.d.ts +55 -0
  20. package/dist/stockpile-3d.js +387 -0
  21. package/dist/stockpile-3d.js.map +1 -0
  22. package/dist/stockpile-grid-3d.d.ts +30 -0
  23. package/dist/stockpile-grid-3d.js +301 -0
  24. package/dist/stockpile-grid-3d.js.map +1 -0
  25. package/dist/stockpile-grid.d.ts +88 -0
  26. package/dist/stockpile-grid.js +429 -0
  27. package/dist/stockpile-grid.js.map +1 -0
  28. package/dist/stockpile.d.ts +133 -0
  29. package/dist/stockpile.js +439 -0
  30. package/dist/stockpile.js.map +1 -0
  31. package/dist/storage-rack.d.ts +12 -0
  32. package/dist/storage-rack.js +20 -10
  33. package/dist/storage-rack.js.map +1 -1
  34. package/dist/templates/index.d.ts +80 -0
  35. package/dist/templates/index.js +7 -1
  36. package/dist/templates/index.js.map +1 -1
  37. package/dist/templates/picking-station.d.ts +20 -0
  38. package/dist/templates/picking-station.js +22 -0
  39. package/dist/templates/picking-station.js.map +1 -0
  40. package/dist/templates/stockpile-grid.d.ts +37 -0
  41. package/dist/templates/stockpile-grid.js +38 -0
  42. package/dist/templates/stockpile-grid.js.map +1 -0
  43. package/dist/templates/stockpile.d.ts +29 -0
  44. package/dist/templates/stockpile.js +31 -0
  45. package/dist/templates/stockpile.js.map +1 -0
  46. package/package.json +3 -3
  47. package/src/index.ts +14 -0
  48. package/src/picking-station-3d.ts +164 -0
  49. package/src/picking-station.ts +243 -0
  50. package/src/rack-capability.ts +26 -0
  51. package/src/rack-grid.ts +3 -8
  52. package/src/spot.ts +62 -0
  53. package/src/stockpile-3d.ts +412 -0
  54. package/src/stockpile-grid-3d.ts +327 -0
  55. package/src/stockpile-grid.ts +456 -0
  56. package/src/stockpile.ts +508 -0
  57. package/src/storage-rack.ts +21 -8
  58. package/src/templates/index.ts +7 -1
  59. package/src/templates/picking-station.ts +23 -0
  60. package/src/templates/stockpile-grid.ts +39 -0
  61. package/src/templates/stockpile.ts +32 -0
  62. package/test/test-rack-capability.ts +51 -0
  63. package/translations/en.json +18 -6
  64. package/translations/ja.json +18 -6
  65. package/translations/ko.json +17 -5
  66. package/translations/ms.json +18 -6
  67. package/translations/zh.json +17 -5
  68. 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
+ }