@vizij/runtime-react 0.1.0 → 0.3.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,31 +1,494 @@
1
1
  // src/VizijRuntimeProvider.tsx
2
- import {
3
- useCallback,
4
- useContext,
5
- useEffect,
6
- useMemo,
7
- useRef,
8
- useState
9
- } from "react";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
10
3
  import {
11
4
  VizijContext,
12
5
  createVizijStore,
13
6
  loadGLTFWithBundle,
14
7
  loadGLTFFromBlobWithBundle
15
8
  } from "@vizij/render";
16
- import {
17
- OrchestratorProvider,
18
- OrchestratorContext,
19
- useOrchestrator,
20
- useOrchFrame
21
- } from "@vizij/orchestrator-react";
22
9
  import { compileIrGraph } from "@vizij/node-graph-authoring";
23
10
  import { valueAsNumber as valueAsNumber2 } from "@vizij/value-json";
11
+ import { getLookup } from "@vizij/utils";
12
+ import { loadAnimationModule } from "@vizij/animation-module";
13
+
14
+ // src/engine/aroraEngine.ts
15
+ import {
16
+ init,
17
+ startRuntime
18
+ } from "@vizij/runtime";
19
+
20
+ // src/engine/graphSpecDiff.ts
21
+ function asRecords(value) {
22
+ return Array.isArray(value) ? value : [];
23
+ }
24
+ function nodeId(node) {
25
+ return typeof node.id === "string" ? node.id : void 0;
26
+ }
27
+ function endpointId(endpoint) {
28
+ const id = endpoint?.node_id;
29
+ return typeof id === "string" ? id : void 0;
30
+ }
31
+ function endpointInput(endpoint) {
32
+ const input = endpoint?.input;
33
+ return typeof input === "string" ? input : void 0;
34
+ }
35
+ function stable(value) {
36
+ return JSON.stringify(sortKeys(value));
37
+ }
38
+ function sortKeys(value) {
39
+ if (Array.isArray(value)) return value.map(sortKeys);
40
+ if (value && typeof value === "object") {
41
+ const out = {};
42
+ for (const key of Object.keys(value).sort()) {
43
+ out[key] = sortKeys(value[key]);
44
+ }
45
+ return out;
46
+ }
47
+ return value;
48
+ }
49
+ function edgeKey(edge) {
50
+ return stable([
51
+ endpointId(edge.from),
52
+ edge.from?.output ?? "out",
53
+ endpointId(edge.to),
54
+ endpointInput(edge.to),
55
+ edge.selector ?? null
56
+ ]);
57
+ }
58
+ function graphSpecDiff(prev, next) {
59
+ const prevNodes = /* @__PURE__ */ new Map();
60
+ for (const node of asRecords(prev.nodes)) {
61
+ const id = nodeId(node);
62
+ if (id) prevNodes.set(id, node);
63
+ }
64
+ const nextNodes = /* @__PURE__ */ new Map();
65
+ for (const node of asRecords(next.nodes)) {
66
+ const id = nodeId(node);
67
+ if (id) nextNodes.set(id, node);
68
+ }
69
+ const prevEdges = asRecords(prev.edges);
70
+ const nextEdges = asRecords(next.edges);
71
+ const remove_nodes = [...prevNodes.keys()].filter((id) => !nextNodes.has(id));
72
+ const upsertIds = /* @__PURE__ */ new Set();
73
+ for (const [id, node] of nextNodes) {
74
+ const before = prevNodes.get(id);
75
+ if (!before || stable(before) !== stable(node)) upsertIds.add(id);
76
+ }
77
+ const prevEdgeKeys = new Set(prevEdges.map(edgeKey));
78
+ const nextEdgeKeys = new Set(nextEdges.map(edgeKey));
79
+ const removedEdges = prevEdges.filter((e) => !nextEdgeKeys.has(edgeKey(e)));
80
+ const changedEdges = [
81
+ ...nextEdges.filter((e) => !prevEdgeKeys.has(edgeKey(e))),
82
+ ...removedEdges
83
+ ];
84
+ for (const edge of changedEdges) {
85
+ for (const id of [endpointId(edge.from), endpointId(edge.to)]) {
86
+ if (id && nextNodes.has(id)) upsertIds.add(id);
87
+ }
88
+ }
89
+ const incidentToUpsert = (edge) => {
90
+ const from = endpointId(edge.from);
91
+ const to = endpointId(edge.to);
92
+ return from !== void 0 && upsertIds.has(from) || to !== void 0 && upsertIds.has(to);
93
+ };
94
+ const upsert_nodes = [...upsertIds].map((id) => nextNodes.get(id));
95
+ const upsert_edges = nextEdges.filter(incidentToUpsert);
96
+ const removeNodeSet = new Set(remove_nodes);
97
+ const remove_edges = removedEdges.flatMap((edge) => {
98
+ const to = endpointId(edge.to);
99
+ const input = endpointInput(edge.to);
100
+ if (!to || !input || removeNodeSet.has(to) || upsertIds.has(to)) return [];
101
+ return [{ node_id: to, input }];
102
+ });
103
+ if (upsert_nodes.length === 0 && remove_nodes.length === 0 && upsert_edges.length === 0 && remove_edges.length === 0) {
104
+ return null;
105
+ }
106
+ return { upsert_nodes, remove_nodes, upsert_edges, remove_edges };
107
+ }
108
+
109
+ // src/engine/aroraEngine.ts
110
+ var BUILT_IN_PREFIX = "arora/";
111
+ function isBuiltInPath(path) {
112
+ return path.startsWith(BUILT_IN_PREFIX);
113
+ }
114
+ var RUN_PERIOD_MS = 1e3 / 60;
115
+ var initPromise = null;
116
+ function ensureWasmInit(input) {
117
+ if (!initPromise) {
118
+ initPromise = init(input);
119
+ }
120
+ return initPromise;
121
+ }
122
+ var DeviceSlot = class {
123
+ constructor() {
124
+ this.handle = null;
125
+ /** Module loading a boot must wait out before it reads `modules`. */
126
+ this.modulesLoading = null;
127
+ /**
128
+ * When set, a booted device is handed to its own loop (`device.run`) at
129
+ * this period. `null`: the host drives `step()` itself.
130
+ */
131
+ this.runPeriodMs = null;
132
+ /**
133
+ * Serializes device operations. Every `recompose` chains behind the
134
+ * previous op so two never run concurrently — a bundle whose import fans
135
+ * out into several graph registrations recomposes in a burst. Failures are
136
+ * swallowed on the chain so one failed op doesn't wedge later ones; each
137
+ * caller still sees its own rejection through the promise `enqueue`
138
+ * returns.
139
+ */
140
+ this.opChain = Promise.resolve();
141
+ }
142
+ get current() {
143
+ return this.handle;
144
+ }
145
+ /**
146
+ * The modules every subsequent device boot loads. The live device (if any)
147
+ * is not touched: a later `recompose` that sees the list changed rebuilds.
148
+ */
149
+ setModules(modules) {
150
+ this.modules = modules && modules.length > 0 ? modules : void 0;
151
+ }
152
+ /**
153
+ * Announce module loading in flight: boots wait for `loading` to settle
154
+ * before reading the module list, so a device never boots without modules
155
+ * that were already on their way. Pass a promise that cannot reject (the
156
+ * owner handles its own failures) — boots resume either way.
157
+ */
158
+ waitForModules(loading) {
159
+ this.modulesLoading = loading;
160
+ }
161
+ /**
162
+ * Hand the live device to its own loop, if there is one and `runPeriodMs`
163
+ * says so — for a host whose drive mode turns self-paced after boot.
164
+ */
165
+ startRun() {
166
+ if (this.handle) {
167
+ this.maybeRun(this.handle.device);
168
+ }
169
+ }
170
+ maybeRun(device) {
171
+ if (this.runPeriodMs === null || device.running) {
172
+ return;
173
+ }
174
+ device.run(this.runPeriodMs).catch((error) => {
175
+ this.onRunEnded?.(error);
176
+ });
177
+ }
178
+ enqueue(op) {
179
+ const run = this.opChain.then(op, op);
180
+ this.opChain = run.then(
181
+ () => void 0,
182
+ () => void 0
183
+ );
184
+ return run;
185
+ }
186
+ async boot(spec) {
187
+ await ensureWasmInit();
188
+ if (this.modulesLoading) {
189
+ await this.modulesLoading;
190
+ }
191
+ const device = await startRuntime(spec, void 0, this.modules);
192
+ this.handle = { device, spec, modules: this.modules };
193
+ this.onDeviceStarted?.(this.handle);
194
+ this.maybeRun(device);
195
+ return this.handle;
196
+ }
197
+ /**
198
+ * Rebuild the device because its module set changed since boot — the one
199
+ * change a live device cannot take. Carries the store across (snapshot
200
+ * minus built-in keys) and replays module setup via `onDeviceStarted`. The
201
+ * old device's wrapper is freed; if it was self-paced its loop keeps
202
+ * ticking unreachable — which is why boots wait for announced modules
203
+ * instead of leaning on this path.
204
+ */
205
+ async rebuild(old, spec) {
206
+ const carried = {};
207
+ for (const [path, value] of Object.entries(old.device.snapshot())) {
208
+ if (!isBuiltInPath(path)) {
209
+ carried[path] = value;
210
+ }
211
+ }
212
+ const device = await startRuntime(spec, void 0, this.modules);
213
+ if (Object.keys(carried).length > 0) {
214
+ device.writeValues(carried);
215
+ }
216
+ this.handle = { device, spec, modules: this.modules };
217
+ old.device.dispose();
218
+ this.onDeviceStarted?.(this.handle);
219
+ this.maybeRun(device);
220
+ return this.handle;
221
+ }
222
+ /**
223
+ * Make the device run `spec`: boot it if none is live, otherwise swap the
224
+ * graph in place (the store is the same store, so nothing is carried).
225
+ * No-op if the spec is identical by reference. Serialized behind any
226
+ * in-flight op (see `opChain`).
227
+ */
228
+ recompose(spec) {
229
+ return this.enqueue(async () => {
230
+ const old = this.handle;
231
+ if (!old) {
232
+ return this.boot(spec);
233
+ }
234
+ if (old.modules !== this.modules) {
235
+ return this.rebuild(old, spec);
236
+ }
237
+ if (old.spec === spec) {
238
+ return old;
239
+ }
240
+ const edits = graphSpecDiff(old.spec, spec);
241
+ if (edits) {
242
+ try {
243
+ await old.device.applyGraphEdits(edits);
244
+ } catch (err) {
245
+ console.warn(
246
+ "[vizij-runtime] applyGraphEdits failed; reloading the whole graph",
247
+ err
248
+ );
249
+ await old.device.loadGraph(spec);
250
+ }
251
+ }
252
+ old.spec = spec;
253
+ return old;
254
+ });
255
+ }
256
+ };
257
+
258
+ // src/utils/composeGraph.ts
259
+ function asArray(value) {
260
+ return Array.isArray(value) ? value.filter((n) => n && typeof n === "object") : [];
261
+ }
262
+ function prefixId(sourceId, id) {
263
+ return typeof id === "string" && id ? `${sourceId}::${id}` : id;
264
+ }
265
+ function prefixEndpoint(sourceId, endpoint) {
266
+ if (!endpoint || typeof endpoint !== "object") {
267
+ return endpoint;
268
+ }
269
+ const record = endpoint;
270
+ if (typeof record.node_id !== "string") {
271
+ return endpoint;
272
+ }
273
+ return { ...record, node_id: prefixId(sourceId, record.node_id) };
274
+ }
275
+ function outputPathsOf(spec) {
276
+ const paths = [];
277
+ for (const node of asArray(spec.nodes)) {
278
+ if (String(node.type ?? "").toLowerCase() !== "output") {
279
+ continue;
280
+ }
281
+ const path = node.params?.path;
282
+ if (typeof path === "string" && path.trim()) {
283
+ paths.push(path.trim());
284
+ }
285
+ }
286
+ return paths;
287
+ }
288
+ function composeGraphSpecs(sources) {
289
+ const nodes = [];
290
+ const edges = [];
291
+ const outputOwners = /* @__PURE__ */ new Map();
292
+ for (const { sourceId, spec } of sources) {
293
+ for (const path of outputPathsOf(spec)) {
294
+ const owner = outputOwners.get(path);
295
+ if (owner && owner !== sourceId) {
296
+ console.warn(
297
+ `[vizij-runtime] output path "${path}" is written by both "${owner}" and "${sourceId}"; last writer wins ("${sourceId}"). There is no combiner node for additive merges yet.`
298
+ );
299
+ }
300
+ outputOwners.set(path, sourceId);
301
+ }
302
+ for (const node of asArray(spec.nodes)) {
303
+ nodes.push({ ...node, id: prefixId(sourceId, node.id) });
304
+ }
305
+ for (const edge of asArray(spec.edges)) {
306
+ edges.push({
307
+ ...edge,
308
+ from: prefixEndpoint(sourceId, edge.from),
309
+ to: prefixEndpoint(sourceId, edge.to)
310
+ });
311
+ }
312
+ }
313
+ return { nodes, edges };
314
+ }
24
315
 
25
316
  // src/context.ts
26
317
  import { createContext } from "react";
27
318
  var VizijRuntimeContext = createContext(null);
28
319
 
320
+ // src/memoryInvestigation.ts
321
+ function getGlobalMemoryInvestigation() {
322
+ return globalThis;
323
+ }
324
+ function updateRuntimeMap(updater) {
325
+ const globalObj = getGlobalMemoryInvestigation();
326
+ if (!globalObj.__VIZIJ_MEMORY_INVESTIGATION__?.enabled) {
327
+ return;
328
+ }
329
+ if (!globalObj.__vizijMemoryDebugState) {
330
+ globalObj.__vizijMemoryDebugState = {};
331
+ }
332
+ if (!globalObj.__vizijMemoryDebugState.runtimes) {
333
+ globalObj.__vizijMemoryDebugState.runtimes = {};
334
+ }
335
+ updater(globalObj.__vizijMemoryDebugState.runtimes);
336
+ }
337
+ function setRuntimeDebugState(runtimeId, state) {
338
+ updateRuntimeMap((runtimeMap) => {
339
+ runtimeMap[runtimeId] = state;
340
+ });
341
+ }
342
+ function clearRuntimeDebugState(runtimeId) {
343
+ updateRuntimeMap((runtimeMap) => {
344
+ delete runtimeMap[runtimeId];
345
+ });
346
+ }
347
+
348
+ // src/updatePolicy.ts
349
+ var blobIdentityMap = /* @__PURE__ */ new WeakMap();
350
+ var nextBlobIdentity = 0;
351
+ function getBlobIdentity(blob) {
352
+ if (!blob) {
353
+ return "missing";
354
+ }
355
+ let identity = blobIdentityMap.get(blob);
356
+ if (identity === void 0) {
357
+ identity = nextBlobIdentity++;
358
+ blobIdentityMap.set(blob, identity);
359
+ }
360
+ return `${identity}:${blob.size}:${blob.type}`;
361
+ }
362
+ function applyRuntimeGraphBundle(base, bundle) {
363
+ const next = {
364
+ ...base
365
+ };
366
+ const hasRigOverride = Object.prototype.hasOwnProperty.call(bundle, "rig");
367
+ const hasPoseOverride = Object.prototype.hasOwnProperty.call(bundle, "pose");
368
+ const hasAnimationsOverride = Object.prototype.hasOwnProperty.call(
369
+ bundle,
370
+ "animations"
371
+ );
372
+ const hasProgramsOverride = Object.prototype.hasOwnProperty.call(
373
+ bundle,
374
+ "programs"
375
+ );
376
+ if (hasRigOverride) {
377
+ next.rig = bundle.rig;
378
+ }
379
+ if (hasPoseOverride) {
380
+ next.pose = bundle.pose;
381
+ }
382
+ if (hasAnimationsOverride) {
383
+ next.animations = Array.isArray(bundle.animations) ? bundle.animations : void 0;
384
+ }
385
+ if (hasProgramsOverride) {
386
+ next.programs = Array.isArray(bundle.programs) ? bundle.programs : void 0;
387
+ }
388
+ return next;
389
+ }
390
+ function normalizeSpecPayload(value) {
391
+ if (!value) {
392
+ return "";
393
+ }
394
+ try {
395
+ return JSON.stringify(value);
396
+ } catch {
397
+ return String(value);
398
+ }
399
+ }
400
+ function glbSignature(glb) {
401
+ if (glb.kind === "url") {
402
+ const importOptions = normalizeSpecPayload({
403
+ aggressiveImport: glb.aggressiveImport ?? false,
404
+ rootBounds: glb.rootBounds ?? null
405
+ });
406
+ return `url:${glb.src}:${importOptions}`;
407
+ }
408
+ if (glb.kind === "blob") {
409
+ const importOptions = normalizeSpecPayload({
410
+ aggressiveImport: glb.aggressiveImport ?? false,
411
+ rootBounds: glb.rootBounds ?? null
412
+ });
413
+ return `blob:${getBlobIdentity(glb.blob)}:${importOptions}`;
414
+ }
415
+ return `world:${normalizeSpecPayload(glb.world)}`;
416
+ }
417
+ function graphSignature(graph) {
418
+ if (!graph) {
419
+ return "";
420
+ }
421
+ const id = graph.id ?? "";
422
+ return `${id}:${normalizeSpecPayload(graph.spec ?? graph.ir ?? null)}`;
423
+ }
424
+ function poseSignature(pose) {
425
+ if (!pose) {
426
+ return "";
427
+ }
428
+ const graph = pose.graph;
429
+ const config = pose.config;
430
+ const graphPart = graph ? graphSignature({ id: graph.id, spec: graph.spec }) : "";
431
+ const configPart = config ? normalizeSpecPayload(config) : "";
432
+ return `${graphPart}:${configPart}`;
433
+ }
434
+ function animationsSignature(animations) {
435
+ if (!Array.isArray(animations) || animations.length === 0) {
436
+ return "";
437
+ }
438
+ return animations.map((animation) => {
439
+ const id = animation.id ?? "";
440
+ const clipSignature = normalizeSpecPayload(animation.clip ?? null);
441
+ const setupSignature = normalizeSpecPayload(animation.setup ?? null);
442
+ const weightSignature = animation.weight == null ? "" : String(animation.weight);
443
+ return `${id}:${clipSignature}:${setupSignature}:${weightSignature}`;
444
+ }).sort().join("|");
445
+ }
446
+ function programsSignature(programs) {
447
+ if (!Array.isArray(programs) || programs.length === 0) {
448
+ return "";
449
+ }
450
+ return programs.map((program) => {
451
+ const id = program.id ?? "";
452
+ const label = program.label ?? "";
453
+ const graphId = program.graph?.id ?? "";
454
+ const graphPayload = normalizeSpecPayload(
455
+ program.graph?.spec ?? program.graph?.ir ?? null
456
+ );
457
+ const resetValues = normalizeSpecPayload(program.resetValues ?? null);
458
+ return `${id}:${label}:${graphId}:${graphPayload}:${resetValues}`;
459
+ }).sort().join("|");
460
+ }
461
+ function resolveRuntimeUpdatePlan(previous, next, tier) {
462
+ if (!previous) {
463
+ return { reloadAssets: true, reregisterGraphs: false };
464
+ }
465
+ const glbChanged = glbSignature(previous.glb) !== glbSignature(next.glb);
466
+ const rigChanged = graphSignature(previous.rig) !== graphSignature(next.rig);
467
+ const poseChanged = poseSignature(previous.pose) !== poseSignature(next.pose);
468
+ const rigReferenceChanged = previous.rig?.id !== next.rig?.id || previous.rig?.spec !== next.rig?.spec || previous.rig?.ir !== next.rig?.ir;
469
+ const poseReferenceChanged = previous.pose?.graph?.id !== next.pose?.graph?.id || previous.pose?.graph?.spec !== next.pose?.graph?.spec || previous.pose?.config !== next.pose?.config;
470
+ const graphsChanged = rigChanged || poseChanged || rigReferenceChanged || poseReferenceChanged;
471
+ const animationsChanged = animationsSignature(previous.animations) !== animationsSignature(next.animations);
472
+ const programsChanged = programsSignature(previous.programs) !== programsSignature(next.programs);
473
+ const controllersChanged = graphsChanged || animationsChanged || programsChanged;
474
+ if (tier === "assets") {
475
+ return { reloadAssets: true, reregisterGraphs: false };
476
+ }
477
+ if (tier === "graphs") {
478
+ if (glbChanged) {
479
+ return { reloadAssets: true, reregisterGraphs: false };
480
+ }
481
+ return { reloadAssets: false, reregisterGraphs: controllersChanged };
482
+ }
483
+ if (glbChanged) {
484
+ return { reloadAssets: true, reregisterGraphs: false };
485
+ }
486
+ if (controllersChanged) {
487
+ return { reloadAssets: false, reregisterGraphs: true };
488
+ }
489
+ return { reloadAssets: false, reregisterGraphs: false };
490
+ }
491
+
29
492
  // src/utils/graph.ts
