@found-in-space/skykit 0.2.0-alpha.0 → 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 +223 -8
- 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 +46 -5
- package/src/__tests__/skykit-anchored-images.test.js +32 -4
- package/src/__tests__/skykit-browser.test.js +442 -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 +170 -0
- package/src/browser.js +369 -0
- package/src/data.d.ts +133 -0
- package/src/data.js +447 -0
- package/src/embed.d.ts +6 -0
- package/src/embed.js +119 -0
- 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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
formatStarLabel,
|
|
6
|
+
loadStarLabels,
|
|
7
|
+
loadStarRows,
|
|
8
|
+
rowsFromStarCells,
|
|
9
|
+
streamStarRows,
|
|
10
|
+
} from '../data.js';
|
|
11
|
+
|
|
12
|
+
test('rowsFromStarCells converts cells to plain app rows', () => {
|
|
13
|
+
const rows = rowsFromStarCells([createCell()], {
|
|
14
|
+
observerPc: { x: 0, y: 0, z: 0 },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
assert.equal(rows.length, 2);
|
|
18
|
+
assert.deepEqual(rows[0].positionPc, { x: 1, y: 2, z: 2 });
|
|
19
|
+
assert.equal(rows[0].distancePc, 3);
|
|
20
|
+
assert.equal(rows[0].apparentMagnitude, 1 + 5 * (Math.log10(3) - 1));
|
|
21
|
+
assert.equal(rows[0].temperatureK, 5800);
|
|
22
|
+
assert.deepEqual(rows[0].ref, {
|
|
23
|
+
datasetId: 'dataset-a',
|
|
24
|
+
level: 1,
|
|
25
|
+
mortonCode: '2',
|
|
26
|
+
ordinal: 0,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('loadStarRows streams provider cells, filters visible rows, and respects maxStars', async () => {
|
|
31
|
+
const provider = createProvider();
|
|
32
|
+
const rows = await loadStarRows({
|
|
33
|
+
provider,
|
|
34
|
+
limitingMagnitude: 4,
|
|
35
|
+
maxStars: 1,
|
|
36
|
+
sortBy: 'distancePc',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
assert.equal(provider.disposed, false);
|
|
40
|
+
assert.equal(provider.streamOptions.length, 1);
|
|
41
|
+
assert.deepEqual(provider.streamOptions[0].view.observerPc, { x: 0, y: 0, z: 0 });
|
|
42
|
+
assert.deepEqual(provider.streamOptions[0].attributes, ['position', 'magAbs', 'teffLog8', 'objectRef']);
|
|
43
|
+
assert.equal(rows.length, 1);
|
|
44
|
+
assert.equal(rows[0].ordinal, 0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('streamStarRows yields row batches without exposing cell deltas', async () => {
|
|
48
|
+
const batches = [];
|
|
49
|
+
for await (const rows of streamStarRows({
|
|
50
|
+
provider: createProvider(),
|
|
51
|
+
limitingMagnitude: 99,
|
|
52
|
+
})) {
|
|
53
|
+
batches.push(rows);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
assert.equal(batches.length, 1);
|
|
57
|
+
assert.equal(batches[0].length, 2);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('loadStarLabels formats metadata labels for rows', async () => {
|
|
61
|
+
const [row] = rowsFromStarCells([createCell()]);
|
|
62
|
+
const labels = await loadStarLabels([row], {
|
|
63
|
+
metaProvider: {
|
|
64
|
+
async getMeta(ref) {
|
|
65
|
+
assert.equal(ref.ordinal, 0);
|
|
66
|
+
return { proper_name: 'Sol', hip_id: 0 };
|
|
67
|
+
},
|
|
68
|
+
dispose() {
|
|
69
|
+
throw new Error('caller-owned meta providers are not disposed');
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
assert.equal(labels.length, 1);
|
|
75
|
+
assert.equal(labels[0].label, 'Sol');
|
|
76
|
+
assert.equal(formatStarLabel(null, 'Fallback'), 'Fallback');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
function createCell() {
|
|
80
|
+
return {
|
|
81
|
+
cellKey: '1:2',
|
|
82
|
+
cell: { level: 1, mortonCode: '2' },
|
|
83
|
+
bounds: {
|
|
84
|
+
centerPc: { x: 0, y: 0, z: 0 },
|
|
85
|
+
halfSizePc: 1,
|
|
86
|
+
gridX: 0,
|
|
87
|
+
gridY: 0,
|
|
88
|
+
gridZ: 0,
|
|
89
|
+
},
|
|
90
|
+
count: 2,
|
|
91
|
+
coordinates: {
|
|
92
|
+
name: 'position',
|
|
93
|
+
frame: 'icrs',
|
|
94
|
+
units: ['pc', 'pc', 'pc'],
|
|
95
|
+
components: new Float32Array([
|
|
96
|
+
1, 2, 2,
|
|
97
|
+
20, 0, 0,
|
|
98
|
+
]),
|
|
99
|
+
},
|
|
100
|
+
attributes: {
|
|
101
|
+
magAbs: new Float32Array([1, 9]),
|
|
102
|
+
teffLog8: new Uint8Array([255, 0]),
|
|
103
|
+
},
|
|
104
|
+
refs: [
|
|
105
|
+
{ datasetId: 'dataset-a', level: 1, mortonCode: '2', ordinal: 0 },
|
|
106
|
+
{ datasetId: 'dataset-a', level: 1, mortonCode: '2', ordinal: 1 },
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createProvider() {
|
|
112
|
+
return {
|
|
113
|
+
disposed: false,
|
|
114
|
+
streamOptions: [],
|
|
115
|
+
async *streamCells(options) {
|
|
116
|
+
this.streamOptions.push(options);
|
|
117
|
+
yield {
|
|
118
|
+
type: 'stars/cells-upsert',
|
|
119
|
+
cells: [createCell()],
|
|
120
|
+
};
|
|
121
|
+
yield {
|
|
122
|
+
type: 'stars/current',
|
|
123
|
+
cellKeys: ['1:2'],
|
|
124
|
+
starCount: 2,
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
dispose() {
|
|
128
|
+
this.disposed = true;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -185,7 +185,7 @@ test('parallax observer moves in the target-relative plane without accumulating
|
|
|
185
185
|
const viewer = await createSkykitViewer({
|
|
186
186
|
view: {
|
|
187
187
|
observerPc: { x: 0, y: 0, z: 0 },
|
|
188
|
-
targetPc: { x: 0, y: 0, z: -10 },
|
|
188
|
+
lookAt: { targetPc: { x: 0, y: 0, z: -10 } },
|
|
189
189
|
},
|
|
190
190
|
plugins: [
|
|
191
191
|
createParallaxObserverPlugin({
|
|
@@ -223,7 +223,7 @@ test('parallax observer smoothing approaches the requested offset', async () =>
|
|
|
223
223
|
const viewer = await createSkykitViewer({
|
|
224
224
|
view: {
|
|
225
225
|
observerPc: { x: 0, y: 0, z: 0 },
|
|
226
|
-
targetPc: { x: 0, y: 0, z: -10 },
|
|
226
|
+
lookAt: { targetPc: { x: 0, y: 0, z: -10 } },
|
|
227
227
|
},
|
|
228
228
|
plugins: [
|
|
229
229
|
createParallaxObserverPlugin({
|
|
@@ -253,7 +253,7 @@ test('parallax observer static upIcrs controls the target-relative up plane', as
|
|
|
253
253
|
const viewer = await createSkykitViewer({
|
|
254
254
|
view: {
|
|
255
255
|
observerPc: { x: 0, y: 0, z: 0 },
|
|
256
|
-
targetPc: { x: 0, y: -10, z: 0 },
|
|
256
|
+
lookAt: { targetPc: { x: 0, y: -10, z: 0 } },
|
|
257
257
|
},
|
|
258
258
|
plugins: [
|
|
259
259
|
createParallaxObserverPlugin({
|
|
@@ -285,7 +285,7 @@ test('parallax observer resolveUpIcrs overrides static up and can change at runt
|
|
|
285
285
|
const viewer = await createSkykitViewer({
|
|
286
286
|
view: {
|
|
287
287
|
observerPc: { x: 0, y: 0, z: 0 },
|
|
288
|
-
targetPc: { x: 0, y: -10, z: 0 },
|
|
288
|
+
lookAt: { targetPc: { x: 0, y: -10, z: 0 } },
|
|
289
289
|
},
|
|
290
290
|
plugins: [
|
|
291
291
|
createParallaxObserverPlugin({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import test from 'node:test';
|
|
3
3
|
import * as THREE from 'three';
|
|
4
|
+
import { createRuntime } from '@found-in-space/touch-os';
|
|
4
5
|
|
|
5
6
|
import {
|
|
6
7
|
SKYKIT_ACTIONS,
|
|
@@ -8,6 +9,8 @@ import {
|
|
|
8
9
|
} from '../index.js';
|
|
9
10
|
import {
|
|
10
11
|
createSkykitShipControlsRoot,
|
|
12
|
+
createSkykitSurfaceApp,
|
|
13
|
+
createSkykitTabletRoot,
|
|
11
14
|
createTouchOsHudPlugin,
|
|
12
15
|
createTouchOsPanelPlugin,
|
|
13
16
|
dispatchTouchOsActionOutputs,
|
|
@@ -72,6 +75,74 @@ test('createSkykitShipControlsRoot builds reusable pseudo-key controls and statu
|
|
|
72
75
|
);
|
|
73
76
|
});
|
|
74
77
|
|
|
78
|
+
test('createSkykitTabletRoot builds a tablet app shell from touch apps', () => {
|
|
79
|
+
const app = createSkykitSurfaceApp({
|
|
80
|
+
id: 'app.surface',
|
|
81
|
+
name: 'Surface',
|
|
82
|
+
node: createSkykitShipControlsRoot({ id: 'surface-child', movePad: false, verticalControls: false }),
|
|
83
|
+
});
|
|
84
|
+
const root = createSkykitTabletRoot({
|
|
85
|
+
id: 'test-tablet',
|
|
86
|
+
apps: [app],
|
|
87
|
+
appStates: { 'app.surface': { ready: true } },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
assert.equal(root.id, 'test-tablet');
|
|
91
|
+
assert.equal(root.component.kind, 'app-shell');
|
|
92
|
+
assert.equal(root.props.presentation.kind, 'tablet-home');
|
|
93
|
+
assert.equal(root.props.appHostMode, 'same-runtime');
|
|
94
|
+
assert.equal(root.props.homeKey, true);
|
|
95
|
+
assert.deepEqual(root.props.registry.list().map((manifest) => manifest.id), ['app.surface']);
|
|
96
|
+
|
|
97
|
+
const runtime = createRuntime({
|
|
98
|
+
root,
|
|
99
|
+
surface: { width: 320, height: 240 },
|
|
100
|
+
});
|
|
101
|
+
const snapshot = runtime.render();
|
|
102
|
+
assert.equal(snapshot.commands.some((command) => command.role === 'tablet-home-button'), true);
|
|
103
|
+
assert.equal(snapshot.commands.some((command) => command.role === 'tablet-home-bar'), false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('createSkykitSurfaceApp wraps display nodes and emits app events', () => {
|
|
107
|
+
const emitted = [];
|
|
108
|
+
const app = createSkykitSurfaceApp({
|
|
109
|
+
id: 'app.hr',
|
|
110
|
+
name: 'HR',
|
|
111
|
+
node: () => createSkykitShipControlsRoot({ id: 'hr-child', movePad: false, verticalControls: false }),
|
|
112
|
+
});
|
|
113
|
+
const instance = app.createApp({
|
|
114
|
+
appId: 'app.hr',
|
|
115
|
+
instanceId: 'app-1',
|
|
116
|
+
windowId: 'app-1-window',
|
|
117
|
+
surface: { width: 420, height: 300, pixelDensity: 1, safeArea: { top: 0, right: 0, bottom: 0, left: 0 } },
|
|
118
|
+
theme: { getTokens() { return {}; } },
|
|
119
|
+
actions: { emit(event) { emitted.push(event); } },
|
|
120
|
+
windows: {
|
|
121
|
+
setTitle() {},
|
|
122
|
+
requestClose() {},
|
|
123
|
+
requestResize() {},
|
|
124
|
+
openApp() {},
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const root = instance.render({});
|
|
129
|
+
assert.equal(app.manifest.id, 'app.hr');
|
|
130
|
+
assert.deepEqual(app.manifest.capabilities, ['surfaces']);
|
|
131
|
+
assert.equal(root.component.kind, 'skykit-surface-app-frame');
|
|
132
|
+
assert.equal(root.props.child.id, 'hr-child');
|
|
133
|
+
|
|
134
|
+
instance.handleOutput({ type: 'action', actionId: 'app.fly', componentId: 'fly', payload: { target: 'sun' } });
|
|
135
|
+
assert.deepEqual(emitted, [{
|
|
136
|
+
type: 'app-action',
|
|
137
|
+
appId: 'app.hr',
|
|
138
|
+
instanceId: 'app-1',
|
|
139
|
+
windowId: 'app-1-window',
|
|
140
|
+
name: 'app.fly',
|
|
141
|
+
payload: { target: 'sun' },
|
|
142
|
+
componentId: 'fly',
|
|
143
|
+
}]);
|
|
144
|
+
});
|
|
145
|
+
|
|
75
146
|
test('touch-os pointer helpers resolve screen input and surface metrics', () => {
|
|
76
147
|
const target = createTarget({ width: 640, height: 360, pixelRatio: 3 });
|
|
77
148
|
const metrics = resolveTouchOsSurfaceMetrics(target, { pixelDensity: 1.5 });
|
|
@@ -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;
|