@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.
@@ -1,13 +1,15 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
3
  import * as THREE from 'three';
4
- import { createJourney } from '@found-in-space/journey';
5
4
  import {
6
5
  createObserverShellStrategy,
7
6
  createStarCellData,
8
7
  encodeMorton3D,
9
8
  } from '@found-in-space/star-trees';
10
9
 
10
+ import {
11
+ resolveSpatialTarget,
12
+ } from '@found-in-space/spatial';
11
13
  import {
12
14
  SKYKIT_ACTION_NAMESPACE,
13
15
  SKYKIT_ACTIONS,
@@ -18,12 +20,12 @@ import {
18
20
  createDesktopSkykitObserverRig,
19
21
  createObject3dLayer,
20
22
  createObject3dPlugin,
23
+ createRaDecLookAt,
21
24
  createMouseLookPlugin,
22
25
  createSkyGrabPlugin,
23
26
  createSkykitDefaultKeyboardNavigationBindings,
24
27
  createSkykitAnimationLoop,
25
28
  createSkykitDebugBridge,
26
- createSkykitJourneyPlugin,
27
29
  createSkykitNavigationPlugin,
28
30
  createSkykitStarPreloadRequestsFromSpatialHints,
29
31
  createSkykitStarStrategiesFromSpatialHints,
@@ -36,6 +38,7 @@ import {
36
38
  createStreamingStarLayer,
37
39
  createStreamingStarsPlugin,
38
40
  installSkykitDebugGlobal,
41
+ parseSpatialLookAtText,
39
42
  } from '../index.js';
40
43
 
41
44
  function createHost() {
@@ -143,10 +146,6 @@ function assertStrategyBehavior(strategy) {
143
146
  }
144
147
  }
145
148
 
146
- function distance(a, b) {
147
- return Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
148
- }
149
-
150
149
  test('createSkykitViewer creates roots, mounts renderer, runs lifecycle, and disposes cleanly', async () => {
151
150
  const host = createHost();
152
151
  const renderer = createRenderer();
@@ -319,6 +318,50 @@ test('viewer derives camera orientation from lookAt targets, sky coordinates, an
319
318
  assertVectorApprox(localVectorFromView(view, { x: 0, y: 1, z: 0 }), { x: 0, y: 1, z: 0 });
320
319
  await skyViewer.dispose();
321
320
 
321
+ const alnilamViewer = await createSkykitViewer({
322
+ renderer: createRenderer(),
323
+ view: {
324
+ lookAt: createRaDecLookAt('05h 36m 12.81s', '−01° 12′ 06.9″'),
325
+ },
326
+ });
327
+ view = alnilamViewer.getViewState();
328
+ assertVectorApprox(
329
+ localVectorFromView(view, { x: 0, y: 0, z: -1 }),
330
+ directionFromRaDec(84.053375, -1.2019166666666667),
331
+ );
332
+ await alnilamViewer.dispose();
333
+
334
+ const siriusSpec = parseSpatialLookAtText('06h 45m 08.9s, -16d 42m 58s, 2.64pc');
335
+ const orionSpec = parseSpatialLookAtText('05h 35m 17.3s, -05d 23m 28s, 414pc');
336
+ const siriusPc = resolveSpatialTarget(siriusSpec);
337
+ const orionPc = resolveSpatialTarget(orionSpec);
338
+ assert.ok(siriusPc && orionPc && orionSpec);
339
+ const solarTargetViewer = await createSkykitViewer({
340
+ renderer: createRenderer(),
341
+ view: {
342
+ observerPc: siriusPc,
343
+ lookAt: orionSpec,
344
+ },
345
+ });
346
+ view = solarTargetViewer.getViewState();
347
+ assertVectorApprox(view.observerPc, siriusPc);
348
+ assertVectorApprox(view.targetPc, orionPc);
349
+ assertVectorApprox(
350
+ localVectorFromView(view, { x: 0, y: 0, z: -1 }),
351
+ normalizeVector(subtractVectors(orionPc, siriusPc)),
352
+ );
353
+
354
+ const movedObserver = { x: -8, y: 3, z: 11 };
355
+ solarTargetViewer.requestViewState({ observerPc: movedObserver, lookAt: orionSpec }, 'test-solar-radec');
356
+ solarTargetViewer.update(0);
357
+ view = solarTargetViewer.getViewState();
358
+ assertVectorApprox(view.targetPc, orionPc);
359
+ assertVectorApprox(
360
+ localVectorFromView(view, { x: 0, y: 0, z: -1 }),
361
+ normalizeVector(subtractVectors(orionPc, movedObserver)),
362
+ );
363
+ await solarTargetViewer.dispose();
364
+
322
365
  const starViewer = await createSkykitViewer({
323
366
  renderer: createRenderer(),
324
367
  view: { lookAt: { star: 'hyades' } },
@@ -429,15 +472,15 @@ test('action registry registers contexts, invokes multiple handlers, and reports
429
472
  offLow();
430
473
  assert.equal(registry.listActions().find((entry) => entry.id === 'lesson:demo.run')?.handlerCount, 2);
431
474
 
432
- const offJourney = registry.registerContext('skykit:journey', {
433
- goToChapter({ payload }) {
475
+ const offChapters = registry.registerContext('lesson:chapters', {
476
+ goTo({ payload }) {
434
477
  calls.push(`chapter:${payload}`);
435
478
  },
436
479
  });
437
- await registry.invoke(SKYKIT_ACTIONS.journey.goToChapter, 'intro');
480
+ await registry.invoke('lesson:chapters.goTo', 'intro');
438
481
  assert.equal(calls.at(-1), 'chapter:intro');
439
- offJourney();
440
- assert.equal(registry.listActions().some((entry) => entry.id === SKYKIT_ACTIONS.journey.goToChapter), false);
482
+ offChapters();
483
+ assert.equal(registry.listActions().some((entry) => entry.id === 'lesson:chapters.goTo'), false);
441
484
  });
442
485
 
443
486
  test('action registry tracks held action sources and control values', () => {
@@ -1533,7 +1576,7 @@ test('navigation transition action restores pose with independent lane durations
1533
1576
  });
1534
1577
  viewer.update(1);
1535
1578
  viewer.update(0);
1536
- assert.deepEqual(viewer.getViewState().observerPc, { x: 20, y: 0, z: 0 });
1579
+ assert.deepEqual(viewer.getViewState().observerPc, { x: 10, y: 0, z: 0 });
1537
1580
  assert.deepEqual(viewer.getViewState().orientationIcrs, orientationAfterExplicitTransition);
1538
1581
 
1539
1582
  await viewer.actions.invoke(SKYKIT_ACTIONS.navigation.transitionTo, {
@@ -1545,504 +1588,20 @@ test('navigation transition action restores pose with independent lane durations
1545
1588
  assert.deepEqual(viewer.getViewState().observerPc, { x: 0, y: 0, z: 0 });
1546
1589
  assert.deepEqual(viewer.getViewState().orientationIcrs, orientationAfterExplicitTransition);
1547
1590
 
1548
- await viewer.dispose();
1549
- });
1550
-
1551
- test('journey plugin registers actions, applies scenes, timed frames, and preload hooks', async () => {
1552
- const preloadEvents = [];
1553
- const cueEvents = [];
1554
- const journeyPlugin = createSkykitJourneyPlugin({
1555
- scenes: {
1556
- intro: { title: 'Intro', view: { limitingMagnitude: 5 } },
1557
- hyades: {
1558
- title: 'Hyades',
1559
- view: { observerPc: { x: 1, y: 2, z: 3 } },
1560
- preloadHints: [
1561
- { kind: 'sphere-volume', centerPc: { x: 1, y: 2, z: 3 }, radiusPc: 4 },
1562
- ],
1563
- },
1564
- },
1565
- initialSceneId: 'intro',
1566
- timedJourney: {
1567
- durationSecs: 2,
1568
- locationWaypoints: [
1569
- { id: 'a', timeSecs: 0, positionPc: { x: 0, y: 0, z: 0 } },
1570
- { id: 'b', timeSecs: 2, positionPc: { x: 2, y: 0, z: 0 } },
1571
- ],
1572
- cameraLookWaypoints: [
1573
- { id: 'look', timeSecs: 0, kind: 'direction', forward: { x: 1, y: 0, z: 0 } },
1574
- ],
1575
- cues: [{ id: 'cue', startSecs: 0, endSecs: 2 }],
1576
- },
1577
- onPreloadHints: (hints) => preloadEvents.push(hints),
1578
- onCue: (cue) => cueEvents.push(cue.id),
1579
- });
1580
- const viewer = await createSkykitViewer({
1581
- renderer: createRenderer(),
1582
- plugins: [journeyPlugin],
1583
- });
1584
-
1585
- viewer.update(0);
1586
- assert.equal(viewer.getViewState().limitingMagnitude, 5);
1587
- assert.equal(journeyPlugin.getSnapshot().initialSceneApplied, true);
1588
- assert.equal(journeyPlugin.getSnapshot().timedPreloadHintsEmitted, true);
1589
-
1590
- assert.equal(viewer.actions.listActions().some((entry) => entry.id === SKYKIT_ACTIONS.journey.goToChapter), true);
1591
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.goToChapter, 'hyades');
1592
- viewer.update(0);
1593
- assert.deepEqual(viewer.getViewState().observerPc, { x: 1, y: 2, z: 3 });
1594
- assert.equal(preloadEvents.length, 1);
1595
-
1596
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.seek, { timeSecs: 1 });
1597
- viewer.update(0);
1598
- assert.ok(viewer.getViewState().observerPc.x > 0);
1599
- assert.deepEqual(cueEvents, ['cue']);
1600
-
1601
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.play);
1602
- const beforePlayX = viewer.getViewState().observerPc.x;
1603
- viewer.update(0.5);
1604
- viewer.update(0);
1605
- assert.ok(viewer.getViewState().observerPc.x > beforePlayX);
1606
-
1607
- await viewer.dispose();
1608
- });
1609
-
1610
- test('journey plugin executes semantic orbit-transfer scenes without snapping', async () => {
1611
- const navigationPlugin = createSkykitNavigationPlugin({ speed: 20, acceleration: 20, deceleration: 20 });
1612
- const journey = createJourney({
1613
- initial: 'inside',
1614
- order: ['inside', 'outside', 'hyades', 'free'],
1615
- targets: {
1616
- sun: { positionPc: { x: 0, y: 0, z: 0 } },
1617
- hyades: { positionPc: { x: 18, y: 42, z: 7 } },
1618
- free: { positionPc: { x: -20, y: 35, z: 12 } },
1619
- orion: { positionPc: { x: 0, y: 0, z: -100 } },
1620
- },
1621
- scenes: {
1622
- inside: {
1623
- view: {
1624
- observerPc: { x: 0, y: 4, z: 0 },
1625
- },
1626
- camera: {
1627
- type: 'orbit',
1628
- center: 'sun',
1629
- radiusPc: 4,
1630
- angularSpeedRadPerSec: 0.4,
1631
- lookAt: 'orion',
1632
- normal: { x: 0, y: 0, z: 1 },
1633
- },
1634
- },
1635
- outside: {
1636
- camera: {
1637
- type: 'orbit',
1638
- center: 'sun',
1639
- radiusPc: 10,
1640
- angularSpeedRadPerSec: 0.2,
1641
- lookAt: 'sun',
1642
- normal: { x: 0, y: 0, z: 1 },
1643
- },
1644
- },
1645
- hyades: {
1646
- camera: {
1647
- type: 'orbit',
1648
- center: 'hyades',
1649
- radiusPc: 6,
1650
- angularSpeedRadPerSec: 0.3,
1651
- lookAt: 'hyades',
1652
- normal: { x: 0, y: 0, z: 1 },
1653
- },
1654
- },
1655
- free: {
1656
- camera: {
1657
- type: 'orbit',
1658
- center: 'free',
1659
- radiusPc: 5,
1660
- angularSpeedRadPerSec: 0.25,
1661
- lookAt: 'free',
1662
- },
1663
- },
1664
- },
1665
- travel: { type: 'orbit-transfer', durationSecs: 2, sampleStepSecs: 0.25 },
1666
- });
1667
- const viewer = await createSkykitViewer({
1668
- renderer: createRenderer(),
1669
- plugins: [
1670
- navigationPlugin,
1671
- createSkykitJourneyPlugin({ journey }),
1672
- ],
1673
- });
1674
-
1675
- await flushMicrotasks();
1676
- viewer.update(0);
1677
- assert.deepEqual(viewer.getViewState().observerPc, { x: 0, y: 4, z: 0 });
1678
- viewer.update(0.001);
1679
- viewer.update(0);
1680
- assert.ok(Math.abs(distance(viewer.getViewState().observerPc, { x: 0, y: 0, z: 0 }) - 4) < 1e-9);
1681
-
1682
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.goToChapter, 'outside');
1683
- await flushMicrotasks();
1684
- viewer.update(1);
1685
- viewer.update(0);
1686
- const halfwayRadius = distance(viewer.getViewState().observerPc, { x: 0, y: 0, z: 0 });
1687
- assert.ok(halfwayRadius > 4);
1688
- assert.ok(halfwayRadius < 10);
1689
-
1690
- viewer.update(1.1);
1691
- viewer.update(0);
1692
- const arrival = { ...viewer.getViewState().observerPc };
1693
- assert.ok(Math.abs(distance(arrival, { x: 0, y: 0, z: 0 }) - 10) < 1e-9);
1694
- assert.equal(navigationPlugin.getSnapshot().navigation.activeAutomation, 'orbit');
1695
- viewer.update(0.25);
1696
- viewer.update(0);
1697
- assert.ok(Math.abs(distance(viewer.getViewState().observerPc, { x: 0, y: 0, z: 0 }) - 10) < 1e-9);
1698
- assert.notDeepEqual(viewer.getViewState().observerPc, arrival);
1699
-
1700
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.goToChapter, 'hyades');
1701
- await flushMicrotasks();
1702
- let handoff = null;
1703
- let previous = { ...viewer.getViewState().observerPc };
1704
- for (let index = 0; index < 120; index += 1) {
1705
- viewer.update(1 / 30);
1706
- viewer.update(0);
1707
- const current = { ...viewer.getViewState().observerPc };
1708
- const step = distance(previous, current);
1709
- if (!handoff && navigationPlugin.getSnapshot().navigation.activeAutomation === 'orbit') {
1710
- handoff = { position: current, step };
1711
- }
1712
- previous = current;
1713
- }
1714
- assert.ok(handoff);
1715
- assert.ok(Math.abs(handoff.position.z - 7) < 1e-6);
1716
- assert.ok(handoff.step < 0.5);
1717
-
1718
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.goToChapter, 'free');
1719
- await flushMicrotasks();
1720
- for (let index = 0; index < 120; index += 1) {
1721
- viewer.update(1 / 30);
1722
- viewer.update(0);
1723
- }
1724
- const freeAutomation = navigationPlugin.getSnapshot().navigation.movementAutomation;
1725
- assert.equal(navigationPlugin.getSnapshot().navigation.activeAutomation, 'orbit');
1726
- assert.ok(freeAutomation?.normal);
1727
- assert.ok(Math.abs(freeAutomation.normal.z - 1) > 0.001);
1728
- assert.ok(Math.abs(distance(viewer.getViewState().observerPc, { x: -20, y: 35, z: 12 }) - 5) < 1e-9);
1729
-
1730
- await viewer.dispose();
1731
- });
1732
-
1733
- test('journey plugin uses explicit route points for authored orbit transfers', async () => {
1734
- const navigationPlugin = createSkykitNavigationPlugin({ speed: 80, acceleration: 80, deceleration: 80 });
1735
- const authoredPoints = [
1736
- { x: 0, y: 4, z: 0 },
1737
- { x: 40, y: 10, z: 80 },
1738
- { x: 100, y: 0, z: 10 },
1739
- ];
1740
- const journey = createJourney({
1741
- initial: 'inside',
1742
- order: ['inside', 'omega'],
1743
- targets: {
1744
- sun: { positionPc: { x: 0, y: 0, z: 0 } },
1745
- omega: { positionPc: { x: 100, y: 0, z: 0 } },
1746
- },
1747
- scenes: {
1748
- inside: {
1749
- view: { observerPc: { x: 0, y: 4, z: 0 } },
1750
- camera: {
1751
- type: 'orbit',
1752
- center: 'sun',
1753
- radiusPc: 4,
1754
- angularSpeedRadPerSec: 0.2,
1755
- normal: { x: 0, y: 0, z: 1 },
1756
- },
1757
- },
1758
- omega: {
1759
- camera: {
1760
- type: 'orbit',
1761
- center: 'omega',
1762
- radiusPc: 10,
1763
- angularSpeedRadPerSec: 0.12,
1764
- normal: { x: 0, y: 0, z: 1 },
1765
- },
1766
- },
1767
- },
1768
- transitions: [
1769
- {
1770
- fromSceneId: 'inside',
1771
- toSceneId: 'omega',
1772
- travel: {
1773
- type: 'orbit-transfer',
1774
- durationSecs: 2,
1775
- pointsPc: authoredPoints,
1776
- arrivalAction: {
1777
- type: 'orbit',
1778
- center: { x: 100, y: 0, z: 0 },
1779
- radius: 10,
1780
- angularSpeedRadPerSec: 0.12,
1781
- normal: { x: 0, y: 0, z: 1 },
1782
- },
1783
- },
1784
- },
1785
- ],
1786
- });
1787
- const viewer = await createSkykitViewer({
1788
- renderer: createRenderer(),
1789
- plugins: [
1790
- navigationPlugin,
1791
- createSkykitJourneyPlugin({ journey }),
1792
- ],
1793
- });
1794
-
1795
- await flushMicrotasks();
1796
- viewer.update(0);
1797
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.goToChapter, 'omega');
1798
- await flushMicrotasks();
1799
-
1800
- viewer.update(1);
1801
- viewer.update(0);
1802
- assert.ok(viewer.getViewState().observerPc.z > 20);
1803
-
1804
- viewer.update(1.1);
1805
- viewer.update(0);
1806
- assert.equal(navigationPlugin.getSnapshot().navigation.activeAutomation, 'orbit');
1807
- assert.ok(Math.abs(distance(viewer.getViewState().observerPc, { x: 100, y: 0, z: 0 }) - 10) < 1e-9);
1808
-
1809
- await viewer.dispose();
1810
- });
1811
-
1812
- test('journey plugin can route to non-orbit scenes before applying the arrival transition', async () => {
1813
- const navigationPlugin = createSkykitNavigationPlugin({ speed: 80, acceleration: 80, deceleration: 80 });
1814
- const journey = createJourney({
1815
- initial: 'omega',
1816
- order: ['omega', 'local'],
1817
- targets: {
1818
- omega: { positionPc: { x: 100, y: 0, z: 0 } },
1819
- },
1820
- scenes: {
1821
- omega: {
1822
- view: { observerPc: { x: 100, y: 0, z: 10 } },
1823
- camera: {
1824
- type: 'orbit',
1825
- center: 'omega',
1826
- radiusPc: 10,
1827
- angularSpeedRadPerSec: 0.12,
1828
- normal: { x: 0, y: 0, z: 1 },
1829
- },
1830
- },
1831
- local: {
1832
- view: { lookAt: { targetPc: { x: 1, y: 0, z: 0 } } },
1833
- navigation: {
1834
- transitionTo: {
1835
- observerPc: { x: 0, y: 0, z: 0 },
1836
- orientationIcrs: { x: 0, y: 0, z: 0, w: 1 },
1837
- durationSecs: 1,
1838
- movement: { durationSecs: 1 },
1839
- orientationTransition: { durationSecs: 1 },
1840
- },
1841
- },
1842
- },
1843
- },
1844
- transitions: [
1845
- {
1846
- fromSceneId: 'omega',
1847
- toSceneId: 'local',
1848
- travel: {
1849
- type: 'orbit-transfer',
1850
- durationSecs: 2,
1851
- pointsPc: [
1852
- { x: 100, y: 0, z: 10 },
1853
- { x: 50, y: 0, z: 80 },
1854
- { x: 0, y: 0, z: 0 },
1855
- ],
1856
- },
1857
- },
1858
- ],
1859
- });
1860
- const viewer = await createSkykitViewer({
1861
- renderer: createRenderer(),
1862
- plugins: [
1863
- navigationPlugin,
1864
- createSkykitJourneyPlugin({ journey }),
1865
- ],
1866
- });
1867
-
1868
- await flushMicrotasks();
1869
- viewer.update(0);
1870
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.goToChapter, 'local');
1871
- await flushMicrotasks();
1872
-
1873
- viewer.update(1);
1874
- viewer.update(0);
1875
- assert.ok(viewer.getViewState().observerPc.z > 20);
1876
-
1877
- viewer.update(1.1);
1878
- await flushMicrotasks();
1879
- viewer.update(0.1);
1880
- viewer.update(0);
1881
- assert.deepEqual(viewer.getViewState().observerPc, { x: 0, y: 0, z: 0 });
1882
-
1883
- await viewer.dispose();
1884
- });
1885
-
1886
- test('journey plugin emits timed preload hints once and not on every frame', async () => {
1887
- const preloadEvents = [];
1888
- const viewer = await createSkykitViewer({
1889
- renderer: createRenderer(),
1890
- plugins: [
1891
- createSkykitJourneyPlugin({
1892
- autoPlay: true,
1893
- timedJourney: {
1894
- durationSecs: 4,
1895
- locationWaypoints: [
1896
- { id: 'a', timeSecs: 0, positionPc: { x: 0, y: 0, z: 0 } },
1897
- { id: 'b', timeSecs: 4, positionPc: { x: 4, y: 0, z: 0 } },
1898
- ],
1899
- },
1900
- evaluatorOptions: {
1901
- pathRadiusPc: 2,
1902
- lookaheadSecs: 1,
1903
- preloadStepSecs: 1,
1904
- },
1905
- onPreloadHints: (hints, source) => preloadEvents.push({ hints, source }),
1906
- }),
1907
- ],
1591
+ await viewer.actions.invoke(SKYKIT_ACTIONS.navigation.transitionTo, {
1592
+ lookAt: '05:36:12.81, −01:12:06.9',
1593
+ orientation: { durationSecs: 1 },
1908
1594
  });
1909
-
1910
- assert.equal(preloadEvents.length, 1);
1911
- assert.equal(preloadEvents[0].source.type, 'journey/timed-preload');
1912
1595
  viewer.update(1);
1913
- viewer.update(1);
1914
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.seek, { timeSecs: 2 });
1915
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.play);
1916
- assert.equal(preloadEvents.length, 1);
1917
-
1918
- await viewer.dispose();
1919
- });
1920
-
1921
- test('journey plugin reports scene arrival immediately for static scenes', async () => {
1922
- const arrivals = [];
1923
- const viewer = await createSkykitViewer({
1924
- renderer: createRenderer(),
1925
- plugins: [
1926
- createSkykitJourneyPlugin({
1927
- scenes: {
1928
- intro: { view: { limitingMagnitude: 5 } },
1929
- },
1930
- initialSceneId: 'intro',
1931
- onSceneArrive(scene) {
1932
- arrivals.push(scene.sceneId);
1933
- },
1934
- }),
1935
- ],
1936
- });
1937
-
1938
- await flushMicrotasks();
1939
- viewer.update(0);
1940
- assert.deepEqual(arrivals, ['intro']);
1941
-
1942
- await viewer.dispose();
1943
- });
1944
-
1945
- test('journey plugin reports scene arrival after transitionTo completes', async () => {
1946
- const arrivals = [];
1947
- const viewer = await createSkykitViewer({
1948
- renderer: createRenderer(),
1949
- plugins: [
1950
- createSkykitNavigationPlugin(),
1951
- createSkykitJourneyPlugin({
1952
- scenes: {
1953
- intro: {},
1954
- away: {
1955
- navigation: {
1956
- transitionTo: {
1957
- observerPc: { x: 10, y: 0, z: 0 },
1958
- durationSecs: 1,
1959
- },
1960
- },
1961
- },
1962
- },
1963
- initialSceneId: 'intro',
1964
- onSceneArrive(scene) {
1965
- arrivals.push(scene.sceneId);
1966
- },
1967
- }),
1968
- ],
1969
- });
1970
-
1971
- await flushMicrotasks();
1972
- arrivals.length = 0;
1973
-
1974
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.goToChapter, 'away');
1975
- await flushMicrotasks();
1976
- viewer.update(0.5);
1977
- viewer.update(0);
1978
- assert.deepEqual(arrivals, []);
1979
-
1980
- viewer.update(0.5);
1981
1596
  viewer.update(0);
1982
- assert.deepEqual(arrivals, ['away']);
1597
+ assertVectorApprox(
1598
+ localVectorFromView(viewer.getViewState(), { x: 0, y: 0, z: -1 }),
1599
+ directionFromRaDec(84.053375, -1.2019166666666667),
1600
+ );
1983
1601
 
1984
1602
  await viewer.dispose();
1985
1603
  });
1986
1604
 
1987
- test('journey plugin reports scene arrival after orbit-transfer travel completes', async () => {
1988
- const arrivals = [];
1989
- const journey = createJourney({
1990
- initial: 'sun',
1991
- order: ['sun', 'cluster'],
1992
- targets: {
1993
- sun: { positionPc: { x: 0, y: 0, z: 0 } },
1994
- cluster: { positionPc: { x: 20, y: 0, z: 0 } },
1995
- },
1996
- scenes: {
1997
- sun: {
1998
- camera: {
1999
- type: 'orbit',
2000
- center: 'sun',
2001
- radiusPc: 4,
2002
- angularSpeedRadPerSec: 0.2,
2003
- normal: { x: 0, y: 0, z: 1 },
2004
- },
2005
- },
2006
- cluster: {
2007
- camera: {
2008
- type: 'orbit',
2009
- center: 'cluster',
2010
- radiusPc: 5,
2011
- angularSpeedRadPerSec: 0.2,
2012
- normal: { x: 0, y: 0, z: 1 },
2013
- },
2014
- },
2015
- },
2016
- travel: { type: 'orbit-transfer', durationSecs: 1, sampleStepSecs: 0.25 },
2017
- });
2018
- const viewer = await createSkykitViewer({
2019
- renderer: createRenderer(),
2020
- plugins: [
2021
- createSkykitNavigationPlugin(),
2022
- createSkykitJourneyPlugin({
2023
- journey,
2024
- onSceneArrive(scene) {
2025
- arrivals.push(scene.sceneId);
2026
- },
2027
- }),
2028
- ],
2029
- });
2030
-
2031
- await flushMicrotasks();
2032
- arrivals.length = 0;
2033
-
2034
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.goToChapter, 'cluster');
2035
- await flushMicrotasks();
2036
- viewer.update(0.5);
2037
- viewer.update(0);
2038
- assert.deepEqual(arrivals, []);
2039
-
2040
- viewer.update(0.6);
2041
- viewer.update(0);
2042
- assert.deepEqual(arrivals, ['cluster']);
2043
-
2044
- await viewer.dispose();
2045
- });
2046
1605
 
2047
1606
  test('spatial preload hints map to star-octree requests without exposing provider internals', () => {
2048
1607
  const hints = [
@@ -2460,6 +2019,34 @@ function assertVectorApprox(actual, expected, epsilon = 1e-9) {
2460
2019
  assert.ok(Math.abs(actual.z - expected.z) < epsilon, `z ${actual.z} !== ${expected.z}`);
2461
2020
  }
2462
2021
 
2022
+ function subtractVectors(a, b) {
2023
+ return {
2024
+ x: a.x - b.x,
2025
+ y: a.y - b.y,
2026
+ z: a.z - b.z,
2027
+ };
2028
+ }
2029
+
2030
+ function normalizeVector(vector) {
2031
+ const length = Math.hypot(vector.x, vector.y, vector.z);
2032
+ return {
2033
+ x: vector.x / length,
2034
+ y: vector.y / length,
2035
+ z: vector.z / length,
2036
+ };
2037
+ }
2038
+
2039
+ function directionFromRaDec(raDeg, decDeg) {
2040
+ const ra = raDeg * Math.PI / 180;
2041
+ const dec = decDeg * Math.PI / 180;
2042
+ const cosDec = Math.cos(dec);
2043
+ return {
2044
+ x: Math.cos(ra) * cosDec,
2045
+ y: Math.sin(ra) * cosDec,
2046
+ z: Math.sin(dec),
2047
+ };
2048
+ }
2049
+
2463
2050
  function createPointerTarget() {
2464
2051
  const target = createEventTarget();
2465
2052
  target.clientWidth = 800;
package/src/actions.js CHANGED
@@ -51,14 +51,6 @@ export const SKYKIT_ACTIONS = Object.freeze({
51
51
  exit: 'skykit:xr.exit',
52
52
  toggle: 'skykit:xr.toggle',
53
53
  }),
54
- journey: Object.freeze({
55
- goToChapter: 'skykit:journey.goToChapter',
56
- next: 'skykit:journey.next',
57
- previous: 'skykit:journey.previous',
58
- seek: 'skykit:journey.seek',
59
- play: 'skykit:journey.play',
60
- pause: 'skykit:journey.pause',
61
- }),
62
54
  });
63
55
 
64
56
  export const SKYKIT_CONTROLS = Object.freeze({