@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.cjs CHANGED
@@ -20,8 +20,27 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ EMOTION_POSE_KEYS: () => EMOTION_POSE_KEYS,
24
+ EXPRESSIVE_EMOTION_POSE_KEYS: () => EXPRESSIVE_EMOTION_POSE_KEYS,
25
+ POSE_WEIGHT_INPUT_PATH_PREFIX: () => POSE_WEIGHT_INPUT_PATH_PREFIX,
26
+ VISEME_POSE_KEYS: () => VISEME_POSE_KEYS,
23
27
  VizijRuntimeFace: () => VizijRuntimeFace,
24
28
  VizijRuntimeProvider: () => VizijRuntimeProvider,
29
+ buildPoseWeightInputPathSegment: () => buildPoseWeightInputPathSegment,
30
+ buildPoseWeightPathMap: () => buildPoseWeightPathMap,
31
+ buildPoseWeightRelativePath: () => buildPoseWeightRelativePath,
32
+ buildRigInputPath: () => buildRigInputPath,
33
+ buildSemanticPoseWeightPathMap: () => buildSemanticPoseWeightPathMap,
34
+ filterPosesBySemanticKind: () => filterPosesBySemanticKind,
35
+ getPoseSemanticKey: () => getPoseSemanticKey,
36
+ mapNormalizedControlValue: () => mapNormalizedControlValue,
37
+ mapUnitControlValue: () => mapUnitControlValue,
38
+ normalizePoseSemanticKey: () => normalizePoseSemanticKey,
39
+ resolveFaceControls: () => resolveFaceControls,
40
+ resolvePoseMembership: () => resolvePoseMembership,
41
+ resolvePoseSemantics: () => resolvePoseSemantics,
42
+ resolveRuntimeUpdatePlan: () => resolveRuntimeUpdatePlan,
43
+ useOptionalVizijRuntime: () => useOptionalVizijRuntime,
25
44
  useRigInput: () => useRigInput,
26
45
  useVizijOutputs: () => useVizijOutputs,
27
46
  useVizijRuntime: () => useVizijRuntime
@@ -34,11 +53,143 @@ var import_render = require("@vizij/render");
34
53
  var import_orchestrator_react = require("@vizij/orchestrator-react");
35
54
  var import_node_graph_authoring = require("@vizij/node-graph-authoring");
36
55
  var import_value_json2 = require("@vizij/value-json");
56
+ var import_utils = require("@vizij/utils");
37
57
 
38
58
  // src/context.ts
39
59
  var import_react = require("react");
40
60
  var VizijRuntimeContext = (0, import_react.createContext)(null);
41
61
 
62
+ // src/updatePolicy.ts
63
+ function applyRuntimeGraphBundle(base, bundle) {
64
+ const next = {
65
+ ...base
66
+ };
67
+ const hasRigOverride = Object.prototype.hasOwnProperty.call(bundle, "rig");
68
+ const hasPoseOverride = Object.prototype.hasOwnProperty.call(bundle, "pose");
69
+ const hasAnimationsOverride = Object.prototype.hasOwnProperty.call(
70
+ bundle,
71
+ "animations"
72
+ );
73
+ const hasProgramsOverride = Object.prototype.hasOwnProperty.call(
74
+ bundle,
75
+ "programs"
76
+ );
77
+ if (hasRigOverride) {
78
+ if (bundle.rig) {
79
+ next.rig = bundle.rig;
80
+ } else {
81
+ delete next.rig;
82
+ }
83
+ }
84
+ if (hasPoseOverride) {
85
+ if (bundle.pose) {
86
+ next.pose = bundle.pose;
87
+ } else {
88
+ delete next.pose;
89
+ }
90
+ }
91
+ if (hasAnimationsOverride) {
92
+ next.animations = Array.isArray(bundle.animations) ? bundle.animations : void 0;
93
+ }
94
+ if (hasProgramsOverride) {
95
+ next.programs = Array.isArray(bundle.programs) ? bundle.programs : void 0;
96
+ }
97
+ return next;
98
+ }
99
+ function normalizeSpecPayload(value) {
100
+ if (!value) {
101
+ return "";
102
+ }
103
+ try {
104
+ return JSON.stringify(value);
105
+ } catch {
106
+ return String(value);
107
+ }
108
+ }
109
+ function glbSignature(glb) {
110
+ if (glb.kind === "url") {
111
+ return `url:${glb.src}`;
112
+ }
113
+ if (glb.kind === "blob") {
114
+ return `blob:${glb.blob?.size ?? 0}`;
115
+ }
116
+ return `world:${normalizeSpecPayload(glb.world)}`;
117
+ }
118
+ function graphSignature(graph) {
119
+ if (!graph) {
120
+ return "";
121
+ }
122
+ const id = graph.id ?? "";
123
+ return `${id}:${normalizeSpecPayload(graph.spec ?? graph.ir ?? null)}`;
124
+ }
125
+ function poseSignature(pose) {
126
+ if (!pose) {
127
+ return "";
128
+ }
129
+ const graph = pose.graph;
130
+ const config = pose.config;
131
+ const graphPart = graph ? graphSignature({ id: graph.id, spec: graph.spec }) : "";
132
+ const configPart = config ? normalizeSpecPayload(config) : "";
133
+ return `${graphPart}:${configPart}`;
134
+ }
135
+ function animationsSignature(animations) {
136
+ if (!Array.isArray(animations) || animations.length === 0) {
137
+ return "";
138
+ }
139
+ return animations.map((animation) => {
140
+ const id = animation.id ?? "";
141
+ const clipSignature = normalizeSpecPayload(animation.clip ?? null);
142
+ const setupSignature = normalizeSpecPayload(animation.setup ?? null);
143
+ const weightSignature = animation.weight == null ? "" : String(animation.weight);
144
+ return `${id}:${clipSignature}:${setupSignature}:${weightSignature}`;
145
+ }).sort().join("|");
146
+ }
147
+ function programsSignature(programs) {
148
+ if (!Array.isArray(programs) || programs.length === 0) {
149
+ return "";
150
+ }
151
+ return programs.map((program) => {
152
+ const id = program.id ?? "";
153
+ const label = program.label ?? "";
154
+ const graphId = program.graph?.id ?? "";
155
+ const graphPayload = normalizeSpecPayload(
156
+ program.graph?.spec ?? program.graph?.ir ?? null
157
+ );
158
+ const resetValues = normalizeSpecPayload(program.resetValues ?? null);
159
+ return `${id}:${label}:${graphId}:${graphPayload}:${resetValues}`;
160
+ }).sort().join("|");
161
+ }
162
+ function resolveRuntimeUpdatePlan(previous, next, tier) {
163
+ if (!previous) {
164
+ return { reloadAssets: true, reregisterGraphs: false };
165
+ }
166
+ const glbChanged = glbSignature(previous.glb) !== glbSignature(next.glb);
167
+ const rigChanged = graphSignature(previous.rig) !== graphSignature(next.rig);
168
+ const poseChanged = poseSignature(previous.pose) !== poseSignature(next.pose);
169
+ const rigReferenceChanged = previous.rig?.id !== next.rig?.id || previous.rig?.spec !== next.rig?.spec || previous.rig?.ir !== next.rig?.ir;
170
+ const poseReferenceChanged = previous.pose?.graph?.id !== next.pose?.graph?.id || previous.pose?.graph?.spec !== next.pose?.graph?.spec || previous.pose?.config !== next.pose?.config;
171
+ const graphsChanged = rigChanged || poseChanged || rigReferenceChanged || poseReferenceChanged;
172
+ const animationsChanged = animationsSignature(previous.animations) !== animationsSignature(next.animations);
173
+ const programsChanged = programsSignature(previous.programs) !== programsSignature(next.programs);
174
+ const controllersChanged = graphsChanged || animationsChanged || programsChanged;
175
+ if (tier === "assets") {
176
+ return { reloadAssets: true, reregisterGraphs: false };
177
+ }
178
+ if (tier === "graphs") {
179
+ if (glbChanged) {
180
+ return { reloadAssets: true, reregisterGraphs: false };
181
+ }
182
+ return { reloadAssets: false, reregisterGraphs: controllersChanged };
183
+ }
184
+ if (glbChanged) {
185
+ return { reloadAssets: true, reregisterGraphs: false };
186
+ }
187
+ if (controllersChanged) {
188
+ return { reloadAssets: false, reregisterGraphs: true };
189
+ }
190
+ return { reloadAssets: false, reregisterGraphs: false };
191
+ }
192
+
42
193
  // src/utils/graph.ts
