@vizij/runtime-react 0.1.0 → 0.2.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.js CHANGED
@@ -21,11 +21,143 @@ import {
21
21
  } from "@vizij/orchestrator-react";
22
22
  import { compileIrGraph } from "@vizij/node-graph-authoring";
23
23
  import { valueAsNumber as valueAsNumber2 } from "@vizij/value-json";
24
+ import { getLookup } from "@vizij/utils";
24
25
 
25
26
  // src/context.ts
26
27
  import { createContext } from "react";
27
28
  var VizijRuntimeContext = createContext(null);
28
29
 
30
+ // src/updatePolicy.ts
31
+ function applyRuntimeGraphBundle(base, bundle) {
32
+ const next = {
33
+ ...base
34
+ };
35
+ const hasRigOverride = Object.prototype.hasOwnProperty.call(bundle, "rig");
36
+ const hasPoseOverride = Object.prototype.hasOwnProperty.call(bundle, "pose");
37
+ const hasAnimationsOverride = Object.prototype.hasOwnProperty.call(
38
+ bundle,
39
+ "animations"
40
+ );
41
+ const hasProgramsOverride = Object.prototype.hasOwnProperty.call(
42
+ bundle,
43
+ "programs"
44
+ );
45
+ if (hasRigOverride) {
46
+ if (bundle.rig) {
47
+ next.rig = bundle.rig;
48
+ } else {
49
+ delete next.rig;
50
+ }
51
+ }
52
+ if (hasPoseOverride) {
53
+ if (bundle.pose) {
54
+ next.pose = bundle.pose;
55
+ } else {
56
+ delete next.pose;
57
+ }
58
+ }
59
+ if (hasAnimationsOverride) {
60
+ next.animations = Array.isArray(bundle.animations) ? bundle.animations : void 0;
61
+ }
62
+ if (hasProgramsOverride) {
63
+ next.programs = Array.isArray(bundle.programs) ? bundle.programs : void 0;
64
+ }
65
+ return next;
66
+ }
67
+ function normalizeSpecPayload(value) {
68
+ if (!value) {
69
+ return "";
70
+ }
71
+ try {
72
+ return JSON.stringify(value);
73
+ } catch {
74
+ return String(value);
75
+ }
76
+ }
77
+ function glbSignature(glb) {
78
+ if (glb.kind === "url") {
79
+ return `url:${glb.src}`;
80
+ }
81
+ if (glb.kind === "blob") {
82
+ return `blob:${glb.blob?.size ?? 0}`;
83
+ }
84
+ return `world:${normalizeSpecPayload(glb.world)}`;
85
+ }
86
+ function graphSignature(graph) {
87
+ if (!graph) {
88
+ return "";
89
+ }
90
+ const id = graph.id ?? "";
91
+ return `${id}:${normalizeSpecPayload(graph.spec ?? graph.ir ?? null)}`;
92
+ }
93
+ function poseSignature(pose) {
94
+ if (!pose) {
95
+ return "";
96
+ }
97
+ const graph = pose.graph;
98
+ const config = pose.config;
99
+ const graphPart = graph ? graphSignature({ id: graph.id, spec: graph.spec }) : "";
100
+ const configPart = config ? normalizeSpecPayload(config) : "";
101
+ return `${graphPart}:${configPart}`;
102
+ }
103
+ function animationsSignature(animations) {
104
+ if (!Array.isArray(animations) || animations.length === 0) {
105
+ return "";
106
+ }
107
+ return animations.map((animation) => {
108
+ const id = animation.id ?? "";
109
+ const clipSignature = normalizeSpecPayload(animation.clip ?? null);
110
+ const setupSignature = normalizeSpecPayload(animation.setup ?? null);
111
+ const weightSignature = animation.weight == null ? "" : String(animation.weight);
112
+ return `${id}:${clipSignature}:${setupSignature}:${weightSignature}`;
113
+ }).sort().join("|");
114
+ }
115
+ function programsSignature(programs) {
116
+ if (!Array.isArray(programs) || programs.length === 0) {
117
+ return "";
118
+ }
119
+ return programs.map((program) => {
120
+ const id = program.id ?? "";
121
+ const label = program.label ?? "";
122
+ const graphId = program.graph?.id ?? "";
123
+ const graphPayload = normalizeSpecPayload(
124
+ program.graph?.spec ?? program.graph?.ir ?? null
125
+ );
126
+ const resetValues = normalizeSpecPayload(program.resetValues ?? null);
127
+ return `${id}:${label}:${graphId}:${graphPayload}:${resetValues}`;
128
+ }).sort().join("|");
129
+ }
130
+ function resolveRuntimeUpdatePlan(previous, next, tier) {
131
+ if (!previous) {
132
+ return { reloadAssets: true, reregisterGraphs: false };
133
+ }
134
+ const glbChanged = glbSignature(previous.glb) !== glbSignature(next.glb);
135
+ const rigChanged = graphSignature(previous.rig) !== graphSignature(next.rig);
136
+ const poseChanged = poseSignature(previous.pose) !== poseSignature(next.pose);
137
+ const rigReferenceChanged = previous.rig?.id !== next.rig?.id || previous.rig?.spec !== next.rig?.spec || previous.rig?.ir !== next.rig?.ir;
138
+ const poseReferenceChanged = previous.pose?.graph?.id !== next.pose?.graph?.id || previous.pose?.graph?.spec !== next.pose?.graph?.spec || previous.pose?.config !== next.pose?.config;
139
+ const graphsChanged = rigChanged || poseChanged || rigReferenceChanged || poseReferenceChanged;
140
+ const animationsChanged = animationsSignature(previous.animations) !== animationsSignature(next.animations);
141
+ const programsChanged = programsSignature(previous.programs) !== programsSignature(next.programs);
142
+ const controllersChanged = graphsChanged || animationsChanged || programsChanged;
143
+ if (tier === "assets") {
144
+ return { reloadAssets: true, reregisterGraphs: false };
145
+ }
146
+ if (tier === "graphs") {
147
+ if (glbChanged) {
148
+ return { reloadAssets: true, reregisterGraphs: false };
149
+ }
150
+ return { reloadAssets: false, reregisterGraphs: controllersChanged };
151
+ }
152
+ if (glbChanged) {
153
+ return { reloadAssets: true, reregisterGraphs: false };
154
+ }
155
+ if (controllersChanged) {
156
+ return { reloadAssets: false, reregisterGraphs: true };
157
+ }
158
+ return { reloadAssets: false, reregisterGraphs: false };
159
+ }
160
+
29
161
  // src/utils/graph.ts
