@vizij/render 2.0.0 → 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
@@ -177,11 +177,32 @@ covers the whole page, and every slot already shows the runtime's stages.
177
177
  their `WithBundle` forms return the world, its animatables, the
178
178
  `VIZIJ_bundle` and the GLB's animations. A blob is parsed in memory, with
179
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`.
180
185
  - `buildSceneGraph(world, animatables, rootId)` builds the three.js graph of a
181
186
  world element with no renderer; `exportScene(root, { bundle, animations })`
182
187
  encodes it as GLB bytes. Every node carries its element's `RobotData`, and
183
188
  its transform, morph weights and material factors are the element's
184
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.
185
206
  - An ellipse or a rectangle exports as a node with no mesh, carrying its
186
207
  `RobotData` — `type`, `width`, `height`, `fillColor`, `fillOpacity` — from
187
208
  which the Bevy view draws it: a unit circle or square scaled by the width
@@ -218,7 +239,8 @@ pnpm --filter "@vizij/render" size
218
239
  ```
219
240
 
220
241
  The tests run on Node (`node --test`): the loader, the headless export round
221
- trip of the shipped faces and of an ellipse and a rectangle, the canvas's frame
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
222
244
  loop, and the loading view's stages, labels and overrides, against a stand-in of
223
245
  `@vizij/runtime` (`tests/runtime-stand-in.mjs`). `size-limit` guards the bundle
224
246
  size.
@@ -227,7 +249,8 @@ Faces are drawn by `@vizij/runtime`'s Bevy view, a peer dependency. `three` is
227
249
  imported only by the loader (`src/functions/load-gltf.ts`,
228
250
  `src/functions/gltf-loading/`) and the exporter
229
251
  (`src/functions/build-scene-graph.ts`, `src/functions/export.ts`,
230
- `src/functions/vizij-bundle.ts`); the rest imports its types at most. An API
252
+ `src/functions/export-animation.ts`, `src/functions/vizij-bundle.ts`); the
253
+ rest imports its types at most. An API
231
254
  change updates the apps that use the package and `@vizij/runtime-react` in the
232
255
  same change.
233
256
 
package/dist/index.d.mts CHANGED
@@ -176,6 +176,24 @@ interface StoredAnimatedFeature {
176
176
  * @param value - The id of the {@link AnimatableValue} used to populate this value if animated, or the value {@link RawValue} if static.
177
177
  */
178
178
  type Feature = AnimatedFeature | StaticFeature;
179
+ /**
180
+ * A compound feature in its per-axis form: one feature per component, named
181
+ * `<feature>.<axis>` (`translation.x`, `rotation.r`, `color.g`), each a
182
+ * number. Semio Studio writes its faces this way. An element may carry a
183
+ * compound feature whole, per axis, or both; a component named per axis
184
+ * stands over that component of the whole value.
185
+ */
186
+ type PerAxisFeatures<Name extends string, Axis extends string> = {
187
+ [Key in `${Name}.${Axis}`]?: Feature;
188
+ };
189
+ /** The components of a translation or a scale. */
190
+ type VectorAxis = "x" | "y" | "z";
191
+ /** The components of a rotation: roll, pitch and yaw, the euler's x, y and z. */
192
+ type RotationAxis = "r" | "p" | "y";
193
+ /** The components of a colour. */
194
+ type ColorAxis = "r" | "g" | "b";
195
+ /** An element's translation, rotation and scale, per axis. */
196
+ type PerAxisTransformFeatures = PerAxisFeatures<"translation", VectorAxis> & PerAxisFeatures<"rotation", RotationAxis> & PerAxisFeatures<"scale", VectorAxis>;
179
197
 
180
198
  interface RenderableBase {
181
199
  id: string;
@@ -218,9 +236,9 @@ interface Ellipse extends RenderableBase {
218
236
  strokeColor?: Feature;
219
237
  strokeWidth?: Feature;
220
238
  strokeOffset?: Feature;
221
- translation: Feature;
222
- rotation: Feature;
223
- };
239
+ translation?: Feature;
240
+ rotation?: Feature;
241
+ } & PerAxisFeatures<"translation", VectorAxis> & PerAxisFeatures<"rotation", RotationAxis> & PerAxisFeatures<"fillColor", ColorAxis> & PerAxisFeatures<"strokeColor", ColorAxis>;
224
242
  }
225
243
  type EllipseFeature = keyof Ellipse["features"];
226
244
  type StoredEllipse = Stored<Ellipse>;
@@ -247,9 +265,9 @@ interface Rectangle extends RenderableBase {
247
265
  strokeWidth?: Feature;
248
266
  strokeRadius?: Feature;
249
267
  strokeOffset?: Feature;
250
- translation: Feature;
251
- rotation: Feature;
252
- };
268
+ translation?: Feature;
269
+ rotation?: Feature;
270
+ } & PerAxisFeatures<"translation", VectorAxis> & PerAxisFeatures<"rotation", RotationAxis> & PerAxisFeatures<"fillColor", ColorAxis> & PerAxisFeatures<"strokeColor", ColorAxis>;
253
271
  }
254
272
  type RectangleFeature = keyof Rectangle["features"];
255
273
  type StoredRectangle = Stored<Rectangle>;
@@ -268,11 +286,12 @@ type StoredRectangle = Stored<Rectangle>;
268
286
  interface Group extends RenderableBase {
269
287
  type: "group";
270
288
  root: boolean;
289
+ /** Translation and rotation, each whole or per axis. */
271
290
  features: {
272
- translation: Feature;
273
- rotation: Feature;
291
+ translation?: Feature;
292
+ rotation?: Feature;
274
293
  scale?: Feature;
275
- };
294
+ } & PerAxisTransformFeatures;
276
295
  rootBounds?: {
277
296
  center: RawVector2;
278
297
  size: RawVector2;
@@ -311,10 +330,10 @@ interface Shape extends RenderableBase {
311
330
  emissive?: Feature;
312
331
  emissiveIntensity?: Feature;
313
332
  specular?: Feature;
314
- translation: Feature;
315
- rotation: Feature;
333
+ translation?: Feature;
334
+ rotation?: Feature;
316
335
  scale?: Feature;
317
- };
336
+ } & PerAxisTransformFeatures & PerAxisFeatures<"color", ColorAxis> & PerAxisFeatures<"emissive", ColorAxis> & PerAxisFeatures<"specular", ColorAxis>;
318
337
  material: ShapeMaterial;
319
338
  geometry: BufferGeometry | ShapeGeometry;
320
339
  morphTargets?: string[];