43
194
  function getNodes(spec) {
44
195
  if (!spec || typeof spec !== "object") {
@@ -83,6 +234,12 @@ function collectInputPaths(spec) {
83
234
  }
84
235
  function collectInputPathMap(spec) {
85
236
  const map = {};
237
+ const addVariant = (key, path, force = false) => {
238
+ if (!key || !force && map[key]) {
239
+ return;
240
+ }
241
+ map[key] = path;
242
+ };
86
243
  const nodes = getNodes(spec);
87
244
  nodes.forEach((node) => {
88
245
  if (String(node.type ?? "").toLowerCase() !== "input") {
@@ -94,11 +251,653 @@ function collectInputPathMap(spec) {
94
251
  }
95
252
  const id = String(node.id ?? "");
96
253
  const key = id.startsWith("input_") ? id.slice("input_".length) : id || path.trim();
97
- map[key] = path.trim();
254
+ const trimmedPath = path.trim();
255
+ addVariant(key, trimmedPath);
256
+ if (key.startsWith("direct_")) {
257
+ addVariant(key.slice("direct_".length), trimmedPath, true);
258
+ }
259
+ if (key.startsWith("pose_control_")) {
260
+ addVariant(key.slice("pose_control_".length), trimmedPath);
261
+ }
262
+ });
263
+ return map;
264
+ }
265
+
266
+ // src/utils/posePaths.ts
267
+ var POSE_WEIGHT_INPUT_PATH_PREFIX = "/poses/";
268
+ var VISEME_POSE_KEYS = [
269
+ "a",
270
+ "at",
271
+ "b",
272
+ "e",
273
+ "e_2",
274
+ "f",
275
+ "i",
276
+ "k",
277
+ "m",
278
+ "o",
279
+ "o_2",
280
+ "p",
281
+ "r",
282
+ "s",
283
+ "t",
284
+ "t_2",
285
+ "u"
286
+ ];
287
+ var EXPRESSIVE_EMOTION_POSE_KEYS = [
288
+ "concerned",
289
+ "happy",
290
+ "sad",
291
+ "sleepy",
292
+ "surprise"
293
+ ];
294
+ var EMOTION_POSE_KEYS = [
295
+ "concerned",
296
+ "happy",
297
+ "neutral",
298
+ "sad",
299
+ "sleepy",
300
+ "surprise",
301
+ "angry"
302
+ ];
303
+ var VISEME_GROUP_NEEDLES = ["viseme", "phoneme", "lip", "mouth"];
304
+ var EMOTION_GROUP_NEEDLES = ["emotion", "expression", "mood", "affect"];
305
+ var VISEME_POSE_KEY_SET = new Set(VISEME_POSE_KEYS);
306
+ var EMOTION_POSE_KEY_SET = new Set(EMOTION_POSE_KEYS);
307
+ var POSE_KEY_ALIASES = {
308
+ concern: "concerned",
309
+ surprised: "surprise"
310
+ };
311
+ function buildRigInputPath(faceId, path) {
312
+ let trimmed = path.startsWith("/") ? path.slice(1) : path;
313
+ if (!trimmed) {
314
+ return `rig/${faceId}`;
315
+ }
316
+ while (trimmed.startsWith("rig/")) {
317
+ const segments = trimmed.split("/");
318
+ if (segments.length >= 3) {
319
+ const existingFaceId = segments[1];
320
+ const remainder = segments.slice(2).join("/");
321
+ if (existingFaceId === faceId) {
322
+ return trimmed;
323
+ }
324
+ trimmed = remainder || "";
325
+ } else {
326
+ trimmed = segments.slice(1).join("/");
327
+ }
328
+ }
329
+ const suffix = trimmed ? `/${trimmed}` : "";
330
+ return `rig/${faceId}${suffix}`;
331
+ }
332
+ function normalizePoseWeightPathSegment(value) {
333
+ const trimmed = value?.trim() ?? "";
334
+ if (!trimmed) {
335
+ return "pose";
336
+ }
337
+ const normalized = trimmed.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
338
+ return normalized || "pose";
339
+ }
340
+ function buildPoseWeightInputPathSegment(poseId) {
341
+ return normalizePoseWeightPathSegment(poseId);
342
+ }
343
+ function buildPoseWeightRelativePath(poseId) {
344
+ return `${POSE_WEIGHT_INPUT_PATH_PREFIX}${buildPoseWeightInputPathSegment(
345
+ poseId
346
+ )}.weight`;
347
+ }
348
+ function buildPoseWeightPathMap(poses, faceId) {
349
+ const faceSegment = faceId?.trim() || "face";
350
+ const map = /* @__PURE__ */ new Map();
351
+ poses.forEach((pose) => {
352
+ map.set(
353
+ pose.id,
354
+ buildRigInputPath(faceSegment, buildPoseWeightRelativePath(pose.id))
355
+ );
356
+ });
357
+ return map;
358
+ }
359
+ function normalizePoseSemanticKey(value) {
360
+ const trimmed = value?.trim() ?? "";
361
+ if (!trimmed) {
362
+ return null;
363
+ }
364
+ const normalized = trimmed.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
365
+ if (!normalized) {
366
+ return null;
367
+ }
368
+ return POSE_KEY_ALIASES[normalized] ?? normalized;
369
+ }
370
+ function derivePoseSemanticKeyFromId(poseId) {
371
+ const trimmed = poseId?.trim() ?? "";
372
+ if (!trimmed) {
373
+ return null;
374
+ }
375
+ const stripped = trimmed.replace(/^pose_d_/i, "").replace(/^pose_/i, "").replace(/^d_/i, "").replace(/_d$/i, "");
376
+ return normalizePoseSemanticKey(stripped);
377
+ }
378
+ function getPoseSemanticKey(pose) {
379
+ return normalizePoseSemanticKey(pose.name) ?? derivePoseSemanticKeyFromId(pose.id) ?? null;
380
+ }
381
+ function normalizePoseGroupPath(value) {
382
+ const trimmed = value?.trim() ?? "";
383
+ if (!trimmed) {
384
+ return null;
385
+ }
386
+ return trimmed.replace(/^\/+|\/+$/g, "").replace(/\/+/g, "/");
387
+ }
388
+ function sanitizePoseGroupId(value, fallback) {
389
+ const normalized = (value ?? "").trim().replace(/[^a-zA-Z0-9_/-]+/g, "_").replace(/^\/+|\/+$/g, "").replace(/\/+/g, "_");
390
+ if (!normalized) {
391
+ return fallback.replace(/\//g, "_");
392
+ }
393
+ return normalized;
394
+ }
395
+ function buildPoseGroupLookup(groups) {
396
+ const byId = /* @__PURE__ */ new Map();
397
+ const byPath = /* @__PURE__ */ new Map();
398
+ const orderById = /* @__PURE__ */ new Map();
399
+ (groups ?? []).forEach((group, index) => {
400
+ const path = normalizePoseGroupPath(group.path ?? group.name ?? group.id);
401
+ if (!path) {
402
+ return;
403
+ }
404
+ const id = sanitizePoseGroupId(group.id, path);
405
+ 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;
406
+ const normalized = {
407
+ ...group,
408
+ id,
409
+ path,
410
+ name: humanizedName
411
+ };
412
+ byId.set(id, normalized);
413
+ if (!orderById.has(id)) {
414
+ orderById.set(id, index);
415
+ }
416
+ if (!byPath.has(path)) {
417
+ byPath.set(path, normalized);
418
+ }
419
+ });
420
+ return { byId, byPath, orderById };
421
+ }
422
+ function orderPoseMembershipIds(groupIds, groups) {
423
+ const { orderById } = buildPoseGroupLookup(groups);
424
+ const unique = Array.from(
425
+ new Set(
426
+ Array.from(groupIds).map((groupId) => groupId.trim()).filter((groupId) => groupId.length > 0)
427
+ )
428
+ );
429
+ unique.sort((left, right) => {
430
+ const leftIndex = orderById.get(left);
431
+ const rightIndex = orderById.get(right);
432
+ if (leftIndex !== void 0 && rightIndex !== void 0) {
433
+ return leftIndex - rightIndex;
434
+ }
435
+ if (leftIndex !== void 0) {
436
+ return -1;
437
+ }
438
+ if (rightIndex !== void 0) {
439
+ return 1;
440
+ }
441
+ const leftPath = normalizePoseGroupPath(left) ?? left;
442
+ const rightPath = normalizePoseGroupPath(right) ?? right;
443
+ const byPath = leftPath.localeCompare(rightPath);
444
+ if (byPath !== 0) {
445
+ return byPath;
446
+ }
447
+ return left.localeCompare(right);
448
+ });
449
+ return unique;
450
+ }
451
+ function valueHasNeedle(value, needles) {
452
+ const normalized = normalizePoseSemanticKey(value);
453
+ if (!normalized) {
454
+ return false;
455
+ }
456
+ return needles.some((needle) => normalized.includes(needle));
457
+ }
458
+ function resolvePoseMembership(pose, groups) {
459
+ const { byId, byPath } = buildPoseGroupLookup(groups);
460
+ const resolvedGroupIds = [];
461
+ const pathById = /* @__PURE__ */ new Map();
462
+ const addMembership = (groupId, path) => {
463
+ if (!groupId) {
464
+ return;
465
+ }
466
+ if (!resolvedGroupIds.includes(groupId)) {
467
+ resolvedGroupIds.push(groupId);
468
+ }
469
+ if (!path) {
470
+ return;
471
+ }
472
+ const existingPath = pathById.get(groupId);
473
+ const normalizedGroupIdPath = normalizePoseGroupPath(groupId);
474
+ const normalizedExistingPath = normalizePoseGroupPath(existingPath) ?? existingPath ?? null;
475
+ const normalizedIncomingPath = normalizePoseGroupPath(path) ?? path ?? null;
476
+ const shouldPromotePath = normalizedExistingPath === null || normalizedGroupIdPath !== null && normalizedExistingPath === normalizedGroupIdPath && normalizedIncomingPath !== normalizedGroupIdPath;
477
+ if (shouldPromotePath) {
478
+ pathById.set(groupId, path);
479
+ }
480
+ };
481
+ const addByPath = (rawPath) => {
482
+ const normalizedPath = normalizePoseGroupPath(rawPath);
483
+ if (!normalizedPath) {
484
+ return;
485
+ }
486
+ const existing = byPath.get(normalizedPath);
487
+ if (existing) {
488
+ addMembership(existing.id, existing.path);
489
+ return;
490
+ }
491
+ addMembership(sanitizePoseGroupId(null, normalizedPath), normalizedPath);
492
+ };
493
+ const addById = (rawId) => {
494
+ const trimmed = rawId?.trim() ?? "";
495
+ if (!trimmed) {
496
+ return;
497
+ }
498
+ const normalizedPath = normalizePoseGroupPath(trimmed);
499
+ const normalizedId = sanitizePoseGroupId(trimmed, trimmed);
500
+ const matchedById = byId.get(trimmed) ?? byId.get(normalizedId);
501
+ if (matchedById) {
502
+ addMembership(matchedById.id, matchedById.path);
503
+ return;
504
+ }
505
+ if (normalizedPath) {
506
+ const matchedByPath = byPath.get(normalizedPath);
507
+ if (matchedByPath) {
508
+ addMembership(matchedByPath.id, matchedByPath.path);
509
+ return;
510
+ }
511
+ }
512
+ addMembership(
513
+ normalizedId,
514
+ normalizedPath && normalizedPath.length > 0 ? normalizedPath : null
515
+ );
516
+ };
517
+ pose.groupIds?.forEach((groupId) => addById(groupId));
518
+ addById(pose.groupId);
519
+ addByPath(pose.group);
520
+ const orderedGroupIds = orderPoseMembershipIds(resolvedGroupIds, groups);
521
+ const primaryGroupId = orderedGroupIds[0] ?? null;
522
+ const primaryGroupPath = primaryGroupId ? byId.get(primaryGroupId)?.path ?? pathById.get(primaryGroupId) ?? null : null;
523
+ const groupPathsById = {};
524
+ orderedGroupIds.forEach((groupId) => {
525
+ const path = byId.get(groupId)?.path ?? pathById.get(groupId) ?? null;
526
+ if (path) {
527
+ groupPathsById[groupId] = path;
528
+ }
529
+ });
530
+ return {
531
+ groupIds: orderedGroupIds,
532
+ primaryGroupId,
533
+ primaryGroupPath,
534
+ groupPathsById
535
+ };
536
+ }
537
+ function poseMatchesGroupKind(pose, groups, needles) {
538
+ const membership = resolvePoseMembership(pose, groups);
539
+ if (valueHasNeedle(pose.group, needles) || valueHasNeedle(pose.groupId, needles)) {
540
+ return true;
541
+ }
542
+ if (pose.groupIds?.some((groupId) => valueHasNeedle(groupId, needles))) {
543
+ return true;
544
+ }
545
+ return membership.groupIds.some((groupId) => {
546
+ const path = membership.groupPathsById[groupId] ?? null;
547
+ const group = (groups ?? []).find((entry) => entry.id === groupId) ?? null;
548
+ return valueHasNeedle(groupId, needles) || valueHasNeedle(path, needles) || valueHasNeedle(group?.name, needles);
549
+ });
550
+ }
551
+ function resolvePoseSemantics(pose, groups) {
552
+ const membership = resolvePoseMembership(pose, groups);
553
+ const key = getPoseSemanticKey(pose);
554
+ const looksLikeVisemeGroup = poseMatchesGroupKind(
555
+ pose,
556
+ groups,
557
+ VISEME_GROUP_NEEDLES
558
+ );
559
+ const looksLikeEmotionGroup = poseMatchesGroupKind(
560
+ pose,
561
+ groups,
562
+ EMOTION_GROUP_NEEDLES
563
+ );
564
+ let kind = "other";
565
+ if (looksLikeVisemeGroup || key && VISEME_POSE_KEY_SET.has(key)) {
566
+ kind = "viseme";
567
+ } else if (looksLikeEmotionGroup || key && EMOTION_POSE_KEY_SET.has(key)) {
568
+ kind = "emotion";
569
+ }
570
+ return { key, kind, membership };
571
+ }
572
+ function filterPosesBySemanticKind(poses, groups, kind) {
573
+ return poses.filter(
574
+ (pose) => resolvePoseSemantics(pose, groups).kind === kind
575
+ );
576
+ }
577
+ function buildSemanticPoseWeightPathMap(poses, groups, faceId, kind) {
578
+ const map = /* @__PURE__ */ new Map();
579
+ const pathMap = buildPoseWeightPathMap(poses, faceId);
580
+ poses.forEach((pose) => {
581
+ const semantics = resolvePoseSemantics(pose, groups);
582
+ const path = pathMap.get(pose.id);
583
+ if (semantics.kind !== kind || !semantics.key || !path || map.has(semantics.key)) {
584
+ return;
585
+ }
586
+ map.set(semantics.key, path);
98
587
  });
99
588
  return map;
100
589
  }
101
590
 
591
+ // src/utils/poseRuntime.ts
592
+ function shouldUseLegacyPoseWeightFallback(hasPoseGraph) {
593
+ return !hasPoseGraph;
594
+ }
595
+ function resolvePoseControlInputPath({
596
+ inputId,
597
+ basePath,
598
+ rigInputPathMap,
599
+ hasNativePoseControlInput
600
+ }) {
601
+ if (!inputId.trim()) {
602
+ return void 0;
603
+ }
604
+ return rigInputPathMap[inputId] ?? rigInputPathMap[`pose_control_${inputId}`] ?? rigInputPathMap[`direct_${inputId}`] ?? (hasNativePoseControlInput ? basePath : void 0);
605
+ }
606
+
607
+ // src/utils/clipPlayback.ts
608
+ var EPSILON = 1e-6;
609
+ function toFiniteNumber(value, fallback) {
610
+ const parsed = Number(value);
611
+ return Number.isFinite(parsed) ? parsed : fallback;
612
+ }
613
+ function normaliseInterpolation(interpolation) {
614
+ const mode = typeof interpolation === "string" ? interpolation.trim().toLowerCase() : "linear";
615
+ if (mode === "step") {
616
+ return "step";
617
+ }
618
+ if (mode === "cubic" || mode === "cubicspline") {
619
+ return "cubic";
620
+ }
621
+ return "linear";
622
+ }
623
+ function asNumericKeyframe(keyframe) {
624
+ if (!keyframe || typeof keyframe !== "object") {
625
+ return null;
626
+ }
627
+ const time = Number(keyframe.time);
628
+ const value = Number(keyframe.value);
629
+ if (!Number.isFinite(time) || !Number.isFinite(value)) {
630
+ return null;
631
+ }
632
+ const inTangentRaw = keyframe.inTangent;
633
+ const outTangentRaw = keyframe.outTangent;
634
+ const inTangent = inTangentRaw == null || Number.isFinite(Number(inTangentRaw)) ? inTangentRaw : void 0;
635
+ const outTangent = outTangentRaw == null || Number.isFinite(Number(outTangentRaw)) ? outTangentRaw : void 0;
636
+ return {
637
+ time,
638
+ value,
639
+ inTangent,
640
+ outTangent
641
+ };
642
+ }
643
+ function getNumericKeyframes(track) {
644
+ const keyframes = Array.isArray(track.keyframes) ? track.keyframes : [];
645
+ const numeric = keyframes.map((keyframe) => asNumericKeyframe(keyframe)).filter((keyframe) => Boolean(keyframe));
646
+ if (numeric.length <= 1) {
647
+ return numeric;
648
+ }
649
+ return [...numeric].sort((a, b) => a.time - b.time);
650
+ }
651
+ function resolveTangent(value, fallback) {
652
+ const parsed = Number(value);
653
+ if (Number.isFinite(parsed)) {
654
+ return parsed;
655
+ }
656
+ return fallback;
657
+ }
658
+ function sampleHermite(startValue, endValue, outTangent, inTangent, factor, duration) {
659
+ const t = factor;
660
+ const t2 = t * t;
661
+ const t3 = t2 * t;
662
+ const h00 = 2 * t3 - 3 * t2 + 1;
663
+ const h10 = t3 - 2 * t2 + t;
664
+ const h01 = -2 * t3 + 3 * t2;
665
+ const h11 = t3 - t2;
666
+ return h00 * startValue + h10 * outTangent * duration + h01 * endValue + h11 * inTangent * duration;
667
+ }
668
+ function resolveClipDurationSeconds(clip, fallbackDurationSeconds = 0) {
669
+ const fallback = Math.max(0, toFiniteNumber(fallbackDurationSeconds, 0));
670
+ if (!clip || typeof clip !== "object") {
671
+ return fallback;
672
+ }
673
+ const clipDuration = Number(clip.duration);
674
+ if (Number.isFinite(clipDuration) && clipDuration > 0) {
675
+ return clipDuration;
676
+ }
677
+ const tracks = Array.isArray(clip.tracks) ? clip.tracks : [];
678
+ let maxTime = 0;
679
+ tracks.forEach((track) => {
680
+ const keyframes = getNumericKeyframes(track);
681
+ const last = keyframes[keyframes.length - 1];
682
+ if (last && last.time > maxTime) {
683
+ maxTime = last.time;
684
+ }
685
+ });
686
+ return maxTime > 0 ? maxTime : fallback;
687
+ }
688
+ function clampAnimationTime(time, duration) {
689
+ if (!Number.isFinite(duration) || duration <= 0) {
690
+ return 0;
691
+ }
692
+ if (!Number.isFinite(time)) {
693
+ return 0;
694
+ }
695
+ if (time <= 0) {
696
+ return 0;
697
+ }
698
+ if (time >= duration) {
699
+ return duration;
700
+ }
701
+ return time;
702
+ }
703
+ function advanceClipTime(state, dt) {
704
+ const duration = Math.max(0, toFiniteNumber(state.duration, 0));
705
+ const speed = Number.isFinite(state.speed) && state.speed > 0 ? state.speed : 1;
706
+ const currentTime = clampAnimationTime(state.time, duration);
707
+ const delta = Number.isFinite(dt) && dt > 0 ? Math.max(0, dt) * speed : 0;
708
+ if (!state.playing || delta <= 0) {
709
+ return { time: currentTime, completed: false };
710
+ }
711
+ if (duration <= 0) {
712
+ return { time: 0, completed: true };
713
+ }
714
+ const nextTime = currentTime + delta;
715
+ if (state.loop) {
716
+ if (nextTime < duration) {
717
+ return { time: nextTime, completed: false };
718
+ }
719
+ const wrapped = (nextTime % duration + duration) % duration;
720
+ return { time: wrapped, completed: false };
721
+ }
722
+ if (nextTime >= duration - EPSILON) {
723
+ return { time: duration, completed: true };
724
+ }
725
+ return { time: nextTime, completed: false };
726
+ }
727
+ function sampleTrackAtTime(track, timeSeconds) {
728
+ const keyframes = getNumericKeyframes(track);
729
+ if (keyframes.length === 0) {
730
+ return 0;
731
+ }
732
+ if (keyframes.length === 1) {
733
+ return keyframes[0].value;
734
+ }
735
+ const mode = normaliseInterpolation(track.interpolation);
736
+ const time = Number.isFinite(timeSeconds) ? timeSeconds : 0;
737
+ const first = keyframes[0];
738
+ if (time <= first.time + EPSILON) {
739
+ return first.value;
740
+ }
741
+ const last = keyframes[keyframes.length - 1];
742
+ if (time >= last.time - EPSILON) {
743
+ return last.value;
744
+ }
745
+ for (let i = 0; i < keyframes.length - 1; i += 1) {
746
+ const current = keyframes[i];
747
+ const next = keyframes[i + 1];
748
+ const start = current.time;
749
+ const end = next.time;
750
+ const duration = end - start;
751
+ if (duration <= EPSILON) {
752
+ if (time <= end + EPSILON) {
753
+ return next.value;
754
+ }
755
+ continue;
756
+ }
757
+ if (Math.abs(time - end) <= EPSILON) {
758
+ return next.value;
759
+ }
760
+ if (time < end) {
761
+ const factor = (time - start) / duration;
762
+ if (mode === "step") {
763
+ return current.value;
764
+ }
765
+ if (mode === "cubic") {
766
+ const slope = (next.value - current.value) / duration;
767
+ const outTangent = resolveTangent(current.outTangent, slope);
768
+ const inTangent = resolveTangent(next.inTangent, slope);
769
+ return sampleHermite(
770
+ current.value,
771
+ next.value,
772
+ outTangent,
773
+ inTangent,
774
+ factor,
775
+ duration
776
+ );
777
+ }
778
+ return current.value + (next.value - current.value) * factor;
779
+ }
780
+ }
781
+ return last.value;
782
+ }
783
+
784
+ // src/utils/animationBridge.ts
785
+ function resolveAnimationBridgeOutputPaths(channel, faceId, rigInputMap) {
786
+ const normalizedChannel = channel.trim().replace(/^\/+/, "");
787
+ if (!normalizedChannel) {
788
+ return [];
789
+ }
790
+ const outputPaths = /* @__PURE__ */ new Set([normalizedChannel]);
791
+ if (normalizedChannel.startsWith("animation/")) {
792
+ return Array.from(outputPaths).sort(
793
+ (left, right) => left.localeCompare(right)
794
+ );
795
+ }
796
+ if (rigInputMap && Object.keys(rigInputMap).length > 0) {
797
+ const candidateKeys = /* @__PURE__ */ new Set([normalizedChannel]);
798
+ const rigChannelMatch2 = /^rig\/[^/]+\/(.+)$/.exec(normalizedChannel);
799
+ if (rigChannelMatch2?.[1]) {
800
+ candidateKeys.add(rigChannelMatch2[1]);
801
+ }
802
+ candidateKeys.forEach((key) => {
803
+ const mapped = rigInputMap[key];
804
+ const normalized = mapped?.trim().replace(/^\/+/, "");
805
+ if (normalized) {
806
+ outputPaths.add(normalized);
807
+ }
808
+ });
809
+ const suffix = normalizedChannel.includes("/") ? normalizedChannel : `/${normalizedChannel}`;
810
+ Object.values(rigInputMap).forEach((mappedPath) => {
811
+ const normalized = mappedPath?.trim().replace(/^\/+/, "");
812
+ if (!normalized) {
813
+ return;
814
+ }
815
+ if (normalized === normalizedChannel || normalized.endsWith(suffix)) {
816
+ outputPaths.add(normalized);
817
+ }
818
+ });
819
+ }
820
+ if (!faceId) {
821
+ return Array.from(outputPaths).sort(
822
+ (left, right) => left.localeCompare(right)
823
+ );
824
+ }
825
+ const rigChannelMatch = /^rig\/[^/]+\/(.+)$/.exec(normalizedChannel);
826
+ if (rigChannelMatch?.[1]) {
827
+ outputPaths.add(`rig/${faceId}/${rigChannelMatch[1]}`);
828
+ } else if (!normalizedChannel.startsWith("rig/")) {
829
+ outputPaths.add(`rig/${faceId}/${normalizedChannel}`);
830
+ }
831
+ return Array.from(outputPaths).sort(
832
+ (left, right) => left.localeCompare(right)
833
+ );
834
+ }
835
+ function collectAnimationClipOutputPaths(clip, faceId, rigInputMap) {
836
+ const outputPaths = /* @__PURE__ */ new Set();
837
+ const tracks = Array.isArray(clip.tracks) ? clip.tracks : [];
838
+ tracks.forEach((track) => {
839
+ const channel = typeof track.channel === "string" ? track.channel.trim() : "";
840
+ if (!channel) {
841
+ return;
842
+ }
843
+ resolveAnimationBridgeOutputPaths(channel, faceId, rigInputMap).forEach(
844
+ (path) => {
845
+ outputPaths.add(path);
846
+ }
847
+ );
848
+ });
849
+ return Array.from(outputPaths).sort(
850
+ (left, right) => left.localeCompare(right)
851
+ );
852
+ }
853
+ function sampleAnimationClipOutputValues(clip, timeSeconds, weight = 1, faceId, rigInputMap) {
854
+ const appliedWeight = Number.isFinite(weight) && weight >= 0 ? Number(weight) : 1;
855
+ const outputValues = /* @__PURE__ */ new Map();
856
+ const tracks = Array.isArray(clip.tracks) ? clip.tracks : [];
857
+ tracks.forEach((track) => {
858
+ const channel = typeof track.channel === "string" ? track.channel.trim() : "";
859
+ if (!channel) {
860
+ return;
861
+ }
862
+ const sampledValue = sampleTrackAtTime(
863
+ track,
864
+ timeSeconds
865
+ );
866
+ const weightedValue = sampledValue * appliedWeight;
867
+ resolveAnimationBridgeOutputPaths(channel, faceId, rigInputMap).forEach(
868
+ (path) => {
869
+ outputValues.set(path, (outputValues.get(path) ?? 0) + weightedValue);
870
+ }
871
+ );
872
+ });
873
+ return outputValues;
874
+ }
875
+ function diffAnimationAggregateValues(previousAggregate, nextAggregate, epsilon = 1e-6) {
876
+ const operations = [];
877
+ const changedPaths = /* @__PURE__ */ new Set();
878
+ previousAggregate.forEach((previousValue, path) => {
879
+ const nextValue = nextAggregate.get(path);
880
+ if (nextValue === void 0 || Math.abs(nextValue - previousValue) > epsilon) {
881
+ changedPaths.add(path);
882
+ }
883
+ });
884
+ nextAggregate.forEach((nextValue, path) => {
885
+ const previousValue = previousAggregate.get(path);
886
+ if (previousValue === void 0 || Math.abs(nextValue - previousValue) > epsilon) {
887
+ changedPaths.add(path);
888
+ }
889
+ });
890
+ changedPaths.forEach((path) => {
891
+ const nextValue = nextAggregate.get(path);
892
+ if (nextValue === void 0) {
893
+ operations.push({ kind: "clear", path });
894
+ return;
895
+ }
896
+ operations.push({ kind: "set", path, value: nextValue });
897
+ });
898
+ return operations;
899
+ }
900
+
102
901
  // src/utils/valueConversion.ts
103
902
  var import_value_json = require("@vizij/value-json");
104
903
  function numericArrayToRaw(arr) {
@@ -224,6 +1023,19 @@ var DEFAULT_MERGE = {
224
1023
  intermediate: "add"
225
1024
  };
226
1025
  var DEFAULT_DURATION = 0.35;
1026
+ var POSE_CONTROL_BRIDGE_EPSILON = 1e-6;
1027
+ var DEV_MODE = (() => {
1028
+ const nodeEnv = globalThis.process?.env?.NODE_ENV;
1029
+ return typeof nodeEnv === "string" && nodeEnv === "development";
1030
+ })();
1031
+ function isRuntimeDebugEnabled() {
1032
+ if (DEV_MODE) {
1033
+ return true;
1034
+ }
1035
+ return Boolean(
1036
+ globalThis.__VIZIJ_RUNTIME_DEBUG__
1037
+ );
1038
+ }
227
1039
  var EASINGS = {
228
1040
  linear: (t) => t,
229
1041
  easeIn: (t) => t * t,
@@ -240,12 +1052,19 @@ function resolveEasing(easing) {
240
1052
  return EASINGS.linear;
241
1053
  }
242
1054
  function findRootId(world) {
1055
+ let fallback = null;
243
1056
  for (const entry of Object.values(world)) {
244
- if (entry && typeof entry === "object" && entry.type === "group" && entry.rootBounds && entry.id) {
1057
+ if (!entry || typeof entry !== "object" || entry.type !== "group") {
1058
+ continue;
1059
+ }
1060
+ if (entry.rootBounds && entry.id) {
245
1061
  return entry.id;
246
1062
  }
1063
+ if (!fallback && entry.id) {
1064
+ fallback = entry.id;
1065
+ }
247
1066
  }
248
- return null;
1067
+ return fallback;
249
1068
  }
250
1069
  function normalisePath(path) {
251
1070
  if (!path) {
@@ -415,6 +1234,30 @@ function namespaceGraphSpec(spec, namespace) {
415
1234
  nodes: nextNodes
416
1235
  };
417
1236
  }
1237
+ function stripNulls(value) {
1238
+ if (value === null) {
1239
+ return void 0;
1240
+ }
1241
+ if (Array.isArray(value)) {
1242
+ const next2 = value.map((entry) => stripNulls(entry)).filter((entry) => entry !== void 0 && entry !== null);
1243
+ return next2;
1244
+ }
1245
+ if (typeof value !== "object" || value === void 0) {
1246
+ return value;
1247
+ }
1248
+ const next = {};
1249
+ Object.entries(value).forEach(([key, entry]) => {
1250
+ if (entry === null) {
1251
+ return;
1252
+ }
1253
+ const cleaned = stripNulls(entry);
1254
+ if (cleaned === void 0) {
1255
+ return;
1256
+ }
1257
+ next[key] = cleaned;
1258
+ });
1259
+ return next;
1260
+ }
418
1261
  var now = () => typeof performance !== "undefined" ? performance.now() : Date.now();
419
1262
  function pickBundleGraph(bundle, preferredKinds) {
420
1263
  if (!bundle?.graphs || bundle.graphs.length === 0) {
@@ -585,26 +1428,49 @@ function convertExtractedAnimations(clips) {
585
1428
  }
586
1429
  const parsedValueSize = track.valueSize != null ? Number(track.valueSize) : NaN;
587
1430
  const valueSize = Number.isFinite(parsedValueSize) && parsedValueSize > 0 ? parsedValueSize : 1;
588
- if (values.length !== times.length * valueSize) {
1431
+ const interpolationRaw = typeof track.interpolation === "string" ? track.interpolation.trim().toLowerCase() : "linear";
1432
+ const interpolation = interpolationRaw === "step" ? "step" : interpolationRaw === "cubic" || interpolationRaw === "cubicspline" ? "cubic" : "linear";
1433
+ const isCubic = interpolation === "cubic";
1434
+ const hasTripletTangents = isCubic && values.length === times.length * valueSize * 3;
1435
+ const hasFlatValues = values.length === times.length * valueSize;
1436
+ if (!hasTripletTangents && !hasFlatValues) {
589
1437
  return;
590
1438
  }
591
1439
  const rawIndex = track.componentIndex != null ? Number(track.componentIndex) : 0;
592
1440
  const componentIndex = Number.isInteger(rawIndex) && rawIndex >= 0 ? Math.min(rawIndex, valueSize - 1) : 0;
593
1441
  const keyframes = [];
594
1442
  times.forEach((time, index) => {
595
- const base = index * valueSize + componentIndex;
596
- const value = values[base];
1443
+ const flatBase = index * valueSize + componentIndex;
1444
+ const valueBase = hasTripletTangents ? index * valueSize * 3 + valueSize + componentIndex : flatBase;
1445
+ const value = values[valueBase];
597
1446
  if (!Number.isFinite(value)) {
598
1447
  return;
599
1448
  }
600
- keyframes.push({ time, value });
1449
+ const keyframe = {
1450
+ time,
1451
+ value
1452
+ };
1453
+ if (hasTripletTangents) {
1454
+ const inBase = index * valueSize * 3 + componentIndex;
1455
+ const outBase = index * valueSize * 3 + valueSize * 2 + componentIndex;
1456
+ const inTangent = values[inBase];
1457
+ const outTangent = values[outBase];
1458
+ if (Number.isFinite(inTangent)) {
1459
+ keyframe.inTangent = inTangent;
1460
+ }
1461
+ if (Number.isFinite(outTangent)) {
1462
+ keyframe.outTangent = outTangent;
1463
+ }
1464
+ }
1465
+ keyframes.push(keyframe);
601
1466
  });
602
1467
  if (keyframes.length === 0) {
603
1468
  return;
604
1469
  }
605
1470
  convertedTracks.push({
606
1471
  channel: channelId,
607
- keyframes
1472
+ keyframes,
1473
+ interpolation
608
1474
  });
609
1475
  });
610
1476
  if (convertedTracks.length === 0) {
@@ -671,29 +1537,91 @@ function mergeAnimationLists(explicit, fromBundle) {
671
1537
  }
672
1538
  return changed ? merged : explicit;
673
1539
  }
674
- function mergeAssetBundle(base, extracted, extractedAnimations) {
675
- const resolvedBundle = base.bundle ?? extracted ?? null;
676
- const rigFromBundle = convertBundleGraph(
677
- pickBundleGraph(resolvedBundle, ["rig"])
1540
+ function extractProgramResetValues(value) {
1541
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1542
+ return void 0;
1543
+ }
1544
+ const entries = Object.entries(value).filter(
1545
+ ([, rawValue]) => Number.isFinite(Number(rawValue))
678
1546
  );
679
- const resolvedRig = base.rig ?? rigFromBundle ?? void 0;
680
- const basePose = base.pose;
681
- const poseStageFilter = basePose?.stageNeutralFilter;
682
- const poseGraphFromBundle = basePose?.graph ? null : convertBundleGraph(
683
- pickBundleGraph(resolvedBundle, ["pose-driver", "pose"])
1547
+ if (entries.length === 0) {
1548
+ return void 0;
1549
+ }
1550
+ return Object.fromEntries(
1551
+ entries.map(([path, rawValue]) => [path, Number(rawValue)])
684
1552
  );
685
- const resolvedPoseGraph = basePose?.graph ?? poseGraphFromBundle ?? void 0;
686
- const resolvedPoseConfig = basePose?.config ?? resolvedBundle?.poses?.config ?? void 0;
687
- let resolvedPose = basePose;
688
- if (basePose) {
689
- const nextPose = { ...basePose };
690
- let changed = false;
691
- if (resolvedPoseGraph && basePose.graph !== resolvedPoseGraph) {
692
- nextPose.graph = resolvedPoseGraph;
693
- changed = true;
1553
+ }
1554
+ function convertBundlePrograms(entries) {
1555
+ if (!Array.isArray(entries) || entries.length === 0) {
1556
+ return [];
1557
+ }
1558
+ return entries.filter((entry) => normaliseBundleKind(entry?.kind) === "motiongraph").map((entry) => {
1559
+ const graph = convertBundleGraph(entry);
1560
+ if (!graph) {
1561
+ return null;
694
1562
  }
695
- if (resolvedPoseConfig && basePose.config !== resolvedPoseConfig) {
696
- nextPose.config = resolvedPoseConfig;
1563
+ const metadata = entry.metadata && typeof entry.metadata === "object" && !Array.isArray(entry.metadata) ? entry.metadata : void 0;
1564
+ return {
1565
+ id: entry.id,
1566
+ label: typeof entry.label === "string" ? entry.label : void 0,
1567
+ graph,
1568
+ metadata,
1569
+ resetValues: extractProgramResetValues(metadata?.resetValues)
1570
+ };
1571
+ }).filter(Boolean);
1572
+ }
1573
+ function mergeProgramLists(explicit, fromBundle) {
1574
+ if (!explicit?.length && fromBundle.length === 0) {
1575
+ return void 0;
1576
+ }
1577
+ if (!explicit?.length) {
1578
+ return fromBundle.length > 0 ? fromBundle : void 0;
1579
+ }
1580
+ if (fromBundle.length === 0) {
1581
+ return explicit;
1582
+ }
1583
+ const seen = new Set(explicit.map((program) => program.id));
1584
+ let changed = false;
1585
+ const merged = [...explicit];
1586
+ for (const program of fromBundle) {
1587
+ if (!program.id || seen.has(program.id)) {
1588
+ continue;
1589
+ }
1590
+ merged.push(program);
1591
+ seen.add(program.id);
1592
+ changed = true;
1593
+ }
1594
+ return changed ? merged : explicit;
1595
+ }
1596
+ function mergeAssetBundle(base, extracted, extractedAnimations) {
1597
+ const resolvedBundle = base.bundle ?? extracted ?? null;
1598
+ const rigFromBundle = convertBundleGraph(
1599
+ pickBundleGraph(resolvedBundle, ["rig"])
1600
+ );
1601
+ const resolvedRig = base.rig ?? rigFromBundle ?? void 0;
1602
+ const basePose = base.pose;
1603
+ const hasBasePoseGraphOverride = Boolean(
1604
+ basePose && Object.prototype.hasOwnProperty.call(basePose, "graph")
1605
+ );
1606
+ const hasBasePoseConfigOverride = Boolean(
1607
+ basePose && Object.prototype.hasOwnProperty.call(basePose, "config")
1608
+ );
1609
+ const poseStageFilter = basePose?.stageNeutralFilter;
1610
+ const poseGraphFromBundle = hasBasePoseGraphOverride ? null : convertBundleGraph(
1611
+ pickBundleGraph(resolvedBundle, ["pose-driver", "pose"])
1612
+ );
1613
+ const resolvedPoseGraph = hasBasePoseGraphOverride ? basePose?.graph : basePose?.graph ?? poseGraphFromBundle ?? void 0;
1614
+ const resolvedPoseConfig = hasBasePoseConfigOverride ? basePose?.config : basePose?.config ?? resolvedBundle?.poses?.config ?? void 0;
1615
+ let resolvedPose = basePose;
1616
+ if (basePose) {
1617
+ const nextPose = { ...basePose };
1618
+ let changed = false;
1619
+ if (resolvedPoseGraph && basePose.graph !== resolvedPoseGraph) {
1620
+ nextPose.graph = resolvedPoseGraph;
1621
+ changed = true;
1622
+ }
1623
+ if (resolvedPoseConfig && basePose.config !== resolvedPoseConfig) {
1624
+ nextPose.config = resolvedPoseConfig;
697
1625
  changed = true;
698
1626
  }
699
1627
  if (!resolvedPoseGraph && !basePose.graph) {
@@ -722,6 +1650,10 @@ function mergeAssetBundle(base, extracted, extractedAnimations) {
722
1650
  animationsFromAsset
723
1651
  );
724
1652
  }
1653
+ const programsFromBundle = mergeProgramLists(
1654
+ base.programs,
1655
+ convertBundlePrograms(resolvedBundle?.graphs)
1656
+ );
725
1657
  const merged = {
726
1658
  ...base
727
1659
  };
@@ -732,14 +1664,106 @@ function mergeAssetBundle(base, extracted, extractedAnimations) {
732
1664
  }
733
1665
  merged.pose = resolvedPose;
734
1666
  merged.animations = resolvedAnimations;
1667
+ merged.programs = programsFromBundle;
735
1668
  merged.bundle = resolvedBundle;
736
1669
  return merged;
737
1670
  }
1671
+ function normalizeStoredAnimationInterpolation(interpolation) {
1672
+ const mode = typeof interpolation === "string" ? interpolation.trim().toLowerCase() : "linear";
1673
+ if (mode === "step") {
1674
+ return "step";
1675
+ }
1676
+ if (mode === "cubic" || mode === "cubicspline") {
1677
+ return "cubic";
1678
+ }
1679
+ return "linear";
1680
+ }
1681
+ function buildStoredAnimationTransitions(mode) {
1682
+ if (mode === "cubic") {
1683
+ return void 0;
1684
+ }
1685
+ if (mode === "step") {
1686
+ return {
1687
+ in: { x: 1, y: 1 },
1688
+ out: { x: 1, y: 0 }
1689
+ };
1690
+ }
1691
+ return {
1692
+ in: { x: 1, y: 1 },
1693
+ out: { x: 0, y: 0 }
1694
+ };
1695
+ }
1696
+ function toStoredAnimationClip(fallbackId, clip) {
1697
+ const clipId = typeof clip.id === "string" && clip.id.trim().length > 0 ? clip.id.trim() : fallbackId;
1698
+ const clipName = typeof clip.name === "string" && clip.name.trim().length > 0 ? clip.name.trim() : clipId;
1699
+ const durationSeconds = resolveClipDurationSeconds(clip, 0);
1700
+ const durationMs = Math.max(1, Math.round(durationSeconds * 1e3));
1701
+ const tracks = (Array.isArray(clip.tracks) ? clip.tracks : []).map((rawTrack, trackIndex) => {
1702
+ const channel = typeof rawTrack.channel === "string" ? rawTrack.channel.trim() : "";
1703
+ if (!channel) {
1704
+ return null;
1705
+ }
1706
+ const keyframes = (Array.isArray(rawTrack.keyframes) ? rawTrack.keyframes : []).map((keyframe) => {
1707
+ const time = Number(keyframe.time);
1708
+ const value = Number(keyframe.value);
1709
+ const keyframeId = keyframe["id"];
1710
+ const keyframeInterpolation = keyframe["interpolation"];
1711
+ if (!Number.isFinite(time) || !Number.isFinite(value)) {
1712
+ return null;
1713
+ }
1714
+ return {
1715
+ id: typeof keyframeId === "string" && keyframeId.trim().length > 0 ? keyframeId.trim() : `${clipId}:track-${trackIndex.toString().padStart(4, "0")}:point-${time.toFixed(6)}`,
1716
+ time,
1717
+ value,
1718
+ mode: normalizeStoredAnimationInterpolation(
1719
+ keyframeInterpolation ?? rawTrack.interpolation
1720
+ )
1721
+ };
1722
+ }).filter(Boolean);
1723
+ if (keyframes.length === 0) {
1724
+ return null;
1725
+ }
1726
+ keyframes.sort((left, right) => {
1727
+ if (left.time !== right.time) {
1728
+ return left.time - right.time;
1729
+ }
1730
+ return left.id.localeCompare(right.id);
1731
+ });
1732
+ const rawTrackId = rawTrack["id"];
1733
+ const rawTrackName = rawTrack["name"];
1734
+ const trackId = typeof rawTrackId === "string" && rawTrackId.trim().length > 0 ? rawTrackId.trim() : `${clipId}:track-${trackIndex.toString().padStart(4, "0")}`;
1735
+ const trackName = typeof rawTrackName === "string" && rawTrackName.trim().length > 0 ? rawTrackName.trim() : channel.replace(/^\/+/, "") || trackId;
1736
+ const denominator = durationSeconds > 0 ? durationSeconds : 1;
1737
+ return {
1738
+ id: trackId,
1739
+ name: trackName,
1740
+ animatableId: channel,
1741
+ points: keyframes.map((keyframe) => {
1742
+ const stamp = Math.max(0, Math.min(1, keyframe.time / denominator));
1743
+ const transitions = buildStoredAnimationTransitions(keyframe.mode);
1744
+ return {
1745
+ id: keyframe.id,
1746
+ stamp,
1747
+ value: keyframe.value,
1748
+ ...transitions ? { transitions } : {}
1749
+ };
1750
+ })
1751
+ };
1752
+ }).filter(Boolean);
1753
+ return {
1754
+ id: clipId,
1755
+ name: clipName,
1756
+ duration: durationMs,
1757
+ groups: {},
1758
+ tracks
1759
+ };
1760
+ }
738
1761
  function VizijRuntimeProvider({
739
1762
  assetBundle,
740
1763
  children,
741
1764
  namespace: namespaceProp,
742
1765
  faceId: faceIdProp,
1766
+ updateTier = "auto",
743
1767
  autoCreate = true,
744
1768
  createOptions,
745
1769
  autostart = false,
@@ -747,6 +1771,7 @@ function VizijRuntimeProvider({
747
1771
  mergeStrategy,
748
1772
  onRegisterControllers,
749
1773
  onStatusChange,
1774
+ transformOutputWrite,
750
1775
  orchestratorScope = "auto"
751
1776
  }) {
752
1777
  const storeRef = (0, import_react2.useRef)(null);
@@ -767,6 +1792,7 @@ function VizijRuntimeProvider({
767
1792
  assetBundle,
768
1793
  namespace: namespaceProp,
769
1794
  faceId: faceIdProp,
1795
+ updateTier,
770
1796
  autoCreate,
771
1797
  autostart,
772
1798
  driveOrchestrator,
@@ -774,6 +1800,7 @@ function VizijRuntimeProvider({
774
1800
  mergeStrategy,
775
1801
  onRegisterControllers,
776
1802
  onStatusChange,
1803
+ transformOutputWrite,
777
1804
  store: storeRef.current,
778
1805
  children
779
1806
  }
@@ -795,9 +1822,11 @@ function VizijRuntimeProviderInner({
795
1822
  assetBundle: initialAssetBundle,
796
1823
  namespace: namespaceProp,
797
1824
  faceId: faceIdProp,
1825
+ updateTier,
798
1826
  mergeStrategy,
799
1827
  onRegisterControllers,
800
1828
  onStatusChange,
1829
+ transformOutputWrite,
801
1830
  store,
802
1831
  children,
803
1832
  autoCreate,
@@ -805,35 +1834,66 @@ function VizijRuntimeProviderInner({
805
1834
  createOptions,
806
1835
  driveOrchestrator
807
1836
  }) {
1837
+ const [assetBundleOverride, setAssetBundleOverride] = (0, import_react2.useState)(null);
1838
+ const [graphUpdateToken, setGraphUpdateToken] = (0, import_react2.useState)(0);
1839
+ const effectiveAssetBundle = assetBundleOverride ?? initialAssetBundle;
1840
+ const latestEffectiveAssetBundleRef = (0, import_react2.useRef)(effectiveAssetBundle);
808
1841
  const [extractedBundle, setExtractedBundle] = (0, import_react2.useState)(() => {
809
- if (initialAssetBundle.bundle) {
810
- return initialAssetBundle.bundle;
1842
+ if (effectiveAssetBundle.bundle) {
1843
+ return effectiveAssetBundle.bundle;
811
1844
  }
812
- if (initialAssetBundle.glb.kind === "world" && initialAssetBundle.glb.bundle) {
813
- return initialAssetBundle.glb.bundle;
1845
+ if (effectiveAssetBundle.glb.kind === "world" && effectiveAssetBundle.glb.bundle) {
1846
+ return effectiveAssetBundle.glb.bundle;
814
1847
  }
815
1848
  return null;
816
1849
  });
817
1850
  const [extractedAnimations, setExtractedAnimations] = (0, import_react2.useState)([]);
1851
+ const previousBundleRef = (0, import_react2.useRef)(null);
1852
+ const suppressNextBundlePlanRef = (0, import_react2.useRef)(false);
1853
+ const pendingPlanRef = (0, import_react2.useRef)(null);
1854
+ const updateTierRef = (0, import_react2.useRef)(updateTier);
818
1855
  (0, import_react2.useEffect)(() => {
819
- if (initialAssetBundle.bundle) {
820
- setExtractedBundle(initialAssetBundle.bundle);
1856
+ if (effectiveAssetBundle.bundle) {
1857
+ setExtractedBundle(effectiveAssetBundle.bundle);
821
1858
  return;
822
1859
  }
823
- if (initialAssetBundle.glb.kind === "world") {
824
- setExtractedBundle(initialAssetBundle.glb.bundle ?? null);
1860
+ if (effectiveAssetBundle.glb.kind === "world") {
1861
+ setExtractedBundle(effectiveAssetBundle.glb.bundle ?? null);
825
1862
  } else {
826
1863
  setExtractedBundle(null);
827
1864
  }
828
- }, [initialAssetBundle]);
1865
+ }, [effectiveAssetBundle]);
1866
+ (0, import_react2.useEffect)(() => {
1867
+ updateTierRef.current = updateTier;
1868
+ }, [updateTier]);
829
1869
  const assetBundle = (0, import_react2.useMemo)(
830
1870
  () => mergeAssetBundle(
831
- initialAssetBundle,
1871
+ effectiveAssetBundle,
832
1872
  extractedBundle,
833
1873
  extractedAnimations
834
1874
  ),
835
- [initialAssetBundle, extractedBundle, extractedAnimations]
1875
+ [effectiveAssetBundle, extractedBundle, extractedAnimations]
836
1876
  );
1877
+ (0, import_react2.useEffect)(() => {
1878
+ latestEffectiveAssetBundleRef.current = effectiveAssetBundle;
1879
+ }, [effectiveAssetBundle]);
1880
+ (0, import_react2.useEffect)(() => {
1881
+ if (suppressNextBundlePlanRef.current) {
1882
+ suppressNextBundlePlanRef.current = false;
1883
+ previousBundleRef.current = effectiveAssetBundle;
1884
+ return;
1885
+ }
1886
+ const plan = resolveRuntimeUpdatePlan(
1887
+ previousBundleRef.current,
1888
+ effectiveAssetBundle,
1889
+ updateTierRef.current
1890
+ );
1891
+ pendingPlanRef.current = plan;
1892
+ previousBundleRef.current = effectiveAssetBundle;
1893
+ if (plan.reregisterGraphs) {
1894
+ setGraphUpdateToken((prev) => prev + 1);
1895
+ }
1896
+ }, [effectiveAssetBundle]);
837
1897
  const {
838
1898
  ready,
839
1899
  createOrchestrator,
@@ -842,6 +1902,7 @@ function VizijRuntimeProviderInner({
842
1902
  registerAnimation,
843
1903
  removeGraph,
844
1904
  removeAnimation,
1905
+ removeInput,
845
1906
  listControllers,
846
1907
  setInput: orchestratorSetInput,
847
1908
  getPathSnapshot,
@@ -869,13 +1930,57 @@ function VizijRuntimeProviderInner({
869
1930
  const namespaceRef = (0, import_react2.useRef)(namespace);
870
1931
  const driveOrchestratorRef = (0, import_react2.useRef)(driveOrchestrator);
871
1932
  const rigInputMapRef = (0, import_react2.useRef)({});
1933
+ const rigPoseControlInputIdsRef = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
872
1934
  const registeredGraphsRef = (0, import_react2.useRef)([]);
873
1935
  const registeredAnimationsRef = (0, import_react2.useRef)([]);
874
1936
  const mergedGraphRef = (0, import_react2.useRef)(null);
1937
+ const poseControlBridgeValuesRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
1938
+ const poseWeightFallbackMap = (0, import_react2.useMemo)(() => {
1939
+ const map = /* @__PURE__ */ new Map();
1940
+ const poseConfig = assetBundle.pose?.config;
1941
+ if (!poseConfig) {
1942
+ return map;
1943
+ }
1944
+ const posePaths = buildPoseWeightPathMap(
1945
+ poseConfig.poses ?? [],
1946
+ poseConfig.faceId ?? faceId ?? "face"
1947
+ );
1948
+ (poseConfig.poses ?? []).forEach((pose) => {
1949
+ const posePath = posePaths.get(pose.id);
1950
+ if (!posePath) {
1951
+ return;
1952
+ }
1953
+ const values = Object.fromEntries(
1954
+ Object.entries(pose.values ?? {}).filter(
1955
+ ([, value]) => Number.isFinite(value)
1956
+ )
1957
+ );
1958
+ map.set(posePath, values);
1959
+ });
1960
+ return map;
1961
+ }, [assetBundle.pose?.config, faceId]);
1962
+ const useLegacyPoseWeightFallback = (0, import_react2.useMemo)(
1963
+ () => shouldUseLegacyPoseWeightFallback(Boolean(assetBundle.pose?.graph)),
1964
+ [assetBundle.pose?.graph]
1965
+ );
1966
+ const resolvedProgramAssets = (0, import_react2.useMemo)(
1967
+ () => assetBundle.programs && assetBundle.programs.length > 0 ? assetBundle.programs : convertBundlePrograms(assetBundle.bundle?.graphs),
1968
+ [assetBundle.bundle?.graphs, assetBundle.programs]
1969
+ );
875
1970
  const [inputConstraints, setInputConstraints] = (0, import_react2.useState)({});
1971
+ const inputConstraintsRef = (0, import_react2.useRef)({});
876
1972
  const avgStepDtRef = (0, import_react2.useRef)(null);
877
1973
  const animationTweensRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
878
1974
  const clipPlaybackRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
1975
+ const programPlaybackRef = (0, import_react2.useRef)(
1976
+ /* @__PURE__ */ new Map()
1977
+ );
1978
+ const programControllerIdsRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
1979
+ const clipOutputValuesRef = (0, import_react2.useRef)(
1980
+ /* @__PURE__ */ new Map()
1981
+ );
1982
+ const clipAggregateValuesRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
1983
+ const animationSystemActiveRef = (0, import_react2.useRef)(true);
879
1984
  const stagedInputsRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
880
1985
  const autostartRef = (0, import_react2.useRef)(autostart);
881
1986
  const lastActivityTimeRef = (0, import_react2.useRef)(now());
@@ -893,6 +1998,7 @@ function VizijRuntimeProviderInner({
893
1998
  (0, import_react2.useEffect)(() => {
894
1999
  const rigAsset = assetBundle.rig;
895
2000
  if (!rigAsset) {
2001
+ inputConstraintsRef.current = {};
896
2002
  setInputConstraints({});
897
2003
  return;
898
2004
  }
@@ -905,15 +2011,8 @@ function VizijRuntimeProviderInner({
905
2011
  rigAsset.inputMetadata,
906
2012
  namespace
907
2013
  );
2014
+ inputConstraintsRef.current = constraints;
908
2015
  setInputConstraints(constraints);
909
- const isDevEnv = typeof globalThis !== "undefined" && Boolean(globalThis?.process?.env?.NODE_ENV !== "production");
910
- if (isDevEnv) {
911
- const size = Object.keys(constraints).length;
912
- console.log("[vizij-runtime] input constraints computed", size, {
913
- namespace,
914
- rigId: rigAsset.id
915
- });
916
- }
917
2016
  }, [assetBundle.rig, namespace]);
918
2017
  const requestLoopMode = (0, import_react2.useCallback)((mode) => {
919
2018
  if (!runtimeMountedRef.current) {
@@ -922,7 +2021,28 @@ function VizijRuntimeProviderInner({
922
2021
  setLoopMode((prev) => prev === mode ? prev : mode);
923
2022
  }, []);
924
2023
  const hasActiveAnimations = (0, import_react2.useCallback)(() => {
925
- return animationTweensRef.current.size > 0 || clipPlaybackRef.current.size > 0;
2024
+ if (animationTweensRef.current.size > 0) {
2025
+ return true;
2026
+ }
2027
+ if (!animationSystemActiveRef.current) {
2028
+ for (const state of programPlaybackRef.current.values()) {
2029
+ if (state.state === "playing") {
2030
+ return true;
2031
+ }
2032
+ }
2033
+ return false;
2034
+ }
2035
+ for (const state of clipPlaybackRef.current.values()) {
2036
+ if (state.playing) {
2037
+ return true;
2038
+ }
2039
+ }
2040
+ for (const state of programPlaybackRef.current.values()) {
2041
+ if (state.state === "playing") {
2042
+ return true;
2043
+ }
2044
+ }
2045
+ return false;
926
2046
  }, []);
927
2047
  const computeDesiredLoopMode = (0, import_react2.useCallback)(() => {
928
2048
  const hasAnimations = hasActiveAnimations();
@@ -944,11 +2064,50 @@ function VizijRuntimeProviderInner({
944
2064
  }, [updateLoopMode]);
945
2065
  const setInput = (0, import_react2.useCallback)(
946
2066
  (path, value, shape) => {
2067
+ const numericValue = (0, import_value_json2.valueAsNumber)(value);
2068
+ const basePath = stripNamespace(
2069
+ normalisePath(path),
2070
+ namespaceRef.current
2071
+ );
2072
+ const poseValues = useLegacyPoseWeightFallback && numericValue != null ? poseWeightFallbackMap.get(basePath) : void 0;
2073
+ if (poseValues && numericValue != null) {
2074
+ const poseFaceId = assetBundle.pose?.config?.faceId ?? faceId ?? "face";
2075
+ const rigMap = rigInputMapRef.current;
2076
+ Object.entries(poseValues).forEach(([inputId, poseValue]) => {
2077
+ if (!Number.isFinite(poseValue)) {
2078
+ return;
2079
+ }
2080
+ const controlPath = resolvePoseControlInputPath({
2081
+ inputId,
2082
+ basePath: buildRigInputPath(
2083
+ poseFaceId,
2084
+ `/pose/control/${inputId}`
2085
+ ),
2086
+ rigInputPathMap: rigMap,
2087
+ hasNativePoseControlInput: true
2088
+ }) ?? buildRigInputPath(poseFaceId, `/pose/control/${inputId}`);
2089
+ setInput(controlPath, { float: Number(poseValue) * numericValue });
2090
+ });
2091
+ return;
2092
+ }
947
2093
  markActivity();
948
2094
  const namespacedPath = namespaceTypedPath(path, namespaceRef.current);
2095
+ if (isRuntimeDebugEnabled() && (namespacedPath.includes("animation/authoring.timeline.main") || namespacedPath.endsWith("/blink"))) {
2096
+ console.log("[vizij-runtime] stage input", {
2097
+ path,
2098
+ namespacedPath,
2099
+ value
2100
+ });
2101
+ }
949
2102
  stagedInputsRef.current.set(namespacedPath, { value, shape });
950
2103
  },
951
- [markActivity]
2104
+ [
2105
+ assetBundle.pose?.config?.faceId,
2106
+ faceId,
2107
+ markActivity,
2108
+ poseWeightFallbackMap,
2109
+ useLegacyPoseWeightFallback
2110
+ ]
952
2111
  );
953
2112
  const reportStatus = (0, import_react2.useCallback)(
954
2113
  (updater) => {
@@ -1012,10 +2171,14 @@ function VizijRuntimeProviderInner({
1012
2171
  });
1013
2172
  registeredGraphsRef.current = [];
1014
2173
  registeredAnimationsRef.current = [];
2174
+ programControllerIdsRef.current.clear();
1015
2175
  mergedGraphRef.current = null;
1016
2176
  outputPathsRef.current = /* @__PURE__ */ new Set();
1017
2177
  baseOutputPathsRef.current = /* @__PURE__ */ new Set();
1018
2178
  namespacedOutputPathsRef.current = /* @__PURE__ */ new Set();
2179
+ rigPoseControlInputIdsRef.current = /* @__PURE__ */ new Set();
2180
+ clipOutputValuesRef.current.clear();
2181
+ clipAggregateValuesRef.current.clear();
1019
2182
  }, [listControllers, removeAnimation, removeGraph, pushError]);
1020
2183
  (0, import_react2.useEffect)(() => {
1021
2184
  namespaceRef.current = namespace;
@@ -1028,10 +2191,19 @@ function VizijRuntimeProviderInner({
1028
2191
  (0, import_react2.useEffect)(() => {
1029
2192
  driveOrchestratorRef.current = driveOrchestrator;
1030
2193
  }, [driveOrchestrator]);
1031
- const glbAsset = initialAssetBundle.glb;
1032
- const baseBundle = initialAssetBundle.bundle ?? null;
2194
+ const glbAsset = effectiveAssetBundle.glb;
2195
+ const baseBundle = effectiveAssetBundle.bundle ?? null;
1033
2196
  (0, import_react2.useEffect)(() => {
1034
2197
  let cancelled = false;
2198
+ const plan = pendingPlanRef.current;
2199
+ if (plan && !plan.reloadAssets && status.rootId !== null) {
2200
+ reportStatus(
2201
+ (prev) => prev.loading ? { ...prev, loading: false } : prev
2202
+ );
2203
+ return () => {
2204
+ cancelled = true;
2205
+ };
2206
+ }
1035
2207
  resetErrors();
1036
2208
  reportStatus((prev) => ({
1037
2209
  ...prev,
@@ -1083,6 +2255,12 @@ function VizijRuntimeProviderInner({
1083
2255
  setExtractedAnimations(convertExtractedAnimations(gltfAnimations));
1084
2256
  const rootId = findRootId(world);
1085
2257
  store.getState().addWorldElements(world, animatables, true);
2258
+ if (pendingPlanRef.current?.reloadAssets) {
2259
+ pendingPlanRef.current = {
2260
+ ...pendingPlanRef.current,
2261
+ reloadAssets: false
2262
+ };
2263
+ }
1086
2264
  reportStatus((prev) => ({
1087
2265
  ...prev,
1088
2266
  loading: false,
@@ -1120,7 +2298,8 @@ function VizijRuntimeProviderInner({
1120
2298
  reportStatus,
1121
2299
  resetErrors,
1122
2300
  setExtractedBundle,
1123
- setExtractedAnimations
2301
+ setExtractedAnimations,
2302
+ status.rootId
1124
2303
  ]);
1125
2304
  (0, import_react2.useEffect)(() => {
1126
2305
  if (!ready && autoCreate) {
@@ -1136,6 +2315,15 @@ function VizijRuntimeProviderInner({
1136
2315
  }, [ready, autoCreate, createOptions, createOrchestrator, pushError]);
1137
2316
  const registerControllers = (0, import_react2.useCallback)(async () => {
1138
2317
  clearControllers();
2318
+ if (isRuntimeDebugEnabled()) {
2319
+ console.log("[vizij-runtime] registerControllers", {
2320
+ hasRig: Boolean(assetBundle.rig),
2321
+ hasPose: Boolean(assetBundle.pose?.graph),
2322
+ animationCount: assetBundle.animations?.length ?? 0,
2323
+ animationIds: (assetBundle.animations ?? []).map((anim) => anim.id),
2324
+ namespace
2325
+ });
2326
+ }
1139
2327
  const baseOutputPaths = /* @__PURE__ */ new Set();
1140
2328
  const namespacedOutputPaths = /* @__PURE__ */ new Set();
1141
2329
  const recordOutputs = (paths) => {
@@ -1147,39 +2335,60 @@ function VizijRuntimeProviderInner({
1147
2335
  namespacedOutputPaths.add(namespaceTypedPath(trimmed, namespace));
1148
2336
  });
1149
2337
  };
2338
+ const graphConfigs = [];
2339
+ rigInputMapRef.current = {};
2340
+ rigPoseControlInputIdsRef.current = /* @__PURE__ */ new Set();
2341
+ poseControlBridgeValuesRef.current.clear();
1150
2342
  const rigAsset = assetBundle.rig;
1151
- if (!rigAsset) {
1152
- pushError({
1153
- message: "Asset bundle is missing a rig graph.",
1154
- phase: "registration",
1155
- timestamp: performance.now()
1156
- });
1157
- return;
1158
- }
1159
- const rigSpec = resolveGraphSpec(rigAsset, `${rigAsset.id ?? "rig"} graph`);
1160
- if (!rigSpec) {
1161
- pushError({
1162
- message: "Rig graph is missing a usable spec or IR payload.",
1163
- phase: "registration",
1164
- timestamp: performance.now()
1165
- });
1166
- return;
1167
- }
1168
- const rigOutputs = collectOutputPaths(rigSpec);
1169
- const rigInputs = collectInputPaths(rigSpec);
1170
- rigInputMapRef.current = collectInputPathMap(rigSpec);
1171
- recordOutputs(rigOutputs);
1172
- const rigSubs = rigAsset.subscriptions ?? {
1173
- inputs: rigInputs,
1174
- outputs: rigOutputs
1175
- };
1176
- const graphConfigs = [
1177
- {
1178
- id: namespaceControllerId(rigAsset.id, namespace, "graph"),
1179
- spec: namespaceGraphSpec(rigSpec, namespace),
1180
- subs: namespaceSubscriptions(rigSubs, namespace)
2343
+ if (rigAsset) {
2344
+ const rigSpec = resolveGraphSpec(
2345
+ rigAsset,
2346
+ `${rigAsset.id ?? "rig"} graph`
2347
+ );
2348
+ if (!rigSpec) {
2349
+ pushError({
2350
+ message: "Rig graph is missing a usable spec or IR payload.",
2351
+ phase: "registration",
2352
+ timestamp: performance.now()
2353
+ });
2354
+ } else {
2355
+ const rigOutputs = collectOutputPaths(rigSpec);
2356
+ const rigInputs = collectInputPaths(rigSpec);
2357
+ const rigPoseControlInputIds = /* @__PURE__ */ new Set();
2358
+ rigInputs.forEach((path) => {
2359
+ const poseControlMatch = /^rig\/[^/]+\/pose\/control\/(.+)$/.exec(
2360
+ path.trim()
2361
+ );
2362
+ const inputId = (poseControlMatch?.[1] ?? "").trim();
2363
+ if (inputId.length > 0) {
2364
+ rigPoseControlInputIds.add(inputId);
2365
+ }
2366
+ });
2367
+ rigInputMapRef.current = collectInputPathMap(rigSpec);
2368
+ rigPoseControlInputIdsRef.current = rigPoseControlInputIds;
2369
+ if (isRuntimeDebugEnabled()) {
2370
+ const blinkKeys = Object.keys(rigInputMapRef.current).filter(
2371
+ (key) => key.toLowerCase().includes("blink")
2372
+ );
2373
+ const blinkMappings = blinkKeys.slice(0, 20).map((key) => `${key} => ${rigInputMapRef.current[key] ?? "?"}`);
2374
+ console.log("[vizij-runtime] rig input map sample", {
2375
+ blink: rigInputMapRef.current["blink"] ?? null,
2376
+ blinkKeys: blinkKeys.slice(0, 12),
2377
+ blinkMappings: blinkMappings.join(" | ")
2378
+ });
2379
+ }
2380
+ recordOutputs(rigOutputs);
2381
+ const rigSubs = rigAsset.subscriptions ?? {
2382
+ inputs: rigInputs,
2383
+ outputs: rigOutputs
2384
+ };
2385
+ graphConfigs.push({
2386
+ id: namespaceControllerId(rigAsset.id, namespace, "graph"),
2387
+ spec: stripNulls(namespaceGraphSpec(rigSpec, namespace)),
2388
+ subs: namespaceSubscriptions(rigSubs, namespace)
2389
+ });
1181
2390
  }
1182
- ];
2391
+ }
1183
2392
  const poseGraphAsset = assetBundle.pose?.graph;
1184
2393
  if (poseGraphAsset) {
1185
2394
  const poseSpec = resolveGraphSpec(
@@ -1196,7 +2405,7 @@ function VizijRuntimeProviderInner({
1196
2405
  };
1197
2406
  graphConfigs.push({
1198
2407
  id: namespaceControllerId(poseGraphAsset.id, namespace, "graph"),
1199
- spec: namespaceGraphSpec(poseSpec, namespace),
2408
+ spec: stripNulls(namespaceGraphSpec(poseSpec, namespace)),
1200
2409
  subs: namespaceSubscriptions(poseSubs, namespace)
1201
2410
  });
1202
2411
  } else {
@@ -1205,6 +2414,31 @@ function VizijRuntimeProviderInner({
1205
2414
  );
1206
2415
  }
1207
2416
  }
2417
+ for (const animation of assetBundle.animations ?? []) {
2418
+ const bridgeOutputs = collectAnimationClipOutputPaths(
2419
+ animation.clip,
2420
+ faceId ?? void 0,
2421
+ rigInputMapRef.current
2422
+ );
2423
+ if (isRuntimeDebugEnabled()) {
2424
+ console.log("[vizij-runtime] animation output routing", {
2425
+ animationId: animation.id,
2426
+ bridgeOutputs,
2427
+ bridgeOutputsText: bridgeOutputs.join(" | ")
2428
+ });
2429
+ }
2430
+ recordOutputs(bridgeOutputs);
2431
+ }
2432
+ for (const program of resolvedProgramAssets) {
2433
+ const programSpec = resolveGraphSpec(
2434
+ program.graph,
2435
+ `${program.id ?? "program"} graph (outputs)`
2436
+ );
2437
+ if (!programSpec) {
2438
+ continue;
2439
+ }
2440
+ recordOutputs(collectOutputPaths(programSpec));
2441
+ }
1208
2442
  outputPathsRef.current = namespacedOutputPaths;
1209
2443
  baseOutputPathsRef.current = baseOutputPaths;
1210
2444
  namespacedOutputPathsRef.current = namespacedOutputPaths;
@@ -1237,20 +2471,30 @@ function VizijRuntimeProviderInner({
1237
2471
  });
1238
2472
  }
1239
2473
  registeredGraphsRef.current = graphIds;
2474
+ if (isRuntimeDebugEnabled()) {
2475
+ console.log("[vizij-runtime] registered graph ids", graphIds);
2476
+ }
1240
2477
  const animationIds = [];
1241
2478
  for (const anim of assetBundle.animations ?? []) {
1242
2479
  try {
1243
2480
  const controllerId = namespaceControllerId(anim.id, namespace, "animation") ?? anim.id;
2481
+ const animationPayload = anim.setup?.animation ?? toStoredAnimationClip(anim.id, anim.clip);
1244
2482
  const config = {
1245
2483
  id: controllerId,
1246
2484
  setup: {
1247
- animation: anim.clip,
1248
- ...anim.setup ?? {}
2485
+ ...anim.setup ?? {},
2486
+ animation: animationPayload
1249
2487
  }
1250
2488
  };
1251
2489
  const id = registerAnimation(config);
1252
2490
  animationIds.push(id);
1253
2491
  } catch (err) {
2492
+ if (isRuntimeDebugEnabled()) {
2493
+ console.warn("[vizij-runtime] failed animation registration", {
2494
+ animationId: anim.id,
2495
+ error: err instanceof Error ? err.message : String(err)
2496
+ });
2497
+ }
1254
2498
  pushError({
1255
2499
  message: `Failed to register animation ${anim.id}`,
1256
2500
  cause: err,
@@ -1275,6 +2519,13 @@ function VizijRuntimeProviderInner({
1275
2519
  });
1276
2520
  }
1277
2521
  const controllers = listControllers();
2522
+ if (isRuntimeDebugEnabled()) {
2523
+ console.log("[vizij-runtime] controllers after register", {
2524
+ controllers,
2525
+ graphIds,
2526
+ animationIds
2527
+ });
2528
+ }
1278
2529
  reportStatus((prev) => ({
1279
2530
  ...prev,
1280
2531
  ready: true,
@@ -1294,12 +2545,18 @@ function VizijRuntimeProviderInner({
1294
2545
  registerGraph,
1295
2546
  registerMergedGraph,
1296
2547
  reportStatus,
2548
+ resolvedProgramAssets,
1297
2549
  setInput
1298
2550
  ]);
1299
2551
  (0, import_react2.useEffect)(() => {
1300
2552
  if (!ready || status.loading) {
1301
2553
  return;
1302
2554
  }
2555
+ const plan = pendingPlanRef.current;
2556
+ const hasRegistered = registeredGraphsRef.current.length > 0 || registeredAnimationsRef.current.length > 0 || programControllerIdsRef.current.size > 0;
2557
+ if (plan && !plan.reregisterGraphs && hasRegistered) {
2558
+ return;
2559
+ }
1303
2560
  registerControllers().catch((err) => {
1304
2561
  pushError({
1305
2562
  message: "Failed to register controllers",
@@ -1308,7 +2565,7 @@ function VizijRuntimeProviderInner({
1308
2565
  timestamp: performance.now()
1309
2566
  });
1310
2567
  });
1311
- }, [ready, status.loading, registerControllers, pushError]);
2568
+ }, [ready, status.loading, graphUpdateToken, registerControllers, pushError]);
1312
2569
  (0, import_react2.useEffect)(() => {
1313
2570
  if (!frame) {
1314
2571
  return;
@@ -1319,26 +2576,66 @@ function VizijRuntimeProviderInner({
1319
2576
  }
1320
2577
  const setWorldValues = store.getState().setValues;
1321
2578
  const namespaceValue = status.namespace;
2579
+ const currentValues = store.getState().values;
2580
+ const rigInputPathMap = rigInputMapRef.current;
2581
+ const rigPoseControlInputIds = rigPoseControlInputIdsRef.current;
1322
2582
  const batched = [];
1323
2583
  const namespacedOutputs = namespacedOutputPathsRef.current;
1324
2584
  const baseOutputs = baseOutputPathsRef.current;
1325
2585
  writes.forEach((write) => {
1326
2586
  const path = normalisePath(write.path);
1327
- if (!namespacedOutputs.has(path)) {
2587
+ const basePath = stripNamespace(path, namespaceValue);
2588
+ const isTrackedOutput = namespacedOutputs.has(path) || baseOutputs.has(basePath);
2589
+ if (!isTrackedOutput) {
1328
2590
  return;
1329
2591
  }
1330
2592
  const raw = valueJSONToRaw(write.value);
1331
2593
  if (raw === void 0) {
1332
2594
  return;
1333
2595
  }
1334
- const basePath = stripNamespace(path, namespaceValue);
2596
+ const poseControlMatch = /^rig\/[^/]+\/pose\/control\/(.+)$/.exec(
2597
+ basePath
2598
+ );
2599
+ if (poseControlMatch && typeof raw === "number" && Number.isFinite(raw)) {
2600
+ const inputId = (poseControlMatch[1] ?? "").trim();
2601
+ const hasNativePoseControlInput = inputId.length > 0 && rigPoseControlInputIds.has(inputId);
2602
+ const mappedInputPath = inputId.length === 0 ? void 0 : resolvePoseControlInputPath({
2603
+ inputId,
2604
+ basePath,
2605
+ rigInputPathMap,
2606
+ hasNativePoseControlInput
2607
+ });
2608
+ if (mappedInputPath) {
2609
+ const bridgeKey = `${namespaceValue}:${mappedInputPath}`;
2610
+ const previousValue = poseControlBridgeValuesRef.current.get(bridgeKey);
2611
+ if (previousValue === void 0 || Math.abs(previousValue - raw) > POSE_CONTROL_BRIDGE_EPSILON) {
2612
+ poseControlBridgeValuesRef.current.set(bridgeKey, raw);
2613
+ setInput(mappedInputPath, { float: raw });
2614
+ }
2615
+ }
2616
+ }
1335
2617
  const targetPath = baseOutputs.has(basePath) ? basePath : path;
1336
- batched.push({ id: targetPath, namespace: namespaceValue, value: raw });
2618
+ const currentValue = currentValues.get(
2619
+ (0, import_utils.getLookup)(namespaceValue, targetPath)
2620
+ );
2621
+ const nextWrite = transformOutputWrite ? transformOutputWrite({
2622
+ id: targetPath,
2623
+ namespace: namespaceValue,
2624
+ value: raw,
2625
+ currentValue
2626
+ }) : {
2627
+ id: targetPath,
2628
+ namespace: namespaceValue,
2629
+ value: raw
2630
+ };
2631
+ if (nextWrite) {
2632
+ batched.push(nextWrite);
2633
+ }
1337
2634
  });
1338
2635
  if (batched.length > 0) {
1339
2636
  setWorldValues(batched);
1340
2637
  }
1341
- }, [frame, status.namespace, store]);
2638
+ }, [frame, status.namespace, store, transformOutputWrite]);
1342
2639
  const stagePoseNeutral = (0, import_react2.useCallback)(
1343
2640
  (force = false) => {
1344
2641
  const neutral = assetBundle.pose?.config?.neutralInputs ?? {};
@@ -1406,88 +2703,237 @@ function VizijRuntimeProviderInner({
1406
2703
  },
1407
2704
  [setInput]
1408
2705
  );
1409
- const sampleTrack = (0, import_react2.useCallback)(
1410
- (track, time) => {
1411
- const keyframes = Array.isArray(track.keyframes) ? track.keyframes : [];
1412
- if (!keyframes.length) {
1413
- return 0;
1414
- }
1415
- const first = keyframes[0];
1416
- if (!first) {
1417
- return 0;
1418
- }
1419
- if (time <= Number(first.time ?? 0)) {
1420
- return Number(first.value ?? 0);
1421
- }
1422
- for (let i = 0; i < keyframes.length - 1; i += 1) {
1423
- const current = keyframes[i] ?? {};
1424
- const next = keyframes[i + 1] ?? {};
1425
- const start = Number(current.time ?? 0);
1426
- const end = Number(next.time ?? start);
1427
- if (time >= start && time <= end) {
1428
- const range = end - start || 1;
1429
- const factor = (time - start) / range;
1430
- const currentValue = Number(current.value ?? 0);
1431
- const nextValue = Number(next.value ?? currentValue);
1432
- return currentValue + (nextValue - currentValue) * factor;
2706
+ const resolveClipById = (0, import_react2.useCallback)(
2707
+ (id) => {
2708
+ return assetBundle.animations?.find((anim) => anim.id === id);
2709
+ },
2710
+ [assetBundle.animations]
2711
+ );
2712
+ const resolveClipPromise = (0, import_react2.useCallback)((state) => {
2713
+ state.resolve?.();
2714
+ state.resolve = null;
2715
+ state.completion = null;
2716
+ }, []);
2717
+ const ensureClipPromise = (0, import_react2.useCallback)((state) => {
2718
+ if (state.completion) {
2719
+ return state.completion;
2720
+ }
2721
+ const completion = new Promise((resolve) => {
2722
+ state.resolve = resolve;
2723
+ });
2724
+ state.completion = completion;
2725
+ return completion;
2726
+ }, []);
2727
+ const setAnimationInput = (0, import_react2.useCallback)(
2728
+ (path, value, options) => {
2729
+ setInput(path, { float: value });
2730
+ if (!options?.immediate) {
2731
+ return;
2732
+ }
2733
+ const namespacedPath = namespaceTypedPath(path, namespaceRef.current);
2734
+ const staged = stagedInputsRef.current.get(namespacedPath);
2735
+ if (staged) {
2736
+ orchestratorSetInput(namespacedPath, staged.value, staged.shape);
2737
+ stagedInputsRef.current.delete(namespacedPath);
2738
+ return;
2739
+ }
2740
+ orchestratorSetInput(namespacedPath, { float: value });
2741
+ },
2742
+ [orchestratorSetInput, setInput]
2743
+ );
2744
+ const clearAnimationInput = (0, import_react2.useCallback)(
2745
+ (path) => {
2746
+ const namespacedPath = namespaceTypedPath(path, namespaceRef.current);
2747
+ stagedInputsRef.current.delete(namespacedPath);
2748
+ removeInput(namespacedPath);
2749
+ },
2750
+ [removeInput]
2751
+ );
2752
+ const buildClipOutputValues = (0, import_react2.useCallback)(
2753
+ (clip, state) => sampleAnimationClipOutputValues(
2754
+ clip.clip,
2755
+ state.time,
2756
+ state.weight,
2757
+ faceId ?? void 0,
2758
+ rigInputMapRef.current
2759
+ ),
2760
+ [faceId]
2761
+ );
2762
+ const computeClipAggregateValues = (0, import_react2.useCallback)(() => {
2763
+ const aggregate = /* @__PURE__ */ new Map();
2764
+ clipOutputValuesRef.current.forEach((outputValues) => {
2765
+ outputValues.forEach((value, path) => {
2766
+ aggregate.set(path, (aggregate.get(path) ?? 0) + value);
2767
+ });
2768
+ });
2769
+ return aggregate;
2770
+ }, []);
2771
+ const stageClipAggregateValues = (0, import_react2.useCallback)(
2772
+ (nextAggregate, options) => {
2773
+ diffAnimationAggregateValues(
2774
+ clipAggregateValuesRef.current,
2775
+ nextAggregate,
2776
+ POSE_CONTROL_BRIDGE_EPSILON
2777
+ ).forEach((operation) => {
2778
+ if (operation.kind === "clear") {
2779
+ clearAnimationInput(operation.path);
2780
+ return;
1433
2781
  }
2782
+ setAnimationInput(operation.path, operation.value, options);
2783
+ });
2784
+ clipAggregateValuesRef.current = nextAggregate;
2785
+ },
2786
+ [clearAnimationInput, setAnimationInput]
2787
+ );
2788
+ const syncClipOutputs = (0, import_react2.useCallback)(
2789
+ (options) => {
2790
+ stageClipAggregateValues(computeClipAggregateValues(), options);
2791
+ },
2792
+ [computeClipAggregateValues, stageClipAggregateValues]
2793
+ );
2794
+ const writeClipOutputs = (0, import_react2.useCallback)(
2795
+ (clip, state, options) => {
2796
+ if (!animationSystemActiveRef.current) {
2797
+ return;
1434
2798
  }
1435
- const last = keyframes[keyframes.length - 1];
1436
- return Number(last?.value ?? 0);
2799
+ clipOutputValuesRef.current.set(
2800
+ clip.id,
2801
+ buildClipOutputValues(clip, state)
2802
+ );
2803
+ syncClipOutputs(options);
2804
+ },
2805
+ [buildClipOutputValues, syncClipOutputs]
2806
+ );
2807
+ const clearClipOutputs = (0, import_react2.useCallback)(
2808
+ (clipId, options) => {
2809
+ if (!clipOutputValuesRef.current.has(clipId)) {
2810
+ return;
2811
+ }
2812
+ clipOutputValuesRef.current.delete(clipId);
2813
+ syncClipOutputs(options);
2814
+ },
2815
+ [syncClipOutputs]
2816
+ );
2817
+ const createClipPlaybackState = (0, import_react2.useCallback)(
2818
+ (clip) => {
2819
+ const duration = resolveClipDurationSeconds(
2820
+ clip.clip
2821
+ );
2822
+ return {
2823
+ id: clip.id,
2824
+ time: 0,
2825
+ duration,
2826
+ speed: 1,
2827
+ weight: Number.isFinite(clip.weight) ? Number(clip.weight) : 1,
2828
+ loop: false,
2829
+ playing: false,
2830
+ resolve: null,
2831
+ completion: null
2832
+ };
1437
2833
  },
1438
2834
  []
1439
2835
  );
2836
+ const ensureClipPlaybackState = (0, import_react2.useCallback)(
2837
+ (id) => {
2838
+ const clip = resolveClipById(id);
2839
+ if (!clip) {
2840
+ return null;
2841
+ }
2842
+ const existing = clipPlaybackRef.current.get(id);
2843
+ if (existing) {
2844
+ existing.duration = resolveClipDurationSeconds(
2845
+ clip.clip,
2846
+ existing.duration
2847
+ );
2848
+ existing.time = clampAnimationTime(existing.time, existing.duration);
2849
+ return { clip, state: existing };
2850
+ }
2851
+ const next = createClipPlaybackState(clip);
2852
+ clipPlaybackRef.current.set(id, next);
2853
+ return { clip, state: next };
2854
+ },
2855
+ [createClipPlaybackState, resolveClipById]
2856
+ );
1440
2857
  const advanceClipPlayback = (0, import_react2.useCallback)(
1441
2858
  (dt) => {
1442
2859
  if (clipPlaybackRef.current.size === 0) {
1443
2860
  return;
1444
2861
  }
1445
- const map = clipPlaybackRef.current;
1446
2862
  const toDelete = [];
1447
- map.forEach((state, key) => {
1448
- state.time += dt * state.speed;
1449
- const clip = assetBundle.animations?.find(
1450
- (anim) => anim.id === state.id
1451
- );
2863
+ clipPlaybackRef.current.forEach((state, key) => {
2864
+ const clip = resolveClipById(state.id);
1452
2865
  if (!clip) {
1453
2866
  toDelete.push(key);
1454
- state.resolve();
2867
+ resolveClipPromise(state);
1455
2868
  return;
1456
2869
  }
1457
- const clipData = clip.clip;
1458
- const duration = Number(clipData?.duration ?? state.duration);
1459
- state.duration = Number.isFinite(duration) && duration > 0 ? duration : state.duration;
1460
- if (state.time >= state.duration) {
1461
- toDelete.push(key);
1462
- state.time = state.duration;
2870
+ state.duration = resolveClipDurationSeconds(
2871
+ clip.clip,
2872
+ state.duration
2873
+ );
2874
+ const { time, completed } = advanceClipTime(
2875
+ {
2876
+ time: state.time,
2877
+ duration: state.duration,
2878
+ speed: state.speed,
2879
+ loop: state.loop,
2880
+ playing: state.playing
2881
+ },
2882
+ dt
2883
+ );
2884
+ state.time = clampAnimationTime(time, state.duration);
2885
+ if (state.playing || completed) {
2886
+ writeClipOutputs(clip, state);
1463
2887
  }
1464
- const tracks = Array.isArray(clipData?.tracks) ? clipData.tracks : [];
1465
- tracks.forEach((track) => {
1466
- const value = sampleTrack(track, state.time) * state.weight;
1467
- const path = `animation/${clip.id}/${track.channel}`;
1468
- setInput(path, { float: value });
1469
- });
1470
- if (toDelete.includes(key)) {
1471
- state.resolve();
2888
+ if (completed) {
2889
+ toDelete.push(key);
2890
+ resolveClipPromise(state);
1472
2891
  }
1473
2892
  });
1474
2893
  toDelete.forEach((key) => {
1475
2894
  clipPlaybackRef.current.delete(key);
1476
- const clip = assetBundle.animations?.find((anim) => anim.id === key);
1477
- if (clip) {
1478
- const clipData = clip.clip;
1479
- const tracks = Array.isArray(clipData?.tracks) ? clipData.tracks : [];
1480
- tracks.forEach((track) => {
1481
- const path = `animation/${clip.id}/${track.channel}`;
1482
- setInput(path, { float: 0 });
1483
- });
2895
+ if (animationSystemActiveRef.current) {
2896
+ clearClipOutputs(key);
1484
2897
  }
1485
2898
  });
1486
2899
  },
1487
- [assetBundle.animations, sampleTrack, setInput]
2900
+ [clearClipOutputs, resolveClipById, resolveClipPromise, writeClipOutputs]
1488
2901
  );
1489
2902
  const animateValue = (0, import_react2.useCallback)(
1490
2903
  (path, target, options) => {
2904
+ const targetValue = (0, import_value_json2.valueAsNumber)(target);
2905
+ const basePath = stripNamespace(
2906
+ normalisePath(path),
2907
+ namespaceRef.current
2908
+ );
2909
+ const poseValues = useLegacyPoseWeightFallback && targetValue != null ? poseWeightFallbackMap.get(basePath) : void 0;
2910
+ if (poseValues && targetValue != null) {
2911
+ const poseFaceId = assetBundle.pose?.config?.faceId ?? faceId ?? "face";
2912
+ const rigMap = rigInputMapRef.current;
2913
+ return Promise.all(
2914
+ Object.entries(poseValues).flatMap(([inputId, poseValue]) => {
2915
+ if (!Number.isFinite(poseValue)) {
2916
+ return [];
2917
+ }
2918
+ const controlPath = resolvePoseControlInputPath({
2919
+ inputId,
2920
+ basePath: buildRigInputPath(
2921
+ poseFaceId,
2922
+ `/pose/control/${inputId}`
2923
+ ),
2924
+ rigInputPathMap: rigMap,
2925
+ hasNativePoseControlInput: true
2926
+ }) ?? buildRigInputPath(poseFaceId, `/pose/control/${inputId}`);
2927
+ return [
2928
+ animateValue(
2929
+ controlPath,
2930
+ { float: Number(poseValue) * targetValue },
2931
+ options
2932
+ )
2933
+ ];
2934
+ })
2935
+ ).then(() => void 0);
2936
+ }
1491
2937
  const easing = resolveEasing(options?.easing);
1492
2938
  const duration = Math.max(0, options?.duration ?? DEFAULT_DURATION);
1493
2939
  cancelAnimation(path);
@@ -1501,7 +2947,9 @@ function VizijRuntimeProviderInner({
1501
2947
  }
1502
2948
  return new Promise((resolve) => {
1503
2949
  animationTweensRef.current.set(path, {
1504
- path: namespacedPath,
2950
+ // Keep the raw path here so tween updates go through setInput() once
2951
+ // and pick up the active namespace exactly once.
2952
+ path,
1505
2953
  from: fromValue,
1506
2954
  to: toValue,
1507
2955
  duration,
@@ -1512,54 +2960,377 @@ function VizijRuntimeProviderInner({
1512
2960
  markActivity();
1513
2961
  });
1514
2962
  },
1515
- [cancelAnimation, getPathSnapshot, markActivity, setInput]
2963
+ [
2964
+ assetBundle.pose?.config?.faceId,
2965
+ cancelAnimation,
2966
+ faceId,
2967
+ getPathSnapshot,
2968
+ markActivity,
2969
+ poseWeightFallbackMap,
2970
+ setInput,
2971
+ useLegacyPoseWeightFallback
2972
+ ]
1516
2973
  );
1517
2974
  const playAnimation = (0, import_react2.useCallback)(
1518
2975
  (id, options) => {
1519
- const clip = assetBundle.animations?.find((anim) => anim.id === id);
1520
- if (!clip) {
2976
+ const ensured = ensureClipPlaybackState(id);
2977
+ if (!ensured) {
1521
2978
  return Promise.reject(
1522
2979
  new Error(`Animation ${id} is not part of the current asset bundle.`)
1523
2980
  );
1524
2981
  }
1525
- if (clipPlaybackRef.current.has(id)) {
1526
- clipPlaybackRef.current.delete(id);
2982
+ const { clip, state } = ensured;
2983
+ const shouldReset = options?.reset === true;
2984
+ if (shouldReset) {
2985
+ resolveClipPromise(state);
2986
+ state.time = 0;
1527
2987
  }
1528
- return new Promise((resolve) => {
1529
- const speed = options?.speed ?? 1;
1530
- const weight = options?.weight ?? clip.weight ?? 1;
1531
- const clipData = clip.clip;
1532
- clipPlaybackRef.current.set(id, {
1533
- id,
1534
- time: options?.reset ? 0 : 0,
1535
- duration: Number(clipData?.duration ?? 0),
1536
- speed: Number.isFinite(speed) && speed > 0 ? speed : 1,
1537
- weight,
1538
- resolve
1539
- });
1540
- markActivity();
1541
- });
2988
+ const speed = options?.speed ?? state.speed ?? 1;
2989
+ const weight = options?.weight ?? state.weight ?? clip.weight ?? 1;
2990
+ state.speed = Number.isFinite(speed) && speed > 0 ? speed : 1;
2991
+ state.weight = Number.isFinite(weight) ? Number(weight) : 1;
2992
+ state.duration = resolveClipDurationSeconds(
2993
+ clip.clip,
2994
+ state.duration
2995
+ );
2996
+ state.time = clampAnimationTime(state.time, state.duration);
2997
+ state.playing = true;
2998
+ const completion = ensureClipPromise(state);
2999
+ clipPlaybackRef.current.set(id, state);
3000
+ writeClipOutputs(clip, state);
3001
+ markActivity();
3002
+ return completion;
1542
3003
  },
1543
- [assetBundle.animations, markActivity]
3004
+ [
3005
+ ensureClipPlaybackState,
3006
+ ensureClipPromise,
3007
+ markActivity,
3008
+ resolveClipPromise,
3009
+ writeClipOutputs
3010
+ ]
1544
3011
  );
1545
- const stopAnimation = (0, import_react2.useCallback)(
3012
+ const pauseAnimation = (0, import_react2.useCallback)(
1546
3013
  (id) => {
1547
- const clip = assetBundle.animations?.find((anim) => anim.id === id);
3014
+ const state = clipPlaybackRef.current.get(id);
3015
+ if (!state || !state.playing) {
3016
+ return;
3017
+ }
3018
+ state.playing = false;
3019
+ updateLoopMode();
3020
+ },
3021
+ [updateLoopMode]
3022
+ );
3023
+ const seekAnimation = (0, import_react2.useCallback)(
3024
+ (id, timeSeconds) => {
3025
+ const ensured = ensureClipPlaybackState(id);
3026
+ if (!ensured) {
3027
+ return;
3028
+ }
3029
+ const { clip, state } = ensured;
3030
+ state.time = clampAnimationTime(timeSeconds, state.duration);
3031
+ clipPlaybackRef.current.set(id, state);
3032
+ writeClipOutputs(clip, state, { immediate: true });
3033
+ },
3034
+ [ensureClipPlaybackState, writeClipOutputs]
3035
+ );
3036
+ const setAnimationLoop = (0, import_react2.useCallback)(
3037
+ (id, enabled) => {
3038
+ const ensured = ensureClipPlaybackState(id);
3039
+ if (!ensured) {
3040
+ return;
3041
+ }
3042
+ ensured.state.loop = Boolean(enabled);
3043
+ clipPlaybackRef.current.set(id, ensured.state);
3044
+ updateLoopMode();
3045
+ },
3046
+ [ensureClipPlaybackState, updateLoopMode]
3047
+ );
3048
+ const getAnimationState = (0, import_react2.useCallback)(
3049
+ (id) => {
3050
+ const state = clipPlaybackRef.current.get(id);
3051
+ if (!state) {
3052
+ return null;
3053
+ }
3054
+ return {
3055
+ time: state.time,
3056
+ duration: state.duration,
3057
+ playing: state.playing,
3058
+ loop: state.loop,
3059
+ speed: state.speed
3060
+ };
3061
+ },
3062
+ []
3063
+ );
3064
+ const stopAnimation = (0, import_react2.useCallback)(
3065
+ (id, options) => {
1548
3066
  const state = clipPlaybackRef.current.get(id);
1549
3067
  if (state) {
1550
3068
  clipPlaybackRef.current.delete(id);
1551
- state.resolve();
3069
+ state.playing = false;
3070
+ resolveClipPromise(state);
1552
3071
  }
1553
- if (clip) {
1554
- const clipData = clip.clip;
1555
- const tracks = Array.isArray(clipData?.tracks) ? clipData.tracks : [];
1556
- tracks.forEach((track) => {
1557
- const path = `animation/${clip.id}/${track.channel}`;
1558
- setInput(path, { float: 0 });
3072
+ if (options?.clearOutputs !== false) {
3073
+ clearClipOutputs(id);
3074
+ }
3075
+ updateLoopMode();
3076
+ },
3077
+ [clearClipOutputs, resolveClipPromise, updateLoopMode]
3078
+ );
3079
+ const refreshControllerStatus = (0, import_react2.useCallback)(() => {
3080
+ const controllers = listControllers();
3081
+ reportStatus((prev) => ({
3082
+ ...prev,
3083
+ controllers,
3084
+ outputPaths: Array.from(outputPathsRef.current)
3085
+ }));
3086
+ onRegisterControllers?.(controllers);
3087
+ }, [listControllers, onRegisterControllers, reportStatus]);
3088
+ const resolveProgramById = (0, import_react2.useCallback)(
3089
+ (id) => {
3090
+ return resolvedProgramAssets.find((program) => program.id === id);
3091
+ },
3092
+ [resolvedProgramAssets]
3093
+ );
3094
+ const buildProgramRegistrationConfig = (0, import_react2.useCallback)(
3095
+ (program) => {
3096
+ const graphSpec = resolveGraphSpec(
3097
+ program.graph,
3098
+ `${program.id ?? "program"} graph`
3099
+ );
3100
+ if (!graphSpec) {
3101
+ return null;
3102
+ }
3103
+ const outputs = collectOutputPaths(graphSpec);
3104
+ const inputs = collectInputPaths(graphSpec);
3105
+ const subs = program.graph.subscriptions ?? {
3106
+ inputs,
3107
+ outputs
3108
+ };
3109
+ return {
3110
+ id: namespaceControllerId(program.id, namespace, "graph"),
3111
+ spec: stripNulls(namespaceGraphSpec(graphSpec, namespace)),
3112
+ subs: namespaceSubscriptions(subs, namespace)
3113
+ };
3114
+ },
3115
+ [namespace]
3116
+ );
3117
+ const deriveProgramResetValues = (0, import_react2.useCallback)(
3118
+ (program) => {
3119
+ if (program.resetValues) {
3120
+ return Object.entries(program.resetValues).filter(([, value]) => Number.isFinite(value)).map(([path, value]) => ({ path, value }));
3121
+ }
3122
+ const graphSpec = resolveGraphSpec(
3123
+ program.graph,
3124
+ `${program.id ?? "program"} graph (reset)`
3125
+ );
3126
+ if (!graphSpec) {
3127
+ return [];
3128
+ }
3129
+ return collectOutputPaths(graphSpec).filter((path) => path.trim().length > 0).map((path) => {
3130
+ const defaultValue = inputConstraintsRef.current[path]?.defaultValue ?? 0;
3131
+ return {
3132
+ path,
3133
+ value: Number.isFinite(defaultValue) && defaultValue != null ? defaultValue : 0
3134
+ };
3135
+ });
3136
+ },
3137
+ []
3138
+ );
3139
+ const syncProgramPlaybackControllers = (0, import_react2.useCallback)(() => {
3140
+ if (!ready) {
3141
+ return;
3142
+ }
3143
+ const availableProgramIds = new Set(
3144
+ resolvedProgramAssets.map((program) => program.id)
3145
+ );
3146
+ Array.from(programPlaybackRef.current.keys()).forEach((id) => {
3147
+ if (availableProgramIds.has(id)) {
3148
+ return;
3149
+ }
3150
+ programPlaybackRef.current.delete(id);
3151
+ const controllerId = programControllerIdsRef.current.get(id);
3152
+ if (controllerId) {
3153
+ try {
3154
+ removeGraph(controllerId);
3155
+ } catch (err) {
3156
+ pushError({
3157
+ message: `Failed to remove program ${id}`,
3158
+ cause: err,
3159
+ phase: "registration",
3160
+ timestamp: performance.now()
3161
+ });
3162
+ }
3163
+ programControllerIdsRef.current.delete(id);
3164
+ }
3165
+ });
3166
+ programPlaybackRef.current.forEach((state, id) => {
3167
+ const program = resolveProgramById(id);
3168
+ const controllerId = programControllerIdsRef.current.get(id);
3169
+ if (!program) {
3170
+ return;
3171
+ }
3172
+ if (state.state !== "playing") {
3173
+ if (!controllerId) {
3174
+ return;
3175
+ }
3176
+ try {
3177
+ removeGraph(controllerId);
3178
+ } catch (err) {
3179
+ pushError({
3180
+ message: `Failed to pause program ${id}`,
3181
+ cause: err,
3182
+ phase: "registration",
3183
+ timestamp: performance.now()
3184
+ });
3185
+ }
3186
+ programControllerIdsRef.current.delete(id);
3187
+ return;
3188
+ }
3189
+ if (controllerId) {
3190
+ return;
3191
+ }
3192
+ const config = buildProgramRegistrationConfig(program);
3193
+ if (!config) {
3194
+ pushError({
3195
+ message: `Program ${id} is missing a usable graph payload.`,
3196
+ phase: "registration",
3197
+ timestamp: performance.now()
3198
+ });
3199
+ return;
3200
+ }
3201
+ try {
3202
+ const nextControllerId = registerGraph(config);
3203
+ programControllerIdsRef.current.set(id, nextControllerId);
3204
+ } catch (err) {
3205
+ pushError({
3206
+ message: `Failed to register program ${id}`,
3207
+ cause: err,
3208
+ phase: "registration",
3209
+ timestamp: performance.now()
3210
+ });
3211
+ }
3212
+ });
3213
+ refreshControllerStatus();
3214
+ }, [
3215
+ buildProgramRegistrationConfig,
3216
+ pushError,
3217
+ ready,
3218
+ refreshControllerStatus,
3219
+ registerGraph,
3220
+ removeGraph,
3221
+ resolvedProgramAssets,
3222
+ resolveProgramById
3223
+ ]);
3224
+ const playProgram = (0, import_react2.useCallback)(
3225
+ (id) => {
3226
+ if (!resolveProgramById(id)) {
3227
+ throw new Error(
3228
+ `Program ${id} is not part of the current asset bundle.`
3229
+ );
3230
+ }
3231
+ programPlaybackRef.current.set(id, {
3232
+ id,
3233
+ state: "playing"
3234
+ });
3235
+ syncProgramPlaybackControllers();
3236
+ markActivity();
3237
+ },
3238
+ [markActivity, resolveProgramById, syncProgramPlaybackControllers]
3239
+ );
3240
+ const pauseProgram = (0, import_react2.useCallback)(
3241
+ (id) => {
3242
+ if (!resolveProgramById(id)) {
3243
+ return;
3244
+ }
3245
+ programPlaybackRef.current.set(id, {
3246
+ id,
3247
+ state: "paused"
3248
+ });
3249
+ syncProgramPlaybackControllers();
3250
+ updateLoopMode();
3251
+ },
3252
+ [resolveProgramById, syncProgramPlaybackControllers, updateLoopMode]
3253
+ );
3254
+ const stopProgram = (0, import_react2.useCallback)(
3255
+ (id, options) => {
3256
+ const program = resolveProgramById(id);
3257
+ const controllerId = programControllerIdsRef.current.get(id);
3258
+ if (controllerId) {
3259
+ try {
3260
+ removeGraph(controllerId);
3261
+ } catch (err) {
3262
+ pushError({
3263
+ message: `Failed to stop program ${id}`,
3264
+ cause: err,
3265
+ phase: "registration",
3266
+ timestamp: performance.now()
3267
+ });
3268
+ }
3269
+ programControllerIdsRef.current.delete(id);
3270
+ }
3271
+ programPlaybackRef.current.set(id, {
3272
+ id,
3273
+ state: "stopped"
3274
+ });
3275
+ if (program && options?.resetOutputs !== false) {
3276
+ deriveProgramResetValues(program).forEach(({ path, value }) => {
3277
+ setInput(path, { float: value });
1559
3278
  });
1560
3279
  }
3280
+ refreshControllerStatus();
3281
+ updateLoopMode();
3282
+ },
3283
+ [
3284
+ deriveProgramResetValues,
3285
+ pushError,
3286
+ refreshControllerStatus,
3287
+ removeGraph,
3288
+ resolveProgramById,
3289
+ setInput,
3290
+ updateLoopMode
3291
+ ]
3292
+ );
3293
+ const getProgramState = (0, import_react2.useCallback)(
3294
+ (id) => {
3295
+ const state = programPlaybackRef.current.get(id);
3296
+ if (!state) {
3297
+ return null;
3298
+ }
3299
+ return { state: state.state };
1561
3300
  },
1562
- [assetBundle.animations, setInput]
3301
+ []
3302
+ );
3303
+ (0, import_react2.useEffect)(() => {
3304
+ if (!ready || status.loading) {
3305
+ return;
3306
+ }
3307
+ syncProgramPlaybackControllers();
3308
+ }, [
3309
+ graphUpdateToken,
3310
+ ready,
3311
+ resolvedProgramAssets,
3312
+ status.loading,
3313
+ syncProgramPlaybackControllers
3314
+ ]);
3315
+ const setAnimationActive = (0, import_react2.useCallback)(
3316
+ (active) => {
3317
+ const next = Boolean(active);
3318
+ if (animationSystemActiveRef.current === next) {
3319
+ return;
3320
+ }
3321
+ animationSystemActiveRef.current = next;
3322
+ if (!next) {
3323
+ clipPlaybackRef.current.forEach((state) => {
3324
+ state.playing = false;
3325
+ });
3326
+ }
3327
+ updateLoopMode();
3328
+ },
3329
+ [updateLoopMode]
3330
+ );
3331
+ const isAnimationActive = (0, import_react2.useCallback)(
3332
+ () => animationSystemActiveRef.current,
3333
+ []
1563
3334
  );
1564
3335
  const registerInputDriver = (0, import_react2.useCallback)(
1565
3336
  (id, factory) => {
@@ -1700,6 +3471,10 @@ function VizijRuntimeProviderInner({
1700
3471
  return () => {
1701
3472
  animationTweensRef.current.clear();
1702
3473
  clipPlaybackRef.current.clear();
3474
+ programPlaybackRef.current.clear();
3475
+ programControllerIdsRef.current.clear();
3476
+ clipOutputValuesRef.current.clear();
3477
+ clipAggregateValuesRef.current.clear();
1703
3478
  };
1704
3479
  }, []);
1705
3480
  (0, import_react2.useEffect)(() => {
@@ -1715,18 +3490,61 @@ function VizijRuntimeProviderInner({
1715
3490
  }, 500);
1716
3491
  return () => window.clearInterval(id);
1717
3492
  }, [reportStatus]);
3493
+ const setGraphBundle = (0, import_react2.useCallback)(
3494
+ (bundle, options) => {
3495
+ const baseAssetBundle = latestEffectiveAssetBundleRef.current;
3496
+ const nextAssetBundle = applyRuntimeGraphBundle(baseAssetBundle, bundle);
3497
+ const plan = resolveRuntimeUpdatePlan(
3498
+ baseAssetBundle,
3499
+ nextAssetBundle,
3500
+ options?.tier ?? updateTierRef.current
3501
+ );
3502
+ pendingPlanRef.current = plan;
3503
+ previousBundleRef.current = nextAssetBundle;
3504
+ latestEffectiveAssetBundleRef.current = nextAssetBundle;
3505
+ suppressNextBundlePlanRef.current = true;
3506
+ setAssetBundleOverride(nextAssetBundle);
3507
+ if (plan.reregisterGraphs) {
3508
+ setGraphUpdateToken((prev) => prev + 1);
3509
+ }
3510
+ if (plan.reloadAssets) {
3511
+ reportStatus((prev) => ({
3512
+ ...prev,
3513
+ loading: true,
3514
+ ready: false
3515
+ }));
3516
+ } else {
3517
+ reportStatus((prev) => ({
3518
+ ...prev,
3519
+ loading: false
3520
+ }));
3521
+ }
3522
+ },
3523
+ [reportStatus]
3524
+ );
1718
3525
  const contextValue = (0, import_react2.useMemo)(
1719
3526
  () => ({
1720
3527
  ...status,
1721
3528
  assetBundle,
1722
3529
  setInput,
3530
+ setGraphBundle,
1723
3531
  setValue: setRendererValue,
1724
3532
  stagePoseNeutral,
1725
3533
  animateValue,
1726
3534
  cancelAnimation,
1727
3535
  registerInputDriver,
1728
3536
  playAnimation,
3537
+ pauseAnimation,
3538
+ seekAnimation,
3539
+ setAnimationLoop,
3540
+ getAnimationState,
1729
3541
  stopAnimation,
3542
+ playProgram,
3543
+ pauseProgram,
3544
+ stopProgram,
3545
+ getProgramState,
3546
+ setAnimationActive,
3547
+ isAnimationActive,
1730
3548
  step,
1731
3549
  advanceAnimations,
1732
3550
  inputConstraints
@@ -1735,13 +3553,24 @@ function VizijRuntimeProviderInner({
1735
3553
  status,
1736
3554
  assetBundle,
1737
3555
  setInput,
3556
+ setGraphBundle,
1738
3557
  setRendererValue,
1739
3558
  stagePoseNeutral,
1740
3559
  animateValue,
1741
3560
  cancelAnimation,
1742
3561
  registerInputDriver,
1743
3562
  playAnimation,
3563
+ pauseAnimation,
3564
+ seekAnimation,
3565
+ setAnimationLoop,
3566
+ getAnimationState,
1744
3567
  stopAnimation,
3568
+ playProgram,
3569
+ pauseProgram,
3570
+ stopProgram,
3571
+ getProgramState,
3572
+ setAnimationActive,
3573
+ isAnimationActive,
1745
3574
  step,
1746
3575
  advanceAnimations,
1747
3576
  inputConstraints
@@ -1787,15 +3616,21 @@ function VizijRuntimeFaceInner({
1787
3616
  }
1788
3617
  var VizijRuntimeFace = (0, import_react4.memo)(VizijRuntimeFaceInner);
1789
3618
 
3619
+ // src/hooks/useOptionalVizijRuntime.ts
3620
+ var import_react5 = require("react");
3621
+ function useOptionalVizijRuntime() {
3622
+ return (0, import_react5.useContext)(VizijRuntimeContext);
3623
+ }
3624
+
1790
3625
  // src/hooks/useVizijOutputs.ts
1791
3626
  var import_render3 = require("@vizij/render");
1792
- var import_utils = require("@vizij/utils");
3627
+ var import_utils2 = require("@vizij/utils");
1793
3628
  function useVizijOutputs(paths) {
1794
3629
  const { namespace } = useVizijRuntime();
1795
3630
  return (0, import_render3.useVizijStore)((state) => {
1796
3631
  const result = {};
1797
3632
  paths.forEach((path) => {
1798
- const lookup = (0, import_utils.getLookup)(namespace, path);
3633
+ const lookup = (0, import_utils2.getLookup)(namespace, path);
1799
3634
  result[path] = state.values.get(lookup);
1800
3635
  });
1801
3636
  return result;
@@ -1803,15 +3638,15 @@ function useVizijOutputs(paths) {
1803
3638
  }
1804
3639
 
1805
3640
  // src/hooks/useRigInput.ts
1806
- var import_react5 = require("react");
3641
+ var import_react6 = require("react");
1807
3642
  var import_render4 = require("@vizij/render");
1808
- var import_utils2 = require("@vizij/utils");
3643
+ var import_utils3 = require("@vizij/utils");
1809
3644
  function useRigInput(path) {
1810
3645
  const { namespace, setInput } = useVizijRuntime();
1811
3646
  const value = (0, import_render4.useVizijStore)((state) => {
1812
- return state.values.get((0, import_utils2.getLookup)(namespace, path));
3647
+ return state.values.get((0, import_utils3.getLookup)(namespace, path));
1813
3648
  });
1814
- const setter = (0, import_react5.useCallback)(
3649
+ const setter = (0, import_react6.useCallback)(
1815
3650
  (next, shape) => {
1816
3651
  setInput(path, next, shape);
1817
3652
  },
@@ -1819,10 +3654,423 @@ function useRigInput(path) {
1819
3654
  );
1820
3655
  return [value, setter];
1821
3656
  }
3657
+
3658
+ // src/utils/faceControls.ts
3659
+ var DEFAULT_GAZE_CONTROL = {
3660
+ min: -1,
3661
+ max: 1,
3662
+ defaultValue: 0
3663
+ };
3664
+ var DEFAULT_BLINK_CONTROL = {
3665
+ min: 0,
3666
+ max: 1,
3667
+ defaultValue: 0
3668
+ };
3669
+ var STANDARD_VIZIJ_EYE_PATHS = {
3670
+ leftX: "/standard/vizij/left_eye/pos/x",
3671
+ leftY: "/standard/vizij/left_eye/pos/y",
3672
+ rightX: "/standard/vizij/right_eye/pos/x",
3673
+ rightY: "/standard/vizij/right_eye/pos/y",
3674
+ leftUpper: "/standard/vizij/left_eye_top_eyelid/pos/y",
3675
+ rightUpper: "/standard/vizij/right_eye_top_eyelid/pos/y"
3676
+ };
3677
+ var LEGACY_STANDARD_EYE_PATHS = {
3678
+ leftX: "/standard/left_eye/pos/x",
3679
+ leftY: "/standard/left_eye/pos/y",
3680
+ rightX: "/standard/right_eye/pos/x",
3681
+ rightY: "/standard/right_eye/pos/y",
3682
+ leftUpper: "/standard/left_eye_top_eyelid/pos/y",
3683
+ rightUpper: "/standard/right_eye_top_eyelid/pos/y"
3684
+ };
3685
+ var PROPSRIG_EYE_PATHS = {
3686
+ leftX: "/propsrig/l_eye/translation/x",
3687
+ leftY: "/propsrig/l_eye/translation/y",
3688
+ rightX: "/propsrig/r_eye/translation/x",
3689
+ rightY: "/propsrig/r_eye/translation/y"
3690
+ };
3691
+ function clamp(value, min, max) {
3692
+ return Math.min(Math.max(value, min), max);
3693
+ }
3694
+ function normalizeInputPath(path) {
3695
+ const trimmed = path?.trim() ?? "";
3696
+ if (!trimmed) {
3697
+ return null;
3698
+ }
3699
+ if (trimmed.startsWith("/")) {
3700
+ return trimmed;
3701
+ }
3702
+ const rigMatch = trimmed.match(/^rig\/[^/]+(\/.*)$/);
3703
+ if (rigMatch?.[1]) {
3704
+ return rigMatch[1];
3705
+ }
3706
+ return `/${trimmed}`;
3707
+ }
3708
+ function buildControlsLookup(assetBundle) {
3709
+ const metadata = assetBundle.rig?.inputMetadata ?? [];
3710
+ const metadataByPath = /* @__PURE__ */ new Map();
3711
+ const pathSet = /* @__PURE__ */ new Set();
3712
+ metadata.forEach((entry) => {
3713
+ const normalized = normalizeInputPath(entry.path);
3714
+ if (!normalized) {
3715
+ return;
3716
+ }
3717
+ pathSet.add(normalized);
3718
+ if (!metadataByPath.has(normalized)) {
3719
+ metadataByPath.set(normalized, entry);
3720
+ }
3721
+ });
3722
+ return { pathSet, metadataByPath };
3723
+ }
3724
+ function resolveConstraint(absolutePath, relativePath, inputConstraints) {
3725
+ if (!inputConstraints) {
3726
+ return null;
3727
+ }
3728
+ const normalizedRelative = relativePath.replace(/^\//, "");
3729
+ const candidates = [
3730
+ absolutePath,
3731
+ relativePath,
3732
+ normalizedRelative,
3733
+ absolutePath.replace(/^rig\/[^/]+\//, "")
3734
+ ];
3735
+ for (const candidate of candidates) {
3736
+ const constraint = inputConstraints[candidate];
3737
+ if (constraint) {
3738
+ return constraint;
3739
+ }
3740
+ }
3741
+ return null;
3742
+ }
3743
+ function createControl(faceId, lookup, relativePath, inputConstraints, fallback) {
3744
+ const normalizedRelative = normalizeInputPath(relativePath);
3745
+ if (!normalizedRelative || !lookup.pathSet.has(normalizedRelative)) {
3746
+ return null;
3747
+ }
3748
+ const absolutePath = buildRigInputPath(faceId, normalizedRelative);
3749
+ const metadata = lookup.metadataByPath.get(normalizedRelative);
3750
+ const constraint = resolveConstraint(
3751
+ absolutePath,
3752
+ normalizedRelative,
3753
+ inputConstraints
3754
+ );
3755
+ const min = Number.isFinite(Number(constraint?.min ?? metadata?.range?.min)) ? Number(constraint?.min ?? metadata?.range?.min) : fallback.min;
3756
+ const max = Number.isFinite(Number(constraint?.max ?? metadata?.range?.max)) ? Number(constraint?.max ?? metadata?.range?.max) : fallback.max;
3757
+ const midpoint = min + (max - min) / 2;
3758
+ const defaultValue = Number.isFinite(
3759
+ Number(constraint?.defaultValue ?? metadata?.defaultValue)
3760
+ ) ? Number(constraint?.defaultValue ?? metadata?.defaultValue) : Number.isFinite(midpoint) ? midpoint : fallback.defaultValue;
3761
+ return {
3762
+ path: absolutePath,
3763
+ min,
3764
+ max,
3765
+ defaultValue
3766
+ };
3767
+ }
3768
+ function hasAll(controls, keys) {
3769
+ return keys.every((key) => Boolean(controls[key]));
3770
+ }
3771
+ function resolveFaceControls(assetBundle, runtimeFaceId, inputConstraints) {
3772
+ const faceId = assetBundle.faceId ?? assetBundle.pose?.config?.faceId ?? runtimeFaceId ?? "face";
3773
+ const lookup = buildControlsLookup(assetBundle);
3774
+ const standardVizijControls = {
3775
+ leftX: createControl(
3776
+ faceId,
3777
+ lookup,
3778
+ STANDARD_VIZIJ_EYE_PATHS.leftX,
3779
+ inputConstraints,
3780
+ DEFAULT_GAZE_CONTROL
3781
+ ),
3782
+ leftY: createControl(
3783
+ faceId,
3784
+ lookup,
3785
+ STANDARD_VIZIJ_EYE_PATHS.leftY,
3786
+ inputConstraints,
3787
+ DEFAULT_GAZE_CONTROL
3788
+ ),
3789
+ rightX: createControl(
3790
+ faceId,
3791
+ lookup,
3792
+ STANDARD_VIZIJ_EYE_PATHS.rightX,
3793
+ inputConstraints,
3794
+ DEFAULT_GAZE_CONTROL
3795
+ ),
3796
+ rightY: createControl(
3797
+ faceId,
3798
+ lookup,
3799
+ STANDARD_VIZIJ_EYE_PATHS.rightY,
3800
+ inputConstraints,
3801
+ DEFAULT_GAZE_CONTROL
3802
+ ),
3803
+ leftUpper: createControl(
3804
+ faceId,
3805
+ lookup,
3806
+ STANDARD_VIZIJ_EYE_PATHS.leftUpper,
3807
+ inputConstraints,
3808
+ DEFAULT_GAZE_CONTROL
3809
+ ),
3810
+ rightUpper: createControl(
3811
+ faceId,
3812
+ lookup,
3813
+ STANDARD_VIZIJ_EYE_PATHS.rightUpper,
3814
+ inputConstraints,
3815
+ DEFAULT_GAZE_CONTROL
3816
+ )
3817
+ };
3818
+ const legacyStandardControls = {
3819
+ leftX: createControl(
3820
+ faceId,
3821
+ lookup,
3822
+ LEGACY_STANDARD_EYE_PATHS.leftX,
3823
+ inputConstraints,
3824
+ DEFAULT_GAZE_CONTROL
3825
+ ),
3826
+ leftY: createControl(
3827
+ faceId,
3828
+ lookup,
3829
+ LEGACY_STANDARD_EYE_PATHS.leftY,
3830
+ inputConstraints,
3831
+ DEFAULT_GAZE_CONTROL
3832
+ ),
3833
+ rightX: createControl(
3834
+ faceId,
3835
+ lookup,
3836
+ LEGACY_STANDARD_EYE_PATHS.rightX,
3837
+ inputConstraints,
3838
+ DEFAULT_GAZE_CONTROL
3839
+ ),
3840
+ rightY: createControl(
3841
+ faceId,
3842
+ lookup,
3843
+ LEGACY_STANDARD_EYE_PATHS.rightY,
3844
+ inputConstraints,
3845
+ DEFAULT_GAZE_CONTROL
3846
+ ),
3847
+ leftUpper: createControl(
3848
+ faceId,
3849
+ lookup,
3850
+ LEGACY_STANDARD_EYE_PATHS.leftUpper,
3851
+ inputConstraints,
3852
+ DEFAULT_GAZE_CONTROL
3853
+ ),
3854
+ rightUpper: createControl(
3855
+ faceId,
3856
+ lookup,
3857
+ LEGACY_STANDARD_EYE_PATHS.rightUpper,
3858
+ inputConstraints,
3859
+ DEFAULT_GAZE_CONTROL
3860
+ )
3861
+ };
3862
+ const propsrigControls = {
3863
+ leftX: createControl(
3864
+ faceId,
3865
+ lookup,
3866
+ PROPSRIG_EYE_PATHS.leftX,
3867
+ inputConstraints,
3868
+ DEFAULT_GAZE_CONTROL
3869
+ ),
3870
+ leftY: createControl(
3871
+ faceId,
3872
+ lookup,
3873
+ PROPSRIG_EYE_PATHS.leftY,
3874
+ inputConstraints,
3875
+ DEFAULT_GAZE_CONTROL
3876
+ ),
3877
+ rightX: createControl(
3878
+ faceId,
3879
+ lookup,
3880
+ PROPSRIG_EYE_PATHS.rightX,
3881
+ inputConstraints,
3882
+ DEFAULT_GAZE_CONTROL
3883
+ ),
3884
+ rightY: createControl(
3885
+ faceId,
3886
+ lookup,
3887
+ PROPSRIG_EYE_PATHS.rightY,
3888
+ inputConstraints,
3889
+ DEFAULT_GAZE_CONTROL
3890
+ )
3891
+ };
3892
+ const coupledGazeControls = {
3893
+ leftX: createControl(
3894
+ faceId,
3895
+ lookup,
3896
+ "/gaze/left_right",
3897
+ inputConstraints,
3898
+ DEFAULT_GAZE_CONTROL
3899
+ ),
3900
+ leftY: createControl(
3901
+ faceId,
3902
+ lookup,
3903
+ "/gaze/up_down",
3904
+ inputConstraints,
3905
+ DEFAULT_GAZE_CONTROL
3906
+ ),
3907
+ rightX: createControl(
3908
+ faceId,
3909
+ lookup,
3910
+ "/gaze/left_right_copy",
3911
+ inputConstraints,
3912
+ DEFAULT_GAZE_CONTROL
3913
+ ) ?? createControl(
3914
+ faceId,
3915
+ lookup,
3916
+ "/gaze/left_right",
3917
+ inputConstraints,
3918
+ DEFAULT_GAZE_CONTROL
3919
+ ),
3920
+ rightY: createControl(
3921
+ faceId,
3922
+ lookup,
3923
+ "/gaze/up_down_copy",
3924
+ inputConstraints,
3925
+ DEFAULT_GAZE_CONTROL
3926
+ ) ?? createControl(
3927
+ faceId,
3928
+ lookup,
3929
+ "/gaze/up_down",
3930
+ inputConstraints,
3931
+ DEFAULT_GAZE_CONTROL
3932
+ )
3933
+ };
3934
+ const blinkFromLids = createControl(
3935
+ faceId,
3936
+ lookup,
3937
+ "/lids/blink",
3938
+ inputConstraints,
3939
+ DEFAULT_BLINK_CONTROL
3940
+ );
3941
+ const blinkDirect = createControl(
3942
+ faceId,
3943
+ lookup,
3944
+ "/blink",
3945
+ inputConstraints,
3946
+ DEFAULT_BLINK_CONTROL
3947
+ );
3948
+ if (hasAll(standardVizijControls, ["leftX", "leftY", "rightX", "rightY"])) {
3949
+ return {
3950
+ faceId,
3951
+ gazeSource: "standard-vizij",
3952
+ blinkSource: blinkFromLids ? "lids" : blinkDirect ? "blink" : "none",
3953
+ eyes: {
3954
+ leftX: standardVizijControls.leftX,
3955
+ leftY: standardVizijControls.leftY,
3956
+ rightX: standardVizijControls.rightX,
3957
+ rightY: standardVizijControls.rightY
3958
+ },
3959
+ eyelids: {
3960
+ leftUpper: standardVizijControls.leftUpper,
3961
+ rightUpper: standardVizijControls.rightUpper
3962
+ },
3963
+ blink: blinkFromLids ?? blinkDirect
3964
+ };
3965
+ }
3966
+ if (hasAll(legacyStandardControls, ["leftX", "leftY", "rightX", "rightY"])) {
3967
+ return {
3968
+ faceId,
3969
+ gazeSource: "standard",
3970
+ blinkSource: blinkDirect ? "blink" : blinkFromLids ? "lids" : "none",
3971
+ eyes: {
3972
+ leftX: legacyStandardControls.leftX,
3973
+ leftY: legacyStandardControls.leftY,
3974
+ rightX: legacyStandardControls.rightX,
3975
+ rightY: legacyStandardControls.rightY
3976
+ },
3977
+ eyelids: {
3978
+ leftUpper: legacyStandardControls.leftUpper,
3979
+ rightUpper: legacyStandardControls.rightUpper
3980
+ },
3981
+ blink: blinkDirect ?? blinkFromLids
3982
+ };
3983
+ }
3984
+ if (hasAll(propsrigControls, ["leftX", "leftY", "rightX", "rightY"])) {
3985
+ return {
3986
+ faceId,
3987
+ gazeSource: "propsrig",
3988
+ blinkSource: blinkDirect ? "blink" : blinkFromLids ? "lids" : "none",
3989
+ eyes: {
3990
+ leftX: propsrigControls.leftX,
3991
+ leftY: propsrigControls.leftY,
3992
+ rightX: propsrigControls.rightX,
3993
+ rightY: propsrigControls.rightY
3994
+ },
3995
+ eyelids: {
3996
+ leftUpper: null,
3997
+ rightUpper: null
3998
+ },
3999
+ blink: blinkDirect ?? blinkFromLids
4000
+ };
4001
+ }
4002
+ if (hasAll(coupledGazeControls, ["leftX", "leftY", "rightX", "rightY"])) {
4003
+ return {
4004
+ faceId,
4005
+ gazeSource: "coupled-gaze",
4006
+ blinkSource: blinkFromLids ? "lids" : blinkDirect ? "blink" : "none",
4007
+ eyes: {
4008
+ leftX: coupledGazeControls.leftX,
4009
+ leftY: coupledGazeControls.leftY,
4010
+ rightX: coupledGazeControls.rightX,
4011
+ rightY: coupledGazeControls.rightY
4012
+ },
4013
+ eyelids: {
4014
+ leftUpper: null,
4015
+ rightUpper: null
4016
+ },
4017
+ blink: blinkFromLids ?? blinkDirect
4018
+ };
4019
+ }
4020
+ return {
4021
+ faceId,
4022
+ gazeSource: "none",
4023
+ blinkSource: blinkFromLids ? "lids" : blinkDirect ? "blink" : "none",
4024
+ eyes: {
4025
+ leftX: null,
4026
+ leftY: null,
4027
+ rightX: null,
4028
+ rightY: null
4029
+ },
4030
+ eyelids: {
4031
+ leftUpper: null,
4032
+ rightUpper: null
4033
+ },
4034
+ blink: blinkFromLids ?? blinkDirect
4035
+ };
4036
+ }
4037
+ function mapNormalizedControlValue(control, normalizedValue) {
4038
+ const value = clamp(normalizedValue, -1, 1);
4039
+ if (value === 0) {
4040
+ return control.defaultValue;
4041
+ }
4042
+ if (value > 0) {
4043
+ return control.defaultValue + (control.max - control.defaultValue) * value;
4044
+ }
4045
+ return control.defaultValue + (control.defaultValue - control.min) * value;
4046
+ }
4047
+ function mapUnitControlValue(control, unitValue) {
4048
+ const value = clamp(unitValue, 0, 1);
4049
+ return control.defaultValue + (control.max - control.defaultValue) * value;
4050
+ }
1822
4051
  // Annotate the CommonJS export names for ESM import in node:
1823
4052
  0 && (module.exports = {
4053
+ EMOTION_POSE_KEYS,
4054
+ EXPRESSIVE_EMOTION_POSE_KEYS,
4055
+ POSE_WEIGHT_INPUT_PATH_PREFIX,
4056
+ VISEME_POSE_KEYS,
1824
4057
  VizijRuntimeFace,
1825
4058
  VizijRuntimeProvider,
4059
+ buildPoseWeightInputPathSegment,
4060
+ buildPoseWeightPathMap,
4061
+ buildPoseWeightRelativePath,
4062
+ buildRigInputPath,
4063
+ buildSemanticPoseWeightPathMap,
4064
+ filterPosesBySemanticKind,
4065
+ getPoseSemanticKey,
4066
+ mapNormalizedControlValue,
4067
+ mapUnitControlValue,
4068
+ normalizePoseSemanticKey,
4069
+ resolveFaceControls,
4070
+ resolvePoseMembership,
4071
+ resolvePoseSemantics,
4072
+ resolveRuntimeUpdatePlan,
4073
+ useOptionalVizijRuntime,
1826
4074
  useRigInput,
1827
4075
  useVizijOutputs,
1828
4076
  useVizijRuntime