@vizij/runtime-react 0.0.14 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs DELETED
@@ -1,1363 +0,0 @@
1
- // src/VizijRuntimeProvider.tsx
2
- import {
3
- useCallback,
4
- useEffect,
5
- useMemo,
6
- useRef,
7
- useState
8
- } from "react";
9
- import {
10
- VizijContext,
11
- createVizijStore,
12
- loadGLTFWithBundle,
13
- loadGLTFFromBlobWithBundle
14
- } from "@vizij/render";
15
- import {
16
- OrchestratorProvider,
17
- useOrchestrator,
18
- useOrchFrame
19
- } from "@vizij/orchestrator-react";
20
- import { valueAsNumber as valueAsNumber2 } from "@vizij/value-json";
21
-
22
- // src/context.ts
23
- import { createContext } from "react";
24
- var VizijRuntimeContext = createContext(null);
25
-
26
- // src/utils/graph.ts
27
- function getNodes(spec) {
28
- if (!spec || typeof spec !== "object") {
29
- return [];
30
- }
31
- const maybeNodes = spec.nodes;
32
- if (!Array.isArray(maybeNodes)) {
33
- return [];
34
- }
35
- return maybeNodes;
36
- }
37
- function collectOutputPaths(spec) {
38
- const nodes = getNodes(spec);
39
- const outputs = /* @__PURE__ */ new Set();
40
- nodes.forEach((node) => {
41
- if (typeof node !== "object" || !node) {
42
- return;
43
- }
44
- if (String(node.type ?? "").toLowerCase() !== "output") {
45
- return;
46
- }
47
- const path = node.params?.path;
48
- if (typeof path === "string" && path.trim()) {
49
- outputs.add(path.trim());
50
- }
51
- });
52
- return Array.from(outputs);
53
- }
54
- function collectInputPaths(spec) {
55
- const nodes = getNodes(spec);
56
- const inputs = /* @__PURE__ */ new Set();
57
- nodes.forEach((node) => {
58
- if (String(node.type ?? "").toLowerCase() !== "input") {
59
- return;
60
- }
61
- const path = node.params?.path;
62
- if (typeof path === "string" && path.trim()) {
63
- inputs.add(path.trim());
64
- }
65
- });
66
- return Array.from(inputs);
67
- }
68
- function collectInputPathMap(spec) {
69
- const map = {};
70
- const nodes = getNodes(spec);
71
- nodes.forEach((node) => {
72
- if (String(node.type ?? "").toLowerCase() !== "input") {
73
- return;
74
- }
75
- const path = node.params?.path;
76
- if (typeof path !== "string" || !path.trim()) {
77
- return;
78
- }
79
- const id = String(node.id ?? "");
80
- const key = id.startsWith("input_") ? id.slice("input_".length) : id || path.trim();
81
- map[key] = path.trim();
82
- });
83
- return map;
84
- }
85
-
86
- // src/utils/valueConversion.ts
87
- import {
88
- isNormalizedValue,
89
- valueAsBool,
90
- valueAsColorRgba,
91
- valueAsNumber,
92
- valueAsText,
93
- valueAsTransform,
94
- valueAsVector
95
- } from "@vizij/value-json";
96
- function numericArrayToRaw(arr) {
97
- const normalised = arr.map((entry) => Number(entry ?? 0));
98
- switch (normalised.length) {
99
- case 2:
100
- return {
101
- x: normalised[0],
102
- y: normalised[1],
103
- r: normalised[0],
104
- g: normalised[1]
105
- };
106
- case 3:
107
- return {
108
- x: normalised[0],
109
- y: normalised[1],
110
- z: normalised[2],
111
- r: normalised[0],
112
- g: normalised[1],
113
- b: normalised[2]
114
- };
115
- case 4:
116
- return {
117
- x: normalised[0],
118
- y: normalised[1],
119
- z: normalised[2],
120
- w: normalised[3],
121
- r: normalised[0],
122
- g: normalised[1],
123
- b: normalised[2],
124
- a: normalised[3]
125
- };
126
- default:
127
- return normalised;
128
- }
129
- }
130
- function valueJSONToRaw(value) {
131
- if (value == null) {
132
- return void 0;
133
- }
134
- if (typeof value === "number" || typeof value === "string" || typeof value === "boolean") {
135
- return value;
136
- }
137
- if (Array.isArray(value)) {
138
- return value.map((entry) => valueJSONToRaw(entry));
139
- }
140
- if (typeof value === "object" && !("type" in value)) {
141
- const entries = Object.entries(value).map(([key, entry]) => [
142
- key,
143
- valueJSONToRaw(entry)
144
- ]);
145
- return Object.fromEntries(entries);
146
- }
147
- if (!isNormalizedValue(value)) {
148
- return void 0;
149
- }
150
- switch (value.type) {
151
- case "float": {
152
- const num = valueAsNumber(value);
153
- return typeof num === "number" ? num : void 0;
154
- }
155
- case "bool": {
156
- const boolVal = valueAsBool(value);
157
- return typeof boolVal === "boolean" ? boolVal : void 0;
158
- }
159
- case "text": {
160
- const text = valueAsText(value);
161
- return typeof text === "string" ? text : void 0;
162
- }
163
- case "vec2":
164
- case "vec3":
165
- case "vec4":
166
- case "quat":
167
- case "vector": {
168
- const vec = valueAsVector(value);
169
- return vec ? numericArrayToRaw(vec) : void 0;
170
- }
171
- case "colorrgba": {
172
- const color = valueAsColorRgba(value);
173
- if (!color) {
174
- return void 0;
175
- }
176
- const [r = 0, g = 0, b = 0, a = 1] = color;
177
- return { r, g, b, a };
178
- }
179
- case "transform": {
180
- const transform = valueAsTransform(value);
181
- if (!transform) {
182
- return void 0;
183
- }
184
- return {
185
- translation: numericArrayToRaw(transform.translation),
186
- rotation: numericArrayToRaw(transform.rotation),
187
- scale: numericArrayToRaw(transform.scale)
188
- };
189
- }
190
- case "record": {
191
- const entries = Object.entries(value.data ?? {}).map(([key, entry]) => [
192
- key,
193
- valueJSONToRaw(entry)
194
- ]);
195
- return Object.fromEntries(entries);
196
- }
197
- case "enum": {
198
- const [tag, inner] = value.data;
199
- return {
200
- tag,
201
- value: valueJSONToRaw(inner)
202
- };
203
- }
204
- default:
205
- return void 0;
206
- }
207
- }
208
-
209
- // src/VizijRuntimeProvider.tsx
210
- import { jsx } from "react/jsx-runtime";
211
- var DEFAULT_MERGE = {
212
- outputs: "add",
213
- intermediate: "add"
214
- };
215
- var DEFAULT_DURATION = 0.35;
216
- var EASINGS = {
217
- linear: (t) => t,
218
- easeIn: (t) => t * t,
219
- easeOut: (t) => 1 - (1 - t) * (1 - t),
220
- easeInOut: (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2
221
- };
222
- function resolveEasing(easing) {
223
- if (typeof easing === "function") {
224
- return easing;
225
- }
226
- if (typeof easing === "string" && easing in EASINGS) {
227
- return EASINGS[easing];
228
- }
229
- return EASINGS.linear;
230
- }
231
- function findRootId(world) {
232
- for (const entry of Object.values(world)) {
233
- if (entry && typeof entry === "object" && entry.type === "group" && entry.rootBounds && entry.id) {
234
- return entry.id;
235
- }
236
- }
237
- return null;
238
- }
239
- function normalisePath(path) {
240
- if (!path) {
241
- return path;
242
- }
243
- return path.startsWith("debug/") ? path.slice("debug/".length) : path;
244
- }
245
- function normaliseBundleKind(kind) {
246
- return typeof kind === "string" ? kind.toLowerCase() : "";
247
- }
248
- function pickBundleGraph(bundle, preferredKinds) {
249
- if (!bundle?.graphs || bundle.graphs.length === 0) {
250
- return null;
251
- }
252
- const preferred = preferredKinds.map((kind) => kind.toLowerCase());
253
- for (const entry of bundle.graphs) {
254
- if (!entry || typeof entry !== "object") {
255
- continue;
256
- }
257
- const kind = normaliseBundleKind(entry.kind);
258
- if (preferred.includes(kind)) {
259
- return entry;
260
- }
261
- }
262
- if (bundle.graphs.length === 1) {
263
- return bundle.graphs[0] ?? null;
264
- }
265
- return null;
266
- }
267
- function convertBundleGraph(entry) {
268
- if (!entry || !entry.id || !entry.spec) {
269
- return null;
270
- }
271
- return {
272
- id: entry.id,
273
- spec: entry.spec
274
- };
275
- }
276
- function convertBundleAnimations(entries) {
277
- if (!Array.isArray(entries) || entries.length === 0) {
278
- return [];
279
- }
280
- return entries.filter(
281
- (entry) => Boolean(entry && typeof entry.id === "string" && entry.clip)
282
- ).map((entry) => ({
283
- id: entry.id,
284
- clip: entry.clip
285
- }));
286
- }
287
- function resolveChannelId(track) {
288
- if (typeof track.componentId !== "string" || track.componentId.length === 0) {
289
- return null;
290
- }
291
- if (typeof track.component === "string" && track.component.length > 0) {
292
- return `${track.componentId}:${track.component}`;
293
- }
294
- const rawIndex = track.componentIndex != null ? Number(track.componentIndex) : void 0;
295
- const valueSize = track.valueSize != null ? Number(track.valueSize) : void 0;
296
- if (Number.isInteger(rawIndex) && Number.isFinite(rawIndex) && rawIndex >= 0 && Number.isFinite(valueSize) && valueSize > 1) {
297
- return `${track.componentId}:${rawIndex}`;
298
- }
299
- return track.componentId;
300
- }
301
- function convertExtractedAnimations(clips) {
302
- if (!Array.isArray(clips) || clips.length === 0) {
303
- return [];
304
- }
305
- const assets = [];
306
- clips.forEach((clip) => {
307
- const clipTracks = Array.isArray(clip.tracks) ? clip.tracks : [];
308
- if (clipTracks.length === 0) {
309
- return;
310
- }
311
- const convertedTracks = [];
312
- clipTracks.forEach((track) => {
313
- const channelId = resolveChannelId(track);
314
- if (!channelId) {
315
- return;
316
- }
317
- const rawTimes = Array.isArray(track.times) ? track.times : [];
318
- const rawValues = Array.isArray(track.values) ? track.values : [];
319
- if (rawTimes.length === 0 || rawValues.length === 0) {
320
- return;
321
- }
322
- const times = [];
323
- for (const entry of rawTimes) {
324
- const time = Number(entry);
325
- if (!Number.isFinite(time)) {
326
- return;
327
- }
328
- times.push(time);
329
- }
330
- const values = [];
331
- for (const entry of rawValues) {
332
- const value = Number(entry);
333
- if (!Number.isFinite(value)) {
334
- return;
335
- }
336
- values.push(value);
337
- }
338
- const parsedValueSize = track.valueSize != null ? Number(track.valueSize) : NaN;
339
- const valueSize = Number.isFinite(parsedValueSize) && parsedValueSize > 0 ? parsedValueSize : 1;
340
- if (values.length !== times.length * valueSize) {
341
- return;
342
- }
343
- const rawIndex = track.componentIndex != null ? Number(track.componentIndex) : 0;
344
- const componentIndex = Number.isInteger(rawIndex) && rawIndex >= 0 ? Math.min(rawIndex, valueSize - 1) : 0;
345
- const keyframes = [];
346
- times.forEach((time, index) => {
347
- const base = index * valueSize + componentIndex;
348
- const value = values[base];
349
- if (!Number.isFinite(value)) {
350
- return;
351
- }
352
- keyframes.push({ time, value });
353
- });
354
- if (keyframes.length === 0) {
355
- return;
356
- }
357
- convertedTracks.push({
358
- channel: channelId,
359
- keyframes
360
- });
361
- });
362
- if (convertedTracks.length === 0) {
363
- return;
364
- }
365
- const durationFromTracks = convertedTracks.reduce((maxTime, track) => {
366
- const keyframes = Array.isArray(track.keyframes) ? track.keyframes : [];
367
- if (keyframes.length === 0) {
368
- return maxTime;
369
- }
370
- const lastKeyframe = keyframes[keyframes.length - 1];
371
- const time = Number(lastKeyframe?.time ?? 0);
372
- if (!Number.isFinite(time)) {
373
- return maxTime;
374
- }
375
- return time > maxTime ? time : maxTime;
376
- }, 0);
377
- const duration = typeof clip.duration === "number" && Number.isFinite(clip.duration) ? clip.duration : durationFromTracks;
378
- const clipId = typeof clip.id === "string" && clip.id.length > 0 ? clip.id : typeof clip.name === "string" && clip.name.length > 0 ? clip.name : `gltf-animation-${assets.length}`;
379
- const metadata = clip.metadata && typeof clip.metadata === "object" && !Array.isArray(clip.metadata) ? clip.metadata : void 0;
380
- assets.push({
381
- id: clipId,
382
- clip: {
383
- id: clipId,
384
- name: typeof clip.name === "string" ? clip.name : clipId,
385
- duration,
386
- tracks: convertedTracks,
387
- metadata
388
- }
389
- });
390
- });
391
- return assets;
392
- }
393
- function pickExtractedAnimations(asset) {
394
- if (!asset || typeof asset !== "object") {
395
- return void 0;
396
- }
397
- const animations = asset.animations;
398
- if (!Array.isArray(animations)) {
399
- return void 0;
400
- }
401
- return animations;
402
- }
403
- function mergeAnimationLists(explicit, fromBundle) {
404
- if (!explicit?.length && fromBundle.length === 0) {
405
- return void 0;
406
- }
407
- if (!explicit?.length) {
408
- return fromBundle.length > 0 ? fromBundle : void 0;
409
- }
410
- if (fromBundle.length === 0) {
411
- return explicit;
412
- }
413
- const seen = new Set(explicit.map((anim) => anim.id));
414
- let changed = false;
415
- const merged = [...explicit];
416
- for (const anim of fromBundle) {
417
- if (!anim.id || seen.has(anim.id)) {
418
- continue;
419
- }
420
- merged.push(anim);
421
- seen.add(anim.id);
422
- changed = true;
423
- }
424
- return changed ? merged : explicit;
425
- }
426
- function mergeAssetBundle(base, extracted, extractedAnimations) {
427
- const resolvedBundle = base.bundle ?? extracted ?? null;
428
- const rigFromBundle = convertBundleGraph(
429
- pickBundleGraph(resolvedBundle, ["rig"])
430
- );
431
- const resolvedRig = base.rig ?? rigFromBundle ?? void 0;
432
- const basePose = base.pose;
433
- const poseStageFilter = basePose?.stageNeutralFilter;
434
- const poseGraphFromBundle = basePose?.graph ? null : convertBundleGraph(
435
- pickBundleGraph(resolvedBundle, ["pose-driver", "pose"])
436
- );
437
- const resolvedPoseGraph = basePose?.graph ?? poseGraphFromBundle ?? void 0;
438
- const resolvedPoseConfig = basePose?.config ?? resolvedBundle?.poses?.config ?? void 0;
439
- let resolvedPose = basePose;
440
- if (basePose) {
441
- const nextPose = { ...basePose };
442
- let changed = false;
443
- if (resolvedPoseGraph && basePose.graph !== resolvedPoseGraph) {
444
- nextPose.graph = resolvedPoseGraph;
445
- changed = true;
446
- }
447
- if (resolvedPoseConfig && basePose.config !== resolvedPoseConfig) {
448
- nextPose.config = resolvedPoseConfig;
449
- changed = true;
450
- }
451
- if (!resolvedPoseGraph && !basePose.graph) {
452
- }
453
- if (!resolvedPoseConfig && !basePose.config) {
454
- }
455
- resolvedPose = changed ? nextPose : basePose;
456
- } else if (resolvedPoseGraph || resolvedPoseConfig || typeof poseStageFilter === "function") {
457
- resolvedPose = {
458
- ...resolvedPoseGraph ? { graph: resolvedPoseGraph } : {},
459
- ...resolvedPoseConfig ? { config: resolvedPoseConfig } : {},
460
- ...typeof poseStageFilter === "function" ? { stageNeutralFilter: poseStageFilter } : {}
461
- };
462
- }
463
- const animationsFromBundle = convertBundleAnimations(
464
- resolvedBundle?.animations
465
- );
466
- let resolvedAnimations = mergeAnimationLists(
467
- base.animations,
468
- animationsFromBundle
469
- );
470
- const animationsFromAsset = extractedAnimations && extractedAnimations.length > 0 ? extractedAnimations : [];
471
- if (animationsFromAsset.length > 0) {
472
- resolvedAnimations = mergeAnimationLists(
473
- resolvedAnimations,
474
- animationsFromAsset
475
- );
476
- }
477
- const merged = {
478
- ...base
479
- };
480
- if (resolvedRig) {
481
- merged.rig = resolvedRig;
482
- } else {
483
- merged.rig = void 0;
484
- }
485
- merged.pose = resolvedPose;
486
- merged.animations = resolvedAnimations;
487
- merged.bundle = resolvedBundle;
488
- return merged;
489
- }
490
- function VizijRuntimeProvider({
491
- assetBundle,
492
- children,
493
- namespace: namespaceProp,
494
- faceId: faceIdProp,
495
- autoCreate = true,
496
- createOptions,
497
- autostart = false,
498
- mergeStrategy,
499
- onRegisterControllers,
500
- onStatusChange
501
- }) {
502
- const storeRef = useRef();
503
- if (!storeRef.current) {
504
- storeRef.current = createVizijStore();
505
- }
506
- return /* @__PURE__ */ jsx(VizijContext.Provider, { value: storeRef.current, children: /* @__PURE__ */ jsx(
507
- OrchestratorProvider,
508
- {
509
- autoCreate,
510
- createOptions,
511
- autostart,
512
- children: /* @__PURE__ */ jsx(
513
- VizijRuntimeProviderInner,
514
- {
515
- assetBundle,
516
- namespace: namespaceProp,
517
- faceId: faceIdProp,
518
- autoCreate,
519
- mergeStrategy,
520
- onRegisterControllers,
521
- onStatusChange,
522
- store: storeRef.current,
523
- children
524
- }
525
- )
526
- }
527
- ) });
528
- }
529
- function VizijRuntimeProviderInner({
530
- assetBundle: initialAssetBundle,
531
- namespace: namespaceProp,
532
- faceId: faceIdProp,
533
- mergeStrategy,
534
- onRegisterControllers,
535
- onStatusChange,
536
- store,
537
- children,
538
- autoCreate
539
- }) {
540
- const [extractedBundle, setExtractedBundle] = useState(() => {
541
- if (initialAssetBundle.bundle) {
542
- return initialAssetBundle.bundle;
543
- }
544
- if (initialAssetBundle.glb.kind === "world" && initialAssetBundle.glb.bundle) {
545
- return initialAssetBundle.glb.bundle;
546
- }
547
- return null;
548
- });
549
- const [extractedAnimations, setExtractedAnimations] = useState([]);
550
- useEffect(() => {
551
- if (initialAssetBundle.bundle) {
552
- setExtractedBundle(initialAssetBundle.bundle);
553
- return;
554
- }
555
- if (initialAssetBundle.glb.kind === "world") {
556
- setExtractedBundle(initialAssetBundle.glb.bundle ?? null);
557
- } else {
558
- setExtractedBundle(null);
559
- }
560
- }, [initialAssetBundle]);
561
- const assetBundle = useMemo(
562
- () => mergeAssetBundle(
563
- initialAssetBundle,
564
- extractedBundle,
565
- extractedAnimations
566
- ),
567
- [initialAssetBundle, extractedBundle, extractedAnimations]
568
- );
569
- const {
570
- ready,
571
- createOrchestrator,
572
- registerGraph,
573
- registerMergedGraph,
574
- registerAnimation,
575
- removeGraph,
576
- removeAnimation,
577
- listControllers,
578
- setInput,
579
- getPathSnapshot,
580
- step: stepRuntime
581
- } = useOrchestrator();
582
- const frame = useOrchFrame();
583
- const namespace = namespaceProp ?? assetBundle.namespace ?? "default";
584
- const faceId = faceIdProp ?? assetBundle.faceId ?? assetBundle.pose?.config?.faceId ?? assetBundle.pose?.config?.faceId ?? void 0;
585
- const [status, setStatus] = useState({
586
- loading: true,
587
- ready: false,
588
- error: null,
589
- errors: [],
590
- namespace,
591
- faceId,
592
- rootId: null,
593
- outputPaths: [],
594
- controllers: { graphs: [], anims: [] }
595
- });
596
- const errorsRef = useRef([]);
597
- const outputPathsRef = useRef(/* @__PURE__ */ new Set());
598
- const rigInputMapRef = useRef({});
599
- const registeredGraphsRef = useRef([]);
600
- const registeredAnimationsRef = useRef([]);
601
- const mergedGraphRef = useRef(null);
602
- const animationTweensRef = useRef(/* @__PURE__ */ new Map());
603
- const clipPlaybackRef = useRef(/* @__PURE__ */ new Map());
604
- const rafHandleRef = useRef(null);
605
- const lastFrameTimeRef = useRef(null);
606
- const reportStatus = useCallback(
607
- (updater) => {
608
- setStatus((prev) => {
609
- const next = updater(prev);
610
- onStatusChange?.(next);
611
- return next;
612
- });
613
- },
614
- [onStatusChange]
615
- );
616
- const pushError = useCallback(
617
- (error) => {
618
- errorsRef.current = [...errorsRef.current, error];
619
- reportStatus((prev) => ({
620
- ...prev,
621
- error,
622
- errors: errorsRef.current
623
- }));
624
- console.warn("[vizij-runtime]", error.message, error.cause);
625
- },
626
- [reportStatus]
627
- );
628
- const resetErrors = useCallback(() => {
629
- errorsRef.current = [];
630
- reportStatus((prev) => ({
631
- ...prev,
632
- error: null,
633
- errors: []
634
- }));
635
- }, [reportStatus]);
636
- const clearControllers = useCallback(() => {
637
- const existing = listControllers();
638
- existing.graphs.forEach((id) => {
639
- try {
640
- removeGraph(id);
641
- } catch (err) {
642
- pushError({
643
- message: `Failed to remove graph ${id}`,
644
- cause: err,
645
- phase: "registration",
646
- timestamp: performance.now()
647
- });
648
- }
649
- });
650
- existing.anims.forEach((id) => {
651
- try {
652
- removeAnimation(id);
653
- } catch (err) {
654
- pushError({
655
- message: `Failed to remove animation ${id}`,
656
- cause: err,
657
- phase: "registration",
658
- timestamp: performance.now()
659
- });
660
- }
661
- });
662
- registeredGraphsRef.current = [];
663
- registeredAnimationsRef.current = [];
664
- mergedGraphRef.current = null;
665
- }, [listControllers, removeAnimation, removeGraph, pushError]);
666
- useEffect(() => {
667
- reportStatus((prev) => ({
668
- ...prev,
669
- namespace,
670
- faceId
671
- }));
672
- }, [namespace, faceId, reportStatus]);
673
- const glbAsset = initialAssetBundle.glb;
674
- const baseBundle = initialAssetBundle.bundle ?? null;
675
- useEffect(() => {
676
- let cancelled = false;
677
- resetErrors();
678
- reportStatus((prev) => ({
679
- ...prev,
680
- loading: true,
681
- rootId: null,
682
- ready: false,
683
- outputPaths: [],
684
- controllers: { graphs: [], anims: [] }
685
- }));
686
- setExtractedAnimations([]);
687
- const loadAssets = async () => {
688
- try {
689
- let world;
690
- let animatables;
691
- let bundle = baseBundle;
692
- let gltfAnimations;
693
- if (glbAsset.kind === "url") {
694
- const loaded = await loadGLTFWithBundle(
695
- glbAsset.src,
696
- [namespace],
697
- glbAsset.aggressiveImport ?? false,
698
- glbAsset.rootBounds
699
- );
700
- world = loaded.world;
701
- animatables = loaded.animatables;
702
- bundle = loaded.bundle ?? bundle;
703
- gltfAnimations = pickExtractedAnimations(loaded);
704
- } else if (glbAsset.kind === "blob") {
705
- const loaded = await loadGLTFFromBlobWithBundle(
706
- glbAsset.blob,
707
- [namespace],
708
- glbAsset.aggressiveImport ?? false,
709
- glbAsset.rootBounds
710
- );
711
- world = loaded.world;
712
- animatables = loaded.animatables;
713
- bundle = loaded.bundle ?? bundle;
714
- gltfAnimations = pickExtractedAnimations(loaded);
715
- } else {
716
- world = glbAsset.world;
717
- animatables = glbAsset.animatables;
718
- bundle = glbAsset.bundle ?? bundle;
719
- gltfAnimations = void 0;
720
- }
721
- if (cancelled) {
722
- return;
723
- }
724
- setExtractedBundle(bundle ?? null);
725
- setExtractedAnimations(convertExtractedAnimations(gltfAnimations));
726
- const rootId = findRootId(world);
727
- store.getState().addWorldElements(world, animatables, true);
728
- reportStatus((prev) => ({
729
- ...prev,
730
- loading: false,
731
- rootId,
732
- namespace,
733
- faceId
734
- }));
735
- } catch (err) {
736
- if (cancelled) {
737
- return;
738
- }
739
- pushError({
740
- message: "Failed to load Vizij assets",
741
- cause: err,
742
- phase: "assets",
743
- timestamp: performance.now()
744
- });
745
- reportStatus((prev) => ({
746
- ...prev,
747
- loading: false
748
- }));
749
- }
750
- };
751
- loadAssets();
752
- return () => {
753
- cancelled = true;
754
- };
755
- }, [
756
- glbAsset,
757
- baseBundle,
758
- namespace,
759
- faceId,
760
- store,
761
- pushError,
762
- reportStatus,
763
- resetErrors,
764
- setExtractedBundle,
765
- setExtractedAnimations
766
- ]);
767
- useEffect(() => {
768
- if (!ready && autoCreate) {
769
- createOrchestrator().catch((err) => {
770
- pushError({
771
- message: "Failed to create orchestrator runtime",
772
- cause: err,
773
- phase: "orchestrator",
774
- timestamp: performance.now()
775
- });
776
- });
777
- }
778
- }, [ready, autoCreate, createOrchestrator, pushError]);
779
- const registerControllers = useCallback(async () => {
780
- const normalize = (spec) => spec;
781
- clearControllers();
782
- const rigAsset = assetBundle.rig;
783
- if (!rigAsset) {
784
- pushError({
785
- message: "Asset bundle is missing a rig graph.",
786
- phase: "registration",
787
- timestamp: performance.now()
788
- });
789
- return;
790
- }
791
- const rigSpec = normalize(rigAsset.spec);
792
- const rigOutputs = collectOutputPaths(rigSpec);
793
- const rigInputs = collectInputPaths(rigSpec);
794
- rigInputMapRef.current = collectInputPathMap(rigSpec);
795
- outputPathsRef.current = new Set(rigOutputs);
796
- const rigConfig = {
797
- id: rigAsset.id,
798
- spec: rigSpec,
799
- subs: rigAsset.subscriptions ?? {
800
- inputs: rigInputs,
801
- outputs: rigOutputs
802
- }
803
- };
804
- const graphConfigs = [rigConfig];
805
- if (assetBundle.pose?.graph) {
806
- const poseSpec = normalize(assetBundle.pose.graph.spec);
807
- const poseOutputs = collectOutputPaths(poseSpec);
808
- const poseInputs = collectInputPaths(poseSpec);
809
- graphConfigs.push({
810
- id: assetBundle.pose.graph.id,
811
- spec: poseSpec,
812
- subs: assetBundle.pose.graph.subscriptions ?? {
813
- inputs: poseInputs,
814
- outputs: poseOutputs
815
- }
816
- });
817
- }
818
- const graphIds = [];
819
- try {
820
- if (graphConfigs.length > 1) {
821
- const mergedId = registerMergedGraph({
822
- graphs: graphConfigs,
823
- strategy: mergeStrategy ?? DEFAULT_MERGE
824
- });
825
- mergedGraphRef.current = mergedId;
826
- graphIds.push(mergedId);
827
- } else {
828
- graphConfigs.forEach((cfg) => {
829
- const id = registerGraph(cfg);
830
- graphIds.push(id);
831
- });
832
- }
833
- } catch (err) {
834
- pushError({
835
- message: "Failed to register rig graphs",
836
- cause: err,
837
- phase: "registration",
838
- timestamp: performance.now()
839
- });
840
- }
841
- registeredGraphsRef.current = graphIds;
842
- const animationIds = [];
843
- for (const anim of assetBundle.animations ?? []) {
844
- try {
845
- const config = {
846
- id: anim.id,
847
- setup: {
848
- animation: anim.clip,
849
- ...anim.setup ?? {}
850
- }
851
- };
852
- const id = registerAnimation(config);
853
- animationIds.push(id);
854
- } catch (err) {
855
- pushError({
856
- message: `Failed to register animation ${anim.id}`,
857
- cause: err,
858
- phase: "animation",
859
- timestamp: performance.now()
860
- });
861
- }
862
- }
863
- registeredAnimationsRef.current = animationIds;
864
- if (assetBundle.initialInputs) {
865
- Object.entries(assetBundle.initialInputs).forEach(([path, value]) => {
866
- try {
867
- setInput(path, value);
868
- } catch (err) {
869
- pushError({
870
- message: `Failed to stage initial input ${path}`,
871
- cause: err,
872
- phase: "registration",
873
- timestamp: performance.now()
874
- });
875
- }
876
- });
877
- }
878
- const controllers = listControllers();
879
- reportStatus((prev) => ({
880
- ...prev,
881
- ready: true,
882
- controllers,
883
- outputPaths: Array.from(outputPathsRef.current)
884
- }));
885
- onRegisterControllers?.(controllers);
886
- }, [
887
- assetBundle,
888
- clearControllers,
889
- listControllers,
890
- mergeStrategy,
891
- onRegisterControllers,
892
- pushError,
893
- registerAnimation,
894
- registerGraph,
895
- registerMergedGraph,
896
- reportStatus,
897
- setInput
898
- ]);
899
- useEffect(() => {
900
- if (!ready || status.loading) {
901
- return;
902
- }
903
- registerControllers().catch((err) => {
904
- pushError({
905
- message: "Failed to register controllers",
906
- cause: err,
907
- phase: "registration",
908
- timestamp: performance.now()
909
- });
910
- });
911
- }, [ready, status.loading, registerControllers, pushError]);
912
- useEffect(() => {
913
- if (!frame) {
914
- return;
915
- }
916
- const writes = frame.merged_writes ?? [];
917
- if (!writes.length) {
918
- return;
919
- }
920
- const setWorldValue = store.getState().setValue;
921
- const namespaceValue = status.namespace;
922
- writes.forEach((write) => {
923
- const path = normalisePath(write.path);
924
- if (!outputPathsRef.current.has(path)) {
925
- return;
926
- }
927
- const raw = valueJSONToRaw(write.value);
928
- if (raw === void 0) {
929
- return;
930
- }
931
- setWorldValue(path, namespaceValue, raw);
932
- });
933
- }, [frame, status.namespace, store]);
934
- const stagePoseNeutral = useCallback(
935
- (force = false) => {
936
- const neutral = assetBundle.pose?.config?.neutralInputs ?? {};
937
- const rigMap = rigInputMapRef.current;
938
- const staged = /* @__PURE__ */ new Set();
939
- Object.entries(neutral).forEach(([id, value]) => {
940
- const path = rigMap[id];
941
- if (!path) {
942
- return;
943
- }
944
- const include = assetBundle.pose?.stageNeutralFilter;
945
- if (include && !include(id, path)) {
946
- return;
947
- }
948
- setInput(path, { float: Number.isFinite(value) ? value : 0 });
949
- staged.add(path);
950
- });
951
- if (force) {
952
- Object.entries(rigMap).forEach(([id, path]) => {
953
- if (staged.has(path)) {
954
- return;
955
- }
956
- const include = assetBundle.pose?.stageNeutralFilter;
957
- if (include && !include(id, path)) {
958
- return;
959
- }
960
- setInput(path, { float: 0 });
961
- });
962
- }
963
- },
964
- [assetBundle.pose?.config?.neutralInputs, setInput]
965
- );
966
- const setRendererValue = useCallback(
967
- (id, ns, value) => {
968
- store.getState().setValue(id, ns, value);
969
- },
970
- [store]
971
- );
972
- const cancelAnimation = useCallback((path) => {
973
- if (animationTweensRef.current.has(path)) {
974
- const entry = animationTweensRef.current.get(path);
975
- animationTweensRef.current.delete(path);
976
- entry?.resolve();
977
- }
978
- }, []);
979
- const scheduleLoop = useCallback(() => {
980
- if (rafHandleRef.current !== null) {
981
- return;
982
- }
983
- const tick = (timestamp) => {
984
- if (lastFrameTimeRef.current == null) {
985
- lastFrameTimeRef.current = timestamp;
986
- }
987
- const dt = Math.max(0, (timestamp - lastFrameTimeRef.current) / 1e3);
988
- lastFrameTimeRef.current = timestamp;
989
- advanceAnimationTweens(dt);
990
- advanceClipPlayback(dt);
991
- if (animationTweensRef.current.size > 0 || clipPlaybackRef.current.size > 0) {
992
- rafHandleRef.current = requestAnimationFrame(tick);
993
- } else {
994
- rafHandleRef.current = null;
995
- lastFrameTimeRef.current = null;
996
- }
997
- };
998
- rafHandleRef.current = requestAnimationFrame(tick);
999
- }, []);
1000
- const advanceAnimationTweens = useCallback(
1001
- (dt) => {
1002
- if (animationTweensRef.current.size === 0) {
1003
- return;
1004
- }
1005
- const map = animationTweensRef.current;
1006
- const toDelete = [];
1007
- map.forEach((state, key) => {
1008
- state.elapsed += dt;
1009
- const progress = state.duration === 0 ? 1 : Math.min(state.elapsed / state.duration, 1);
1010
- const eased = state.easing(progress);
1011
- const value = state.from + (state.to - state.from) * eased;
1012
- setInput(state.path, { float: value });
1013
- if (progress >= 1) {
1014
- toDelete.push(key);
1015
- state.resolve();
1016
- }
1017
- });
1018
- toDelete.forEach((key) => map.delete(key));
1019
- },
1020
- [setInput]
1021
- );
1022
- const sampleTrack = useCallback(
1023
- (track, time) => {
1024
- const keyframes = Array.isArray(track.keyframes) ? track.keyframes : [];
1025
- if (!keyframes.length) {
1026
- return 0;
1027
- }
1028
- const first = keyframes[0];
1029
- if (!first) {
1030
- return 0;
1031
- }
1032
- if (time <= Number(first.time ?? 0)) {
1033
- return Number(first.value ?? 0);
1034
- }
1035
- for (let i = 0; i < keyframes.length - 1; i += 1) {
1036
- const current = keyframes[i] ?? {};
1037
- const next = keyframes[i + 1] ?? {};
1038
- const start = Number(current.time ?? 0);
1039
- const end = Number(next.time ?? start);
1040
- if (time >= start && time <= end) {
1041
- const range = end - start || 1;
1042
- const factor = (time - start) / range;
1043
- const currentValue = Number(current.value ?? 0);
1044
- const nextValue = Number(next.value ?? currentValue);
1045
- return currentValue + (nextValue - currentValue) * factor;
1046
- }
1047
- }
1048
- const last = keyframes[keyframes.length - 1];
1049
- return Number(last?.value ?? 0);
1050
- },
1051
- []
1052
- );
1053
- const advanceClipPlayback = useCallback(
1054
- (dt) => {
1055
- if (clipPlaybackRef.current.size === 0) {
1056
- return;
1057
- }
1058
- const map = clipPlaybackRef.current;
1059
- const toDelete = [];
1060
- map.forEach((state, key) => {
1061
- state.time += dt * state.speed;
1062
- const clip = assetBundle.animations?.find(
1063
- (anim) => anim.id === state.id
1064
- );
1065
- if (!clip) {
1066
- toDelete.push(key);
1067
- state.resolve();
1068
- return;
1069
- }
1070
- const clipData = clip.clip;
1071
- const duration = Number(clipData?.duration ?? state.duration);
1072
- state.duration = Number.isFinite(duration) && duration > 0 ? duration : state.duration;
1073
- if (state.time >= state.duration) {
1074
- toDelete.push(key);
1075
- state.time = state.duration;
1076
- }
1077
- const tracks = Array.isArray(clipData?.tracks) ? clipData.tracks : [];
1078
- tracks.forEach((track) => {
1079
- const value = sampleTrack(track, state.time) * state.weight;
1080
- const path = `animation/${clip.id}/${track.channel}`;
1081
- setInput(path, { float: value });
1082
- });
1083
- if (toDelete.includes(key)) {
1084
- state.resolve();
1085
- }
1086
- });
1087
- toDelete.forEach((key) => {
1088
- clipPlaybackRef.current.delete(key);
1089
- const clip = assetBundle.animations?.find((anim) => anim.id === key);
1090
- if (clip) {
1091
- const clipData = clip.clip;
1092
- const tracks = Array.isArray(clipData?.tracks) ? clipData.tracks : [];
1093
- tracks.forEach((track) => {
1094
- const path = `animation/${clip.id}/${track.channel}`;
1095
- setInput(path, { float: 0 });
1096
- });
1097
- }
1098
- });
1099
- },
1100
- [assetBundle.animations, sampleTrack, setInput]
1101
- );
1102
- const animateValue = useCallback(
1103
- (path, target, options) => {
1104
- const easing = resolveEasing(options?.easing);
1105
- const duration = Math.max(0, options?.duration ?? DEFAULT_DURATION);
1106
- cancelAnimation(path);
1107
- const current = getPathSnapshot(path);
1108
- const fromValue = valueAsNumber2(current);
1109
- const toValue = valueAsNumber2(target);
1110
- if (fromValue == null || toValue == null || duration === 0) {
1111
- setInput(path, target);
1112
- return Promise.resolve();
1113
- }
1114
- return new Promise((resolve) => {
1115
- animationTweensRef.current.set(path, {
1116
- path,
1117
- from: fromValue,
1118
- to: toValue,
1119
- duration,
1120
- elapsed: 0,
1121
- easing,
1122
- resolve
1123
- });
1124
- scheduleLoop();
1125
- });
1126
- },
1127
- [cancelAnimation, getPathSnapshot, scheduleLoop, setInput]
1128
- );
1129
- const playAnimation = useCallback(
1130
- (id, options) => {
1131
- const clip = assetBundle.animations?.find((anim) => anim.id === id);
1132
- if (!clip) {
1133
- return Promise.reject(
1134
- new Error(`Animation ${id} is not part of the current asset bundle.`)
1135
- );
1136
- }
1137
- if (clipPlaybackRef.current.has(id)) {
1138
- clipPlaybackRef.current.delete(id);
1139
- }
1140
- return new Promise((resolve) => {
1141
- const speed = options?.speed ?? 1;
1142
- const weight = options?.weight ?? clip.weight ?? 1;
1143
- const clipData = clip.clip;
1144
- clipPlaybackRef.current.set(id, {
1145
- id,
1146
- time: options?.reset ? 0 : 0,
1147
- duration: Number(clipData?.duration ?? 0),
1148
- speed: Number.isFinite(speed) && speed > 0 ? speed : 1,
1149
- weight,
1150
- resolve
1151
- });
1152
- scheduleLoop();
1153
- });
1154
- },
1155
- [assetBundle.animations, scheduleLoop]
1156
- );
1157
- const stopAnimation = useCallback(
1158
- (id) => {
1159
- const clip = assetBundle.animations?.find((anim) => anim.id === id);
1160
- const state = clipPlaybackRef.current.get(id);
1161
- if (state) {
1162
- clipPlaybackRef.current.delete(id);
1163
- state.resolve();
1164
- }
1165
- if (clip) {
1166
- const clipData = clip.clip;
1167
- const tracks = Array.isArray(clipData?.tracks) ? clipData.tracks : [];
1168
- tracks.forEach((track) => {
1169
- const path = `animation/${clip.id}/${track.channel}`;
1170
- setInput(path, { float: 0 });
1171
- });
1172
- }
1173
- },
1174
- [assetBundle.animations, setInput]
1175
- );
1176
- const registerInputDriver = useCallback(
1177
- (id, factory) => {
1178
- const driver = factory({
1179
- setInput,
1180
- setRendererValue,
1181
- namespace,
1182
- faceId
1183
- });
1184
- const wrapped = {
1185
- start: () => {
1186
- try {
1187
- driver.start();
1188
- } catch (err) {
1189
- pushError({
1190
- message: `Input driver ${id} failed to start`,
1191
- cause: err,
1192
- phase: "driver",
1193
- timestamp: performance.now()
1194
- });
1195
- }
1196
- },
1197
- stop: () => {
1198
- try {
1199
- driver.stop();
1200
- } catch (err) {
1201
- pushError({
1202
- message: `Input driver ${id} failed to stop`,
1203
- cause: err,
1204
- phase: "driver",
1205
- timestamp: performance.now()
1206
- });
1207
- }
1208
- },
1209
- dispose: () => {
1210
- try {
1211
- driver.dispose();
1212
- } catch (err) {
1213
- pushError({
1214
- message: `Input driver ${id} failed to dispose`,
1215
- cause: err,
1216
- phase: "driver",
1217
- timestamp: performance.now()
1218
- });
1219
- }
1220
- }
1221
- };
1222
- return wrapped;
1223
- },
1224
- [faceId, namespace, pushError, setInput, setRendererValue]
1225
- );
1226
- const advanceAnimations = useCallback(
1227
- (dt) => {
1228
- advanceAnimationTweens(dt);
1229
- advanceClipPlayback(dt);
1230
- },
1231
- [advanceAnimationTweens, advanceClipPlayback]
1232
- );
1233
- const step = useCallback(
1234
- (dt) => {
1235
- advanceAnimations(dt);
1236
- stepRuntime(dt);
1237
- },
1238
- [advanceAnimations, stepRuntime]
1239
- );
1240
- useEffect(() => {
1241
- return () => {
1242
- if (rafHandleRef.current !== null) {
1243
- cancelAnimationFrame(rafHandleRef.current);
1244
- rafHandleRef.current = null;
1245
- }
1246
- lastFrameTimeRef.current = null;
1247
- animationTweensRef.current.clear();
1248
- clipPlaybackRef.current.clear();
1249
- };
1250
- }, []);
1251
- const contextValue = useMemo(
1252
- () => ({
1253
- ...status,
1254
- assetBundle,
1255
- setInput,
1256
- setValue: setRendererValue,
1257
- stagePoseNeutral,
1258
- animateValue,
1259
- cancelAnimation,
1260
- registerInputDriver,
1261
- playAnimation,
1262
- stopAnimation,
1263
- step,
1264
- advanceAnimations
1265
- }),
1266
- [
1267
- status,
1268
- assetBundle,
1269
- setInput,
1270
- setRendererValue,
1271
- stagePoseNeutral,
1272
- animateValue,
1273
- cancelAnimation,
1274
- registerInputDriver,
1275
- playAnimation,
1276
- stopAnimation,
1277
- step,
1278
- advanceAnimations
1279
- ]
1280
- );
1281
- return /* @__PURE__ */ jsx(VizijRuntimeContext.Provider, { value: contextValue, children });
1282
- }
1283
-
1284
- // src/VizijRuntimeFace.tsx
1285
- import { memo } from "react";
1286
- import { Vizij } from "@vizij/render";
1287
-
1288
- // src/hooks/useVizijRuntime.ts
1289
- import { useContext } from "react";
1290
- function useVizijRuntime() {
1291
- const ctx = useContext(VizijRuntimeContext);
1292
- if (!ctx) {
1293
- throw new Error(
1294
- "useVizijRuntime must be used within a VizijRuntimeProvider."
1295
- );
1296
- }
1297
- return ctx;
1298
- }
1299
-
1300
- // src/VizijRuntimeFace.tsx
1301
- import { jsx as jsx2 } from "react/jsx-runtime";
1302
- function VizijRuntimeFaceInner({
1303
- namespaceOverride,
1304
- ...props
1305
- }) {
1306
- const { rootId, namespace } = useVizijRuntime();
1307
- if (!rootId) {
1308
- return null;
1309
- }
1310
- return /* @__PURE__ */ jsx2(
1311
- Vizij,
1312
- {
1313
- ...props,
1314
- rootId,
1315
- namespace: namespaceOverride ?? namespace
1316
- }
1317
- );
1318
- }
1319
- var VizijRuntimeFace = memo(VizijRuntimeFaceInner);
1320
-
1321
- // src/hooks/useVizijOutputs.ts
1322
- import {
1323
- useVizijStore
1324
- } from "@vizij/render";
1325
- import { getLookup } from "@vizij/utils";
1326
- function useVizijOutputs(paths) {
1327
- const { namespace } = useVizijRuntime();
1328
- return useVizijStore((state) => {
1329
- const result = {};
1330
- paths.forEach((path) => {
1331
- const lookup = getLookup(namespace, path);
1332
- result[path] = state.values.get(lookup);
1333
- });
1334
- return result;
1335
- });
1336
- }
1337
-
1338
- // src/hooks/useRigInput.ts
1339
- import { useCallback as useCallback2 } from "react";
1340
- import {
1341
- useVizijStore as useVizijStore2
1342
- } from "@vizij/render";
1343
- import { getLookup as getLookup2 } from "@vizij/utils";
1344
- function useRigInput(path) {
1345
- const { namespace, setInput } = useVizijRuntime();
1346
- const value = useVizijStore2((state) => {
1347
- return state.values.get(getLookup2(namespace, path));
1348
- });
1349
- const setter = useCallback2(
1350
- (next, shape) => {
1351
- setInput(path, next, shape);
1352
- },
1353
- [path, setInput]
1354
- );
1355
- return [value, setter];
1356
- }
1357
- export {
1358
- VizijRuntimeFace,
1359
- VizijRuntimeProvider,
1360
- useRigInput,
1361
- useVizijOutputs,
1362
- useVizijRuntime
1363
- };