@@ -737,9 +756,15 @@ declare function useVizijStoreSetter(): {
737
756
 
738
757
  declare function useVizijStoreGetter(): () => VizijData & VizijActions;
739
758
 
759
+ /**
760
+ * The error a load ends with when the GLB holds no face: no element carries
761
+ * `RobotData`, and the aggressive import finds no mesh, ellipse or rectangle
762
+ * to make one from.
763
+ */
740
764
  declare class EmptyModelError extends Error {
741
765
  constructor(message: string);
742
766
  }
767
+
743
768
  type ParserJsonFallbackSource = {
744
769
  url?: string;
745
770
  arrayBuffer?: ArrayBuffer;
@@ -821,6 +846,64 @@ declare function patchVizijBundleMetadata(glb: ArrayBuffer, metadata: Record<str
821
846
  */
822
847
  declare function exportScene(root: THREE.Object3D, options?: ExportSceneOptions): Promise<ArrayBuffer>;
823
848
 
849
+ /** A glTF-animatable node property, under three.js's property name. */
850
+ type ExportAnimationProperty = "position" | "quaternion" | "scale" | "morphTargetInfluences";
851
+ /** One node property's keys, to export as a glTF animation channel. */
852
+ interface ExportAnimationTrack {
853
+ /**
854
+ * The property-binding path: `<node>.<property>`, or
855
+ * `<node>.morphTargetInfluences[<morph target name>]`.
856
+ */
857
+ name: string;
858
+ property: ExportAnimationProperty;
859
+ /** The name of the node the track drives. */
860
+ elementName: string;
861
+ /** Key times, in seconds, ascending. */
862
+ times: number[];
863
+ /** Flattened key values, `times.length * stride` of them. */
864
+ values: number[];
865
+ /** Values per key: 3 for a position or scale, 4 for a quaternion, 1 for a morph weight. */
866
+ stride: number;
867
+ }
868
+ interface ExportAnimationIssue {
869
+ trackName: string;
870
+ reason: "node-not-found" | "morph-target-not-found" | "bad-stride";
871
+ detail?: string;
872
+ }
873
+ /**
874
+ * What a track binds against: the export root and its descendants, found by
875
+ * name. The graph `buildSceneGraph` returns is one.
876
+ */
877
+ interface ExportAnimationRoot {
878
+ name: string;
879
+ traverse: (callback: (child: ExportAnimationRoot) => void) => void;
880
+ morphTargetDictionary?: Record<string, number>;
881
+ }
882
+ interface ExportAnimationResult {
883
+ /** For `exportScene({ animations })`; `null` when no track binds. */
884
+ animation: AnimationClip | null;
885
+ /** The tracks left out, each with why. */
886
+ issues: ExportAnimationIssue[];
887
+ }
888
+ /**
889
+ * An animation for `exportScene`, from baked tracks, each checked against the
890
+ * tree it will be exported with.
891
+ *
892
+ * The exporter drops a whole animation when one track fails to bind, and
893
+ * throws when a morph target name is missing from the mesh's
894
+ * `morphTargetDictionary`. So each track is bound here first, by node name
895
+ * (`exportScene` clones a root that is not a `Scene`, which keeps names and
896
+ * changes uuids), and a track that would not bind is left out and reported.
897
+ */
898
+ declare function createExportAnimation(options: {
899
+ name: string;
900
+ /** In seconds. */
901
+ duration: number;
902
+ tracks: ReadonlyArray<ExportAnimationTrack>;
903
+ /** The tree the animation will be exported with. */
904
+ root: ExportAnimationRoot;
905
+ }): ExportAnimationResult;
906
+
824
907
  /**
825
908
  * Build the three.js graph of the world element `rootId` and its descendants,
826
909
  * for `exportScene`.
@@ -830,6 +913,12 @@ declare function exportScene(root: THREE.Object3D, options?: ExportSceneOptions)
830
913
  * them, with the element's `RobotData` in `userData`. No renderer is involved,
831
914
  * so this runs in Node.
832
915
  *
916
+ * A compound feature — translation, rotation, scale, a colour — is read whole,
917
+ * per axis (`translation.x`, `rotation.r`, `color.g`: Semio Studio's form),
918
+ * or both, a component given per axis standing over the whole value's. A
919
+ * rotation is an euler `{ x, y, z }` or, as Studio names it, `{ r, p, y }`
920
+ * (roll, pitch, yaw), applied in ZYX order.
921
+ *
833
922
  * Every feature takes its authored value: a static feature its value, an
834
923
  * animated one its animatable's `default` in `animatables`. Node translation,
835
924
  * rotation and scale, morph weights and material factors in the exported file
@@ -886,4 +975,4 @@ declare function sanitizeMorphKey(name: string, fallbackIndex: number, used: Set
886
975
  */
887
976
  declare function deriveMorphFeatureKeys(names: ReadonlyArray<string>): string[];
888
977
 
889
- export { type AnimatedFeature, DEFAULT_TONE_MAPPING_MODE, type Ellipse, type EllipseFeature, EmptyModelError, type ExportSceneOptions, type Feature, type Group, type GroupFeature, type LoadedVizijAsset, type LoadingStage, type Rectangle, type RectangleFeature, type RenderableBase, type RenderableFeature, type Selection, type Shape, type ShapeFeature, ShapeMaterial, type StaticFeature, type Stored, type StoredAnimatedFeature, type StoredEllipse, type StoredFeatures, type StoredGroup, type StoredRectangle, type StoredRenderable, type StoredShape, TONE_MAPPING_LABELS, TONE_MAPPING_MODES, type ToneMappingMode, Vizij, type VizijActions, type VizijAnimationClipData, type VizijAnimationId, type VizijAnimationTrackData, type VizijBundleAnimationClip, type VizijBundleAnimationEntry, type VizijBundleAnimationKeyframe, type VizijBundleAnimationTrack, type VizijBundleExtension, type VizijBundleGraphEntry, type VizijBundleGraphKind, type VizijBundleGraphMetadata, type VizijBundlePoseSection, type VizijBundleProfile, type VizijBundleStarredSection, type VizijBundleVersion, VizijCanvas, type VizijCanvasProps, VizijContext, type VizijData, type VizijGraphId, type VizijLoading, type VizijPoseDefinition, type VizijPoseId, type VizijPoseRigConfig, type VizijProfileKey, type VizijProgress, type VizijProps, VizijSlice, type VizijSpeechConfig, type VizijStarredItem, type VizijStarredKind, type VizijStore, type VizijStoreGetter, type VizijStoreSetter, type World, applyVizijBundle, buildSceneGraph, canvasMounted, createVizijStore, deriveMorphFeatureKeys, deriveRootBounds, exportScene, extractVizijBundle, isToneMappingMode, loadGLTF, loadGLTFFromBlob, loadGLTFFromBlobWithBundle, loadGLTFWithBundle, normalizeToneMappingMode, parseGlbJsonChunk, patchGlbJson, patchVizijBundleMetadata, sanitizeMorphKey, setVizijProgress, useDefaultVizijStore, useVizijStore, useVizijStoreGetter, useVizijStoreSetter, useVizijStoreSubscription };
978
+ export { type AnimatedFeature, type ColorAxis, DEFAULT_TONE_MAPPING_MODE, type Ellipse, type EllipseFeature, EmptyModelError, type ExportAnimationIssue, type ExportAnimationProperty, type ExportAnimationResult, type ExportAnimationRoot, type ExportAnimationTrack, type ExportSceneOptions, type Feature, type Group, type GroupFeature, type LoadedVizijAsset, type LoadingStage, type PerAxisFeatures, type PerAxisTransformFeatures, type Rectangle, type RectangleFeature, type RenderableBase, type RenderableFeature, type RotationAxis, type Selection, type Shape, type ShapeFeature, ShapeMaterial, type StaticFeature, type Stored, type StoredAnimatedFeature, type StoredEllipse, type StoredFeatures, type StoredGroup, type StoredRectangle, type StoredRenderable, type StoredShape, TONE_MAPPING_LABELS, TONE_MAPPING_MODES, type ToneMappingMode, type VectorAxis, Vizij, type VizijActions, type VizijAnimationClipData, type VizijAnimationId, type VizijAnimationTrackData, type VizijBundleAnimationClip, type VizijBundleAnimationEntry, type VizijBundleAnimationKeyframe, type VizijBundleAnimationTrack, type VizijBundleExtension, type VizijBundleGraphEntry, type VizijBundleGraphKind, type VizijBundleGraphMetadata, type VizijBundlePoseSection, type VizijBundleProfile, type VizijBundleStarredSection, type VizijBundleVersion, VizijCanvas, type VizijCanvasProps, VizijContext, type VizijData, type VizijGraphId, type VizijLoading, type VizijPoseDefinition, type VizijPoseId, type VizijPoseRigConfig, type VizijProfileKey, type VizijProgress, type VizijProps, VizijSlice, type VizijSpeechConfig, type VizijStarredItem, type VizijStarredKind, type VizijStore, type VizijStoreGetter, type VizijStoreSetter, type World, applyVizijBundle, buildSceneGraph, canvasMounted, createExportAnimation, createVizijStore, deriveMorphFeatureKeys, deriveRootBounds, exportScene, extractVizijBundle, isToneMappingMode, loadGLTF, loadGLTFFromBlob, loadGLTFFromBlobWithBundle, loadGLTFWithBundle, normalizeToneMappingMode, parseGlbJsonChunk, patchGlbJson, patchVizijBundleMetadata, sanitizeMorphKey, setVizijProgress, useDefaultVizijStore, useVizijStore, useVizijStoreGetter, useVizijStoreSetter, useVizijStoreSubscription };
package/dist/index.d.ts CHANGED
@@ -176,6 +176,24 @@ interface StoredAnimatedFeature {
176
176
  * @param value - The id of the {@link AnimatableValue} used to populate this value if animated, or the value {@link RawValue} if static.
177
177
  */
178
178
  type Feature = AnimatedFeature | StaticFeature;
179
+ /**
180
+ * A compound feature in its per-axis form: one feature per component, named
181
+ * `<feature>.<axis>` (`translation.x`, `rotation.r`, `color.g`), each a
182
+ * number. Semio Studio writes its faces this way. An element may carry a
183
+ * compound feature whole, per axis, or both; a component named per axis
184
+ * stands over that component of the whole value.
185
+ */
186
+ type PerAxisFeatures<Name extends string, Axis extends string> = {
187
+ [Key in `${Name}.${Axis}`]?: Feature;
188
+ };
189
+ /** The components of a translation or a scale. */
190
+ type VectorAxis = "x" | "y" | "z";
191
+ /** The components of a rotation: roll, pitch and yaw, the euler's x, y and z. */
192
+ type RotationAxis = "r" | "p" | "y";
193
+ /** The components of a colour. */
194
+ type ColorAxis = "r" | "g" | "b";
195
+ /** An element's translation, rotation and scale, per axis. */
196
+ type PerAxisTransformFeatures = PerAxisFeatures<"translation", VectorAxis> & PerAxisFeatures<"rotation", RotationAxis> & PerAxisFeatures<"scale", VectorAxis>;
179
197
 
180
198
  interface RenderableBase {
181
199
  id: string;
@@ -218,9 +236,9 @@ interface Ellipse extends RenderableBase {
218
236
  strokeColor?: Feature;
219
237
  strokeWidth?: Feature;
220
238
  strokeOffset?: Feature;
221
- translation: Feature;
222
- rotation: Feature;
223
- };
239
+ translation?: Feature;
240
+ rotation?: Feature;
241
+ } & PerAxisFeatures<"translation", VectorAxis> & PerAxisFeatures<"rotation", RotationAxis> & PerAxisFeatures<"fillColor", ColorAxis> & PerAxisFeatures<"strokeColor", ColorAxis>;
224
242
  }
225
243
  type EllipseFeature = keyof Ellipse["features"];
226
244
  type StoredEllipse = Stored<Ellipse>;
@@ -247,9 +265,9 @@ interface Rectangle extends RenderableBase {
247
265
  strokeWidth?: Feature;
248
266
  strokeRadius?: Feature;
249
267
  strokeOffset?: Feature;
250
- translation: Feature;
251
- rotation: Feature;
252
- };
268
+ translation?: Feature;
269
+ rotation?: Feature;
270
+ } & PerAxisFeatures<"translation", VectorAxis> & PerAxisFeatures<"rotation", RotationAxis> & PerAxisFeatures<"fillColor", ColorAxis> & PerAxisFeatures<"strokeColor", ColorAxis>;
253
271
  }
254
272
  type RectangleFeature = keyof Rectangle["features"];
255
273
  type StoredRectangle = Stored<Rectangle>;
@@ -268,11 +286,12 @@ type StoredRectangle = Stored<Rectangle>;
268
286
  interface Group extends RenderableBase {
269
287
  type: "group";
270
288
  root: boolean;
289
+ /** Translation and rotation, each whole or per axis. */
271
290
  features: {
272
- translation: Feature;
273
- rotation: Feature;
291
+ translation?: Feature;
292
+ rotation?: Feature;
274
293
  scale?: Feature;
275
- };
294
+ } & PerAxisTransformFeatures;
276
295
  rootBounds?: {
277
296
  center: RawVector2;
278
297
  size: RawVector2;
@@ -311,10 +330,10 @@ interface Shape extends RenderableBase {
311
330
  emissive?: Feature;
312
331
  emissiveIntensity?: Feature;
313
332
  specular?: Feature;
314
- translation: Feature;
315
- rotation: Feature;
333
+ translation?: Feature;
334
+ rotation?: Feature;
316
335
  scale?: Feature;
317
- };
336
+ } & PerAxisTransformFeatures & PerAxisFeatures<"color", ColorAxis> & PerAxisFeatures<"emissive", ColorAxis> & PerAxisFeatures<"specular", ColorAxis>;
318
337
  material: ShapeMaterial;
319
338
  geometry: BufferGeometry | ShapeGeometry;
320
339
  morphTargets?: string[];
@@ -737,9 +756,15 @@ declare function useVizijStoreSetter(): {
737
756
 
738
757
  declare function useVizijStoreGetter(): () => VizijData & VizijActions;
739
758
 
759
+ /**
760
+ * The error a load ends with when the GLB holds no face: no element carries
761
+ * `RobotData`, and the aggressive import finds no mesh, ellipse or rectangle
762
+ * to make one from.
763
+ */
740
764
  declare class EmptyModelError extends Error {
741
765
  constructor(message: string);
742
766
  }
767
+
743
768
  type ParserJsonFallbackSource = {
744
769
  url?: string;
745
770
  arrayBuffer?: ArrayBuffer;
@@ -821,6 +846,64 @@ declare function patchVizijBundleMetadata(glb: ArrayBuffer, metadata: Record<str
821
846
  */
822
847
  declare function exportScene(root: THREE.Object3D, options?: ExportSceneOptions): Promise<ArrayBuffer>;
823
848
 
849
+ /** A glTF-animatable node property, under three.js's property name. */
850
+ type ExportAnimationProperty = "position" | "quaternion" | "scale" | "morphTargetInfluences";
851
+ /** One node property's keys, to export as a glTF animation channel. */
852
+ interface ExportAnimationTrack {
853
+ /**
854
+ * The property-binding path: `<node>.<property>`, or
855
+ * `<node>.morphTargetInfluences[<morph target name>]`.
856
+ */
857
+ name: string;
858
+ property: ExportAnimationProperty;
859
+ /** The name of the node the track drives. */
860
+ elementName: string;
861
+ /** Key times, in seconds, ascending. */
862
+ times: number[];
863
+ /** Flattened key values, `times.length * stride` of them. */
864
+ values: number[];
865
+ /** Values per key: 3 for a position or scale, 4 for a quaternion, 1 for a morph weight. */
866
+ stride: number;
867
+ }
868
+ interface ExportAnimationIssue {
869
+ trackName: string;
870
+ reason: "node-not-found" | "morph-target-not-found" | "bad-stride";
871
+ detail?: string;
872
+ }
873
+ /**
874
+ * What a track binds against: the export root and its descendants, found by
875
+ * name. The graph `buildSceneGraph` returns is one.
876
+ */
877
+ interface ExportAnimationRoot {
878
+ name: string;
879
+ traverse: (callback: (child: ExportAnimationRoot) => void) => void;
880
+ morphTargetDictionary?: Record<string, number>;
881
+ }
882
+ interface ExportAnimationResult {
883
+ /** For `exportScene({ animations })`; `null` when no track binds. */
884
+ animation: AnimationClip | null;
885
+ /** The tracks left out, each with why. */
886
+ issues: ExportAnimationIssue[];
887
+ }
888
+ /**
889
+ * An animation for `exportScene`, from baked tracks, each checked against the
890
+ * tree it will be exported with.
891
+ *
892
+ * The exporter drops a whole animation when one track fails to bind, and
893
+ * throws when a morph target name is missing from the mesh's
894
+ * `morphTargetDictionary`. So each track is bound here first, by node name
895
+ * (`exportScene` clones a root that is not a `Scene`, which keeps names and
896
+ * changes uuids), and a track that would not bind is left out and reported.
897
+ */
898
+ declare function createExportAnimation(options: {
899
+ name: string;
900
+ /** In seconds. */
901
+ duration: number;
902
+ tracks: ReadonlyArray<ExportAnimationTrack>;
903
+ /** The tree the animation will be exported with. */
904
+ root: ExportAnimationRoot;
905
+ }): ExportAnimationResult;
906
+
824
907
  /**
825
908
  * Build the three.js graph of the world element `rootId` and its descendants,
826
909
  * for `exportScene`.
@@ -830,6 +913,12 @@ declare function exportScene(root: THREE.Object3D, options?: ExportSceneOptions)
830
913
  * them, with the element's `RobotData` in `userData`. No renderer is involved,
831
914
  * so this runs in Node.
832
915
  *
916
+ * A compound feature — translation, rotation, scale, a colour — is read whole,
917
+ * per axis (`translation.x`, `rotation.r`, `color.g`: Semio Studio's form),
918
+ * or both, a component given per axis standing over the whole value's. A
919
+ * rotation is an euler `{ x, y, z }` or, as Studio names it, `{ r, p, y }`
920
+ * (roll, pitch, yaw), applied in ZYX order.
921
+ *
833
922
  * Every feature takes its authored value: a static feature its value, an
834
923
  * animated one its animatable's `default` in `animatables`. Node translation,
835
924
  * rotation and scale, morph weights and material factors in the exported file
@@ -886,4 +975,4 @@ declare function sanitizeMorphKey(name: string, fallbackIndex: number, used: Set
886
975
  */
887
976
  declare function deriveMorphFeatureKeys(names: ReadonlyArray<string>): string[];
888
977
 
889
- export { type AnimatedFeature, DEFAULT_TONE_MAPPING_MODE, type Ellipse, type EllipseFeature, EmptyModelError, type ExportSceneOptions, type Feature, type Group, type GroupFeature, type LoadedVizijAsset, type LoadingStage, type Rectangle, type RectangleFeature, type RenderableBase, type RenderableFeature, type Selection, type Shape, type ShapeFeature, ShapeMaterial, type StaticFeature, type Stored, type StoredAnimatedFeature, type StoredEllipse, type StoredFeatures, type StoredGroup, type StoredRectangle, type StoredRenderable, type StoredShape, TONE_MAPPING_LABELS, TONE_MAPPING_MODES, type ToneMappingMode, Vizij, type VizijActions, type VizijAnimationClipData, type VizijAnimationId, type VizijAnimationTrackData, type VizijBundleAnimationClip, type VizijBundleAnimationEntry, type VizijBundleAnimationKeyframe, type VizijBundleAnimationTrack, type VizijBundleExtension, type VizijBundleGraphEntry, type VizijBundleGraphKind, type VizijBundleGraphMetadata, type VizijBundlePoseSection, type VizijBundleProfile, type VizijBundleStarredSection, type VizijBundleVersion, VizijCanvas, type VizijCanvasProps, VizijContext, type VizijData, type VizijGraphId, type VizijLoading, type VizijPoseDefinition, type VizijPoseId, type VizijPoseRigConfig, type VizijProfileKey, type VizijProgress, type VizijProps, VizijSlice, type VizijSpeechConfig, type VizijStarredItem, type VizijStarredKind, type VizijStore, type VizijStoreGetter, type VizijStoreSetter, type World, applyVizijBundle, buildSceneGraph, canvasMounted, createVizijStore, deriveMorphFeatureKeys, deriveRootBounds, exportScene, extractVizijBundle, isToneMappingMode, loadGLTF, loadGLTFFromBlob, loadGLTFFromBlobWithBundle, loadGLTFWithBundle, normalizeToneMappingMode, parseGlbJsonChunk, patchGlbJson, patchVizijBundleMetadata, sanitizeMorphKey, setVizijProgress, useDefaultVizijStore, useVizijStore, useVizijStoreGetter, useVizijStoreSetter, useVizijStoreSubscription };
978
+ export { type AnimatedFeature, type ColorAxis, DEFAULT_TONE_MAPPING_MODE, type Ellipse, type EllipseFeature, EmptyModelError, type ExportAnimationIssue, type ExportAnimationProperty, type ExportAnimationResult, type ExportAnimationRoot, type ExportAnimationTrack, type ExportSceneOptions, type Feature, type Group, type GroupFeature, type LoadedVizijAsset, type LoadingStage, type PerAxisFeatures, type PerAxisTransformFeatures, type Rectangle, type RectangleFeature, type RenderableBase, type RenderableFeature, type RotationAxis, type Selection, type Shape, type ShapeFeature, ShapeMaterial, type StaticFeature, type Stored, type StoredAnimatedFeature, type StoredEllipse, type StoredFeatures, type StoredGroup, type StoredRectangle, type StoredRenderable, type StoredShape, TONE_MAPPING_LABELS, TONE_MAPPING_MODES, type ToneMappingMode, type VectorAxis, Vizij, type VizijActions, type VizijAnimationClipData, type VizijAnimationId, type VizijAnimationTrackData, type VizijBundleAnimationClip, type VizijBundleAnimationEntry, type VizijBundleAnimationKeyframe, type VizijBundleAnimationTrack, type VizijBundleExtension, type VizijBundleGraphEntry, type VizijBundleGraphKind, type VizijBundleGraphMetadata, type VizijBundlePoseSection, type VizijBundleProfile, type VizijBundleStarredSection, type VizijBundleVersion, VizijCanvas, type VizijCanvasProps, VizijContext, type VizijData, type VizijGraphId, type VizijLoading, type VizijPoseDefinition, type VizijPoseId, type VizijPoseRigConfig, type VizijProfileKey, type VizijProgress, type VizijProps, VizijSlice, type VizijSpeechConfig, type VizijStarredItem, type VizijStarredKind, type VizijStore, type VizijStoreGetter, type VizijStoreSetter, type World, applyVizijBundle, buildSceneGraph, canvasMounted, createExportAnimation, createVizijStore, deriveMorphFeatureKeys, deriveRootBounds, exportScene, extractVizijBundle, isToneMappingMode, loadGLTF, loadGLTFFromBlob, loadGLTFFromBlobWithBundle, loadGLTFWithBundle, normalizeToneMappingMode, parseGlbJsonChunk, patchGlbJson, patchVizijBundleMetadata, sanitizeMorphKey, setVizijProgress, useDefaultVizijStore, useVizijStore, useVizijStoreGetter, useVizijStoreSetter, useVizijStoreSubscription };
package/dist/index.js CHANGED
@@ -42,6 +42,7 @@ __export(index_exports, {
42
42
  applyVizijBundle: () => applyVizijBundle,
43
43
  buildSceneGraph: () => buildSceneGraph,
44
44
  canvasMounted: () => canvasMounted,
45
+ createExportAnimation: () => createExportAnimation,
45
46
  createVizijStore: () => createVizijStore,
46
47
  deriveMorphFeatureKeys: () => deriveMorphFeatureKeys,
47
48
  deriveRootBounds: () => deriveRootBounds,
@@ -814,19 +815,10 @@ var VizijSlice = (set, _get) => ({
814
815
  (0, import_immer.produce)((state) => {
815
816
  const lookupId = (0, import_utils3.getLookup)(namespace, id);
816
817
  if (typeof value === "function") {
817
- const current = state.values.get(lookupId);
818
- if (current !== void 0) {
819
- if (value(current !== void 0)) {
820
- state.values.set(lookupId, value(current));
821
- }
822
- } else {
823
- const animatableLookup = state.animatables[id];
824
- const updatedValue = value(
825
- animatableLookup !== void 0 ? animatableLookup.default : void 0
826
- );
827
- if (updatedValue !== void 0) {
828
- state.values.set(lookupId, updatedValue);
829
- }
818
+ const current = state.values.get(lookupId) ?? state.animatables[id]?.default;
819
+ const updatedValue = value(current);
820
+ if (updatedValue !== void 0) {
821
+ state.values.set(lookupId, updatedValue);
830
822
  }
831
823
  } else {
832
824
  state.values.set(lookupId, value);
@@ -1106,6 +1098,14 @@ function recordRenderCounter(key, delta = 1) {
1106
1098
  // src/functions/gltf-loading/traverse-three.ts
1107
1099
  var THREE = __toESM(require("three"));
1108
1100
 
1101
+ // src/functions/empty-model-error.ts
1102
+ var EmptyModelError = class extends Error {
1103
+ constructor(message) {
1104
+ super(message);
1105
+ this.name = "EmptyModelError";
1106
+ }
1107
+ };
1108
+
1109
1109
  // src/functions/gltf-loading/map-features.ts
1110
1110
  function mapFeatures(features) {
1111
1111
  const animatableValues = {};
@@ -1656,7 +1656,9 @@ function traverseThree(group, aggressiveImport = false, rootBounds) {
1656
1656
  } else {
1657
1657
  const derivedRootBounds = rootBounds ?? deriveRootBounds(group);
1658
1658
  if (!derivedRootBounds) {
1659
- throw new Error("Root bounds are expected if using an aggressive import");
1659
+ throw new EmptyModelError(
1660
+ "The GLB has no RobotData and no mesh, ellipse or rectangle to import"
1661
+ );
1660
1662
  }
1661
1663
  const [newWorldData, newAnimatableData] = importScene(
1662
1664
  group,
@@ -1761,41 +1763,40 @@ function stripMaterialSuffixes(name) {
1761
1763
  function isStoredAnimatedFeature(feature) {
1762
1764
  return Boolean(feature) && typeof feature === "object" && feature.animated === true && "value" in feature;
1763
1765
  }
1764
- function isGroupFeatures(value) {
1766
+ var TRANSFORM = [
1767
+ ["translation", ["x", "y", "z"]],
1768
+ ["rotation", ["r", "p", "y"]]
1769
+ ];
1770
+ var REQUIRED_FEATURES = {
1771
+ group: TRANSFORM,
1772
+ shape: TRANSFORM,
1773
+ ellipse: [...TRANSFORM, ["width", []], ["height", []]],
1774
+ rectangle: [...TRANSFORM, ["width", []], ["height", []]]
1775
+ };
1776
+ function assertRequiredFeatures(kind, value) {
1765
1777
  if (!value || typeof value !== "object") {
1766
1778
  throw new Error("Expected object");
1767
1779
  }
1768
- if (!["translation", "rotation"].every((key) => key in value)) {
1769
- throw new Error("Expected translation and rotation keys in features");
1780
+ const missing = REQUIRED_FEATURES[kind].filter(
1781
+ ([name, axes]) => !(name in value) && !axes.some((axis) => `${name}.${axis}` in value)
1782
+ ).map(([name]) => name);
1783
+ if (missing.length > 0) {
1784
+ throw new Error(
1785
+ `An element of type ${kind} lacks ${missing.join(", ")} in its features`
1786
+ );
1770
1787
  }
1771
1788
  }
1789
+ function isGroupFeatures(value) {
1790
+ assertRequiredFeatures("group", value);
1791
+ }
1772
1792
  function isShapeFeatures(value) {
1773
- if (!value || typeof value !== "object") {
1774
- throw new Error("Expected object");
1775
- }
1776
- if (!["translation", "rotation"].every((key) => key in value)) {
1777
- throw new Error("Expected translation and rotation keys in features");
1778
- }
1793
+ assertRequiredFeatures("shape", value);
1779
1794
  }
1780
1795
  function isEllipseFeatures(value) {
1781
- if (!value || typeof value !== "object") {
1782
- throw new Error("Expected object");
1783
- }
1784
- if (!["translation", "rotation", "height", "width"].every((key) => key in value)) {
1785
- throw new Error(
1786
- "Expected translation, rotation, width, and height keys in features"
1787
- );
1788
- }
1796
+ assertRequiredFeatures("ellipse", value);
1789
1797
  }
1790
1798
  function isRectangleFeatures(value) {
1791
- if (!value || typeof value !== "object") {
1792
- throw new Error("Expected object");
1793
- }
1794
- if (!["translation", "rotation", "height", "width"].every((key) => key in value)) {
1795
- throw new Error(
1796
- "Expected translation, rotation, width, and height keys in features"
1797
- );
1798
- }
1799
+ assertRequiredFeatures("rectangle", value);
1799
1800
  }
1800
1801
  var PRIMITIVE_HALF_EXTENT = {
1801
1802
  ellipse: 1,
@@ -2357,12 +2358,6 @@ var GLB_VERSION = 2;
2357
2358
  var GLB_HEADER_BYTES = 12;
2358
2359
  var GLB_CHUNK_HEADER_BYTES = 8;
2359
2360
  var GLB_JSON_CHUNK_TYPE = 1313821514;
2360
- var EmptyModelError = class extends Error {
2361
- constructor(message) {
2362
- super(message);
2363
- this.name = "EmptyModelError";
2364
- }
2365
- };
2366
2361
  function parseGlbJsonChunk(buffer) {
2367
2362
  if (buffer.byteLength < GLB_HEADER_BYTES + GLB_CHUNK_HEADER_BYTES) {
2368
2363
  return void 0;
@@ -2702,6 +2697,71 @@ async function exportScene(root, options = {}) {
2702
2697
  return sanitizeExportedGlb(gltf, root.name?.trim() || void 0);
2703
2698
  }
2704
2699
 
2700
+ // src/functions/export-animation.ts
2701
+ var import_three4 = require("three");
2702
+ function findByName(root, name) {
2703
+ let found = null;
2704
+ root.traverse((child) => {
2705
+ if (!found && child.name === name) {
2706
+ found = child;
2707
+ }
2708
+ });
2709
+ return found;
2710
+ }
2711
+ function parseMorphTargetName(trackName) {
2712
+ const match = /\.morphTargetInfluences\[(.+)\]$/.exec(trackName);
2713
+ return match ? match[1] : null;
2714
+ }
2715
+ function createExportAnimation(options) {
2716
+ const { name, duration, tracks, root } = options;
2717
+ const issues = [];
2718
+ const built = [];
2719
+ for (const spec of tracks) {
2720
+ const node = findByName(root, spec.elementName);
2721
+ if (!node) {
2722
+ issues.push({
2723
+ trackName: spec.name,
2724
+ reason: "node-not-found",
2725
+ detail: spec.elementName
2726
+ });
2727
+ continue;
2728
+ }
2729
+ if (spec.values.length !== spec.times.length * spec.stride) {
2730
+ issues.push({
2731
+ trackName: spec.name,
2732
+ reason: "bad-stride",
2733
+ detail: `${spec.values.length} values for ${spec.times.length} times at stride ${spec.stride}`
2734
+ });
2735
+ continue;
2736
+ }
2737
+ if (spec.property === "morphTargetInfluences") {
2738
+ const morphName = parseMorphTargetName(spec.name);
2739
+ const dictionary = node.morphTargetDictionary;
2740
+ if (!morphName || !dictionary || !(morphName in dictionary)) {
2741
+ issues.push({
2742
+ trackName: spec.name,
2743
+ reason: "morph-target-not-found",
2744
+ detail: morphName ?? void 0
2745
+ });
2746
+ continue;
2747
+ }
2748
+ built.push(new import_three4.NumberKeyframeTrack(spec.name, spec.times, spec.values));
2749
+ continue;
2750
+ }
2751
+ if (spec.property === "quaternion") {
2752
+ built.push(
2753
+ new import_three4.QuaternionKeyframeTrack(spec.name, spec.times, spec.values)
2754
+ );
2755
+ continue;
2756
+ }
2757
+ built.push(new import_three4.VectorKeyframeTrack(spec.name, spec.times, spec.values));
2758
+ }
2759
+ if (built.length === 0) {
2760
+ return { animation: null, issues };
2761
+ }
2762
+ return { animation: new import_three4.AnimationClip(name, duration, built), issues };
2763
+ }
2764
+
2705
2765
  // src/functions/build-scene-graph.ts
2706
2766
  var THREE4 = __toESM(require("three"));
2707
2767
  var import_utils9 = require("@vizij/utils");
@@ -2807,7 +2867,10 @@ function buildSceneGraph(world, animatables, rootId) {
2807
2867
  const object = new THREE4.Group();
2808
2868
  object.name = entry.name;
2809
2869
  object.userData = { gltfExtensions: { RobotData: robotData } };
2810
- applyFeatures(entry.features, animatables, groupTransform(object));
2870
+ applyFeatures(entry.features, animatables, {
2871
+ ...groupTransform(object),
2872
+ ...perAxisTransform(object, { scale: true })
2873
+ });
2811
2874
  for (const child of entry.children) {
2812
2875
  object.add(buildSceneGraph(world, animatables, child));
2813
2876
  }
@@ -2825,7 +2888,9 @@ function buildSceneGraph(world, animatables, rootId) {
2825
2888
  object.userData = { gltfExtensions: { RobotData: robotData } };
2826
2889
  applyFeatures(entry.features, animatables, {
2827
2890
  ...shapeTransform(object),
2891
+ ...perAxisTransform(object, { scale: true }),
2828
2892
  ...materialFactors(material),
2893
+ ...perAxisMaterialFactors(material),
2829
2894
  ...morphWeights(object, entry)
2830
2895
  });
2831
2896
  for (const child of entry.children ?? []) {
@@ -2842,6 +2907,7 @@ function buildSceneGraph(world, animatables, rootId) {
2842
2907
  applyFeatures(entry.features, animatables, {
2843
2908
  translation,
2844
2909
  rotation,
2910
+ ...perAxisTransform(object, { scale: false }),
2845
2911
  width: (value) => {
2846
2912
  if ((0, import_utils9.instanceOfRawNumber)(value)) object.scale.x = value;
2847
2913
  },
@@ -2882,8 +2948,9 @@ function groupTransform(object) {
2882
2948
  }
2883
2949
  },
2884
2950
  rotation: (value) => {
2885
- if ((0, import_utils9.instanceOfRawEuler)(value)) {
2886
- object.rotation.set(value.x, value.y, value.z, "ZYX");
2951
+ const euler = eulerOf(value);
2952
+ if (euler) {
2953
+ object.rotation.set(...euler, "ZYX");
2887
2954
  } else if ((0, import_utils9.instanceOfRawNumber)(value)) {
2888
2955
  object.rotation.set(0, 0, 0);
2889
2956
  object.rotateZ(value);
@@ -2912,8 +2979,9 @@ function shapeTransform(object) {
2912
2979
  }
2913
2980
  },
2914
2981
  rotation: (value) => {
2915
- if ((0, import_utils9.instanceOfRawEuler)(value)) {
2916
- object.rotation.set(value.x, value.y, value.z, "ZYX");
2982
+ const euler = eulerOf(value);
2983
+ if (euler) {
2984
+ object.rotation.set(...euler, "ZYX");
2917
2985
  }
2918
2986
  },
2919
2987
  scale: (value) => {
@@ -2927,6 +2995,58 @@ function shapeTransform(object) {
2927
2995
  }
2928
2996
  };
2929
2997
  }
2998
+ function eulerOf(value) {
2999
+ if ((0, import_utils9.instanceOfRawEuler)(value)) {
3000
+ return [value.x, value.y, value.z];
3001
+ }
3002
+ const rpy = value;
3003
+ if (rpy !== null && typeof rpy === "object" && typeof rpy.r === "number" && typeof rpy.p === "number" && typeof rpy.y === "number") {
3004
+ return [rpy.r, rpy.p, rpy.y];
3005
+ }
3006
+ return void 0;
3007
+ }
3008
+ function perAxis(name, axes, set) {
3009
+ return Object.fromEntries(
3010
+ axes.map((axis, index) => [
3011
+ `${name}.${axis}`,
3012
+ (value) => {
3013
+ if ((0, import_utils9.instanceOfRawNumber)(value)) {
3014
+ set(index, value);
3015
+ }
3016
+ }
3017
+ ])
3018
+ );
3019
+ }
3020
+ function perAxisTransform(object, { scale }) {
3021
+ return {
3022
+ ...perAxis("translation", ["x", "y", "z"], (index, value) => {
3023
+ object.position.setComponent(index, value);
3024
+ }),
3025
+ ...perAxis("rotation", ["r", "p", "y"], (index, value) => {
3026
+ const euler = [object.rotation.x, object.rotation.y, object.rotation.z];
3027
+ euler[index] = value;
3028
+ object.rotation.set(euler[0], euler[1], euler[2], "ZYX");
3029
+ }),
3030
+ ...scale ? perAxis("scale", ["x", "y", "z"], (index, value) => {
3031
+ object.scale.setComponent(index, value);
3032
+ }) : {}
3033
+ };
3034
+ }
3035
+ function perAxisMaterialFactors(material) {
3036
+ const standard = material;
3037
+ const phong = material;
3038
+ const channels = ["r", "g", "b"];
3039
+ const component = (color) => (index, value) => {
3040
+ if (color) {
3041
+ color[channels[index]] = value;
3042
+ }
3043
+ };
3044
+ return {
3045
+ ...perAxis("color", channels, component(standard.color)),
3046
+ ...perAxis("emissive", channels, component(standard.emissive)),
3047
+ ...perAxis("specular", channels, component(phong.specular))
3048
+ };
3049
+ }
2930
3050
  function materialFactors(material) {
2931
3051
  const standard = material;
2932
3052
  const phong = material;
@@ -3073,6 +3193,7 @@ function authoredDefault(animatable) {
3073
3193
  applyVizijBundle,
3074
3194
  buildSceneGraph,
3075
3195
  canvasMounted,
3196
+ createExportAnimation,
3076
3197
  createVizijStore,
3077
3198
  deriveMorphFeatureKeys,
3078
3199
  deriveRootBounds,
package/dist/index.mjs CHANGED
@@ -757,19 +757,10 @@ var VizijSlice = (set, _get) => ({
757
757
  produce((state) => {
758
758
  const lookupId = getLookup(namespace, id);
759
759
  if (typeof value === "function") {
760
- const current = state.values.get(lookupId);
761
- if (current !== void 0) {
762
- if (value(current !== void 0)) {
763
- state.values.set(lookupId, value(current));
764
- }
765
- } else {
766
- const animatableLookup = state.animatables[id];
767
- const updatedValue = value(
768
- animatableLookup !== void 0 ? animatableLookup.default : void 0
769
- );
770
- if (updatedValue !== void 0) {
771
- state.values.set(lookupId, updatedValue);
772
- }
760
+ const current = state.values.get(lookupId) ?? state.animatables[id]?.default;
761
+ const updatedValue = value(current);
762
+ if (updatedValue !== void 0) {
763
+ state.values.set(lookupId, updatedValue);
773
764
  }
774
765
  } else {
775
766
  state.values.set(lookupId, value);
@@ -1049,6 +1040,14 @@ function recordRenderCounter(key, delta = 1) {
1049
1040
  // src/functions/gltf-loading/traverse-three.ts
1050
1041
  import * as THREE from "three";
1051
1042
 
1043
+ // src/functions/empty-model-error.ts
1044
+ var EmptyModelError = class extends Error {
1045
+ constructor(message) {
1046
+ super(message);
1047
+ this.name = "EmptyModelError";
1048
+ }
1049
+ };
1050
+
1052
1051
  // src/functions/gltf-loading/map-features.ts
1053
1052
  function mapFeatures(features) {
1054
1053
  const animatableValues = {};
@@ -1601,7 +1600,9 @@ function traverseThree(group, aggressiveImport = false, rootBounds) {
1601
1600
  } else {
1602
1601
  const derivedRootBounds = rootBounds ?? deriveRootBounds(group);
1603
1602
  if (!derivedRootBounds) {
1604
- throw new Error("Root bounds are expected if using an aggressive import");
1603
+ throw new EmptyModelError(
1604
+ "The GLB has no RobotData and no mesh, ellipse or rectangle to import"
1605
+ );
1605
1606
  }
1606
1607
  const [newWorldData, newAnimatableData] = importScene(
1607
1608
  group,
@@ -1706,41 +1707,40 @@ function stripMaterialSuffixes(name) {
1706
1707
  function isStoredAnimatedFeature(feature) {
1707
1708
  return Boolean(feature) && typeof feature === "object" && feature.animated === true && "value" in feature;
1708
1709
  }
1709
- function isGroupFeatures(value) {
1710
+ var TRANSFORM = [
1711
+ ["translation", ["x", "y", "z"]],
1712
+ ["rotation", ["r", "p", "y"]]
1713
+ ];
1714
+ var REQUIRED_FEATURES = {
1715
+ group: TRANSFORM,
1716
+ shape: TRANSFORM,
1717
+ ellipse: [...TRANSFORM, ["width", []], ["height", []]],
1718
+ rectangle: [...TRANSFORM, ["width", []], ["height", []]]
1719
+ };
1720
+ function assertRequiredFeatures(kind, value) {
1710
1721
  if (!value || typeof value !== "object") {
1711
1722
  throw new Error("Expected object");
1712
1723
  }
1713
- if (!["translation", "rotation"].every((key) => key in value)) {
1714
- throw new Error("Expected translation and rotation keys in features");
1724
+ const missing = REQUIRED_FEATURES[kind].filter(
1725
+ ([name, axes]) => !(name in value) && !axes.some((axis) => `${name}.${axis}` in value)
1726
+ ).map(([name]) => name);
1727
+ if (missing.length > 0) {
1728
+ throw new Error(
1729
+ `An element of type ${kind} lacks ${missing.join(", ")} in its features`
1730
+ );
1715
1731
  }
1716
1732
  }
1733
+ function isGroupFeatures(value) {
1734
+ assertRequiredFeatures("group", value);
1735
+ }
1717
1736
  function isShapeFeatures(value) {
1718
- if (!value || typeof value !== "object") {
1719
- throw new Error("Expected object");
1720
- }
1721
- if (!["translation", "rotation"].every((key) => key in value)) {
1722
- throw new Error("Expected translation and rotation keys in features");
1723
- }
1737
+ assertRequiredFeatures("shape", value);
1724
1738
  }
1725
1739
  function isEllipseFeatures(value) {
1726
- if (!value || typeof value !== "object") {
1727
- throw new Error("Expected object");
1728
- }
1729
- if (!["translation", "rotation", "height", "width"].every((key) => key in value)) {
1730
- throw new Error(
1731
- "Expected translation, rotation, width, and height keys in features"
1732
- );
1733
- }
1740
+ assertRequiredFeatures("ellipse", value);
1734
1741
  }
1735
1742
  function isRectangleFeatures(value) {
1736
- if (!value || typeof value !== "object") {
1737
- throw new Error("Expected object");
1738
- }
1739
- if (!["translation", "rotation", "height", "width"].every((key) => key in value)) {
1740
- throw new Error(
1741
- "Expected translation, rotation, width, and height keys in features"
1742
- );
1743
- }
1743
+ assertRequiredFeatures("rectangle", value);
1744
1744
  }
1745
1745
  var PRIMITIVE_HALF_EXTENT = {
1746
1746
  ellipse: 1,
@@ -2302,12 +2302,6 @@ var GLB_VERSION = 2;
2302
2302
  var GLB_HEADER_BYTES = 12;
2303
2303
  var GLB_CHUNK_HEADER_BYTES = 8;
2304
2304
  var GLB_JSON_CHUNK_TYPE = 1313821514;
2305
- var EmptyModelError = class extends Error {
2306
- constructor(message) {
2307
- super(message);
2308
- this.name = "EmptyModelError";
2309
- }
2310
- };
2311
2305
  function parseGlbJsonChunk(buffer) {
2312
2306
  if (buffer.byteLength < GLB_HEADER_BYTES + GLB_CHUNK_HEADER_BYTES) {
2313
2307
  return void 0;
@@ -2647,6 +2641,76 @@ async function exportScene(root, options = {}) {
2647
2641
  return sanitizeExportedGlb(gltf, root.name?.trim() || void 0);
2648
2642
  }
2649
2643
 
2644
+ // src/functions/export-animation.ts
2645
+ import {
2646
+ AnimationClip,
2647
+ NumberKeyframeTrack,
2648
+ QuaternionKeyframeTrack,
2649
+ VectorKeyframeTrack
2650
+ } from "three";
2651
+ function findByName(root, name) {
2652
+ let found = null;
2653
+ root.traverse((child) => {
2654
+ if (!found && child.name === name) {
2655
+ found = child;
2656
+ }
2657
+ });
2658
+ return found;
2659
+ }
2660
+ function parseMorphTargetName(trackName) {
2661
+ const match = /\.morphTargetInfluences\[(.+)\]$/.exec(trackName);
2662
+ return match ? match[1] : null;
2663
+ }
2664
+ function createExportAnimation(options) {
2665
+ const { name, duration, tracks, root } = options;
2666
+ const issues = [];
2667
+ const built = [];
2668
+ for (const spec of tracks) {
2669
+ const node = findByName(root, spec.elementName);
2670
+ if (!node) {
2671
+ issues.push({
2672
+ trackName: spec.name,
2673
+ reason: "node-not-found",
2674
+ detail: spec.elementName
2675
+ });
2676
+ continue;
2677
+ }
2678
+ if (spec.values.length !== spec.times.length * spec.stride) {
2679
+ issues.push({
2680
+ trackName: spec.name,
2681
+ reason: "bad-stride",
2682
+ detail: `${spec.values.length} values for ${spec.times.length} times at stride ${spec.stride}`
2683
+ });
2684
+ continue;
2685
+ }
2686
+ if (spec.property === "morphTargetInfluences") {
2687
+ const morphName = parseMorphTargetName(spec.name);
2688
+ const dictionary = node.morphTargetDictionary;
2689
+ if (!morphName || !dictionary || !(morphName in dictionary)) {
2690
+ issues.push({
2691
+ trackName: spec.name,
2692
+ reason: "morph-target-not-found",
2693
+ detail: morphName ?? void 0
2694
+ });
2695
+ continue;
2696
+ }
2697
+ built.push(new NumberKeyframeTrack(spec.name, spec.times, spec.values));
2698
+ continue;
2699
+ }
2700
+ if (spec.property === "quaternion") {
2701
+ built.push(
2702
+ new QuaternionKeyframeTrack(spec.name, spec.times, spec.values)
2703
+ );
2704
+ continue;
2705
+ }
2706
+ built.push(new VectorKeyframeTrack(spec.name, spec.times, spec.values));
2707
+ }
2708
+ if (built.length === 0) {
2709
+ return { animation: null, issues };
2710
+ }
2711
+ return { animation: new AnimationClip(name, duration, built), issues };
2712
+ }
2713
+
2650
2714
  // src/functions/build-scene-graph.ts
2651
2715
  import * as THREE4 from "three";
2652
2716
  import {
@@ -2759,7 +2823,10 @@ function buildSceneGraph(world, animatables, rootId) {
2759
2823
  const object = new THREE4.Group();
2760
2824
  object.name = entry.name;
2761
2825
  object.userData = { gltfExtensions: { RobotData: robotData } };
2762
- applyFeatures(entry.features, animatables, groupTransform(object));
2826
+ applyFeatures(entry.features, animatables, {
2827
+ ...groupTransform(object),
2828
+ ...perAxisTransform(object, { scale: true })
2829
+ });
2763
2830
  for (const child of entry.children) {
2764
2831
  object.add(buildSceneGraph(world, animatables, child));
2765
2832
  }
@@ -2777,7 +2844,9 @@ function buildSceneGraph(world, animatables, rootId) {
2777
2844
  object.userData = { gltfExtensions: { RobotData: robotData } };
2778
2845
  applyFeatures(entry.features, animatables, {
2779
2846
  ...shapeTransform(object),
2847
+ ...perAxisTransform(object, { scale: true }),
2780
2848
  ...materialFactors(material),
2849
+ ...perAxisMaterialFactors(material),
2781
2850
  ...morphWeights(object, entry)
2782
2851
  });
2783
2852
  for (const child of entry.children ?? []) {
@@ -2794,6 +2863,7 @@ function buildSceneGraph(world, animatables, rootId) {
2794
2863
  applyFeatures(entry.features, animatables, {
2795
2864
  translation,
2796
2865
  rotation,
2866
+ ...perAxisTransform(object, { scale: false }),
2797
2867
  width: (value) => {
2798
2868
  if (instanceOfRawNumber(value)) object.scale.x = value;
2799
2869
  },
@@ -2834,8 +2904,9 @@ function groupTransform(object) {
2834
2904
  }
2835
2905
  },
2836
2906
  rotation: (value) => {
2837
- if (instanceOfRawEuler(value)) {
2838
- object.rotation.set(value.x, value.y, value.z, "ZYX");
2907
+ const euler = eulerOf(value);
2908
+ if (euler) {
2909
+ object.rotation.set(...euler, "ZYX");
2839
2910
  } else if (instanceOfRawNumber(value)) {
2840
2911
  object.rotation.set(0, 0, 0);
2841
2912
  object.rotateZ(value);
@@ -2864,8 +2935,9 @@ function shapeTransform(object) {
2864
2935
  }
2865
2936
  },
2866
2937
  rotation: (value) => {
2867
- if (instanceOfRawEuler(value)) {
2868
- object.rotation.set(value.x, value.y, value.z, "ZYX");
2938
+ const euler = eulerOf(value);
2939
+ if (euler) {
2940
+ object.rotation.set(...euler, "ZYX");
2869
2941
  }
2870
2942
  },
2871
2943
  scale: (value) => {
@@ -2879,6 +2951,58 @@ function shapeTransform(object) {
2879
2951
  }
2880
2952
  };
2881
2953
  }
2954
+ function eulerOf(value) {
2955
+ if (instanceOfRawEuler(value)) {
2956
+ return [value.x, value.y, value.z];
2957
+ }
2958
+ const rpy = value;
2959
+ if (rpy !== null && typeof rpy === "object" && typeof rpy.r === "number" && typeof rpy.p === "number" && typeof rpy.y === "number") {
2960
+ return [rpy.r, rpy.p, rpy.y];
2961
+ }
2962
+ return void 0;
2963
+ }
2964
+ function perAxis(name, axes, set) {
2965
+ return Object.fromEntries(
2966
+ axes.map((axis, index) => [
2967
+ `${name}.${axis}`,
2968
+ (value) => {
2969
+ if (instanceOfRawNumber(value)) {
2970
+ set(index, value);
2971
+ }
2972
+ }
2973
+ ])
2974
+ );
2975
+ }
2976
+ function perAxisTransform(object, { scale }) {
2977
+ return {
2978
+ ...perAxis("translation", ["x", "y", "z"], (index, value) => {
2979
+ object.position.setComponent(index, value);
2980
+ }),
2981
+ ...perAxis("rotation", ["r", "p", "y"], (index, value) => {
2982
+ const euler = [object.rotation.x, object.rotation.y, object.rotation.z];
2983
+ euler[index] = value;
2984
+ object.rotation.set(euler[0], euler[1], euler[2], "ZYX");
2985
+ }),
2986
+ ...scale ? perAxis("scale", ["x", "y", "z"], (index, value) => {
2987
+ object.scale.setComponent(index, value);
2988
+ }) : {}
2989
+ };
2990
+ }
2991
+ function perAxisMaterialFactors(material) {
2992
+ const standard = material;
2993
+ const phong = material;
2994
+ const channels = ["r", "g", "b"];
2995
+ const component = (color) => (index, value) => {
2996
+ if (color) {
2997
+ color[channels[index]] = value;
2998
+ }
2999
+ };
3000
+ return {
3001
+ ...perAxis("color", channels, component(standard.color)),
3002
+ ...perAxis("emissive", channels, component(standard.emissive)),
3003
+ ...perAxis("specular", channels, component(phong.specular))
3004
+ };
3005
+ }
2882
3006
  function materialFactors(material) {
2883
3007
  const standard = material;
2884
3008
  const phong = material;
@@ -3024,6 +3148,7 @@ export {
3024
3148
  applyVizijBundle,
3025
3149
  buildSceneGraph,
3026
3150
  canvasMounted,
3151
+ createExportAnimation,
3027
3152
  createVizijStore,
3028
3153
  deriveMorphFeatureKeys,
3029
3154
  deriveRootBounds,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vizij/render",
3
3
  "description": "Vizij faces on a page: the shared canvas and face slots over @vizij/runtime's Bevy view, and the GLB loader, exporter and world model.",
4
- "version": "2.0.0",
4
+ "version": "3.0.0",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/index.d.ts",