@vizij/runtime-react 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1054 @@
1
+ // src/VizijRuntimeProvider.tsx
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState
8
+ } from "react";
9
+ import { VizijContext, createVizijStore } from "@vizij/render";
10
+ import {
11
+ OrchestratorProvider,
12
+ useOrchestrator,
13
+ useOrchFrame
14
+ } from "@vizij/orchestrator-react";
15
+ import { valueAsNumber as valueAsNumber2 } from "@vizij/value-json";
16
+
17
+ // src/context.ts
18
+ import { createContext } from "react";
19
+ var VizijRuntimeContext = createContext(null);
20
+
21
+ // src/utils/graph.ts
22
+ function getNodes(spec) {
23
+ if (!spec || typeof spec !== "object") {
24
+ return [];
25
+ }
26
+ const maybeNodes = spec.nodes;
27
+ if (!Array.isArray(maybeNodes)) {
28
+ return [];
29
+ }
30
+ return maybeNodes;
31
+ }
32
+ function collectOutputPaths(spec) {
33
+ const nodes = getNodes(spec);
34
+ const outputs = /* @__PURE__ */ new Set();
35
+ nodes.forEach((node) => {
36
+ if (typeof node !== "object" || !node) {
37
+ return;
38
+ }
39
+ if (String(node.type ?? "").toLowerCase() !== "output") {
40
+ return;
41
+ }
42
+ const path = node.params?.path;
43
+ if (typeof path === "string" && path.trim()) {
44
+ outputs.add(path.trim());
45
+ }
46
+ });
47
+ return Array.from(outputs);
48
+ }
49
+ function collectInputPaths(spec) {
50
+ const nodes = getNodes(spec);
51
+ const inputs = /* @__PURE__ */ new Set();
52
+ nodes.forEach((node) => {
53
+ if (String(node.type ?? "").toLowerCase() !== "input") {
54
+ return;
55
+ }
56
+ const path = node.params?.path;
57
+ if (typeof path === "string" && path.trim()) {
58
+ inputs.add(path.trim());
59
+ }
60
+ });
61
+ return Array.from(inputs);
62
+ }
63
+ function collectInputPathMap(spec) {
64
+ const map = {};
65
+ const nodes = getNodes(spec);
66
+ nodes.forEach((node) => {
67
+ if (String(node.type ?? "").toLowerCase() !== "input") {
68
+ return;
69
+ }
70
+ const path = node.params?.path;
71
+ if (typeof path !== "string" || !path.trim()) {
72
+ return;
73
+ }
74
+ const id = String(node.id ?? "");
75
+ const key = id.startsWith("input_") ? id.slice("input_".length) : id || path.trim();
76
+ map[key] = path.trim();
77
+ });
78
+ return map;
79
+ }
80
+
81
+ // src/utils/valueConversion.ts
82
+ import {
83
+ isNormalizedValue,
84
+ valueAsBool,
85
+ valueAsColorRgba,
86
+ valueAsNumber,
87
+ valueAsText,
88
+ valueAsTransform,
89
+ valueAsVector
90
+ } from "@vizij/value-json";
91
+ function numericArrayToRaw(arr) {
92
+ const normalised = arr.map((entry) => Number(entry ?? 0));
93
+ switch (normalised.length) {
94
+ case 2:
95
+ return {
96
+ x: normalised[0],
97
+ y: normalised[1],
98
+ r: normalised[0],
99
+ g: normalised[1]
100
+ };
101
+ case 3:
102
+ return {
103
+ x: normalised[0],
104
+ y: normalised[1],
105
+ z: normalised[2],
106
+ r: normalised[0],
107
+ g: normalised[1],
108
+ b: normalised[2]
109
+ };
110
+ case 4:
111
+ return {
112
+ x: normalised[0],
113
+ y: normalised[1],
114
+ z: normalised[2],
115
+ w: normalised[3],
116
+ r: normalised[0],
117
+ g: normalised[1],
118
+ b: normalised[2],
119
+ a: normalised[3]
120
+ };
121
+ default:
122
+ return normalised;
123
+ }
124
+ }
125
+ function valueJSONToRaw(value) {
126
+ if (value == null) {
127
+ return void 0;
128
+ }
129
+ if (typeof value === "number" || typeof value === "string" || typeof value === "boolean") {
130
+ return value;
131
+ }
132
+ if (Array.isArray(value)) {
133
+ return value.map((entry) => valueJSONToRaw(entry));
134
+ }
135
+ if (typeof value === "object" && !("type" in value)) {
136
+ const entries = Object.entries(value).map(([key, entry]) => [
137
+ key,
138
+ valueJSONToRaw(entry)
139
+ ]);
140
+ return Object.fromEntries(entries);
141
+ }
142
+ if (!isNormalizedValue(value)) {
143
+ return void 0;
144
+ }
145
+ switch (value.type) {
146
+ case "float": {
147
+ const num = valueAsNumber(value);
148
+ return typeof num === "number" ? num : void 0;
149
+ }
150
+ case "bool": {
151
+ const boolVal = valueAsBool(value);
152
+ return typeof boolVal === "boolean" ? boolVal : void 0;
153
+ }
154
+ case "text": {
155
+ const text = valueAsText(value);
156
+ return typeof text === "string" ? text : void 0;
157
+ }
158
+ case "vec2":
159
+ case "vec3":
160
+ case "vec4":
161
+ case "quat":
162
+ case "vector": {
163
+ const vec = valueAsVector(value);
164
+ return vec ? numericArrayToRaw(vec) : void 0;
165
+ }
166
+ case "colorrgba": {
167
+ const color = valueAsColorRgba(value);
168
+ if (!color) {
169
+ return void 0;
170
+ }
171
+ const [r = 0, g = 0, b = 0, a = 1] = color;
172
+ return { r, g, b, a };
173
+ }
174
+ case "transform": {
175
+ const transform = valueAsTransform(value);
176
+ if (!transform) {
177
+ return void 0;
178
+ }
179
+ return {
180
+ translation: numericArrayToRaw(transform.translation),
181
+ rotation: numericArrayToRaw(transform.rotation),
182
+ scale: numericArrayToRaw(transform.scale)
183
+ };
184
+ }
185
+ case "record": {
186
+ const entries = Object.entries(value.data ?? {}).map(([key, entry]) => [
187
+ key,
188
+ valueJSONToRaw(entry)
189
+ ]);
190
+ return Object.fromEntries(entries);
191
+ }
192
+ case "enum": {
193
+ const [tag, inner] = value.data;
194
+ return {
195
+ tag,
196
+ value: valueJSONToRaw(inner)
197
+ };
198
+ }
199
+ default:
200
+ return void 0;
201
+ }
202
+ }
203
+
204
+ // src/VizijRuntimeProvider.tsx
205
+ import { loadGLTF, loadGLTFFromBlob } from "@vizij/render";
206
+ import { jsx } from "react/jsx-runtime";
207
+ var DEFAULT_MERGE = {
208
+ outputs: "add",
209
+ intermediate: "add"
210
+ };
211
+ var DEFAULT_DURATION = 0.35;
212
+ var EASINGS = {
213
+ linear: (t) => t,
214
+ easeIn: (t) => t * t,
215
+ easeOut: (t) => 1 - (1 - t) * (1 - t),
216
+ easeInOut: (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2
217
+ };
218
+ function resolveEasing(easing) {
219
+ if (typeof easing === "function") {
220
+ return easing;
221
+ }
222
+ if (typeof easing === "string" && easing in EASINGS) {
223
+ return EASINGS[easing];
224
+ }
225
+ return EASINGS.linear;
226
+ }
227
+ function findRootId(world) {
228
+ for (const entry of Object.values(world)) {
229
+ if (entry && typeof entry === "object" && entry.type === "group" && entry.rootBounds && entry.id) {
230
+ return entry.id;
231
+ }
232
+ }
233
+ return null;
234
+ }
235
+ function normalisePath(path) {
236
+ if (!path) {
237
+ return path;
238
+ }
239
+ return path.startsWith("debug/") ? path.slice("debug/".length) : path;
240
+ }
241
+ function VizijRuntimeProvider({
242
+ assetBundle,
243
+ children,
244
+ namespace: namespaceProp,
245
+ faceId: faceIdProp,
246
+ autoCreate = true,
247
+ createOptions,
248
+ autostart = false,
249
+ mergeStrategy,
250
+ onRegisterControllers,
251
+ onStatusChange
252
+ }) {
253
+ const storeRef = useRef();
254
+ if (!storeRef.current) {
255
+ storeRef.current = createVizijStore();
256
+ }
257
+ return /* @__PURE__ */ jsx(VizijContext.Provider, { value: storeRef.current, children: /* @__PURE__ */ jsx(
258
+ OrchestratorProvider,
259
+ {
260
+ autoCreate,
261
+ createOptions,
262
+ autostart,
263
+ children: /* @__PURE__ */ jsx(
264
+ VizijRuntimeProviderInner,
265
+ {
266
+ assetBundle,
267
+ namespace: namespaceProp,
268
+ faceId: faceIdProp,
269
+ autoCreate,
270
+ mergeStrategy,
271
+ onRegisterControllers,
272
+ onStatusChange,
273
+ store: storeRef.current,
274
+ children
275
+ }
276
+ )
277
+ }
278
+ ) });
279
+ }
280
+ function VizijRuntimeProviderInner({
281
+ assetBundle,
282
+ namespace: namespaceProp,
283
+ faceId: faceIdProp,
284
+ mergeStrategy,
285
+ onRegisterControllers,
286
+ onStatusChange,
287
+ store,
288
+ children,
289
+ autoCreate
290
+ }) {
291
+ const {
292
+ ready,
293
+ createOrchestrator,
294
+ registerGraph,
295
+ registerMergedGraph,
296
+ registerAnimation,
297
+ removeGraph,
298
+ removeAnimation,
299
+ listControllers,
300
+ setInput,
301
+ getPathSnapshot,
302
+ step: stepRuntime
303
+ } = useOrchestrator();
304
+ const frame = useOrchFrame();
305
+ const namespace = namespaceProp ?? assetBundle.namespace ?? "default";
306
+ const faceId = faceIdProp ?? assetBundle.faceId ?? assetBundle.pose?.config?.faceId ?? assetBundle.pose?.config?.faceId ?? void 0;
307
+ const [status, setStatus] = useState({
308
+ loading: true,
309
+ ready: false,
310
+ error: null,
311
+ errors: [],
312
+ namespace,
313
+ faceId,
314
+ rootId: null,
315
+ outputPaths: [],
316
+ controllers: { graphs: [], anims: [] }
317
+ });
318
+ const errorsRef = useRef([]);
319
+ const outputPathsRef = useRef(/* @__PURE__ */ new Set());
320
+ const rigInputMapRef = useRef({});
321
+ const registeredGraphsRef = useRef([]);
322
+ const registeredAnimationsRef = useRef([]);
323
+ const mergedGraphRef = useRef(null);
324
+ const animationTweensRef = useRef(/* @__PURE__ */ new Map());
325
+ const clipPlaybackRef = useRef(/* @__PURE__ */ new Map());
326
+ const rafHandleRef = useRef(null);
327
+ const lastFrameTimeRef = useRef(null);
328
+ const reportStatus = useCallback(
329
+ (updater) => {
330
+ setStatus((prev) => {
331
+ const next = updater(prev);
332
+ onStatusChange?.(next);
333
+ return next;
334
+ });
335
+ },
336
+ [onStatusChange]
337
+ );
338
+ const pushError = useCallback(
339
+ (error) => {
340
+ errorsRef.current = [...errorsRef.current, error];
341
+ reportStatus((prev) => ({
342
+ ...prev,
343
+ error,
344
+ errors: errorsRef.current
345
+ }));
346
+ console.warn("[vizij-runtime]", error.message, error.cause);
347
+ },
348
+ [reportStatus]
349
+ );
350
+ const resetErrors = useCallback(() => {
351
+ errorsRef.current = [];
352
+ reportStatus((prev) => ({
353
+ ...prev,
354
+ error: null,
355
+ errors: []
356
+ }));
357
+ }, [reportStatus]);
358
+ const clearControllers = useCallback(() => {
359
+ const existing = listControllers();
360
+ existing.graphs.forEach((id) => {
361
+ try {
362
+ removeGraph(id);
363
+ } catch (err) {
364
+ pushError({
365
+ message: `Failed to remove graph ${id}`,
366
+ cause: err,
367
+ phase: "registration",
368
+ timestamp: performance.now()
369
+ });
370
+ }
371
+ });
372
+ existing.anims.forEach((id) => {
373
+ try {
374
+ removeAnimation(id);
375
+ } catch (err) {
376
+ pushError({
377
+ message: `Failed to remove animation ${id}`,
378
+ cause: err,
379
+ phase: "registration",
380
+ timestamp: performance.now()
381
+ });
382
+ }
383
+ });
384
+ registeredGraphsRef.current = [];
385
+ registeredAnimationsRef.current = [];
386
+ mergedGraphRef.current = null;
387
+ }, [listControllers, removeAnimation, removeGraph, pushError]);
388
+ useEffect(() => {
389
+ reportStatus((prev) => ({
390
+ ...prev,
391
+ namespace,
392
+ faceId
393
+ }));
394
+ }, [namespace, faceId, reportStatus]);
395
+ useEffect(() => {
396
+ let cancelled = false;
397
+ resetErrors();
398
+ reportStatus((prev) => ({
399
+ ...prev,
400
+ loading: true,
401
+ rootId: null,
402
+ ready: false,
403
+ outputPaths: [],
404
+ controllers: { graphs: [], anims: [] }
405
+ }));
406
+ const loadAssets = async () => {
407
+ try {
408
+ let world;
409
+ let animatables;
410
+ if (assetBundle.glb.kind === "url") {
411
+ [world, animatables] = await loadGLTF(
412
+ assetBundle.glb.src,
413
+ [namespace],
414
+ assetBundle.glb.aggressiveImport ?? false,
415
+ assetBundle.glb.rootBounds
416
+ );
417
+ } else if (assetBundle.glb.kind === "blob") {
418
+ [world, animatables] = await loadGLTFFromBlob(
419
+ assetBundle.glb.blob,
420
+ [namespace],
421
+ assetBundle.glb.aggressiveImport ?? false,
422
+ assetBundle.glb.rootBounds
423
+ );
424
+ } else {
425
+ world = assetBundle.glb.world;
426
+ animatables = assetBundle.glb.animatables;
427
+ }
428
+ if (cancelled) {
429
+ return;
430
+ }
431
+ const rootId = findRootId(world);
432
+ store.getState().addWorldElements(world, animatables, true);
433
+ reportStatus((prev) => ({
434
+ ...prev,
435
+ loading: false,
436
+ rootId,
437
+ namespace,
438
+ faceId
439
+ }));
440
+ } catch (err) {
441
+ if (cancelled) {
442
+ return;
443
+ }
444
+ pushError({
445
+ message: "Failed to load Vizij assets",
446
+ cause: err,
447
+ phase: "assets",
448
+ timestamp: performance.now()
449
+ });
450
+ reportStatus((prev) => ({
451
+ ...prev,
452
+ loading: false
453
+ }));
454
+ }
455
+ };
456
+ loadAssets();
457
+ return () => {
458
+ cancelled = true;
459
+ };
460
+ }, [
461
+ assetBundle,
462
+ namespace,
463
+ faceId,
464
+ store,
465
+ pushError,
466
+ reportStatus,
467
+ resetErrors
468
+ ]);
469
+ useEffect(() => {
470
+ if (!ready && autoCreate) {
471
+ createOrchestrator().catch((err) => {
472
+ pushError({
473
+ message: "Failed to create orchestrator runtime",
474
+ cause: err,
475
+ phase: "orchestrator",
476
+ timestamp: performance.now()
477
+ });
478
+ });
479
+ }
480
+ }, [ready, autoCreate, createOrchestrator, pushError]);
481
+ const registerControllers = useCallback(async () => {
482
+ const normalize = (spec) => spec;
483
+ const rigSpec = normalize(assetBundle.rig.spec);
484
+ const rigOutputs = collectOutputPaths(rigSpec);
485
+ const rigInputs = collectInputPaths(rigSpec);
486
+ rigInputMapRef.current = collectInputPathMap(rigSpec);
487
+ outputPathsRef.current = new Set(rigOutputs);
488
+ const rigConfig = {
489
+ id: assetBundle.rig.id,
490
+ spec: rigSpec,
491
+ subs: assetBundle.rig.subscriptions ?? {
492
+ inputs: rigInputs,
493
+ outputs: rigOutputs
494
+ }
495
+ };
496
+ const graphConfigs = [rigConfig];
497
+ if (assetBundle.pose?.graph) {
498
+ const poseSpec = normalize(assetBundle.pose.graph.spec);
499
+ const poseOutputs = collectOutputPaths(poseSpec);
500
+ const poseInputs = collectInputPaths(poseSpec);
501
+ graphConfigs.push({
502
+ id: assetBundle.pose.graph.id,
503
+ spec: poseSpec,
504
+ subs: assetBundle.pose.graph.subscriptions ?? {
505
+ inputs: poseInputs,
506
+ outputs: poseOutputs
507
+ }
508
+ });
509
+ }
510
+ clearControllers();
511
+ const graphIds = [];
512
+ try {
513
+ if (graphConfigs.length > 1) {
514
+ const mergedId = registerMergedGraph({
515
+ graphs: graphConfigs,
516
+ strategy: mergeStrategy ?? DEFAULT_MERGE
517
+ });
518
+ mergedGraphRef.current = mergedId;
519
+ graphIds.push(mergedId);
520
+ } else {
521
+ graphConfigs.forEach((cfg) => {
522
+ const id = registerGraph(cfg);
523
+ graphIds.push(id);
524
+ });
525
+ }
526
+ } catch (err) {
527
+ pushError({
528
+ message: "Failed to register rig graphs",
529
+ cause: err,
530
+ phase: "registration",
531
+ timestamp: performance.now()
532
+ });
533
+ }
534
+ registeredGraphsRef.current = graphIds;
535
+ const animationIds = [];
536
+ for (const anim of assetBundle.animations ?? []) {
537
+ try {
538
+ const config = {
539
+ id: anim.id,
540
+ setup: {
541
+ animation: anim.clip,
542
+ ...anim.setup ?? {}
543
+ }
544
+ };
545
+ const id = registerAnimation(config);
546
+ animationIds.push(id);
547
+ } catch (err) {
548
+ pushError({
549
+ message: `Failed to register animation ${anim.id}`,
550
+ cause: err,
551
+ phase: "animation",
552
+ timestamp: performance.now()
553
+ });
554
+ }
555
+ }
556
+ registeredAnimationsRef.current = animationIds;
557
+ if (assetBundle.initialInputs) {
558
+ Object.entries(assetBundle.initialInputs).forEach(([path, value]) => {
559
+ try {
560
+ setInput(path, value);
561
+ } catch (err) {
562
+ pushError({
563
+ message: `Failed to stage initial input ${path}`,
564
+ cause: err,
565
+ phase: "registration",
566
+ timestamp: performance.now()
567
+ });
568
+ }
569
+ });
570
+ }
571
+ const controllers = listControllers();
572
+ reportStatus((prev) => ({
573
+ ...prev,
574
+ ready: true,
575
+ controllers,
576
+ outputPaths: Array.from(outputPathsRef.current)
577
+ }));
578
+ onRegisterControllers?.(controllers);
579
+ }, [
580
+ assetBundle,
581
+ clearControllers,
582
+ listControllers,
583
+ mergeStrategy,
584
+ onRegisterControllers,
585
+ pushError,
586
+ registerAnimation,
587
+ registerGraph,
588
+ registerMergedGraph,
589
+ reportStatus,
590
+ setInput
591
+ ]);
592
+ useEffect(() => {
593
+ if (!ready || status.loading) {
594
+ return;
595
+ }
596
+ registerControllers().catch((err) => {
597
+ pushError({
598
+ message: "Failed to register controllers",
599
+ cause: err,
600
+ phase: "registration",
601
+ timestamp: performance.now()
602
+ });
603
+ });
604
+ }, [ready, status.loading, registerControllers, pushError]);
605
+ useEffect(() => {
606
+ if (!frame) {
607
+ return;
608
+ }
609
+ const writes = frame.merged_writes ?? [];
610
+ if (!writes.length) {
611
+ return;
612
+ }
613
+ const setWorldValue = store.getState().setValue;
614
+ const namespaceValue = status.namespace;
615
+ writes.forEach((write) => {
616
+ const path = normalisePath(write.path);
617
+ if (!outputPathsRef.current.has(path)) {
618
+ return;
619
+ }
620
+ const raw = valueJSONToRaw(write.value);
621
+ if (raw === void 0) {
622
+ return;
623
+ }
624
+ setWorldValue(path, namespaceValue, raw);
625
+ });
626
+ }, [frame, status.namespace, store]);
627
+ const stagePoseNeutral = useCallback(
628
+ (force = false) => {
629
+ const neutral = assetBundle.pose?.config?.neutralInputs ?? {};
630
+ const rigMap = rigInputMapRef.current;
631
+ const staged = /* @__PURE__ */ new Set();
632
+ Object.entries(neutral).forEach(([id, value]) => {
633
+ const path = rigMap[id];
634
+ if (!path) {
635
+ return;
636
+ }
637
+ const include = assetBundle.pose?.stageNeutralFilter;
638
+ if (include && !include(id, path)) {
639
+ return;
640
+ }
641
+ setInput(path, { float: Number.isFinite(value) ? value : 0 });
642
+ staged.add(path);
643
+ });
644
+ if (force) {
645
+ Object.entries(rigMap).forEach(([id, path]) => {
646
+ if (staged.has(path)) {
647
+ return;
648
+ }
649
+ const include = assetBundle.pose?.stageNeutralFilter;
650
+ if (include && !include(id, path)) {
651
+ return;
652
+ }
653
+ setInput(path, { float: 0 });
654
+ });
655
+ }
656
+ },
657
+ [assetBundle.pose?.config?.neutralInputs, setInput]
658
+ );
659
+ const setRendererValue = useCallback(
660
+ (id, ns, value) => {
661
+ store.getState().setValue(id, ns, value);
662
+ },
663
+ [store]
664
+ );
665
+ const cancelAnimation = useCallback((path) => {
666
+ if (animationTweensRef.current.has(path)) {
667
+ const entry = animationTweensRef.current.get(path);
668
+ animationTweensRef.current.delete(path);
669
+ entry?.resolve();
670
+ }
671
+ }, []);
672
+ const scheduleLoop = useCallback(() => {
673
+ if (rafHandleRef.current !== null) {
674
+ return;
675
+ }
676
+ const tick = (timestamp) => {
677
+ if (lastFrameTimeRef.current == null) {
678
+ lastFrameTimeRef.current = timestamp;
679
+ }
680
+ const dt = Math.max(0, (timestamp - lastFrameTimeRef.current) / 1e3);
681
+ lastFrameTimeRef.current = timestamp;
682
+ advanceAnimationTweens(dt);
683
+ advanceClipPlayback(dt);
684
+ if (animationTweensRef.current.size > 0 || clipPlaybackRef.current.size > 0) {
685
+ rafHandleRef.current = requestAnimationFrame(tick);
686
+ } else {
687
+ rafHandleRef.current = null;
688
+ lastFrameTimeRef.current = null;
689
+ }
690
+ };
691
+ rafHandleRef.current = requestAnimationFrame(tick);
692
+ }, []);
693
+ const advanceAnimationTweens = useCallback(
694
+ (dt) => {
695
+ if (animationTweensRef.current.size === 0) {
696
+ return;
697
+ }
698
+ const map = animationTweensRef.current;
699
+ const toDelete = [];
700
+ map.forEach((state, key) => {
701
+ state.elapsed += dt;
702
+ const progress = state.duration === 0 ? 1 : Math.min(state.elapsed / state.duration, 1);
703
+ const eased = state.easing(progress);
704
+ const value = state.from + (state.to - state.from) * eased;
705
+ setInput(state.path, { float: value });
706
+ if (progress >= 1) {
707
+ toDelete.push(key);
708
+ state.resolve();
709
+ }
710
+ });
711
+ toDelete.forEach((key) => map.delete(key));
712
+ },
713
+ [setInput]
714
+ );
715
+ const sampleTrack = useCallback(
716
+ (track, time) => {
717
+ const keyframes = Array.isArray(track.keyframes) ? track.keyframes : [];
718
+ if (!keyframes.length) {
719
+ return 0;
720
+ }
721
+ const first = keyframes[0];
722
+ if (!first) {
723
+ return 0;
724
+ }
725
+ if (time <= Number(first.time ?? 0)) {
726
+ return Number(first.value ?? 0);
727
+ }
728
+ for (let i = 0; i < keyframes.length - 1; i += 1) {
729
+ const current = keyframes[i] ?? {};
730
+ const next = keyframes[i + 1] ?? {};
731
+ const start = Number(current.time ?? 0);
732
+ const end = Number(next.time ?? start);
733
+ if (time >= start && time <= end) {
734
+ const range = end - start || 1;
735
+ const factor = (time - start) / range;
736
+ const currentValue = Number(current.value ?? 0);
737
+ const nextValue = Number(next.value ?? currentValue);
738
+ return currentValue + (nextValue - currentValue) * factor;
739
+ }
740
+ }
741
+ const last = keyframes[keyframes.length - 1];
742
+ return Number(last?.value ?? 0);
743
+ },
744
+ []
745
+ );
746
+ const advanceClipPlayback = useCallback(
747
+ (dt) => {
748
+ if (clipPlaybackRef.current.size === 0) {
749
+ return;
750
+ }
751
+ const map = clipPlaybackRef.current;
752
+ const toDelete = [];
753
+ map.forEach((state, key) => {
754
+ state.time += dt * state.speed;
755
+ const clip = assetBundle.animations?.find(
756
+ (anim) => anim.id === state.id
757
+ );
758
+ if (!clip) {
759
+ toDelete.push(key);
760
+ state.resolve();
761
+ return;
762
+ }
763
+ const clipData = clip.clip;
764
+ const duration = Number(clipData?.duration ?? state.duration);
765
+ state.duration = Number.isFinite(duration) && duration > 0 ? duration : state.duration;
766
+ if (state.time >= state.duration) {
767
+ toDelete.push(key);
768
+ state.time = state.duration;
769
+ }
770
+ const tracks = Array.isArray(clipData?.tracks) ? clipData.tracks : [];
771
+ tracks.forEach((track) => {
772
+ const value = sampleTrack(track, state.time) * state.weight;
773
+ const path = `animation/${clip.id}/${track.channel}`;
774
+ setInput(path, { float: value });
775
+ });
776
+ if (toDelete.includes(key)) {
777
+ state.resolve();
778
+ }
779
+ });
780
+ toDelete.forEach((key) => {
781
+ clipPlaybackRef.current.delete(key);
782
+ const clip = assetBundle.animations?.find((anim) => anim.id === key);
783
+ if (clip) {
784
+ const clipData = clip.clip;
785
+ const tracks = Array.isArray(clipData?.tracks) ? clipData.tracks : [];
786
+ tracks.forEach((track) => {
787
+ const path = `animation/${clip.id}/${track.channel}`;
788
+ setInput(path, { float: 0 });
789
+ });
790
+ }
791
+ });
792
+ },
793
+ [assetBundle.animations, sampleTrack, setInput]
794
+ );
795
+ const animateValue = useCallback(
796
+ (path, target, options) => {
797
+ const easing = resolveEasing(options?.easing);
798
+ const duration = Math.max(0, options?.duration ?? DEFAULT_DURATION);
799
+ cancelAnimation(path);
800
+ const current = getPathSnapshot(path);
801
+ const fromValue = valueAsNumber2(current);
802
+ const toValue = valueAsNumber2(target);
803
+ if (fromValue == null || toValue == null || duration === 0) {
804
+ setInput(path, target);
805
+ return Promise.resolve();
806
+ }
807
+ return new Promise((resolve) => {
808
+ animationTweensRef.current.set(path, {
809
+ path,
810
+ from: fromValue,
811
+ to: toValue,
812
+ duration,
813
+ elapsed: 0,
814
+ easing,
815
+ resolve
816
+ });
817
+ scheduleLoop();
818
+ });
819
+ },
820
+ [cancelAnimation, getPathSnapshot, scheduleLoop, setInput]
821
+ );
822
+ const playAnimation = useCallback(
823
+ (id, options) => {
824
+ const clip = assetBundle.animations?.find((anim) => anim.id === id);
825
+ if (!clip) {
826
+ return Promise.reject(
827
+ new Error(`Animation ${id} is not part of the current asset bundle.`)
828
+ );
829
+ }
830
+ if (clipPlaybackRef.current.has(id)) {
831
+ clipPlaybackRef.current.delete(id);
832
+ }
833
+ return new Promise((resolve) => {
834
+ const speed = options?.speed ?? 1;
835
+ const weight = options?.weight ?? clip.weight ?? 1;
836
+ const clipData = clip.clip;
837
+ clipPlaybackRef.current.set(id, {
838
+ id,
839
+ time: options?.reset ? 0 : 0,
840
+ duration: Number(clipData?.duration ?? 0),
841
+ speed: Number.isFinite(speed) && speed > 0 ? speed : 1,
842
+ weight,
843
+ resolve
844
+ });
845
+ scheduleLoop();
846
+ });
847
+ },
848
+ [assetBundle.animations, scheduleLoop]
849
+ );
850
+ const stopAnimation = useCallback(
851
+ (id) => {
852
+ const clip = assetBundle.animations?.find((anim) => anim.id === id);
853
+ const state = clipPlaybackRef.current.get(id);
854
+ if (state) {
855
+ clipPlaybackRef.current.delete(id);
856
+ state.resolve();
857
+ }
858
+ if (clip) {
859
+ const clipData = clip.clip;
860
+ const tracks = Array.isArray(clipData?.tracks) ? clipData.tracks : [];
861
+ tracks.forEach((track) => {
862
+ const path = `animation/${clip.id}/${track.channel}`;
863
+ setInput(path, { float: 0 });
864
+ });
865
+ }
866
+ },
867
+ [assetBundle.animations, setInput]
868
+ );
869
+ const registerInputDriver = useCallback(
870
+ (id, factory) => {
871
+ const driver = factory({
872
+ setInput,
873
+ setRendererValue,
874
+ namespace,
875
+ faceId
876
+ });
877
+ const wrapped = {
878
+ start: () => {
879
+ try {
880
+ driver.start();
881
+ } catch (err) {
882
+ pushError({
883
+ message: `Input driver ${id} failed to start`,
884
+ cause: err,
885
+ phase: "driver",
886
+ timestamp: performance.now()
887
+ });
888
+ }
889
+ },
890
+ stop: () => {
891
+ try {
892
+ driver.stop();
893
+ } catch (err) {
894
+ pushError({
895
+ message: `Input driver ${id} failed to stop`,
896
+ cause: err,
897
+ phase: "driver",
898
+ timestamp: performance.now()
899
+ });
900
+ }
901
+ },
902
+ dispose: () => {
903
+ try {
904
+ driver.dispose();
905
+ } catch (err) {
906
+ pushError({
907
+ message: `Input driver ${id} failed to dispose`,
908
+ cause: err,
909
+ phase: "driver",
910
+ timestamp: performance.now()
911
+ });
912
+ }
913
+ }
914
+ };
915
+ return wrapped;
916
+ },
917
+ [faceId, namespace, pushError, setInput, setRendererValue]
918
+ );
919
+ const advanceAnimations = useCallback(
920
+ (dt) => {
921
+ advanceAnimationTweens(dt);
922
+ advanceClipPlayback(dt);
923
+ },
924
+ [advanceAnimationTweens, advanceClipPlayback]
925
+ );
926
+ const step = useCallback(
927
+ (dt) => {
928
+ advanceAnimations(dt);
929
+ stepRuntime(dt);
930
+ },
931
+ [advanceAnimations, stepRuntime]
932
+ );
933
+ useEffect(() => {
934
+ return () => {
935
+ if (rafHandleRef.current !== null) {
936
+ cancelAnimationFrame(rafHandleRef.current);
937
+ rafHandleRef.current = null;
938
+ }
939
+ lastFrameTimeRef.current = null;
940
+ animationTweensRef.current.clear();
941
+ clipPlaybackRef.current.clear();
942
+ };
943
+ }, []);
944
+ const contextValue = useMemo(
945
+ () => ({
946
+ ...status,
947
+ assetBundle,
948
+ setInput,
949
+ setValue: setRendererValue,
950
+ stagePoseNeutral,
951
+ animateValue,
952
+ cancelAnimation,
953
+ registerInputDriver,
954
+ playAnimation,
955
+ stopAnimation,
956
+ step,
957
+ advanceAnimations
958
+ }),
959
+ [
960
+ status,
961
+ assetBundle,
962
+ setInput,
963
+ setRendererValue,
964
+ stagePoseNeutral,
965
+ animateValue,
966
+ cancelAnimation,
967
+ registerInputDriver,
968
+ playAnimation,
969
+ stopAnimation,
970
+ step,
971
+ advanceAnimations
972
+ ]
973
+ );
974
+ return /* @__PURE__ */ jsx(VizijRuntimeContext.Provider, { value: contextValue, children });
975
+ }
976
+
977
+ // src/VizijRuntimeFace.tsx
978
+ import { memo } from "react";
979
+ import { Vizij } from "@vizij/render";
980
+
981
+ // src/hooks/useVizijRuntime.ts
982
+ import { useContext } from "react";
983
+ function useVizijRuntime() {
984
+ const ctx = useContext(VizijRuntimeContext);
985
+ if (!ctx) {
986
+ throw new Error(
987
+ "useVizijRuntime must be used within a VizijRuntimeProvider."
988
+ );
989
+ }
990
+ return ctx;
991
+ }
992
+
993
+ // src/VizijRuntimeFace.tsx
994
+ import { jsx as jsx2 } from "react/jsx-runtime";
995
+ function VizijRuntimeFaceInner({
996
+ namespaceOverride,
997
+ ...props
998
+ }) {
999
+ const { rootId, namespace } = useVizijRuntime();
1000
+ if (!rootId) {
1001
+ return null;
1002
+ }
1003
+ return /* @__PURE__ */ jsx2(
1004
+ Vizij,
1005
+ {
1006
+ ...props,
1007
+ rootId,
1008
+ namespace: namespaceOverride ?? namespace
1009
+ }
1010
+ );
1011
+ }
1012
+ var VizijRuntimeFace = memo(VizijRuntimeFaceInner);
1013
+
1014
+ // src/hooks/useVizijOutputs.ts
1015
+ import {
1016
+ useVizijStore
1017
+ } from "@vizij/render";
1018
+ import { getLookup } from "@vizij/utils";
1019
+ function useVizijOutputs(paths) {
1020
+ const { namespace } = useVizijRuntime();
1021
+ return useVizijStore((state) => {
1022
+ const result = {};
1023
+ paths.forEach((path) => {
1024
+ const lookup = getLookup(namespace, path);
1025
+ result[path] = state.values.get(lookup);
1026
+ });
1027
+ return result;
1028
+ });
1029
+ }
1030
+
1031
+ // src/hooks/useRigInput.ts
1032
+ import { useCallback as useCallback2 } from "react";
1033
+ import { useVizijStore as useVizijStore2 } from "@vizij/render";
1034
+ import { getLookup as getLookup2 } from "@vizij/utils";
1035
+ function useRigInput(path) {
1036
+ const { namespace, setInput } = useVizijRuntime();
1037
+ const value = useVizijStore2(
1038
+ (state) => state.values.get(getLookup2(namespace, path))
1039
+ );
1040
+ const setter = useCallback2(
1041
+ (next, shape) => {
1042
+ setInput(path, next, shape);
1043
+ },
1044
+ [path, setInput]
1045
+ );
1046
+ return [value, setter];
1047
+ }
1048
+ export {
1049
+ VizijRuntimeFace,
1050
+ VizijRuntimeProvider,
1051
+ useRigInput,
1052
+ useVizijOutputs,
1053
+ useVizijRuntime
1054
+ };