@found-in-space/skykit 0.2.0-dev.20260527.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/plugins.js CHANGED
@@ -1,10 +1,5 @@
1
1
  import * as THREE from 'three';
2
2
 
3
- import {
4
- createJourneyController,
5
- createTimedJourneyEvaluator,
6
- } from '@found-in-space/journey';
7
-
8
3
  import {
9
4
  combineStrategies,
10
5
  createLookaheadStrategy,
@@ -16,7 +11,6 @@ import {
16
11
 
17
12
  import {
18
13
  IDENTITY_QUATERNION as SPATIAL_IDENTITY_QUATERNION,
19
- createOrbitTransferRoute,
20
14
  createSpatialPoseTransition,
21
15
  createSpatialNavigationAutomation,
22
16
  resolveSpatialLookAt,
@@ -408,600 +402,6 @@ export function createSkykitNavigationPlugin(options = {}) {
408
402
  }
409
403
  }
410
404
 
411
- /**
412
- * @param {import('./index.d.ts').SkykitJourneyPluginOptions} [options]
413
- * @returns {import('./index.d.ts').SkykitJourneyPlugin}
414
- */
415
- export function createSkykitJourneyPlugin(options = {}) {
416
- const id = options.id ?? 'journey';
417
- const controller = options.controller
418
- ?? (options.journey
419
- ? createJourneyController({ graph: options.journey })
420
- : (options.graph || options.scenes ? createJourneyController(options) : null));
421
- const evaluator = options.evaluator
422
- ?? (options.timedJourney ? createTimedJourneyEvaluator(options.timedJourney, options.evaluatorOptions) : null);
423
- let disposed = false;
424
- let playing = options.autoPlay === true;
425
- let currentTimeSecs = Math.max(0, finiteNumber(options.startTimeSecs, 0));
426
- let lastCueId = /** @type {string | null} */ (null);
427
- let initialSceneApplied = false;
428
- let timedPreloadHintsEmitted = false;
429
- /** @type {import('./index.d.ts').SkykitThreePluginContext | null} */
430
- let pluginContext = null;
431
- const resolvedJourneyOrbits = new Map();
432
- /** @type {(() => void) | null} */
433
- let unsubscribeController = null;
434
- /** @type {Promise<unknown>} */
435
- let pendingSceneApplication = Promise.resolve(null);
436
-
437
- /** @type {SkykitThreePart} */
438
- const part = {
439
- id,
440
- priority: options.priority,
441
- attach(context) {
442
- pluginContext = context;
443
- if (controller) {
444
- unsubscribeController = controller.subscribe((event) => {
445
- queueSceneApplication(event.spec, context, event);
446
- });
447
- applyInitialScene(context);
448
- }
449
- emitTimedPreloadHints(context, 'attach');
450
- },
451
- update(frame) {
452
- if (disposed || !evaluator || !playing) return;
453
- currentTimeSecs = Math.min(
454
- evaluator.durationSecs,
455
- currentTimeSecs + Math.max(0, finiteNumber(frame.deltaSeconds, 0)),
456
- );
457
- applyTimedFrame(evaluator.evaluate(currentTimeSecs), frame);
458
- if (currentTimeSecs >= evaluator.durationSecs) {
459
- playing = options.loop === true;
460
- currentTimeSecs = playing ? 0 : evaluator.durationSecs;
461
- }
462
- },
463
- detach() {
464
- unsubscribeController?.();
465
- unsubscribeController = null;
466
- pluginContext = null;
467
- },
468
- dispose() {
469
- disposed = true;
470
- this.detach?.();
471
- if (options.disposeController !== false) controller?.dispose?.();
472
- },
473
- getSnapshot,
474
- };
475
-
476
- return {
477
- id,
478
- setup(context) {
479
- const threeContext = /** @type {import('./index.d.ts').SkykitThreePluginContext} */ (context);
480
- const removePart = threeContext.addPart(part);
481
- const unregisterActions = registerJourneyActions(threeContext);
482
- return () => {
483
- unregisterActions();
484
- removePart();
485
- };
486
- },
487
- goTo,
488
- next,
489
- previous,
490
- seek,
491
- play,
492
- pause,
493
- getSnapshot,
494
- };
495
-
496
- /** @param {unknown} payload */
497
- async function goTo(payload) {
498
- await pendingSceneApplication;
499
- const sceneId = resolveSceneId(payload);
500
- const spec = sceneId && controller ? controller.goTo(sceneId, { source: SKYKIT_ACTIONS.journey.goToChapter }) : null;
501
- await pendingSceneApplication;
502
- return spec;
503
- }
504
-
505
- async function next() {
506
- await pendingSceneApplication;
507
- const spec = controller?.next({ source: SKYKIT_ACTIONS.journey.next }) ?? null;
508
- await pendingSceneApplication;
509
- return spec;
510
- }
511
-
512
- async function previous() {
513
- await pendingSceneApplication;
514
- const spec = controller?.previous({ source: SKYKIT_ACTIONS.journey.previous }) ?? null;
515
- await pendingSceneApplication;
516
- return spec;
517
- }
518
-
519
- /** @param {unknown} payload */
520
- function seek(payload) {
521
- currentTimeSecs = clampTime(resolveTimeSecs(payload), evaluator?.durationSecs ?? Number.POSITIVE_INFINITY);
522
- if (evaluator && pluginContext) applyTimedFrame(evaluator.evaluate(currentTimeSecs), { viewer: pluginContext.viewer, view: pluginContext.getViewState() });
523
- if (pluginContext) emitTimedPreloadHints(pluginContext, 'seek');
524
- return currentTimeSecs;
525
- }
526
-
527
- /** @param {unknown} [payload] */
528
- function play(payload) {
529
- if (payload && typeof payload === 'object' && 'timeSecs' in payload) {
530
- currentTimeSecs = clampTime(resolveTimeSecs(payload), evaluator?.durationSecs ?? Number.POSITIVE_INFINITY);
531
- }
532
- playing = true;
533
- if (pluginContext) emitTimedPreloadHints(pluginContext, 'play');
534
- return currentTimeSecs;
535
- }
536
-
537
- function pause() {
538
- playing = false;
539
- return currentTimeSecs;
540
- }
541
-
542
- /** @param {import('./index.d.ts').SkykitThreePluginContext} context */
543
- function registerJourneyActions(context) {
544
- const unregisters = [
545
- context.actions.registerAction(SKYKIT_ACTIONS.journey.goToChapter, ({ payload }) => goTo(payload), { label: 'Go to journey chapter' }),
546
- context.actions.registerAction(SKYKIT_ACTIONS.journey.next, () => next(), {
547
- label: 'Next journey chapter',
548
- }),
549
- context.actions.registerAction(SKYKIT_ACTIONS.journey.previous, () => previous(), {
550
- label: 'Previous journey chapter',
551
- }),
552
- context.actions.registerAction(SKYKIT_ACTIONS.journey.seek, ({ payload }) => seek(payload), { label: 'Seek journey time' }),
553
- context.actions.registerAction(SKYKIT_ACTIONS.journey.play, ({ payload }) => play(payload), { label: 'Play journey' }),
554
- context.actions.registerAction(SKYKIT_ACTIONS.journey.pause, () => pause(), { label: 'Pause journey' }),
555
- ];
556
- return () => {
557
- for (const unregister of unregisters.reverse()) unregister();
558
- };
559
- }
560
-
561
- /** @param {import('./index.d.ts').SkykitThreePluginContext} context */
562
- function applyInitialScene(context) {
563
- if (!controller || initialSceneApplied) return;
564
- const snapshot = controller.getSnapshot();
565
- const sceneId = snapshot.activeSceneId;
566
- if (!sceneId) {
567
- initialSceneApplied = true;
568
- return;
569
- }
570
- initialSceneApplied = true;
571
- const spec = controller.graph.resolveSceneSpec(sceneId, { fromSceneId: snapshot.previousSceneId });
572
- queueSceneApplication(spec, context, {
573
- type: 'journey/initial',
574
- sceneId,
575
- previousSceneId: snapshot.previousSceneId,
576
- source: 'attach',
577
- spec,
578
- });
579
- }
580
-
581
- /**
582
- * @param {unknown} spec
583
- * @param {import('./index.d.ts').SkykitThreePluginContext} context
584
- * @param {unknown} event
585
- */
586
- function queueSceneApplication(spec, context, event) {
587
- pendingSceneApplication = Promise.resolve(applySceneSpec(spec, context, event)).catch((error) => {
588
- context.emit?.({
589
- type: 'journey/scene/error',
590
- pluginId: id,
591
- message: 'Failed to apply journey scene.',
592
- error,
593
- event,
594
- });
595
- return null;
596
- });
597
- return pendingSceneApplication;
598
- }
599
-
600
- /**
601
- * @param {import('./index.d.ts').SkykitThreePluginContext} context
602
- * @param {string} source
603
- */
604
- function emitTimedPreloadHints(context, source) {
605
- if (!evaluator || timedPreloadHintsEmitted) return;
606
- const preloadHints = typeof evaluator.getPreloadHints === 'function'
607
- ? evaluator.getPreloadHints()
608
- : evaluator.evaluate(currentTimeSecs).preloadHints;
609
- timedPreloadHintsEmitted = true;
610
- if (preloadHints.length > 0) {
611
- options.onPreloadHints?.(preloadHints, {
612
- type: 'journey/timed-preload',
613
- journeyId: evaluator.journey.id,
614
- source,
615
- }, context);
616
- }
617
- }
618
-
619
- /**
620
- * @param {unknown} spec
621
- * @param {import('./index.d.ts').SkykitThreePluginContext} context
622
- * @param {unknown} event
623
- */
624
- async function applySceneSpec(spec, context, event) {
625
- const scene = /** @type {Record<string, unknown> | null} */ (spec && typeof spec === 'object' ? spec : null);
626
- options.onScene?.(scene, context, event);
627
- if (!scene) return;
628
- const onArrive = () => {
629
- void notifySceneArrive(scene, context, event);
630
- };
631
- let arrivalDeferred = false;
632
- if (scene.view && typeof scene.view === 'object') {
633
- context.requestViewState(/** @type {Partial<import('./index.d.ts').SkykitViewState>} */ (scene.view), id);
634
- }
635
- const navigation = /** @type {Record<string, unknown> | null} */ (scene.navigation && typeof scene.navigation === 'object' ? scene.navigation : null);
636
- if (isOrbitCameraScene(scene)) {
637
- arrivalDeferred = await applyOrbitCameraScene(scene, context, event, onArrive);
638
- } else if (navigation?.transitionTo) {
639
- const travel = normalizeJourneySceneTravel(scene.travel);
640
- const explicitRoutePoints = await resolveJourneyRoutePoints(scene, travel, context);
641
- const results = explicitRoutePoints.length >= 2
642
- ? await context.actions.invoke(SKYKIT_ACTIONS.navigation.flyPolyline, {
643
- points: explicitRoutePoints,
644
- durationSecs: travel.durationSecs,
645
- arrivalThreshold: travel.arrivalThreshold,
646
- onArrive: () => {
647
- void context.actions.invoke(SKYKIT_ACTIONS.navigation.transitionTo, {
648
- ...createRouteArrivalTransitionPayload(navigation.transitionTo),
649
- onArrive,
650
- }, {
651
- source: id,
652
- });
653
- },
654
- }, {
655
- source: id,
656
- })
657
- : await context.actions.invoke(SKYKIT_ACTIONS.navigation.transitionTo, {
658
- .../** @type {Record<string, unknown>} */ (navigation.transitionTo),
659
- onArrive,
660
- }, {
661
- source: id,
662
- });
663
- arrivalDeferred = hasActionResult(results);
664
- }
665
- if (Array.isArray(scene.preloadHints)) {
666
- options.onPreloadHints?.(scene.preloadHints, scene, context);
667
- }
668
- options.onLayerState?.(scene, context);
669
- if (!arrivalDeferred) {
670
- onArrive();
671
- }
672
- }
673
-
674
- /**
675
- * @param {Record<string, unknown>} scene
676
- * @param {import('./index.d.ts').SkykitThreePluginContext} context
677
- * @param {unknown} event
678
- * @param {() => void} onArrive
679
- * @returns {Promise<boolean>} true when arrival will be reported asynchronously
680
- */
681
- async function applyOrbitCameraScene(scene, context, event, onArrive) {
682
- const camera = /** @type {Record<string, unknown>} */ (scene.camera);
683
- const destinationOrbit = await resolveJourneyOrbit(camera, context);
684
- if (!destinationOrbit) return false;
685
- const lookTarget = await resolveJourneyTarget(camera.lookAt ?? camera.center, context)
686
- ?? destinationOrbit.center;
687
- const source = { source: id };
688
- await context.actions.invoke(SKYKIT_ACTIONS.navigation.cancel, null, source);
689
- context.requestViewState({ targetPc: lookTarget }, id);
690
- await context.actions.invoke(SKYKIT_ACTIONS.navigation.lockAt, {
691
- ...lookTarget,
692
- ...(destinationOrbit.normal ? { up: destinationOrbit.normal } : {}),
693
- dwellSecs: resolveJourneyDwellSecs(camera, scene.travel),
694
- recenterSpeed: 0.06,
695
- }, source);
696
-
697
- if (isInitialJourneyEvent(event)) {
698
- const initialNormal = destinationOrbit.normal ?? { x: 0, y: 1, z: 0 };
699
- const initialObserverPc = resolveInitialSceneObserverPc(scene)
700
- ?? defaultOrbitPosition(destinationOrbit.center, destinationOrbit.radius, initialNormal);
701
- rememberResolvedJourneyOrbit(scene, {
702
- ...destinationOrbit,
703
- normal: initialNormal,
704
- });
705
- context.requestViewState({
706
- observerPc: initialObserverPc,
707
- targetPc: lookTarget,
708
- }, id);
709
- await context.actions.invoke(SKYKIT_ACTIONS.navigation.orbit, {
710
- center: destinationOrbit.center,
711
- radius: destinationOrbit.radius,
712
- angularSpeedRadPerSec: destinationOrbit.angularSpeedRadPerSec,
713
- normal: initialNormal,
714
- }, source);
715
- return false;
716
- }
717
-
718
- const travel = normalizeJourneySceneTravel(scene.travel);
719
- const explicitRoutePoints = await resolveJourneyRoutePoints(scene, travel, context);
720
- if (explicitRoutePoints.length >= 2) {
721
- const arrivalAction = normalizeJourneyOrbitAction(
722
- travel.arrivalAction ?? scene.travelPathArrivalAction,
723
- destinationOrbit,
724
- );
725
- rememberResolvedJourneyOrbit(scene, arrivalAction);
726
- const results = await context.actions.invoke(SKYKIT_ACTIONS.navigation.flyPolyline, {
727
- points: explicitRoutePoints,
728
- durationSecs: travel.durationSecs,
729
- arrivalThreshold: travel.arrivalThreshold,
730
- arrivalAction,
731
- onArrive,
732
- }, source);
733
- return hasActionResult(results);
734
- }
735
- const route = createOrbitTransferRoute({
736
- start: context.getViewState().observerPc,
737
- sourceOrbit: await resolveSourceJourneyOrbit(event, context),
738
- destinationOrbit,
739
- durationSecs: travel.durationSecs,
740
- sampleStepSecs: travel.sampleStepSecs,
741
- });
742
- if (route && Array.isArray(route.points) && route.points.length >= 2) {
743
- rememberResolvedJourneyOrbit(scene, route.arrivalAction);
744
- const results = await context.actions.invoke(SKYKIT_ACTIONS.navigation.flyPolyline, {
745
- points: route.points,
746
- durationSecs: travel.durationSecs,
747
- currentSpeed: route.departureSpeed,
748
- arrivalSpeed: route.arrivalSpeed,
749
- arrivalThreshold: travel.arrivalThreshold,
750
- arrivalAction: route.arrivalAction,
751
- onArrive,
752
- }, source);
753
- return hasActionResult(results);
754
- }
755
- const fallbackNormal = destinationOrbit.normal ?? { x: 0, y: 1, z: 0 };
756
- rememberResolvedJourneyOrbit(scene, {
757
- ...destinationOrbit,
758
- normal: fallbackNormal,
759
- });
760
- await context.actions.invoke(SKYKIT_ACTIONS.navigation.orbit, {
761
- center: destinationOrbit.center,
762
- radius: destinationOrbit.radius,
763
- angularSpeedRadPerSec: destinationOrbit.angularSpeedRadPerSec,
764
- normal: fallbackNormal,
765
- }, source);
766
- return false;
767
- }
768
-
769
- /**
770
- * @param {unknown} transitionTo
771
- * @returns {Record<string, unknown>}
772
- */
773
- function createRouteArrivalTransitionPayload(transitionTo) {
774
- const payload = /** @type {Record<string, unknown>} */ (
775
- transitionTo && typeof transitionTo === 'object' ? transitionTo : {}
776
- );
777
- return {
778
- ...payload,
779
- movement: {
780
- ...(payload.movement && typeof payload.movement === 'object'
781
- ? /** @type {Record<string, unknown>} */ (payload.movement)
782
- : {}),
783
- durationSecs: 0.001,
784
- },
785
- };
786
- }
787
-
788
- /**
789
- * @param {Record<string, unknown>} scene
790
- * @param {Record<string, unknown>} travel
791
- * @param {import('./index.d.ts').SkykitThreePluginContext} context
792
- * @returns {Promise<Vector3Like[]>}
793
- */
794
- async function resolveJourneyRoutePoints(scene, travel, context) {
795
- const rawPoints = resolveJourneyRoutePointSource(scene, travel);
796
- if (!rawPoints) return [];
797
- const points = [];
798
- for (const point of Array.from(rawPoints)) {
799
- const resolved = await resolveJourneyTarget(point, context);
800
- if (resolved) points.push(resolved);
801
- }
802
- return points;
803
- }
804
-
805
- /**
806
- * @param {Record<string, unknown>} scene
807
- * @param {Record<string, unknown>} travel
808
- * @returns {Iterable<unknown> | null}
809
- */
810
- function resolveJourneyRoutePointSource(scene, travel) {
811
- return firstIterable(
812
- scene.travelPathPc,
813
- scene.travelPath,
814
- travel.pathPointsPc,
815
- travel.pointsPc,
816
- travel.points,
817
- );
818
- }
819
-
820
- /**
821
- * @param {...unknown} candidates
822
- * @returns {Iterable<unknown> | null}
823
- */
824
- function firstIterable(...candidates) {
825
- for (const candidate of candidates) {
826
- if (
827
- candidate
828
- && typeof /** @type {{ [Symbol.iterator]?: unknown }} */ (candidate)[Symbol.iterator] === 'function'
829
- ) {
830
- return /** @type {Iterable<unknown>} */ (candidate);
831
- }
832
- }
833
- return null;
834
- }
835
-
836
- /**
837
- * @param {unknown} value
838
- * @param {{ center: Vector3Like; radius: number; angularSpeedRadPerSec: number; normal?: Vector3Like }} fallback
839
- * @returns {{ type: 'orbit'; center: Vector3Like; radius: number; angularSpeedRadPerSec: number; normal: Vector3Like }}
840
- */
841
- function normalizeJourneyOrbitAction(value, fallback) {
842
- const source = value && typeof value === 'object'
843
- ? /** @type {Record<string, unknown>} */ (value)
844
- : {};
845
- const fallbackNormal = fallback.normal ?? { x: 0, y: 1, z: 0 };
846
- return {
847
- type: 'orbit',
848
- center: normalizeOptionalVector3(source.center) ?? cloneVector3(fallback.center),
849
- radius: positiveFinite(source.radius ?? source.radiusPc, fallback.radius),
850
- angularSpeedRadPerSec: finiteNumber(
851
- source.angularSpeedRadPerSec ?? source.angularSpeed,
852
- fallback.angularSpeedRadPerSec,
853
- ),
854
- normal: normalizeDirectionVector(source.normal, fallbackNormal),
855
- };
856
- }
857
-
858
- /**
859
- * @param {Record<string, unknown>} camera
860
- * @param {import('./index.d.ts').SkykitThreePluginContext} context
861
- */
862
- async function resolveJourneyOrbit(camera, context) {
863
- const center = await resolveJourneyTarget(camera.center, context);
864
- if (!center) return null;
865
- return {
866
- center,
867
- radius: positiveFinite(camera.radiusPc, 1),
868
- angularSpeedRadPerSec: finiteNumber(camera.angularSpeedRadPerSec, 0.1),
869
- ...(camera.normal != null ? { normal: normalizeDirectionVector(camera.normal, { x: 0, y: 1, z: 0 }) } : {}),
870
- };
871
- }
872
-
873
- /**
874
- * @param {Record<string, unknown>} scene
875
- * @param {import('./index.d.ts').SkykitThreePluginContext} context
876
- * @param {unknown} event
877
- */
878
- async function notifySceneArrive(scene, context, event) {
879
- try {
880
- await options.onSceneArrive?.(scene, context, event);
881
- } catch (error) {
882
- context.emit?.({
883
- type: 'journey/scene-arrive/error',
884
- pluginId: id,
885
- message: 'Failed to handle journey scene arrival.',
886
- error,
887
- event,
888
- });
889
- }
890
- }
891
-
892
- /**
893
- * @param {unknown} input
894
- * @param {import('./index.d.ts').SkykitThreePluginContext} context
895
- */
896
- async function resolveJourneyTarget(input, context) {
897
- if (typeof input === 'string') {
898
- const targets = resolveJourneyTargets();
899
- const target = targets[input];
900
- if (target && typeof target === 'object' && target.positionPc) {
901
- return normalizeVector3(target.positionPc, { x: 0, y: 0, z: 0 });
902
- }
903
- return null;
904
- }
905
- return await resolveSpatialTarget(
906
- /** @type {import('@found-in-space/spatial').SpatialTargetInput} */ (input),
907
- { observerPc: context.getViewState().observerPc },
908
- );
909
- }
910
-
911
- function resolveJourneyTargets() {
912
- const graph = controller?.graph;
913
- if (graph && typeof graph === 'object' && 'targets' in graph) {
914
- return /** @type {Record<string, { positionPc?: unknown }>} */ (
915
- /** @type {Record<string, unknown>} */ (graph).targets ?? {}
916
- );
917
- }
918
- return /** @type {Record<string, { positionPc?: unknown }>} */ (options.journey?.targets ?? {});
919
- }
920
-
921
- /**
922
- * @param {unknown} event
923
- * @param {import('./index.d.ts').SkykitThreePluginContext} context
924
- */
925
- async function resolveSourceJourneyOrbit(event, context) {
926
- const previousSceneId = event && typeof event === 'object'
927
- ? /** @type {{ previousSceneId?: unknown }} */ (event).previousSceneId
928
- : null;
929
- if (typeof previousSceneId !== 'string') return null;
930
- const resolved = resolvedJourneyOrbits.get(previousSceneId);
931
- if (resolved) return resolved;
932
- const previousScene = controller?.graph.getScene(previousSceneId);
933
- const camera = previousScene?.camera && typeof previousScene.camera === 'object'
934
- ? /** @type {Record<string, unknown>} */ (previousScene.camera)
935
- : null;
936
- return camera && camera.type === 'orbit' ? resolveJourneyOrbit(camera, context) : null;
937
- }
938
-
939
- /** @param {Record<string, unknown>} scene */
940
- function resolveInitialSceneObserverPc(scene) {
941
- const view = scene.view && typeof scene.view === 'object'
942
- ? /** @type {{ observerPc?: unknown }} */ (scene.view)
943
- : null;
944
- return normalizeOptionalVector3(view?.observerPc);
945
- }
946
-
947
- /**
948
- * @param {Record<string, unknown>} scene
949
- * @param {{ center: Vector3Like; radius: number; angularSpeedRadPerSec: number; normal: Vector3Like }} orbit
950
- */
951
- function rememberResolvedJourneyOrbit(scene, orbit) {
952
- const sceneId = typeof scene.sceneId === 'string' ? scene.sceneId : null;
953
- if (!sceneId) return;
954
- resolvedJourneyOrbits.set(sceneId, {
955
- center: cloneVector3(orbit.center),
956
- radius: orbit.radius,
957
- angularSpeedRadPerSec: orbit.angularSpeedRadPerSec,
958
- normal: cloneVector3(orbit.normal),
959
- });
960
- }
961
-
962
- /**
963
- * @param {import('@found-in-space/journey').TimedJourneyFrame} frameState
964
- * @param {{ viewer: import('./index.d.ts').SkykitViewer; view: import('./index.d.ts').SkykitViewState }} frame
965
- */
966
- function applyTimedFrame(frameState, frame) {
967
- const context = pluginContext;
968
- const hookResult = options.applyFrame?.(frameState, context, frame);
969
- if (hookResult === false) return;
970
- frame.viewer.requestViewState({
971
- observerPc: frameState.observerPc,
972
- orientationIcrs: frameState.orientationIcrs,
973
- targetPc: frameState.targetPc,
974
- motion: {
975
- velocityPcPerSec: frameState.velocityPcPerSec,
976
- speedPcPerSec: frameState.speedPcPerSec,
977
- },
978
- }, id);
979
- if (frameState.cue && frameState.cue.id !== lastCueId) {
980
- lastCueId = frameState.cue.id;
981
- options.onCue?.(frameState.cue, frameState, context);
982
- } else if (!frameState.cue) {
983
- lastCueId = null;
984
- }
985
- }
986
-
987
- function getSnapshot() {
988
- return {
989
- id,
990
- disposed,
991
- playing,
992
- currentTimeSecs,
993
- initialSceneApplied,
994
- timedPreloadHintsEmitted,
995
- controller: controller?.getSnapshot?.() ?? null,
996
- timedJourney: evaluator
997
- ? {
998
- durationSecs: evaluator.durationSecs,
999
- journeyId: evaluator.journey.id,
1000
- }
1001
- : null,
1002
- };
1003
- }
1004
- }
1005
405
 
1006
406
  /**
1007
407
  * Strategy-only convenience helper. View-bound lookahead hints stay in preload
@@ -1594,108 +994,6 @@ function getDefaultEventTarget() {
1594
994
  return typeof globalThis.addEventListener === 'function' ? globalThis : null;
1595
995
  }
1596
996
 
1597
- /** @param {Record<string, unknown>} scene */
1598
- function isOrbitCameraScene(scene) {
1599
- return Boolean(
1600
- scene.camera
1601
- && typeof scene.camera === 'object'
1602
- && /** @type {{ type?: unknown }} */ (scene.camera).type === 'orbit',
1603
- );
1604
- }
1605
-
1606
- /** @param {unknown} event */
1607
- function isInitialJourneyEvent(event) {
1608
- return Boolean(
1609
- event
1610
- && typeof event === 'object'
1611
- && /** @type {{ type?: unknown; previousSceneId?: unknown }} */ (event).type === 'journey/initial',
1612
- );
1613
- }
1614
-
1615
- /**
1616
- * @param {unknown} value
1617
- * @returns {Record<string, unknown> & { durationSecs: number; sampleStepSecs: number; arrivalThreshold: number }}
1618
- */
1619
- function normalizeJourneySceneTravel(value) {
1620
- const source = /** @type {Record<string, unknown>} */ (
1621
- value && typeof value === 'object' ? value : {}
1622
- );
1623
- return {
1624
- ...source,
1625
- durationSecs: positiveFinite(source.durationSecs, 5),
1626
- sampleStepSecs: positiveFinite(source.sampleStepSecs, 1 / 60),
1627
- arrivalThreshold: positiveFinite(source.arrivalThreshold, 0.05),
1628
- };
1629
- }
1630
-
1631
- /**
1632
- * @param {Record<string, unknown>} camera
1633
- * @param {unknown} travel
1634
- */
1635
- function resolveJourneyDwellSecs(camera, travel) {
1636
- if (camera.dwellSecs != null) return Math.max(0, finiteNumber(camera.dwellSecs, 0));
1637
- if (travel && typeof travel === 'object' && 'dwellSecs' in travel) {
1638
- return Math.max(0, finiteNumber(/** @type {{ dwellSecs?: unknown }} */ (travel).dwellSecs, 0));
1639
- }
1640
- return 0;
1641
- }
1642
-
1643
- /**
1644
- * @param {Vector3Like} center
1645
- * @param {number} radius
1646
- * @param {Vector3Like} normal
1647
- * @returns {Vector3Like}
1648
- */
1649
- function defaultOrbitPosition(center, radius, normal) {
1650
- const axis = Math.abs(normal.x) < 0.9 ? { x: 1, y: 0, z: 0 } : { x: 0, y: 1, z: 0 };
1651
- const projected = projectOnPlane(axis, normal);
1652
- const length = Math.hypot(projected.x, projected.y, projected.z);
1653
- const direction = length > 1e-9
1654
- ? { x: projected.x / length, y: projected.y / length, z: projected.z / length }
1655
- : { x: 1, y: 0, z: 0 };
1656
- return {
1657
- x: center.x + direction.x * radius,
1658
- y: center.y + direction.y * radius,
1659
- z: center.z + direction.z * radius,
1660
- };
1661
- }
1662
-
1663
- /**
1664
- * @param {unknown} value
1665
- * @param {Vector3Like} fallback
1666
- * @returns {Vector3Like}
1667
- */
1668
- function normalizeDirectionVector(value, fallback) {
1669
- const vector = normalizeVector3(value, fallback);
1670
- const length = Math.hypot(vector.x, vector.y, vector.z);
1671
- return length > 1e-9
1672
- ? { x: vector.x / length, y: vector.y / length, z: vector.z / length }
1673
- : cloneVector3(fallback);
1674
- }
1675
-
1676
- /** @param {unknown} value */
1677
- function normalizeOptionalVector3(value) {
1678
- if (!value || typeof value !== 'object') return null;
1679
- const vector = /** @type {{ x?: unknown; y?: unknown; z?: unknown }} */ (value);
1680
- const x = Number(vector.x);
1681
- const y = Number(vector.y);
1682
- const z = Number(vector.z);
1683
- return [x, y, z].every(Number.isFinite) ? { x, y, z } : null;
1684
- }
1685
-
1686
- /**
1687
- * @param {Vector3Like} vector
1688
- * @param {Vector3Like} normal
1689
- * @returns {Vector3Like}
1690
- */
1691
- function projectOnPlane(vector, normal) {
1692
- const amount = vector.x * normal.x + vector.y * normal.y + vector.z * normal.z;
1693
- return {
1694
- x: vector.x - normal.x * amount,
1695
- y: vector.y - normal.y * amount,
1696
- z: vector.z - normal.z * amount,
1697
- };
1698
- }
1699
997
 
1700
998
  /**
1701
999
  * @param {unknown} payload
@@ -1734,11 +1032,6 @@ function resolveOnArrive(payload) {
1734
1032
  : null;
1735
1033
  }
1736
1034
 
1737
- /** @param {PromiseSettledResult<unknown>[]} results */
1738
- function hasActionResult(results) {
1739
- return results.some((result) => result.status === 'fulfilled' && result.value != null);
1740
- }
1741
-
1742
1035
  /** @param {Record<string, unknown>} targetSource */
1743
1036
  function resolveTransitionPositionInput(targetSource) {
1744
1037
  if (targetSource.observerPc !== undefined) return targetSource.observerPc;
@@ -1761,29 +1054,6 @@ function isSpatialTargetLike(value) {
1761
1054
  return Array.isArray(value) && value.length >= 3;
1762
1055
  }
1763
1056
 
1764
- /** @param {unknown} payload */
1765
- function resolveSceneId(payload) {
1766
- if (typeof payload === 'string') return payload;
1767
- if (!payload || typeof payload !== 'object') return null;
1768
- const source = /** @type {Record<string, unknown>} */ (payload);
1769
- const value = source.chapterId ?? source.sceneId ?? source.id;
1770
- return typeof value === 'string' ? value : null;
1771
- }
1772
-
1773
- /** @param {unknown} payload */
1774
- function resolveTimeSecs(payload) {
1775
- if (Number.isFinite(Number(payload))) return Number(payload);
1776
- if (!payload || typeof payload !== 'object') return 0;
1777
- const value = /** @type {Record<string, unknown>} */ (payload).timeSecs
1778
- ?? /** @type {Record<string, unknown>} */ (payload).sceneTimeSecs
1779
- ?? /** @type {Record<string, unknown>} */ (payload).time;
1780
- return finiteNumber(value, 0);
1781
- }
1782
-
1783
- /** @param {number} timeSecs @param {number} durationSecs */
1784
- function clampTime(timeSecs, durationSecs) {
1785
- return Math.min(Math.max(0, finiteNumber(timeSecs, 0)), Math.max(0, finiteNumber(durationSecs, 0)));
1786
- }
1787
1057
 
1788
1058
  /**
1789
1059
  * @param {unknown} value