@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.
Files changed (40) hide show
  1. package/README.md +142 -11
  2. package/examples/custom-object-layer/custom-object-layer.js +1 -24
  3. package/examples/xr-free-roam/index.html +62 -4
  4. package/examples/xr-free-roam/xr-free-roam.css +249 -18
  5. package/examples/xr-free-roam/xr-free-roam.js +675 -274
  6. package/package.json +22 -6
  7. package/src/__tests__/skykit-anchored-images.test.js +32 -4
  8. package/src/__tests__/skykit-browser.test.js +267 -0
  9. package/src/__tests__/skykit-data.test.js +131 -0
  10. package/src/__tests__/skykit-parallax.test.js +4 -4
  11. package/src/__tests__/skykit-touch-os.test.js +71 -0
  12. package/src/__tests__/skykit-xr.test.js +179 -2
  13. package/src/__tests__/skykit.test.js +142 -506
  14. package/src/actions.js +0 -8
  15. package/src/anchored-images.js +14 -15
  16. package/src/browser-addons.d.ts +16 -0
  17. package/src/browser-addons.js +155 -0
  18. package/src/browser-constellations.d.ts +13 -0
  19. package/src/browser-constellations.js +387 -0
  20. package/src/browser.d.ts +81 -0
  21. package/src/browser.js +192 -13
  22. package/src/data.d.ts +133 -0
  23. package/src/data.js +447 -0
  24. package/src/embed.d.ts +5 -0
  25. package/src/embed.js +53 -2
  26. package/src/hr-diagram.js +23 -5
  27. package/src/index.d.ts +21 -73
  28. package/src/index.js +0 -1
  29. package/src/plugins.js +22 -708
  30. package/src/three-shim.d.ts +32 -0
  31. package/src/touch-os.d.ts +70 -0
  32. package/src/touch-os.js +275 -0
  33. package/src/utils.js +96 -6
  34. package/src/viewer-entry.d.ts +10 -0
  35. package/src/viewer-entry.js +4 -0
  36. package/src/viewer.js +110 -12
  37. package/src/xr/plugins.js +298 -13
  38. package/src/xr/session.js +60 -14
  39. package/src/xr.d.ts +40 -0
  40. package/src/xr.js +2 -0
@@ -1,16 +1,19 @@
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
 
