@hyprune/schema 0.6.0 → 0.7.0

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
@@ -185,3 +185,116 @@ definition exists in 0.16 and the graphics properties differ only by the new key
185
185
  ## @hyprune/schema 0.6.0 (2026-10-11)
186
186
 
187
187
  Publishes IPC 0.20 and world format 0.8 to npm.
188
+
189
+ ## Object manifests 0.9, libraries and the library release record (RFC-0011)
190
+
191
+ New contract files; no IPC or world format change (world formats stay 0.1–0.8).
192
+ Object manifests use the world format numbering: a world may use objects whose
193
+ `formatVersion` is at most its own.
194
+
195
+ - `v0.9/object.schema.json` (`object.json`): `id` (slug, equals the directory
196
+ name), SemVer `version`, `name`, `category`, `tags`, a directory-relative
197
+ `scene`, `license` + `attribution`, rest-pose `bounds`, optional `thumbnail`
198
+ and `lighting`, `traits` and optional `slots`. Traits: `decorative` (0.8 node
199
+ animations), `collidable` (0.8 proxy), `movable` (`surfaces`, `upright`,
200
+ `scale`), `character` (measured `clips` with `walk` and `idle`, `height`,
201
+ `radius`, `accent`, asset-level `say`/`panel` interactions) and `seat`
202
+ (anchors, `capacity`). `mover`, `machine`, `mount` and `interactive` are
203
+ reserved and rejected with a reason. Slots `states`, `labels`, `spawns` and
204
+ `effects` (at most 8 each) are validated and inert.
205
+ - `v0.9/library.schema.json` (`library.json`): reverse-domain `id`, `version`,
206
+ `name`, `license` + `attribution` and 1–256 listed `objects`.
207
+ - `v0.9/library-releases.schema.json`: the meta repository's
208
+ `libraries/releases.json`, one record per `id@version` (`id`, `version`,
209
+ `formatVersion`, `url`, `sha256`, `nixHash`, `published`).
210
+ - **Draft** `v0.10/object.schema.json` and `v0.10/library.schema.json`: 0.9 plus
211
+ the `mover` trait, RFC-0013's `machine` trait (typed `inputs` with `repeat`,
212
+ `drives` in the mapping language, `primary`, `positions`, `recipes`) and the
213
+ slot additions (`labels` font/align/colour/plate, `spawns` track/items/maxRate,
214
+ `effects` kind/colour/durationMs, new `tracks`, `stacks`, `toggles`, `ports`).
215
+ It may change until world format 0.10 is defined.
216
+ - `validate('object', manifest, undefined, format?, options)`: trait
217
+ combinations, origin conventions (floor, wall and ceiling within 1 cm; a
218
+ character's rest-pose feet within 5 cm), slot
219
+ ids, poses inside the bounds grown by 0.5 m, palettes, seat anchors, track
220
+ points and arcs, stack layouts, machine input references, drive targets and
221
+ output domains. With `options.gltf`: the format's glTF extension rules, node
222
+ names (unique, in the scene; the proxy may sit outside it), the 0.8 animation
223
+ and proxy rules, states-slot materials and nodes (one slot each),
224
+ `KHR_materials_variants` mappings (variant names of a states slot are palette
225
+ ids), character skins, clips and `extras.speed` (2%), triangle budgets
226
+ (50,000; characters 16,000; items 2,000; proxy 2,000) and bounds (exact for
227
+ axis-aligned meshes, else a containing box). `options.files`, `thumbnail`,
228
+ `measured` and `directory` add the directory limits, attribution coverage, the
229
+ 256 x 256 thumbnail, the 1 cm bounds check and texture and GPU budgets.
230
+ Results carry `warnings`.
231
+ - `validate('library', manifest, undefined, format?, {objects, files})` and
232
+ `validate('library-releases', record)`. `objectFormats` and `objectLimits` are
233
+ exported. The CLI accepts `object` (with an optional glTF/GLB), `library` and
234
+ `library-releases`.
235
+
236
+ ## IPC 0.21: markers, personal quests and focus sessions
237
+
238
+ - New grant `quest.control` (hello capabilities): every quest and focus mutation
239
+ and `guidance.set`.
240
+ - Required state keys: `markers {reveal, persistent, calm, coreDraw, list}`
241
+ (reveal `holdMs` 0 = toggle or 1000..15000, `fadeMs` 0..600; at most 64
242
+ markers with tier 1..4, labels up to 48 characters, one target of position,
243
+ zone, area, place or workspace), `quests {enabled, tracked, active[<=16],
244
+ counts, world: null}` and `focus {state, pausedState, sessionId,
245
+ phaseStartedAtMs, phaseEndsAtMs, phaseMs, remainingMs, creditedMs, crediting,
246
+ cycle, cycles, breakKind, countMode, target, calm}`. Times are monotonic ms.
247
+ - Methods: `markers.reveal {durationMs?}` (shell.control); `guidance.get`,
248
+ `guidance.set {patch}` returning `{config, defaults, overrides, ranges,
249
+ options, path, error}` over 21 flat dotted keys; `quests.list {states?,
250
+ sources?}`, `quests.get {questRef}`, `quests.track {questRef|null}`,
251
+ `quests.accept/abandon/resume/delete {questRef}`, `quests.create {quest}`
252
+ (`{revision, questRef}`), `quests.update {questRef, quest}`, `quests.mark
253
+ {questRef, stepId, objectiveId}`; `focus.start {target?, questRef?,
254
+ objectiveId?, overrides?}` (`{revision, sessionId}`),
255
+ `focus.pause/resume/skip/stop {}`, `focus.history {days?}` (at most 512
256
+ intervals). Personal quests: 1..16 steps, 1..8 `manual`, `reach` or `focus`
257
+ objectives per step.
258
+ - Events: `quest.progress` and `focus.phase` (`focusSessionId`; every event
259
+ carries the IPC `sessionId` and `revision`).
260
+ - `inputConfig` world mode gains `markers.reveal`; `overlayStyle` gains
261
+ required `questColor` (optional in the patch); `spatial.projection` markers
262
+ gain optional `markerId`.
263
+ - File schemas: `v0.21/quests-personal.schema.json`,
264
+ `v0.21/guidance.schema.json`, `v0.21/focus-history.schema.json`.
265
+ - Everything else is inherited from 0.20 unchanged; `session.hello` reports
266
+ `0.21`. Inheritance test included.
267
+
268
+ ## IPC 0.22: object catalog, user placements and libraries (RFC-0011 P1)
269
+
270
+ - New grant `objects.control` (hello capabilities): every overlay change and
271
+ library install/remove.
272
+ - Required state key `objects {catalogRevision, instances[<=512], pocket[<=16],
273
+ usage, limits}`. An instance is `{id, source (authored|user), ref, name,
274
+ areaId, pose, scale, state (active|hidden|missing|blocked|loading|suspended),
275
+ movable, locked, rule, path, activity, seat}`; `path`, `activity` and `seat`
276
+ are always null until P2. `usage`/`limits` count this world's user
277
+ placements (placements, assets, triangles, gpuBytes, collisionTriangles).
278
+ - Methods: `objects.catalog {}` (`{revision, entries[<=1024]}`; an entry carries
279
+ ref, source, id, version, formatVersion, name, description, category, tags,
280
+ traits, surfaces, scale range, bounds, triangles, gpuBytes,
281
+ collisionTriangles, an absolute thumbnail path, license, library, other
282
+ versions, state, placeable, reason and up to 8 errors);
283
+ `objects.pocket {ref}` (`{revision, itemId}`); `objects.place {ref, pose?,
284
+ scale?}` (`{revision, id}`); `objects.transform {id, pose, scale?}`;
285
+ `objects.remove {id}|{all:true}`; `objects.hide {id, hidden}`;
286
+ `objects.reset {id}|{all:true}`; `objects.configure {id, patch:{locked}}`;
287
+ `libraries.list {}`; `libraries.install {archive, sha256}` (`{revision, id,
288
+ version}`); `libraries.remove {id, version}`. Refs are
289
+ `package/<asset>`, `library/<id>@<semver>/<object>` or `user/<object>`.
290
+ - `editor.selectedKind` gains `node`; `editor.submode` gains `universal`;
291
+ `editor.unplaced` gains `objects` (pocket items, `count` up to 32); new
292
+ required `editor.gizmo {space, pivot, snap}`.
293
+ - Error `data` may carry `rule` (the placement rule or limit that refused a
294
+ method).
295
+ - Everything else is inherited from 0.21 unchanged; `session.hello` reports
296
+ `0.22`. Inheritance test included.
297
+
298
+ ## @hyprune/schema 0.7.0 (2026-10-12)
299
+
300
+ Publishes IPC 0.21 (markers, personal quests, focus sessions) and IPC 0.22 (object catalog, user placements, libraries), plus the object manifest (`object.json` 0.9 and the 0.10 draft), library and library release-record schemas.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **Hyprune** is an explorable, world-driven desktop environment for Hyprland: your workspaces become places you can walk through, with live windows on the walls. This repository holds the versioned **schemas** for world packages, IPC and manifests, and their validator. Install Hyprune through the [hyprune](https://github.com/hyprune/hyprune) repository (starter kits, Nix, Arch); learn more at [hyprune.com](https://hyprune.com). Licence: MIT.
4
4
 
5
- Versioned contracts for Hyprune, a world-driven Hyprland desktop: world packages (formats 0.1–0.8), the IPC protocol (0.1–0.19), extension and screen-type manifests and the input keymap. JSON Schema dialect: **2020-12**. Every version is frozen once published; core, the shell and the SDK pin these exact files. Readable reference: [hyprune.com/reference/schema](https://hyprune.com/reference/schema/).
5
+ Versioned contracts for Hyprune, a world-driven Hyprland desktop: world packages (formats 0.1–0.8), the IPC protocol (0.1–0.22), object and library manifests (0.9, draft 0.10), extension and screen-type manifests and the input keymap. JSON Schema dialect: **2020-12**. Every version is frozen once published; core, the shell and the SDK pin these exact files. Readable reference: [hyprune.com/reference/schema](https://hyprune.com/reference/schema/).
6
6
 
7
7
  ## Install
8
8
 
@@ -156,3 +156,55 @@ stays `editor.unplaced.windows`. `spatial.map` gains optional `levels` (1-6
156
156
  per-level blueprint SVGs). `extensions/blueprint.schema.json` defines the world
157
157
  extension `com.hyprune.blueprint` 0.1 that supplies them. Every other 0.19
158
158
  definition is unchanged. See the RFC-0004 and RFC-0005 amendments for IPC 0.20.
159
+
160
+ IPC **0.21** adds markers, personal quests and focus sessions (RFC-0012 P1). Three
161
+ new required state keys: `markers` (the marker reveal, the persistent-marker mode
162
+ and up to 64 resolved markers), `quests` (tracked and up to 16 active quests,
163
+ counts) and `focus` (the focus session phase, monotonic phase times, credited
164
+ time, cycles and target). New methods `markers.reveal` (shell.control),
165
+ `guidance.get`/`guidance.set` (marker, quest and focus settings, the `player.get`
166
+ pattern), `quests.list/get/track/accept/abandon/resume/create/update/delete/mark`
167
+ and `focus.start/pause/resume/skip/stop/history`; mutations need the new
168
+ `quest.control` grant. Events `quest.progress` and `focus.phase`. The world keymap
169
+ gains `markers.reveal`, the overlay style a required `questColor`, and
170
+ `spatial.projection` markers an optional `markerId`. File schemas in `v0.21/`:
171
+ `quests-personal.schema.json` (`quests/personal.json`), `guidance.schema.json`
172
+ (`guidance.json`) and `focus-history.schema.json` (one `focus/history.jsonl`
173
+ line). Every other 0.20 definition is unchanged. See RFC-0012.
174
+
175
+ IPC **0.22** adds the object catalog and user placements (RFC-0011 P1): the
176
+ `objects.control` grant, a required `objects` state key (this world's authored
177
+ and user instances, object pocket items, user-object usage and limits),
178
+ `objects.catalog/pocket/place/transform/remove/hide/reset/configure` and
179
+ `libraries.list/install/remove`. `editor.selectedKind` gains `node`,
180
+ `editor.submode` gains `universal`, `editor.unplaced` gains `objects` and the
181
+ new `editor.gizmo` carries the tool 4 axis space, pivot and snapping. Error
182
+ `data` may name the refusing placement `rule`. Every other 0.21 definition is
183
+ unchanged. See the RFC-0011 amendment "P1 in IPC 0.22".
184
+
185
+ ## Objects and libraries (RFC-0011)
186
+
187
+ Object manifests (`object.json`) and library manifests (`library.json`) use the
188
+ world format numbering and start at **0.9** (`v0.9/object.schema.json`,
189
+ `v0.9/library.schema.json`); `v0.9/library-releases.schema.json` describes the
190
+ meta repository's `libraries/releases.json`. `v0.10/` holds a **draft** that adds
191
+ the `mover` trait, RFC-0013's `machine` trait and the extra slot kinds; it may
192
+ change until world format 0.10 is defined.
193
+
194
+ ```js
195
+ import { validate, objectFormats } from '@hyprune/schema';
196
+ const result = validate('object', manifest, undefined, undefined, { gltf, directory: 'street-lantern' });
197
+ // result.errors, result.warnings, result.triangles
198
+ ```
199
+
200
+ ```sh
201
+ npm run validate -- object examples/object.json
202
+ npm run validate -- object objects/bench/object.json - 0.9 objects/bench/object.glb
203
+ npm run validate -- library examples/library.json
204
+ npm run validate -- library-releases examples/library-releases.json
205
+ ```
206
+
207
+ The validator is pure: it never reads files. Pass `gltf` (the parsed glTF or a
208
+ GLB's JSON chunk) for the scene cross-checks, and `files`, `thumbnail` and
209
+ `measured` (exact rest bounds, largest texture, GPU bytes) when you have them;
210
+ `@hyprune/sdk`'s `hyprune object check <dir>` gathers all of them for a directory.
package/bin/validate.mjs CHANGED
@@ -2,17 +2,27 @@
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { validate } from '../lib/validate.mjs';
4
4
  const [kind, file, method, version, worldFile, asset, ...extra] = process.argv.slice(2);
5
- if (!kind || !file || extra.length || ((worldFile || asset) && kind !== 'scene')) {
5
+ // For kind `object` the fifth argument is the object's glTF (.gltf or .glb) for the cross-checks.
6
+ const gltfJson = path => {
7
+ const bytes = readFileSync(path);
8
+ if (!/\.glb$/i.test(path)) return JSON.parse(bytes.toString('utf8'));
9
+ if (bytes.length < 20 || bytes.readUInt32LE(0) !== 0x46546c67 || bytes.readUInt32LE(16) !== 0x4e4f534a) throw new Error('Invalid GLB header/JSON chunk');
10
+ return JSON.parse(bytes.subarray(20, 20 + bytes.readUInt32LE(12)).toString('utf8'));
11
+ };
12
+ if (!kind || !file || extra.length || ((worldFile || asset) && kind !== 'scene' && kind !== 'object') || (asset && kind === 'object')) {
6
13
  console.error('Usage: hyprune-validate <world|extension|screen-type|ipc> <file.json|-> [response-method|-] [contract-version]\n' +
7
- ' hyprune-validate scene <scene.gltf> - <world-format> <world.json> [object-asset-id]\n(in this repository: npm run validate -- ...)');
14
+ ' hyprune-validate scene <scene.gltf> - <world-format> <world.json> [object-asset-id]\n' +
15
+ ' hyprune-validate <object|library|library-releases> <file.json|-> [-] [format] [object scene.gltf|.glb]\n(in this repository: npm run validate -- ...)');
8
16
  process.exitCode = 2;
9
17
  } else {
10
18
  try {
11
19
  const bytes = readFileSync(file === '-' ? 0 : file);
12
20
  if (bytes.length > 1_048_576) throw new Error('Document exceeds 1 MiB');
13
21
  const value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
14
- const world = worldFile ? JSON.parse(readFileSync(worldFile, 'utf8')) : undefined;
15
- const result = validate(kind, value, method === '-' ? undefined : method, version, { world, asset });
22
+ const options = kind === 'object' ? { gltf: worldFile ? gltfJson(worldFile) : undefined }
23
+ : { world: worldFile ? JSON.parse(readFileSync(worldFile, 'utf8')) : undefined, asset };
24
+ const result = validate(kind, value, method === '-' ? undefined : method, version || undefined, options);
25
+ for (const warning of result.warnings || []) console.error(`warning: ${warning}`);
16
26
  if (result.valid) console.log(`VALID ${kind}: ${file}`);
17
27
  else { console.error(result.errors.join('\n')); process.exitCode = 1; }
18
28
  } catch (error) { console.error(error.message); process.exitCode = 2; }
@@ -0,0 +1,11 @@
1
+ {
2
+ "com.example.street@1.2.0": {
3
+ "id": "com.example.street",
4
+ "version": "1.2.0",
5
+ "formatVersion": "0.9",
6
+ "url": "https://example.com/releases/street-v1.2.0/com.example.street-1.2.0.tar.zst",
7
+ "sha256": "f06838d7b0725080016c84ae4f47aa01367ee665e3844637b14ed7f23706c894",
8
+ "nixHash": "sha256-8Gg417ByUIABbISuT0eqATZ+5mXjhEY3sU7X8jcGyJQ=",
9
+ "published": false
10
+ }
11
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "formatVersion": "0.9",
3
+ "id": "com.example.street",
4
+ "version": "1.2.0",
5
+ "name": "Street kit",
6
+ "license": "CC-BY-4.0",
7
+ "attribution": [
8
+ {
9
+ "path": ".",
10
+ "author": "Example Author",
11
+ "license": "CC-BY-4.0"
12
+ }
13
+ ],
14
+ "objects": [
15
+ "street-lantern",
16
+ "bench"
17
+ ]
18
+ }
@@ -0,0 +1,69 @@
1
+ {
2
+ "formatVersion": "0.9",
3
+ "id": "resident",
4
+ "version": "1.0.0",
5
+ "name": "Resident",
6
+ "category": "characters",
7
+ "scene": "object.glb",
8
+ "license": "CC-BY-4.0",
9
+ "attribution": [
10
+ {
11
+ "path": ".",
12
+ "author": "Example Author",
13
+ "license": "CC-BY-4.0"
14
+ }
15
+ ],
16
+ "bounds": {
17
+ "min": [
18
+ -0.45,
19
+ 0,
20
+ -0.2
21
+ ],
22
+ "max": [
23
+ 0.45,
24
+ 1.69,
25
+ 0.22
26
+ ]
27
+ },
28
+ "thumbnail": "thumbnail.png",
29
+ "traits": {
30
+ "character": {
31
+ "clips": {
32
+ "walk": {
33
+ "speed": 1.12
34
+ },
35
+ "run": {
36
+ "speed": 2.9
37
+ },
38
+ "idle": {},
39
+ "sit": {
40
+ "seatHeight": 0.46
41
+ },
42
+ "sit_idle": {}
43
+ },
44
+ "height": 1.69,
45
+ "radius": 0.3,
46
+ "accent": false,
47
+ "interactions": [
48
+ {
49
+ "id": "talk",
50
+ "label": "Talk",
51
+ "action": {
52
+ "type": "say",
53
+ "lines": [
54
+ "The lifts run every few minutes."
55
+ ]
56
+ }
57
+ },
58
+ {
59
+ "id": "where",
60
+ "label": "Where am I?",
61
+ "action": {
62
+ "type": "panel",
63
+ "panel": "map"
64
+ }
65
+ }
66
+ ]
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,338 @@
1
+ {
2
+ "formatVersion": "0.10",
3
+ "id": "conveyor",
4
+ "version": "1.0.0",
5
+ "name": "Conveyor",
6
+ "category": "machines",
7
+ "tags": [
8
+ "ci",
9
+ "pipeline"
10
+ ],
11
+ "scene": "object.glb",
12
+ "license": "CC-BY-4.0",
13
+ "attribution": [
14
+ {
15
+ "path": ".",
16
+ "author": "Example Author",
17
+ "license": "CC-BY-4.0"
18
+ }
19
+ ],
20
+ "bounds": {
21
+ "min": [
22
+ -2.2,
23
+ 0,
24
+ -0.8
25
+ ],
26
+ "max": [
27
+ 2.2,
28
+ 1.9,
29
+ 0.8
30
+ ]
31
+ },
32
+ "traits": {
33
+ "decorative": {
34
+ "animations": [
35
+ {
36
+ "node": "belt-rollers",
37
+ "type": "spin",
38
+ "axis": [
39
+ 1,
40
+ 0,
41
+ 0
42
+ ],
43
+ "pivot": [
44
+ 0,
45
+ 0.6,
46
+ 0
47
+ ],
48
+ "period": 1.2
49
+ }
50
+ ]
51
+ },
52
+ "collidable": {
53
+ "node": "proxy"
54
+ },
55
+ "movable": {
56
+ "surfaces": [
57
+ "floor"
58
+ ]
59
+ },
60
+ "machine": {
61
+ "primary": "status",
62
+ "inputs": [
63
+ {
64
+ "id": "status",
65
+ "type": "enum",
66
+ "values": [
67
+ "idle",
68
+ "running",
69
+ "ok",
70
+ "fail"
71
+ ],
72
+ "default": "idle",
73
+ "label": "Pipeline status",
74
+ "explain": "Lamp colour and belt motion: the belt runs while the pipeline runs."
75
+ },
76
+ {
77
+ "id": "jobStarted",
78
+ "type": "event",
79
+ "label": "Job started",
80
+ "explain": "Each started job drops a crate onto the belt."
81
+ },
82
+ {
83
+ "id": "queued",
84
+ "type": "int",
85
+ "min": 0,
86
+ "max": 64,
87
+ "default": 0,
88
+ "label": "Queued jobs",
89
+ "explain": "Crates waiting beside the entry, one per queued job."
90
+ },
91
+ {
92
+ "id": "lastJob",
93
+ "type": "text",
94
+ "maxLength": 24,
95
+ "label": "Last job",
96
+ "explain": "The most recent job's name."
97
+ }
98
+ ],
99
+ "drives": [
100
+ {
101
+ "input": "status",
102
+ "target": "state:lamp"
103
+ },
104
+ {
105
+ "input": "status",
106
+ "target": "animation:belt-rollers",
107
+ "map": {
108
+ "cases": [
109
+ {
110
+ "equals": "running",
111
+ "to": 1
112
+ }
113
+ ],
114
+ "else": 0
115
+ }
116
+ },
117
+ {
118
+ "input": "queued",
119
+ "target": "count:waiting"
120
+ },
121
+ {
122
+ "input": "lastJob",
123
+ "target": "label:sign"
124
+ },
125
+ {
126
+ "input": "jobStarted",
127
+ "target": "spawn:belt-in"
128
+ },
129
+ {
130
+ "input": "status",
131
+ "target": "effect:confetti",
132
+ "map": {
133
+ "becomes": "ok",
134
+ "from": "running"
135
+ }
136
+ }
137
+ ],
138
+ "recipes": [
139
+ {
140
+ "name": "Pipeline summary JSON",
141
+ "fields": {
142
+ "status": "/pipelines/0/status",
143
+ "jobStarted": "/pipelines/0/jobs_started",
144
+ "queued": {
145
+ "countWhere": {
146
+ "array": "/jobs",
147
+ "pointer": "/status",
148
+ "equals": "queued"
149
+ }
150
+ },
151
+ "lastJob": "/pipelines/0/last_job/name"
152
+ }
153
+ }
154
+ ]
155
+ }
156
+ },
157
+ "slots": {
158
+ "states": [
159
+ {
160
+ "id": "lamp",
161
+ "materials": [
162
+ "lamp-glass"
163
+ ],
164
+ "palette": [
165
+ {
166
+ "id": "idle",
167
+ "colour": [
168
+ 0.35,
169
+ 0.45,
170
+ 0.6
171
+ ],
172
+ "emission": 0.5
173
+ },
174
+ {
175
+ "id": "running",
176
+ "colour": [
177
+ 1.0,
178
+ 0.7,
179
+ 0.2
180
+ ],
181
+ "emission": 2
182
+ },
183
+ {
184
+ "id": "ok",
185
+ "colour": [
186
+ 0.3,
187
+ 1.0,
188
+ 0.45
189
+ ],
190
+ "emission": 2
191
+ },
192
+ {
193
+ "id": "fail",
194
+ "colour": [
195
+ 1.0,
196
+ 0.2,
197
+ 0.15
198
+ ],
199
+ "emission": 3
200
+ },
201
+ {
202
+ "id": "stale",
203
+ "colour": [
204
+ 0.4,
205
+ 0.4,
206
+ 0.4
207
+ ],
208
+ "emission": 0.2
209
+ }
210
+ ]
211
+ }
212
+ ],
213
+ "labels": [
214
+ {
215
+ "id": "sign",
216
+ "pose": {
217
+ "position": [
218
+ 0,
219
+ 1.75,
220
+ 0.81
221
+ ],
222
+ "rotation": [
223
+ 0,
224
+ 0,
225
+ 0,
226
+ 1
227
+ ]
228
+ },
229
+ "size": [
230
+ 1.2,
231
+ 0.3
232
+ ],
233
+ "maxChars": 24,
234
+ "font": "mono"
235
+ }
236
+ ],
237
+ "tracks": [
238
+ {
239
+ "id": "belt",
240
+ "speed": 0.6,
241
+ "points": [
242
+ {
243
+ "position": [
244
+ -2,
245
+ 0.8,
246
+ 0
247
+ ],
248
+ "type": "line"
249
+ },
250
+ {
251
+ "position": [
252
+ 2,
253
+ 0.8,
254
+ 0
255
+ ],
256
+ "type": "line"
257
+ }
258
+ ],
259
+ "items": {
260
+ "assets": [
261
+ "job-crate"
262
+ ],
263
+ "max": 24,
264
+ "spacing": 0.45
265
+ },
266
+ "sink": true
267
+ }
268
+ ],
269
+ "spawns": [
270
+ {
271
+ "id": "belt-in",
272
+ "pose": {
273
+ "position": [
274
+ -2,
275
+ 0.8,
276
+ 0
277
+ ],
278
+ "rotation": [
279
+ 0,
280
+ 0.707,
281
+ 0,
282
+ 0.707
283
+ ]
284
+ },
285
+ "track": "belt",
286
+ "items": [
287
+ "job-crate"
288
+ ],
289
+ "maxRate": 2
290
+ }
291
+ ],
292
+ "stacks": [
293
+ {
294
+ "id": "waiting",
295
+ "node": "crate",
296
+ "cap": 64,
297
+ "layout": {
298
+ "grid": [
299
+ 4,
300
+ 4,
301
+ 4
302
+ ],
303
+ "spacing": [
304
+ 0.35,
305
+ 0.35,
306
+ 0.35
307
+ ],
308
+ "fill": "xzy"
309
+ }
310
+ }
311
+ ],
312
+ "effects": [
313
+ {
314
+ "id": "confetti",
315
+ "pose": {
316
+ "position": [
317
+ 0,
318
+ 2.0,
319
+ 0
320
+ ],
321
+ "rotation": [
322
+ 0,
323
+ 0,
324
+ 0,
325
+ 1
326
+ ]
327
+ },
328
+ "kind": "burst",
329
+ "colour": [
330
+ 1.0,
331
+ 0.8,
332
+ 0.2
333
+ ],
334
+ "durationMs": 1500
335
+ }
336
+ ]
337
+ }
338
+ }