@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.
- package/CHANGELOG.md +24 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/picking-station-3d.d.ts +20 -0
- package/dist/picking-station-3d.js +162 -0
- package/dist/picking-station-3d.js.map +1 -0
- package/dist/picking-station.d.ts +56 -0
- package/dist/picking-station.js +212 -0
- package/dist/picking-station.js.map +1 -0
- package/dist/rack-capability.d.ts +11 -0
- package/dist/rack-capability.js +25 -0
- package/dist/rack-capability.js.map +1 -0
- package/dist/rack-grid.js +3 -10
- package/dist/rack-grid.js.map +1 -1
- package/dist/spot.d.ts +19 -1
- package/dist/spot.js +63 -1
- package/dist/spot.js.map +1 -1
- package/dist/stockpile-3d.d.ts +55 -0
- package/dist/stockpile-3d.js +387 -0
- package/dist/stockpile-3d.js.map +1 -0
- package/dist/stockpile-grid-3d.d.ts +30 -0
- package/dist/stockpile-grid-3d.js +301 -0
- package/dist/stockpile-grid-3d.js.map +1 -0
- package/dist/stockpile-grid.d.ts +88 -0
- package/dist/stockpile-grid.js +429 -0
- package/dist/stockpile-grid.js.map +1 -0
- package/dist/stockpile.d.ts +133 -0
- package/dist/stockpile.js +439 -0
- package/dist/stockpile.js.map +1 -0
- package/dist/storage-rack.d.ts +12 -0
- package/dist/storage-rack.js +20 -10
- package/dist/storage-rack.js.map +1 -1
- package/dist/templates/index.d.ts +80 -0
- package/dist/templates/index.js +7 -1
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/picking-station.d.ts +20 -0
- package/dist/templates/picking-station.js +22 -0
- package/dist/templates/picking-station.js.map +1 -0
- package/dist/templates/stockpile-grid.d.ts +37 -0
- package/dist/templates/stockpile-grid.js +38 -0
- package/dist/templates/stockpile-grid.js.map +1 -0
- package/dist/templates/stockpile.d.ts +29 -0
- package/dist/templates/stockpile.js +31 -0
- package/dist/templates/stockpile.js.map +1 -0
- package/package.json +3 -3
- package/src/index.ts +14 -0
- package/src/picking-station-3d.ts +164 -0
- package/src/picking-station.ts +243 -0
- package/src/rack-capability.ts +26 -0
- package/src/rack-grid.ts +3 -8
- package/src/spot.ts +62 -0
- package/src/stockpile-3d.ts +412 -0
- package/src/stockpile-grid-3d.ts +327 -0
- package/src/stockpile-grid.ts +456 -0
- package/src/stockpile.ts +508 -0
- package/src/storage-rack.ts +21 -8
- package/src/templates/index.ts +7 -1
- package/src/templates/picking-station.ts +23 -0
- package/src/templates/stockpile-grid.ts +39 -0
- package/src/templates/stockpile.ts +32 -0
- package/test/test-rack-capability.ts +51 -0
- package/translations/en.json +18 -6
- package/translations/ja.json +18 -6
- package/translations/ko.json +17 -5
- package/translations/ms.json +18 -6
- package/translations/zh.json +17 -5
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Reuse parcel.png as a placeholder icon until a dedicated stockpile-grid icon is drawn.
|
|
2
|
+
const icon = new URL('../../icons/parcel.png', import.meta.url).href;
|
|
3
|
+
export default {
|
|
4
|
+
type: 'stockpile-grid',
|
|
5
|
+
description: 'grid of block/floor storage cells — each cell is a stockpile with its own records, capacity, and preset override',
|
|
6
|
+
group: 'storage',
|
|
7
|
+
icon,
|
|
8
|
+
model: {
|
|
9
|
+
type: 'stockpile-grid',
|
|
10
|
+
top: 200,
|
|
11
|
+
left: 400,
|
|
12
|
+
width: 300,
|
|
13
|
+
height: 200,
|
|
14
|
+
depth: 5,
|
|
15
|
+
rotation: 0,
|
|
16
|
+
fillStyle: '#c89c5c',
|
|
17
|
+
strokeStyle: '#7a5a2e',
|
|
18
|
+
cols: 3,
|
|
19
|
+
rows: 2,
|
|
20
|
+
cellWidth: 100,
|
|
21
|
+
cellHeight: 100,
|
|
22
|
+
stackPattern: 'row',
|
|
23
|
+
carrierPreset: 'box',
|
|
24
|
+
carrierWidth: 30,
|
|
25
|
+
carrierHeight: 30,
|
|
26
|
+
carrierDepth: 22,
|
|
27
|
+
carrierGap: 10,
|
|
28
|
+
capacity: 20,
|
|
29
|
+
pickPolicy: 'lifo',
|
|
30
|
+
// 데모 — 일부 cell 에 records 미리 채워서 끌어 놓자 마자 적치 보이게.
|
|
31
|
+
data: [
|
|
32
|
+
{ col: 0, row: 0, data: [{ id: 'a1' }, { id: 'a2' }] },
|
|
33
|
+
{ col: 1, row: 0, data: [{ id: 'b1' }, { id: 'b2' }, { id: 'b3' }] },
|
|
34
|
+
{ col: 2, row: 1, data: [{ id: 'c1' }] }
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=stockpile-grid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stockpile-grid.js","sourceRoot":"","sources":["../../src/templates/stockpile-grid.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,wBAAwB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAA;AAEpE,eAAe;IACb,IAAI,EAAE,gBAAgB;IACtB,WAAW,EACT,kHAAkH;IACpH,KAAK,EAAE,SAAS;IAChB,IAAI;IACJ,KAAK,EAAE;QACL,IAAI,EAAE,gBAAgB;QACtB,GAAG,EAAE,GAAG;QACR,IAAI,EAAE,GAAG;QACT,KAAK,EAAE,GAAG;QACV,MAAM,EAAE,GAAG;QACX,KAAK,EAAE,CAAC;QACR,QAAQ,EAAE,CAAC;QACX,SAAS,EAAE,SAAS;QACpB,WAAW,EAAE,SAAS;QACtB,IAAI,EAAE,CAAC;QACP,IAAI,EAAE,CAAC;QACP,SAAS,EAAE,GAAG;QACd,UAAU,EAAE,GAAG;QACf,YAAY,EAAE,KAAK;QACnB,aAAa,EAAE,KAAK;QACpB,YAAY,EAAE,EAAE;QAChB,aAAa,EAAE,EAAE;QACjB,YAAY,EAAE,EAAE;QAChB,UAAU,EAAE,EAAE;QACd,QAAQ,EAAE,EAAE;QACZ,UAAU,EAAE,MAAM;QAClB,iDAAiD;QACjD,IAAI,EAAE;YACJ,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE;YACtD,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE;YACpE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE;SACzC;KACF;CACF,CAAA","sourcesContent":["// Reuse parcel.png as a placeholder icon until a dedicated stockpile-grid icon is drawn.\nconst icon = new URL('../../icons/parcel.png', import.meta.url).href\n\nexport default {\n type: 'stockpile-grid',\n description:\n 'grid of block/floor storage cells — each cell is a stockpile with its own records, capacity, and preset override',\n group: 'storage',\n icon,\n model: {\n type: 'stockpile-grid',\n top: 200,\n left: 400,\n width: 300,\n height: 200,\n depth: 5,\n rotation: 0,\n fillStyle: '#c89c5c',\n strokeStyle: '#7a5a2e',\n cols: 3,\n rows: 2,\n cellWidth: 100,\n cellHeight: 100,\n stackPattern: 'row',\n carrierPreset: 'box',\n carrierWidth: 30,\n carrierHeight: 30,\n carrierDepth: 22,\n carrierGap: 10,\n capacity: 20,\n pickPolicy: 'lifo',\n // 데모 — 일부 cell 에 records 미리 채워서 끌어 놓자 마자 적치 보이게.\n data: [\n { col: 0, row: 0, data: [{ id: 'a1' }, { id: 'a2' }] },\n { col: 1, row: 0, data: [{ id: 'b1' }, { id: 'b2' }, { id: 'b3' }] },\n { col: 2, row: 1, data: [{ id: 'c1' }] }\n ]\n }\n}\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
type: string;
|
|
3
|
+
description: string;
|
|
4
|
+
group: string;
|
|
5
|
+
icon: string;
|
|
6
|
+
model: {
|
|
7
|
+
type: string;
|
|
8
|
+
top: number;
|
|
9
|
+
left: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
depth: number;
|
|
13
|
+
rotation: number;
|
|
14
|
+
fillStyle: string;
|
|
15
|
+
strokeStyle: string;
|
|
16
|
+
stackPattern: string;
|
|
17
|
+
carrierPreset: string;
|
|
18
|
+
carrierWidth: number;
|
|
19
|
+
carrierHeight: number;
|
|
20
|
+
carrierDepth: number;
|
|
21
|
+
carrierGap: number;
|
|
22
|
+
capacity: number;
|
|
23
|
+
pickPolicy: string;
|
|
24
|
+
data: {
|
|
25
|
+
id: string;
|
|
26
|
+
}[];
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export default _default;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Reuse parcel.png as a placeholder icon until a dedicated stockpile icon is drawn.
|
|
2
|
+
const icon = new URL('../../icons/parcel.png', import.meta.url).href;
|
|
3
|
+
export default {
|
|
4
|
+
type: 'stockpile',
|
|
5
|
+
description: 'block/floor storage — rectangular footprint with auto-stacked virtual carriers (stackPattern × carrierPreset) driven by state.data records',
|
|
6
|
+
group: 'storage' /* line|shape|textAndMedia|chartAndGauge|table|container|dataSource|3D|facility|storage|conveyance|transport|manufacturing|form|etc */,
|
|
7
|
+
icon,
|
|
8
|
+
model: {
|
|
9
|
+
type: 'stockpile',
|
|
10
|
+
top: 200,
|
|
11
|
+
left: 400,
|
|
12
|
+
width: 200,
|
|
13
|
+
height: 150,
|
|
14
|
+
depth: 5,
|
|
15
|
+
rotation: 0,
|
|
16
|
+
fillStyle: '#c89c5c',
|
|
17
|
+
// 바닥 페인트 라인 의도 — fillStyle 보다 진한 색으로 영역 마킹이 눈에 띄게.
|
|
18
|
+
strokeStyle: '#7a5a2e',
|
|
19
|
+
stackPattern: 'row',
|
|
20
|
+
carrierPreset: 'box',
|
|
21
|
+
carrierWidth: 30,
|
|
22
|
+
carrierHeight: 30,
|
|
23
|
+
carrierDepth: 22,
|
|
24
|
+
carrierGap: 10,
|
|
25
|
+
capacity: 30,
|
|
26
|
+
pickPolicy: 'lifo',
|
|
27
|
+
// 데모용 — 새로 끌어다 놓았을 때 적치된 모습이 즉시 보이도록 record 몇 개.
|
|
28
|
+
data: [{ id: 'p-1' }, { id: 'p-2' }, { id: 'p-3' }]
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=stockpile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stockpile.js","sourceRoot":"","sources":["../../src/templates/stockpile.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,wBAAwB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAA;AAEpE,eAAe;IACb,IAAI,EAAE,WAAW;IACjB,WAAW,EACT,4IAA4I;IAC9I,KAAK,EAAE,SAAS,CAAC,sIAAsI;IACvJ,IAAI;IACJ,KAAK,EAAE;QACL,IAAI,EAAE,WAAW;QACjB,GAAG,EAAE,GAAG;QACR,IAAI,EAAE,GAAG;QACT,KAAK,EAAE,GAAG;QACV,MAAM,EAAE,GAAG;QACX,KAAK,EAAE,CAAC;QACR,QAAQ,EAAE,CAAC;QACX,SAAS,EAAE,SAAS;QACpB,mDAAmD;QACnD,WAAW,EAAE,SAAS;QACtB,YAAY,EAAE,KAAK;QACnB,aAAa,EAAE,KAAK;QACpB,YAAY,EAAE,EAAE;QAChB,aAAa,EAAE,EAAE;QACjB,YAAY,EAAE,EAAE;QAChB,UAAU,EAAE,EAAE;QACd,QAAQ,EAAE,EAAE;QACZ,UAAU,EAAE,MAAM;QAClB,iDAAiD;QACjD,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC;KACpD;CACF,CAAA","sourcesContent":["// Reuse parcel.png as a placeholder icon until a dedicated stockpile icon is drawn.\nconst icon = new URL('../../icons/parcel.png', import.meta.url).href\n\nexport default {\n type: 'stockpile',\n description:\n 'block/floor storage — rectangular footprint with auto-stacked virtual carriers (stackPattern × carrierPreset) driven by state.data records',\n group: 'storage' /* line|shape|textAndMedia|chartAndGauge|table|container|dataSource|3D|facility|storage|conveyance|transport|manufacturing|form|etc */,\n icon,\n model: {\n type: 'stockpile',\n top: 200,\n left: 400,\n width: 200,\n height: 150,\n depth: 5,\n rotation: 0,\n fillStyle: '#c89c5c',\n // 바닥 페인트 라인 의도 — fillStyle 보다 진한 색으로 영역 마킹이 눈에 띄게.\n strokeStyle: '#7a5a2e',\n stackPattern: 'row',\n carrierPreset: 'box',\n carrierWidth: 30,\n carrierHeight: 30,\n carrierDepth: 22,\n carrierGap: 10,\n capacity: 30,\n pickPolicy: 'lifo',\n // 데모용 — 새로 끌어다 놓았을 때 적치된 모습이 즉시 보이도록 record 몇 개.\n data: [{ id: 'p-1' }, { id: 'p-2' }, { id: 'p-3' }]\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@operato/scene-storage",
|
|
3
3
|
"description": "Storage-domain components for things-scene (smart factory / logistics) — pallet, box, parcel; AS/RS and shelves planned.",
|
|
4
4
|
"author": "heartyoh",
|
|
5
|
-
"version": "10.0.0-beta.
|
|
5
|
+
"version": "10.0.0-beta.50",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"module": "dist/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@hatiolab/things-scene": "^10.0.0-beta.1",
|
|
29
|
-
"@operato/scene-base": "^10.0.0-beta.
|
|
29
|
+
"@operato/scene-base": "^10.0.0-beta.50",
|
|
30
30
|
"three": "^0.183.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
@@ -45,5 +45,5 @@
|
|
|
45
45
|
"typescript": "^5.0.4"
|
|
46
46
|
},
|
|
47
47
|
"prettier": "@hatiolab/prettier-config",
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "9578b7fbd4cdde41a9610de591de086e5f231eb1"
|
|
49
49
|
}
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,20 @@ export { Crane3D } from './crane-3d.js'
|
|
|
25
25
|
export { default as Spot } from './spot.js'
|
|
26
26
|
export { Spot3D } from './spot-3d.js'
|
|
27
27
|
|
|
28
|
+
export { default as Stockpile } from './stockpile.js'
|
|
29
|
+
export type {
|
|
30
|
+
StockpileState, StockpileRecord, StackPattern, CarrierPreset, PickPolicy
|
|
31
|
+
} from './stockpile.js'
|
|
32
|
+
export { Stockpile3D } from './stockpile-3d.js'
|
|
33
|
+
|
|
34
|
+
export { default as StockpileGrid } from './stockpile-grid.js'
|
|
35
|
+
export type { StockpileGridState, StockpileGridCell } from './stockpile-grid.js'
|
|
36
|
+
export { StockpileGrid3D } from './stockpile-grid-3d.js'
|
|
37
|
+
|
|
38
|
+
export { default as PickingStation } from './picking-station.js'
|
|
39
|
+
export type { PickingStationState, PickingStationStatus } from './picking-station.js'
|
|
40
|
+
export { PickingStation3D } from './picking-station-3d.js'
|
|
41
|
+
|
|
28
42
|
export { default as GenericContainer } from './generic-container.js'
|
|
29
43
|
export type { ContainerStatus } from './generic-container.js'
|
|
30
44
|
export { GenericContainer3D } from './generic-container-3d.js'
|
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
|
|
9
|
+
import * as THREE from 'three'
|
|
10
|
+
import { RealObjectGroup } from '@hatiolab/things-scene'
|
|
11
|
+
|
|
12
|
+
import type PickingStation from './picking-station.js'
|
|
13
|
+
import type { PickingStationStatus } from './picking-station.js'
|
|
14
|
+
|
|
15
|
+
const PAD_DEPTH = 2
|
|
16
|
+
|
|
17
|
+
const STATUS_TINT: Record<PickingStationStatus, number> = {
|
|
18
|
+
idle: 0x4a7a9b, // 차분한 blue-grey
|
|
19
|
+
processing: 0xd9943a, // 작업 중 — 주황
|
|
20
|
+
busy: 0xc04040 // busy/blocked — 빨강
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class PickingStation3D extends RealObjectGroup {
|
|
24
|
+
private _padMesh?: THREE.Mesh
|
|
25
|
+
private _padOutline?: THREE.LineSegments
|
|
26
|
+
private _tableMesh?: THREE.Mesh
|
|
27
|
+
|
|
28
|
+
build() {
|
|
29
|
+
super.build()
|
|
30
|
+
const state = this.component.state as any
|
|
31
|
+
const w = state.width ?? 100
|
|
32
|
+
const h = state.height ?? 100
|
|
33
|
+
const depth = state.depth ?? 5
|
|
34
|
+
|
|
35
|
+
// ── pad (영역) ─────────────────────────────────────────────
|
|
36
|
+
const padGeom = new THREE.BoxGeometry(w, PAD_DEPTH, h)
|
|
37
|
+
const padMat = new THREE.MeshStandardMaterial({
|
|
38
|
+
color: this._padColor(), roughness: 0.85, transparent: true, opacity: 0.5
|
|
39
|
+
})
|
|
40
|
+
this._padMesh = new THREE.Mesh(padGeom, padMat)
|
|
41
|
+
this._padMesh.position.y = PAD_DEPTH / 2
|
|
42
|
+
this._padMesh.receiveShadow = true
|
|
43
|
+
this.object3d.add(this._padMesh)
|
|
44
|
+
|
|
45
|
+
// pad outline
|
|
46
|
+
const outline = new THREE.LineBasicMaterial({ color: this._strokeColor() })
|
|
47
|
+
this._padOutline = new THREE.LineSegments(new THREE.EdgesGeometry(padGeom), outline)
|
|
48
|
+
this._padOutline.position.y = PAD_DEPTH / 2
|
|
49
|
+
this.object3d.add(this._padOutline)
|
|
50
|
+
|
|
51
|
+
// ── 작업대 (table) — pad 위 가운데 box. top face = carrier 안착면. ──
|
|
52
|
+
const tableW = w * 0.55
|
|
53
|
+
const tableH = h * 0.45
|
|
54
|
+
const tableD = Math.max(8, depth) // 작업면 높이 — operation height 근사
|
|
55
|
+
const tableGeom = new THREE.BoxGeometry(tableW, tableD, tableH)
|
|
56
|
+
const tableMat = new THREE.MeshStandardMaterial({
|
|
57
|
+
color: this._statusColor(), roughness: 0.6, metalness: 0.1
|
|
58
|
+
})
|
|
59
|
+
this._tableMesh = new THREE.Mesh(tableGeom, tableMat)
|
|
60
|
+
this._tableMesh.position.y = PAD_DEPTH + tableD / 2
|
|
61
|
+
this._tableMesh.castShadow = true
|
|
62
|
+
this._tableMesh.receiveShadow = true
|
|
63
|
+
this.object3d.add(this._tableMesh)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** carrier 안착 anchor — 작업대 top face. */
|
|
67
|
+
getAttachFrame(): THREE.Object3D | undefined {
|
|
68
|
+
return this._tableMesh ?? this._padMesh
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private _padColor(): THREE.Color {
|
|
72
|
+
const raw = (this.component.state as any)?.fillStyle
|
|
73
|
+
if (typeof raw === 'string' && raw.length > 0) {
|
|
74
|
+
try { return new THREE.Color(raw) } catch { /* fallthrough */ }
|
|
75
|
+
}
|
|
76
|
+
return new THREE.Color(0x5a8ab8)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private _strokeColor(): THREE.Color {
|
|
80
|
+
const raw = (this.component.state as any)?.strokeStyle
|
|
81
|
+
if (typeof raw === 'string' && raw.length > 0) {
|
|
82
|
+
try { return new THREE.Color(raw) } catch { /* fallthrough */ }
|
|
83
|
+
}
|
|
84
|
+
return this._padColor().clone().multiplyScalar(0.6)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** status 에 따른 작업대 색 — idle/processing/busy. */
|
|
88
|
+
private _statusColor(): THREE.Color {
|
|
89
|
+
const s = (this.component.state as any)?.status as PickingStationStatus | undefined
|
|
90
|
+
return new THREE.Color(STATUS_TINT[s ?? 'idle'] ?? STATUS_TINT.idle)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private _applyPadColor(): void {
|
|
94
|
+
if (this._padMesh) {
|
|
95
|
+
const m = this._padMesh.material as THREE.MeshStandardMaterial
|
|
96
|
+
m.color.copy(this._padColor())
|
|
97
|
+
m.needsUpdate = true
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
private _applyStrokeColor(): void {
|
|
101
|
+
if (this._padOutline) {
|
|
102
|
+
const m = this._padOutline.material as THREE.LineBasicMaterial
|
|
103
|
+
m.color.copy(this._strokeColor())
|
|
104
|
+
m.needsUpdate = true
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
private _applyStatusColor(): void {
|
|
108
|
+
if (this._tableMesh) {
|
|
109
|
+
const m = this._tableMesh.material as THREE.MeshStandardMaterial
|
|
110
|
+
m.color.copy(this._statusColor())
|
|
111
|
+
m.needsUpdate = true
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
updateAlpha(): void {
|
|
116
|
+
const alpha = typeof (this.component.state as any).alpha === 'number'
|
|
117
|
+
? (this.component.state as any).alpha : 1
|
|
118
|
+
if (this._padMesh) {
|
|
119
|
+
const m = this._padMesh.material as THREE.MeshStandardMaterial
|
|
120
|
+
m.opacity = 0.5 * alpha
|
|
121
|
+
m.transparent = m.opacity < 1
|
|
122
|
+
m.needsUpdate = true
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
updateDimension(): void {
|
|
127
|
+
if (this._padMesh) {
|
|
128
|
+
const state = this.component.state as any
|
|
129
|
+
const w = state.width ?? 100
|
|
130
|
+
const h = state.height ?? 100
|
|
131
|
+
const depth = state.depth ?? 5
|
|
132
|
+
this._padMesh.geometry.dispose()
|
|
133
|
+
this._padMesh.geometry = new THREE.BoxGeometry(w, PAD_DEPTH, h)
|
|
134
|
+
if (this._padOutline) {
|
|
135
|
+
this._padOutline.geometry.dispose()
|
|
136
|
+
this._padOutline.geometry = new THREE.EdgesGeometry(this._padMesh.geometry)
|
|
137
|
+
}
|
|
138
|
+
if (this._tableMesh) {
|
|
139
|
+
const tableW = w * 0.55
|
|
140
|
+
const tableH = h * 0.45
|
|
141
|
+
const tableD = Math.max(8, depth)
|
|
142
|
+
this._tableMesh.geometry.dispose()
|
|
143
|
+
this._tableMesh.geometry = new THREE.BoxGeometry(tableW, tableD, tableH)
|
|
144
|
+
this._tableMesh.position.y = PAD_DEPTH + tableD / 2
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>): void {
|
|
150
|
+
if ('width' in after || 'height' in after || 'depth' in after) {
|
|
151
|
+
this.updateDimension()
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
if ('alpha' in after) { this.updateAlpha(); return }
|
|
155
|
+
if ('fillStyle' in after) {
|
|
156
|
+
this._applyPadColor()
|
|
157
|
+
if ((this.component.state as any).strokeStyle == null) this._applyStrokeColor()
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
if ('strokeStyle' in after) { this._applyStrokeColor(); return }
|
|
161
|
+
if ('status' in after) { this._applyStatusColor(); return }
|
|
162
|
+
super.onchange?.(after, before)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
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
|
+
|
|
11
|
+
import * as THREE from 'three'
|
|
12
|
+
import { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'
|
|
13
|
+
import type { State, Material3D } from '@hatiolab/things-scene'
|
|
14
|
+
import {
|
|
15
|
+
CarrierHolder,
|
|
16
|
+
Placeable,
|
|
17
|
+
SlotTarget,
|
|
18
|
+
type AttachFrame,
|
|
19
|
+
type Alignment,
|
|
20
|
+
type Heights,
|
|
21
|
+
type PlacementArchetype
|
|
22
|
+
} from '@operato/scene-base'
|
|
23
|
+
|
|
24
|
+
import { PickingStation3D } from './picking-station-3d.js'
|
|
25
|
+
|
|
26
|
+
export type PickingStationStatus = 'idle' | 'processing' | 'busy'
|
|
27
|
+
|
|
28
|
+
export interface PickingStationState extends State {
|
|
29
|
+
/** carrier 가 머무는 처리 시간 (ms). 0/미설정이면 즉시 idle 유지. */
|
|
30
|
+
processingTimeMs?: number
|
|
31
|
+
/** 현재 상태 (자동). */
|
|
32
|
+
status?: PickingStationStatus
|
|
33
|
+
/** click 시 invoke 할 Popup 컴포넌트 id. */
|
|
34
|
+
popupRef?: string
|
|
35
|
+
material3d?: Material3D
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SLOT_ID = 'station'
|
|
39
|
+
|
|
40
|
+
const NATURE: ComponentNature = {
|
|
41
|
+
mutable: false,
|
|
42
|
+
resizable: true,
|
|
43
|
+
rotatable: true,
|
|
44
|
+
properties: [
|
|
45
|
+
{ type: 'number', label: 'processing-time-ms', name: 'processingTimeMs' },
|
|
46
|
+
{ type: 'select', label: 'status', name: 'status',
|
|
47
|
+
property: { options: ['idle', 'processing', 'busy'] } },
|
|
48
|
+
{ type: 'id-input', label: 'popup-ref', name: 'popupRef',
|
|
49
|
+
property: { component: 'popup' } }
|
|
50
|
+
],
|
|
51
|
+
help: 'scene/component/picking-station'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@sceneComponent('picking-station')
|
|
55
|
+
export default class PickingStation extends CarrierHolder(Placeable(ContainerAbstract)) {
|
|
56
|
+
declare state: PickingStationState
|
|
57
|
+
declare _realObject?: PickingStation3D
|
|
58
|
+
|
|
59
|
+
static placement: PlacementArchetype = 'floor'
|
|
60
|
+
static align: Alignment = 'bottom'
|
|
61
|
+
static defaultDepth = (h: Heights) => h.operation - h.floor
|
|
62
|
+
|
|
63
|
+
get nature(): ComponentNature { return NATURE }
|
|
64
|
+
get anchors() { return [] }
|
|
65
|
+
|
|
66
|
+
// ── SlottedHolder duck-type (단일 slot 'station') ─────────────
|
|
67
|
+
slotIds(): ReadonlyArray<string> { return [SLOT_ID] }
|
|
68
|
+
|
|
69
|
+
hasCarrierAt(slotId: string): boolean {
|
|
70
|
+
return slotId === SLOT_ID &&
|
|
71
|
+
((this as any).components ?? []).some((c: any) => c?.isCarriable)
|
|
72
|
+
}
|
|
73
|
+
canReceiveAt(slotId: string, _carrier?: Component): boolean {
|
|
74
|
+
return slotId === SLOT_ID && !this.hasCarrierAt(slotId)
|
|
75
|
+
}
|
|
76
|
+
occupiedSlotIds(): ReadonlyArray<string> {
|
|
77
|
+
return this.hasCarrierAt(SLOT_ID) ? [SLOT_ID] : []
|
|
78
|
+
}
|
|
79
|
+
emptySlotIds(): ReadonlyArray<string> {
|
|
80
|
+
return this.hasCarrierAt(SLOT_ID) ? [] : [SLOT_ID]
|
|
81
|
+
}
|
|
82
|
+
obtainCarrier(slotId: string): Component | null {
|
|
83
|
+
if (slotId !== SLOT_ID) return null
|
|
84
|
+
return ((this as any).components ?? []).find((c: any) => c?.isCarriable) ?? null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* carrier 도착 — components + 3D 로 reparent (Spot 패턴). processingTimeMs > 0
|
|
89
|
+
* 이면 status='processing' 으로 전환했다가 timer 후 'idle' 로 복귀.
|
|
90
|
+
*
|
|
91
|
+
* 단순 setTimeout — frameClock 통합(view-mode/일시정지 동기) 은 추후.
|
|
92
|
+
*/
|
|
93
|
+
async receiveAt(_slotId: string, carrier: Component, options?: any): Promise<void> {
|
|
94
|
+
;(this as any).reparent?.(carrier, { ...(options ?? {}), animated: false })
|
|
95
|
+
const procMs = this.state.processingTimeMs ?? 0
|
|
96
|
+
if (procMs > 0) {
|
|
97
|
+
this.setState({ status: 'processing' as PickingStationStatus })
|
|
98
|
+
setTimeout(() => {
|
|
99
|
+
if ((this.state.status as PickingStationStatus) === 'processing') {
|
|
100
|
+
this.setState({ status: 'idle' as PickingStationStatus })
|
|
101
|
+
}
|
|
102
|
+
}, procMs)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async accept(carrier: Component, options?: any): Promise<void> {
|
|
107
|
+
return this.receiveAt(SLOT_ID, carrier, options)
|
|
108
|
+
}
|
|
109
|
+
async receive(carrier: Component, options?: any): Promise<void> {
|
|
110
|
+
return this.receiveAt(SLOT_ID, carrier, options)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
slotTargetAt(slotId: string): SlotTarget { return new SlotTarget(this as any, slotId) }
|
|
114
|
+
getSlotAttachObject3d(_slotId: string): any {
|
|
115
|
+
return (this as any)._realObject?.getAttachFrame?.()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** carrier 를 작업대(table) 상단에 안착. */
|
|
119
|
+
attachPointFor(carrier: Component): AttachFrame | null {
|
|
120
|
+
const ro = this._realObject
|
|
121
|
+
const frame = ro?.getAttachFrame?.()
|
|
122
|
+
if (!frame) return null
|
|
123
|
+
const carrierDepth = resolveDepth(carrier)
|
|
124
|
+
return { attach: frame, localPosition: { x: 0, y: carrierDepth / 2, z: 0 } }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── 2D render ───────────────────────────────────────────────
|
|
128
|
+
render(ctx: CanvasRenderingContext2D) {
|
|
129
|
+
const { left = 0, top = 0, width = 100, height = 100 } = this.state
|
|
130
|
+
const fillStyle = (this.state.fillStyle as string) || '#5a8ab8'
|
|
131
|
+
const strokeStyle = (this.state.strokeStyle as string) || '#3d6a8f'
|
|
132
|
+
const status = (this.state.status ?? 'idle') as PickingStationStatus
|
|
133
|
+
|
|
134
|
+
// pad
|
|
135
|
+
ctx.save()
|
|
136
|
+
ctx.fillStyle = fillStyle
|
|
137
|
+
ctx.globalAlpha = 0.15
|
|
138
|
+
ctx.fillRect(left, top, width, height)
|
|
139
|
+
ctx.restore()
|
|
140
|
+
|
|
141
|
+
// outline
|
|
142
|
+
ctx.save()
|
|
143
|
+
ctx.strokeStyle = strokeStyle
|
|
144
|
+
ctx.lineWidth = 1.5
|
|
145
|
+
ctx.strokeRect(left + 0.75, top + 0.75, width - 1.5, height - 1.5)
|
|
146
|
+
ctx.restore()
|
|
147
|
+
|
|
148
|
+
// 작업대 (가운데 작은 사각)
|
|
149
|
+
const tw = width * 0.55, th = height * 0.45
|
|
150
|
+
const tx = left + (width - tw) / 2
|
|
151
|
+
const ty = top + (height - th) / 2
|
|
152
|
+
ctx.save()
|
|
153
|
+
ctx.fillStyle = strokeStyle
|
|
154
|
+
ctx.globalAlpha = 0.35
|
|
155
|
+
ctx.fillRect(tx, ty, tw, th)
|
|
156
|
+
ctx.restore()
|
|
157
|
+
|
|
158
|
+
// 상태
|
|
159
|
+
ctx.save()
|
|
160
|
+
const fontSize = Math.min(width, height) * 0.16
|
|
161
|
+
ctx.fillStyle = '#222'
|
|
162
|
+
ctx.font = `bold ${fontSize}px sans-serif`
|
|
163
|
+
ctx.textAlign = 'center'
|
|
164
|
+
ctx.textBaseline = 'middle'
|
|
165
|
+
ctx.fillText(status.toUpperCase(), left + width / 2, top + height / 2)
|
|
166
|
+
ctx.restore()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Popup + click ────────────────────────────────────────────
|
|
170
|
+
get eventMap() {
|
|
171
|
+
return { '(self)': { '(self)': { click: this._onStationClick } } }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private _onStationClick = (mouseEvent: MouseEvent) => {
|
|
175
|
+
if (!(this as any).app?.isViewMode) return
|
|
176
|
+
const hit = this._raycastStationHit(mouseEvent)
|
|
177
|
+
if (!hit) return
|
|
178
|
+
this._invokePopup()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private _invokePopup(): void {
|
|
182
|
+
const popupRefId = this.state.popupRef
|
|
183
|
+
if (!popupRefId) return
|
|
184
|
+
const popupComp: any = (this as any).root?.findById?.(popupRefId)
|
|
185
|
+
if (!popupComp || typeof popupComp.openPopup !== 'function') return
|
|
186
|
+
const anchor = this.slotTargetAt(SLOT_ID)
|
|
187
|
+
const carrier = this.obtainCarrier(SLOT_ID)
|
|
188
|
+
popupComp.openPopup({
|
|
189
|
+
componentId: (this.state as any).id,
|
|
190
|
+
status: this.state.status ?? 'idle',
|
|
191
|
+
processingTimeMs: this.state.processingTimeMs,
|
|
192
|
+
currentCarrierId: (carrier as any)?.state?.id ?? null
|
|
193
|
+
}, { anchor })
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private _raycastStationHit(mouseEvent: MouseEvent): THREE.Intersection | undefined {
|
|
197
|
+
const ro: any = (this as any)._realObject
|
|
198
|
+
if (!ro?.object3d) return undefined
|
|
199
|
+
const tc: any = ro.threeContainer
|
|
200
|
+
if (!tc) return undefined
|
|
201
|
+
const cap: any = tc._threeCapability ?? tc._capability
|
|
202
|
+
let intersects: THREE.Intersection[] | undefined
|
|
203
|
+
if (cap?.getObjectsByRaycast) intersects = cap.getObjectsByRaycast() as THREE.Intersection[] | undefined
|
|
204
|
+
if (!intersects || intersects.length === 0) {
|
|
205
|
+
const scene = tc.scene3d as THREE.Scene | undefined
|
|
206
|
+
const renderer = tc.renderer3d as THREE.WebGLRenderer | undefined
|
|
207
|
+
const camera =
|
|
208
|
+
(tc.activeCamera3d as THREE.Camera | undefined) ??
|
|
209
|
+
(cap?.activeCamera as THREE.Camera | undefined) ??
|
|
210
|
+
(cap?.camera as THREE.Camera | undefined)
|
|
211
|
+
const canvas = renderer?.domElement
|
|
212
|
+
if (!scene || !canvas || !camera) return undefined
|
|
213
|
+
const rect = canvas.getBoundingClientRect()
|
|
214
|
+
if (rect.width === 0 || rect.height === 0) return undefined
|
|
215
|
+
const ndc = new THREE.Vector2(
|
|
216
|
+
((mouseEvent.clientX - rect.left) / rect.width) * 2 - 1,
|
|
217
|
+
-((mouseEvent.clientY - rect.top) / rect.height) * 2 + 1
|
|
218
|
+
)
|
|
219
|
+
const raycaster = new THREE.Raycaster()
|
|
220
|
+
raycaster.setFromCamera(ndc, camera)
|
|
221
|
+
intersects = raycaster.intersectObjects(scene.children, true)
|
|
222
|
+
}
|
|
223
|
+
if (!intersects || intersects.length === 0) return undefined
|
|
224
|
+
const closest = intersects[0]
|
|
225
|
+
let obj: THREE.Object3D | null = closest.object
|
|
226
|
+
while (obj) {
|
|
227
|
+
if (obj.userData?.context === ro) return closest
|
|
228
|
+
obj = obj.parent
|
|
229
|
+
}
|
|
230
|
+
return undefined
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
buildRealObject(): RealObject | undefined {
|
|
234
|
+
return new PickingStation3D(this)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function resolveDepth(c: Component): number {
|
|
239
|
+
const eff = (c as any)._realObject?.effectiveDepth
|
|
240
|
+
if (typeof eff === 'number' && Number.isFinite(eff)) return eff
|
|
241
|
+
const d = (c as any)?.state?.depth
|
|
242
|
+
return typeof d === 'number' && Number.isFinite(d) ? d : 0
|
|
243
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* rack 이 *_이 mover 의 toolType_* 을 선반 적재용으로 수용하는가.
|
|
11
|
+
*
|
|
12
|
+
* rack 선반 적재는 높이 도달 mover (crane / stacker / forklift) 의 몫. 평탄 데크
|
|
13
|
+
* 차량(agv-deck)은 바닥 운반 전용 — 선반 직접 적재 불가 → 거부. 거부된 mover 는
|
|
14
|
+
* transfer planner 가 자동으로 in-port 경유(환승)를 택하게 만든다.
|
|
15
|
+
*
|
|
16
|
+
* @param moverToolType 적재하려는 mover 의 toolType (undefined 면 능력 미상 → 허용)
|
|
17
|
+
* @param blockedTools 거부 toolType 목록 (default ['agv-deck'])
|
|
18
|
+
*/
|
|
19
|
+
export function rackAcceptsMoverTool(
|
|
20
|
+
moverToolType: string | undefined | null,
|
|
21
|
+
blockedTools: readonly string[] = ['agv-deck']
|
|
22
|
+
): boolean {
|
|
23
|
+
if (moverToolType == null) return true
|
|
24
|
+
if (!Array.isArray(blockedTools)) return true
|
|
25
|
+
return !blockedTools.includes(moverToolType)
|
|
26
|
+
}
|
package/src/rack-grid.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
CarrierHolder,
|
|
40
40
|
Placeable,
|
|
41
41
|
SlotTarget,
|
|
42
|
+
componentBoundingBox,
|
|
42
43
|
type AttachFrame,
|
|
43
44
|
type Alignment,
|
|
44
45
|
type Heights,
|
|
@@ -278,14 +279,8 @@ export default class RackGrid
|
|
|
278
279
|
return (this.state as any)?.isObstacle !== false
|
|
279
280
|
}
|
|
280
281
|
obstacleBoundingBox(): { left: number; top: number; width: number; height: number; y?: number; zHeight?: number } | null {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return {
|
|
284
|
-
left: s.left, top: s.top,
|
|
285
|
-
width: s.width, height: s.height,
|
|
286
|
-
y: typeof s.zPos === 'number' ? s.zPos : 0,
|
|
287
|
-
zHeight: typeof s.depth === 'number' ? s.depth : 0
|
|
288
|
-
}
|
|
282
|
+
// scene-base 의 componentBoundingBox 위임 — rotation 적용된 AABB.
|
|
283
|
+
return componentBoundingBox(this)
|
|
289
284
|
}
|
|
290
285
|
|
|
291
286
|
static placement: PlacementArchetype = 'floor'
|