@found-in-space/skykit 0.2.0-alpha.1 → 0.2.0-alpha.20260528

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.
Files changed (40) hide show
  1. package/README.md +142 -11
  2. package/examples/custom-object-layer/custom-object-layer.js +1 -24
  3. package/examples/xr-free-roam/index.html +62 -4
  4. package/examples/xr-free-roam/xr-free-roam.css +249 -18
  5. package/examples/xr-free-roam/xr-free-roam.js +675 -274
  6. package/package.json +22 -6
  7. package/src/__tests__/skykit-anchored-images.test.js +32 -4
  8. package/src/__tests__/skykit-browser.test.js +267 -0
  9. package/src/__tests__/skykit-data.test.js +131 -0
  10. package/src/__tests__/skykit-parallax.test.js +4 -4
  11. package/src/__tests__/skykit-touch-os.test.js +71 -0
  12. package/src/__tests__/skykit-xr.test.js +179 -2
  13. package/src/__tests__/skykit.test.js +142 -506
  14. package/src/actions.js +0 -8
  15. package/src/anchored-images.js +14 -15
  16. package/src/browser-addons.d.ts +16 -0
  17. package/src/browser-addons.js +155 -0
  18. package/src/browser-constellations.d.ts +13 -0
  19. package/src/browser-constellations.js +387 -0
  20. package/src/browser.d.ts +81 -0
  21. package/src/browser.js +192 -13
  22. package/src/data.d.ts +133 -0
  23. package/src/data.js +447 -0
  24. package/src/embed.d.ts +5 -0
  25. package/src/embed.js +53 -2
  26. package/src/hr-diagram.js +23 -5
  27. package/src/index.d.ts +21 -73
  28. package/src/index.js +0 -1
  29. package/src/plugins.js +22 -708
  30. package/src/three-shim.d.ts +32 -0
  31. package/src/touch-os.d.ts +70 -0
  32. package/src/touch-os.js +275 -0
  33. package/src/utils.js +96 -6
  34. package/src/viewer-entry.d.ts +10 -0
  35. package/src/viewer-entry.js +4 -0
  36. package/src/viewer.js +110 -12
  37. package/src/xr/plugins.js +298 -13
  38. package/src/xr/session.js +60 -14
  39. package/src/xr.d.ts +40 -0
  40. package/src/xr.js +2 -0
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,9 +11,9 @@ import {
16
11
 
17
12
  import {
18
13
  IDENTITY_QUATERNION as SPATIAL_IDENTITY_QUATERNION,
19
- createOrbitTransferRoute,
20
14
  createSpatialPoseTransition,
21
15
  createSpatialNavigationAutomation,
16
+ resolveSpatialLookAt,
22
17
  resolveSpatialTarget,
23
18
  } from '@found-in-space/spatial';
24
19
 
@@ -219,8 +214,12 @@ export function createSkykitNavigationPlugin(options = {}) {
219
214
  id,
220
215
  setup(context) {
221
216
  const threeContext = /** @type {import('./index.d.ts').SkykitThreePluginContext} */ (context);
222
- threeContext.addPart(part);
223
- threeContext.addDisposable(registerNavigationActions(threeContext));
217
+ const removePart = threeContext.addPart(part);
218
+ const unregisterActions = registerNavigationActions(threeContext);
219
+ return () => {
220
+ unregisterActions();
221
+ removePart();
222
+ };
224
223
  },
225
224
  getSnapshot: () => part.getSnapshot?.() ?? null,
226
225
  };
@@ -366,10 +365,24 @@ export function createSkykitNavigationPlugin(options = {}) {
366
365
  const position = positionInput === undefined
367
366
  ? current.observerPc
368
367
  : await resolveTarget(positionInput, context) ?? current.observerPc;
368
+ const lookAtInput = targetSource.lookAt;
369
+ const resolvedLook = lookAtInput !== undefined
370
+ ? await resolveSpatialLookAt(lookAtInput, {
371
+ observerPc: position,
372
+ resolveBookmark: typeof options.resolveBookmark === 'function'
373
+ ? (bookmarkId, original) => options.resolveBookmark?.(
374
+ bookmarkId,
375
+ /** @type {import('@found-in-space/spatial').SpatialTargetInput} */ (original),
376
+ context,
377
+ ) ?? null
378
+ : undefined,
379
+ })
380
+ : null;
369
381
  const orientationInput = targetSource.orientationIcrs
370
382
  ?? (targetSource.orientation && isQuaternionLike(targetSource.orientation) ? targetSource.orientation : undefined)
371
383
  ?? source.orientationIcrs;
372
- const orientation = normalizeQuaternion(orientationInput, current.orientationIcrs ?? IDENTITY_QUATERNION);
384
+ const orientation = resolvedLook?.orientationIcrs
385
+ ?? normalizeQuaternion(orientationInput, current.orientationIcrs ?? IDENTITY_QUATERNION);
373
386
  return createSpatialPoseTransition({
374
387
  from: {
375
388
  position: current.observerPc,
@@ -389,575 +402,6 @@ export function createSkykitNavigationPlugin(options = {}) {
389
402
  }
390
403
  }
391
404
 
392
- /**
393
- * @param {import('./index.d.ts').SkykitJourneyPluginOptions} [options]
394
- * @returns {SkykitPlugin & { getSnapshot(): unknown }}
395
- */
396
- export function createSkykitJourneyPlugin(options = {}) {
397
- const id = options.id ?? 'journey';
398
- const controller = options.controller
399
- ?? (options.journey
400
- ? createJourneyController({ graph: options.journey })
401
- : (options.graph || options.scenes ? createJourneyController(options) : null));
402
- const evaluator = options.evaluator
403
- ?? (options.timedJourney ? createTimedJourneyEvaluator(options.timedJourney, options.evaluatorOptions) : null);
404
- let disposed = false;
405
- let playing = options.autoPlay === true;
406
- let currentTimeSecs = Math.max(0, finiteNumber(options.startTimeSecs, 0));
407
- let lastCueId = /** @type {string | null} */ (null);
408
- let initialSceneApplied = false;
409
- let timedPreloadHintsEmitted = false;
410
- /** @type {import('./index.d.ts').SkykitThreePluginContext | null} */
411
- let pluginContext = null;
412
- const resolvedJourneyOrbits = new Map();
413
- /** @type {(() => void) | null} */
414
- let unsubscribeController = null;
415
- /** @type {Promise<unknown>} */
416
- let pendingSceneApplication = Promise.resolve(null);
417
-
418
- /** @type {SkykitThreePart} */
419
- const part = {
420
- id,
421
- priority: options.priority,
422
- attach(context) {
423
- pluginContext = context;
424
- if (controller) {
425
- unsubscribeController = controller.subscribe((event) => {
426
- queueSceneApplication(event.spec, context, event);
427
- });
428
- applyInitialScene(context);
429
- }
430
- emitTimedPreloadHints(context, 'attach');
431
- },
432
- update(frame) {
433
- if (disposed || !evaluator || !playing) return;
434
- currentTimeSecs = Math.min(
435
- evaluator.durationSecs,
436
- currentTimeSecs + Math.max(0, finiteNumber(frame.deltaSeconds, 0)),
437
- );
438
- applyTimedFrame(evaluator.evaluate(currentTimeSecs), frame);
439
- if (currentTimeSecs >= evaluator.durationSecs) {
440
- playing = options.loop === true;
441
- currentTimeSecs = playing ? 0 : evaluator.durationSecs;
442
- }
443
- },
444
- detach() {
445
- unsubscribeController?.();
446
- unsubscribeController = null;
447
- pluginContext = null;
448
- },
449
- dispose() {
450
- disposed = true;
451
- this.detach?.();
452
- if (options.disposeController !== false) controller?.dispose?.();
453
- },
454
- getSnapshot,
455
- };
456
-
457
- return {
458
- id,
459
- setup(context) {
460
- const threeContext = /** @type {import('./index.d.ts').SkykitThreePluginContext} */ (context);
461
- threeContext.addPart(part);
462
- threeContext.addDisposable(registerJourneyActions(threeContext));
463
- },
464
- getSnapshot,
465
- };
466
-
467
- /** @param {import('./index.d.ts').SkykitThreePluginContext} context */
468
- function registerJourneyActions(context) {
469
- const unregisters = [
470
- context.actions.registerAction(SKYKIT_ACTIONS.journey.goToChapter, async ({ payload }) => {
471
- await pendingSceneApplication;
472
- const sceneId = resolveSceneId(payload);
473
- const spec = sceneId && controller ? controller.goTo(sceneId, { source: SKYKIT_ACTIONS.journey.goToChapter }) : null;
474
- await pendingSceneApplication;
475
- return spec;
476
- }, { label: 'Go to journey chapter' }),
477
- context.actions.registerAction(SKYKIT_ACTIONS.journey.next, async () => {
478
- await pendingSceneApplication;
479
- const spec = controller?.next({ source: SKYKIT_ACTIONS.journey.next }) ?? null;
480
- await pendingSceneApplication;
481
- return spec;
482
- }, {
483
- label: 'Next journey chapter',
484
- }),
485
- context.actions.registerAction(SKYKIT_ACTIONS.journey.previous, async () => {
486
- await pendingSceneApplication;
487
- const spec = controller?.previous({ source: SKYKIT_ACTIONS.journey.previous }) ?? null;
488
- await pendingSceneApplication;
489
- return spec;
490
- }, {
491
- label: 'Previous journey chapter',
492
- }),
493
- context.actions.registerAction(SKYKIT_ACTIONS.journey.seek, ({ payload }) => {
494
- currentTimeSecs = clampTime(resolveTimeSecs(payload), evaluator?.durationSecs ?? Number.POSITIVE_INFINITY);
495
- if (evaluator) applyTimedFrame(evaluator.evaluate(currentTimeSecs), { viewer: context.viewer, view: context.getViewState() });
496
- emitTimedPreloadHints(context, 'seek');
497
- return currentTimeSecs;
498
- }, { label: 'Seek journey time' }),
499
- context.actions.registerAction(SKYKIT_ACTIONS.journey.play, ({ payload }) => {
500
- if (payload && typeof payload === 'object' && 'timeSecs' in payload) {
501
- currentTimeSecs = clampTime(resolveTimeSecs(payload), evaluator?.durationSecs ?? Number.POSITIVE_INFINITY);
502
- }
503
- playing = true;
504
- emitTimedPreloadHints(context, 'play');
505
- return currentTimeSecs;
506
- }, { label: 'Play journey' }),
507
- context.actions.registerAction(SKYKIT_ACTIONS.journey.pause, () => {
508
- playing = false;
509
- return currentTimeSecs;
510
- }, { label: 'Pause journey' }),
511
- ];
512
- return () => {
513
- for (const unregister of unregisters.reverse()) unregister();
514
- };
515
- }
516
-
517
- /** @param {import('./index.d.ts').SkykitThreePluginContext} context */
518
- function applyInitialScene(context) {
519
- if (!controller || initialSceneApplied) return;
520
- const snapshot = controller.getSnapshot();
521
- const sceneId = snapshot.activeSceneId;
522
- if (!sceneId) {
523
- initialSceneApplied = true;
524
- return;
525
- }
526
- initialSceneApplied = true;
527
- const spec = controller.graph.resolveSceneSpec(sceneId, { fromSceneId: snapshot.previousSceneId });
528
- queueSceneApplication(spec, context, {
529
- type: 'journey/initial',
530
- sceneId,
531
- previousSceneId: snapshot.previousSceneId,
532
- source: 'attach',
533
- spec,
534
- });
535
- }
536
-
537
- /**
538
- * @param {unknown} spec
539
- * @param {import('./index.d.ts').SkykitThreePluginContext} context
540
- * @param {unknown} event
541
- */
542
- function queueSceneApplication(spec, context, event) {
543
- pendingSceneApplication = Promise.resolve(applySceneSpec(spec, context, event)).catch((error) => {
544
- context.emit?.({
545
- type: 'journey/scene/error',
546
- pluginId: id,
547
- message: 'Failed to apply journey scene.',
548
- error,
549
- event,
550
- });
551
- return null;
552
- });
553
- return pendingSceneApplication;
554
- }
555
-
556
- /**
557
- * @param {import('./index.d.ts').SkykitThreePluginContext} context
558
- * @param {string} source
559
- */
560
- function emitTimedPreloadHints(context, source) {
561
- if (!evaluator || timedPreloadHintsEmitted) return;
562
- const preloadHints = typeof evaluator.getPreloadHints === 'function'
563
- ? evaluator.getPreloadHints()
564
- : evaluator.evaluate(currentTimeSecs).preloadHints;
565
- timedPreloadHintsEmitted = true;
566
- if (preloadHints.length > 0) {
567
- options.onPreloadHints?.(preloadHints, {
568
- type: 'journey/timed-preload',
569
- journeyId: evaluator.journey.id,
570
- source,
571
- }, context);
572
- }
573
- }
574
-
575
- /**
576
- * @param {unknown} spec
577
- * @param {import('./index.d.ts').SkykitThreePluginContext} context
578
- * @param {unknown} event
579
- */
580
- async function applySceneSpec(spec, context, event) {
581
- const scene = /** @type {Record<string, unknown> | null} */ (spec && typeof spec === 'object' ? spec : null);
582
- options.onScene?.(scene, context, event);
583
- if (!scene) return;
584
- const onArrive = () => {
585
- void notifySceneArrive(scene, context, event);
586
- };
587
- let arrivalDeferred = false;
588
- if (scene.view && typeof scene.view === 'object') {
589
- context.requestViewState(/** @type {Partial<import('./index.d.ts').SkykitViewState>} */ (scene.view), id);
590
- }
591
- const navigation = /** @type {Record<string, unknown> | null} */ (scene.navigation && typeof scene.navigation === 'object' ? scene.navigation : null);
592
- if (isOrbitCameraScene(scene)) {
593
- arrivalDeferred = await applyOrbitCameraScene(scene, context, event, onArrive);
594
- } else if (navigation?.transitionTo) {
595
- const travel = normalizeJourneySceneTravel(scene.travel);
596
- const explicitRoutePoints = await resolveJourneyRoutePoints(scene, travel, context);
597
- const results = explicitRoutePoints.length >= 2
598
- ? await context.actions.invoke(SKYKIT_ACTIONS.navigation.flyPolyline, {
599
- points: explicitRoutePoints,
600
- durationSecs: travel.durationSecs,
601
- arrivalThreshold: travel.arrivalThreshold,
602
- onArrive: () => {
603
- void context.actions.invoke(SKYKIT_ACTIONS.navigation.transitionTo, {
604
- ...createRouteArrivalTransitionPayload(navigation.transitionTo),
605
- onArrive,
606
- }, {
607
- source: id,
608
- });
609
- },
610
- }, {
611
- source: id,
612
- })
613
- : await context.actions.invoke(SKYKIT_ACTIONS.navigation.transitionTo, {
614
- .../** @type {Record<string, unknown>} */ (navigation.transitionTo),
615
- onArrive,
616
- }, {
617
- source: id,
618
- });
619
- arrivalDeferred = hasActionResult(results);
620
- }
621
- if (Array.isArray(scene.preloadHints)) {
622
- options.onPreloadHints?.(scene.preloadHints, scene, context);
623
- }
624
- options.onLayerState?.(scene, context);
625
- if (!arrivalDeferred) {
626
- onArrive();
627
- }
628
- }
629
-
630
- /**
631
- * @param {Record<string, unknown>} scene
632
- * @param {import('./index.d.ts').SkykitThreePluginContext} context
633
- * @param {unknown} event
634
- * @param {() => void} onArrive
635
- * @returns {Promise<boolean>} true when arrival will be reported asynchronously
636
- */
637
- async function applyOrbitCameraScene(scene, context, event, onArrive) {
638
- const camera = /** @type {Record<string, unknown>} */ (scene.camera);
639
- const destinationOrbit = await resolveJourneyOrbit(camera, context);
640
- if (!destinationOrbit) return false;
641
- const lookTarget = await resolveJourneyTarget(camera.lookAt ?? camera.center, context)
642
- ?? destinationOrbit.center;
643
- const source = { source: id };
644
- await context.actions.invoke(SKYKIT_ACTIONS.navigation.cancel, null, source);
645
- context.requestViewState({ targetPc: lookTarget }, id);
646
- await context.actions.invoke(SKYKIT_ACTIONS.navigation.lockAt, {
647
- ...lookTarget,
648
- ...(destinationOrbit.normal ? { up: destinationOrbit.normal } : {}),
649
- dwellSecs: resolveJourneyDwellSecs(camera, scene.travel),
650
- recenterSpeed: 0.06,
651
- }, source);
652
-
653
- if (isInitialJourneyEvent(event)) {
654
- const initialNormal = destinationOrbit.normal ?? { x: 0, y: 1, z: 0 };
655
- const initialObserverPc = resolveInitialSceneObserverPc(scene)
656
- ?? defaultOrbitPosition(destinationOrbit.center, destinationOrbit.radius, initialNormal);
657
- rememberResolvedJourneyOrbit(scene, {
658
- ...destinationOrbit,
659
- normal: initialNormal,
660
- });
661
- context.requestViewState({
662
- observerPc: initialObserverPc,
663
- targetPc: lookTarget,
664
- }, id);
665
- await context.actions.invoke(SKYKIT_ACTIONS.navigation.orbit, {
666
- center: destinationOrbit.center,
667
- radius: destinationOrbit.radius,
668
- angularSpeedRadPerSec: destinationOrbit.angularSpeedRadPerSec,
669
- normal: initialNormal,
670
- }, source);
671
- return false;
672
- }
673
-
674
- const travel = normalizeJourneySceneTravel(scene.travel);
675
- const explicitRoutePoints = await resolveJourneyRoutePoints(scene, travel, context);
676
- if (explicitRoutePoints.length >= 2) {
677
- const arrivalAction = normalizeJourneyOrbitAction(
678
- travel.arrivalAction ?? scene.travelPathArrivalAction,
679
- destinationOrbit,
680
- );
681
- rememberResolvedJourneyOrbit(scene, arrivalAction);
682
- const results = await context.actions.invoke(SKYKIT_ACTIONS.navigation.flyPolyline, {
683
- points: explicitRoutePoints,
684
- durationSecs: travel.durationSecs,
685
- arrivalThreshold: travel.arrivalThreshold,
686
- arrivalAction,
687
- onArrive,
688
- }, source);
689
- return hasActionResult(results);
690
- }
691
- const route = createOrbitTransferRoute({
692
- start: context.getViewState().observerPc,
693
- sourceOrbit: await resolveSourceJourneyOrbit(event, context),
694
- destinationOrbit,
695
- durationSecs: travel.durationSecs,
696
- sampleStepSecs: travel.sampleStepSecs,
697
- });
698
- if (route && Array.isArray(route.points) && route.points.length >= 2) {
699
- rememberResolvedJourneyOrbit(scene, route.arrivalAction);
700
- const results = await context.actions.invoke(SKYKIT_ACTIONS.navigation.flyPolyline, {
701
- points: route.points,
702
- durationSecs: travel.durationSecs,
703
- currentSpeed: route.departureSpeed,
704
- arrivalSpeed: route.arrivalSpeed,
705
- arrivalThreshold: travel.arrivalThreshold,
706
- arrivalAction: route.arrivalAction,
707
- onArrive,
708
- }, source);
709
- return hasActionResult(results);
710
- }
711
- const fallbackNormal = destinationOrbit.normal ?? { x: 0, y: 1, z: 0 };
712
- rememberResolvedJourneyOrbit(scene, {
713
- ...destinationOrbit,
714
- normal: fallbackNormal,
715
- });
716
- await context.actions.invoke(SKYKIT_ACTIONS.navigation.orbit, {
717
- center: destinationOrbit.center,
718
- radius: destinationOrbit.radius,
719
- angularSpeedRadPerSec: destinationOrbit.angularSpeedRadPerSec,
720
- normal: fallbackNormal,
721
- }, source);
722
- return false;
723
- }
724
-
725
- /**
726
- * @param {unknown} transitionTo
727
- * @returns {Record<string, unknown>}
728
- */
729
- function createRouteArrivalTransitionPayload(transitionTo) {
730
- const payload = /** @type {Record<string, unknown>} */ (
731
- transitionTo && typeof transitionTo === 'object' ? transitionTo : {}
732
- );
733
- return {
734
- ...payload,
735
- movement: {
736
- ...(payload.movement && typeof payload.movement === 'object'
737
- ? /** @type {Record<string, unknown>} */ (payload.movement)
738
- : {}),
739
- durationSecs: 0.001,
740
- },
741
- };
742
- }
743
-
744
- /**
745
- * @param {Record<string, unknown>} scene
746
- * @param {Record<string, unknown>} travel
747
- * @param {import('./index.d.ts').SkykitThreePluginContext} context
748
- * @returns {Promise<Vector3Like[]>}
749
- */
750
- async function resolveJourneyRoutePoints(scene, travel, context) {
751
- const rawPoints = resolveJourneyRoutePointSource(scene, travel);
752
- if (!rawPoints) return [];
753
- const points = [];
754
- for (const point of Array.from(rawPoints)) {
755
- const resolved = await resolveJourneyTarget(point, context);
756
- if (resolved) points.push(resolved);
757
- }
758
- return points;
759
- }
760
-
761
- /**
762
- * @param {Record<string, unknown>} scene
763
- * @param {Record<string, unknown>} travel
764
- * @returns {Iterable<unknown> | null}
765
- */
766
- function resolveJourneyRoutePointSource(scene, travel) {
767
- return firstIterable(
768
- scene.travelPathPc,
769
- scene.travelPath,
770
- travel.pathPointsPc,
771
- travel.pointsPc,
772
- travel.points,
773
- );
774
- }
775
-
776
- /**
777
- * @param {...unknown} candidates
778
- * @returns {Iterable<unknown> | null}
779
- */
780
- function firstIterable(...candidates) {
781
- for (const candidate of candidates) {
782
- if (
783
- candidate
784
- && typeof /** @type {{ [Symbol.iterator]?: unknown }} */ (candidate)[Symbol.iterator] === 'function'
785
- ) {
786
- return /** @type {Iterable<unknown>} */ (candidate);
787
- }
788
- }
789
- return null;
790
- }
791
-
792
- /**
793
- * @param {unknown} value
794
- * @param {{ center: Vector3Like; radius: number; angularSpeedRadPerSec: number; normal?: Vector3Like }} fallback
795
- * @returns {{ type: 'orbit'; center: Vector3Like; radius: number; angularSpeedRadPerSec: number; normal: Vector3Like }}
796
- */
797
- function normalizeJourneyOrbitAction(value, fallback) {
798
- const source = value && typeof value === 'object'
799
- ? /** @type {Record<string, unknown>} */ (value)
800
- : {};
801
- const fallbackNormal = fallback.normal ?? { x: 0, y: 1, z: 0 };
802
- return {
803
- type: 'orbit',
804
- center: normalizeOptionalVector3(source.center) ?? cloneVector3(fallback.center),
805
- radius: positiveFinite(source.radius ?? source.radiusPc, fallback.radius),
806
- angularSpeedRadPerSec: finiteNumber(
807
- source.angularSpeedRadPerSec ?? source.angularSpeed,
808
- fallback.angularSpeedRadPerSec,
809
- ),
810
- normal: normalizeDirectionVector(source.normal, fallbackNormal),
811
- };
812
- }
813
-
814
- /**
815
- * @param {Record<string, unknown>} camera
816
- * @param {import('./index.d.ts').SkykitThreePluginContext} context
817
- */
818
- async function resolveJourneyOrbit(camera, context) {
819
- const center = await resolveJourneyTarget(camera.center, context);
820
- if (!center) return null;
821
- return {
822
- center,
823
- radius: positiveFinite(camera.radiusPc, 1),
824
- angularSpeedRadPerSec: finiteNumber(camera.angularSpeedRadPerSec, 0.1),
825
- ...(camera.normal != null ? { normal: normalizeDirectionVector(camera.normal, { x: 0, y: 1, z: 0 }) } : {}),
826
- };
827
- }
828
-
829
- /**
830
- * @param {Record<string, unknown>} scene
831
- * @param {import('./index.d.ts').SkykitThreePluginContext} context
832
- * @param {unknown} event
833
- */
834
- async function notifySceneArrive(scene, context, event) {
835
- try {
836
- await options.onSceneArrive?.(scene, context, event);
837
- } catch (error) {
838
- context.emit?.({
839
- type: 'journey/scene-arrive/error',
840
- pluginId: id,
841
- message: 'Failed to handle journey scene arrival.',
842
- error,
843
- event,
844
- });
845
- }
846
- }
847
-
848
- /**
849
- * @param {unknown} input
850
- * @param {import('./index.d.ts').SkykitThreePluginContext} context
851
- */
852
- async function resolveJourneyTarget(input, context) {
853
- if (typeof input === 'string') {
854
- const targets = resolveJourneyTargets();
855
- const target = targets[input];
856
- if (target && typeof target === 'object' && target.positionPc) {
857
- return normalizeVector3(target.positionPc, { x: 0, y: 0, z: 0 });
858
- }
859
- return null;
860
- }
861
- return await resolveSpatialTarget(
862
- /** @type {import('@found-in-space/spatial').SpatialTargetInput} */ (input),
863
- { observerPc: context.getViewState().observerPc },
864
- );
865
- }
866
-
867
- function resolveJourneyTargets() {
868
- const graph = controller?.graph;
869
- if (graph && typeof graph === 'object' && 'targets' in graph) {
870
- return /** @type {Record<string, { positionPc?: unknown }>} */ (
871
- /** @type {Record<string, unknown>} */ (graph).targets ?? {}
872
- );
873
- }
874
- return /** @type {Record<string, { positionPc?: unknown }>} */ (options.journey?.targets ?? {});
875
- }
876
-
877
- /**
878
- * @param {unknown} event
879
- * @param {import('./index.d.ts').SkykitThreePluginContext} context
880
- */
881
- async function resolveSourceJourneyOrbit(event, context) {
882
- const previousSceneId = event && typeof event === 'object'
883
- ? /** @type {{ previousSceneId?: unknown }} */ (event).previousSceneId
884
- : null;
885
- if (typeof previousSceneId !== 'string') return null;
886
- const resolved = resolvedJourneyOrbits.get(previousSceneId);
887
- if (resolved) return resolved;
888
- const previousScene = controller?.graph.getScene(previousSceneId);
889
- const camera = previousScene?.camera && typeof previousScene.camera === 'object'
890
- ? /** @type {Record<string, unknown>} */ (previousScene.camera)
891
- : null;
892
- return camera && camera.type === 'orbit' ? resolveJourneyOrbit(camera, context) : null;
893
- }
894
-
895
- /** @param {Record<string, unknown>} scene */
896
- function resolveInitialSceneObserverPc(scene) {
897
- const view = scene.view && typeof scene.view === 'object'
898
- ? /** @type {{ observerPc?: unknown }} */ (scene.view)
899
- : null;
900
- return normalizeOptionalVector3(view?.observerPc);
901
- }
902
-
903
- /**
904
- * @param {Record<string, unknown>} scene
905
- * @param {{ center: Vector3Like; radius: number; angularSpeedRadPerSec: number; normal: Vector3Like }} orbit
906
- */
907
- function rememberResolvedJourneyOrbit(scene, orbit) {
908
- const sceneId = typeof scene.sceneId === 'string' ? scene.sceneId : null;
909
- if (!sceneId) return;
910
- resolvedJourneyOrbits.set(sceneId, {
911
- center: cloneVector3(orbit.center),
912
- radius: orbit.radius,
913
- angularSpeedRadPerSec: orbit.angularSpeedRadPerSec,
914
- normal: cloneVector3(orbit.normal),
915
- });
916
- }
917
-
918
- /**
919
- * @param {import('@found-in-space/journey').TimedJourneyFrame} frameState
920
- * @param {{ viewer: import('./index.d.ts').SkykitViewer; view: import('./index.d.ts').SkykitViewState }} frame
921
- */
922
- function applyTimedFrame(frameState, frame) {
923
- const context = pluginContext;
924
- const hookResult = options.applyFrame?.(frameState, context, frame);
925
- if (hookResult === false) return;
926
- frame.viewer.requestViewState({
927
- observerPc: frameState.observerPc,
928
- orientationIcrs: frameState.orientationIcrs,
929
- targetPc: frameState.targetPc,
930
- motion: {
931
- velocityPcPerSec: frameState.velocityPcPerSec,
932
- speedPcPerSec: frameState.speedPcPerSec,
933
- },
934
- }, id);
935
- if (frameState.cue && frameState.cue.id !== lastCueId) {
936
- lastCueId = frameState.cue.id;
937
- options.onCue?.(frameState.cue, frameState, context);
938
- } else if (!frameState.cue) {
939
- lastCueId = null;
940
- }
941
- }
942
-
943
- function getSnapshot() {
944
- return {
945
- id,
946
- disposed,
947
- playing,
948
- currentTimeSecs,
949
- initialSceneApplied,
950
- timedPreloadHintsEmitted,
951
- controller: controller?.getSnapshot?.() ?? null,
952
- timedJourney: evaluator
953
- ? {
954
- durationSecs: evaluator.durationSecs,
955
- journeyId: evaluator.journey.id,
956
- }
957
- : null,
958
- };
959
- }
960
- }
961
405
 
962
406
  /**
963
407
  * Strategy-only convenience helper. View-bound lookahead hints stay in preload
@@ -1550,108 +994,6 @@ function getDefaultEventTarget() {
1550
994
  return typeof globalThis.addEventListener === 'function' ? globalThis : null;
1551
995
  }
1552
996
 
1553
- /** @param {Record<string, unknown>} scene */
1554
- function isOrbitCameraScene(scene) {
1555
- return Boolean(
1556
- scene.camera
1557
- && typeof scene.camera === 'object'
1558
- && /** @type {{ type?: unknown }} */ (scene.camera).type === 'orbit',
1559
- );
1560
- }
1561
-
1562
- /** @param {unknown} event */
1563
- function isInitialJourneyEvent(event) {
1564
- return Boolean(
1565
- event
1566
- && typeof event === 'object'
1567
- && /** @type {{ type?: unknown; previousSceneId?: unknown }} */ (event).type === 'journey/initial',
1568
- );
1569
- }
1570
-
1571
- /**
1572
- * @param {unknown} value
1573
- * @returns {Record<string, unknown> & { durationSecs: number; sampleStepSecs: number; arrivalThreshold: number }}
1574
- */
1575
- function normalizeJourneySceneTravel(value) {
1576
- const source = /** @type {Record<string, unknown>} */ (
1577
- value && typeof value === 'object' ? value : {}
1578
- );
1579
- return {
1580
- ...source,
1581
- durationSecs: positiveFinite(source.durationSecs, 5),
1582
- sampleStepSecs: positiveFinite(source.sampleStepSecs, 1 / 60),
1583
- arrivalThreshold: positiveFinite(source.arrivalThreshold, 0.05),
1584
- };
1585
- }
1586
-
1587
- /**
1588
- * @param {Record<string, unknown>} camera
1589
- * @param {unknown} travel
1590
- */
1591
- function resolveJourneyDwellSecs(camera, travel) {
1592
- if (camera.dwellSecs != null) return Math.max(0, finiteNumber(camera.dwellSecs, 0));
1593
- if (travel && typeof travel === 'object' && 'dwellSecs' in travel) {
1594
- return Math.max(0, finiteNumber(/** @type {{ dwellSecs?: unknown }} */ (travel).dwellSecs, 0));
1595
- }
1596
- return 0;
1597
- }
1598
-
1599
- /**
1600
- * @param {Vector3Like} center
1601
- * @param {number} radius
1602
- * @param {Vector3Like} normal
1603
- * @returns {Vector3Like}
1604
- */
1605
- function defaultOrbitPosition(center, radius, normal) {
1606
- const axis = Math.abs(normal.x) < 0.9 ? { x: 1, y: 0, z: 0 } : { x: 0, y: 1, z: 0 };
1607
- const projected = projectOnPlane(axis, normal);
1608
- const length = Math.hypot(projected.x, projected.y, projected.z);
1609
- const direction = length > 1e-9
1610
- ? { x: projected.x / length, y: projected.y / length, z: projected.z / length }
1611
- : { x: 1, y: 0, z: 0 };
1612
- return {
1613
- x: center.x + direction.x * radius,
1614
- y: center.y + direction.y * radius,
1615
- z: center.z + direction.z * radius,
1616
- };
1617
- }
1618
-
1619
- /**
1620
- * @param {unknown} value
1621
- * @param {Vector3Like} fallback
1622
- * @returns {Vector3Like}
1623
- */
1624
- function normalizeDirectionVector(value, fallback) {
1625
- const vector = normalizeVector3(value, fallback);
1626
- const length = Math.hypot(vector.x, vector.y, vector.z);
1627
- return length > 1e-9
1628
- ? { x: vector.x / length, y: vector.y / length, z: vector.z / length }
1629
- : cloneVector3(fallback);
1630
- }
1631
-
1632
- /** @param {unknown} value */
1633
- function normalizeOptionalVector3(value) {
1634
- if (!value || typeof value !== 'object') return null;
1635
- const vector = /** @type {{ x?: unknown; y?: unknown; z?: unknown }} */ (value);
1636
- const x = Number(vector.x);
1637
- const y = Number(vector.y);
1638
- const z = Number(vector.z);
1639
- return [x, y, z].every(Number.isFinite) ? { x, y, z } : null;
1640
- }
1641
-
1642
- /**
1643
- * @param {Vector3Like} vector
1644
- * @param {Vector3Like} normal
1645
- * @returns {Vector3Like}
1646
- */
1647
- function projectOnPlane(vector, normal) {
1648
- const amount = vector.x * normal.x + vector.y * normal.y + vector.z * normal.z;
1649
- return {
1650
- x: vector.x - normal.x * amount,
1651
- y: vector.y - normal.y * amount,
1652
- z: vector.z - normal.z * amount,
1653
- };
1654
- }
1655
997
 
1656
998
  /**
1657
999
  * @param {unknown} payload
@@ -1690,11 +1032,6 @@ function resolveOnArrive(payload) {
1690
1032
  : null;
1691
1033
  }
1692
1034
 
1693
- /** @param {PromiseSettledResult<unknown>[]} results */
1694
- function hasActionResult(results) {
1695
- return results.some((result) => result.status === 'fulfilled' && result.value != null);
1696
- }
1697
-
1698
1035
  /** @param {Record<string, unknown>} targetSource */
1699
1036
  function resolveTransitionPositionInput(targetSource) {
1700
1037
  if (targetSource.observerPc !== undefined) return targetSource.observerPc;
@@ -1717,29 +1054,6 @@ function isSpatialTargetLike(value) {
1717
1054
  return Array.isArray(value) && value.length >= 3;
1718
1055
  }
1719
1056
 
1720
- /** @param {unknown} payload */
1721
- function resolveSceneId(payload) {
1722
- if (typeof payload === 'string') return payload;
1723
- if (!payload || typeof payload !== 'object') return null;
1724
- const source = /** @type {Record<string, unknown>} */ (payload);
1725
- const value = source.chapterId ?? source.sceneId ?? source.id;
1726
- return typeof value === 'string' ? value : null;
1727
- }
1728
-
1729
- /** @param {unknown} payload */
1730
- function resolveTimeSecs(payload) {
1731
- if (Number.isFinite(Number(payload))) return Number(payload);
1732
- if (!payload || typeof payload !== 'object') return 0;
1733
- const value = /** @type {Record<string, unknown>} */ (payload).timeSecs
1734
- ?? /** @type {Record<string, unknown>} */ (payload).sceneTimeSecs
1735
- ?? /** @type {Record<string, unknown>} */ (payload).time;
1736
- return finiteNumber(value, 0);
1737
- }
1738
-
1739
- /** @param {number} timeSecs @param {number} durationSecs */
1740
- function clampTime(timeSecs, durationSecs) {
1741
- return Math.min(Math.max(0, finiteNumber(timeSecs, 0)), Math.max(0, finiteNumber(durationSecs, 0)));
1742
- }
1743
1057
 
1744
1058
  /**
1745
1059
  * @param {unknown} value