5
6
  import {
6
7
  applySkykitXrDepthRange,
7
8
  computeSkykitXrDepthRange,
9
+ createSkykitXrBodyPlugin,
8
10
  createSkykitXrBodyTracker,
9
11
  createSkykitXrControlBindings,
10
12
  createSkykitXrNavigationPlugin,
11
13
  createSkykitXrObserverRig,
12
14
  createSkykitXrPickRouter,
13
15
  createSkykitXrRaySource,
16
+ createSkykitXrRayVisualPlugin,
14
17
  createSkykitXrRig,
15
18
  createSkykitXrSessionPlugin,
16
19
  createSkykitXrStarPickingPlugin,
@@ -20,6 +23,38 @@ import {
20
23
  } from '../xr.js';
21
24
  import { createSkykitActionRegistry } from '../index.js';
22
25
 
26
+ test('xr free-roam demo uses restored alpha XR regressions defaults', () => {
27
+ const source = readFileSync(new URL('../../examples/xr-free-roam/xr-free-roam.js', import.meta.url), 'utf8');
28
+
29
+ assert.match(source, /createDefaultThreeStarFieldMaterialProfile/);
30
+ assert.doesNotMatch(source, /createVrThreeStarFieldMaterialProfile/);
31
+ assert.match(source, /createSkykitXrRayVisualPlugin/);
32
+ assert.match(source, /createSkykitXrBodyPlugin/);
33
+ assert.match(source, /createSurfaceShell/);
34
+ assert.match(source, /createMetaSidecarProviderService/);
35
+ assert.match(source, /deriveMetaSidecarUrlFromRenderUrl/);
36
+ assert.match(source, /metaSidecarEntryDisplayFields/);
37
+ assert.match(source, /datasetId:\s*DATASET_ID_c56103/);
38
+ assert.match(source, /attributes:\s*\[\s*'objectRef'\s*,\s*'pickMeta'\s*\]/);
39
+ assert.match(source, /selectSun:\s*'xr-demo:selected\.sun'/);
40
+ assert.match(source, /primaryActionId:\s*XR_DEMO_ACTIONS\.goSelected/);
41
+ assert.match(source, /primaryActionLabel:\s*'Fly to'/);
42
+ assert.match(source, /homeControl:\s*'button'/);
43
+ assert.match(source, /pointerType:\s*'ray'/);
44
+ assert.match(source, /return xrRig\.leftHandRoot/);
45
+ assert.match(source, /latestPanelFrame = rootContext\?\.frame \?\? latestPanelFrame/);
46
+ assert.doesNotMatch(source, /dragThreshold/);
47
+ assert.doesNotMatch(source, /driver:\s*'pose-anchored'/);
48
+ assert.doesNotMatch(source, /anchorPose/);
49
+ assert.doesNotMatch(source, /latestPanelFrame = frame/);
50
+ assert.doesNotMatch(source, /createChoiceGroup/);
51
+ assert.doesNotMatch(source, /createSlider/);
52
+ assert.doesNotMatch(source, /createToggle/);
53
+ assert.doesNotMatch(source, /createStack/);
54
+ assert.doesNotMatch(source, /waypointPrefix/);
55
+ assert.doesNotMatch(source, /panelState\.page/);
56
+ });
57
+
23
58
  test('skykit/xr rig builds multi-root hierarchy', () => {
24
59
  const camera = new THREE.PerspectiveCamera();
25
60
  const rig = createSkykitXrRig({
@@ -84,6 +119,55 @@ test('skykit/xr body, rays, and pick router compose generic route results', () =
84
119
  assert.equal(route.hit.object, 'target');
85
120
  });
86
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
+
87
171
  test('skykit/xr depth helpers compute and apply render state', () => {
88
172
  const range = computeSkykitXrDepthRange({
89
173
  visibleBounds: { min: { x: -1, y: -1, z: -20 }, max: { x: 1, y: 1, z: -10 } },
@@ -102,6 +186,32 @@ test('skykit/xr depth helpers compute and apply render state', () => {
102
186
  assert.deepEqual(state, { depthNear: range.depthNear, depthFar: range.depthFar });
103
187
  });
104
188
 
189
+ test('skykit/xr depth helpers include distant visible star bounds', () => {
190
+ const range = computeSkykitXrDepthRange({
191
+ observer: { x: 0, y: 0, z: 0 },
192
+ visibleBounds: {
193
+ min: { x: 62, y: 602, z: -13 },
194
+ max: { x: 64, y: 604, z: -11 },
195
+ },
196
+ observerCentricSpheres: [{ radiusNavigationUnits: 16 }],
197
+ scale: {
198
+ navigationUnits: 'pc',
199
+ metersPerNavigationUnit: 1,
200
+ worldUnitsPerNavigationUnit: 1,
201
+ },
202
+ policy: {
203
+ near: 0.03,
204
+ minFar: 100,
205
+ maxFar: 2000000,
206
+ marginFactor: 1.2,
207
+ },
208
+ });
209
+
210
+ assert.ok(range.far > 720);
211
+ assert.ok(range.telemetry.farthestVisibleBoundsDistance > 600);
212
+ assert.equal(range.telemetry.farthestObserverCentricSphereDistance, 16);
213
+ });
214
+
105
215
  test('skykit/xr session helpers use injected navigator', async () => {
106
216
  let ended = false;
107
217
  const session = {
@@ -118,7 +228,8 @@ test('skykit/xr session helpers use injected navigator', async () => {
118
228
  async isSessionSupported(mode) {
119
229
  return mode === 'immersive-vr';
120
230
  },
121
- async requestSession() {
231
+ async requestSession(_mode, init) {
232
+ this.lastInit = init;
122
233
  return session;
123
234
  },
124
235
  },
@@ -126,6 +237,7 @@ test('skykit/xr session helpers use injected navigator', async () => {
126
237
  assert.equal(await isSkykitXrModeSupported('immersive-vr', { navigator }), true);
127
238
  const handle = await enterSkykitXrSession({ navigator, mode: 'immersive-vr' });
128
239
  assert.equal(handle.presenting, true);
240
+ assert.deepEqual(navigator.xr.lastInit.optionalFeatures, ['local-floor']);
129
241
  await exitSkykitXrSession(handle);
130
242
  assert.equal(ended, true);
131
243
  });
@@ -147,15 +259,20 @@ test('skykit/xr observer rig bridges viewer state to an XR rig without camera re
147
259
 
148
260
  test('skykit/xr session plugin registers enter/exit actions and syncs snapshot state', async () => {
149
261
  let activeSession = null;
262
+ let requestedReferenceSpaceCount = 0;
150
263
  const session = {
151
264
  async requestReferenceSpace(type) {
265
+ requestedReferenceSpaceCount += 1;
152
266
  return { type };
153
267
  },
154
268
  async end() {
155
269
  this.ended = true;
270
+ activeSession = null;
271
+ renderer.xr.isPresenting = false;
156
272
  },
157
273
  addEventListener() {},
158
274
  };
275
+ let rendererReferenceSpaceType = null;
159
276
  const renderer = {
160
277
  xr: {
161
278
  enabled: false,
@@ -163,6 +280,9 @@ test('skykit/xr session plugin registers enter/exit actions and syncs snapshot s
163
280
  getSession() {
164
281
  return activeSession;
165
282
  },
283
+ setReferenceSpaceType(type) {
284
+ rendererReferenceSpaceType = type;
285
+ },
166
286
  async setSession(nextSession) {
167
287
  activeSession = nextSession;
168
288
  this.isPresenting = Boolean(nextSession);
@@ -174,7 +294,8 @@ test('skykit/xr session plugin registers enter/exit actions and syncs snapshot s
174
294
  async isSessionSupported() {
175
295
  return true;
176
296
  },
177
- async requestSession() {
297
+ async requestSession(_mode, init) {
298
+ this.lastInit = init;
178
299
  return session;
179
300
  },
180
301
  },
@@ -195,8 +316,12 @@ test('skykit/xr session plugin registers enter/exit actions and syncs snapshot s
195
316
 
196
317
  await actions.invoke('skykit:xr.enter');
197
318
  assert.equal(renderer.xr.enabled, true);
319
+ assert.equal(rendererReferenceSpaceType, 'local-floor');
320
+ assert.deepEqual(navigator.xr.lastInit.optionalFeatures, ['local-floor']);
321
+ assert.equal(requestedReferenceSpaceCount, 0);
198
322
  assert.equal(activeSession, session);
199
323
  assert.equal(plugin.getSnapshot().presenting, true);
324
+ assert.equal(plugin.getSnapshot().enterStage, 'presenting');
200
325
 
201
326
  const frame = createXrFrame({ renderer });
202
327
  part.update(frame);
@@ -240,6 +365,58 @@ test('skykit/xr navigation plugin updates viewer state from controller axes', ()
240
365
  assert.deepEqual(actions.getControlValue('skykit:ship.control.move'), { x: 0, y: 0, z: -10 });
241
366
  });
242
367
 
368
+ test('skykit/xr ray visual shows the controller ray and shortens at blockers', () => {
369
+ let part = null;
370
+ let disposedRaySource = false;
371
+ const plugin = createSkykitXrRayVisualPlugin({
372
+ raySource: {
373
+ getRay() {
374
+ return {
375
+ id: 'ray',
376
+ kind: 'target-ray',
377
+ handedness: 'right',
378
+ origin: { x: 1, y: 2, z: 3 },
379
+ direction: { x: 0, y: 0, z: -1 },
380
+ length: 10,
381
+ };
382
+ },
383
+ getSnapshot() {
384
+ return { id: 'ray-source' };
385
+ },
386
+ dispose() {
387
+ disposedRaySource = true;
388
+ },
389
+ },
390
+ blockers: [{
391
+ blockRay() {
392
+ return { blocked: true, distance: 3, hit: { componentId: 'panel' } };
393
+ },
394
+ }],
395
+ });
396
+ const context = createPluginContext({
397
+ addPart(nextPart) {
398
+ part = nextPart;
399
+ },
400
+ });
401
+ plugin.setup(context);
402
+ part.attach(context);
403
+
404
+ part.update(createXrFrame());
405
+
406
+ assert.equal(part.object3d.visible, true);
407
+ assert.equal(plugin.getSnapshot().blocked, true);
408
+ assert.equal(plugin.getSnapshot().lastLength, 3);
409
+ assert.deepEqual(
410
+ Array.from(part.object3d.children[0].geometry.getAttribute('position').array),
411
+ [1, 2, 3, 1, 2, 0],
412
+ );
413
+
414
+ part.update({ ...createXrFrame(), xr: { presenting: false } });
415
+ assert.equal(part.object3d.visible, false);
416
+ part.dispose();
417
+ assert.equal(disposedRaySource, true);
418
+ });
419
+
243
420
  test('skykit/xr star picking fires only on trigger edge and registers attribute-only demand', () => {
244
421
  const actions = createSkykitActionRegistry();
245
422
  let part = null;