@vizij/runtime-react 0.3.0 → 0.4.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/dist/index.cjs CHANGED
@@ -31,11 +31,14 @@ __export(index_exports, {
31
31
  buildPoseWeightRelativePath: () => buildPoseWeightRelativePath,
32
32
  buildRigInputPath: () => buildRigInputPath,
33
33
  buildSemanticPoseWeightPathMap: () => buildSemanticPoseWeightPathMap,
34
+ collectInputPathMap: () => collectInputPathMap,
35
+ composeGraphSpecs: () => composeGraphSpecs,
34
36
  filterPosesBySemanticKind: () => filterPosesBySemanticKind,
35
37
  getPoseSemanticKey: () => getPoseSemanticKey,
36
38
  mapNormalizedControlValue: () => mapNormalizedControlValue,
37
39
  mapUnitControlValue: () => mapUnitControlValue,
38
40
  normalizePoseSemanticKey: () => normalizePoseSemanticKey,
41
+ resolveAnimationBridgeOutputPaths: () => resolveAnimationBridgeOutputPaths,
39
42
  resolveFaceControls: () => resolveFaceControls,
40
43
  resolvePoseMembership: () => resolvePoseMembership,
41
44
  resolvePoseSemantics: () => resolvePoseSemantics,
@@ -1475,7 +1478,7 @@ function fieldNumber(value) {
1475
1478
  if (!value || typeof value !== "object") {
1476
1479
  return null;
1477
1480
  }
1478
- for (const key of ["u64", "u32", "f32", "f64"]) {
1481
+ for (const key of ["u64", "u32", "f32", "f64", "float"]) {
1479
1482
  if (key in value) {
1480
1483
  const parsed = Number(value[key]);
1481
1484
  return Number.isFinite(parsed) ? parsed : null;
@@ -1483,38 +1486,90 @@ function fieldNumber(value) {
1483
1486
  }
1484
1487
  return null;
1485
1488
  }
1489
+ function fieldText(value) {
1490
+ if (!value || typeof value !== "object") {
1491
+ return "";
1492
+ }
1493
+ const record = value;
1494
+ for (const key of ["str", "text"]) {
1495
+ if (typeof record[key] === "string") {
1496
+ return record[key];
1497
+ }
1498
+ }
1499
+ return "";
1500
+ }
1501
+ function playerStateFromFields(byId) {
1502
+ const player = fieldNumber(byId.get(ANIMATION_MODULE_FIELD.statePlayer));
1503
+ if (player === null) {
1504
+ return null;
1505
+ }
1506
+ return {
1507
+ player,
1508
+ state: fieldText(byId.get(ANIMATION_MODULE_FIELD.stateState)),
1509
+ time: (fieldNumber(byId.get(ANIMATION_MODULE_FIELD.stateTimeNs)) ?? 0) / 1e9,
1510
+ duration: (fieldNumber(byId.get(ANIMATION_MODULE_FIELD.stateDurationNs)) ?? 0) / 1e9,
1511
+ speed: fieldNumber(byId.get(ANIMATION_MODULE_FIELD.stateSpeed)) ?? 1
1512
+ };
1513
+ }
1514
+ function fieldsToMap(fields) {
1515
+ if (!Array.isArray(fields)) {
1516
+ return null;
1517
+ }
1518
+ const byId = /* @__PURE__ */ new Map();
1519
+ for (const entry of fields) {
1520
+ const id = entry?.id;
1521
+ const value = entry?.value;
1522
+ if (typeof id === "string" && value && typeof value === "object") {
1523
+ byId.set(id, value);
1524
+ }
1525
+ }
1526
+ return byId;
1527
+ }
1528
+ function recordToMap(element) {
1529
+ if (!element || typeof element !== "object") {
1530
+ return null;
1531
+ }
1532
+ const record = element.record;
1533
+ if (!record || typeof record !== "object" || Array.isArray(record)) {
1534
+ return null;
1535
+ }
1536
+ const byId = /* @__PURE__ */ new Map();
1537
+ for (const [id, value] of Object.entries(record)) {
1538
+ if (value && typeof value === "object") {
1539
+ byId.set(id, value);
1540
+ }
1541
+ }
1542
+ return byId;
1543
+ }
1486
1544
  function decodePlayerStates(raw) {
1487
- if (!raw || typeof raw !== "object" || !("structs" in raw)) {
1545
+ if (!raw || typeof raw !== "object") {
1488
1546
  return [];
1489
1547
  }
1490
- const structs = raw.structs;
1491
- const elements = Array.isArray(structs?.elements) ? structs.elements : [];
1548
+ const container = raw;
1492
1549
  const states = [];
1493
- for (const element of elements) {
1494
- const fields = element?.fields;
1495
- if (!Array.isArray(fields)) {
1496
- continue;
1497
- }
1498
- const byId = /* @__PURE__ */ new Map();
1499
- for (const entry of fields) {
1500
- const id = entry?.id;
1501
- const value = entry?.value;
1502
- if (typeof id === "string" && value && typeof value === "object") {
1503
- byId.set(id, value);
1550
+ const structs = container.structs;
1551
+ if (structs && Array.isArray(structs.elements)) {
1552
+ for (const element of structs.elements) {
1553
+ const byId = fieldsToMap(element?.fields);
1554
+ const state = byId ? playerStateFromFields(byId) : null;
1555
+ if (state) {
1556
+ states.push(state);
1504
1557
  }
1505
1558
  }
1506
- const player = fieldNumber(byId.get(ANIMATION_MODULE_FIELD.statePlayer));
1507
- if (player === null) {
1508
- continue;
1559
+ return states;
1560
+ }
1561
+ const elements = Array.isArray(container.list) ? container.list : Array.isArray(container.array) ? container.array : null;
1562
+ if (!elements) {
1563
+ return states;
1564
+ }
1565
+ for (const element of elements) {
1566
+ const byId = recordToMap(element) ?? fieldsToMap(
1567
+ element?.struct?.fields ?? element?.fields
1568
+ );
1569
+ const state = byId ? playerStateFromFields(byId) : null;
1570
+ if (state) {
1571
+ states.push(state);
1509
1572
  }
1510
- const stateValue = byId.get(ANIMATION_MODULE_FIELD.stateState);
1511
- states.push({
1512
- player,
1513
- state: stateValue && "str" in stateValue ? String(stateValue.str) : "",
1514
- time: (fieldNumber(byId.get(ANIMATION_MODULE_FIELD.stateTimeNs)) ?? 0) / 1e9,
1515
- duration: (fieldNumber(byId.get(ANIMATION_MODULE_FIELD.stateDurationNs)) ?? 0) / 1e9,
1516
- speed: fieldNumber(byId.get(ANIMATION_MODULE_FIELD.stateSpeed)) ?? 1
1517
- });
1518
1573
  }
1519
1574
  return states;
1520
1575
  }
@@ -1584,6 +1639,7 @@ var AnimationModuleHost = class {
1584
1639
  speed: 1,
1585
1640
  loop: "loop",
1586
1641
  weight: 1,
1642
+ pendingSeekSeconds: null,
1587
1643
  animId: null,
1588
1644
  playerId: null,
1589
1645
  instId: null
@@ -1622,6 +1678,8 @@ var AnimationModuleHost = class {
1622
1678
  }
1623
1679
  entry.playing = false;
1624
1680
  this.dispatch(
1681
+ "pause",
1682
+ clipId,
1625
1683
  (device) => entry.playerId !== null ? device.call(pauseCall(entry.playerId)) : null
1626
1684
  );
1627
1685
  }
@@ -1637,6 +1695,8 @@ var AnimationModuleHost = class {
1637
1695
  }
1638
1696
  entry.playing = false;
1639
1697
  this.dispatch(
1698
+ "stop",
1699
+ clipId,
1640
1700
  (device) => entry.playerId !== null ? device.call(stopCall(entry.playerId)) : null
1641
1701
  );
1642
1702
  }
@@ -1646,7 +1706,14 @@ var AnimationModuleHost = class {
1646
1706
  if (!entry) {
1647
1707
  return;
1648
1708
  }
1709
+ if (entry.playerId === null) {
1710
+ entry.pendingSeekSeconds = seconds;
1711
+ return;
1712
+ }
1713
+ entry.pendingSeekSeconds = null;
1649
1714
  this.dispatch(
1715
+ "seek",
1716
+ clipId,
1650
1717
  (device) => entry.playerId !== null ? device.call(seekCall(entry.playerId, seconds * 1e9)) : null
1651
1718
  );
1652
1719
  }
@@ -1658,6 +1725,8 @@ var AnimationModuleHost = class {
1658
1725
  }
1659
1726
  entry.speed = speed;
1660
1727
  this.dispatch(
1728
+ "set speed",
1729
+ clipId,
1661
1730
  (device) => entry.playerId !== null ? device.call(setSpeedCall(entry.playerId, speed)) : null
1662
1731
  );
1663
1732
  }
@@ -1669,6 +1738,8 @@ var AnimationModuleHost = class {
1669
1738
  }
1670
1739
  entry.loop = mode;
1671
1740
  this.dispatch(
1741
+ "set loop",
1742
+ clipId,
1672
1743
  (device) => entry.playerId !== null ? device.call(setLoopCall(entry.playerId, mode)) : null
1673
1744
  );
1674
1745
  }
@@ -1680,6 +1751,8 @@ var AnimationModuleHost = class {
1680
1751
  }
1681
1752
  entry.weight = weight;
1682
1753
  this.dispatch(
1754
+ "set weight",
1755
+ clipId,
1683
1756
  (device) => entry.playerId !== null && entry.instId !== null ? device.call(setWeightCall(entry.playerId, entry.instId, weight)) : null
1684
1757
  );
1685
1758
  }
@@ -1687,6 +1760,9 @@ var AnimationModuleHost = class {
1687
1760
  * Re-issue setup and transport for every playing clip against a freshly
1688
1761
  * started device (module guest state did not survive the rebuild). Voids
1689
1762
  * stale ids first. Called from `DeviceSlot.onDeviceStarted`.
1763
+ *
1764
+ * A freshly loaded player advances without an explicit `play` — asserted by
1765
+ * the rebuild case in `animationPipeline.device.test.ts`.
1690
1766
  */
1691
1767
  replayInto(device) {
1692
1768
  for (const entry of this.clips.values()) {
@@ -1699,12 +1775,35 @@ var AnimationModuleHost = class {
1699
1775
  }
1700
1776
  }
1701
1777
  /** Fire-and-forget a transport call against the live device, if any. */
1702
- dispatch(issue) {
1778
+ /**
1779
+ * Issue a transport call, reporting rather than swallowing a rejection.
1780
+ *
1781
+ * This used to discard the error on the assumption that the only cause was
1782
+ * a rebuild racing the call, which `replayInto` would repair. When that is
1783
+ * not the cause — the module missing from the live device, say — the call
1784
+ * fails, the player keeps running, and the next frame of `player_states`
1785
+ * feedback tells the transport UI it is still playing. The button flips
1786
+ * back and the pause looks like it did nothing, with nothing anywhere
1787
+ * saying why. Whatever the cause, a dropped transport command is worth a
1788
+ * line in the console.
1789
+ */
1790
+ dispatch(what, clipId, issue) {
1703
1791
  const device = this.getDevice();
1704
1792
  if (!device) {
1705
1793
  return;
1706
1794
  }
1707
- void issue(device)?.catch(() => {
1795
+ const issued = issue(device);
1796
+ if (!issued) {
1797
+ console.warn(
1798
+ `[vizij-runtime] animation ${what} had no player to act on for "${clipId}"`
1799
+ );
1800
+ return;
1801
+ }
1802
+ void issued.catch((err) => {
1803
+ console.warn(
1804
+ `[vizij-runtime] animation ${what} was dropped for "${clipId}"`,
1805
+ err
1806
+ );
1708
1807
  });
1709
1808
  }
1710
1809
  async ensureLoaded(device, entry) {
@@ -1742,6 +1841,12 @@ var AnimationModuleHost = class {
1742
1841
  setWeightCall(entry.playerId, entry.instId, entry.weight)
1743
1842
  );
1744
1843
  }
1844
+ if (entry.pendingSeekSeconds !== null) {
1845
+ followups.push(
1846
+ seekCall(entry.playerId, entry.pendingSeekSeconds * 1e9)
1847
+ );
1848
+ entry.pendingSeekSeconds = null;
1849
+ }
1745
1850
  }
1746
1851
  for (const call of followups) {
1747
1852
  await device.call(call);
@@ -2322,6 +2427,23 @@ function convertBundlePrograms(entries) {
2322
2427
  };
2323
2428
  }).filter(Boolean);
2324
2429
  }
2430
+ function convertBundleStarred(section) {
2431
+ const items = section?.items;
2432
+ if (!Array.isArray(items) || items.length === 0) {
2433
+ return void 0;
2434
+ }
2435
+ const resolved = [];
2436
+ for (const item of items) {
2437
+ if (!item || typeof item !== "object") {
2438
+ continue;
2439
+ }
2440
+ const { kind, id } = item;
2441
+ if ((kind === "driver" || kind === "pose") && typeof id === "string" && id) {
2442
+ resolved.push({ kind, id });
2443
+ }
2444
+ }
2445
+ return resolved.length > 0 ? resolved : void 0;
2446
+ }
2325
2447
  function resolveConstraintDefault(path, namespace, inputConstraints) {
2326
2448
  const trimmed = path.trim();
2327
2449
  if (!trimmed) {
@@ -2479,6 +2601,12 @@ function mergeAssetBundle(base, extracted, extractedAnimations) {
2479
2601
  hasBaseProgramsOverride,
2480
2602
  convertBundlePrograms(resolvedBundle?.graphs)
2481
2603
  );
2604
+ const hasBaseStarredOverride = Object.prototype.hasOwnProperty.call(
2605
+ base,
2606
+ "starred"
2607
+ );
2608
+ const starredFromBundle = convertBundleStarred(resolvedBundle?.starred);
2609
+ const resolvedStarred = hasBaseStarredOverride ? base.starred : base.starred ?? starredFromBundle;
2482
2610
  const merged = {
2483
2611
  ...base
2484
2612
  };
@@ -2490,6 +2618,7 @@ function mergeAssetBundle(base, extracted, extractedAnimations) {
2490
2618
  merged.pose = resolvedPose;
2491
2619
  merged.animations = resolvedAnimations;
2492
2620
  merged.programs = programsFromBundle;
2621
+ merged.starred = resolvedStarred;
2493
2622
  merged.bundle = resolvedBundle;
2494
2623
  return merged;
2495
2624
  }
@@ -2778,6 +2907,8 @@ function VizijRuntimeProviderInner({
2778
2907
  const animationModuleRef = (0, import_react2.useRef)(null);
2779
2908
  const animationHostRef = (0, import_react2.useRef)(null);
2780
2909
  const animationsSourceRegisteredRef = (0, import_react2.useRef)(false);
2910
+ const lastRegisteredModuleRevisionRef = (0, import_react2.useRef)(0);
2911
+ const lastBehaviorErrorRef = (0, import_react2.useRef)(void 0);
2781
2912
  const animationGapWarnedRef = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
2782
2913
  const animationSystemActiveRef = (0, import_react2.useRef)(true);
2783
2914
  const stagedInputsRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
@@ -3004,14 +3135,15 @@ function VizijRuntimeProviderInner({
3004
3135
  return null;
3005
3136
  }
3006
3137
  if (!animationHostRef.current) {
3007
- animationHostRef.current = new AnimationModuleHost(
3008
- getDevice,
3009
- (key) => resolveAnimationBridgeOutputPaths(
3138
+ animationHostRef.current = new AnimationModuleHost(getDevice, (key) => {
3139
+ const namespace2 = namespaceRef.current;
3140
+ const resolved = resolveAnimationBridgeOutputPaths(
3010
3141
  key,
3011
3142
  faceIdRef.current ?? void 0,
3012
3143
  rigInputMapRef.current
3013
- )
3014
- );
3144
+ ).map((path) => namespaceTypedPath(path, namespace2));
3145
+ return [...new Set(resolved)];
3146
+ });
3015
3147
  }
3016
3148
  return animationHostRef.current;
3017
3149
  }, [getDevice]);
@@ -3040,6 +3172,7 @@ function VizijRuntimeProviderInner({
3040
3172
  setReady(true);
3041
3173
  }, []);
3042
3174
  const hasBundleAnimations = (assetBundle.animations?.length ?? 0) > 0;
3175
+ const [animationModuleReadyRevision, setAnimationModuleReadyRevision] = (0, import_react2.useState)(0);
3043
3176
  (0, import_react2.useEffect)(() => {
3044
3177
  if (!hasBundleAnimations || animationModuleRef.current) {
3045
3178
  return;
@@ -3055,6 +3188,7 @@ function VizijRuntimeProviderInner({
3055
3188
  animationHostRef.current?.replayInto(handle.device);
3056
3189
  };
3057
3190
  ensureAnimationHost();
3191
+ setAnimationModuleReadyRevision((previous) => previous + 1);
3058
3192
  }).catch((err) => {
3059
3193
  pushError({
3060
3194
  message: "Failed to load the animation module",
@@ -3187,6 +3321,17 @@ function VizijRuntimeProviderInner({
3187
3321
  if (!handle.device.running) {
3188
3322
  handle.device.step(dt * 1e3);
3189
3323
  }
3324
+ const behaviorError = handle.device.behaviorError;
3325
+ if (behaviorError !== lastBehaviorErrorRef.current) {
3326
+ lastBehaviorErrorRef.current = behaviorError;
3327
+ if (behaviorError) {
3328
+ pushError({
3329
+ message: `Graph tick failed: ${behaviorError}`,
3330
+ phase: "engine",
3331
+ timestamp: performance.now()
3332
+ });
3333
+ }
3334
+ }
3190
3335
  const changes = handle.device.drainChanges();
3191
3336
  applyEngineChangesRef.current(changes);
3192
3337
  if (storeChangeListenersRef.current.size > 0) {
@@ -3516,11 +3661,9 @@ function VizijRuntimeProviderInner({
3516
3661
  rigInputMapRef.current
3517
3662
  );
3518
3663
  if (isRuntimeDebugEnabled()) {
3519
- console.log("[vizij-runtime] animation output routing", {
3520
- animationId: animation.id,
3521
- bridgeOutputs,
3522
- bridgeOutputsText: bridgeOutputs.join(" | ")
3523
- });
3664
+ console.log(
3665
+ `[vizij-runtime] animation output routing ${animation.id} (${bridgeOutputs.length}) | ` + bridgeOutputs.join(" | ")
3666
+ );
3524
3667
  }
3525
3668
  recordOutputs(bridgeOutputs);
3526
3669
  }
@@ -3592,7 +3735,35 @@ function VizijRuntimeProviderInner({
3592
3735
  });
3593
3736
  }
3594
3737
  }
3595
- ensureAnimationHost()?.setClips(hostClips);
3738
+ const host = ensureAnimationHost();
3739
+ host?.setClips(hostClips);
3740
+ if (host) {
3741
+ let resumed = false;
3742
+ clipPlaybackRef.current.forEach((clipState, clipId) => {
3743
+ if (!clipState.playing || !host.has(clipId)) {
3744
+ return;
3745
+ }
3746
+ resumed = true;
3747
+ host.setSpeed(clipId, clipState.speed);
3748
+ host.setWeight(clipId, clipState.weight);
3749
+ host.setLoop(clipId, clipState.loop ? "loop" : "once");
3750
+ if (clipState.playhead > 0) {
3751
+ host.seek(clipId, clipState.playhead);
3752
+ }
3753
+ void host.play(clipId).catch((err) => {
3754
+ pushError({
3755
+ message: `Failed to resume animation ${clipId} after the module became ready`,
3756
+ cause: err,
3757
+ phase: "animation",
3758
+ timestamp: performance.now()
3759
+ });
3760
+ });
3761
+ });
3762
+ if (resumed) {
3763
+ setAnimationsSourceRegistered(host.hasPlaying());
3764
+ updateLoopMode();
3765
+ }
3766
+ }
3596
3767
  registeredAnimationsRef.current = animationIds;
3597
3768
  if (assetBundle.initialInputs) {
3598
3769
  Object.entries(assetBundle.initialInputs).forEach(([path, value]) => {
@@ -3637,7 +3808,9 @@ function VizijRuntimeProviderInner({
3637
3808
  registerMergedGraph,
3638
3809
  reportStatus,
3639
3810
  resolvedProgramAssets,
3640
- setInput
3811
+ setInput,
3812
+ setAnimationsSourceRegistered,
3813
+ updateLoopMode
3641
3814
  ]);
3642
3815
  (0, import_react2.useEffect)(() => {
3643
3816
  if (!ready || status.loading) {
@@ -3645,7 +3818,9 @@ function VizijRuntimeProviderInner({
3645
3818
  }
3646
3819
  const plan = pendingPlanRef.current;
3647
3820
  const hasRegistered = registeredGraphsRef.current.length > 0 || registeredAnimationsRef.current.length > 0 || programControllerIdsRef.current.size > 0;
3648
- if (plan && !plan.reregisterGraphs && hasRegistered) {
3821
+ const forcedByModuleReady = animationModuleReadyRevision !== lastRegisteredModuleRevisionRef.current;
3822
+ lastRegisteredModuleRevisionRef.current = animationModuleReadyRevision;
3823
+ if (plan && !plan.reregisterGraphs && hasRegistered && !forcedByModuleReady) {
3649
3824
  return;
3650
3825
  }
3651
3826
  registerControllers().catch((err) => {
@@ -3656,7 +3831,14 @@ function VizijRuntimeProviderInner({
3656
3831
  timestamp: performance.now()
3657
3832
  });
3658
3833
  });
3659
- }, [ready, status.loading, graphUpdateToken, registerControllers, pushError]);
3834
+ }, [
3835
+ ready,
3836
+ status.loading,
3837
+ graphUpdateToken,
3838
+ registerControllers,
3839
+ pushError,
3840
+ animationModuleReadyRevision
3841
+ ]);
3660
3842
  (0, import_react2.useEffect)(() => {
3661
3843
  applyEngineChangesRef.current = (changes) => {
3662
3844
  const entries = Object.entries(changes);
@@ -3774,6 +3956,23 @@ function VizijRuntimeProviderInner({
3774
3956
  entry?.resolve();
3775
3957
  }
3776
3958
  }, []);
3959
+ const advanceClipPlayheads = (0, import_react2.useCallback)((dt) => {
3960
+ if (!(dt > 0) || !Number.isFinite(dt)) {
3961
+ return;
3962
+ }
3963
+ clipPlaybackRef.current.forEach((state) => {
3964
+ if (!state.playing) {
3965
+ return;
3966
+ }
3967
+ const speed = Number.isFinite(state.speed) && state.speed > 0 ? state.speed : 1;
3968
+ const next = state.playhead + dt * speed;
3969
+ if (!(state.duration > 0)) {
3970
+ state.playhead = next;
3971
+ return;
3972
+ }
3973
+ state.playhead = state.loop ? next % state.duration : Math.min(next, state.duration);
3974
+ });
3975
+ }, []);
3777
3976
  const advanceAnimationTweens = (0, import_react2.useCallback)(
3778
3977
  (dt) => {
3779
3978
  if (animationTweensRef.current.size === 0) {
@@ -3838,6 +4037,7 @@ function VizijRuntimeProviderInner({
3838
4037
  weight: 1,
3839
4038
  loop: true,
3840
4039
  playing: false,
4040
+ playhead: 0,
3841
4041
  resolve: null,
3842
4042
  completion: null
3843
4043
  };
@@ -3936,8 +4136,17 @@ function VizijRuntimeProviderInner({
3936
4136
  return [];
3937
4137
  }
3938
4138
  const raw = device.readValues([ANIMATION_PLAYERS_PATH])[ANIMATION_PLAYERS_PATH];
3939
- return decodePlayerStates(raw);
3940
- }, [deviceSlot]);
4139
+ const states = decodePlayerStates(raw);
4140
+ if (DEV_MODE && states.length === 0 && raw && typeof raw === "object") {
4141
+ warnAnimationGap(
4142
+ "player-states-undecodable",
4143
+ `Animation player_states feedback at "${ANIMATION_PLAYERS_PATH}" could not be decoded (top-level keys: ${Object.keys(
4144
+ raw
4145
+ ).join(", ")}). The transport falls back to a host-side playhead.`
4146
+ );
4147
+ }
4148
+ return states;
4149
+ }, [deviceSlot, warnAnimationGap]);
3941
4150
  const settleFinishedClips = (0, import_react2.useCallback)(() => {
3942
4151
  let pending = false;
3943
4152
  clipPlaybackRef.current.forEach((state) => {
@@ -3993,6 +4202,7 @@ function VizijRuntimeProviderInner({
3993
4202
  if (host) {
3994
4203
  if (options?.reset === true) {
3995
4204
  host.seek(id, 0);
4205
+ state.playhead = 0;
3996
4206
  }
3997
4207
  host.setSpeed(id, state.speed);
3998
4208
  host.setWeight(id, state.weight);
@@ -4029,6 +4239,9 @@ function VizijRuntimeProviderInner({
4029
4239
  (id) => {
4030
4240
  const state = clipPlaybackRef.current.get(id);
4031
4241
  if (!state || !state.playing) {
4242
+ console.warn(
4243
+ `[vizij-runtime] pause ignored for "${id}": ${state ? "the clip is not marked playing" : "no clip state"}`
4244
+ );
4032
4245
  return;
4033
4246
  }
4034
4247
  state.playing = false;
@@ -4038,10 +4251,13 @@ function VizijRuntimeProviderInner({
4038
4251
  [syncAnimationsSource]
4039
4252
  );
4040
4253
  const seekAnimation = (0, import_react2.useCallback)((id, timeSeconds) => {
4041
- if (!clipPlaybackRef.current.has(id)) {
4254
+ const state = clipPlaybackRef.current.get(id);
4255
+ if (!state) {
4042
4256
  return;
4043
4257
  }
4044
- animationHostRef.current?.seek(id, Math.max(0, timeSeconds));
4258
+ const target = Math.max(0, timeSeconds);
4259
+ state.playhead = state.duration > 0 ? Math.min(target, state.duration) : target;
4260
+ animationHostRef.current?.seek(id, target);
4045
4261
  }, []);
4046
4262
  const setAnimationLoop = (0, import_react2.useCallback)(
4047
4263
  (id, enabled) => {
@@ -4064,9 +4280,23 @@ function VizijRuntimeProviderInner({
4064
4280
  const playerId = animationHostRef.current?.playerIdOf(id) ?? null;
4065
4281
  const feedback = playerId !== null ? readPlayerStates().find((entry) => entry.player === playerId) : void 0;
4066
4282
  return {
4067
- time: feedback?.time ?? 0,
4283
+ // Feedback is authoritative; the host-side playhead stands in when it
4284
+ // is unavailable. Never fall back to a hard 0 — that reads as "the
4285
+ // animation is parked at the start" rather than "no telemetry", and it
4286
+ // pins the transport clock and defeats seeking.
4287
+ time: feedback?.time ?? state.playhead,
4068
4288
  duration: feedback && feedback.duration > 0 ? feedback.duration : state.duration,
4069
- playing: feedback ? feedback.state === "playing" : state.playing,
4289
+ // A clip commanded to pause is paused, whatever the feedback says.
4290
+ //
4291
+ // Feedback goes stale by construction: pausing the last playing clip
4292
+ // unregisters the animations graph source, so the module stops being
4293
+ // stepped and `player_states` keeps reporting whatever it last said —
4294
+ // `state: "playing"`, with the playhead frozen. Treating that as
4295
+ // authoritative meant the transport reverted to playing a frame after
4296
+ // every pause, leaving a Pause button over a clip that was not
4297
+ // advancing. Feedback still decides while the clip is *meant* to be
4298
+ // playing, which is what notices a clip reaching its end.
4299
+ playing: state.playing && (feedback ? feedback.state === "playing" : true),
4070
4300
  loop: state.loop,
4071
4301
  speed: feedback?.speed ?? state.speed
4072
4302
  };
@@ -4087,6 +4317,9 @@ function VizijRuntimeProviderInner({
4087
4317
  syncAnimationsSource();
4088
4318
  } else {
4089
4319
  host?.stop(id);
4320
+ if (state) {
4321
+ state.playhead = 0;
4322
+ }
4090
4323
  const unsubscribe = subscribeToStep(() => {
4091
4324
  unsubscribe();
4092
4325
  syncAnimationsSource();
@@ -4421,6 +4654,13 @@ function VizijRuntimeProviderInner({
4421
4654
  },
4422
4655
  [faceId, namespace, pushError, setInput, setRendererValue]
4423
4656
  );
4657
+ const reconcileAnimationsSource = (0, import_react2.useCallback)(() => {
4658
+ const host = animationHostRef.current;
4659
+ if (!host || !host.hasPlaying()) {
4660
+ return;
4661
+ }
4662
+ setAnimationsSourceRegistered(true);
4663
+ }, [setAnimationsSourceRegistered]);
4424
4664
  const flushStagedInputs = (0, import_react2.useCallback)(() => {
4425
4665
  if (stagedInputsRef.current.size === 0) {
4426
4666
  return;
@@ -4438,6 +4678,8 @@ function VizijRuntimeProviderInner({
4438
4678
  avgStepDtRef.current = prev * (1 - alpha) + dt * alpha;
4439
4679
  }
4440
4680
  advanceAnimationTweens(dt);
4681
+ advanceClipPlayheads(dt);
4682
+ reconcileAnimationsSource();
4441
4683
  flushStagedInputs();
4442
4684
  if (driveRuntimeRef.current || opts?.forceRuntime) {
4443
4685
  stepRuntime(dt);
@@ -4446,7 +4688,9 @@ function VizijRuntimeProviderInner({
4446
4688
  },
4447
4689
  [
4448
4690
  advanceAnimationTweens,
4691
+ advanceClipPlayheads,
4449
4692
  flushStagedInputs,
4693
+ reconcileAnimationsSource,
4450
4694
  settleFinishedClips,
4451
4695
  stepRuntime
4452
4696
  ]
@@ -5103,11 +5347,14 @@ function mapUnitControlValue(control, unitValue) {
5103
5347
  buildPoseWeightRelativePath,
5104
5348
  buildRigInputPath,
5105
5349
  buildSemanticPoseWeightPathMap,
5350
+ collectInputPathMap,
5351
+ composeGraphSpecs,
5106
5352
  filterPosesBySemanticKind,
5107
5353
  getPoseSemanticKey,
5108
5354
  mapNormalizedControlValue,
5109
5355
  mapUnitControlValue,
5110
5356
  normalizePoseSemanticKey,
5357
+ resolveAnimationBridgeOutputPaths,
5111
5358
  resolveFaceControls,
5112
5359
  resolvePoseMembership,
5113
5360
  resolvePoseSemantics,
package/dist/index.d.cts CHANGED
@@ -4,7 +4,7 @@ import { ReactNode, PropsWithChildren } from 'react';
4
4
  import { ValueJSON } from '@vizij/value-json';
5
5
  import { IrGraph } from '@vizij/node-graph-authoring';
6
6
  import { RawValue, AnimatableValue } from '@vizij/utils';
7
- import { World, VizijBundleExtension, VizijProps } from '@vizij/render';
7
+ import { World, VizijBundleExtension, VizijStarredItem, VizijProps } from '@vizij/render';
8
8
 
9
9
  /** Value shape hint. Accepted through the public surface, unused by the device. */
10
10
  type ShapeJSON = Record<string, unknown>;
@@ -156,6 +156,12 @@ type VizijAssetBundle = {
156
156
  };
157
157
  animations?: VizijAnimationAsset[];
158
158
  programs?: VizijProgramAsset[];
159
+ /**
160
+ * Designer-curated set of starred drivers/poses, resolved from the loaded
161
+ * bundle's `starred` section. References real functionality by stable id
162
+ * (driver = standard-input id, pose = pose id).
163
+ */
164
+ starred?: VizijStarredItem[];
159
165
  initialInputs?: Record<string, ValueJSON>;
160
166
  metadata?: Record<string, unknown>;
161
167
  bundle?: VizijBundleExtension | null;
@@ -394,4 +400,43 @@ declare function resolveFaceControls(assetBundle: VizijAssetBundle, runtimeFaceI
394
400
  declare function mapNormalizedControlValue(control: FaceScalarControl, normalizedValue: number): number;
395
401
  declare function mapUnitControlValue(control: FaceScalarControl, unitValue: number): number;
396
402
 
397
- export { type AnimateValueOptions, type AnimationClipLike, type AnimationKeyframeLike, type AnimationPlaybackState, type AnimationTrackLike, EMOTION_POSE_KEYS, EXPRESSIVE_EMOTION_POSE_KEYS, type FaceScalarControl, type InputDriverContext, type InputDriverFactory, type InputDriverLifecycle, POSE_WEIGHT_INPUT_PATH_PREFIX, type PlayAnimationOptions, type PoseDefinition, type PoseGroupDefinition, type PoseRigConfig, type ProgramPlaybackState, type RootBounds, type RuntimeGraphBundle, type RuntimeOutputWrite, type RuntimeUpdatePlan, type RuntimeUpdateTier, type StopProgramOptions, VISEME_POSE_KEYS, type VizijAnimationAsset, type VizijAssetBundle, type VizijGlbAsset, type VizijGraphAsset, type VizijProgramAsset, VizijRuntimeFace, type VizijRuntimeFaceProps, VizijRuntimeProvider, type VizijRuntimeProviderProps, type VizijRuntimeStatus, buildPoseWeightInputPathSegment, buildPoseWeightPathMap, buildPoseWeightRelativePath, buildRigInputPath, buildSemanticPoseWeightPathMap, filterPosesBySemanticKind, getPoseSemanticKey, mapNormalizedControlValue, mapUnitControlValue, normalizePoseSemanticKey, resolveFaceControls, resolvePoseMembership, resolvePoseSemantics, resolveRuntimeUpdatePlan, useOptionalVizijRuntime, useRigInput, useVizijOutputs, useVizijRuntime };
403
+ /**
404
+ * Compose several Vizij graph specs into the ONE graph an Arora device runs.
405
+ *
406
+ * The device runs a single ProcessingGraph as its behavior, so the separate
407
+ * graph sources become a union of nodes and edges here. Sources and their
408
+ * provenance (see `graphSourcesRef` in VizijRuntimeProvider for details):
409
+ * the **rig** graph shipped in the loaded GLB/asset bundle, the bundle's
410
+ * **pose-driver** graph, and one source per **playing program** (bundle
411
+ * programs and the authoring motiongraph, which publishes the editor's
412
+ * graph as a program). Node ids
413
+ * are prefixed `${sourceId}::` so sources can't collide; `params.path` is
414
+ * deliberately NOT prefixed — path identity on the device's shared store is
415
+ * the cross-graph contract (a pose output written to a store path is read by
416
+ * a rig input node with the same path on the next tick).
417
+ *
418
+ * Output-path collisions across sources (two `output` nodes writing one
419
+ * path) are warned and resolved last-writer-wins by evaluation order; there
420
+ * is no additive merge for that case yet, and current bundles don't collide.
421
+ * If a real bundle does, insert an explicit combiner node here instead of
422
+ * widening this warning.
423
+ */
424
+ type SpecRecord = Record<string, unknown>;
425
+ /** A graph spec treated structurally (same tolerance as `utils/graph.ts`). */
426
+ type ComposableSpec = SpecRecord;
427
+ interface GraphSource {
428
+ /** Stable id of the source graph (e.g. "rig", "pose", a program id). */
429
+ sourceId: string;
430
+ spec: ComposableSpec;
431
+ }
432
+ /**
433
+ * Union `sources` into one spec. Later sources evaluate after earlier ones,
434
+ * so on an output-path collision the last source wins (warned).
435
+ */
436
+ declare function composeGraphSpecs(sources: GraphSource[]): ComposableSpec;
437
+
438
+ declare function collectInputPathMap(spec: GraphRegistrationConfig["spec"]): Record<string, string>;
439
+
440
+ declare function resolveAnimationBridgeOutputPaths(channel: string, faceId?: string, rigInputMap?: Record<string, string>): string[];
441
+
442
+ export { type AnimateValueOptions, type AnimationClipLike, type AnimationKeyframeLike, type AnimationPlaybackState, type AnimationTrackLike, type ComposableSpec, EMOTION_POSE_KEYS, EXPRESSIVE_EMOTION_POSE_KEYS, type FaceScalarControl, type GraphSource, type InputDriverContext, type InputDriverFactory, type InputDriverLifecycle, POSE_WEIGHT_INPUT_PATH_PREFIX, type PlayAnimationOptions, type PoseDefinition, type PoseGroupDefinition, type PoseRigConfig, type ProgramPlaybackState, type RootBounds, type RuntimeGraphBundle, type RuntimeOutputWrite, type RuntimeUpdatePlan, type RuntimeUpdateTier, type StopProgramOptions, VISEME_POSE_KEYS, type VizijAnimationAsset, type VizijAssetBundle, type VizijGlbAsset, type VizijGraphAsset, type VizijProgramAsset, VizijRuntimeFace, type VizijRuntimeFaceProps, VizijRuntimeProvider, type VizijRuntimeProviderProps, type VizijRuntimeStatus, buildPoseWeightInputPathSegment, buildPoseWeightPathMap, buildPoseWeightRelativePath, buildRigInputPath, buildSemanticPoseWeightPathMap, collectInputPathMap, composeGraphSpecs, filterPosesBySemanticKind, getPoseSemanticKey, mapNormalizedControlValue, mapUnitControlValue, normalizePoseSemanticKey, resolveAnimationBridgeOutputPaths, resolveFaceControls, resolvePoseMembership, resolvePoseSemantics, resolveRuntimeUpdatePlan, useOptionalVizijRuntime, useRigInput, useVizijOutputs, useVizijRuntime };