@vizij/render 1.0.1 → 3.0.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/README.md CHANGED
@@ -1,119 +1,230 @@
1
1
  # @vizij/render
2
2
 
3
- > Three.js + React renderer, scene store, and helper controllers for Vizij faces.
4
-
5
- This package exposes the `Vizij` canvas component along with hooks, stores, and GLTF helpers that power Vizij’s real-time character visualisation. It is the foundation that other packages (`@vizij/runtime-react`, etc.) build upon.
6
-
7
- ---
8
-
9
- ## Table of Contents
10
-
11
- 1. [Overview](#overview)
12
- 2. [Installation](#installation)
13
- 3. [Usage](#usage)
14
- 4. [Store & Hooks](#store--hooks)
15
- 5. [Controllers & Helpers](#controllers--helpers)
16
- 6. [Development & Testing](#development--testing)
17
- 7. [Publishing](#publishing)
18
- 8. [Related Packages](#related-packages)
3
+ > Vizij faces on a page — the page's canvas and the face slots over
4
+ > [`@vizij/runtime`](https://www.npmjs.com/package/@vizij/runtime)'s Bevy view —
5
+ > and the GLB loader, exporter and world model the authoring app edits.
19
6
 
20
7
  ---
21
8
 
22
9
  ## Overview
23
10
 
24
- - `Vizij` renders a fully managed `@react-three/fiber` canvas with sensible defaults for orthographic cameras and safe-area overlays.
25
- - A Zustand-powered store (`useVizijStore`) tracks renderables, controllers, and transient state. Hooks let you read or mutate slices without re-rendering entire scenes.
26
- - The loader (`loadGLTF`, `loadGLTFFromBlob` and their `WithBundle` forms) turns a rigged GLB into the world model; `buildSceneGraph` and `exportScene` turn the world model back into GLB bytes. Both run in Node as well as in a browser. The tuple helpers return `[world, animatables, animations]`, where `animations` contains parsed clip metadata for any channels embedded in the GLB.
27
- - Controllers wrap common behaviours (e.g., pointer interaction, safe-area visualisation) so you can compose features quickly.
11
+ - **The page's canvas, `<VizijCanvas>`.** `@vizij/runtime` runs one Bevy App
12
+ per page, on one canvas. `<VizijCanvas>` renders it and mounts it once;
13
+ while it is shown, one animation-frame loop places each face over its slot
14
+ and hands each pointer pick to the slot it names.
15
+ - **A face slot, `<Vizij>`.** A `<div>` marking where one face draws on that
16
+ canvas. It renders no canvas of its own, so a page shows any number of faces
17
+ for the cost of one App. Until its face is drawn, it shows a loading view:
18
+ the runtime's download and start, then the face's own stages.
19
+ - **The loader and the exporter.** `loadGLTF`, `loadGLTFFromBlob` and their
20
+ `WithBundle` forms turn a rigged GLB into the world model; `buildSceneGraph`
21
+ and `exportScene` turn the world model back into GLB bytes. Both run in Node
22
+ as well as in a browser.
23
+ - **The world model and its store.** `useVizijStore` and its hooks hold the
24
+ world, its animatables and the element selection: the document the authoring
25
+ app edits.
26
+
27
+ A face's device — loading it, writing its keys, calling its methods — is
28
+ `@vizij/runtime`'s `Runtime`; [`@vizij/runtime-react`](../runtime-react/README.md)
29
+ binds one to a React tree.
28
30
 
29
31
  ---
30
32
 
31
33
  ## Installation
32
34
 
33
35
  ```bash
34
- # pnpm
35
- pnpm add @vizij/render three @react-three/fiber @react-three/drei zustand @vizij/utils
36
-
37
- # npm
38
- npm install @vizij/render three @react-three/fiber @react-three/drei zustand @vizij/utils
39
-
40
- # yarn
41
- yarn add @vizij/render three @react-three/fiber @react-three/drei zustand @vizij/utils
36
+ pnpm add @vizij/render @vizij/runtime three zustand @vizij/utils
42
37
  ```
43
38
 
44
- Peer requirements:
45
-
46
- - `react >= 18`
47
- - `three >= 0.170`
48
- - `@react-three/fiber >= 8`
49
- - `@react-three/drei >= 9`
50
- - `zustand >= 5`
51
- - Optional UI integrations: `tailwindcss >= 4.1` (declared as a peer for theme utilities)
52
-
53
- Ensure these versions align with the rest of your app to avoid duplicate React or Three.js instances.
39
+ Peer requirements: `@vizij/runtime ^8` (its `onInitProgress` feeds the
40
+ loading view), `react >= 18`, `three ^0.170` (the
41
+ loader and the exporter), `zustand ^5` (the store). `@vizij/runtime` is a peer
42
+ so that a page resolves one copy of it, and therefore one wasm module.
54
43
 
55
44
  ---
56
45
 
57
46
  ## Usage
58
47
 
59
48
  ```tsx
60
- import { Vizij, useVizijStore } from "@vizij/render";
61
- import { useEffect } from "react";
62
-
63
- export function VizijCanvas() {
64
- const setDebug = useVizijStore((state) => state.setDebugState);
65
-
66
- useEffect(() => {
67
- setDebug((debug) => ({ ...debug, showGrid: true }));
68
- }, [setDebug]);
49
+ import { VizijCanvas, Vizij } from "@vizij/render";
69
50
 
51
+ export function Page() {
70
52
  return (
71
- <Vizij
72
- rootId="default/root"
73
- namespace="default"
74
- showSafeArea
75
- style={{ width: "100%", height: 480 }}
76
- />
53
+ <>
54
+ <VizijCanvas />
55
+ <section>
56
+ <Vizij vizijId="quori" style={{ width: 480, height: 360 }} />
57
+ </section>
58
+ </>
77
59
  );
78
60
  }
79
61
  ```
80
62
 
81
- Wrap the component tree with `VizijContext.Provider` if you want to supply a custom store; otherwise the `Vizij` component creates one internally.
82
-
83
- ---
84
-
85
- ## Store & Hooks
86
-
87
- - `useVizijStore(selector?)` – Access or mutate the renderer store with automatic subscription management.
88
- - `useVizijStoreGetter`, `useVizijStoreSetter`, `useVizijStoreSubscription` – Fine-grained accessors when you need optimised reads/writes.
89
- - `useFeatures()` – Inspect feature flags registered in the store.
90
- - Store types (`VizijData`, `VizijActions`) are exported from `store-types` for strongly typed selectors.
63
+ The face itself is loaded under the same id, once the canvas is mounted:
91
64
 
92
- The store tracks world graph entries, controllers, debug overlays, and renderable metadata. See `src/store.ts` for the full surface.
65
+ ```ts
66
+ import { canvasMounted } from "@vizij/render";
67
+ import { loadVizij } from "@vizij/runtime";
93
68
 
94
- ---
95
-
96
- ## Controllers & Helpers
97
-
98
- - Controllers under `src/controllers` encapsulate input handling, camera logic, and other behaviours. Compose them with your own React components.
99
- - `loadGLTF` / `loadGLTFFromBlob` load rig assets and extract the animatable metadata used by `@vizij/runtime-react`. A blob is parsed in memory, with nothing fetched.
100
- - `buildSceneGraph(world, animatables, rootId)` builds the three.js graph of a world element with no renderer, and `exportScene(root, { bundle, animations })` encodes a graph as GLB bytes. Saving or uploading the bytes is the caller's job.
101
-
102
- All exports are re-exported through `src/index.tsx`, so a simple `import { loadGLTF } from "@vizij/render"` works.
103
-
104
- ### Dual-format Vizij bundles
69
+ await canvasMounted();
70
+ const quori = await loadVizij("quori", glbBytes);
71
+ void quori.run();
72
+ ```
105
73
 
106
- Vizij scenes persist authoring metadata inside GLBs so third-party tools see baked animation, while Vizij runtimes retain the authored graphs and clips.
74
+ `@vizij/runtime-react`'s `VizijRuntimeProvider` does this for a React tree.
75
+
76
+ ### `<VizijCanvas options? className? style? />`
77
+
78
+ - Its first mount calls `@vizij/runtime`'s `mount` with `options` (every
79
+ face's defaults: `fit`, `zoom`, `background`, `ambient`, `unlit`). A second
80
+ `<VizijCanvas>`, a remount or React StrictMode's second effect shows the same
81
+ canvas and App; the options of the first mount stand for the page's
82
+ lifetime.
83
+ - `canvasMounted()` resolves with the canvas once it is mounted, which
84
+ `loadVizij` requires, and rejects if mounting failed.
85
+ - Default style: `position: fixed; inset: 0; pointer-events: none`, over the
86
+ page content. Faces then draw wherever their slots lie, whatever the
87
+ backgrounds of the sections between, and the page's own UI keeps the
88
+ pointer. Content that must draw over a face stacks above the canvas.
89
+ - `className` or `style` replace the default. A page that takes picks (an
90
+ authoring viewport) places the canvas itself and gives it pointer events.
91
+
92
+ The canvas's loop reads every slot's rectangle each frame and calls
93
+ `placeVizij` when it moved, which follows scrolling, resizes and layout shifts
94
+ alike. `placeVizij` and `drainPicks` act on the whole page, which is why they
95
+ have one caller.
96
+
97
+ ### `<Vizij vizijId className? style? toneMapping? fit? zoom? bounds? showSafeArea? selection? onPick? loadingImage? loading? />`
98
+
99
+ | Prop | Effect |
100
+ | -------------------- | ------------------------------------------------------------------------------------------------------------ |
101
+ | `vizijId` | The face the slot shows: the id it is loaded under. |
102
+ | `className`, `style` | Size and place the slot; the face draws over its border box. |
103
+ | `toneMapping` | The face's tone-mapping curve (`none`, `agx`, `aces`, `neutral`): `setView`. |
104
+ | `fit`, `zoom` | How the framed bounds fit the slot, and the magnification after the fit; the canvas's options when absent. |
105
+ | `bounds` | The world rectangle the face's camera frames, in place of the GLB's authored `rootBounds`. |
106
+ | `showSafeArea` | Outlines the framed bounds (`safeArea(vizijId)`) over the face, as a DOM overlay. |
107
+ | `selection` | The element ids whose outlines glow (`setSelection`). Absent, the slot leaves the face's selection as it is. |
108
+ | `onPick` | A press on the face: `{ vizijId, elementId }`, `elementId` `null` for a press that hit no element. |
109
+ | `loadingImage` | The default loading view's image, in place of Vizij's logo, which it ships; `null` for none. |
110
+ | `loading` | The slot's own loading view, `(loading: VizijLoading) => ReactNode`; `null` for none. |
111
+
112
+ The slot carries `data-vizij-id` and `data-vizij-ready`, `"true"` once the
113
+ face's scene shows its device's pose — what an e2e test waits on. When a slot
114
+ unmounts, its face is placed on an empty rectangle, where it draws nothing.
115
+ One slot shows a face at a time.
116
+
117
+ ### The loading view
118
+
119
+ Until its face is first drawn, a slot shows a loading view over its border
120
+ box, centred: an image, Vizij's logo by default, then a progress bar and a
121
+ short label of the stage in the slot's text colour. The stages, in order:
122
+
123
+ | Stage | Label | Bar | Reported by |
124
+ | ---------- | -------------------------------------- | ------------- | -------------------------------------------- |
125
+ | `download` | Downloading the runtime: 3.1 of 9.8 MB | the bytes | `<VizijCanvas>`, from `onInitProgress` |
126
+ | `compile` | Compiling the runtime | indeterminate | `<VizijCanvas>`, from `onInitProgress` |
127
+ | `start` | Starting the runtime | indeterminate | `<VizijCanvas>`, from `onInitProgress` |
128
+ | `mount` | Starting the view | indeterminate | `<VizijCanvas>`, until `mount` resolves |
129
+ | `fetch` | Loading the face: 1.2 of 4.0 MB | the bytes | the face's loader, `setVizijProgress` |
130
+ | `load` | Loading the face | indeterminate | the face's loader; the default after `mount` |
131
+ | `draw` | Drawing the face | indeterminate | the face's loader |
132
+ | `failed` | Loading failed | none | `<VizijCanvas>` or the face's loader |
133
+
134
+ Without a known total, a byte count reads "3.1 MB" and its bar is
135
+ indeterminate; before any byte, the label has no count. A failure shows first;
136
+ the runtime's stages show before the face's, which wait on them. The view
137
+ hides once the canvas's loop reads the face ready (`data-vizij-ready="true"`),
138
+ and stays hidden through a reload, whose previous scene draws until the new
139
+ one is ready.
140
+
141
+ - **The face's stages** are reported by whoever loads the face, keyed by its
142
+ id: `setVizijProgress(vizijId, progress)`, with `progress` a
143
+ `VizijProgress` — `{ stage: "fetch", loaded, total }` (`total` `null` when
144
+ unknown), `{ stage: "load" }`, `{ stage: "draw" }` or
145
+ `{ stage: "failed", error }`. `@vizij/runtime-react`'s provider reports its
146
+ face. A page that calls `loadVizij` itself reports the same way, or leaves
147
+ its slot at "Loading the face" until the face is drawn. `null` says no face
148
+ is loaded under the id any more, and its slot shows the loading view again.
149
+ - **Its image:** `loadingImage` replaces Vizij's logo, which the view ships (a
150
+ small inline SVG in the logo's own colours); `null` leaves the image out.
151
+ - **Its whole element:** `loading` is called with a `VizijLoading` —
152
+ `{ stage, label, fraction }`, `fraction` from 0 to 1 for a byte count with
153
+ a known total and `null` otherwise — and its result is shown over the slot
154
+ in place of the default view. `loading={null}` shows none.
107
155
 
108
- - Every renderable still carries a `RobotData` extension in `userData` describing features and animatable bindings.
109
- - The exporter now writes a root-level `extensions.VIZIJ_bundle` block following the schema in [`src/types/vizij-bundle.ts`](./src/types/vizij-bundle.ts). It contains rig graphs, pose configs, stored Vizij clips, and provenance hashes.
110
- - Use `exportScene(root, { bundle, animations })` to embed both the Vizij bundle and optional baked `THREE.AnimationClip` instances; it resolves to the GLB as an `ArrayBuffer`. The helper attaches the bundle only for the export call and restores the original object.
111
- - A graph from `buildSceneGraph` carries each element's authored values: node translation, rotation and scale, morph weights and material factors equal the `RobotData` defaults written beside them, not a runtime pose.
112
- - When loading assets, prefer `loadGLTFWithBundle` / `loadGLTFFromBlobWithBundle` to retrieve `{ world, animatables, bundle, animations }`. The legacy tuple helpers return `[world, animatables, animations]` if you only need the renderer state.
113
- - The new `animations` field exposes `VizijAnimationClipData[]`, which maps each glTF animation channel back to Vizij animatable ids (`RobotData.features.*.value.id`). Each track provides component-aware `times`/`values` arrays so runtimes can register clips without re-parsing the GLB.
114
- - `extractVizijBundle(scene)` and `applyVizijBundle(scene, bundle)` (under `src/functions/vizij-bundle.ts`) let advanced tooling inspect or mutate bundles without triggering a fresh export.
156
+ ```tsx
157
+ <Vizij vizijId="quori" loadingImage={<img src="/logo.svg" alt="" />} />
158
+ <Vizij
159
+ vizijId="quori"
160
+ loading={({ label, fraction }) => (
161
+ <Spinner label={label} value={fraction ?? undefined} />
162
+ )}
163
+ />
164
+ ```
115
165
 
116
- With this structure, authoring tools can round-trip Vizij runtime assets while shipping native glTF animations for viewers that do not understand Vizij.
166
+ The default bar is a `role="progressbar"` named "Loading the face", its
167
+ `aria-valuetext` the label, and its `aria-valuenow` the percentage when the
168
+ stage has a fraction (indeterminate otherwise); a failure is a
169
+ `role="status"`. The view is a DOM overlay inside the slot, with pointer
170
+ events off, so it shows wherever the canvas draws nothing — the whole slot
171
+ until its face draws. `<VizijCanvas>` shows no loading view of its own: by default it
172
+ covers the whole page, and every slot already shows the runtime's stages.
173
+
174
+ ### Loading and exporting
175
+
176
+ - `loadGLTF(url, aggressiveImport?, rootBounds?)`, `loadGLTFFromBlob` and
177
+ their `WithBundle` forms return the world, its animatables, the
178
+ `VIZIJ_bundle` and the GLB's animations. A blob is parsed in memory, with
179
+ nothing fetched.
180
+ - The elements are the nodes that carry `RobotData`. With `aggressiveImport`,
181
+ a GLB in which no node does (a plain Blender export) is imported from its
182
+ meshes instead, framed on `rootBounds` or, absent, on its geometry's
183
+ bounds; one with no mesh, ellipse or rectangle either fails with
184
+ `EmptyModelError`.
185
+ - `buildSceneGraph(world, animatables, rootId)` builds the three.js graph of a
186
+ world element with no renderer; `exportScene(root, { bundle, animations })`
187
+ encodes it as GLB bytes. Every node carries its element's `RobotData`, and
188
+ its transform, morph weights and material factors are the element's
189
+ authored defaults.
190
+ - `createExportAnimation({ name, duration, tracks, root })` turns baked
191
+ tracks — plain data, `ExportAnimationTrack`: a node's name, a
192
+ `position`, `quaternion`, `scale` or `morphTargetInfluences` property, key
193
+ times in seconds and flattened values — into one of the `animations`
194
+ `exportScene` writes. Each track is bound against `root` by node name first,
195
+ and one that would not bind (no such node, no such morph target, a value
196
+ count that is not `times × stride`) is left out and returned in `issues`,
197
+ since the exporter otherwise drops the whole animation or throws.
198
+ `animation` is `null` when no track binds.
199
+ - An element's compound features — translation, rotation, scale and the
200
+ colours — are whole (`translation`, a vector), per axis (`translation.x`,
201
+ `rotation.r`, `color.g`, each a number: the form Semio Studio writes its
202
+ faces in), or both, a component given per axis standing over the whole
203
+ value's. Both forms load, build into the scene graph and export unchanged.
204
+ A rotation is an euler `{ x, y, z }`, or `{ r, p, y }` (roll, pitch, yaw)
205
+ as Studio names the same angles; either applies in ZYX order.
206
+ - An ellipse or a rectangle exports as a node with no mesh, carrying its
207
+ `RobotData` — `type`, `width`, `height`, `fillColor`, `fillOpacity` — from
208
+ which the Bevy view draws it: a unit circle or square scaled by the width
209
+ and height, filled with the colour and opacity. Its stroke is not drawn.
210
+ - `extractVizijBundle(scene)` and `applyVizijBundle(scene, bundle)` read and
211
+ write the bundle on a three.js graph.
212
+
213
+ The `VIZIJ_bundle` extension follows the schema in
214
+ [`src/types/vizij-bundle.ts`](./src/types/vizij-bundle.ts): rig graphs, pose
215
+ configs, programs, stored clips and provenance hashes, beside glTF animations
216
+ that viewers which do not understand Vizij still play.
217
+
218
+ ### The store
219
+
220
+ - `useVizijStore(selector)` reads the store of the nearest
221
+ `VizijContext.Provider`; `useVizijStoreGetter`, `useVizijStoreSetter` and
222
+ `useVizijStoreSubscription` are its fine-grained forms.
223
+ - `createVizijStore()` and `useDefaultVizijStore` create and hold one.
224
+ - It holds the world, its animatables, values keyed by namespace, the element
225
+ selection, and the edits the authoring app makes to them (`setVizij`,
226
+ `addWorldElements`, `setStaticFeature`, `createAnimatable`, …). Its types are
227
+ `VizijData` and `VizijActions`.
117
228
 
118
229
  ---
119
230
 
@@ -127,39 +238,36 @@ pnpm --filter "@vizij/render" lint
127
238
  pnpm --filter "@vizij/render" size
128
239
  ```
129
240
 
130
- `tsup` produces both ESM and CJS bundles with type declarations. Tests run via Vitest (currently smoke-level), and `size-limit` guards against unexpected bundle growth.
241
+ The tests run on Node (`node --test`): the loader, the headless export round
242
+ trip of the shipped faces and of an ellipse and a rectangle, the binding and
243
+ export of baked animations, the canvas's frame
244
+ loop, and the loading view's stages, labels and overrides, against a stand-in of
245
+ `@vizij/runtime` (`tests/runtime-stand-in.mjs`). `size-limit` guards the bundle
246
+ size.
247
+
248
+ Faces are drawn by `@vizij/runtime`'s Bevy view, a peer dependency. `three` is
249
+ imported only by the loader (`src/functions/load-gltf.ts`,
250
+ `src/functions/gltf-loading/`) and the exporter
251
+ (`src/functions/build-scene-graph.ts`, `src/functions/export.ts`,
252
+ `src/functions/export-animation.ts`, `src/functions/vizij-bundle.ts`); the
253
+ rest imports its types at most. An API
254
+ change updates the apps that use the package and `@vizij/runtime-react` in the
255
+ same change.
131
256
 
132
257
  ---
133
258
 
134
259
  ## Publishing
135
260
 
136
- Use the shared workflow at [`.github/workflows/publish-npm.yml`](../../../.github/workflows/publish-npm.yml).
137
-
138
- 1. Align dependency versions (`three`, `@react-three/*`, `zustand`, Vizij packages) and generate a changeset:
139
-
140
- ```bash
141
- pnpm changeset
142
- pnpm version:packages
143
- ```
144
-
145
- 2. Validate locally:
146
-
147
- ```bash
148
- pnpm install
149
- pnpm --filter "@vizij/render" build
150
- pnpm --filter "@vizij/render" test
151
- pnpm --filter "@vizij/render" typecheck
152
- pnpm --filter "@vizij/render" lint
153
- pnpm --filter "@vizij/render" exec npm pack --dry-run
154
- ```
155
-
156
- 3. Merge the version bump to `main`. CI publishes any `@vizij/*` version not yet on npm (with provenance) — no release tag. See the root [README → Publishing & Versioning](../../../README.md#publishing--versioning).
261
+ A change carries a changeset (`pnpm changeset`). Merging to `main` updates the
262
+ Version Packages PR, and merging that publishes through
263
+ [`.github/workflows/publish-npm.yml`](../../../.github/workflows/publish-npm.yml).
264
+ See [`CONTRIBUTING.md` › Releasing](../../../CONTRIBUTING.md#releasing).
157
265
 
158
266
  ---
159
267
 
160
268
  ## Related Packages
161
269
 
162
- - [`@vizij/runtime-react`](../runtime-react/README.md) – Hooks that consume the renderer to load rigged models.
163
- - [`@vizij/animation-react`](../animation-react/README.md) – React bindings that feed animation values back into the renderer.
164
-
165
- Questions or contributions? Open an issue so we can keep the renderer API and docs sharp for the whole Vizij ecosystem. 🎨
270
+ - [`@vizij/runtime`](https://www.npmjs.com/package/@vizij/runtime) – the Bevy
271
+ view and each face's Arora device, in one wasm module.
272
+ - [`@vizij/runtime-react`](../runtime-react/README.md) – one face's lifecycle
273
+ in React and the fan-out of its store changes.