@found-in-space/skykit 0.2.0-alpha.0 → 0.2.0-dev.20260527.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.
Files changed (42) hide show
  1. package/README.md +223 -8
  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 +644 -217
  6. package/package.json +46 -5
  7. package/src/__tests__/skykit-anchored-images.test.js +32 -4
  8. package/src/__tests__/skykit-browser.test.js +442 -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 +123 -2
  13. package/src/__tests__/skykit.test.js +138 -1
  14. package/src/anchored-images.js +14 -15
  15. package/src/browser-addons.d.ts +16 -0
  16. package/src/browser-addons.js +155 -0
  17. package/src/browser-constellations.d.ts +13 -0
  18. package/src/browser-constellations.js +387 -0
  19. package/src/browser-journey.d.ts +8 -0
  20. package/src/browser-journey.js +240 -0
  21. package/src/browser.d.ts +170 -0
  22. package/src/browser.js +369 -0
  23. package/src/data.d.ts +133 -0
  24. package/src/data.js +447 -0
  25. package/src/embed.d.ts +6 -0
  26. package/src/embed.js +119 -0
  27. package/src/hr-diagram.js +23 -5
  28. package/src/index.d.ts +32 -7
  29. package/src/plugins.js +87 -43
  30. package/src/story.d.ts +57 -0
  31. package/src/story.js +396 -0
  32. package/src/three-shim.d.ts +32 -0
  33. package/src/touch-os.d.ts +70 -0
  34. package/src/touch-os.js +275 -0
  35. package/src/utils.js +96 -6
  36. package/src/viewer-entry.d.ts +10 -0
  37. package/src/viewer-entry.js +4 -0
  38. package/src/viewer.js +110 -12
  39. package/src/xr/plugins.js +224 -13
  40. package/src/xr/session.js +60 -14
  41. package/src/xr.d.ts +22 -0
  42. package/src/xr.js +1 -0
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@found-in-space/skykit",
3
- "version": "0.2.0-alpha.0",
3
+ "version": "0.2.0-dev.20260527.0",
4
4
  "description": "Slim composition and teaching layer for Found in Space packages",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/Found-in-Space/skykit",
10
+ "directory": "packages/skykit"
11
+ },
7
12
  "publishConfig": {
8
13
  "access": "public"
9
14
  },
@@ -14,6 +19,38 @@
14
19
  "types": "./src/index.d.ts",
15
20
  "default": "./src/index.js"
16
21
  },
22
+ "./browser": {
23
+ "types": "./src/browser.d.ts",
24
+ "default": "./src/browser.js"
25
+ },
26
+ "./browser-addons": {
27
+ "types": "./src/browser-addons.d.ts",
28
+ "default": "./src/browser-addons.js"
29
+ },
30
+ "./browser-constellations": {
31
+ "types": "./src/browser-constellations.d.ts",
32
+ "default": "./src/browser-constellations.js"
33
+ },
34
+ "./browser-journey": {
35
+ "types": "./src/browser-journey.d.ts",
36
+ "default": "./src/browser-journey.js"
37
+ },
38
+ "./embed": {
39
+ "types": "./src/embed.d.ts",
40
+ "default": "./src/embed.js"
41
+ },
42
+ "./viewer": {
43
+ "types": "./src/viewer-entry.d.ts",
44
+ "default": "./src/viewer-entry.js"
45
+ },
46
+ "./data": {
47
+ "types": "./src/data.d.ts",
48
+ "default": "./src/data.js"
49
+ },
50
+ "./story": {
51
+ "types": "./src/story.d.ts",
52
+ "default": "./src/story.js"
53
+ },
17
54
  "./parallax": {
18
55
  "types": "./src/parallax.d.ts",
19
56
  "default": "./src/parallax.js"
@@ -32,7 +69,10 @@
32
69
  "examples",
33
70
  "README.md"
34
71
  ],
