@operato/scene-storage 10.0.0-beta.22 → 10.0.0-beta.24

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 CHANGED
@@ -3,6 +3,20 @@
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.24](https://github.com/things-scene/operato-scene/compare/v10.0.0-beta.23...v10.0.0-beta.24) (2026-05-02)
7
+
8
+
9
+ ### :bug: Bug Fix
10
+
11
+ * **scene:** Container → ContainerAbstract for 3D-only components ([89bdea7](https://github.com/things-scene/operato-scene/commit/89bdea73b37247601932b92692ea5d5e7780976c))
12
+
13
+
14
+ ### :mega: Other
15
+
16
+ * tune default template sizes for board overview ([582232a](https://github.com/things-scene/operato-scene/commit/582232ae078631e13754ba2e06320e211c3a1de1))
17
+
18
+
19
+
6
20
  ## [10.0.0-beta.22](https://github.com/things-scene/operato-scene/compare/v10.0.0-beta.21...v10.0.0-beta.22) (2026-04-30)
7
21
 
8
22
 
package/dist/asrs-rack.js CHANGED
@@ -2,7 +2,7 @@ import { __decorate } from "tslib";
2
2
  /*
3
3
  * Copyright © HatioLab Inc. All rights reserved.
4
4
  */
5
- import { Container, sceneComponent } from '@hatiolab/things-scene';
5
+ import { ContainerAbstract, sceneComponent } from '@hatiolab/things-scene';
6
6
  import { Placeable } from '@operato/scene-base';
7
7
  import { AsrsRack3D } from './asrs-rack-3d.js';
8
8
  const NATURE = {
@@ -25,7 +25,10 @@ const NATURE = {
25
25
  ],
26
26
  help: 'scene/component/asrs-rack'
27
27
  };
28
- const Base = Placeable(Container);
28
+ // `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),
29
+ // which forces `isHTMLElement(): true` and trips the 3D pipeline's
30
+ // addObject DOM-skip gate. ASRS rack lives only in the 3D scene graph.
31
+ const Base = Placeable(ContainerAbstract);
29
32
  /**
30
33
  * AsrsRack — a multi-level high-bay storage rack, the structural backbone of
31
34
  * an AS/RS (Automated Storage / Retrieval System).
@@ -1 +1 @@
1
- {"version":3,"file":"asrs-rack.js","sourceRoot":"","sources":["../src/asrs-rack.ts"],"names":[],"mappings":";AAAA;;GAEG;AACH,OAAO,EAA8B,SAAS,EAAc,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAC1G,OAAO,EACL,SAAS,EAIV,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAE9C,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,QAAQ;YACf,IAAI,EAAE,QAAQ;YACd,WAAW,EAAE,kCAAkC;SAChD;QACD;YACE,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,MAAM;YACb,IAAI,EAAE,MAAM;YACZ,WAAW,EAAE,kCAAkC;SAChD;KACF;IACD,IAAI,EAAE,2BAA2B;CAClC,CAAA;AAED,MAAM,IAAI,GAAG,SAAS,CAAC,SAAS,CAAgC,CAAA;AAEhE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEY,IAAM,QAAQ,GAAd,MAAM,QAAS,SAAQ,IAAI;IACxC,MAAM,CAAC,SAAS,GAAuB,OAAO,CAAA;IAC9C,MAAM,CAAC,KAAK,GAAc,QAAQ,CAAA;IAClC,MAAM,CAAC,YAAY,GAAG,CAAC,CAAU,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,KAAK,CAAA;IAEzD,IAAI,MAAM;QACR,OAAO,MAAM,CAAA;IACf,CAAC;IAED,IAAI,OAAO;QACT,OAAO,EAAE,CAAA;IACX,CAAC;IAED,4EAA4E;IAC5E,WAAW,CAAC,SAAoB;QAC9B,MAAM,SAAS,GAAI,SAAS,CAAC,WAAmB,CAAC,SAAS,CAAA;QAC1D,IAAI,SAAS,KAAK,WAAW;YAAE,OAAO,IAAI,CAAA;QAC1C,OAAO,SAAS,CAAC,aAAa,CAAC,IAAW,CAAC,CAAA;IAC7C,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,GAA6B;QAClC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAA;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAE,IAAI,CAAC,KAAK,CAAC,IAAe,IAAI,CAAC,CAAC,CAAC,CAAA;QAEtE,GAAG,CAAC,SAAS,EAAE,CAAA;QACf,kBAAkB;QAClB,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;QAClC,oCAAoC;QACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;YAC9B,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,CAAA;YACnC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;YAClB,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,CAAC,CAAA;QAC7B,CAAC;IACH,CAAC;IAED,IAAI,SAAS;QACX,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,eAAe;QACb,OAAO,IAAI,UAAU,CAAC,IAAW,CAAC,CAAA;IACpC,CAAC;;AA7CkB,QAAQ;IAD5B,cAAc,CAAC,WAAW,CAAC;GACP,QAAQ,CA8C5B;eA9CoB,QAAQ","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\nimport { Component, ComponentNature, Container, RealObject, sceneComponent } from '@hatiolab/things-scene'\nimport {\n Placeable,\n type Alignment,\n type Heights,\n type PlacementArchetype\n} from '@operato/scene-base'\n\nimport { AsrsRack3D } from './asrs-rack-3d.js'\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [\n {\n type: 'number',\n label: 'levels',\n name: 'levels',\n placeholder: '# of vertical levels (default 4)'\n },\n {\n type: 'number',\n label: 'bays',\n name: 'bays',\n placeholder: '# of horizontal bays (default 5)'\n }\n ],\n help: 'scene/component/asrs-rack'\n}\n\nconst Base = Placeable(Container) as unknown as typeof Component\n\n/**\n * AsrsRack — a multi-level high-bay storage rack, the structural backbone of\n * an AS/RS (Automated Storage / Retrieval System).\n *\n * `levels` × `bays` cells form a vertical grid. Each cell holds one logistics\n * package (Pallet / Box / Parcel). A pair of AsrsRacks separated by an aisle\n * (where an AsrsCrane runs) is the typical AS/RS configuration; v1 ships the\n * single-rack unit and lets users compose multi-rack systems by placing them\n * side by side. A future `AsrsAisle` composite may bundle the pair + crane.\n *\n * **Placement**: `floor` archetype, full ceiling depth by default — AS/RS\n * racks typically span floor to ceiling, with levels sized to fit the tallest\n * pallet load. Users can shorten via explicit `state.depth` for warehouses\n * with smaller envelopes.\n *\n * **Container-based**. Cells host stored cargo as children — each child's\n * left/top within the rack's bounds determines which cell it occupies. The\n * stacking pass in `Placeable.computeDefaultZPos` ensures each child cargo's\n * z lands on the rack's overall bottom (parent.zPos + parent.depth = ceiling),\n * which isn't quite cell-level resolution — true per-cell z positioning is\n * a v3 concern (the cargo would need to know which cell-row it's in).\n *\n * No Legendable for v1 — racks are passive structures; their per-cell\n * occupancy state is implicit in the children, not a status flag.\n */\n@sceneComponent('asrs-rack')\nexport default class AsrsRack extends Base {\n static placement: PlacementArchetype = 'floor'\n static align: Alignment = 'bottom'\n static defaultDepth = (h: Heights) => h.ceiling - h.floor\n\n get nature() {\n return NATURE\n }\n\n get anchors() {\n return []\n }\n\n /** Operation cargo (pallets / boxes / parcels) goes in the rack's cells. */\n containable(component: Component) {\n const archetype = (component.constructor as any).placement\n if (archetype === 'operation') return true\n return component.isDescendible(this as any)\n }\n\n /**\n * 2D — top-down rectangle showing the rack footprint, with subdivisions\n * suggesting the bay layout (lines parallel to the aisle).\n */\n render(ctx: CanvasRenderingContext2D) {\n const { width, height, left, top } = this.state\n const bays = Math.max(1, Math.floor((this.state.bays as number) || 5))\n\n ctx.beginPath()\n // Outer rectangle\n ctx.rect(left, top, width, height)\n // Bay subdivisions (vertical lines)\n for (let i = 1; i < bays; i++) {\n const x = left + (width * i) / bays\n ctx.moveTo(x, top)\n ctx.lineTo(x, top + height)\n }\n }\n\n get fillStyle() {\n return '#a0a0a8'\n }\n\n buildRealObject(): RealObject | undefined {\n return new AsrsRack3D(this as any)\n }\n}\n"]}
1
+ {"version":3,"file":"asrs-rack.js","sourceRoot":"","sources":["../src/asrs-rack.ts"],"names":[],"mappings":";AAAA;;GAEG;AACH,OAAO,EAA8B,iBAAiB,EAAc,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAClH,OAAO,EACL,SAAS,EAIV,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAE9C,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,QAAQ;YACf,IAAI,EAAE,QAAQ;YACd,WAAW,EAAE,kCAAkC;SAChD;QACD;YACE,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,MAAM;YACb,IAAI,EAAE,MAAM;YACZ,WAAW,EAAE,kCAAkC;SAChD;KACF;IACD,IAAI,EAAE,2BAA2B;CAClC,CAAA;AAED,2FAA2F;AAC3F,mEAAmE;AACnE,uEAAuE;AACvE,MAAM,IAAI,GAAG,SAAS,CAAC,iBAAiB,CAAgC,CAAA;AAExE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEY,IAAM,QAAQ,GAAd,MAAM,QAAS,SAAQ,IAAI;IACxC,MAAM,CAAC,SAAS,GAAuB,OAAO,CAAA;IAC9C,MAAM,CAAC,KAAK,GAAc,QAAQ,CAAA;IAClC,MAAM,CAAC,YAAY,GAAG,CAAC,CAAU,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,KAAK,CAAA;IAEzD,IAAI,MAAM;QACR,OAAO,MAAM,CAAA;IACf,CAAC;IAED,IAAI,OAAO;QACT,OAAO,EAAE,CAAA;IACX,CAAC;IAED,4EAA4E;IAC5E,WAAW,CAAC,SAAoB;QAC9B,MAAM,SAAS,GAAI,SAAS,CAAC,WAAmB,CAAC,SAAS,CAAA;QAC1D,IAAI,SAAS,KAAK,WAAW;YAAE,OAAO,IAAI,CAAA;QAC1C,OAAO,SAAS,CAAC,aAAa,CAAC,IAAW,CAAC,CAAA;IAC7C,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,GAA6B;QAClC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAA;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAE,IAAI,CAAC,KAAK,CAAC,IAAe,IAAI,CAAC,CAAC,CAAC,CAAA;QAEtE,GAAG,CAAC,SAAS,EAAE,CAAA;QACf,kBAAkB;QAClB,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;QAClC,oCAAoC;QACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;YAC9B,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,CAAA;YACnC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;YAClB,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,CAAC,CAAA;QAC7B,CAAC;IACH,CAAC;IAED,IAAI,SAAS;QACX,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,eAAe;QACb,OAAO,IAAI,UAAU,CAAC,IAAW,CAAC,CAAA;IACpC,CAAC;;AA7CkB,QAAQ;IAD5B,cAAc,CAAC,WAAW,CAAC;GACP,QAAQ,CA8C5B;eA9CoB,QAAQ","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\nimport { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'\nimport {\n Placeable,\n type Alignment,\n type Heights,\n type PlacementArchetype\n} from '@operato/scene-base'\n\nimport { AsrsRack3D } from './asrs-rack-3d.js'\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [\n {\n type: 'number',\n label: 'levels',\n name: 'levels',\n placeholder: '# of vertical levels (default 4)'\n },\n {\n type: 'number',\n label: 'bays',\n name: 'bays',\n placeholder: '# of horizontal bays (default 5)'\n }\n ],\n help: 'scene/component/asrs-rack'\n}\n\n// `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),\n// which forces `isHTMLElement(): true` and trips the 3D pipeline's\n// addObject DOM-skip gate. ASRS rack lives only in the 3D scene graph.\nconst Base = Placeable(ContainerAbstract) as unknown as typeof Component\n\n/**\n * AsrsRack — a multi-level high-bay storage rack, the structural backbone of\n * an AS/RS (Automated Storage / Retrieval System).\n *\n * `levels` × `bays` cells form a vertical grid. Each cell holds one logistics\n * package (Pallet / Box / Parcel). A pair of AsrsRacks separated by an aisle\n * (where an AsrsCrane runs) is the typical AS/RS configuration; v1 ships the\n * single-rack unit and lets users compose multi-rack systems by placing them\n * side by side. A future `AsrsAisle` composite may bundle the pair + crane.\n *\n * **Placement**: `floor` archetype, full ceiling depth by default — AS/RS\n * racks typically span floor to ceiling, with levels sized to fit the tallest\n * pallet load. Users can shorten via explicit `state.depth` for warehouses\n * with smaller envelopes.\n *\n * **Container-based**. Cells host stored cargo as children — each child's\n * left/top within the rack's bounds determines which cell it occupies. The\n * stacking pass in `Placeable.computeDefaultZPos` ensures each child cargo's\n * z lands on the rack's overall bottom (parent.zPos + parent.depth = ceiling),\n * which isn't quite cell-level resolution — true per-cell z positioning is\n * a v3 concern (the cargo would need to know which cell-row it's in).\n *\n * No Legendable for v1 — racks are passive structures; their per-cell\n * occupancy state is implicit in the children, not a status flag.\n */\n@sceneComponent('asrs-rack')\nexport default class AsrsRack extends Base {\n static placement: PlacementArchetype = 'floor'\n static align: Alignment = 'bottom'\n static defaultDepth = (h: Heights) => h.ceiling - h.floor\n\n get nature() {\n return NATURE\n }\n\n get anchors() {\n return []\n }\n\n /** Operation cargo (pallets / boxes / parcels) goes in the rack's cells. */\n containable(component: Component) {\n const archetype = (component.constructor as any).placement\n if (archetype === 'operation') return true\n return component.isDescendible(this as any)\n }\n\n /**\n * 2D — top-down rectangle showing the rack footprint, with subdivisions\n * suggesting the bay layout (lines parallel to the aisle).\n */\n render(ctx: CanvasRenderingContext2D) {\n const { width, height, left, top } = this.state\n const bays = Math.max(1, Math.floor((this.state.bays as number) || 5))\n\n ctx.beginPath()\n // Outer rectangle\n ctx.rect(left, top, width, height)\n // Bay subdivisions (vertical lines)\n for (let i = 1; i < bays; i++) {\n const x = left + (width * i) / bays\n ctx.moveTo(x, top)\n ctx.lineTo(x, top + height)\n }\n }\n\n get fillStyle() {\n return '#a0a0a8'\n }\n\n buildRealObject(): RealObject | undefined {\n return new AsrsRack3D(this as any)\n }\n}\n"]}
package/dist/pallet.d.ts CHANGED
@@ -48,8 +48,22 @@ export default class Pallet extends Base {
48
48
  get anchors(): never[];
49
49
  /** Accept other operation-archetype cargo (boxes, parcels, smaller pallets) as stacked children. */
50
50
  containable(component: Component): boolean;
51
- /** 2D — top-down rectangle. */
51
+ /**
52
+ * 2D — top-down silhouette. Body is a flat rectangle (wood/plastic deck);
53
+ * `postrender()` adds the deck pattern + edge stroke so the pallet reads
54
+ * as a pallet instead of a featureless rectangle.
55
+ */
52
56
  render(ctx: CanvasRenderingContext2D): void;
57
+ /**
58
+ * Deck pattern + edge stroke. Wood: parallel slats with darker grooves
59
+ * between (typical EUR pallet deck). Plastic: cross-cutout pattern
60
+ * suggesting the molded reinforcement ribs.
61
+ *
62
+ * Slats run along the *short* axis of the rectangle (= along the longer
63
+ * stringer direction in real life), so a 1200×800 pallet shows multiple
64
+ * narrow slats across the 1200mm dimension — matching the EUR layout.
65
+ */
66
+ postrender(ctx: CanvasRenderingContext2D): void;
53
67
  get fillStyle(): string;
54
68
  buildRealObject(): RealObject | undefined;
55
69
  }
package/dist/pallet.js CHANGED
@@ -2,7 +2,7 @@ import { __decorate } from "tslib";
2
2
  /*
3
3
  * Copyright © HatioLab Inc. All rights reserved.
4
4
  */
5
- import { Container, sceneComponent } from '@hatiolab/things-scene';
5
+ import { ContainerAbstract, sceneComponent } from '@hatiolab/things-scene';
6
6
  import { Carriable, Legendable, Placeable } from '@operato/scene-base';
7
7
  import { Pallet3D } from './pallet-3d.js';
8
8
  const BODY_LEGEND = {
@@ -30,9 +30,14 @@ const NATURE = {
30
30
  help: 'scene/component/pallet'
31
31
  };
32
32
  // Carriable: a pallet can sit on AGV / Forklift / robot-arm gripper / Spot
33
- // and also accept boxes / parcels as children (Container base provides the
34
- // child-container behavior; Carriable only adds the holder-mount hook).
35
- const Base = Carriable(Legendable(Placeable(Container)));
33
+ // and also accept boxes / parcels as children (ContainerAbstract base
34
+ // provides the child-container behavior; Carriable only adds the
35
+ // holder-mount hook).
36
+ //
37
+ // `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),
38
+ // which forces `isHTMLElement(): true` and trips the 3D pipeline's
39
+ // addObject DOM-skip gate. Pallet renders only as a 3D mesh.
40
+ const Base = Carriable(Legendable(Placeable(ContainerAbstract)));
36
41
  /**
37
42
  * Pallet — a flat transport structure that goods are stacked and stored on.
38
43
  *
@@ -79,12 +84,72 @@ let Pallet = class Pallet extends Base {
79
84
  return true;
80
85
  return component.isDescendible(this);
81
86
  }
82
- /** 2D — top-down rectangle. */
87
+ /**
88
+ * 2D — top-down silhouette. Body is a flat rectangle (wood/plastic deck);
89
+ * `postrender()` adds the deck pattern + edge stroke so the pallet reads
90
+ * as a pallet instead of a featureless rectangle.
91
+ */
83
92
  render(ctx) {
84
93
  const { width, height, left, top } = this.state;
85
94
  ctx.beginPath();
86
95
  ctx.rect(left, top, width, height);
87
96
  }
97
+ /**
98
+ * Deck pattern + edge stroke. Wood: parallel slats with darker grooves
99
+ * between (typical EUR pallet deck). Plastic: cross-cutout pattern
100
+ * suggesting the molded reinforcement ribs.
101
+ *
102
+ * Slats run along the *short* axis of the rectangle (= along the longer
103
+ * stringer direction in real life), so a 1200×800 pallet shows multiple
104
+ * narrow slats across the 1200mm dimension — matching the EUR layout.
105
+ */
106
+ postrender(ctx) {
107
+ super.postrender?.(ctx);
108
+ const { width, height, left, top } = this.state;
109
+ const isPlastic = this.state.material === 'plastic';
110
+ ctx.save();
111
+ if (!isPlastic) {
112
+ // Wood — slats. Run them along the longer axis so the grooves are
113
+ // perpendicular to the longer side (typical pallet appearance).
114
+ const longAxisHorizontal = width >= height;
115
+ const slatCount = 5;
116
+ const grooveColor = '#7a4f25';
117
+ ctx.fillStyle = grooveColor;
118
+ if (longAxisHorizontal) {
119
+ // grooves vertical (X direction across the width)
120
+ const grooveW = Math.max(1, width * 0.012);
121
+ const slatW = (width - grooveW * (slatCount - 1)) / slatCount;
122
+ for (let i = 1; i < slatCount; i++) {
123
+ const x = left + i * slatW + (i - 1) * grooveW;
124
+ ctx.fillRect(x, top + height * 0.05, grooveW, height * 0.9);
125
+ }
126
+ }
127
+ else {
128
+ const grooveH = Math.max(1, height * 0.012);
129
+ const slatH = (height - grooveH * (slatCount - 1)) / slatCount;
130
+ for (let i = 1; i < slatCount; i++) {
131
+ const y = top + i * slatH + (i - 1) * grooveH;
132
+ ctx.fillRect(left + width * 0.05, y, width * 0.9, grooveH);
133
+ }
134
+ }
135
+ }
136
+ else {
137
+ // Plastic — cross + corner cutouts hint
138
+ ctx.strokeStyle = '#3a4956';
139
+ ctx.lineWidth = Math.max(1, Math.min(width, height) * 0.012);
140
+ ctx.beginPath();
141
+ ctx.moveTo(left + width * 0.5, top + height * 0.1);
142
+ ctx.lineTo(left + width * 0.5, top + height * 0.9);
143
+ ctx.moveTo(left + width * 0.1, top + height * 0.5);
144
+ ctx.lineTo(left + width * 0.9, top + height * 0.5);
145
+ ctx.stroke();
146
+ }
147
+ // Edge stroke
148
+ ctx.strokeStyle = isPlastic ? '#2a3946' : '#5e3818';
149
+ ctx.lineWidth = 1;
150
+ ctx.strokeRect(left, top, width, height);
151
+ ctx.restore();
152
+ }
88
153
  get fillStyle() {
89
154
  return this.state.bodyColor || '#a87644';
90
155
  }
@@ -1 +1 @@
1
- {"version":3,"file":"pallet.js","sourceRoot":"","sources":["../src/pallet.ts"],"names":[],"mappings":";AAAA;;GAEG;AACH,OAAO,EAA8B,SAAS,EAAc,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAC1G,OAAO,EACL,SAAS,EACT,UAAU,EACV,SAAS,EAIV,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAezC,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,wBAAwB;CAC/B,CAAA;AAED,2EAA2E;AAC3E,2EAA2E;AAC3E,wEAAwE;AACxE,MAAM,IAAI,GAAG,SAAS,CAAC,UAAU,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAgC,CAAA;AAEvF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEY,IAAM,MAAM,GAAZ,MAAM,MAAO,SAAQ,IAAI;IACtC,MAAM,CAAC,OAAO,GAAkC;QAC9C,SAAS,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE;KACrD,CAAA;IAED,MAAM,CAAC,SAAS,GAAuB,WAAW,CAAA;IAClD,MAAM,CAAC,KAAK,GAAc,QAAQ,CAAA;IAClC,MAAM,CAAC,YAAY,GAAG,GAAG,CAAA,CAAC,0DAA0D;IAEpF,IAAI,MAAM;QACR,OAAO,MAAM,CAAA;IACf,CAAC;IAED,IAAI,OAAO;QACT,OAAO,EAAE,CAAA;IACX,CAAC;IAED,oGAAoG;IACpG,WAAW,CAAC,SAAoB;QAC9B,MAAM,SAAS,GAAI,SAAS,CAAC,WAAmB,CAAC,SAAS,CAAA;QAC1D,IAAI,SAAS,KAAK,WAAW;YAAE,OAAO,IAAI,CAAA;QAC1C,OAAO,SAAS,CAAC,aAAa,CAAC,IAAW,CAAC,CAAA;IAC7C,CAAC;IAED,+BAA+B;IAC/B,MAAM,CAAC,GAA6B;QAClC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAA;QAC/C,GAAG,CAAC,SAAS,EAAE,CAAA;QACf,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;IACpC,CAAC;IAED,IAAI,SAAS;QACX,OAAQ,IAAI,CAAC,KAAK,CAAC,SAAoB,IAAI,SAAS,CAAA;IACtD,CAAC;IAED,eAAe;QACb,OAAO,IAAI,QAAQ,CAAC,IAAW,CAAC,CAAA;IAClC,CAAC;;AArCkB,MAAM;IAD1B,cAAc,CAAC,QAAQ,CAAC;GACJ,MAAM,CAsC1B;eAtCoB,MAAM","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\nimport { Component, ComponentNature, Container, RealObject, sceneComponent } 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\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 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 (Container base provides the\n// child-container behavior; Carriable only adds the holder-mount hook).\nconst Base = Carriable(Legendable(Placeable(Container))) as unknown as typeof Component\n\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 Base {\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 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 as any)\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 Pallet3D(this as any)\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 { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } 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\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 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.\nconst Base = Carriable(Legendable(Placeable(ContainerAbstract))) as unknown as typeof Component\n\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 Base {\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 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 as any)\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 as any).material as PalletMaterial) === '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 as any)\n }\n}\n"]}
package/dist/spot.js CHANGED
@@ -29,7 +29,7 @@
29
29
  * the top face of the pad (overrides default attachPointFor).
30
30
  */
31
31
  import { __decorate } from "tslib";
32
- import { Container, sceneComponent } from '@hatiolab/things-scene';
32
+ import { ContainerAbstract, sceneComponent } from '@hatiolab/things-scene';
33
33
  import { CarrierHolder, Placeable } from '@operato/scene-base';
34
34
  import { Spot3D } from './spot-3d.js';
35
35
  const NATURE = {
@@ -42,10 +42,14 @@ const NATURE = {
42
42
  properties: [],
43
43
  help: 'scene/component/spot'
44
44
  };
45
- // Container base — Spot accepts carrier children (parcel/box/pallet/...).
45
+ // ContainerAbstract base — Spot accepts carrier children (parcel/box/pallet/...).
46
46
  // CarrierHolder mixin only publishes the attach-point hook; the actual
47
- // child-list management comes from the things-scene Container.
48
- const Base = CarrierHolder(Placeable(Container));
47
+ // child-list management comes from things-scene's container abstract.
48
+ //
49
+ // `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),
50
+ // which forces `isHTMLElement(): true` and trips the 3D pipeline's
51
+ // addObject DOM-skip gate. Spot is purely 3D.
52
+ const Base = CarrierHolder(Placeable(ContainerAbstract));
49
53
  let Spot = class Spot extends Base {
50
54
  static placement = 'floor';
51
55
  static align = 'bottom';
package/dist/spot.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"spot.js","sourceRoot":"","sources":["../src/spot.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Spot — virtual pickup / drop zone.\n *\n * A modeling-time anchor for \"this is where things land\" — the destination\n * of a robot arm pick-and-place, the slot of an AGV stop, the staging\n * zone next to a conveyor. Spot itself does not move and does\n * not perform any logistics action; it only marks a location and accepts\n * carrier components as children.\n *\n * Visual identity:\n * - 2D: outlined rectangle with corner \"L\" marks (so it reads as a\n * virtual zone, not a solid object).\n * - 3D: a thin translucent floor pad — only the floor of the conceptual\n * box is rendered, the side walls are absent.\n *\n * Standard things-scene properties used (no component-specific extras —\n * keep the property-panel UX uniform with other components):\n * - `fillStyle` — pad / outline color (sole color source)\n * - `strokeStyle` — outline color override (defaults to fillStyle)\n * - `lineWidth` / `lineDash` — outline stroke style\n * - `alpha` — overall transparency, framework-applied\n * - `text` / `fontColor` / `fontSize` / `fontFamily` / `bold` / `italic`\n * — label rendered by the standard text pipeline\n * - `material3d` (3D) — metalness / roughness / castShadow / receiveShadow\n *\n * Role: `CarrierHolder` — accepts any Carrier as a child and lays it on\n * the top face of the pad (overrides default attachPointFor).\n */\n\nimport { Component, ComponentNature, Container, RealObject, sceneComponent } from '@hatiolab/things-scene'\nimport {\n CarrierHolder,\n Placeable,\n type Alignment,\n type Heights,\n type PlacementArchetype\n} from '@operato/scene-base'\n\nimport { Spot3D } from './spot-3d.js'\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n // No component-specific properties — fillStyle / strokeStyle / lineWidth /\n // alpha / text / font* are framework-standard, surfaced by the property\n // panel automatically.\n properties: [],\n help: 'scene/component/spot'\n}\n\n// Container base — Spot accepts carrier children (parcel/box/pallet/...).\n// CarrierHolder mixin only publishes the attach-point hook; the actual\n// child-list management comes from the things-scene Container.\nconst Base = CarrierHolder(Placeable(Container)) as unknown as typeof Component\n\n@sceneComponent('spot')\nexport default class Spot extends Base {\n static placement: PlacementArchetype = 'floor'\n static align: Alignment = 'bottom'\n static defaultDepth = (_h: Heights) => 2 // a thin pad\n\n get nature(): ComponentNature {\n return NATURE\n }\n\n get anchors() {\n return []\n }\n\n /**\n * 2D — outlined rectangle + corner L marks. The pad body is drawn with a\n * fixed low alpha (0.15) on top of the user's `fillStyle` so the zone reads\n * as virtual even when fillStyle is fully opaque. Outline + corner marks\n * use `strokeStyle` (or fall back to `fillStyle`) at the user's `lineWidth`\n * and `lineDash`. The text label is drawn by the framework's standard\n * postrender pipeline using `text` / `fontColor` / `fontSize` / etc.\n */\n render(ctx: CanvasRenderingContext2D) {\n const { left = 0, top = 0, width = 100, height = 100 } = this.state\n const fillStyle = (this.state.fillStyle as string) || '#3a8fbd'\n const strokeStyle = (this.state.strokeStyle as string) || fillStyle\n const lineWidth = numOr(this.state.lineWidth, 1)\n const lineDashStyle = String(this.state.lineDash ?? 'dash')\n\n // ── Pad body (fixed-low-alpha tint of fillStyle) ───────────────────\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 = lineWidth\n applyLineDash(ctx, lineDashStyle, lineWidth)\n ctx.strokeRect(\n left + lineWidth / 2,\n top + lineWidth / 2,\n width - lineWidth,\n height - lineWidth\n )\n ctx.setLineDash([])\n ctx.restore()\n\n // ── Corner L marks (solid, slightly heavier than outline) ──────────\n const ml = Math.min(width, height) * 0.18\n const cornerW = Math.max(lineWidth * 1.5, 1.5)\n ctx.save()\n ctx.strokeStyle = strokeStyle\n ctx.lineWidth = cornerW\n for (const [cx, cy, sx, sy] of [\n [left, top, 1, 1],\n [left + width, top, -1, 1],\n [left + width, top + height, -1, -1],\n [left, top + height, 1, -1]\n ] as [number, number, number, number][]) {\n ctx.beginPath()\n ctx.moveTo(cx + sx * ml, cy)\n ctx.lineTo(cx, cy)\n ctx.lineTo(cx, cy + sy * ml)\n ctx.stroke()\n }\n ctx.restore()\n }\n\n buildRealObject(): RealObject | undefined {\n return new Spot3D(this as any)\n }\n\n /**\n * Mount carriers on the TOP of the pad (Spot3D's `getAttachFrame` is\n * already at pad-top in spot-local). Then lift the carrier by its\n * own halfDepth so the carrier's BOTTOM rests ON the pad surface, not\n * its volumetric center — without this lift, half the carrier would\n * sink below the pad / floor.\n *\n * Reads `_realObject.effectiveDepth` first (the framework-resolved\n * value, accounting for `static defaultDepth` and parent context),\n * falling back to raw `state.depth` for components built before\n * RealObject creation.\n */\n attachPointFor(carrier: Component) {\n const ro = (this as any)._realObject as Spot3D | undefined\n const frame = ro?.getAttachFrame?.()\n if (!frame) return undefined\n const carrierDepth = resolveDepth(carrier)\n return {\n attach: frame,\n localPosition: { x: 0, y: carrierDepth / 2, z: 0 }\n }\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 return numOr((c as any)?.state?.depth, 0)\n}\n\nfunction numOr(v: unknown, dflt: number): number {\n return typeof v === 'number' && Number.isFinite(v) ? v : dflt\n}\n\n/**\n * Map a things-scene `lineDash` string to a Canvas dash pattern. Mirrors\n * the keys understood by things-scene's `drawer/stroke.ts` so users see\n * consistent options across components. Unknown strings fall through to\n * a plain dashed pattern instead of throwing on setLineDash.\n */\nfunction applyLineDash(ctx: CanvasRenderingContext2D, style: string, lw: number) {\n switch (style) {\n case 'solid':\n ctx.setLineDash([])\n return\n case 'round-dot':\n ctx.setLineDash([0.1, lw * 2])\n ctx.lineCap = 'round'\n return\n case 'square-dot':\n ctx.setLineDash([lw, lw])\n return\n case 'long-dash':\n ctx.setLineDash([lw * 6, lw * 3])\n return\n case 'dash-dot':\n ctx.setLineDash([lw * 4, lw * 2, lw, lw * 2])\n return\n case 'dash':\n default:\n ctx.setLineDash([lw * 4, lw * 1.5])\n return\n }\n}\n"]}
1
+ {"version":3,"file":"spot.js","sourceRoot":"","sources":["../src/spot.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Spot — virtual pickup / drop zone.\n *\n * A modeling-time anchor for \"this is where things land\" — the destination\n * of a robot arm pick-and-place, the slot of an AGV stop, the staging\n * zone next to a conveyor. Spot itself does not move and does\n * not perform any logistics action; it only marks a location and accepts\n * carrier components as children.\n *\n * Visual identity:\n * - 2D: outlined rectangle with corner \"L\" marks (so it reads as a\n * virtual zone, not a solid object).\n * - 3D: a thin translucent floor pad — only the floor of the conceptual\n * box is rendered, the side walls are absent.\n *\n * Standard things-scene properties used (no component-specific extras —\n * keep the property-panel UX uniform with other components):\n * - `fillStyle` — pad / outline color (sole color source)\n * - `strokeStyle` — outline color override (defaults to fillStyle)\n * - `lineWidth` / `lineDash` — outline stroke style\n * - `alpha` — overall transparency, framework-applied\n * - `text` / `fontColor` / `fontSize` / `fontFamily` / `bold` / `italic`\n * — label rendered by the standard text pipeline\n * - `material3d` (3D) — metalness / roughness / castShadow / receiveShadow\n *\n * Role: `CarrierHolder` — accepts any Carrier as a child and lays it on\n * the top face of the pad (overrides default attachPointFor).\n */\n\nimport { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'\nimport {\n CarrierHolder,\n Placeable,\n type Alignment,\n type Heights,\n type PlacementArchetype\n} from '@operato/scene-base'\n\nimport { Spot3D } from './spot-3d.js'\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n // No component-specific properties — fillStyle / strokeStyle / lineWidth /\n // alpha / text / font* are framework-standard, surfaced by the property\n // panel automatically.\n properties: [],\n help: 'scene/component/spot'\n}\n\n// ContainerAbstract base — Spot accepts carrier children (parcel/box/pallet/...).\n// CarrierHolder mixin only publishes the attach-point hook; the actual\n// child-list management comes from things-scene's container abstract.\n//\n// `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),\n// which forces `isHTMLElement(): true` and trips the 3D pipeline's\n// addObject DOM-skip gate. Spot is purely 3D.\nconst Base = CarrierHolder(Placeable(ContainerAbstract)) as unknown as typeof Component\n\n@sceneComponent('spot')\nexport default class Spot extends Base {\n static placement: PlacementArchetype = 'floor'\n static align: Alignment = 'bottom'\n static defaultDepth = (_h: Heights) => 2 // a thin pad\n\n get nature(): ComponentNature {\n return NATURE\n }\n\n get anchors() {\n return []\n }\n\n /**\n * 2D — outlined rectangle + corner L marks. The pad body is drawn with a\n * fixed low alpha (0.15) on top of the user's `fillStyle` so the zone reads\n * as virtual even when fillStyle is fully opaque. Outline + corner marks\n * use `strokeStyle` (or fall back to `fillStyle`) at the user's `lineWidth`\n * and `lineDash`. The text label is drawn by the framework's standard\n * postrender pipeline using `text` / `fontColor` / `fontSize` / etc.\n */\n render(ctx: CanvasRenderingContext2D) {\n const { left = 0, top = 0, width = 100, height = 100 } = this.state\n const fillStyle = (this.state.fillStyle as string) || '#3a8fbd'\n const strokeStyle = (this.state.strokeStyle as string) || fillStyle\n const lineWidth = numOr(this.state.lineWidth, 1)\n const lineDashStyle = String(this.state.lineDash ?? 'dash')\n\n // ── Pad body (fixed-low-alpha tint of fillStyle) ───────────────────\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 = lineWidth\n applyLineDash(ctx, lineDashStyle, lineWidth)\n ctx.strokeRect(\n left + lineWidth / 2,\n top + lineWidth / 2,\n width - lineWidth,\n height - lineWidth\n )\n ctx.setLineDash([])\n ctx.restore()\n\n // ── Corner L marks (solid, slightly heavier than outline) ──────────\n const ml = Math.min(width, height) * 0.18\n const cornerW = Math.max(lineWidth * 1.5, 1.5)\n ctx.save()\n ctx.strokeStyle = strokeStyle\n ctx.lineWidth = cornerW\n for (const [cx, cy, sx, sy] of [\n [left, top, 1, 1],\n [left + width, top, -1, 1],\n [left + width, top + height, -1, -1],\n [left, top + height, 1, -1]\n ] as [number, number, number, number][]) {\n ctx.beginPath()\n ctx.moveTo(cx + sx * ml, cy)\n ctx.lineTo(cx, cy)\n ctx.lineTo(cx, cy + sy * ml)\n ctx.stroke()\n }\n ctx.restore()\n }\n\n buildRealObject(): RealObject | undefined {\n return new Spot3D(this as any)\n }\n\n /**\n * Mount carriers on the TOP of the pad (Spot3D's `getAttachFrame` is\n * already at pad-top in spot-local). Then lift the carrier by its\n * own halfDepth so the carrier's BOTTOM rests ON the pad surface, not\n * its volumetric center — without this lift, half the carrier would\n * sink below the pad / floor.\n *\n * Reads `_realObject.effectiveDepth` first (the framework-resolved\n * value, accounting for `static defaultDepth` and parent context),\n * falling back to raw `state.depth` for components built before\n * RealObject creation.\n */\n attachPointFor(carrier: Component) {\n const ro = (this as any)._realObject as Spot3D | undefined\n const frame = ro?.getAttachFrame?.()\n if (!frame) return undefined\n const carrierDepth = resolveDepth(carrier)\n return {\n attach: frame,\n localPosition: { x: 0, y: carrierDepth / 2, z: 0 }\n }\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 return numOr((c as any)?.state?.depth, 0)\n}\n\nfunction numOr(v: unknown, dflt: number): number {\n return typeof v === 'number' && Number.isFinite(v) ? v : dflt\n}\n\n/**\n * Map a things-scene `lineDash` string to a Canvas dash pattern. Mirrors\n * the keys understood by things-scene's `drawer/stroke.ts` so users see\n * consistent options across components. Unknown strings fall through to\n * a plain dashed pattern instead of throwing on setLineDash.\n */\nfunction applyLineDash(ctx: CanvasRenderingContext2D, style: string, lw: number) {\n switch (style) {\n case 'solid':\n ctx.setLineDash([])\n return\n case 'round-dot':\n ctx.setLineDash([0.1, lw * 2])\n ctx.lineCap = 'round'\n return\n case 'square-dot':\n ctx.setLineDash([lw, lw])\n return\n case 'long-dash':\n ctx.setLineDash([lw * 6, lw * 3])\n return\n case 'dash-dot':\n ctx.setLineDash([lw * 4, lw * 2, lw, lw * 2])\n return\n case 'dash':\n default:\n ctx.setLineDash([lw * 4, lw * 1.5])\n return\n }\n}\n"]}
@@ -20,7 +20,7 @@ export default [
20
20
  type: 'pallet',
21
21
  top: 100,
22
22
  left: 100,
23
- width: 120,
23
+ width: 80,
24
24
  height: 80,
25
25
  material: 'wood'
26
26
  }
@@ -34,7 +34,7 @@ export default [
34
34
  type: 'pallet',
35
35
  top: 100,
36
36
  left: 250,
37
- width: 120,
37
+ width: 80,
38
38
  height: 80,
39
39
  material: 'plastic'
40
40
  }
@@ -77,7 +77,7 @@ export default [
77
77
  top: 100,
78
78
  left: 640,
79
79
  width: 60,
80
- height: 90
80
+ height: 80
81
81
  }
82
82
  },
83
83
  {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * things-scene catalog templates for the storage domain — pallet/box/parcel\n * variants, ASRS rack/crane, and the virtual `spot` placement marker.\n */\nimport spot from './spot.js'\nconst palletWood = new URL('../../icons/pallet-wood.png', import.meta.url).href\nconst palletPlastic = new URL('../../icons/pallet-plastic.png', import.meta.url).href\nconst boxWood = new URL('../../icons/box-wood.png', import.meta.url).href\nconst boxPlastic = new URL('../../icons/box-plastic.png', import.meta.url).href\nconst parcel = new URL('../../icons/parcel.png', import.meta.url).href\nconst asrsRack = new URL('../../icons/asrs-rack.png', import.meta.url).href\nconst asrsCrane = new URL('../../icons/asrs-crane.png', import.meta.url).href\n\nexport default [\n {\n type: 'pallet',\n description: 'wood pallet (EUR / EPAL)',\n group: 'storage',\n icon: palletWood,\n model: {\n type: 'pallet',\n top: 100,\n left: 100,\n width: 120,\n height: 80,\n material: 'wood'\n }\n },\n {\n type: 'pallet',\n description: 'plastic pallet',\n group: 'storage',\n icon: palletPlastic,\n model: {\n type: 'pallet',\n top: 100,\n left: 250,\n width: 120,\n height: 80,\n material: 'plastic'\n }\n },\n {\n type: 'box',\n description: 'wood crate',\n group: 'storage',\n icon: boxWood,\n model: {\n type: 'box',\n top: 100,\n left: 400,\n width: 80,\n height: 80,\n material: 'wood'\n }\n },\n {\n type: 'box',\n description: 'plastic tote',\n group: 'storage',\n icon: boxPlastic,\n model: {\n type: 'box',\n top: 100,\n left: 520,\n width: 80,\n height: 80,\n material: 'plastic'\n }\n },\n {\n type: 'parcel',\n description: 'cardboard parcel',\n group: 'storage',\n icon: parcel,\n model: {\n type: 'parcel',\n top: 100,\n left: 640,\n width: 60,\n height: 90\n }\n },\n {\n type: 'asrs-rack',\n description: 'AS/RS storage rack (multi-level)',\n group: 'storage',\n icon: asrsRack,\n model: {\n type: 'asrs-rack',\n top: 300,\n left: 100,\n width: 800,\n height: 200,\n levels: 4,\n bays: 5\n }\n },\n {\n type: 'asrs-crane',\n description: 'AS/RS stacker crane',\n group: 'storage',\n icon: asrsCrane,\n model: {\n type: 'asrs-crane',\n top: 550,\n left: 400,\n width: 100,\n height: 200,\n status: 'idle',\n carriageHeight: 100\n }\n },\n spot\n]\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * things-scene catalog templates for the storage domain — pallet/box/parcel\n * variants, ASRS rack/crane, and the virtual `spot` placement marker.\n */\nimport spot from './spot.js'\nconst palletWood = new URL('../../icons/pallet-wood.png', import.meta.url).href\nconst palletPlastic = new URL('../../icons/pallet-plastic.png', import.meta.url).href\nconst boxWood = new URL('../../icons/box-wood.png', import.meta.url).href\nconst boxPlastic = new URL('../../icons/box-plastic.png', import.meta.url).href\nconst parcel = new URL('../../icons/parcel.png', import.meta.url).href\nconst asrsRack = new URL('../../icons/asrs-rack.png', import.meta.url).href\nconst asrsCrane = new URL('../../icons/asrs-crane.png', import.meta.url).href\n\nexport default [\n {\n type: 'pallet',\n description: 'wood pallet (EUR / EPAL)',\n group: 'storage',\n icon: palletWood,\n model: {\n type: 'pallet',\n top: 100,\n left: 100,\n width: 80,\n height: 80,\n material: 'wood'\n }\n },\n {\n type: 'pallet',\n description: 'plastic pallet',\n group: 'storage',\n icon: palletPlastic,\n model: {\n type: 'pallet',\n top: 100,\n left: 250,\n width: 80,\n height: 80,\n material: 'plastic'\n }\n },\n {\n type: 'box',\n description: 'wood crate',\n group: 'storage',\n icon: boxWood,\n model: {\n type: 'box',\n top: 100,\n left: 400,\n width: 80,\n height: 80,\n material: 'wood'\n }\n },\n {\n type: 'box',\n description: 'plastic tote',\n group: 'storage',\n icon: boxPlastic,\n model: {\n type: 'box',\n top: 100,\n left: 520,\n width: 80,\n height: 80,\n material: 'plastic'\n }\n },\n {\n type: 'parcel',\n description: 'cardboard parcel',\n group: 'storage',\n icon: parcel,\n model: {\n type: 'parcel',\n top: 100,\n left: 640,\n width: 60,\n height: 80\n }\n },\n {\n type: 'asrs-rack',\n description: 'AS/RS storage rack (multi-level)',\n group: 'storage',\n icon: asrsRack,\n model: {\n type: 'asrs-rack',\n top: 300,\n left: 100,\n width: 800,\n height: 200,\n levels: 4,\n bays: 5\n }\n },\n {\n type: 'asrs-crane',\n description: 'AS/RS stacker crane',\n group: 'storage',\n icon: asrsCrane,\n model: {\n type: 'asrs-crane',\n top: 550,\n left: 400,\n width: 100,\n height: 200,\n status: 'idle',\n carriageHeight: 100\n }\n },\n spot\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.22",
5
+ "version": "10.0.0-beta.24",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "module": "dist/index.js",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@hatiolab/things-scene": "^10.0.0-beta.1",
28
- "@operato/scene-base": "^10.0.0-beta.22",
28
+ "@operato/scene-base": "^10.0.0-beta.24",
29
29
  "three": "^0.183.0"
30
30
  },
31
31
  "devDependencies": {
@@ -40,5 +40,5 @@
40
40
  "typescript": "^5.0.4"
41
41
  },
42
42
  "prettier": "@hatiolab/prettier-config",
43
- "gitHead": "f48e52f4f5fdc30ec06af9da7cf253f6e29cfb0e"
43
+ "gitHead": "236ded8c545c4b172b2e86e4843b3221c4bf2fd8"
44
44
  }
package/src/asrs-rack.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * Copyright © HatioLab Inc. All rights reserved.
3
3
  */
4
- import { Component, ComponentNature, Container, RealObject, sceneComponent } from '@hatiolab/things-scene'
4
+ import { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'
5
5
  import {
6
6
  Placeable,
7
7
  type Alignment,
@@ -32,7 +32,10 @@ const NATURE: ComponentNature = {
32
32
  help: 'scene/component/asrs-rack'
33
33
  }
34
34
 
35
- const Base = Placeable(Container) as unknown as typeof Component
35
+ // `ContainerAbstract` (not `Container`) Container = MixinHTMLElement(ContainerAbstract),
36
+ // which forces `isHTMLElement(): true` and trips the 3D pipeline's
37
+ // addObject DOM-skip gate. ASRS rack lives only in the 3D scene graph.
38
+ const Base = Placeable(ContainerAbstract) as unknown as typeof Component
36
39
 
37
40
  /**
38
41
  * AsrsRack — a multi-level high-bay storage rack, the structural backbone of
package/src/pallet.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * Copyright © HatioLab Inc. All rights reserved.
3
3
  */
4
- import { Component, ComponentNature, Container, RealObject, sceneComponent } from '@hatiolab/things-scene'
4
+ import { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'
5
5
  import {
6
6
  Carriable,
7
7
  Legendable,
@@ -53,9 +53,14 @@ const NATURE: ComponentNature = {
53
53
  }
54
54
 
55
55
  // Carriable: a pallet can sit on AGV / Forklift / robot-arm gripper / Spot
56
- // and also accept boxes / parcels as children (Container base provides the
57
- // child-container behavior; Carriable only adds the holder-mount hook).
58
- const Base = Carriable(Legendable(Placeable(Container))) as unknown as typeof Component
56
+ // and also accept boxes / parcels as children (ContainerAbstract base
57
+ // provides the child-container behavior; Carriable only adds the
58
+ // holder-mount hook).
59
+ //
60
+ // `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),
61
+ // which forces `isHTMLElement(): true` and trips the 3D pipeline's
62
+ // addObject DOM-skip gate. Pallet renders only as a 3D mesh.
63
+ const Base = Carriable(Legendable(Placeable(ContainerAbstract))) as unknown as typeof Component
59
64
 
60
65
  /**
61
66
  * Pallet — a flat transport structure that goods are stacked and stored on.
@@ -108,13 +113,77 @@ export default class Pallet extends Base {
108
113
  return component.isDescendible(this as any)
109
114
  }
110
115
 
111
- /** 2D — top-down rectangle. */
116
+ /**
117
+ * 2D — top-down silhouette. Body is a flat rectangle (wood/plastic deck);
118
+ * `postrender()` adds the deck pattern + edge stroke so the pallet reads
119
+ * as a pallet instead of a featureless rectangle.
120
+ */
112
121
  render(ctx: CanvasRenderingContext2D) {
113
122
  const { width, height, left, top } = this.state
114
123
  ctx.beginPath()
115
124
  ctx.rect(left, top, width, height)
116
125
  }
117
126
 
127
+ /**
128
+ * Deck pattern + edge stroke. Wood: parallel slats with darker grooves
129
+ * between (typical EUR pallet deck). Plastic: cross-cutout pattern
130
+ * suggesting the molded reinforcement ribs.
131
+ *
132
+ * Slats run along the *short* axis of the rectangle (= along the longer
133
+ * stringer direction in real life), so a 1200×800 pallet shows multiple
134
+ * narrow slats across the 1200mm dimension — matching the EUR layout.
135
+ */
136
+ postrender(ctx: CanvasRenderingContext2D) {
137
+ super.postrender?.(ctx)
138
+
139
+ const { width, height, left, top } = this.state
140
+ const isPlastic = ((this.state as any).material as PalletMaterial) === 'plastic'
141
+
142
+ ctx.save()
143
+
144
+ if (!isPlastic) {
145
+ // Wood — slats. Run them along the longer axis so the grooves are
146
+ // perpendicular to the longer side (typical pallet appearance).
147
+ const longAxisHorizontal = width >= height
148
+ const slatCount = 5
149
+ const grooveColor = '#7a4f25'
150
+ ctx.fillStyle = grooveColor
151
+ if (longAxisHorizontal) {
152
+ // grooves vertical (X direction across the width)
153
+ const grooveW = Math.max(1, width * 0.012)
154
+ const slatW = (width - grooveW * (slatCount - 1)) / slatCount
155
+ for (let i = 1; i < slatCount; i++) {
156
+ const x = left + i * slatW + (i - 1) * grooveW
157
+ ctx.fillRect(x, top + height * 0.05, grooveW, height * 0.9)
158
+ }
159
+ } else {
160
+ const grooveH = Math.max(1, height * 0.012)
161
+ const slatH = (height - grooveH * (slatCount - 1)) / slatCount
162
+ for (let i = 1; i < slatCount; i++) {
163
+ const y = top + i * slatH + (i - 1) * grooveH
164
+ ctx.fillRect(left + width * 0.05, y, width * 0.9, grooveH)
165
+ }
166
+ }
167
+ } else {
168
+ // Plastic — cross + corner cutouts hint
169
+ ctx.strokeStyle = '#3a4956'
170
+ ctx.lineWidth = Math.max(1, Math.min(width, height) * 0.012)
171
+ ctx.beginPath()
172
+ ctx.moveTo(left + width * 0.5, top + height * 0.1)
173
+ ctx.lineTo(left + width * 0.5, top + height * 0.9)
174
+ ctx.moveTo(left + width * 0.1, top + height * 0.5)
175
+ ctx.lineTo(left + width * 0.9, top + height * 0.5)
176
+ ctx.stroke()
177
+ }
178
+
179
+ // Edge stroke
180
+ ctx.strokeStyle = isPlastic ? '#2a3946' : '#5e3818'
181
+ ctx.lineWidth = 1
182
+ ctx.strokeRect(left, top, width, height)
183
+
184
+ ctx.restore()
185
+ }
186
+
118
187
  get fillStyle() {
119
188
  return (this.state.bodyColor as string) || '#a87644'
120
189
  }
package/src/spot.ts CHANGED
@@ -29,7 +29,7 @@
29
29
  * the top face of the pad (overrides default attachPointFor).
30
30
  */
31
31
 
32
- import { Component, ComponentNature, Container, RealObject, sceneComponent } from '@hatiolab/things-scene'
32
+ import { Component, ComponentNature, ContainerAbstract, RealObject, sceneComponent } from '@hatiolab/things-scene'
33
33
  import {
34
34
  CarrierHolder,
35
35
  Placeable,
@@ -51,10 +51,14 @@ const NATURE: ComponentNature = {
51
51
  help: 'scene/component/spot'
52
52
  }
53
53
 
54
- // Container base — Spot accepts carrier children (parcel/box/pallet/...).
54
+ // ContainerAbstract base — Spot accepts carrier children (parcel/box/pallet/...).
55
55
  // CarrierHolder mixin only publishes the attach-point hook; the actual
56
- // child-list management comes from the things-scene Container.
57
- const Base = CarrierHolder(Placeable(Container)) as unknown as typeof Component
56
+ // child-list management comes from things-scene's container abstract.
57
+ //
58
+ // `ContainerAbstract` (not `Container`) — Container = MixinHTMLElement(ContainerAbstract),
59
+ // which forces `isHTMLElement(): true` and trips the 3D pipeline's
60
+ // addObject DOM-skip gate. Spot is purely 3D.
61
+ const Base = CarrierHolder(Placeable(ContainerAbstract)) as unknown as typeof Component
58
62
 
59
63
  @sceneComponent('spot')
60
64
  export default class Spot extends Base {
@@ -21,7 +21,7 @@ export default [
21
21
  type: 'pallet',
22
22
  top: 100,
23
23
  left: 100,
24
- width: 120,
24
+ width: 80,
25
25
  height: 80,
26
26
  material: 'wood'
27
27
  }
@@ -35,7 +35,7 @@ export default [
35
35
  type: 'pallet',
36
36
  top: 100,
37
37
  left: 250,
38
- width: 120,
38
+ width: 80,
39
39
  height: 80,
40
40
  material: 'plastic'
41
41
  }
@@ -78,7 +78,7 @@ export default [
78
78
  top: 100,
79
79
  left: 640,
80
80
  width: 60,
81
- height: 90
81
+ height: 80
82
82
  }
83
83
  },
84
84
  {