@operato/scene-storage 10.0.0-beta.48 → 10.0.0-beta.53

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 (78) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/box.js +2 -2
  3. package/dist/box.js.map +1 -1
  4. package/dist/index.d.ts +9 -0
  5. package/dist/index.js +6 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/pallet.js +2 -2
  8. package/dist/pallet.js.map +1 -1
  9. package/dist/parcel.js +2 -2
  10. package/dist/parcel.js.map +1 -1
  11. package/dist/picking-station-3d.d.ts +20 -0
  12. package/dist/picking-station-3d.js +162 -0
  13. package/dist/picking-station-3d.js.map +1 -0
  14. package/dist/picking-station.d.ts +50 -0
  15. package/dist/picking-station.js +186 -0
  16. package/dist/picking-station.js.map +1 -0
  17. package/dist/rack-capability.d.ts +11 -0
  18. package/dist/rack-capability.js +25 -0
  19. package/dist/rack-capability.js.map +1 -0
  20. package/dist/rack-grid.d.ts +4 -22
  21. package/dist/rack-grid.js +23 -115
  22. package/dist/rack-grid.js.map +1 -1
  23. package/dist/spot.d.ts +1 -0
  24. package/dist/spot.js +6 -2
  25. package/dist/spot.js.map +1 -1
  26. package/dist/stockpile-3d.d.ts +55 -0
  27. package/dist/stockpile-3d.js +387 -0
  28. package/dist/stockpile-3d.js.map +1 -0
  29. package/dist/stockpile-grid-3d.d.ts +30 -0
  30. package/dist/stockpile-grid-3d.js +301 -0
  31. package/dist/stockpile-grid-3d.js.map +1 -0
  32. package/dist/stockpile-grid.d.ts +85 -0
  33. package/dist/stockpile-grid.js +361 -0
  34. package/dist/stockpile-grid.js.map +1 -0
  35. package/dist/stockpile.d.ts +116 -0
  36. package/dist/stockpile.js +345 -0
  37. package/dist/stockpile.js.map +1 -0
  38. package/dist/storage-rack.d.ts +39 -44
  39. package/dist/storage-rack.js +71 -146
  40. package/dist/storage-rack.js.map +1 -1
  41. package/dist/templates/index.d.ts +80 -0
  42. package/dist/templates/index.js +7 -1
  43. package/dist/templates/index.js.map +1 -1
  44. package/dist/templates/picking-station.d.ts +20 -0
  45. package/dist/templates/picking-station.js +22 -0
  46. package/dist/templates/picking-station.js.map +1 -0
  47. package/dist/templates/stockpile-grid.d.ts +37 -0
  48. package/dist/templates/stockpile-grid.js +38 -0
  49. package/dist/templates/stockpile-grid.js.map +1 -0
  50. package/dist/templates/stockpile.d.ts +29 -0
  51. package/dist/templates/stockpile.js +31 -0
  52. package/dist/templates/stockpile.js.map +1 -0
  53. package/package.json +3 -3
  54. package/src/box.ts +2 -1
  55. package/src/index.ts +14 -0
  56. package/src/pallet.ts +2 -1
  57. package/src/parcel.ts +2 -1
  58. package/src/picking-station-3d.ts +164 -0
  59. package/src/picking-station.ts +220 -0
  60. package/src/rack-capability.ts +26 -0
  61. package/src/rack-grid.ts +24 -108
  62. package/src/spot.ts +15 -1
  63. package/src/stockpile-3d.ts +412 -0
  64. package/src/stockpile-grid-3d.ts +327 -0
  65. package/src/stockpile-grid.ts +408 -0
  66. package/src/stockpile.ts +427 -0
  67. package/src/storage-rack.ts +82 -137
  68. package/src/templates/index.ts +7 -1
  69. package/src/templates/picking-station.ts +23 -0
  70. package/src/templates/stockpile-grid.ts +39 -0
  71. package/src/templates/stockpile.ts +32 -0
  72. package/test/test-rack-capability.ts +51 -0
  73. package/translations/en.json +23 -6
  74. package/translations/ja.json +23 -6
  75. package/translations/ko.json +22 -5
  76. package/translations/ms.json +23 -6
  77. package/translations/zh.json +22 -5
  78. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,186 @@
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, SingleSlotHolder } 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 SingleSlotHolder()(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
+ // SingleSlotHolder hook overrides ───────────────────────────────────────────
36
+ _singleSlotId() { return SLOT_ID; }
37
+ /**
38
+ * SlottedHolder duck (slotIds / hasCarrierAt / canReceiveAt / occupiedSlotIds
39
+ * / emptySlotIds / obtainCarrier / receiveAt / accept / receive / slotTargetAt
40
+ * / getSlotAttachObject3d) — SingleSlotHolder mixin 제공.
41
+ *
42
+ * receiveAt 후 부가 dwell 동작은 `_onCarrierReceived` hook 으로 위임.
43
+ */
44
+ _onCarrierReceived(_carrier, _options) {
45
+ const procMs = this.state.processingTimeMs ?? 0;
46
+ if (procMs <= 0)
47
+ return;
48
+ this.setState({ status: 'processing' });
49
+ setTimeout(() => {
50
+ if (this.state.status === 'processing') {
51
+ this.setState({ status: 'idle' });
52
+ }
53
+ }, procMs);
54
+ }
55
+ // Phase ZT-V PR-6 ProcessStep ─────────────────────────────────────────────
56
+ isProcessStep = true;
57
+ get processingTimeMs() { return this.state.processingTimeMs ?? 0; }
58
+ /** carrier 를 작업대(table) 상단에 안착. */
59
+ attachPointFor(carrier) {
60
+ const ro = this._realObject;
61
+ const frame = ro?.getAttachFrame?.();
62
+ if (!frame)
63
+ return null;
64
+ const carrierDepth = resolveDepth(carrier);
65
+ return { attach: frame, localPosition: { x: 0, y: carrierDepth / 2, z: 0 } };
66
+ }
67
+ // ── 2D render ───────────────────────────────────────────────
68
+ render(ctx) {
69
+ const { left = 0, top = 0, width = 100, height = 100 } = this.state;
70
+ const fillStyle = this.state.fillStyle || '#5a8ab8';
71
+ const strokeStyle = this.state.strokeStyle || '#3d6a8f';
72
+ const status = (this.state.status ?? 'idle');
73
+ // pad
74
+ ctx.save();
75
+ ctx.fillStyle = fillStyle;
76
+ ctx.globalAlpha = 0.15;
77
+ ctx.fillRect(left, top, width, height);
78
+ ctx.restore();
79
+ // outline
80
+ ctx.save();
81
+ ctx.strokeStyle = strokeStyle;
82
+ ctx.lineWidth = 1.5;
83
+ ctx.strokeRect(left + 0.75, top + 0.75, width - 1.5, height - 1.5);
84
+ ctx.restore();
85
+ // 작업대 (가운데 작은 사각)
86
+ const tw = width * 0.55, th = height * 0.45;
87
+ const tx = left + (width - tw) / 2;
88
+ const ty = top + (height - th) / 2;
89
+ ctx.save();
90
+ ctx.fillStyle = strokeStyle;
91
+ ctx.globalAlpha = 0.35;
92
+ ctx.fillRect(tx, ty, tw, th);
93
+ ctx.restore();
94
+ // 상태
95
+ ctx.save();
96
+ const fontSize = Math.min(width, height) * 0.16;
97
+ ctx.fillStyle = '#222';
98
+ ctx.font = `bold ${fontSize}px sans-serif`;
99
+ ctx.textAlign = 'center';
100
+ ctx.textBaseline = 'middle';
101
+ ctx.fillText(status.toUpperCase(), left + width / 2, top + height / 2);
102
+ ctx.restore();
103
+ }
104
+ // ── Popup + click ────────────────────────────────────────────
105
+ get eventMap() {
106
+ return { '(self)': { '(self)': { click: this._onStationClick } } };
107
+ }
108
+ _onStationClick = (mouseEvent) => {
109
+ if (!this.app?.isViewMode)
110
+ return;
111
+ const hit = this._raycastStationHit(mouseEvent);
112
+ if (!hit)
113
+ return;
114
+ this._invokePopup();
115
+ };
116
+ _invokePopup() {
117
+ const popupRefId = this.state.popupRef;
118
+ if (!popupRefId)
119
+ return;
120
+ const popupComp = this.root?.findById?.(popupRefId);
121
+ if (!popupComp || typeof popupComp.openPopup !== 'function')
122
+ return;
123
+ const anchor = this.slotTargetAt(SLOT_ID);
124
+ const carrier = this.obtainCarrier(SLOT_ID);
125
+ popupComp.openPopup({
126
+ componentId: this.state.id,
127
+ status: this.state.status ?? 'idle',
128
+ processingTimeMs: this.state.processingTimeMs,
129
+ currentCarrierId: carrier?.state?.id ?? null
130
+ }, { anchor });
131
+ }
132
+ _raycastStationHit(mouseEvent) {
133
+ const ro = this._realObject;
134
+ if (!ro?.object3d)
135
+ return undefined;
136
+ const tc = ro.threeContainer;
137
+ if (!tc)
138
+ return undefined;
139
+ const cap = tc._threeCapability ?? tc._capability;
140
+ let intersects;
141
+ if (cap?.getObjectsByRaycast)
142
+ intersects = cap.getObjectsByRaycast();
143
+ if (!intersects || intersects.length === 0) {
144
+ const scene = tc.scene3d;
145
+ const renderer = tc.renderer3d;
146
+ const camera = tc.activeCamera3d ??
147
+ cap?.activeCamera ??
148
+ cap?.camera;
149
+ const canvas = renderer?.domElement;
150
+ if (!scene || !canvas || !camera)
151
+ return undefined;
152
+ const rect = canvas.getBoundingClientRect();
153
+ if (rect.width === 0 || rect.height === 0)
154
+ return undefined;
155
+ const ndc = new THREE.Vector2(((mouseEvent.clientX - rect.left) / rect.width) * 2 - 1, -((mouseEvent.clientY - rect.top) / rect.height) * 2 + 1);
156
+ const raycaster = new THREE.Raycaster();
157
+ raycaster.setFromCamera(ndc, camera);
158
+ intersects = raycaster.intersectObjects(scene.children, true);
159
+ }
160
+ if (!intersects || intersects.length === 0)
161
+ return undefined;
162
+ const closest = intersects[0];
163
+ let obj = closest.object;
164
+ while (obj) {
165
+ if (obj.userData?.context === ro)
166
+ return closest;
167
+ obj = obj.parent;
168
+ }
169
+ return undefined;
170
+ }
171
+ buildRealObject() {
172
+ return new PickingStation3D(this);
173
+ }
174
+ };
175
+ PickingStation = __decorate([
176
+ sceneComponent('picking-station')
177
+ ], PickingStation);
178
+ export default PickingStation;
179
+ function resolveDepth(c) {
180
+ const eff = c._realObject?.effectiveDepth;
181
+ if (typeof eff === 'number' && Number.isFinite(eff))
182
+ return eff;
183
+ const d = c?.state?.depth;
184
+ return typeof d === 'number' && Number.isFinite(d) ? d : 0;
185
+ }
186
+ //# 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 SingleSlotHolder,\n type AttachFrame,\n type Alignment,\n type Heights,\n type PlacementArchetype,\n type ProcessStep\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\n extends SingleSlotHolder()(CarrierHolder(Placeable(ContainerAbstract)))\n implements ProcessStep\n{\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 // SingleSlotHolder hook overrides ───────────────────────────────────────────\n _singleSlotId() { return SLOT_ID }\n\n /**\n * SlottedHolder duck (slotIds / hasCarrierAt / canReceiveAt / occupiedSlotIds\n * / emptySlotIds / obtainCarrier / receiveAt / accept / receive / slotTargetAt\n * / getSlotAttachObject3d) — SingleSlotHolder mixin 제공.\n *\n * receiveAt 후 부가 dwell 동작은 `_onCarrierReceived` hook 으로 위임.\n */\n _onCarrierReceived(_carrier: Component, _options?: any) {\n const procMs = this.state.processingTimeMs ?? 0\n if (procMs <= 0) return\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 // Phase ZT-V PR-6 ProcessStep ─────────────────────────────────────────────\n readonly isProcessStep = true as const\n get processingTimeMs() { return this.state.processingTimeMs ?? 0 }\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"]}
@@ -73,6 +73,10 @@ export interface RackGridState extends State {
73
73
  declare const RackGrid_base: any;
74
74
  export default class RackGrid extends RackGrid_base implements SlottedHolder {
75
75
  state: RackGridState;
76
+ _recordToSlotId(record: {
77
+ cellId: string;
78
+ }): string;
79
+ _rebuildVisual(): void;
76
80
  get isObstacle(): boolean;
77
81
  obstacleBoundingBox(): {
78
82
  left: number;
@@ -106,25 +110,6 @@ export default class RackGrid extends RackGrid_base implements SlottedHolder {
106
110
  onchange(after: Properties, _before: Properties): void;
107
111
  /** state.data 변경 시 호출 (things-scene 의 onchange<PropName>). */
108
112
  onchangeData(): void;
109
- /** state.data 의 records — Plan A 의 stock 보관소. */
110
- get records(): Array<{
111
- cellId: string;
112
- [key: string]: any;
113
- }>;
114
- private _legendTarget?;
115
- /**
116
- * Legend 컴포넌트 lookup. 우선순위:
117
- * 1) state.legendTarget id 명시
118
- * 2) scene 전체 의 type='legend' 첫 번째 컴포넌트 (자동 발견)
119
- */
120
- get legendTarget(): Component | undefined;
121
- private _onLegendChanged;
122
- /**
123
- * record 의 legend.field 값을 ranges 와 매칭해 색상 해석.
124
- * - `range.value === recordValue` (카테고리 일치)
125
- * - `range.min ≤ Number(v) < range.max` (수치 범위)
126
- * - 매칭 없으면 `defaultColor` 또는 undefined
127
- */
128
113
  get eventMap(): {
129
114
  '(self)': {
130
115
  '(self)': {
@@ -133,13 +118,10 @@ export default class RackGrid extends RackGrid_base implements SlottedHolder {
133
118
  };
134
119
  };
135
120
  private _onViewClick;
136
- /** state.popupRef Popup 컴포넌트 invoke. anchor = 클릭된 cell 의 SlotTarget. */
137
- private _invokePopup;
138
121
  /** raycast → 우리 RackGrid 의 어떤 mesh 가 closest hit 인지. */
139
122
  private _raycastHit;
140
123
  /** world point → cellId (col-row-shelf) 역산. */
141
124
  private _cellIdFromWorldPoint;
142
- resolveLegendColor(record: any): string | undefined;
143
125
  /**
144
126
  * 새 (rows × columns) 에 맞춰 children 재구성. rack-table.buildCells 정확 클론.
145
127
  */
package/dist/rack-grid.js CHANGED
@@ -32,7 +32,7 @@ import { __decorate } from "tslib";
32
32
  */
33
33
  import { Component, ContainerAbstract, Layout, Model, sceneComponent } from '@hatiolab/things-scene';
34
34
  import * as THREE from 'three';
35
- import { CarrierHolder, Placeable, SlotTarget } from '@operato/scene-base';
35
+ import { CarrierHolder, Placeable, RecordStorage, SlotTarget, componentBoundingBox } from '@operato/scene-base';
36
36
  import { RackGrid3D } from './rack-grid-3d.js';
37
37
  // RackGridCell 의 refid 충돌 회피 — load 시 동적 생성되는 cell 의 refid 가 *부모
38
38
  // RackGrid + 모델 안 다른 컴포넌트* refid 와 겹치지 않도록 *높은 시작값 + monotonic
@@ -156,22 +156,28 @@ const rowControlHandler = {
156
156
  // handler 안 `this` 가 *해당 RackGrid 인스턴스* 를 가리키도록 (화살표 함수가
157
157
  // getter 의 `this` 를 capture). rack-table-cell 의 동일 패턴.
158
158
  // ─── RackGrid ─────────────────────────────────────────────────────────────────
159
- let RackGrid = class RackGrid extends CarrierHolder(Placeable(ContainerAbstract)) {
159
+ let RackGrid = class RackGrid extends RecordStorage()(CarrierHolder(Placeable(ContainerAbstract))) {
160
+ // RecordStorage mixin hook overrides.
161
+ // record.cellId 가 slotId (3-segment '{col}-{row}-{shelf}' format).
162
+ _recordToSlotId(record) {
163
+ return record.cellId ?? '';
164
+ }
165
+ // 3D rebuild — rebuildStockMesh 우선, 없으면 mixin default.
166
+ _rebuildVisual() {
167
+ const ro = this._realObject;
168
+ if (ro?.rebuildStockMesh)
169
+ ro.rebuildStockMesh();
170
+ else if (ro?.update)
171
+ ro.update();
172
+ }
160
173
  // Phase Auto-Nav (AN-PR-2) — Obstacle 자격. AMR / mover 의 자동 path planning
161
174
  // 시 회피 대상. `state.isObstacle` 명시 false 시 override (예외 처리).
162
175
  get isObstacle() {
163
176
  return this.state?.isObstacle !== false;
164
177
  }
165
178
  obstacleBoundingBox() {
166
- const s = this.state;
167
- if (typeof s?.left !== 'number')
168
- return null;
169
- return {
170
- left: s.left, top: s.top,
171
- width: s.width, height: s.height,
172
- y: typeof s.zPos === 'number' ? s.zPos : 0,
173
- zHeight: typeof s.depth === 'number' ? s.depth : 0
174
- };
179
+ // scene-base componentBoundingBox 위임 — rotation 적용된 AABB.
180
+ return componentBoundingBox(this);
175
181
  }
176
182
  static placement = 'floor';
177
183
  static align = 'bottom';
@@ -306,61 +312,7 @@ let RackGrid = class RackGrid extends CarrierHolder(Placeable(ContainerAbstract)
306
312
  ;
307
313
  this._realObject?.rebuildStockMesh?.();
308
314
  }
309
- /** state.data records Plan A stock 보관소. */
310
- get records() {
311
- return this.state.data ?? [];
312
- }
313
- // ── Legend integration ──────────────────────────────────
314
- _legendTarget;
315
- /**
316
- * Legend 컴포넌트 lookup. 우선순위:
317
- * 1) state.legendTarget id 명시
318
- * 2) scene 전체 의 type='legend' 첫 번째 컴포넌트 (자동 발견)
319
- */
320
- get legendTarget() {
321
- if (this._legendTarget)
322
- return this._legendTarget;
323
- const id = this.state.legendTarget;
324
- if (id) {
325
- const found = this.root?.findById?.(id);
326
- if (found) {
327
- this._legendTarget = found;
328
- found.on?.('change', this._onLegendChanged, this);
329
- return found;
330
- }
331
- }
332
- const visit = (node) => {
333
- if (!node)
334
- return undefined;
335
- if (node.state?.type === 'legend')
336
- return node;
337
- const children = node.components;
338
- if (children) {
339
- for (const c of children) {
340
- const r = visit(c);
341
- if (r)
342
- return r;
343
- }
344
- }
345
- return undefined;
346
- };
347
- const found = visit(this.root);
348
- if (found) {
349
- this._legendTarget = found;
350
- found.on?.('change', this._onLegendChanged, this);
351
- }
352
- return found;
353
- }
354
- _onLegendChanged = () => {
355
- ;
356
- this._realObject?.rebuildStockMesh?.();
357
- };
358
- /**
359
- * record 의 legend.field 값을 ranges 와 매칭해 색상 해석.
360
- * - `range.value === recordValue` (카테고리 일치)
361
- * - `range.min ≤ Number(v) < range.max` (수치 범위)
362
- * - 매칭 없으면 `defaultColor` 또는 undefined
363
- */
315
+ // records / legendTarget / _onLegendChanged / resolveLegendColor RecordStorage mixin 제공.
364
316
  // ── View-mode click → rack-grid-cell-click event + popup ──
365
317
  get eventMap() {
366
318
  return {
@@ -402,23 +354,12 @@ let RackGrid = class RackGrid extends CarrierHolder(Placeable(ContainerAbstract)
402
354
  }
403
355
  const payload = { cellId, record, hitPoint: hit.point, instanceId, isStock };
404
356
  this.trigger('rack-grid-cell-click', payload);
405
- this._invokePopup(cellId, record);
357
+ if (cellId)
358
+ this._invokePopup(cellId, record ?? { cellId });
406
359
  // popup 외부 click 으로 인식되어 자동 close 되는 회귀 차단
407
360
  mouseEvent.stopPropagation?.();
408
361
  };
409
- /** state.popupRef Popup 컴포넌트 invoke. anchor = 클릭된 cell 의 SlotTarget. */
410
- _invokePopup(cellId, record) {
411
- const popupRefId = this.state.popupRef;
412
- if (!popupRefId || !cellId)
413
- return;
414
- const popupComp = this.root?.findById?.(popupRefId);
415
- if (!popupComp || typeof popupComp.openPopup !== 'function') {
416
- console.warn(`[rack-grid] popupRef="${popupRefId}" 가 가리키는 컴포넌트 없거나 openPopup 미지원`);
417
- return;
418
- }
419
- const anchor = this.slotTargetAt(cellId);
420
- popupComp.openPopup(record ?? { cellId }, { anchor });
421
- }
362
+ // _invokePopup RecordStorage mixin 제공 (signature 동일: slotId=cellId, payload=record).
422
363
  /** raycast → 우리 RackGrid 의 어떤 mesh 가 closest hit 인지. */
423
364
  _raycastHit(mouseEvent) {
424
365
  const ro = this._realObject;
@@ -489,40 +430,7 @@ let RackGrid = class RackGrid extends CarrierHolder(Placeable(ContainerAbstract)
489
430
  }
490
431
  return `${col}-${row}-${shelf}`;
491
432
  }
492
- resolveLegendColor(record) {
493
- const legend = this.legendTarget;
494
- if (!legend)
495
- return undefined;
496
- const status = legend.getState?.('status') ?? legend.state?.status;
497
- if (!status)
498
- return undefined;
499
- const field = status.field;
500
- const ranges = status.ranges;
501
- if (!field || !Array.isArray(ranges))
502
- return undefined;
503
- const value = record?.[field];
504
- if (value === undefined || value === null)
505
- return status.defaultColor;
506
- for (const range of ranges) {
507
- if (!range)
508
- continue;
509
- if (range.value !== undefined) {
510
- if (range.value === value)
511
- return range.color;
512
- continue;
513
- }
514
- const num = Number(value);
515
- if (!Number.isFinite(num))
516
- continue;
517
- const min = range.min !== undefined && range.min !== '' ? Number(range.min) : undefined;
518
- const max = range.max !== undefined && range.max !== '' ? Number(range.max) : undefined;
519
- const minOk = min === undefined || num >= min;
520
- const maxOk = max === undefined || num < max;
521
- if (minOk && maxOk)
522
- return range.color;
523
- }
524
- return status.defaultColor;
525
- }
433
+ // resolveLegendColor — RecordStorage mixin 제공.
526
434
  /**
527
435
  * 새 (rows × columns) 에 맞춰 children 재구성. rack-table.buildCells 정확 클론.
528
436
  */
@@ -1121,7 +1029,7 @@ let RackGrid = class RackGrid extends CarrierHolder(Placeable(ContainerAbstract)
1121
1029
  return false;
1122
1030
  if (this._carrierChildAt(cellId))
1123
1031
  return true;
1124
- return this.records.some(r => r.cellId === cellId);
1032
+ return this.records.some((r) => r.cellId === cellId);
1125
1033
  }
1126
1034
  /**
1127
1035
  * carrier 를 obtain — 이미 child 면 그대로, 아니면 state.data record 로 transient