35
- "sideEffects": false,
72
+ "sideEffects": [
73
+ "./src/embed.js",
74
+ "./src/story.js"
75
+ ],
36
76
  "scripts": {
37
77
  "typecheck": "tsc -p tsconfig.json --noEmit",
38
78
  "test": "node --test"
@@ -41,16 +81,17 @@
41
81
  "@found-in-space/anchored-image": "0.2.0-alpha.0",
42
82
  "@found-in-space/hr-diagram": "0.2.0-alpha.0",
43
83
  "@found-in-space/journey": "0.2.0-alpha.0",
44
- "@found-in-space/spatial": "0.2.0-alpha.0",
84
+ "@found-in-space/meta-sidecar-provider": "0.2.0-alpha.0",
85
+ "@found-in-space/spatial": "0.2.0-dev.20260527.0",
45
86
  "@found-in-space/star-octree-provider": "0.2.0-alpha.0",
46
87
  "@found-in-space/star-trees": "0.2.0-alpha.0",
47
88
  "@found-in-space/three-star-field": "0.2.0-alpha.0"
48
89
  },
49
90
  "devDependencies": {
50
- "@found-in-space/touch-os": "0.2.0-dev.1"
91
+ "@found-in-space/touch-os": "0.2.0-dev.3"
51
92
  },
52
93
  "peerDependencies": {
53
- "@found-in-space/touch-os": ">=0.2.0-dev.1 <1",
94
+ "@found-in-space/touch-os": ">=0.2.0-dev.3 <1",
54
95
  "three": "^0.170.0"
55
96
  },
56
97
  "peerDependenciesMeta": {
@@ -147,7 +147,7 @@ test('anchored image sky plugin preloads controller selection and mounts fixed-a
147
147
  });
148
148
  const viewer = await createSkykitViewer({
149
149
  renderer: createRenderer(),
150
- view: { directionIcrs: { x: 1, y: 0, z: 0 } },
150
+ view: { lookAt: { raDeg: 0, decDeg: 0 } },
151
151
  plugins: [plugin],
152
152
  });
153
153
 
@@ -161,6 +161,34 @@ test('anchored image sky plugin preloads controller selection and mounts fixed-a
161
161
  await viewer.dispose();
162
162
  });
163
163
 
164
+ test('anchored image sky plugin can mount art into a named scale band', async () => {
165
+ const catalog = await createAnchoredImageCatalog({ manifest: MANIFEST });
166
+ const requests = [];
167
+ const scaleRoot = new THREE.Group();
168
+ const plugin = createAnchoredImageSkyPlugin({
169
+ id: 'banded-art',
170
+ catalog,
171
+ controller: createManualAnchoredImageController({ selection: 'alpha' }),
172
+ loading: 'preload',
173
+ textureLoader: createTextureLoader(requests),
174
+ anchorMode: 'scale-banded',
175
+ scaleBandId: 'constellation-art',
176
+ });
177
+ const viewer = await createSkykitViewer({
178
+ renderer: createRenderer(),
179
+ roots: {
180
+ scaleBandedContentRoots: new Map([['constellation-art', scaleRoot]]),
181
+ },
182
+ view: { lookAt: { raDeg: 0, decDeg: 0 } },
183
+ plugins: [plugin],
184
+ });
185
+
186
+ assert.deepEqual(requests, ['alpha.png']);
187
+ assert.equal(scaleRoot.children.some((child) => child.name === 'banded-art'), true);
188
+
189
+ await viewer.dispose();
190
+ });
191
+
164
192
  test('anchored image sky plugin lazy-loads active controller entries and caches them', async () => {
165
193
  const catalog = await createAnchoredImageCatalog({ manifest: MANIFEST });
166
194
  const requests = [];
@@ -174,13 +202,13 @@ test('anchored image sky plugin lazy-loads active controller entries and caches
174
202
  });
175
203
  const viewer = await createSkykitViewer({
176
204
  renderer: createRenderer(),
177
- view: { directionIcrs: { x: 1, y: 0, z: 0 } },
205
+ view: { lookAt: { raDeg: 0, decDeg: 0 } },
178
206
  plugins: [plugin],
179
207
  });
180
208
  await flushPromises();
181
209
 
182
210
  assert.deepEqual(requests, ['alpha.png']);
183
- viewer.requestViewState({ directionIcrs: { x: 0, y: 1, z: 0 } }, 'test-active-change');
211
+ viewer.requestViewState({ lookAt: { raDeg: 90, decDeg: 0 } }, 'test-active-change');
184
212
  viewer.update(0);
185
213
  await flushPromises();
186
214
  assert.deepEqual(requests, ['alpha.png', 'beta.png']);
@@ -208,7 +236,7 @@ test('anchored image sky plugin fades opacity by seconds and keeps fading object
208
236
  });
