@operato/scene-storage 10.0.0-beta.30 → 10.0.0-beta.32
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/asrs-crane.d.ts +16 -1
- package/dist/asrs-crane.js +8 -0
- package/dist/asrs-crane.js.map +1 -1
- package/dist/asrs-rack.d.ts +9 -0
- package/dist/asrs-rack.js +2 -3
- package/dist/asrs-rack.js.map +1 -1
- package/dist/box.d.ts +16 -0
- package/dist/box.js +28 -1
- package/dist/box.js.map +1 -1
- package/dist/generic-container-3d.js.map +1 -1
- package/dist/generic-container.d.ts +10 -0
- package/dist/generic-container.js.map +1 -1
- package/dist/pallet.d.ts +20 -0
- package/dist/pallet.js +39 -1
- package/dist/pallet.js.map +1 -1
- package/dist/parcel.d.ts +13 -0
- package/dist/parcel.js +25 -1
- package/dist/parcel.js.map +1 -1
- package/dist/rack-cell-3d.js +1 -1
- package/dist/rack-cell-3d.js.map +1 -1
- package/dist/rack-cell.d.ts +8 -0
- package/dist/rack-cell.js +7 -10
- package/dist/rack-cell.js.map +1 -1
- package/dist/spot-3d.js.map +1 -1
- package/dist/spot.d.ts +8 -0
- package/dist/spot.js.map +1 -1
- package/package.json +3 -3
- package/src/asrs-crane.ts +30 -6
- package/src/asrs-rack.ts +23 -7
- package/src/box.ts +51 -2
- package/src/generic-container-3d.ts +1 -1
- package/src/generic-container.ts +21 -4
- package/src/pallet.ts +67 -4
- package/src/parcel.ts +48 -2
- package/src/rack-cell-3d.ts +1 -1
- package/src/rack-cell.ts +23 -10
- package/src/spot-3d.ts +1 -1
- package/src/spot.ts +13 -2
- package/test/test-phase-h-carrier-pickable.ts +105 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/asrs-rack.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
|
CellContainer,
|
|
7
8
|
CellMap,
|
|
@@ -15,6 +16,19 @@ import {
|
|
|
15
16
|
|
|
16
17
|
import { AsrsRack3D } from './asrs-rack-3d.js'
|
|
17
18
|
|
|
19
|
+
/** AsrsRack 컴포넌트 state */
|
|
20
|
+
export interface AsrsRackState extends State {
|
|
21
|
+
// ── 토폴로지 ──
|
|
22
|
+
bays?: number
|
|
23
|
+
levels?: number
|
|
24
|
+
|
|
25
|
+
// ── 디버그 ──
|
|
26
|
+
debugCells?: boolean
|
|
27
|
+
|
|
28
|
+
// ── 3D 재질 ──
|
|
29
|
+
material3d?: Material3D
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
const NATURE: ComponentNature = {
|
|
19
33
|
mutable: false,
|
|
20
34
|
resizable: true,
|
|
@@ -64,6 +78,8 @@ const NATURE: ComponentNature = {
|
|
|
64
78
|
*/
|
|
65
79
|
@sceneComponent('asrs-rack')
|
|
66
80
|
export default class AsrsRack extends CellContainer(CarrierHolder(Placeable(ContainerAbstract))) {
|
|
81
|
+
declare state: AsrsRackState
|
|
82
|
+
|
|
67
83
|
static placement: PlacementArchetype = 'floor'
|
|
68
84
|
static align: Alignment = 'bottom'
|
|
69
85
|
static defaultDepth = (h: Heights) => h.ceiling - h.floor
|
|
@@ -115,10 +131,10 @@ export default class AsrsRack extends CellContainer(CarrierHolder(Placeable(Cont
|
|
|
115
131
|
*/
|
|
116
132
|
_buildCells(): void {
|
|
117
133
|
// Remove existing rack-cell children
|
|
118
|
-
const existing = (
|
|
134
|
+
const existing = (this.components as Component[] | undefined) ?? []
|
|
119
135
|
for (const child of [...existing]) {
|
|
120
136
|
if ((child as any).state?.type === 'rack-cell') {
|
|
121
|
-
|
|
137
|
+
this.removeComponent(child)
|
|
122
138
|
}
|
|
123
139
|
}
|
|
124
140
|
|
|
@@ -129,7 +145,7 @@ export default class AsrsRack extends CellContainer(CarrierHolder(Placeable(Cont
|
|
|
129
145
|
return
|
|
130
146
|
}
|
|
131
147
|
|
|
132
|
-
const context =
|
|
148
|
+
const context = this._app
|
|
133
149
|
for (const cell of this.cellMap.cells) {
|
|
134
150
|
const model = {
|
|
135
151
|
type: 'rack-cell',
|
|
@@ -139,7 +155,7 @@ export default class AsrsRack extends CellContainer(CarrierHolder(Placeable(Cont
|
|
|
139
155
|
depth: cell.size.height // 3D Y = level height
|
|
140
156
|
}
|
|
141
157
|
const rackCell = new RackCellClass(model, context)
|
|
142
|
-
|
|
158
|
+
this.addComponent(rackCell)
|
|
143
159
|
}
|
|
144
160
|
}
|
|
145
161
|
|
|
@@ -157,7 +173,7 @@ export default class AsrsRack extends CellContainer(CarrierHolder(Placeable(Cont
|
|
|
157
173
|
if ((component as any).state?.type === 'rack-cell') return true
|
|
158
174
|
const archetype = (component.constructor as any).placement
|
|
159
175
|
if (archetype === 'operation') return true
|
|
160
|
-
return component.isDescendible(this
|
|
176
|
+
return component.isDescendible(this)
|
|
161
177
|
}
|
|
162
178
|
|
|
163
179
|
// ── CarrierHolder — attach frame for direct carrier children ─────────────
|
|
@@ -173,7 +189,7 @@ export default class AsrsRack extends CellContainer(CarrierHolder(Placeable(Cont
|
|
|
173
189
|
* returns the rack's own object3d as the attach frame (default behavior).
|
|
174
190
|
*/
|
|
175
191
|
attachPointFor(_carrier: Component): AttachFrame | null {
|
|
176
|
-
const root =
|
|
192
|
+
const root = this._realObject?.object3d
|
|
177
193
|
if (!root) return null
|
|
178
194
|
return { attach: root }
|
|
179
195
|
}
|
|
@@ -206,6 +222,6 @@ export default class AsrsRack extends CellContainer(CarrierHolder(Placeable(Cont
|
|
|
206
222
|
// ── 3D ───────────────────────────────────────────────────────────────────
|
|
207
223
|
|
|
208
224
|
buildRealObject(): RealObject | undefined {
|
|
209
|
-
return new AsrsRack3D(this
|
|
225
|
+
return new AsrsRack3D(this)
|
|
210
226
|
}
|
|
211
227
|
}
|
package/src/box.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ComponentNature,
|
|
6
|
+
RealObject,
|
|
7
|
+
RectPath,
|
|
8
|
+
Shape,
|
|
9
|
+
topApproachFrame,
|
|
10
|
+
getWorldPose,
|
|
11
|
+
sceneComponent
|
|
12
|
+
} from '@hatiolab/things-scene'
|
|
13
|
+
import type { State, Material3D, PickupFrame, PoseSerialized } from '@hatiolab/things-scene'
|
|
5
14
|
import {
|
|
6
15
|
Carriable,
|
|
7
16
|
Legendable,
|
|
@@ -26,6 +35,15 @@ import { Box3D } from './box-3d.js'
|
|
|
26
35
|
*/
|
|
27
36
|
export type BoxMaterial = 'wood' | 'plastic'
|
|
28
37
|
|
|
38
|
+
/** Box 컴포넌트 state */
|
|
39
|
+
export interface BoxState extends State {
|
|
40
|
+
// ── 외관 ──
|
|
41
|
+
material?: BoxMaterial
|
|
42
|
+
|
|
43
|
+
// ── 3D 재질 ──
|
|
44
|
+
material3d?: Material3D
|
|
45
|
+
}
|
|
46
|
+
|
|
29
47
|
const BODY_LEGEND = {
|
|
30
48
|
wood: '#a87644',
|
|
31
49
|
plastic: '#3a5078',
|
|
@@ -64,6 +82,8 @@ const NATURE: ComponentNature = {
|
|
|
64
82
|
*/
|
|
65
83
|
@sceneComponent('box')
|
|
66
84
|
export default class Box extends Carriable(Legendable(Placeable(RectPath(Shape)))) {
|
|
85
|
+
declare state: BoxState
|
|
86
|
+
|
|
67
87
|
static legends: Record<string, LegendBinding> = {
|
|
68
88
|
bodyColor: { from: 'material', legend: BODY_LEGEND }
|
|
69
89
|
}
|
|
@@ -92,6 +112,35 @@ export default class Box extends Carriable(Legendable(Placeable(RectPath(Shape))
|
|
|
92
112
|
}
|
|
93
113
|
|
|
94
114
|
buildRealObject(): RealObject | undefined {
|
|
95
|
-
return new Box3D(this
|
|
115
|
+
return new Box3D(this)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Phase H — pickup contract. Box 는 위에서 gripper / vacuum cup 으로 집기 —
|
|
120
|
+
* 단일 entry (top center). Box 의 dimensions 가 작아서 forklift fork 보다는
|
|
121
|
+
* gripper 가 일반적. forklift 로 들어올릴 box 는 통상 pallet 위에 stacking
|
|
122
|
+
* 후 pallet 째로 운반.
|
|
123
|
+
*
|
|
124
|
+
* tolerance 가 pallet 보다 빡빡 (gripper 정밀도 vs forklift pocket 폭).
|
|
125
|
+
*/
|
|
126
|
+
pickupFrames(): PickupFrame[] {
|
|
127
|
+
const wp = getWorldPose(this)
|
|
128
|
+
const me: PoseSerialized = {
|
|
129
|
+
position: { x: wp.position.x, y: wp.position.y, z: wp.position.z },
|
|
130
|
+
rotation: { x: wp.rotation.x, y: wp.rotation.y, z: wp.rotation.z, w: wp.rotation.w }
|
|
131
|
+
}
|
|
132
|
+
const boxDepth = (this.constructor as any).defaultDepth ?? 300
|
|
133
|
+
|
|
134
|
+
return [
|
|
135
|
+
topApproachFrame({
|
|
136
|
+
carrierWorld: me,
|
|
137
|
+
topY: boxDepth, // Box top in carrier-local Y (depth = full height; top at depth)
|
|
138
|
+
approachDistance: 50, // gripper 가 hover 하는 거리
|
|
139
|
+
toolType: 'gripper',
|
|
140
|
+
tolerance: { positionMm: 5, angleDeg: 1 },
|
|
141
|
+
priority: 0,
|
|
142
|
+
id: 'top-gripper'
|
|
143
|
+
})
|
|
144
|
+
]
|
|
96
145
|
}
|
|
97
146
|
}
|
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',
|
|
@@ -89,6 +104,8 @@ const NATURE: ComponentNature = {
|
|
|
89
104
|
// (GenericFacility 와 동일 패턴)
|
|
90
105
|
@sceneComponent('container')
|
|
91
106
|
export default class GenericContainer extends GltfComponent(Legendable(Placeable(ContainerAbstract))) {
|
|
107
|
+
declare state: GenericContainerState
|
|
108
|
+
|
|
92
109
|
static legends: Record<string, LegendBinding> = {
|
|
93
110
|
bodyColor: { from: 'fill', legend: BODY_LEGEND },
|
|
94
111
|
lampEmissive: { from: 'fill', legend: LAMP_EMISSIVE_LEGEND }
|
|
@@ -107,18 +124,18 @@ export default class GenericContainer extends GltfComponent(Legendable(Placeable
|
|
|
107
124
|
}
|
|
108
125
|
|
|
109
126
|
get actuators(): Record<string, ActuatorDef> {
|
|
110
|
-
return
|
|
127
|
+
return this.state.actuators ?? {}
|
|
111
128
|
}
|
|
112
129
|
|
|
113
130
|
get actuatorValues(): Record<string, number> {
|
|
114
|
-
return
|
|
131
|
+
return this.state.actuatorValues ?? {}
|
|
115
132
|
}
|
|
116
133
|
|
|
117
134
|
containable(component: Component): boolean {
|
|
118
|
-
return component.isDescendible(this
|
|
135
|
+
return component.isDescendible(this)
|
|
119
136
|
}
|
|
120
137
|
|
|
121
138
|
buildRealObject() {
|
|
122
|
-
return new GenericContainer3D(this
|
|
139
|
+
return new GenericContainer3D(this)
|
|
123
140
|
}
|
|
124
141
|
}
|
package/src/pallet.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
Component,
|
|
6
|
+
ComponentNature,
|
|
7
|
+
ContainerAbstract,
|
|
8
|
+
RealObject,
|
|
9
|
+
Pose6DOF,
|
|
10
|
+
rectangularFootprintFrames,
|
|
11
|
+
getWorldPose,
|
|
12
|
+
sceneComponent
|
|
13
|
+
} from '@hatiolab/things-scene'
|
|
14
|
+
import type { State, Material3D, PickupFrame, PoseSerialized } from '@hatiolab/things-scene'
|
|
5
15
|
import {
|
|
6
16
|
Carriable,
|
|
7
17
|
Legendable,
|
|
@@ -26,6 +36,15 @@ import { Pallet3D } from './pallet-3d.js'
|
|
|
26
36
|
*/
|
|
27
37
|
export type PalletMaterial = 'wood' | 'plastic'
|
|
28
38
|
|
|
39
|
+
/** Pallet 컴포넌트 state */
|
|
40
|
+
export interface PalletState extends State {
|
|
41
|
+
// ── 외관 ──
|
|
42
|
+
material?: PalletMaterial
|
|
43
|
+
|
|
44
|
+
// ── 3D 재질 ──
|
|
45
|
+
material3d?: Material3D
|
|
46
|
+
}
|
|
47
|
+
|
|
29
48
|
const BODY_LEGEND = {
|
|
30
49
|
wood: '#a87644',
|
|
31
50
|
plastic: '#5a6a78',
|
|
@@ -88,6 +107,8 @@ const NATURE: ComponentNature = {
|
|
|
88
107
|
*/
|
|
89
108
|
@sceneComponent('pallet')
|
|
90
109
|
export default class Pallet extends Carriable(Legendable(Placeable(ContainerAbstract))) {
|
|
110
|
+
declare state: PalletState
|
|
111
|
+
|
|
91
112
|
static legends: Record<string, LegendBinding> = {
|
|
92
113
|
bodyColor: { from: 'material', legend: BODY_LEGEND }
|
|
93
114
|
}
|
|
@@ -108,7 +129,7 @@ export default class Pallet extends Carriable(Legendable(Placeable(ContainerAbst
|
|
|
108
129
|
containable(component: Component) {
|
|
109
130
|
const archetype = (component.constructor as any).placement
|
|
110
131
|
if (archetype === 'operation') return true
|
|
111
|
-
return component.isDescendible(this
|
|
132
|
+
return component.isDescendible(this)
|
|
112
133
|
}
|
|
113
134
|
|
|
114
135
|
/**
|
|
@@ -135,7 +156,7 @@ export default class Pallet extends Carriable(Legendable(Placeable(ContainerAbst
|
|
|
135
156
|
super.postrender?.(ctx)
|
|
136
157
|
|
|
137
158
|
const { width, height, left, top } = this.state
|
|
138
|
-
const isPlastic =
|
|
159
|
+
const isPlastic = this.state.material === 'plastic'
|
|
139
160
|
|
|
140
161
|
ctx.save()
|
|
141
162
|
|
|
@@ -187,6 +208,48 @@ export default class Pallet extends Carriable(Legendable(Placeable(ContainerAbst
|
|
|
187
208
|
}
|
|
188
209
|
|
|
189
210
|
buildRealObject(): RealObject | undefined {
|
|
190
|
-
return new Pallet3D(this
|
|
211
|
+
return new Pallet3D(this)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Phase H — pickup contract. EUR/EPAL pallet 의 fork 진입은 양 단면 (긴 변
|
|
216
|
+
* 두 군데). 'east' / 'west' = pallet 의 짧은 축 방향에서 fork 가 들어감.
|
|
217
|
+
*
|
|
218
|
+
* fork 진입 높이 (entryHeight) 는 pallet 의 fork pocket 위치 — pallet 의
|
|
219
|
+
* 바닥에서 약 50mm (top deck 와 bottom deck 사이의 stringer 영역). 표준
|
|
220
|
+
* EUR pallet 의 fork pocket 은 144mm 두께의 약 50% 지점.
|
|
221
|
+
*
|
|
222
|
+
* approachDistance 는 forklift 가 fork 끝을 pallet 에 닿기 직전 hover 자세 —
|
|
223
|
+
* pallet 길이만큼 떨어져서 fork 길이 (보통 1100mm) 가 다 들어가도록 한다.
|
|
224
|
+
* 여기선 conservative 하게 pallet 너비 + 200mm.
|
|
225
|
+
*/
|
|
226
|
+
pickupFrames(): PickupFrame[] {
|
|
227
|
+
const { width = 1200, height = 800 } = this.state
|
|
228
|
+
const palletDepth = (this.constructor as any).defaultDepth ?? 150
|
|
229
|
+
|
|
230
|
+
// 4-way pallet: 모든 면에서 fork 진입 가능. 2-way 는 sides 를 ['east','west']
|
|
231
|
+
// 로 한정 — pallet 의 stringer 방향에 따라 다르나 default 는 4-way 가정.
|
|
232
|
+
// (state.palletType 같은 속성 추가 시 분기 가능 — 현재는 4-way 가정.)
|
|
233
|
+
const me: PoseSerialized = (() => {
|
|
234
|
+
const wp = getWorldPose(this)
|
|
235
|
+
return {
|
|
236
|
+
position: { x: wp.position.x, y: wp.position.y, z: wp.position.z },
|
|
237
|
+
rotation: { x: wp.rotation.x, y: wp.rotation.y, z: wp.rotation.z, w: wp.rotation.w }
|
|
238
|
+
}
|
|
239
|
+
})()
|
|
240
|
+
|
|
241
|
+
const longerAxis = Math.max(width, height)
|
|
242
|
+
|
|
243
|
+
return rectangularFootprintFrames({
|
|
244
|
+
carrierWorld: me,
|
|
245
|
+
width,
|
|
246
|
+
depth: height,
|
|
247
|
+
entryHeight: palletDepth * 0.4, // fork pocket 중심
|
|
248
|
+
approachDistance: longerAxis + 200,
|
|
249
|
+
sides: ['east', 'west', 'north', 'south'],
|
|
250
|
+
toolType: 'forklift-fork',
|
|
251
|
+
tolerance: { positionMm: 50, angleDeg: 5 },
|
|
252
|
+
priority: 0
|
|
253
|
+
})
|
|
191
254
|
}
|
|
192
255
|
}
|
package/src/parcel.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ComponentNature,
|
|
6
|
+
RealObject,
|
|
7
|
+
RectPath,
|
|
8
|
+
Shape,
|
|
9
|
+
topApproachFrame,
|
|
10
|
+
getWorldPose,
|
|
11
|
+
sceneComponent
|
|
12
|
+
} from '@hatiolab/things-scene'
|
|
13
|
+
import type { State, Material3D, PickupFrame, PoseSerialized } from '@hatiolab/things-scene'
|
|
5
14
|
import {
|
|
6
15
|
Carriable,
|
|
7
16
|
Placeable,
|
|
@@ -11,6 +20,15 @@ import {
|
|
|
11
20
|
|
|
12
21
|
import { Parcel3D } from './parcel-3d.js'
|
|
13
22
|
|
|
23
|
+
/** Parcel 컴포넌트 state */
|
|
24
|
+
export interface ParcelState extends State {
|
|
25
|
+
// ── 정체 ──
|
|
26
|
+
trackingId?: string
|
|
27
|
+
|
|
28
|
+
// ── 3D 재질 ──
|
|
29
|
+
material3d?: Material3D
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
const NATURE: ComponentNature = {
|
|
15
33
|
mutable: false,
|
|
16
34
|
resizable: true,
|
|
@@ -45,6 +63,8 @@ const NATURE: ComponentNature = {
|
|
|
45
63
|
*/
|
|
46
64
|
@sceneComponent('parcel')
|
|
47
65
|
export default class Parcel extends Carriable(Placeable(RectPath(Shape))) {
|
|
66
|
+
declare state: ParcelState
|
|
67
|
+
|
|
48
68
|
static placement: PlacementArchetype = 'operation'
|
|
49
69
|
static align: Alignment = 'bottom'
|
|
50
70
|
static defaultDepth = 150
|
|
@@ -69,6 +89,32 @@ export default class Parcel extends Carriable(Placeable(RectPath(Shape))) {
|
|
|
69
89
|
}
|
|
70
90
|
|
|
71
91
|
buildRealObject(): RealObject | undefined {
|
|
72
|
-
return new Parcel3D(this
|
|
92
|
+
return new Parcel3D(this)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Phase H — pickup contract. Parcel 은 위에서 vacuum gripper / suction cup 으로
|
|
97
|
+
* 집기 — Box 와 동일한 패턴이지만 cardboard 표면이라 더 큰 흡착 면 필요.
|
|
98
|
+
* tolerance 도 약간 완화 (cardboard 변형 가능성).
|
|
99
|
+
*/
|
|
100
|
+
pickupFrames(): PickupFrame[] {
|
|
101
|
+
const wp = getWorldPose(this)
|
|
102
|
+
const me: PoseSerialized = {
|
|
103
|
+
position: { x: wp.position.x, y: wp.position.y, z: wp.position.z },
|
|
104
|
+
rotation: { x: wp.rotation.x, y: wp.rotation.y, z: wp.rotation.z, w: wp.rotation.w }
|
|
105
|
+
}
|
|
106
|
+
const parcelDepth = (this.constructor as any).defaultDepth ?? 150
|
|
107
|
+
|
|
108
|
+
return [
|
|
109
|
+
topApproachFrame({
|
|
110
|
+
carrierWorld: me,
|
|
111
|
+
topY: parcelDepth,
|
|
112
|
+
approachDistance: 80, // gripper hover 거리 (Box 보다 더 — vacuum 펼침)
|
|
113
|
+
toolType: 'gripper',
|
|
114
|
+
tolerance: { positionMm: 10, angleDeg: 2 }, // cardboard 변형 감안
|
|
115
|
+
priority: 0,
|
|
116
|
+
id: 'top-suction'
|
|
117
|
+
})
|
|
118
|
+
]
|
|
73
119
|
}
|
|
74
120
|
}
|
package/src/rack-cell-3d.ts
CHANGED
|
@@ -56,7 +56,7 @@ export class RackCell3D extends RealObjectGroup {
|
|
|
56
56
|
const rack = (this.component as any).parent
|
|
57
57
|
if (!rack?.cellMap) return
|
|
58
58
|
|
|
59
|
-
const cellId =
|
|
59
|
+
const cellId = this.component.state.cellId as string | undefined
|
|
60
60
|
if (!cellId) return
|
|
61
61
|
|
|
62
62
|
const cell = rack.cellMap.findById(cellId)
|
package/src/rack-cell.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
TRANSFER_SLOT_KEY,
|
|
33
33
|
sceneComponent
|
|
34
34
|
} from '@hatiolab/things-scene'
|
|
35
|
+
import type { State, Material3D } from '@hatiolab/things-scene'
|
|
35
36
|
import { CarrierHolder, type AttachFrame } from '@operato/scene-base'
|
|
36
37
|
|
|
37
38
|
import { RackCell3D } from './rack-cell-3d.js'
|
|
@@ -44,6 +45,16 @@ import { RackCell3D } from './rack-cell-3d.js'
|
|
|
44
45
|
*/
|
|
45
46
|
export type RackCellType = 'single' | 'multi' | 'bulk'
|
|
46
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
|
+
|
|
47
58
|
const NATURE: ComponentNature = {
|
|
48
59
|
mutable: false,
|
|
49
60
|
resizable: false,
|
|
@@ -84,14 +95,16 @@ const NATURE: ComponentNature = {
|
|
|
84
95
|
*/
|
|
85
96
|
@sceneComponent('rack-cell')
|
|
86
97
|
export default class RackCell extends CarrierHolder(ContainerAbstract) {
|
|
98
|
+
declare state: RackCellState
|
|
99
|
+
|
|
87
100
|
// ── Identification ────────────────────────────────────────────────────────
|
|
88
101
|
|
|
89
102
|
get cellId(): string {
|
|
90
|
-
return
|
|
103
|
+
return this.state.cellId ?? ''
|
|
91
104
|
}
|
|
92
105
|
|
|
93
106
|
get cellType(): RackCellType {
|
|
94
|
-
return
|
|
107
|
+
return this.state.cellType ?? 'single'
|
|
95
108
|
}
|
|
96
109
|
|
|
97
110
|
/** Maximum carrier count for this cell based on cellType. */
|
|
@@ -117,7 +130,7 @@ export default class RackCell extends CarrierHolder(ContainerAbstract) {
|
|
|
117
130
|
|
|
118
131
|
/** True when fewer carriers are currently held than capacity. */
|
|
119
132
|
canReceive(_component?: any): boolean {
|
|
120
|
-
const occupied = (
|
|
133
|
+
const occupied = (this.components as Component[] | undefined)?.length ?? 0
|
|
121
134
|
return occupied < this.capacity
|
|
122
135
|
}
|
|
123
136
|
|
|
@@ -128,7 +141,7 @@ export default class RackCell extends CarrierHolder(ContainerAbstract) {
|
|
|
128
141
|
*/
|
|
129
142
|
async receive(carrier: any, options: any = {}): Promise<void> {
|
|
130
143
|
if (!this.canReceive(carrier)) {
|
|
131
|
-
|
|
144
|
+
this.trigger('transfer-rejected', {
|
|
132
145
|
type: 'transfer-rejected',
|
|
133
146
|
component: carrier,
|
|
134
147
|
container: this,
|
|
@@ -137,8 +150,8 @@ export default class RackCell extends CarrierHolder(ContainerAbstract) {
|
|
|
137
150
|
return
|
|
138
151
|
}
|
|
139
152
|
carrier[TRANSFER_SLOT_KEY] = this.cellId
|
|
140
|
-
|
|
141
|
-
|
|
153
|
+
this.reparent(carrier, options)
|
|
154
|
+
this.trigger('transfer-received', {
|
|
142
155
|
type: 'transfer-received',
|
|
143
156
|
component: carrier,
|
|
144
157
|
container: this,
|
|
@@ -152,7 +165,7 @@ export default class RackCell extends CarrierHolder(ContainerAbstract) {
|
|
|
152
165
|
*/
|
|
153
166
|
async dispatch(carrier: any, target: any, options: any = {}): Promise<void> {
|
|
154
167
|
if (target?.canReceive && !target.canReceive(carrier)) {
|
|
155
|
-
|
|
168
|
+
this.trigger('transfer-rejected', {
|
|
156
169
|
type: 'transfer-rejected',
|
|
157
170
|
component: carrier,
|
|
158
171
|
container: this,
|
|
@@ -166,7 +179,7 @@ export default class RackCell extends CarrierHolder(ContainerAbstract) {
|
|
|
166
179
|
} else {
|
|
167
180
|
;(target as any).reparent?.(carrier, options)
|
|
168
181
|
}
|
|
169
|
-
|
|
182
|
+
this.trigger('transfer-dispatched', {
|
|
170
183
|
type: 'transfer-dispatched',
|
|
171
184
|
component: carrier,
|
|
172
185
|
container: this,
|
|
@@ -194,7 +207,7 @@ export default class RackCell extends CarrierHolder(ContainerAbstract) {
|
|
|
194
207
|
* rests at the cell's Y-center (which is levelHeight/2 above the beam).
|
|
195
208
|
*/
|
|
196
209
|
attachPointFor(carrier: Component): AttachFrame | null {
|
|
197
|
-
const root =
|
|
210
|
+
const root = this._realObject?.object3d
|
|
198
211
|
if (!root) return null
|
|
199
212
|
const carrierDepth = resolveCarrierDepth(carrier)
|
|
200
213
|
return {
|
|
@@ -213,7 +226,7 @@ export default class RackCell extends CarrierHolder(ContainerAbstract) {
|
|
|
213
226
|
// ── 3D ───────────────────────────────────────────────────────────────────
|
|
214
227
|
|
|
215
228
|
buildRealObject(): RealObject | undefined {
|
|
216
|
-
return new RackCell3D(this
|
|
229
|
+
return new RackCell3D(this)
|
|
217
230
|
}
|
|
218
231
|
}
|
|
219
232
|
|
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,6 +30,7 @@
|
|
|
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,
|
|
@@ -41,6 +42,13 @@ import {
|
|
|
41
42
|
|
|
42
43
|
import { Spot3D } from './spot-3d.js'
|
|
43
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
|
+
|
|
44
52
|
const NATURE: ComponentNature = {
|
|
45
53
|
mutable: false,
|
|
46
54
|
resizable: true,
|
|
@@ -61,6 +69,9 @@ const NATURE: ComponentNature = {
|
|
|
61
69
|
// addObject DOM-skip gate. Spot is purely 3D.
|
|
62
70
|
@sceneComponent('spot')
|
|
63
71
|
export default class Spot extends CarrierHolder(Placeable(ContainerAbstract)) {
|
|
72
|
+
declare state: SpotState
|
|
73
|
+
declare _realObject?: Spot3D
|
|
74
|
+
|
|
64
75
|
static placement: PlacementArchetype = 'floor'
|
|
65
76
|
static align: Alignment = 'bottom'
|
|
66
77
|
static defaultDepth = (_h: Heights) => 2 // a thin pad
|
|
@@ -131,7 +142,7 @@ export default class Spot extends CarrierHolder(Placeable(ContainerAbstract)) {
|
|
|
131
142
|
}
|
|
132
143
|
|
|
133
144
|
buildRealObject(): RealObject | undefined {
|
|
134
|
-
return new Spot3D(this
|
|
145
|
+
return new Spot3D(this)
|
|
135
146
|
}
|
|
136
147
|
|
|
137
148
|
/**
|
|
@@ -147,7 +158,7 @@ export default class Spot extends CarrierHolder(Placeable(ContainerAbstract)) {
|
|
|
147
158
|
* RealObject creation.
|
|
148
159
|
*/
|
|
149
160
|
attachPointFor(carrier: Component): AttachFrame | null {
|
|
150
|
-
const ro =
|
|
161
|
+
const ro = this._realObject
|
|
151
162
|
const frame = ro?.getAttachFrame?.()
|
|
152
163
|
if (!frame) return null
|
|
153
164
|
const carrierDepth = resolveDepth(carrier)
|