30
162
  function getNodes(spec) {
31
163
  if (!spec || typeof spec !== "object") {
@@ -70,6 +202,12 @@ function collectInputPaths(spec) {
70
202
  }
71
203
  function collectInputPathMap(spec) {
72
204
  const map = {};
205
+ const addVariant = (key, path, force = false) => {
206
+ if (!key || !force && map[key]) {
207
+ return;
208
+ }
209
+ map[key] = path;
210
+ };
73
211
  const nodes = getNodes(spec);
74
212
  nodes.forEach((node) => {
75
213
  if (String(node.type ?? "").toLowerCase() !== "input") {
@@ -81,11 +219,653 @@ function collectInputPathMap(spec) {
81
219
  }
82
220
  const id = String(node.id ?? "");
83
221
  const key = id.startsWith("input_") ? id.slice("input_".length) : id || path.trim();
84
- map[key] = path.trim();
222
+ const trimmedPath = path.trim();
223
+ addVariant(key, trimmedPath);
224
+ if (key.startsWith("direct_")) {
225
+ addVariant(key.slice("direct_".length), trimmedPath, true);
226
+ }
227
+ if (key.startsWith("pose_control_")) {
228
+ addVariant(key.slice("pose_control_".length), trimmedPath);
229
+ }
230
+ });
231
+ return map;
232
+ }
233
+
234
+ // src/utils/posePaths.ts
235
+ var POSE_WEIGHT_INPUT_PATH_PREFIX = "/poses/";
236
+ var VISEME_POSE_KEYS = [
237
+ "a",
238
+ "at",
239
+ "b",
240
+ "e",
241
+ "e_2",
242
+ "f",
243
+ "i",
244
+ "k",
245
+ "m",
246
+ "o",
247
+ "o_2",
248
+ "p",
249
+ "r",
250
+ "s",
251
+ "t",
252
+ "t_2",
253
+ "u"
254
+ ];
255
+ var EXPRESSIVE_EMOTION_POSE_KEYS = [
256
+ "concerned",
257
+ "happy",
258
+ "sad",
259
+ "sleepy",
260
+ "surprise"
261
+ ];
262
+ var EMOTION_POSE_KEYS = [
263
+ "concerned",
264
+ "happy",
265
+ "neutral",
266
+ "sad",
267
+ "sleepy",
268
+ "surprise",
269
+ "angry"
270
+ ];
271
+ var VISEME_GROUP_NEEDLES = ["viseme", "phoneme", "lip", "mouth"];
272
+ var EMOTION_GROUP_NEEDLES = ["emotion", "expression", "mood", "affect"];
273
+ var VISEME_POSE_KEY_SET = new Set(VISEME_POSE_KEYS);
274
+ var EMOTION_POSE_KEY_SET = new Set(EMOTION_POSE_KEYS);
275
+ var POSE_KEY_ALIASES = {
276
+ concern: "concerned",
277
+ surprised: "surprise"
278
+ };
279
+ function buildRigInputPath(faceId, path) {
280
+ let trimmed = path.startsWith("/") ? path.slice(1) : path;
281
+ if (!trimmed) {
282
+ return `rig/${faceId}`;
283
+ }
284
+ while (trimmed.startsWith("rig/")) {
285
+ const segments = trimmed.split("/");
286
+ if (segments.length >= 3) {
287
+ const existingFaceId = segments[1];
288
+ const remainder = segments.slice(2).join("/");
289
+ if (existingFaceId === faceId) {
290
+ return trimmed;
291
+ }
292
+ trimmed = remainder || "";
293
+ } else {
294
+ trimmed = segments.slice(1).join("/");
295
+ }
296
+ }
297
+ const suffix = trimmed ? `/${trimmed}` : "";
298
+ return `rig/${faceId}${suffix}`;
299
+ }
300
+ function normalizePoseWeightPathSegment(value) {
301
+ const trimmed = value?.trim() ?? "";
302
+ if (!trimmed) {
303
+ return "pose";
304
+ }
305
+ const normalized = trimmed.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
306
+ return normalized || "pose";
307
+ }
308
+ function buildPoseWeightInputPathSegment(poseId) {
309
+ return normalizePoseWeightPathSegment(poseId);
310
+ }
311
+ function buildPoseWeightRelativePath(poseId) {
312
+ return `${POSE_WEIGHT_INPUT_PATH_PREFIX}${buildPoseWeightInputPathSegment(
313
+ poseId
314
+ )}.weight`;
315
+ }
316
+ function buildPoseWeightPathMap(poses, faceId) {
317
+ const faceSegment = faceId?.trim() || "face";
318
+ const map = /* @__PURE__ */ new Map();
319
+ poses.forEach((pose) => {
320
+ map.set(
321
+ pose.id,
322
+ buildRigInputPath(faceSegment, buildPoseWeightRelativePath(pose.id))
323
+ );
324
+ });
325
+ return map;
326
+ }
327
+ function normalizePoseSemanticKey(value) {
328
+ const trimmed = value?.trim() ?? "";
329
+ if (!trimmed) {
330
+ return null;
331
+ }
332
+ const normalized = trimmed.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
333
+ if (!normalized) {
334
+ return null;
335
+ }
336
+ return POSE_KEY_ALIASES[normalized] ?? normalized;
337
+ }
338
+ function derivePoseSemanticKeyFromId(poseId) {
339
+ const trimmed = poseId?.trim() ?? "";
340
+ if (!trimmed) {
341
+ return null;
342
+ }
343
+ const stripped = trimmed.replace(/^pose_d_/i, "").replace(/^pose_/i, "").replace(/^d_/i, "").replace(/_d$/i, "");
344
+ return normalizePoseSemanticKey(stripped);
345
+ }
346
+ function getPoseSemanticKey(pose) {
347
+ return normalizePoseSemanticKey(pose.name) ?? derivePoseSemanticKeyFromId(pose.id) ?? null;
348
+ }
349
+ function normalizePoseGroupPath(value) {
350
+ const trimmed = value?.trim() ?? "";
351
+ if (!trimmed) {
352
+ return null;
353
+ }
354
+ return trimmed.replace(/^\/+|\/+$/g, "").replace(/\/+/g, "/");
355
+ }
356
+ function sanitizePoseGroupId(value, fallback) {
357
+ const normalized = (value ?? "").trim().replace(/[^a-zA-Z0-9_/-]+/g, "_").replace(/^\/+|\/+$/g, "").replace(/\/+/g, "_");
358
+ if (!normalized) {
359
+ return fallback.replace(/\//g, "_");
360
+ }
361
+ return normalized;
362
+ }
363
+ function buildPoseGroupLookup(groups) {
364
+ const byId = /* @__PURE__ */ new Map();
365
+ const byPath = /* @__PURE__ */ new Map();
366
+ const orderById = /* @__PURE__ */ new Map();
367
+ (groups ?? []).forEach((group, index) => {
368
+ const path = normalizePoseGroupPath(group.path ?? group.name ?? group.id);
369
+ if (!path) {
370
+ return;
371
+ }
372
+ const id = sanitizePoseGroupId(group.id, path);
373
+ const humanizedName = typeof group.name === "string" && group.name.trim().length > 0 ? group.name.trim() : path.split("/").filter(Boolean).pop()?.split(/[_-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ") ?? path;
374
+ const normalized = {
375
+ ...group,
376
+ id,
377
+ path,
378
+ name: humanizedName
379
+ };
380
+ byId.set(id, normalized);
381
+ if (!orderById.has(id)) {
382
+ orderById.set(id, index);
383
+ }
384
+ if (!byPath.has(path)) {
385
+ byPath.set(path, normalized);
386
+ }
387
+ });
388
+ return { byId, byPath, orderById };
389
+ }
390
+ function orderPoseMembershipIds(groupIds, groups) {
391
+ const { orderById } = buildPoseGroupLookup(groups);
392
+ const unique = Array.from(
393
+ new Set(
394
+ Array.from(groupIds).map((groupId) => groupId.trim()).filter((groupId) => groupId.length > 0)
395
+ )
396
+ );
397
+ unique.sort((left, right) => {
398
+ const leftIndex = orderById.get(left);
399
+ const rightIndex = orderById.get(right);
400
+ if (leftIndex !== void 0 && rightIndex !== void 0) {
401
+ return leftIndex - rightIndex;
402
+ }
403
+ if (leftIndex !== void 0) {
404
+ return -1;
405
+ }
406
+ if (rightIndex !== void 0) {
407
+ return 1;
408
+ }
409
+ const leftPath = normalizePoseGroupPath(left) ?? left;
410
+ const rightPath = normalizePoseGroupPath(right) ?? right;
411
+ const byPath = leftPath.localeCompare(rightPath);
412
+ if (byPath !== 0) {
413
+ return byPath;
414
+ }
415
+ return left.localeCompare(right);
416
+ });
417
+ return unique;
418
+ }
419
+ function valueHasNeedle(value, needles) {
420
+ const normalized = normalizePoseSemanticKey(value);
421
+ if (!normalized) {
422
+ return false;
423
+ }
424
+ return needles.some((needle) => normalized.includes(needle));
425
+ }
426
+ function resolvePoseMembership(pose, groups) {
427
+ const { byId, byPath } = buildPoseGroupLookup(groups);
428
+ const resolvedGroupIds = [];
429
+ const pathById = /* @__PURE__ */ new Map();
430
+ const addMembership = (groupId, path) => {
431
+ if (!groupId) {
432
+ return;
433
+ }
434
+ if (!resolvedGroupIds.includes(groupId)) {
435
+ resolvedGroupIds.push(groupId);
436
+ }
437
+ if (!path) {
438
+ return;
439
+ }
440
+ const existingPath = pathById.get(groupId);
441
+ const normalizedGroupIdPath = normalizePoseGroupPath(groupId);
442
+ const normalizedExistingPath = normalizePoseGroupPath(existingPath) ?? existingPath ?? null;
443
+ const normalizedIncomingPath = normalizePoseGroupPath(path) ?? path ?? null;
444
+ const shouldPromotePath = normalizedExistingPath === null || normalizedGroupIdPath !== null && normalizedExistingPath === normalizedGroupIdPath && normalizedIncomingPath !== normalizedGroupIdPath;
445
+ if (shouldPromotePath) {
446
+ pathById.set(groupId, path);
447
+ }
448
+ };
449
+ const addByPath = (rawPath) => {
450
+ const normalizedPath = normalizePoseGroupPath(rawPath);
451
+ if (!normalizedPath) {
452
+ return;
453
+ }
454
+ const existing = byPath.get(normalizedPath);
455
+ if (existing) {
456
+ addMembership(existing.id, existing.path);
457
+ return;
458
+ }
459
+ addMembership(sanitizePoseGroupId(null, normalizedPath), normalizedPath);
460
+ };
461
+ const addById = (rawId) => {
462
+ const trimmed = rawId?.trim() ?? "";
463
+ if (!trimmed) {
464
+ return;
465
+ }
466
+ const normalizedPath = normalizePoseGroupPath(trimmed);
467
+ const normalizedId = sanitizePoseGroupId(trimmed, trimmed);
468
+ const matchedById = byId.get(trimmed) ?? byId.get(normalizedId);
469
+ if (matchedById) {
470
+ addMembership(matchedById.id, matchedById.path);
471
+ return;
472
+ }
473
+ if (normalizedPath) {
474
+ const matchedByPath = byPath.get(normalizedPath);
475
+ if (matchedByPath) {
476
+ addMembership(matchedByPath.id, matchedByPath.path);
477
+ return;
478
+ }
479
+ }
480
+ addMembership(
481
+ normalizedId,
482
+ normalizedPath && normalizedPath.length > 0 ? normalizedPath : null
483
+ );
484
+ };
485
+ pose.groupIds?.forEach((groupId) => addById(groupId));
486
+ addById(pose.groupId);
487
+ addByPath(pose.group);
488
+ const orderedGroupIds = orderPoseMembershipIds(resolvedGroupIds, groups);
489
+ const primaryGroupId = orderedGroupIds[0] ?? null;
490
+ const primaryGroupPath = primaryGroupId ? byId.get(primaryGroupId)?.path ?? pathById.get(primaryGroupId) ?? null : null;
491
+ const groupPathsById = {};
492
+ orderedGroupIds.forEach((groupId) => {
493
+ const path = byId.get(groupId)?.path ?? pathById.get(groupId) ?? null;
494
+ if (path) {
495
+ groupPathsById[groupId] = path;
496
+ }
497
+ });
498
+ return {
499
+ groupIds: orderedGroupIds,
500
+ primaryGroupId,
501
+ primaryGroupPath,
502
+ groupPathsById
503
+ };
504
+ }
505
+ function poseMatchesGroupKind(pose, groups, needles) {
506
+ const membership = resolvePoseMembership(pose, groups);
507
+ if (valueHasNeedle(pose.group, needles) || valueHasNeedle(pose.groupId, needles)) {
508
+ return true;
509
+ }
510
+ if (pose.groupIds?.some((groupId) => valueHasNeedle(groupId, needles))) {
511
+ return true;
512
+ }
513
+ return membership.groupIds.some((groupId) => {
514
+ const path = membership.groupPathsById[groupId] ?? null;
515
+ const group = (groups ?? []).find((entry) => entry.id === groupId) ?? null;
516
+ return valueHasNeedle(groupId, needles) || valueHasNeedle(path, needles) || valueHasNeedle(group?.name, needles);
517
+ });
518
+ }
519
+ function resolvePoseSemantics(pose, groups) {
520
+ const membership = resolvePoseMembership(pose, groups);
521
+ const key = getPoseSemanticKey(pose);
522
+ const looksLikeVisemeGroup = poseMatchesGroupKind(
523
+ pose,
524
+ groups,
525
+ VISEME_GROUP_NEEDLES
526
+ );
527
+ const looksLikeEmotionGroup = poseMatchesGroupKind(
528
+ pose,
529
+ groups,
530
+ EMOTION_GROUP_NEEDLES
531
+ );
532
+ let kind = "other";
533
+ if (looksLikeVisemeGroup || key && VISEME_POSE_KEY_SET.has(key)) {
534
+ kind = "viseme";
535
+ } else if (looksLikeEmotionGroup || key && EMOTION_POSE_KEY_SET.has(key)) {
536
+ kind = "emotion";
537
+ }
538
+ return { key, kind, membership };
539
+ }
540
+ function filterPosesBySemanticKind(poses, groups, kind) {
541
+ return poses.filter(
542
+ (pose) => resolvePoseSemantics(pose, groups).kind === kind
543
+ );
544
+ }
545
+ function buildSemanticPoseWeightPathMap(poses, groups, faceId, kind) {
546
+ const map = /* @__PURE__ */ new Map();
547
+ const pathMap = buildPoseWeightPathMap(poses, faceId);
548
+ poses.forEach((pose) => {
549
+ const semantics = resolvePoseSemantics(pose, groups);
550
+ const path = pathMap.get(pose.id);
551
+ if (semantics.kind !== kind || !semantics.key || !path || map.has(semantics.key)) {
552
+ return;
553
+ }
554
+ map.set(semantics.key, path);
85
555
  });
86
556
  return map;
87
557
  }
88
558
 
559
+ // src/utils/poseRuntime.ts
560
+ function shouldUseLegacyPoseWeightFallback(hasPoseGraph) {
561
+ return !hasPoseGraph;
562
+ }
563
+ function resolvePoseControlInputPath({
564
+ inputId,
565
+ basePath,
566
+ rigInputPathMap,
567
+ hasNativePoseControlInput
568
+ }) {
569
+ if (!inputId.trim()) {
570
+ return void 0;
571
+ }
572
+ return rigInputPathMap[inputId] ?? rigInputPathMap[`pose_control_${inputId}`] ?? rigInputPathMap[`direct_${inputId}`] ?? (hasNativePoseControlInput ? basePath : void 0);
573
+ }
574
+
575
+ // src/utils/clipPlayback.ts
576
+ var EPSILON = 1e-6;
577
+ function toFiniteNumber(value, fallback) {
578
+ const parsed = Number(value);
579
+ return Number.isFinite(parsed) ? parsed : fallback;
580
+ }
581
+ function normaliseInterpolation(interpolation) {
582
+ const mode = typeof interpolation === "string" ? interpolation.trim().toLowerCase() : "linear";
583
+ if (mode === "step") {
584
+ return "step";
585
+ }
586
+ if (mode === "cubic" || mode === "cubicspline") {
587
+ return "cubic";
588
+ }
589
+ return "linear";
590
+ }
591
+ function asNumericKeyframe(keyframe) {
592
+ if (!keyframe || typeof keyframe !== "object") {
593
+ return null;
594
+ }
595
+ const time = Number(keyframe.time);
596
+ const value = Number(keyframe.value);
597
+ if (!Number.isFinite(time) || !Number.isFinite(value)) {
598
+ return null;
599
+ }
600
+ const inTangentRaw = keyframe.inTangent;
601
+ const outTangentRaw = keyframe.outTangent;
602
+ const inTangent = inTangentRaw == null || Number.isFinite(Number(inTangentRaw)) ? inTangentRaw : void 0;
603
+ const outTangent = outTangentRaw == null || Number.isFinite(Number(outTangentRaw)) ? outTangentRaw : void 0;
604
+ return {
605
+ time,
606
+ value,
607
+ inTangent,
608
+ outTangent
609
+ };
610
+ }
611
+ function getNumericKeyframes(track) {
612
+ const keyframes = Array.isArray(track.keyframes) ? track.keyframes : [];
613
+ const numeric = keyframes.map((keyframe) => asNumericKeyframe(keyframe)).filter((keyframe) => Boolean(keyframe));
614
+ if (numeric.length <= 1) {
615
+ return numeric;
616
+ }
617
+ return [...numeric].sort((a, b) => a.time - b.time);
618
+ }
619
+ function resolveTangent(value, fallback) {
620
+ const parsed = Number(value);
621
+ if (Number.isFinite(parsed)) {
622
+ return parsed;
623
+ }
624
+ return fallback;
625
+ }
626
+ function sampleHermite(startValue, endValue, outTangent, inTangent, factor, duration) {
627
+ const t = factor;
628
+ const t2 = t * t;
629
+ const t3 = t2 * t;
630
+ const h00 = 2 * t3 - 3 * t2 + 1;
631
+ const h10 = t3 - 2 * t2 + t;
632
+ const h01 = -2 * t3 + 3 * t2;
633
+ const h11 = t3 - t2;
634
+ return h00 * startValue + h10 * outTangent * duration + h01 * endValue + h11 * inTangent * duration;
635
+ }
636
+ function resolveClipDurationSeconds(clip, fallbackDurationSeconds = 0) {
637
+ const fallback = Math.max(0, toFiniteNumber(fallbackDurationSeconds, 0));
638
+ if (!clip || typeof clip !== "object") {
639
+ return fallback;
640
+ }
641
+ const clipDuration = Number(clip.duration);
642
+ if (Number.isFinite(clipDuration) && clipDuration > 0) {
643
+ return clipDuration;
644
+ }
645
+ const tracks = Array.isArray(clip.tracks) ? clip.tracks : [];
646
+ let maxTime = 0;
647
+ tracks.forEach((track) => {
648
+ const keyframes = getNumericKeyframes(track);
649
+ const last = keyframes[keyframes.length - 1];
650
+ if (last && last.time > maxTime) {
651
+ maxTime = last.time;
652
+ }
653
+ });
654
+ return maxTime > 0 ? maxTime : fallback;
655
+ }
656
+ function clampAnimationTime(time, duration) {
657
+ if (!Number.isFinite(duration) || duration <= 0) {
658
+ return 0;
659
+ }
660
+ if (!Number.isFinite(time)) {
661
+ return 0;
662
+ }
663
+ if (time <= 0) {
664
+ return 0;
665
+ }
666
+ if (time >= duration) {
667
+ return duration;
668
+ }
669
+ return time;
670
+ }
671
+ function advanceClipTime(state, dt) {
672
+ const duration = Math.max(0, toFiniteNumber(state.duration, 0));
673
+ const speed = Number.isFinite(state.speed) && state.speed > 0 ? state.speed : 1;
674
+ const currentTime = clampAnimationTime(state.time, duration);
675
+ const delta = Number.isFinite(dt) && dt > 0 ? Math.max(0, dt) * speed : 0;
676
+ if (!state.playing || delta <= 0) {
677
+ return { time: currentTime, completed: false };
678
+ }
679
+ if (duration <= 0) {
680
+ return { time: 0, completed: true };
681
+ }
682
+ const nextTime = currentTime + delta;
683
+ if (state.loop) {
684
+ if (nextTime < duration) {
685
+ return { time: nextTime, completed: false };
686
+ }
687
+ const wrapped = (nextTime % duration + duration) % duration;
688
+ return { time: wrapped, completed: false };
689
+ }
690
+ if (nextTime >= duration - EPSILON) {
691
+ return { time: duration, completed: true };
692
+ }
693
+ return { time: nextTime, completed: false };
694
+ }
695
+ function sampleTrackAtTime(track, timeSeconds) {
696
+ const keyframes = getNumericKeyframes(track);
697
+ if (keyframes.length === 0) {
698
+ return 0;
699
+ }
700
+ if (keyframes.length === 1) {
701
+ return keyframes[0].value;
702
+ }
703
+ const mode = normaliseInterpolation(track.interpolation);
704
+ const time = Number.isFinite(timeSeconds) ? timeSeconds : 0;
705
+ const first = keyframes[0];
706
+ if (time <= first.time + EPSILON) {
707
+ return first.value;
708
+ }
709
+ const last = keyframes[keyframes.length - 1];
710
+ if (time >= last.time - EPSILON) {
711
+ return last.value;
712
+ }
713
+ for (let i = 0; i < keyframes.length - 1; i += 1) {
714
+ const current = keyframes[i];
715
+ const next = keyframes[i + 1];
716
+ const start = current.time;
717
+ const end = next.time;
718
+ const duration = end - start;
719
+ if (duration <= EPSILON) {
720
+ if (time <= end + EPSILON) {
721
+ return next.value;
722
+ }
723
+ continue;
724
+ }
725
+ if (Math.abs(time - end) <= EPSILON) {
726
+ return next.value;
727
+ }
728
+ if (time < end) {
729
+ const factor = (time - start) / duration;
730
+ if (mode === "step") {
731
+ return current.value;
732
+ }
733
+ if (mode === "cubic") {
734
+ const slope = (next.value - current.value) / duration;
735
+ const outTangent = resolveTangent(current.outTangent, slope);
736
+ const inTangent = resolveTangent(next.inTangent, slope);
737
+ return sampleHermite(
738
+ current.value,
739
+ next.value,
740
+ outTangent,
741
+ inTangent,
742
+ factor,
743
+ duration
744
+ );
745
+ }
746
+ return current.value + (next.value - current.value) * factor;
747
+ }
748
+ }
749
+ return last.value;
750
+ }
751
+
752
+ // src/utils/animationBridge.ts
753
+ function resolveAnimationBridgeOutputPaths(channel, faceId, rigInputMap) {
754
+ const normalizedChannel = channel.trim().replace(/^\/+/, "");
755
+ if (!normalizedChannel) {
756
+ return [];
757
+ }
758
+ const outputPaths = /* @__PURE__ */ new Set([normalizedChannel]);
759
+ if (normalizedChannel.startsWith("animation/")) {
760
+ return Array.from(outputPaths).sort(
761
+ (left, right) => left.localeCompare(right)
762
+ );
763
+ }
764
+ if (rigInputMap && Object.keys(rigInputMap).length > 0) {
765
+ const candidateKeys = /* @__PURE__ */ new Set([normalizedChannel]);
766
+ const rigChannelMatch2 = /^rig\/[^/]+\/(.+)$/.exec(normalizedChannel);
767
+ if (rigChannelMatch2?.[1]) {
768
+ candidateKeys.add(rigChannelMatch2[1]);
769
+ }
770
+ candidateKeys.forEach((key) => {
771
+ const mapped = rigInputMap[key];
772
+ const normalized = mapped?.trim().replace(/^\/+/, "");
773
+ if (normalized) {
774
+ outputPaths.add(normalized);
775
+ }
776
+ });
777
+ const suffix = normalizedChannel.includes("/") ? normalizedChannel : `/${normalizedChannel}`;
778
+ Object.values(rigInputMap).forEach((mappedPath) => {
779
+ const normalized = mappedPath?.trim().replace(/^\/+/, "");
780
+ if (!normalized) {
781
+ return;
782
+ }
783
+ if (normalized === normalizedChannel || normalized.endsWith(suffix)) {
784
+ outputPaths.add(normalized);
785
+ }
786
+ });
787
+ }
788
+ if (!faceId) {
789
+ return Array.from(outputPaths).sort(
790
+ (left, right) => left.localeCompare(right)
791
+ );
792
+ }
793
+ const rigChannelMatch = /^rig\/[^/]+\/(.+)$/.exec(normalizedChannel);
794
+ if (rigChannelMatch?.[1]) {
795
+ outputPaths.add(`rig/${faceId}/${rigChannelMatch[1]}`);
796
+ } else if (!normalizedChannel.startsWith("rig/")) {
797
+ outputPaths.add(`rig/${faceId}/${normalizedChannel}`);
798
+ }
799
+ return Array.from(outputPaths).sort(
800
+ (left, right) => left.localeCompare(right)
801
+ );
802
+ }
803
+ function collectAnimationClipOutputPaths(clip, faceId, rigInputMap) {
804
+ const outputPaths = /* @__PURE__ */ new Set();
805
+ const tracks = Array.isArray(clip.tracks) ? clip.tracks : [];
806
+ tracks.forEach((track) => {
807
+ const channel = typeof track.channel === "string" ? track.channel.trim() : "";
808
+ if (!channel) {
809
+ return;
810
+ }
811
+ resolveAnimationBridgeOutputPaths(channel, faceId, rigInputMap).forEach(
812
+ (path) => {
813
+ outputPaths.add(path);
814
+ }
815
+ );
816
+ });
817
+ return Array.from(outputPaths).sort(
818
+ (left, right) => left.localeCompare(right)
819
+ );
820
+ }
821
+ function sampleAnimationClipOutputValues(clip, timeSeconds, weight = 1, faceId, rigInputMap) {
822
+ const appliedWeight = Number.isFinite(weight) && weight >= 0 ? Number(weight) : 1;
823
+ const outputValues = /* @__PURE__ */ new Map();
824
+ const tracks = Array.isArray(clip.tracks) ? clip.tracks : [];
825
+ tracks.forEach((track) => {
826
+ const channel = typeof track.channel === "string" ? track.channel.trim() : "";
827
+ if (!channel) {
828
+ return;
829
+ }
830
+ const sampledValue = sampleTrackAtTime(
831
+ track,
832
+ timeSeconds
833
+ );
834
+ const weightedValue = sampledValue * appliedWeight;
835
+ resolveAnimationBridgeOutputPaths(channel, faceId, rigInputMap).forEach(
836
+ (path) => {
837
+ outputValues.set(path, (outputValues.get(path) ?? 0) + weightedValue);
838
+ }
839
+ );
840
+ });
841
+ return outputValues;
842
+ }
843
+ function diffAnimationAggregateValues(previousAggregate, nextAggregate, epsilon = 1e-6) {
844
+ const operations = [];
845
+ const changedPaths = /* @__PURE__ */ new Set();
846
+ previousAggregate.forEach((previousValue, path) => {
847
+ const nextValue = nextAggregate.get(path);
848
+ if (nextValue === void 0 || Math.abs(nextValue - previousValue) > epsilon) {
849
+ changedPaths.add(path);
850
+ }
851
+ });
852
+ nextAggregate.forEach((nextValue, path) => {
853
+ const previousValue = previousAggregate.get(path);
854
+ if (previousValue === void 0 || Math.abs(nextValue - previousValue) > epsilon) {
855
+ changedPaths.add(path);
856
+ }
857
+ });
858
+ changedPaths.forEach((path) => {
859
+ const nextValue = nextAggregate.get(path);
860
+ if (nextValue === void 0) {
861
+ operations.push({ kind: "clear", path });
862
+ return;
863
+ }
864
+ operations.push({ kind: "set", path, value: nextValue });
865
+ });
866
+ return operations;
867
+ }
868
+
89
869
  // src/utils/valueConversion.ts
90
870
  import {
91
871
  isNormalizedValue,
@@ -219,6 +999,19 @@ var DEFAULT_MERGE = {
219
999
  intermediate: "add"
220
1000
  };
221
1001
  var DEFAULT_DURATION = 0.35;
1002
+ var POSE_CONTROL_BRIDGE_EPSILON = 1e-6;
1003
+ var DEV_MODE = (() => {
1004
+ const nodeEnv = globalThis.process?.env?.NODE_ENV;
1005
+ return typeof nodeEnv === "string" && nodeEnv === "development";
1006
+ })();
1007
+ function isRuntimeDebugEnabled() {
1008
+ if (DEV_MODE) {
1009
+ return true;
1010
+ }
1011
+ return Boolean(
1012
+ globalThis.__VIZIJ_RUNTIME_DEBUG__
1013
+ );
1014
+ }
222
1015
  var EASINGS = {
223
1016
  linear: (t) => t,
224
1017
  easeIn: (t) => t * t,
@@ -235,12 +1028,19 @@ function resolveEasing(easing) {
235
1028
  return EASINGS.linear;
236
1029
  }
237
1030
  function findRootId(world) {
1031
+ let fallback = null;
238
1032
  for (const entry of Object.values(world)) {
239
- if (entry && typeof entry === "object" && entry.type === "group" && entry.rootBounds && entry.id) {
1033
+ if (!entry || typeof entry !== "object" || entry.type !== "group") {
1034
+ continue;
1035
+ }
1036
+ if (entry.rootBounds && entry.id) {
240
1037
  return entry.id;
241
1038
  }
1039
+ if (!fallback && entry.id) {
1040
+ fallback = entry.id;
1041
+ }
242
1042
  }
243
- return null;
1043
+ return fallback;
244
1044
  }
245
1045
  function normalisePath(path) {
246
1046
  if (!path) {
@@ -410,6 +1210,30 @@ function namespaceGraphSpec(spec, namespace) {
410
1210
  nodes: nextNodes
411
1211
  };
412
1212
  }
1213
+ function stripNulls(value) {
1214
+ if (value === null) {
1215
+ return void 0;
1216
+ }
1217
+ if (Array.isArray(value)) {
1218
+ const next2 = value.map((entry) => stripNulls(entry)).filter((entry) => entry !== void 0 && entry !== null);
1219
+ return next2;
1220
+ }
1221
+ if (typeof value !== "object" || value === void 0) {
1222
+ return value;
1223
+ }
1224
+ const next = {};
1225
+ Object.entries(value).forEach(([key, entry]) => {
1226
+ if (entry === null) {
1227
+ return;
1228
+ }
1229
+ const cleaned = stripNulls(entry);
1230
+ if (cleaned === void 0) {
1231
+ return;
1232
+ }
1233
+ next[key] = cleaned;
1234
+ });
1235
+ return next;
1236
+ }
413
1237
  var now = () => typeof performance !== "undefined" ? performance.now() : Date.now();
414
1238
  function pickBundleGraph(bundle, preferredKinds) {
415
1239
  if (!bundle?.graphs || bundle.graphs.length === 0) {
@@ -580,26 +1404,49 @@ function convertExtractedAnimations(clips) {
580
1404
  }
581
1405
  const parsedValueSize = track.valueSize != null ? Number(track.valueSize) : NaN;
582
1406
  const valueSize = Number.isFinite(parsedValueSize) && parsedValueSize > 0 ? parsedValueSize : 1;
583
- if (values.length !== times.length * valueSize) {
1407
+ const interpolationRaw = typeof track.interpolation === "string" ? track.interpolation.trim().toLowerCase() : "linear";
1408
+ const interpolation = interpolationRaw === "step" ? "step" : interpolationRaw === "cubic" || interpolationRaw === "cubicspline" ? "cubic" : "linear";
1409
+ const isCubic = interpolation === "cubic";
1410
+ const hasTripletTangents = isCubic && values.length === times.length * valueSize * 3;
1411
+ const hasFlatValues = values.length === times.length * valueSize;
1412
+ if (!hasTripletTangents && !hasFlatValues) {
584
1413
  return;
585
1414
  }
586
1415
  const rawIndex = track.componentIndex != null ? Number(track.componentIndex) : 0;
587
1416
  const componentIndex = Number.isInteger(rawIndex) && rawIndex >= 0 ? Math.min(rawIndex, valueSize - 1) : 0;
588
1417
  const keyframes = [];
589
1418
  times.forEach((time, index) => {
590
- const base = index * valueSize + componentIndex;
591
- const value = values[base];
1419
+ const flatBase = index * valueSize + componentIndex;
1420
+ const valueBase = hasTripletTangents ? index * valueSize * 3 + valueSize + componentIndex : flatBase;
1421
+ const value = values[valueBase];
592
1422
  if (!Number.isFinite(value)) {
593
1423
  return;
594
1424
  }
595
- keyframes.push({ time, value });
1425
+ const keyframe = {
1426
+ time,
1427
+ value
1428
+ };
1429
+ if (hasTripletTangents) {
1430
+ const inBase = index * valueSize * 3 + componentIndex;
1431
+ const outBase = index * valueSize * 3 + valueSize * 2 + componentIndex;
1432
+ const inTangent = values[inBase];
1433
+ const outTangent = values[outBase];
1434
+ if (Number.isFinite(inTangent)) {
1435
+ keyframe.inTangent = inTangent;
1436
+ }
1437
+ if (Number.isFinite(outTangent)) {
1438
+ keyframe.outTangent = outTangent;
1439
+ }
1440
+ }
1441
+ keyframes.push(keyframe);
596
1442
  });
597
1443
  if (keyframes.length === 0) {
598
1444
  return;
599
1445
  }
600
1446
  convertedTracks.push({
601
1447
  channel: channelId,
602
- keyframes
1448
+ keyframes,
1449
+ interpolation
603
1450
  });
604
1451
  });
605
1452
  if (convertedTracks.length === 0) {
@@ -666,29 +1513,91 @@ function mergeAnimationLists(explicit, fromBundle) {
666
1513
  }
667
1514
  return changed ? merged : explicit;
668
1515
  }
669
- function mergeAssetBundle(base, extracted, extractedAnimations) {
670
- const resolvedBundle = base.bundle ?? extracted ?? null;
671
- const rigFromBundle = convertBundleGraph(
672
- pickBundleGraph(resolvedBundle, ["rig"])
1516
+ function extractProgramResetValues(value) {
1517
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1518
+ return void 0;
1519
+ }
1520
+ const entries = Object.entries(value).filter(
1521
+ ([, rawValue]) => Number.isFinite(Number(rawValue))
673
1522
  );
674
- const resolvedRig = base.rig ?? rigFromBundle ?? void 0;
675
- const basePose = base.pose;
676
- const poseStageFilter = basePose?.stageNeutralFilter;
677
- const poseGraphFromBundle = basePose?.graph ? null : convertBundleGraph(
678
- pickBundleGraph(resolvedBundle, ["pose-driver", "pose"])
1523
+ if (entries.length === 0) {
1524
+ return void 0;
1525
+ }
1526
+ return Object.fromEntries(
1527
+ entries.map(([path, rawValue]) => [path, Number(rawValue)])
679
1528
  );
680
- const resolvedPoseGraph = basePose?.graph ?? poseGraphFromBundle ?? void 0;
681
- const resolvedPoseConfig = basePose?.config ?? resolvedBundle?.poses?.config ?? void 0;
682
- let resolvedPose = basePose;
683
- if (basePose) {
684
- const nextPose = { ...basePose };
685
- let changed = false;
686
- if (resolvedPoseGraph && basePose.graph !== resolvedPoseGraph) {
687
- nextPose.graph = resolvedPoseGraph;
688
- changed = true;
1529
+ }
1530
+ function convertBundlePrograms(entries) {
1531
+ if (!Array.isArray(entries) || entries.length === 0) {
1532
+ return [];
1533
+ }
1534
+ return entries.filter((entry) => normaliseBundleKind(entry?.kind) === "motiongraph").map((entry) => {
1535
+ const graph = convertBundleGraph(entry);
1536
+ if (!graph) {
1537
+ return null;
689
1538
  }
690
- if (resolvedPoseConfig && basePose.config !== resolvedPoseConfig) {
691
- nextPose.config = resolvedPoseConfig;
1539
+ const metadata = entry.metadata && typeof entry.metadata === "object" && !Array.isArray(entry.metadata) ? entry.metadata : void 0;
1540
+ return {
1541
+ id: entry.id,
1542
+ label: typeof entry.label === "string" ? entry.label : void 0,
1543
+ graph,
1544
+ metadata,
1545
+ resetValues: extractProgramResetValues(metadata?.resetValues)
1546
+ };
1547
+ }).filter(Boolean);
1548
+ }
1549
+ function mergeProgramLists(explicit, fromBundle) {
1550
+ if (!explicit?.length && fromBundle.length === 0) {
1551
+ return void 0;
1552
+ }
1553
+ if (!explicit?.length) {
1554
+ return fromBundle.length > 0 ? fromBundle : void 0;
1555
+ }
1556
+ if (fromBundle.length === 0) {
1557
+ return explicit;
1558
+ }
1559
+ const seen = new Set(explicit.map((program) => program.id));
1560
+ let changed = false;
1561
+ const merged = [...explicit];
1562
+ for (const program of fromBundle) {
1563
+ if (!program.id || seen.has(program.id)) {
1564
+ continue;
1565
+ }
1566
+ merged.push(program);
1567
+ seen.add(program.id);
1568
+ changed = true;
1569
+ }
1570
+ return changed ? merged : explicit;
1571
+ }
1572
+ function mergeAssetBundle(base, extracted, extractedAnimations) {
1573
+ const resolvedBundle = base.bundle ?? extracted ?? null;
1574
+ const rigFromBundle = convertBundleGraph(
1575
+ pickBundleGraph(resolvedBundle, ["rig"])
1576
+ );
1577
+ const resolvedRig = base.rig ?? rigFromBundle ?? void 0;
1578
+ const basePose = base.pose;
1579
+ const hasBasePoseGraphOverride = Boolean(
1580
+ basePose && Object.prototype.hasOwnProperty.call(basePose, "graph")
1581
+ );
1582
+ const hasBasePoseConfigOverride = Boolean(
1583
+ basePose && Object.prototype.hasOwnProperty.call(basePose, "config")
1584
+ );
1585
+ const poseStageFilter = basePose?.stageNeutralFilter;
1586
+ const poseGraphFromBundle = hasBasePoseGraphOverride ? null : convertBundleGraph(
1587
+ pickBundleGraph(resolvedBundle, ["pose-driver", "pose"])
1588
+ );
1589
+ const resolvedPoseGraph = hasBasePoseGraphOverride ? basePose?.graph : basePose?.graph ?? poseGraphFromBundle ?? void 0;
1590
+ const resolvedPoseConfig = hasBasePoseConfigOverride ? basePose?.config : basePose?.config ?? resolvedBundle?.poses?.config ?? void 0;
1591
+ let resolvedPose = basePose;
1592
+ if (basePose) {
1593
+ const nextPose = { ...basePose };
1594
+ let changed = false;
1595
+ if (resolvedPoseGraph && basePose.graph !== resolvedPoseGraph) {
1596
+ nextPose.graph = resolvedPoseGraph;
1597
+ changed = true;
1598
+ }
1599
+ if (resolvedPoseConfig && basePose.config !== resolvedPoseConfig) {
1600
+ nextPose.config = resolvedPoseConfig;
692
1601
  changed = true;
693
1602
  }
694
1603
  if (!resolvedPoseGraph && !basePose.graph) {
@@ -717,6 +1626,10 @@ function mergeAssetBundle(base, extracted, extractedAnimations) {
717
1626
  animationsFromAsset
718
1627
  );
719
1628
  }
1629
+ const programsFromBundle = mergeProgramLists(
1630
+ base.programs,
1631
+ convertBundlePrograms(resolvedBundle?.graphs)
1632
+ );
720
1633
  const merged = {
721
1634
  ...base
722
1635
  };
@@ -727,14 +1640,106 @@ function mergeAssetBundle(base, extracted, extractedAnimations) {
727
1640
  }
728
1641
  merged.pose = resolvedPose;
729
1642
  merged.animations = resolvedAnimations;
1643
+ merged.programs = programsFromBundle;
730
1644
  merged.bundle = resolvedBundle;
731
1645
  return merged;
732
1646
  }
1647
+ function normalizeStoredAnimationInterpolation(interpolation) {
1648
+ const mode = typeof interpolation === "string" ? interpolation.trim().toLowerCase() : "linear";
1649
+ if (mode === "step") {
1650
+ return "step";
1651
+ }
1652
+ if (mode === "cubic" || mode === "cubicspline") {
1653
+ return "cubic";
1654
+ }
1655
+ return "linear";
1656
+ }
1657
+ function buildStoredAnimationTransitions(mode) {
1658
+ if (mode === "cubic") {
1659
+ return void 0;
1660
+ }
1661
+ if (mode === "step") {
1662
+ return {
1663
+ in: { x: 1, y: 1 },
1664
+ out: { x: 1, y: 0 }
1665
+ };
1666
+ }
1667
+ return {
1668
+ in: { x: 1, y: 1 },
1669
+ out: { x: 0, y: 0 }
1670
+ };
1671
+ }
1672
+ function toStoredAnimationClip(fallbackId, clip) {
1673
+ const clipId = typeof clip.id === "string" && clip.id.trim().length > 0 ? clip.id.trim() : fallbackId;
1674
+ const clipName = typeof clip.name === "string" && clip.name.trim().length > 0 ? clip.name.trim() : clipId;
1675
+ const durationSeconds = resolveClipDurationSeconds(clip, 0);
1676
+ const durationMs = Math.max(1, Math.round(durationSeconds * 1e3));
1677
+ const tracks = (Array.isArray(clip.tracks) ? clip.tracks : []).map((rawTrack, trackIndex) => {
1678
+ const channel = typeof rawTrack.channel === "string" ? rawTrack.channel.trim() : "";
1679
+ if (!channel) {
1680
+ return null;
1681
+ }
1682
+ const keyframes = (Array.isArray(rawTrack.keyframes) ? rawTrack.keyframes : []).map((keyframe) => {
1683
+ const time = Number(keyframe.time);
1684
+ const value = Number(keyframe.value);
1685
+ const keyframeId = keyframe["id"];
1686
+ const keyframeInterpolation = keyframe["interpolation"];
1687
+ if (!Number.isFinite(time) || !Number.isFinite(value)) {
1688
+ return null;
1689
+ }
1690
+ return {
1691
+ id: typeof keyframeId === "string" && keyframeId.trim().length > 0 ? keyframeId.trim() : `${clipId}:track-${trackIndex.toString().padStart(4, "0")}:point-${time.toFixed(6)}`,
1692
+ time,
1693
+ value,
1694
+ mode: normalizeStoredAnimationInterpolation(
1695
+ keyframeInterpolation ?? rawTrack.interpolation
1696
+ )
1697
+ };
1698
+ }).filter(Boolean);
1699
+ if (keyframes.length === 0) {
1700
+ return null;
1701
+ }
1702
+ keyframes.sort((left, right) => {
1703
+ if (left.time !== right.time) {
1704
+ return left.time - right.time;
1705
+ }
1706
+ return left.id.localeCompare(right.id);
1707
+ });
1708
+ const rawTrackId = rawTrack["id"];
1709
+ const rawTrackName = rawTrack["name"];
1710
+ const trackId = typeof rawTrackId === "string" && rawTrackId.trim().length > 0 ? rawTrackId.trim() : `${clipId}:track-${trackIndex.toString().padStart(4, "0")}`;
1711
+ const trackName = typeof rawTrackName === "string" && rawTrackName.trim().length > 0 ? rawTrackName.trim() : channel.replace(/^\/+/, "") || trackId;
1712
+ const denominator = durationSeconds > 0 ? durationSeconds : 1;
1713
+ return {
1714
+ id: trackId,
1715
+ name: trackName,
1716
+ animatableId: channel,
1717
+ points: keyframes.map((keyframe) => {
1718
+ const stamp = Math.max(0, Math.min(1, keyframe.time / denominator));
1719
+ const transitions = buildStoredAnimationTransitions(keyframe.mode);
1720
+ return {
1721
+ id: keyframe.id,
1722
+ stamp,
1723
+ value: keyframe.value,
1724
+ ...transitions ? { transitions } : {}
1725
+ };
1726
+ })
1727
+ };
1728
+ }).filter(Boolean);
1729
+ return {
1730
+ id: clipId,
1731
+ name: clipName,
1732
+ duration: durationMs,
1733
+ groups: {},
1734
+ tracks
1735
+ };
1736
+ }
733
1737
  function VizijRuntimeProvider({
734
1738
  assetBundle,
735
1739
  children,
736
1740
  namespace: namespaceProp,
737
1741
  faceId: faceIdProp,
1742
+ updateTier = "auto",
738
1743
  autoCreate = true,
739
1744
  createOptions,
740
1745
  autostart = false,
@@ -742,6 +1747,7 @@ function VizijRuntimeProvider({
742
1747
  mergeStrategy,
743
1748
  onRegisterControllers,
744
1749
  onStatusChange,
1750
+ transformOutputWrite,
745
1751
  orchestratorScope = "auto"
746
1752
  }) {
747
1753
  const storeRef = useRef(null);
@@ -762,6 +1768,7 @@ function VizijRuntimeProvider({
762
1768
  assetBundle,
763
1769
  namespace: namespaceProp,
764
1770
  faceId: faceIdProp,
1771
+ updateTier,
765
1772
  autoCreate,
766
1773
  autostart,
767
1774
  driveOrchestrator,
@@ -769,6 +1776,7 @@ function VizijRuntimeProvider({
769
1776
  mergeStrategy,
770
1777
  onRegisterControllers,
771
1778
  onStatusChange,
1779
+ transformOutputWrite,
772
1780
  store: storeRef.current,
773
1781
  children
774
1782
  }
@@ -790,9 +1798,11 @@ function VizijRuntimeProviderInner({
790
1798
  assetBundle: initialAssetBundle,
791
1799
  namespace: namespaceProp,
792
1800
  faceId: faceIdProp,
1801
+ updateTier,
793
1802
  mergeStrategy,
794
1803
  onRegisterControllers,
795
1804
  onStatusChange,
1805
+ transformOutputWrite,
796
1806
  store,
797
1807
  children,
798
1808
  autoCreate,
@@ -800,35 +1810,66 @@ function VizijRuntimeProviderInner({
800
1810
  createOptions,
801
1811
  driveOrchestrator
802
1812
  }) {
1813
+ const [assetBundleOverride, setAssetBundleOverride] = useState(null);
1814
+ const [graphUpdateToken, setGraphUpdateToken] = useState(0);
1815
+ const effectiveAssetBundle = assetBundleOverride ?? initialAssetBundle;
1816
+ const latestEffectiveAssetBundleRef = useRef(effectiveAssetBundle);
803
1817
  const [extractedBundle, setExtractedBundle] = useState(() => {
804
- if (initialAssetBundle.bundle) {
805
- return initialAssetBundle.bundle;
1818
+ if (effectiveAssetBundle.bundle) {
1819
+ return effectiveAssetBundle.bundle;
806
1820
  }
807
- if (initialAssetBundle.glb.kind === "world" && initialAssetBundle.glb.bundle) {
808
- return initialAssetBundle.glb.bundle;
1821
+ if (effectiveAssetBundle.glb.kind === "world" && effectiveAssetBundle.glb.bundle) {
1822
+ return effectiveAssetBundle.glb.bundle;
809
1823
  }
810
1824
  return null;
811
1825
  });
812
1826
  const [extractedAnimations, setExtractedAnimations] = useState([]);
1827
+ const previousBundleRef = useRef(null);
1828
+ const suppressNextBundlePlanRef = useRef(false);
1829
+ const pendingPlanRef = useRef(null);
1830
+ const updateTierRef = useRef(updateTier);
813
1831
  useEffect(() => {
814
- if (initialAssetBundle.bundle) {
815
- setExtractedBundle(initialAssetBundle.bundle);
1832
+ if (effectiveAssetBundle.bundle) {
1833
+ setExtractedBundle(effectiveAssetBundle.bundle);
816
1834
  return;
817
1835
  }
818
- if (initialAssetBundle.glb.kind === "world") {
819
- setExtractedBundle(initialAssetBundle.glb.bundle ?? null);
1836
+ if (effectiveAssetBundle.glb.kind === "world") {
1837
+ setExtractedBundle(effectiveAssetBundle.glb.bundle ?? null);
820
1838
  } else {
821
1839
  setExtractedBundle(null);
822
1840
  }
823
- }, [initialAssetBundle]);
1841
+ }, [effectiveAssetBundle]);
1842
+ useEffect(() => {
1843
+ updateTierRef.current = updateTier;
1844
+ }, [updateTier]);
824
1845
  const assetBundle = useMemo(
825
1846
  () => mergeAssetBundle(
826
- initialAssetBundle,
1847
+ effectiveAssetBundle,
827
1848
  extractedBundle,
828
1849
  extractedAnimations
829
1850
  ),
830
- [initialAssetBundle, extractedBundle, extractedAnimations]
1851
+ [effectiveAssetBundle, extractedBundle, extractedAnimations]
831
1852
  );
1853
+ useEffect(() => {
1854
+ latestEffectiveAssetBundleRef.current = effectiveAssetBundle;
1855
+ }, [effectiveAssetBundle]);
1856
+ useEffect(() => {
1857
+ if (suppressNextBundlePlanRef.current) {
1858
+ suppressNextBundlePlanRef.current = false;
1859
+ previousBundleRef.current = effectiveAssetBundle;
1860
+ return;
1861
+ }
1862
+ const plan = resolveRuntimeUpdatePlan(
1863
+ previousBundleRef.current,
1864
+ effectiveAssetBundle,
1865
+ updateTierRef.current
1866
+ );
1867
+ pendingPlanRef.current = plan;
1868
+ previousBundleRef.current = effectiveAssetBundle;
1869
+ if (plan.reregisterGraphs) {
1870
+ setGraphUpdateToken((prev) => prev + 1);
1871
+ }
1872
+ }, [effectiveAssetBundle]);
832
1873
  const {
833
1874
  ready,
834
1875
  createOrchestrator,
@@ -837,6 +1878,7 @@ function VizijRuntimeProviderInner({
837
1878
  registerAnimation,
838
1879
  removeGraph,
839
1880
  removeAnimation,
1881
+ removeInput,
840
1882
  listControllers,
841
1883
  setInput: orchestratorSetInput,
842
1884
  getPathSnapshot,
@@ -864,13 +1906,57 @@ function VizijRuntimeProviderInner({
864
1906
  const namespaceRef = useRef(namespace);
865
1907
  const driveOrchestratorRef = useRef(driveOrchestrator);
866
1908
  const rigInputMapRef = useRef({});
1909
+ const rigPoseControlInputIdsRef = useRef(/* @__PURE__ */ new Set());
867
1910
  const registeredGraphsRef = useRef([]);
868
1911
  const registeredAnimationsRef = useRef([]);
869
1912
  const mergedGraphRef = useRef(null);
1913
+ const poseControlBridgeValuesRef = useRef(/* @__PURE__ */ new Map());
1914
+ const poseWeightFallbackMap = useMemo(() => {
1915
+ const map = /* @__PURE__ */ new Map();
1916
+ const poseConfig = assetBundle.pose?.config;
1917
+ if (!poseConfig) {
1918
+ return map;
1919
+ }
1920
+ const posePaths = buildPoseWeightPathMap(
1921
+ poseConfig.poses ?? [],
1922
+ poseConfig.faceId ?? faceId ?? "face"
1923
+ );
1924
+ (poseConfig.poses ?? []).forEach((pose) => {
1925
+ const posePath = posePaths.get(pose.id);
1926
+ if (!posePath) {
1927
+ return;
1928
+ }
1929
+ const values = Object.fromEntries(
1930
+ Object.entries(pose.values ?? {}).filter(
1931
+ ([, value]) => Number.isFinite(value)
1932
+ )
1933
+ );
1934
+ map.set(posePath, values);
1935
+ });
1936
+ return map;
1937
+ }, [assetBundle.pose?.config, faceId]);
1938
+ const useLegacyPoseWeightFallback = useMemo(
1939
+ () => shouldUseLegacyPoseWeightFallback(Boolean(assetBundle.pose?.graph)),
1940
+ [assetBundle.pose?.graph]
1941
+ );
1942
+ const resolvedProgramAssets = useMemo(
1943
+ () => assetBundle.programs && assetBundle.programs.length > 0 ? assetBundle.programs : convertBundlePrograms(assetBundle.bundle?.graphs),
1944
+ [assetBundle.bundle?.graphs, assetBundle.programs]
1945
+ );
870
1946
  const [inputConstraints, setInputConstraints] = useState({});
1947
+ const inputConstraintsRef = useRef({});
871
1948
  const avgStepDtRef = useRef(null);
872
1949
  const animationTweensRef = useRef(/* @__PURE__ */ new Map());
873
1950
  const clipPlaybackRef = useRef(/* @__PURE__ */ new Map());
1951
+ const programPlaybackRef = useRef(
1952
+ /* @__PURE__ */ new Map()
1953
+ );
1954
+ const programControllerIdsRef = useRef(/* @__PURE__ */ new Map());
1955
+ const clipOutputValuesRef = useRef(
1956
+ /* @__PURE__ */ new Map()
1957
+ );
1958
+ const clipAggregateValuesRef = useRef(/* @__PURE__ */ new Map());
1959
+ const animationSystemActiveRef = useRef(true);
874
1960
  const stagedInputsRef = useRef(/* @__PURE__ */ new Map());
875
1961
  const autostartRef = useRef(autostart);
876
1962
  const lastActivityTimeRef = useRef(now());
@@ -888,6 +1974,7 @@ function VizijRuntimeProviderInner({
888
1974
  useEffect(() => {
889
1975
  const rigAsset = assetBundle.rig;
890
1976
  if (!rigAsset) {
1977
+ inputConstraintsRef.current = {};
891
1978
  setInputConstraints({});
892
1979
  return;
893
1980
  }
@@ -900,15 +1987,8 @@ function VizijRuntimeProviderInner({
900
1987
  rigAsset.inputMetadata,
901
1988
  namespace
902
1989
  );
1990
+ inputConstraintsRef.current = constraints;
903
1991
  setInputConstraints(constraints);
904
- const isDevEnv = typeof globalThis !== "undefined" && Boolean(globalThis?.process?.env?.NODE_ENV !== "production");
905
- if (isDevEnv) {
906
- const size = Object.keys(constraints).length;
907
- console.log("[vizij-runtime] input constraints computed", size, {
908
- namespace,
909
- rigId: rigAsset.id
910
- });
911
- }
912
1992
  }, [assetBundle.rig, namespace]);
913
1993
  const requestLoopMode = useCallback((mode) => {
914
1994
  if (!runtimeMountedRef.current) {
@@ -917,7 +1997,28 @@ function VizijRuntimeProviderInner({
917
1997
  setLoopMode((prev) => prev === mode ? prev : mode);
918
1998
  }, []);
919
1999
  const hasActiveAnimations = useCallback(() => {
920
- return animationTweensRef.current.size > 0 || clipPlaybackRef.current.size > 0;
2000
+ if (animationTweensRef.current.size > 0) {
2001
+ return true;
2002
+ }
2003
+ if (!animationSystemActiveRef.current) {
2004
+ for (const state of programPlaybackRef.current.values()) {
2005
+ if (state.state === "playing") {
2006
+ return true;
2007
+ }
2008
+ }
2009
+ return false;
2010
+ }
2011
+ for (const state of clipPlaybackRef.current.values()) {
2012
+ if (state.playing) {
2013
+ return true;
2014
+ }
2015
+ }
2016
+ for (const state of programPlaybackRef.current.values()) {
2017
+ if (state.state === "playing") {
2018
+ return true;
2019
+ }
2020
+ }
2021
+ return false;
921
2022
  }, []);
922
2023
  const computeDesiredLoopMode = useCallback(() => {
923
2024
  const hasAnimations = hasActiveAnimations();
@@ -939,11 +2040,50 @@ function VizijRuntimeProviderInner({
939
2040
  }, [updateLoopMode]);
940
2041
  const setInput = useCallback(
941
2042
  (path, value, shape) => {
2043
+ const numericValue = valueAsNumber2(value);
2044
+ const basePath = stripNamespace(
2045
+ normalisePath(path),
2046
+ namespaceRef.current
2047
+ );
2048
+ const poseValues = useLegacyPoseWeightFallback && numericValue != null ? poseWeightFallbackMap.get(basePath) : void 0;
2049
+ if (poseValues && numericValue != null) {
2050
+ const poseFaceId = assetBundle.pose?.config?.faceId ?? faceId ?? "face";
2051
+ const rigMap = rigInputMapRef.current;
2052
+ Object.entries(poseValues).forEach(([inputId, poseValue]) => {
2053
+ if (!Number.isFinite(poseValue)) {
2054
+ return;
2055
+ }
2056
+ const controlPath = resolvePoseControlInputPath({
2057
+ inputId,
2058
+ basePath: buildRigInputPath(
2059
+ poseFaceId,
2060
+ `/pose/control/${inputId}`
2061
+ ),
2062
+ rigInputPathMap: rigMap,
2063
+ hasNativePoseControlInput: true
2064
+ }) ?? buildRigInputPath(poseFaceId, `/pose/control/${inputId}`);
2065
+ setInput(controlPath, { float: Number(poseValue) * numericValue });
2066
+ });
2067
+ return;
2068
+ }
942
2069
  markActivity();
943
2070
  const namespacedPath = namespaceTypedPath(path, namespaceRef.current);
2071
+ if (isRuntimeDebugEnabled() && (namespacedPath.includes("animation/authoring.timeline.main") || namespacedPath.endsWith("/blink"))) {
2072
+ console.log("[vizij-runtime] stage input", {
2073
+ path,
2074
+ namespacedPath,
2075
+ value
2076
+ });
2077
+ }
944
2078
  stagedInputsRef.current.set(namespacedPath, { value, shape });
945
2079
  },
946
- [markActivity]
2080
+ [
2081
+ assetBundle.pose?.config?.faceId,
2082
+ faceId,
2083
+ markActivity,
2084
+ poseWeightFallbackMap,
2085
+ useLegacyPoseWeightFallback
2086
+ ]
947
2087
  );
948
2088
  const reportStatus = useCallback(
949
2089
  (updater) => {
@@ -1007,10 +2147,14 @@ function VizijRuntimeProviderInner({
1007
2147
  });
1008
2148
  registeredGraphsRef.current = [];
1009
2149
  registeredAnimationsRef.current = [];
2150
+ programControllerIdsRef.current.clear();
1010
2151
  mergedGraphRef.current = null;
1011
2152
  outputPathsRef.current = /* @__PURE__ */ new Set();
1012
2153
  baseOutputPathsRef.current = /* @__PURE__ */ new Set();
1013
2154
  namespacedOutputPathsRef.current = /* @__PURE__ */ new Set();
2155
+ rigPoseControlInputIdsRef.current = /* @__PURE__ */ new Set();
2156
+ clipOutputValuesRef.current.clear();
2157
+ clipAggregateValuesRef.current.clear();
1014
2158
  }, [listControllers, removeAnimation, removeGraph, pushError]);
1015
2159
  useEffect(() => {
1016
2160
  namespaceRef.current = namespace;
@@ -1023,10 +2167,19 @@ function VizijRuntimeProviderInner({
1023
2167
  useEffect(() => {
1024
2168
  driveOrchestratorRef.current = driveOrchestrator;
1025
2169
  }, [driveOrchestrator]);
1026
- const glbAsset = initialAssetBundle.glb;
1027
- const baseBundle = initialAssetBundle.bundle ?? null;
2170
+ const glbAsset = effectiveAssetBundle.glb;
2171
+ const baseBundle = effectiveAssetBundle.bundle ?? null;
1028
2172
  useEffect(() => {
1029
2173
  let cancelled = false;
2174
+ const plan = pendingPlanRef.current;
2175
+ if (plan && !plan.reloadAssets && status.rootId !== null) {
2176
+ reportStatus(
2177
+ (prev) => prev.loading ? { ...prev, loading: false } : prev
2178
+ );
2179
+ return () => {
2180
+ cancelled = true;
2181
+ };
2182
+ }
1030
2183
  resetErrors();
1031
2184
  reportStatus((prev) => ({
1032
2185
  ...prev,
@@ -1078,6 +2231,12 @@ function VizijRuntimeProviderInner({
1078
2231
  setExtractedAnimations(convertExtractedAnimations(gltfAnimations));
1079
2232
  const rootId = findRootId(world);
1080
2233
  store.getState().addWorldElements(world, animatables, true);
2234
+ if (pendingPlanRef.current?.reloadAssets) {
2235
+ pendingPlanRef.current = {
2236
+ ...pendingPlanRef.current,
2237
+ reloadAssets: false
2238
+ };
2239
+ }
1081
2240
  reportStatus((prev) => ({
1082
2241
  ...prev,
1083
2242
  loading: false,
@@ -1115,7 +2274,8 @@ function VizijRuntimeProviderInner({
1115
2274
  reportStatus,
1116
2275
  resetErrors,
1117
2276
  setExtractedBundle,
1118
- setExtractedAnimations
2277
+ setExtractedAnimations,
2278
+ status.rootId
1119
2279
  ]);
1120
2280
  useEffect(() => {
1121
2281
  if (!ready && autoCreate) {
@@ -1131,6 +2291,15 @@ function VizijRuntimeProviderInner({
1131
2291
  }, [ready, autoCreate, createOptions, createOrchestrator, pushError]);
1132
2292
  const registerControllers = useCallback(async () => {
1133
2293
  clearControllers();
2294
+ if (isRuntimeDebugEnabled()) {
2295
+ console.log("[vizij-runtime] registerControllers", {
2296
+ hasRig: Boolean(assetBundle.rig),
2297
+ hasPose: Boolean(assetBundle.pose?.graph),
2298
+ animationCount: assetBundle.animations?.length ?? 0,
2299
+ animationIds: (assetBundle.animations ?? []).map((anim) => anim.id),
2300
+ namespace
2301
+ });
2302
+ }
1134
2303
  const baseOutputPaths = /* @__PURE__ */ new Set();
1135
2304
  const namespacedOutputPaths = /* @__PURE__ */ new Set();
1136
2305
  const recordOutputs = (paths) => {
@@ -1142,39 +2311,60 @@ function VizijRuntimeProviderInner({
1142
2311
  namespacedOutputPaths.add(namespaceTypedPath(trimmed, namespace));
1143
2312
  });
1144
2313
  };
2314
+ const graphConfigs = [];
2315
+ rigInputMapRef.current = {};
2316
+ rigPoseControlInputIdsRef.current = /* @__PURE__ */ new Set();
2317
+ poseControlBridgeValuesRef.current.clear();
1145
2318
  const rigAsset = assetBundle.rig;
1146
- if (!rigAsset) {
1147
- pushError({
1148
- message: "Asset bundle is missing a rig graph.",
1149
- phase: "registration",
1150
- timestamp: performance.now()
1151
- });
1152
- return;
1153
- }
1154
- const rigSpec = resolveGraphSpec(rigAsset, `${rigAsset.id ?? "rig"} graph`);
1155
- if (!rigSpec) {
1156
- pushError({
1157
- message: "Rig graph is missing a usable spec or IR payload.",
1158
- phase: "registration",
1159
- timestamp: performance.now()
1160
- });
1161
- return;
1162
- }
1163
- const rigOutputs = collectOutputPaths(rigSpec);
1164
- const rigInputs = collectInputPaths(rigSpec);
1165
- rigInputMapRef.current = collectInputPathMap(rigSpec);
1166
- recordOutputs(rigOutputs);
1167
- const rigSubs = rigAsset.subscriptions ?? {
1168
- inputs: rigInputs,
1169
- outputs: rigOutputs
1170
- };
1171
- const graphConfigs = [
1172
- {
1173
- id: namespaceControllerId(rigAsset.id, namespace, "graph"),
1174
- spec: namespaceGraphSpec(rigSpec, namespace),
1175
- subs: namespaceSubscriptions(rigSubs, namespace)
2319
+ if (rigAsset) {
2320
+ const rigSpec = resolveGraphSpec(
2321
+ rigAsset,
2322
+ `${rigAsset.id ?? "rig"} graph`
2323
+ );
2324
+ if (!rigSpec) {
2325
+ pushError({
2326
+ message: "Rig graph is missing a usable spec or IR payload.",
2327
+ phase: "registration",
2328
+ timestamp: performance.now()
2329
+ });
2330
+ } else {
2331
+ const rigOutputs = collectOutputPaths(rigSpec);
2332
+ const rigInputs = collectInputPaths(rigSpec);
2333
+ const rigPoseControlInputIds = /* @__PURE__ */ new Set();
2334
+ rigInputs.forEach((path) => {
2335
+ const poseControlMatch = /^rig\/[^/]+\/pose\/control\/(.+)$/.exec(
2336
+ path.trim()
2337
+ );
2338
+ const inputId = (poseControlMatch?.[1] ?? "").trim();
2339
+ if (inputId.length > 0) {
2340
+ rigPoseControlInputIds.add(inputId);
2341
+ }
2342
+ });
2343
+ rigInputMapRef.current = collectInputPathMap(rigSpec);
2344
+ rigPoseControlInputIdsRef.current = rigPoseControlInputIds;
2345
+ if (isRuntimeDebugEnabled()) {
2346
+ const blinkKeys = Object.keys(rigInputMapRef.current).filter(
2347
+ (key) => key.toLowerCase().includes("blink")
2348
+ );
2349
+ const blinkMappings = blinkKeys.slice(0, 20).map((key) => `${key} => ${rigInputMapRef.current[key] ?? "?"}`);
2350
+ console.log("[vizij-runtime] rig input map sample", {
2351
+ blink: rigInputMapRef.current["blink"] ?? null,
2352
+ blinkKeys: blinkKeys.slice(0, 12),
2353
+ blinkMappings: blinkMappings.join(" | ")
2354
+ });
2355
+ }
2356
+ recordOutputs(rigOutputs);
2357
+ const rigSubs = rigAsset.subscriptions ?? {
2358
+ inputs: rigInputs,
2359
+ outputs: rigOutputs
2360
+ };
2361
+ graphConfigs.push({
2362
+ id: namespaceControllerId(rigAsset.id, namespace, "graph"),
2363
+ spec: stripNulls(namespaceGraphSpec(rigSpec, namespace)),
2364
+ subs: namespaceSubscriptions(rigSubs, namespace)
2365
+ });
1176
2366
  }
1177
- ];
2367
+ }
1178
2368
  const poseGraphAsset = assetBundle.pose?.graph;
1179
2369
  if (poseGraphAsset) {
1180
2370
  const poseSpec = resolveGraphSpec(
@@ -1191,7 +2381,7 @@ function VizijRuntimeProviderInner({
1191
2381
  };
1192
2382
  graphConfigs.push({
1193
2383
  id: namespaceControllerId(poseGraphAsset.id, namespace, "graph"),
1194
- spec: namespaceGraphSpec(poseSpec, namespace),
2384
+ spec: stripNulls(namespaceGraphSpec(poseSpec, namespace)),
1195
2385
  subs: namespaceSubscriptions(poseSubs, namespace)
1196
2386
  });
1197
2387
  } else {
@@ -1200,6 +2390,31 @@ function VizijRuntimeProviderInner({
1200
2390
  );
1201
2391
  }
1202
2392
  }
2393
+ for (const animation of assetBundle.animations ?? []) {
2394
+ const bridgeOutputs = collectAnimationClipOutputPaths(
2395
+ animation.clip,
2396
+ faceId ?? void 0,
2397
+ rigInputMapRef.current
2398
+ );
2399
+ if (isRuntimeDebugEnabled()) {
2400
+ console.log("[vizij-runtime] animation output routing", {
2401
+ animationId: animation.id,
2402
+ bridgeOutputs,
2403
+ bridgeOutputsText: bridgeOutputs.join(" | ")
2404
+ });
2405
+ }
2406
+ recordOutputs(bridgeOutputs);
2407
+ }
2408
+ for (const program of resolvedProgramAssets) {
2409
+ const programSpec = resolveGraphSpec(
2410
+ program.graph,
2411
+ `${program.id ?? "program"} graph (outputs)`
2412
+ );
2413
+ if (!programSpec) {
2414
+ continue;
2415
+ }
2416
+ recordOutputs(collectOutputPaths(programSpec));
2417
+ }
1203
2418
  outputPathsRef.current = namespacedOutputPaths;
1204
2419
  baseOutputPathsRef.current = baseOutputPaths;
1205
2420
  namespacedOutputPathsRef.current = namespacedOutputPaths;
@@ -1232,20 +2447,30 @@ function VizijRuntimeProviderInner({
1232
2447
  });
1233
2448
  }
1234
2449
  registeredGraphsRef.current = graphIds;
2450
+ if (isRuntimeDebugEnabled()) {
2451
+ console.log("[vizij-runtime] registered graph ids", graphIds);
2452
+ }
1235
2453
  const animationIds = [];
1236
2454
  for (const anim of assetBundle.animations ?? []) {
1237
2455
  try {
1238
2456
  const controllerId = namespaceControllerId(anim.id, namespace, "animation") ?? anim.id;
2457
+ const animationPayload = anim.setup?.animation ?? toStoredAnimationClip(anim.id, anim.clip);
1239
2458
  const config = {
1240
2459
  id: controllerId,
1241
2460
  setup: {
1242
- animation: anim.clip,
1243
- ...anim.setup ?? {}
2461
+ ...anim.setup ?? {},
2462
+ animation: animationPayload
1244
2463
  }
1245
2464
  };
1246
2465
  const id = registerAnimation(config);
1247
2466
  animationIds.push(id);
1248
2467
  } catch (err) {
2468
+ if (isRuntimeDebugEnabled()) {
2469
+ console.warn("[vizij-runtime] failed animation registration", {
2470
+ animationId: anim.id,
2471
+ error: err instanceof Error ? err.message : String(err)
2472
+ });
2473
+ }
1249
2474
  pushError({
1250
2475
  message: `Failed to register animation ${anim.id}`,
1251
2476
  cause: err,
@@ -1270,6 +2495,13 @@ function VizijRuntimeProviderInner({
1270
2495
  });
1271
2496
  }
1272
2497
  const controllers = listControllers();
2498
+ if (isRuntimeDebugEnabled()) {
2499
+ console.log("[vizij-runtime] controllers after register", {
2500
+ controllers,
2501
+ graphIds,
2502
+ animationIds
2503
+ });
2504
+ }
1273
2505
  reportStatus((prev) => ({
1274
2506
  ...prev,
1275
2507
  ready: true,
@@ -1289,12 +2521,18 @@ function VizijRuntimeProviderInner({
1289
2521
  registerGraph,
1290
2522
  registerMergedGraph,
1291
2523
  reportStatus,
2524
+ resolvedProgramAssets,
1292
2525
  setInput
1293
2526
  ]);
1294
2527
  useEffect(() => {
1295
2528
  if (!ready || status.loading) {
1296
2529
  return;
1297
2530
  }
2531
+ const plan = pendingPlanRef.current;
2532
+ const hasRegistered = registeredGraphsRef.current.length > 0 || registeredAnimationsRef.current.length > 0 || programControllerIdsRef.current.size > 0;
2533
+ if (plan && !plan.reregisterGraphs && hasRegistered) {
2534
+ return;
2535
+ }
1298
2536
  registerControllers().catch((err) => {
1299
2537
  pushError({
1300
2538
  message: "Failed to register controllers",
@@ -1303,7 +2541,7 @@ function VizijRuntimeProviderInner({
1303
2541
  timestamp: performance.now()
1304
2542
  });
1305
2543
  });
1306
- }, [ready, status.loading, registerControllers, pushError]);
2544
+ }, [ready, status.loading, graphUpdateToken, registerControllers, pushError]);
1307
2545
  useEffect(() => {
1308
2546
  if (!frame) {
1309
2547
  return;
@@ -1314,26 +2552,66 @@ function VizijRuntimeProviderInner({
1314
2552
  }
1315
2553
  const setWorldValues = store.getState().setValues;
1316
2554
  const namespaceValue = status.namespace;
2555
+ const currentValues = store.getState().values;
2556
+ const rigInputPathMap = rigInputMapRef.current;
2557
+ const rigPoseControlInputIds = rigPoseControlInputIdsRef.current;
1317
2558
  const batched = [];
1318
2559
  const namespacedOutputs = namespacedOutputPathsRef.current;
1319
2560
  const baseOutputs = baseOutputPathsRef.current;
1320
2561
  writes.forEach((write) => {
1321
2562
  const path = normalisePath(write.path);
1322
- if (!namespacedOutputs.has(path)) {
2563
+ const basePath = stripNamespace(path, namespaceValue);
2564
+ const isTrackedOutput = namespacedOutputs.has(path) || baseOutputs.has(basePath);
2565
+ if (!isTrackedOutput) {
1323
2566
  return;
1324
2567
  }
1325
2568
  const raw = valueJSONToRaw(write.value);
1326
2569
  if (raw === void 0) {
1327
2570
  return;
1328
2571
  }
1329
- const basePath = stripNamespace(path, namespaceValue);
2572
+ const poseControlMatch = /^rig\/[^/]+\/pose\/control\/(.+)$/.exec(
2573
+ basePath
2574
+ );
2575
+ if (poseControlMatch && typeof raw === "number" && Number.isFinite(raw)) {
2576
+ const inputId = (poseControlMatch[1] ?? "").trim();
2577
+ const hasNativePoseControlInput = inputId.length > 0 && rigPoseControlInputIds.has(inputId);
2578
+ const mappedInputPath = inputId.length === 0 ? void 0 : resolvePoseControlInputPath({
2579
+ inputId,
2580
+ basePath,
2581
+ rigInputPathMap,
2582
+ hasNativePoseControlInput
2583
+ });
2584
+ if (mappedInputPath) {
2585
+ const bridgeKey = `${namespaceValue}:${mappedInputPath}`;
2586
+ const previousValue = poseControlBridgeValuesRef.current.get(bridgeKey);
2587
+ if (previousValue === void 0 || Math.abs(previousValue - raw) > POSE_CONTROL_BRIDGE_EPSILON) {
2588
+ poseControlBridgeValuesRef.current.set(bridgeKey, raw);
2589
+ setInput(mappedInputPath, { float: raw });
2590
+ }
2591
+ }
2592
+ }
1330
2593
  const targetPath = baseOutputs.has(basePath) ? basePath : path;
1331
- batched.push({ id: targetPath, namespace: namespaceValue, value: raw });
2594
+ const currentValue = currentValues.get(
2595
+ getLookup(namespaceValue, targetPath)
2596
+ );
2597
+ const nextWrite = transformOutputWrite ? transformOutputWrite({
2598
+ id: targetPath,
2599
+ namespace: namespaceValue,
2600
+ value: raw,
2601
+ currentValue
2602
+ }) : {
2603
+ id: targetPath,
2604
+ namespace: namespaceValue,
2605
+ value: raw
2606
+ };
2607
+ if (nextWrite) {
2608
+ batched.push(nextWrite);
2609
+ }
1332
2610
  });
1333
2611
  if (batched.length > 0) {
1334
2612
  setWorldValues(batched);
1335
2613
  }
1336
- }, [frame, status.namespace, store]);
2614
+ }, [frame, status.namespace, store, transformOutputWrite]);
1337
2615
  const stagePoseNeutral = useCallback(
1338
2616
  (force = false) => {
1339
2617
  const neutral = assetBundle.pose?.config?.neutralInputs ?? {};
@@ -1401,88 +2679,237 @@ function VizijRuntimeProviderInner({
1401
2679
  },
1402
2680
  [setInput]
1403
2681
  );
1404
- const sampleTrack = useCallback(
1405
- (track, time) => {
1406
- const keyframes = Array.isArray(track.keyframes) ? track.keyframes : [];
1407
- if (!keyframes.length) {
1408
- return 0;
1409
- }
1410
- const first = keyframes[0];
1411
- if (!first) {
1412
- return 0;
1413
- }
1414
- if (time <= Number(first.time ?? 0)) {
1415
- return Number(first.value ?? 0);
1416
- }
1417
- for (let i = 0; i < keyframes.length - 1; i += 1) {
1418
- const current = keyframes[i] ?? {};
1419
- const next = keyframes[i + 1] ?? {};
1420
- const start = Number(current.time ?? 0);
1421
- const end = Number(next.time ?? start);
1422
- if (time >= start && time <= end) {
1423
- const range = end - start || 1;
1424
- const factor = (time - start) / range;
1425
- const currentValue = Number(current.value ?? 0);
1426
- const nextValue = Number(next.value ?? currentValue);
1427
- return currentValue + (nextValue - currentValue) * factor;
2682
+ const resolveClipById = useCallback(
2683
+ (id) => {
2684
+ return assetBundle.animations?.find((anim) => anim.id === id);
2685
+ },
2686
+ [assetBundle.animations]
2687
+ );
2688
+ const resolveClipPromise = useCallback((state) => {
2689
+ state.resolve?.();
2690
+ state.resolve = null;
2691
+ state.completion = null;
2692
+ }, []);
2693
+ const ensureClipPromise = useCallback((state) => {
2694
+ if (state.completion) {
2695
+ return state.completion;
2696
+ }
2697
+ const completion = new Promise((resolve) => {
2698
+ state.resolve = resolve;
2699
+ });
2700
+ state.completion = completion;
2701
+ return completion;
2702
+ }, []);
2703
+ const setAnimationInput = useCallback(
2704
+ (path, value, options) => {
2705
+ setInput(path, { float: value });
2706
+ if (!options?.immediate) {
2707
+ return;
2708
+ }
2709
+ const namespacedPath = namespaceTypedPath(path, namespaceRef.current);
2710
+ const staged = stagedInputsRef.current.get(namespacedPath);
2711
+ if (staged) {
2712
+ orchestratorSetInput(namespacedPath, staged.value, staged.shape);
2713
+ stagedInputsRef.current.delete(namespacedPath);
2714
+ return;
2715
+ }
2716
+ orchestratorSetInput(namespacedPath, { float: value });
2717
+ },
2718
+ [orchestratorSetInput, setInput]
2719
+ );
2720
+ const clearAnimationInput = useCallback(
2721
+ (path) => {
2722
+ const namespacedPath = namespaceTypedPath(path, namespaceRef.current);
2723
+ stagedInputsRef.current.delete(namespacedPath);
2724
+ removeInput(namespacedPath);
2725
+ },
2726
+ [removeInput]
2727
+ );
2728
+ const buildClipOutputValues = useCallback(
2729
+ (clip, state) => sampleAnimationClipOutputValues(
2730
+ clip.clip,
2731
+ state.time,
2732
+ state.weight,
2733
+ faceId ?? void 0,
2734
+ rigInputMapRef.current
2735
+ ),
2736
+ [faceId]
2737
+ );
2738
+ const computeClipAggregateValues = useCallback(() => {
2739
+ const aggregate = /* @__PURE__ */ new Map();
2740
+ clipOutputValuesRef.current.forEach((outputValues) => {
2741
+ outputValues.forEach((value, path) => {
2742
+ aggregate.set(path, (aggregate.get(path) ?? 0) + value);
2743
+ });
2744
+ });
2745
+ return aggregate;
2746
+ }, []);
2747
+ const stageClipAggregateValues = useCallback(
2748
+ (nextAggregate, options) => {
2749
+ diffAnimationAggregateValues(
2750
+ clipAggregateValuesRef.current,
2751
+ nextAggregate,
2752
+ POSE_CONTROL_BRIDGE_EPSILON
2753
+ ).forEach((operation) => {
2754
+ if (operation.kind === "clear") {
2755
+ clearAnimationInput(operation.path);
2756
+ return;
1428
2757
  }
2758
+ setAnimationInput(operation.path, operation.value, options);
2759
+ });
2760
+ clipAggregateValuesRef.current = nextAggregate;
2761
+ },
2762
+ [clearAnimationInput, setAnimationInput]
2763
+ );
2764
+ const syncClipOutputs = useCallback(
2765
+ (options) => {
2766
+ stageClipAggregateValues(computeClipAggregateValues(), options);
2767
+ },
2768
+ [computeClipAggregateValues, stageClipAggregateValues]
2769
+ );
2770
+ const writeClipOutputs = useCallback(
2771
+ (clip, state, options) => {
2772
+ if (!animationSystemActiveRef.current) {
2773
+ return;
1429
2774
  }
1430
- const last = keyframes[keyframes.length - 1];
1431
- return Number(last?.value ?? 0);
2775
+ clipOutputValuesRef.current.set(
2776
+ clip.id,
2777
+ buildClipOutputValues(clip, state)
2778
+ );
2779
+ syncClipOutputs(options);
2780
+ },
2781
+ [buildClipOutputValues, syncClipOutputs]
2782
+ );
2783
+ const clearClipOutputs = useCallback(
2784
+ (clipId, options) => {
2785
+ if (!clipOutputValuesRef.current.has(clipId)) {
2786
+ return;
2787
+ }
2788
+ clipOutputValuesRef.current.delete(clipId);
2789
+ syncClipOutputs(options);
2790
+ },
2791
+ [syncClipOutputs]
2792
+ );
2793
+ const createClipPlaybackState = useCallback(
2794
+ (clip) => {
2795
+ const duration = resolveClipDurationSeconds(
2796
+ clip.clip
2797
+ );
2798
+ return {
2799
+ id: clip.id,
2800
+ time: 0,
2801
+ duration,
2802
+ speed: 1,
2803
+ weight: Number.isFinite(clip.weight) ? Number(clip.weight) : 1,
2804
+ loop: false,
2805
+ playing: false,
2806
+ resolve: null,
2807
+ completion: null
2808
+ };
1432
2809
  },
1433
2810
  []
1434
2811
  );
2812
+ const ensureClipPlaybackState = useCallback(
2813
+ (id) => {
2814
+ const clip = resolveClipById(id);
2815
+ if (!clip) {
2816
+ return null;
2817
+ }
2818
+ const existing = clipPlaybackRef.current.get(id);
2819
+ if (existing) {
2820
+ existing.duration = resolveClipDurationSeconds(
2821
+ clip.clip,
2822
+ existing.duration
2823
+ );
2824
+ existing.time = clampAnimationTime(existing.time, existing.duration);
2825
+ return { clip, state: existing };
2826
+ }
2827
+ const next = createClipPlaybackState(clip);
2828
+ clipPlaybackRef.current.set(id, next);
2829
+ return { clip, state: next };
2830
+ },
2831
+ [createClipPlaybackState, resolveClipById]
2832
+ );
1435
2833
  const advanceClipPlayback = useCallback(
1436
2834
  (dt) => {
1437
2835
  if (clipPlaybackRef.current.size === 0) {
1438
2836
  return;
1439
2837
  }
1440
- const map = clipPlaybackRef.current;
1441
2838
  const toDelete = [];
1442
- map.forEach((state, key) => {
1443
- state.time += dt * state.speed;
1444
- const clip = assetBundle.animations?.find(
1445
- (anim) => anim.id === state.id
1446
- );
2839
+ clipPlaybackRef.current.forEach((state, key) => {
2840
+ const clip = resolveClipById(state.id);
1447
2841
  if (!clip) {
1448
2842
  toDelete.push(key);
1449
- state.resolve();
2843
+ resolveClipPromise(state);
1450
2844
  return;
1451
2845
  }
1452
- const clipData = clip.clip;
1453
- const duration = Number(clipData?.duration ?? state.duration);
1454
- state.duration = Number.isFinite(duration) && duration > 0 ? duration : state.duration;
1455
- if (state.time >= state.duration) {
1456
- toDelete.push(key);
1457
- state.time = state.duration;
2846
+ state.duration = resolveClipDurationSeconds(
2847
+ clip.clip,
2848
+ state.duration
2849
+ );
2850
+ const { time, completed } = advanceClipTime(
2851
+ {
2852
+ time: state.time,
2853
+ duration: state.duration,
2854
+ speed: state.speed,
2855
+ loop: state.loop,
2856
+ playing: state.playing
2857
+ },
2858
+ dt
2859
+ );
2860
+ state.time = clampAnimationTime(time, state.duration);
2861
+ if (state.playing || completed) {
2862
+ writeClipOutputs(clip, state);
1458
2863
  }
1459
- const tracks = Array.isArray(clipData?.tracks) ? clipData.tracks : [];
1460
- tracks.forEach((track) => {
1461
- const value = sampleTrack(track, state.time) * state.weight;
1462
- const path = `animation/${clip.id}/${track.channel}`;
1463
- setInput(path, { float: value });
1464
- });
1465
- if (toDelete.includes(key)) {
1466
- state.resolve();
2864
+ if (completed) {
2865
+ toDelete.push(key);
2866
+ resolveClipPromise(state);
1467
2867
  }
1468
2868
  });
1469
2869
  toDelete.forEach((key) => {
1470
2870
  clipPlaybackRef.current.delete(key);
1471
- const clip = assetBundle.animations?.find((anim) => anim.id === key);
1472
- if (clip) {
1473
- const clipData = clip.clip;
1474
- const tracks = Array.isArray(clipData?.tracks) ? clipData.tracks : [];
1475
- tracks.forEach((track) => {
1476
- const path = `animation/${clip.id}/${track.channel}`;
1477
- setInput(path, { float: 0 });
1478
- });
2871
+ if (animationSystemActiveRef.current) {
2872
+ clearClipOutputs(key);
1479
2873
  }
1480
2874
  });
1481
2875
  },
1482
- [assetBundle.animations, sampleTrack, setInput]
2876
+ [clearClipOutputs, resolveClipById, resolveClipPromise, writeClipOutputs]
1483
2877
  );
1484
2878
  const animateValue = useCallback(
1485
2879
  (path, target, options) => {
2880
+ const targetValue = valueAsNumber2(target);
2881
+ const basePath = stripNamespace(
2882
+ normalisePath(path),
2883
+ namespaceRef.current
2884
+ );
2885
+ const poseValues = useLegacyPoseWeightFallback && targetValue != null ? poseWeightFallbackMap.get(basePath) : void 0;
2886
+ if (poseValues && targetValue != null) {
2887
+ const poseFaceId = assetBundle.pose?.config?.faceId ?? faceId ?? "face";
2888
+ const rigMap = rigInputMapRef.current;
2889
+ return Promise.all(
2890
+ Object.entries(poseValues).flatMap(([inputId, poseValue]) => {
2891
+ if (!Number.isFinite(poseValue)) {
2892
+ return [];
2893
+ }
2894
+ const controlPath = resolvePoseControlInputPath({
2895
+ inputId,
2896
+ basePath: buildRigInputPath(
2897
+ poseFaceId,
2898
+ `/pose/control/${inputId}`
2899
+ ),
2900
+ rigInputPathMap: rigMap,
2901
+ hasNativePoseControlInput: true
2902
+ }) ?? buildRigInputPath(poseFaceId, `/pose/control/${inputId}`);
2903
+ return [
2904
+ animateValue(
2905
+ controlPath,
2906
+ { float: Number(poseValue) * targetValue },
2907
+ options
2908
+ )
2909
+ ];
2910
+ })
2911
+ ).then(() => void 0);
2912
+ }
1486
2913
  const easing = resolveEasing(options?.easing);
1487
2914
  const duration = Math.max(0, options?.duration ?? DEFAULT_DURATION);
1488
2915
  cancelAnimation(path);
@@ -1496,7 +2923,9 @@ function VizijRuntimeProviderInner({
1496
2923
  }
1497
2924
  return new Promise((resolve) => {
1498
2925
  animationTweensRef.current.set(path, {
1499
- path: namespacedPath,
2926
+ // Keep the raw path here so tween updates go through setInput() once
2927
+ // and pick up the active namespace exactly once.
2928
+ path,
1500
2929
  from: fromValue,
1501
2930
  to: toValue,
1502
2931
  duration,
@@ -1507,54 +2936,377 @@ function VizijRuntimeProviderInner({
1507
2936
  markActivity();
1508
2937
  });
1509
2938
  },
1510
- [cancelAnimation, getPathSnapshot, markActivity, setInput]
2939
+ [
2940
+ assetBundle.pose?.config?.faceId,
2941
+ cancelAnimation,
2942
+ faceId,
2943
+ getPathSnapshot,
2944
+ markActivity,
2945
+ poseWeightFallbackMap,
2946
+ setInput,
2947
+ useLegacyPoseWeightFallback
2948
+ ]
1511
2949
  );
1512
2950
  const playAnimation = useCallback(
1513
2951
  (id, options) => {
1514
- const clip = assetBundle.animations?.find((anim) => anim.id === id);
1515
- if (!clip) {
2952
+ const ensured = ensureClipPlaybackState(id);
2953
+ if (!ensured) {
1516
2954
  return Promise.reject(
1517
2955
  new Error(`Animation ${id} is not part of the current asset bundle.`)
1518
2956
  );
1519
2957
  }
1520
- if (clipPlaybackRef.current.has(id)) {
1521
- clipPlaybackRef.current.delete(id);
2958
+ const { clip, state } = ensured;
2959
+ const shouldReset = options?.reset === true;
2960
+ if (shouldReset) {
2961
+ resolveClipPromise(state);
2962
+ state.time = 0;
1522
2963
  }
1523
- return new Promise((resolve) => {
1524
- const speed = options?.speed ?? 1;
1525
- const weight = options?.weight ?? clip.weight ?? 1;
1526
- const clipData = clip.clip;
1527
- clipPlaybackRef.current.set(id, {
1528
- id,
1529
- time: options?.reset ? 0 : 0,
1530
- duration: Number(clipData?.duration ?? 0),
1531
- speed: Number.isFinite(speed) && speed > 0 ? speed : 1,
1532
- weight,
1533
- resolve
1534
- });
1535
- markActivity();
1536
- });
2964
+ const speed = options?.speed ?? state.speed ?? 1;
2965
+ const weight = options?.weight ?? state.weight ?? clip.weight ?? 1;
2966
+ state.speed = Number.isFinite(speed) && speed > 0 ? speed : 1;
2967
+ state.weight = Number.isFinite(weight) ? Number(weight) : 1;
2968
+ state.duration = resolveClipDurationSeconds(
2969
+ clip.clip,
2970
+ state.duration
2971
+ );
2972
+ state.time = clampAnimationTime(state.time, state.duration);
2973
+ state.playing = true;
2974
+ const completion = ensureClipPromise(state);
2975
+ clipPlaybackRef.current.set(id, state);
2976
+ writeClipOutputs(clip, state);
2977
+ markActivity();
2978
+ return completion;
1537
2979
  },
1538
- [assetBundle.animations, markActivity]
2980
+ [
2981
+ ensureClipPlaybackState,
2982
+ ensureClipPromise,
2983
+ markActivity,
2984
+ resolveClipPromise,
2985
+ writeClipOutputs
2986
+ ]
1539
2987
  );
1540
- const stopAnimation = useCallback(
2988
+ const pauseAnimation = useCallback(
1541
2989
  (id) => {
1542
- const clip = assetBundle.animations?.find((anim) => anim.id === id);
2990
+ const state = clipPlaybackRef.current.get(id);
2991
+ if (!state || !state.playing) {
2992
+ return;
2993
+ }
2994
+ state.playing = false;
2995
+ updateLoopMode();
2996
+ },
2997
+ [updateLoopMode]
2998
+ );
2999
+ const seekAnimation = useCallback(
3000
+ (id, timeSeconds) => {
3001
+ const ensured = ensureClipPlaybackState(id);
3002
+ if (!ensured) {
3003
+ return;
3004
+ }
3005
+ const { clip, state } = ensured;
3006
+ state.time = clampAnimationTime(timeSeconds, state.duration);
3007
+ clipPlaybackRef.current.set(id, state);
3008
+ writeClipOutputs(clip, state, { immediate: true });
3009
+ },
3010
+ [ensureClipPlaybackState, writeClipOutputs]
3011
+ );
3012
+ const setAnimationLoop = useCallback(
3013
+ (id, enabled) => {
3014
+ const ensured = ensureClipPlaybackState(id);
3015
+ if (!ensured) {
3016
+ return;
3017
+ }
3018
+ ensured.state.loop = Boolean(enabled);
3019
+ clipPlaybackRef.current.set(id, ensured.state);
3020
+ updateLoopMode();
3021
+ },
3022
+ [ensureClipPlaybackState, updateLoopMode]
3023
+ );
3024
+ const getAnimationState = useCallback(
3025
+ (id) => {
3026
+ const state = clipPlaybackRef.current.get(id);
3027
+ if (!state) {
3028
+ return null;
3029
+ }
3030
+ return {
3031
+ time: state.time,
3032
+ duration: state.duration,
3033
+ playing: state.playing,
3034
+ loop: state.loop,
3035
+ speed: state.speed
3036
+ };
3037
+ },
3038
+ []
3039
+ );
3040
+ const stopAnimation = useCallback(
3041
+ (id, options) => {
1543
3042
  const state = clipPlaybackRef.current.get(id);
1544
3043
  if (state) {
1545
3044
  clipPlaybackRef.current.delete(id);
1546
- state.resolve();
3045
+ state.playing = false;
3046
+ resolveClipPromise(state);
1547
3047
  }
1548
- if (clip) {
1549
- const clipData = clip.clip;
1550
- const tracks = Array.isArray(clipData?.tracks) ? clipData.tracks : [];
1551
- tracks.forEach((track) => {
1552
- const path = `animation/${clip.id}/${track.channel}`;
1553
- setInput(path, { float: 0 });
3048
+ if (options?.clearOutputs !== false) {
3049
+ clearClipOutputs(id);
3050
+ }
3051
+ updateLoopMode();
3052
+ },
3053
+ [clearClipOutputs, resolveClipPromise, updateLoopMode]
3054
+ );
3055
+ const refreshControllerStatus = useCallback(() => {
3056
+ const controllers = listControllers();
3057
+ reportStatus((prev) => ({
3058
+ ...prev,
3059
+ controllers,
3060
+ outputPaths: Array.from(outputPathsRef.current)
3061
+ }));
3062
+ onRegisterControllers?.(controllers);
3063
+ }, [listControllers, onRegisterControllers, reportStatus]);
3064
+ const resolveProgramById = useCallback(
3065
+ (id) => {
3066
+ return resolvedProgramAssets.find((program) => program.id === id);
3067
+ },
3068
+ [resolvedProgramAssets]
3069
+ );
3070
+ const buildProgramRegistrationConfig = useCallback(
3071
+ (program) => {
3072
+ const graphSpec = resolveGraphSpec(
3073
+ program.graph,
3074
+ `${program.id ?? "program"} graph`
3075
+ );
3076
+ if (!graphSpec) {
3077
+ return null;
3078
+ }
3079
+ const outputs = collectOutputPaths(graphSpec);
3080
+ const inputs = collectInputPaths(graphSpec);
3081
+ const subs = program.graph.subscriptions ?? {
3082
+ inputs,
3083
+ outputs
3084
+ };
3085
+ return {
3086
+ id: namespaceControllerId(program.id, namespace, "graph"),
3087
+ spec: stripNulls(namespaceGraphSpec(graphSpec, namespace)),
3088
+ subs: namespaceSubscriptions(subs, namespace)
3089
+ };
3090
+ },
3091
+ [namespace]
3092
+ );
3093
+ const deriveProgramResetValues = useCallback(
3094
+ (program) => {
3095
+ if (program.resetValues) {
3096
+ return Object.entries(program.resetValues).filter(([, value]) => Number.isFinite(value)).map(([path, value]) => ({ path, value }));
3097
+ }
3098
+ const graphSpec = resolveGraphSpec(
3099
+ program.graph,
3100
+ `${program.id ?? "program"} graph (reset)`
3101
+ );
3102
+ if (!graphSpec) {
3103
+ return [];
3104
+ }
3105
+ return collectOutputPaths(graphSpec).filter((path) => path.trim().length > 0).map((path) => {
3106
+ const defaultValue = inputConstraintsRef.current[path]?.defaultValue ?? 0;
3107
+ return {
3108
+ path,
3109
+ value: Number.isFinite(defaultValue) && defaultValue != null ? defaultValue : 0
3110
+ };
3111
+ });
3112
+ },
3113
+ []
3114
+ );
3115
+ const syncProgramPlaybackControllers = useCallback(() => {
3116
+ if (!ready) {
3117
+ return;
3118
+ }
3119
+ const availableProgramIds = new Set(
3120
+ resolvedProgramAssets.map((program) => program.id)
3121
+ );
3122
+ Array.from(programPlaybackRef.current.keys()).forEach((id) => {
3123
+ if (availableProgramIds.has(id)) {
3124
+ return;
3125
+ }
3126
+ programPlaybackRef.current.delete(id);
3127
+ const controllerId = programControllerIdsRef.current.get(id);
3128
+ if (controllerId) {
3129
+ try {
3130
+ removeGraph(controllerId);
3131
+ } catch (err) {
3132
+ pushError({
3133
+ message: `Failed to remove program ${id}`,
3134
+ cause: err,
3135
+ phase: "registration",
3136
+ timestamp: performance.now()
3137
+ });
3138
+ }
3139
+ programControllerIdsRef.current.delete(id);
3140
+ }
3141
+ });
3142
+ programPlaybackRef.current.forEach((state, id) => {
3143
+ const program = resolveProgramById(id);
3144
+ const controllerId = programControllerIdsRef.current.get(id);
3145
+ if (!program) {
3146
+ return;
3147
+ }
3148
+ if (state.state !== "playing") {
3149
+ if (!controllerId) {
3150
+ return;
3151
+ }
3152
+ try {
3153
+ removeGraph(controllerId);
3154
+ } catch (err) {
3155
+ pushError({
3156
+ message: `Failed to pause program ${id}`,
3157
+ cause: err,
3158
+ phase: "registration",
3159
+ timestamp: performance.now()
3160
+ });
3161
+ }
3162
+ programControllerIdsRef.current.delete(id);
3163
+ return;
3164
+ }
3165
+ if (controllerId) {
3166
+ return;
3167
+ }
3168
+ const config = buildProgramRegistrationConfig(program);
3169
+ if (!config) {
3170
+ pushError({
3171
+ message: `Program ${id} is missing a usable graph payload.`,
3172
+ phase: "registration",
3173
+ timestamp: performance.now()
3174
+ });
3175
+ return;
3176
+ }
3177
+ try {
3178
+ const nextControllerId = registerGraph(config);
3179
+ programControllerIdsRef.current.set(id, nextControllerId);
3180
+ } catch (err) {
3181
+ pushError({
3182
+ message: `Failed to register program ${id}`,
3183
+ cause: err,
3184
+ phase: "registration",
3185
+ timestamp: performance.now()
1554
3186
  });
1555
3187
  }
3188
+ });
3189
+ refreshControllerStatus();
3190
+ }, [
3191
+ buildProgramRegistrationConfig,
3192
+ pushError,
3193
+ ready,
3194
+ refreshControllerStatus,
3195
+ registerGraph,
3196
+ removeGraph,
3197
+ resolvedProgramAssets,
3198
+ resolveProgramById
3199
+ ]);
3200
+ const playProgram = useCallback(
3201
+ (id) => {
3202
+ if (!resolveProgramById(id)) {
3203
+ throw new Error(
3204
+ `Program ${id} is not part of the current asset bundle.`
3205
+ );
3206
+ }
3207
+ programPlaybackRef.current.set(id, {
3208
+ id,
3209
+ state: "playing"
3210
+ });
3211
+ syncProgramPlaybackControllers();
3212
+ markActivity();
1556
3213
  },
1557
- [assetBundle.animations, setInput]
3214
+ [markActivity, resolveProgramById, syncProgramPlaybackControllers]
3215
+ );
3216
+ const pauseProgram = useCallback(
3217
+ (id) => {
3218
+ if (!resolveProgramById(id)) {
3219
+ return;
3220
+ }
3221
+ programPlaybackRef.current.set(id, {
3222
+ id,
3223
+ state: "paused"
3224
+ });
3225
+ syncProgramPlaybackControllers();
3226
+ updateLoopMode();
3227
+ },
3228
+ [resolveProgramById, syncProgramPlaybackControllers, updateLoopMode]
3229
+ );
3230
+ const stopProgram = useCallback(
3231
+ (id, options) => {
3232
+ const program = resolveProgramById(id);
3233
+ const controllerId = programControllerIdsRef.current.get(id);
3234
+ if (controllerId) {
3235
+ try {
3236
+ removeGraph(controllerId);
3237
+ } catch (err) {
3238
+ pushError({
3239
+ message: `Failed to stop program ${id}`,
3240
+ cause: err,
3241
+ phase: "registration",
3242
+ timestamp: performance.now()
3243
+ });
3244
+ }
3245
+ programControllerIdsRef.current.delete(id);
3246
+ }
3247
+ programPlaybackRef.current.set(id, {
3248
+ id,
3249
+ state: "stopped"
3250
+ });
3251
+ if (program && options?.resetOutputs !== false) {
3252
+ deriveProgramResetValues(program).forEach(({ path, value }) => {
3253
+ setInput(path, { float: value });
3254
+ });
3255
+ }
3256
+ refreshControllerStatus();
3257
+ updateLoopMode();
3258
+ },
3259
+ [
3260
+ deriveProgramResetValues,
3261
+ pushError,
3262
+ refreshControllerStatus,
3263
+ removeGraph,
3264
+ resolveProgramById,
3265
+ setInput,
3266
+ updateLoopMode
3267
+ ]
3268
+ );
3269
+ const getProgramState = useCallback(
3270
+ (id) => {
3271
+ const state = programPlaybackRef.current.get(id);
3272
+ if (!state) {
3273
+ return null;
3274
+ }
3275
+ return { state: state.state };
3276
+ },
3277
+ []
3278
+ );
3279
+ useEffect(() => {
3280
+ if (!ready || status.loading) {
3281
+ return;
3282
+ }
3283
+ syncProgramPlaybackControllers();
3284
+ }, [
3285
+ graphUpdateToken,
3286
+ ready,
3287
+ resolvedProgramAssets,
3288
+ status.loading,
3289
+ syncProgramPlaybackControllers
3290
+ ]);
3291
+ const setAnimationActive = useCallback(
3292
+ (active) => {
3293
+ const next = Boolean(active);
3294
+ if (animationSystemActiveRef.current === next) {
3295
+ return;
3296
+ }
3297
+ animationSystemActiveRef.current = next;
3298
+ if (!next) {
3299
+ clipPlaybackRef.current.forEach((state) => {
3300
+ state.playing = false;
3301
+ });
3302
+ }
3303
+ updateLoopMode();
3304
+ },
3305
+ [updateLoopMode]
3306
+ );
3307
+ const isAnimationActive = useCallback(
3308
+ () => animationSystemActiveRef.current,
3309
+ []
1558
3310
  );
1559
3311
  const registerInputDriver = useCallback(
1560
3312
  (id, factory) => {
@@ -1695,6 +3447,10 @@ function VizijRuntimeProviderInner({
1695
3447
  return () => {
1696
3448
  animationTweensRef.current.clear();
1697
3449
  clipPlaybackRef.current.clear();
3450
+ programPlaybackRef.current.clear();
3451
+ programControllerIdsRef.current.clear();
3452
+ clipOutputValuesRef.current.clear();
3453
+ clipAggregateValuesRef.current.clear();
1698
3454
  };
1699
3455
  }, []);
1700
3456
  useEffect(() => {
@@ -1710,18 +3466,61 @@ function VizijRuntimeProviderInner({
1710
3466
  }, 500);
1711
3467
  return () => window.clearInterval(id);
1712
3468
  }, [reportStatus]);
3469
+ const setGraphBundle = useCallback(
3470
+ (bundle, options) => {
3471
+ const baseAssetBundle = latestEffectiveAssetBundleRef.current;
3472
+ const nextAssetBundle = applyRuntimeGraphBundle(baseAssetBundle, bundle);
3473
+ const plan = resolveRuntimeUpdatePlan(
3474
+ baseAssetBundle,
3475
+ nextAssetBundle,
3476
+ options?.tier ?? updateTierRef.current
3477
+ );
3478
+ pendingPlanRef.current = plan;
3479
+ previousBundleRef.current = nextAssetBundle;
3480
+ latestEffectiveAssetBundleRef.current = nextAssetBundle;
3481
+ suppressNextBundlePlanRef.current = true;
3482
+ setAssetBundleOverride(nextAssetBundle);
3483
+ if (plan.reregisterGraphs) {
3484
+ setGraphUpdateToken((prev) => prev + 1);
3485
+ }
3486
+ if (plan.reloadAssets) {
3487
+ reportStatus((prev) => ({
3488
+ ...prev,
3489
+ loading: true,
3490
+ ready: false
3491
+ }));
3492
+ } else {
3493
+ reportStatus((prev) => ({
3494
+ ...prev,
3495
+ loading: false
3496
+ }));
3497
+ }
3498
+ },
3499
+ [reportStatus]
3500
+ );
1713
3501
  const contextValue = useMemo(
1714
3502
  () => ({
1715
3503
  ...status,
1716
3504
  assetBundle,
1717
3505
  setInput,
3506
+ setGraphBundle,
1718
3507
  setValue: setRendererValue,
1719
3508
  stagePoseNeutral,
1720
3509
  animateValue,
1721
3510
  cancelAnimation,
1722
3511
  registerInputDriver,
1723
3512
  playAnimation,
3513
+ pauseAnimation,
3514
+ seekAnimation,
3515
+ setAnimationLoop,
3516
+ getAnimationState,
1724
3517
  stopAnimation,
3518
+ playProgram,
3519
+ pauseProgram,
3520
+ stopProgram,
3521
+ getProgramState,
3522
+ setAnimationActive,
3523
+ isAnimationActive,
1725
3524
  step,
1726
3525
  advanceAnimations,
1727
3526
  inputConstraints
@@ -1730,13 +3529,24 @@ function VizijRuntimeProviderInner({
1730
3529
  status,
1731
3530
  assetBundle,
1732
3531
  setInput,
3532
+ setGraphBundle,
1733
3533
  setRendererValue,
1734
3534
  stagePoseNeutral,
1735
3535
  animateValue,
1736
3536
  cancelAnimation,
1737
3537
  registerInputDriver,
1738
3538
  playAnimation,
3539
+ pauseAnimation,
3540
+ seekAnimation,
3541
+ setAnimationLoop,
3542
+ getAnimationState,
1739
3543
  stopAnimation,
3544
+ playProgram,
3545
+ pauseProgram,
3546
+ stopProgram,
3547
+ getProgramState,
3548
+ setAnimationActive,
3549
+ isAnimationActive,
1740
3550
  step,
1741
3551
  advanceAnimations,
1742
3552
  inputConstraints
@@ -1782,17 +3592,23 @@ function VizijRuntimeFaceInner({
1782
3592
  }
1783
3593
  var VizijRuntimeFace = memo(VizijRuntimeFaceInner);
1784
3594
 
3595
+ // src/hooks/useOptionalVizijRuntime.ts
3596
+ import { useContext as useContext3 } from "react";
3597
+ function useOptionalVizijRuntime() {
3598
+ return useContext3(VizijRuntimeContext);
3599
+ }
3600
+
1785
3601
  // src/hooks/useVizijOutputs.ts
1786
3602
  import {
1787
3603
  useVizijStore
1788
3604
  } from "@vizij/render";
1789
- import { getLookup } from "@vizij/utils";
3605
+ import { getLookup as getLookup2 } from "@vizij/utils";
1790
3606
  function useVizijOutputs(paths) {
1791
3607
  const { namespace } = useVizijRuntime();
1792
3608
  return useVizijStore((state) => {
1793
3609
  const result = {};
1794
3610
  paths.forEach((path) => {
1795
- const lookup = getLookup(namespace, path);
3611
+ const lookup = getLookup2(namespace, path);
1796
3612
  result[path] = state.values.get(lookup);
1797
3613
  });
1798
3614
  return result;
@@ -1804,11 +3620,11 @@ import { useCallback as useCallback2 } from "react";
1804
3620
  import {
1805
3621
  useVizijStore as useVizijStore2
1806
3622
  } from "@vizij/render";
1807
- import { getLookup as getLookup2 } from "@vizij/utils";
3623
+ import { getLookup as getLookup3 } from "@vizij/utils";
1808
3624
  function useRigInput(path) {
1809
3625
  const { namespace, setInput } = useVizijRuntime();
1810
3626
  const value = useVizijStore2((state) => {
1811
- return state.values.get(getLookup2(namespace, path));
3627
+ return state.values.get(getLookup3(namespace, path));
1812
3628
  });
1813
3629
  const setter = useCallback2(
1814
3630
  (next, shape) => {
@@ -1818,9 +3634,422 @@ function useRigInput(path) {
1818
3634
  );
1819
3635
  return [value, setter];
1820
3636
  }
3637
+
3638
+ // src/utils/faceControls.ts
3639
+ var DEFAULT_GAZE_CONTROL = {
3640
+ min: -1,
3641
+ max: 1,
3642
+ defaultValue: 0
3643
+ };
3644
+ var DEFAULT_BLINK_CONTROL = {
3645
+ min: 0,
3646
+ max: 1,
3647
+ defaultValue: 0
3648
+ };
3649
+ var STANDARD_VIZIJ_EYE_PATHS = {
3650
+ leftX: "/standard/vizij/left_eye/pos/x",
3651
+ leftY: "/standard/vizij/left_eye/pos/y",
3652
+ rightX: "/standard/vizij/right_eye/pos/x",
3653
+ rightY: "/standard/vizij/right_eye/pos/y",
3654
+ leftUpper: "/standard/vizij/left_eye_top_eyelid/pos/y",
3655
+ rightUpper: "/standard/vizij/right_eye_top_eyelid/pos/y"
3656
+ };
3657
+ var LEGACY_STANDARD_EYE_PATHS = {
3658
+ leftX: "/standard/left_eye/pos/x",
3659
+ leftY: "/standard/left_eye/pos/y",
3660
+ rightX: "/standard/right_eye/pos/x",
3661
+ rightY: "/standard/right_eye/pos/y",
3662
+ leftUpper: "/standard/left_eye_top_eyelid/pos/y",
3663
+ rightUpper: "/standard/right_eye_top_eyelid/pos/y"
3664
+ };
3665
+ var PROPSRIG_EYE_PATHS = {
3666
+ leftX: "/propsrig/l_eye/translation/x",
3667
+ leftY: "/propsrig/l_eye/translation/y",
3668
+ rightX: "/propsrig/r_eye/translation/x",
3669
+ rightY: "/propsrig/r_eye/translation/y"
3670
+ };
3671
+ function clamp(value, min, max) {
3672
+ return Math.min(Math.max(value, min), max);
3673
+ }
3674
+ function normalizeInputPath(path) {
3675
+ const trimmed = path?.trim() ?? "";
3676
+ if (!trimmed) {
3677
+ return null;
3678
+ }
3679
+ if (trimmed.startsWith("/")) {
3680
+ return trimmed;
3681
+ }
3682
+ const rigMatch = trimmed.match(/^rig\/[^/]+(\/.*)$/);
3683
+ if (rigMatch?.[1]) {
3684
+ return rigMatch[1];
3685
+ }
3686
+ return `/${trimmed}`;
3687
+ }
3688
+ function buildControlsLookup(assetBundle) {
3689
+ const metadata = assetBundle.rig?.inputMetadata ?? [];
3690
+ const metadataByPath = /* @__PURE__ */ new Map();
3691
+ const pathSet = /* @__PURE__ */ new Set();
3692
+ metadata.forEach((entry) => {
3693
+ const normalized = normalizeInputPath(entry.path);
3694
+ if (!normalized) {
3695
+ return;
3696
+ }
3697
+ pathSet.add(normalized);
3698
+ if (!metadataByPath.has(normalized)) {
3699
+ metadataByPath.set(normalized, entry);
3700
+ }
3701
+ });
3702
+ return { pathSet, metadataByPath };
3703
+ }
3704
+ function resolveConstraint(absolutePath, relativePath, inputConstraints) {
3705
+ if (!inputConstraints) {
3706
+ return null;
3707
+ }
3708
+ const normalizedRelative = relativePath.replace(/^\//, "");
3709
+ const candidates = [
3710
+ absolutePath,
3711
+ relativePath,
3712
+ normalizedRelative,
3713
+ absolutePath.replace(/^rig\/[^/]+\//, "")
3714
+ ];
3715
+ for (const candidate of candidates) {
3716
+ const constraint = inputConstraints[candidate];
3717
+ if (constraint) {
3718
+ return constraint;
3719
+ }
3720
+ }
3721
+ return null;
3722
+ }
3723
+ function createControl(faceId, lookup, relativePath, inputConstraints, fallback) {
3724
+ const normalizedRelative = normalizeInputPath(relativePath);
3725
+ if (!normalizedRelative || !lookup.pathSet.has(normalizedRelative)) {
3726
+ return null;
3727
+ }
3728
+ const absolutePath = buildRigInputPath(faceId, normalizedRelative);
3729
+ const metadata = lookup.metadataByPath.get(normalizedRelative);
3730
+ const constraint = resolveConstraint(
3731
+ absolutePath,
3732
+ normalizedRelative,
3733
+ inputConstraints
3734
+ );
3735
+ const min = Number.isFinite(Number(constraint?.min ?? metadata?.range?.min)) ? Number(constraint?.min ?? metadata?.range?.min) : fallback.min;
3736
+ const max = Number.isFinite(Number(constraint?.max ?? metadata?.range?.max)) ? Number(constraint?.max ?? metadata?.range?.max) : fallback.max;
3737
+ const midpoint = min + (max - min) / 2;
3738
+ const defaultValue = Number.isFinite(
3739
+ Number(constraint?.defaultValue ?? metadata?.defaultValue)
3740
+ ) ? Number(constraint?.defaultValue ?? metadata?.defaultValue) : Number.isFinite(midpoint) ? midpoint : fallback.defaultValue;
3741
+ return {
3742
+ path: absolutePath,
3743
+ min,
3744
+ max,
3745
+ defaultValue
3746
+ };
3747
+ }
3748
+ function hasAll(controls, keys) {
3749
+ return keys.every((key) => Boolean(controls[key]));
3750
+ }
3751
+ function resolveFaceControls(assetBundle, runtimeFaceId, inputConstraints) {
3752
+ const faceId = assetBundle.faceId ?? assetBundle.pose?.config?.faceId ?? runtimeFaceId ?? "face";
3753
+ const lookup = buildControlsLookup(assetBundle);
3754
+ const standardVizijControls = {
3755
+ leftX: createControl(
3756
+ faceId,
3757
+ lookup,
3758
+ STANDARD_VIZIJ_EYE_PATHS.leftX,
3759
+ inputConstraints,
3760
+ DEFAULT_GAZE_CONTROL
3761
+ ),
3762
+ leftY: createControl(
3763
+ faceId,
3764
+ lookup,
3765
+ STANDARD_VIZIJ_EYE_PATHS.leftY,
3766
+ inputConstraints,
3767
+ DEFAULT_GAZE_CONTROL
3768
+ ),
3769
+ rightX: createControl(
3770
+ faceId,
3771
+ lookup,
3772
+ STANDARD_VIZIJ_EYE_PATHS.rightX,
3773
+ inputConstraints,
3774
+ DEFAULT_GAZE_CONTROL
3775
+ ),
3776
+ rightY: createControl(
3777
+ faceId,
3778
+ lookup,
3779
+ STANDARD_VIZIJ_EYE_PATHS.rightY,
3780
+ inputConstraints,
3781
+ DEFAULT_GAZE_CONTROL
3782
+ ),
3783
+ leftUpper: createControl(
3784
+ faceId,
3785
+ lookup,
3786
+ STANDARD_VIZIJ_EYE_PATHS.leftUpper,
3787
+ inputConstraints,
3788
+ DEFAULT_GAZE_CONTROL
3789
+ ),
3790
+ rightUpper: createControl(
3791
+ faceId,
3792
+ lookup,
3793
+ STANDARD_VIZIJ_EYE_PATHS.rightUpper,
3794
+ inputConstraints,
3795
+ DEFAULT_GAZE_CONTROL
3796
+ )
3797
+ };
3798
+ const legacyStandardControls = {
3799
+ leftX: createControl(
3800
+ faceId,
3801
+ lookup,
3802
+ LEGACY_STANDARD_EYE_PATHS.leftX,
3803
+ inputConstraints,
3804
+ DEFAULT_GAZE_CONTROL
3805
+ ),
3806
+ leftY: createControl(
3807
+ faceId,
3808
+ lookup,
3809
+ LEGACY_STANDARD_EYE_PATHS.leftY,
3810
+ inputConstraints,
3811
+ DEFAULT_GAZE_CONTROL
3812
+ ),
3813
+ rightX: createControl(
3814
+ faceId,
3815
+ lookup,
3816
+ LEGACY_STANDARD_EYE_PATHS.rightX,
3817
+ inputConstraints,
3818
+ DEFAULT_GAZE_CONTROL
3819
+ ),
3820
+ rightY: createControl(
3821
+ faceId,
3822
+ lookup,
3823
+ LEGACY_STANDARD_EYE_PATHS.rightY,
3824
+ inputConstraints,
3825
+ DEFAULT_GAZE_CONTROL
3826
+ ),
3827
+ leftUpper: createControl(
3828
+ faceId,
3829
+ lookup,
3830
+ LEGACY_STANDARD_EYE_PATHS.leftUpper,
3831
+ inputConstraints,
3832
+ DEFAULT_GAZE_CONTROL
3833
+ ),
3834
+ rightUpper: createControl(
3835
+ faceId,
3836
+ lookup,
3837
+ LEGACY_STANDARD_EYE_PATHS.rightUpper,
3838
+ inputConstraints,
3839
+ DEFAULT_GAZE_CONTROL
3840
+ )
3841
+ };
3842
+ const propsrigControls = {
3843
+ leftX: createControl(
3844
+ faceId,
3845
+ lookup,
3846
+ PROPSRIG_EYE_PATHS.leftX,
3847
+ inputConstraints,
3848
+ DEFAULT_GAZE_CONTROL
3849
+ ),
3850
+ leftY: createControl(
3851
+ faceId,
3852
+ lookup,
3853
+ PROPSRIG_EYE_PATHS.leftY,
3854
+ inputConstraints,
3855
+ DEFAULT_GAZE_CONTROL
3856
+ ),
3857
+ rightX: createControl(
3858
+ faceId,
3859
+ lookup,
3860
+ PROPSRIG_EYE_PATHS.rightX,
3861
+ inputConstraints,
3862
+ DEFAULT_GAZE_CONTROL
3863
+ ),
3864
+ rightY: createControl(
3865
+ faceId,
3866
+ lookup,
3867
+ PROPSRIG_EYE_PATHS.rightY,
3868
+ inputConstraints,
3869
+ DEFAULT_GAZE_CONTROL
3870
+ )
3871
+ };
3872
+ const coupledGazeControls = {
3873
+ leftX: createControl(
3874
+ faceId,
3875
+ lookup,
3876
+ "/gaze/left_right",
3877
+ inputConstraints,
3878
+ DEFAULT_GAZE_CONTROL
3879
+ ),
3880
+ leftY: createControl(
3881
+ faceId,
3882
+ lookup,
3883
+ "/gaze/up_down",
3884
+ inputConstraints,
3885
+ DEFAULT_GAZE_CONTROL
3886
+ ),
3887
+ rightX: createControl(
3888
+ faceId,
3889
+ lookup,
3890
+ "/gaze/left_right_copy",
3891
+ inputConstraints,
3892
+ DEFAULT_GAZE_CONTROL
3893
+ ) ?? createControl(
3894
+ faceId,
3895
+ lookup,
3896
+ "/gaze/left_right",
3897
+ inputConstraints,
3898
+ DEFAULT_GAZE_CONTROL
3899
+ ),
3900
+ rightY: createControl(
3901
+ faceId,
3902
+ lookup,
3903
+ "/gaze/up_down_copy",
3904
+ inputConstraints,
3905
+ DEFAULT_GAZE_CONTROL
3906
+ ) ?? createControl(
3907
+ faceId,
3908
+ lookup,
3909
+ "/gaze/up_down",
3910
+ inputConstraints,
3911
+ DEFAULT_GAZE_CONTROL
3912
+ )
3913
+ };
3914
+ const blinkFromLids = createControl(
3915
+ faceId,
3916
+ lookup,
3917
+ "/lids/blink",
3918
+ inputConstraints,
3919
+ DEFAULT_BLINK_CONTROL
3920
+ );
3921
+ const blinkDirect = createControl(
3922
+ faceId,
3923
+ lookup,
3924
+ "/blink",
3925
+ inputConstraints,
3926
+ DEFAULT_BLINK_CONTROL
3927
+ );
3928
+ if (hasAll(standardVizijControls, ["leftX", "leftY", "rightX", "rightY"])) {
3929
+ return {
3930
+ faceId,
3931
+ gazeSource: "standard-vizij",
3932
+ blinkSource: blinkFromLids ? "lids" : blinkDirect ? "blink" : "none",
3933
+ eyes: {
3934
+ leftX: standardVizijControls.leftX,
3935
+ leftY: standardVizijControls.leftY,
3936
+ rightX: standardVizijControls.rightX,
3937
+ rightY: standardVizijControls.rightY
3938
+ },
3939
+ eyelids: {
3940
+ leftUpper: standardVizijControls.leftUpper,
3941
+ rightUpper: standardVizijControls.rightUpper
3942
+ },
3943
+ blink: blinkFromLids ?? blinkDirect
3944
+ };
3945
+ }
3946
+ if (hasAll(legacyStandardControls, ["leftX", "leftY", "rightX", "rightY"])) {
3947
+ return {
3948
+ faceId,
3949
+ gazeSource: "standard",
3950
+ blinkSource: blinkDirect ? "blink" : blinkFromLids ? "lids" : "none",
3951
+ eyes: {
3952
+ leftX: legacyStandardControls.leftX,
3953
+ leftY: legacyStandardControls.leftY,
3954
+ rightX: legacyStandardControls.rightX,
3955
+ rightY: legacyStandardControls.rightY
3956
+ },
3957
+ eyelids: {
3958
+ leftUpper: legacyStandardControls.leftUpper,
3959
+ rightUpper: legacyStandardControls.rightUpper
3960
+ },
3961
+ blink: blinkDirect ?? blinkFromLids
3962
+ };
3963
+ }
3964
+ if (hasAll(propsrigControls, ["leftX", "leftY", "rightX", "rightY"])) {
3965
+ return {
3966
+ faceId,
3967
+ gazeSource: "propsrig",
3968
+ blinkSource: blinkDirect ? "blink" : blinkFromLids ? "lids" : "none",
3969
+ eyes: {
3970
+ leftX: propsrigControls.leftX,
3971
+ leftY: propsrigControls.leftY,
3972
+ rightX: propsrigControls.rightX,
3973
+ rightY: propsrigControls.rightY
3974
+ },
3975
+ eyelids: {
3976
+ leftUpper: null,
3977
+ rightUpper: null
3978
+ },
3979
+ blink: blinkDirect ?? blinkFromLids
3980
+ };
3981
+ }
3982
+ if (hasAll(coupledGazeControls, ["leftX", "leftY", "rightX", "rightY"])) {
3983
+ return {
3984
+ faceId,
3985
+ gazeSource: "coupled-gaze",
3986
+ blinkSource: blinkFromLids ? "lids" : blinkDirect ? "blink" : "none",
3987
+ eyes: {
3988
+ leftX: coupledGazeControls.leftX,
3989
+ leftY: coupledGazeControls.leftY,
3990
+ rightX: coupledGazeControls.rightX,
3991
+ rightY: coupledGazeControls.rightY
3992
+ },
3993
+ eyelids: {
3994
+ leftUpper: null,
3995
+ rightUpper: null
3996
+ },
3997
+ blink: blinkFromLids ?? blinkDirect
3998
+ };
3999
+ }
4000
+ return {
4001
+ faceId,
4002
+ gazeSource: "none",
4003
+ blinkSource: blinkFromLids ? "lids" : blinkDirect ? "blink" : "none",
4004
+ eyes: {
4005
+ leftX: null,
4006
+ leftY: null,
4007
+ rightX: null,
4008
+ rightY: null
4009
+ },
4010
+ eyelids: {
4011
+ leftUpper: null,
4012
+ rightUpper: null
4013
+ },
4014
+ blink: blinkFromLids ?? blinkDirect
4015
+ };
4016
+ }
4017
+ function mapNormalizedControlValue(control, normalizedValue) {
4018
+ const value = clamp(normalizedValue, -1, 1);
4019
+ if (value === 0) {
4020
+ return control.defaultValue;
4021
+ }
4022
+ if (value > 0) {
4023
+ return control.defaultValue + (control.max - control.defaultValue) * value;
4024
+ }
4025
+ return control.defaultValue + (control.defaultValue - control.min) * value;
4026
+ }
4027
+ function mapUnitControlValue(control, unitValue) {
4028
+ const value = clamp(unitValue, 0, 1);
4029
+ return control.defaultValue + (control.max - control.defaultValue) * value;
4030
+ }
1821
4031
  export {
4032
+ EMOTION_POSE_KEYS,
4033
+ EXPRESSIVE_EMOTION_POSE_KEYS,
4034
+ POSE_WEIGHT_INPUT_PATH_PREFIX,
4035
+ VISEME_POSE_KEYS,
1822
4036
  VizijRuntimeFace,
1823
4037
  VizijRuntimeProvider,
4038
+ buildPoseWeightInputPathSegment,
4039
+ buildPoseWeightPathMap,
4040
+ buildPoseWeightRelativePath,
4041
+ buildRigInputPath,
4042
+ buildSemanticPoseWeightPathMap,
4043
+ filterPosesBySemanticKind,
4044
+ getPoseSemanticKey,
4045
+ mapNormalizedControlValue,
4046
+ mapUnitControlValue,
4047
+ normalizePoseSemanticKey,
4048
+ resolveFaceControls,
4049
+ resolvePoseMembership,
4050
+ resolvePoseSemantics,
4051
+ resolveRuntimeUpdatePlan,
4052
+ useOptionalVizijRuntime,
1824
4053
  useRigInput,
1825
4054
  useVizijOutputs,
1826
4055
  useVizijRuntime