@found-in-space/skykit 0.2.0-dev.20260527.1 → 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.
package/src/utils.js CHANGED
@@ -211,6 +211,7 @@ function resolveViewLook(input, observerPc, options = {}) {
211
211
  * @returns {import('./index.d.ts').SkykitLookAtInput | null}
212
212
  */
213
213
  function cloneLookAt(lookAt) {
214
+ if (typeof lookAt === 'string') return lookAt;
214
215
  if (!lookAt || typeof lookAt !== 'object') return null;
215
216
  const source = /** @type {Record<string, unknown>} */ (lookAt);
216
217
  return {
package/src/xr/plugins.js CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  positiveFinite,
12
12
  } from '../utils.js';
13
13
  import { createSkykitXrControlBindings } from './controls.js';
14
+ import { createSkykitXrBodyTracker } from './body.js';
14
15
  import { createSkykitXrRaySource } from './rays.js';
15
16
  import { enterSkykitXrSession, exitSkykitXrSession, isSkykitXrModeSupported } from './session.js';
16
17
 
@@ -121,6 +122,69 @@ export function createSkykitXrObserverRig(options) {
121
122
  }
122
123
  }
123
124
 
125
+ /**
126
+ * @param {import('../xr.d.ts').SkykitXrBodyPluginOptions} [options]
127
+ * @returns {import('../xr.d.ts').SkykitXrBodyPlugin}
128
+ */
129
+ export function createSkykitXrBodyPlugin(options = {}) {
130
+ const id = options.id ?? 'skykit-xr-body';
131
+ const tracker = options.tracker ?? createSkykitXrBodyTracker();
132
+ const hideUntrackedHands = options.hideUntrackedHands !== false;
133
+ let disposed = false;
134
+ let body = tracker.getBody();
135
+
136
+ const part = {
137
+ id,
138
+ priority: options.priority ?? -900,
139
+ /** @param {import('../index.d.ts').SkykitThreeFrame} frame */
140
+ update(frame) {
141
+ if (disposed) return;
142
+ const session = frame.xr?.session && typeof frame.xr.session === 'object'
143
+ ? /** @type {{ inputSources?: Iterable<unknown> }} */ (frame.xr.session)
144
+ : null;
145
+ body = tracker.update({
146
+ frame: frame.xr?.frame,
147
+ referenceSpace: frame.xr?.referenceSpace,
148
+ session: /** @type {any} */ (frame.xr?.session),
149
+ inputSources: session?.inputSources ?? [],
150
+ rig: options.rig,
151
+ shipPose: options.rig?.getNavigationPose?.(),
152
+ });
153
+ if (hideUntrackedHands && options.rig) {
154
+ setObjectVisible(options.rig.leftHandRoot, Boolean(body.leftHand?.grip ?? body.leftHand?.targetRay));
155
+ setObjectVisible(options.rig.rightHandRoot, Boolean(body.rightHand?.grip ?? body.rightHand?.targetRay));
156
+ }
157
+ options.onBody?.(body, frame);
158
+ },
159
+ dispose() {
160
+ disposed = true;
161
+ if (options.disposeTracker !== false) {
162
+ tracker.dispose?.();
163
+ }
164
+ },
165
+ getSnapshot,
166
+ };
167
+
168
+ return {
169
+ id,
170
+ setup(context) {
171
+ context.addPart(part);
172
+ },
173
+ getBody() {
174
+ return body;
175
+ },
176
+ getSnapshot,
177
+ };
178
+
179
+ function getSnapshot() {
180
+ return {
181
+ id,
182
+ disposed,
183
+ body,
184
+ };
185
+ }
186
+ }
187
+
124
188
  /**
125
189
  * @param {import('../xr.d.ts').SkykitXrSessionPluginOptions} options
126
190
  * @returns {import('../index.d.ts').SkykitPlugin & { enter(): Promise<import('../xr.d.ts').SkykitXrSessionHandle>; exit(): Promise<void>; getSnapshot(): unknown }}
@@ -798,3 +862,13 @@ function resolveRayVisualParent(parent, context) {
798
862
  }
799
863
  return parent ?? context.scene;
800
864
  }
865
+
866
+ /**
867
+ * @param {unknown} object
868
+ * @param {boolean} visible
869
+ */
870
+ function setObjectVisible(object, visible) {
871
+ if (object && typeof object === 'object' && 'visible' in object) {
872
+ /** @type {{ visible: boolean }} */ (object).visible = visible;
873
+ }
874
+ }
package/src/xr.d.ts CHANGED
@@ -14,6 +14,7 @@ import type {
14
14
  SkykitEvent,
15
15
  SkykitPlugin,
16
16
  SkykitStarCellSource,
17
+ SkykitThreeFrame,
17
18
  SkykitViewState,
18
19
  } from './index.js';
19
20
 
@@ -105,6 +106,22 @@ export interface SkykitXrBodyTracker {
105
106
  dispose(): void;
106
107
  }
107
108
 
109
+ export interface SkykitXrBodyPluginOptions {
110
+ id?: string;
111
+ priority?: number;
112
+ rig?: SkykitXrRig;
113
+ tracker?: SkykitXrBodyTracker;
114
+ hideUntrackedHands?: boolean;
115
+ disposeTracker?: boolean;
116
+ onBody?: (body: SkykitXrBodyModel, frame: SkykitThreeFrame) => void;
117
+ }
118
+
119
+ export interface SkykitXrBodyPlugin extends SkykitPlugin {
120
+ readonly id: string;
121
+ getBody(): SkykitXrBodyModel;
122
+ getSnapshot(): unknown;
123
+ }
124
+
108
125
  export interface SkykitXrAxisBinding {
109
126
  hand?: 'left' | 'right' | 'any';
110
127
  stick?: 'primary' | 'secondary';
@@ -445,6 +462,7 @@ export interface SkykitXrStarPickingPluginOptions {
445
462
 
446
463
  export declare function createSkykitXrRig(options?: CreateSkykitXrRigOptions): SkykitXrRig;
447
464
  export declare function createSkykitXrBodyTracker(options?: CreateSkykitXrBodyTrackerOptions): SkykitXrBodyTracker;
465
+ export declare function createSkykitXrBodyPlugin(options?: SkykitXrBodyPluginOptions): SkykitXrBodyPlugin;
448
466
  export declare function createSkykitXrControlBindings(options?: SkykitXrControlBindingsOptions): SkykitXrControlBindingsHandle;
449
467
  export declare function readSkykitXrAxis(inputSources: Iterable<any>, binding?: SkykitXrAxisBinding & { deadzone?: number }): SkykitXrAxisState;
450
468
  export declare function readSkykitXrButton(inputSources: Iterable<any>, binding?: SkykitXrButtonBinding, previous?: SkykitXrButtonState | null): SkykitXrButtonState;
package/src/xr.js CHANGED
@@ -14,6 +14,7 @@ export {
14
14
  export { createSkykitXrRaySource } from './xr/rays.js';
15
15
  export { createSkykitXrPickRouter } from './xr/pick-router.js';
16
16
  export {
17
+ createSkykitXrBodyPlugin,
17
18
  createSkykitXrNavigationPlugin,
18
19
  createSkykitXrObserverRig,
19
20
  createSkykitXrRayVisualPlugin,
@@ -1,8 +0,0 @@
1
- import type {
2
- SkykitBrowser,
3
- SkykitBrowserJourneyFacade,
4
- } from './browser.js';
5
-
6
- export declare function installSkykitJourneyBrowserCapability(
7
- context: { browser: SkykitBrowser }
8
- ): Promise<SkykitBrowserJourneyFacade>;
@@ -1,240 +0,0 @@
1
- import { createJourney } from '@found-in-space/journey';
2
- import { parseSpatialLookAtText } from '@found-in-space/spatial';
3
-
4
- import { SKYKIT_ACTIONS } from './actions.js';
5
- import {
6
- createSkykitJourneyPlugin,
7
- createSkykitNavigationPlugin,
8
- } from './plugins.js';
9
-
10
- const NAVIGATION_CAPABILITY = 'skykit:navigation';
11
- const JOURNEY_CAPABILITY = 'skykit:browser.journey';
12
- const SOURCE = 'browser.journey';
13
-
14
- /**
15
- * @param {{ browser: import('./browser.d.ts').SkykitBrowser }} context
16
- * @returns {Promise<import('./browser.d.ts').SkykitBrowserJourneyFacade>}
17
- */
18
- export async function installSkykitJourneyBrowserCapability({ browser }) {
19
- browser.capabilities.add(JOURNEY_CAPABILITY);
20
-
21
- return {
22
- transitionTo(viewOrScene, options) {
23
- return transitionTo(browser, viewOrScene, options);
24
- },
25
- applyScene(sceneSpec) {
26
- return applyScene(browser, sceneSpec);
27
- },
28
- load(input, options) {
29
- return loadJourney(browser, input, options);
30
- },
31
- getSnapshot() {
32
- return {
33
- capability: JOURNEY_CAPABILITY,
34
- navigationInstalled: browser.capabilities.has(NAVIGATION_CAPABILITY),
35
- };
36
- },
37
- };
38
- }
39
-
40
- /**
41
- * @param {import('./browser.d.ts').SkykitBrowser} browser
42
- */
43
- async function ensureNavigation(browser) {
44
- if (browser.capabilities.has(NAVIGATION_CAPABILITY)) return;
45
- await browser.install(createSkykitNavigationPlugin());
46
- browser.capabilities.add(NAVIGATION_CAPABILITY);
47
- }
48
-
49
- /**
50
- * @param {import('./browser.d.ts').SkykitBrowser} browser
51
- * @param {unknown} viewOrScene
52
- * @param {Record<string, unknown>} [options]
53
- */
54
- async function transitionTo(browser, viewOrScene, options = {}) {
55
- await ensureNavigation(browser);
56
- const payload = normalizeTransitionPayload(viewOrScene, options);
57
- return browser.viewer.actions.invoke(SKYKIT_ACTIONS.navigation.transitionTo, payload, { source: SOURCE });
58
- }
59
-
60
- /**
61
- * @param {import('./browser.d.ts').SkykitBrowser} browser
62
- * @param {unknown} sceneSpec
63
- */
64
- async function applyScene(browser, sceneSpec) {
65
- const scene = /** @type {Record<string, unknown> | null} */ (
66
- sceneSpec && typeof sceneSpec === 'object' ? sceneSpec : null
67
- );
68
- if (!scene) return null;
69
- if (scene.view && typeof scene.view === 'object') {
70
- browser.viewer.requestViewState(normalizeView(/** @type {Record<string, unknown>} */ (scene.view)), SOURCE);
71
- }
72
- const navigation = /** @type {Record<string, unknown> | null} */ (
73
- scene.navigation && typeof scene.navigation === 'object' ? scene.navigation : null
74
- );
75
- if (navigation?.transitionTo) {
76
- return transitionTo(browser, navigation.transitionTo);
77
- }
78
- return scene;
79
- }
80
-
81
- /**
82
- * @param {import('./browser.d.ts').SkykitBrowser} browser
83
- * @param {unknown} input
84
- * @param {Record<string, unknown>} [options]
85
- * @returns {Promise<import('./browser.d.ts').SkykitBrowserJourneyInstance>}
86
- */
87
- async function loadJourney(browser, input, options = {}) {
88
- await ensureNavigation(browser);
89
- const definition = await loadJourneyDefinition(input);
90
- const plugin = isTimedJourney(definition)
91
- ? createSkykitJourneyPlugin({
92
- ...options,
93
- timedJourney: definition,
94
- })
95
- : createSkykitJourneyPlugin({
96
- ...options,
97
- journey: createJourney(/** @type {import('@found-in-space/journey').CreateJourneyOptions} */ (definition)),
98
- });
99
- const uninstall = await browser.install(plugin);
100
- let disposed = false;
101
-
102
- return {
103
- goTo(sceneId) {
104
- assertActive();
105
- return plugin.goTo?.(sceneId) ?? Promise.resolve(null);
106
- },
107
- next() {
108
- assertActive();
109
- return plugin.next?.() ?? Promise.resolve(null);
110
- },
111
- previous() {
112
- assertActive();
113
- return plugin.previous?.() ?? Promise.resolve(null);
114
- },
115
- play(payload) {
116
- assertActive();
117
- return plugin.play?.(payload) ?? 0;
118
- },
119
- pause() {
120
- assertActive();
121
- return plugin.pause?.() ?? 0;
122
- },
123
- seek(timeSecs) {
124
- assertActive();
125
- return plugin.seek?.({ timeSecs }) ?? 0;
126
- },
127
- getSnapshot() {
128
- return {
129
- disposed,
130
- plugin: plugin.getSnapshot?.() ?? null,
131
- };
132
- },
133
- dispose() {
134
- if (disposed) return;
135
- disposed = true;
136
- uninstall();
137
- },
138
- };
139
-
140
- function assertActive() {
141
- if (disposed) throw new Error('SkyKit journey instance has been disposed.');
142
- }
143
- }
144
-
145
- /**
146
- * @param {unknown} input
147
- * @returns {Promise<Record<string, unknown>>}
148
- */
149
- async function loadJourneyDefinition(input) {
150
- if (typeof input === 'string') {
151
- const response = await fetch(input);
152
- if (!response.ok) throw new Error(`Failed to load SkyKit journey: ${response.status} ${response.statusText}`);
153
- return /** @type {Record<string, unknown>} */ (await response.json());
154
- }
155
- if (!input || typeof input !== 'object') {
156
- throw new TypeError('browser.journey.load() requires a URL or journey definition.');
157
- }
158
- return /** @type {Record<string, unknown>} */ (input);
159
- }
160
-
161
- /** @param {Record<string, unknown>} definition */
162
- function isTimedJourney(definition) {
163
- return Number.isFinite(Number(definition.durationSecs))
164
- && (Array.isArray(definition.locationWaypoints)
165
- || Array.isArray(definition.cameraLookWaypoints)
166
- || Array.isArray(definition.cameraWaypoints));
167
- }
168
-
169
- /**
170
- * @param {unknown} input
171
- * @param {Record<string, unknown>} [options]
172
- */
173
- function normalizeTransitionPayload(input, options = {}) {
174
- const source = /** @type {Record<string, unknown>} */ (
175
- input && typeof input === 'object' ? input : { lookAt: input }
176
- );
177
- if (source.navigation && typeof source.navigation === 'object') {
178
- const navigation = /** @type {Record<string, unknown>} */ (source.navigation);
179
- if (navigation.transitionTo) return normalizeTransitionPayload(navigation.transitionTo, options);
180
- }
181
- const viewSource = source.view && typeof source.view === 'object'
182
- ? /** @type {Record<string, unknown>} */ (source.view)
183
- : source.to && typeof source.to === 'object'
184
- ? /** @type {Record<string, unknown>} */ (source.to)
185
- : stripTransitionOptions(source);
186
- return {
187
- ...stripUndefined({
188
- ...source,
189
- ...options,
190
- view: normalizeView(viewSource),
191
- durationSecs: options.durationSecs ?? source.durationSecs,
192
- }),
193
- };
194
- }
195
-
196
- /** @param {Record<string, unknown>} input */
197
- function normalizeView(input) {
198
- const lookAt = input.lookAt ?? (
199
- hasLookAtShape(input) ? input : undefined
200
- );
201
- return {
202
- ...input,
203
- ...(lookAt !== undefined ? { lookAt: normalizeLookAt(lookAt) } : {}),
204
- };
205
- }
206
-
207
- /** @param {unknown} input */
208
- function normalizeLookAt(input) {
209
- if (typeof input !== 'string') return input;
210
- return parseSpatialLookAtText(input) ?? { star: input };
211
- }
212
-
213
- /** @param {Record<string, unknown>} input */
214
- function stripTransitionOptions(input) {
215
- const {
216
- durationSecs: _durationSecs,
217
- movement: _movement,
218
- orientation: _orientation,
219
- orientationDurationSecs: _orientationDurationSecs,
220
- movementDurationSecs: _movementDurationSecs,
221
- onArrive: _onArrive,
222
- ...view
223
- } = input;
224
- return view;
225
- }
226
-
227
- /** @param {Record<string, unknown>} input */
228
- function hasLookAtShape(input) {
229
- return 'raDeg' in input
230
- || 'raHours' in input
231
- || 'decDeg' in input
232
- || 'targetPc' in input
233
- || 'orientationIcrs' in input
234
- || 'star' in input;
235
- }
236
-
237
- /** @param {Record<string, unknown>} input */
238
- function stripUndefined(input) {
239
- return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
240
- }
package/src/story.d.ts DELETED
@@ -1,57 +0,0 @@
1
- import type { SkykitBrowser, SkykitBrowserHost, SkykitBrowserOptions } from './browser.js';
2
- import type { SkykitViewState, SkykitViewer, Vector3Like } from './index.js';
3
-
4
- export type SkykitStoryHost = SkykitBrowserHost;
5
-
6
- export interface SkykitStoryChapterInput {
7
- id?: string;
8
- title?: string;
9
- body?: string;
10
- bodyHtml?: string | null;
11
- targetPc?: Vector3Like | null;
12
- observerPc?: Vector3Like | null;
13
- view?: Partial<SkykitViewState> | null;
14
- annotations?: unknown[];
15
- }
16
-
17
- export interface SkykitStoryChapter {
18
- id: string;
19
- title: string;
20
- body: string;
21
- bodyHtml: string | null;
22
- targetPc: Vector3Like | null;
23
- observerPc: Vector3Like | null;
24
- view: Partial<SkykitViewState> | null;
25
- annotations: unknown[];
26
- }
27
-
28
- export interface SkykitStoryOptions extends SkykitBrowserOptions {
29
- host?: SkykitStoryHost;
30
- src?: string;
31
- chapters?: Iterable<SkykitStoryChapterInput>;
32
- initialChapter?: string | number;
33
- controls?: boolean;
34
- previousLabel?: string;
35
- nextLabel?: string;
36
- }
37
-
38
- export interface SkykitStory {
39
- browser: SkykitBrowser;
40
- viewer: SkykitViewer;
41
- chapters: SkykitStoryChapter[];
42
- readonly currentIndex: number;
43
- readonly currentChapter: SkykitStoryChapter | null;
44
- next(): SkykitStoryChapter | null;
45
- previous(): SkykitStoryChapter | null;
46
- goTo(indexOrId: number | string): SkykitStoryChapter | null;
47
- dispose(): Promise<void>;
48
- }
49
-
50
- export declare function createSkykitStory(
51
- host: SkykitStoryHost,
52
- options?: SkykitStoryOptions
53
- ): Promise<SkykitStory>;
54
-
55
- export declare function createSkykitStory(
56
- options?: SkykitStoryOptions
57
- ): Promise<SkykitStory>;