@operato/scene-storage 10.0.0-beta.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +59 -0
  3. package/dist/asrs-crane-3d.d.ts +7 -0
  4. package/dist/asrs-crane-3d.js +164 -0
  5. package/dist/asrs-crane-3d.js.map +1 -0
  6. package/dist/asrs-crane.d.ts +47 -0
  7. package/dist/asrs-crane.js +104 -0
  8. package/dist/asrs-crane.js.map +1 -0
  9. package/dist/asrs-rack-3d.d.ts +7 -0
  10. package/dist/asrs-rack-3d.js +129 -0
  11. package/dist/asrs-rack-3d.js.map +1 -0
  12. package/dist/asrs-rack.d.ts +45 -0
  13. package/dist/asrs-rack.js +99 -0
  14. package/dist/asrs-rack.js.map +1 -0
  15. package/dist/box-3d.d.ts +11 -0
  16. package/dist/box-3d.js +166 -0
  17. package/dist/box-3d.js.map +1 -0
  18. package/dist/box.d.ts +36 -0
  19. package/dist/box.js +73 -0
  20. package/dist/box.js.map +1 -0
  21. package/dist/index.d.ts +10 -0
  22. package/dist/index.js +11 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/pallet-3d.d.ts +11 -0
  25. package/dist/pallet-3d.js +162 -0
  26. package/dist/pallet-3d.js.map +1 -0
  27. package/dist/pallet.d.ts +56 -0
  28. package/dist/pallet.js +99 -0
  29. package/dist/pallet.js.map +1 -0
  30. package/dist/parcel-3d.d.ts +7 -0
  31. package/dist/parcel-3d.js +82 -0
  32. package/dist/parcel-3d.js.map +1 -0
  33. package/dist/parcel.d.ts +30 -0
  34. package/dist/parcel.js +67 -0
  35. package/dist/parcel.js.map +1 -0
  36. package/dist/spot-3d.d.ts +30 -0
  37. package/dist/spot-3d.js +176 -0
  38. package/dist/spot-3d.js.map +1 -0
  39. package/dist/spot.d.ts +41 -0
  40. package/dist/spot.js +177 -0
  41. package/dist/spot.js.map +1 -0
  42. package/dist/templates/index.d.ts +92 -0
  43. package/dist/templates/index.js +115 -0
  44. package/dist/templates/index.js.map +1 -0
  45. package/dist/templates/spot.d.ts +24 -0
  46. package/dist/templates/spot.js +26 -0
  47. package/dist/templates/spot.js.map +1 -0
  48. package/icons/asrs-crane.png +0 -0
  49. package/icons/asrs-rack.png +0 -0
  50. package/icons/box-plastic.png +0 -0
  51. package/icons/box-wood.png +0 -0
  52. package/icons/pallet-plastic.png +0 -0
  53. package/icons/pallet-wood.png +0 -0
  54. package/icons/parcel.png +0 -0
  55. package/package.json +44 -0
  56. package/src/asrs-crane-3d.ts +191 -0
  57. package/src/asrs-crane.ts +130 -0
  58. package/src/asrs-rack-3d.ts +146 -0
  59. package/src/asrs-rack.ts +109 -0
  60. package/src/box-3d.ts +189 -0
  61. package/src/box.ts +99 -0
  62. package/src/index.ts +17 -0
  63. package/src/pallet-3d.ts +181 -0
  64. package/src/pallet.ts +125 -0
  65. package/src/parcel-3d.ts +90 -0
  66. package/src/parcel.ts +76 -0
  67. package/src/spot-3d.ts +200 -0
  68. package/src/spot.ts +197 -0
  69. package/src/templates/index.ts +115 -0
  70. package/src/templates/spot.ts +26 -0
  71. package/things-scene.config.js +5 -0
  72. package/translations/en.json +12 -0
  73. package/translations/ja.json +12 -0
  74. package/translations/ko.json +12 -0
  75. package/translations/ms.json +12 -0
  76. package/translations/zh.json +12 -0
  77. package/tsconfig.json +23 -0
  78. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +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"]}
