@found-in-space/skykit 0.2.0-alpha.1 → 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.
- package/README.md +143 -6
- 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 +644 -217
- package/package.json +31 -5
- package/src/__tests__/skykit-anchored-images.test.js +32 -4
- package/src/__tests__/skykit-browser.test.js +217 -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 +123 -2
- package/src/__tests__/skykit.test.js +138 -1
- 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-journey.d.ts +8 -0
- package/src/browser-journey.js +240 -0
- package/src/browser.d.ts +98 -0
- package/src/browser.js +215 -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 +52 -2
- package/src/hr-diagram.js +23 -5
- package/src/index.d.ts +32 -7
- package/src/plugins.js +87 -43
- package/src/story.d.ts +57 -0
- package/src/story.js +396 -0
- 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 +224 -13
- package/src/xr/session.js +60 -14
- package/src/xr.d.ts +22 -0
- package/src/xr.js +1 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import test from 'node:test';
|
|
3
4
|
import * as THREE from 'three';
|
|
4
5
|
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
createSkykitXrObserverRig,
|
|
12
13
|
createSkykitXrPickRouter,
|
|
13
14
|
createSkykitXrRaySource,
|
|
15
|
+
createSkykitXrRayVisualPlugin,
|
|
14
16
|
createSkykitXrRig,
|
|
15
17
|
createSkykitXrSessionPlugin,
|
|
16
18
|
createSkykitXrStarPickingPlugin,
|
|
@@ -20,6 +22,32 @@ import {
|
|
|
20
22
|
} from '../xr.js';
|
|
21
23
|
import { createSkykitActionRegistry } from '../index.js';
|
|
22
24
|
|
|
25
|
+
test('xr free-roam demo uses restored alpha XR regressions defaults', () => {
|
|
26
|
+
const source = readFileSync(new URL('../../examples/xr-free-roam/xr-free-roam.js', import.meta.url), 'utf8');
|
|
27
|
+
|
|
28
|
+
assert.match(source, /createDefaultThreeStarFieldMaterialProfile/);
|
|
29
|
+
assert.doesNotMatch(source, /createVrThreeStarFieldMaterialProfile/);
|
|
30
|
+
assert.match(source, /createSkykitXrRayVisualPlugin/);
|
|
31
|
+
assert.match(source, /createSurfaceShell/);
|
|
32
|
+
assert.match(source, /createMetaSidecarProviderService/);
|
|
33
|
+
assert.match(source, /deriveMetaSidecarUrlFromRenderUrl/);
|
|
34
|
+
assert.match(source, /metaSidecarEntryDisplayFields/);
|
|
35
|
+
assert.match(source, /datasetId:\s*DATASET_ID_c56103/);
|
|
36
|
+
assert.match(source, /attributes:\s*\[\s*'objectRef'\s*,\s*'pickMeta'\s*\]/);
|
|
37
|
+
assert.match(source, /selectSun:\s*'xr-demo:selected\.sun'/);
|
|
38
|
+
assert.match(source, /primaryActionId:\s*XR_DEMO_ACTIONS\.goSelected/);
|
|
39
|
+
assert.match(source, /primaryActionLabel:\s*'Fly to'/);
|
|
40
|
+
assert.match(source, /homeControl:\s*'button'/);
|
|
41
|
+
assert.match(source, /pointerType:\s*'ray'/);
|
|
42
|
+
assert.doesNotMatch(source, /dragThreshold/);
|
|
43
|
+
assert.doesNotMatch(source, /createChoiceGroup/);
|
|
44
|
+
assert.doesNotMatch(source, /createSlider/);
|
|
45
|
+
assert.doesNotMatch(source, /createToggle/);
|
|
46
|
+
assert.doesNotMatch(source, /createStack/);
|
|
47
|
+
assert.doesNotMatch(source, /waypointPrefix/);
|
|
48
|
+
assert.doesNotMatch(source, /panelState\.page/);
|
|
49
|
+
});
|
|
50
|
+
|
|
23
51
|
test('skykit/xr rig builds multi-root hierarchy', () => {
|
|
24
52
|
const camera = new THREE.PerspectiveCamera();
|
|
25
53
|
const rig = createSkykitXrRig({
|
|
@@ -102,6 +130,32 @@ test('skykit/xr depth helpers compute and apply render state', () => {
|
|
|
102
130
|
assert.deepEqual(state, { depthNear: range.depthNear, depthFar: range.depthFar });
|
|
103
131
|
});
|
|
104
132
|
|
|
133
|
+
test('skykit/xr depth helpers include distant visible star bounds', () => {
|
|
134
|
+
const range = computeSkykitXrDepthRange({
|
|
135
|
+
observer: { x: 0, y: 0, z: 0 },
|
|
136
|
+
visibleBounds: {
|
|
137
|
+
min: { x: 62, y: 602, z: -13 },
|
|
138
|
+
max: { x: 64, y: 604, z: -11 },
|
|
139
|
+
},
|
|
140
|
+
observerCentricSpheres: [{ radiusNavigationUnits: 16 }],
|
|
141
|
+
scale: {
|
|
142
|
+
navigationUnits: 'pc',
|
|
143
|
+
metersPerNavigationUnit: 1,
|
|
144
|
+
worldUnitsPerNavigationUnit: 1,
|
|
145
|
+
},
|
|
146
|
+
policy: {
|
|
147
|
+
near: 0.03,
|
|
148
|
+
minFar: 100,
|
|
149
|
+
maxFar: 2000000,
|
|
150
|
+
marginFactor: 1.2,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
assert.ok(range.far > 720);
|
|
155
|
+
assert.ok(range.telemetry.farthestVisibleBoundsDistance > 600);
|
|
156
|
+
assert.equal(range.telemetry.farthestObserverCentricSphereDistance, 16);
|
|
157
|
+
});
|
|
158
|
+
|
|
105
159
|
test('skykit/xr session helpers use injected navigator', async () => {
|
|
106
160
|
let ended = false;
|
|
107
161
|
const session = {
|
|
@@ -118,7 +172,8 @@ test('skykit/xr session helpers use injected navigator', async () => {
|
|
|
118
172
|
async isSessionSupported(mode) {
|
|
119
173
|
return mode === 'immersive-vr';
|
|
120
174
|
},
|
|
121
|
-
async requestSession() {
|
|
175
|
+
async requestSession(_mode, init) {
|
|
176
|
+
this.lastInit = init;
|
|
122
177
|
return session;
|
|
123
178
|
},
|
|
124
179
|
},
|
|
@@ -126,6 +181,7 @@ test('skykit/xr session helpers use injected navigator', async () => {
|
|
|
126
181
|
assert.equal(await isSkykitXrModeSupported('immersive-vr', { navigator }), true);
|
|
127
182
|
const handle = await enterSkykitXrSession({ navigator, mode: 'immersive-vr' });
|
|
128
183
|
assert.equal(handle.presenting, true);
|
|
184
|
+
assert.deepEqual(navigator.xr.lastInit.optionalFeatures, ['local-floor']);
|
|
129
185
|
await exitSkykitXrSession(handle);
|
|
130
186
|
assert.equal(ended, true);
|
|
131
187
|
});
|
|
@@ -147,15 +203,20 @@ test('skykit/xr observer rig bridges viewer state to an XR rig without camera re
|
|
|
147
203
|
|
|
148
204
|
test('skykit/xr session plugin registers enter/exit actions and syncs snapshot state', async () => {
|
|
149
205
|
let activeSession = null;
|
|
206
|
+
let requestedReferenceSpaceCount = 0;
|
|
150
207
|
const session = {
|
|
151
208
|
async requestReferenceSpace(type) {
|
|
209
|
+
requestedReferenceSpaceCount += 1;
|
|
152
210
|
return { type };
|
|
153
211
|
},
|
|
154
212
|
async end() {
|
|
155
213
|
this.ended = true;
|
|
214
|
+
activeSession = null;
|
|
215
|
+
renderer.xr.isPresenting = false;
|
|
156
216
|
},
|
|
157
217
|
addEventListener() {},
|
|
158
218
|
};
|
|
219
|
+
let rendererReferenceSpaceType = null;
|
|
159
220
|
const renderer = {
|
|
160
221
|
xr: {
|
|
161
222
|
enabled: false,
|
|
@@ -163,6 +224,9 @@ test('skykit/xr session plugin registers enter/exit actions and syncs snapshot s
|
|
|
163
224
|
getSession() {
|
|
164
225
|
return activeSession;
|
|
165
226
|
},
|
|
227
|
+
setReferenceSpaceType(type) {
|
|
228
|
+
rendererReferenceSpaceType = type;
|
|
229
|
+
},
|
|
166
230
|
async setSession(nextSession) {
|
|
167
231
|
activeSession = nextSession;
|
|
168
232
|
this.isPresenting = Boolean(nextSession);
|
|
@@ -174,7 +238,8 @@ test('skykit/xr session plugin registers enter/exit actions and syncs snapshot s
|
|
|
174
238
|
async isSessionSupported() {
|
|
175
239
|
return true;
|
|
176
240
|
},
|
|
177
|
-
async requestSession() {
|
|
241
|
+
async requestSession(_mode, init) {
|
|
242
|
+
this.lastInit = init;
|
|
178
243
|
return session;
|
|
179
244
|
},
|
|
180
245
|
},
|
|
@@ -195,8 +260,12 @@ test('skykit/xr session plugin registers enter/exit actions and syncs snapshot s
|
|
|
195
260
|
|
|
196
261
|
await actions.invoke('skykit:xr.enter');
|
|
197
262
|
assert.equal(renderer.xr.enabled, true);
|
|
263
|
+
assert.equal(rendererReferenceSpaceType, 'local-floor');
|
|
264
|
+
assert.deepEqual(navigator.xr.lastInit.optionalFeatures, ['local-floor']);
|
|
265
|
+
assert.equal(requestedReferenceSpaceCount, 0);
|
|
198
266
|
assert.equal(activeSession, session);
|
|
199
267
|
assert.equal(plugin.getSnapshot().presenting, true);
|
|
268
|
+
assert.equal(plugin.getSnapshot().enterStage, 'presenting');
|
|
200
269
|
|
|
201
270
|
const frame = createXrFrame({ renderer });
|
|
202
271
|
part.update(frame);
|
|
@@ -240,6 +309,58 @@ test('skykit/xr navigation plugin updates viewer state from controller axes', ()
|
|
|
240
309
|
assert.deepEqual(actions.getControlValue('skykit:ship.control.move'), { x: 0, y: 0, z: -10 });
|
|
241
310
|
});
|
|
242
311
|
|
|
312
|
+
test('skykit/xr ray visual shows the controller ray and shortens at blockers', () => {
|
|
313
|
+
let part = null;
|
|
314
|
+
let disposedRaySource = false;
|
|
315
|
+
const plugin = createSkykitXrRayVisualPlugin({
|
|
316
|
+
raySource: {
|
|
317
|
+
getRay() {
|
|
318
|
+
return {
|
|
319
|
+
id: 'ray',
|
|
320
|
+
kind: 'target-ray',
|
|
321
|
+
handedness: 'right',
|
|
322
|
+
origin: { x: 1, y: 2, z: 3 },
|
|
323
|
+
direction: { x: 0, y: 0, z: -1 },
|
|
324
|
+
length: 10,
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
getSnapshot() {
|
|
328
|
+
return { id: 'ray-source' };
|
|
329
|
+
},
|
|
330
|
+
dispose() {
|
|
331
|
+
disposedRaySource = true;
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
blockers: [{
|
|
335
|
+
blockRay() {
|
|
336
|
+
return { blocked: true, distance: 3, hit: { componentId: 'panel' } };
|
|
337
|
+
},
|
|
338
|
+
}],
|
|
339
|
+
});
|
|
340
|
+
const context = createPluginContext({
|
|
341
|
+
addPart(nextPart) {
|
|
342
|
+
part = nextPart;
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
plugin.setup(context);
|
|
346
|
+
part.attach(context);
|
|
347
|
+
|
|
348
|
+
part.update(createXrFrame());
|
|
349
|
+
|
|
350
|
+
assert.equal(part.object3d.visible, true);
|
|
351
|
+
assert.equal(plugin.getSnapshot().blocked, true);
|
|
352
|
+
assert.equal(plugin.getSnapshot().lastLength, 3);
|
|
353
|
+
assert.deepEqual(
|
|
354
|
+
Array.from(part.object3d.children[0].geometry.getAttribute('position').array),
|
|
355
|
+
[1, 2, 3, 1, 2, 0],
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
part.update({ ...createXrFrame(), xr: { presenting: false } });
|
|
359
|
+
assert.equal(part.object3d.visible, false);
|
|
360
|
+
part.dispose();
|
|
361
|
+
assert.equal(disposedRaySource, true);
|
|
362
|
+
});
|
|
363
|
+
|
|
243
364
|
test('skykit/xr star picking fires only on trigger edge and registers attribute-only demand', () => {
|
|
244
365
|
const actions = createSkykitActionRegistry();
|
|
245
366
|
let part = null;
|
|
@@ -77,6 +77,57 @@ function createRenderer() {
|
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function createTextureRenderer() {
|
|
81
|
+
const renderer = createRenderer();
|
|
82
|
+
return {
|
|
83
|
+
...renderer,
|
|
84
|
+
xr: { enabled: true },
|
|
85
|
+
currentTarget: null,
|
|
86
|
+
viewport: new THREE.Vector4(0, 0, 1, 1),
|
|
87
|
+
scissor: new THREE.Vector4(0, 0, 1, 1),
|
|
88
|
+
scissorTest: false,
|
|
89
|
+
getRenderTarget() {
|
|
90
|
+
return this.currentTarget;
|
|
91
|
+
},
|
|
92
|
+
setRenderTarget(target) {
|
|
93
|
+
this.currentTarget = target;
|
|
94
|
+
},
|
|
95
|
+
getViewport(target) {
|
|
96
|
+
return target.copy(this.viewport);
|
|
97
|
+
},
|
|
98
|
+
setViewport(value) {
|
|
99
|
+
if (value?.isVector4) this.viewport.copy(value);
|
|
100
|
+
},
|
|
101
|
+
getScissor(target) {
|
|
102
|
+
return target.copy(this.scissor);
|
|
103
|
+
},
|
|
104
|
+
setScissor(value) {
|
|
105
|
+
if (value?.isVector4) this.scissor.copy(value);
|
|
106
|
+
},
|
|
107
|
+
getScissorTest() {
|
|
108
|
+
return this.scissorTest;
|
|
109
|
+
},
|
|
110
|
+
setScissorTest(value) {
|
|
111
|
+
this.scissorTest = Boolean(value);
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function createEmbeddedSurfaceSpy() {
|
|
117
|
+
const publishCalls = [];
|
|
118
|
+
const unpublishCalls = [];
|
|
119
|
+
return {
|
|
120
|
+
publishCalls,
|
|
121
|
+
unpublishCalls,
|
|
122
|
+
publish(sourceId, update) {
|
|
123
|
+
publishCalls.push({ sourceId, update });
|
|
124
|
+
},
|
|
125
|
+
unpublish(sourceId) {
|
|
126
|
+
unpublishCalls.push(sourceId);
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
80
131
|
async function flushMicrotasks(count = 10) {
|
|
81
132
|
for (let index = 0; index < count; index += 1) {
|
|
82
133
|
await Promise.resolve();
|
|
@@ -242,6 +293,51 @@ test('viewer exposes action registry, emits action events, and resets to initial
|
|
|
242
293
|
await viewer.dispose();
|
|
243
294
|
});
|
|
244
295
|
|
|
296
|
+
test('viewer derives camera orientation from lookAt targets, sky coordinates, and stars', async () => {
|
|
297
|
+
const targetViewer = await createSkykitViewer({
|
|
298
|
+
renderer: createRenderer(),
|
|
299
|
+
view: {
|
|
300
|
+
observerPc: { x: 0, y: 0, z: 0 },
|
|
301
|
+
lookAt: { targetPc: { x: 10, y: 0, z: 0 }, positionAngleDeg: 0 },
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
let view = targetViewer.getViewState();
|
|
305
|
+
assert.deepEqual(view.targetPc, { x: 10, y: 0, z: 0 });
|
|
306
|
+
assertVectorApprox(localVectorFromView(view, { x: 0, y: 0, z: -1 }), { x: 1, y: 0, z: 0 });
|
|
307
|
+
assertVectorApprox(localVectorFromView(view, { x: 0, y: 1, z: 0 }), { x: 0, y: 0, z: 1 });
|
|
308
|
+
await targetViewer.dispose();
|
|
309
|
+
|
|
310
|
+
const skyViewer = await createSkykitViewer({
|
|
311
|
+
renderer: createRenderer(),
|
|
312
|
+
view: {
|
|
313
|
+
lookAt: { raDeg: 0, decDeg: 0, positionAngleDeg: 90 },
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
view = skyViewer.getViewState();
|
|
317
|
+
assert.equal(view.targetPc, null);
|
|
318
|
+
assertVectorApprox(localVectorFromView(view, { x: 0, y: 0, z: -1 }), { x: 1, y: 0, z: 0 });
|
|
319
|
+
assertVectorApprox(localVectorFromView(view, { x: 0, y: 1, z: 0 }), { x: 0, y: 1, z: 0 });
|
|
320
|
+
await skyViewer.dispose();
|
|
321
|
+
|
|
322
|
+
const starViewer = await createSkykitViewer({
|
|
323
|
+
renderer: createRenderer(),
|
|
324
|
+
view: { lookAt: { star: 'hyades' } },
|
|
325
|
+
resolveLookAtStar: async (star) => star === 'hyades'
|
|
326
|
+
? { targetPc: { x: 4, y: 5, z: 6 } }
|
|
327
|
+
: null,
|
|
328
|
+
});
|
|
329
|
+
view = starViewer.getViewState();
|
|
330
|
+
assert.equal(view.lookAt?.star, 'hyades');
|
|
331
|
+
assert.deepEqual(view.targetPc, { x: 4, y: 5, z: 6 });
|
|
332
|
+
assert.ok(view.orientationIcrs);
|
|
333
|
+
|
|
334
|
+
starViewer.requestViewState({ lookAt: { star: 'orion' } }, 'test-star-look');
|
|
335
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
336
|
+
starViewer.update(0);
|
|
337
|
+
assert.deepEqual(starViewer.getViewState().targetPc, null);
|
|
338
|
+
await starViewer.dispose();
|
|
339
|
+
});
|
|
340
|
+
|
|
245
341
|
test('requestViewState batches patches and observer-centric root follows translation without rotation', async () => {
|
|
246
342
|
const viewer = await createSkykitViewer({
|
|
247
343
|
renderer: createRenderer(),
|
|
@@ -682,6 +778,41 @@ test('HR diagram mode changes refresh shared demand and update the renderer view
|
|
|
682
778
|
await viewer.dispose();
|
|
683
779
|
});
|
|
684
780
|
|
|
781
|
+
test('HR diagram touch-os surfaces can be resolved lazily from panel runtimes', async () => {
|
|
782
|
+
const session = createFakeSession();
|
|
783
|
+
const provider = {
|
|
784
|
+
id: 'provider',
|
|
785
|
+
createSession() {
|
|
786
|
+
return session;
|
|
787
|
+
},
|
|
788
|
+
};
|
|
789
|
+
const source = createSkykitStarSourcePlugin({ provider });
|
|
790
|
+
const surfaces = createEmbeddedSurfaceSpy();
|
|
791
|
+
let activeSurfaces = null;
|
|
792
|
+
const hr = createSkykitHrDiagramPlugin({
|
|
793
|
+
id: 'hr',
|
|
794
|
+
source,
|
|
795
|
+
touchOs: {
|
|
796
|
+
sourceId: 'hr:surface',
|
|
797
|
+
surfaces: () => activeSurfaces,
|
|
798
|
+
},
|
|
799
|
+
});
|
|
800
|
+
const viewer = await createSkykitViewer({
|
|
801
|
+
renderer: createTextureRenderer(),
|
|
802
|
+
plugins: [source, hr],
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
activeSurfaces = surfaces;
|
|
806
|
+
viewer.frame(0.016);
|
|
807
|
+
|
|
808
|
+
assert.equal(surfaces.publishCalls.length, 1);
|
|
809
|
+
assert.equal(surfaces.publishCalls[0].sourceId, 'hr:surface');
|
|
810
|
+
assert.equal(surfaces.publishCalls[0].update.available, true);
|
|
811
|
+
|
|
812
|
+
await viewer.dispose();
|
|
813
|
+
assert.deepEqual(surfaces.unpublishCalls, ['hr:surface']);
|
|
814
|
+
});
|
|
815
|
+
|
|
685
816
|
test('HR diagram demand strategy override can be supplied and restored at runtime', async () => {
|
|
686
817
|
const sessions = [];
|
|
687
818
|
const provider = {
|
|
@@ -1698,7 +1829,7 @@ test('journey plugin can route to non-orbit scenes before applying the arrival t
|
|
|
1698
1829
|
},
|
|
1699
1830
|
},
|
|
1700
1831
|
local: {
|
|
1701
|
-
view: { targetPc: { x: 1, y: 0, z: 0 } },
|
|
1832
|
+
view: { lookAt: { targetPc: { x: 1, y: 0, z: 0 } } },
|
|
1702
1833
|
navigation: {
|
|
1703
1834
|
transitionTo: {
|
|
1704
1835
|
observerPc: { x: 0, y: 0, z: 0 },
|
|
@@ -2323,6 +2454,12 @@ function localVectorFromView(view, vector) {
|
|
|
2323
2454
|
return { x: result.x, y: result.y, z: result.z };
|
|
2324
2455
|
}
|
|
2325
2456
|
|
|
2457
|
+
function assertVectorApprox(actual, expected, epsilon = 1e-9) {
|
|
2458
|
+
assert.ok(Math.abs(actual.x - expected.x) < epsilon, `x ${actual.x} !== ${expected.x}`);
|
|
2459
|
+
assert.ok(Math.abs(actual.y - expected.y) < epsilon, `y ${actual.y} !== ${expected.y}`);
|
|
2460
|
+
assert.ok(Math.abs(actual.z - expected.z) < epsilon, `z ${actual.z} !== ${expected.z}`);
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2326
2463
|
function createPointerTarget() {
|
|
2327
2464
|
const target = createEventTarget();
|
|
2328
2465
|
target.clientWidth = 800;
|
package/src/anchored-images.js
CHANGED
|
@@ -107,16 +107,16 @@ export async function createAnchoredImageCatalog(options = {}) {
|
|
|
107
107
|
}),
|
|
108
108
|
};
|
|
109
109
|
},
|
|
110
|
-
resolveNearest(
|
|
111
|
-
const matches = scoreEntries(
|
|
110
|
+
resolveNearest(lookDirection, nearestOptions = {}) {
|
|
111
|
+
const matches = scoreEntries(lookDirection, selectEntries(entries, nearestOptions.selection, catalog));
|
|
112
112
|
if (matches.length === 0) return null;
|
|
113
113
|
const maxAngleRad = optionalAngleRad(nearestOptions.maxAngleDeg);
|
|
114
114
|
return matches.find((match) => maxAngleRad == null || match.viewDistanceRad <= maxAngleRad) ?? null;
|
|
115
115
|
},
|
|
116
|
-
resolveWithinAngle(
|
|
116
|
+
resolveWithinAngle(lookDirection, withinOptions) {
|
|
117
117
|
const maxAngleRad = requiredAngleRad(withinOptions?.maxAngleDeg);
|
|
118
118
|
if (!Number.isFinite(maxAngleRad)) return [];
|
|
119
|
-
return scoreEntries(
|
|
119
|
+
return scoreEntries(lookDirection, selectEntries(entries, withinOptions?.selection, catalog))
|
|
120
120
|
.filter((match) => match.viewDistanceRad <= maxAngleRad);
|
|
121
121
|
},
|
|
122
122
|
};
|
|
@@ -308,7 +308,8 @@ export function createAnchoredImageSkyPlugin(options) {
|
|
|
308
308
|
id,
|
|
309
309
|
priority: options.priority,
|
|
310
310
|
object3d: root,
|
|
311
|
-
anchorMode: fixedAtInfinity ? 'observer-centric' : 'world-space',
|
|
311
|
+
anchorMode: options.anchorMode ?? (fixedAtInfinity ? 'observer-centric' : 'world-space'),
|
|
312
|
+
scaleBandId: options.scaleBandId,
|
|
312
313
|
});
|
|
313
314
|
pluginContext.addPart({
|
|
314
315
|
...layer,
|
|
@@ -661,26 +662,24 @@ function targetDirectionAt(solved, x, y) {
|
|
|
661
662
|
|
|
662
663
|
/** @param {SkykitViewState} view @returns {Vector3Like} */
|
|
663
664
|
function resolveViewDirection(view) {
|
|
664
|
-
const explicitDirection = normalizeVector3(view.directionIcrs, null);
|
|
665
|
-
if (explicitDirection) return explicitDirection;
|
|
666
|
-
const orientation = normalizeQuaternion(view.orientationIcrs);
|
|
667
|
-
if (orientation) return normalizeVector3(rotateVectorByQuaternion(LOCAL_FORWARD, orientation), LOCAL_FORWARD) ?? { ...LOCAL_FORWARD };
|
|
668
665
|
const targetPc = normalizeVector3(view.targetPc, null);
|
|
669
666
|
const observerPc = normalizeVector3(view.observerPc, { x: 0, y: 0, z: 0 }) ?? { x: 0, y: 0, z: 0 };
|
|
670
667
|
if (targetPc) {
|
|
671
668
|
const direction = normalizeVector3(subtractVectors(targetPc, observerPc), null);
|
|
672
669
|
if (direction) return direction;
|
|
673
670
|
}
|
|
671
|
+
const orientation = normalizeQuaternion(view.orientationIcrs);
|
|
672
|
+
if (orientation) return normalizeVector3(rotateVectorByQuaternion(LOCAL_FORWARD, orientation), LOCAL_FORWARD) ?? { ...LOCAL_FORWARD };
|
|
674
673
|
return { ...LOCAL_FORWARD };
|
|
675
674
|
}
|
|
676
675
|
|
|
677
676
|
/**
|
|
678
|
-
* @param {Vector3Like | [number, number, number]}
|
|
677
|
+
* @param {Vector3Like | [number, number, number]} lookDirection
|
|
679
678
|
* @param {AnchoredImageCatalogEntry[]} entries
|
|
680
679
|
* @returns {AnchoredImageMatch[]}
|
|
681
680
|
*/
|
|
682
|
-
function scoreEntries(
|
|
683
|
-
const direction = vector3FromArray(normalizeDirection(
|
|
681
|
+
function scoreEntries(lookDirection, entries) {
|
|
682
|
+
const direction = vector3FromArray(normalizeDirection(lookDirection));
|
|
684
683
|
if (!direction) return [];
|
|
685
684
|
return entries
|
|
686
685
|
.map((entry) => scoreEntry(direction, entry))
|
|
@@ -689,13 +688,13 @@ function scoreEntries(directionIcrs, entries) {
|
|
|
689
688
|
}
|
|
690
689
|
|
|
691
690
|
/**
|
|
692
|
-
* @param {Vector3Like | [number, number, number]}
|
|
691
|
+
* @param {Vector3Like | [number, number, number]} lookDirection
|
|
693
692
|
* @param {AnchoredImageCatalogEntry | null | undefined} entry
|
|
694
693
|
* @returns {AnchoredImageMatch | null}
|
|
695
694
|
*/
|
|
696
|
-
function scoreEntry(
|
|
695
|
+
function scoreEntry(lookDirection, entry) {
|
|
697
696
|
if (!entry) return null;
|
|
698
|
-
const direction = vector3FromArray(normalizeDirection(
|
|
697
|
+
const direction = vector3FromArray(normalizeDirection(lookDirection));
|
|
699
698
|
if (!direction) return null;
|
|
700
699
|
const angleRad = angularDistance(direction, entry.centroidIcrs);
|
|
701
700
|
const viewDistanceRad = Math.max(0, angleRad - entry.boundsConeRadiusRad);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SkykitBrowser,
|
|
3
|
+
SkykitBrowserAddon,
|
|
4
|
+
SkykitBrowserGlobal,
|
|
5
|
+
} from './browser.js';
|
|
6
|
+
|
|
7
|
+
export declare function installSkykitBrowserGlobal(target?: typeof globalThis): SkykitBrowserGlobal;
|
|
8
|
+
export declare function registerBrowserAddon(
|
|
9
|
+
service: SkykitBrowserGlobal,
|
|
10
|
+
addon: SkykitBrowserAddon
|
|
11
|
+
): () => void;
|
|
12
|
+
export declare function registerBrowserInstance(
|
|
13
|
+
service: SkykitBrowserGlobal,
|
|
14
|
+
host: unknown,
|
|
15
|
+
browser: SkykitBrowser
|
|
16
|
+
): () => void;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const STATE = Symbol.for('found-in-space.skykit.browserAddons');
|
|
2
|
+
const GLOBAL_NAME = 'Skykit';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Install or upgrade the small global service used by the noob embed path.
|
|
6
|
+
*
|
|
7
|
+
* @param {typeof globalThis} [target]
|
|
8
|
+
* @returns {import('./browser.d.ts').SkykitBrowserGlobal}
|
|
9
|
+
*/
|
|
10
|
+
export function installSkykitBrowserGlobal(target = globalThis) {
|
|
11
|
+
const globalTarget = /** @type {typeof globalThis & { Skykit?: Partial<import('./browser.d.ts').SkykitBrowserGlobal> & Record<PropertyKey, unknown> }} */ (target);
|
|
12
|
+
const service = /** @type {Partial<import('./browser.d.ts').SkykitBrowserGlobal> & Record<PropertyKey, unknown>} */ (
|
|
13
|
+
globalTarget[GLOBAL_NAME] && typeof globalTarget[GLOBAL_NAME] === 'object'
|
|
14
|
+
? globalTarget[GLOBAL_NAME]
|
|
15
|
+
: {}
|
|
16
|
+
);
|
|
17
|
+
const state = getState(service);
|
|
18
|
+
const queuedAddons = Array.isArray(service.browserAddons)
|
|
19
|
+
? service.browserAddons.splice(0)
|
|
20
|
+
: [];
|
|
21
|
+
|
|
22
|
+
service.browserAddons = state.addons;
|
|
23
|
+
service.registerBrowserAddon = (addon) => registerBrowserAddon(service, addon);
|
|
24
|
+
service.whenReady = (targetOrSelector) => whenReady(service, targetOrSelector);
|
|
25
|
+
service.getBrowsers = () => state.records.map((record) => record.browser);
|
|
26
|
+
globalTarget[GLOBAL_NAME] = service;
|
|
27
|
+
|
|
28
|
+
for (const addon of queuedAddons) {
|
|
29
|
+
registerBrowserAddon(service, addon);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return /** @type {import('./browser.d.ts').SkykitBrowserGlobal} */ (service);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {import('./browser.d.ts').SkykitBrowserGlobal} service
|
|
37
|
+
* @param {import('./browser.d.ts').SkykitBrowserAddon} addon
|
|
38
|
+
*/
|
|
39
|
+
export function registerBrowserAddon(service, addon) {
|
|
40
|
+
if (!addon || typeof addon.install !== 'function') {
|
|
41
|
+
throw new TypeError('SkyKit browser add-ons must provide install(context).');
|
|
42
|
+
}
|
|
43
|
+
const state = getState(service);
|
|
44
|
+
if (state.addons.includes(addon)) return () => {};
|
|
45
|
+
state.addons.push(addon);
|
|
46
|
+
for (const record of state.records) {
|
|
47
|
+
void installAddonOnRecord(record, addon);
|
|
48
|
+
}
|
|
49
|
+
return () => {
|
|
50
|
+
const index = state.addons.indexOf(addon);
|
|
51
|
+
if (index >= 0) state.addons.splice(index, 1);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {import('./browser.d.ts').SkykitBrowserGlobal} service
|
|
57
|
+
* @param {unknown} host
|
|
58
|
+
* @param {import('./browser.d.ts').SkykitBrowser} browser
|
|
59
|
+
* @returns {() => void}
|
|
60
|
+
*/
|
|
61
|
+
export function registerBrowserInstance(service, host, browser) {
|
|
62
|
+
const state = getState(service);
|
|
63
|
+
const record = {
|
|
64
|
+
host,
|
|
65
|
+
browser,
|
|
66
|
+
installedAddonIds: new Set(),
|
|
67
|
+
};
|
|
68
|
+
state.records.push(record);
|
|
69
|
+
for (const addon of state.addons) {
|
|
70
|
+
void installAddonOnRecord(record, addon);
|
|
71
|
+
}
|
|
72
|
+
resolveWaiters(state, record);
|
|
73
|
+
return () => {
|
|
74
|
+
const index = state.records.indexOf(record);
|
|
75
|
+
if (index >= 0) state.records.splice(index, 1);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {Partial<import('./browser.d.ts').SkykitBrowserGlobal> & Record<PropertyKey, unknown>} service
|
|
81
|
+
* @returns {{
|
|
82
|
+
* addons: import('./browser.d.ts').SkykitBrowserAddon[];
|
|
83
|
+
* records: Array<{ host: unknown; browser: import('./browser.d.ts').SkykitBrowser; installedAddonIds: Set<string> }>;
|
|
84
|
+
* waiters: Array<{ target: unknown; resolve: (browser: import('./browser.d.ts').SkykitBrowser) => void; reject: (error: unknown) => void }>;
|
|
85
|
+
* }}
|
|
86
|
+
*/
|
|
87
|
+
function getState(service) {
|
|
88
|
+
if (!service[STATE]) {
|
|
89
|
+
Object.defineProperty(service, STATE, {
|
|
90
|
+
configurable: false,
|
|
91
|
+
enumerable: false,
|
|
92
|
+
value: {
|
|
93
|
+
addons: [],
|
|
94
|
+
records: [],
|
|
95
|
+
waiters: [],
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return /** @type {ReturnType<typeof getState>} */ (service[STATE]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {ReturnType<typeof getState>['records'][number]} record
|
|
104
|
+
* @param {import('./browser.d.ts').SkykitBrowserAddon} addon
|
|
105
|
+
*/
|
|
106
|
+
async function installAddonOnRecord(record, addon) {
|
|
107
|
+
const addonId = addon.id ?? addon.install;
|
|
108
|
+
const dedupeId = typeof addonId === 'string' ? addonId : String(record.installedAddonIds.size + 1);
|
|
109
|
+
if (record.installedAddonIds.has(dedupeId)) return;
|
|
110
|
+
record.installedAddonIds.add(dedupeId);
|
|
111
|
+
await record.browser.install(addon);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {import('./browser.d.ts').SkykitBrowserGlobal} service
|
|
116
|
+
* @param {unknown} target
|
|
117
|
+
* @returns {Promise<import('./browser.d.ts').SkykitBrowser>}
|
|
118
|
+
*/
|
|
119
|
+
function whenReady(service, target) {
|
|
120
|
+
const state = getState(service);
|
|
121
|
+
const existing = state.records.find((record) => matchesTarget(record.host, target));
|
|
122
|
+
if (existing) return Promise.resolve(existing.browser);
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
state.waiters.push({ target, resolve, reject });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {ReturnType<typeof getState>} state
|
|
130
|
+
* @param {ReturnType<typeof getState>['records'][number]} record
|
|
131
|
+
*/
|
|
132
|
+
function resolveWaiters(state, record) {
|
|
133
|
+
for (const waiter of [...state.waiters]) {
|
|
134
|
+
if (!matchesTarget(record.host, waiter.target)) continue;
|
|
135
|
+
const index = state.waiters.indexOf(waiter);
|
|
136
|
+
if (index >= 0) state.waiters.splice(index, 1);
|
|
137
|
+
waiter.resolve(record.browser);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** @param {unknown} host @param {unknown} target */
|
|
142
|
+
function matchesTarget(host, target) {
|
|
143
|
+
if (target == null) return true;
|
|
144
|
+
if (target === host) return true;
|
|
145
|
+
if (typeof target !== 'string') return false;
|
|
146
|
+
const element = /** @type {{ matches?: (selector: string) => boolean }} */ (host);
|
|
147
|
+
if (typeof element.matches === 'function') {
|
|
148
|
+
try {
|
|
149
|
+
if (element.matches(target)) return true;
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SkykitBrowser,
|
|
3
|
+
SkykitBrowserConstellationsFacade,
|
|
4
|
+
SkykitBrowserConstellationsOptions,
|
|
5
|
+
} from './browser.js';
|
|
6
|
+
|
|
7
|
+
export declare function installSkykitConstellationsBrowserCapability(
|
|
8
|
+
context: {
|
|
9
|
+
browser: SkykitBrowser;
|
|
10
|
+
host?: unknown;
|
|
11
|
+
options?: SkykitBrowserConstellationsOptions;
|
|
12
|
+
}
|
|
13
|
+
): Promise<SkykitBrowserConstellationsFacade>;
|