@operato/scene-storage 10.0.0-beta.28 → 10.0.0-beta.31
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 +29 -0
- package/dist/asrs-crane-3d.d.ts +10 -0
- package/dist/asrs-crane-3d.js +17 -0
- package/dist/asrs-crane-3d.js.map +1 -1
- package/dist/asrs-crane.d.ts +58 -13
- package/dist/asrs-crane.js +120 -16
- package/dist/asrs-crane.js.map +1 -1
- package/dist/asrs-rack.d.ts +58 -19
- package/dist/asrs-rack.js +107 -20
- package/dist/asrs-rack.js.map +1 -1
- package/dist/box.d.ts +10 -3
- package/dist/box.js +1 -2
- package/dist/box.js.map +1 -1
- package/dist/generic-container-3d.js.map +1 -1
- package/dist/generic-container.d.ts +12 -2
- package/dist/generic-container.js +1 -2
- package/dist/generic-container.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/pallet.d.ts +9 -2
- package/dist/pallet.js +1 -2
- package/dist/pallet.js.map +1 -1
- package/dist/parcel.d.ts +10 -3
- package/dist/parcel.js +1 -2
- package/dist/parcel.js.map +1 -1
- package/dist/rack-cell-3d.d.ts +25 -0
- package/dist/rack-cell-3d.js +88 -0
- package/dist/rack-cell-3d.js.map +1 -0
- package/dist/rack-cell.d.ts +64 -0
- package/dist/rack-cell.js +197 -0
- package/dist/rack-cell.js.map +1 -0
- package/dist/spot-3d.js.map +1 -1
- package/dist/spot.d.ts +12 -11
- package/dist/spot.js +2 -3
- package/dist/spot.js.map +1 -1
- package/dist/templates/index.d.ts +42 -0
- package/dist/templates/index.js +43 -1
- package/dist/templates/index.js.map +1 -1
- package/package.json +9 -4
- package/src/asrs-crane-3d.ts +20 -0
- package/src/asrs-crane.ts +153 -17
- package/src/asrs-rack.ts +137 -22
- package/src/box.ts +15 -5
- package/src/generic-container-3d.ts +1 -1
- package/src/generic-container.ts +22 -7
- package/src/index.ts +3 -0
- package/src/pallet.ts +16 -6
- package/src/parcel.ts +15 -5
- package/src/rack-cell-3d.ts +101 -0
- package/src/rack-cell.ts +241 -0
- package/src/spot-3d.ts +1 -1
- package/src/spot.ts +17 -7
- package/src/templates/index.ts +43 -1
- package/test/setup.js +279 -0
- package/test/test-asrs-crane.ts +319 -0
- package/tsconfig.json +2 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/generic-container.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
gltfNatureProperties,
|
|
33
33
|
sceneComponent
|
|
34
34
|
} from '@hatiolab/things-scene'
|
|
35
|
+
import type { State, Material3D } from '@hatiolab/things-scene'
|
|
35
36
|
import {
|
|
36
37
|
Legendable,
|
|
37
38
|
Placeable,
|
|
@@ -45,6 +46,20 @@ import { GenericContainer3D } from './generic-container-3d.js'
|
|
|
45
46
|
import type { ActuatorDef } from '@operato/scene-base'
|
|
46
47
|
|
|
47
48
|
export type ContainerStatus = 'empty' | 'partial' | 'full' | 'error'
|
|
49
|
+
export type ContainerFill = ContainerStatus
|
|
50
|
+
|
|
51
|
+
/** GenericContainer 컴포넌트 state */
|
|
52
|
+
export interface GenericContainerState extends State {
|
|
53
|
+
// ── 운영 상태 ──
|
|
54
|
+
fill?: ContainerFill
|
|
55
|
+
|
|
56
|
+
// ── GLB 동적 노드 ──
|
|
57
|
+
actuators?: Record<string, ActuatorDef>
|
|
58
|
+
actuatorValues?: Record<string, number>
|
|
59
|
+
|
|
60
|
+
// ── 3D 재질 ──
|
|
61
|
+
material3d?: Material3D
|
|
62
|
+
}
|
|
48
63
|
|
|
49
64
|
const BODY_LEGEND = {
|
|
50
65
|
empty: '#a8b8c4',
|
|
@@ -87,10 +102,10 @@ const NATURE: ComponentNature = {
|
|
|
87
102
|
|
|
88
103
|
// 합성 순서: 안쪽부터 → ContainerAbstract → Placeable → Legendable → GltfComponent
|
|
89
104
|
// (GenericFacility 와 동일 패턴)
|
|
90
|
-
const Base = GltfComponent(Legendable(Placeable(ContainerAbstract))) as unknown as typeof Component
|
|
91
|
-
|
|
92
105
|
@sceneComponent('container')
|
|
93
|
-
export default class GenericContainer extends
|
|
106
|
+
export default class GenericContainer extends GltfComponent(Legendable(Placeable(ContainerAbstract))) {
|
|
107
|
+
declare state: GenericContainerState
|
|
108
|
+
|
|
94
109
|
static legends: Record<string, LegendBinding> = {
|
|
95
110
|
bodyColor: { from: 'fill', legend: BODY_LEGEND },
|
|
96
111
|
lampEmissive: { from: 'fill', legend: LAMP_EMISSIVE_LEGEND }
|
|
@@ -109,18 +124,18 @@ export default class GenericContainer extends Base {
|
|
|
109
124
|
}
|
|
110
125
|
|
|
111
126
|
get actuators(): Record<string, ActuatorDef> {
|
|
112
|
-
return
|
|
127
|
+
return this.state.actuators ?? {}
|
|
113
128
|
}
|
|
114
129
|
|
|
115
130
|
get actuatorValues(): Record<string, number> {
|
|
116
|
-
return
|
|
131
|
+
return this.state.actuatorValues ?? {}
|
|
117
132
|
}
|
|
118
133
|
|
|
119
134
|
containable(component: Component): boolean {
|
|
120
|
-
return component.isDescendible(this
|
|
135
|
+
return component.isDescendible(this)
|
|
121
136
|
}
|
|
122
137
|
|
|
123
138
|
buildRealObject() {
|
|
124
|
-
return new GenericContainer3D(this
|
|
139
|
+
return new GenericContainer3D(this)
|
|
125
140
|
}
|
|
126
141
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,9 @@ export type { BoxMaterial } from './box.js'
|
|
|
10
10
|
export { default as Parcel } from './parcel.js'
|
|
11
11
|
|
|
12
12
|
export { default as AsrsRack } from './asrs-rack.js'
|
|
13
|
+
export { default as RackCell } from './rack-cell.js'
|
|
14
|
+
export type { RackCellType } from './rack-cell.js'
|
|
15
|
+
export { RackCell3D } from './rack-cell-3d.js'
|
|
13
16
|
export { default as AsrsCrane } from './asrs-crane.js'
|
|
14
17
|
export type { AsrsCraneStatus } from './asrs-crane.js'
|
|
15
18
|
|
package/src/pallet.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
import { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'
|
|
5
|
+
import type { State, Material3D } from '@hatiolab/things-scene'
|
|
5
6
|
import {
|
|
6
7
|
Carriable,
|
|
7
8
|
Legendable,
|
|
@@ -26,6 +27,15 @@ import { Pallet3D } from './pallet-3d.js'
|
|
|
26
27
|
*/
|
|
27
28
|
export type PalletMaterial = 'wood' | 'plastic'
|
|
28
29
|
|
|
30
|
+
/** Pallet 컴포넌트 state */
|
|
31
|
+
export interface PalletState extends State {
|
|
32
|
+
// ── 외관 ──
|
|
33
|
+
material?: PalletMaterial
|
|
34
|
+
|
|
35
|
+
// ── 3D 재질 ──
|
|
36
|
+
material3d?: Material3D
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
const BODY_LEGEND = {
|
|
30
40
|
wood: '#a87644',
|
|
31
41
|
plastic: '#5a6a78',
|
|
@@ -60,8 +70,6 @@ const NATURE: ComponentNature = {
|
|
|
60
70
|
// `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),
|
|
61
71
|
// which forces `isHTMLElement(): true` and trips the 3D pipeline's
|
|
62
72
|
// addObject DOM-skip gate. Pallet renders only as a 3D mesh.
|
|
63
|
-
const Base = Carriable(Legendable(Placeable(ContainerAbstract))) as unknown as typeof Component
|
|
64
|
-
|
|
65
73
|
/**
|
|
66
74
|
* Pallet — a flat transport structure that goods are stacked and stored on.
|
|
67
75
|
*
|
|
@@ -89,7 +97,9 @@ const Base = Carriable(Legendable(Placeable(ContainerAbstract))) as unknown as t
|
|
|
89
97
|
* detection.
|
|
90
98
|
*/
|
|
91
99
|
@sceneComponent('pallet')
|
|
92
|
-
export default class Pallet extends
|
|
100
|
+
export default class Pallet extends Carriable(Legendable(Placeable(ContainerAbstract))) {
|
|
101
|
+
declare state: PalletState
|
|
102
|
+
|
|
93
103
|
static legends: Record<string, LegendBinding> = {
|
|
94
104
|
bodyColor: { from: 'material', legend: BODY_LEGEND }
|
|
95
105
|
}
|
|
@@ -110,7 +120,7 @@ export default class Pallet extends Base {
|
|
|
110
120
|
containable(component: Component) {
|
|
111
121
|
const archetype = (component.constructor as any).placement
|
|
112
122
|
if (archetype === 'operation') return true
|
|
113
|
-
return component.isDescendible(this
|
|
123
|
+
return component.isDescendible(this)
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
/**
|
|
@@ -137,7 +147,7 @@ export default class Pallet extends Base {
|
|
|
137
147
|
super.postrender?.(ctx)
|
|
138
148
|
|
|
139
149
|
const { width, height, left, top } = this.state
|
|
140
|
-
const isPlastic =
|
|
150
|
+
const isPlastic = this.state.material === 'plastic'
|
|
141
151
|
|
|
142
152
|
ctx.save()
|
|
143
153
|
|
|
@@ -189,6 +199,6 @@ export default class Pallet extends Base {
|
|
|
189
199
|
}
|
|
190
200
|
|
|
191
201
|
buildRealObject(): RealObject | undefined {
|
|
192
|
-
return new Pallet3D(this
|
|
202
|
+
return new Pallet3D(this)
|
|
193
203
|
}
|
|
194
204
|
}
|
package/src/parcel.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { ComponentNature, RealObject, RectPath, Shape, sceneComponent } from '@hatiolab/things-scene'
|
|
5
|
+
import type { State, Material3D } from '@hatiolab/things-scene'
|
|
5
6
|
import {
|
|
6
7
|
Carriable,
|
|
7
8
|
Placeable,
|
|
@@ -11,6 +12,15 @@ import {
|
|
|
11
12
|
|
|
12
13
|
import { Parcel3D } from './parcel-3d.js'
|
|
13
14
|
|
|
15
|
+
/** Parcel 컴포넌트 state */
|
|
16
|
+
export interface ParcelState extends State {
|
|
17
|
+
// ── 정체 ──
|
|
18
|
+
trackingId?: string
|
|
19
|
+
|
|
20
|
+
// ── 3D 재질 ──
|
|
21
|
+
material3d?: Material3D
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
const NATURE: ComponentNature = {
|
|
15
25
|
mutable: false,
|
|
16
26
|
resizable: true,
|
|
@@ -28,8 +38,6 @@ const NATURE: ComponentNature = {
|
|
|
28
38
|
// Carriable: parcel can be a child of any CarrierHolder (Spot, robot-arm
|
|
29
39
|
// gripper, AGV deck, …). Mixin wraps add() so the parcel's 3D object3d
|
|
30
40
|
// is reattached to the holder's chosen mount frame.
|
|
31
|
-
const Base = Carriable(Placeable(RectPath(Shape))) as unknown as typeof Component
|
|
32
|
-
|
|
33
41
|
/**
|
|
34
42
|
* Parcel — a cardboard package, the typical e-commerce / parcel-sortation unit.
|
|
35
43
|
*
|
|
@@ -46,7 +54,9 @@ const Base = Carriable(Placeable(RectPath(Shape))) as unknown as typeof Componen
|
|
|
46
54
|
* inspected indicators would add a status legend then.
|
|
47
55
|
*/
|
|
48
56
|
@sceneComponent('parcel')
|
|
49
|
-
export default class Parcel extends
|
|
57
|
+
export default class Parcel extends Carriable(Placeable(RectPath(Shape))) {
|
|
58
|
+
declare state: ParcelState
|
|
59
|
+
|
|
50
60
|
static placement: PlacementArchetype = 'operation'
|
|
51
61
|
static align: Alignment = 'bottom'
|
|
52
62
|
static defaultDepth = 150
|
|
@@ -71,6 +81,6 @@ export default class Parcel extends Base {
|
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
buildRealObject(): RealObject | undefined {
|
|
74
|
-
return new Parcel3D(this
|
|
84
|
+
return new Parcel3D(this)
|
|
75
85
|
}
|
|
76
86
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* RackCell 3D — invisible anchor group positioned at the cell's location
|
|
5
|
+
* within the parent rack's 3D coordinate space.
|
|
6
|
+
*
|
|
7
|
+
* RackCell has no geometry of its own. Its sole 3D purpose is to provide
|
|
8
|
+
* an Object3D that carriers can be attached to (via Three.js `.attach()`),
|
|
9
|
+
* placed at the exact cell position within the rack. The position is derived
|
|
10
|
+
* from the parent AsrsRack's CellMap (by cellId), not from 2D state fields —
|
|
11
|
+
* rack cells occupy 3D levels that have no 2D analogue.
|
|
12
|
+
*
|
|
13
|
+
* updateTransform() override: things-scene's standard updateTransform
|
|
14
|
+
* reads `component.center` (2D) and flattens it to 3D. For rack cells this
|
|
15
|
+
* is wrong — we need the 3D cell position (bay x, level y, row z) from the
|
|
16
|
+
* parent rack. So we override and read it directly from the CellMap.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import * as THREE from 'three'
|
|
20
|
+
import { RealObjectGroup } from '@hatiolab/things-scene'
|
|
21
|
+
|
|
22
|
+
export class RackCell3D extends RealObjectGroup {
|
|
23
|
+
build() {
|
|
24
|
+
super.build()
|
|
25
|
+
this._repositionFromCellMap()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
updateDimension() {
|
|
29
|
+
// intentional no-op — size comes from the cell definition, not state
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
updateTransform() {
|
|
33
|
+
this._repositionFromCellMap()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
updateAlpha() {
|
|
37
|
+
// invisible — no materials to update
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Position this group at the cell's localPosition within the rack's 3D space.
|
|
42
|
+
*
|
|
43
|
+
* CellMap.grid() places cell origins at:
|
|
44
|
+
* x = b * bayWidth, y = l * levelHeight, z = r * rowDepth
|
|
45
|
+
* (starting from 0,0,0 at the rack's bottom-left-front corner).
|
|
46
|
+
*
|
|
47
|
+
* The rack's object3d is centered: X spans [-width/2, +width/2],
|
|
48
|
+
* Y spans [-depth/2, +depth/2], Z spans [-height/2, +height/2].
|
|
49
|
+
*
|
|
50
|
+
* So the cell centre in rack-local 3D space:
|
|
51
|
+
* x3d = cellPos.x + bayWidth/2 - width/2
|
|
52
|
+
* y3d = cellPos.y + levelHeight/2 - rackDepth/2
|
|
53
|
+
* z3d = cellPos.z + rowDepth/2 - rackHeight/2
|
|
54
|
+
*/
|
|
55
|
+
_repositionFromCellMap() {
|
|
56
|
+
const rack = (this.component as any).parent
|
|
57
|
+
if (!rack?.cellMap) return
|
|
58
|
+
|
|
59
|
+
const cellId = this.component.state.cellId as string | undefined
|
|
60
|
+
if (!cellId) return
|
|
61
|
+
|
|
62
|
+
const cell = rack.cellMap.findById(cellId)
|
|
63
|
+
if (!cell) return
|
|
64
|
+
|
|
65
|
+
const rs = rack.state as any
|
|
66
|
+
const rackWidth = (rs?.width as number) || 1000
|
|
67
|
+
const rackDepth = (rs?.depth as number) || 3000 // Y dimension (floor→ceiling)
|
|
68
|
+
const rackHeight = (rs?.height as number) || 600 // Z dimension (front→back, 2D height)
|
|
69
|
+
const bays = Math.max(1, Math.floor(rs?.bays || 5))
|
|
70
|
+
const levels = Math.max(1, Math.floor(rs?.levels || 4))
|
|
71
|
+
const rows = 1
|
|
72
|
+
|
|
73
|
+
const bayWidth = rackWidth / bays
|
|
74
|
+
const levelHeight = rackDepth / levels
|
|
75
|
+
const rowDepth = rackHeight / rows
|
|
76
|
+
|
|
77
|
+
const x3d = cell.localPosition.x + bayWidth / 2 - rackWidth / 2
|
|
78
|
+
const y3d = cell.localPosition.y + levelHeight / 2 - rackDepth / 2
|
|
79
|
+
const z3d = cell.localPosition.z + rowDepth / 2 - rackHeight / 2
|
|
80
|
+
|
|
81
|
+
this.object3d.position.set(x3d, y3d, z3d)
|
|
82
|
+
|
|
83
|
+
// Optionally visualise cells in debug mode (outline box, very faint)
|
|
84
|
+
if (process.env.NODE_ENV !== 'production' && (rack.state as any)?.debugCells) {
|
|
85
|
+
this._addDebugBox(bayWidth, levelHeight, rowDepth)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private _debugBox?: THREE.LineSegments
|
|
90
|
+
|
|
91
|
+
private _addDebugBox(w: number, h: number, d: number) {
|
|
92
|
+
if (this._debugBox) return
|
|
93
|
+
const geo = new THREE.BoxGeometry(w * 0.98, h * 0.98, d * 0.98)
|
|
94
|
+
const edges = new THREE.EdgesGeometry(geo)
|
|
95
|
+
this._debugBox = new THREE.LineSegments(
|
|
96
|
+
edges,
|
|
97
|
+
new THREE.LineBasicMaterial({ color: 0x00ff88, transparent: true, opacity: 0.2 })
|
|
98
|
+
)
|
|
99
|
+
this.object3d.add(this._debugBox)
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/rack-cell.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* RackCell — a single storage slot within an AsrsRack.
|
|
5
|
+
*
|
|
6
|
+
* A RackCell is a virtual component: it occupies a specific (bay, row, level)
|
|
7
|
+
* coordinate within the parent rack and acts as a CarrierHolder for one carrier
|
|
8
|
+
* (or several, depending on `cellType`).
|
|
9
|
+
*
|
|
10
|
+
* The crane (AsrsCrane) navigates toward a RackCell as its `place()` destination —
|
|
11
|
+
* because the rack cell is a discrete component, Mover.moveTo() can target it
|
|
12
|
+
* directly and arrive at exactly the right bay × level position.
|
|
13
|
+
*
|
|
14
|
+
* Visual: invisible in 2D (no visible 2D footprint — rack cells don't make
|
|
15
|
+
* sense as 2D top-down boxes). In 3D, each cell is an invisible Group
|
|
16
|
+
* positioned within the rack's coordinate space (RackCell3D handles
|
|
17
|
+
* this via updateTransform override).
|
|
18
|
+
*
|
|
19
|
+
* Lifecycle: AsrsRack._buildCells() instantiates RackCell children.
|
|
20
|
+
* Do not create RackCell components manually — they are managed by the rack.
|
|
21
|
+
*
|
|
22
|
+
* Domain aliases:
|
|
23
|
+
* cell.store(carrier) ← cell.receive(carrier)
|
|
24
|
+
* cell.retrieve(carrier, target) ← cell.dispatch(carrier, target)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
Component,
|
|
29
|
+
ComponentNature,
|
|
30
|
+
ContainerAbstract,
|
|
31
|
+
RealObject,
|
|
32
|
+
TRANSFER_SLOT_KEY,
|
|
33
|
+
sceneComponent
|
|
34
|
+
} from '@hatiolab/things-scene'
|
|
35
|
+
import type { State, Material3D } from '@hatiolab/things-scene'
|
|
36
|
+
import { CarrierHolder, type AttachFrame } from '@operato/scene-base'
|
|
37
|
+
|
|
38
|
+
import { RackCell3D } from './rack-cell-3d.js'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* How many carriers a cell can hold simultaneously.
|
|
42
|
+
* - single: exactly 1 (typical pallet bay)
|
|
43
|
+
* - multi: small stack (up to 4, e.g. a multi-deep tray)
|
|
44
|
+
* - bulk: unlimited (e.g. a floor area measured in slots)
|
|
45
|
+
*/
|
|
46
|
+
export type RackCellType = 'single' | 'multi' | 'bulk'
|
|
47
|
+
|
|
48
|
+
/** RackCell 컴포넌트 state */
|
|
49
|
+
export interface RackCellState extends State {
|
|
50
|
+
// ── 식별 ──
|
|
51
|
+
cellId?: string
|
|
52
|
+
cellType?: RackCellType
|
|
53
|
+
|
|
54
|
+
// ── 3D 재질 ──
|
|
55
|
+
material3d?: Material3D
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const NATURE: ComponentNature = {
|
|
59
|
+
mutable: false,
|
|
60
|
+
resizable: false,
|
|
61
|
+
rotatable: false,
|
|
62
|
+
properties: [
|
|
63
|
+
{
|
|
64
|
+
type: 'string',
|
|
65
|
+
label: 'cell-id',
|
|
66
|
+
name: 'cellId',
|
|
67
|
+
placeholder: 'e.g. 0-0-0'
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
type: 'select',
|
|
71
|
+
label: 'cell-type',
|
|
72
|
+
name: 'cellType',
|
|
73
|
+
property: {
|
|
74
|
+
options: [
|
|
75
|
+
{ display: 'Single', value: 'single' },
|
|
76
|
+
{ display: 'Multi', value: 'multi' },
|
|
77
|
+
{ display: 'Bulk', value: 'bulk' }
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
help: 'scene/component/rack-cell'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* RackCell — single-slot storage cell inside an AsrsRack.
|
|
87
|
+
*
|
|
88
|
+
* Mixin chain: CarrierHolder(ContainerAbstract)
|
|
89
|
+
* - CarrierHolder: publishes attachPointFor(), gates containable() to Carriables
|
|
90
|
+
* - ContainerAbstract: manages child carrier components
|
|
91
|
+
*
|
|
92
|
+
* No Placeable mixin — RackCell3D self-positions from the parent rack's
|
|
93
|
+
* CellMap (via updateTransform override), bypassing things-scene's standard
|
|
94
|
+
* 2D→3D coordinate mapping which cannot express 3D levels.
|
|
95
|
+
*/
|
|
96
|
+
@sceneComponent('rack-cell')
|
|
97
|
+
export default class RackCell extends CarrierHolder(ContainerAbstract) {
|
|
98
|
+
declare state: RackCellState
|
|
99
|
+
|
|
100
|
+
// ── Identification ────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
get cellId(): string {
|
|
103
|
+
return this.state.cellId ?? ''
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get cellType(): RackCellType {
|
|
107
|
+
return this.state.cellType ?? 'single'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Maximum carrier count for this cell based on cellType. */
|
|
111
|
+
get capacity(): number {
|
|
112
|
+
switch (this.cellType) {
|
|
113
|
+
case 'single': return 1
|
|
114
|
+
case 'multi': return 4
|
|
115
|
+
case 'bulk': return Infinity
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Interface ─────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
get nature(): ComponentNature {
|
|
122
|
+
return NATURE
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
get anchors(): [] {
|
|
126
|
+
return []
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Transfer protocol ─────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
/** True when fewer carriers are currently held than capacity. */
|
|
132
|
+
canReceive(_component?: any): boolean {
|
|
133
|
+
const occupied = (this.components as Component[] | undefined)?.length ?? 0
|
|
134
|
+
return occupied < this.capacity
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Accept a carrier into this cell.
|
|
139
|
+
* Sets TRANSFER_SLOT_KEY = cellId on the carrier, then reparents.
|
|
140
|
+
* Fires 'transfer-received' so monitors can react.
|
|
141
|
+
*/
|
|
142
|
+
async receive(carrier: any, options: any = {}): Promise<void> {
|
|
143
|
+
if (!this.canReceive(carrier)) {
|
|
144
|
+
this.trigger('transfer-rejected', {
|
|
145
|
+
type: 'transfer-rejected',
|
|
146
|
+
component: carrier,
|
|
147
|
+
container: this,
|
|
148
|
+
reason: 'no-slot'
|
|
149
|
+
})
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
carrier[TRANSFER_SLOT_KEY] = this.cellId
|
|
153
|
+
this.reparent(carrier, options)
|
|
154
|
+
this.trigger('transfer-received', {
|
|
155
|
+
type: 'transfer-received',
|
|
156
|
+
component: carrier,
|
|
157
|
+
container: this,
|
|
158
|
+
slotId: this.cellId
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Release a carrier from this cell to `target`.
|
|
164
|
+
* Delegates to `target.receive()` if available, otherwise `target.reparent()`.
|
|
165
|
+
*/
|
|
166
|
+
async dispatch(carrier: any, target: any, options: any = {}): Promise<void> {
|
|
167
|
+
if (target?.canReceive && !target.canReceive(carrier)) {
|
|
168
|
+
this.trigger('transfer-rejected', {
|
|
169
|
+
type: 'transfer-rejected',
|
|
170
|
+
component: carrier,
|
|
171
|
+
container: this,
|
|
172
|
+
reason: 'target-full'
|
|
173
|
+
})
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
delete carrier[TRANSFER_SLOT_KEY]
|
|
177
|
+
if (typeof target?.receive === 'function') {
|
|
178
|
+
await target.receive(carrier, options)
|
|
179
|
+
} else {
|
|
180
|
+
;(target as any).reparent?.(carrier, options)
|
|
181
|
+
}
|
|
182
|
+
this.trigger('transfer-dispatched', {
|
|
183
|
+
type: 'transfer-dispatched',
|
|
184
|
+
component: carrier,
|
|
185
|
+
container: this,
|
|
186
|
+
target
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Domain aliases ────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
/** Alias for receive() — semantic sugar for the storage domain. */
|
|
193
|
+
store(carrier: any, options?: any): Promise<void> {
|
|
194
|
+
return this.receive(carrier, options)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Alias for dispatch() — semantic sugar for the storage domain. */
|
|
198
|
+
retrieve(carrier: any, target: any, options?: any): Promise<void> {
|
|
199
|
+
return this.dispatch(carrier, target, options)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── 3D attach frame ───────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Return the 3D attach frame for carriers placed in this cell.
|
|
206
|
+
* Carriers are lifted by their own halfDepth so the bottom face
|
|
207
|
+
* rests at the cell's Y-center (which is levelHeight/2 above the beam).
|
|
208
|
+
*/
|
|
209
|
+
attachPointFor(carrier: Component): AttachFrame | null {
|
|
210
|
+
const root = this._realObject?.object3d
|
|
211
|
+
if (!root) return null
|
|
212
|
+
const carrierDepth = resolveCarrierDepth(carrier)
|
|
213
|
+
return {
|
|
214
|
+
attach: root,
|
|
215
|
+
localPosition: { x: 0, y: carrierDepth / 2, z: 0 }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── 2D rendering ──────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
/** RackCell has no 2D visual — the rack draws its own structure. */
|
|
222
|
+
render(_ctx: CanvasRenderingContext2D) {
|
|
223
|
+
// intentional no-op
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── 3D ───────────────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
buildRealObject(): RealObject | undefined {
|
|
229
|
+
return new RackCell3D(this)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function resolveCarrierDepth(c: Component): number {
|
|
234
|
+
const eff = (c as any)._realObject?.effectiveDepth
|
|
235
|
+
if (typeof eff === 'number' && Number.isFinite(eff)) return eff
|
|
236
|
+
return numOr((c as any)?.state?.depth, 0)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function numOr(v: unknown, dflt: number): number {
|
|
240
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : dflt
|
|
241
|
+
}
|
package/src/spot-3d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export class Spot3D extends RealObjectGroup {
|
|
|
37
37
|
build() {
|
|
38
38
|
super.build()
|
|
39
39
|
|
|
40
|
-
const state = this.component.state
|
|
40
|
+
const state = this.component.state
|
|
41
41
|
const w = Math.max(Math.abs(numOr(state.width, 100)), 1)
|
|
42
42
|
const h = Math.max(Math.abs(numOr(state.height, 100)), 1)
|
|
43
43
|
const d = this.effectiveDepth // 2 by default (thin pad)
|
package/src/spot.ts
CHANGED
|
@@ -30,9 +30,11 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'
|
|
33
|
+
import type { State, Material3D } from '@hatiolab/things-scene'
|
|
33
34
|
import {
|
|
34
35
|
CarrierHolder,
|
|
35
36
|
Placeable,
|
|
37
|
+
type AttachFrame,
|
|
36
38
|
type Alignment,
|
|
37
39
|
type Heights,
|
|
38
40
|
type PlacementArchetype
|
|
@@ -40,6 +42,13 @@ import {
|
|
|
40
42
|
|
|
41
43
|
import { Spot3D } from './spot-3d.js'
|
|
42
44
|
|
|
45
|
+
/** Spot 컴포넌트 state */
|
|
46
|
+
export interface SpotState extends State {
|
|
47
|
+
// Spot has no component-specific state — it uses standard fillStyle /
|
|
48
|
+
// strokeStyle / lineWidth / alpha / text / font* / material3d.
|
|
49
|
+
material3d?: Material3D
|
|
50
|
+
}
|
|
51
|
+
|
|
43
52
|
const NATURE: ComponentNature = {
|
|
44
53
|
mutable: false,
|
|
45
54
|
resizable: true,
|
|
@@ -58,10 +67,11 @@ const NATURE: ComponentNature = {
|
|
|
58
67
|
// `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),
|
|
59
68
|
// which forces `isHTMLElement(): true` and trips the 3D pipeline's
|
|
60
69
|
// addObject DOM-skip gate. Spot is purely 3D.
|
|
61
|
-
const Base = CarrierHolder(Placeable(ContainerAbstract)) as unknown as typeof Component
|
|
62
|
-
|
|
63
70
|
@sceneComponent('spot')
|
|
64
|
-
export default class Spot extends
|
|
71
|
+
export default class Spot extends CarrierHolder(Placeable(ContainerAbstract)) {
|
|
72
|
+
declare state: SpotState
|
|
73
|
+
declare _realObject?: Spot3D
|
|
74
|
+
|
|
65
75
|
static placement: PlacementArchetype = 'floor'
|
|
66
76
|
static align: Alignment = 'bottom'
|
|
67
77
|
static defaultDepth = (_h: Heights) => 2 // a thin pad
|
|
@@ -132,7 +142,7 @@ export default class Spot extends Base {
|
|
|
132
142
|
}
|
|
133
143
|
|
|
134
144
|
buildRealObject(): RealObject | undefined {
|
|
135
|
-
return new Spot3D(this
|
|
145
|
+
return new Spot3D(this)
|
|
136
146
|
}
|
|
137
147
|
|
|
138
148
|
/**
|
|
@@ -147,10 +157,10 @@ export default class Spot extends Base {
|
|
|
147
157
|
* falling back to raw `state.depth` for components built before
|
|
148
158
|
* RealObject creation.
|
|
149
159
|
*/
|
|
150
|
-
attachPointFor(carrier: Component) {
|
|
151
|
-
const ro =
|
|
160
|
+
attachPointFor(carrier: Component): AttachFrame | null {
|
|
161
|
+
const ro = this._realObject
|
|
152
162
|
const frame = ro?.getAttachFrame?.()
|
|
153
|
-
if (!frame) return
|
|
163
|
+
if (!frame) return null
|
|
154
164
|
const carrierDepth = resolveDepth(carrier)
|
|
155
165
|
return {
|
|
156
166
|
attach: frame,
|
package/src/templates/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* things-scene catalog templates for the storage domain — pallet/box/parcel
|
|
3
|
-
* variants, ASRS rack/crane, and the virtual `spot` placement marker.
|
|
3
|
+
* variants, ASRS rack/crane, ASRS aisle composite, and the virtual `spot` placement marker.
|
|
4
4
|
*/
|
|
5
5
|
import spot from './spot.js'
|
|
6
6
|
const pallet = new URL('../../icons/pallet.png', import.meta.url).href
|
|
@@ -109,5 +109,47 @@ export default [
|
|
|
109
109
|
carriageHeight: 100
|
|
110
110
|
}
|
|
111
111
|
},
|
|
112
|
+
{
|
|
113
|
+
type: 'group',
|
|
114
|
+
description: 'AS/RS aisle — 4-level rack pair + stacker crane',
|
|
115
|
+
group: 'storage',
|
|
116
|
+
icon: asrsRack,
|
|
117
|
+
model: {
|
|
118
|
+
type: 'group',
|
|
119
|
+
top: 100,
|
|
120
|
+
left: 100,
|
|
121
|
+
width: 840,
|
|
122
|
+
height: 220,
|
|
123
|
+
components: [
|
|
124
|
+
{
|
|
125
|
+
type: 'asrs-rack',
|
|
126
|
+
top: 100,
|
|
127
|
+
left: 100,
|
|
128
|
+
width: 800,
|
|
129
|
+
height: 80,
|
|
130
|
+
levels: 4,
|
|
131
|
+
bays: 8
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
type: 'asrs-crane',
|
|
135
|
+
top: 140,
|
|
136
|
+
left: 420,
|
|
137
|
+
width: 60,
|
|
138
|
+
height: 220,
|
|
139
|
+
status: 'idle',
|
|
140
|
+
carriageHeight: 0
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: 'asrs-rack',
|
|
144
|
+
top: 320,
|
|
145
|
+
left: 100,
|
|
146
|
+
width: 800,
|
|
147
|
+
height: 80,
|
|
148
|
+
levels: 4,
|
|
149
|
+
bays: 8
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
},
|
|
112
154
|
spot
|
|
113
155
|
]
|