@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/README.md +1 -0
- package/dist/index.cjs +295 -50
- package/dist/index.d.cts +47 -2
- package/dist/index.d.ts +47 -2
- package/dist/index.js +292 -50
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -460,6 +460,7 @@ See:
|
|
|
460
460
|
|
|
461
461
|
- [`apps/tutorial-fullscreen-face/tutorial.md`](../../../apps/tutorial-fullscreen-face/tutorial.md)
|
|
462
462
|
- [`apps/tutorial-agent-face/tutorial.md`](../../../apps/tutorial-agent-face/tutorial.md)
|
|
463
|
+
- [`apps/vizij-standalone`](../../../apps/vizij-standalone/README.md) — the face page with a voice agent, on `@vizij/runtime` directly
|
|
463
464
|
|
|
464
465
|
### Runtime-truthful authoring
|
|
465
466
|
|
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"
|
|
1545
|
+
if (!raw || typeof raw !== "object") {
|
|
1488
1546
|
return [];
|
|
1489
1547
|
}
|
|
1490
|
-
const
|
|
1491
|
-
const elements = Array.isArray(structs?.elements) ? structs.elements : [];
|
|
1548
|
+
const container = raw;
|
|
1492
1549
|
const states = [];
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
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
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3009
|
-
|
|
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) {
|
|
@@ -3318,7 +3463,6 @@ function VizijRuntimeProviderInner({
|
|
|
3318
3463
|
if (glbAsset.kind === "url") {
|
|
3319
3464
|
const loaded = await (0, import_render.loadGLTFWithBundle)(
|
|
3320
3465
|
glbAsset.src,
|
|
3321
|
-
[namespace],
|
|
3322
3466
|
glbAsset.aggressiveImport ?? false,
|
|
3323
3467
|
glbAsset.rootBounds
|
|
3324
3468
|
);
|
|
@@ -3329,7 +3473,6 @@ function VizijRuntimeProviderInner({
|
|
|
3329
3473
|
} else if (glbAsset.kind === "blob") {
|
|
3330
3474
|
const loaded = await (0, import_render.loadGLTFFromBlobWithBundle)(
|
|
3331
3475
|
glbAsset.blob,
|
|
3332
|
-
[namespace],
|
|
3333
3476
|
glbAsset.aggressiveImport ?? false,
|
|
3334
3477
|
glbAsset.rootBounds
|
|
3335
3478
|
);
|
|
@@ -3516,11 +3659,9 @@ function VizijRuntimeProviderInner({
|
|
|
3516
3659
|
rigInputMapRef.current
|
|
3517
3660
|
);
|
|
3518
3661
|
if (isRuntimeDebugEnabled()) {
|
|
3519
|
-
console.log(
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
bridgeOutputsText: bridgeOutputs.join(" | ")
|
|
3523
|
-
});
|
|
3662
|
+
console.log(
|
|
3663
|
+
`[vizij-runtime] animation output routing ${animation.id} (${bridgeOutputs.length}) | ` + bridgeOutputs.join(" | ")
|
|
3664
|
+
);
|
|
3524
3665
|
}
|
|
3525
3666
|
recordOutputs(bridgeOutputs);
|
|
3526
3667
|
}
|
|
@@ -3592,7 +3733,35 @@ function VizijRuntimeProviderInner({
|
|
|
3592
3733
|
});
|
|
3593
3734
|
}
|
|
3594
3735
|
}
|
|
3595
|
-
ensureAnimationHost()
|
|
3736
|
+
const host = ensureAnimationHost();
|
|
3737
|
+
host?.setClips(hostClips);
|
|
3738
|
+
if (host) {
|
|
3739
|
+
let resumed = false;
|
|
3740
|
+
clipPlaybackRef.current.forEach((clipState, clipId) => {
|
|
3741
|
+
if (!clipState.playing || !host.has(clipId)) {
|
|
3742
|
+
return;
|
|
3743
|
+
}
|
|
3744
|
+
resumed = true;
|
|
3745
|
+
host.setSpeed(clipId, clipState.speed);
|
|
3746
|
+
host.setWeight(clipId, clipState.weight);
|
|
3747
|
+
host.setLoop(clipId, clipState.loop ? "loop" : "once");
|
|
3748
|
+
if (clipState.playhead > 0) {
|
|
3749
|
+
host.seek(clipId, clipState.playhead);
|
|
3750
|
+
}
|
|
3751
|
+
void host.play(clipId).catch((err) => {
|
|
3752
|
+
pushError({
|
|
3753
|
+
message: `Failed to resume animation ${clipId} after the module became ready`,
|
|
3754
|
+
cause: err,
|
|
3755
|
+
phase: "animation",
|
|
3756
|
+
timestamp: performance.now()
|
|
3757
|
+
});
|
|
3758
|
+
});
|
|
3759
|
+
});
|
|
3760
|
+
if (resumed) {
|
|
3761
|
+
setAnimationsSourceRegistered(host.hasPlaying());
|
|
3762
|
+
updateLoopMode();
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3596
3765
|
registeredAnimationsRef.current = animationIds;
|
|
3597
3766
|
if (assetBundle.initialInputs) {
|
|
3598
3767
|
Object.entries(assetBundle.initialInputs).forEach(([path, value]) => {
|
|
@@ -3637,7 +3806,9 @@ function VizijRuntimeProviderInner({
|
|
|
3637
3806
|
registerMergedGraph,
|
|
3638
3807
|
reportStatus,
|
|
3639
3808
|
resolvedProgramAssets,
|
|
3640
|
-
setInput
|
|
3809
|
+
setInput,
|
|
3810
|
+
setAnimationsSourceRegistered,
|
|
3811
|
+
updateLoopMode
|
|
3641
3812
|
]);
|
|
3642
3813
|
(0, import_react2.useEffect)(() => {
|
|
3643
3814
|
if (!ready || status.loading) {
|
|
@@ -3645,7 +3816,9 @@ function VizijRuntimeProviderInner({
|
|
|
3645
3816
|
}
|
|
3646
3817
|
const plan = pendingPlanRef.current;
|
|
3647
3818
|
const hasRegistered = registeredGraphsRef.current.length > 0 || registeredAnimationsRef.current.length > 0 || programControllerIdsRef.current.size > 0;
|
|
3648
|
-
|
|
3819
|
+
const forcedByModuleReady = animationModuleReadyRevision !== lastRegisteredModuleRevisionRef.current;
|
|
3820
|
+
lastRegisteredModuleRevisionRef.current = animationModuleReadyRevision;
|
|
3821
|
+
if (plan && !plan.reregisterGraphs && hasRegistered && !forcedByModuleReady) {
|
|
3649
3822
|
return;
|
|
3650
3823
|
}
|
|
3651
3824
|
registerControllers().catch((err) => {
|
|
@@ -3656,7 +3829,14 @@ function VizijRuntimeProviderInner({
|
|
|
3656
3829
|
timestamp: performance.now()
|
|
3657
3830
|
});
|
|
3658
3831
|
});
|
|
3659
|
-
}, [
|
|
3832
|
+
}, [
|
|
3833
|
+
ready,
|
|
3834
|
+
status.loading,
|
|
3835
|
+
graphUpdateToken,
|
|
3836
|
+
registerControllers,
|
|
3837
|
+
pushError,
|
|
3838
|
+
animationModuleReadyRevision
|
|
3839
|
+
]);
|
|
3660
3840
|
(0, import_react2.useEffect)(() => {
|
|
3661
3841
|
applyEngineChangesRef.current = (changes) => {
|
|
3662
3842
|
const entries = Object.entries(changes);
|
|
@@ -3774,6 +3954,23 @@ function VizijRuntimeProviderInner({
|
|
|
3774
3954
|
entry?.resolve();
|
|
3775
3955
|
}
|
|
3776
3956
|
}, []);
|
|
3957
|
+
const advanceClipPlayheads = (0, import_react2.useCallback)((dt) => {
|
|
3958
|
+
if (!(dt > 0) || !Number.isFinite(dt)) {
|
|
3959
|
+
return;
|
|
3960
|
+
}
|
|
3961
|
+
clipPlaybackRef.current.forEach((state) => {
|
|
3962
|
+
if (!state.playing) {
|
|
3963
|
+
return;
|
|
3964
|
+
}
|
|
3965
|
+
const speed = Number.isFinite(state.speed) && state.speed > 0 ? state.speed : 1;
|
|
3966
|
+
const next = state.playhead + dt * speed;
|
|
3967
|
+
if (!(state.duration > 0)) {
|
|
3968
|
+
state.playhead = next;
|
|
3969
|
+
return;
|
|
3970
|
+
}
|
|
3971
|
+
state.playhead = state.loop ? next % state.duration : Math.min(next, state.duration);
|
|
3972
|
+
});
|
|
3973
|
+
}, []);
|
|
3777
3974
|
const advanceAnimationTweens = (0, import_react2.useCallback)(
|
|
3778
3975
|
(dt) => {
|
|
3779
3976
|
if (animationTweensRef.current.size === 0) {
|
|
@@ -3838,6 +4035,7 @@ function VizijRuntimeProviderInner({
|
|
|
3838
4035
|
weight: 1,
|
|
3839
4036
|
loop: true,
|
|
3840
4037
|
playing: false,
|
|
4038
|
+
playhead: 0,
|
|
3841
4039
|
resolve: null,
|
|
3842
4040
|
completion: null
|
|
3843
4041
|
};
|
|
@@ -3936,8 +4134,17 @@ function VizijRuntimeProviderInner({
|
|
|
3936
4134
|
return [];
|
|
3937
4135
|
}
|
|
3938
4136
|
const raw = device.readValues([ANIMATION_PLAYERS_PATH])[ANIMATION_PLAYERS_PATH];
|
|
3939
|
-
|
|
3940
|
-
|
|
4137
|
+
const states = decodePlayerStates(raw);
|
|
4138
|
+
if (DEV_MODE && states.length === 0 && raw && typeof raw === "object") {
|
|
4139
|
+
warnAnimationGap(
|
|
4140
|
+
"player-states-undecodable",
|
|
4141
|
+
`Animation player_states feedback at "${ANIMATION_PLAYERS_PATH}" could not be decoded (top-level keys: ${Object.keys(
|
|
4142
|
+
raw
|
|
4143
|
+
).join(", ")}). The transport falls back to a host-side playhead.`
|
|
4144
|
+
);
|
|
4145
|
+
}
|
|
4146
|
+
return states;
|
|
4147
|
+
}, [deviceSlot, warnAnimationGap]);
|
|
3941
4148
|
const settleFinishedClips = (0, import_react2.useCallback)(() => {
|
|
3942
4149
|
let pending = false;
|
|
3943
4150
|
clipPlaybackRef.current.forEach((state) => {
|
|
@@ -3993,6 +4200,7 @@ function VizijRuntimeProviderInner({
|
|
|
3993
4200
|
if (host) {
|
|
3994
4201
|
if (options?.reset === true) {
|
|
3995
4202
|
host.seek(id, 0);
|
|
4203
|
+
state.playhead = 0;
|
|
3996
4204
|
}
|
|
3997
4205
|
host.setSpeed(id, state.speed);
|
|
3998
4206
|
host.setWeight(id, state.weight);
|
|
@@ -4029,6 +4237,9 @@ function VizijRuntimeProviderInner({
|
|
|
4029
4237
|
(id) => {
|
|
4030
4238
|
const state = clipPlaybackRef.current.get(id);
|
|
4031
4239
|
if (!state || !state.playing) {
|
|
4240
|
+
console.warn(
|
|
4241
|
+
`[vizij-runtime] pause ignored for "${id}": ${state ? "the clip is not marked playing" : "no clip state"}`
|
|
4242
|
+
);
|
|
4032
4243
|
return;
|
|
4033
4244
|
}
|
|
4034
4245
|
state.playing = false;
|
|
@@ -4038,10 +4249,13 @@ function VizijRuntimeProviderInner({
|
|
|
4038
4249
|
[syncAnimationsSource]
|
|
4039
4250
|
);
|
|
4040
4251
|
const seekAnimation = (0, import_react2.useCallback)((id, timeSeconds) => {
|
|
4041
|
-
|
|
4252
|
+
const state = clipPlaybackRef.current.get(id);
|
|
4253
|
+
if (!state) {
|
|
4042
4254
|
return;
|
|
4043
4255
|
}
|
|
4044
|
-
|
|
4256
|
+
const target = Math.max(0, timeSeconds);
|
|
4257
|
+
state.playhead = state.duration > 0 ? Math.min(target, state.duration) : target;
|
|
4258
|
+
animationHostRef.current?.seek(id, target);
|
|
4045
4259
|
}, []);
|
|
4046
4260
|
const setAnimationLoop = (0, import_react2.useCallback)(
|
|
4047
4261
|
(id, enabled) => {
|
|
@@ -4064,9 +4278,23 @@ function VizijRuntimeProviderInner({
|
|
|
4064
4278
|
const playerId = animationHostRef.current?.playerIdOf(id) ?? null;
|
|
4065
4279
|
const feedback = playerId !== null ? readPlayerStates().find((entry) => entry.player === playerId) : void 0;
|
|
4066
4280
|
return {
|
|
4067
|
-
|
|
4281
|
+
// Feedback is authoritative; the host-side playhead stands in when it
|
|
4282
|
+
// is unavailable. Never fall back to a hard 0 — that reads as "the
|
|
4283
|
+
// animation is parked at the start" rather than "no telemetry", and it
|
|
4284
|
+
// pins the transport clock and defeats seeking.
|
|
4285
|
+
time: feedback?.time ?? state.playhead,
|
|
4068
4286
|
duration: feedback && feedback.duration > 0 ? feedback.duration : state.duration,
|
|
4069
|
-
|
|
4287
|
+
// A clip commanded to pause is paused, whatever the feedback says.
|
|
4288
|
+
//
|
|
4289
|
+
// Feedback goes stale by construction: pausing the last playing clip
|
|
4290
|
+
// unregisters the animations graph source, so the module stops being
|
|
4291
|
+
// stepped and `player_states` keeps reporting whatever it last said —
|
|
4292
|
+
// `state: "playing"`, with the playhead frozen. Treating that as
|
|
4293
|
+
// authoritative meant the transport reverted to playing a frame after
|
|
4294
|
+
// every pause, leaving a Pause button over a clip that was not
|
|
4295
|
+
// advancing. Feedback still decides while the clip is *meant* to be
|
|
4296
|
+
// playing, which is what notices a clip reaching its end.
|
|
4297
|
+
playing: state.playing && (feedback ? feedback.state === "playing" : true),
|
|
4070
4298
|
loop: state.loop,
|
|
4071
4299
|
speed: feedback?.speed ?? state.speed
|
|
4072
4300
|
};
|
|
@@ -4087,6 +4315,9 @@ function VizijRuntimeProviderInner({
|
|
|
4087
4315
|
syncAnimationsSource();
|
|
4088
4316
|
} else {
|
|
4089
4317
|
host?.stop(id);
|
|
4318
|
+
if (state) {
|
|
4319
|
+
state.playhead = 0;
|
|
4320
|
+
}
|
|
4090
4321
|
const unsubscribe = subscribeToStep(() => {
|
|
4091
4322
|
unsubscribe();
|
|
4092
4323
|
syncAnimationsSource();
|
|
@@ -4421,6 +4652,13 @@ function VizijRuntimeProviderInner({
|
|
|
4421
4652
|
},
|
|
4422
4653
|
[faceId, namespace, pushError, setInput, setRendererValue]
|
|
4423
4654
|
);
|
|
4655
|
+
const reconcileAnimationsSource = (0, import_react2.useCallback)(() => {
|
|
4656
|
+
const host = animationHostRef.current;
|
|
4657
|
+
if (!host || !host.hasPlaying()) {
|
|
4658
|
+
return;
|
|
4659
|
+
}
|
|
4660
|
+
setAnimationsSourceRegistered(true);
|
|
4661
|
+
}, [setAnimationsSourceRegistered]);
|
|
4424
4662
|
const flushStagedInputs = (0, import_react2.useCallback)(() => {
|
|
4425
4663
|
if (stagedInputsRef.current.size === 0) {
|
|
4426
4664
|
return;
|
|
@@ -4438,6 +4676,8 @@ function VizijRuntimeProviderInner({
|
|
|
4438
4676
|
avgStepDtRef.current = prev * (1 - alpha) + dt * alpha;
|
|
4439
4677
|
}
|
|
4440
4678
|
advanceAnimationTweens(dt);
|
|
4679
|
+
advanceClipPlayheads(dt);
|
|
4680
|
+
reconcileAnimationsSource();
|
|
4441
4681
|
flushStagedInputs();
|
|
4442
4682
|
if (driveRuntimeRef.current || opts?.forceRuntime) {
|
|
4443
4683
|
stepRuntime(dt);
|
|
@@ -4446,7 +4686,9 @@ function VizijRuntimeProviderInner({
|
|
|
4446
4686
|
},
|
|
4447
4687
|
[
|
|
4448
4688
|
advanceAnimationTweens,
|
|
4689
|
+
advanceClipPlayheads,
|
|
4449
4690
|
flushStagedInputs,
|
|
4691
|
+
reconcileAnimationsSource,
|
|
4450
4692
|
settleFinishedClips,
|
|
4451
4693
|
stepRuntime
|
|
4452
4694
|
]
|
|
@@ -5103,11 +5345,14 @@ function mapUnitControlValue(control, unitValue) {
|
|
|
5103
5345
|
buildPoseWeightRelativePath,
|
|
5104
5346
|
buildRigInputPath,
|
|
5105
5347
|
buildSemanticPoseWeightPathMap,
|
|
5348
|
+
collectInputPathMap,
|
|
5349
|
+
composeGraphSpecs,
|
|
5106
5350
|
filterPosesBySemanticKind,
|
|
5107
5351
|
getPoseSemanticKey,
|
|
5108
5352
|
mapNormalizedControlValue,
|
|
5109
5353
|
mapUnitControlValue,
|
|
5110
5354
|
normalizePoseSemanticKey,
|
|
5355
|
+
resolveAnimationBridgeOutputPaths,
|
|
5111
5356
|
resolveFaceControls,
|
|
5112
5357
|
resolvePoseMembership,
|
|
5113
5358
|
resolvePoseSemantics,
|