@@ -0,0 +1,24 @@
1
+ declare const _default: {
2
+ type: string;
3
+ description: string;
4
+ group: string;
5
+ icon: string;
6
+ model: {
7
+ type: string;
8
+ top: number;
9
+ left: number;
10
+ width: number;
11
+ height: number;
12
+ text: string;
13
+ fillStyle: string;
14
+ strokeStyle: string;
15
+ lineWidth: number;
16
+ lineDash: string;
17
+ alpha: number;
18
+ fontColor: string;
19
+ fontSize: number;
20
+ fontFamily: string;
21
+ bold: boolean;
22
+ };
23
+ };
24
+ export default _default;
@@ -0,0 +1,26 @@
1
+ // Reuse parcel.png as a placeholder icon until a dedicated spot icon is drawn.
2
+ const icon = new URL('../../icons/parcel.png', import.meta.url).href;
3
+ export default {
4
+ type: 'spot',
5
+ description: 'virtual pickup / drop zone — translucent floor pad that accepts carrier components as children',
6
+ group: 'storage' /* line|shape|textAndMedia|chartAndGauge|table|container|dataSource|3D|facility|storage|conveyance|transport|manufacturing|form|etc */,
7
+ icon,
8
+ model: {
9
+ type: 'spot',
10
+ top: 200,
11
+ left: 400,
12
+ width: 80,
13
+ height: 80,
14
+ text: 'SPOT_A',
15
+ fillStyle: '#3a8fbd',
16
+ strokeStyle: '#3a8fbd',
17
+ lineWidth: 1,
18
+ lineDash: 'dash',
19
+ alpha: 1,
20
+ fontColor: '#3a8fbd',
21
+ fontSize: 16,
22
+ fontFamily: 'sans-serif',
23
+ bold: true
24
+ }
25
+ };
26
+ //# sourceMappingURL=spot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spot.js","sourceRoot":"","sources":["../../src/templates/spot.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,wBAAwB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAA;AAEpE,eAAe;IACb,IAAI,EAAE,MAAM;IACZ,WAAW,EAAE,gGAAgG;IAC7G,KAAK,EAAE,SAAS,CAAC,sIAAsI;IACvJ,IAAI;IACJ,KAAK,EAAE;QACL,IAAI,EAAE,MAAM;QACZ,GAAG,EAAE,GAAG;QACR,IAAI,EAAE,GAAG;QACT,KAAK,EAAE,EAAE;QACT,MAAM,EAAE,EAAE;QACV,IAAI,EAAE,QAAQ;QACd,SAAS,EAAE,SAAS;QACpB,WAAW,EAAE,SAAS;QACtB,SAAS,EAAE,CAAC;QACZ,QAAQ,EAAE,MAAM;QAChB,KAAK,EAAE,CAAC;QACR,SAAS,EAAE,SAAS;QACpB,QAAQ,EAAE,EAAE;QACZ,UAAU,EAAE,YAAY;QACxB,IAAI,EAAE,IAAI;KACX;CACF,CAAA","sourcesContent":["// Reuse parcel.png as a placeholder icon until a dedicated spot icon is drawn.\nconst icon = new URL('../../icons/parcel.png', import.meta.url).href\n\nexport default {\n type: 'spot',\n description: 'virtual pickup / drop zone — translucent floor pad that accepts carrier components as children',\n group: 'storage' /* line|shape|textAndMedia|chartAndGauge|table|container|dataSource|3D|facility|storage|conveyance|transport|manufacturing|form|etc */,\n icon,\n model: {\n type: 'spot',\n top: 200,\n left: 400,\n width: 80,\n height: 80,\n text: 'SPOT_A',\n fillStyle: '#3a8fbd',\n strokeStyle: '#3a8fbd',\n lineWidth: 1,\n lineDash: 'dash',\n alpha: 1,\n fontColor: '#3a8fbd',\n fontSize: 16,\n fontFamily: 'sans-serif',\n bold: true\n }\n}\n"]}
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@operato/scene-storage",
3
+ "description": "Storage-domain components for things-scene (smart factory / logistics) — pallet, box, parcel; AS/RS and shelves planned.",
4
+ "author": "heartyoh",
5
+ "version": "10.0.0-beta.22",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "module": "dist/index.js",
9
+ "license": "MIT",
10
+ "things-scene": true,
11
+ "publishConfig": {
12
+ "access": "public",
13
+ "@operato:registry": "https://registry.npmjs.org"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/things-scene/operato-scene.git",
18
+ "directory": "packages/storage"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "prepublishOnly": "tsc",
23
+ "lint": "eslint src/ && prettier \"src/**/*.ts\" --check",
24
+ "format": "eslint src/ --fix && prettier \"src/**/*.ts\" --write"
25
+ },
26
+ "dependencies": {
27
+ "@hatiolab/things-scene": "^10.0.0-beta.1",
28
+ "@operato/scene-base": "^10.0.0-beta.22",
29
+ "three": "^0.183.0"
30
+ },
31
+ "devDependencies": {
32
+ "@hatiolab/prettier-config": "^1.0.0",
33
+ "@types/three": "^0.183.0",
34
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
35
+ "@typescript-eslint/parser": "^8.0.0",
36
+ "eslint": "^9.18.0",
37
+ "eslint-config-prettier": "^10.0.1",
38
+ "prettier": "^3.2.5",
39
+ "tslib": "^2.3.1",
40
+ "typescript": "^5.0.4"
41
+ },
42
+ "prettier": "@hatiolab/prettier-config",
43
+ "gitHead": "f48e52f4f5fdc30ec06af9da7cf253f6e29cfb0e"
44
+ }
@@ -0,0 +1,191 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * AsrsCrane 3D — stacker crane for AS/RS aisles.
5
+ *
6
+ * LO-POLY but structurally complete. The signature parts:
7
+ *
8
+ * - tall vertical mast (the dominant element — runs floor → ceiling)
9
+ * - base unit (motor housing at floor level, runs on the floor rail)
10
+ * - top guide (sliding block at ceiling that runs on the ceiling rail —
11
+ * stabilizes the mast under acceleration)
12
+ * - carriage (horizontal block that slides up/down the mast at
13
+ * `state.carriageHeight`)
14
+ * - shuttle/fork attachment on the carriage (extends sideways into a cell
15
+ * to extract / insert a pallet)
16
+ * - status lamp on top of the base unit
17
+ *
18
+ * The base unit + top guide combo is what visually distinguishes a stacker
19
+ * crane from a forklift — the stacker is *constrained* to the aisle, riding
20
+ * rails top and bottom, and that constraint is the visual signature.
21
+ */
22
+
23
+ import * as THREE from 'three'
24
+ import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
25
+ import { RealObjectGroup } from '@hatiolab/things-scene'
26
+
27
+ const MAST_COLOR = 0x4a5060
28
+ const BASE_COLOR = 0x33394a
29
+ const SHUTTLE_COLOR = 0x222233
30
+ const RAIL_COLOR = 0x222222
31
+
32
+ export class AsrsCrane3D extends RealObjectGroup {
33
+ build() {
34
+ super.build()
35
+
36
+ const { width, height, depth = 200 } = this.component.state
37
+ const bodyColor = (this.component.state.bodyColor as string) || '#888'
38
+ const emissiveColor = (this.component.state.lampEmissive as string) || '#222222'
39
+ const status = this.component.state.status
40
+ const lampIntensity = status && status !== 'idle' ? 1.5 : 0.2
41
+ // Clamp carriageHeight to the available mast travel — keeps the carriage
42
+ // inside the shaft even if state.carriageHeight is stale (e.g. left over
43
+ // from an older height-scale convention).
44
+ const carriageRaw = (this.component.state.carriageHeight as number) ?? depth * 0.4
45
+ const carriageHeight = Math.max(0, Math.min(carriageRaw, depth * 0.85))
46
+
47
+ const baseY = -depth / 2
48
+
49
+ // Proportions
50
+ const baseH = depth * 0.10
51
+ const topGuideH = depth * 0.05
52
+ const mastH = depth - baseH - topGuideH
53
+ const mastW = width * 0.35
54
+ const mastD = height * 0.35
55
+
56
+ const bodyMaterial = new THREE.MeshStandardMaterial({
57
+ color: bodyColor,
58
+ metalness: 0.4,
59
+ roughness: 0.5
60
+ })
61
+ const mastMaterial = new THREE.MeshStandardMaterial({
62
+ color: MAST_COLOR,
63
+ metalness: 0.85,
64
+ roughness: 0.3
65
+ })
66
+ const baseMaterial = new THREE.MeshStandardMaterial({
67
+ color: BASE_COLOR,
68
+ metalness: 0.7,
69
+ roughness: 0.4
70
+ })
71
+ const shuttleMaterial = new THREE.MeshStandardMaterial({
72
+ color: SHUTTLE_COLOR,
73
+ metalness: 0.85,
74
+ roughness: 0.3
75
+ })
76
+
77
+ // ── Floor rail (visible track under the base) ─────────────────────
78
+ const railH = baseH * 0.25
79
+ const railGeo = new THREE.BoxGeometry(width * 1.15, railH, mastD * 0.4)
80
+ const railMesh = new THREE.Mesh(
81
+ railGeo,
82
+ new THREE.MeshStandardMaterial({ color: RAIL_COLOR, metalness: 0.9, roughness: 0.3 })
83
+ )
84
+ railMesh.position.set(0, baseY + railH / 2, 0)
85
+ railMesh.receiveShadow = true
86
+ this.object3d.add(railMesh)
87
+
88
+ // ── Base unit (motor housing on floor rail) ───────────────────────
89
+ const baseGeo = new THREE.BoxGeometry(width * 0.95, baseH, height * 0.85)
90
+ const baseMesh = new THREE.Mesh(baseGeo, baseMaterial)
91
+ baseMesh.position.set(0, baseY + railH + baseH / 2, 0)
92
+ baseMesh.castShadow = true
93
+ baseMesh.receiveShadow = true
94
+ this.object3d.add(baseMesh)
95
+
96
+ // Body color tint band on base unit (subtle status indication)
97
+ const tintH = baseH * 0.15
98
+ const tintGeo = new THREE.BoxGeometry(width * 0.95, tintH, height * 0.85)
99
+ const tintMaterial = new THREE.MeshStandardMaterial({
100
+ color: bodyColor,
101
+ transparent: true,
102
+ opacity: 0.6,
103
+ metalness: 0.1,
104
+ roughness: 0.6
105
+ })
106
+ const tintMesh = new THREE.Mesh(tintGeo, tintMaterial)
107
+ tintMesh.position.set(0, baseY + railH + baseH - tintH / 2, 0)
108
+ this.object3d.add(tintMesh)
109
+
110
+ // ── Mast (vertical column from base top to ceiling) ───────────────
111
+ const mastY = baseY + railH + baseH + mastH / 2
112
+ const mastGeo = new THREE.BoxGeometry(mastW, mastH, mastD)
113
+ const mastMesh = new THREE.Mesh(mastGeo, mastMaterial)
114
+ mastMesh.position.set(0, mastY, 0)
115
+ mastMesh.castShadow = true
116
+ this.object3d.add(mastMesh)
117
+
118
+ // ── Carriage (horizontal block sliding on mast at carriageHeight) ─
119
+ const carriageW = width * 0.85
120
+ const carriageH = baseH * 0.7
121
+ const carriageD = mastD * 1.4
122
+ const carriageY = baseY + railH + baseH + carriageHeight + carriageH / 2
123
+ const carriageGeo = new THREE.BoxGeometry(carriageW, carriageH, carriageD)
124
+ const carriageMesh = new THREE.Mesh(carriageGeo, bodyMaterial)
125
+ carriageMesh.position.set(0, carriageY, 0)
126
+ carriageMesh.castShadow = true
127
+ this.object3d.add(carriageMesh)
128
+
129
+ // ── Shuttle / fork attachment on carriage (extends sideways) ──────
130
+ const shuttleW = width * 1.05
131
+ const shuttleH = carriageH * 0.5
132
+ const shuttleD = carriageD * 0.6
133
+ const shuttleGeo = new THREE.BoxGeometry(shuttleW, shuttleH, shuttleD)
134
+ const shuttleMesh = new THREE.Mesh(shuttleGeo, shuttleMaterial)
135
+ shuttleMesh.position.set(0, carriageY - carriageH / 2 - shuttleH / 2, 0)
136
+ shuttleMesh.castShadow = true
137
+ this.object3d.add(shuttleMesh)
138
+
139
+ // ── Top guide (sliding block on ceiling rail) ─────────────────────
140
+ const topGuideGeo = new THREE.BoxGeometry(width * 0.7, topGuideH, height * 0.6)
141
+ const topGuideMesh = new THREE.Mesh(topGuideGeo, baseMaterial)
142
+ topGuideMesh.position.set(0, baseY + depth - topGuideH / 2, 0)
143
+ topGuideMesh.castShadow = true
144
+ this.object3d.add(topGuideMesh)
145
+
146
+ // Ceiling rail (small visual cue at very top)
147
+ const ceilingRailGeo = new THREE.BoxGeometry(width * 1.15, topGuideH * 0.4, mastD * 0.4)
148
+ const ceilingRailMesh = new THREE.Mesh(
149
+ ceilingRailGeo,
150
+ new THREE.MeshStandardMaterial({ color: RAIL_COLOR, metalness: 0.9, roughness: 0.3 })
151
+ )
152
+ ceilingRailMesh.position.set(0, baseY + depth - topGuideH * 0.2, 0)
153
+ this.object3d.add(ceilingRailMesh)
154
+
155
+ // ── Status lamp on top of base unit ───────────────────────────────
156
+ const lampR = Math.min(width, height) * 0.04
157
+ const lampH = lampR * 1.5
158
+ const lampMaterial = new THREE.MeshStandardMaterial({
159
+ color: emissiveColor,
160
+ emissive: emissiveColor,
161
+ emissiveIntensity: lampIntensity,
162
+ metalness: 0,
163
+ roughness: 0.3
164
+ })
165
+ const lampGeo = new THREE.CylinderGeometry(lampR, lampR * 0.85, lampH, 12)
166
+ const lampMesh = new THREE.Mesh(lampGeo, lampMaterial)
167
+ // Place lamp near the corner of the base, away from the mast
168
+ lampMesh.position.set(width * 0.3, baseY + railH + baseH + lampH / 2, height * 0.3)
169
+ this.object3d.add(lampMesh)
170
+ }
171
+
172
+ updateDimension() {}
173
+
174
+ onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
175
+ if (
176
+ 'status' in after ||
177
+ 'bodyColor' in after ||
178
+ 'lampEmissive' in after ||
179
+ 'carriageHeight' in after ||
180
+ 'width' in after ||
181
+ 'height' in after ||
182
+ 'depth' in after
183
+ ) {
184
+ this.update()
185
+ return
186
+ }
187
+ super.onchange(after, before)
188
+ }
189
+
190
+ updateAlpha() {}
191
+ }
@@ -0,0 +1,130 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+ import { Component, ComponentNature, RealObject, RectPath, Shape, sceneComponent } from '@hatiolab/things-scene'
5
+ import {
6
+ Legendable,
7
+ Placeable,
8
+ type Alignment,
9
+ type Heights,
10
+ type LegendBinding,
11
+ type PlacementArchetype
12
+ } from '@operato/scene-base'
13
+
14
+ import { AsrsCrane3D } from './asrs-crane-3d.js'
15
+
16
+ /**
17
+ * AsrsCrane status — the operating state of a stacker crane in an AS/RS aisle.
18
+ *
19
+ * - `idle` — parked at home position
20
+ * - `moving` — translating along the aisle (horizontal) or along the
21
+ * mast (vertical); the two motions are typically combined
22
+ * - `loading` — extracting a pallet from a rack cell
23
+ * - `unloading` — placing a pallet into a rack cell
24
+ * - `error` — fault / e-stop / collision warning
25
+ */
26
+ export type AsrsCraneStatus = 'idle' | 'moving' | 'loading' | 'unloading' | 'error'
27
+
28
+ const BODY_LEGEND = {
29
+ idle: '#888',
30
+ moving: '#aabbcc',
31
+ loading: '#ffaa00',
32
+ unloading: '#ffaa00',
33
+ error: '#c66',
34
+ default: '#888'
35
+ }
36
+
37
+ const LAMP_EMISSIVE_LEGEND = {
38
+ idle: '#222222',
39
+ moving: '#44ff44',
40
+ loading: '#ffaa00',
41
+ unloading: '#ffaa00',
42
+ error: '#ff3333',
43
+ default: '#222222'
44
+ }
45
+
46
+ const NATURE: ComponentNature = {
47
+ mutable: false,
48
+ resizable: true,
49
+ rotatable: true,
50
+ properties: [
51
+ {
52
+ type: 'select',
53
+ label: 'status',
54
+ name: 'status',
55
+ property: {
56
+ options: [
57
+ { display: 'Idle', value: 'idle' },
58
+ { display: 'Moving', value: 'moving' },
59
+ { display: 'Loading', value: 'loading' },
60
+ { display: 'Unloading', value: 'unloading' },
61
+ { display: 'Error', value: 'error' }
62
+ ]
63
+ }
64
+ },
65
+ {
66
+ type: 'number',
67
+ label: 'carriage-height',
68
+ name: 'carriageHeight',
69
+ placeholder: 'mm — height of carriage on mast'
70
+ }
71
+ ],
72
+ help: 'scene/component/asrs-crane'
73
+ }
74
+
75
+ const Base = Legendable(Placeable(RectPath(Shape))) as unknown as typeof Component
76
+
77
+ /**
78
+ * AsrsCrane — the stacker / retrieval crane that runs in the aisle of an
79
+ * AS/RS, moving cargo between the load port and the rack cells.
80
+ *
81
+ * Structure: a tall vertical mast that translates along a floor + ceiling
82
+ * rail (the aisle), with a carriage that slides up/down the mast carrying a
83
+ * shuttle / forks. The whole assembly's footprint is narrow (mast width)
84
+ * but its visual height is full ceiling — by far the tallest single
85
+ * component in a typical scene.
86
+ *
87
+ * Currently Shape-based (no children). The carrier the crane is *currently
88
+ * carrying* is best modeled via data binding (a `currentCarrier` data field
89
+ * on the crane → looked up to a Pallet/Box elsewhere in the scene), as in
90
+ * fmsim's CarrierManager pattern. Adding the carrier as a child would mix
91
+ * static placement with the dynamic data-driven flow we deliberately keep
92
+ * separate (see Phase A4 commit notes).
93
+ */
94
+ @sceneComponent('asrs-crane')
95
+ export default class AsrsCrane extends Base {
96
+ static legends: Record<string, LegendBinding> = {
97
+ bodyColor: { from: 'status', legend: BODY_LEGEND },
98
+ lampEmissive: { from: 'status', legend: LAMP_EMISSIVE_LEGEND }
99
+ }
100
+
101
+ static placement: PlacementArchetype = 'floor'
102
+ static align: Alignment = 'bottom'
103
+ static defaultDepth = (h: Heights) => h.ceiling - h.floor
104
+
105
+ get nature() {
106
+ return NATURE
107
+ }
108
+
109
+ get anchors() {
110
+ return []
111
+ }
112
+
113
+ /**
114
+ * 2D — top-down rectangle showing the crane's footprint along the aisle.
115
+ * The crane is much taller than wide, so the 2D mark is small.
116
+ */
117
+ render(ctx: CanvasRenderingContext2D) {
118
+ const { width, height, left, top } = this.state
119
+ ctx.beginPath()
120
+ ctx.rect(left, top, width, height)
121
+ }
122
+
123
+ get fillStyle() {
124
+ return (this.state.bodyColor as string) || '#888'
125
+ }
126
+
127
+ buildRealObject(): RealObject | undefined {
128
+ return new AsrsCrane3D(this as any)
129
+ }
130
+ }
@@ -0,0 +1,146 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * AsrsRack 3D — multi-level high-bay storage rack.
5
+ *
6
+ * LO-POLY but visually unambiguous as a rack. The signature parts:
7
+ *
8
+ * - 4 corner uprights (vertical posts running floor → top)
9
+ * - intermediate uprights between bays (one between each adjacent bay pair)
10
+ * - horizontal beams at each level on both front and back faces (defining
11
+ * the cell decks)
12
+ * - diagonal cross-bracing on the back face (the "X" pattern that says
13
+ * this is a load-bearing storage rack, not just a generic frame)
14
+ *
15
+ * No floor / ceiling panels — the rack is open by design (cells are accessed
16
+ * by the stacker crane from the front/aisle side).
17
+ *
18
+ * Cargo (pallets, boxes) added as children render at their own z position.
19
+ * The rack itself is purely structural geometry.
20
+ */
21
+
22
+ import * as THREE from 'three'
23
+ import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
24
+ import { RealObjectGroup } from '@hatiolab/things-scene'
25
+
26
+ const POST_COLOR = 0x6a7080
27
+ const BEAM_COLOR = 0x556070
28
+ const BRACE_COLOR = 0x556070
29
+
30
+ export class AsrsRack3D extends RealObjectGroup {
31
+ build() {
32
+ super.build()
33
+
34
+ const { width, height, depth = 3000 } = this.component.state
35
+ const levels = Math.max(1, Math.floor((this.component.state.levels as number) || 4))
36
+ const bays = Math.max(1, Math.floor((this.component.state.bays as number) || 5))
37
+
38
+ const baseY = -depth / 2
39
+ const postW = Math.min(width / bays, height) * 0.06
40
+ const beamH = depth * 0.025
41
+ const braceT = postW * 0.6
42
+
43
+ const postMaterial = new THREE.MeshStandardMaterial({
44
+ color: POST_COLOR,
45
+ metalness: 0.7,
46
+ roughness: 0.4
47
+ })
48
+ const beamMaterial = new THREE.MeshStandardMaterial({
49
+ color: BEAM_COLOR,
50
+ metalness: 0.7,
51
+ roughness: 0.4
52
+ })
53
+ const braceMaterial = new THREE.MeshStandardMaterial({
54
+ color: BRACE_COLOR,
55
+ metalness: 0.7,
56
+ roughness: 0.4
57
+ })
58
+
59
+ // ── Uprights (vertical posts at every bay boundary) ──────────────
60
+ // bays + 1 vertical positions; for each, one front post + one back post.
61
+ const postGeos: THREE.BufferGeometry[] = []
62
+ for (let i = 0; i <= bays; i++) {
63
+ const xFrac = i / bays - 0.5
64
+ const x = xFrac * width
65
+ // Front + back posts
66
+ for (const zSign of [-1, 1]) {
67
+ const post = new THREE.BoxGeometry(postW, depth, postW)
68
+ post.translate(x, 0, zSign * (height / 2 - postW / 2))
69
+ postGeos.push(post)
70
+ }
71
+ }
72
+ const postMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(postGeos), postMaterial)
73
+ postMesh.castShadow = true
74
+ postMesh.receiveShadow = true
75
+ this.object3d.add(postMesh)
76
+
77
+ // ── Horizontal beams (front + back faces at each level) ──────────
78
+ // levels + 1 vertical positions (level 0 = ground, level N = top).
79
+ const beamGeos: THREE.BufferGeometry[] = []
80
+ for (let lv = 0; lv <= levels; lv++) {
81
+ const yFrac = lv / levels
82
+ const y = baseY + yFrac * depth - beamH / 2 + (lv === 0 ? beamH : 0)
83
+
84
+ for (const zSign of [-1, 1]) {
85
+ const beam = new THREE.BoxGeometry(width, beamH, beamH)
86
+ beam.translate(0, y, zSign * (height / 2 - beamH / 2))
87
+ beamGeos.push(beam)
88
+ }
89
+ }
90
+ const beamMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(beamGeos), beamMaterial)
91
+ beamMesh.castShadow = true
92
+ beamMesh.receiveShadow = true
93
+ this.object3d.add(beamMesh)
94
+
95
+ // ── Diagonal cross-bracing on the back face (the "X" pattern) ────
96
+ // Two diagonals per level — "/" and "\" — making an X across each
97
+ // bay-tall cell. Visual signature of a load-bearing rack.
98
+ const braceGeos: THREE.BufferGeometry[] = []
99
+ const cellW = width / bays
100
+ const cellH = depth / levels
101
+ const braceLen = Math.sqrt(cellW * cellW + cellH * cellH)
102
+ const braceAngle = Math.atan2(cellH, cellW)
103
+ const backZ = height / 2 - postW * 0.6
104
+
105
+ for (let bay = 0; bay < bays; bay++) {
106
+ // Brace only every other bay to keep things visually open
107
+ if (bay % 2 !== 0) continue
108
+
109
+ const cellCenterX = (bay - bays / 2 + 0.5) * cellW
110
+
111
+ for (let lv = 0; lv < levels; lv++) {
112
+ const cellCenterY = baseY + (lv + 0.5) * cellH
113
+
114
+ for (const sign of [-1, 1]) {
115
+ const brace = new THREE.BoxGeometry(braceLen, braceT, braceT)
116
+ brace.rotateZ(sign * braceAngle)
117
+ brace.translate(cellCenterX, cellCenterY, backZ)
118
+ braceGeos.push(brace)
119
+ }
120
+ }
121
+ }
122
+ if (braceGeos.length > 0) {
123
+ const braceMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(braceGeos), braceMaterial)
124
+ braceMesh.castShadow = true
125
+ this.object3d.add(braceMesh)
126
+ }
127
+ }
128
+
129
+ updateDimension() {}
130
+
131
+ onchange(after: Record<string, unknown>, before: Record<string, unknown>) {
132
+ if (
133
+ 'levels' in after ||
134
+ 'bays' in after ||
135
+ 'width' in after ||
136
+ 'height' in after ||
137
+ 'depth' in after
138
+ ) {
139
+ this.update()
140
+ return
141
+ }
142
+ super.onchange(after, before)
143
+ }
144
+
145
+ updateAlpha() {}
146
+ }