209
237
  const viewer = await createSkykitViewer({
210
238
  renderer: createRenderer(),
211
- view: { directionIcrs: { x: 1, y: 0, z: 0 } },
239
+ view: { lookAt: { raDeg: 0, decDeg: 0 } },
212
240
  plugins: [plugin],
213
241
  });
214
242
  await flushPromises();
@@ -0,0 +1,442 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import * as THREE from 'three';
4
+
5
+ import {
6
+ installSkykitBrowserGlobal,
7
+ registerBrowserInstance,
8
+ } from '../browser-addons.js';
9
+ import { createSkykitBrowser } from '../browser.js';
10
+
11
+ test('createSkykitBrowser wires the starter viewer and extra plugins', async () => {
12
+ await withFakeWindow(async (fakeWindow) => {
13
+ const host = createHost();
14
+ const status = { textContent: '' };
15
+ const renderer = createRenderer();
16
+ const provider = createProvider();
17
+ const starField = createStarField();
18
+ const extraPartCalls = [];
19
+
20
+ const browser = await createSkykitBrowser({
21
+ host,
22
+ status,
23
+ renderer,
24
+ camera: new THREE.PerspectiveCamera(),
25
+ provider,
26
+ starField,
27
+ autoResize: false,
28
+ autoDispose: false,
29
+ autoStart: false,
30
+ maxDevicePixelRatio: 1.5,
31
+ plugins: [
32
+ (context) => context.addPart({
33
+ id: 'extra-part',
34
+ attach() { extraPartCalls.push('attach'); },
35
+ start() { extraPartCalls.push('start'); },
36
+ }),
37
+ ],
38
+ });
39
+
40
+ assert.equal(host.children[0], renderer.domElement);
41
+ assert.equal(host.style.touchAction, 'none');
42
+ assert.equal(renderer.clearColor.color, 0x02040b);
43
+ assert.deepEqual(renderer.size, { width: 640, height: 360, updateStyle: true });
44
+ assert.equal(renderer.pixelRatio, 1.5);
45
+ assert.equal(provider.sessions.length, 1);
46
+ assert.equal(provider.sessions[0].subscribers.size, 1);
47
+ assert.equal(provider.sessions[0].updateViewCalls.length, 2);
48
+ assert.deepEqual(extraPartCalls, ['attach', 'start']);
49
+ assert.match(status.textContent, /"starsLoaded": 0/);
50
+
51
+ const marker = new THREE.Object3D();
52
+ const markerHandle = browser.addObject(marker, {
53
+ id: 'hyades-marker',
54
+ positionPc: { x: 17.574, y: 42.316, z: 13.963 },
55
+ });
56
+ await Promise.resolve();
57
+ assert.ok(Math.abs(marker.position.x - 0.017574) < 1e-12);
58
+ assert.ok(Math.abs(marker.position.y - 0.042316) < 1e-12);
59
+ assert.ok(Math.abs(marker.position.z - 0.013963) < 1e-12);
60
+ assert.equal(browser.viewer.roots.originContentRoot.children.includes(marker), true);
61
+ markerHandle.remove();
62
+ await Promise.resolve();
63
+ assert.equal(browser.viewer.roots.originContentRoot.children.includes(marker), false);
64
+
65
+ browser.resize();
66
+ assert.equal(fakeWindow.addedEvents.length, 0);
67
+
68
+ await browser.dispose();
69
+ assert.equal(provider.disposed, false, 'caller-owned provider should not be disposed');
70
+ assert.equal(provider.sessions[0].disposed, true);
71
+ assert.equal(starField.disposed, true);
72
+ assert.equal(renderer.disposed, false, 'caller-owned renderer should not be disposed');
73
+ assert.equal(host.children.length, 0);
74
+ });
75
+ });
76
+
77
+ test('createSkykitBrowser registers resize and page-lifecycle cleanup by default', async () => {
78
+ await withFakeWindow(async (fakeWindow) => {
79
+ const host = createHost();
80
+ const renderer = createRenderer();
81
+ const provider = createProvider();
82
+ const starField = createStarField();
83
+
84
+ const browser = await createSkykitBrowser({
85
+ host,
86
+ status: false,
87
+ renderer,
88
+ provider,
89
+ starField,
90
+ autoStart: false,
91
+ });
92
+
93
+ assert.deepEqual(fakeWindow.addedEvents.map((entry) => entry.type), [
94
+ 'resize',
95
+ 'pagehide',
96
+ 'beforeunload',
97
+ ]);
98
+
99
+ await browser.dispose();
100
+
101
+ assert.deepEqual(fakeWindow.removedEvents.map((entry) => entry.type), [
102
+ 'resize',
103
+ 'pagehide',
104
+ 'beforeunload',
105
+ ]);
106
+ });
107
+ });
108
+
109
+ test('createSkykitBrowser accepts startup lookAt and mouse look mode', async () => {
110
+ await withFakeWindow(async () => {
111
+ const browser = await createSkykitBrowser({
112
+ host: createHost(),
113
+ status: false,
114
+ renderer: createRenderer(),
115
+ provider: createProvider(),
116
+ starField: createStarField(),
117
+ autoResize: false,
118
+ autoDispose: false,
119
+ autoStart: false,
120
+ mouseMode: 'strafe',
121
+ lookAt: { targetPc: { x: 10, y: 0, z: 0 } },
122
+ });
123
+
124
+ const view = browser.viewer.getViewState();
125
+ assert.deepEqual(view.targetPc, { x: 10, y: 0, z: 0 });
126
+ assert.ok(view.orientationIcrs);
127
+ assert.equal(
128
+ browser.viewer.getSnapshot().parts.some((part) => part.id === 'mouse-look'),
129
+ true,
130
+ );
131
+ assert.equal(
132
+ browser.viewer.getSnapshot().parts.some((part) => part.id === 'sky-grab'),
133
+ false,
134
+ );
135
+
136
+ await browser.dispose();
137
+ });
138
+ });
139
+
140
+ test('createSkykitBrowser can disable mouse drag controls', async () => {
141
+ await withFakeWindow(async () => {
142
+ const browser = await createSkykitBrowser({
143
+ host: createHost(),
144
+ status: false,
145
+ renderer: createRenderer(),
146
+ provider: createProvider(),
147
+ starField: createStarField(),
148
+ autoResize: false,
149
+ autoDispose: false,
150
+ autoStart: false,
151
+ mouseMode: 'none',
152
+ });
153
+
154
+ assert.equal(
155
+ browser.viewer.getSnapshot().parts.some((part) => part.id === 'mouse-look' || part.id === 'sky-grab'),
156
+ false,
157
+ );
158
+
159
+ await browser.dispose();
160
+ });
161
+ });
162
+
163
+ test('browser.install adds plugins after startup and cleans returned teardowns', async () => {
164
+ await withFakeWindow(async () => {
165
+ const calls = [];
166
+ const browser = await createSkykitBrowser({
167
+ host: createHost(),
168
+ status: false,
169
+ renderer: createRenderer(),
170
+ provider: createProvider(),
171
+ starField: createStarField(),
172
+ autoResize: false,
173
+ autoDispose: false,
174
+ autoStart: false,
175
+ });
176
+
177
+ const uninstall = await browser.install((context) => {
178
+ context.addPart({
179
+ id: 'late-part',
180
+ attach() { calls.push('attach'); },
181
+ start() { calls.push('start'); },
182
+ });
183
+ return () => calls.push('teardown');
184
+ });
185
+
186
+ await Promise.resolve();
187
+ assert.deepEqual(calls, ['attach', 'start']);
188
+ assert.equal(browser.viewer.getSnapshot().parts.some((part) => part.id === 'late-part'), true);
189
+ uninstall();
190
+ assert.deepEqual(calls, ['attach', 'start', 'teardown']);
191
+
192
+ await browser.dispose();
193
+ });
194
+ });
195
+
196
+ test('browser journey capability transitions through navigation actions and loads instances', async () => {
197
+ await withFakeWindow(async () => {
198
+ const browser = await createSkykitBrowser({
199
+ host: createHost(),
200
+ status: false,
201
+ renderer: createRenderer(),
202
+ provider: createProvider(),
203
+ starField: createStarField(),
204
+ autoResize: false,
205
+ autoDispose: false,
206
+ autoStart: false,
207
+ });
208
+
209
+ await browser.journey.transitionTo({
210
+ lookAt: { targetPc: { x: 10, y: 0, z: 0 } },
211
+ durationSecs: 1,
212
+ });
213
+ browser.viewer.update(1);
214
+ browser.viewer.update(0);
215
+ assert.ok(browser.viewer.getViewState().orientationIcrs);
216
+ assert.equal(browser.capabilities.has('skykit:navigation'), true);
217
+
218
+ const journey = await browser.journey.load({
219
+ initial: 'home',
220
+ scenes: {
221
+ home: { view: { observerPc: { x: 0, y: 0, z: 0 } } },
222
+ away: { view: { observerPc: { x: 1, y: 2, z: 3 } } },
223
+ },
224
+ });
225
+ await journey.goTo('away');
226
+ browser.viewer.update(0);
227
+ assert.deepEqual(browser.viewer.getViewState().observerPc, { x: 1, y: 2, z: 3 });
228
+ assert.equal(journey.getSnapshot().disposed, false);
229
+ journey.dispose();
230
+ assert.equal(journey.getSnapshot().disposed, true);
231
+
232
+ await browser.dispose();
233
+ });
234
+ });
235
+
236
+ test('browser constellations capability loads manifest boundaries without art', async () => {
237
+ await withFakeWindow(async () => {
238
+ const browser = await createSkykitBrowser({
239
+ host: createHost(),
240
+ status: false,
241
+ renderer: createRenderer(),
242
+ provider: createProvider(),
243
+ starField: createStarField(),
244
+ autoResize: false,
245
+ autoDispose: false,
246
+ autoStart: false,
247
+ });
248
+
249
+ const constellations = await browser.constellations.load({
250
+ art: 'off',
251
+ manifest: {
252
+ id: 'test-skyculture',
253
+ boundaries: {
254
+ edges: ['001:002 M+ 00:00:00 +00:00:00 01:00:00 +00:00:00 AAA BBB'],
255
+ },
256
+ constellations: [],
257
+ },
258
+ });
259
+
260
+ assert.equal(constellations.getSnapshot().lineCount, 1);
261
+ assert.equal(browser.capabilities.has('skykit:browser.constellations'), true);
262
+ assert.equal(browser.viewer.roots.observerContentRoot.children.some((child) => child.name === 'constellation-boundaries'), true);
263
+ assert.equal(constellations.hide(), false);
264
+ assert.equal(constellations.show(), true);
265
+
266
+ await browser.dispose();
267
+ });
268
+ });
269
+
270
+ test('Skykit browser global resolves existing and future browsers and installs add-ons once', async () => {
271
+ await withFakeWindow(async () => {
272
+ const service = installSkykitBrowserGlobal(/** @type {typeof globalThis} */ ({}));
273
+ const host = createHost();
274
+ const browser = await createSkykitBrowser({
275
+ host,
276
+ status: false,
277
+ renderer: createRenderer(),
278
+ provider: createProvider(),
279
+ starField: createStarField(),
280
+ autoResize: false,
281
+ autoDispose: false,
282
+ autoStart: false,
283
+ });
284
+ let installs = 0;
285
+ service.registerBrowserAddon({
286
+ id: 'test-addon',
287
+ install({ browser: installedBrowser }) {
288
+ installs += 1;
289
+ assert.equal(installedBrowser, browser);
290
+ },
291
+ });
292
+
293
+ const unregister = registerBrowserInstance(service, host, browser);
294
+ assert.equal(await service.whenReady(), browser);
295
+ assert.equal(installs, 1);
296
+ service.registerBrowserAddon({
297
+ id: 'test-addon',
298
+ install() {
299
+ installs += 1;
300
+ },
301
+ });
302
+ assert.equal(installs, 1);
303
+ unregister();
304
+ await browser.dispose();
305
+ });
306
+ });
307
+
308
+ async function withFakeWindow(callback) {
309
+ const previousWindow = globalThis.window;
310
+ const fakeWindow = {
311
+ devicePixelRatio: 2,
312
+ addedEvents: [],
313
+ removedEvents: [],
314
+ addEventListener(type, listener, options) {
315
+ this.addedEvents.push({ type, listener, options });
316
+ },
317
+ removeEventListener(type, listener) {
318
+ this.removedEvents.push({ type, listener });
319
+ },
320
+ };
321
+
322
+ Object.defineProperty(globalThis, 'window', {
323
+ configurable: true,
324
+ value: fakeWindow,
325
+ });
326
+
327
+ try {
328
+ await callback(fakeWindow);
329
+ } finally {
330
+ if (previousWindow === undefined) {
331
+ delete globalThis.window;
332
+ } else {
333
+ Object.defineProperty(globalThis, 'window', {
334
+ configurable: true,
335
+ value: previousWindow,
336
+ });
337
+ }
338
+ }
339
+ }
340
+
341
+ function createHost() {
342
+ return {
343
+ children: [],
344
+ clientWidth: 640,
345
+ clientHeight: 360,
346
+ style: {},
347
+ appendChild(node) {
348
+ this.children.push(node);
349
+ },
350
+ removeChild(node) {
351
+ const index = this.children.indexOf(node);
352
+ if (index >= 0) this.children.splice(index, 1);
353
+ },
354
+ };
355
+ }
356
+
357
+ function createRenderer() {
358
+ return {
359
+ domElement: { nodeName: 'CANVAS' },
360
+ clearColor: null,
361
+ size: null,
362
+ pixelRatio: null,
363
+ disposed: false,
364
+ setClearColor(color, alpha) {
365
+ this.clearColor = { color, alpha };
366
+ },
367
+ setSize(width, height, updateStyle) {
368
+ this.size = { width, height, updateStyle };
369
+ },
370
+ setPixelRatio(value) {
371
+ this.pixelRatio = value;
372
+ },
373
+ render() {},
374
+ dispose() {
375
+ this.disposed = true;
376
+ },
377
+ };
378
+ }
379
+
380
+ function createProvider() {
381
+ return {
382
+ sessions: [],
383
+ disposed: false,
384
+ createSession(options) {
385
+ const session = createSession(options);
386
+ this.sessions.push(session);
387
+ return session;
388
+ },
389
+ dispose() {
390
+ this.disposed = true;
391
+ },
392
+ };
393
+ }
394
+
395
+ function createSession(options) {
396
+ return {
397
+ id: 'test-session',
398
+ options,
399
+ subscribers: new Set(),
400
+ updateViewCalls: [],
401
+ disposed: false,
402
+ subscribe(callback) {
403
+ this.subscribers.add(callback);
404
+ return () => {
405
+ this.subscribers.delete(callback);
406
+ };
407
+ },
408
+ updateView(view, options) {
409
+ this.updateViewCalls.push({ view, options });
410
+ },
411
+ getSnapshot() {
412
+ return {
413
+ id: this.id,
414
+ updateViewCalls: this.updateViewCalls.length,
415
+ };
416
+ },
417
+ async dispose() {
418
+ this.disposed = true;
419
+ },
420
+ };
421
+ }
422
+
423
+ function createStarField() {
424
+ return {
425
+ object3d: new THREE.Group(),
426
+ disposed: false,
427
+ view: null,
428
+ apply() {},
429
+ setView(view) {
430
+ this.view = view;
431
+ },
432
+ getSnapshot() {
433
+ return {
434
+ starCount: 0,
435
+ hasView: Boolean(this.view),
436
+ };
437
+ },
438
+ dispose() {
439
+ this.disposed = true;
440
+ },
441
+ };
442
+ }