@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/src/browser.js ADDED
@@ -0,0 +1,369 @@
1
+ import * as THREE from 'three';
2
+
3
+ import {
4
+ OCTREE_DEFAULT,
5
+ createStarOctreeProviderService,
6
+ } from '@found-in-space/star-octree-provider';
7
+ import { createObserverShellStrategy } from '@found-in-space/star-trees';
8
+ import { createThreeStarField } from '@found-in-space/three-star-field';
9
+
10
+ import { createSkykitAnimationLoop } from './animation-loop.js';
11
+ import { SKYKIT_ACTIONS, SKYKIT_CONTROLS } from './actions.js';
12
+ import {
13
+ createKeyboardNavigationPlugin,
14
+ createObject3dPlugin,
15
+ createMouseLookPlugin,
16
+ createSkykitJourneyPlugin,
17
+ createSkykitNavigationPlugin,
18
+ createSkyGrabPlugin,
19
+ createSkykitStatusPlugin,
20
+ createStreamingStarsPlugin,
21
+ } from './plugins.js';
22
+ import { createObject3dLayer } from './layers.js';
23
+ import { createSkykitViewer } from './viewer.js';
24
+
25
+ const DEFAULT_LIMITING_MAGNITUDE = 6.5;
26
+ const DEFAULT_EXPOSURE = 2400;
27
+ const DEFAULT_UNITS_PER_PARSEC = 0.001;
28
+ const DEFAULT_MAX_DEVICE_PIXEL_RATIO = 2;
29
+
30
+ /**
31
+ * Create the default browser star viewer used by the starter lessons.
32
+ *
33
+ * @param {import('./browser.d.ts').SkykitBrowserOptions | import('./browser.d.ts').SkykitBrowserHost} [input]
34
+ * @returns {Promise<import('./browser.d.ts').SkykitBrowser>}
35
+ */
36
+ export async function createSkykitBrowser(input = {}) {
37
+ const options = normalizeOptions(input);
38
+ const host = resolveTarget(options.host ?? '#viewer', 'SkyKit browser host');
39
+ const statusInput = options.status === true ? '#status' : options.status;
40
+ const statusTarget = statusInput === false || statusInput == null
41
+ ? null
42
+ : resolveTarget(statusInput, 'SkyKit status target');
43
+ const limitingMagnitude = positive(options.limitingMagnitude, DEFAULT_LIMITING_MAGNITUDE);
44
+ const renderer = options.renderer ?? new THREE.WebGLRenderer({
45
+ antialias: options.antialias !== false,
46
+ });
47
+ const camera = options.camera ?? new THREE.PerspectiveCamera(
48
+ positive(options.fovDeg, 58),
49
+ 1,
50
+ positive(options.near, 0.0001),
51
+ positive(options.far, 1000),
52
+ );
53
+ const provider = options.provider ?? createStarOctreeProviderService({
54
+ url: options.octreeUrl ?? OCTREE_DEFAULT,
55
+ });
56
+ const starField = options.starField ?? createThreeStarField({
57
+ limitingMagnitude,
58
+ exposure: positive(options.exposure, DEFAULT_EXPOSURE),
59
+ });
60
+
61
+ renderer.setClearColor?.(options.background ?? 0x02040b, 1);
62
+ if (host.style && options.disableTouchAction !== false) host.style.touchAction = 'none';
63
+
64
+ const viewer = await createSkykitViewer({
65
+ host,
66
+ renderer,
67
+ camera,
68
+ view: {
69
+ observerPc: { x: 0, y: 0, z: 0 },
70
+ coordinateUnitsPerParsec: positive(options.coordinateUnitsPerParsec, DEFAULT_UNITS_PER_PARSEC),
71
+ limitingMagnitude,
72
+ ...(options.lookAt ? { lookAt: options.lookAt } : {}),
73
+ ...(options.view ?? {}),
74
+ },
75
+ plugins: [
76
+ createStreamingStarsPlugin({
77
+ id: 'stars',
78
+ provider,
79
+ renderer: starField,
80
+ session: {
81
+ strategy: options.strategy ?? createObserverShellStrategy(),
82
+ ...(options.session ?? {}),
83
+ },
84
+ }),
85
+ ...(options.keyboard === false ? [] : [
86
+ createKeyboardNavigationPlugin({
87
+ speedPcPerSec: positive(options.speedPcPerSec, 2),
88
+ ...(options.keyboard ?? {}),
89
+ }),
90
+ ]),
91
+ ...createPointerPlugins(options, host),
92
+ ...(statusTarget ? [createStatusPlugin(statusTarget)] : []),
93
+ ...(options.plugins ?? []),
94
+ ],
95
+ });
96
+
97
+ const loop = createSkykitAnimationLoop(viewer, options.loop);
98
+ const capabilities = new Set();
99
+ /** @type {Array<() => void | Promise<void>>} */
100
+ const browserDisposables = [];
101
+ let disposed = false;
102
+ /** @type {import('./browser.d.ts').SkykitBrowser} */
103
+ const browser = {
104
+ viewer,
105
+ renderer,
106
+ camera,
107
+ provider,
108
+ starField,
109
+ loop,
110
+ capabilities,
111
+ install,
112
+ addObject,
113
+ resize,
114
+ dispose,
115
+ };
116
+ browser.journey = createLazyJourneyFacade(browser);
117
+ browser.constellations = createLazyConstellationsFacade(browser, host);
118
+
119
+ function resize() {
120
+ viewer.resize({
121
+ devicePixelRatio: Math.min(
122
+ window.devicePixelRatio || 1,
123
+ positive(options.maxDevicePixelRatio, DEFAULT_MAX_DEVICE_PIXEL_RATIO),
124
+ ),
125
+ });
126
+ }
127
+ async function dispose() {
128
+ if (disposed) return;
129
+ disposed = true;
130
+ window.removeEventListener('resize', resize);
131
+ window.removeEventListener('pagehide', disposeSoon);
132
+ window.removeEventListener('beforeunload', disposeSoon);
133
+ for (const disposable of browserDisposables.splice(0).reverse()) {
134
+ await disposable();
135
+ }
136
+ loop.dispose();
137
+ await viewer.dispose();
138
+ if (!options.provider) await provider.dispose?.();
139
+ if (!options.renderer) renderer.dispose?.();
140
+ }
141
+ function disposeSoon() { void dispose(); }
142
+
143
+ if (options.autoResize !== false) window.addEventListener('resize', resize);
144
+ if (options.autoDispose !== false) {
145
+ window.addEventListener('pagehide', disposeSoon, { once: true });
146
+ window.addEventListener('beforeunload', disposeSoon, { once: true });
147
+ }
148
+ resize();
149
+ if (options.autoStart !== false) loop.start();
150
+
151
+ return browser;
152
+
153
+ /**
154
+ * @param {import('./browser.d.ts').SkykitBrowserInstallInput} input
155
+ * @returns {Promise<import('./index.d.ts').SkykitPluginTeardown>}
156
+ */
157
+ async function install(input) {
158
+ if (!input) return () => {};
159
+ const teardown = isBrowserAddon(input)
160
+ ? await input.install(createBrowserAddonContext(input))
161
+ : await viewer.addPlugin(/** @type {import('./index.d.ts').SkykitPluginInput} */ (input));
162
+ if (typeof teardown !== 'function') return () => {};
163
+ browserDisposables.push(teardown);
164
+ return () => {
165
+ const index = browserDisposables.indexOf(teardown);
166
+ if (index >= 0) browserDisposables.splice(index, 1);
167
+ void teardown();
168
+ };
169
+ }
170
+
171
+ /**
172
+ * @param {import('./browser.d.ts').SkykitBrowserAddon} addon
173
+ * @returns {import('./browser.d.ts').SkykitBrowserAddonContext}
174
+ */
175
+ function createBrowserAddonContext(addon) {
176
+ return {
177
+ id: addon.id,
178
+ host,
179
+ browser,
180
+ viewer,
181
+ THREE,
182
+ skykit: {
183
+ SKYKIT_ACTIONS,
184
+ SKYKIT_CONTROLS,
185
+ createObject3dPlugin,
186
+ createSkykitJourneyPlugin,
187
+ createSkykitNavigationPlugin,
188
+ },
189
+ };
190
+ }
191
+
192
+ /**
193
+ * @param {THREE.Object3D} object3d
194
+ * @param {import('./browser.d.ts').SkykitBrowserObjectOptions} [objectOptions]
195
+ * @returns {import('./browser.d.ts').SkykitBrowserObjectHandle}
196
+ */
197
+ function addObject(object3d, objectOptions = {}) {
198
+ if (!object3d) {
199
+ throw new TypeError('SkykitBrowser.addObject() requires a THREE.Object3D.');
200
+ }
201
+ const { positionPc, ...layerOptions } = objectOptions;
202
+ if (positionPc) {
203
+ setObjectPositionPc(
204
+ object3d,
205
+ positionPc,
206
+ viewer.getViewState().coordinateUnitsPerParsec,
207
+ );
208
+ }
209
+ const part = createObject3dLayer({
210
+ ...layerOptions,
211
+ object3d,
212
+ anchorMode: layerOptions.anchorMode ?? 'world-space',
213
+ });
214
+ const remove = viewer.addPart(part);
215
+ return {
216
+ object3d,
217
+ part,
218
+ remove,
219
+ dispose: remove,
220
+ };
221
+ }
222
+ }
223
+
224
+ /** @param {unknown} input */
225
+ function isBrowserAddon(input) {
226
+ return Boolean(input && typeof input === 'object' && typeof /** @type {{ install?: unknown }} */ (input).install === 'function');
227
+ }
228
+
229
+ /**
230
+ * @param {import('./browser.d.ts').SkykitBrowser} browser
231
+ * @returns {import('./browser.d.ts').SkykitBrowserJourneyFacade}
232
+ */
233
+ function createLazyJourneyFacade(browser) {
234
+ /** @type {Promise<import('./browser.d.ts').SkykitBrowserJourneyFacade> | null} */
235
+ let loaded = null;
236
+ const loadCapability = () => {
237
+ loaded ??= import('./browser-journey.js')
238
+ .then((module) => module.installSkykitJourneyBrowserCapability({ browser }));
239
+ return loaded;
240
+ };
241
+ return {
242
+ async transitionTo(viewOrScene, options) {
243
+ return (await loadCapability()).transitionTo(viewOrScene, options);
244
+ },
245
+ async applyScene(sceneSpec) {
246
+ return (await loadCapability()).applyScene(sceneSpec);
247
+ },
248
+ async load(input, options) {
249
+ return (await loadCapability()).load(input, options);
250
+ },
251
+ async getSnapshot() {
252
+ return (await loadCapability()).getSnapshot();
253
+ },
254
+ };
255
+ }
256
+
257
+ /**
258
+ * @param {import('./browser.d.ts').SkykitBrowser} browser
259
+ * @param {Element | import('./browser.d.ts').SkykitBrowserHost} host
260
+ * @returns {import('./browser.d.ts').SkykitBrowserConstellationsFacade}
261
+ */
262
+ function createLazyConstellationsFacade(browser, host) {
263
+ /** @type {Promise<import('./browser.d.ts').SkykitBrowserConstellationsFacade> | null} */
264
+ let loaded = null;
265
+ const loadCapability = (options = {}) => {
266
+ loaded ??= import('./browser-constellations.js')
267
+ .then((module) => module.installSkykitConstellationsBrowserCapability({
268
+ browser,
269
+ host,
270
+ options,
271
+ }));
272
+ return loaded;
273
+ };
274
+ return {
275
+ async load(options) {
276
+ return loadCapability(options);
277
+ },
278
+ async show() {
279
+ return (await loadCapability()).show();
280
+ },
281
+ async hide() {
282
+ return (await loadCapability()).hide();
283
+ },
284
+ async toggle(force) {
285
+ return (await loadCapability()).toggle(force);
286
+ },
287
+ async setArt(mode) {
288
+ return (await loadCapability()).setArt(mode);
289
+ },
290
+ async getSnapshot() {
291
+ return (await loadCapability()).getSnapshot();
292
+ },
293
+ };
294
+ }
295
+
296
+ function createPointerPlugins(options, host) {
297
+ const mouseMode = normalizeMouseMode(options.mouseMode);
298
+ if (options.grab === false || mouseMode === 'none') return [];
299
+ const pointerOptions = {
300
+ target: host,
301
+ sensitivityRadiansPerPixel: 0.00075,
302
+ ...(options.grab ?? {}),
303
+ };
304
+ return [
305
+ mouseMode === 'look' || mouseMode === 'strafe'
306
+ ? createMouseLookPlugin(pointerOptions)
307
+ : createSkyGrabPlugin(pointerOptions),
308
+ ];
309
+ }
310
+
311
+ function normalizeMouseMode(value) {
312
+ const mode = String(value ?? 'grab').trim().toLowerCase();
313
+ if (mode === 'look' || mode === 'mouse-look' || mode === 'mouselook' || mode === 'game' || mode === 'strafe') {
314
+ return mode === 'strafe' ? 'strafe' : 'look';
315
+ }
316
+ if (mode === 'none' || mode === 'off' || mode === 'false') return 'none';
317
+ return 'grab';
318
+ }
319
+
320
+ function createStatusPlugin(target) {
321
+ return createSkykitStatusPlugin({
322
+ intervalSeconds: 0.5,
323
+ render({ viewer }) {
324
+ const stars = viewer.parts.find((part) => part.id === 'stars')?.snapshot;
325
+ target.textContent = JSON.stringify({
326
+ observerPc: viewer.view.observerPc,
327
+ starsLoaded: stars?.renderer?.starCount ?? 0,
328
+ stream: stars?.status ?? 'starting',
329
+ }, null, 2);
330
+ },
331
+ });
332
+ }
333
+
334
+ function normalizeOptions(input) {
335
+ if (typeof input === 'string' || isElementLike(input)) return { host: input };
336
+ return input ?? {};
337
+ }
338
+
339
+ function resolveTarget(input, label) {
340
+ if (typeof input !== 'string') {
341
+ if (input) return input;
342
+ throw new Error(`${label} is missing.`);
343
+ }
344
+ const target = document.querySelector(input);
345
+ if (!target) throw new Error(`${label} not found: ${input}`);
346
+ return target;
347
+ }
348
+
349
+ function isElementLike(value) {
350
+ return Boolean(value && typeof value === 'object' && 'appendChild' in value);
351
+ }
352
+
353
+ function positive(value, fallback) {
354
+ const number = Number(value);
355
+ return Number.isFinite(number) && number > 0 ? number : fallback;
356
+ }
357
+
358
+ /**
359
+ * @param {THREE.Object3D} object3d
360
+ * @param {{ x: number; y: number; z: number }} positionPc
361
+ * @param {number} unitsPerParsec
362
+ */
363
+ function setObjectPositionPc(object3d, positionPc, unitsPerParsec) {
364
+ object3d.position?.set?.(
365
+ positionPc.x * unitsPerParsec,
366
+ positionPc.y * unitsPerParsec,
367
+ positionPc.z * unitsPerParsec,
368
+ );
369
+ }
package/src/data.d.ts ADDED
@@ -0,0 +1,133 @@
1
+ import type { MetaSidecarEntry, MetaSidecarProviderService } from '@found-in-space/meta-sidecar-provider';
2
+ import type {
3
+ StarOctreeCellStreamOptions,
4
+ StarOctreeProviderService,
5
+ StarOctreeProviderServiceOptions,
6
+ } from '@found-in-space/star-octree-provider';
7
+ import type {
8
+ StarObjectRef,
9
+ StarTreePointPc,
10
+ } from '@found-in-space/star-trees';
11
+
12
+ export {
13
+ OCTREE_DEFAULT,
14
+ createStarOctreeProviderService,
15
+ } from '@found-in-space/star-octree-provider';
16
+ export {
17
+ createObserverShellStrategy,
18
+ createSphereVolumeStrategy,
19
+ createStarCellKey,
20
+ decodeTemperatureK,
21
+ temperatureToRgb,
22
+ } from '@found-in-space/star-trees';
23
+ export {
24
+ createMetaSidecarProviderService,
25
+ deriveMetaSidecarUrlFromRenderUrl,
26
+ metaSidecarEntryDisplayFields,
27
+ } from '@found-in-space/meta-sidecar-provider';
28
+
29
+ export interface SkykitStarRow {
30
+ ref: StarObjectRef | null;
31
+ cellKey: string;
32
+ level: number;
33
+ mortonCode: string;
34
+ ordinal: number;
35
+ positionPc: StarTreePointPc;
36
+ xPc: number;
37
+ yPc: number;
38
+ zPc: number;
39
+ distancePc: number;
40
+ magAbs: number | null;
41
+ absoluteMagnitude: number | null;
42
+ apparentMagnitude: number | null;
43
+ teffLog8: number | null;
44
+ temperatureK: number | null;
45
+ }
46
+
47
+ export interface SkykitRowsFromCellsOptions {
48
+ observerPc?: StarTreePointPc;
49
+ limitingMagnitude?: number;
50
+ filterVisible?: boolean;
51
+ }
52
+
53
+ export interface SkykitStarDataOptions extends Omit<StarOctreeCellStreamOptions, 'view' | 'strategy'> {
54
+ provider?: StarOctreeProviderService;
55
+ providerId?: string;
56
+ octreeUrl?: string;
57
+ persistentCache?: StarOctreeProviderServiceOptions['persistentCache'];
58
+ limits?: StarOctreeProviderServiceOptions['limits'];
59
+ observerPc?: StarTreePointPc;
60
+ centerPc?: StarTreePointPc;
61
+ radiusPc?: number;
62
+ limitingMagnitude?: number;
63
+ maxStars?: number;
64
+ filterVisible?: boolean;
65
+ sortBy?:
66
+ | 'apparentMagnitude'
67
+ | 'distancePc'
68
+ | 'magAbs'
69
+ | 'absoluteMagnitude'
70
+ | null
71
+ | false
72
+ | ((left: SkykitStarRow, right: SkykitStarRow) => number);
73
+ view?: StarOctreeCellStreamOptions['view'];
74
+ strategy?: StarOctreeCellStreamOptions['strategy'];
75
+ }
76
+
77
+ export type SkykitStarLabelInput = SkykitStarRow | StarObjectRef;
78
+
79
+ export interface SkykitStarLabelOptions {
80
+ metaProvider?: MetaSidecarProviderService;
81
+ metaProviderId?: string;
82
+ metaUrl?: string;
83
+ parentDatasetId?: string;
84
+ datasetId?: string;
85
+ persistentCache?: 'on' | 'off';
86
+ metaLimits?: {
87
+ shardPrefetchBytes?: number;
88
+ };
89
+ provider?: StarOctreeProviderService;
90
+ octreeUrl?: string;
91
+ }
92
+
93
+ export interface SkykitStarLabel {
94
+ star: SkykitStarLabelInput;
95
+ ref: StarObjectRef | null;
96
+ entry: MetaSidecarEntry | null;
97
+ fields: {
98
+ properName: string;
99
+ bayer: string;
100
+ hd: string;
101
+ hip: string;
102
+ gaia: string;
103
+ primaryLabel: string;
104
+ };
105
+ label: string;
106
+ }
107
+
108
+ export declare function createStarStream(
109
+ options?: SkykitStarDataOptions
110
+ ): AsyncIterable<SkykitStarRow[]>;
111
+
112
+ export declare function loadStarRows(
113
+ options?: SkykitStarDataOptions
114
+ ): Promise<SkykitStarRow[]>;
115
+
116
+ export declare function streamStarRows(
117
+ options?: SkykitStarDataOptions
118
+ ): AsyncIterable<SkykitStarRow[]>;
119
+
120
+ export declare function rowsFromStarCells(
121
+ cells: Iterable<import('@found-in-space/star-trees').StarCellData>,
122
+ options?: SkykitRowsFromCellsOptions
123
+ ): SkykitStarRow[];
124
+
125
+ export declare function loadStarLabels(
126
+ input: Iterable<SkykitStarLabelInput> | SkykitStarDataOptions,
127
+ options?: SkykitStarLabelOptions
128
+ ): Promise<SkykitStarLabel[]>;
129
+
130
+ export declare function formatStarLabel(
131
+ entry?: MetaSidecarEntry | null,
132
+ fallback?: string
133
+ ): string;