@operato/scene-storage 10.0.0-beta.22 → 10.0.0-beta.27

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@operato/scene-storage",
3
3
  "description": "Storage-domain components for things-scene (smart factory / logistics) — pallet, box, parcel; AS/RS and shelves planned.",
4
4
  "author": "heartyoh",
5
- "version": "10.0.0-beta.22",
5
+ "version": "10.0.0-beta.27",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "module": "dist/index.js",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@hatiolab/things-scene": "^10.0.0-beta.1",
28
- "@operato/scene-base": "^10.0.0-beta.22",
28
+ "@operato/scene-base": "^10.0.0-beta.24",
29
29
  "three": "^0.183.0"
30
30
  },
31
31
  "devDependencies": {
@@ -40,5 +40,5 @@
40
40
  "typescript": "^5.0.4"
41
41
  },
42
42
  "prettier": "@hatiolab/prettier-config",
43
- "gitHead": "f48e52f4f5fdc30ec06af9da7cf253f6e29cfb0e"
43
+ "gitHead": "17135d7379cf0dee5e36332d08d75b58fbf2ab47"
44
44
  }
package/src/asrs-rack.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * Copyright © HatioLab Inc. All rights reserved.
3
3
  */
4
- import { Component, ComponentNature, Container, RealObject, sceneComponent } from '@hatiolab/things-scene'
4
+ import { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'
5
5
  import {
6
6
  Placeable,
7
7
  type Alignment,
@@ -32,7 +32,10 @@ const NATURE: ComponentNature = {
32
32
  help: 'scene/component/asrs-rack'
33
33
  }
34
34
 
35
- const Base = Placeable(Container) as unknown as typeof Component
35
+ // `ContainerAbstract` (not `Container`) Container = MixinHTMLElement(ContainerAbstract),
36
+ // which forces `isHTMLElement(): true` and trips the 3D pipeline's
37
+ // addObject DOM-skip gate. ASRS rack lives only in the 3D scene graph.
38
+ const Base = Placeable(ContainerAbstract) as unknown as typeof Component
36
39
 
37
40
  /**
38
41
  * AsrsRack — a multi-level high-bay storage rack, the structural backbone of
@@ -0,0 +1,45 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * GenericContainer3D — RealObjectGLTF 상속, 보관소 (Stocker / Buffer / Rack 류) 의 3D 표현.
5
+ *
6
+ * 상속 체인:
7
+ * RealObjectGroup → RealObjectExternalModel → RealObjectGLTF → GenericContainer3D
8
+ *
9
+ * 재사용:
10
+ * - GLB load (`_loadExternal` + 캐시)
11
+ * - node index (`getNode`, `nodeNames`)
12
+ * - 노드 상태 매핑 (`nodes` state → color/visible/opacity)
13
+ * - 애니메이션 (`animations` / `playTargets`)
14
+ * - top-view 스냅샷 (2D 렌더용)
15
+ * - dispose / clear
16
+ *
17
+ * 추가:
18
+ * - actuator (state 값 → 노드 transform). rack 의 lift, shutter 열림 등 동적 표현.
19
+ *
20
+ * GenericTransport3D 와 차이: mount / cargo reparent 미사용 (cargo holder 가 아니라 placeholder
21
+ * 단계의 정적 보관소). cargo 보유는 사용자가 구체 type 으로 변환 후에 부여.
22
+ */
23
+
24
+ import { RealObjectGLTF } from '@hatiolab/things-scene'
25
+ import { applyActuators } from '@operato/scene-base'
26
+
27
+ export class GenericContainer3D extends RealObjectGLTF {
28
+ protected _onLoaded(gltf: any) {
29
+ super._onLoaded(gltf)
30
+ this._applyActuators()
31
+ }
32
+
33
+ private _applyActuators() {
34
+ const state = this.component.state as any
35
+ applyActuators(this, state.actuators, state.actuatorValues)
36
+ }
37
+
38
+ onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
39
+ if ('actuatorValues' in after || 'actuators' in after) {
40
+ this._applyActuators()
41
+ return
42
+ }
43
+ super.onchange(after, before)
44
+ }
45
+ }
@@ -0,0 +1,126 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * GenericContainer — GLB 모델 기반 범용 보관소 컴포넌트.
5
+ *
6
+ * 합성 구조:
7
+ * GltfComponent (things-scene) — GLB 컨벤션 (src, 2D 스냅샷, ratioLock,
8
+ * gltf-* 에디터, RealObjectGLTF)
9
+ * ⊕ ContainerAbstract (things-scene) — 자식 보유 (DOM-free 컨테이너)
10
+ * ⊕ Placeable (scene-base) — floor placement, depth=operation
11
+ * ⊕ Legendable (scene-base) — fill 별 bodyColor / lampEmissive
12
+ *
13
+ * 추가 (이 클래스 고유):
14
+ * - state.actuators / actuatorValues — 동적 노드 transform (rack lift, shutter 열림 등)
15
+ *
16
+ * 동일 컴포넌트 하나로 stocker / buffer / shelf / rack 모두 표현 가능 — 각 보관소별로
17
+ * 다른 GLB + 다른 actuator 매핑만 다름.
18
+ *
19
+ * GenericTransport / GenericFacility 와의 차이:
20
+ * - cargo holder 가 아님 — 사용자가 구체 type (e.g. AsrsRack) 으로 변환 후에 cargo 부여
21
+ * - container 카테고리 (정적 보관소). 분류는 동일 floor placement 지만 의미는 다름
22
+ *
23
+ * board-import 의 categoryFallback 에서 container 카테고리의 default placeholder. 사용자가
24
+ * 모델러에서 src 부착하거나 type 변경으로 도메인 type (Stocker / Buffer / AsrsRack 등) 으로 변환.
25
+ */
26
+
27
+ import {
28
+ Component,
29
+ ComponentNature,
30
+ ContainerAbstract,
31
+ GltfComponent,
32
+ gltfNatureProperties,
33
+ sceneComponent
34
+ } from '@hatiolab/things-scene'
35
+ import {
36
+ Legendable,
37
+ Placeable,
38
+ type Alignment,
39
+ type Heights,
40
+ type LegendBinding,
41
+ type PlacementArchetype
42
+ } from '@operato/scene-base'
43
+
44
+ import { GenericContainer3D } from './generic-container-3d.js'
45
+ import type { ActuatorDef } from '@operato/scene-base'
46
+
47
+ export type ContainerStatus = 'empty' | 'partial' | 'full' | 'error'
48
+
49
+ const BODY_LEGEND = {
50
+ empty: '#a8b8c4',
51
+ partial: '#7d96b0',
52
+ full: '#5a7c9a',
53
+ error: '#e9746b',
54
+ default: '#a8b8c4'
55
+ }
56
+
57
+ const LAMP_EMISSIVE_LEGEND = {
58
+ empty: '#222222',
59
+ partial: '#3388cc',
60
+ full: '#2266aa',
61
+ error: '#ff3333',
62
+ default: '#222222'
63
+ }
64
+
65
+ const NATURE: ComponentNature = {
66
+ mutable: false,
67
+ resizable: true,
68
+ rotatable: true,
69
+ properties: [
70
+ ...gltfNatureProperties,
71
+ {
72
+ type: 'select',
73
+ label: 'fill',
74
+ name: 'fill',
75
+ property: {
76
+ options: [
77
+ { display: 'Empty', value: 'empty' },
78
+ { display: 'Partial', value: 'partial' },
79
+ { display: 'Full', value: 'full' },
80
+ { display: 'Error', value: 'error' }
81
+ ]
82
+ }
83
+ }
84
+ ],
85
+ help: 'scene/component/generic-container'
86
+ }
87
+
88
+ // 합성 순서: 안쪽부터 → ContainerAbstract → Placeable → Legendable → GltfComponent
89
+ // (GenericFacility 와 동일 패턴)
90
+ const Base = GltfComponent(Legendable(Placeable(ContainerAbstract))) as unknown as typeof Component
91
+
92
+ @sceneComponent('container')
93
+ export default class GenericContainer extends Base {
94
+ static legends: Record<string, LegendBinding> = {
95
+ bodyColor: { from: 'fill', legend: BODY_LEGEND },
96
+ lampEmissive: { from: 'fill', legend: LAMP_EMISSIVE_LEGEND }
97
+ }
98
+
99
+ static placement: PlacementArchetype = 'floor'
100
+ static align: Alignment = 'bottom'
101
+ static defaultDepth = (h: Heights) => h.operation - h.floor
102
+
103
+ get nature() {
104
+ return NATURE
105
+ }
106
+
107
+ get anchors() {
108
+ return []
109
+ }
110
+
111
+ get actuators(): Record<string, ActuatorDef> {
112
+ return ((this.state as any).actuators as Record<string, ActuatorDef> | undefined) ?? {}
113
+ }
114
+
115
+ get actuatorValues(): Record<string, number> {
116
+ return ((this.state as any).actuatorValues as Record<string, number> | undefined) ?? {}
117
+ }
118
+
119
+ containable(component: Component): boolean {
120
+ return component.isDescendible(this as any)
121
+ }
122
+
123
+ buildRealObject() {
124
+ return new GenericContainer3D(this as any)
125
+ }
126
+ }
package/src/index.ts CHANGED
@@ -15,3 +15,7 @@ export type { AsrsCraneStatus } from './asrs-crane.js'
15
15
 
16
16
  export { default as Spot } from './spot.js'
17
17
  export { Spot3D } from './spot-3d.js'
18
+
19
+ export { default as GenericContainer } from './generic-container.js'
20
+ export type { ContainerStatus } from './generic-container.js'
21
+ export { GenericContainer3D } from './generic-container-3d.js'
package/src/pallet.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * Copyright © HatioLab Inc. All rights reserved.
3
3
  */
4
- import { Component, ComponentNature, Container, RealObject, sceneComponent } from '@hatiolab/things-scene'
4
+ import { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'
5
5
  import {
6
6
  Carriable,
7
7
  Legendable,
@@ -53,9 +53,14 @@ const NATURE: ComponentNature = {
53
53
  }
54
54
 
55
55
  // Carriable: a pallet can sit on AGV / Forklift / robot-arm gripper / Spot
56
- // and also accept boxes / parcels as children (Container base provides the
57
- // child-container behavior; Carriable only adds the holder-mount hook).
58
- const Base = Carriable(Legendable(Placeable(Container))) as unknown as typeof Component
56
+ // and also accept boxes / parcels as children (ContainerAbstract base
57
+ // provides the child-container behavior; Carriable only adds the
58
+ // holder-mount hook).
59
+ //
60
+ // `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),
61
+ // which forces `isHTMLElement(): true` and trips the 3D pipeline's
62
+ // addObject DOM-skip gate. Pallet renders only as a 3D mesh.
63
+ const Base = Carriable(Legendable(Placeable(ContainerAbstract))) as unknown as typeof Component
59
64
 
60
65
  /**
61
66
  * Pallet — a flat transport structure that goods are stacked and stored on.
@@ -108,13 +113,77 @@ export default class Pallet extends Base {
108
113
  return component.isDescendible(this as any)
109
114
  }
110
115
 
111
- /** 2D — top-down rectangle. */
116
+ /**
117
+ * 2D — top-down silhouette. Body is a flat rectangle (wood/plastic deck);
118
+ * `postrender()` adds the deck pattern + edge stroke so the pallet reads
119
+ * as a pallet instead of a featureless rectangle.
120
+ */
112
121
  render(ctx: CanvasRenderingContext2D) {
113
122
  const { width, height, left, top } = this.state
114
123
  ctx.beginPath()
115
124
  ctx.rect(left, top, width, height)
116
125
  }
117
126
 
127
+ /**
128
+ * Deck pattern + edge stroke. Wood: parallel slats with darker grooves
129
+ * between (typical EUR pallet deck). Plastic: cross-cutout pattern
130
+ * suggesting the molded reinforcement ribs.
131
+ *
132
+ * Slats run along the *short* axis of the rectangle (= along the longer
133
+ * stringer direction in real life), so a 1200×800 pallet shows multiple
134
+ * narrow slats across the 1200mm dimension — matching the EUR layout.
135
+ */
136
+ postrender(ctx: CanvasRenderingContext2D) {
137
+ super.postrender?.(ctx)
138
+
139
+ const { width, height, left, top } = this.state
140
+ const isPlastic = ((this.state as any).material as PalletMaterial) === 'plastic'
141
+
142
+ ctx.save()
143
+
144
+ if (!isPlastic) {
145
+ // Wood — slats. Run them along the longer axis so the grooves are
146
+ // perpendicular to the longer side (typical pallet appearance).
147
+ const longAxisHorizontal = width >= height
148
+ const slatCount = 5
149
+ const grooveColor = '#7a4f25'
150
+ ctx.fillStyle = grooveColor
151
+ if (longAxisHorizontal) {
152
+ // grooves vertical (X direction across the width)
153
+ const grooveW = Math.max(1, width * 0.012)
154
+ const slatW = (width - grooveW * (slatCount - 1)) / slatCount
155
+ for (let i = 1; i < slatCount; i++) {
156
+ const x = left + i * slatW + (i - 1) * grooveW
157
+ ctx.fillRect(x, top + height * 0.05, grooveW, height * 0.9)
158
+ }
159
+ } else {
160
+ const grooveH = Math.max(1, height * 0.012)
161
+ const slatH = (height - grooveH * (slatCount - 1)) / slatCount
162
+ for (let i = 1; i < slatCount; i++) {
163
+ const y = top + i * slatH + (i - 1) * grooveH
164
+ ctx.fillRect(left + width * 0.05, y, width * 0.9, grooveH)
165
+ }
166
+ }
167
+ } else {
168
+ // Plastic — cross + corner cutouts hint
169
+ ctx.strokeStyle = '#3a4956'
170
+ ctx.lineWidth = Math.max(1, Math.min(width, height) * 0.012)
171
+ ctx.beginPath()
172
+ ctx.moveTo(left + width * 0.5, top + height * 0.1)
173
+ ctx.lineTo(left + width * 0.5, top + height * 0.9)
174
+ ctx.moveTo(left + width * 0.1, top + height * 0.5)
175
+ ctx.lineTo(left + width * 0.9, top + height * 0.5)
176
+ ctx.stroke()
177
+ }
178
+
179
+ // Edge stroke
180
+ ctx.strokeStyle = isPlastic ? '#2a3946' : '#5e3818'
181
+ ctx.lineWidth = 1
182
+ ctx.strokeRect(left, top, width, height)
183
+
184
+ ctx.restore()
185
+ }
186
+
118
187
  get fillStyle() {
119
188
  return (this.state.bodyColor as string) || '#a87644'
120
189
  }
package/src/spot.ts CHANGED
@@ -29,7 +29,7 @@
29
29
  * the top face of the pad (overrides default attachPointFor).
30
30
  */
31
31
 
32
- import { Component, ComponentNature, Container, RealObject, sceneComponent } from '@hatiolab/things-scene'
32
+ import { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'
33
33
  import {
34
34
  CarrierHolder,
35
35
  Placeable,
@@ -51,10 +51,14 @@ const NATURE: ComponentNature = {
51
51
  help: 'scene/component/spot'
52
52
  }
53
53
 
54
- // Container base — Spot accepts carrier children (parcel/box/pallet/...).
54
+ // ContainerAbstract base — Spot accepts carrier children (parcel/box/pallet/...).
55
55
  // CarrierHolder mixin only publishes the attach-point hook; the actual
56
- // child-list management comes from the things-scene Container.
57
- const Base = CarrierHolder(Placeable(Container)) as unknown as typeof Component
56
+ // child-list management comes from things-scene's container abstract.
57
+ //
58
+ // `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),
59
+ // which forces `isHTMLElement(): true` and trips the 3D pipeline's
60
+ // addObject DOM-skip gate. Spot is purely 3D.
61
+ const Base = CarrierHolder(Placeable(ContainerAbstract)) as unknown as typeof Component
58
62
 
59
63
  @sceneComponent('spot')
60
64
  export default class Spot extends Base {
@@ -21,7 +21,7 @@ export default [
21
21
  type: 'pallet',
22
22
  top: 100,
23
23
  left: 100,
24
- width: 120,
24
+ width: 80,
25
25
  height: 80,
26
26
  material: 'wood'
27
27
  }
@@ -35,7 +35,7 @@ export default [
35
35
  type: 'pallet',
36
36
  top: 100,
37
37
  left: 250,
38
- width: 120,
38
+ width: 80,
39
39
  height: 80,
40
40
  material: 'plastic'
41
41
  }
@@ -78,7 +78,7 @@ export default [
78
78
  top: 100,
79
79
  left: 640,
80
80
  width: 60,
81
- height: 90
81
+ height: 80
82
82
  }
83
83
  },
84
84
  {