@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.
- package/README.md +142 -11
- package/examples/custom-object-layer/custom-object-layer.js +1 -24
- package/examples/xr-free-roam/index.html +62 -4
- package/examples/xr-free-roam/xr-free-roam.css +249 -18
- package/examples/xr-free-roam/xr-free-roam.js +675 -274
- package/package.json +22 -6
- package/src/__tests__/skykit-anchored-images.test.js +32 -4
- package/src/__tests__/skykit-browser.test.js +267 -0
- package/src/__tests__/skykit-data.test.js +131 -0
- package/src/__tests__/skykit-parallax.test.js +4 -4
- package/src/__tests__/skykit-touch-os.test.js +71 -0
- package/src/__tests__/skykit-xr.test.js +179 -2
- package/src/__tests__/skykit.test.js +142 -506
- package/src/actions.js +0 -8
- package/src/anchored-images.js +14 -15
- package/src/browser-addons.d.ts +16 -0
- package/src/browser-addons.js +155 -0
- package/src/browser-constellations.d.ts +13 -0
- package/src/browser-constellations.js +387 -0
- package/src/browser.d.ts +81 -0
- package/src/browser.js +192 -13
- package/src/data.d.ts +133 -0
- package/src/data.js +447 -0
- package/src/embed.d.ts +5 -0
- package/src/embed.js +53 -2
- package/src/hr-diagram.js +23 -5
- package/src/index.d.ts +21 -73
- package/src/index.js +0 -1
- package/src/plugins.js +22 -708
- package/src/three-shim.d.ts +32 -0
- package/src/touch-os.d.ts +70 -0
- package/src/touch-os.js +275 -0
- package/src/utils.js +96 -6
- package/src/viewer-entry.d.ts +10 -0
- package/src/viewer-entry.js +4 -0
- package/src/viewer.js +110 -12
- package/src/xr/plugins.js +298 -13
- package/src/xr/session.js +60 -14
- package/src/xr.d.ts +40 -0
- package/src/xr.js +2 -0
package/src/viewer.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
mountRenderer,
|
|
16
16
|
normalizeViewState,
|
|
17
17
|
positiveFinite,
|
|
18
|
+
resolveViewLookAtInput,
|
|
18
19
|
setupPlugin,
|
|
19
20
|
snapshotPart,
|
|
20
21
|
syncRootsFromView,
|
|
@@ -71,7 +72,7 @@ export async function createSkykitViewer(options = {}) {
|
|
|
71
72
|
let started = false;
|
|
72
73
|
let elapsedSeconds = 0;
|
|
73
74
|
const initialProjectionView = resolveCameraProjectionView(camera);
|
|
74
|
-
|
|
75
|
+
const initialViewInput = await resolveViewLookAtInput({
|
|
75
76
|
...options.view,
|
|
76
77
|
...(options.view?.verticalFovDeg === undefined && initialProjectionView.verticalFovDeg !== undefined
|
|
77
78
|
? { verticalFovDeg: initialProjectionView.verticalFovDeg }
|
|
@@ -83,8 +84,11 @@ export async function createSkykitViewer(options = {}) {
|
|
|
83
84
|
renderObserverPosition: options.view?.renderObserverPosition ?? observerRig.getRenderObserverPosition(),
|
|
84
85
|
orientationIcrs: options.view?.orientationIcrs ?? observerRig.getOrientationIcrs?.() ?? null,
|
|
85
86
|
motion: options.view?.motion ?? observerRig.getMotion?.() ?? null,
|
|
86
|
-
},
|
|
87
|
+
}, viewLookResolverOptions(options.view?.observerPc ?? observerRig.getObserverPc()));
|
|
88
|
+
let view = normalizeViewState(initialViewInput, 0, viewLookResolverOptions(initialViewInput.observerPc));
|
|
87
89
|
const initialView = cloneViewState(view);
|
|
90
|
+
observerRig.setObserverPc?.(view.observerPc);
|
|
91
|
+
if (view.orientationIcrs) observerRig.setOrientationIcrs?.(view.orientationIcrs);
|
|
88
92
|
|
|
89
93
|
addRootToScene(scene, roots.originContentRoot);
|
|
90
94
|
addRootToScene(scene, roots.observerContentRoot);
|
|
@@ -112,6 +116,7 @@ export async function createSkykitViewer(options = {}) {
|
|
|
112
116
|
observerRig,
|
|
113
117
|
actions,
|
|
114
118
|
addPart,
|
|
119
|
+
addPlugin,
|
|
115
120
|
getViewState,
|
|
116
121
|
requestViewState,
|
|
117
122
|
update,
|
|
@@ -139,10 +144,7 @@ export async function createSkykitViewer(options = {}) {
|
|
|
139
144
|
}
|
|
140
145
|
|
|
141
146
|
for (const plugin of pluginInputs) {
|
|
142
|
-
|
|
143
|
-
if (typeof teardown === 'function') {
|
|
144
|
-
disposables.push(teardown);
|
|
145
|
-
}
|
|
147
|
+
await installPlugin(plugin);
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
for (const part of orderedParts()) {
|
|
@@ -176,6 +178,32 @@ export async function createSkykitViewer(options = {}) {
|
|
|
176
178
|
};
|
|
177
179
|
}
|
|
178
180
|
|
|
181
|
+
/**
|
|
182
|
+
* @param {import('./index.d.ts').SkykitPluginInput} plugin
|
|
183
|
+
* @returns {Promise<SkykitPluginTeardown>}
|
|
184
|
+
*/
|
|
185
|
+
async function addPlugin(plugin) {
|
|
186
|
+
assertActive();
|
|
187
|
+
return installPlugin(plugin, true);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @param {import('./index.d.ts').SkykitPluginInput} plugin
|
|
192
|
+
* @param {boolean} [record]
|
|
193
|
+
* @returns {Promise<SkykitPluginTeardown>}
|
|
194
|
+
*/
|
|
195
|
+
async function installPlugin(plugin, record = false) {
|
|
196
|
+
const teardown = await setupPlugin(plugin, context);
|
|
197
|
+
if (record) pluginInputs.push(plugin);
|
|
198
|
+
if (typeof teardown !== 'function') return () => {};
|
|
199
|
+
disposables.push(teardown);
|
|
200
|
+
return () => {
|
|
201
|
+
const index = disposables.indexOf(teardown);
|
|
202
|
+
if (index >= 0) disposables.splice(index, 1);
|
|
203
|
+
void teardown();
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
179
207
|
/**
|
|
180
208
|
* @param {SkykitThreePart} part
|
|
181
209
|
*/
|
|
@@ -192,9 +220,8 @@ export async function createSkykitViewer(options = {}) {
|
|
|
192
220
|
*/
|
|
193
221
|
async function removePart(part) {
|
|
194
222
|
const index = parts.indexOf(part);
|
|
195
|
-
if (index
|
|
196
|
-
|
|
197
|
-
}
|
|
223
|
+
if (index < 0) return;
|
|
224
|
+
parts.splice(index, 1);
|
|
198
225
|
emit({ type: 'part/remove', part });
|
|
199
226
|
await detachAndDisposePart(part);
|
|
200
227
|
}
|
|
@@ -209,11 +236,32 @@ export async function createSkykitViewer(options = {}) {
|
|
|
209
236
|
*/
|
|
210
237
|
function requestViewState(patch, reason) {
|
|
211
238
|
assertActive();
|
|
239
|
+
const normalizedPatch = normalizeRequestedViewPatch(patch);
|
|
240
|
+
if (lookAtNeedsAsyncResolution(normalizedPatch)) {
|
|
241
|
+
const observerPc = /** @type {Partial<SkykitViewState>} */ (normalizedPatch).observerPc ?? view.observerPc;
|
|
242
|
+
void resolveViewLookAtInput(normalizedPatch, viewLookResolverOptions(observerPc))
|
|
243
|
+
.then((resolvedPatch) => {
|
|
244
|
+
if (disposed) return;
|
|
245
|
+
pendingViewPatch = {
|
|
246
|
+
...(pendingViewPatch ?? {}),
|
|
247
|
+
...resolvedPatch,
|
|
248
|
+
};
|
|
249
|
+
})
|
|
250
|
+
.catch((error) => {
|
|
251
|
+
emit({
|
|
252
|
+
type: 'view/lookAt-error',
|
|
253
|
+
reason,
|
|
254
|
+
error: error instanceof Error ? error.message : String(error),
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
emit({ type: 'view/request', reason, patch: normalizedPatch });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
212
260
|
pendingViewPatch = {
|
|
213
261
|
...(pendingViewPatch ?? {}),
|
|
214
|
-
...
|
|
262
|
+
...normalizedPatch,
|
|
215
263
|
};
|
|
216
|
-
emit({ type: 'view/request', reason, patch });
|
|
264
|
+
emit({ type: 'view/request', reason, patch: normalizedPatch });
|
|
217
265
|
}
|
|
218
266
|
|
|
219
267
|
/**
|
|
@@ -356,7 +404,7 @@ export async function createSkykitViewer(options = {}) {
|
|
|
356
404
|
}
|
|
357
405
|
}
|
|
358
406
|
for (const part of [...orderedParts()].reverse()) {
|
|
359
|
-
await
|
|
407
|
+
await removePart(part);
|
|
360
408
|
}
|
|
361
409
|
for (const disposable of [...disposables].reverse()) {
|
|
362
410
|
await disposable();
|
|
@@ -454,6 +502,56 @@ export async function createSkykitViewer(options = {}) {
|
|
|
454
502
|
return [...parts].sort(compareParts);
|
|
455
503
|
}
|
|
456
504
|
|
|
505
|
+
/** @param {Partial<SkykitViewState>} patch */
|
|
506
|
+
function normalizeRequestedViewPatch(patch) {
|
|
507
|
+
if (!patch || typeof patch !== 'object') return patch;
|
|
508
|
+
if ('lookAt' in patch) return patch;
|
|
509
|
+
if (patch.orientationIcrs) {
|
|
510
|
+
return {
|
|
511
|
+
...patch,
|
|
512
|
+
lookAt: { orientationIcrs: patch.orientationIcrs },
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
if (patch.targetPc) {
|
|
516
|
+
return {
|
|
517
|
+
...patch,
|
|
518
|
+
lookAt: { targetPc: patch.targetPc },
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
return patch;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** @param {Partial<SkykitViewState>} patch */
|
|
525
|
+
function lookAtNeedsAsyncResolution(patch) {
|
|
526
|
+
if (!patch || typeof patch !== 'object' || !('lookAt' in patch)) return false;
|
|
527
|
+
const lookAt = /** @type {Record<string, unknown> | null} */ (patch.lookAt);
|
|
528
|
+
return !!lookAt
|
|
529
|
+
&& typeof lookAt === 'object'
|
|
530
|
+
&& 'star' in lookAt
|
|
531
|
+
&& !('targetPc' in lookAt)
|
|
532
|
+
&& typeof options.resolveLookAtStar === 'function';
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** @param {unknown} observerPc */
|
|
536
|
+
function viewLookResolverOptions(observerPc) {
|
|
537
|
+
return {
|
|
538
|
+
observerPc,
|
|
539
|
+
resolveStar: typeof options.resolveLookAtStar === 'function'
|
|
540
|
+
? /** @type {import('@found-in-space/spatial').ResolveSpatialLookAtOptions['resolveStar']} */ (
|
|
541
|
+
(star, lookAt) => options.resolveLookAtStar?.(
|
|
542
|
+
star,
|
|
543
|
+
/** @type {import('./index.d.ts').SkykitLookAtInput} */ (lookAt),
|
|
544
|
+
)
|
|
545
|
+
)
|
|
546
|
+
: undefined,
|
|
547
|
+
resolveBookmark: typeof options.resolveLookAtBookmark === 'function'
|
|
548
|
+
? /** @type {import('@found-in-space/spatial').ResolveSpatialLookAtOptions['resolveBookmark']} */ (
|
|
549
|
+
(bookmarkId, lookAt) => options.resolveLookAtBookmark?.(bookmarkId, lookAt)
|
|
550
|
+
)
|
|
551
|
+
: undefined,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
457
555
|
/**
|
|
458
556
|
* @param {SkykitViewer} currentViewer
|
|
459
557
|
* @returns {SkykitThreePluginContext}
|
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 }}
|
|
@@ -137,6 +201,10 @@ export function createSkykitXrSessionPlugin(options = {}) {
|
|
|
137
201
|
let disposed = false;
|
|
138
202
|
/** @type {boolean | null} */
|
|
139
203
|
let supported = null;
|
|
204
|
+
/** @type {string} */
|
|
205
|
+
let enterStage = 'idle';
|
|
206
|
+
/** @type {string | null} */
|
|
207
|
+
let lastError = null;
|
|
140
208
|
|
|
141
209
|
const part = {
|
|
142
210
|
id,
|
|
@@ -198,15 +266,40 @@ export function createSkykitXrSessionPlugin(options = {}) {
|
|
|
198
266
|
const activeRenderer = renderer ?? context?.renderer;
|
|
199
267
|
const rendererXr = resolveRendererXr(activeRenderer);
|
|
200
268
|
if (rendererXr) rendererXr.enabled = true;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
269
|
+
rendererXr?.setReferenceSpaceType?.(referenceSpaceType);
|
|
270
|
+
enterStage = 'requesting-session';
|
|
271
|
+
lastError = null;
|
|
272
|
+
/** @type {import('../xr.d.ts').SkykitXrSessionHandle | null} */
|
|
273
|
+
let nextHandle = null;
|
|
274
|
+
try {
|
|
275
|
+
nextHandle = await enterSkykitXrSession({
|
|
276
|
+
navigator: options.navigator,
|
|
277
|
+
mode,
|
|
278
|
+
referenceSpaceType,
|
|
279
|
+
sessionInit: options.sessionInit,
|
|
280
|
+
requestReferenceSpace: false,
|
|
281
|
+
});
|
|
282
|
+
enterStage = 'binding-renderer';
|
|
283
|
+
if (rendererXr && typeof rendererXr.setSession === 'function') {
|
|
284
|
+
await rendererXr.setSession(nextHandle.session);
|
|
285
|
+
}
|
|
286
|
+
handle = nextHandle;
|
|
287
|
+
enterStage = 'presenting';
|
|
288
|
+
options.onSessionStarted?.(handle);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
enterStage = 'failed';
|
|
291
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
292
|
+
if (nextHandle) {
|
|
293
|
+
await exitSkykitXrSession(nextHandle).catch(() => {});
|
|
294
|
+
}
|
|
295
|
+
const activeSession = rendererXr?.getSession?.();
|
|
296
|
+
if (activeSession && typeof /** @type {{ end?: unknown }} */ (activeSession).end === 'function') {
|
|
297
|
+
const endResult = /** @type {{ end: () => Promise<void> | void }} */ (activeSession).end();
|
|
298
|
+
if (endResult && typeof /** @type {Promise<void>} */ (endResult).catch === 'function') {
|
|
299
|
+
await /** @type {Promise<void>} */ (endResult).catch(() => {});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
throw error;
|
|
210
303
|
}
|
|
211
304
|
context?.emit?.({
|
|
212
305
|
type: 'xr/session-start',
|
|
@@ -220,14 +313,20 @@ export function createSkykitXrSessionPlugin(options = {}) {
|
|
|
220
313
|
async function exit() {
|
|
221
314
|
const previous = handle;
|
|
222
315
|
handle = null;
|
|
316
|
+
enterStage = 'exiting';
|
|
223
317
|
const activeRenderer = renderer ?? context?.renderer;
|
|
224
318
|
const rendererXr = resolveRendererXr(activeRenderer);
|
|
225
|
-
if (rendererXr && typeof rendererXr.setSession === 'function' && rendererXr.getSession?.()) {
|
|
226
|
-
await rendererXr.setSession(null);
|
|
227
|
-
}
|
|
228
319
|
if (previous) {
|
|
229
320
|
await exitSkykitXrSession(previous);
|
|
321
|
+
} else {
|
|
322
|
+
const activeSession = rendererXr?.getSession?.();
|
|
323
|
+
if (activeSession && typeof /** @type {{ end?: unknown }} */ (activeSession).end === 'function') {
|
|
324
|
+
await /** @type {{ end: () => Promise<void> | void }} */ (activeSession).end();
|
|
325
|
+
} else if (rendererXr && typeof rendererXr.setSession === 'function' && rendererXr.getSession?.()) {
|
|
326
|
+
await rendererXr.setSession(null);
|
|
327
|
+
}
|
|
230
328
|
}
|
|
329
|
+
enterStage = 'idle';
|
|
231
330
|
context?.emit?.({
|
|
232
331
|
type: 'xr/session-end',
|
|
233
332
|
id,
|
|
@@ -246,7 +345,9 @@ export function createSkykitXrSessionPlugin(options = {}) {
|
|
|
246
345
|
referenceSpaceType,
|
|
247
346
|
supported,
|
|
248
347
|
presenting: isPresenting(),
|
|
348
|
+
enterStage,
|
|
249
349
|
session: handle?.getSnapshot?.() ?? null,
|
|
350
|
+
lastError,
|
|
250
351
|
disposed,
|
|
251
352
|
};
|
|
252
353
|
}
|
|
@@ -365,6 +466,134 @@ export function createSkykitXrNavigationPlugin(options = {}) {
|
|
|
365
466
|
};
|
|
366
467
|
}
|
|
367
468
|
|
|
469
|
+
/**
|
|
470
|
+
* @param {import('../xr.d.ts').SkykitXrRayVisualPluginOptions} options
|
|
471
|
+
* @returns {import('../index.d.ts').SkykitPlugin & { getSnapshot(): unknown }}
|
|
472
|
+
*/
|
|
473
|
+
export function createSkykitXrRayVisualPlugin(options) {
|
|
474
|
+
if (!options?.raySource || typeof options.raySource.getRay !== 'function') {
|
|
475
|
+
throw new TypeError('createSkykitXrRayVisualPlugin() requires a raySource.');
|
|
476
|
+
}
|
|
477
|
+
const id = options.id ?? 'skykit-xr-ray-visual';
|
|
478
|
+
const root = new THREE.Group();
|
|
479
|
+
root.name = id;
|
|
480
|
+
root.visible = false;
|
|
481
|
+
const positions = new Float32Array(6);
|
|
482
|
+
const geometry = new THREE.BufferGeometry();
|
|
483
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
484
|
+
const material = options.material ?? new THREE.LineBasicMaterial({
|
|
485
|
+
color: options.color ?? 0x66ffe8,
|
|
486
|
+
transparent: true,
|
|
487
|
+
opacity: options.opacity ?? 0.72,
|
|
488
|
+
depthTest: options.depthTest ?? false,
|
|
489
|
+
depthWrite: false,
|
|
490
|
+
});
|
|
491
|
+
const ownsMaterial = options.material == null;
|
|
492
|
+
const line = new THREE.Line(geometry, material);
|
|
493
|
+
line.name = `${id}:line`;
|
|
494
|
+
line.frustumCulled = false;
|
|
495
|
+
line.renderOrder = finiteNumber(options.renderOrder, 10_000);
|
|
496
|
+
root.add(line);
|
|
497
|
+
|
|
498
|
+
/** @type {THREE.Object3D | null} */
|
|
499
|
+
let parent = null;
|
|
500
|
+
let disposed = false;
|
|
501
|
+
let visible = false;
|
|
502
|
+
let blocked = false;
|
|
503
|
+
let lastLength = 0;
|
|
504
|
+
|
|
505
|
+
const part = {
|
|
506
|
+
id,
|
|
507
|
+
priority: options.priority ?? 45,
|
|
508
|
+
object3d: root,
|
|
509
|
+
/** @param {import('../index.d.ts').SkykitThreePluginContext} context */
|
|
510
|
+
attach(context) {
|
|
511
|
+
parent = resolveRayVisualParent(options.parent, context);
|
|
512
|
+
parent.add(root);
|
|
513
|
+
},
|
|
514
|
+
/** @param {import('../index.d.ts').SkykitThreeFrame} frame */
|
|
515
|
+
update(frame) {
|
|
516
|
+
if (disposed || frame.xr?.presenting !== true) {
|
|
517
|
+
setVisible(false);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const session = frame.xr.session && typeof frame.xr.session === 'object'
|
|
521
|
+
? /** @type {{ inputSources?: Iterable<unknown> }} */ (frame.xr.session)
|
|
522
|
+
: null;
|
|
523
|
+
const ray = options.raySource.getRay({
|
|
524
|
+
frame: frame.xr.frame,
|
|
525
|
+
referenceSpace: frame.xr.referenceSpace,
|
|
526
|
+
session: /** @type {any} */ (frame.xr.session),
|
|
527
|
+
inputSources: session?.inputSources ?? [],
|
|
528
|
+
rig: options.rig,
|
|
529
|
+
viewer: frame.viewer,
|
|
530
|
+
});
|
|
531
|
+
if (!ray) {
|
|
532
|
+
setVisible(false);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const resolved = resolveRayVisualLength(ray, frame, options);
|
|
537
|
+
blocked = resolved.blocked;
|
|
538
|
+
if (!(resolved.length > 0)) {
|
|
539
|
+
setVisible(false);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const direction = new THREE.Vector3(ray.direction.x, ray.direction.y, ray.direction.z).normalize();
|
|
544
|
+
positions[0] = ray.origin.x;
|
|
545
|
+
positions[1] = ray.origin.y;
|
|
546
|
+
positions[2] = ray.origin.z;
|
|
547
|
+
positions[3] = ray.origin.x + direction.x * resolved.length;
|
|
548
|
+
positions[4] = ray.origin.y + direction.y * resolved.length;
|
|
549
|
+
positions[5] = ray.origin.z + direction.z * resolved.length;
|
|
550
|
+
geometry.attributes.position.needsUpdate = true;
|
|
551
|
+
geometry.computeBoundingSphere();
|
|
552
|
+
lastLength = resolved.length;
|
|
553
|
+
setVisible(true);
|
|
554
|
+
},
|
|
555
|
+
detach() {
|
|
556
|
+
parent?.remove(root);
|
|
557
|
+
parent = null;
|
|
558
|
+
},
|
|
559
|
+
dispose() {
|
|
560
|
+
disposed = true;
|
|
561
|
+
parent?.remove(root);
|
|
562
|
+
parent = null;
|
|
563
|
+
geometry.dispose();
|
|
564
|
+
if (ownsMaterial) {
|
|
565
|
+
material.dispose();
|
|
566
|
+
}
|
|
567
|
+
options.raySource.dispose?.();
|
|
568
|
+
},
|
|
569
|
+
getSnapshot() {
|
|
570
|
+
return {
|
|
571
|
+
id,
|
|
572
|
+
disposed,
|
|
573
|
+
visible,
|
|
574
|
+
blocked,
|
|
575
|
+
lastLength,
|
|
576
|
+
parentName: parent?.name ?? null,
|
|
577
|
+
raySource: options.raySource.getSnapshot?.() ?? null,
|
|
578
|
+
};
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
return {
|
|
583
|
+
id,
|
|
584
|
+
setup(context) {
|
|
585
|
+
context.addPart(part);
|
|
586
|
+
},
|
|
587
|
+
getSnapshot: () => part.getSnapshot(),
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
/** @param {boolean} nextVisible */
|
|
591
|
+
function setVisible(nextVisible) {
|
|
592
|
+
visible = nextVisible;
|
|
593
|
+
root.visible = nextVisible;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
368
597
|
/**
|
|
369
598
|
* @param {import('../xr.d.ts').SkykitXrStarPickingPluginOptions} options
|
|
370
599
|
* @returns {import('../index.d.ts').SkykitPlugin & { getSnapshot(): unknown }}
|
|
@@ -505,7 +734,7 @@ export function createSkykitXrStarPickingPlugin(options) {
|
|
|
505
734
|
*/
|
|
506
735
|
function resolveRendererXr(renderer) {
|
|
507
736
|
return renderer && typeof renderer === 'object'
|
|
508
|
-
? /** @type {{ enabled?: boolean; isPresenting?: boolean; getSession?: () => unknown; getReferenceSpace?: () => unknown; setSession?: (session: unknown) => Promise<void> | void }} */ (
|
|
737
|
+
? /** @type {{ enabled?: boolean; isPresenting?: boolean; getSession?: () => unknown; getReferenceSpace?: () => unknown; setReferenceSpaceType?: (type: string) => void; setSession?: (session: unknown) => Promise<void> | void }} */ (
|
|
509
738
|
/** @type {{ xr?: unknown }} */ (renderer).xr
|
|
510
739
|
)
|
|
511
740
|
: null;
|
|
@@ -587,3 +816,59 @@ function callBlocker(blocker, ray, context) {
|
|
|
587
816
|
}
|
|
588
817
|
return blocker.blockRay?.(ray, context) ?? blocker.pick?.(ray, context) ?? null;
|
|
589
818
|
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* @param {import('../xr.d.ts').SkykitXrRay} ray
|
|
822
|
+
* @param {import('../index.d.ts').SkykitThreeFrame} frame
|
|
823
|
+
* @param {import('../xr.d.ts').SkykitXrRayVisualPluginOptions} options
|
|
824
|
+
*/
|
|
825
|
+
function resolveRayVisualLength(ray, frame, options) {
|
|
826
|
+
const fallbackLength = positiveFinite(options.length, 12);
|
|
827
|
+
let length = positiveFinite(ray.length, fallbackLength);
|
|
828
|
+
let blocked = false;
|
|
829
|
+
for (const blocker of options.blockers ?? []) {
|
|
830
|
+
const result = callBlocker(blocker, ray, {
|
|
831
|
+
frame: frame.xr?.frame,
|
|
832
|
+
referenceSpace: frame.xr?.referenceSpace,
|
|
833
|
+
session: /** @type {any} */ (frame.xr?.session),
|
|
834
|
+
ray,
|
|
835
|
+
viewer: frame.viewer,
|
|
836
|
+
maxDistance: length,
|
|
837
|
+
});
|
|
838
|
+
if (!result) continue;
|
|
839
|
+
const hitDistance = finiteNumber(
|
|
840
|
+
result.distance
|
|
841
|
+
?? result.maxDistance
|
|
842
|
+
?? /** @type {{ length?: unknown }} */ (result.hit ?? {}).length,
|
|
843
|
+
Number.NaN,
|
|
844
|
+
);
|
|
845
|
+
if (Number.isFinite(hitDistance) && hitDistance >= 0) {
|
|
846
|
+
length = Math.min(length, hitDistance);
|
|
847
|
+
} else if (result.consumed === true || result.blocked === true) {
|
|
848
|
+
length = 0;
|
|
849
|
+
}
|
|
850
|
+
blocked = blocked || result.consumed === true || result.blocked === true;
|
|
851
|
+
}
|
|
852
|
+
return { length, blocked };
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* @param {import('../xr.d.ts').SkykitXrRayVisualPluginOptions['parent']} parent
|
|
857
|
+
* @param {import('../index.d.ts').SkykitThreePluginContext} context
|
|
858
|
+
*/
|
|
859
|
+
function resolveRayVisualParent(parent, context) {
|
|
860
|
+
if (typeof parent === 'function') {
|
|
861
|
+
return parent(context) ?? context.scene;
|
|
862
|
+
}
|
|
863
|
+
return parent ?? context.scene;
|
|
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/session.js
CHANGED
|
@@ -26,20 +26,35 @@ export async function enterSkykitXrSession(options = {}) {
|
|
|
26
26
|
if (!xr || typeof xr.requestSession !== 'function') {
|
|
27
27
|
throw new Error('WebXR is not available.');
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
/** @type {{ requestReferenceSpace?: (type: string) => Promise<unknown>; end?: () => Promise<void> | void } | null} */
|
|
30
|
+
let session = null;
|
|
31
|
+
try {
|
|
32
|
+
session = /** @type {{ requestReferenceSpace?: (type: string) => Promise<unknown>; end?: () => Promise<void> | void }} */ (
|
|
33
|
+
await xr.requestSession(mode, createSessionInit(options.sessionInit, referenceSpaceType))
|
|
34
|
+
);
|
|
35
|
+
const referenceSpace = options.requestReferenceSpace === false
|
|
36
|
+
? null
|
|
37
|
+
: typeof session.requestReferenceSpace === 'function'
|
|
38
|
+
? await session.requestReferenceSpace(referenceSpaceType)
|
|
39
|
+
: null;
|
|
40
|
+
const handle = createSessionHandle({
|
|
41
|
+
mode,
|
|
42
|
+
referenceSpaceType,
|
|
43
|
+
session,
|
|
44
|
+
referenceSpace,
|
|
45
|
+
});
|
|
46
|
+
options.onSessionStarted?.(handle);
|
|
47
|
+
return handle;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (session && typeof session.end === 'function') {
|
|
50
|
+
try {
|
|
51
|
+
await session.end();
|
|
52
|
+
} catch {
|
|
53
|
+
// Preserve the original session-enter error.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
/**
|
|
@@ -106,3 +121,34 @@ function resolveSkykitXr(navigatorLike) {
|
|
|
106
121
|
const nav = navigatorLike ?? globalThis.navigator;
|
|
107
122
|
return /** @type {{ xr?: { isSessionSupported?: (mode: string) => Promise<boolean>; requestSession?: (mode: string, init?: unknown) => Promise<unknown> } }} */ (nav)?.xr ?? null;
|
|
108
123
|
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {unknown} sessionInit
|
|
127
|
+
* @param {string} referenceSpaceType
|
|
128
|
+
*/
|
|
129
|
+
function createSessionInit(sessionInit, referenceSpaceType) {
|
|
130
|
+
const base = sessionInit && typeof sessionInit === 'object'
|
|
131
|
+
? /** @type {Record<string, unknown>} */ (sessionInit)
|
|
132
|
+
: {};
|
|
133
|
+
const requiredFeatures = normalizeFeatureList(base.requiredFeatures);
|
|
134
|
+
const optionalFeatures = normalizeFeatureList(base.optionalFeatures);
|
|
135
|
+
if (
|
|
136
|
+
referenceSpaceType &&
|
|
137
|
+
!requiredFeatures.includes(referenceSpaceType) &&
|
|
138
|
+
!optionalFeatures.includes(referenceSpaceType)
|
|
139
|
+
) {
|
|
140
|
+
optionalFeatures.push(referenceSpaceType);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
...base,
|
|
144
|
+
...(requiredFeatures.length > 0 ? { requiredFeatures } : {}),
|
|
145
|
+
...(optionalFeatures.length > 0 ? { optionalFeatures } : {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** @param {unknown} value */
|
|
150
|
+
function normalizeFeatureList(value) {
|
|
151
|
+
return Array.isArray(value)
|
|
152
|
+
? Array.from(new Set(value.filter((entry) => typeof entry === 'string' && entry.trim())))
|
|
153
|
+
: [];
|
|
154
|
+
}
|