@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
@@ -1,7 +1,6 @@
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,
@@ -23,7 +22,6 @@ import {
23
22
  createSkykitDefaultKeyboardNavigationBindings,
24
23
  createSkykitAnimationLoop,
25
24
  createSkykitDebugBridge,
26
- createSkykitJourneyPlugin,
27
25
  createSkykitNavigationPlugin,
28
26
  createSkykitStarPreloadRequestsFromSpatialHints,
29
27
  createSkykitStarStrategiesFromSpatialHints,
@@ -77,6 +75,57 @@ function createRenderer() {
77
75
  };
78
76
  }
79
77
 
78
+ function createTextureRenderer() {
79
+ const renderer = createRenderer();
80
+ return {
81
+ ...renderer,
82
+ xr: { enabled: true },
83
+ currentTarget: null,
84
+ viewport: new THREE.Vector4(0, 0, 1, 1),
85
+ scissor: new THREE.Vector4(0, 0, 1, 1),
86
+ scissorTest: false,
87
+ getRenderTarget() {
88
+ return this.currentTarget;
89
+ },
90
+ setRenderTarget(target) {
91
+ this.currentTarget = target;
92
+ },
93
+ getViewport(target) {
94
+ return target.copy(this.viewport);
95
+ },
96
+ setViewport(value) {
97
+ if (value?.isVector4) this.viewport.copy(value);
98
+ },
99
+ getScissor(target) {
100
+ return target.copy(this.scissor);
101
+ },
102
+ setScissor(value) {
103
+ if (value?.isVector4) this.scissor.copy(value);
104
+ },
105
+ getScissorTest() {
106
+ return this.scissorTest;
107
+ },
108
+ setScissorTest(value) {
109
+ this.scissorTest = Boolean(value);
110
+ },
111
+ };
112
+ }
113
+
114
+ function createEmbeddedSurfaceSpy() {
115
+ const publishCalls = [];
116
+ const unpublishCalls = [];
117
+ return {
118
+ publishCalls,
119
+ unpublishCalls,
120
+ publish(sourceId, update) {
121
+ publishCalls.push({ sourceId, update });
122
+ },
123
+ unpublish(sourceId) {
124
+ unpublishCalls.push(sourceId);
125
+ },
126
+ };
127
+ }
128
+
80
129
  async function flushMicrotasks(count = 10) {
81
130
  for (let index = 0; index < count; index += 1) {
82
131
  await Promise.resolve();
@@ -92,10 +141,6 @@ function assertStrategyBehavior(strategy) {
92
141
  }
93
142
  }
94
143
 
95
- function distance(a, b) {
96
- return Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
97
- }
98
-
99
144
  test('createSkykitViewer creates roots, mounts renderer, runs lifecycle, and disposes cleanly', async () => {
100
145
  const host = createHost();
101
146
  const renderer = createRenderer();
@@ -242,6 +287,51 @@ test('viewer exposes action registry, emits action events, and resets to initial
242
287
  await viewer.dispose();
243
288
  });
244
289
 
290
+ test('viewer derives camera orientation from lookAt targets, sky coordinates, and stars', async () => {
291
+ const targetViewer = await createSkykitViewer({
292
+ renderer: createRenderer(),
293
+ view: {
294
+ observerPc: { x: 0, y: 0, z: 0 },
295
+ lookAt: { targetPc: { x: 10, y: 0, z: 0 }, positionAngleDeg: 0 },
296
+ },
297
+ });
298
+ let view = targetViewer.getViewState();
299
+ assert.deepEqual(view.targetPc, { x: 10, y: 0, z: 0 });
300
+ assertVectorApprox(localVectorFromView(view, { x: 0, y: 0, z: -1 }), { x: 1, y: 0, z: 0 });
301
+ assertVectorApprox(localVectorFromView(view, { x: 0, y: 1, z: 0 }), { x: 0, y: 0, z: 1 });
302
+ await targetViewer.dispose();
303
+
304
+ const skyViewer = await createSkykitViewer({
305
+ renderer: createRenderer(),
306
+ view: {
307
+ lookAt: { raDeg: 0, decDeg: 0, positionAngleDeg: 90 },
308
+ },
309
+ });
310
+ view = skyViewer.getViewState();
311
+ assert.equal(view.targetPc, null);
312
+ assertVectorApprox(localVectorFromView(view, { x: 0, y: 0, z: -1 }), { x: 1, y: 0, z: 0 });
313
+ assertVectorApprox(localVectorFromView(view, { x: 0, y: 1, z: 0 }), { x: 0, y: 1, z: 0 });
314
+ await skyViewer.dispose();
315
+
316
+ const starViewer = await createSkykitViewer({
317
+ renderer: createRenderer(),
318
+ view: { lookAt: { star: 'hyades' } },
319
+ resolveLookAtStar: async (star) => star === 'hyades'
320
+ ? { targetPc: { x: 4, y: 5, z: 6 } }
321
+ : null,
322
+ });
323
+ view = starViewer.getViewState();
324
+ assert.equal(view.lookAt?.star, 'hyades');
325
+ assert.deepEqual(view.targetPc, { x: 4, y: 5, z: 6 });
326
+ assert.ok(view.orientationIcrs);
327
+
328
+ starViewer.requestViewState({ lookAt: { star: 'orion' } }, 'test-star-look');
329
+ await new Promise((resolve) => setTimeout(resolve, 0));
330
+ starViewer.update(0);
331
+ assert.deepEqual(starViewer.getViewState().targetPc, null);
332
+ await starViewer.dispose();
333
+ });
334
+
245
335
  test('requestViewState batches patches and observer-centric root follows translation without rotation', async () => {
246
336
  const viewer = await createSkykitViewer({
247
337
  renderer: createRenderer(),
@@ -333,15 +423,15 @@ test('action registry registers contexts, invokes multiple handlers, and reports
333
423
  offLow();
334
424
  assert.equal(registry.listActions().find((entry) => entry.id === 'lesson:demo.run')?.handlerCount, 2);
335
425
 
336
- const offJourney = registry.registerContext('skykit:journey', {
337
- goToChapter({ payload }) {
426
+ const offChapters = registry.registerContext('lesson:chapters', {
427
+ goTo({ payload }) {
338
428
  calls.push(`chapter:${payload}`);
339
429
  },
340
430
  });
341
- await registry.invoke(SKYKIT_ACTIONS.journey.goToChapter, 'intro');
431
+ await registry.invoke('lesson:chapters.goTo', 'intro');
342
432
  assert.equal(calls.at(-1), 'chapter:intro');
343
- offJourney();
344
- assert.equal(registry.listActions().some((entry) => entry.id === SKYKIT_ACTIONS.journey.goToChapter), false);
433
+ offChapters();
434
+ assert.equal(registry.listActions().some((entry) => entry.id === 'lesson:chapters.goTo'), false);
345
435
  });
346
436
 
347
437
  test('action registry tracks held action sources and control values', () => {
@@ -682,6 +772,41 @@ test('HR diagram mode changes refresh shared demand and update the renderer view
682
772
  await viewer.dispose();
683
773
  });
684
774
 
775
+ test('HR diagram touch-os surfaces can be resolved lazily from panel runtimes', async () => {
776
+ const session = createFakeSession();
777
+ const provider = {
778
+ id: 'provider',
779
+ createSession() {
780
+ return session;
781
+ },
782
+ };
783
+ const source = createSkykitStarSourcePlugin({ provider });
784
+ const surfaces = createEmbeddedSurfaceSpy();
785
+ let activeSurfaces = null;
786
+ const hr = createSkykitHrDiagramPlugin({
787
+ id: 'hr',
788
+ source,
789
+ touchOs: {
790
+ sourceId: 'hr:surface',
791
+ surfaces: () => activeSurfaces,
792
+ },
793
+ });
794
+ const viewer = await createSkykitViewer({
795
+ renderer: createTextureRenderer(),
796
+ plugins: [source, hr],
797
+ });
798
+
799
+ activeSurfaces = surfaces;
800
+ viewer.frame(0.016);
801
+
802
+ assert.equal(surfaces.publishCalls.length, 1);
803
+ assert.equal(surfaces.publishCalls[0].sourceId, 'hr:surface');
804
+ assert.equal(surfaces.publishCalls[0].update.available, true);
805
+
806
+ await viewer.dispose();
807
+ assert.deepEqual(surfaces.unpublishCalls, ['hr:surface']);
808
+ });
809
+
685
810
  test('HR diagram demand strategy override can be supplied and restored at runtime', async () => {
686
811
  const sessions = [];
687
812
  const provider = {
@@ -1417,501 +1542,6 @@ test('navigation transition action restores pose with independent lane durations
1417
1542
  await viewer.dispose();
1418
1543
  });
1419
1544
 
1420
- test('journey plugin registers actions, applies scenes, timed frames, and preload hooks', async () => {
1421
- const preloadEvents = [];
1422
- const cueEvents = [];
1423
- const journeyPlugin = createSkykitJourneyPlugin({
1424
- scenes: {
1425
- intro: { title: 'Intro', view: { limitingMagnitude: 5 } },
1426
- hyades: {
1427
- title: 'Hyades',
1428
- view: { observerPc: { x: 1, y: 2, z: 3 } },
1429
- preloadHints: [
1430
- { kind: 'sphere-volume', centerPc: { x: 1, y: 2, z: 3 }, radiusPc: 4 },
1431
- ],
1432
- },
1433
- },
1434
- initialSceneId: 'intro',
1435
- timedJourney: {
1436
- durationSecs: 2,
1437
- locationWaypoints: [
1438
- { id: 'a', timeSecs: 0, positionPc: { x: 0, y: 0, z: 0 } },
1439
- { id: 'b', timeSecs: 2, positionPc: { x: 2, y: 0, z: 0 } },
1440
- ],
1441
- cameraLookWaypoints: [
1442
- { id: 'look', timeSecs: 0, kind: 'direction', forward: { x: 1, y: 0, z: 0 } },
1443
- ],
1444
- cues: [{ id: 'cue', startSecs: 0, endSecs: 2 }],
1445
- },
1446
- onPreloadHints: (hints) => preloadEvents.push(hints),
1447
- onCue: (cue) => cueEvents.push(cue.id),
1448
- });
1449
- const viewer = await createSkykitViewer({
1450
- renderer: createRenderer(),
1451
- plugins: [journeyPlugin],
1452
- });
1453
-
1454
- viewer.update(0);
1455
- assert.equal(viewer.getViewState().limitingMagnitude, 5);
1456
- assert.equal(journeyPlugin.getSnapshot().initialSceneApplied, true);
1457
- assert.equal(journeyPlugin.getSnapshot().timedPreloadHintsEmitted, true);
1458
-
1459
- assert.equal(viewer.actions.listActions().some((entry) => entry.id === SKYKIT_ACTIONS.journey.goToChapter), true);
1460
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.goToChapter, 'hyades');
1461
- viewer.update(0);
1462
- assert.deepEqual(viewer.getViewState().observerPc, { x: 1, y: 2, z: 3 });
1463
- assert.equal(preloadEvents.length, 1);
1464
-
1465
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.seek, { timeSecs: 1 });
1466
- viewer.update(0);
1467
- assert.ok(viewer.getViewState().observerPc.x > 0);
1468
- assert.deepEqual(cueEvents, ['cue']);
1469
-
1470
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.play);
1471
- const beforePlayX = viewer.getViewState().observerPc.x;
1472
- viewer.update(0.5);
1473
- viewer.update(0);
1474
- assert.ok(viewer.getViewState().observerPc.x > beforePlayX);
1475
-
1476
- await viewer.dispose();
1477
- });
1478
-
1479
- test('journey plugin executes semantic orbit-transfer scenes without snapping', async () => {
1480
- const navigationPlugin = createSkykitNavigationPlugin({ speed: 20, acceleration: 20, deceleration: 20 });
1481
- const journey = createJourney({
1482
- initial: 'inside',
1483
- order: ['inside', 'outside', 'hyades', 'free'],
1484
- targets: {
1485
- sun: { positionPc: { x: 0, y: 0, z: 0 } },
1486
- hyades: { positionPc: { x: 18, y: 42, z: 7 } },
1487
- free: { positionPc: { x: -20, y: 35, z: 12 } },
1488
- orion: { positionPc: { x: 0, y: 0, z: -100 } },
1489
- },
1490
- scenes: {
1491
- inside: {
1492
- view: {
1493
- observerPc: { x: 0, y: 4, z: 0 },
1494
- },
1495
- camera: {
1496
- type: 'orbit',
1497
- center: 'sun',
1498
- radiusPc: 4,
1499
- angularSpeedRadPerSec: 0.4,
1500
- lookAt: 'orion',
1501
- normal: { x: 0, y: 0, z: 1 },
1502
- },
1503
- },
1504
- outside: {
1505
- camera: {
1506
- type: 'orbit',
1507
- center: 'sun',
1508
- radiusPc: 10,
1509
- angularSpeedRadPerSec: 0.2,
1510
- lookAt: 'sun',
1511
- normal: { x: 0, y: 0, z: 1 },
1512
- },
1513
- },
1514
- hyades: {
1515
- camera: {
1516
- type: 'orbit',
1517
- center: 'hyades',
1518
- radiusPc: 6,
1519
- angularSpeedRadPerSec: 0.3,
1520
- lookAt: 'hyades',
1521
- normal: { x: 0, y: 0, z: 1 },
1522
- },
1523
- },
1524
- free: {
1525
- camera: {
1526
- type: 'orbit',
1527
- center: 'free',
1528
- radiusPc: 5,
1529
- angularSpeedRadPerSec: 0.25,
1530
- lookAt: 'free',
1531
- },
1532
- },
1533
- },
1534
- travel: { type: 'orbit-transfer', durationSecs: 2, sampleStepSecs: 0.25 },
1535
- });
1536
- const viewer = await createSkykitViewer({
1537
- renderer: createRenderer(),
1538
- plugins: [
1539
- navigationPlugin,
1540
- createSkykitJourneyPlugin({ journey }),
1541
- ],
1542
- });
1543
-
1544
- await flushMicrotasks();
1545
- viewer.update(0);
1546
- assert.deepEqual(viewer.getViewState().observerPc, { x: 0, y: 4, z: 0 });
1547
- viewer.update(0.001);
1548
- viewer.update(0);
1549
- assert.ok(Math.abs(distance(viewer.getViewState().observerPc, { x: 0, y: 0, z: 0 }) - 4) < 1e-9);
1550
-
1551
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.goToChapter, 'outside');
1552
- await flushMicrotasks();
1553
- viewer.update(1);
1554
- viewer.update(0);
1555
- const halfwayRadius = distance(viewer.getViewState().observerPc, { x: 0, y: 0, z: 0 });
1556
- assert.ok(halfwayRadius > 4);
1557
- assert.ok(halfwayRadius < 10);
1558
-
1559
- viewer.update(1.1);
1560
- viewer.update(0);
1561
- const arrival = { ...viewer.getViewState().observerPc };
1562
- assert.ok(Math.abs(distance(arrival, { x: 0, y: 0, z: 0 }) - 10) < 1e-9);
1563
- assert.equal(navigationPlugin.getSnapshot().navigation.activeAutomation, 'orbit');
1564
- viewer.update(0.25);
1565
- viewer.update(0);
1566
- assert.ok(Math.abs(distance(viewer.getViewState().observerPc, { x: 0, y: 0, z: 0 }) - 10) < 1e-9);
1567
- assert.notDeepEqual(viewer.getViewState().observerPc, arrival);
1568
-
1569
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.goToChapter, 'hyades');
1570
- await flushMicrotasks();
1571
- let handoff = null;
1572
- let previous = { ...viewer.getViewState().observerPc };
1573
- for (let index = 0; index < 120; index += 1) {
1574
- viewer.update(1 / 30);
1575
- viewer.update(0);
1576
- const current = { ...viewer.getViewState().observerPc };
1577
- const step = distance(previous, current);
1578
- if (!handoff && navigationPlugin.getSnapshot().navigation.activeAutomation === 'orbit') {
1579
- handoff = { position: current, step };
1580
- }
1581
- previous = current;
1582
- }
1583
- assert.ok(handoff);
1584
- assert.ok(Math.abs(handoff.position.z - 7) < 1e-6);
1585
- assert.ok(handoff.step < 0.5);
1586
-
1587
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.goToChapter, 'free');
1588
- await flushMicrotasks();
1589
- for (let index = 0; index < 120; index += 1) {
1590
- viewer.update(1 / 30);
1591
- viewer.update(0);
1592
- }
1593
- const freeAutomation = navigationPlugin.getSnapshot().navigation.movementAutomation;
1594
- assert.equal(navigationPlugin.getSnapshot().navigation.activeAutomation, 'orbit');
1595
- assert.ok(freeAutomation?.normal);
1596
- assert.ok(Math.abs(freeAutomation.normal.z - 1) > 0.001);
1597
- assert.ok(Math.abs(distance(viewer.getViewState().observerPc, { x: -20, y: 35, z: 12 }) - 5) < 1e-9);
1598
-
1599
- await viewer.dispose();
1600
- });
1601
-
1602
- test('journey plugin uses explicit route points for authored orbit transfers', async () => {
1603
- const navigationPlugin = createSkykitNavigationPlugin({ speed: 80, acceleration: 80, deceleration: 80 });
1604
- const authoredPoints = [
1605
- { x: 0, y: 4, z: 0 },
1606
- { x: 40, y: 10, z: 80 },
1607
- { x: 100, y: 0, z: 10 },
1608
- ];
1609
- const journey = createJourney({
1610
- initial: 'inside',
1611
- order: ['inside', 'omega'],
1612
- targets: {
1613
- sun: { positionPc: { x: 0, y: 0, z: 0 } },
1614
- omega: { positionPc: { x: 100, y: 0, z: 0 } },
1615
- },
1616
- scenes: {
1617
- inside: {
1618
- view: { observerPc: { x: 0, y: 4, z: 0 } },
1619
- camera: {
1620
- type: 'orbit',
1621
- center: 'sun',
1622
- radiusPc: 4,
1623
- angularSpeedRadPerSec: 0.2,
1624
- normal: { x: 0, y: 0, z: 1 },
1625
- },
1626
- },
1627
- omega: {
1628
- camera: {
1629
- type: 'orbit',
1630
- center: 'omega',
1631
- radiusPc: 10,
1632
- angularSpeedRadPerSec: 0.12,
1633
- normal: { x: 0, y: 0, z: 1 },
1634
- },
1635
- },
1636
- },
1637
- transitions: [
1638
- {
1639
- fromSceneId: 'inside',
1640
- toSceneId: 'omega',
1641
- travel: {
1642
- type: 'orbit-transfer',
1643
- durationSecs: 2,
1644
- pointsPc: authoredPoints,
1645
- arrivalAction: {
1646
- type: 'orbit',
1647
- center: { x: 100, y: 0, z: 0 },
1648
- radius: 10,
1649
- angularSpeedRadPerSec: 0.12,
1650
- normal: { x: 0, y: 0, z: 1 },
1651
- },
1652
- },
1653
- },
1654
- ],
1655
- });
1656
- const viewer = await createSkykitViewer({
1657
- renderer: createRenderer(),
1658
- plugins: [
1659
- navigationPlugin,
1660
- createSkykitJourneyPlugin({ journey }),
1661
- ],
1662
- });
1663
-
1664
- await flushMicrotasks();
1665
- viewer.update(0);
1666
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.goToChapter, 'omega');
1667
- await flushMicrotasks();
1668
-
1669
- viewer.update(1);
1670
- viewer.update(0);
1671
- assert.ok(viewer.getViewState().observerPc.z > 20);
1672
-
1673
- viewer.update(1.1);
1674
- viewer.update(0);
1675
- assert.equal(navigationPlugin.getSnapshot().navigation.activeAutomation, 'orbit');
1676
- assert.ok(Math.abs(distance(viewer.getViewState().observerPc, { x: 100, y: 0, z: 0 }) - 10) < 1e-9);
1677
-
1678
- await viewer.dispose();
1679
- });
1680
-
1681
- test('journey plugin can route to non-orbit scenes before applying the arrival transition', async () => {
1682
- const navigationPlugin = createSkykitNavigationPlugin({ speed: 80, acceleration: 80, deceleration: 80 });
1683
- const journey = createJourney({
1684
- initial: 'omega',
1685
- order: ['omega', 'local'],
1686
- targets: {
1687
- omega: { positionPc: { x: 100, y: 0, z: 0 } },
1688
- },
1689
- scenes: {
1690
- omega: {
1691
- view: { observerPc: { x: 100, y: 0, z: 10 } },
1692
- camera: {
1693
- type: 'orbit',
1694
- center: 'omega',
1695
- radiusPc: 10,
1696
- angularSpeedRadPerSec: 0.12,
1697
- normal: { x: 0, y: 0, z: 1 },
1698
- },
1699
- },
1700
- local: {
1701
- view: { targetPc: { x: 1, y: 0, z: 0 } },
1702
- navigation: {
1703
- transitionTo: {
1704
- observerPc: { x: 0, y: 0, z: 0 },
1705
- orientationIcrs: { x: 0, y: 0, z: 0, w: 1 },
1706
- durationSecs: 1,
1707
- movement: { durationSecs: 1 },
1708
- orientationTransition: { durationSecs: 1 },
1709
- },
1710
- },
1711
- },
1712
- },
1713
- transitions: [
1714
- {
1715
- fromSceneId: 'omega',
1716
- toSceneId: 'local',
1717
- travel: {
1718
- type: 'orbit-transfer',
1719
- durationSecs: 2,
1720
- pointsPc: [
1721
- { x: 100, y: 0, z: 10 },
1722
- { x: 50, y: 0, z: 80 },
1723
- { x: 0, y: 0, z: 0 },
1724
- ],
1725
- },
1726
- },
1727
- ],
1728
- });
1729
- const viewer = await createSkykitViewer({
1730
- renderer: createRenderer(),
1731
- plugins: [
1732
- navigationPlugin,
1733
- createSkykitJourneyPlugin({ journey }),
1734
- ],
1735
- });
1736
-
1737
- await flushMicrotasks();
1738
- viewer.update(0);
1739
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.goToChapter, 'local');
1740
- await flushMicrotasks();
1741
-
1742
- viewer.update(1);
1743
- viewer.update(0);
1744
- assert.ok(viewer.getViewState().observerPc.z > 20);
1745
-
1746
- viewer.update(1.1);
1747
- await flushMicrotasks();
1748
- viewer.update(0.1);
1749
- viewer.update(0);
1750
- assert.deepEqual(viewer.getViewState().observerPc, { x: 0, y: 0, z: 0 });
1751
-
1752
- await viewer.dispose();
1753
- });
1754
-
1755
- test('journey plugin emits timed preload hints once and not on every frame', async () => {
1756
- const preloadEvents = [];
1757
- const viewer = await createSkykitViewer({
1758
- renderer: createRenderer(),
1759
- plugins: [
1760
- createSkykitJourneyPlugin({
1761
- autoPlay: true,
1762
- timedJourney: {
1763
- durationSecs: 4,
1764
- locationWaypoints: [
1765
- { id: 'a', timeSecs: 0, positionPc: { x: 0, y: 0, z: 0 } },
1766
- { id: 'b', timeSecs: 4, positionPc: { x: 4, y: 0, z: 0 } },
1767
- ],
1768
- },
1769
- evaluatorOptions: {
1770
- pathRadiusPc: 2,
1771
- lookaheadSecs: 1,
1772
- preloadStepSecs: 1,
1773
- },
1774
- onPreloadHints: (hints, source) => preloadEvents.push({ hints, source }),
1775
- }),
1776
- ],
1777
- });
1778
-
1779
- assert.equal(preloadEvents.length, 1);
1780
- assert.equal(preloadEvents[0].source.type, 'journey/timed-preload');
1781
- viewer.update(1);
1782
- viewer.update(1);
1783
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.seek, { timeSecs: 2 });
1784
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.play);
1785
- assert.equal(preloadEvents.length, 1);
1786
-
1787
- await viewer.dispose();
1788
- });
1789
-
1790
- test('journey plugin reports scene arrival immediately for static scenes', async () => {
1791
- const arrivals = [];
1792
- const viewer = await createSkykitViewer({
1793
- renderer: createRenderer(),
1794
- plugins: [
1795
- createSkykitJourneyPlugin({
1796
- scenes: {
1797
- intro: { view: { limitingMagnitude: 5 } },
1798
- },
1799
- initialSceneId: 'intro',
1800
- onSceneArrive(scene) {
1801
- arrivals.push(scene.sceneId);
1802
- },
1803
- }),
1804
- ],
1805
- });
1806
-
1807
- await flushMicrotasks();
1808
- viewer.update(0);
1809
- assert.deepEqual(arrivals, ['intro']);
1810
-
1811
- await viewer.dispose();
1812
- });
1813
-
1814
- test('journey plugin reports scene arrival after transitionTo completes', async () => {
1815
- const arrivals = [];
1816
- const viewer = await createSkykitViewer({
1817
- renderer: createRenderer(),
1818
- plugins: [
1819
- createSkykitNavigationPlugin(),
1820
- createSkykitJourneyPlugin({
1821
- scenes: {
1822
- intro: {},
1823
- away: {
1824
- navigation: {
1825
- transitionTo: {
1826
- observerPc: { x: 10, y: 0, z: 0 },
1827
- durationSecs: 1,
1828
- },
1829
- },
1830
- },
1831
- },
1832
- initialSceneId: 'intro',
1833
- onSceneArrive(scene) {
1834
- arrivals.push(scene.sceneId);
1835
- },
1836
- }),
1837
- ],
1838
- });
1839
-
1840
- await flushMicrotasks();
1841
- arrivals.length = 0;
1842
-
1843
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.goToChapter, 'away');
1844
- await flushMicrotasks();
1845
- viewer.update(0.5);
1846
- viewer.update(0);
1847
- assert.deepEqual(arrivals, []);
1848
-
1849
- viewer.update(0.5);
1850
- viewer.update(0);
1851
- assert.deepEqual(arrivals, ['away']);
1852
-
1853
- await viewer.dispose();
1854
- });
1855
-
1856
- test('journey plugin reports scene arrival after orbit-transfer travel completes', async () => {
1857
- const arrivals = [];
1858
- const journey = createJourney({
1859
- initial: 'sun',
1860
- order: ['sun', 'cluster'],
1861
- targets: {
1862
- sun: { positionPc: { x: 0, y: 0, z: 0 } },
1863
- cluster: { positionPc: { x: 20, y: 0, z: 0 } },
1864
- },
1865
- scenes: {
1866
- sun: {
1867
- camera: {
1868
- type: 'orbit',
1869
- center: 'sun',
1870
- radiusPc: 4,
1871
- angularSpeedRadPerSec: 0.2,
1872
- normal: { x: 0, y: 0, z: 1 },
1873
- },
1874
- },
1875
- cluster: {
1876
- camera: {
1877
- type: 'orbit',
1878
- center: 'cluster',
1879
- radiusPc: 5,
1880
- angularSpeedRadPerSec: 0.2,
1881
- normal: { x: 0, y: 0, z: 1 },
1882
- },
1883
- },
1884
- },
1885
- travel: { type: 'orbit-transfer', durationSecs: 1, sampleStepSecs: 0.25 },
1886
- });
1887
- const viewer = await createSkykitViewer({
1888
- renderer: createRenderer(),
1889
- plugins: [
1890
- createSkykitNavigationPlugin(),
1891
- createSkykitJourneyPlugin({
1892
- journey,
1893
- onSceneArrive(scene) {
1894
- arrivals.push(scene.sceneId);
1895
- },
1896
- }),
1897
- ],
1898
- });
1899
-
1900
- await flushMicrotasks();
1901
- arrivals.length = 0;
1902
-
1903
- await viewer.actions.invoke(SKYKIT_ACTIONS.journey.goToChapter, 'cluster');
1904
- await flushMicrotasks();
1905
- viewer.update(0.5);
1906
- viewer.update(0);
1907
- assert.deepEqual(arrivals, []);
1908
-
1909
- viewer.update(0.6);
1910
- viewer.update(0);
1911
- assert.deepEqual(arrivals, ['cluster']);
1912
-
1913
- await viewer.dispose();
1914
- });
1915
1545
 
1916
1546
  test('spatial preload hints map to star-octree requests without exposing provider internals', () => {
1917
1547
  const hints = [
@@ -2323,6 +1953,12 @@ function localVectorFromView(view, vector) {
2323
1953
  return { x: result.x, y: result.y, z: result.z };
2324
1954
  }
2325
1955
 
1956
+ function assertVectorApprox(actual, expected, epsilon = 1e-9) {
1957
+ assert.ok(Math.abs(actual.x - expected.x) < epsilon, `x ${actual.x} !== ${expected.x}`);
1958
+ assert.ok(Math.abs(actual.y - expected.y) < epsilon, `y ${actual.y} !== ${expected.y}`);
1959
+ assert.ok(Math.abs(actual.z - expected.z) < epsilon, `z ${actual.z} !== ${expected.z}`);
1960
+ }
1961
+
2326
1962
  function createPointerTarget() {
2327
1963
  const target = createEventTarget();
2328
1964
  target.clientWidth = 800;