@hyprune/schema 0.5.2 → 0.6.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 +43 -0
- package/README.md +21 -4
- package/bin/validate.mjs +4 -4
- package/extensions/blueprint.schema.json +29 -0
- package/lib/validate.d.mts +6 -0
- package/lib/validate.mjs +54 -2
- package/package.json +1 -1
- package/v0.20/ipc.schema.json +11800 -0
- package/v0.8/common.schema.json +478 -0
- package/v0.8/world.schema.json +1261 -0
package/CHANGELOG.md
CHANGED
|
@@ -142,3 +142,46 @@ definition exists in 0.16 and the graphics properties differ only by the new key
|
|
|
142
142
|
action it would take (`""|click|grab|pocket|place|mount|select|path|drop`).
|
|
143
143
|
- Everything else is inherited from 0.18 unchanged, including the offload cap
|
|
144
144
|
and device keys; `session.hello` reports `0.19`.
|
|
145
|
+
|
|
146
|
+
## IPC 0.20: pocket actions and blueprint map levels
|
|
147
|
+
|
|
148
|
+
- `surface.pocket {surfaceId}` and `surface.unpocket {surfaceId}` (surface.control).
|
|
149
|
+
Pocket: the window leaves the world for the Placement queue, its placement kept;
|
|
150
|
+
idempotent. Unpocket: placed at a free spot in front of the player
|
|
151
|
+
(`placed: true`) or left in the pocket, selected for the Placement tool
|
|
152
|
+
(`placed: false`). Results `{revision}` / `{revision, placed}`.
|
|
153
|
+
- `map.levels` (optional, 1..6): `{id, name, minY, maxY, svg, sha256}` per
|
|
154
|
+
blueprint level, sorted by `minY`, in the frame of the map (`worldToMap`).
|
|
155
|
+
- New `extensions/blueprint.schema.json`: the `com.hyprune.blueprint` 0.1 world
|
|
156
|
+
extension (levels with package-relative SVG paths; byte budgets, ordering and
|
|
157
|
+
the `class` vocabulary are stated in its description and enforced by consumers).
|
|
158
|
+
- Everything else is inherited from 0.19 unchanged; `session.hello` reports
|
|
159
|
+
`0.20` and lists the two methods. Inheritance test included.
|
|
160
|
+
|
|
161
|
+
## World 0.8: placed world objects
|
|
162
|
+
|
|
163
|
+
- World format **0.8** adds `objectAssets` (at most 32) and `objects` (at most
|
|
164
|
+
256). An asset is a package-relative glTF (`scene`, same extension rules as the
|
|
165
|
+
world's scene.gltf; never lightmapped) with optional declarative node animation
|
|
166
|
+
(at most 16: `spin` or `orbit` about `pivot`/`axis` in asset space, or `bob`
|
|
167
|
+
along `axis` by `amplitude` metres; `period` 0.5–3600 s, optional `phase`), an
|
|
168
|
+
optional static `collision` proxy node and `lighting.mode` (`probe` default, or
|
|
169
|
+
`unlit`). Nodes are referenced by unique glTF node name. `library` (`id@version`)
|
|
170
|
+
is reserved for shared object libraries and rejected in 0.8. An object places an
|
|
171
|
+
asset with a `pose`, uniform `scale` (0.01–100), animation `speed` (0–10, default
|
|
172
|
+
1), `phase` (0–1) and an optional `ambient` RGB override (0–16).
|
|
173
|
+
- `validate('world')` (from 0.8): unique asset and object ids, known asset
|
|
174
|
+
references, normalised poses, unit axes, per-type fields (spin/orbit need a
|
|
175
|
+
pivot and take no amplitude; bob the reverse), one animation per node, collision
|
|
176
|
+
node not animated, library references rejected. `validate('scene', gltf,
|
|
177
|
+
undefined, '0.8', {world, asset: id})` checks an asset's glTF: extension rules,
|
|
178
|
+
referenced node names exist exactly once, no animated node inside another
|
|
179
|
+
animated subtree, the collision node has a mesh and is outside every animated
|
|
180
|
+
subtree.
|
|
181
|
+
- `v0.8/common.schema.json` is the 0.7 common definitions under the 0.8 `$id`
|
|
182
|
+
(the directory already held IPC 0.8, which keeps its own references).
|
|
183
|
+
Everything else is inherited from 0.7 unchanged. Inheritance test included.
|
|
184
|
+
|
|
185
|
+
## @hyprune/schema 0.6.0 (2026-10-11)
|
|
186
|
+
|
|
187
|
+
Publishes IPC 0.20 and world format 0.8 to npm.
|
package/README.md
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
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.
|
|
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/).
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
npm i -D @hyprune/schema
|
|
11
|
-
npx hyprune-validate world world.json - 0.
|
|
12
|
-
npx hyprune-validate scene scene.gltf - 0.
|
|
11
|
+
npx hyprune-validate world world.json - 0.8 # a world package manifest
|
|
12
|
+
npx hyprune-validate scene scene.gltf - 0.8 world.json # its glTF against the world format
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
```js
|
|
16
16
|
import { validate, worldFormats } from '@hyprune/schema';
|
|
17
|
-
import schema from '@hyprune/schema/v0.
|
|
17
|
+
import schema from '@hyprune/schema/v0.8/world.schema.json' with { type: 'json' };
|
|
18
18
|
|
|
19
19
|
const result = validate('world', manifest, undefined, worldFormats.at(-1));
|
|
20
20
|
if (!result.valid) console.error(result.errors.join('\n'));
|
|
@@ -118,6 +118,15 @@ optimised scene assets: `KHR_texture_basisu` (KTX2, material textures only),
|
|
|
118
118
|
`extensionsRequired` and lightmap textures against the world format. Version
|
|
119
119
|
gates use `atLeast()`; see RFC-0004 "world 0.7".
|
|
120
120
|
|
|
121
|
+
World format **0.8** adds placed world objects: `objectAssets` (package glTF
|
|
122
|
+
assets with optional `spin`/`orbit`/`bob` node animation, a static collision
|
|
123
|
+
proxy and `probe`/`unlit` lighting) and `objects` (instances with pose, scale,
|
|
124
|
+
speed, phase and an optional ambient override). Objects are render-only apart
|
|
125
|
+
from the proxy. Check an asset's glTF with
|
|
126
|
+
`validate('scene', gltf, undefined, '0.8', {world, asset: 'armillary'})` (CLI:
|
|
127
|
+
`npm run validate -- scene objects/armillary.gltf - 0.8 world.json armillary`).
|
|
128
|
+
See RFC-0004 "world 0.8 placed world objects".
|
|
129
|
+
|
|
121
130
|
IPC **0.16** adds the `upscaler` graphics key (`"fsr1"`, `"bilinear"` or `null` to
|
|
122
131
|
inherit the preset) to `graphicsConfig`/`graphicsPatch`. Every 0.15 definition is
|
|
123
132
|
otherwise unchanged; earlier schemas stay frozen. See RFC-0003/0008 amendments.
|
|
@@ -139,3 +148,11 @@ bob, reticle style/size/colour and reach limits), widens graphics `fov` to 30-12
|
|
|
139
148
|
adds `reticleColor`/`validColor`/`invalidColor` to the overlay style and the
|
|
140
149
|
required `interaction.reticle {state, action}`. Every other 0.18 definition is
|
|
141
150
|
unchanged. See the RFC-0006 amendment "IPC 0.19 player settings and reticle".
|
|
151
|
+
|
|
152
|
+
IPC **0.20** adds `surface.pocket`/`surface.unpocket` (surface.control): put a
|
|
153
|
+
world window into the pocket (the Placement queue) and take it out again, placed
|
|
154
|
+
in front of the player or selected for the Placement tool (`placed`). Pocket state
|
|
155
|
+
stays `editor.unplaced.windows`. `spatial.map` gains optional `levels` (1-6
|
|
156
|
+
per-level blueprint SVGs). `extensions/blueprint.schema.json` defines the world
|
|
157
|
+
extension `com.hyprune.blueprint` 0.1 that supplies them. Every other 0.19
|
|
158
|
+
definition is unchanged. See the RFC-0004 and RFC-0005 amendments for IPC 0.20.
|
package/bin/validate.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { validate } from '../lib/validate.mjs';
|
|
4
|
-
const [kind, file, method, version, worldFile, ...extra] = process.argv.slice(2);
|
|
5
|
-
if (!kind || !file || extra.length || (worldFile && kind !== 'scene')) {
|
|
4
|
+
const [kind, file, method, version, worldFile, asset, ...extra] = process.argv.slice(2);
|
|
5
|
+
if (!kind || !file || extra.length || ((worldFile || asset) && kind !== 'scene')) {
|
|
6
6
|
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
|
|
7
|
+
' hyprune-validate scene <scene.gltf> - <world-format> <world.json> [object-asset-id]\n(in this repository: npm run validate -- ...)');
|
|
8
8
|
process.exitCode = 2;
|
|
9
9
|
} else {
|
|
10
10
|
try {
|
|
@@ -12,7 +12,7 @@ if (!kind || !file || extra.length || (worldFile && kind !== 'scene')) {
|
|
|
12
12
|
if (bytes.length > 1_048_576) throw new Error('Document exceeds 1 MiB');
|
|
13
13
|
const value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
14
14
|
const world = worldFile ? JSON.parse(readFileSync(worldFile, 'utf8')) : undefined;
|
|
15
|
-
const result = validate(kind, value, method === '-' ? undefined : method, version, { world });
|
|
15
|
+
const result = validate(kind, value, method === '-' ? undefined : method, version, { world, asset });
|
|
16
16
|
if (result.valid) console.log(`VALID ${kind}: ${file}`);
|
|
17
17
|
else { console.error(result.errors.join('\n')); process.exitCode = 1; }
|
|
18
18
|
} catch (error) { console.error(error.message); process.exitCode = 2; }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://hyprune.com/schema/extensions/blueprint.schema.json",
|
|
4
|
+
"title": "com.hyprune.blueprint 0.1 world manifest value",
|
|
5
|
+
"description": "Per-level blueprint SVGs in the frame of the package's com.hyprune.map 0.1 (RFC-0004 amendment). Levels are sorted by minY and must not overlap (maxY[i] <= minY[i+1]); each SVG is at most 65536 bytes, all levels together at most 262144 bytes, using the map SVG subset plus a class attribute from: grid floor wall stair opening screen mount area label route waypoint void outline. Consumers enforce those rules.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["version", "levels"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"version": {"const": "0.1"},
|
|
11
|
+
"levels": {
|
|
12
|
+
"type": "array",
|
|
13
|
+
"minItems": 1,
|
|
14
|
+
"maxItems": 6,
|
|
15
|
+
"items": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"additionalProperties": false,
|
|
18
|
+
"required": ["id", "name", "minY", "maxY", "path"],
|
|
19
|
+
"properties": {
|
|
20
|
+
"id": {"type": "string", "pattern": "^[a-z][a-z0-9-]{0,31}$"},
|
|
21
|
+
"name": {"type": "string", "minLength": 1, "maxLength": 64},
|
|
22
|
+
"minY": {"type": "number"},
|
|
23
|
+
"maxY": {"type": "number"},
|
|
24
|
+
"path": {"type": "string", "minLength": 1, "maxLength": 200, "pattern": "^(?!/)(?!.*(^|/)\\.\\.(/|$))[A-Za-z0-9_./-]+\\.svg$"}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
package/lib/validate.d.mts
CHANGED
|
@@ -12,6 +12,12 @@ export interface ValidationResult {
|
|
|
12
12
|
export interface ValidateOptions {
|
|
13
13
|
/** For kind `scene`: the package's parsed world.json (lightmap checks). */
|
|
14
14
|
world?: unknown;
|
|
15
|
+
/**
|
|
16
|
+
* For kind `scene` from world 0.8: validate `value` as the glTF of this
|
|
17
|
+
* `world.objectAssets[].id` (extension rules, animation and collision node
|
|
18
|
+
* names, nesting) instead of the world's scene.gltf.
|
|
19
|
+
*/
|
|
20
|
+
asset?: string;
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
/**
|
package/lib/validate.mjs
CHANGED
|
@@ -36,9 +36,35 @@ export function sceneExtensions(version) {
|
|
|
36
36
|
const base = ['KHR_materials_unlit','KHR_texture_transform','KHR_materials_emissive_strength'];
|
|
37
37
|
return atLeast(version, '0.7') ? [...base,'KHR_texture_basisu','EXT_meshopt_compression','KHR_mesh_quantization'] : base;
|
|
38
38
|
}
|
|
39
|
+
// World 0.8 object asset rules that need the asset's glTF (RFC-0004 "world 0.8"):
|
|
40
|
+
// referenced node names exist once, animated subtrees do not nest, and the
|
|
41
|
+
// collision proxy has a mesh and lies outside every animated subtree.
|
|
42
|
+
function validateObjectAsset(gltf, asset, fail) {
|
|
43
|
+
const nodes = gltf.nodes || [], parents = new Map(), label = `objectAssets ${asset.id}`;
|
|
44
|
+
nodes.forEach((n, i) => { for (const c of n.children || []) parents.set(c, i); });
|
|
45
|
+
const find = name => {
|
|
46
|
+
const hits = nodes.flatMap((n, i) => n.name === name ? [i] : []);
|
|
47
|
+
if (hits.length !== 1) fail(`${label}: node "${name}" ${hits.length ? 'is not unique' : 'not found'} in the asset`);
|
|
48
|
+
return hits.length === 1 ? hits[0] : undefined;
|
|
49
|
+
};
|
|
50
|
+
const ancestors = node => { const out = []; for (let p = parents.get(node), n = 0; p !== undefined && n <= nodes.length; p = parents.get(p), n++) out.push(p); return out; };
|
|
51
|
+
const targets = new Map();
|
|
52
|
+
for (const a of asset.animations || []) { const i = find(a.node); if (i !== undefined) targets.set(i, a.node); }
|
|
53
|
+
for (const [node, name] of targets) for (const p of ancestors(node))
|
|
54
|
+
if (targets.has(p)) fail(`${label}: animation "${name}" is inside animated node "${targets.get(p)}"`);
|
|
55
|
+
if (asset.collision) {
|
|
56
|
+
const i = find(asset.collision.node);
|
|
57
|
+
if (i !== undefined) {
|
|
58
|
+
if (nodes[i].mesh === undefined) fail(`${label}: collision node "${asset.collision.node}" has no mesh`);
|
|
59
|
+
for (const p of [i, ...ancestors(i)]) if (targets.has(p)) fail(`${label}: collision node "${asset.collision.node}" is inside animated node "${targets.get(p)}"`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
39
63
|
// scene.gltf rules that depend on the world format: extensionsRequired, and
|
|
40
64
|
// lightmap textures (world.lightmaps[].texture) stay PNG in every v0 format.
|
|
41
|
-
|
|
65
|
+
// With options.asset, `gltf` is that world 0.8 object asset's glTF instead: the
|
|
66
|
+
// same extension rules, the asset rules above, and no lightmaps (assets are never baked).
|
|
67
|
+
function validateScene(gltf, version, world, assetId) {
|
|
42
68
|
const errors = [], fail = m => errors.push(m);
|
|
43
69
|
if (!worldFormats.includes(version)) throw new Error(`Unknown world format: ${version}`);
|
|
44
70
|
const allowed = new Set(sceneExtensions(version)), required = new Set(gltf.extensionsRequired || []);
|
|
@@ -55,6 +81,12 @@ function validateScene(gltf, version, world) {
|
|
|
55
81
|
if ((gltf.bufferViews || []).some(v => v.extensions?.EXT_meshopt_compression) && !required.has('EXT_meshopt_compression'))
|
|
56
82
|
fail(allowed.has('EXT_meshopt_compression') ? 'extensionsRequired: EXT_meshopt_compression must be listed when buffer views use it'
|
|
57
83
|
: 'bufferViews: EXT_meshopt_compression needs world 0.7');
|
|
84
|
+
if (assetId !== undefined) {
|
|
85
|
+
const asset = atLeast(version, '0.8') ? (world?.objectAssets || []).find(a => a.id === assetId) : undefined;
|
|
86
|
+
if (!asset) fail(`objectAssets: unknown asset ${assetId} for world ${version}`);
|
|
87
|
+
else validateObjectAsset(gltf, asset, fail);
|
|
88
|
+
return { valid: errors.length === 0, errors };
|
|
89
|
+
}
|
|
58
90
|
for (const lightmap of world?.lightmaps || []) {
|
|
59
91
|
const texture = textures[lightmap.texture];
|
|
60
92
|
if (!texture) { fail(`lightmaps: texture ${lightmap.texture} missing from scene`); continue; }
|
|
@@ -70,7 +102,7 @@ function validateScene(gltf, version, world) {
|
|
|
70
102
|
export function validate(kind, value, method, version = '0.1', options = {}) {
|
|
71
103
|
if (kind === 'scene') {
|
|
72
104
|
if (method) throw new Error('Method context is only valid for IPC');
|
|
73
|
-
return validateScene(value, version, options.world);
|
|
105
|
+
return validateScene(value, version, options.world, options.asset);
|
|
74
106
|
}
|
|
75
107
|
if (!names.includes(kind) || kind === 'common') throw new Error(`Unknown schema: ${kind}`);
|
|
76
108
|
if (!contractVersions.includes(version)) throw new Error(`Unknown contract: ${version}`);
|
|
@@ -172,6 +204,26 @@ export function validate(kind, value, method, version = '0.1', options = {}) {
|
|
|
172
204
|
if (Math.abs(Math.hypot(...c.facing)-1) > .001 || Math.abs(axis.reduce((v,n,i)=>v+n*c.facing[i],0)/length) > .001) fail(`${c.id}: facing must be unit and perpendicular to segment`);
|
|
173
205
|
for (const end of ['top','bottom']) if (Math.hypot(...c.exits[end].map((v,i)=>v-c[end][i])) > 3) fail(`${c.id}: exit too far from segment`);
|
|
174
206
|
}
|
|
207
|
+
if (atLeast(value.formatVersion, '0.8')) {
|
|
208
|
+
const assets = value.objectAssets || [], objects = value.objects || [];
|
|
209
|
+
unique(assets, 'id', 'objectAssets'); unique(objects, 'id', 'objects');
|
|
210
|
+
const ids = new Set(assets.map(a => a.id));
|
|
211
|
+
for (const a of assets) {
|
|
212
|
+
const label = `objectAssets ${a.id}`, animations = a.animations || [];
|
|
213
|
+
if (a.library !== undefined) fail(`${label}: library references are reserved for a later world format; use a package scene`);
|
|
214
|
+
unique(animations, 'node', `${label} animations`);
|
|
215
|
+
for (const n of animations) {
|
|
216
|
+
if (Math.abs(Math.hypot(...n.axis) - 1) > 0.001) fail(`${label}: animation "${n.node}" axis must be a unit vector`);
|
|
217
|
+
if (n.type === 'bob' ? n.amplitude === undefined : n.pivot === undefined) fail(`${label}: ${n.type} animation "${n.node}" needs ${n.type === 'bob' ? 'an amplitude' : 'a pivot'}`);
|
|
218
|
+
if (n.type === 'bob' ? n.pivot !== undefined : n.amplitude !== undefined) fail(`${label}: ${n.type} animation "${n.node}" takes no ${n.type === 'bob' ? 'pivot' : 'amplitude'}`);
|
|
219
|
+
}
|
|
220
|
+
if (a.collision && animations.some(n => n.node === a.collision.node)) fail(`${label}: collision node "${a.collision.node}" is animated`);
|
|
221
|
+
}
|
|
222
|
+
for (const o of objects) {
|
|
223
|
+
if (!ids.has(o.asset)) fail(`objects ${o.id}: unknown asset ${o.asset}`);
|
|
224
|
+
quat(o.pose.rotation, `objects ${o.id}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
175
227
|
unique(value.attribution, 'path', 'attribution');
|
|
176
228
|
for (const a of value.attribution) license(a.license, `attribution ${a.path}`);
|
|
177
229
|
}
|
package/package.json
CHANGED