@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/README.md +40 -35
- package/examples/xr-free-roam/xr-free-roam.js +31 -57
- package/package.json +11 -21
- package/src/__tests__/skykit-browser.test.js +184 -40
- package/src/__tests__/skykit-xr.test.js +56 -0
- package/src/__tests__/skykit.test.js +90 -503
- package/src/actions.js +0 -8
- package/src/browser.d.ts +2 -19
- package/src/browser.js +18 -31
- package/src/embed.js +22 -2
- package/src/hr-diagram.js +3 -1
- package/src/index.d.ts +10 -78
- package/src/index.js +6 -1
- package/src/plugins.js +0 -730
- package/src/utils.js +1 -0
- package/src/xr/plugins.js +74 -0
- package/src/xr.d.ts +18 -0
- package/src/xr.js +1 -0
- package/src/browser-journey.d.ts +0 -8
- package/src/browser-journey.js +0 -240
- package/src/story.d.ts +0 -57
- package/src/story.js +0 -396
package/README.md
CHANGED
|
@@ -28,11 +28,11 @@ The beginner website entries are use-case bounded:
|
|
|
28
28
|
| --- | --- | --- |
|
|
29
29
|
| Viewer | "Put stars on my page and let me customize the scene." | `embed.js`, `viewer.js` |
|
|
30
30
|
| Data | "Give me star data so I can render, list, map, or game it myself." | `data.js` |
|
|
31
|
-
| Story | "Let me tell a curated story through space." | `story.js` |
|
|
32
31
|
|
|
33
32
|
`embed.js` is the no-code viewer entry. It is not a separate use-case.
|
|
34
33
|
`viewer.js` is the JavaScript-customizable viewer entry. `data.js` is renderer
|
|
35
|
-
independent.
|
|
34
|
+
independent. Authored chapters stay in website or lesson code and call SkyKit
|
|
35
|
+
navigation actions directly.
|
|
36
36
|
|
|
37
37
|
## Paste into a static page or CMS
|
|
38
38
|
|
|
@@ -69,17 +69,29 @@ Optional attributes keep small tweaks HTML-only:
|
|
|
69
69
|
data-skykit-magnitude="7"
|
|
70
70
|
data-skykit-speed="4"
|
|
71
71
|
data-skykit-exposure="2600"
|
|
72
|
-
data-skykit-
|
|
72
|
+
data-skykit-observer="06h 45m 08.9s, -16d 42m 58s, 2.64pc"
|
|
73
|
+
data-skykit-look-at="05h 35m 17.3s, -05d 23m 28s, 414pc"
|
|
74
|
+
data-skykit-coordinate-origin="solar"
|
|
73
75
|
data-skykit-mouse-mode="strafe"
|
|
76
|
+
data-skykit-persistent-cache="off"
|
|
74
77
|
style="width: 100%; height: 520px; background: #02040b"
|
|
75
78
|
></div>
|
|
76
79
|
```
|
|
77
80
|
|
|
81
|
+
`data-skykit-observer` accepts fixed parsec-space `x,y,z` coordinates or
|
|
82
|
+
RA/Dec/distance text such as `06h 45m 08.9s, -16d 42m 58s, 2.64pc`.
|
|
78
83
|
`data-skykit-look-at` accepts RA/Dec text such as
|
|
79
|
-
`
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
`05h 36m 12.81s, −01° 12′ 06.9″`, decimal degrees such as
|
|
85
|
+
`84.053393,-1.201926`, RA/Dec/distance text for a fixed heliocentric target,
|
|
86
|
+
or a parsec-space `x,y,z` target for exact generated coordinates.
|
|
87
|
+
RA/Dec/distance resolves from the solar origin; pure RA/Dec remains a
|
|
88
|
+
directional look. `data-skykit-coordinate-origin="solar"` is accepted as
|
|
89
|
+
clarifying markup, while observer-relative shorthand is not part of this alpha
|
|
90
|
+
embed yet. `data-skykit-mouse-mode` defaults to `grab`; use `look` or
|
|
91
|
+
`strafe` for the first-person mouse-look direction, or `none` to disable mouse
|
|
92
|
+
drag controls. Persistent browser Cache API storage is enabled by default for
|
|
93
|
+
octree ranges; set
|
|
94
|
+
`data-skykit-persistent-cache="off"` to keep caching session-only.
|
|
83
95
|
|
|
84
96
|
The host dispatches `skykit-browser-ready` with `{ browser, viewer }` in
|
|
85
97
|
`event.detail` after startup, and `skykit-browser-error` if startup fails. The
|
|
@@ -115,12 +127,20 @@ For small scripted interactions, use the browser handle:
|
|
|
115
127
|
|
|
116
128
|
```html
|
|
117
129
|
<script type="module">
|
|
130
|
+
import {
|
|
131
|
+
SKYKIT_ACTIONS,
|
|
132
|
+
createRaDecLookAt,
|
|
133
|
+
createSkykitNavigationPlugin,
|
|
134
|
+
} from 'https://esm.sh/@found-in-space/skykit';
|
|
135
|
+
|
|
118
136
|
const browser = await Skykit.whenReady();
|
|
137
|
+
await browser.install(createSkykitNavigationPlugin());
|
|
138
|
+
const alnilam = createRaDecLookAt('05h 36m 12.81s', '−01° 12′ 06.9″');
|
|
119
139
|
|
|
120
140
|
document.querySelector('#orion').addEventListener('click', () => {
|
|
121
|
-
browser.
|
|
122
|
-
lookAt:
|
|
123
|
-
durationSecs: 3,
|
|
141
|
+
browser.viewer.actions.invoke(SKYKIT_ACTIONS.navigation.transitionTo, {
|
|
142
|
+
view: { lookAt: alnilam },
|
|
143
|
+
movement: { durationSecs: 3 },
|
|
124
144
|
});
|
|
125
145
|
});
|
|
126
146
|
</script>
|
|
@@ -190,25 +210,11 @@ const stars = await loadStarRows({
|
|
|
190
210
|
});
|
|
191
211
|
```
|
|
192
212
|
|
|
193
|
-
##
|
|
194
|
-
|
|
195
|
-
Use `story.js` when the page is an authored article or tour:
|
|
196
|
-
|
|
197
|
-
```html
|
|
198
|
-
<div data-skykit-story style="height:600px;background:#02040b">
|
|
199
|
-
<section data-skykit-chapter data-title="The Sun" data-target-pc="0,0,0">
|
|
200
|
-
We start at the Sun.
|
|
201
|
-
</section>
|
|
202
|
-
<section data-skykit-chapter data-title="The Hyades" data-target-pc="17.574,42.316,13.963">
|
|
203
|
-
Now jump to the Hyades cluster.
|
|
204
|
-
</section>
|
|
205
|
-
</div>
|
|
213
|
+
## Author Chapters
|
|
206
214
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
></script>
|
|
211
|
-
```
|
|
215
|
+
Keep named chapters in the website or lesson script. Each chapter can call
|
|
216
|
+
navigation actions such as `skykit:navigation.transitionTo` and
|
|
217
|
+
`skykit:navigation.orbit` from its own `goTo(id)` dispatcher.
|
|
212
218
|
|
|
213
219
|
Use the lower-level factories when a lesson is teaching composition or replacing
|
|
214
220
|
a part of the stack:
|
|
@@ -323,7 +329,7 @@ names, not renderer or loader factory names:
|
|
|
323
329
|
SKYKIT_ACTIONS.ship.moveForward; // "skykit:ship.move.forward"
|
|
324
330
|
SKYKIT_CONTROLS.observer.parallaxOffset; // "skykit:observer.control.parallaxOffset"
|
|
325
331
|
SKYKIT_ACTIONS.viewer.reset; // "skykit:viewer.reset"
|
|
326
|
-
SKYKIT_ACTIONS.
|
|
332
|
+
SKYKIT_ACTIONS.navigation.transitionTo; // "skykit:navigation.transitionTo"
|
|
327
333
|
```
|
|
328
334
|
|
|
329
335
|
Plugins can add their own namespaces:
|
|
@@ -336,13 +342,13 @@ const firePlugin = (ctx) => {
|
|
|
336
342
|
};
|
|
337
343
|
```
|
|
338
344
|
|
|
339
|
-
DOM buttons, touch surfaces, keyboard bindings, XR controls,
|
|
340
|
-
tools can all call the same action:
|
|
345
|
+
DOM buttons, touch surfaces, keyboard bindings, XR controls, app-owned chapters,
|
|
346
|
+
and debug tools can all call the same action:
|
|
341
347
|
|
|
342
348
|
```js
|
|
343
349
|
button.addEventListener('click', () => {
|
|
344
|
-
viewer.actions.invoke(
|
|
345
|
-
|
|
350
|
+
viewer.actions.invoke('website:chapter.goTo', {
|
|
351
|
+
id: 'hyades-arrival',
|
|
346
352
|
});
|
|
347
353
|
});
|
|
348
354
|
```
|
|
@@ -397,8 +403,7 @@ Skykit.registerBrowserAddon({
|
|
|
397
403
|
```
|
|
398
404
|
|
|
399
405
|
See `docs/skykit-browser-plugins.md` for the browser add-on spec,
|
|
400
|
-
`Skykit.whenReady()`, first-party constellation support
|
|
401
|
-
`browser.journey` API.
|
|
406
|
+
`Skykit.whenReady()`, and first-party constellation support.
|
|
402
407
|
|
|
403
408
|
Browser lessons:
|
|
404
409
|
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import {
|
|
25
25
|
applySkykitXrDepthRange,
|
|
26
26
|
computeSkykitXrDepthRange,
|
|
27
|
+
createSkykitXrBodyPlugin,
|
|
27
28
|
createSkykitXrControlBindings,
|
|
28
29
|
createSkykitXrNavigationPlugin,
|
|
29
30
|
createSkykitXrObserverRig,
|
|
@@ -169,6 +170,7 @@ async function main() {
|
|
|
169
170
|
let cachedPanelRevision = -1;
|
|
170
171
|
let cachedPanelRoot = null;
|
|
171
172
|
let latestPanelFrame = null;
|
|
173
|
+
let leftHandPanelTracked = false;
|
|
172
174
|
let activeXrHandle = null;
|
|
173
175
|
let artController = null;
|
|
174
176
|
let preflightController = null;
|
|
@@ -207,7 +209,7 @@ async function main() {
|
|
|
207
209
|
touchPanel = createTouchOsPanelPlugin({
|
|
208
210
|
id: 'xr-free-roam-touch-panel',
|
|
209
211
|
priority: 20,
|
|
210
|
-
driver: '
|
|
212
|
+
driver: 'scene',
|
|
211
213
|
root: createPanelRoot,
|
|
212
214
|
surfaceMetrics: XR_PANEL_SURFACE,
|
|
213
215
|
runtimeOptions: {
|
|
@@ -215,18 +217,23 @@ async function main() {
|
|
|
215
217
|
longPressDelay: 360,
|
|
216
218
|
},
|
|
217
219
|
pointerSources: [touchPointerSource],
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return resolveLeftHandPanelPose(frame, xrRig.xrOrigin);
|
|
220
|
+
parent() {
|
|
221
|
+
return xrRig.leftHandRoot;
|
|
221
222
|
},
|
|
222
223
|
driverOptions: {
|
|
223
224
|
panelWidth: 0.32,
|
|
224
225
|
panelHeight: 0.44,
|
|
225
|
-
offset: { x: 0.04, y: 0.02, z: -0.08 },
|
|
226
|
-
tiltRadians: -0.22,
|
|
227
226
|
transparent: true,
|
|
228
227
|
depthTest: false,
|
|
229
228
|
renderOrder: 50,
|
|
229
|
+
updatePlacement(mesh) {
|
|
230
|
+
if (!leftHandPanelTracked) return false;
|
|
231
|
+
applyLocalTabletPlacement(mesh, {
|
|
232
|
+
offset: { x: 0.04, y: 0.02, z: -0.08 },
|
|
233
|
+
tiltRadians: -0.22,
|
|
234
|
+
});
|
|
235
|
+
return true;
|
|
236
|
+
},
|
|
230
237
|
},
|
|
231
238
|
});
|
|
232
239
|
|
|
@@ -271,6 +278,12 @@ async function main() {
|
|
|
271
278
|
createStreamingStarsPlugin({ id: 'xr-stars', source, renderer: starField }),
|
|
272
279
|
hrDiagram,
|
|
273
280
|
...(artPlugin ? [artPlugin] : []),
|
|
281
|
+
createSkykitXrBodyPlugin({
|
|
282
|
+
rig: xrRig,
|
|
283
|
+
onBody(body) {
|
|
284
|
+
leftHandPanelTracked = Boolean(body.leftHand?.grip ?? body.leftHand?.targetRay);
|
|
285
|
+
},
|
|
286
|
+
}),
|
|
274
287
|
createXrFreeRoamFrameSyncPlugin({
|
|
275
288
|
update() {
|
|
276
289
|
selectedTarget.update(camera);
|
|
@@ -389,7 +402,8 @@ async function main() {
|
|
|
389
402
|
});
|
|
390
403
|
}
|
|
391
404
|
|
|
392
|
-
function createPanelRoot() {
|
|
405
|
+
function createPanelRoot(rootContext) {
|
|
406
|
+
latestPanelFrame = rootContext?.frame ?? latestPanelFrame;
|
|
393
407
|
if (cachedPanelRoot && cachedPanelRevision === panelRevision) return cachedPanelRoot;
|
|
394
408
|
cachedPanelRevision = panelRevision;
|
|
395
409
|
cachedPanelRoot = createSkykitTabletRoot({
|
|
@@ -1023,57 +1037,17 @@ function createWorldXrRaySource(source, transformRoot) {
|
|
|
1023
1037
|
};
|
|
1024
1038
|
}
|
|
1025
1039
|
|
|
1026
|
-
function
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
const xr = frame.xr;
|
|
1034
|
-
const xrFrame = xr?.frame;
|
|
1035
|
-
const referenceSpace = xr?.referenceSpace;
|
|
1036
|
-
const inputSources = xr?.session && typeof xr.session === 'object'
|
|
1037
|
-
? xr.session.inputSources ?? []
|
|
1038
|
-
: [];
|
|
1039
|
-
if (!xrFrame || !referenceSpace || typeof xrFrame.getPose !== 'function') return null;
|
|
1040
|
-
for (const inputSource of inputSources) {
|
|
1041
|
-
if (inputSource?.handedness !== handedness || !inputSource[spaceKey]) continue;
|
|
1042
|
-
const pose = xrFrame.getPose(inputSource[spaceKey], referenceSpace);
|
|
1043
|
-
const transform = pose?.transform;
|
|
1044
|
-
if (!transform) continue;
|
|
1045
|
-
return {
|
|
1046
|
-
position: {
|
|
1047
|
-
x: Number(transform.position?.x ?? 0),
|
|
1048
|
-
y: Number(transform.position?.y ?? 0),
|
|
1049
|
-
z: Number(transform.position?.z ?? 0),
|
|
1050
|
-
},
|
|
1051
|
-
orientation: {
|
|
1052
|
-
x: Number(transform.orientation?.x ?? 0),
|
|
1053
|
-
y: Number(transform.orientation?.y ?? 0),
|
|
1054
|
-
z: Number(transform.orientation?.z ?? 0),
|
|
1055
|
-
w: Number(transform.orientation?.w ?? 1),
|
|
1056
|
-
},
|
|
1057
|
-
};
|
|
1040
|
+
function applyLocalTabletPlacement(mesh, options = {}) {
|
|
1041
|
+
const offset = options.offset ?? {};
|
|
1042
|
+
mesh.position.set(0, 0, 0);
|
|
1043
|
+
mesh.quaternion.identity();
|
|
1044
|
+
mesh.scale.set(1, 1, 1);
|
|
1045
|
+
if (Number.isFinite(options.tiltRadians)) {
|
|
1046
|
+
mesh.rotateX(options.tiltRadians);
|
|
1058
1047
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
function transformPoseByObject(pose, object) {
|
|
1063
|
-
object.updateMatrixWorld(true);
|
|
1064
|
-
const position = new THREE.Vector3(pose.position.x, pose.position.y, pose.position.z)
|
|
1065
|
-
.applyMatrix4(object.matrixWorld);
|
|
1066
|
-
const objectQuaternion = object.getWorldQuaternion(new THREE.Quaternion());
|
|
1067
|
-
const orientation = new THREE.Quaternion(
|
|
1068
|
-
pose.orientation.x,
|
|
1069
|
-
pose.orientation.y,
|
|
1070
|
-
pose.orientation.z,
|
|
1071
|
-
pose.orientation.w,
|
|
1072
|
-
).premultiply(objectQuaternion).normalize();
|
|
1073
|
-
return {
|
|
1074
|
-
position: { x: position.x, y: position.y, z: position.z },
|
|
1075
|
-
orientation: { x: orientation.x, y: orientation.y, z: orientation.z, w: orientation.w },
|
|
1076
|
-
};
|
|
1048
|
+
mesh.translateX(offset.x ?? 0);
|
|
1049
|
+
mesh.translateY(offset.y ?? 0);
|
|
1050
|
+
mesh.translateZ(offset.z ?? 0);
|
|
1077
1051
|
}
|
|
1078
1052
|
|
|
1079
1053
|
function createSelectedStarTarget() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@found-in-space/skykit",
|
|
3
|
-
"version": "0.2.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Slim composition and teaching layer for Found in Space packages",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,10 +31,6 @@
|
|
|
31
31
|
"types": "./src/browser-constellations.d.ts",
|
|
32
32
|
"default": "./src/browser-constellations.js"
|
|
33
33
|
},
|
|
34
|
-
"./browser-journey": {
|
|
35
|
-
"types": "./src/browser-journey.d.ts",
|
|
36
|
-
"default": "./src/browser-journey.js"
|
|
37
|
-
},
|
|
38
34
|
"./embed": {
|
|
39
35
|
"types": "./src/embed.d.ts",
|
|
40
36
|
"default": "./src/embed.js"
|
|
@@ -47,10 +43,6 @@
|
|
|
47
43
|
"types": "./src/data.d.ts",
|
|
48
44
|
"default": "./src/data.js"
|
|
49
45
|
},
|
|
50
|
-
"./story": {
|
|
51
|
-
"types": "./src/story.d.ts",
|
|
52
|
-
"default": "./src/story.js"
|
|
53
|
-
},
|
|
54
46
|
"./parallax": {
|
|
55
47
|
"types": "./src/parallax.d.ts",
|
|
56
48
|
"default": "./src/parallax.js"
|
|
@@ -70,28 +62,26 @@
|
|
|
70
62
|
"README.md"
|
|
71
63
|
],
|
|
72
64
|
"sideEffects": [
|
|
73
|
-
"./src/embed.js"
|
|
74
|
-
"./src/story.js"
|
|
65
|
+
"./src/embed.js"
|
|
75
66
|
],
|
|
76
67
|
"scripts": {
|
|
77
68
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
78
69
|
"test": "node --test"
|
|
79
70
|
},
|
|
80
71
|
"dependencies": {
|
|
81
|
-
"@found-in-space/anchored-image": "0.2.0
|
|
82
|
-
"@found-in-space/hr-diagram": "0.2.0
|
|
83
|
-
"@found-in-space/
|
|
84
|
-
"@found-in-space/
|
|
85
|
-
"@found-in-space/
|
|
86
|
-
"@found-in-space/star-
|
|
87
|
-
"@found-in-space/star-
|
|
88
|
-
"@found-in-space/three-star-field": "0.2.0-alpha.0"
|
|
72
|
+
"@found-in-space/anchored-image": "0.2.0",
|
|
73
|
+
"@found-in-space/hr-diagram": "0.2.0",
|
|
74
|
+
"@found-in-space/meta-sidecar-provider": "0.2.0",
|
|
75
|
+
"@found-in-space/spatial": "0.2.0",
|
|
76
|
+
"@found-in-space/star-octree-provider": "0.2.0",
|
|
77
|
+
"@found-in-space/star-trees": "0.2.0",
|
|
78
|
+
"@found-in-space/three-star-field": "0.2.0"
|
|
89
79
|
},
|
|
90
80
|
"devDependencies": {
|
|
91
|
-
"@found-in-space/touch-os": "0.2.0
|
|
81
|
+
"@found-in-space/touch-os": "0.2.0"
|
|
92
82
|
},
|
|
93
83
|
"peerDependencies": {
|
|
94
|
-
"@found-in-space/touch-os": ">=0.2.0
|
|
84
|
+
"@found-in-space/touch-os": ">=0.2.0 <1",
|
|
95
85
|
"three": "^0.170.0"
|
|
96
86
|
},
|
|
97
87
|
"peerDependenciesMeta": {
|
|
@@ -137,6 +137,35 @@ test('createSkykitBrowser accepts startup lookAt and mouse look mode', async ()
|
|
|
137
137
|
});
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
test('createSkykitBrowser starts from observer and solar RA/Dec distance lookAt', async () => {
|
|
141
|
+
await withFakeWindow(async () => {
|
|
142
|
+
const browser = await createSkykitBrowser({
|
|
143
|
+
host: createHost(),
|
|
144
|
+
status: false,
|
|
145
|
+
renderer: createRenderer(),
|
|
146
|
+
provider: createProvider(),
|
|
147
|
+
starField: createStarField(),
|
|
148
|
+
autoResize: false,
|
|
149
|
+
autoDispose: false,
|
|
150
|
+
autoStart: false,
|
|
151
|
+
lookAt: { raDeg: 90, decDeg: 0, distancePc: 10 },
|
|
152
|
+
view: {
|
|
153
|
+
observerPc: { x: 1, y: 0, z: 0 },
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const view = browser.viewer.getViewState();
|
|
158
|
+
assert.deepEqual(view.observerPc, { x: 1, y: 0, z: 0 });
|
|
159
|
+
assertVectorApprox(view.targetPc, { x: 0, y: 10, z: 0 });
|
|
160
|
+
assertVectorApprox(
|
|
161
|
+
localVectorFromView(view, { x: 0, y: 0, z: -1 }),
|
|
162
|
+
normalizeVector({ x: -1, y: 10, z: 0 }),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
await browser.dispose();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
140
169
|
test('createSkykitBrowser can disable mouse drag controls', async () => {
|
|
141
170
|
await withFakeWindow(async () => {
|
|
142
171
|
const browser = await createSkykitBrowser({
|
|
@@ -160,6 +189,56 @@ test('createSkykitBrowser can disable mouse drag controls', async () => {
|
|
|
160
189
|
});
|
|
161
190
|
});
|
|
162
191
|
|
|
192
|
+
test('createSkykitBrowser enables persistent provider cache by default and can opt out', async () => {
|
|
193
|
+
await withFakeWindow(async () => {
|
|
194
|
+
await withFakeCaches(async () => {
|
|
195
|
+
const cached = await createSkykitBrowser({
|
|
196
|
+
host: createHost(),
|
|
197
|
+
status: false,
|
|
198
|
+
renderer: createRenderer(),
|
|
199
|
+
starField: createStarField(),
|
|
200
|
+
autoResize: false,
|
|
201
|
+
autoDispose: false,
|
|
202
|
+
autoStart: false,
|
|
203
|
+
});
|
|
204
|
+
assert.equal(cached.provider.describe().capabilities.persistentCache, true);
|
|
205
|
+
await cached.dispose();
|
|
206
|
+
|
|
207
|
+
const uncached = await createSkykitBrowser({
|
|
208
|
+
host: createHost(),
|
|
209
|
+
status: false,
|
|
210
|
+
renderer: createRenderer(),
|
|
211
|
+
starField: createStarField(),
|
|
212
|
+
persistentCache: 'off',
|
|
213
|
+
autoResize: false,
|
|
214
|
+
autoDispose: false,
|
|
215
|
+
autoStart: false,
|
|
216
|
+
});
|
|
217
|
+
assert.equal(uncached.provider.describe().capabilities.persistentCache, false);
|
|
218
|
+
await uncached.dispose();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('createSkykitBrowser treats forbidden Cache API access as disabled', async () => {
|
|
224
|
+
await withFakeWindow(async () => {
|
|
225
|
+
await withForbiddenCaches(async () => {
|
|
226
|
+
const browser = await createSkykitBrowser({
|
|
227
|
+
host: createHost(),
|
|
228
|
+
status: false,
|
|
229
|
+
renderer: createRenderer(),
|
|
230
|
+
starField: createStarField(),
|
|
231
|
+
autoResize: false,
|
|
232
|
+
autoDispose: false,
|
|
233
|
+
autoStart: false,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
assert.equal(browser.provider.describe().capabilities.persistentCache, false);
|
|
237
|
+
await browser.dispose();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
163
242
|
test('browser.install adds plugins after startup and cleans returned teardowns', async () => {
|
|
164
243
|
await withFakeWindow(async () => {
|
|
165
244
|
const calls = [];
|
|
@@ -193,46 +272,6 @@ test('browser.install adds plugins after startup and cleans returned teardowns',
|
|
|
193
272
|
});
|
|
194
273
|
});
|
|
195
274
|
|
|
196
|
-
test('browser journey capability transitions through navigation actions and loads instances', async () => {
|
|
197
|
-
await withFakeWindow(async () => {
|
|
198
|
-
const browser = await createSkykitBrowser({
|
|
199
|
-
host: createHost(),
|
|
200
|
-
status: false,
|
|
201
|
-
renderer: createRenderer(),
|
|
202
|
-
provider: createProvider(),
|
|
203
|
-
starField: createStarField(),
|
|
204
|
-
autoResize: false,
|
|
205
|
-
autoDispose: false,
|
|
206
|
-
autoStart: false,
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
await browser.journey.transitionTo({
|
|
210
|
-
lookAt: { targetPc: { x: 10, y: 0, z: 0 } },
|
|
211
|
-
durationSecs: 1,
|
|
212
|
-
});
|
|
213
|
-
browser.viewer.update(1);
|
|
214
|
-
browser.viewer.update(0);
|
|
215
|
-
assert.ok(browser.viewer.getViewState().orientationIcrs);
|
|
216
|
-
assert.equal(browser.capabilities.has('skykit:navigation'), true);
|
|
217
|
-
|
|
218
|
-
const journey = await browser.journey.load({
|
|
219
|
-
initial: 'home',
|
|
220
|
-
scenes: {
|
|
221
|
-
home: { view: { observerPc: { x: 0, y: 0, z: 0 } } },
|
|
222
|
-
away: { view: { observerPc: { x: 1, y: 2, z: 3 } } },
|
|
223
|
-
},
|
|
224
|
-
});
|
|
225
|
-
await journey.goTo('away');
|
|
226
|
-
browser.viewer.update(0);
|
|
227
|
-
assert.deepEqual(browser.viewer.getViewState().observerPc, { x: 1, y: 2, z: 3 });
|
|
228
|
-
assert.equal(journey.getSnapshot().disposed, false);
|
|
229
|
-
journey.dispose();
|
|
230
|
-
assert.equal(journey.getSnapshot().disposed, true);
|
|
231
|
-
|
|
232
|
-
await browser.dispose();
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
275
|
test('browser constellations capability loads manifest boundaries without art', async () => {
|
|
237
276
|
await withFakeWindow(async () => {
|
|
238
277
|
const browser = await createSkykitBrowser({
|
|
@@ -338,6 +377,88 @@ async function withFakeWindow(callback) {
|
|
|
338
377
|
}
|
|
339
378
|
}
|
|
340
379
|
|
|
380
|
+
async function withFakeCaches(callback) {
|
|
381
|
+
const previousCaches = Object.getOwnPropertyDescriptor(globalThis, 'caches');
|
|
382
|
+
const previousFetch = globalThis.fetch;
|
|
383
|
+
const cache = {
|
|
384
|
+
async match() {
|
|
385
|
+
return null;
|
|
386
|
+
},
|
|
387
|
+
async put() {},
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
Object.defineProperty(globalThis, 'caches', {
|
|
391
|
+
configurable: true,
|
|
392
|
+
value: {
|
|
393
|
+
async open() {
|
|
394
|
+
return cache;
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
399
|
+
configurable: true,
|
|
400
|
+
value(_url, options = {}) {
|
|
401
|
+
return new Promise((_resolve, reject) => {
|
|
402
|
+
if (options.signal?.aborted) {
|
|
403
|
+
const error = new Error('Range fetch aborted.');
|
|
404
|
+
error.name = 'AbortError';
|
|
405
|
+
reject(error);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
options.signal?.addEventListener?.('abort', () => {
|
|
409
|
+
const error = new Error('Range fetch aborted.');
|
|
410
|
+
error.name = 'AbortError';
|
|
411
|
+
reject(error);
|
|
412
|
+
}, { once: true });
|
|
413
|
+
});
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
await callback();
|
|
419
|
+
} finally {
|
|
420
|
+
restoreGlobalProperty('caches', previousCaches);
|
|
421
|
+
if (previousFetch === undefined) {
|
|
422
|
+
delete globalThis.fetch;
|
|
423
|
+
} else {
|
|
424
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
425
|
+
configurable: true,
|
|
426
|
+
value: previousFetch,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function withForbiddenCaches(callback) {
|
|
433
|
+
const previousCaches = Object.getOwnPropertyDescriptor(globalThis, 'caches');
|
|
434
|
+
const previousWarn = console.warn;
|
|
435
|
+
|
|
436
|
+
Object.defineProperty(globalThis, 'caches', {
|
|
437
|
+
configurable: true,
|
|
438
|
+
get() {
|
|
439
|
+
const error = new Error('Cache API storage is blocked.');
|
|
440
|
+
error.name = 'SecurityError';
|
|
441
|
+
throw error;
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
console.warn = () => {};
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
await callback();
|
|
448
|
+
} finally {
|
|
449
|
+
console.warn = previousWarn;
|
|
450
|
+
restoreGlobalProperty('caches', previousCaches);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function restoreGlobalProperty(name, descriptor) {
|
|
455
|
+
if (descriptor) {
|
|
456
|
+
Object.defineProperty(globalThis, name, descriptor);
|
|
457
|
+
} else {
|
|
458
|
+
delete globalThis[name];
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
341
462
|
function createHost() {
|
|
342
463
|
return {
|
|
343
464
|
children: [],
|
|
@@ -440,3 +561,26 @@ function createStarField() {
|
|
|
440
561
|
},
|
|
441
562
|
};
|
|
442
563
|
}
|
|
564
|
+
|
|
565
|
+
function localVectorFromView(view, vector) {
|
|
566
|
+
const q = view.orientationIcrs ?? { x: 0, y: 0, z: 0, w: 1 };
|
|
567
|
+
const result = new THREE.Vector3(vector.x, vector.y, vector.z).applyQuaternion(
|
|
568
|
+
new THREE.Quaternion(q.x, q.y, q.z, q.w),
|
|
569
|
+
);
|
|
570
|
+
return { x: result.x, y: result.y, z: result.z };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function assertVectorApprox(actual, expected, epsilon = 1e-9) {
|
|
574
|
+
assert.ok(Math.abs(actual.x - expected.x) < epsilon, `x ${actual.x} !== ${expected.x}`);
|
|
575
|
+
assert.ok(Math.abs(actual.y - expected.y) < epsilon, `y ${actual.y} !== ${expected.y}`);
|
|
576
|
+
assert.ok(Math.abs(actual.z - expected.z) < epsilon, `z ${actual.z} !== ${expected.z}`);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function normalizeVector(vector) {
|
|
580
|
+
const length = Math.hypot(vector.x, vector.y, vector.z);
|
|
581
|
+
return {
|
|
582
|
+
x: vector.x / length,
|
|
583
|
+
y: vector.y / length,
|
|
584
|
+
z: vector.z / length,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
@@ -6,6 +6,7 @@ import * as THREE from 'three';
|
|
|
6
6
|
import {
|
|
7
7
|
applySkykitXrDepthRange,
|
|
8
8
|
computeSkykitXrDepthRange,
|
|
9
|
+
createSkykitXrBodyPlugin,
|
|
9
10
|
createSkykitXrBodyTracker,
|
|
10
11
|
createSkykitXrControlBindings,
|
|
11
12
|
createSkykitXrNavigationPlugin,
|
|
@@ -28,6 +29,7 @@ test('xr free-roam demo uses restored alpha XR regressions defaults', () => {
|
|
|
28
29
|
assert.match(source, /createDefaultThreeStarFieldMaterialProfile/);
|
|
29
30
|
assert.doesNotMatch(source, /createVrThreeStarFieldMaterialProfile/);
|
|
30
31
|
assert.match(source, /createSkykitXrRayVisualPlugin/);
|
|
32
|
+
assert.match(source, /createSkykitXrBodyPlugin/);
|
|
31
33
|
assert.match(source, /createSurfaceShell/);
|
|
32
34
|
assert.match(source, /createMetaSidecarProviderService/);
|
|
33
35
|
assert.match(source, /deriveMetaSidecarUrlFromRenderUrl/);
|
|
@@ -39,7 +41,12 @@ test('xr free-roam demo uses restored alpha XR regressions defaults', () => {
|
|
|
39
41
|
assert.match(source, /primaryActionLabel:\s*'Fly to'/);
|
|
40
42
|
assert.match(source, /homeControl:\s*'button'/);
|
|
41
43
|
assert.match(source, /pointerType:\s*'ray'/);
|
|
44
|
+
assert.match(source, /return xrRig\.leftHandRoot/);
|
|
45
|
+
assert.match(source, /latestPanelFrame = rootContext\?\.frame \?\? latestPanelFrame/);
|
|
42
46
|
assert.doesNotMatch(source, /dragThreshold/);
|
|
47
|
+
assert.doesNotMatch(source, /driver:\s*'pose-anchored'/);
|
|
48
|
+
assert.doesNotMatch(source, /anchorPose/);
|
|
49
|
+
assert.doesNotMatch(source, /latestPanelFrame = frame/);
|
|
43
50
|
assert.doesNotMatch(source, /createChoiceGroup/);
|
|
44
51
|
assert.doesNotMatch(source, /createSlider/);
|
|
45
52
|
assert.doesNotMatch(source, /createToggle/);
|
|
@@ -112,6 +119,55 @@ test('skykit/xr body, rays, and pick router compose generic route results', () =
|
|
|
112
119
|
assert.equal(route.hit.object, 'target');
|
|
113
120
|
});
|
|
114
121
|
|
|
122
|
+
test('skykit/xr body plugin drives tracked hand roots before dependent parts', () => {
|
|
123
|
+
const rig = createSkykitXrRig();
|
|
124
|
+
const leftGripSpace = {};
|
|
125
|
+
const referenceSpace = {};
|
|
126
|
+
const bodyUpdates = [];
|
|
127
|
+
let part = null;
|
|
128
|
+
const plugin = createSkykitXrBodyPlugin({
|
|
129
|
+
rig,
|
|
130
|
+
onBody(body) {
|
|
131
|
+
bodyUpdates.push(body);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
plugin.setup(createPluginContext({
|
|
135
|
+
addPart(nextPart) {
|
|
136
|
+
part = nextPart;
|
|
137
|
+
},
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
part.update(createXrFrame({
|
|
141
|
+
referenceSpace,
|
|
142
|
+
inputSources: [{
|
|
143
|
+
handedness: 'left',
|
|
144
|
+
gripSpace: leftGripSpace,
|
|
145
|
+
gamepad: {
|
|
146
|
+
buttons: [{ pressed: false, touched: false, value: 0 }],
|
|
147
|
+
axes: [0, 0],
|
|
148
|
+
},
|
|
149
|
+
}],
|
|
150
|
+
xrFrame: {
|
|
151
|
+
getPose(space, ref) {
|
|
152
|
+
assert.equal(ref, referenceSpace);
|
|
153
|
+
if (space !== leftGripSpace) return null;
|
|
154
|
+
return {
|
|
155
|
+
transform: {
|
|
156
|
+
position: { x: 0.1, y: 0.2, z: 0.3 },
|
|
157
|
+
orientation: { x: 0, y: 0, z: 0, w: 1 },
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
assert.deepEqual(rig.leftHandRoot.position.toArray(), [0.1, 0.2, 0.3]);
|
|
165
|
+
assert.equal(rig.leftHandRoot.visible, true);
|
|
166
|
+
assert.equal(rig.rightHandRoot.visible, false);
|
|
167
|
+
assert.equal(plugin.getBody().leftHand?.buttons, 1);
|
|
168
|
+
assert.equal(bodyUpdates.length, 1);
|
|
169
|
+
});
|
|
170
|
+
|
|
115
171
|
test('skykit/xr depth helpers compute and apply render state', () => {
|
|
116
172
|
const range = computeSkykitXrDepthRange({
|
|
117
173
|
visibleBounds: { min: { x: -1, y: -1, z: -20 }, max: { x: 1, y: 1, z: -10 } },
|