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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/index.d.ts +9 -0
  3. package/dist/index.js +6 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/picking-station-3d.d.ts +20 -0
  6. package/dist/picking-station-3d.js +162 -0
  7. package/dist/picking-station-3d.js.map +1 -0
  8. package/dist/picking-station.d.ts +56 -0
  9. package/dist/picking-station.js +212 -0
  10. package/dist/picking-station.js.map +1 -0
  11. package/dist/rack-capability.d.ts +11 -0
  12. package/dist/rack-capability.js +25 -0
  13. package/dist/rack-capability.js.map +1 -0
  14. package/dist/rack-grid.js +3 -10
  15. package/dist/rack-grid.js.map +1 -1
  16. package/dist/spot.d.ts +19 -1
  17. package/dist/spot.js +63 -1
  18. package/dist/spot.js.map +1 -1
  19. package/dist/stockpile-3d.d.ts +55 -0
  20. package/dist/stockpile-3d.js +387 -0
  21. package/dist/stockpile-3d.js.map +1 -0
  22. package/dist/stockpile-grid-3d.d.ts +30 -0
  23. package/dist/stockpile-grid-3d.js +301 -0
  24. package/dist/stockpile-grid-3d.js.map +1 -0
  25. package/dist/stockpile-grid.d.ts +88 -0
  26. package/dist/stockpile-grid.js +429 -0
  27. package/dist/stockpile-grid.js.map +1 -0
  28. package/dist/stockpile.d.ts +133 -0
  29. package/dist/stockpile.js +439 -0
  30. package/dist/stockpile.js.map +1 -0
  31. package/dist/storage-rack.d.ts +12 -0
  32. package/dist/storage-rack.js +20 -10
  33. package/dist/storage-rack.js.map +1 -1
  34. package/dist/templates/index.d.ts +80 -0
  35. package/dist/templates/index.js +7 -1
  36. package/dist/templates/index.js.map +1 -1
  37. package/dist/templates/picking-station.d.ts +20 -0
  38. package/dist/templates/picking-station.js +22 -0
  39. package/dist/templates/picking-station.js.map +1 -0
  40. package/dist/templates/stockpile-grid.d.ts +37 -0
  41. package/dist/templates/stockpile-grid.js +38 -0
  42. package/dist/templates/stockpile-grid.js.map +1 -0
  43. package/dist/templates/stockpile.d.ts +29 -0
  44. package/dist/templates/stockpile.js +31 -0
  45. package/dist/templates/stockpile.js.map +1 -0
  46. package/package.json +3 -3
  47. package/src/index.ts +14 -0
  48. package/src/picking-station-3d.ts +164 -0
  49. package/src/picking-station.ts +243 -0
  50. package/src/rack-capability.ts +26 -0
  51. package/src/rack-grid.ts +3 -8
  52. package/src/spot.ts +62 -0
  53. package/src/stockpile-3d.ts +412 -0
  54. package/src/stockpile-grid-3d.ts +327 -0
  55. package/src/stockpile-grid.ts +456 -0
  56. package/src/stockpile.ts +508 -0
  57. package/src/storage-rack.ts +21 -8
  58. package/src/templates/index.ts +7 -1
  59. package/src/templates/picking-station.ts +23 -0
  60. package/src/templates/stockpile-grid.ts +39 -0
  61. package/src/templates/stockpile.ts +32 -0
  62. package/test/test-rack-capability.ts +51 -0
  63. package/translations/en.json +18 -6
  64. package/translations/ja.json +18 -6
  65. package/translations/ko.json +17 -5
  66. package/translations/ms.json +18 -6
  67. package/translations/zh.json +17 -5
  68. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,30 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [10.0.0-beta.50](https://github.com/things-scene/operato-scene/compare/v10.0.0-beta.49...v10.0.0-beta.50) (2026-05-28)
7
+
8
+
9
+ ### :rocket: New Features
10
+
11
+ * **scene-base,transport:** AMR 자동 운반 — planTransfer 다중 hop + zone 좌표 정합 + slot reservation ([3474236](https://github.com/things-scene/operato-scene/commit/347423683c9814627334f134259b203117a32a69))
12
+ * **storage,transport:** PickingStation + PickingCart — picking workflow 컴포넌트 ([8017e39](https://github.com/things-scene/operato-scene/commit/8017e39c5caeed81200b1c922bf1176cf85784b8))
13
+ * **storage:** Stockpile + StockpileGrid — 평치 보관 컴포넌트 ([d144cb0](https://github.com/things-scene/operato-scene/commit/d144cb0e7731800569594c29e0cf49c7e2fd71d9))
14
+
15
+
16
+ ### :house: Code Refactoring
17
+
18
+ * **storage:** StockpileGrid 데이터 구조 단순화 — cells → data 통일, cell override 제거 ([475e72d](https://github.com/things-scene/operato-scene/commit/475e72d4c03b562b240da00f56177af51f27b2c6))
19
+
20
+
21
+
22
+ ## [10.0.0-beta.48](https://github.com/things-scene/operato-scene/compare/v10.0.0-beta.47...v10.0.0-beta.48) (2026-05-26)
23
+
24
+ **Note:** Version bump only for package @operato/scene-storage
25
+
26
+
27
+
28
+
29
+
6
30
  ## [10.0.0-beta.47](https://github.com/things-scene/operato-scene/compare/v10.0.0-beta.46...v10.0.0-beta.47) (2026-05-26)
7
31
 
8
32
 
package/dist/index.d.ts CHANGED
@@ -17,6 +17,15 @@ export type { CraneState, CraneStatus } from './crane.js';
17
17
  export { Crane3D } from './crane-3d.js';
18
18
  export { default as Spot } from './spot.js';
19
19
  export { Spot3D } from './spot-3d.js';
20
+ export { default as Stockpile } from './stockpile.js';
21
+ export type { StockpileState, StockpileRecord, StackPattern, CarrierPreset, PickPolicy } from './stockpile.js';
22
+ export { Stockpile3D } from './stockpile-3d.js';
23
+ export { default as StockpileGrid } from './stockpile-grid.js';
24
+ export type { StockpileGridState, StockpileGridCell } from './stockpile-grid.js';
25
+ export { StockpileGrid3D } from './stockpile-grid-3d.js';
26
+ export { default as PickingStation } from './picking-station.js';
27
+ export type { PickingStationState, PickingStationStatus } from './picking-station.js';
28
+ export { PickingStation3D } from './picking-station-3d.js';
20
29
  export { default as GenericContainer } from './generic-container.js';
21
30
  export type { ContainerStatus } from './generic-container.js';
22
31
  export { GenericContainer3D } from './generic-container-3d.js';
package/dist/index.js CHANGED
@@ -14,6 +14,12 @@ export { default as Crane } from './crane.js';
14
14
  export { Crane3D } from './crane-3d.js';
15
15
  export { default as Spot } from './spot.js';
16
16
  export { Spot3D } from './spot-3d.js';
17
+ export { default as Stockpile } from './stockpile.js';
18
+ export { Stockpile3D } from './stockpile-3d.js';
19
+ export { default as StockpileGrid } from './stockpile-grid.js';
20
+ export { StockpileGrid3D } from './stockpile-grid-3d.js';
21
+ export { default as PickingStation } from './picking-station.js';
22
+ export { PickingStation3D } from './picking-station-3d.js';
17
23
  export { default as GenericContainer } from './generic-container.js';
18
24
  export { GenericContainer3D } from './generic-container-3d.js';
19
25
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,aAAa,CAAA;AAG/C,OAAO,EAAE,OAAO,IAAI,GAAG,EAAE,MAAM,UAAU,CAAA;AAGzC,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,aAAa,CAAA;AAE/C,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE1D,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEpD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAC9C,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAE7D,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AACvE,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,YAAY,CAAA;AAE7C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAEvC,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAErC,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAEpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\nexport { default as Pallet } from './pallet.js'\nexport type { PalletMaterial } from './pallet.js'\n\nexport { default as Box } from './box.js'\nexport type { BoxMaterial } from './box.js'\n\nexport { default as Parcel } from './parcel.js'\n\nexport { default as StorageRack } from './storage-rack.js'\nexport type { StorageRackState } from './storage-rack.js'\nexport { StorageRack3D } from './storage-rack-3d.js'\nexport { default as RackGrid } from './rack-grid.js'\nexport type { RackGridState, CellOverride } from './rack-grid.js'\nexport { RackGrid3D } from './rack-grid-3d.js'\nexport { default as RackGridCell } from './rack-grid-cell.js'\nexport type { RackGridCellState } from './rack-grid-cell.js'\nexport { default as MobileStorageRack } from './mobile-storage-rack.js'\nexport { default as Crane } from './crane.js'\nexport type { CraneState, CraneStatus } from './crane.js'\nexport { Crane3D } from './crane-3d.js'\n\nexport { default as Spot } from './spot.js'\nexport { Spot3D } from './spot-3d.js'\n\nexport { default as GenericContainer } from './generic-container.js'\nexport type { ContainerStatus } from './generic-container.js'\nexport { GenericContainer3D } from './generic-container-3d.js'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,aAAa,CAAA;AAG/C,OAAO,EAAE,OAAO,IAAI,GAAG,EAAE,MAAM,UAAU,CAAA;AAGzC,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,aAAa,CAAA;AAE/C,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE1D,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEpD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAC9C,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAE7D,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AACvE,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,YAAY,CAAA;AAE7C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAEvC,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAErC,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAIrD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAE9D,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAExD,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAEhE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAE1D,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAEpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\nexport { default as Pallet } from './pallet.js'\nexport type { PalletMaterial } from './pallet.js'\n\nexport { default as Box } from './box.js'\nexport type { BoxMaterial } from './box.js'\n\nexport { default as Parcel } from './parcel.js'\n\nexport { default as StorageRack } from './storage-rack.js'\nexport type { StorageRackState } from './storage-rack.js'\nexport { StorageRack3D } from './storage-rack-3d.js'\nexport { default as RackGrid } from './rack-grid.js'\nexport type { RackGridState, CellOverride } from './rack-grid.js'\nexport { RackGrid3D } from './rack-grid-3d.js'\nexport { default as RackGridCell } from './rack-grid-cell.js'\nexport type { RackGridCellState } from './rack-grid-cell.js'\nexport { default as MobileStorageRack } from './mobile-storage-rack.js'\nexport { default as Crane } from './crane.js'\nexport type { CraneState, CraneStatus } from './crane.js'\nexport { Crane3D } from './crane-3d.js'\n\nexport { default as Spot } from './spot.js'\nexport { Spot3D } from './spot-3d.js'\n\nexport { default as Stockpile } from './stockpile.js'\nexport type {\n StockpileState, StockpileRecord, StackPattern, CarrierPreset, PickPolicy\n} from './stockpile.js'\nexport { Stockpile3D } from './stockpile-3d.js'\n\nexport { default as StockpileGrid } from './stockpile-grid.js'\nexport type { StockpileGridState, StockpileGridCell } from './stockpile-grid.js'\nexport { StockpileGrid3D } from './stockpile-grid-3d.js'\n\nexport { default as PickingStation } from './picking-station.js'\nexport type { PickingStationState, PickingStationStatus } from './picking-station.js'\nexport { PickingStation3D } from './picking-station-3d.js'\n\nexport { default as GenericContainer } from './generic-container.js'\nexport type { ContainerStatus } from './generic-container.js'\nexport { GenericContainer3D } from './generic-container-3d.js'\n"]}
@@ -0,0 +1,20 @@
1
+ import * as THREE from 'three';
2
+ import { RealObjectGroup } from '@hatiolab/things-scene';
3
+ export declare class PickingStation3D extends RealObjectGroup {
4
+ private _padMesh?;
5
+ private _padOutline?;
6
+ private _tableMesh?;
7
+ build(): void;
8
+ /** carrier 안착 anchor — 작업대 top face. */
9
+ getAttachFrame(): THREE.Object3D | undefined;
10
+ private _padColor;
11
+ private _strokeColor;
12
+ /** status 에 따른 작업대 색 — idle/processing/busy. */
13
+ private _statusColor;
14
+ private _applyPadColor;
15
+ private _applyStrokeColor;
16
+ private _applyStatusColor;
17
+ updateAlpha(): void;
18
+ updateDimension(): void;
19
+ onchange(after: Record<string, unknown>, before: Record<string, unknown>): void;
20
+ }
@@ -0,0 +1,162 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * PickingStation3D — 사람 작업 위치의 3D 시각화. pad(영역) + 작업대(가운데 box, top
5
+ * face 가 carrier 안착면). status 별 색 변화 (idle/processing/busy) 로 작업 진행
6
+ * 인지성.
7
+ */
8
+ import * as THREE from 'three';
9
+ import { RealObjectGroup } from '@hatiolab/things-scene';
10
+ const PAD_DEPTH = 2;
11
+ const STATUS_TINT = {
12
+ idle: 0x4a7a9b, // 차분한 blue-grey
13
+ processing: 0xd9943a, // 작업 중 — 주황
14
+ busy: 0xc04040 // busy/blocked — 빨강
15
+ };
16
+ export class PickingStation3D extends RealObjectGroup {
17
+ _padMesh;
18
+ _padOutline;
19
+ _tableMesh;
20
+ build() {
21
+ super.build();
22
+ const state = this.component.state;
23
+ const w = state.width ?? 100;
24
+ const h = state.height ?? 100;
25
+ const depth = state.depth ?? 5;
26
+ // ── pad (영역) ─────────────────────────────────────────────
27
+ const padGeom = new THREE.BoxGeometry(w, PAD_DEPTH, h);
28
+ const padMat = new THREE.MeshStandardMaterial({
29
+ color: this._padColor(), roughness: 0.85, transparent: true, opacity: 0.5
30
+ });
31
+ this._padMesh = new THREE.Mesh(padGeom, padMat);
32
+ this._padMesh.position.y = PAD_DEPTH / 2;
33
+ this._padMesh.receiveShadow = true;
34
+ this.object3d.add(this._padMesh);
35
+ // pad outline
36
+ const outline = new THREE.LineBasicMaterial({ color: this._strokeColor() });
37
+ this._padOutline = new THREE.LineSegments(new THREE.EdgesGeometry(padGeom), outline);
38
+ this._padOutline.position.y = PAD_DEPTH / 2;
39
+ this.object3d.add(this._padOutline);
40
+ // ── 작업대 (table) — pad 위 가운데 box. top face = carrier 안착면. ──
41
+ const tableW = w * 0.55;
42
+ const tableH = h * 0.45;
43
+ const tableD = Math.max(8, depth); // 작업면 높이 — operation height 근사
44
+ const tableGeom = new THREE.BoxGeometry(tableW, tableD, tableH);
45
+ const tableMat = new THREE.MeshStandardMaterial({
46
+ color: this._statusColor(), roughness: 0.6, metalness: 0.1
47
+ });
48
+ this._tableMesh = new THREE.Mesh(tableGeom, tableMat);
49
+ this._tableMesh.position.y = PAD_DEPTH + tableD / 2;
50
+ this._tableMesh.castShadow = true;
51
+ this._tableMesh.receiveShadow = true;
52
+ this.object3d.add(this._tableMesh);
53
+ }
54
+ /** carrier 안착 anchor — 작업대 top face. */
55
+ getAttachFrame() {
56
+ return this._tableMesh ?? this._padMesh;
57
+ }
58
+ _padColor() {
59
+ const raw = this.component.state?.fillStyle;
60
+ if (typeof raw === 'string' && raw.length > 0) {
61
+ try {
62
+ return new THREE.Color(raw);
63
+ }
64
+ catch { /* fallthrough */ }
65
+ }
66
+ return new THREE.Color(0x5a8ab8);
67
+ }
68
+ _strokeColor() {
69
+ const raw = this.component.state?.strokeStyle;
70
+ if (typeof raw === 'string' && raw.length > 0) {
71
+ try {
72
+ return new THREE.Color(raw);
73
+ }
74
+ catch { /* fallthrough */ }
75
+ }
76
+ return this._padColor().clone().multiplyScalar(0.6);
77
+ }
78
+ /** status 에 따른 작업대 색 — idle/processing/busy. */
79
+ _statusColor() {
80
+ const s = this.component.state?.status;
81
+ return new THREE.Color(STATUS_TINT[s ?? 'idle'] ?? STATUS_TINT.idle);
82
+ }
83
+ _applyPadColor() {
84
+ if (this._padMesh) {
85
+ const m = this._padMesh.material;
86
+ m.color.copy(this._padColor());
87
+ m.needsUpdate = true;
88
+ }
89
+ }
90
+ _applyStrokeColor() {
91
+ if (this._padOutline) {
92
+ const m = this._padOutline.material;
93
+ m.color.copy(this._strokeColor());
94
+ m.needsUpdate = true;
95
+ }
96
+ }
97
+ _applyStatusColor() {
98
+ if (this._tableMesh) {
99
+ const m = this._tableMesh.material;
100
+ m.color.copy(this._statusColor());
101
+ m.needsUpdate = true;
102
+ }
103
+ }
104
+ updateAlpha() {
105
+ const alpha = typeof this.component.state.alpha === 'number'
106
+ ? this.component.state.alpha : 1;
107
+ if (this._padMesh) {
108
+ const m = this._padMesh.material;
109
+ m.opacity = 0.5 * alpha;
110
+ m.transparent = m.opacity < 1;
111
+ m.needsUpdate = true;
112
+ }
113
+ }
114
+ updateDimension() {
115
+ if (this._padMesh) {
116
+ const state = this.component.state;
117
+ const w = state.width ?? 100;
118
+ const h = state.height ?? 100;
119
+ const depth = state.depth ?? 5;
120
+ this._padMesh.geometry.dispose();
121
+ this._padMesh.geometry = new THREE.BoxGeometry(w, PAD_DEPTH, h);
122
+ if (this._padOutline) {
123
+ this._padOutline.geometry.dispose();
124
+ this._padOutline.geometry = new THREE.EdgesGeometry(this._padMesh.geometry);
125
+ }
126
+ if (this._tableMesh) {
127
+ const tableW = w * 0.55;
128
+ const tableH = h * 0.45;
129
+ const tableD = Math.max(8, depth);
130
+ this._tableMesh.geometry.dispose();
131
+ this._tableMesh.geometry = new THREE.BoxGeometry(tableW, tableD, tableH);
132
+ this._tableMesh.position.y = PAD_DEPTH + tableD / 2;
133
+ }
134
+ }
135
+ }
136
+ onchange(after, before) {
137
+ if ('width' in after || 'height' in after || 'depth' in after) {
138
+ this.updateDimension();
139
+ return;
140
+ }
141
+ if ('alpha' in after) {
142
+ this.updateAlpha();
143
+ return;
144
+ }
145
+ if ('fillStyle' in after) {
146
+ this._applyPadColor();
147
+ if (this.component.state.strokeStyle == null)
148
+ this._applyStrokeColor();
149
+ return;
150
+ }
151
+ if ('strokeStyle' in after) {
152
+ this._applyStrokeColor();
153
+ return;
154
+ }
155
+ if ('status' in after) {
156
+ this._applyStatusColor();
157
+ return;
158
+ }
159
+ super.onchange?.(after, before);
160
+ }
161
+ }
162
+ //# sourceMappingURL=picking-station-3d.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"picking-station-3d.js","sourceRoot":"","sources":["../src/picking-station-3d.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAKxD,MAAM,SAAS,GAAG,CAAC,CAAA;AAEnB,MAAM,WAAW,GAAyC;IACxD,IAAI,EAAE,QAAQ,EAAS,gBAAgB;IACvC,UAAU,EAAE,QAAQ,EAAG,YAAY;IACnC,IAAI,EAAE,QAAQ,CAAU,oBAAoB;CAC7C,CAAA;AAED,MAAM,OAAO,gBAAiB,SAAQ,eAAe;IAC3C,QAAQ,CAAa;IACrB,WAAW,CAAqB;IAChC,UAAU,CAAa;IAE/B,KAAK;QACH,KAAK,CAAC,KAAK,EAAE,CAAA;QACb,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,KAAY,CAAA;QACzC,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,IAAI,GAAG,CAAA;QAC5B,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,IAAI,GAAG,CAAA;QAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,CAAC,CAAA;QAE9B,4DAA4D;QAC5D,MAAM,OAAO,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAA;QACtD,MAAM,MAAM,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC;YAC5C,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG;SAC1E,CAAC,CAAA;QACF,IAAI,CAAC,QAAQ,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QAC/C,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,GAAG,SAAS,GAAG,CAAC,CAAA;QACxC,IAAI,CAAC,QAAQ,CAAC,aAAa,GAAG,IAAI,CAAA;QAClC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QAEhC,cAAc;QACd,MAAM,OAAO,GAAG,IAAI,KAAK,CAAC,iBAAiB,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC,CAAA;QAC3E,IAAI,CAAC,WAAW,GAAG,IAAI,KAAK,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAA;QACpF,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,GAAG,SAAS,GAAG,CAAC,CAAA;QAC3C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;QAEnC,6DAA6D;QAC7D,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAA;QACvB,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAA;QACvB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA,CAAG,+BAA+B;QACnE,MAAM,SAAS,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;QAC/D,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC;YAC9C,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG;SAC3D,CAAC,CAAA;QACF,IAAI,CAAC,UAAU,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAA;QACrD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,GAAG,SAAS,GAAG,MAAM,GAAG,CAAC,CAAA;QACnD,IAAI,CAAC,UAAU,CAAC,UAAU,GAAG,IAAI,CAAA;QACjC,IAAI,CAAC,UAAU,CAAC,aAAa,GAAG,IAAI,CAAA;QACpC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IACpC,CAAC;IAED,wCAAwC;IACxC,cAAc;QACZ,OAAO,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,QAAQ,CAAA;IACzC,CAAC;IAEO,SAAS;QACf,MAAM,GAAG,GAAI,IAAI,CAAC,SAAS,CAAC,KAAa,EAAE,SAAS,CAAA;QACpD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9C,IAAI,CAAC;gBAAC,OAAO,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;QACjE,CAAC;QACD,OAAO,IAAI,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IAClC,CAAC;IAEO,YAAY;QAClB,MAAM,GAAG,GAAI,IAAI,CAAC,SAAS,CAAC,KAAa,EAAE,WAAW,CAAA;QACtD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9C,IAAI,CAAC;gBAAC,OAAO,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;QACjE,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC,CAAA;IACrD,CAAC;IAED,gDAAgD;IACxC,YAAY;QAClB,MAAM,CAAC,GAAI,IAAI,CAAC,SAAS,CAAC,KAAa,EAAE,MAA0C,CAAA;QACnF,OAAO,IAAI,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,CAAA;IACtE,CAAC;IAEO,cAAc;QACpB,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAsC,CAAA;YAC9D,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAA;YAC9B,CAAC,CAAC,WAAW,GAAG,IAAI,CAAA;QACtB,CAAC;IACH,CAAC;IACO,iBAAiB;QACvB,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,QAAmC,CAAA;YAC9D,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAA;YACjC,CAAC,CAAC,WAAW,GAAG,IAAI,CAAA;QACtB,CAAC;IACH,CAAC;IACO,iBAAiB;QACvB,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,MAAM,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,QAAsC,CAAA;YAChE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAA;YACjC,CAAC,CAAC,WAAW,GAAG,IAAI,CAAA;QACtB,CAAC;IACH,CAAC;IAED,WAAW;QACT,MAAM,KAAK,GAAG,OAAQ,IAAI,CAAC,SAAS,CAAC,KAAa,CAAC,KAAK,KAAK,QAAQ;YACnE,CAAC,CAAE,IAAI,CAAC,SAAS,CAAC,KAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;QAC3C,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAsC,CAAA;YAC9D,CAAC,CAAC,OAAO,GAAG,GAAG,GAAG,KAAK,CAAA;YACvB,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,OAAO,GAAG,CAAC,CAAA;YAC7B,CAAC,CAAC,WAAW,GAAG,IAAI,CAAA;QACtB,CAAC;IACH,CAAC;IAED,eAAe;QACb,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,KAAY,CAAA;YACzC,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,IAAI,GAAG,CAAA;YAC5B,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,IAAI,GAAG,CAAA;YAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,CAAC,CAAA;YAC9B,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAA;YAChC,IAAI,CAAC,QAAQ,CAAC,QAAQ,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAA;YAC/D,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;gBACrB,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAA;gBACnC,IAAI,CAAC,WAAW,CAAC,QAAQ,GAAG,IAAI,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;YAC7E,CAAC;YACD,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;gBACpB,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAA;gBACvB,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAA;gBACvB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;gBACjC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAA;gBAClC,IAAI,CAAC,UAAU,CAAC,QAAQ,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;gBACxE,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,GAAG,SAAS,GAAG,MAAM,GAAG,CAAC,CAAA;YACrD,CAAC;QACH,CAAC;IACH,CAAC;IAED,QAAQ,CAAC,KAA8B,EAAE,MAA+B;QACtE,IAAI,OAAO,IAAI,KAAK,IAAI,QAAQ,IAAI,KAAK,IAAI,OAAO,IAAI,KAAK,EAAE,CAAC;YAC9D,IAAI,CAAC,eAAe,EAAE,CAAA;YACtB,OAAM;QACR,CAAC;QACD,IAAI,OAAO,IAAI,KAAK,EAAE,CAAC;YAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YAAC,OAAM;QAAC,CAAC;QACpD,IAAI,WAAW,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC,cAAc,EAAE,CAAA;YACrB,IAAK,IAAI,CAAC,SAAS,CAAC,KAAa,CAAC,WAAW,IAAI,IAAI;gBAAE,IAAI,CAAC,iBAAiB,EAAE,CAAA;YAC/E,OAAM;QACR,CAAC;QACD,IAAI,aAAa,IAAI,KAAK,EAAE,CAAC;YAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAAC,OAAM;QAAC,CAAC;QAChE,IAAI,QAAQ,IAAI,KAAK,EAAE,CAAC;YAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAAC,OAAM;QAAC,CAAC;QAC3D,KAAK,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IACjC,CAAC;CACF","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * PickingStation3D — 사람 작업 위치의 3D 시각화. pad(영역) + 작업대(가운데 box, top\n * face 가 carrier 안착면). status 별 색 변화 (idle/processing/busy) 로 작업 진행\n * 인지성.\n */\n\nimport * as THREE from 'three'\nimport { RealObjectGroup } from '@hatiolab/things-scene'\n\nimport type PickingStation from './picking-station.js'\nimport type { PickingStationStatus } from './picking-station.js'\n\nconst PAD_DEPTH = 2\n\nconst STATUS_TINT: Record<PickingStationStatus, number> = {\n idle: 0x4a7a9b, // 차분한 blue-grey\n processing: 0xd9943a, // 작업 중 — 주황\n busy: 0xc04040 // busy/blocked — 빨강\n}\n\nexport class PickingStation3D extends RealObjectGroup {\n private _padMesh?: THREE.Mesh\n private _padOutline?: THREE.LineSegments\n private _tableMesh?: THREE.Mesh\n\n build() {\n super.build()\n const state = this.component.state as any\n const w = state.width ?? 100\n const h = state.height ?? 100\n const depth = state.depth ?? 5\n\n // ── pad (영역) ─────────────────────────────────────────────\n const padGeom = new THREE.BoxGeometry(w, PAD_DEPTH, h)\n const padMat = new THREE.MeshStandardMaterial({\n color: this._padColor(), roughness: 0.85, transparent: true, opacity: 0.5\n })\n this._padMesh = new THREE.Mesh(padGeom, padMat)\n this._padMesh.position.y = PAD_DEPTH / 2\n this._padMesh.receiveShadow = true\n this.object3d.add(this._padMesh)\n\n // pad outline\n const outline = new THREE.LineBasicMaterial({ color: this._strokeColor() })\n this._padOutline = new THREE.LineSegments(new THREE.EdgesGeometry(padGeom), outline)\n this._padOutline.position.y = PAD_DEPTH / 2\n this.object3d.add(this._padOutline)\n\n // ── 작업대 (table) — pad 위 가운데 box. top face = carrier 안착면. ──\n const tableW = w * 0.55\n const tableH = h * 0.45\n const tableD = Math.max(8, depth) // 작업면 높이 — operation height 근사\n const tableGeom = new THREE.BoxGeometry(tableW, tableD, tableH)\n const tableMat = new THREE.MeshStandardMaterial({\n color: this._statusColor(), roughness: 0.6, metalness: 0.1\n })\n this._tableMesh = new THREE.Mesh(tableGeom, tableMat)\n this._tableMesh.position.y = PAD_DEPTH + tableD / 2\n this._tableMesh.castShadow = true\n this._tableMesh.receiveShadow = true\n this.object3d.add(this._tableMesh)\n }\n\n /** carrier 안착 anchor — 작업대 top face. */\n getAttachFrame(): THREE.Object3D | undefined {\n return this._tableMesh ?? this._padMesh\n }\n\n private _padColor(): THREE.Color {\n const raw = (this.component.state as any)?.fillStyle\n if (typeof raw === 'string' && raw.length > 0) {\n try { return new THREE.Color(raw) } catch { /* fallthrough */ }\n }\n return new THREE.Color(0x5a8ab8)\n }\n\n private _strokeColor(): THREE.Color {\n const raw = (this.component.state as any)?.strokeStyle\n if (typeof raw === 'string' && raw.length > 0) {\n try { return new THREE.Color(raw) } catch { /* fallthrough */ }\n }\n return this._padColor().clone().multiplyScalar(0.6)\n }\n\n /** status 에 따른 작업대 색 — idle/processing/busy. */\n private _statusColor(): THREE.Color {\n const s = (this.component.state as any)?.status as PickingStationStatus | undefined\n return new THREE.Color(STATUS_TINT[s ?? 'idle'] ?? STATUS_TINT.idle)\n }\n\n private _applyPadColor(): void {\n if (this._padMesh) {\n const m = this._padMesh.material as THREE.MeshStandardMaterial\n m.color.copy(this._padColor())\n m.needsUpdate = true\n }\n }\n private _applyStrokeColor(): void {\n if (this._padOutline) {\n const m = this._padOutline.material as THREE.LineBasicMaterial\n m.color.copy(this._strokeColor())\n m.needsUpdate = true\n }\n }\n private _applyStatusColor(): void {\n if (this._tableMesh) {\n const m = this._tableMesh.material as THREE.MeshStandardMaterial\n m.color.copy(this._statusColor())\n m.needsUpdate = true\n }\n }\n\n updateAlpha(): void {\n const alpha = typeof (this.component.state as any).alpha === 'number'\n ? (this.component.state as any).alpha : 1\n if (this._padMesh) {\n const m = this._padMesh.material as THREE.MeshStandardMaterial\n m.opacity = 0.5 * alpha\n m.transparent = m.opacity < 1\n m.needsUpdate = true\n }\n }\n\n updateDimension(): void {\n if (this._padMesh) {\n const state = this.component.state as any\n const w = state.width ?? 100\n const h = state.height ?? 100\n const depth = state.depth ?? 5\n this._padMesh.geometry.dispose()\n this._padMesh.geometry = new THREE.BoxGeometry(w, PAD_DEPTH, h)\n if (this._padOutline) {\n this._padOutline.geometry.dispose()\n this._padOutline.geometry = new THREE.EdgesGeometry(this._padMesh.geometry)\n }\n if (this._tableMesh) {\n const tableW = w * 0.55\n const tableH = h * 0.45\n const tableD = Math.max(8, depth)\n this._tableMesh.geometry.dispose()\n this._tableMesh.geometry = new THREE.BoxGeometry(tableW, tableD, tableH)\n this._tableMesh.position.y = PAD_DEPTH + tableD / 2\n }\n }\n }\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>): void {\n if ('width' in after || 'height' in after || 'depth' in after) {\n this.updateDimension()\n return\n }\n if ('alpha' in after) { this.updateAlpha(); return }\n if ('fillStyle' in after) {\n this._applyPadColor()\n if ((this.component.state as any).strokeStyle == null) this._applyStrokeColor()\n return\n }\n if ('strokeStyle' in after) { this._applyStrokeColor(); return }\n if ('status' in after) { this._applyStatusColor(); return }\n super.onchange?.(after, before)\n }\n}\n"]}
@@ -0,0 +1,56 @@
1
+ import { Component, ComponentNature, RealObject } from '@hatiolab/things-scene';
2
+ import type { State, Material3D } from '@hatiolab/things-scene';
3
+ import { SlotTarget, type AttachFrame, type Alignment, type Heights, type PlacementArchetype } from '@operato/scene-base';
4
+ import { PickingStation3D } from './picking-station-3d.js';
5
+ export type PickingStationStatus = 'idle' | 'processing' | 'busy';
6
+ export interface PickingStationState extends State {
7
+ /** carrier 가 머무는 처리 시간 (ms). 0/미설정이면 즉시 idle 유지. */
8
+ processingTimeMs?: number;
9
+ /** 현재 상태 (자동). */
10
+ status?: PickingStationStatus;
11
+ /** click 시 invoke 할 Popup 컴포넌트 id. */
12
+ popupRef?: string;
13
+ material3d?: Material3D;
14
+ }
15
+ declare const PickingStation_base: any;
16
+ export default class PickingStation extends PickingStation_base {
17
+ state: PickingStationState;
18
+ _realObject?: PickingStation3D;
19
+ static placement: PlacementArchetype;
20
+ static align: Alignment;
21
+ static defaultDepth: (h: Heights) => number;
22
+ get nature(): ComponentNature;
23
+ get anchors(): never[];
24
+ slotIds(): ReadonlyArray<string>;
25
+ hasCarrierAt(slotId: string): boolean;
26
+ canReceiveAt(slotId: string, _carrier?: Component): boolean;
27
+ occupiedSlotIds(): ReadonlyArray<string>;
28
+ emptySlotIds(): ReadonlyArray<string>;
29
+ obtainCarrier(slotId: string): Component | null;
30
+ /**
31
+ * carrier 도착 — components + 3D 로 reparent (Spot 패턴). processingTimeMs > 0
32
+ * 이면 status='processing' 으로 전환했다가 timer 후 'idle' 로 복귀.
33
+ *
34
+ * 단순 setTimeout — frameClock 통합(view-mode/일시정지 동기) 은 추후.
35
+ */
36
+ receiveAt(_slotId: string, carrier: Component, options?: any): Promise<void>;
37
+ accept(carrier: Component, options?: any): Promise<void>;
38
+ receive(carrier: Component, options?: any): Promise<void>;
39
+ slotTargetAt(slotId: string): SlotTarget;
40
+ getSlotAttachObject3d(_slotId: string): any;
41
+ /** carrier 를 작업대(table) 상단에 안착. */
42
+ attachPointFor(carrier: Component): AttachFrame | null;
43
+ render(ctx: CanvasRenderingContext2D): void;
44
+ get eventMap(): {
45
+ '(self)': {
46
+ '(self)': {
47
+ click: (mouseEvent: MouseEvent) => void;
48
+ };
49
+ };
50
+ };
51
+ private _onStationClick;
52
+ private _invokePopup;
53
+ private _raycastStationHit;
54
+ buildRealObject(): RealObject | undefined;
55
+ }
56
+ export {};
@@ -0,0 +1,212 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * PickingStation — 사람(또는 자동화) 작업 위치. carrier 가 도착하면 *_processingTimeMs_*
5
+ * 동안 머문 뒤 status='idle' 로 자동 전환되어 다음 mover 가 가져갈 수 있다.
6
+ *
7
+ * 단일 slot SlottedHolder (Spot 비슷) + 처리 시간/상태 + popupRef + click raycast.
8
+ * 3D 는 pad(영역) + 작업대(가운데 box) — picking/QC 작업 자리의 인지성을 위해.
9
+ */
10
+ import { __decorate } from "tslib";
11
+ import * as THREE from 'three';
12
+ import { ContainerAbstract, sceneComponent } from '@hatiolab/things-scene';
13
+ import { CarrierHolder, Placeable, SlotTarget } from '@operato/scene-base';
14
+ import { PickingStation3D } from './picking-station-3d.js';
15
+ const SLOT_ID = 'station';
16
+ const NATURE = {
17
+ mutable: false,
18
+ resizable: true,
19
+ rotatable: true,
20
+ properties: [
21
+ { type: 'number', label: 'processing-time-ms', name: 'processingTimeMs' },
22
+ { type: 'select', label: 'status', name: 'status',
23
+ property: { options: ['idle', 'processing', 'busy'] } },
24
+ { type: 'id-input', label: 'popup-ref', name: 'popupRef',
25
+ property: { component: 'popup' } }
26
+ ],
27
+ help: 'scene/component/picking-station'
28
+ };
29
+ let PickingStation = class PickingStation extends CarrierHolder(Placeable(ContainerAbstract)) {
30
+ static placement = 'floor';
31
+ static align = 'bottom';
32
+ static defaultDepth = (h) => h.operation - h.floor;
33
+ get nature() { return NATURE; }
34
+ get anchors() { return []; }
35
+ // ── SlottedHolder duck-type (단일 slot 'station') ─────────────
36
+ slotIds() { return [SLOT_ID]; }
37
+ hasCarrierAt(slotId) {
38
+ return slotId === SLOT_ID &&
39
+ (this.components ?? []).some((c) => c?.isCarriable);
40
+ }
41
+ canReceiveAt(slotId, _carrier) {
42
+ return slotId === SLOT_ID && !this.hasCarrierAt(slotId);
43
+ }
44
+ occupiedSlotIds() {
45
+ return this.hasCarrierAt(SLOT_ID) ? [SLOT_ID] : [];
46
+ }
47
+ emptySlotIds() {
48
+ return this.hasCarrierAt(SLOT_ID) ? [] : [SLOT_ID];
49
+ }
50
+ obtainCarrier(slotId) {
51
+ if (slotId !== SLOT_ID)
52
+ return null;
53
+ return (this.components ?? []).find((c) => c?.isCarriable) ?? null;
54
+ }
55
+ /**
56
+ * carrier 도착 — components + 3D 로 reparent (Spot 패턴). processingTimeMs > 0
57
+ * 이면 status='processing' 으로 전환했다가 timer 후 'idle' 로 복귀.
58
+ *
59
+ * 단순 setTimeout — frameClock 통합(view-mode/일시정지 동기) 은 추후.
60
+ */
61
+ async receiveAt(_slotId, carrier, options) {
62
+ ;
63
+ this.reparent?.(carrier, { ...(options ?? {}), animated: false });
64
+ const procMs = this.state.processingTimeMs ?? 0;
65
+ if (procMs > 0) {
66
+ this.setState({ status: 'processing' });
67
+ setTimeout(() => {
68
+ if (this.state.status === 'processing') {
69
+ this.setState({ status: 'idle' });
70
+ }
71
+ }, procMs);
72
+ }
73
+ }
74
+ async accept(carrier, options) {
75
+ return this.receiveAt(SLOT_ID, carrier, options);
76
+ }
77
+ async receive(carrier, options) {
78
+ return this.receiveAt(SLOT_ID, carrier, options);
79
+ }
80
+ slotTargetAt(slotId) { return new SlotTarget(this, slotId); }
81
+ getSlotAttachObject3d(_slotId) {
82
+ return this._realObject?.getAttachFrame?.();
83
+ }
84
+ /** carrier 를 작업대(table) 상단에 안착. */
85
+ attachPointFor(carrier) {
86
+ const ro = this._realObject;
87
+ const frame = ro?.getAttachFrame?.();
88
+ if (!frame)
89
+ return null;
90
+ const carrierDepth = resolveDepth(carrier);
91
+ return { attach: frame, localPosition: { x: 0, y: carrierDepth / 2, z: 0 } };
92
+ }
93
+ // ── 2D render ───────────────────────────────────────────────
94
+ render(ctx) {
95
+ const { left = 0, top = 0, width = 100, height = 100 } = this.state;
96
+ const fillStyle = this.state.fillStyle || '#5a8ab8';
97
+ const strokeStyle = this.state.strokeStyle || '#3d6a8f';
98
+ const status = (this.state.status ?? 'idle');
99
+ // pad
100
+ ctx.save();
101
+ ctx.fillStyle = fillStyle;
102
+ ctx.globalAlpha = 0.15;
103
+ ctx.fillRect(left, top, width, height);
104
+ ctx.restore();
105
+ // outline
106
+ ctx.save();
107
+ ctx.strokeStyle = strokeStyle;
108
+ ctx.lineWidth = 1.5;
109
+ ctx.strokeRect(left + 0.75, top + 0.75, width - 1.5, height - 1.5);
110
+ ctx.restore();
111
+ // 작업대 (가운데 작은 사각)
112
+ const tw = width * 0.55, th = height * 0.45;
113
+ const tx = left + (width - tw) / 2;
114
+ const ty = top + (height - th) / 2;
115
+ ctx.save();
116
+ ctx.fillStyle = strokeStyle;
117
+ ctx.globalAlpha = 0.35;
118
+ ctx.fillRect(tx, ty, tw, th);
119
+ ctx.restore();
120
+ // 상태
121
+ ctx.save();
122
+ const fontSize = Math.min(width, height) * 0.16;
123
+ ctx.fillStyle = '#222';
124
+ ctx.font = `bold ${fontSize}px sans-serif`;
125
+ ctx.textAlign = 'center';
126
+ ctx.textBaseline = 'middle';
127
+ ctx.fillText(status.toUpperCase(), left + width / 2, top + height / 2);
128
+ ctx.restore();
129
+ }
130
+ // ── Popup + click ────────────────────────────────────────────
131
+ get eventMap() {
132
+ return { '(self)': { '(self)': { click: this._onStationClick } } };
133
+ }
134
+ _onStationClick = (mouseEvent) => {
135
+ if (!this.app?.isViewMode)
136
+ return;
137
+ const hit = this._raycastStationHit(mouseEvent);
138
+ if (!hit)
139
+ return;
140
+ this._invokePopup();
141
+ };
142
+ _invokePopup() {
143
+ const popupRefId = this.state.popupRef;
144
+ if (!popupRefId)
145
+ return;
146
+ const popupComp = this.root?.findById?.(popupRefId);
147
+ if (!popupComp || typeof popupComp.openPopup !== 'function')
148
+ return;
149
+ const anchor = this.slotTargetAt(SLOT_ID);
150
+ const carrier = this.obtainCarrier(SLOT_ID);
151
+ popupComp.openPopup({
152
+ componentId: this.state.id,
153
+ status: this.state.status ?? 'idle',
154
+ processingTimeMs: this.state.processingTimeMs,
155
+ currentCarrierId: carrier?.state?.id ?? null
156
+ }, { anchor });
157
+ }
158
+ _raycastStationHit(mouseEvent) {
159
+ const ro = this._realObject;
160
+ if (!ro?.object3d)
161
+ return undefined;
162
+ const tc = ro.threeContainer;
163
+ if (!tc)
164
+ return undefined;
165
+ const cap = tc._threeCapability ?? tc._capability;
166
+ let intersects;
167
+ if (cap?.getObjectsByRaycast)
168
+ intersects = cap.getObjectsByRaycast();
169
+ if (!intersects || intersects.length === 0) {
170
+ const scene = tc.scene3d;
171
+ const renderer = tc.renderer3d;
172
+ const camera = tc.activeCamera3d ??
173
+ cap?.activeCamera ??
174
+ cap?.camera;
175
+ const canvas = renderer?.domElement;
176
+ if (!scene || !canvas || !camera)
177
+ return undefined;
178
+ const rect = canvas.getBoundingClientRect();
179
+ if (rect.width === 0 || rect.height === 0)
180
+ return undefined;
181
+ const ndc = new THREE.Vector2(((mouseEvent.clientX - rect.left) / rect.width) * 2 - 1, -((mouseEvent.clientY - rect.top) / rect.height) * 2 + 1);
182
+ const raycaster = new THREE.Raycaster();
183
+ raycaster.setFromCamera(ndc, camera);
184
+ intersects = raycaster.intersectObjects(scene.children, true);
185
+ }
186
+ if (!intersects || intersects.length === 0)
187
+ return undefined;
188
+ const closest = intersects[0];
189
+ let obj = closest.object;
190
+ while (obj) {
191
+ if (obj.userData?.context === ro)
192
+ return closest;
193
+ obj = obj.parent;
194
+ }
195
+ return undefined;
196
+ }
197
+ buildRealObject() {
198
+ return new PickingStation3D(this);
199
+ }
200
+ };
201
+ PickingStation = __decorate([
202
+ sceneComponent('picking-station')
203
+ ], PickingStation);
204
+ export default PickingStation;
205
+ function resolveDepth(c) {
206
+ const eff = c._realObject?.effectiveDepth;
207
+ if (typeof eff === 'number' && Number.isFinite(eff))
208
+ return eff;
209
+ const d = c?.state?.depth;
210
+ return typeof d === 'number' && Number.isFinite(d) ? d : 0;
211
+ }
212
+ //# sourceMappingURL=picking-station.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"picking-station.js","sourceRoot":"","sources":["../src/picking-station.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * PickingStation — 사람(또는 자동화) 작업 위치. carrier 가 도착하면 *_processingTimeMs_*\n * 동안 머문 뒤 status='idle' 로 자동 전환되어 다음 mover 가 가져갈 수 있다.\n *\n * 단일 slot SlottedHolder (Spot 비슷) + 처리 시간/상태 + popupRef + click raycast.\n * 3D 는 pad(영역) + 작업대(가운데 box) — picking/QC 작업 자리의 인지성을 위해.\n */\n\nimport * as THREE from 'three'\nimport { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'\nimport type { State, Material3D } from '@hatiolab/things-scene'\nimport {\n CarrierHolder,\n Placeable,\n SlotTarget,\n type AttachFrame,\n type Alignment,\n type Heights,\n type PlacementArchetype\n} from '@operato/scene-base'\n\nimport { PickingStation3D } from './picking-station-3d.js'\n\nexport type PickingStationStatus = 'idle' | 'processing' | 'busy'\n\nexport interface PickingStationState extends State {\n /** carrier 가 머무는 처리 시간 (ms). 0/미설정이면 즉시 idle 유지. */\n processingTimeMs?: number\n /** 현재 상태 (자동). */\n status?: PickingStationStatus\n /** click 시 invoke 할 Popup 컴포넌트 id. */\n popupRef?: string\n material3d?: Material3D\n}\n\nconst SLOT_ID = 'station'\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [\n { type: 'number', label: 'processing-time-ms', name: 'processingTimeMs' },\n { type: 'select', label: 'status', name: 'status',\n property: { options: ['idle', 'processing', 'busy'] } },\n { type: 'id-input', label: 'popup-ref', name: 'popupRef',\n property: { component: 'popup' } }\n ],\n help: 'scene/component/picking-station'\n}\n\n@sceneComponent('picking-station')\nexport default class PickingStation extends CarrierHolder(Placeable(ContainerAbstract)) {\n declare state: PickingStationState\n declare _realObject?: PickingStation3D\n\n static placement: PlacementArchetype = 'floor'\n static align: Alignment = 'bottom'\n static defaultDepth = (h: Heights) => h.operation - h.floor\n\n get nature(): ComponentNature { return NATURE }\n get anchors() { return [] }\n\n // ── SlottedHolder duck-type (단일 slot 'station') ─────────────\n slotIds(): ReadonlyArray<string> { return [SLOT_ID] }\n\n hasCarrierAt(slotId: string): boolean {\n return slotId === SLOT_ID &&\n ((this as any).components ?? []).some((c: any) => c?.isCarriable)\n }\n canReceiveAt(slotId: string, _carrier?: Component): boolean {\n return slotId === SLOT_ID && !this.hasCarrierAt(slotId)\n }\n occupiedSlotIds(): ReadonlyArray<string> {\n return this.hasCarrierAt(SLOT_ID) ? [SLOT_ID] : []\n }\n emptySlotIds(): ReadonlyArray<string> {\n return this.hasCarrierAt(SLOT_ID) ? [] : [SLOT_ID]\n }\n obtainCarrier(slotId: string): Component | null {\n if (slotId !== SLOT_ID) return null\n return ((this as any).components ?? []).find((c: any) => c?.isCarriable) ?? null\n }\n\n /**\n * carrier 도착 — components + 3D 로 reparent (Spot 패턴). processingTimeMs > 0\n * 이면 status='processing' 으로 전환했다가 timer 후 'idle' 로 복귀.\n *\n * 단순 setTimeout — frameClock 통합(view-mode/일시정지 동기) 은 추후.\n */\n async receiveAt(_slotId: string, carrier: Component, options?: any): Promise<void> {\n ;(this as any).reparent?.(carrier, { ...(options ?? {}), animated: false })\n const procMs = this.state.processingTimeMs ?? 0\n if (procMs > 0) {\n this.setState({ status: 'processing' as PickingStationStatus })\n setTimeout(() => {\n if ((this.state.status as PickingStationStatus) === 'processing') {\n this.setState({ status: 'idle' as PickingStationStatus })\n }\n }, procMs)\n }\n }\n\n async accept(carrier: Component, options?: any): Promise<void> {\n return this.receiveAt(SLOT_ID, carrier, options)\n }\n async receive(carrier: Component, options?: any): Promise<void> {\n return this.receiveAt(SLOT_ID, carrier, options)\n }\n\n slotTargetAt(slotId: string): SlotTarget { return new SlotTarget(this as any, slotId) }\n getSlotAttachObject3d(_slotId: string): any {\n return (this as any)._realObject?.getAttachFrame?.()\n }\n\n /** carrier 를 작업대(table) 상단에 안착. */\n attachPointFor(carrier: Component): AttachFrame | null {\n const ro = this._realObject\n const frame = ro?.getAttachFrame?.()\n if (!frame) return null\n const carrierDepth = resolveDepth(carrier)\n return { attach: frame, localPosition: { x: 0, y: carrierDepth / 2, z: 0 } }\n }\n\n // ── 2D render ───────────────────────────────────────────────\n render(ctx: CanvasRenderingContext2D) {\n const { left = 0, top = 0, width = 100, height = 100 } = this.state\n const fillStyle = (this.state.fillStyle as string) || '#5a8ab8'\n const strokeStyle = (this.state.strokeStyle as string) || '#3d6a8f'\n const status = (this.state.status ?? 'idle') as PickingStationStatus\n\n // pad\n ctx.save()\n ctx.fillStyle = fillStyle\n ctx.globalAlpha = 0.15\n ctx.fillRect(left, top, width, height)\n ctx.restore()\n\n // outline\n ctx.save()\n ctx.strokeStyle = strokeStyle\n ctx.lineWidth = 1.5\n ctx.strokeRect(left + 0.75, top + 0.75, width - 1.5, height - 1.5)\n ctx.restore()\n\n // 작업대 (가운데 작은 사각)\n const tw = width * 0.55, th = height * 0.45\n const tx = left + (width - tw) / 2\n const ty = top + (height - th) / 2\n ctx.save()\n ctx.fillStyle = strokeStyle\n ctx.globalAlpha = 0.35\n ctx.fillRect(tx, ty, tw, th)\n ctx.restore()\n\n // 상태\n ctx.save()\n const fontSize = Math.min(width, height) * 0.16\n ctx.fillStyle = '#222'\n ctx.font = `bold ${fontSize}px sans-serif`\n ctx.textAlign = 'center'\n ctx.textBaseline = 'middle'\n ctx.fillText(status.toUpperCase(), left + width / 2, top + height / 2)\n ctx.restore()\n }\n\n // ── Popup + click ────────────────────────────────────────────\n get eventMap() {\n return { '(self)': { '(self)': { click: this._onStationClick } } }\n }\n\n private _onStationClick = (mouseEvent: MouseEvent) => {\n if (!(this as any).app?.isViewMode) return\n const hit = this._raycastStationHit(mouseEvent)\n if (!hit) return\n this._invokePopup()\n }\n\n private _invokePopup(): void {\n const popupRefId = this.state.popupRef\n if (!popupRefId) return\n const popupComp: any = (this as any).root?.findById?.(popupRefId)\n if (!popupComp || typeof popupComp.openPopup !== 'function') return\n const anchor = this.slotTargetAt(SLOT_ID)\n const carrier = this.obtainCarrier(SLOT_ID)\n popupComp.openPopup({\n componentId: (this.state as any).id,\n status: this.state.status ?? 'idle',\n processingTimeMs: this.state.processingTimeMs,\n currentCarrierId: (carrier as any)?.state?.id ?? null\n }, { anchor })\n }\n\n private _raycastStationHit(mouseEvent: MouseEvent): THREE.Intersection | undefined {\n const ro: any = (this as any)._realObject\n if (!ro?.object3d) return undefined\n const tc: any = ro.threeContainer\n if (!tc) return undefined\n const cap: any = tc._threeCapability ?? tc._capability\n let intersects: THREE.Intersection[] | undefined\n if (cap?.getObjectsByRaycast) intersects = cap.getObjectsByRaycast() as THREE.Intersection[] | undefined\n if (!intersects || intersects.length === 0) {\n const scene = tc.scene3d as THREE.Scene | undefined\n const renderer = tc.renderer3d as THREE.WebGLRenderer | undefined\n const camera =\n (tc.activeCamera3d as THREE.Camera | undefined) ??\n (cap?.activeCamera as THREE.Camera | undefined) ??\n (cap?.camera as THREE.Camera | undefined)\n const canvas = renderer?.domElement\n if (!scene || !canvas || !camera) return undefined\n const rect = canvas.getBoundingClientRect()\n if (rect.width === 0 || rect.height === 0) return undefined\n const ndc = new THREE.Vector2(\n ((mouseEvent.clientX - rect.left) / rect.width) * 2 - 1,\n -((mouseEvent.clientY - rect.top) / rect.height) * 2 + 1\n )\n const raycaster = new THREE.Raycaster()\n raycaster.setFromCamera(ndc, camera)\n intersects = raycaster.intersectObjects(scene.children, true)\n }\n if (!intersects || intersects.length === 0) return undefined\n const closest = intersects[0]\n let obj: THREE.Object3D | null = closest.object\n while (obj) {\n if (obj.userData?.context === ro) return closest\n obj = obj.parent\n }\n return undefined\n }\n\n buildRealObject(): RealObject | undefined {\n return new PickingStation3D(this)\n }\n}\n\nfunction resolveDepth(c: Component): number {\n const eff = (c as any)._realObject?.effectiveDepth\n if (typeof eff === 'number' && Number.isFinite(eff)) return eff\n const d = (c as any)?.state?.depth\n return typeof d === 'number' && Number.isFinite(d) ? d : 0\n}\n"]}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * rack 이 *_이 mover 의 toolType_* 을 선반 적재용으로 수용하는가.
3
+ *
4
+ * rack 선반 적재는 높이 도달 mover (crane / stacker / forklift) 의 몫. 평탄 데크
5
+ * 차량(agv-deck)은 바닥 운반 전용 — 선반 직접 적재 불가 → 거부. 거부된 mover 는
6
+ * transfer planner 가 자동으로 in-port 경유(환승)를 택하게 만든다.
7
+ *
8
+ * @param moverToolType 적재하려는 mover 의 toolType (undefined 면 능력 미상 → 허용)
9
+ * @param blockedTools 거부 toolType 목록 (default ['agv-deck'])
10
+ */
11
+ export declare function rackAcceptsMoverTool(moverToolType: string | undefined | null, blockedTools?: readonly string[]): boolean;
@@ -0,0 +1,25 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * Rack 적재 mover 능력 판정 — *_순수 로직_* (things-scene 무관). storage-rack 의
5
+ * canAcceptFromMover 가 위임. mocha 환경에서 StorageRack 직접 import 불가하므로
6
+ * 판정 로직만 분리해 단위 검증.
7
+ */
8
+ /**
9
+ * rack 이 *_이 mover 의 toolType_* 을 선반 적재용으로 수용하는가.
10
+ *
11
+ * rack 선반 적재는 높이 도달 mover (crane / stacker / forklift) 의 몫. 평탄 데크
12
+ * 차량(agv-deck)은 바닥 운반 전용 — 선반 직접 적재 불가 → 거부. 거부된 mover 는
13
+ * transfer planner 가 자동으로 in-port 경유(환승)를 택하게 만든다.
14
+ *
15
+ * @param moverToolType 적재하려는 mover 의 toolType (undefined 면 능력 미상 → 허용)
16
+ * @param blockedTools 거부 toolType 목록 (default ['agv-deck'])
17
+ */
18
+ export function rackAcceptsMoverTool(moverToolType, blockedTools = ['agv-deck']) {
19
+ if (moverToolType == null)
20
+ return true;
21
+ if (!Array.isArray(blockedTools))
22
+ return true;
23
+ return !blockedTools.includes(moverToolType);
24
+ }
25
+ //# sourceMappingURL=rack-capability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rack-capability.js","sourceRoot":"","sources":["../src/rack-capability.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAClC,aAAwC,EACxC,eAAkC,CAAC,UAAU,CAAC;IAE9C,IAAI,aAAa,IAAI,IAAI;QAAE,OAAO,IAAI,CAAA;IACtC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC;QAAE,OAAO,IAAI,CAAA;IAC7C,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAA;AAC9C,CAAC","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Rack 적재 mover 능력 판정 — *_순수 로직_* (things-scene 무관). storage-rack 의\n * canAcceptFromMover 가 위임. mocha 환경에서 StorageRack 직접 import 불가하므로\n * 판정 로직만 분리해 단위 검증.\n */\n\n/**\n * rack 이 *_이 mover 의 toolType_* 을 선반 적재용으로 수용하는가.\n *\n * rack 선반 적재는 높이 도달 mover (crane / stacker / forklift) 의 몫. 평탄 데크\n * 차량(agv-deck)은 바닥 운반 전용 — 선반 직접 적재 불가 → 거부. 거부된 mover 는\n * transfer planner 가 자동으로 in-port 경유(환승)를 택하게 만든다.\n *\n * @param moverToolType 적재하려는 mover 의 toolType (undefined 면 능력 미상 → 허용)\n * @param blockedTools 거부 toolType 목록 (default ['agv-deck'])\n */\nexport function rackAcceptsMoverTool(\n moverToolType: string | undefined | null,\n blockedTools: readonly string[] = ['agv-deck']\n): boolean {\n if (moverToolType == null) return true\n if (!Array.isArray(blockedTools)) return true\n return !blockedTools.includes(moverToolType)\n}\n"]}