@vizij/runtime-react 0.0.14 → 0.2.0

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