@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/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 = ((this as any).components as Component[] | undefined) ?? []
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
- ;(this as any).removeComponent?.(child)
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 = (this as any)._app
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
- ;(this as any).addComponent?.(rackCell)
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 as any)
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 = (this as any)._realObject?.object3d
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 as any)
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 { ComponentNature, RealObject, RectPath, Shape, sceneComponent } from '@hatiolab/things-scene'
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 as any)
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
  }
@@ -31,7 +31,7 @@ export class GenericContainer3D extends RealObjectGLTF {
31
31
  }
32
32
 
33
33
  private _applyActuators() {
34
- const state = this.component.state as any
34
+ const state = this.component.state
35
35
  applyActuators(this, state.actuators, state.actuatorValues)
36
36
  }
37
37
 
@@ -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 ((this.state as any).actuators as Record<string, ActuatorDef> | undefined) ?? {}
127
+ return this.state.actuators ?? {}
111
128
  }
112
129
 
113
130
  get actuatorValues(): Record<string, number> {
114
- return ((this.state as any).actuatorValues as Record<string, number> | undefined) ?? {}
131
+ return this.state.actuatorValues ?? {}
115
132
  }
116
133
 
117
134
  containable(component: Component): boolean {
118
- return component.isDescendible(this as any)
135
+ return component.isDescendible(this)
119
136
  }
120
137
 
121
138
  buildRealObject() {
122
- return new GenericContainer3D(this as any)
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 { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'
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 as any)
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 = ((this.state as any).material as PalletMaterial) === 'plastic'
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 as any)
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 { ComponentNature, RealObject, RectPath, Shape, sceneComponent } from '@hatiolab/things-scene'
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 as any)
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
  }
@@ -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 = (this.component as any).state?.cellId as string | undefined
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 (this.state.cellId as string) || ''
103
+ return this.state.cellId ?? ''
91
104
  }
92
105
 
93
106
  get cellType(): RackCellType {
94
- return ((this.state.cellType as RackCellType) || 'single')
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 = ((this as any).components as Component[] | undefined)?.length ?? 0
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
- ;(this as any).trigger?.('transfer-rejected', {
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
- ;(this as any).reparent?.(carrier, options)
141
- ;(this as any).trigger?.('transfer-received', {
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
- ;(this as any).trigger?.('transfer-rejected', {
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
- ;(this as any).trigger?.('transfer-dispatched', {
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 = (this as any)._realObject?.object3d
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 as any)
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 as any
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 as any)
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 = (this as any)._realObject as Spot3D | undefined
161
+ const ro = this._realObject
151
162
  const frame = ro?.getAttachFrame?.()
152
163
  if (!frame) return null
153
164
  const carrierDepth = resolveDepth(carrier)