30
493
  function getNodes(spec) {
31
494
  if (!spec || typeof spec !== "object") {
@@ -70,6 +533,12 @@ function collectInputPaths(spec) {
70
533
  }
71
534
  function collectInputPathMap(spec) {
72
535
  const map = {};
536
+ const addVariant = (key, path, force = false) => {
537
+ if (!key || !force && map[key]) {
538
+ return;
539
+ }
540
+ map[key] = path;
541
+ };
73
542
  const nodes = getNodes(spec);
74
543
  nodes.forEach((node) => {
75
544
  if (String(node.type ?? "").toLowerCase() !== "input") {
@@ -81,13 +550,468 @@ function collectInputPathMap(spec) {
81
550
  }
82
551
  const id = String(node.id ?? "");
83
552
  const key = id.startsWith("input_") ? id.slice("input_".length) : id || path.trim();
84
- map[key] = path.trim();
553
+ const trimmedPath = path.trim();
554
+ addVariant(key, trimmedPath);
555
+ if (key.startsWith("direct_")) {
556
+ addVariant(key.slice("direct_".length), trimmedPath, true);
557
+ }
558
+ if (key.startsWith("pose_control_")) {
559
+ addVariant(key.slice("pose_control_".length), trimmedPath);
560
+ }
85
561
  });
86
562
  return map;
87
563
  }
88
564
 
565
+ // src/utils/posePaths.ts
566
+ var POSE_WEIGHT_INPUT_PATH_PREFIX = "/poses/";
567
+ var VISEME_POSE_KEYS = [
568
+ "a",
569
+ "at",
570
+ "b",
571
+ "e",
572
+ "e_2",
573
+ "f",
574
+ "i",
575
+ "k",
576
+ "m",
577
+ "o",
578
+ "o_2",
579
+ "p",
580
+ "r",
581
+ "s",
582
+ "t",
583
+ "t_2",
584
+ "u"
585
+ ];
586
+ var EXPRESSIVE_EMOTION_POSE_KEYS = [
587
+ "concerned",
588
+ "happy",
589
+ "sad",
590
+ "sleepy",
591
+ "surprise"
592
+ ];
593
+ var EMOTION_POSE_KEYS = [
594
+ "concerned",
595
+ "happy",
596
+ "neutral",
597
+ "sad",
598
+ "sleepy",
599
+ "surprise",
600
+ "angry"
601
+ ];
602
+ var VISEME_GROUP_NEEDLES = ["viseme", "phoneme", "lip", "mouth"];
603
+ var EMOTION_GROUP_NEEDLES = ["emotion", "expression", "mood", "affect"];
604
+ var VISEME_POSE_KEY_SET = new Set(VISEME_POSE_KEYS);
605
+ var EMOTION_POSE_KEY_SET = new Set(EMOTION_POSE_KEYS);
606
+ var POSE_KEY_ALIASES = {
607
+ concern: "concerned",
608
+ surprised: "surprise"
609
+ };
610
+ function buildRigInputPath(faceId, path) {
611
+ let trimmed = path.startsWith("/") ? path.slice(1) : path;
612
+ if (!trimmed) {
613
+ return `rig/${faceId}`;
614
+ }
615
+ while (trimmed.startsWith("rig/")) {
616
+ const segments = trimmed.split("/");
617
+ if (segments.length >= 3) {
618
+ const existingFaceId = segments[1];
619
+ const remainder = segments.slice(2).join("/");
620
+ if (existingFaceId === faceId) {
621
+ return trimmed;
622
+ }
623
+ trimmed = remainder || "";
624
+ } else {
625
+ trimmed = segments.slice(1).join("/");
626
+ }
627
+ }
628
+ const suffix = trimmed ? `/${trimmed}` : "";
629
+ return `rig/${faceId}${suffix}`;
630
+ }
631
+ function normalizePoseWeightPathSegment(value) {
632
+ const trimmed = value?.trim() ?? "";
633
+ if (!trimmed) {
634
+ return "pose";
635
+ }
636
+ const normalized = trimmed.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
637
+ return normalized || "pose";
638
+ }
639
+ function buildPoseWeightInputPathSegment(poseId) {
640
+ return normalizePoseWeightPathSegment(poseId);
641
+ }
642
+ function buildPoseWeightRelativePath(poseId) {
643
+ return `${POSE_WEIGHT_INPUT_PATH_PREFIX}${buildPoseWeightInputPathSegment(
644
+ poseId
645
+ )}.weight`;
646
+ }
647
+ function buildPoseWeightPathMap(poses, faceId) {
648
+ const faceSegment = faceId?.trim() || "face";
649
+ const map = /* @__PURE__ */ new Map();
650
+ poses.forEach((pose) => {
651
+ map.set(
652
+ pose.id,
653
+ buildRigInputPath(faceSegment, buildPoseWeightRelativePath(pose.id))
654
+ );
655
+ });
656
+ return map;
657
+ }
658
+ function normalizePoseSemanticKey(value) {
659
+ const trimmed = value?.trim() ?? "";
660
+ if (!trimmed) {
661
+ return null;
662
+ }
663
+ const normalized = trimmed.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
664
+ if (!normalized) {
665
+ return null;
666
+ }
667
+ return POSE_KEY_ALIASES[normalized] ?? normalized;
668
+ }
669
+ function derivePoseSemanticKeyFromId(poseId) {
670
+ const trimmed = poseId?.trim() ?? "";
671
+ if (!trimmed) {
672
+ return null;
673
+ }
674
+ const stripped = trimmed.replace(/^pose_d_/i, "").replace(/^pose_/i, "").replace(/^d_/i, "").replace(/_d$/i, "");
675
+ return normalizePoseSemanticKey(stripped);
676
+ }
677
+ function getPoseSemanticKey(pose) {
678
+ return normalizePoseSemanticKey(pose.name) ?? derivePoseSemanticKeyFromId(pose.id) ?? null;
679
+ }
680
+ function normalizePoseGroupPath(value) {
681
+ const trimmed = value?.trim() ?? "";
682
+ if (!trimmed) {
683
+ return null;
684
+ }
685
+ return trimmed.replace(/^\/+|\/+$/g, "").replace(/\/+/g, "/");
686
+ }
687
+ function sanitizePoseGroupId(value, fallback) {
688
+ const normalized = (value ?? "").trim().replace(/[^a-zA-Z0-9_/-]+/g, "_").replace(/^\/+|\/+$/g, "").replace(/\/+/g, "_");
689
+ if (!normalized) {
690
+ return fallback.replace(/\//g, "_");
691
+ }
692
+ return normalized;
693
+ }
694
+ function buildPoseGroupLookup(groups) {
695
+ const byId = /* @__PURE__ */ new Map();
696
+ const byPath = /* @__PURE__ */ new Map();
697
+ const orderById = /* @__PURE__ */ new Map();
698
+ (groups ?? []).forEach((group, index) => {
699
+ const path = normalizePoseGroupPath(group.path ?? group.name ?? group.id);
700
+ if (!path) {
701
+ return;
702
+ }
703
+ const id = sanitizePoseGroupId(group.id, path);
704
+ 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;
705
+ const normalized = {
706
+ ...group,
707
+ id,
708
+ path,
709
+ name: humanizedName
710
+ };
711
+ byId.set(id, normalized);
712
+ if (!orderById.has(id)) {
713
+ orderById.set(id, index);
714
+ }
715
+ if (!byPath.has(path)) {
716
+ byPath.set(path, normalized);
717
+ }
718
+ });
719
+ return { byId, byPath, orderById };
720
+ }
721
+ function orderPoseMembershipIds(groupIds, groups) {
722
+ const { orderById } = buildPoseGroupLookup(groups);
723
+ const unique = Array.from(
724
+ new Set(
725
+ Array.from(groupIds).map((groupId) => groupId.trim()).filter((groupId) => groupId.length > 0)
726
+ )
727
+ );
728
+ unique.sort((left, right) => {
729
+ const leftIndex = orderById.get(left);
730
+ const rightIndex = orderById.get(right);
731
+ if (leftIndex !== void 0 && rightIndex !== void 0) {
732
+ return leftIndex - rightIndex;
733
+ }
734
+ if (leftIndex !== void 0) {
735
+ return -1;
736
+ }
737
+ if (rightIndex !== void 0) {
738
+ return 1;
739
+ }
740
+ const leftPath = normalizePoseGroupPath(left) ?? left;
741
+ const rightPath = normalizePoseGroupPath(right) ?? right;
742
+ const byPath = leftPath.localeCompare(rightPath);
743
+ if (byPath !== 0) {
744
+ return byPath;
745
+ }
746
+ return left.localeCompare(right);
747
+ });
748
+ return unique;
749
+ }
750
+ function valueHasNeedle(value, needles) {
751
+ const normalized = normalizePoseSemanticKey(value);
752
+ if (!normalized) {
753
+ return false;
754
+ }
755
+ return needles.some((needle) => normalized.includes(needle));
756
+ }
757
+ function resolvePoseMembership(pose, groups) {
758
+ const { byId, byPath } = buildPoseGroupLookup(groups);
759
+ const resolvedGroupIds = [];
760
+ const pathById = /* @__PURE__ */ new Map();
761
+ const addMembership = (groupId, path) => {
762
+ if (!groupId) {
763
+ return;
764
+ }
765
+ if (!resolvedGroupIds.includes(groupId)) {
766
+ resolvedGroupIds.push(groupId);
767
+ }
768
+ if (!path) {
769
+ return;
770
+ }
771
+ const existingPath = pathById.get(groupId);
772
+ const normalizedGroupIdPath = normalizePoseGroupPath(groupId);
773
+ const normalizedExistingPath = normalizePoseGroupPath(existingPath) ?? existingPath ?? null;
774
+ const normalizedIncomingPath = normalizePoseGroupPath(path) ?? path ?? null;
775
+ const shouldPromotePath = normalizedExistingPath === null || normalizedGroupIdPath !== null && normalizedExistingPath === normalizedGroupIdPath && normalizedIncomingPath !== normalizedGroupIdPath;
776
+ if (shouldPromotePath) {
777
+ pathById.set(groupId, path);
778
+ }
779
+ };
780
+ const addByPath = (rawPath) => {
781
+ const normalizedPath = normalizePoseGroupPath(rawPath);
782
+ if (!normalizedPath) {
783
+ return;
784
+ }
785
+ const existing = byPath.get(normalizedPath);
786
+ if (existing) {
787
+ addMembership(existing.id, existing.path);
788
+ return;
789
+ }
790
+ addMembership(sanitizePoseGroupId(null, normalizedPath), normalizedPath);
791
+ };
792
+ const addById = (rawId) => {
793
+ const trimmed = rawId?.trim() ?? "";
794
+ if (!trimmed) {
795
+ return;
796
+ }
797
+ const normalizedPath = normalizePoseGroupPath(trimmed);
798
+ const normalizedId = sanitizePoseGroupId(trimmed, trimmed);
799
+ const matchedById = byId.get(trimmed) ?? byId.get(normalizedId);
800
+ if (matchedById) {
801
+ addMembership(matchedById.id, matchedById.path);
802
+ return;
803
+ }
804
+ if (normalizedPath) {
805
+ const matchedByPath = byPath.get(normalizedPath);
806
+ if (matchedByPath) {
807
+ addMembership(matchedByPath.id, matchedByPath.path);
808
+ return;
809
+ }
810
+ }
811
+ addMembership(
812
+ normalizedId,
813
+ normalizedPath && normalizedPath.length > 0 ? normalizedPath : null
814
+ );
815
+ };
816
+ pose.groupIds?.forEach((groupId) => addById(groupId));
817
+ addById(pose.groupId);
818
+ addByPath(pose.group);
819
+ const orderedGroupIds = orderPoseMembershipIds(resolvedGroupIds, groups);
820
+ const primaryGroupId = orderedGroupIds[0] ?? null;
821
+ const primaryGroupPath = primaryGroupId ? byId.get(primaryGroupId)?.path ?? pathById.get(primaryGroupId) ?? null : null;
822
+ const groupPathsById = {};
823
+ orderedGroupIds.forEach((groupId) => {
824
+ const path = byId.get(groupId)?.path ?? pathById.get(groupId) ?? null;
825
+ if (path) {
826
+ groupPathsById[groupId] = path;
827
+ }
828
+ });
829
+ return {
830
+ groupIds: orderedGroupIds,
831
+ primaryGroupId,
832
+ primaryGroupPath,
833
+ groupPathsById
834
+ };
835
+ }
836
+ function poseMatchesGroupKind(pose, groups, needles) {
837
+ const membership = resolvePoseMembership(pose, groups);
838
+ if (valueHasNeedle(pose.group, needles) || valueHasNeedle(pose.groupId, needles)) {
839
+ return true;
840
+ }
841
+ if (pose.groupIds?.some((groupId) => valueHasNeedle(groupId, needles))) {
842
+ return true;
843
+ }
844
+ return membership.groupIds.some((groupId) => {
845
+ const path = membership.groupPathsById[groupId] ?? null;
846
+ const group = (groups ?? []).find((entry) => entry.id === groupId) ?? null;
847
+ return valueHasNeedle(groupId, needles) || valueHasNeedle(path, needles) || valueHasNeedle(group?.name, needles);
848
+ });
849
+ }
850
+ function resolvePoseSemantics(pose, groups) {
851
+ const membership = resolvePoseMembership(pose, groups);
852
+ const key = getPoseSemanticKey(pose);
853
+ const looksLikeVisemeGroup = poseMatchesGroupKind(
854
+ pose,
855
+ groups,
856
+ VISEME_GROUP_NEEDLES
857
+ );
858
+ const looksLikeEmotionGroup = poseMatchesGroupKind(
859
+ pose,
860
+ groups,
861
+ EMOTION_GROUP_NEEDLES
862
+ );
863
+ let kind = "other";
864
+ if (looksLikeVisemeGroup || key && VISEME_POSE_KEY_SET.has(key)) {
865
+ kind = "viseme";
866
+ } else if (looksLikeEmotionGroup || key && EMOTION_POSE_KEY_SET.has(key)) {
867
+ kind = "emotion";
868
+ }
869
+ return { key, kind, membership };
870
+ }
871
+ function filterPosesBySemanticKind(poses, groups, kind) {
872
+ return poses.filter(
873
+ (pose) => resolvePoseSemantics(pose, groups).kind === kind
874
+ );
875
+ }
876
+ function buildSemanticPoseWeightPathMap(poses, groups, faceId, kind) {
877
+ const map = /* @__PURE__ */ new Map();
878
+ const pathMap = buildPoseWeightPathMap(poses, faceId);
879
+ poses.forEach((pose) => {
880
+ const semantics = resolvePoseSemantics(pose, groups);
881
+ const path = pathMap.get(pose.id);
882
+ if (semantics.kind !== kind || !semantics.key || !path || map.has(semantics.key)) {
883
+ return;
884
+ }
885
+ map.set(semantics.key, path);
886
+ });
887
+ return map;
888
+ }
889
+
890
+ // src/utils/poseRuntime.ts
891
+ function shouldUseLegacyPoseWeightFallback(hasPoseGraph) {
892
+ return !hasPoseGraph;
893
+ }
894
+ function resolvePoseControlInputPath({
895
+ inputId,
896
+ basePath,
897
+ rigInputPathMap,
898
+ hasNativePoseControlInput
899
+ }) {
900
+ if (!inputId.trim()) {
901
+ return void 0;
902
+ }
903
+ return rigInputPathMap[inputId] ?? rigInputPathMap[`pose_control_${inputId}`] ?? rigInputPathMap[`direct_${inputId}`] ?? (hasNativePoseControlInput ? basePath : void 0);
904
+ }
905
+
906
+ // src/utils/clipPlayback.ts
907
+ function toFiniteNumber(value, fallback) {
908
+ const parsed = Number(value);
909
+ return Number.isFinite(parsed) ? parsed : fallback;
910
+ }
911
+ function lastKeyframeTime(track) {
912
+ const keyframes = Array.isArray(track.keyframes) ? track.keyframes : [];
913
+ let maxTime = 0;
914
+ keyframes.forEach((keyframe) => {
915
+ const time = Number(keyframe?.time);
916
+ if (Number.isFinite(time) && time > maxTime) {
917
+ maxTime = time;
918
+ }
919
+ });
920
+ return maxTime;
921
+ }
922
+ function resolveClipDurationSeconds(clip, fallbackDurationSeconds = 0) {
923
+ const fallback = Math.max(0, toFiniteNumber(fallbackDurationSeconds, 0));
924
+ if (!clip || typeof clip !== "object") {
925
+ return fallback;
926
+ }
927
+ const clipDuration = Number(clip.duration);
928
+ if (Number.isFinite(clipDuration) && clipDuration > 0) {
929
+ return clipDuration;
930
+ }
931
+ const tracks = Array.isArray(clip.tracks) ? clip.tracks : [];
932
+ let maxTime = 0;
933
+ tracks.forEach((track) => {
934
+ const time = lastKeyframeTime(track);
935
+ if (time > maxTime) {
936
+ maxTime = time;
937
+ }
938
+ });
939
+ return maxTime > 0 ? maxTime : fallback;
940
+ }
941
+
942
+ // src/utils/animationBridge.ts
943
+ function resolveAnimationBridgeOutputPaths(channel, faceId, rigInputMap) {
944
+ const normalizedChannel = channel.trim().replace(/^\/+/, "");
945
+ if (!normalizedChannel) {
946
+ return [];
947
+ }
948
+ const outputPaths = /* @__PURE__ */ new Set([normalizedChannel]);
949
+ if (normalizedChannel.startsWith("animation/")) {
950
+ return Array.from(outputPaths).sort(
951
+ (left, right) => left.localeCompare(right)
952
+ );
953
+ }
954
+ if (rigInputMap && Object.keys(rigInputMap).length > 0) {
955
+ const candidateKeys = /* @__PURE__ */ new Set([normalizedChannel]);
956
+ const rigChannelMatch2 = /^rig\/[^/]+\/(.+)$/.exec(normalizedChannel);
957
+ if (rigChannelMatch2?.[1]) {
958
+ candidateKeys.add(rigChannelMatch2[1]);
959
+ }
960
+ candidateKeys.forEach((key) => {
961
+ const mapped = rigInputMap[key];
962
+ const normalized = mapped?.trim().replace(/^\/+/, "");
963
+ if (normalized) {
964
+ outputPaths.add(normalized);
965
+ }
966
+ });
967
+ const suffix = normalizedChannel.includes("/") ? normalizedChannel : `/${normalizedChannel}`;
968
+ Object.values(rigInputMap).forEach((mappedPath) => {
969
+ const normalized = mappedPath?.trim().replace(/^\/+/, "");
970
+ if (!normalized) {
971
+ return;
972
+ }
973
+ if (normalized === normalizedChannel || normalized.endsWith(suffix)) {
974
+ outputPaths.add(normalized);
975
+ }
976
+ });
977
+ }
978
+ if (!faceId) {
979
+ return Array.from(outputPaths).sort(
980
+ (left, right) => left.localeCompare(right)
981
+ );
982
+ }
983
+ const rigChannelMatch = /^rig\/[^/]+\/(.+)$/.exec(normalizedChannel);
984
+ if (rigChannelMatch?.[1]) {
985
+ outputPaths.add(`rig/${faceId}/${rigChannelMatch[1]}`);
986
+ } else if (!normalizedChannel.startsWith("rig/")) {
987
+ outputPaths.add(`rig/${faceId}/${normalizedChannel}`);
988
+ }
989
+ return Array.from(outputPaths).sort(
990
+ (left, right) => left.localeCompare(right)
991
+ );
992
+ }
993
+ function collectAnimationClipOutputPaths(clip, faceId, rigInputMap) {
994
+ const outputPaths = /* @__PURE__ */ new Set();
995
+ const tracks = Array.isArray(clip.tracks) ? clip.tracks : [];
996
+ tracks.forEach((track) => {
997
+ const channel = typeof track.channel === "string" ? track.channel.trim() : "";
998
+ if (!channel) {
999
+ return;
1000
+ }
1001
+ resolveAnimationBridgeOutputPaths(channel, faceId, rigInputMap).forEach(
1002
+ (path) => {
1003
+ outputPaths.add(path);
1004
+ }
1005
+ );
1006
+ });
1007
+ return Array.from(outputPaths).sort(
1008
+ (left, right) => left.localeCompare(right)
1009
+ );
1010
+ }
1011
+
89
1012
  // src/utils/valueConversion.ts
90
1013
  import {
1014
+ fromAroraValueJSON,
91
1015
  isNormalizedValue,
92
1016
  valueAsBool,
93
1017
  valueAsColorRgba,
@@ -140,27 +1064,28 @@ function valueJSONToRaw(value) {
140
1064
  if (Array.isArray(value)) {
141
1065
  return value.map((entry) => valueJSONToRaw(entry));
142
1066
  }
143
- if (typeof value === "object" && !("type" in value)) {
144
- const entries = Object.entries(value).map(([key, entry]) => [
145
- key,
146
- valueJSONToRaw(entry)
147
- ]);
148
- return Object.fromEntries(entries);
149
- }
150
- if (!isNormalizedValue(value)) {
1067
+ const normalized = isNormalizedValue(value) ? value : fromAroraValueJSON(value);
1068
+ if (normalized === void 0) {
1069
+ if (typeof value === "object" && !("type" in value)) {
1070
+ const entries = Object.entries(value).map(([key, entry]) => [
1071
+ key,
1072
+ valueJSONToRaw(entry)
1073
+ ]);
1074
+ return Object.fromEntries(entries);
1075
+ }
151
1076
  return void 0;
152
1077
  }
153
- switch (value.type) {
1078
+ switch (normalized.type) {
154
1079
  case "float": {
155
- const num = valueAsNumber(value);
1080
+ const num = valueAsNumber(normalized);
156
1081
  return typeof num === "number" ? num : void 0;
157
1082
  }
158
1083
  case "bool": {
159
- const boolVal = valueAsBool(value);
1084
+ const boolVal = valueAsBool(normalized);
160
1085
  return typeof boolVal === "boolean" ? boolVal : void 0;
161
1086
  }
162
1087
  case "text": {
163
- const text = valueAsText(value);
1088
+ const text = valueAsText(normalized);
164
1089
  return typeof text === "string" ? text : void 0;
165
1090
  }
166
1091
  case "vec2":
@@ -168,11 +1093,11 @@ function valueJSONToRaw(value) {
168
1093
  case "vec4":
169
1094
  case "quat":
170
1095
  case "vector": {
171
- const vec = valueAsVector(value);
1096
+ const vec = valueAsVector(normalized);
172
1097
  return vec ? numericArrayToRaw(vec) : void 0;
173
1098
  }
174
1099
  case "colorrgba": {
175
- const color = valueAsColorRgba(value);
1100
+ const color = valueAsColorRgba(normalized);
176
1101
  if (!color) {
177
1102
  return void 0;
178
1103
  }
@@ -180,7 +1105,7 @@ function valueJSONToRaw(value) {
180
1105
  return { r, g, b, a };
181
1106
  }
182
1107
  case "transform": {
183
- const transform = valueAsTransform(value);
1108
+ const transform = valueAsTransform(normalized);
184
1109
  if (!transform) {
185
1110
  return void 0;
186
1111
  }
@@ -190,15 +1115,20 @@ function valueJSONToRaw(value) {
190
1115
  scale: numericArrayToRaw(transform.scale)
191
1116
  };
192
1117
  }
1118
+ case "array":
1119
+ case "list":
1120
+ case "tuple":
1121
+ return (normalized.data ?? []).map(
1122
+ (entry) => valueJSONToRaw(entry)
1123
+ );
193
1124
  case "record": {
194
- const entries = Object.entries(value.data ?? {}).map(([key, entry]) => [
195
- key,
196
- valueJSONToRaw(entry)
197
- ]);
1125
+ const entries = Object.entries(normalized.data ?? {}).map(
1126
+ ([key, entry]) => [key, valueJSONToRaw(entry)]
1127
+ );
198
1128
  return Object.fromEntries(entries);
199
1129
  }
200
1130
  case "enum": {
201
- const [tag, inner] = value.data;
1131
+ const [tag, inner] = normalized.data;
202
1132
  return {
203
1133
  tag,
204
1134
  value: valueJSONToRaw(inner)
@@ -209,16 +1139,620 @@ function valueJSONToRaw(value) {
209
1139
  }
210
1140
  }
211
1141
 
1142
+ // src/engine/animationModule.ts
1143
+ var ANIMATION_MODULE_FN = {
1144
+ loadAnimation: "76697a69-6a00-0000-0f00-000000000001",
1145
+ createPlayer: "76697a69-6a00-0000-0f00-000000000002",
1146
+ addInstance: "76697a69-6a00-0000-0f00-000000000003",
1147
+ step: "76697a69-6a00-0000-0f00-000000000004",
1148
+ play: "76697a69-6a00-0000-0f00-000000000005",
1149
+ pause: "76697a69-6a00-0000-0f00-000000000006",
1150
+ stop: "76697a69-6a00-0000-0f00-000000000007",
1151
+ seek: "76697a69-6a00-0000-0f00-000000000008",
1152
+ setSpeed: "76697a69-6a00-0000-0f00-000000000009",
1153
+ setLoop: "76697a69-6a00-0000-0f00-00000000000a",
1154
+ setWeight: "76697a69-6a00-0000-0f00-00000000000b",
1155
+ removeInstance: "76697a69-6a00-0000-0f00-00000000000c",
1156
+ playerStates: "76697a69-6a00-0000-0f00-00000000000d"
1157
+ };
1158
+ var ANIMATION_MODULE_PARAM = {
1159
+ clip: "76697a69-6a00-0000-0f01-000000000001",
1160
+ playerName: "76697a69-6a00-0000-0f02-000000000001",
1161
+ player: "76697a69-6a00-0000-0f03-000000000001",
1162
+ anim: "76697a69-6a00-0000-0f03-000000000002",
1163
+ dtNs: "76697a69-6a00-0000-0f04-000000000001",
1164
+ playPlayer: "76697a69-6a00-0000-0f05-000000000001",
1165
+ pausePlayer: "76697a69-6a00-0000-0f06-000000000001",
1166
+ stopPlayer: "76697a69-6a00-0000-0f07-000000000001",
1167
+ seekPlayer: "76697a69-6a00-0000-0f08-000000000001",
1168
+ seekTimeNs: "76697a69-6a00-0000-0f08-000000000002",
1169
+ speedPlayer: "76697a69-6a00-0000-0f09-000000000001",
1170
+ speedValue: "76697a69-6a00-0000-0f09-000000000002",
1171
+ loopPlayer: "76697a69-6a00-0000-0f0a-000000000001",
1172
+ loopMode: "76697a69-6a00-0000-0f0a-000000000002",
1173
+ weightPlayer: "76697a69-6a00-0000-0f0b-000000000001",
1174
+ weightInstance: "76697a69-6a00-0000-0f0b-000000000002",
1175
+ weightValue: "76697a69-6a00-0000-0f0b-000000000003",
1176
+ removePlayer: "76697a69-6a00-0000-0f0c-000000000001",
1177
+ removeInstance: "76697a69-6a00-0000-0f0c-000000000002"
1178
+ };
1179
+ var ANIMATION_MODULE_TYPE = {
1180
+ clip: "76697a69-6a00-0000-0000-000000000100",
1181
+ track: "76697a69-6a00-0000-0000-000000000101",
1182
+ keypoint: "76697a69-6a00-0000-0000-000000000102",
1183
+ transitionHandle: "76697a69-6a00-0000-0000-000000000103",
1184
+ trackOutput: "76697a69-6a00-0000-0000-000000000110",
1185
+ playerState: "76697a69-6a00-0000-0000-000000000111"
1186
+ };
1187
+ var ANIMATION_MODULE_FIELD = {
1188
+ clipName: "76697a69-6a00-0000-0100-000000000001",
1189
+ clipDuration: "76697a69-6a00-0000-0100-000000000002",
1190
+ clipTracks: "76697a69-6a00-0000-0100-000000000003",
1191
+ trackId: "76697a69-6a00-0000-0101-000000000001",
1192
+ trackName: "76697a69-6a00-0000-0101-000000000002",
1193
+ trackAnimatable: "76697a69-6a00-0000-0101-000000000003",
1194
+ trackPoints: "76697a69-6a00-0000-0101-000000000004",
1195
+ keypointId: "76697a69-6a00-0000-0102-000000000001",
1196
+ keypointStamp: "76697a69-6a00-0000-0102-000000000002",
1197
+ keypointValue: "76697a69-6a00-0000-0102-000000000003",
1198
+ keypointTransitionsIn: "76697a69-6a00-0000-0102-000000000004",
1199
+ keypointTransitionsOut: "76697a69-6a00-0000-0102-000000000005",
1200
+ handleX: "76697a69-6a00-0000-0103-000000000001",
1201
+ handleY: "76697a69-6a00-0000-0103-000000000002",
1202
+ outputTrackId: "76697a69-6a00-0000-0110-000000000001",
1203
+ outputDefaultKey: "76697a69-6a00-0000-0110-000000000002",
1204
+ outputValue: "76697a69-6a00-0000-0110-000000000003",
1205
+ statePlayer: "76697a69-6a00-0000-0111-000000000001",
1206
+ stateState: "76697a69-6a00-0000-0111-000000000002",
1207
+ stateTimeNs: "76697a69-6a00-0000-0111-000000000003",
1208
+ stateDurationNs: "76697a69-6a00-0000-0111-000000000004",
1209
+ stateSpeed: "76697a69-6a00-0000-0111-000000000005"
1210
+ };
1211
+ var field = (id, value) => ({
1212
+ id,
1213
+ value
1214
+ });
1215
+ function storedClipToModuleValue(clip, resolveKeys) {
1216
+ const name = typeof clip.name === "string" && clip.name.trim().length > 0 ? clip.name.trim() : typeof clip.id === "string" ? clip.id : "";
1217
+ const durationMs = Number(clip.duration);
1218
+ const duration = Number.isFinite(durationMs) && durationMs > 0 ? Math.round(durationMs) : 1;
1219
+ const tracks = (Array.isArray(clip.tracks) ? clip.tracks : []).map((rawTrack, index) => {
1220
+ if (!rawTrack || typeof rawTrack !== "object") {
1221
+ return null;
1222
+ }
1223
+ const animatableId = typeof rawTrack.animatableId === "string" ? rawTrack.animatableId.trim() : "";
1224
+ if (!animatableId) {
1225
+ return null;
1226
+ }
1227
+ const trackId = typeof rawTrack.id === "string" && rawTrack.id.trim().length > 0 ? rawTrack.id.trim() : `track-${index}`;
1228
+ const trackName = typeof rawTrack.name === "string" && rawTrack.name.trim().length > 0 ? rawTrack.name.trim() : trackId;
1229
+ const points = (Array.isArray(rawTrack.points) ? rawTrack.points : []).map((rawPoint, pointIndex) => {
1230
+ if (!rawPoint || typeof rawPoint !== "object") {
1231
+ return null;
1232
+ }
1233
+ const stamp = Number(rawPoint.stamp);
1234
+ const value = Number(rawPoint.value);
1235
+ if (!Number.isFinite(stamp) || !Number.isFinite(value)) {
1236
+ return null;
1237
+ }
1238
+ const pointId = typeof rawPoint.id === "string" && rawPoint.id.trim().length > 0 ? rawPoint.id.trim() : `${trackId}:point-${pointIndex}`;
1239
+ const handles = (handle) => ({
1240
+ structs: {
1241
+ id: ANIMATION_MODULE_TYPE.transitionHandle,
1242
+ elements: handle ? [
1243
+ {
1244
+ fields: [
1245
+ field(ANIMATION_MODULE_FIELD.handleX, {
1246
+ f32: handle.x
1247
+ }),
1248
+ field(ANIMATION_MODULE_FIELD.handleY, {
1249
+ f32: handle.y
1250
+ })
1251
+ ]
1252
+ }
1253
+ ] : []
1254
+ }
1255
+ });
1256
+ return {
1257
+ fields: [
1258
+ field(ANIMATION_MODULE_FIELD.keypointId, { str: pointId }),
1259
+ field(ANIMATION_MODULE_FIELD.keypointStamp, {
1260
+ f32: Math.max(0, Math.min(1, stamp))
1261
+ }),
1262
+ field(ANIMATION_MODULE_FIELD.keypointValue, { f32: value }),
1263
+ field(
1264
+ ANIMATION_MODULE_FIELD.keypointTransitionsIn,
1265
+ handles(rawPoint.transitions?.in)
1266
+ ),
1267
+ field(
1268
+ ANIMATION_MODULE_FIELD.keypointTransitionsOut,
1269
+ handles(rawPoint.transitions?.out)
1270
+ )
1271
+ ]
1272
+ };
1273
+ }).filter(Boolean);
1274
+ if (points.length === 0) {
1275
+ return null;
1276
+ }
1277
+ const targets = resolveKeys?.(animatableId) ?? [animatableId];
1278
+ return targets.map((target, targetIndex) => ({
1279
+ fields: [
1280
+ field(ANIMATION_MODULE_FIELD.trackId, {
1281
+ str: targetIndex === 0 ? trackId : `${trackId}~${targetIndex}`
1282
+ }),
1283
+ field(ANIMATION_MODULE_FIELD.trackName, { str: trackName }),
1284
+ field(ANIMATION_MODULE_FIELD.trackAnimatable, { str: target }),
1285
+ field(ANIMATION_MODULE_FIELD.trackPoints, {
1286
+ structs: {
1287
+ id: ANIMATION_MODULE_TYPE.keypoint,
1288
+ elements: points
1289
+ }
1290
+ })
1291
+ ]
1292
+ }));
1293
+ }).filter(Boolean).flat();
1294
+ return {
1295
+ struct: {
1296
+ id: ANIMATION_MODULE_TYPE.clip,
1297
+ fields: [
1298
+ field(ANIMATION_MODULE_FIELD.clipName, { str: name }),
1299
+ field(ANIMATION_MODULE_FIELD.clipDuration, { u32: duration }),
1300
+ field(ANIMATION_MODULE_FIELD.clipTracks, {
1301
+ structs: {
1302
+ id: ANIMATION_MODULE_TYPE.track,
1303
+ elements: tracks
1304
+ }
1305
+ })
1306
+ ]
1307
+ }
1308
+ };
1309
+ }
1310
+ function loadAnimationCall(clipValue) {
1311
+ return {
1312
+ id: ANIMATION_MODULE_FN.loadAnimation,
1313
+ args: [field(ANIMATION_MODULE_PARAM.clip, clipValue)]
1314
+ };
1315
+ }
1316
+ function createPlayerCall(name) {
1317
+ return {
1318
+ id: ANIMATION_MODULE_FN.createPlayer,
1319
+ args: [field(ANIMATION_MODULE_PARAM.playerName, { str: name })]
1320
+ };
1321
+ }
1322
+ function addInstanceCall(player, anim) {
1323
+ return {
1324
+ id: ANIMATION_MODULE_FN.addInstance,
1325
+ args: [
1326
+ field(ANIMATION_MODULE_PARAM.player, { u32: player }),
1327
+ field(ANIMATION_MODULE_PARAM.anim, { u32: anim })
1328
+ ]
1329
+ };
1330
+ }
1331
+ function playCall(player) {
1332
+ return {
1333
+ id: ANIMATION_MODULE_FN.play,
1334
+ args: [field(ANIMATION_MODULE_PARAM.playPlayer, { u32: player })]
1335
+ };
1336
+ }
1337
+ function pauseCall(player) {
1338
+ return {
1339
+ id: ANIMATION_MODULE_FN.pause,
1340
+ args: [field(ANIMATION_MODULE_PARAM.pausePlayer, { u32: player })]
1341
+ };
1342
+ }
1343
+ function stopCall(player) {
1344
+ return {
1345
+ id: ANIMATION_MODULE_FN.stop,
1346
+ args: [field(ANIMATION_MODULE_PARAM.stopPlayer, { u32: player })]
1347
+ };
1348
+ }
1349
+ function seekCall(player, timeNs) {
1350
+ return {
1351
+ id: ANIMATION_MODULE_FN.seek,
1352
+ args: [
1353
+ field(ANIMATION_MODULE_PARAM.seekPlayer, { u32: player }),
1354
+ field(ANIMATION_MODULE_PARAM.seekTimeNs, {
1355
+ u64: Math.max(0, Math.round(timeNs))
1356
+ })
1357
+ ]
1358
+ };
1359
+ }
1360
+ function setSpeedCall(player, speed) {
1361
+ return {
1362
+ id: ANIMATION_MODULE_FN.setSpeed,
1363
+ args: [
1364
+ field(ANIMATION_MODULE_PARAM.speedPlayer, { u32: player }),
1365
+ field(ANIMATION_MODULE_PARAM.speedValue, { f32: speed })
1366
+ ]
1367
+ };
1368
+ }
1369
+ function setLoopCall(player, mode) {
1370
+ return {
1371
+ id: ANIMATION_MODULE_FN.setLoop,
1372
+ args: [
1373
+ field(ANIMATION_MODULE_PARAM.loopPlayer, { u32: player }),
1374
+ field(ANIMATION_MODULE_PARAM.loopMode, { str: mode })
1375
+ ]
1376
+ };
1377
+ }
1378
+ function setWeightCall(player, instance, weight) {
1379
+ return {
1380
+ id: ANIMATION_MODULE_FN.setWeight,
1381
+ args: [
1382
+ field(ANIMATION_MODULE_PARAM.weightPlayer, { u32: player }),
1383
+ field(ANIMATION_MODULE_PARAM.weightInstance, { u32: instance }),
1384
+ field(ANIMATION_MODULE_PARAM.weightValue, { f32: weight })
1385
+ ]
1386
+ };
1387
+ }
1388
+ function callResultU32(result) {
1389
+ const ret = result.ret;
1390
+ if (ret && typeof ret === "object" && "u32" in ret) {
1391
+ const value = Number(ret.u32);
1392
+ return Number.isFinite(value) ? value : null;
1393
+ }
1394
+ return null;
1395
+ }
1396
+ var ANIMATION_PLAYERS_PATH = "vizij/animations/players";
1397
+ var ANIMATIONS_SOURCE_ID = "animations";
1398
+ function animationsGraphSource() {
1399
+ return {
1400
+ sourceId: ANIMATIONS_SOURCE_ID,
1401
+ spec: {
1402
+ nodes: [
1403
+ { id: "dt", type: "input", params: { path: "arora/dt" } },
1404
+ {
1405
+ id: "step",
1406
+ type: "externalfunction",
1407
+ params: {
1408
+ function: ANIMATION_MODULE_FN.step,
1409
+ param_ids: [ANIMATION_MODULE_PARAM.dtNs]
1410
+ }
1411
+ },
1412
+ {
1413
+ id: "apply",
1414
+ type: "output",
1415
+ params: {
1416
+ key_field: ANIMATION_MODULE_FIELD.outputDefaultKey,
1417
+ value_field: ANIMATION_MODULE_FIELD.outputValue
1418
+ }
1419
+ },
1420
+ {
1421
+ id: "states",
1422
+ type: "externalfunction",
1423
+ params: { function: ANIMATION_MODULE_FN.playerStates, param_ids: [] }
1424
+ },
1425
+ {
1426
+ id: "states-out",
1427
+ type: "output",
1428
+ params: { path: ANIMATION_PLAYERS_PATH }
1429
+ }
1430
+ ],
1431
+ edges: [
1432
+ { from: { node_id: "dt" }, to: { node_id: "step", input: "args_0" } },
1433
+ { from: { node_id: "step" }, to: { node_id: "apply", input: "in" } },
1434
+ {
1435
+ from: { node_id: "states" },
1436
+ to: { node_id: "states-out", input: "in" }
1437
+ }
1438
+ ]
1439
+ }
1440
+ };
1441
+ }
1442
+ function fieldNumber(value) {
1443
+ if (!value || typeof value !== "object") {
1444
+ return null;
1445
+ }
1446
+ for (const key of ["u64", "u32", "f32", "f64"]) {
1447
+ if (key in value) {
1448
+ const parsed = Number(value[key]);
1449
+ return Number.isFinite(parsed) ? parsed : null;
1450
+ }
1451
+ }
1452
+ return null;
1453
+ }
1454
+ function decodePlayerStates(raw) {
1455
+ if (!raw || typeof raw !== "object" || !("structs" in raw)) {
1456
+ return [];
1457
+ }
1458
+ const structs = raw.structs;
1459
+ const elements = Array.isArray(structs?.elements) ? structs.elements : [];
1460
+ const states = [];
1461
+ for (const element of elements) {
1462
+ const fields = element?.fields;
1463
+ if (!Array.isArray(fields)) {
1464
+ continue;
1465
+ }
1466
+ const byId = /* @__PURE__ */ new Map();
1467
+ for (const entry of fields) {
1468
+ const id = entry?.id;
1469
+ const value = entry?.value;
1470
+ if (typeof id === "string" && value && typeof value === "object") {
1471
+ byId.set(id, value);
1472
+ }
1473
+ }
1474
+ const player = fieldNumber(byId.get(ANIMATION_MODULE_FIELD.statePlayer));
1475
+ if (player === null) {
1476
+ continue;
1477
+ }
1478
+ const stateValue = byId.get(ANIMATION_MODULE_FIELD.stateState);
1479
+ states.push({
1480
+ player,
1481
+ state: stateValue && "str" in stateValue ? String(stateValue.str) : "",
1482
+ time: (fieldNumber(byId.get(ANIMATION_MODULE_FIELD.stateTimeNs)) ?? 0) / 1e9,
1483
+ duration: (fieldNumber(byId.get(ANIMATION_MODULE_FIELD.stateDurationNs)) ?? 0) / 1e9,
1484
+ speed: fieldNumber(byId.get(ANIMATION_MODULE_FIELD.stateSpeed)) ?? 1
1485
+ });
1486
+ }
1487
+ return states;
1488
+ }
1489
+
1490
+ // src/engine/animationModuleHost.ts
1491
+ var AnimationModuleHost = class {
1492
+ constructor(getDevice, resolveKeys = (key) => [key]) {
1493
+ this.getDevice = getDevice;
1494
+ this.resolveKeys = resolveKeys;
1495
+ this.clips = /* @__PURE__ */ new Map();
1496
+ }
1497
+ /** Whether any clip is currently marked playing (drives source lifecycle). */
1498
+ hasPlaying() {
1499
+ for (const entry of this.clips.values()) {
1500
+ if (entry.playing) {
1501
+ return true;
1502
+ }
1503
+ }
1504
+ return false;
1505
+ }
1506
+ /** Whether a clip id is known to the host. */
1507
+ has(clipId) {
1508
+ return this.clips.has(clipId);
1509
+ }
1510
+ /** The clip's module `PlayerId` on the live device (feedback correlation). */
1511
+ playerIdOf(clipId) {
1512
+ return this.clips.get(clipId)?.playerId ?? null;
1513
+ }
1514
+ /** The clip's transport state as last commanded. */
1515
+ transportOf(clipId) {
1516
+ const entry = this.clips.get(clipId);
1517
+ if (!entry) {
1518
+ return null;
1519
+ }
1520
+ return {
1521
+ playing: entry.playing,
1522
+ speed: entry.speed,
1523
+ loop: entry.loop,
1524
+ weight: entry.weight
1525
+ };
1526
+ }
1527
+ /**
1528
+ * Declare the clips the bundle carries, as data. Keeps playing/transport
1529
+ * state for clips that survive; clips no longer present are dropped. Does
1530
+ * not touch the device — loading is lazy (on first play), so muted/unused
1531
+ * clips never enter the module, and conversion happens at load with the
1532
+ * key resolver's state at that moment.
1533
+ */
1534
+ setClips(clips) {
1535
+ const nextIds = new Set(clips.map((clip) => clip.id));
1536
+ for (const id of Array.from(this.clips.keys())) {
1537
+ if (!nextIds.has(id)) {
1538
+ this.clips.delete(id);
1539
+ }
1540
+ }
1541
+ for (const clip of clips) {
1542
+ const existing = this.clips.get(clip.id);
1543
+ if (existing) {
1544
+ existing.stored = clip.stored;
1545
+ existing.animId = null;
1546
+ existing.playerId = null;
1547
+ existing.instId = null;
1548
+ } else {
1549
+ this.clips.set(clip.id, {
1550
+ stored: clip.stored,
1551
+ playing: false,
1552
+ speed: 1,
1553
+ loop: "loop",
1554
+ weight: 1,
1555
+ animId: null,
1556
+ playerId: null,
1557
+ instId: null
1558
+ });
1559
+ }
1560
+ }
1561
+ }
1562
+ /**
1563
+ * Mark a clip playing, ensure it is loaded into the live device with a
1564
+ * player + instance, and issue `play` (a resume after `pause` needs it; on
1565
+ * a fresh player it is a no-op). Idempotent. Resolves once the calls have
1566
+ * dispatched (or immediately when there is no live device yet — the
1567
+ * provider registers the source, which boots the device and replays via
1568
+ * `replayInto`).
1569
+ */
1570
+ async play(clipId) {
1571
+ const entry = this.clips.get(clipId);
1572
+ if (!entry) {
1573
+ return;
1574
+ }
1575
+ entry.playing = true;
1576
+ const device = this.getDevice();
1577
+ if (!device) {
1578
+ return;
1579
+ }
1580
+ await this.ensureLoaded(device, entry);
1581
+ if (entry.playerId !== null) {
1582
+ await device.call(playCall(entry.playerId));
1583
+ }
1584
+ }
1585
+ /** Hold the clip's playhead (`pause` on its player). */
1586
+ pause(clipId) {
1587
+ const entry = this.clips.get(clipId);
1588
+ if (!entry) {
1589
+ return;
1590
+ }
1591
+ entry.playing = false;
1592
+ this.dispatch(
1593
+ (device) => entry.playerId !== null ? device.call(pauseCall(entry.playerId)) : null
1594
+ );
1595
+ }
1596
+ /**
1597
+ * Stop a clip: not playing, playhead reset to the window start (`stop` on
1598
+ * its player — the next tick emits the clip's t=0 pose). The clip stays
1599
+ * loaded for a cheap replay.
1600
+ */
1601
+ stop(clipId) {
1602
+ const entry = this.clips.get(clipId);
1603
+ if (!entry) {
1604
+ return;
1605
+ }
1606
+ entry.playing = false;
1607
+ this.dispatch(
1608
+ (device) => entry.playerId !== null ? device.call(stopCall(entry.playerId)) : null
1609
+ );
1610
+ }
1611
+ /** Move the clip's playhead to `seconds`. */
1612
+ seek(clipId, seconds) {
1613
+ const entry = this.clips.get(clipId);
1614
+ if (!entry) {
1615
+ return;
1616
+ }
1617
+ this.dispatch(
1618
+ (device) => entry.playerId !== null ? device.call(seekCall(entry.playerId, seconds * 1e9)) : null
1619
+ );
1620
+ }
1621
+ /** Set the clip's playback speed multiplier. */
1622
+ setSpeed(clipId, speed) {
1623
+ const entry = this.clips.get(clipId);
1624
+ if (!entry) {
1625
+ return;
1626
+ }
1627
+ entry.speed = speed;
1628
+ this.dispatch(
1629
+ (device) => entry.playerId !== null ? device.call(setSpeedCall(entry.playerId, speed)) : null
1630
+ );
1631
+ }
1632
+ /** Set how the clip's player time maps into clip time. */
1633
+ setLoop(clipId, mode) {
1634
+ const entry = this.clips.get(clipId);
1635
+ if (!entry) {
1636
+ return;
1637
+ }
1638
+ entry.loop = mode;
1639
+ this.dispatch(
1640
+ (device) => entry.playerId !== null ? device.call(setLoopCall(entry.playerId, mode)) : null
1641
+ );
1642
+ }
1643
+ /** Set the clip instance's blend weight. */
1644
+ setWeight(clipId, weight) {
1645
+ const entry = this.clips.get(clipId);
1646
+ if (!entry) {
1647
+ return;
1648
+ }
1649
+ entry.weight = weight;
1650
+ this.dispatch(
1651
+ (device) => entry.playerId !== null && entry.instId !== null ? device.call(setWeightCall(entry.playerId, entry.instId, weight)) : null
1652
+ );
1653
+ }
1654
+ /**
1655
+ * Re-issue setup and transport for every playing clip against a freshly
1656
+ * started device (module guest state did not survive the rebuild). Voids
1657
+ * stale ids first. Called from `DeviceSlot.onDeviceStarted`.
1658
+ */
1659
+ replayInto(device) {
1660
+ for (const entry of this.clips.values()) {
1661
+ entry.animId = null;
1662
+ entry.playerId = null;
1663
+ entry.instId = null;
1664
+ if (entry.playing) {
1665
+ void this.ensureLoaded(device, entry);
1666
+ }
1667
+ }
1668
+ }
1669
+ /** Fire-and-forget a transport call against the live device, if any. */
1670
+ dispatch(issue) {
1671
+ const device = this.getDevice();
1672
+ if (!device) {
1673
+ return;
1674
+ }
1675
+ void issue(device)?.catch(() => {
1676
+ });
1677
+ }
1678
+ async ensureLoaded(device, entry) {
1679
+ if (entry.instId !== null) {
1680
+ return;
1681
+ }
1682
+ if (entry.animId === null) {
1683
+ const anim = await device.call(
1684
+ loadAnimationCall(
1685
+ storedClipToModuleValue(entry.stored, this.resolveKeys)
1686
+ )
1687
+ );
1688
+ entry.animId = callResultU32(anim);
1689
+ }
1690
+ if (entry.playerId === null) {
1691
+ const player = await device.call(createPlayerCall(""));
1692
+ entry.playerId = callResultU32(player);
1693
+ }
1694
+ if (entry.instId === null && entry.animId !== null && entry.playerId !== null) {
1695
+ const inst = await device.call(
1696
+ addInstanceCall(entry.playerId, entry.animId)
1697
+ );
1698
+ entry.instId = callResultU32(inst);
1699
+ }
1700
+ const followups = [];
1701
+ if (entry.playerId !== null) {
1702
+ if (entry.speed !== 1) {
1703
+ followups.push(setSpeedCall(entry.playerId, entry.speed));
1704
+ }
1705
+ if (entry.loop !== "loop") {
1706
+ followups.push(setLoopCall(entry.playerId, entry.loop));
1707
+ }
1708
+ if (entry.instId !== null && entry.weight !== 1) {
1709
+ followups.push(
1710
+ setWeightCall(entry.playerId, entry.instId, entry.weight)
1711
+ );
1712
+ }
1713
+ }
1714
+ for (const call of followups) {
1715
+ await device.call(call);
1716
+ }
1717
+ }
1718
+ };
1719
+
212
1720
  // src/VizijRuntimeProvider.tsx
213
1721
  import { jsx } from "react/jsx-runtime";
214
1722
  var ACTIVE_GRACE_MS = 250;
215
1723
  var VISIBLE_IDLE_FPS = 30;
216
1724
  var HIDDEN_IDLE_FPS = 1;
1725
+ var IMPLICIT_PAUSE_GAP_S = 5;
1726
+ var inferImplicitPause = (dt) => {
1727
+ if (dt <= IMPLICIT_PAUSE_GAP_S) {
1728
+ return dt;
1729
+ }
1730
+ console.warn(
1731
+ `[vizij-runtime] step loop woke after a ${dt.toFixed(
1732
+ 1
1733
+ )}s wall-clock gap; treating it as a suspension (dt = 0)`
1734
+ );
1735
+ return 0;
1736
+ };
217
1737
  var DEFAULT_MERGE = {
218
1738
  outputs: "add",
219
1739
  intermediate: "add"
220
1740
  };
221
1741
  var DEFAULT_DURATION = 0.35;
1742
+ var POSE_CONTROL_BRIDGE_EPSILON = 1e-6;
1743
+ var runtimeDebugInstanceSequence = 0;
1744
+ var DEV_MODE = (() => {
1745
+ const nodeEnv = globalThis.process?.env?.NODE_ENV;
1746
+ return typeof nodeEnv === "string" && nodeEnv === "development";
1747
+ })();
1748
+ function isRuntimeDebugEnabled() {
1749
+ if (DEV_MODE) {
1750
+ return true;
1751
+ }
1752
+ return Boolean(
1753
+ globalThis.__VIZIJ_RUNTIME_DEBUG__
1754
+ );
1755
+ }
222
1756
  var EASINGS = {
223
1757
  linear: (t) => t,
224
1758
  easeIn: (t) => t * t,
@@ -235,12 +1769,19 @@ function resolveEasing(easing) {
235
1769
  return EASINGS.linear;
236
1770
  }
237
1771
  function findRootId(world) {
1772
+ let fallback = null;
238
1773
  for (const entry of Object.values(world)) {
239
- if (entry && typeof entry === "object" && entry.type === "group" && entry.rootBounds && entry.id) {
1774
+ if (!entry || typeof entry !== "object" || entry.type !== "group") {
1775
+ continue;
1776
+ }
1777
+ if (entry.rootBounds && entry.id) {
240
1778
  return entry.id;
241
1779
  }
1780
+ if (!fallback && entry.id) {
1781
+ fallback = entry.id;
1782
+ }
242
1783
  }
243
- return null;
1784
+ return fallback;
244
1785
  }
245
1786
  function normalisePath(path) {
246
1787
  if (!path) {
@@ -410,6 +1951,30 @@ function namespaceGraphSpec(spec, namespace) {
410
1951
  nodes: nextNodes
411
1952
  };
412
1953
  }
1954
+ function stripNulls(value) {
1955
+ if (value === null) {
1956
+ return void 0;
1957
+ }
1958
+ if (Array.isArray(value)) {
1959
+ const next2 = value.map((entry) => stripNulls(entry)).filter((entry) => entry !== void 0 && entry !== null);
1960
+ return next2;
1961
+ }
1962
+ if (typeof value !== "object" || value === void 0) {
1963
+ return value;
1964
+ }
1965
+ const next = {};
1966
+ Object.entries(value).forEach(([key, entry]) => {
1967
+ if (entry === null) {
1968
+ return;
1969
+ }
1970
+ const cleaned = stripNulls(entry);
1971
+ if (cleaned === void 0) {
1972
+ return;
1973
+ }
1974
+ next[key] = cleaned;
1975
+ });
1976
+ return next;
1977
+ }
413
1978
  var now = () => typeof performance !== "undefined" ? performance.now() : Date.now();
414
1979
  function pickBundleGraph(bundle, preferredKinds) {
415
1980
  if (!bundle?.graphs || bundle.graphs.length === 0) {
@@ -580,26 +2145,49 @@ function convertExtractedAnimations(clips) {
580
2145
  }
581
2146
  const parsedValueSize = track.valueSize != null ? Number(track.valueSize) : NaN;
582
2147
  const valueSize = Number.isFinite(parsedValueSize) && parsedValueSize > 0 ? parsedValueSize : 1;
583
- if (values.length !== times.length * valueSize) {
2148
+ const interpolationRaw = typeof track.interpolation === "string" ? track.interpolation.trim().toLowerCase() : "linear";
2149
+ const interpolation = interpolationRaw === "step" ? "step" : interpolationRaw === "cubic" || interpolationRaw === "cubicspline" ? "cubic" : "linear";
2150
+ const isCubic = interpolation === "cubic";
2151
+ const hasTripletTangents = isCubic && values.length === times.length * valueSize * 3;
2152
+ const hasFlatValues = values.length === times.length * valueSize;
2153
+ if (!hasTripletTangents && !hasFlatValues) {
584
2154
  return;
585
2155
  }
586
2156
  const rawIndex = track.componentIndex != null ? Number(track.componentIndex) : 0;
587
2157
  const componentIndex = Number.isInteger(rawIndex) && rawIndex >= 0 ? Math.min(rawIndex, valueSize - 1) : 0;
588
2158
  const keyframes = [];
589
2159
  times.forEach((time, index) => {
590
- const base = index * valueSize + componentIndex;
591
- const value = values[base];
2160
+ const flatBase = index * valueSize + componentIndex;
2161
+ const valueBase = hasTripletTangents ? index * valueSize * 3 + valueSize + componentIndex : flatBase;
2162
+ const value = values[valueBase];
592
2163
  if (!Number.isFinite(value)) {
593
2164
  return;
594
2165
  }
595
- keyframes.push({ time, value });
2166
+ const keyframe = {
2167
+ time,
2168
+ value
2169
+ };
2170
+ if (hasTripletTangents) {
2171
+ const inBase = index * valueSize * 3 + componentIndex;
2172
+ const outBase = index * valueSize * 3 + valueSize * 2 + componentIndex;
2173
+ const inTangent = values[inBase];
2174
+ const outTangent = values[outBase];
2175
+ if (Number.isFinite(inTangent)) {
2176
+ keyframe.inTangent = inTangent;
2177
+ }
2178
+ if (Number.isFinite(outTangent)) {
2179
+ keyframe.outTangent = outTangent;
2180
+ }
2181
+ }
2182
+ keyframes.push(keyframe);
596
2183
  });
597
2184
  if (keyframes.length === 0) {
598
2185
  return;
599
2186
  }
600
2187
  convertedTracks.push({
601
2188
  channel: channelId,
602
- keyframes
2189
+ keyframes,
2190
+ interpolation
603
2191
  });
604
2192
  });
605
2193
  if (convertedTracks.length === 0) {
@@ -643,12 +2231,15 @@ function pickExtractedAnimations(asset) {
643
2231
  }
644
2232
  return animations;
645
2233
  }
646
- function mergeAnimationLists(explicit, fromBundle) {
647
- if (!explicit?.length && fromBundle.length === 0) {
2234
+ function mergeAnimationLists(explicit, hasExplicitOverride, fromBundle) {
2235
+ if (!hasExplicitOverride) {
2236
+ return fromBundle.length > 0 ? fromBundle : void 0;
2237
+ }
2238
+ if (!Array.isArray(explicit)) {
648
2239
  return void 0;
649
2240
  }
650
- if (!explicit?.length) {
651
- return fromBundle.length > 0 ? fromBundle : void 0;
2241
+ if (explicit.length === 0) {
2242
+ return [];
652
2243
  }
653
2244
  if (fromBundle.length === 0) {
654
2245
  return explicit;
@@ -666,21 +2257,153 @@ function mergeAnimationLists(explicit, fromBundle) {
666
2257
  }
667
2258
  return changed ? merged : explicit;
668
2259
  }
2260
+ function extractProgramResetValues(value) {
2261
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2262
+ return void 0;
2263
+ }
2264
+ const entries = Object.entries(value).filter(
2265
+ ([, rawValue]) => Number.isFinite(Number(rawValue))
2266
+ );
2267
+ if (entries.length === 0) {
2268
+ return void 0;
2269
+ }
2270
+ return Object.fromEntries(
2271
+ entries.map(([path, rawValue]) => [path, Number(rawValue)])
2272
+ );
2273
+ }
2274
+ function convertBundlePrograms(entries) {
2275
+ if (!Array.isArray(entries) || entries.length === 0) {
2276
+ return [];
2277
+ }
2278
+ return entries.filter((entry) => normaliseBundleKind(entry?.kind) === "motiongraph").map((entry) => {
2279
+ const graph = convertBundleGraph(entry);
2280
+ if (!graph) {
2281
+ return null;
2282
+ }
2283
+ const metadata = entry.metadata && typeof entry.metadata === "object" && !Array.isArray(entry.metadata) ? entry.metadata : void 0;
2284
+ return {
2285
+ id: entry.id,
2286
+ label: typeof entry.label === "string" ? entry.label : void 0,
2287
+ graph,
2288
+ metadata,
2289
+ resetValues: extractProgramResetValues(metadata?.resetValues)
2290
+ };
2291
+ }).filter(Boolean);
2292
+ }
2293
+ function resolveConstraintDefault(path, namespace, inputConstraints) {
2294
+ const trimmed = path.trim();
2295
+ if (!trimmed) {
2296
+ return void 0;
2297
+ }
2298
+ const stripped = stripRigFacePrefix(trimmed);
2299
+ const relativePath = stripped ? `/${stripped}` : "";
2300
+ const candidates = [
2301
+ namespaceTypedPath(trimmed, namespace),
2302
+ trimmed,
2303
+ stripped ? namespaceTypedPath(stripped, namespace) : void 0,
2304
+ stripped || void 0,
2305
+ relativePath || void 0
2306
+ ];
2307
+ for (const candidate of candidates) {
2308
+ if (!candidate) {
2309
+ continue;
2310
+ }
2311
+ const defaultValue = inputConstraints[candidate]?.defaultValue;
2312
+ if (Number.isFinite(defaultValue)) {
2313
+ return Number(defaultValue);
2314
+ }
2315
+ }
2316
+ return void 0;
2317
+ }
2318
+ function deriveProgramInputSeedValues(args) {
2319
+ const graphSpec = resolveGraphSpec(
2320
+ args.program.graph,
2321
+ `${args.program.id ?? "program"} graph (seed defaults)`
2322
+ );
2323
+ if (!graphSpec) {
2324
+ return [];
2325
+ }
2326
+ return collectInputPaths(graphSpec).map((path) => path.trim()).filter((path) => path.length > 0).flatMap((path) => {
2327
+ const namespacedPath = namespaceTypedPath(path, args.namespace);
2328
+ if (args.stagedInputs.has(namespacedPath)) {
2329
+ return [];
2330
+ }
2331
+ if (args.getPathSnapshot(namespacedPath) !== void 0) {
2332
+ return [];
2333
+ }
2334
+ const defaultValue = resolveConstraintDefault(
2335
+ path,
2336
+ args.namespace,
2337
+ args.inputConstraints
2338
+ );
2339
+ if (!Number.isFinite(defaultValue)) {
2340
+ return [];
2341
+ }
2342
+ return [{ path, value: { float: Number(defaultValue) } }];
2343
+ });
2344
+ }
2345
+ function mergeProgramLists(explicit, hasExplicitOverride, fromBundle) {
2346
+ if (!hasExplicitOverride) {
2347
+ return fromBundle.length > 0 ? fromBundle : void 0;
2348
+ }
2349
+ if (!Array.isArray(explicit)) {
2350
+ return void 0;
2351
+ }
2352
+ if (explicit.length === 0) {
2353
+ return [];
2354
+ }
2355
+ if (fromBundle.length === 0) {
2356
+ return explicit;
2357
+ }
2358
+ const seen = new Set(explicit.map((program) => program.id));
2359
+ let changed = false;
2360
+ const merged = [...explicit];
2361
+ for (const program of fromBundle) {
2362
+ if (!program.id || seen.has(program.id)) {
2363
+ continue;
2364
+ }
2365
+ merged.push(program);
2366
+ seen.add(program.id);
2367
+ changed = true;
2368
+ }
2369
+ return changed ? merged : explicit;
2370
+ }
669
2371
  function mergeAssetBundle(base, extracted, extractedAnimations) {
670
2372
  const resolvedBundle = base.bundle ?? extracted ?? null;
2373
+ const hasBaseRigOverride = Object.prototype.hasOwnProperty.call(base, "rig");
2374
+ const hasBasePoseOverride = Object.prototype.hasOwnProperty.call(
2375
+ base,
2376
+ "pose"
2377
+ );
2378
+ const hasBaseAnimationsOverride = Object.prototype.hasOwnProperty.call(
2379
+ base,
2380
+ "animations"
2381
+ );
2382
+ const hasBaseProgramsOverride = Object.prototype.hasOwnProperty.call(
2383
+ base,
2384
+ "programs"
2385
+ );
671
2386
  const rigFromBundle = convertBundleGraph(
672
2387
  pickBundleGraph(resolvedBundle, ["rig"])
673
2388
  );
674
- const resolvedRig = base.rig ?? rigFromBundle ?? void 0;
2389
+ const resolvedRig = hasBaseRigOverride ? base.rig : base.rig ?? rigFromBundle ?? void 0;
675
2390
  const basePose = base.pose;
2391
+ const hasBasePoseGraphOverride = Boolean(
2392
+ basePose && Object.prototype.hasOwnProperty.call(basePose, "graph")
2393
+ );
2394
+ const hasBasePoseConfigOverride = Boolean(
2395
+ basePose && Object.prototype.hasOwnProperty.call(basePose, "config")
2396
+ );
676
2397
  const poseStageFilter = basePose?.stageNeutralFilter;
677
- const poseGraphFromBundle = basePose?.graph ? null : convertBundleGraph(
2398
+ const poseGraphFromBundle = hasBasePoseOverride && !basePose ? null : hasBasePoseGraphOverride ? null : convertBundleGraph(
678
2399
  pickBundleGraph(resolvedBundle, ["pose-driver", "pose"])
679
2400
  );
680
- const resolvedPoseGraph = basePose?.graph ?? poseGraphFromBundle ?? void 0;
681
- const resolvedPoseConfig = basePose?.config ?? resolvedBundle?.poses?.config ?? void 0;
2401
+ const resolvedPoseGraph = hasBasePoseGraphOverride ? basePose?.graph : basePose?.graph ?? poseGraphFromBundle ?? void 0;
2402
+ const resolvedPoseConfig = hasBasePoseConfigOverride ? basePose?.config : basePose?.config ?? resolvedBundle?.poses?.config ?? void 0;
682
2403
  let resolvedPose = basePose;
683
- if (basePose) {
2404
+ if (hasBasePoseOverride && !basePose) {
2405
+ resolvedPose = void 0;
2406
+ } else if (basePose) {
684
2407
  const nextPose = { ...basePose };
685
2408
  let changed = false;
686
2409
  if (resolvedPoseGraph && basePose.graph !== resolvedPoseGraph) {
@@ -708,15 +2431,22 @@ function mergeAssetBundle(base, extracted, extractedAnimations) {
708
2431
  );
709
2432
  let resolvedAnimations = mergeAnimationLists(
710
2433
  base.animations,
2434
+ hasBaseAnimationsOverride,
711
2435
  animationsFromBundle
712
2436
  );
713
2437
  const animationsFromAsset = extractedAnimations && extractedAnimations.length > 0 ? extractedAnimations : [];
714
2438
  if (animationsFromAsset.length > 0) {
715
2439
  resolvedAnimations = mergeAnimationLists(
716
2440
  resolvedAnimations,
2441
+ true,
717
2442
  animationsFromAsset
718
2443
  );
719
2444
  }
2445
+ const programsFromBundle = mergeProgramLists(
2446
+ base.programs,
2447
+ hasBaseProgramsOverride,
2448
+ convertBundlePrograms(resolvedBundle?.graphs)
2449
+ );
720
2450
  const merged = {
721
2451
  ...base
722
2452
  };
@@ -727,124 +2457,223 @@ function mergeAssetBundle(base, extracted, extractedAnimations) {
727
2457
  }
728
2458
  merged.pose = resolvedPose;
729
2459
  merged.animations = resolvedAnimations;
2460
+ merged.programs = programsFromBundle;
730
2461
  merged.bundle = resolvedBundle;
731
2462
  return merged;
732
2463
  }
2464
+ function normalizeStoredAnimationInterpolation(interpolation) {
2465
+ const mode = typeof interpolation === "string" ? interpolation.trim().toLowerCase() : "linear";
2466
+ if (mode === "step") {
2467
+ return "step";
2468
+ }
2469
+ if (mode === "cubic" || mode === "cubicspline") {
2470
+ return "cubic";
2471
+ }
2472
+ return "linear";
2473
+ }
2474
+ function buildStoredAnimationTransitions(mode) {
2475
+ if (mode === "cubic") {
2476
+ return void 0;
2477
+ }
2478
+ if (mode === "step") {
2479
+ return {
2480
+ in: { x: 1, y: 1 },
2481
+ out: { x: 1, y: 0 }
2482
+ };
2483
+ }
2484
+ return {
2485
+ in: { x: 1, y: 1 },
2486
+ out: { x: 0, y: 0 }
2487
+ };
2488
+ }
2489
+ function toStoredAnimationClip(fallbackId, clip) {
2490
+ const clipId = typeof clip.id === "string" && clip.id.trim().length > 0 ? clip.id.trim() : fallbackId;
2491
+ const clipName = typeof clip.name === "string" && clip.name.trim().length > 0 ? clip.name.trim() : clipId;
2492
+ const durationSeconds = resolveClipDurationSeconds(clip, 0);
2493
+ const durationMs = Math.max(1, Math.round(durationSeconds * 1e3));
2494
+ const tracks = (Array.isArray(clip.tracks) ? clip.tracks : []).map((rawTrack, trackIndex) => {
2495
+ const channel = typeof rawTrack.channel === "string" ? rawTrack.channel.trim() : "";
2496
+ if (!channel) {
2497
+ return null;
2498
+ }
2499
+ const keyframes = (Array.isArray(rawTrack.keyframes) ? rawTrack.keyframes : []).map((keyframe) => {
2500
+ const time = Number(keyframe.time);
2501
+ const value = Number(keyframe.value);
2502
+ const keyframeId = keyframe["id"];
2503
+ const keyframeInterpolation = keyframe["interpolation"];
2504
+ if (!Number.isFinite(time) || !Number.isFinite(value)) {
2505
+ return null;
2506
+ }
2507
+ return {
2508
+ id: typeof keyframeId === "string" && keyframeId.trim().length > 0 ? keyframeId.trim() : `${clipId}:track-${trackIndex.toString().padStart(4, "0")}:point-${time.toFixed(6)}`,
2509
+ time,
2510
+ value,
2511
+ mode: normalizeStoredAnimationInterpolation(
2512
+ keyframeInterpolation ?? rawTrack.interpolation
2513
+ )
2514
+ };
2515
+ }).filter(Boolean);
2516
+ if (keyframes.length === 0) {
2517
+ return null;
2518
+ }
2519
+ keyframes.sort((left, right) => {
2520
+ if (left.time !== right.time) {
2521
+ return left.time - right.time;
2522
+ }
2523
+ return left.id.localeCompare(right.id);
2524
+ });
2525
+ const rawTrackId = rawTrack["id"];
2526
+ const rawTrackName = rawTrack["name"];
2527
+ const trackId = typeof rawTrackId === "string" && rawTrackId.trim().length > 0 ? rawTrackId.trim() : `${clipId}:track-${trackIndex.toString().padStart(4, "0")}`;
2528
+ const trackName = typeof rawTrackName === "string" && rawTrackName.trim().length > 0 ? rawTrackName.trim() : channel.replace(/^\/+/, "") || trackId;
2529
+ const denominator = durationSeconds > 0 ? durationSeconds : 1;
2530
+ return {
2531
+ id: trackId,
2532
+ name: trackName,
2533
+ animatableId: channel,
2534
+ points: keyframes.map((keyframe) => {
2535
+ const stamp = Math.max(0, Math.min(1, keyframe.time / denominator));
2536
+ const transitions = buildStoredAnimationTransitions(keyframe.mode);
2537
+ return {
2538
+ id: keyframe.id,
2539
+ stamp,
2540
+ value: keyframe.value,
2541
+ ...transitions ? { transitions } : {}
2542
+ };
2543
+ })
2544
+ };
2545
+ }).filter(Boolean);
2546
+ return {
2547
+ id: clipId,
2548
+ name: clipName,
2549
+ duration: durationMs,
2550
+ groups: {},
2551
+ tracks
2552
+ };
2553
+ }
733
2554
  function VizijRuntimeProvider({
734
2555
  assetBundle,
735
2556
  children,
736
2557
  namespace: namespaceProp,
737
2558
  faceId: faceIdProp,
2559
+ updateTier = "auto",
738
2560
  autoCreate = true,
739
- createOptions,
740
2561
  autostart = false,
741
- driveOrchestrator = true,
2562
+ driveRuntime = true,
742
2563
  mergeStrategy,
743
2564
  onRegisterControllers,
744
2565
  onStatusChange,
745
- orchestratorScope = "auto"
2566
+ transformOutputWrite
746
2567
  }) {
747
2568
  const storeRef = useRef(null);
748
2569
  if (!storeRef.current) {
749
2570
  storeRef.current = createVizijStore();
750
2571
  }
751
- const parentOrchestrator = useContext(OrchestratorContext);
752
- const hasParentOrchestrator = Boolean(parentOrchestrator);
753
- const shouldProvideOrchestrator = orchestratorScope === "isolated" || !hasParentOrchestrator && orchestratorScope !== "shared";
754
- if (orchestratorScope === "shared" && !hasParentOrchestrator) {
755
- console.warn(
756
- '[vizij-runtime] orchestratorScope="shared" requires an OrchestratorProvider higher in the tree; falling back to an isolated provider.'
757
- );
758
- }
759
- const runtimeTree = /* @__PURE__ */ jsx(VizijContext.Provider, { value: storeRef.current, children: /* @__PURE__ */ jsx(
2572
+ return /* @__PURE__ */ jsx(VizijContext.Provider, { value: storeRef.current, children: /* @__PURE__ */ jsx(
760
2573
  VizijRuntimeProviderInner,
761
2574
  {
762
2575
  assetBundle,
763
2576
  namespace: namespaceProp,
764
2577
  faceId: faceIdProp,
2578
+ updateTier,
765
2579
  autoCreate,
766
2580
  autostart,
767
- driveOrchestrator,
768
- createOptions,
2581
+ driveRuntime,
769
2582
  mergeStrategy,
770
2583
  onRegisterControllers,
771
2584
  onStatusChange,
2585
+ transformOutputWrite,
772
2586
  store: storeRef.current,
773
2587
  children
774
2588
  }
775
2589
  ) });
776
- if (!shouldProvideOrchestrator) {
777
- return runtimeTree;
778
- }
779
- return /* @__PURE__ */ jsx(
780
- OrchestratorProvider,
781
- {
782
- autoCreate,
783
- createOptions,
784
- autostart: false,
785
- children: runtimeTree
786
- }
787
- );
788
2590
  }
789
2591
  function VizijRuntimeProviderInner({
790
2592
  assetBundle: initialAssetBundle,
791
2593
  namespace: namespaceProp,
792
2594
  faceId: faceIdProp,
2595
+ updateTier,
793
2596
  mergeStrategy,
794
2597
  onRegisterControllers,
795
2598
  onStatusChange,
2599
+ transformOutputWrite,
796
2600
  store,
797
2601
  children,
798
2602
  autoCreate,
799
2603
  autostart,
800
- createOptions,
801
- driveOrchestrator
2604
+ driveRuntime
802
2605
  }) {
2606
+ const [assetBundleOverride, setAssetBundleOverride] = useState(null);
2607
+ const [graphUpdateToken, setGraphUpdateToken] = useState(0);
2608
+ const effectiveAssetBundle = assetBundleOverride ?? initialAssetBundle;
2609
+ const latestEffectiveAssetBundleRef = useRef(effectiveAssetBundle);
803
2610
  const [extractedBundle, setExtractedBundle] = useState(() => {
804
- if (initialAssetBundle.bundle) {
805
- return initialAssetBundle.bundle;
2611
+ if (effectiveAssetBundle.bundle) {
2612
+ return effectiveAssetBundle.bundle;
806
2613
  }
807
- if (initialAssetBundle.glb.kind === "world" && initialAssetBundle.glb.bundle) {
808
- return initialAssetBundle.glb.bundle;
2614
+ if (effectiveAssetBundle.glb.kind === "world" && effectiveAssetBundle.glb.bundle) {
2615
+ return effectiveAssetBundle.glb.bundle;
809
2616
  }
810
2617
  return null;
811
2618
  });
812
2619
  const [extractedAnimations, setExtractedAnimations] = useState([]);
2620
+ const previousBundleRef = useRef(null);
2621
+ const suppressNextBundlePlanRef = useRef(false);
2622
+ const pendingPlanRef = useRef(null);
2623
+ const updateTierRef = useRef(updateTier);
813
2624
  useEffect(() => {
814
- if (initialAssetBundle.bundle) {
815
- setExtractedBundle(initialAssetBundle.bundle);
2625
+ if (effectiveAssetBundle.bundle) {
2626
+ setExtractedBundle(effectiveAssetBundle.bundle);
816
2627
  return;
817
2628
  }
818
- if (initialAssetBundle.glb.kind === "world") {
819
- setExtractedBundle(initialAssetBundle.glb.bundle ?? null);
2629
+ if (effectiveAssetBundle.glb.kind === "world") {
2630
+ setExtractedBundle(effectiveAssetBundle.glb.bundle ?? null);
820
2631
  } else {
821
2632
  setExtractedBundle(null);
822
2633
  }
823
- }, [initialAssetBundle]);
2634
+ }, [effectiveAssetBundle]);
2635
+ useEffect(() => {
2636
+ updateTierRef.current = updateTier;
2637
+ }, [updateTier]);
824
2638
  const assetBundle = useMemo(
825
2639
  () => mergeAssetBundle(
826
- initialAssetBundle,
2640
+ effectiveAssetBundle,
827
2641
  extractedBundle,
828
2642
  extractedAnimations
829
2643
  ),
830
- [initialAssetBundle, extractedBundle, extractedAnimations]
2644
+ [effectiveAssetBundle, extractedBundle, extractedAnimations]
831
2645
  );
832
- const {
833
- ready,
834
- createOrchestrator,
835
- registerGraph,
836
- registerMergedGraph,
837
- registerAnimation,
838
- removeGraph,
839
- removeAnimation,
840
- listControllers,
841
- setInput: orchestratorSetInput,
842
- getPathSnapshot,
843
- step: stepRuntime
844
- } = useOrchestrator();
845
- const frame = useOrchFrame();
2646
+ useEffect(() => {
2647
+ latestEffectiveAssetBundleRef.current = effectiveAssetBundle;
2648
+ }, [effectiveAssetBundle]);
2649
+ useEffect(() => {
2650
+ if (suppressNextBundlePlanRef.current) {
2651
+ suppressNextBundlePlanRef.current = false;
2652
+ previousBundleRef.current = effectiveAssetBundle;
2653
+ return;
2654
+ }
2655
+ const plan = resolveRuntimeUpdatePlan(
2656
+ previousBundleRef.current,
2657
+ effectiveAssetBundle,
2658
+ updateTierRef.current
2659
+ );
2660
+ pendingPlanRef.current = plan;
2661
+ previousBundleRef.current = effectiveAssetBundle;
2662
+ if (plan.reregisterGraphs) {
2663
+ setGraphUpdateToken((prev) => prev + 1);
2664
+ }
2665
+ }, [effectiveAssetBundle]);
2666
+ const deviceSlotRef = useRef(null);
2667
+ if (!deviceSlotRef.current) {
2668
+ deviceSlotRef.current = new DeviceSlot();
2669
+ }
2670
+ const deviceSlot = deviceSlotRef.current;
2671
+ const [ready, setReady] = useState(false);
2672
+ const graphSourcesRef = useRef([]);
846
2673
  const namespace = namespaceProp ?? assetBundle.namespace ?? "default";
847
2674
  const faceId = faceIdProp ?? assetBundle.faceId ?? assetBundle.pose?.config?.faceId ?? assetBundle.pose?.config?.faceId ?? void 0;
2675
+ const faceIdRef = useRef(faceId);
2676
+ faceIdRef.current = faceId;
848
2677
  const [status, setStatus] = useState({
849
2678
  loading: true,
850
2679
  ready: false,
@@ -857,20 +2686,68 @@ function VizijRuntimeProviderInner({
857
2686
  stepHz: void 0,
858
2687
  controllers: { graphs: [], anims: [] }
859
2688
  });
2689
+ const runtimeDebugInstanceIdRef = useRef(
2690
+ `vizij-runtime:${runtimeDebugInstanceSequence++}`
2691
+ );
860
2692
  const errorsRef = useRef([]);
861
2693
  const outputPathsRef = useRef(/* @__PURE__ */ new Set());
862
2694
  const baseOutputPathsRef = useRef(/* @__PURE__ */ new Set());
863
2695
  const namespacedOutputPathsRef = useRef(/* @__PURE__ */ new Set());
864
2696
  const namespaceRef = useRef(namespace);
865
- const driveOrchestratorRef = useRef(driveOrchestrator);
2697
+ const driveRuntimeRef = useRef(driveRuntime);
866
2698
  const rigInputMapRef = useRef({});
2699
+ const rigPoseControlInputIdsRef = useRef(/* @__PURE__ */ new Set());
867
2700
  const registeredGraphsRef = useRef([]);
868
2701
  const registeredAnimationsRef = useRef([]);
869
2702
  const mergedGraphRef = useRef(null);
2703
+ const poseControlBridgeValuesRef = useRef(/* @__PURE__ */ new Map());
2704
+ const poseWeightFallbackMap = useMemo(() => {
2705
+ const map = /* @__PURE__ */ new Map();
2706
+ const poseConfig = assetBundle.pose?.config;
2707
+ if (!poseConfig) {
2708
+ return map;
2709
+ }
2710
+ const posePaths = buildPoseWeightPathMap(
2711
+ poseConfig.poses ?? [],
2712
+ poseConfig.faceId ?? faceId ?? "face"
2713
+ );
2714
+ (poseConfig.poses ?? []).forEach((pose) => {
2715
+ const posePath = posePaths.get(pose.id);
2716
+ if (!posePath) {
2717
+ return;
2718
+ }
2719
+ const values = Object.fromEntries(
2720
+ Object.entries(pose.values ?? {}).filter(
2721
+ ([, value]) => Number.isFinite(value)
2722
+ )
2723
+ );
2724
+ map.set(posePath, values);
2725
+ });
2726
+ return map;
2727
+ }, [assetBundle.pose?.config, faceId]);
2728
+ const useLegacyPoseWeightFallback = useMemo(
2729
+ () => shouldUseLegacyPoseWeightFallback(Boolean(assetBundle.pose?.graph)),
2730
+ [assetBundle.pose?.graph]
2731
+ );
2732
+ const resolvedProgramAssets = useMemo(
2733
+ () => assetBundle.programs && assetBundle.programs.length > 0 ? assetBundle.programs : convertBundlePrograms(assetBundle.bundle?.graphs),
2734
+ [assetBundle.bundle?.graphs, assetBundle.programs]
2735
+ );
870
2736
  const [inputConstraints, setInputConstraints] = useState({});
2737
+ const inputConstraintsRef = useRef({});
871
2738
  const avgStepDtRef = useRef(null);
2739
+ const inputDriverIdsRef = useRef(/* @__PURE__ */ new Set());
872
2740
  const animationTweensRef = useRef(/* @__PURE__ */ new Map());
873
2741
  const clipPlaybackRef = useRef(/* @__PURE__ */ new Map());
2742
+ const programPlaybackRef = useRef(
2743
+ /* @__PURE__ */ new Map()
2744
+ );
2745
+ const programControllerIdsRef = useRef(/* @__PURE__ */ new Map());
2746
+ const animationModuleRef = useRef(null);
2747
+ const animationHostRef = useRef(null);
2748
+ const animationsSourceRegisteredRef = useRef(false);
2749
+ const animationGapWarnedRef = useRef(/* @__PURE__ */ new Set());
2750
+ const animationSystemActiveRef = useRef(true);
874
2751
  const stagedInputsRef = useRef(/* @__PURE__ */ new Map());
875
2752
  const autostartRef = useRef(autostart);
876
2753
  const lastActivityTimeRef = useRef(now());
@@ -885,9 +2762,62 @@ function VizijRuntimeProviderInner({
885
2762
  runtimeMountedRef.current = false;
886
2763
  };
887
2764
  }, []);
2765
+ const publishRuntimeDebugState = useCallback(() => {
2766
+ const storeState = store.getState();
2767
+ setRuntimeDebugState(runtimeDebugInstanceIdRef.current, {
2768
+ namespace,
2769
+ faceId: faceId ?? null,
2770
+ rootId: status.rootId,
2771
+ ready: status.ready,
2772
+ loading: status.loading,
2773
+ autostart,
2774
+ driveRuntime,
2775
+ loopMode,
2776
+ outputCount: status.outputPaths.length,
2777
+ graphControllerCount: status.controllers.graphs.length,
2778
+ animationControllerCount: status.controllers.anims.length,
2779
+ registeredGraphCount: registeredGraphsRef.current.length,
2780
+ registeredAnimationCount: registeredAnimationsRef.current.length,
2781
+ programControllerCount: programControllerIdsRef.current.size,
2782
+ animationTweenCount: animationTweensRef.current.size,
2783
+ clipPlaybackCount: clipPlaybackRef.current.size,
2784
+ programPlaybackCount: programPlaybackRef.current.size,
2785
+ stagedInputCount: stagedInputsRef.current.size,
2786
+ activeDriverCount: inputDriverIdsRef.current.size,
2787
+ worldEntryCount: Object.keys(storeState.world ?? {}).length,
2788
+ animatableCount: Object.keys(storeState.animatables ?? {}).length,
2789
+ valuesSize: storeState.values instanceof Map ? storeState.values.size : null,
2790
+ stepHz: status.stepHz ?? null
2791
+ });
2792
+ }, [
2793
+ autostart,
2794
+ driveRuntime,
2795
+ faceId,
2796
+ loopMode,
2797
+ namespace,
2798
+ status.controllers.anims.length,
2799
+ status.controllers.graphs.length,
2800
+ status.loading,
2801
+ status.outputPaths.length,
2802
+ status.ready,
2803
+ status.rootId,
2804
+ status.stepHz,
2805
+ store
2806
+ ]);
2807
+ useEffect(() => {
2808
+ publishRuntimeDebugState();
2809
+ const unsubscribe = store.subscribe(() => {
2810
+ publishRuntimeDebugState();
2811
+ });
2812
+ return () => {
2813
+ unsubscribe();
2814
+ clearRuntimeDebugState(runtimeDebugInstanceIdRef.current);
2815
+ };
2816
+ }, [publishRuntimeDebugState, store]);
888
2817
  useEffect(() => {
889
2818
  const rigAsset = assetBundle.rig;
890
2819
  if (!rigAsset) {
2820
+ inputConstraintsRef.current = {};
891
2821
  setInputConstraints({});
892
2822
  return;
893
2823
  }
@@ -900,15 +2830,8 @@ function VizijRuntimeProviderInner({
900
2830
  rigAsset.inputMetadata,
901
2831
  namespace
902
2832
  );
2833
+ inputConstraintsRef.current = constraints;
903
2834
  setInputConstraints(constraints);
904
- const isDevEnv = typeof globalThis !== "undefined" && Boolean(globalThis?.process?.env?.NODE_ENV !== "production");
905
- if (isDevEnv) {
906
- const size = Object.keys(constraints).length;
907
- console.log("[vizij-runtime] input constraints computed", size, {
908
- namespace,
909
- rigId: rigAsset.id
910
- });
911
- }
912
2835
  }, [assetBundle.rig, namespace]);
913
2836
  const requestLoopMode = useCallback((mode) => {
914
2837
  if (!runtimeMountedRef.current) {
@@ -917,7 +2840,28 @@ function VizijRuntimeProviderInner({
917
2840
  setLoopMode((prev) => prev === mode ? prev : mode);
918
2841
  }, []);
919
2842
  const hasActiveAnimations = useCallback(() => {
920
- return animationTweensRef.current.size > 0 || clipPlaybackRef.current.size > 0;
2843
+ if (animationTweensRef.current.size > 0) {
2844
+ return true;
2845
+ }
2846
+ if (!animationSystemActiveRef.current) {
2847
+ for (const state of programPlaybackRef.current.values()) {
2848
+ if (state.state === "playing") {
2849
+ return true;
2850
+ }
2851
+ }
2852
+ return false;
2853
+ }
2854
+ for (const state of clipPlaybackRef.current.values()) {
2855
+ if (state.playing) {
2856
+ return true;
2857
+ }
2858
+ }
2859
+ for (const state of programPlaybackRef.current.values()) {
2860
+ if (state.state === "playing") {
2861
+ return true;
2862
+ }
2863
+ }
2864
+ return false;
921
2865
  }, []);
922
2866
  const computeDesiredLoopMode = useCallback(() => {
923
2867
  const hasAnimations = hasActiveAnimations();
@@ -939,11 +2883,50 @@ function VizijRuntimeProviderInner({
939
2883
  }, [updateLoopMode]);
940
2884
  const setInput = useCallback(
941
2885
  (path, value, shape) => {
2886
+ const numericValue = valueAsNumber2(value);
2887
+ const basePath = stripNamespace(
2888
+ normalisePath(path),
2889
+ namespaceRef.current
2890
+ );
2891
+ const poseValues = useLegacyPoseWeightFallback && numericValue != null ? poseWeightFallbackMap.get(basePath) : void 0;
2892
+ if (poseValues && numericValue != null) {
2893
+ const poseFaceId = assetBundle.pose?.config?.faceId ?? faceId ?? "face";
2894
+ const rigMap = rigInputMapRef.current;
2895
+ Object.entries(poseValues).forEach(([inputId, poseValue]) => {
2896
+ if (!Number.isFinite(poseValue)) {
2897
+ return;
2898
+ }
2899
+ const controlPath = resolvePoseControlInputPath({
2900
+ inputId,
2901
+ basePath: buildRigInputPath(
2902
+ poseFaceId,
2903
+ `/pose/control/${inputId}`
2904
+ ),
2905
+ rigInputPathMap: rigMap,
2906
+ hasNativePoseControlInput: true
2907
+ }) ?? buildRigInputPath(poseFaceId, `/pose/control/${inputId}`);
2908
+ setInput(controlPath, { float: Number(poseValue) * numericValue });
2909
+ });
2910
+ return;
2911
+ }
942
2912
  markActivity();
943
2913
  const namespacedPath = namespaceTypedPath(path, namespaceRef.current);
2914
+ if (isRuntimeDebugEnabled() && (namespacedPath.includes("animation/authoring.timeline.main") || namespacedPath.endsWith("/blink"))) {
2915
+ console.log("[vizij-runtime] stage input", {
2916
+ path,
2917
+ namespacedPath,
2918
+ value
2919
+ });
2920
+ }
944
2921
  stagedInputsRef.current.set(namespacedPath, { value, shape });
945
2922
  },
946
- [markActivity]
2923
+ [
2924
+ assetBundle.pose?.config?.faceId,
2925
+ faceId,
2926
+ markActivity,
2927
+ poseWeightFallbackMap,
2928
+ useLegacyPoseWeightFallback
2929
+ ]
947
2930
  );
948
2931
  const reportStatus = useCallback(
949
2932
  (updater) => {
@@ -953,32 +2936,262 @@ function VizijRuntimeProviderInner({
953
2936
  return next;
954
2937
  });
955
2938
  },
956
- [onStatusChange]
2939
+ [onStatusChange]
2940
+ );
2941
+ const pushError = useCallback(
2942
+ (error) => {
2943
+ errorsRef.current = [...errorsRef.current, error];
2944
+ reportStatus((prev) => ({
2945
+ ...prev,
2946
+ error,
2947
+ errors: errorsRef.current
2948
+ }));
2949
+ console.warn("[vizij-runtime]", error.message, error.cause);
2950
+ },
2951
+ [reportStatus]
2952
+ );
2953
+ const resetErrors = useCallback(() => {
2954
+ errorsRef.current = [];
2955
+ reportStatus((prev) => ({
2956
+ ...prev,
2957
+ error: null,
2958
+ errors: []
2959
+ }));
2960
+ }, [reportStatus]);
2961
+ useEffect(() => {
2962
+ autostartRef.current = autostart;
2963
+ updateLoopMode();
2964
+ }, [autostart, updateLoopMode]);
2965
+ const pendingWritesRef = useRef(/* @__PURE__ */ new Map());
2966
+ const getDevice = useCallback(
2967
+ () => deviceSlot.current?.device ?? null,
2968
+ [deviceSlot]
2969
+ );
2970
+ const ensureAnimationHost = useCallback(() => {
2971
+ if (!animationModuleRef.current) {
2972
+ return null;
2973
+ }
2974
+ if (!animationHostRef.current) {
2975
+ animationHostRef.current = new AnimationModuleHost(
2976
+ getDevice,
2977
+ (key) => resolveAnimationBridgeOutputPaths(
2978
+ key,
2979
+ faceIdRef.current ?? void 0,
2980
+ rigInputMapRef.current
2981
+ )
2982
+ );
2983
+ }
2984
+ return animationHostRef.current;
2985
+ }, [getDevice]);
2986
+ const applyEngineChangesRef = useRef(() => {
2987
+ });
2988
+ const recomposeDevice = useCallback(() => {
2989
+ const spec = composeGraphSpecs(graphSourcesRef.current);
2990
+ deviceSlot.recompose(spec).then((handle) => {
2991
+ if (pendingWritesRef.current.size > 0) {
2992
+ handle.device.writeValues(
2993
+ Object.fromEntries(pendingWritesRef.current)
2994
+ );
2995
+ pendingWritesRef.current.clear();
2996
+ }
2997
+ }).catch((err) => {
2998
+ pushError({
2999
+ message: "Failed to (re)compose the arora device",
3000
+ cause: err,
3001
+ phase: "engine",
3002
+ timestamp: performance.now()
3003
+ });
3004
+ });
3005
+ }, [deviceSlot, pushError]);
3006
+ const initEngine = useCallback(async () => {
3007
+ await ensureWasmInit();
3008
+ setReady(true);
3009
+ }, []);
3010
+ const hasBundleAnimations = (assetBundle.animations?.length ?? 0) > 0;
3011
+ useEffect(() => {
3012
+ if (!hasBundleAnimations || animationModuleRef.current) {
3013
+ return;
3014
+ }
3015
+ let cancelled = false;
3016
+ const loading = loadAnimationModule().then((module) => {
3017
+ if (cancelled) {
3018
+ return;
3019
+ }
3020
+ animationModuleRef.current = module;
3021
+ deviceSlot.setModules([module]);
3022
+ deviceSlot.onDeviceStarted = (handle) => {
3023
+ animationHostRef.current?.replayInto(handle.device);
3024
+ };
3025
+ ensureAnimationHost();
3026
+ }).catch((err) => {
3027
+ pushError({
3028
+ message: "Failed to load the animation module",
3029
+ cause: err,
3030
+ phase: "engine",
3031
+ timestamp: performance.now()
3032
+ });
3033
+ });
3034
+ deviceSlot.waitForModules(loading);
3035
+ return () => {
3036
+ cancelled = true;
3037
+ };
3038
+ }, [deviceSlot, ensureAnimationHost, hasBundleAnimations, pushError]);
3039
+ const removeGraph = useCallback(
3040
+ (id) => {
3041
+ graphSourcesRef.current = graphSourcesRef.current.filter(
3042
+ (s) => s.sourceId !== id && !s.sourceId.startsWith(`${id}#`)
3043
+ );
3044
+ recomposeDevice();
3045
+ },
3046
+ [recomposeDevice]
3047
+ );
3048
+ const registerGraph = useCallback(
3049
+ (cfg) => {
3050
+ const id = cfg.id ?? `graph-${graphSourcesRef.current.length}`;
3051
+ graphSourcesRef.current = [
3052
+ ...graphSourcesRef.current.filter((s) => s.sourceId !== id),
3053
+ { sourceId: id, spec: cfg.spec ?? {} }
3054
+ ];
3055
+ recomposeDevice();
3056
+ return id;
3057
+ },
3058
+ [recomposeDevice]
3059
+ );
3060
+ const registerMergedGraph = useCallback(
3061
+ (cfg) => {
3062
+ const id = cfg.id ?? `merged-${graphSourcesRef.current.length}`;
3063
+ const members = cfg.graphs.map((graph, index) => ({
3064
+ sourceId: `${id}#${graph.id ?? index}`,
3065
+ spec: graph.spec ?? {}
3066
+ }));
3067
+ graphSourcesRef.current = [
3068
+ ...graphSourcesRef.current.filter(
3069
+ (s) => s.sourceId !== id && !s.sourceId.startsWith(`${id}#`)
3070
+ ),
3071
+ ...members
3072
+ ];
3073
+ recomposeDevice();
3074
+ return id;
3075
+ },
3076
+ [recomposeDevice]
3077
+ );
3078
+ const registerAnimation = useCallback(
3079
+ (cfg) => cfg.id ?? `animation-${registeredAnimationsRef.current.length}`,
3080
+ []
3081
+ );
3082
+ const removeAnimation = useCallback((_id) => {
3083
+ }, []);
3084
+ const setAnimationsSourceRegistered = useCallback(
3085
+ (registered) => {
3086
+ if (registered === animationsSourceRegisteredRef.current) {
3087
+ return;
3088
+ }
3089
+ animationsSourceRegisteredRef.current = registered;
3090
+ if (registered) {
3091
+ graphSourcesRef.current = [
3092
+ ...graphSourcesRef.current.filter(
3093
+ (s) => s.sourceId !== animationsGraphSource().sourceId
3094
+ ),
3095
+ animationsGraphSource()
3096
+ ];
3097
+ } else {
3098
+ graphSourcesRef.current = graphSourcesRef.current.filter(
3099
+ (s) => s.sourceId !== animationsGraphSource().sourceId
3100
+ );
3101
+ }
3102
+ recomposeDevice();
3103
+ },
3104
+ [recomposeDevice]
3105
+ );
3106
+ const listControllers = useCallback(
3107
+ () => ({
3108
+ graphs: [...registeredGraphsRef.current],
3109
+ anims: [...registeredAnimationsRef.current]
3110
+ }),
3111
+ []
3112
+ );
3113
+ const deviceSetInput = useCallback(
3114
+ (path, value, _shape) => {
3115
+ const handle = deviceSlot.current;
3116
+ if (handle) {
3117
+ handle.device.setValue(path, value);
3118
+ } else {
3119
+ pendingWritesRef.current.set(path, value);
3120
+ }
3121
+ },
3122
+ [deviceSlot]
3123
+ );
3124
+ const getPathSnapshot = useCallback(
3125
+ (path) => {
3126
+ const handle = deviceSlot.current;
3127
+ if (!handle) {
3128
+ return pendingWritesRef.current.get(path);
3129
+ }
3130
+ return handle.device.readValues([path])[path] ?? void 0;
3131
+ },
3132
+ [deviceSlot]
3133
+ );
3134
+ const getStoreSnapshot = useCallback(() => {
3135
+ const handle = deviceSlot.current;
3136
+ if (!handle) {
3137
+ return void 0;
3138
+ }
3139
+ return handle.device.snapshot();
3140
+ }, [deviceSlot]);
3141
+ const stepListenersRef = useRef(/* @__PURE__ */ new Set());
3142
+ const subscribeToStep = useCallback((listener) => {
3143
+ const listeners = stepListenersRef.current;
3144
+ listeners.add(listener);
3145
+ return () => {
3146
+ listeners.delete(listener);
3147
+ };
3148
+ }, []);
3149
+ const stepRuntime = useCallback(
3150
+ (dt) => {
3151
+ const handle = deviceSlot.current;
3152
+ if (!handle) {
3153
+ return;
3154
+ }
3155
+ if (!handle.device.running) {
3156
+ handle.device.step(dt * 1e3);
3157
+ }
3158
+ const changes = handle.device.drainChanges();
3159
+ applyEngineChangesRef.current(changes);
3160
+ if (storeChangeListenersRef.current.size > 0) {
3161
+ const paths = Object.keys(changes);
3162
+ if (paths.length > 0) {
3163
+ storeChangeListenersRef.current.forEach((listener) => {
3164
+ try {
3165
+ listener(changes);
3166
+ } catch (err) {
3167
+ console.error("[vizij-runtime] store-change listener error", err);
3168
+ }
3169
+ });
3170
+ }
3171
+ }
3172
+ stepListenersRef.current.forEach((listener) => {
3173
+ try {
3174
+ listener();
3175
+ } catch (err) {
3176
+ console.error("[vizij-runtime] step listener error", err);
3177
+ }
3178
+ });
3179
+ },
3180
+ [deviceSlot]
957
3181
  );
958
- const pushError = useCallback(
959
- (error) => {
960
- errorsRef.current = [...errorsRef.current, error];
961
- reportStatus((prev) => ({
962
- ...prev,
963
- error,
964
- errors: errorsRef.current
965
- }));
966
- console.warn("[vizij-runtime]", error.message, error.cause);
3182
+ const storeChangeListenersRef = useRef(
3183
+ /* @__PURE__ */ new Set()
3184
+ );
3185
+ const subscribeToStoreChanges = useCallback(
3186
+ (listener) => {
3187
+ const listeners = storeChangeListenersRef.current;
3188
+ listeners.add(listener);
3189
+ return () => {
3190
+ listeners.delete(listener);
3191
+ };
967
3192
  },
968
- [reportStatus]
3193
+ []
969
3194
  );
970
- const resetErrors = useCallback(() => {
971
- errorsRef.current = [];
972
- reportStatus((prev) => ({
973
- ...prev,
974
- error: null,
975
- errors: []
976
- }));
977
- }, [reportStatus]);
978
- useEffect(() => {
979
- autostartRef.current = autostart;
980
- updateLoopMode();
981
- }, [autostart, updateLoopMode]);
982
3195
  const clearControllers = useCallback(() => {
983
3196
  const existing = listControllers();
984
3197
  existing.graphs.forEach((id) => {
@@ -1007,10 +3220,12 @@ function VizijRuntimeProviderInner({
1007
3220
  });
1008
3221
  registeredGraphsRef.current = [];
1009
3222
  registeredAnimationsRef.current = [];
3223
+ programControllerIdsRef.current.clear();
1010
3224
  mergedGraphRef.current = null;
1011
3225
  outputPathsRef.current = /* @__PURE__ */ new Set();
1012
3226
  baseOutputPathsRef.current = /* @__PURE__ */ new Set();
1013
3227
  namespacedOutputPathsRef.current = /* @__PURE__ */ new Set();
3228
+ rigPoseControlInputIdsRef.current = /* @__PURE__ */ new Set();
1014
3229
  }, [listControllers, removeAnimation, removeGraph, pushError]);
1015
3230
  useEffect(() => {
1016
3231
  namespaceRef.current = namespace;
@@ -1021,12 +3236,37 @@ function VizijRuntimeProviderInner({
1021
3236
  }));
1022
3237
  }, [namespace, faceId, reportStatus]);
1023
3238
  useEffect(() => {
1024
- driveOrchestratorRef.current = driveOrchestrator;
1025
- }, [driveOrchestrator]);
1026
- const glbAsset = initialAssetBundle.glb;
1027
- const baseBundle = initialAssetBundle.bundle ?? null;
3239
+ driveRuntimeRef.current = driveRuntime;
3240
+ }, [driveRuntime]);
3241
+ useEffect(() => {
3242
+ deviceSlot.onRunEnded = (error) => {
3243
+ pushError({
3244
+ message: "The device's run loop ended: stepping failed",
3245
+ cause: error,
3246
+ phase: "engine",
3247
+ timestamp: performance.now()
3248
+ });
3249
+ };
3250
+ if (driveRuntime) {
3251
+ deviceSlot.runPeriodMs = RUN_PERIOD_MS;
3252
+ deviceSlot.startRun();
3253
+ } else {
3254
+ deviceSlot.runPeriodMs = null;
3255
+ }
3256
+ }, [deviceSlot, driveRuntime, pushError]);
3257
+ const glbAsset = effectiveAssetBundle.glb;
3258
+ const baseBundle = effectiveAssetBundle.bundle ?? null;
1028
3259
  useEffect(() => {
1029
3260
  let cancelled = false;
3261
+ const plan = pendingPlanRef.current;
3262
+ if (plan && !plan.reloadAssets && status.rootId !== null) {
3263
+ reportStatus(
3264
+ (prev) => prev.loading ? { ...prev, loading: false } : prev
3265
+ );
3266
+ return () => {
3267
+ cancelled = true;
3268
+ };
3269
+ }
1030
3270
  resetErrors();
1031
3271
  reportStatus((prev) => ({
1032
3272
  ...prev,
@@ -1078,6 +3318,12 @@ function VizijRuntimeProviderInner({
1078
3318
  setExtractedAnimations(convertExtractedAnimations(gltfAnimations));
1079
3319
  const rootId = findRootId(world);
1080
3320
  store.getState().addWorldElements(world, animatables, true);
3321
+ if (pendingPlanRef.current?.reloadAssets) {
3322
+ pendingPlanRef.current = {
3323
+ ...pendingPlanRef.current,
3324
+ reloadAssets: false
3325
+ };
3326
+ }
1081
3327
  reportStatus((prev) => ({
1082
3328
  ...prev,
1083
3329
  loading: false,
@@ -1115,22 +3361,32 @@ function VizijRuntimeProviderInner({
1115
3361
  reportStatus,
1116
3362
  resetErrors,
1117
3363
  setExtractedBundle,
1118
- setExtractedAnimations
3364
+ setExtractedAnimations,
3365
+ status.rootId
1119
3366
  ]);
1120
3367
  useEffect(() => {
1121
3368
  if (!ready && autoCreate) {
1122
- createOrchestrator(createOptions).catch((err) => {
3369
+ initEngine().catch((err) => {
1123
3370
  pushError({
1124
- message: "Failed to create orchestrator runtime",
3371
+ message: "Failed to initialize the engine",
1125
3372
  cause: err,
1126
- phase: "orchestrator",
3373
+ phase: "engine",
1127
3374
  timestamp: performance.now()
1128
3375
  });
1129
3376
  });
1130
3377
  }
1131
- }, [ready, autoCreate, createOptions, createOrchestrator, pushError]);
3378
+ }, [ready, autoCreate, initEngine, pushError]);
1132
3379
  const registerControllers = useCallback(async () => {
1133
3380
  clearControllers();
3381
+ if (isRuntimeDebugEnabled()) {
3382
+ console.log("[vizij-runtime] registerControllers", {
3383
+ hasRig: Boolean(assetBundle.rig),
3384
+ hasPose: Boolean(assetBundle.pose?.graph),
3385
+ animationCount: assetBundle.animations?.length ?? 0,
3386
+ animationIds: (assetBundle.animations ?? []).map((anim) => anim.id),
3387
+ namespace
3388
+ });
3389
+ }
1134
3390
  const baseOutputPaths = /* @__PURE__ */ new Set();
1135
3391
  const namespacedOutputPaths = /* @__PURE__ */ new Set();
1136
3392
  const recordOutputs = (paths) => {
@@ -1142,39 +3398,60 @@ function VizijRuntimeProviderInner({
1142
3398
  namespacedOutputPaths.add(namespaceTypedPath(trimmed, namespace));
1143
3399
  });
1144
3400
  };
3401
+ const graphConfigs = [];
3402
+ rigInputMapRef.current = {};
3403
+ rigPoseControlInputIdsRef.current = /* @__PURE__ */ new Set();
3404
+ poseControlBridgeValuesRef.current.clear();
1145
3405
  const rigAsset = assetBundle.rig;
1146
- if (!rigAsset) {
1147
- pushError({
1148
- message: "Asset bundle is missing a rig graph.",
1149
- phase: "registration",
1150
- timestamp: performance.now()
1151
- });
1152
- return;
1153
- }
1154
- const rigSpec = resolveGraphSpec(rigAsset, `${rigAsset.id ?? "rig"} graph`);
1155
- if (!rigSpec) {
1156
- pushError({
1157
- message: "Rig graph is missing a usable spec or IR payload.",
1158
- phase: "registration",
1159
- timestamp: performance.now()
1160
- });
1161
- return;
1162
- }
1163
- const rigOutputs = collectOutputPaths(rigSpec);
1164
- const rigInputs = collectInputPaths(rigSpec);
1165
- rigInputMapRef.current = collectInputPathMap(rigSpec);
1166
- recordOutputs(rigOutputs);
1167
- const rigSubs = rigAsset.subscriptions ?? {
1168
- inputs: rigInputs,
1169
- outputs: rigOutputs
1170
- };
1171
- const graphConfigs = [
1172
- {
1173
- id: namespaceControllerId(rigAsset.id, namespace, "graph"),
1174
- spec: namespaceGraphSpec(rigSpec, namespace),
1175
- subs: namespaceSubscriptions(rigSubs, namespace)
3406
+ if (rigAsset) {
3407
+ const rigSpec = resolveGraphSpec(
3408
+ rigAsset,
3409
+ `${rigAsset.id ?? "rig"} graph`
3410
+ );
3411
+ if (!rigSpec) {
3412
+ pushError({
3413
+ message: "Rig graph is missing a usable spec or IR payload.",
3414
+ phase: "registration",
3415
+ timestamp: performance.now()
3416
+ });
3417
+ } else {
3418
+ const rigOutputs = collectOutputPaths(rigSpec);
3419
+ const rigInputs = collectInputPaths(rigSpec);
3420
+ const rigPoseControlInputIds = /* @__PURE__ */ new Set();
3421
+ rigInputs.forEach((path) => {
3422
+ const poseControlMatch = /^rig\/[^/]+\/pose\/control\/(.+)$/.exec(
3423
+ path.trim()
3424
+ );
3425
+ const inputId = (poseControlMatch?.[1] ?? "").trim();
3426
+ if (inputId.length > 0) {
3427
+ rigPoseControlInputIds.add(inputId);
3428
+ }
3429
+ });
3430
+ rigInputMapRef.current = collectInputPathMap(rigSpec);
3431
+ rigPoseControlInputIdsRef.current = rigPoseControlInputIds;
3432
+ if (isRuntimeDebugEnabled()) {
3433
+ const blinkKeys = Object.keys(rigInputMapRef.current).filter(
3434
+ (key) => key.toLowerCase().includes("blink")
3435
+ );
3436
+ const blinkMappings = blinkKeys.slice(0, 20).map((key) => `${key} => ${rigInputMapRef.current[key] ?? "?"}`);
3437
+ console.log("[vizij-runtime] rig input map sample", {
3438
+ blink: rigInputMapRef.current["blink"] ?? null,
3439
+ blinkKeys: blinkKeys.slice(0, 12),
3440
+ blinkMappings: blinkMappings.join(" | ")
3441
+ });
3442
+ }
3443
+ recordOutputs(rigOutputs);
3444
+ const rigSubs = rigAsset.subscriptions ?? {
3445
+ inputs: rigInputs,
3446
+ outputs: rigOutputs
3447
+ };
3448
+ graphConfigs.push({
3449
+ id: namespaceControllerId(rigAsset.id, namespace, "graph"),
3450
+ spec: stripNulls(namespaceGraphSpec(rigSpec, namespace)),
3451
+ subs: namespaceSubscriptions(rigSubs, namespace)
3452
+ });
1176
3453
  }
1177
- ];
3454
+ }
1178
3455
  const poseGraphAsset = assetBundle.pose?.graph;
1179
3456
  if (poseGraphAsset) {
1180
3457
  const poseSpec = resolveGraphSpec(
@@ -1191,7 +3468,7 @@ function VizijRuntimeProviderInner({
1191
3468
  };
1192
3469
  graphConfigs.push({
1193
3470
  id: namespaceControllerId(poseGraphAsset.id, namespace, "graph"),
1194
- spec: namespaceGraphSpec(poseSpec, namespace),
3471
+ spec: stripNulls(namespaceGraphSpec(poseSpec, namespace)),
1195
3472
  subs: namespaceSubscriptions(poseSubs, namespace)
1196
3473
  });
1197
3474
  } else {
@@ -1200,6 +3477,31 @@ function VizijRuntimeProviderInner({
1200
3477
  );
1201
3478
  }
1202
3479
  }
3480
+ for (const animation of assetBundle.animations ?? []) {
3481
+ const bridgeOutputs = collectAnimationClipOutputPaths(
3482
+ animation.clip,
3483
+ faceId ?? void 0,
3484
+ rigInputMapRef.current
3485
+ );
3486
+ if (isRuntimeDebugEnabled()) {
3487
+ console.log("[vizij-runtime] animation output routing", {
3488
+ animationId: animation.id,
3489
+ bridgeOutputs,
3490
+ bridgeOutputsText: bridgeOutputs.join(" | ")
3491
+ });
3492
+ }
3493
+ recordOutputs(bridgeOutputs);
3494
+ }
3495
+ for (const program of resolvedProgramAssets) {
3496
+ const programSpec = resolveGraphSpec(
3497
+ program.graph,
3498
+ `${program.id ?? "program"} graph (outputs)`
3499
+ );
3500
+ if (!programSpec) {
3501
+ continue;
3502
+ }
3503
+ recordOutputs(collectOutputPaths(programSpec));
3504
+ }
1203
3505
  outputPathsRef.current = namespacedOutputPaths;
1204
3506
  baseOutputPathsRef.current = baseOutputPaths;
1205
3507
  namespacedOutputPathsRef.current = namespacedOutputPaths;
@@ -1232,20 +3534,24 @@ function VizijRuntimeProviderInner({
1232
3534
  });
1233
3535
  }
1234
3536
  registeredGraphsRef.current = graphIds;
3537
+ if (isRuntimeDebugEnabled()) {
3538
+ console.log("[vizij-runtime] registered graph ids", graphIds);
3539
+ }
1235
3540
  const animationIds = [];
3541
+ const hostClips = [];
1236
3542
  for (const anim of assetBundle.animations ?? []) {
1237
3543
  try {
1238
- const controllerId = namespaceControllerId(anim.id, namespace, "animation") ?? anim.id;
1239
- const config = {
1240
- id: controllerId,
1241
- setup: {
1242
- animation: anim.clip,
1243
- ...anim.setup ?? {}
1244
- }
1245
- };
1246
- const id = registerAnimation(config);
1247
- animationIds.push(id);
3544
+ const stored = anim.setup?.animation ?? toStoredAnimationClip(anim.id, anim.clip);
3545
+ hostClips.push({ id: anim.id, stored });
3546
+ registerAnimation({ id: anim.id });
3547
+ animationIds.push(anim.id);
1248
3548
  } catch (err) {
3549
+ if (isRuntimeDebugEnabled()) {
3550
+ console.warn("[vizij-runtime] failed animation registration", {
3551
+ animationId: anim.id,
3552
+ error: err instanceof Error ? err.message : String(err)
3553
+ });
3554
+ }
1249
3555
  pushError({
1250
3556
  message: `Failed to register animation ${anim.id}`,
1251
3557
  cause: err,
@@ -1254,6 +3560,7 @@ function VizijRuntimeProviderInner({
1254
3560
  });
1255
3561
  }
1256
3562
  }
3563
+ ensureAnimationHost()?.setClips(hostClips);
1257
3564
  registeredAnimationsRef.current = animationIds;
1258
3565
  if (assetBundle.initialInputs) {
1259
3566
  Object.entries(assetBundle.initialInputs).forEach(([path, value]) => {
@@ -1270,6 +3577,13 @@ function VizijRuntimeProviderInner({
1270
3577
  });
1271
3578
  }
1272
3579
  const controllers = listControllers();
3580
+ if (isRuntimeDebugEnabled()) {
3581
+ console.log("[vizij-runtime] controllers after register", {
3582
+ controllers,
3583
+ graphIds,
3584
+ animationIds
3585
+ });
3586
+ }
1273
3587
  reportStatus((prev) => ({
1274
3588
  ...prev,
1275
3589
  ready: true,
@@ -1280,6 +3594,7 @@ function VizijRuntimeProviderInner({
1280
3594
  }, [
1281
3595
  assetBundle,
1282
3596
  clearControllers,
3597
+ ensureAnimationHost,
1283
3598
  listControllers,
1284
3599
  mergeStrategy,
1285
3600
  namespace,
@@ -1289,12 +3604,18 @@ function VizijRuntimeProviderInner({
1289
3604
  registerGraph,
1290
3605
  registerMergedGraph,
1291
3606
  reportStatus,
3607
+ resolvedProgramAssets,
1292
3608
  setInput
1293
3609
  ]);
1294
3610
  useEffect(() => {
1295
3611
  if (!ready || status.loading) {
1296
3612
  return;
1297
3613
  }
3614
+ const plan = pendingPlanRef.current;
3615
+ const hasRegistered = registeredGraphsRef.current.length > 0 || registeredAnimationsRef.current.length > 0 || programControllerIdsRef.current.size > 0;
3616
+ if (plan && !plan.reregisterGraphs && hasRegistered) {
3617
+ return;
3618
+ }
1298
3619
  registerControllers().catch((err) => {
1299
3620
  pushError({
1300
3621
  message: "Failed to register controllers",
@@ -1303,37 +3624,79 @@ function VizijRuntimeProviderInner({
1303
3624
  timestamp: performance.now()
1304
3625
  });
1305
3626
  });
1306
- }, [ready, status.loading, registerControllers, pushError]);
3627
+ }, [ready, status.loading, graphUpdateToken, registerControllers, pushError]);
1307
3628
  useEffect(() => {
1308
- if (!frame) {
1309
- return;
1310
- }
1311
- const writes = frame.merged_writes ?? [];
1312
- if (!writes.length) {
1313
- return;
1314
- }
1315
- const setWorldValues = store.getState().setValues;
1316
- const namespaceValue = status.namespace;
1317
- const batched = [];
1318
- const namespacedOutputs = namespacedOutputPathsRef.current;
1319
- const baseOutputs = baseOutputPathsRef.current;
1320
- writes.forEach((write) => {
1321
- const path = normalisePath(write.path);
1322
- if (!namespacedOutputs.has(path)) {
3629
+ applyEngineChangesRef.current = (changes) => {
3630
+ const entries = Object.entries(changes);
3631
+ if (!entries.length) {
1323
3632
  return;
1324
3633
  }
1325
- const raw = valueJSONToRaw(write.value);
1326
- if (raw === void 0) {
1327
- return;
3634
+ const setWorldValues = store.getState().setValues;
3635
+ const namespaceValue = status.namespace;
3636
+ const currentValues = store.getState().values;
3637
+ const rigInputPathMap = rigInputMapRef.current;
3638
+ const rigPoseControlInputIds = rigPoseControlInputIdsRef.current;
3639
+ const batched = [];
3640
+ const namespacedOutputs = namespacedOutputPathsRef.current;
3641
+ const baseOutputs = baseOutputPathsRef.current;
3642
+ entries.forEach(([changedPath, changedValue]) => {
3643
+ if (changedValue === null || isBuiltInPath(changedPath)) {
3644
+ return;
3645
+ }
3646
+ const path = normalisePath(changedPath);
3647
+ const basePath = stripNamespace(path, namespaceValue);
3648
+ const isTrackedOutput = namespacedOutputs.has(path) || baseOutputs.has(basePath);
3649
+ if (!isTrackedOutput) {
3650
+ return;
3651
+ }
3652
+ const raw = valueJSONToRaw(changedValue);
3653
+ if (raw === void 0) {
3654
+ return;
3655
+ }
3656
+ const poseControlMatch = /^rig\/[^/]+\/pose\/control\/(.+)$/.exec(
3657
+ basePath
3658
+ );
3659
+ if (poseControlMatch && typeof raw === "number" && Number.isFinite(raw)) {
3660
+ const inputId = (poseControlMatch[1] ?? "").trim();
3661
+ const hasNativePoseControlInput = inputId.length > 0 && rigPoseControlInputIds.has(inputId);
3662
+ const mappedInputPath = inputId.length === 0 ? void 0 : resolvePoseControlInputPath({
3663
+ inputId,
3664
+ basePath,
3665
+ rigInputPathMap,
3666
+ hasNativePoseControlInput
3667
+ });
3668
+ if (mappedInputPath) {
3669
+ const bridgeKey = `${namespaceValue}:${mappedInputPath}`;
3670
+ const previousValue = poseControlBridgeValuesRef.current.get(bridgeKey);
3671
+ if (previousValue === void 0 || Math.abs(previousValue - raw) > POSE_CONTROL_BRIDGE_EPSILON) {
3672
+ poseControlBridgeValuesRef.current.set(bridgeKey, raw);
3673
+ setInput(mappedInputPath, { float: raw });
3674
+ }
3675
+ }
3676
+ }
3677
+ const targetPath = baseOutputs.has(basePath) ? basePath : path;
3678
+ const currentValue = currentValues.get(
3679
+ getLookup(namespaceValue, targetPath)
3680
+ );
3681
+ const nextWrite = transformOutputWrite ? transformOutputWrite({
3682
+ id: targetPath,
3683
+ namespace: namespaceValue,
3684
+ value: raw,
3685
+ currentValue
3686
+ }) : {
3687
+ id: targetPath,
3688
+ namespace: namespaceValue,
3689
+ value: raw
3690
+ };
3691
+ if (nextWrite) {
3692
+ batched.push(nextWrite);
3693
+ }
3694
+ });
3695
+ if (batched.length > 0) {
3696
+ setWorldValues(batched);
1328
3697
  }
1329
- const basePath = stripNamespace(path, namespaceValue);
1330
- const targetPath = baseOutputs.has(basePath) ? basePath : path;
1331
- batched.push({ id: targetPath, namespace: namespaceValue, value: raw });
1332
- });
1333
- if (batched.length > 0) {
1334
- setWorldValues(batched);
1335
- }
1336
- }, [frame, status.namespace, store]);
3698
+ };
3699
+ }, [status.namespace, store, transformOutputWrite]);
1337
3700
  const stagePoseNeutral = useCallback(
1338
3701
  (force = false) => {
1339
3702
  const neutral = assetBundle.pose?.config?.neutralInputs ?? {};
@@ -1399,165 +3762,583 @@ function VizijRuntimeProviderInner({
1399
3762
  });
1400
3763
  toDelete.forEach((key) => map.delete(key));
1401
3764
  },
1402
- [setInput]
3765
+ [setInput]
3766
+ );
3767
+ const resolveClipById = useCallback(
3768
+ (id) => {
3769
+ return assetBundle.animations?.find((anim) => anim.id === id);
3770
+ },
3771
+ [assetBundle.animations]
3772
+ );
3773
+ const resolveClipPromise = useCallback((state) => {
3774
+ state.resolve?.();
3775
+ state.resolve = null;
3776
+ state.completion = null;
3777
+ }, []);
3778
+ const ensureClipPromise = useCallback((state) => {
3779
+ if (state.completion) {
3780
+ return state.completion;
3781
+ }
3782
+ const completion = new Promise((resolve) => {
3783
+ state.resolve = resolve;
3784
+ });
3785
+ state.completion = completion;
3786
+ return completion;
3787
+ }, []);
3788
+ const ensureClipState = useCallback(
3789
+ (id) => {
3790
+ const clip = resolveClipById(id);
3791
+ if (!clip) {
3792
+ return null;
3793
+ }
3794
+ const duration = resolveClipDurationSeconds(
3795
+ clip.clip
3796
+ );
3797
+ const existing = clipPlaybackRef.current.get(id);
3798
+ if (existing) {
3799
+ existing.duration = duration;
3800
+ return { clip, state: existing };
3801
+ }
3802
+ const state = {
3803
+ id,
3804
+ duration,
3805
+ speed: 1,
3806
+ weight: 1,
3807
+ loop: true,
3808
+ playing: false,
3809
+ resolve: null,
3810
+ completion: null
3811
+ };
3812
+ clipPlaybackRef.current.set(id, state);
3813
+ return { clip, state };
3814
+ },
3815
+ [resolveClipById]
3816
+ );
3817
+ const animateValue = useCallback(
3818
+ (path, target, options) => {
3819
+ const targetValue = valueAsNumber2(target);
3820
+ const basePath = stripNamespace(
3821
+ normalisePath(path),
3822
+ namespaceRef.current
3823
+ );
3824
+ const poseValues = useLegacyPoseWeightFallback && targetValue != null ? poseWeightFallbackMap.get(basePath) : void 0;
3825
+ if (poseValues && targetValue != null) {
3826
+ const poseFaceId = assetBundle.pose?.config?.faceId ?? faceId ?? "face";
3827
+ const rigMap = rigInputMapRef.current;
3828
+ return Promise.all(
3829
+ Object.entries(poseValues).flatMap(([inputId, poseValue]) => {
3830
+ if (!Number.isFinite(poseValue)) {
3831
+ return [];
3832
+ }
3833
+ const controlPath = resolvePoseControlInputPath({
3834
+ inputId,
3835
+ basePath: buildRigInputPath(
3836
+ poseFaceId,
3837
+ `/pose/control/${inputId}`
3838
+ ),
3839
+ rigInputPathMap: rigMap,
3840
+ hasNativePoseControlInput: true
3841
+ }) ?? buildRigInputPath(poseFaceId, `/pose/control/${inputId}`);
3842
+ return [
3843
+ animateValue(
3844
+ controlPath,
3845
+ { float: Number(poseValue) * targetValue },
3846
+ options
3847
+ )
3848
+ ];
3849
+ })
3850
+ ).then(() => void 0);
3851
+ }
3852
+ const easing = resolveEasing(options?.easing);
3853
+ const duration = Math.max(0, options?.duration ?? DEFAULT_DURATION);
3854
+ cancelAnimation(path);
3855
+ const namespacedPath = namespaceTypedPath(path, namespaceRef.current);
3856
+ const current = getPathSnapshot(namespacedPath);
3857
+ const fromValue = valueAsNumber2(current);
3858
+ const toValue = valueAsNumber2(target);
3859
+ if (fromValue == null || toValue == null || duration === 0) {
3860
+ setInput(path, target);
3861
+ return Promise.resolve();
3862
+ }
3863
+ return new Promise((resolve) => {
3864
+ animationTweensRef.current.set(path, {
3865
+ // Keep the raw path here so tween updates go through setInput() once
3866
+ // and pick up the active namespace exactly once.
3867
+ path,
3868
+ from: fromValue,
3869
+ to: toValue,
3870
+ duration,
3871
+ elapsed: 0,
3872
+ easing,
3873
+ resolve
3874
+ });
3875
+ markActivity();
3876
+ });
3877
+ },
3878
+ [
3879
+ assetBundle.pose?.config?.faceId,
3880
+ cancelAnimation,
3881
+ faceId,
3882
+ getPathSnapshot,
3883
+ markActivity,
3884
+ poseWeightFallbackMap,
3885
+ setInput,
3886
+ useLegacyPoseWeightFallback
3887
+ ]
3888
+ );
3889
+ const warnAnimationGap = useCallback((key, message) => {
3890
+ if (animationGapWarnedRef.current.has(key)) {
3891
+ return;
3892
+ }
3893
+ animationGapWarnedRef.current.add(key);
3894
+ console.warn(`[vizij-runtime] ${message}`);
3895
+ }, []);
3896
+ const syncAnimationsSource = useCallback(() => {
3897
+ const host = animationHostRef.current;
3898
+ setAnimationsSourceRegistered(host ? host.hasPlaying() : false);
3899
+ updateLoopMode();
3900
+ }, [setAnimationsSourceRegistered, updateLoopMode]);
3901
+ const readPlayerStates = useCallback(() => {
3902
+ const device = deviceSlot.current?.device;
3903
+ if (!device) {
3904
+ return [];
3905
+ }
3906
+ const raw = device.readValues([ANIMATION_PLAYERS_PATH])[ANIMATION_PLAYERS_PATH];
3907
+ return decodePlayerStates(raw);
3908
+ }, [deviceSlot]);
3909
+ const settleFinishedClips = useCallback(() => {
3910
+ let pending = false;
3911
+ clipPlaybackRef.current.forEach((state) => {
3912
+ if (state.completion && !state.loop) {
3913
+ pending = true;
3914
+ }
3915
+ });
3916
+ const host = animationHostRef.current;
3917
+ if (!pending || !host) {
3918
+ return;
3919
+ }
3920
+ const states = readPlayerStates();
3921
+ if (states.length === 0) {
3922
+ return;
3923
+ }
3924
+ clipPlaybackRef.current.forEach((state, id) => {
3925
+ if (!state.completion || state.loop) {
3926
+ return;
3927
+ }
3928
+ const playerId = host.playerIdOf(id);
3929
+ if (playerId === null) {
3930
+ return;
3931
+ }
3932
+ const feedback = states.find((entry) => entry.player === playerId);
3933
+ if (!feedback) {
3934
+ return;
3935
+ }
3936
+ const duration = feedback.duration > 0 ? feedback.duration : state.duration;
3937
+ const atEnd = duration > 0 && feedback.time >= duration - 1e-3;
3938
+ if (feedback.state === "stopped" || atEnd) {
3939
+ state.playing = false;
3940
+ resolveClipPromise(state);
3941
+ host.pause(id);
3942
+ syncAnimationsSource();
3943
+ }
3944
+ });
3945
+ }, [readPlayerStates, resolveClipPromise, syncAnimationsSource]);
3946
+ const playAnimation = useCallback(
3947
+ (id, options) => {
3948
+ const ensured = ensureClipState(id);
3949
+ if (!ensured) {
3950
+ return Promise.reject(
3951
+ new Error(`Animation ${id} is not part of the current asset bundle.`)
3952
+ );
3953
+ }
3954
+ const { state } = ensured;
3955
+ const host = ensureAnimationHost();
3956
+ state.speed = options?.speed ?? state.speed;
3957
+ state.weight = options?.weight ?? state.weight;
3958
+ state.playing = true;
3959
+ const completion = ensureClipPromise(state);
3960
+ clipPlaybackRef.current.set(id, state);
3961
+ if (host) {
3962
+ if (options?.reset === true) {
3963
+ host.seek(id, 0);
3964
+ }
3965
+ host.setSpeed(id, state.speed);
3966
+ host.setWeight(id, state.weight);
3967
+ host.setLoop(id, state.loop ? "loop" : "once");
3968
+ void host.play(id).catch((err) => {
3969
+ pushError({
3970
+ message: `Failed to start animation ${id} on the device`,
3971
+ cause: err,
3972
+ phase: "animation",
3973
+ timestamp: performance.now()
3974
+ });
3975
+ });
3976
+ } else {
3977
+ warnAnimationGap(
3978
+ "module-not-ready",
3979
+ "playAnimation called before the animation module finished loading; playback starts once it is ready."
3980
+ );
3981
+ }
3982
+ syncAnimationsSource();
3983
+ markActivity();
3984
+ return completion;
3985
+ },
3986
+ [
3987
+ ensureAnimationHost,
3988
+ ensureClipPromise,
3989
+ ensureClipState,
3990
+ markActivity,
3991
+ pushError,
3992
+ syncAnimationsSource,
3993
+ warnAnimationGap
3994
+ ]
3995
+ );
3996
+ const pauseAnimation = useCallback(
3997
+ (id) => {
3998
+ const state = clipPlaybackRef.current.get(id);
3999
+ if (!state || !state.playing) {
4000
+ return;
4001
+ }
4002
+ state.playing = false;
4003
+ animationHostRef.current?.pause(id);
4004
+ syncAnimationsSource();
4005
+ },
4006
+ [syncAnimationsSource]
4007
+ );
4008
+ const seekAnimation = useCallback((id, timeSeconds) => {
4009
+ if (!clipPlaybackRef.current.has(id)) {
4010
+ return;
4011
+ }
4012
+ animationHostRef.current?.seek(id, Math.max(0, timeSeconds));
4013
+ }, []);
4014
+ const setAnimationLoop = useCallback(
4015
+ (id, enabled) => {
4016
+ const ensured = ensureClipState(id);
4017
+ if (!ensured) {
4018
+ return;
4019
+ }
4020
+ ensured.state.loop = Boolean(enabled);
4021
+ animationHostRef.current?.setLoop(id, enabled ? "loop" : "once");
4022
+ updateLoopMode();
4023
+ },
4024
+ [ensureClipState, updateLoopMode]
4025
+ );
4026
+ const getAnimationState = useCallback(
4027
+ (id) => {
4028
+ const state = clipPlaybackRef.current.get(id);
4029
+ if (!state) {
4030
+ return null;
4031
+ }
4032
+ const playerId = animationHostRef.current?.playerIdOf(id) ?? null;
4033
+ const feedback = playerId !== null ? readPlayerStates().find((entry) => entry.player === playerId) : void 0;
4034
+ return {
4035
+ time: feedback?.time ?? 0,
4036
+ duration: feedback && feedback.duration > 0 ? feedback.duration : state.duration,
4037
+ playing: feedback ? feedback.state === "playing" : state.playing,
4038
+ loop: state.loop,
4039
+ speed: feedback?.speed ?? state.speed
4040
+ };
4041
+ },
4042
+ [readPlayerStates]
4043
+ );
4044
+ const stopAnimation = useCallback(
4045
+ (id, options) => {
4046
+ const state = clipPlaybackRef.current.get(id);
4047
+ if (state) {
4048
+ clipPlaybackRef.current.delete(id);
4049
+ state.playing = false;
4050
+ resolveClipPromise(state);
4051
+ }
4052
+ const host = animationHostRef.current;
4053
+ if (options?.clearOutputs === false) {
4054
+ host?.pause(id);
4055
+ syncAnimationsSource();
4056
+ } else {
4057
+ host?.stop(id);
4058
+ const unsubscribe = subscribeToStep(() => {
4059
+ unsubscribe();
4060
+ syncAnimationsSource();
4061
+ });
4062
+ }
4063
+ },
4064
+ [resolveClipPromise, subscribeToStep, syncAnimationsSource]
4065
+ );
4066
+ const refreshControllerStatus = useCallback(() => {
4067
+ const controllers = listControllers();
4068
+ reportStatus((prev) => ({
4069
+ ...prev,
4070
+ controllers,
4071
+ outputPaths: Array.from(outputPathsRef.current)
4072
+ }));
4073
+ onRegisterControllers?.(controllers);
4074
+ }, [listControllers, onRegisterControllers, reportStatus]);
4075
+ const resolveProgramById = useCallback(
4076
+ (id) => {
4077
+ return resolvedProgramAssets.find((program) => program.id === id);
4078
+ },
4079
+ [resolvedProgramAssets]
1403
4080
  );
1404
- const sampleTrack = useCallback(
1405
- (track, time) => {
1406
- const keyframes = Array.isArray(track.keyframes) ? track.keyframes : [];
1407
- if (!keyframes.length) {
1408
- return 0;
1409
- }
1410
- const first = keyframes[0];
1411
- if (!first) {
1412
- return 0;
1413
- }
1414
- if (time <= Number(first.time ?? 0)) {
1415
- return Number(first.value ?? 0);
1416
- }
1417
- for (let i = 0; i < keyframes.length - 1; i += 1) {
1418
- const current = keyframes[i] ?? {};
1419
- const next = keyframes[i + 1] ?? {};
1420
- const start = Number(current.time ?? 0);
1421
- const end = Number(next.time ?? start);
1422
- if (time >= start && time <= end) {
1423
- const range = end - start || 1;
1424
- const factor = (time - start) / range;
1425
- const currentValue = Number(current.value ?? 0);
1426
- const nextValue = Number(next.value ?? currentValue);
1427
- return currentValue + (nextValue - currentValue) * factor;
1428
- }
4081
+ const buildProgramRegistrationConfig = useCallback(
4082
+ (program) => {
4083
+ const graphSpec = resolveGraphSpec(
4084
+ program.graph,
4085
+ `${program.id ?? "program"} graph`
4086
+ );
4087
+ if (!graphSpec) {
4088
+ return null;
4089
+ }
4090
+ const outputs = collectOutputPaths(graphSpec);
4091
+ const inputs = collectInputPaths(graphSpec);
4092
+ const subs = program.graph.subscriptions ?? {
4093
+ inputs,
4094
+ outputs
4095
+ };
4096
+ return {
4097
+ id: namespaceControllerId(program.id, namespace, "graph"),
4098
+ spec: stripNulls(namespaceGraphSpec(graphSpec, namespace)),
4099
+ subs: namespaceSubscriptions(subs, namespace)
4100
+ };
4101
+ },
4102
+ [namespace]
4103
+ );
4104
+ const deriveProgramResetValues = useCallback(
4105
+ (program) => {
4106
+ if (program.resetValues) {
4107
+ return Object.entries(program.resetValues).filter(([, value]) => Number.isFinite(value)).map(([path, value]) => ({ path, value }));
4108
+ }
4109
+ const graphSpec = resolveGraphSpec(
4110
+ program.graph,
4111
+ `${program.id ?? "program"} graph (reset)`
4112
+ );
4113
+ if (!graphSpec) {
4114
+ return [];
1429
4115
  }
1430
- const last = keyframes[keyframes.length - 1];
1431
- return Number(last?.value ?? 0);
4116
+ return collectOutputPaths(graphSpec).filter((path) => path.trim().length > 0).map((path) => {
4117
+ const defaultValue = inputConstraintsRef.current[path]?.defaultValue ?? 0;
4118
+ return {
4119
+ path,
4120
+ value: Number.isFinite(defaultValue) && defaultValue != null ? defaultValue : 0
4121
+ };
4122
+ });
1432
4123
  },
1433
4124
  []
1434
4125
  );
1435
- const advanceClipPlayback = useCallback(
1436
- (dt) => {
1437
- if (clipPlaybackRef.current.size === 0) {
4126
+ const syncProgramPlaybackControllers = useCallback(() => {
4127
+ if (!ready) {
4128
+ return;
4129
+ }
4130
+ const availableProgramIds = new Set(
4131
+ resolvedProgramAssets.map((program) => program.id)
4132
+ );
4133
+ Array.from(programPlaybackRef.current.keys()).forEach((id) => {
4134
+ if (availableProgramIds.has(id)) {
1438
4135
  return;
1439
4136
  }
1440
- const map = clipPlaybackRef.current;
1441
- const toDelete = [];
1442
- map.forEach((state, key) => {
1443
- state.time += dt * state.speed;
1444
- const clip = assetBundle.animations?.find(
1445
- (anim) => anim.id === state.id
1446
- );
1447
- if (!clip) {
1448
- toDelete.push(key);
1449
- state.resolve();
4137
+ programPlaybackRef.current.delete(id);
4138
+ const controllerId = programControllerIdsRef.current.get(id);
4139
+ if (controllerId) {
4140
+ try {
4141
+ removeGraph(controllerId);
4142
+ } catch (err) {
4143
+ pushError({
4144
+ message: `Failed to remove program ${id}`,
4145
+ cause: err,
4146
+ phase: "registration",
4147
+ timestamp: performance.now()
4148
+ });
4149
+ }
4150
+ programControllerIdsRef.current.delete(id);
4151
+ }
4152
+ });
4153
+ programPlaybackRef.current.forEach((state, id) => {
4154
+ const program = resolveProgramById(id);
4155
+ const controllerId = programControllerIdsRef.current.get(id);
4156
+ if (!program) {
4157
+ return;
4158
+ }
4159
+ if (state.state !== "playing") {
4160
+ if (!controllerId) {
1450
4161
  return;
1451
4162
  }
1452
- const clipData = clip.clip;
1453
- const duration = Number(clipData?.duration ?? state.duration);
1454
- state.duration = Number.isFinite(duration) && duration > 0 ? duration : state.duration;
1455
- if (state.time >= state.duration) {
1456
- toDelete.push(key);
1457
- state.time = state.duration;
4163
+ try {
4164
+ removeGraph(controllerId);
4165
+ } catch (err) {
4166
+ pushError({
4167
+ message: `Failed to pause program ${id}`,
4168
+ cause: err,
4169
+ phase: "registration",
4170
+ timestamp: performance.now()
4171
+ });
1458
4172
  }
1459
- const tracks = Array.isArray(clipData?.tracks) ? clipData.tracks : [];
1460
- tracks.forEach((track) => {
1461
- const value = sampleTrack(track, state.time) * state.weight;
1462
- const path = `animation/${clip.id}/${track.channel}`;
1463
- setInput(path, { float: value });
4173
+ programControllerIdsRef.current.delete(id);
4174
+ return;
4175
+ }
4176
+ if (controllerId) {
4177
+ return;
4178
+ }
4179
+ const config = buildProgramRegistrationConfig(program);
4180
+ if (!config) {
4181
+ pushError({
4182
+ message: `Program ${id} is missing a usable graph payload.`,
4183
+ phase: "registration",
4184
+ timestamp: performance.now()
1464
4185
  });
1465
- if (toDelete.includes(key)) {
1466
- state.resolve();
1467
- }
4186
+ return;
4187
+ }
4188
+ try {
4189
+ const nextControllerId = registerGraph(config);
4190
+ programControllerIdsRef.current.set(id, nextControllerId);
4191
+ } catch (err) {
4192
+ pushError({
4193
+ message: `Failed to register program ${id}`,
4194
+ cause: err,
4195
+ phase: "registration",
4196
+ timestamp: performance.now()
4197
+ });
4198
+ }
4199
+ });
4200
+ refreshControllerStatus();
4201
+ }, [
4202
+ buildProgramRegistrationConfig,
4203
+ pushError,
4204
+ ready,
4205
+ refreshControllerStatus,
4206
+ registerGraph,
4207
+ removeGraph,
4208
+ resolvedProgramAssets,
4209
+ resolveProgramById
4210
+ ]);
4211
+ const playProgram = useCallback(
4212
+ (id) => {
4213
+ const program = resolveProgramById(id);
4214
+ if (!program) {
4215
+ throw new Error(
4216
+ `Program ${id} is not part of the current asset bundle.`
4217
+ );
4218
+ }
4219
+ deriveProgramInputSeedValues({
4220
+ program,
4221
+ namespace,
4222
+ inputConstraints: inputConstraintsRef.current,
4223
+ getPathSnapshot,
4224
+ stagedInputs: stagedInputsRef.current
4225
+ }).forEach(({ path, value }) => {
4226
+ setInput(path, value);
1468
4227
  });
1469
- toDelete.forEach((key) => {
1470
- clipPlaybackRef.current.delete(key);
1471
- const clip = assetBundle.animations?.find((anim) => anim.id === key);
1472
- if (clip) {
1473
- const clipData = clip.clip;
1474
- const tracks = Array.isArray(clipData?.tracks) ? clipData.tracks : [];
1475
- tracks.forEach((track) => {
1476
- const path = `animation/${clip.id}/${track.channel}`;
1477
- setInput(path, { float: 0 });
1478
- });
1479
- }
4228
+ programPlaybackRef.current.set(id, {
4229
+ id,
4230
+ state: "playing"
1480
4231
  });
4232
+ syncProgramPlaybackControllers();
4233
+ markActivity();
1481
4234
  },
1482
- [assetBundle.animations, sampleTrack, setInput]
4235
+ [
4236
+ getPathSnapshot,
4237
+ markActivity,
4238
+ namespace,
4239
+ resolveProgramById,
4240
+ setInput,
4241
+ syncProgramPlaybackControllers
4242
+ ]
1483
4243
  );
1484
- const animateValue = useCallback(
1485
- (path, target, options) => {
1486
- const easing = resolveEasing(options?.easing);
1487
- const duration = Math.max(0, options?.duration ?? DEFAULT_DURATION);
1488
- cancelAnimation(path);
1489
- const namespacedPath = namespaceTypedPath(path, namespaceRef.current);
1490
- const current = getPathSnapshot(namespacedPath);
1491
- const fromValue = valueAsNumber2(current);
1492
- const toValue = valueAsNumber2(target);
1493
- if (fromValue == null || toValue == null || duration === 0) {
1494
- setInput(path, target);
1495
- return Promise.resolve();
4244
+ const pauseProgram = useCallback(
4245
+ (id) => {
4246
+ if (!resolveProgramById(id)) {
4247
+ return;
1496
4248
  }
1497
- return new Promise((resolve) => {
1498
- animationTweensRef.current.set(path, {
1499
- path: namespacedPath,
1500
- from: fromValue,
1501
- to: toValue,
1502
- duration,
1503
- elapsed: 0,
1504
- easing,
1505
- resolve
1506
- });
1507
- markActivity();
4249
+ programPlaybackRef.current.set(id, {
4250
+ id,
4251
+ state: "paused"
1508
4252
  });
4253
+ syncProgramPlaybackControllers();
4254
+ updateLoopMode();
1509
4255
  },
1510
- [cancelAnimation, getPathSnapshot, markActivity, setInput]
4256
+ [resolveProgramById, syncProgramPlaybackControllers, updateLoopMode]
1511
4257
  );
1512
- const playAnimation = useCallback(
4258
+ const stopProgram = useCallback(
1513
4259
  (id, options) => {
1514
- const clip = assetBundle.animations?.find((anim) => anim.id === id);
1515
- if (!clip) {
1516
- return Promise.reject(
1517
- new Error(`Animation ${id} is not part of the current asset bundle.`)
1518
- );
1519
- }
1520
- if (clipPlaybackRef.current.has(id)) {
1521
- clipPlaybackRef.current.delete(id);
4260
+ const program = resolveProgramById(id);
4261
+ const controllerId = programControllerIdsRef.current.get(id);
4262
+ if (controllerId) {
4263
+ try {
4264
+ removeGraph(controllerId);
4265
+ } catch (err) {
4266
+ pushError({
4267
+ message: `Failed to stop program ${id}`,
4268
+ cause: err,
4269
+ phase: "registration",
4270
+ timestamp: performance.now()
4271
+ });
4272
+ }
4273
+ programControllerIdsRef.current.delete(id);
1522
4274
  }
1523
- return new Promise((resolve) => {
1524
- const speed = options?.speed ?? 1;
1525
- const weight = options?.weight ?? clip.weight ?? 1;
1526
- const clipData = clip.clip;
1527
- clipPlaybackRef.current.set(id, {
1528
- id,
1529
- time: options?.reset ? 0 : 0,
1530
- duration: Number(clipData?.duration ?? 0),
1531
- speed: Number.isFinite(speed) && speed > 0 ? speed : 1,
1532
- weight,
1533
- resolve
1534
- });
1535
- markActivity();
4275
+ programPlaybackRef.current.set(id, {
4276
+ id,
4277
+ state: "stopped"
1536
4278
  });
4279
+ if (program && options?.resetOutputs !== false) {
4280
+ deriveProgramResetValues(program).forEach(({ path, value }) => {
4281
+ setInput(path, { float: value });
4282
+ });
4283
+ }
4284
+ refreshControllerStatus();
4285
+ updateLoopMode();
1537
4286
  },
1538
- [assetBundle.animations, markActivity]
4287
+ [
4288
+ deriveProgramResetValues,
4289
+ pushError,
4290
+ refreshControllerStatus,
4291
+ removeGraph,
4292
+ resolveProgramById,
4293
+ setInput,
4294
+ updateLoopMode
4295
+ ]
1539
4296
  );
1540
- const stopAnimation = useCallback(
4297
+ const getProgramState = useCallback(
1541
4298
  (id) => {
1542
- const clip = assetBundle.animations?.find((anim) => anim.id === id);
1543
- const state = clipPlaybackRef.current.get(id);
1544
- if (state) {
1545
- clipPlaybackRef.current.delete(id);
1546
- state.resolve();
4299
+ const state = programPlaybackRef.current.get(id);
4300
+ if (!state) {
4301
+ return null;
1547
4302
  }
1548
- if (clip) {
1549
- const clipData = clip.clip;
1550
- const tracks = Array.isArray(clipData?.tracks) ? clipData.tracks : [];
1551
- tracks.forEach((track) => {
1552
- const path = `animation/${clip.id}/${track.channel}`;
1553
- setInput(path, { float: 0 });
4303
+ return { state: state.state };
4304
+ },
4305
+ []
4306
+ );
4307
+ useEffect(() => {
4308
+ if (!ready || status.loading) {
4309
+ return;
4310
+ }
4311
+ syncProgramPlaybackControllers();
4312
+ }, [
4313
+ graphUpdateToken,
4314
+ ready,
4315
+ resolvedProgramAssets,
4316
+ status.loading,
4317
+ syncProgramPlaybackControllers
4318
+ ]);
4319
+ const setAnimationActive = useCallback(
4320
+ (active) => {
4321
+ const next = Boolean(active);
4322
+ if (animationSystemActiveRef.current === next) {
4323
+ return;
4324
+ }
4325
+ animationSystemActiveRef.current = next;
4326
+ if (!next) {
4327
+ clipPlaybackRef.current.forEach((state) => {
4328
+ state.playing = false;
1554
4329
  });
1555
4330
  }
4331
+ updateLoopMode();
1556
4332
  },
1557
- [assetBundle.animations, setInput]
4333
+ [updateLoopMode]
4334
+ );
4335
+ const isAnimationActive = useCallback(
4336
+ () => animationSystemActiveRef.current,
4337
+ []
1558
4338
  );
1559
4339
  const registerInputDriver = useCallback(
1560
4340
  (id, factory) => {
4341
+ inputDriverIdsRef.current.add(id);
1561
4342
  const driver = factory({
1562
4343
  setInput,
1563
4344
  setRendererValue,
@@ -1599,6 +4380,8 @@ function VizijRuntimeProviderInner({
1599
4380
  phase: "driver",
1600
4381
  timestamp: performance.now()
1601
4382
  });
4383
+ } finally {
4384
+ inputDriverIdsRef.current.delete(id);
1602
4385
  }
1603
4386
  }
1604
4387
  };
@@ -1606,22 +4389,15 @@ function VizijRuntimeProviderInner({
1606
4389
  },
1607
4390
  [faceId, namespace, pushError, setInput, setRendererValue]
1608
4391
  );
1609
- const advanceAnimations = useCallback(
1610
- (dt) => {
1611
- advanceAnimationTweens(dt);
1612
- advanceClipPlayback(dt);
1613
- },
1614
- [advanceAnimationTweens, advanceClipPlayback]
1615
- );
1616
4392
  const flushStagedInputs = useCallback(() => {
1617
4393
  if (stagedInputsRef.current.size === 0) {
1618
4394
  return;
1619
4395
  }
1620
4396
  stagedInputsRef.current.forEach(({ value, shape }, path) => {
1621
- orchestratorSetInput(path, value, shape);
4397
+ deviceSetInput(path, value, shape);
1622
4398
  });
1623
4399
  stagedInputsRef.current.clear();
1624
- }, [orchestratorSetInput]);
4400
+ }, [deviceSetInput]);
1625
4401
  const step = useCallback(
1626
4402
  (dt, opts) => {
1627
4403
  if (dt > 0 && Number.isFinite(dt)) {
@@ -1629,13 +4405,19 @@ function VizijRuntimeProviderInner({
1629
4405
  const alpha = 0.1;
1630
4406
  avgStepDtRef.current = prev * (1 - alpha) + dt * alpha;
1631
4407
  }
1632
- advanceAnimations(dt);
4408
+ advanceAnimationTweens(dt);
1633
4409
  flushStagedInputs();
1634
- if (driveOrchestratorRef.current || opts?.forceRuntime) {
4410
+ if (driveRuntimeRef.current || opts?.forceRuntime) {
1635
4411
  stepRuntime(dt);
1636
4412
  }
4413
+ settleFinishedClips();
1637
4414
  },
1638
- [advanceAnimations, flushStagedInputs, stepRuntime]
4415
+ [
4416
+ advanceAnimationTweens,
4417
+ flushStagedInputs,
4418
+ settleFinishedClips,
4419
+ stepRuntime
4420
+ ]
1639
4421
  );
1640
4422
  useEffect(() => {
1641
4423
  if (loopMode !== "active") {
@@ -1650,7 +4432,7 @@ function VizijRuntimeProviderInner({
1650
4432
  if (lastTime == null) {
1651
4433
  lastTime = timestamp;
1652
4434
  }
1653
- const dt = Math.max(0, (timestamp - lastTime) / 1e3);
4435
+ const dt = inferImplicitPause(Math.max(0, (timestamp - lastTime) / 1e3));
1654
4436
  lastTime = timestamp;
1655
4437
  step(dt || 0);
1656
4438
  requestLoopMode(computeDesiredLoopMode());
@@ -1681,7 +4463,7 @@ function VizijRuntimeProviderInner({
1681
4463
  return;
1682
4464
  }
1683
4465
  const current = now();
1684
- const dt = Math.max(0, (current - lastTime) / 1e3);
4466
+ const dt = inferImplicitPause(Math.max(0, (current - lastTime) / 1e3));
1685
4467
  lastTime = current;
1686
4468
  step(dt || 0);
1687
4469
  requestLoopMode(computeDesiredLoopMode());
@@ -1695,6 +4477,8 @@ function VizijRuntimeProviderInner({
1695
4477
  return () => {
1696
4478
  animationTweensRef.current.clear();
1697
4479
  clipPlaybackRef.current.clear();
4480
+ programPlaybackRef.current.clear();
4481
+ programControllerIdsRef.current.clear();
1698
4482
  };
1699
4483
  }, []);
1700
4484
  useEffect(() => {
@@ -1710,35 +4494,95 @@ function VizijRuntimeProviderInner({
1710
4494
  }, 500);
1711
4495
  return () => window.clearInterval(id);
1712
4496
  }, [reportStatus]);
4497
+ const setGraphBundle = useCallback(
4498
+ (bundle, options) => {
4499
+ const baseAssetBundle = latestEffectiveAssetBundleRef.current;
4500
+ const nextAssetBundle = applyRuntimeGraphBundle(baseAssetBundle, bundle);
4501
+ const plan = resolveRuntimeUpdatePlan(
4502
+ baseAssetBundle,
4503
+ nextAssetBundle,
4504
+ options?.tier ?? updateTierRef.current
4505
+ );
4506
+ pendingPlanRef.current = plan;
4507
+ previousBundleRef.current = nextAssetBundle;
4508
+ latestEffectiveAssetBundleRef.current = nextAssetBundle;
4509
+ suppressNextBundlePlanRef.current = true;
4510
+ setAssetBundleOverride(nextAssetBundle);
4511
+ if (plan.reregisterGraphs) {
4512
+ setGraphUpdateToken((prev) => prev + 1);
4513
+ }
4514
+ if (plan.reloadAssets) {
4515
+ reportStatus((prev) => ({
4516
+ ...prev,
4517
+ loading: true,
4518
+ ready: false
4519
+ }));
4520
+ } else {
4521
+ reportStatus((prev) => ({
4522
+ ...prev,
4523
+ loading: false
4524
+ }));
4525
+ }
4526
+ },
4527
+ [reportStatus]
4528
+ );
1713
4529
  const contextValue = useMemo(
1714
4530
  () => ({
1715
4531
  ...status,
1716
4532
  assetBundle,
1717
4533
  setInput,
4534
+ getValueSnapshot: getPathSnapshot,
4535
+ getStoreSnapshot,
4536
+ subscribeToStoreChanges,
4537
+ subscribeToStep,
4538
+ setGraphBundle,
1718
4539
  setValue: setRendererValue,
1719
4540
  stagePoseNeutral,
1720
4541
  animateValue,
1721
4542
  cancelAnimation,
1722
4543
  registerInputDriver,
1723
4544
  playAnimation,
4545
+ pauseAnimation,
4546
+ seekAnimation,
4547
+ setAnimationLoop,
4548
+ getAnimationState,
1724
4549
  stopAnimation,
4550
+ playProgram,
4551
+ pauseProgram,
4552
+ stopProgram,
4553
+ getProgramState,
4554
+ setAnimationActive,
4555
+ isAnimationActive,
1725
4556
  step,
1726
- advanceAnimations,
1727
4557
  inputConstraints
1728
4558
  }),
1729
4559
  [
1730
4560
  status,
1731
4561
  assetBundle,
1732
4562
  setInput,
4563
+ getPathSnapshot,
4564
+ getStoreSnapshot,
4565
+ subscribeToStoreChanges,
4566
+ subscribeToStep,
4567
+ setGraphBundle,
1733
4568
  setRendererValue,
1734
4569
  stagePoseNeutral,
1735
4570
  animateValue,
1736
4571
  cancelAnimation,
1737
4572
  registerInputDriver,
1738
4573
  playAnimation,
4574
+ pauseAnimation,
4575
+ seekAnimation,
4576
+ setAnimationLoop,
4577
+ getAnimationState,
1739
4578
  stopAnimation,
4579
+ playProgram,
4580
+ pauseProgram,
4581
+ stopProgram,
4582
+ getProgramState,
4583
+ setAnimationActive,
4584
+ isAnimationActive,
1740
4585
  step,
1741
- advanceAnimations,
1742
4586
  inputConstraints
1743
4587
  ]
1744
4588
  );
@@ -1750,9 +4594,9 @@ import { memo } from "react";
1750
4594
  import { Vizij } from "@vizij/render";
1751
4595
 
1752
4596
  // src/hooks/useVizijRuntime.ts
1753
- import { useContext as useContext2 } from "react";
4597
+ import { useContext } from "react";
1754
4598
  function useVizijRuntime() {
1755
- const ctx = useContext2(VizijRuntimeContext);
4599
+ const ctx = useContext(VizijRuntimeContext);
1756
4600
  if (!ctx) {
1757
4601
  throw new Error(
1758
4602
  "useVizijRuntime must be used within a VizijRuntimeProvider."
@@ -1782,17 +4626,23 @@ function VizijRuntimeFaceInner({
1782
4626
  }
1783
4627
  var VizijRuntimeFace = memo(VizijRuntimeFaceInner);
1784
4628
 
4629
+ // src/hooks/useOptionalVizijRuntime.ts
4630
+ import { useContext as useContext2 } from "react";
4631
+ function useOptionalVizijRuntime() {
4632
+ return useContext2(VizijRuntimeContext);
4633
+ }
4634
+
1785
4635
  // src/hooks/useVizijOutputs.ts
1786
4636
  import {
1787
4637
  useVizijStore
1788
4638
  } from "@vizij/render";
1789
- import { getLookup } from "@vizij/utils";
4639
+ import { getLookup as getLookup2 } from "@vizij/utils";
1790
4640
  function useVizijOutputs(paths) {
1791
4641
  const { namespace } = useVizijRuntime();
1792
4642
  return useVizijStore((state) => {
1793
4643
  const result = {};
1794
4644
  paths.forEach((path) => {
1795
- const lookup = getLookup(namespace, path);
4645
+ const lookup = getLookup2(namespace, path);
1796
4646
  result[path] = state.values.get(lookup);
1797
4647
  });
1798
4648
  return result;
@@ -1804,11 +4654,11 @@ import { useCallback as useCallback2 } from "react";
1804
4654
  import {
1805
4655
  useVizijStore as useVizijStore2
1806
4656
  } from "@vizij/render";
1807
- import { getLookup as getLookup2 } from "@vizij/utils";
4657
+ import { getLookup as getLookup3 } from "@vizij/utils";
1808
4658
  function useRigInput(path) {
1809
4659
  const { namespace, setInput } = useVizijRuntime();
1810
4660
  const value = useVizijStore2((state) => {
1811
- return state.values.get(getLookup2(namespace, path));
4661
+ return state.values.get(getLookup3(namespace, path));
1812
4662
  });
1813
4663
  const setter = useCallback2(
1814
4664
  (next, shape) => {
@@ -1818,9 +4668,422 @@ function useRigInput(path) {
1818
4668
  );
1819
4669
  return [value, setter];
1820
4670
  }
4671
+
4672
+ // src/utils/faceControls.ts
4673
+ var DEFAULT_GAZE_CONTROL = {
4674
+ min: -1,
4675
+ max: 1,
4676
+ defaultValue: 0
4677
+ };
4678
+ var DEFAULT_BLINK_CONTROL = {
4679
+ min: 0,
4680
+ max: 1,
4681
+ defaultValue: 0
4682
+ };
4683
+ var STANDARD_VIZIJ_EYE_PATHS = {
4684
+ leftX: "/standard/vizij/left_eye/pos/x",
4685
+ leftY: "/standard/vizij/left_eye/pos/y",
4686
+ rightX: "/standard/vizij/right_eye/pos/x",
4687
+ rightY: "/standard/vizij/right_eye/pos/y",
4688
+ leftUpper: "/standard/vizij/left_eye_top_eyelid/pos/y",
4689
+ rightUpper: "/standard/vizij/right_eye_top_eyelid/pos/y"
4690
+ };
4691
+ var LEGACY_STANDARD_EYE_PATHS = {
4692
+ leftX: "/standard/left_eye/pos/x",
4693
+ leftY: "/standard/left_eye/pos/y",
4694
+ rightX: "/standard/right_eye/pos/x",
4695
+ rightY: "/standard/right_eye/pos/y",
4696
+ leftUpper: "/standard/left_eye_top_eyelid/pos/y",
4697
+ rightUpper: "/standard/right_eye_top_eyelid/pos/y"
4698
+ };
4699
+ var PROPSRIG_EYE_PATHS = {
4700
+ leftX: "/propsrig/l_eye/translation/x",
4701
+ leftY: "/propsrig/l_eye/translation/y",
4702
+ rightX: "/propsrig/r_eye/translation/x",
4703
+ rightY: "/propsrig/r_eye/translation/y"
4704
+ };
4705
+ function clamp(value, min, max) {
4706
+ return Math.min(Math.max(value, min), max);
4707
+ }
4708
+ function normalizeInputPath(path) {
4709
+ const trimmed = path?.trim() ?? "";
4710
+ if (!trimmed) {
4711
+ return null;
4712
+ }
4713
+ if (trimmed.startsWith("/")) {
4714
+ return trimmed;
4715
+ }
4716
+ const rigMatch = trimmed.match(/^rig\/[^/]+(\/.*)$/);
4717
+ if (rigMatch?.[1]) {
4718
+ return rigMatch[1];
4719
+ }
4720
+ return `/${trimmed}`;
4721
+ }
4722
+ function buildControlsLookup(assetBundle) {
4723
+ const metadata = assetBundle.rig?.inputMetadata ?? [];
4724
+ const metadataByPath = /* @__PURE__ */ new Map();
4725
+ const pathSet = /* @__PURE__ */ new Set();
4726
+ metadata.forEach((entry) => {
4727
+ const normalized = normalizeInputPath(entry.path);
4728
+ if (!normalized) {
4729
+ return;
4730
+ }
4731
+ pathSet.add(normalized);
4732
+ if (!metadataByPath.has(normalized)) {
4733
+ metadataByPath.set(normalized, entry);
4734
+ }
4735
+ });
4736
+ return { pathSet, metadataByPath };
4737
+ }
4738
+ function resolveConstraint(absolutePath, relativePath, inputConstraints) {
4739
+ if (!inputConstraints) {
4740
+ return null;
4741
+ }
4742
+ const normalizedRelative = relativePath.replace(/^\//, "");
4743
+ const candidates = [
4744
+ absolutePath,
4745
+ relativePath,
4746
+ normalizedRelative,
4747
+ absolutePath.replace(/^rig\/[^/]+\//, "")
4748
+ ];
4749
+ for (const candidate of candidates) {
4750
+ const constraint = inputConstraints[candidate];
4751
+ if (constraint) {
4752
+ return constraint;
4753
+ }
4754
+ }
4755
+ return null;
4756
+ }
4757
+ function createControl(faceId, lookup, relativePath, inputConstraints, fallback) {
4758
+ const normalizedRelative = normalizeInputPath(relativePath);
4759
+ if (!normalizedRelative || !lookup.pathSet.has(normalizedRelative)) {
4760
+ return null;
4761
+ }
4762
+ const absolutePath = buildRigInputPath(faceId, normalizedRelative);
4763
+ const metadata = lookup.metadataByPath.get(normalizedRelative);
4764
+ const constraint = resolveConstraint(
4765
+ absolutePath,
4766
+ normalizedRelative,
4767
+ inputConstraints
4768
+ );
4769
+ const min = Number.isFinite(Number(constraint?.min ?? metadata?.range?.min)) ? Number(constraint?.min ?? metadata?.range?.min) : fallback.min;
4770
+ const max = Number.isFinite(Number(constraint?.max ?? metadata?.range?.max)) ? Number(constraint?.max ?? metadata?.range?.max) : fallback.max;
4771
+ const midpoint = min + (max - min) / 2;
4772
+ const defaultValue = Number.isFinite(
4773
+ Number(constraint?.defaultValue ?? metadata?.defaultValue)
4774
+ ) ? Number(constraint?.defaultValue ?? metadata?.defaultValue) : Number.isFinite(midpoint) ? midpoint : fallback.defaultValue;
4775
+ return {
4776
+ path: absolutePath,
4777
+ min,
4778
+ max,
4779
+ defaultValue
4780
+ };
4781
+ }
4782
+ function hasAll(controls, keys) {
4783
+ return keys.every((key) => Boolean(controls[key]));
4784
+ }
4785
+ function resolveFaceControls(assetBundle, runtimeFaceId, inputConstraints) {
4786
+ const faceId = assetBundle.faceId ?? assetBundle.pose?.config?.faceId ?? runtimeFaceId ?? "face";
4787
+ const lookup = buildControlsLookup(assetBundle);
4788
+ const standardVizijControls = {
4789
+ leftX: createControl(
4790
+ faceId,
4791
+ lookup,
4792
+ STANDARD_VIZIJ_EYE_PATHS.leftX,
4793
+ inputConstraints,
4794
+ DEFAULT_GAZE_CONTROL
4795
+ ),
4796
+ leftY: createControl(
4797
+ faceId,
4798
+ lookup,
4799
+ STANDARD_VIZIJ_EYE_PATHS.leftY,
4800
+ inputConstraints,
4801
+ DEFAULT_GAZE_CONTROL
4802
+ ),
4803
+ rightX: createControl(
4804
+ faceId,
4805
+ lookup,
4806
+ STANDARD_VIZIJ_EYE_PATHS.rightX,
4807
+ inputConstraints,
4808
+ DEFAULT_GAZE_CONTROL
4809
+ ),
4810
+ rightY: createControl(
4811
+ faceId,
4812
+ lookup,
4813
+ STANDARD_VIZIJ_EYE_PATHS.rightY,
4814
+ inputConstraints,
4815
+ DEFAULT_GAZE_CONTROL
4816
+ ),
4817
+ leftUpper: createControl(
4818
+ faceId,
4819
+ lookup,
4820
+ STANDARD_VIZIJ_EYE_PATHS.leftUpper,
4821
+ inputConstraints,
4822
+ DEFAULT_GAZE_CONTROL
4823
+ ),
4824
+ rightUpper: createControl(
4825
+ faceId,
4826
+ lookup,
4827
+ STANDARD_VIZIJ_EYE_PATHS.rightUpper,
4828
+ inputConstraints,
4829
+ DEFAULT_GAZE_CONTROL
4830
+ )
4831
+ };
4832
+ const legacyStandardControls = {
4833
+ leftX: createControl(
4834
+ faceId,
4835
+ lookup,
4836
+ LEGACY_STANDARD_EYE_PATHS.leftX,
4837
+ inputConstraints,
4838
+ DEFAULT_GAZE_CONTROL
4839
+ ),
4840
+ leftY: createControl(
4841
+ faceId,
4842
+ lookup,
4843
+ LEGACY_STANDARD_EYE_PATHS.leftY,
4844
+ inputConstraints,
4845
+ DEFAULT_GAZE_CONTROL
4846
+ ),
4847
+ rightX: createControl(
4848
+ faceId,
4849
+ lookup,
4850
+ LEGACY_STANDARD_EYE_PATHS.rightX,
4851
+ inputConstraints,
4852
+ DEFAULT_GAZE_CONTROL
4853
+ ),
4854
+ rightY: createControl(
4855
+ faceId,
4856
+ lookup,
4857
+ LEGACY_STANDARD_EYE_PATHS.rightY,
4858
+ inputConstraints,
4859
+ DEFAULT_GAZE_CONTROL
4860
+ ),
4861
+ leftUpper: createControl(
4862
+ faceId,
4863
+ lookup,
4864
+ LEGACY_STANDARD_EYE_PATHS.leftUpper,
4865
+ inputConstraints,
4866
+ DEFAULT_GAZE_CONTROL
4867
+ ),
4868
+ rightUpper: createControl(
4869
+ faceId,
4870
+ lookup,
4871
+ LEGACY_STANDARD_EYE_PATHS.rightUpper,
4872
+ inputConstraints,
4873
+ DEFAULT_GAZE_CONTROL
4874
+ )
4875
+ };
4876
+ const propsrigControls = {
4877
+ leftX: createControl(
4878
+ faceId,
4879
+ lookup,
4880
+ PROPSRIG_EYE_PATHS.leftX,
4881
+ inputConstraints,
4882
+ DEFAULT_GAZE_CONTROL
4883
+ ),
4884
+ leftY: createControl(
4885
+ faceId,
4886
+ lookup,
4887
+ PROPSRIG_EYE_PATHS.leftY,
4888
+ inputConstraints,
4889
+ DEFAULT_GAZE_CONTROL
4890
+ ),
4891
+ rightX: createControl(
4892
+ faceId,
4893
+ lookup,
4894
+ PROPSRIG_EYE_PATHS.rightX,
4895
+ inputConstraints,
4896
+ DEFAULT_GAZE_CONTROL
4897
+ ),
4898
+ rightY: createControl(
4899
+ faceId,
4900
+ lookup,
4901
+ PROPSRIG_EYE_PATHS.rightY,
4902
+ inputConstraints,
4903
+ DEFAULT_GAZE_CONTROL
4904
+ )
4905
+ };
4906
+ const coupledGazeControls = {
4907
+ leftX: createControl(
4908
+ faceId,
4909
+ lookup,
4910
+ "/gaze/left_right",
4911
+ inputConstraints,
4912
+ DEFAULT_GAZE_CONTROL
4913
+ ),
4914
+ leftY: createControl(
4915
+ faceId,
4916
+ lookup,
4917
+ "/gaze/up_down",
4918
+ inputConstraints,
4919
+ DEFAULT_GAZE_CONTROL
4920
+ ),
4921
+ rightX: createControl(
4922
+ faceId,
4923
+ lookup,
4924
+ "/gaze/left_right_copy",
4925
+ inputConstraints,
4926
+ DEFAULT_GAZE_CONTROL
4927
+ ) ?? createControl(
4928
+ faceId,
4929
+ lookup,
4930
+ "/gaze/left_right",
4931
+ inputConstraints,
4932
+ DEFAULT_GAZE_CONTROL
4933
+ ),
4934
+ rightY: createControl(
4935
+ faceId,
4936
+ lookup,
4937
+ "/gaze/up_down_copy",
4938
+ inputConstraints,
4939
+ DEFAULT_GAZE_CONTROL
4940
+ ) ?? createControl(
4941
+ faceId,
4942
+ lookup,
4943
+ "/gaze/up_down",
4944
+ inputConstraints,
4945
+ DEFAULT_GAZE_CONTROL
4946
+ )
4947
+ };
4948
+ const blinkFromLids = createControl(
4949
+ faceId,
4950
+ lookup,
4951
+ "/lids/blink",
4952
+ inputConstraints,
4953
+ DEFAULT_BLINK_CONTROL
4954
+ );
4955
+ const blinkDirect = createControl(
4956
+ faceId,
4957
+ lookup,
4958
+ "/blink",
4959
+ inputConstraints,
4960
+ DEFAULT_BLINK_CONTROL
4961
+ );
4962
+ if (hasAll(standardVizijControls, ["leftX", "leftY", "rightX", "rightY"])) {
4963
+ return {
4964
+ faceId,
4965
+ gazeSource: "standard-vizij",
4966
+ blinkSource: blinkFromLids ? "lids" : blinkDirect ? "blink" : "none",
4967
+ eyes: {
4968
+ leftX: standardVizijControls.leftX,
4969
+ leftY: standardVizijControls.leftY,
4970
+ rightX: standardVizijControls.rightX,
4971
+ rightY: standardVizijControls.rightY
4972
+ },
4973
+ eyelids: {
4974
+ leftUpper: standardVizijControls.leftUpper,
4975
+ rightUpper: standardVizijControls.rightUpper
4976
+ },
4977
+ blink: blinkFromLids ?? blinkDirect
4978
+ };
4979
+ }
4980
+ if (hasAll(legacyStandardControls, ["leftX", "leftY", "rightX", "rightY"])) {
4981
+ return {
4982
+ faceId,
4983
+ gazeSource: "standard",
4984
+ blinkSource: blinkDirect ? "blink" : blinkFromLids ? "lids" : "none",
4985
+ eyes: {
4986
+ leftX: legacyStandardControls.leftX,
4987
+ leftY: legacyStandardControls.leftY,
4988
+ rightX: legacyStandardControls.rightX,
4989
+ rightY: legacyStandardControls.rightY
4990
+ },
4991
+ eyelids: {
4992
+ leftUpper: legacyStandardControls.leftUpper,
4993
+ rightUpper: legacyStandardControls.rightUpper
4994
+ },
4995
+ blink: blinkDirect ?? blinkFromLids
4996
+ };
4997
+ }
4998
+ if (hasAll(propsrigControls, ["leftX", "leftY", "rightX", "rightY"])) {
4999
+ return {
5000
+ faceId,
5001
+ gazeSource: "propsrig",
5002
+ blinkSource: blinkDirect ? "blink" : blinkFromLids ? "lids" : "none",
5003
+ eyes: {
5004
+ leftX: propsrigControls.leftX,
5005
+ leftY: propsrigControls.leftY,
5006
+ rightX: propsrigControls.rightX,
5007
+ rightY: propsrigControls.rightY
5008
+ },
5009
+ eyelids: {
5010
+ leftUpper: null,
5011
+ rightUpper: null
5012
+ },
5013
+ blink: blinkDirect ?? blinkFromLids
5014
+ };
5015
+ }
5016
+ if (hasAll(coupledGazeControls, ["leftX", "leftY", "rightX", "rightY"])) {
5017
+ return {
5018
+ faceId,
5019
+ gazeSource: "coupled-gaze",
5020
+ blinkSource: blinkFromLids ? "lids" : blinkDirect ? "blink" : "none",
5021
+ eyes: {
5022
+ leftX: coupledGazeControls.leftX,
5023
+ leftY: coupledGazeControls.leftY,
5024
+ rightX: coupledGazeControls.rightX,
5025
+ rightY: coupledGazeControls.rightY
5026
+ },
5027
+ eyelids: {
5028
+ leftUpper: null,
5029
+ rightUpper: null
5030
+ },
5031
+ blink: blinkFromLids ?? blinkDirect
5032
+ };
5033
+ }
5034
+ return {
5035
+ faceId,
5036
+ gazeSource: "none",
5037
+ blinkSource: blinkFromLids ? "lids" : blinkDirect ? "blink" : "none",
5038
+ eyes: {
5039
+ leftX: null,
5040
+ leftY: null,
5041
+ rightX: null,
5042
+ rightY: null
5043
+ },
5044
+ eyelids: {
5045
+ leftUpper: null,
5046
+ rightUpper: null
5047
+ },
5048
+ blink: blinkFromLids ?? blinkDirect
5049
+ };
5050
+ }
5051
+ function mapNormalizedControlValue(control, normalizedValue) {
5052
+ const value = clamp(normalizedValue, -1, 1);
5053
+ if (value === 0) {
5054
+ return control.defaultValue;
5055
+ }
5056
+ if (value > 0) {
5057
+ return control.defaultValue + (control.max - control.defaultValue) * value;
5058
+ }
5059
+ return control.defaultValue + (control.defaultValue - control.min) * value;
5060
+ }
5061
+ function mapUnitControlValue(control, unitValue) {
5062
+ const value = clamp(unitValue, 0, 1);
5063
+ return control.defaultValue + (control.max - control.defaultValue) * value;
5064
+ }
1821
5065
  export {
5066
+ EMOTION_POSE_KEYS,
5067
+ EXPRESSIVE_EMOTION_POSE_KEYS,
5068
+ POSE_WEIGHT_INPUT_PATH_PREFIX,
5069
+ VISEME_POSE_KEYS,
1822
5070
  VizijRuntimeFace,
1823
5071
  VizijRuntimeProvider,
5072
+ buildPoseWeightInputPathSegment,
5073
+ buildPoseWeightPathMap,
5074
+ buildPoseWeightRelativePath,
5075
+ buildRigInputPath,
5076
+ buildSemanticPoseWeightPathMap,
5077
+ filterPosesBySemanticKind,
5078
+ getPoseSemanticKey,
5079
+ mapNormalizedControlValue,
5080
+ mapUnitControlValue,
5081
+ normalizePoseSemanticKey,
5082
+ resolveFaceControls,
5083
+ resolvePoseMembership,
5084
+ resolvePoseSemantics,
5085
+ resolveRuntimeUpdatePlan,
5086
+ useOptionalVizijRuntime,
1824
5087
  useRigInput,
1825
5088
  useVizijOutputs,
1826
5089
  useVizijRuntime