@vizij/runtime-react 0.3.1 → 0.4.1

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.js CHANGED
@@ -1443,7 +1443,7 @@ function fieldNumber(value) {
1443
1443
  if (!value || typeof value !== "object") {
1444
1444
  return null;
1445
1445
  }
1446
- for (const key of ["u64", "u32", "f32", "f64"]) {
1446
+ for (const key of ["u64", "u32", "f32", "f64", "float"]) {
1447
1447
  if (key in value) {
1448
1448
  const parsed = Number(value[key]);
1449
1449
  return Number.isFinite(parsed) ? parsed : null;
@@ -1451,38 +1451,90 @@ function fieldNumber(value) {
1451
1451
  }
1452
1452
  return null;
1453
1453
  }
1454
+ function fieldText(value) {
1455
+ if (!value || typeof value !== "object") {
1456
+ return "";
1457
+ }
1458
+ const record = value;
1459
+ for (const key of ["str", "text"]) {
1460
+ if (typeof record[key] === "string") {
1461
+ return record[key];
1462
+ }
1463
+ }
1464
+ return "";
1465
+ }
1466
+ function playerStateFromFields(byId) {
1467
+ const player = fieldNumber(byId.get(ANIMATION_MODULE_FIELD.statePlayer));
1468
+ if (player === null) {
1469
+ return null;
1470
+ }
1471
+ return {
1472
+ player,
1473
+ state: fieldText(byId.get(ANIMATION_MODULE_FIELD.stateState)),
1474
+ time: (fieldNumber(byId.get(ANIMATION_MODULE_FIELD.stateTimeNs)) ?? 0) / 1e9,
1475
+ duration: (fieldNumber(byId.get(ANIMATION_MODULE_FIELD.stateDurationNs)) ?? 0) / 1e9,
1476
+ speed: fieldNumber(byId.get(ANIMATION_MODULE_FIELD.stateSpeed)) ?? 1
1477
+ };
1478
+ }
1479
+ function fieldsToMap(fields) {
1480
+ if (!Array.isArray(fields)) {
1481
+ return null;
1482
+ }
1483
+ const byId = /* @__PURE__ */ new Map();
1484
+ for (const entry of fields) {
1485
+ const id = entry?.id;
1486
+ const value = entry?.value;
1487
+ if (typeof id === "string" && value && typeof value === "object") {
1488
+ byId.set(id, value);
1489
+ }
1490
+ }
1491
+ return byId;
1492
+ }
1493
+ function recordToMap(element) {
1494
+ if (!element || typeof element !== "object") {
1495
+ return null;
1496
+ }
1497
+ const record = element.record;
1498
+ if (!record || typeof record !== "object" || Array.isArray(record)) {
1499
+ return null;
1500
+ }
1501
+ const byId = /* @__PURE__ */ new Map();
1502
+ for (const [id, value] of Object.entries(record)) {
1503
+ if (value && typeof value === "object") {
1504
+ byId.set(id, value);
1505
+ }
1506
+ }
1507
+ return byId;
1508
+ }
1454
1509
  function decodePlayerStates(raw) {
1455
- if (!raw || typeof raw !== "object" || !("structs" in raw)) {
1510
+ if (!raw || typeof raw !== "object") {
1456
1511
  return [];
1457
1512
  }
1458
- const structs = raw.structs;
1459
- const elements = Array.isArray(structs?.elements) ? structs.elements : [];
1513
+ const container = raw;
1460
1514
  const states = [];
1461
- for (const element of elements) {
1462
- const fields = element?.fields;
1463
- if (!Array.isArray(fields)) {
1464
- continue;
1465
- }
1466
- const byId = /* @__PURE__ */ new Map();
1467
- for (const entry of fields) {
1468
- const id = entry?.id;
1469
- const value = entry?.value;
1470
- if (typeof id === "string" && value && typeof value === "object") {
1471
- byId.set(id, value);
1515
+ const structs = container.structs;
1516
+ if (structs && Array.isArray(structs.elements)) {
1517
+ for (const element of structs.elements) {
1518
+ const byId = fieldsToMap(element?.fields);
1519
+ const state = byId ? playerStateFromFields(byId) : null;
1520
+ if (state) {
1521
+ states.push(state);
1472
1522
  }
1473
1523
  }
1474
- const player = fieldNumber(byId.get(ANIMATION_MODULE_FIELD.statePlayer));
1475
- if (player === null) {
1476
- continue;
1524
+ return states;
1525
+ }
1526
+ const elements = Array.isArray(container.list) ? container.list : Array.isArray(container.array) ? container.array : null;
1527
+ if (!elements) {
1528
+ return states;
1529
+ }
1530
+ for (const element of elements) {
1531
+ const byId = recordToMap(element) ?? fieldsToMap(
1532
+ element?.struct?.fields ?? element?.fields
1533
+ );
1534
+ const state = byId ? playerStateFromFields(byId) : null;
1535
+ if (state) {
1536
+ states.push(state);
1477
1537
  }
1478
- const stateValue = byId.get(ANIMATION_MODULE_FIELD.stateState);
1479
- states.push({
1480
- player,
1481
- state: stateValue && "str" in stateValue ? String(stateValue.str) : "",
1482
- time: (fieldNumber(byId.get(ANIMATION_MODULE_FIELD.stateTimeNs)) ?? 0) / 1e9,
1483
- duration: (fieldNumber(byId.get(ANIMATION_MODULE_FIELD.stateDurationNs)) ?? 0) / 1e9,
1484
- speed: fieldNumber(byId.get(ANIMATION_MODULE_FIELD.stateSpeed)) ?? 1
1485
- });
1486
1538
  }
1487
1539
  return states;
1488
1540
  }
@@ -1552,6 +1604,7 @@ var AnimationModuleHost = class {
1552
1604
  speed: 1,
1553
1605
  loop: "loop",
1554
1606
  weight: 1,
1607
+ pendingSeekSeconds: null,
1555
1608
  animId: null,
1556
1609
  playerId: null,
1557
1610
  instId: null
@@ -1590,6 +1643,8 @@ var AnimationModuleHost = class {
1590
1643
  }
1591
1644
  entry.playing = false;
1592
1645
  this.dispatch(
1646
+ "pause",
1647
+ clipId,
1593
1648
  (device) => entry.playerId !== null ? device.call(pauseCall(entry.playerId)) : null
1594
1649
  );
1595
1650
  }
@@ -1605,6 +1660,8 @@ var AnimationModuleHost = class {
1605
1660
  }
1606
1661
  entry.playing = false;
1607
1662
  this.dispatch(
1663
+ "stop",
1664
+ clipId,
1608
1665
  (device) => entry.playerId !== null ? device.call(stopCall(entry.playerId)) : null
1609
1666
  );
1610
1667
  }
@@ -1614,7 +1671,14 @@ var AnimationModuleHost = class {
1614
1671
  if (!entry) {
1615
1672
  return;
1616
1673
  }
1674
+ if (entry.playerId === null) {
1675
+ entry.pendingSeekSeconds = seconds;
1676
+ return;
1677
+ }
1678
+ entry.pendingSeekSeconds = null;
1617
1679
  this.dispatch(
1680
+ "seek",
1681
+ clipId,
1618
1682
  (device) => entry.playerId !== null ? device.call(seekCall(entry.playerId, seconds * 1e9)) : null
1619
1683
  );
1620
1684
  }
@@ -1626,6 +1690,8 @@ var AnimationModuleHost = class {
1626
1690
  }
1627
1691
  entry.speed = speed;
1628
1692
  this.dispatch(
1693
+ "set speed",
1694
+ clipId,
1629
1695
  (device) => entry.playerId !== null ? device.call(setSpeedCall(entry.playerId, speed)) : null
1630
1696
  );
1631
1697
  }
@@ -1637,6 +1703,8 @@ var AnimationModuleHost = class {
1637
1703
  }
1638
1704
  entry.loop = mode;
1639
1705
  this.dispatch(
1706
+ "set loop",
1707
+ clipId,
1640
1708
  (device) => entry.playerId !== null ? device.call(setLoopCall(entry.playerId, mode)) : null
1641
1709
  );
1642
1710
  }
@@ -1648,6 +1716,8 @@ var AnimationModuleHost = class {
1648
1716
  }
1649
1717
  entry.weight = weight;
1650
1718
  this.dispatch(
1719
+ "set weight",
1720
+ clipId,
1651
1721
  (device) => entry.playerId !== null && entry.instId !== null ? device.call(setWeightCall(entry.playerId, entry.instId, weight)) : null
1652
1722
  );
1653
1723
  }
@@ -1655,6 +1725,9 @@ var AnimationModuleHost = class {
1655
1725
  * Re-issue setup and transport for every playing clip against a freshly
1656
1726
  * started device (module guest state did not survive the rebuild). Voids
1657
1727
  * stale ids first. Called from `DeviceSlot.onDeviceStarted`.
1728
+ *
1729
+ * A freshly loaded player advances without an explicit `play` — asserted by
1730
+ * the rebuild case in `animationPipeline.device.test.ts`.
1658
1731
  */
1659
1732
  replayInto(device) {
1660
1733
  for (const entry of this.clips.values()) {
@@ -1667,12 +1740,35 @@ var AnimationModuleHost = class {
1667
1740
  }
1668
1741
  }
1669
1742
  /** Fire-and-forget a transport call against the live device, if any. */
1670
- dispatch(issue) {
1743
+ /**
1744
+ * Issue a transport call, reporting rather than swallowing a rejection.
1745
+ *
1746
+ * This used to discard the error on the assumption that the only cause was
1747
+ * a rebuild racing the call, which `replayInto` would repair. When that is
1748
+ * not the cause — the module missing from the live device, say — the call
1749
+ * fails, the player keeps running, and the next frame of `player_states`
1750
+ * feedback tells the transport UI it is still playing. The button flips
1751
+ * back and the pause looks like it did nothing, with nothing anywhere
1752
+ * saying why. Whatever the cause, a dropped transport command is worth a
1753
+ * line in the console.
1754
+ */
1755
+ dispatch(what, clipId, issue) {
1671
1756
  const device = this.getDevice();
1672
1757
  if (!device) {
1673
1758
  return;
1674
1759
  }
1675
- void issue(device)?.catch(() => {
1760
+ const issued = issue(device);
1761
+ if (!issued) {
1762
+ console.warn(
1763
+ `[vizij-runtime] animation ${what} had no player to act on for "${clipId}"`
1764
+ );
1765
+ return;
1766
+ }
1767
+ void issued.catch((err) => {
1768
+ console.warn(
1769
+ `[vizij-runtime] animation ${what} was dropped for "${clipId}"`,
1770
+ err
1771
+ );
1676
1772
  });
1677
1773
  }
1678
1774
  async ensureLoaded(device, entry) {
@@ -1710,6 +1806,12 @@ var AnimationModuleHost = class {
1710
1806
  setWeightCall(entry.playerId, entry.instId, entry.weight)
1711
1807
  );
1712
1808
  }
1809
+ if (entry.pendingSeekSeconds !== null) {
1810
+ followups.push(
1811
+ seekCall(entry.playerId, entry.pendingSeekSeconds * 1e9)
1812
+ );
1813
+ entry.pendingSeekSeconds = null;
1814
+ }
1713
1815
  }
1714
1816
  for (const call of followups) {
1715
1817
  await device.call(call);
@@ -2290,6 +2392,23 @@ function convertBundlePrograms(entries) {
2290
2392
  };
2291
2393
  }).filter(Boolean);
2292
2394
  }
2395
+ function convertBundleStarred(section) {
2396
+ const items = section?.items;
2397
+ if (!Array.isArray(items) || items.length === 0) {
2398
+ return void 0;
2399
+ }
2400
+ const resolved = [];
2401
+ for (const item of items) {
2402
+ if (!item || typeof item !== "object") {
2403
+ continue;
2404
+ }
2405
+ const { kind, id } = item;
2406
+ if ((kind === "driver" || kind === "pose") && typeof id === "string" && id) {
2407
+ resolved.push({ kind, id });
2408
+ }
2409
+ }
2410
+ return resolved.length > 0 ? resolved : void 0;
2411
+ }
2293
2412
  function resolveConstraintDefault(path, namespace, inputConstraints) {
2294
2413
  const trimmed = path.trim();
2295
2414
  if (!trimmed) {
@@ -2447,6 +2566,12 @@ function mergeAssetBundle(base, extracted, extractedAnimations) {
2447
2566
  hasBaseProgramsOverride,
2448
2567
  convertBundlePrograms(resolvedBundle?.graphs)
2449
2568
  );
2569
+ const hasBaseStarredOverride = Object.prototype.hasOwnProperty.call(
2570
+ base,
2571
+ "starred"
2572
+ );
2573
+ const starredFromBundle = convertBundleStarred(resolvedBundle?.starred);
2574
+ const resolvedStarred = hasBaseStarredOverride ? base.starred : base.starred ?? starredFromBundle;
2450
2575
  const merged = {
2451
2576
  ...base
2452
2577
  };
@@ -2458,6 +2583,7 @@ function mergeAssetBundle(base, extracted, extractedAnimations) {
2458
2583
  merged.pose = resolvedPose;
2459
2584
  merged.animations = resolvedAnimations;
2460
2585
  merged.programs = programsFromBundle;
2586
+ merged.starred = resolvedStarred;
2461
2587
  merged.bundle = resolvedBundle;
2462
2588
  return merged;
2463
2589
  }
@@ -2746,6 +2872,8 @@ function VizijRuntimeProviderInner({
2746
2872
  const animationModuleRef = useRef(null);
2747
2873
  const animationHostRef = useRef(null);
2748
2874
  const animationsSourceRegisteredRef = useRef(false);
2875
+ const lastRegisteredModuleRevisionRef = useRef(0);
2876
+ const lastBehaviorErrorRef = useRef(void 0);
2749
2877
  const animationGapWarnedRef = useRef(/* @__PURE__ */ new Set());
2750
2878
  const animationSystemActiveRef = useRef(true);
2751
2879
  const stagedInputsRef = useRef(/* @__PURE__ */ new Map());
@@ -2972,14 +3100,15 @@ function VizijRuntimeProviderInner({
2972
3100
  return null;
2973
3101
  }
2974
3102
  if (!animationHostRef.current) {
2975
- animationHostRef.current = new AnimationModuleHost(
2976
- getDevice,
2977
- (key) => resolveAnimationBridgeOutputPaths(
3103
+ animationHostRef.current = new AnimationModuleHost(getDevice, (key) => {
3104
+ const namespace2 = namespaceRef.current;
3105
+ const resolved = resolveAnimationBridgeOutputPaths(
2978
3106
  key,
2979
3107
  faceIdRef.current ?? void 0,
2980
3108
  rigInputMapRef.current
2981
- )
2982
- );
3109
+ ).map((path) => namespaceTypedPath(path, namespace2));
3110
+ return [...new Set(resolved)];
3111
+ });
2983
3112
  }
2984
3113
  return animationHostRef.current;
2985
3114
  }, [getDevice]);
@@ -3008,6 +3137,7 @@ function VizijRuntimeProviderInner({
3008
3137
  setReady(true);
3009
3138
  }, []);
3010
3139
  const hasBundleAnimations = (assetBundle.animations?.length ?? 0) > 0;
3140
+ const [animationModuleReadyRevision, setAnimationModuleReadyRevision] = useState(0);
3011
3141
  useEffect(() => {
3012
3142
  if (!hasBundleAnimations || animationModuleRef.current) {
3013
3143
  return;
@@ -3023,6 +3153,7 @@ function VizijRuntimeProviderInner({
3023
3153
  animationHostRef.current?.replayInto(handle.device);
3024
3154
  };
3025
3155
  ensureAnimationHost();
3156
+ setAnimationModuleReadyRevision((previous) => previous + 1);
3026
3157
  }).catch((err) => {
3027
3158
  pushError({
3028
3159
  message: "Failed to load the animation module",
@@ -3155,6 +3286,17 @@ function VizijRuntimeProviderInner({
3155
3286
  if (!handle.device.running) {
3156
3287
  handle.device.step(dt * 1e3);
3157
3288
  }
3289
+ const behaviorError = handle.device.behaviorError;
3290
+ if (behaviorError !== lastBehaviorErrorRef.current) {
3291
+ lastBehaviorErrorRef.current = behaviorError;
3292
+ if (behaviorError) {
3293
+ pushError({
3294
+ message: `Graph tick failed: ${behaviorError}`,
3295
+ phase: "engine",
3296
+ timestamp: performance.now()
3297
+ });
3298
+ }
3299
+ }
3158
3300
  const changes = handle.device.drainChanges();
3159
3301
  applyEngineChangesRef.current(changes);
3160
3302
  if (storeChangeListenersRef.current.size > 0) {
@@ -3286,7 +3428,6 @@ function VizijRuntimeProviderInner({
3286
3428
  if (glbAsset.kind === "url") {
3287
3429
  const loaded = await loadGLTFWithBundle(
3288
3430
  glbAsset.src,
3289
- [namespace],
3290
3431
  glbAsset.aggressiveImport ?? false,
3291
3432
  glbAsset.rootBounds
3292
3433
  );
@@ -3297,7 +3438,6 @@ function VizijRuntimeProviderInner({
3297
3438
  } else if (glbAsset.kind === "blob") {
3298
3439
  const loaded = await loadGLTFFromBlobWithBundle(
3299
3440
  glbAsset.blob,
3300
- [namespace],
3301
3441
  glbAsset.aggressiveImport ?? false,
3302
3442
  glbAsset.rootBounds
3303
3443
  );
@@ -3484,11 +3624,9 @@ function VizijRuntimeProviderInner({
3484
3624
  rigInputMapRef.current
3485
3625
  );
3486
3626
  if (isRuntimeDebugEnabled()) {
3487
- console.log("[vizij-runtime] animation output routing", {
3488
- animationId: animation.id,
3489
- bridgeOutputs,
3490
- bridgeOutputsText: bridgeOutputs.join(" | ")
3491
- });
3627
+ console.log(
3628
+ `[vizij-runtime] animation output routing ${animation.id} (${bridgeOutputs.length}) | ` + bridgeOutputs.join(" | ")
3629
+ );
3492
3630
  }
3493
3631
  recordOutputs(bridgeOutputs);
3494
3632
  }
@@ -3560,7 +3698,35 @@ function VizijRuntimeProviderInner({
3560
3698
  });
3561
3699
  }
3562
3700
  }
3563
- ensureAnimationHost()?.setClips(hostClips);
3701
+ const host = ensureAnimationHost();
3702
+ host?.setClips(hostClips);
3703
+ if (host) {
3704
+ let resumed = false;
3705
+ clipPlaybackRef.current.forEach((clipState, clipId) => {
3706
+ if (!clipState.playing || !host.has(clipId)) {
3707
+ return;
3708
+ }
3709
+ resumed = true;
3710
+ host.setSpeed(clipId, clipState.speed);
3711
+ host.setWeight(clipId, clipState.weight);
3712
+ host.setLoop(clipId, clipState.loop ? "loop" : "once");
3713
+ if (clipState.playhead > 0) {
3714
+ host.seek(clipId, clipState.playhead);
3715
+ }
3716
+ void host.play(clipId).catch((err) => {
3717
+ pushError({
3718
+ message: `Failed to resume animation ${clipId} after the module became ready`,
3719
+ cause: err,
3720
+ phase: "animation",
3721
+ timestamp: performance.now()
3722
+ });
3723
+ });
3724
+ });
3725
+ if (resumed) {
3726
+ setAnimationsSourceRegistered(host.hasPlaying());
3727
+ updateLoopMode();
3728
+ }
3729
+ }
3564
3730
  registeredAnimationsRef.current = animationIds;
3565
3731
  if (assetBundle.initialInputs) {
3566
3732
  Object.entries(assetBundle.initialInputs).forEach(([path, value]) => {
@@ -3605,7 +3771,9 @@ function VizijRuntimeProviderInner({
3605
3771
  registerMergedGraph,
3606
3772
  reportStatus,
3607
3773
  resolvedProgramAssets,
3608
- setInput
3774
+ setInput,
3775
+ setAnimationsSourceRegistered,
3776
+ updateLoopMode
3609
3777
  ]);
3610
3778
  useEffect(() => {
3611
3779
  if (!ready || status.loading) {
@@ -3613,7 +3781,9 @@ function VizijRuntimeProviderInner({
3613
3781
  }
3614
3782
  const plan = pendingPlanRef.current;
3615
3783
  const hasRegistered = registeredGraphsRef.current.length > 0 || registeredAnimationsRef.current.length > 0 || programControllerIdsRef.current.size > 0;
3616
- if (plan && !plan.reregisterGraphs && hasRegistered) {
3784
+ const forcedByModuleReady = animationModuleReadyRevision !== lastRegisteredModuleRevisionRef.current;
3785
+ lastRegisteredModuleRevisionRef.current = animationModuleReadyRevision;
3786
+ if (plan && !plan.reregisterGraphs && hasRegistered && !forcedByModuleReady) {
3617
3787
  return;
3618
3788
  }
3619
3789
  registerControllers().catch((err) => {
@@ -3624,7 +3794,14 @@ function VizijRuntimeProviderInner({
3624
3794
  timestamp: performance.now()
3625
3795
  });
3626
3796
  });
3627
- }, [ready, status.loading, graphUpdateToken, registerControllers, pushError]);
3797
+ }, [
3798
+ ready,
3799
+ status.loading,
3800
+ graphUpdateToken,
3801
+ registerControllers,
3802
+ pushError,
3803
+ animationModuleReadyRevision
3804
+ ]);
3628
3805
  useEffect(() => {
3629
3806
  applyEngineChangesRef.current = (changes) => {
3630
3807
  const entries = Object.entries(changes);
@@ -3742,6 +3919,23 @@ function VizijRuntimeProviderInner({
3742
3919
  entry?.resolve();
3743
3920
  }
3744
3921
  }, []);
3922
+ const advanceClipPlayheads = useCallback((dt) => {
3923
+ if (!(dt > 0) || !Number.isFinite(dt)) {
3924
+ return;
3925
+ }
3926
+ clipPlaybackRef.current.forEach((state) => {
3927
+ if (!state.playing) {
3928
+ return;
3929
+ }
3930
+ const speed = Number.isFinite(state.speed) && state.speed > 0 ? state.speed : 1;
3931
+ const next = state.playhead + dt * speed;
3932
+ if (!(state.duration > 0)) {
3933
+ state.playhead = next;
3934
+ return;
3935
+ }
3936
+ state.playhead = state.loop ? next % state.duration : Math.min(next, state.duration);
3937
+ });
3938
+ }, []);
3745
3939
  const advanceAnimationTweens = useCallback(
3746
3940
  (dt) => {
3747
3941
  if (animationTweensRef.current.size === 0) {
@@ -3806,6 +4000,7 @@ function VizijRuntimeProviderInner({
3806
4000
  weight: 1,
3807
4001
  loop: true,
3808
4002
  playing: false,
4003
+ playhead: 0,
3809
4004
  resolve: null,
3810
4005
  completion: null
3811
4006
  };
@@ -3904,8 +4099,17 @@ function VizijRuntimeProviderInner({
3904
4099
  return [];
3905
4100
  }
3906
4101
  const raw = device.readValues([ANIMATION_PLAYERS_PATH])[ANIMATION_PLAYERS_PATH];
3907
- return decodePlayerStates(raw);
3908
- }, [deviceSlot]);
4102
+ const states = decodePlayerStates(raw);
4103
+ if (DEV_MODE && states.length === 0 && raw && typeof raw === "object") {
4104
+ warnAnimationGap(
4105
+ "player-states-undecodable",
4106
+ `Animation player_states feedback at "${ANIMATION_PLAYERS_PATH}" could not be decoded (top-level keys: ${Object.keys(
4107
+ raw
4108
+ ).join(", ")}). The transport falls back to a host-side playhead.`
4109
+ );
4110
+ }
4111
+ return states;
4112
+ }, [deviceSlot, warnAnimationGap]);
3909
4113
  const settleFinishedClips = useCallback(() => {
3910
4114
  let pending = false;
3911
4115
  clipPlaybackRef.current.forEach((state) => {
@@ -3961,6 +4165,7 @@ function VizijRuntimeProviderInner({
3961
4165
  if (host) {
3962
4166
  if (options?.reset === true) {
3963
4167
  host.seek(id, 0);
4168
+ state.playhead = 0;
3964
4169
  }
3965
4170
  host.setSpeed(id, state.speed);
3966
4171
  host.setWeight(id, state.weight);
@@ -3997,6 +4202,9 @@ function VizijRuntimeProviderInner({
3997
4202
  (id) => {
3998
4203
  const state = clipPlaybackRef.current.get(id);
3999
4204
  if (!state || !state.playing) {
4205
+ console.warn(
4206
+ `[vizij-runtime] pause ignored for "${id}": ${state ? "the clip is not marked playing" : "no clip state"}`
4207
+ );
4000
4208
  return;
4001
4209
  }
4002
4210
  state.playing = false;
@@ -4006,10 +4214,13 @@ function VizijRuntimeProviderInner({
4006
4214
  [syncAnimationsSource]
4007
4215
  );
4008
4216
  const seekAnimation = useCallback((id, timeSeconds) => {
4009
- if (!clipPlaybackRef.current.has(id)) {
4217
+ const state = clipPlaybackRef.current.get(id);
4218
+ if (!state) {
4010
4219
  return;
4011
4220
  }
4012
- animationHostRef.current?.seek(id, Math.max(0, timeSeconds));
4221
+ const target = Math.max(0, timeSeconds);
4222
+ state.playhead = state.duration > 0 ? Math.min(target, state.duration) : target;
4223
+ animationHostRef.current?.seek(id, target);
4013
4224
  }, []);
4014
4225
  const setAnimationLoop = useCallback(
4015
4226
  (id, enabled) => {
@@ -4032,9 +4243,23 @@ function VizijRuntimeProviderInner({
4032
4243
  const playerId = animationHostRef.current?.playerIdOf(id) ?? null;
4033
4244
  const feedback = playerId !== null ? readPlayerStates().find((entry) => entry.player === playerId) : void 0;
4034
4245
  return {
4035
- time: feedback?.time ?? 0,
4246
+ // Feedback is authoritative; the host-side playhead stands in when it
4247
+ // is unavailable. Never fall back to a hard 0 — that reads as "the
4248
+ // animation is parked at the start" rather than "no telemetry", and it
4249
+ // pins the transport clock and defeats seeking.
4250
+ time: feedback?.time ?? state.playhead,
4036
4251
  duration: feedback && feedback.duration > 0 ? feedback.duration : state.duration,
4037
- playing: feedback ? feedback.state === "playing" : state.playing,
4252
+ // A clip commanded to pause is paused, whatever the feedback says.
4253
+ //
4254
+ // Feedback goes stale by construction: pausing the last playing clip
4255
+ // unregisters the animations graph source, so the module stops being
4256
+ // stepped and `player_states` keeps reporting whatever it last said —
4257
+ // `state: "playing"`, with the playhead frozen. Treating that as
4258
+ // authoritative meant the transport reverted to playing a frame after
4259
+ // every pause, leaving a Pause button over a clip that was not
4260
+ // advancing. Feedback still decides while the clip is *meant* to be
4261
+ // playing, which is what notices a clip reaching its end.
4262
+ playing: state.playing && (feedback ? feedback.state === "playing" : true),
4038
4263
  loop: state.loop,
4039
4264
  speed: feedback?.speed ?? state.speed
4040
4265
  };
@@ -4055,6 +4280,9 @@ function VizijRuntimeProviderInner({
4055
4280
  syncAnimationsSource();
4056
4281
  } else {
4057
4282
  host?.stop(id);
4283
+ if (state) {
4284
+ state.playhead = 0;
4285
+ }
4058
4286
  const unsubscribe = subscribeToStep(() => {
4059
4287
  unsubscribe();
4060
4288
  syncAnimationsSource();
@@ -4389,6 +4617,13 @@ function VizijRuntimeProviderInner({
4389
4617
  },
4390
4618
  [faceId, namespace, pushError, setInput, setRendererValue]
4391
4619
  );
4620
+ const reconcileAnimationsSource = useCallback(() => {
4621
+ const host = animationHostRef.current;
4622
+ if (!host || !host.hasPlaying()) {
4623
+ return;
4624
+ }
4625
+ setAnimationsSourceRegistered(true);
4626
+ }, [setAnimationsSourceRegistered]);
4392
4627
  const flushStagedInputs = useCallback(() => {
4393
4628
  if (stagedInputsRef.current.size === 0) {
4394
4629
  return;
@@ -4406,6 +4641,8 @@ function VizijRuntimeProviderInner({
4406
4641
  avgStepDtRef.current = prev * (1 - alpha) + dt * alpha;
4407
4642
  }
4408
4643
  advanceAnimationTweens(dt);
4644
+ advanceClipPlayheads(dt);
4645
+ reconcileAnimationsSource();
4409
4646
  flushStagedInputs();
4410
4647
  if (driveRuntimeRef.current || opts?.forceRuntime) {
4411
4648
  stepRuntime(dt);
@@ -4414,7 +4651,9 @@ function VizijRuntimeProviderInner({
4414
4651
  },
4415
4652
  [
4416
4653
  advanceAnimationTweens,
4654
+ advanceClipPlayheads,
4417
4655
  flushStagedInputs,
4656
+ reconcileAnimationsSource,
4418
4657
  settleFinishedClips,
4419
4658
  stepRuntime
4420
4659
  ]
@@ -5074,11 +5313,14 @@ export {
5074
5313
  buildPoseWeightRelativePath,
5075
5314
  buildRigInputPath,
5076
5315
  buildSemanticPoseWeightPathMap,
5316
+ collectInputPathMap,
5317
+ composeGraphSpecs,
5077
5318
  filterPosesBySemanticKind,
5078
5319
  getPoseSemanticKey,
5079
5320
  mapNormalizedControlValue,
5080
5321
  mapUnitControlValue,
5081
5322
  normalizePoseSemanticKey,
5323
+ resolveAnimationBridgeOutputPaths,
5082
5324
  resolveFaceControls,
5083
5325
  resolvePoseMembership,
5084
5326
  resolvePoseSemantics,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vizij/runtime-react",
3
3
  "description": "Runtime provider that drives Vizij renderer assets with an arora device engine for React apps.",
4
- "version": "0.3.1",
4
+ "version": "0.4.1",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -38,9 +38,9 @@
38
38
  "prepack": "pnpm run build && pnpm run test && pnpm run typecheck && pnpm run lint"
39
39
  },
40
40
  "dependencies": {
41
- "@vizij/animation-module": "^0.2.0",
42
- "@vizij/node-graph-authoring": "^0.2.1",
43
- "@vizij/render": "^0.1.2",
41
+ "@vizij/animation-module": "^1.0.0",
42
+ "@vizij/node-graph-authoring": "^0.2.2",
43
+ "@vizij/render": "^1.0.0",
44
44
  "@vizij/runtime": "^2.4.0",
45
45
  "@vizij/utils": "^0.2.0",
46
46
  "@vizij/value-json": "^0.2.0"