@operato/scene-storage 10.0.0-beta.50 → 10.0.0-beta.54
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 +29 -0
- package/dist/box.js +8 -2
- package/dist/box.js.map +1 -1
- package/dist/pallet.js +2 -2
- package/dist/pallet.js.map +1 -1
- package/dist/parcel.js +8 -2
- package/dist/parcel.js.map +1 -1
- package/dist/picking-station.d.ts +10 -16
- package/dist/picking-station.js +20 -46
- package/dist/picking-station.js.map +1 -1
- package/dist/rack-grid.d.ts +4 -22
- package/dist/rack-grid.js +21 -106
- package/dist/rack-grid.js.map +1 -1
- package/dist/spot.d.ts +2 -19
- package/dist/spot.js +4 -62
- package/dist/spot.js.map +1 -1
- package/dist/stockpile-grid.d.ts +2 -5
- package/dist/stockpile-grid.js +18 -86
- package/dist/stockpile-grid.js.map +1 -1
- package/dist/stockpile.d.ts +5 -22
- package/dist/stockpile.js +21 -115
- package/dist/stockpile.js.map +1 -1
- package/dist/storage-rack.d.ts +27 -44
- package/dist/storage-rack.js +52 -137
- package/dist/storage-rack.js.map +1 -1
- package/package.json +3 -3
- package/src/box.ts +8 -1
- package/src/pallet.ts +2 -1
- package/src/parcel.ts +8 -1
- package/src/picking-station.ts +26 -49
- package/src/rack-grid.ts +21 -100
- package/src/spot.ts +11 -59
- package/src/stockpile-grid.ts +21 -69
- package/src/stockpile.ts +23 -104
- package/src/storage-rack.ts +61 -129
- package/translations/en.json +7 -1
- package/translations/ja.json +7 -1
- package/translations/ko.json +7 -1
- package/translations/ms.json +7 -1
- package/translations/zh.json +7 -1
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,35 @@
|
|
|
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.54](https://github.com/things-scene/operato-scene/compare/v10.0.0-beta.53...v10.0.0-beta.54) (2026-06-05)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### :rocket: New Features
|
|
10
|
+
|
|
11
|
+
* **carrier,sortation:** carrier.state.data 도 sortation rule 의 lookup 대상 ([931a6ac](https://github.com/things-scene/operato-scene/commit/931a6acc6fae8e80122da949dcb244a2430e442f))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## [10.0.0-beta.53](https://github.com/things-scene/operato-scene/compare/v10.0.0-beta.52...v10.0.0-beta.53) (2026-06-03)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### :rocket: New Features
|
|
19
|
+
|
|
20
|
+
* **scene-base,storage:** Phase ZT-V PR-3a — RecordStorage mixin + Stockpile 마이그 ([e83e054](https://github.com/things-scene/operato-scene/commit/e83e054170d0526c3a8d7290c9ccedd75861149c))
|
|
21
|
+
* **scene-base,storage:** Phase ZT-V PR-9 — SingleSlotHolder mixin + Spot/PickingStation 마이그 ([cb11c4e](https://github.com/things-scene/operato-scene/commit/cb11c4e7dc97a671b63fabd4aef518d0aa215a61))
|
|
22
|
+
* **scene-base:** Phase ZT-V PR-1 — Identity + CapabilityRegistry ([3353b99](https://github.com/things-scene/operato-scene/commit/3353b99e28f80e6a32eb072af3adbd9dad36a2cf))
|
|
23
|
+
* **scene:** dock-door pure gate + trailer-container + forklift home/smooth ([e7302ca](https://github.com/things-scene/operato-scene/commit/e7302ca425209b7870b7d6d2d94c89ad67a25a28))
|
|
24
|
+
* **storage,scene-base:** Step 1+2 — StorageRack cell-precise approach + fromPos 전달 ([e79b266](https://github.com/things-scene/operato-scene/commit/e79b26633a69545de4b75e3519d20fcb2ed5d6ed))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### :house: Code Refactoring
|
|
28
|
+
|
|
29
|
+
* **storage:** Phase ZT-V PR-3b — StockpileGrid 의 RecordStorage 마이그 ([7257b83](https://github.com/things-scene/operato-scene/commit/7257b83f517173418c86762de33f6585119bd866))
|
|
30
|
+
* **storage:** Phase ZT-V PR-3c — StorageRack 의 RecordStorage 마이그 ([27b930e](https://github.com/things-scene/operato-scene/commit/27b930e0cd50806cf6db55435d300febdaf93158))
|
|
31
|
+
* **storage:** Phase ZT-V PR-3d — RackGrid 의 RecordStorage 마이그 ([5055129](https://github.com/things-scene/operato-scene/commit/505512970ee2a9eec2d836b2b2c2539961e64056))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
6
35
|
## [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
36
|
|
|
8
37
|
|
package/dist/box.js
CHANGED
|
@@ -3,7 +3,7 @@ import { __decorate } from "tslib";
|
|
|
3
3
|
* Copyright © HatioLab Inc. All rights reserved.
|
|
4
4
|
*/
|
|
5
5
|
import { RectPath, Shape, topApproachFrame, getWorldPose, sceneComponent } from '@hatiolab/things-scene';
|
|
6
|
-
import { Carriable, Legendable, Placeable } from '@operato/scene-base';
|
|
6
|
+
import { Carriable, Identifiable, Legendable, Placeable } from '@operato/scene-base';
|
|
7
7
|
import { Box3D } from './box-3d.js';
|
|
8
8
|
const BODY_LEGEND = {
|
|
9
9
|
wood: '#a87644',
|
|
@@ -25,6 +25,12 @@ const NATURE = {
|
|
|
25
25
|
{ display: 'Plastic', value: 'plastic' }
|
|
26
26
|
]
|
|
27
27
|
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
type: 'textarea',
|
|
31
|
+
label: 'data (JSON)',
|
|
32
|
+
name: 'data',
|
|
33
|
+
placeholder: '{ "sku": "P-1234", "dest": "zone-A", "priority": 1 }'
|
|
28
34
|
}
|
|
29
35
|
],
|
|
30
36
|
help: 'scene/component/box'
|
|
@@ -39,7 +45,7 @@ const NATURE = {
|
|
|
39
45
|
* logistics visualization (a *case* of items inside a box is data, not
|
|
40
46
|
* scene-tree). If a future use case needs nested boxes, extend Container.
|
|
41
47
|
*/
|
|
42
|
-
let Box = class Box extends Carriable(Legendable(Placeable(RectPath(Shape)))) {
|
|
48
|
+
let Box = class Box extends Identifiable(Carriable(Legendable(Placeable(RectPath(Shape))))) {
|
|
43
49
|
static legends = {
|
|
44
50
|
bodyColor: { from: 'material', legend: BODY_LEGEND }
|
|
45
51
|
};
|
package/dist/box.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"box.js","sourceRoot":"","sources":["../src/box.ts"],"names":[],"mappings":";AAAA;;GAEG;AACH,OAAO,EAGL,QAAQ,EACR,KAAK,EACL,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACf,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EACL,SAAS,EACT,UAAU,EACV,SAAS,EAIV,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAwBnC,MAAM,WAAW,GAAG;IAClB,IAAI,EAAE,SAAS;IACf,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;CACnB,CAAA;AAED,MAAM,MAAM,GAAoB;IAC9B,OAAO,EAAE,KAAK;IACd,SAAS,EAAE,IAAI;IACf,SAAS,EAAE,IAAI;IACf,UAAU,EAAE;QACV;YACE,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,UAAU;YACjB,IAAI,EAAE,UAAU;YAChB,QAAQ,EAAE;gBACR,OAAO,EAAE;oBACP,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE;oBAClC,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE;iBACzC;aACF;SACF;KACF;IACD,IAAI,EAAE,qBAAqB;CAC5B,CAAA;AAED,6EAA6E;AAC7E,kDAAkD;AAClD;;;;;;;GAOG;AAEY,IAAM,GAAG,GAAT,MAAM,GAAI,SAAQ,SAAS,CAAC,UAAU,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"box.js","sourceRoot":"","sources":["../src/box.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\nimport {\n ComponentNature,\n RealObject,\n RectPath,\n Shape,\n topApproachFrame,\n getWorldPose,\n sceneComponent\n} from '@hatiolab/things-scene'\nimport type { State, Material3D, PickupFrame, PoseSerialized } from '@hatiolab/things-scene'\nimport {\n Carriable,\n Identifiable,\n Legendable,\n Placeable,\n type Alignment,\n type LegendBinding,\n type PlacementArchetype\n} from '@operato/scene-base'\n\nimport { Box3D } from './box-3d.js'\n\n/**\n * Box material — drives 3D structure and color.\n *\n * - `wood` — wood crate: visible vertical slats, gaps between, open or\n * semi-open top. Used for heavy / industrial parts.\n * - `plastic` — plastic tote / bin: solid molded walls with stackable lip\n * at top. Used for fulfillment, parts kitting.\n *\n * Cardboard parcels are a separate component (see `parcel.ts`) — they have\n * different proportions, taping, and labels that warrant a distinct class.\n */\nexport type BoxMaterial = 'wood' | 'plastic'\n\n/** Box 컴포넌트 state */\nexport interface BoxState extends State {\n // ── 외관 ──\n material?: BoxMaterial\n\n // ── 3D 재질 ──\n material3d?: Material3D\n}\n\nconst BODY_LEGEND = {\n wood: '#a87644',\n plastic: '#3a5078',\n default: '#a87644'\n}\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [\n {\n type: 'select',\n label: 'material',\n name: 'material',\n property: {\n options: [\n { display: 'Wood', value: 'wood' },\n { display: 'Plastic', value: 'plastic' }\n ]\n }\n },\n {\n type: 'textarea',\n label: 'data (JSON)',\n name: 'data',\n placeholder: '{ \"sku\": \"P-1234\", \"dest\": \"zone-A\", \"priority\": 1 }'\n }\n ],\n help: 'scene/component/box'\n}\n\n// Carriable: a box can be a child of any CarrierHolder (Pallet for stacking,\n// AGV deck, robot-arm gripper, Spot for staging).\n/**\n * Box — a generic stackable container for goods. Wood crate or plastic tote\n * variants distinguished by `material` prop.\n *\n * Shape-based (not Container) — boxes nesting other components is rare in\n * logistics visualization (a *case* of items inside a box is data, not\n * scene-tree). If a future use case needs nested boxes, extend Container.\n */\n@sceneComponent('box')\nexport default class Box extends Identifiable(Carriable(Legendable(Placeable(RectPath(Shape))))) {\n declare state: BoxState\n\n static legends: Record<string, LegendBinding> = {\n bodyColor: { from: 'material', legend: BODY_LEGEND }\n }\n\n static placement: PlacementArchetype = 'operation'\n static align: Alignment = 'bottom'\n static defaultDepth = 300\n\n get nature() {\n return NATURE\n }\n\n get anchors() {\n return []\n }\n\n /** 2D — top-down rectangle. */\n render(ctx: CanvasRenderingContext2D) {\n const { width, height, left, top } = this.state\n ctx.beginPath()\n ctx.rect(left, top, width, height)\n }\n\n get fillStyle() {\n return (this.state.bodyColor as string) || '#a87644'\n }\n\n buildRealObject(): RealObject | undefined {\n return new Box3D(this)\n }\n\n /**\n * Phase H — pickup contract. Box 는 위에서 gripper / vacuum cup 으로 집기 —\n * 단일 entry (top center). Box 의 dimensions 가 작아서 forklift fork 보다는\n * gripper 가 일반적. forklift 로 들어올릴 box 는 통상 pallet 위에 stacking\n * 후 pallet 째로 운반.\n *\n * tolerance 가 pallet 보다 빡빡 (gripper 정밀도 vs forklift pocket 폭).\n */\n pickupFrames(): PickupFrame[] {\n const wp = getWorldPose(this)\n const me: PoseSerialized = {\n position: { x: wp.position.x, y: wp.position.y, z: wp.position.z },\n rotation: { x: wp.rotation.x, y: wp.rotation.y, z: wp.rotation.z, w: wp.rotation.w }\n }\n const boxDepth = (this.constructor as any).defaultDepth ?? 300\n\n return [\n topApproachFrame({\n carrierWorld: me,\n topY: boxDepth, // Box top in carrier-local Y (depth = full height; top at depth)\n approachDistance: 50, // gripper 가 hover 하는 거리\n toolType: 'gripper',\n tolerance: { positionMm: 5, angleDeg: 1 },\n priority: 0,\n id: 'top-gripper'\n }),\n topApproachFrame({\n carrierWorld: me,\n topY: boxDepth,\n approachDistance: 40,\n toolType: 'agv-deck',\n tolerance: { positionMm: 15, angleDeg: 3 },\n priority: 1,\n id: 'top-deck'\n }),\n topApproachFrame({\n carrierWorld: me,\n topY: boxDepth,\n approachDistance: 80, // crane fork hover\n toolType: 'forklift-fork',\n tolerance: { positionMm: 25, angleDeg: 4 },\n priority: 2,\n id: 'top-fork'\n })\n ]\n }\n}\n"]}
|
package/dist/pallet.js
CHANGED
|
@@ -3,7 +3,7 @@ import { __decorate } from "tslib";
|
|
|
3
3
|
* Copyright © HatioLab Inc. All rights reserved.
|
|
4
4
|
*/
|
|
5
5
|
import { ContainerAbstract, rectangularFootprintFrames, topApproachFrame, getWorldPose, sceneComponent } from '@hatiolab/things-scene';
|
|
6
|
-
import { Carriable, Legendable, Placeable } from '@operato/scene-base';
|
|
6
|
+
import { Carriable, Identifiable, Legendable, Placeable } from '@operato/scene-base';
|
|
7
7
|
import { Pallet3D } from './pallet-3d.js';
|
|
8
8
|
const BODY_LEGEND = {
|
|
9
9
|
wood: '#a87644',
|
|
@@ -69,7 +69,7 @@ const NATURE = {
|
|
|
69
69
|
* (machine-3d.ts:113-122). v1 accepts the visual overlap; v2 will add the
|
|
70
70
|
* detection.
|
|
71
71
|
*/
|
|
72
|
-
let Pallet = class Pallet extends Carriable(Legendable(Placeable(ContainerAbstract))) {
|
|
72
|
+
let Pallet = class Pallet extends Identifiable(Carriable(Legendable(Placeable(ContainerAbstract)))) {
|
|
73
73
|
static legends = {
|
|
74
74
|
bodyColor: { from: 'material', legend: BODY_LEGEND }
|
|
75
75
|
};
|
package/dist/pallet.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pallet.js","sourceRoot":"","sources":["../src/pallet.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\nimport {\n Component,\n ComponentNature,\n ContainerAbstract,\n RealObject,\n Pose6DOF,\n rectangularFootprintFrames,\n topApproachFrame,\n getWorldPose,\n sceneComponent\n} from '@hatiolab/things-scene'\nimport type { State, Material3D, PickupFrame, PoseSerialized } from '@hatiolab/things-scene'\nimport {\n Carriable,\n Legendable,\n Placeable,\n type Alignment,\n type LegendBinding,\n type PlacementArchetype\n} from '@operato/scene-base'\n\nimport { Pallet3D } from './pallet-3d.js'\n\n/**\n * Pallet material — drives both 2D fill color and 3D structure.\n *\n * - `wood` — traditional EUR / EPAL pallet: parallel slats on top and\n * bottom, three perpendicular stringers between them.\n * - `plastic` — molded one-piece pallet: solid top deck with cutouts,\n * hollow underside with feet. Distinct ribbed underside.\n *\n * Adding a third material (e.g. metal, composite) is a one-line change to the\n * legend + a 3D variant in pallet-3d.ts.\n */\nexport type PalletMaterial = 'wood' | 'plastic'\n\n/** Pallet 컴포넌트 state */\nexport interface PalletState extends State {\n // ── 외관 ──\n material?: PalletMaterial\n\n /**\n * Fork pocket 의 깊이 (mm) — pallet 외부 bottom (skid bottom) 부터 deck bottom\n * 까지의 수직 거리. fork blade 가 이 *pocket 안* 으로 수평 진입. 표준 EUR\n * pallet (144mm depth) 의 pocket ≈ 50~60mm.\n *\n * 미명시 시 default = depth 의 40% (= ~60mm for 150mm pallet).\n */\n pocketDepth?: number\n\n // ── 3D 재질 ──\n material3d?: Material3D\n}\n\nconst BODY_LEGEND = {\n wood: '#a87644',\n plastic: '#5a6a78',\n default: '#a87644'\n}\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [\n {\n type: 'select',\n label: 'material',\n name: 'material',\n property: {\n options: [\n { display: 'Wood', value: 'wood' },\n { display: 'Plastic', value: 'plastic' }\n ]\n }\n },\n {\n type: 'number',\n label: 'pocket-depth',\n name: 'pocketDepth',\n placeholder: 'mm — fork 진입 pocket 깊이 (skid bottom → deck bottom). default depth × 40%.'\n }\n ],\n help: 'scene/component/pallet'\n}\n\n// Carriable: a pallet can sit on AGV / Forklift / robot-arm gripper / Spot\n// and also accept boxes / parcels as children (ContainerAbstract base\n// provides the child-container behavior; Carriable only adds the\n// holder-mount hook).\n//\n// `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),\n// which forces `isHTMLElement(): true` and trips the 3D pipeline's\n// addObject DOM-skip gate. Pallet renders only as a 3D mesh.\n/**\n * Pallet — a flat transport structure that goods are stacked and stored on.\n *\n * Standard EUR pallet is 1200 × 800mm × 144mm; we don't enforce these\n * dimensions but they're a good starting point for the catalog templates.\n *\n * **Container-based.** Boxes / parcels stacked on the pallet are added as\n * children — same `containable()` archetype-filter pattern as Forklift / Agv.\n * Visual stacking (children rendering on top of the pallet rather than at\n * absolute operation level) is a v2 concern; see ARCHITECTURE NOTES below.\n *\n * **Placement = `operation`.** A pallet's *normal* state is loaded and in\n * transit on a conveyor / AGV / forklift fork — at operation level. Empty\n * pallets in a floor-storage area are an exceptional state where the user\n * sets `state.zPos = 0` explicitly. Default to the common case.\n *\n * ## ARCHITECTURE NOTES — visual stacking\n *\n * When a Box (also `placement: 'operation'`) is added as a child of a\n * Pallet, both default to z = operation_height. They overlap visually\n * rather than the box sitting on top of the pallet. Solving this cleanly\n * (parent-relative z derivation when the parent is a structural carrier)\n * is a follow-up — fmsim's pattern is to detect parent type at render time\n * (machine-3d.ts:113-122). v1 accepts the visual overlap; v2 will add the\n * detection.\n */\n@sceneComponent('pallet')\nexport default class Pallet extends Carriable(Legendable(Placeable(ContainerAbstract))) {\n declare state: PalletState\n\n static legends: Record<string, LegendBinding> = {\n bodyColor: { from: 'material', legend: BODY_LEGEND }\n }\n\n static placement: PlacementArchetype = 'operation'\n static align: Alignment = 'bottom'\n static defaultDepth = 150 // EUR pallet is 144mm; 150 is the round number convention\n\n get nature() {\n return NATURE\n }\n\n /**\n * Fork pocket 의 깊이 — fork blade 가 진입하는 *pallet 외부 bottom 부터 deck\n * bottom* 까지의 수직 거리. crane.attachPointFor 가 이 값을 차감해서\n * carrier 외부 bottom (skid) 이 fork blade bottom *아래로 pocketDepth 만큼*\n * 깊이 정렬 (= fork 가 pallet 의 *deck 와 skid 사이* 안 으로 들어간 자세).\n */\n get pocketDepth(): number {\n const explicit = (this.state as any).pocketDepth\n if (typeof explicit === 'number' && Number.isFinite(explicit) && explicit >= 0) {\n return explicit\n }\n const d = this.state.depth\n const depth = typeof d === 'number' && Number.isFinite(d) && d > 0\n ? d\n : ((this.constructor as any).defaultDepth ?? 150)\n return depth * 0.4\n }\n\n get anchors() {\n return []\n }\n\n /** Accept other operation-archetype cargo (boxes, parcels, smaller pallets) as stacked children. */\n containable(component: Component) {\n const archetype = (component.constructor as any).placement\n if (archetype === 'operation') return true\n return component.isDescendible(this)\n }\n\n /**\n * 2D — top-down silhouette. Body is a flat rectangle (wood/plastic deck);\n * `postrender()` adds the deck pattern + edge stroke so the pallet reads\n * as a pallet instead of a featureless rectangle.\n */\n render(ctx: CanvasRenderingContext2D) {\n const { width, height, left, top } = this.state\n ctx.beginPath()\n ctx.rect(left, top, width, height)\n }\n\n /**\n * Deck pattern + edge stroke. Wood: parallel slats with darker grooves\n * between (typical EUR pallet deck). Plastic: cross-cutout pattern\n * suggesting the molded reinforcement ribs.\n *\n * Slats run along the *short* axis of the rectangle (= along the longer\n * stringer direction in real life), so a 1200×800 pallet shows multiple\n * narrow slats across the 1200mm dimension — matching the EUR layout.\n */\n postrender(ctx: CanvasRenderingContext2D) {\n super.postrender?.(ctx)\n\n const { width, height, left, top } = this.state\n const isPlastic = this.state.material === 'plastic'\n\n ctx.save()\n\n if (!isPlastic) {\n // Wood — slats. Run them along the longer axis so the grooves are\n // perpendicular to the longer side (typical pallet appearance).\n const longAxisHorizontal = width >= height\n const slatCount = 5\n const grooveColor = '#7a4f25'\n ctx.fillStyle = grooveColor\n if (longAxisHorizontal) {\n // grooves vertical (X direction across the width)\n const grooveW = Math.max(1, width * 0.012)\n const slatW = (width - grooveW * (slatCount - 1)) / slatCount\n for (let i = 1; i < slatCount; i++) {\n const x = left + i * slatW + (i - 1) * grooveW\n ctx.fillRect(x, top + height * 0.05, grooveW, height * 0.9)\n }\n } else {\n const grooveH = Math.max(1, height * 0.012)\n const slatH = (height - grooveH * (slatCount - 1)) / slatCount\n for (let i = 1; i < slatCount; i++) {\n const y = top + i * slatH + (i - 1) * grooveH\n ctx.fillRect(left + width * 0.05, y, width * 0.9, grooveH)\n }\n }\n } else {\n // Plastic — cross + corner cutouts hint\n ctx.strokeStyle = '#3a4956'\n ctx.lineWidth = Math.max(1, Math.min(width, height) * 0.012)\n ctx.beginPath()\n ctx.moveTo(left + width * 0.5, top + height * 0.1)\n ctx.lineTo(left + width * 0.5, top + height * 0.9)\n ctx.moveTo(left + width * 0.1, top + height * 0.5)\n ctx.lineTo(left + width * 0.9, top + height * 0.5)\n ctx.stroke()\n }\n\n // Edge stroke\n ctx.strokeStyle = isPlastic ? '#2a3946' : '#5e3818'\n ctx.lineWidth = 1\n ctx.strokeRect(left, top, width, height)\n\n ctx.restore()\n }\n\n get fillStyle() {\n return (this.state.bodyColor as string) || '#a87644'\n }\n\n buildRealObject(): RealObject | undefined {\n return new Pallet3D(this)\n }\n\n /**\n * Phase H — pickup contract. EUR/EPAL pallet 의 fork 진입은 양 단면 (긴 변\n * 두 군데). 'east' / 'west' = pallet 의 짧은 축 방향에서 fork 가 들어감.\n *\n * fork 진입 높이 (entryHeight) 는 pallet 의 fork pocket 위치 — pallet 의\n * 바닥에서 약 50mm (top deck 와 bottom deck 사이의 stringer 영역). 표준\n * EUR pallet 의 fork pocket 은 144mm 두께의 약 50% 지점.\n *\n * approachDistance 는 forklift 가 fork 끝을 pallet 에 닿기 직전 hover 자세 —\n * pallet 길이만큼 떨어져서 fork 길이 (보통 1100mm) 가 다 들어가도록 한다.\n * 여기선 conservative 하게 pallet 너비 + 200mm.\n */\n pickupFrames(): PickupFrame[] {\n const { width = 1200, height = 800 } = this.state\n const palletDepth = (this.constructor as any).defaultDepth ?? 150\n\n // 4-way pallet: 모든 면에서 fork 진입 가능. 2-way 는 sides 를 ['east','west']\n // 로 한정 — pallet 의 stringer 방향에 따라 다르나 default 는 4-way 가정.\n // (state.palletType 같은 속성 추가 시 분기 가능 — 현재는 4-way 가정.)\n const me: PoseSerialized = (() => {\n const wp = getWorldPose(this)\n return {\n position: { x: wp.position.x, y: wp.position.y, z: wp.position.z },\n rotation: { x: wp.rotation.x, y: wp.rotation.y, z: wp.rotation.z, w: wp.rotation.w }\n }\n })()\n\n const longerAxis = Math.max(width, height)\n\n const forkFrames = rectangularFootprintFrames({\n carrierWorld: me,\n width,\n depth: height,\n entryHeight: palletDepth * 0.4, // fork pocket 중심\n approachDistance: longerAxis + 200,\n sides: ['east', 'west', 'north', 'south'],\n toolType: 'forklift-fork',\n tolerance: { positionMm: 50, angleDeg: 5 },\n priority: 0\n })\n\n // 큰 AGV (deck size 충분) 가 pallet 을 위에 적재하는 패턴. priority 낮음\n // — forklift fork 가 더 자연스러운 default. AGV deck size 가 pallet 보다\n // 작으면 application 측이 carrier 의 dimensions check 필요.\n const deckFrame = topApproachFrame({\n carrierWorld: me,\n topY: palletDepth,\n approachDistance: longerAxis * 0.5 + 100,\n toolType: 'agv-deck',\n tolerance: { positionMm: 30, angleDeg: 4 },\n priority: 1,\n id: 'top-deck'\n })\n\n return [...forkFrames, deckFrame]\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"pallet.js","sourceRoot":"","sources":["../src/pallet.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\nimport {\n Component,\n ComponentNature,\n ContainerAbstract,\n RealObject,\n Pose6DOF,\n rectangularFootprintFrames,\n topApproachFrame,\n getWorldPose,\n sceneComponent\n} from '@hatiolab/things-scene'\nimport type { State, Material3D, PickupFrame, PoseSerialized } from '@hatiolab/things-scene'\nimport {\n Carriable,\n Identifiable,\n Legendable,\n Placeable,\n type Alignment,\n type LegendBinding,\n type PlacementArchetype\n} from '@operato/scene-base'\n\nimport { Pallet3D } from './pallet-3d.js'\n\n/**\n * Pallet material — drives both 2D fill color and 3D structure.\n *\n * - `wood` — traditional EUR / EPAL pallet: parallel slats on top and\n * bottom, three perpendicular stringers between them.\n * - `plastic` — molded one-piece pallet: solid top deck with cutouts,\n * hollow underside with feet. Distinct ribbed underside.\n *\n * Adding a third material (e.g. metal, composite) is a one-line change to the\n * legend + a 3D variant in pallet-3d.ts.\n */\nexport type PalletMaterial = 'wood' | 'plastic'\n\n/** Pallet 컴포넌트 state */\nexport interface PalletState extends State {\n // ── 외관 ──\n material?: PalletMaterial\n\n /**\n * Fork pocket 의 깊이 (mm) — pallet 외부 bottom (skid bottom) 부터 deck bottom\n * 까지의 수직 거리. fork blade 가 이 *pocket 안* 으로 수평 진입. 표준 EUR\n * pallet (144mm depth) 의 pocket ≈ 50~60mm.\n *\n * 미명시 시 default = depth 의 40% (= ~60mm for 150mm pallet).\n */\n pocketDepth?: number\n\n // ── 3D 재질 ──\n material3d?: Material3D\n}\n\nconst BODY_LEGEND = {\n wood: '#a87644',\n plastic: '#5a6a78',\n default: '#a87644'\n}\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [\n {\n type: 'select',\n label: 'material',\n name: 'material',\n property: {\n options: [\n { display: 'Wood', value: 'wood' },\n { display: 'Plastic', value: 'plastic' }\n ]\n }\n },\n {\n type: 'number',\n label: 'pocket-depth',\n name: 'pocketDepth',\n placeholder: 'mm — fork 진입 pocket 깊이 (skid bottom → deck bottom). default depth × 40%.'\n }\n ],\n help: 'scene/component/pallet'\n}\n\n// Carriable: a pallet can sit on AGV / Forklift / robot-arm gripper / Spot\n// and also accept boxes / parcels as children (ContainerAbstract base\n// provides the child-container behavior; Carriable only adds the\n// holder-mount hook).\n//\n// `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),\n// which forces `isHTMLElement(): true` and trips the 3D pipeline's\n// addObject DOM-skip gate. Pallet renders only as a 3D mesh.\n/**\n * Pallet — a flat transport structure that goods are stacked and stored on.\n *\n * Standard EUR pallet is 1200 × 800mm × 144mm; we don't enforce these\n * dimensions but they're a good starting point for the catalog templates.\n *\n * **Container-based.** Boxes / parcels stacked on the pallet are added as\n * children — same `containable()` archetype-filter pattern as Forklift / Agv.\n * Visual stacking (children rendering on top of the pallet rather than at\n * absolute operation level) is a v2 concern; see ARCHITECTURE NOTES below.\n *\n * **Placement = `operation`.** A pallet's *normal* state is loaded and in\n * transit on a conveyor / AGV / forklift fork — at operation level. Empty\n * pallets in a floor-storage area are an exceptional state where the user\n * sets `state.zPos = 0` explicitly. Default to the common case.\n *\n * ## ARCHITECTURE NOTES — visual stacking\n *\n * When a Box (also `placement: 'operation'`) is added as a child of a\n * Pallet, both default to z = operation_height. They overlap visually\n * rather than the box sitting on top of the pallet. Solving this cleanly\n * (parent-relative z derivation when the parent is a structural carrier)\n * is a follow-up — fmsim's pattern is to detect parent type at render time\n * (machine-3d.ts:113-122). v1 accepts the visual overlap; v2 will add the\n * detection.\n */\n@sceneComponent('pallet')\nexport default class Pallet extends Identifiable(Carriable(Legendable(Placeable(ContainerAbstract)))) {\n declare state: PalletState\n\n static legends: Record<string, LegendBinding> = {\n bodyColor: { from: 'material', legend: BODY_LEGEND }\n }\n\n static placement: PlacementArchetype = 'operation'\n static align: Alignment = 'bottom'\n static defaultDepth = 150 // EUR pallet is 144mm; 150 is the round number convention\n\n get nature() {\n return NATURE\n }\n\n /**\n * Fork pocket 의 깊이 — fork blade 가 진입하는 *pallet 외부 bottom 부터 deck\n * bottom* 까지의 수직 거리. crane.attachPointFor 가 이 값을 차감해서\n * carrier 외부 bottom (skid) 이 fork blade bottom *아래로 pocketDepth 만큼*\n * 깊이 정렬 (= fork 가 pallet 의 *deck 와 skid 사이* 안 으로 들어간 자세).\n */\n get pocketDepth(): number {\n const explicit = (this.state as any).pocketDepth\n if (typeof explicit === 'number' && Number.isFinite(explicit) && explicit >= 0) {\n return explicit\n }\n const d = this.state.depth\n const depth = typeof d === 'number' && Number.isFinite(d) && d > 0\n ? d\n : ((this.constructor as any).defaultDepth ?? 150)\n return depth * 0.4\n }\n\n get anchors() {\n return []\n }\n\n /** Accept other operation-archetype cargo (boxes, parcels, smaller pallets) as stacked children. */\n containable(component: Component) {\n const archetype = (component.constructor as any).placement\n if (archetype === 'operation') return true\n return component.isDescendible(this)\n }\n\n /**\n * 2D — top-down silhouette. Body is a flat rectangle (wood/plastic deck);\n * `postrender()` adds the deck pattern + edge stroke so the pallet reads\n * as a pallet instead of a featureless rectangle.\n */\n render(ctx: CanvasRenderingContext2D) {\n const { width, height, left, top } = this.state\n ctx.beginPath()\n ctx.rect(left, top, width, height)\n }\n\n /**\n * Deck pattern + edge stroke. Wood: parallel slats with darker grooves\n * between (typical EUR pallet deck). Plastic: cross-cutout pattern\n * suggesting the molded reinforcement ribs.\n *\n * Slats run along the *short* axis of the rectangle (= along the longer\n * stringer direction in real life), so a 1200×800 pallet shows multiple\n * narrow slats across the 1200mm dimension — matching the EUR layout.\n */\n postrender(ctx: CanvasRenderingContext2D) {\n super.postrender?.(ctx)\n\n const { width, height, left, top } = this.state\n const isPlastic = this.state.material === 'plastic'\n\n ctx.save()\n\n if (!isPlastic) {\n // Wood — slats. Run them along the longer axis so the grooves are\n // perpendicular to the longer side (typical pallet appearance).\n const longAxisHorizontal = width >= height\n const slatCount = 5\n const grooveColor = '#7a4f25'\n ctx.fillStyle = grooveColor\n if (longAxisHorizontal) {\n // grooves vertical (X direction across the width)\n const grooveW = Math.max(1, width * 0.012)\n const slatW = (width - grooveW * (slatCount - 1)) / slatCount\n for (let i = 1; i < slatCount; i++) {\n const x = left + i * slatW + (i - 1) * grooveW\n ctx.fillRect(x, top + height * 0.05, grooveW, height * 0.9)\n }\n } else {\n const grooveH = Math.max(1, height * 0.012)\n const slatH = (height - grooveH * (slatCount - 1)) / slatCount\n for (let i = 1; i < slatCount; i++) {\n const y = top + i * slatH + (i - 1) * grooveH\n ctx.fillRect(left + width * 0.05, y, width * 0.9, grooveH)\n }\n }\n } else {\n // Plastic — cross + corner cutouts hint\n ctx.strokeStyle = '#3a4956'\n ctx.lineWidth = Math.max(1, Math.min(width, height) * 0.012)\n ctx.beginPath()\n ctx.moveTo(left + width * 0.5, top + height * 0.1)\n ctx.lineTo(left + width * 0.5, top + height * 0.9)\n ctx.moveTo(left + width * 0.1, top + height * 0.5)\n ctx.lineTo(left + width * 0.9, top + height * 0.5)\n ctx.stroke()\n }\n\n // Edge stroke\n ctx.strokeStyle = isPlastic ? '#2a3946' : '#5e3818'\n ctx.lineWidth = 1\n ctx.strokeRect(left, top, width, height)\n\n ctx.restore()\n }\n\n get fillStyle() {\n return (this.state.bodyColor as string) || '#a87644'\n }\n\n buildRealObject(): RealObject | undefined {\n return new Pallet3D(this)\n }\n\n /**\n * Phase H — pickup contract. EUR/EPAL pallet 의 fork 진입은 양 단면 (긴 변\n * 두 군데). 'east' / 'west' = pallet 의 짧은 축 방향에서 fork 가 들어감.\n *\n * fork 진입 높이 (entryHeight) 는 pallet 의 fork pocket 위치 — pallet 의\n * 바닥에서 약 50mm (top deck 와 bottom deck 사이의 stringer 영역). 표준\n * EUR pallet 의 fork pocket 은 144mm 두께의 약 50% 지점.\n *\n * approachDistance 는 forklift 가 fork 끝을 pallet 에 닿기 직전 hover 자세 —\n * pallet 길이만큼 떨어져서 fork 길이 (보통 1100mm) 가 다 들어가도록 한다.\n * 여기선 conservative 하게 pallet 너비 + 200mm.\n */\n pickupFrames(): PickupFrame[] {\n const { width = 1200, height = 800 } = this.state\n const palletDepth = (this.constructor as any).defaultDepth ?? 150\n\n // 4-way pallet: 모든 면에서 fork 진입 가능. 2-way 는 sides 를 ['east','west']\n // 로 한정 — pallet 의 stringer 방향에 따라 다르나 default 는 4-way 가정.\n // (state.palletType 같은 속성 추가 시 분기 가능 — 현재는 4-way 가정.)\n const me: PoseSerialized = (() => {\n const wp = getWorldPose(this)\n return {\n position: { x: wp.position.x, y: wp.position.y, z: wp.position.z },\n rotation: { x: wp.rotation.x, y: wp.rotation.y, z: wp.rotation.z, w: wp.rotation.w }\n }\n })()\n\n const longerAxis = Math.max(width, height)\n\n const forkFrames = rectangularFootprintFrames({\n carrierWorld: me,\n width,\n depth: height,\n entryHeight: palletDepth * 0.4, // fork pocket 중심\n approachDistance: longerAxis + 200,\n sides: ['east', 'west', 'north', 'south'],\n toolType: 'forklift-fork',\n tolerance: { positionMm: 50, angleDeg: 5 },\n priority: 0\n })\n\n // 큰 AGV (deck size 충분) 가 pallet 을 위에 적재하는 패턴. priority 낮음\n // — forklift fork 가 더 자연스러운 default. AGV deck size 가 pallet 보다\n // 작으면 application 측이 carrier 의 dimensions check 필요.\n const deckFrame = topApproachFrame({\n carrierWorld: me,\n topY: palletDepth,\n approachDistance: longerAxis * 0.5 + 100,\n toolType: 'agv-deck',\n tolerance: { positionMm: 30, angleDeg: 4 },\n priority: 1,\n id: 'top-deck'\n })\n\n return [...forkFrames, deckFrame]\n }\n}\n"]}
|
package/dist/parcel.js
CHANGED
|
@@ -3,7 +3,7 @@ import { __decorate } from "tslib";
|
|
|
3
3
|
* Copyright © HatioLab Inc. All rights reserved.
|
|
4
4
|
*/
|
|
5
5
|
import { RectPath, Shape, topApproachFrame, getWorldPose, sceneComponent } from '@hatiolab/things-scene';
|
|
6
|
-
import { Carriable, Placeable } from '@operato/scene-base';
|
|
6
|
+
import { Carriable, Identifiable, Placeable } from '@operato/scene-base';
|
|
7
7
|
import { Parcel3D } from './parcel-3d.js';
|
|
8
8
|
const NATURE = {
|
|
9
9
|
mutable: false,
|
|
@@ -14,6 +14,12 @@ const NATURE = {
|
|
|
14
14
|
type: 'string',
|
|
15
15
|
label: 'tracking-id',
|
|
16
16
|
name: 'trackingId'
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
type: 'textarea',
|
|
20
|
+
label: 'data (JSON)',
|
|
21
|
+
name: 'data',
|
|
22
|
+
placeholder: '{ "sku": "P-1234", "dest": "chute-A" }'
|
|
17
23
|
}
|
|
18
24
|
],
|
|
19
25
|
help: 'scene/component/parcel'
|
|
@@ -36,7 +42,7 @@ const NATURE = {
|
|
|
36
42
|
* No Legendable for v1 — parcel color is fixed cardboard. Future damaged /
|
|
37
43
|
* inspected indicators would add a status legend then.
|
|
38
44
|
*/
|
|
39
|
-
let Parcel = class Parcel extends Carriable(Placeable(RectPath(Shape))) {
|
|
45
|
+
let Parcel = class Parcel extends Identifiable(Carriable(Placeable(RectPath(Shape)))) {
|
|
40
46
|
static placement = 'operation';
|
|
41
47
|
static align = 'bottom';
|
|
42
48
|
static defaultDepth = 150;
|
package/dist/parcel.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parcel.js","sourceRoot":"","sources":["../src/parcel.ts"],"names":[],"mappings":";AAAA;;GAEG;AACH,OAAO,EAGL,QAAQ,EACR,KAAK,EACL,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACf,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EACL,SAAS,EACT,SAAS,EAGV,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAWzC,MAAM,MAAM,GAAoB;IAC9B,OAAO,EAAE,KAAK;IACd,SAAS,EAAE,IAAI;IACf,SAAS,EAAE,IAAI;IACf,UAAU,EAAE;QACV;YACE,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,aAAa;YACpB,IAAI,EAAE,YAAY;SACnB;KACF;IACD,IAAI,EAAE,wBAAwB;CAC/B,CAAA;AAED,yEAAyE;AACzE,uEAAuE;AACvE,oDAAoD;AACpD;;;;;;;;;;;;;;GAcG;AAEY,IAAM,MAAM,GAAZ,MAAM,MAAO,SAAQ,SAAS,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"parcel.js","sourceRoot":"","sources":["../src/parcel.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\nimport {\n ComponentNature,\n RealObject,\n RectPath,\n Shape,\n topApproachFrame,\n getWorldPose,\n sceneComponent\n} from '@hatiolab/things-scene'\nimport type { State, Material3D, PickupFrame, PoseSerialized } from '@hatiolab/things-scene'\nimport {\n Carriable,\n Identifiable,\n Placeable,\n type Alignment,\n type PlacementArchetype\n} from '@operato/scene-base'\n\nimport { Parcel3D } from './parcel-3d.js'\n\n/** Parcel 컴포넌트 state */\nexport interface ParcelState extends State {\n // ── 정체 ──\n trackingId?: string\n\n // ── 3D 재질 ──\n material3d?: Material3D\n}\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [\n {\n type: 'string',\n label: 'tracking-id',\n name: 'trackingId'\n },\n {\n type: 'textarea',\n label: 'data (JSON)',\n name: 'data',\n placeholder: '{ \"sku\": \"P-1234\", \"dest\": \"chute-A\" }'\n }\n ],\n help: 'scene/component/parcel'\n}\n\n// Carriable: parcel can be a child of any CarrierHolder (Spot, robot-arm\n// gripper, AGV deck, …). Mixin wraps add() so the parcel's 3D object3d\n// is reattached to the holder's chosen mount frame.\n/**\n * Parcel — a cardboard package, the typical e-commerce / parcel-sortation unit.\n *\n * Distinct from `Box` because parcels have:\n * - cardboard appearance (tan/brown corrugate, not wood / plastic)\n * - tape line down the center (the visual signature that says \"package\")\n * - typically a label on top (where shipping info goes)\n * - flatter / more elongated proportions in real-world parcel networks\n *\n * No `material` prop — parcels are always cardboard. If a future shipping\n * domain needs metal cases or polybags, those become separate components.\n *\n * No Legendable for v1 — parcel color is fixed cardboard. Future damaged /\n * inspected indicators would add a status legend then.\n */\n@sceneComponent('parcel')\nexport default class Parcel extends Identifiable(Carriable(Placeable(RectPath(Shape)))) {\n declare state: ParcelState\n\n static placement: PlacementArchetype = 'operation'\n static align: Alignment = 'bottom'\n static defaultDepth = 150\n\n get nature() {\n return NATURE\n }\n\n get anchors() {\n return []\n }\n\n /** 2D — top-down rectangle in cardboard tan. */\n render(ctx: CanvasRenderingContext2D) {\n const { width, height, left, top } = this.state\n ctx.beginPath()\n ctx.rect(left, top, width, height)\n }\n\n get fillStyle() {\n return '#c8a878'\n }\n\n buildRealObject(): RealObject | undefined {\n return new Parcel3D(this)\n }\n\n /**\n * Phase H — pickup contract. Parcel 의 pickup 방식:\n * - gripper (vacuum / suction): 위에서 흡착 — RobotArm\n * - agv-deck: AGV/Forklift 의 deck 위에 위에서 얹기 — 같은 top approach 지만\n * deck 자체가 운반체라 tolerance 더 완화\n */\n pickupFrames(): PickupFrame[] {\n const wp = getWorldPose(this)\n const me: PoseSerialized = {\n position: { x: wp.position.x, y: wp.position.y, z: wp.position.z },\n rotation: { x: wp.rotation.x, y: wp.rotation.y, z: wp.rotation.z, w: wp.rotation.w }\n }\n const parcelDepth = (this.constructor as any).defaultDepth ?? 150\n\n return [\n topApproachFrame({\n carrierWorld: me,\n topY: parcelDepth,\n approachDistance: 80,\n toolType: 'gripper',\n tolerance: { positionMm: 10, angleDeg: 2 },\n priority: 0,\n id: 'top-suction'\n }),\n topApproachFrame({\n carrierWorld: me,\n topY: parcelDepth,\n approachDistance: 60,\n toolType: 'agv-deck',\n tolerance: { positionMm: 20, angleDeg: 5 },\n priority: 1,\n id: 'top-deck'\n }),\n topApproachFrame({\n carrierWorld: me,\n topY: parcelDepth,\n approachDistance: 100, // crane fork 가 cell 진입 hover\n toolType: 'forklift-fork',\n tolerance: { positionMm: 30, angleDeg: 5 }, // fork 적재 tolerance\n priority: 2, // gripper/deck 다음\n id: 'top-fork'\n })\n ]\n }\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Component, ComponentNature, RealObject } from '@hatiolab/things-scene';
|
|
2
2
|
import type { State, Material3D } from '@hatiolab/things-scene';
|
|
3
|
-
import {
|
|
3
|
+
import { type AttachFrame, type Alignment, type Heights, type PlacementArchetype, type ProcessStep } from '@operato/scene-base';
|
|
4
4
|
import { PickingStation3D } from './picking-station-3d.js';
|
|
5
5
|
export type PickingStationStatus = 'idle' | 'processing' | 'busy';
|
|
6
6
|
export interface PickingStationState extends State {
|
|
@@ -13,7 +13,7 @@ export interface PickingStationState extends State {
|
|
|
13
13
|
material3d?: Material3D;
|
|
14
14
|
}
|
|
15
15
|
declare const PickingStation_base: any;
|
|
16
|
-
export default class PickingStation extends PickingStation_base {
|
|
16
|
+
export default class PickingStation extends PickingStation_base implements ProcessStep {
|
|
17
17
|
state: PickingStationState;
|
|
18
18
|
_realObject?: PickingStation3D;
|
|
19
19
|
static placement: PlacementArchetype;
|
|
@@ -21,23 +21,17 @@ export default class PickingStation extends PickingStation_base {
|
|
|
21
21
|
static defaultDepth: (h: Heights) => number;
|
|
22
22
|
get nature(): ComponentNature;
|
|
23
23
|
get anchors(): never[];
|
|
24
|
-
|
|
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;
|
|
24
|
+
_singleSlotId(): string;
|
|
30
25
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
26
|
+
* SlottedHolder duck (slotIds / hasCarrierAt / canReceiveAt / occupiedSlotIds
|
|
27
|
+
* / emptySlotIds / obtainCarrier / receiveAt / accept / receive / slotTargetAt
|
|
28
|
+
* / getSlotAttachObject3d) — SingleSlotHolder mixin 제공.
|
|
33
29
|
*
|
|
34
|
-
*
|
|
30
|
+
* receiveAt 후 부가 dwell 동작은 `_onCarrierReceived` hook 으로 위임.
|
|
35
31
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
slotTargetAt(slotId: string): SlotTarget;
|
|
40
|
-
getSlotAttachObject3d(_slotId: string): any;
|
|
32
|
+
_onCarrierReceived(_carrier: Component, _options?: any): void;
|
|
33
|
+
readonly isProcessStep: true;
|
|
34
|
+
get processingTimeMs(): number;
|
|
41
35
|
/** carrier 를 작업대(table) 상단에 안착. */
|
|
42
36
|
attachPointFor(carrier: Component): AttachFrame | null;
|
|
43
37
|
render(ctx: CanvasRenderingContext2D): void;
|
package/dist/picking-station.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { __decorate } from "tslib";
|
|
11
11
|
import * as THREE from 'three';
|
|
12
12
|
import { ContainerAbstract, sceneComponent } from '@hatiolab/things-scene';
|
|
13
|
-
import { CarrierHolder, Placeable,
|
|
13
|
+
import { CarrierHolder, Placeable, SingleSlotHolder } from '@operato/scene-base';
|
|
14
14
|
import { PickingStation3D } from './picking-station-3d.js';
|
|
15
15
|
const SLOT_ID = 'station';
|
|
16
16
|
const NATURE = {
|
|
@@ -26,61 +26,35 @@ const NATURE = {
|
|
|
26
26
|
],
|
|
27
27
|
help: 'scene/component/picking-station'
|
|
28
28
|
};
|
|
29
|
-
let PickingStation = class PickingStation extends CarrierHolder(Placeable(ContainerAbstract)) {
|
|
29
|
+
let PickingStation = class PickingStation extends SingleSlotHolder()(CarrierHolder(Placeable(ContainerAbstract))) {
|
|
30
30
|
static placement = 'floor';
|
|
31
31
|
static align = 'bottom';
|
|
32
32
|
static defaultDepth = (h) => h.operation - h.floor;
|
|
33
33
|
get nature() { return NATURE; }
|
|
34
34
|
get anchors() { return []; }
|
|
35
|
-
//
|
|
36
|
-
|
|
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
|
-
}
|
|
35
|
+
// SingleSlotHolder hook overrides ───────────────────────────────────────────
|
|
36
|
+
_singleSlotId() { return SLOT_ID; }
|
|
55
37
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
38
|
+
* SlottedHolder duck (slotIds / hasCarrierAt / canReceiveAt / occupiedSlotIds
|
|
39
|
+
* / emptySlotIds / obtainCarrier / receiveAt / accept / receive / slotTargetAt
|
|
40
|
+
* / getSlotAttachObject3d) — SingleSlotHolder mixin 제공.
|
|
58
41
|
*
|
|
59
|
-
*
|
|
42
|
+
* receiveAt 후 부가 dwell 동작은 `_onCarrierReceived` hook 으로 위임.
|
|
60
43
|
*/
|
|
61
|
-
|
|
62
|
-
;
|
|
63
|
-
this.reparent?.(carrier, { ...(options ?? {}), animated: false });
|
|
44
|
+
_onCarrierReceived(_carrier, _options) {
|
|
64
45
|
const procMs = this.state.processingTimeMs ?? 0;
|
|
65
|
-
if (procMs
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
}
|
|
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?.();
|
|
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);
|
|
83
54
|
}
|
|
55
|
+
// Phase ZT-V PR-6 ProcessStep ─────────────────────────────────────────────
|
|
56
|
+
isProcessStep = true;
|
|
57
|
+
get processingTimeMs() { return this.state.processingTimeMs ?? 0; }
|
|
84
58
|
/** carrier 를 작업대(table) 상단에 안착. */
|
|
85
59
|
attachPointFor(carrier) {
|
|
86
60
|
const ro = this._realObject;
|
|
@@ -1 +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"]}
|
|
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"]}
|
package/dist/rack-grid.d.ts
CHANGED
|
@@ -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
|
*/
|