@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.
Files changed (42) hide show
  1. package/README.md +143 -6
  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 +644 -217
  6. package/package.json +31 -5
  7. package/src/__tests__/skykit-anchored-images.test.js +32 -4
  8. package/src/__tests__/skykit-browser.test.js +217 -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 +123 -2
  13. package/src/__tests__/skykit.test.js +138 -1
  14. package/src/anchored-images.js +14 -15
  15. package/src/browser-addons.d.ts +16 -0
  16. package/src/browser-addons.js +155 -0
  17. package/src/browser-constellations.d.ts +13 -0
  18. package/src/browser-constellations.js +387 -0
  19. package/src/browser-journey.d.ts +8 -0
  20. package/src/browser-journey.js +240 -0
  21. package/src/browser.d.ts +98 -0
  22. package/src/browser.js +215 -13
  23. package/src/data.d.ts +133 -0
  24. package/src/data.js +447 -0
  25. package/src/embed.d.ts +5 -0
  26. package/src/embed.js +52 -2
  27. package/src/hr-diagram.js +23 -5
  28. package/src/index.d.ts +32 -7
  29. package/src/plugins.js +87 -43
  30. package/src/story.d.ts +57 -0
  31. package/src/story.js +396 -0
  32. package/src/three-shim.d.ts +32 -0
  33. package/src/touch-os.d.ts +70 -0
  34. package/src/touch-os.js +275 -0
  35. package/src/utils.js +96 -6
  36. package/src/viewer-entry.d.ts +10 -0
  37. package/src/viewer-entry.js +4 -0
  38. package/src/viewer.js +110 -12
  39. package/src/xr/plugins.js +224 -13
  40. package/src/xr/session.js +60 -14
  41. package/src/xr.d.ts +22 -0
  42. package/src/xr.js +1 -0
@@ -8,6 +8,7 @@ import {
8
8
  createSkyGrabPlugin,
9
9
  createSkykitAnimationLoop,
10
10
  createSkykitDebugBridge,
11
+ createSkykitHrDiagramPlugin,
11
12
  createSkykitNavigationPlugin,
12
13
  createSkykitStarSourcePlugin,
13
14
  createSkykitViewer,
@@ -15,7 +16,11 @@ import {
15
16
  createViewAnchoredImageController,
16
17
  installSkykitDebugGlobal,
17
18
  } from '@found-in-space/skykit';
18
- import { createTouchOsPanelPlugin } from '@found-in-space/skykit/touch-os';
19
+ import {
20
+ createSkykitSurfaceApp,
21
+ createSkykitTabletRoot,
22
+ createTouchOsPanelPlugin,
23
+ } from '@found-in-space/skykit/touch-os';
19
24
  import {
20
25
  applySkykitXrDepthRange,
21
26
  computeSkykitXrDepthRange,
@@ -23,60 +28,74 @@ import {
23
28
  createSkykitXrNavigationPlugin,
24
29
  createSkykitXrObserverRig,
25
30
  createSkykitXrRaySource,
31
+ createSkykitXrRayVisualPlugin,
26
32
  createSkykitXrRig,
27
33
  createSkykitXrSessionPlugin,
28
34
  createSkykitXrStarPickingPlugin,
29
35
  } from '@found-in-space/skykit/xr';
30
36
  import {
37
+ createActionCard,
31
38
  createButton,
32
- createChoiceGroup,
33
- createColumn,
34
- createSlider,
35
- createTextLabel,
36
- createToggle,
37
- createValueReadout,
39
+ createSurfaceShell,
40
+ defineControlsApp,
41
+ defineTouchApp,
38
42
  } from '@found-in-space/touch-os';
39
43
  import { createXrRayPointerSource } from '@found-in-space/touch-os/hosts/three';
40
44
  import {
41
45
  OCTREE_DEFAULT,
42
46
  createStarOctreeProviderService,
43
47
  } from '@found-in-space/star-octree-provider';
48
+ import {
49
+ createMetaSidecarProviderService,
50
+ deriveMetaSidecarUrlFromRenderUrl,
51
+ metaSidecarEntryDisplayFields,
52
+ } from '@found-in-space/meta-sidecar-provider';
44
53
  import {
45
54
  createThreeStarField,
46
- createVrThreeStarFieldMaterialProfile,
55
+ createDefaultThreeStarFieldMaterialProfile,
47
56
  } from '@found-in-space/three-star-field';
48
57
 
49
58
  const WESTERN_SKYCULTURE_MANIFEST_URL = 'https://unpkg.com/@found-in-space/stellarium-skycultures-western@0.1.0/dist/manifest.json';
50
- const PROXIMA_CENTAURI_PC = { x: -0.47, y: -0.36, z: -1.16 };
51
- const SIRIUS_PC = { x: -0.49, y: 2.48, z: -0.76 };
52
- const BETELGEUSE_PC = { x: 4.2, y: 198.3, z: 25.8 };
53
- const DEFAULT_WORLD_SCALE = 0.001;
59
+ const DATASET_ID_c56103 = 'c56103e6-ad4c-41f9-be06-048b48ec632b';
60
+ const SOL_PC = { x: 0, y: 0, z: 0 };
61
+ const ORION_CENTER_PC = { x: 62.775, y: 602.667, z: -12.713 };
62
+ const DEFAULT_WORLD_SCALE = 1;
54
63
  const DEFAULT_LIMITING_MAGNITUDE = 7.5;
55
64
  const DEFAULT_EXPOSURE_LOG10 = 5;
56
65
  const DEFAULT_EXPOSURE = 10 ** DEFAULT_EXPOSURE_LOG10;
57
66
  const XR_CONSTELLATION_RADIUS_PC = 8;
67
+ const XR_PANEL_SURFACE = Object.freeze({ width: 420, height: 560, pixelDensity: 1 });
68
+ const XR_PANEL_THEME = Object.freeze({
69
+ backgroundColor: '#07111e',
70
+ surfaceColor: '#101b2a',
71
+ textColor: '#eef8ff',
72
+ mutedTextColor: '#8aa7b4',
73
+ accentColor: '#35d6c8',
74
+ accentTextColor: '#031416',
75
+ borderColor: 'rgba(120, 210, 220, 0.28)',
76
+ focusColor: '#79ffe8',
77
+ overlayColor: 'rgba(4, 12, 23, 0.65)',
78
+ controlHeight: 28,
79
+ spacing: 5,
80
+ padding: 7,
81
+ radius: 6,
82
+ typography: {
83
+ fontFamily: 'ui-sans-serif',
84
+ fontSize: 11,
85
+ lineHeight: 14,
86
+ fontWeight: 500,
87
+ },
88
+ });
58
89
  const XR_DEMO_ACTIONS = Object.freeze({
59
90
  goSelected: 'xr-demo:selected.go',
60
- pages: Object.freeze({
61
- home: 'xr-demo:page.home',
62
- waypoints: 'xr-demo:page.waypoints',
63
- selected: 'xr-demo:page.selected',
64
- rendering: 'xr-demo:page.rendering',
65
- }),
66
- waypointPrefix: 'xr-demo:waypoint.',
91
+ selectSun: 'xr-demo:selected.sun',
67
92
  });
68
- const PAGE_OPTIONS = Object.freeze([
69
- { value: 'home', label: 'Home' },
70
- { value: 'waypoints', label: 'Waypoints' },
71
- { value: 'selected', label: 'Target' },
72
- { value: 'rendering', label: 'Rendering' },
73
- ]);
74
- const WAYPOINTS = Object.freeze([
75
- { id: 'sol', label: 'Sol', targetPc: { x: 0, y: 0, z: 0 }, approachPc: 2 },
76
- { id: 'proxima', label: 'Proxima', targetPc: PROXIMA_CENTAURI_PC, approachPc: 1.6 },
77
- { id: 'sirius', label: 'Sirius', targetPc: SIRIUS_PC, approachPc: 1.6 },
78
- { id: 'betelgeuse', label: 'Betelgeuse', targetPc: BETELGEUSE_PC, approachPc: 8 },
79
- ]);
93
+ const XR_TABLET_APP_IDS = Object.freeze({
94
+ target: 'space.found.skykit.xr-free-roam.target',
95
+ rendering: 'space.found.skykit.xr-free-roam.rendering',
96
+ hrDiagram: 'space.found.skykit.xr-free-roam.hr-diagram',
97
+ });
98
+ const HR_SURFACE_SIZE = Object.freeze({ width: 1024, height: 640 });
80
99
 
81
100
  const debug = createSkykitDebugBridge();
82
101
  installSkykitDebugGlobal(debug);
@@ -101,30 +120,44 @@ async function main() {
101
120
  renderer.xr.enabled = true;
102
121
 
103
122
  const camera = new THREE.PerspectiveCamera(60, 1, 0.02, 2000000);
104
- const xrRig = createSkykitXrRig({ scaleBandIds: ['constellation-art'] });
105
- xrRig.deckRoot.add(createShipDeckSlab());
123
+ const initialOrientation = orientationLookingAt(SOL_PC, ORION_CENTER_PC);
124
+ const xrRig = createSkykitXrRig({
125
+ navigationPose: {
126
+ position: SOL_PC,
127
+ orientation: initialOrientation,
128
+ },
129
+ });
130
+ const shipDeck = createShipDeckSlab();
131
+ shipDeck.visible = false;
132
+ xrRig.deckRoot.add(shipDeck);
106
133
 
107
- const provider = createStarOctreeProviderService({ url: OCTREE_DEFAULT });
134
+ const provider = createStarOctreeProviderService({
135
+ url: OCTREE_DEFAULT,
136
+ datasetId: DATASET_ID_c56103,
137
+ });
138
+ const metaProvider = createMetaSidecarProviderService({
139
+ url: deriveMetaSidecarUrlFromRenderUrl(OCTREE_DEFAULT),
140
+ parentDatasetId: DATASET_ID_c56103,
141
+ });
108
142
  const source = createSkykitStarSourcePlugin({ provider });
109
143
  const starField = createThreeStarField({
110
144
  limitingMagnitude: DEFAULT_LIMITING_MAGNITUDE,
111
145
  coordinateUnitsPerParsec: DEFAULT_WORLD_SCALE,
112
146
  exposure: DEFAULT_EXPOSURE,
113
- materialProfile: createVrThreeStarFieldMaterialProfile({
147
+ materialProfile: createDefaultThreeStarFieldMaterialProfile({
114
148
  limitingMagnitude: DEFAULT_LIMITING_MAGNITUDE,
115
149
  coordinateUnitsPerParsec: DEFAULT_WORLD_SCALE,
116
150
  exposure: DEFAULT_EXPOSURE,
117
151
  }),
118
152
  });
119
153
  const selectedTarget = createSelectedStarTarget();
120
- starField.object3d.add(selectedTarget.object3d);
154
+ xrRig.originContentRoot.add(selectedTarget.object3d);
121
155
  const rightRaySource = createWorldXrRaySource(
122
156
  createSkykitXrRaySource({ kind: 'target-ray', handedness: 'right', length: 2000000 }),
123
157
  xrRig.xrOrigin,
124
158
  );
125
159
  const touchPointerSource = createRightHandTouchPointerSource(rightRaySource);
126
160
  const panelState = {
127
- page: 'home',
128
161
  limitingMagnitude: DEFAULT_LIMITING_MAGNITUDE,
129
162
  exposureLog10: DEFAULT_EXPOSURE_LOG10,
130
163
  worldScaleLog10: Math.log10(DEFAULT_WORLD_SCALE),
@@ -138,6 +171,28 @@ async function main() {
138
171
  let latestPanelFrame = null;
139
172
  let activeXrHandle = null;
140
173
  let artController = null;
174
+ let preflightController = null;
175
+ let touchPanel = null;
176
+ let selectionGeneration = 0;
177
+ const hrDiagram = createSkykitHrDiagramPlugin({
178
+ id: 'xr-free-roam-hr-diagram',
179
+ source,
180
+ mode: 'frustum',
181
+ limitingMagnitude: DEFAULT_LIMITING_MAGNITUDE,
182
+ width: HR_SURFACE_SIZE.width,
183
+ height: HR_SURFACE_SIZE.height,
184
+ touchOs: {
185
+ sourceId: 'xr-free-roam-hr-diagram:surface',
186
+ componentId: 'xr-free-roam-hr-diagram:node',
187
+ surfaces: () => touchPanel?.getRuntime()?.getServices().surfaces,
188
+ width: HR_SURFACE_SIZE.width,
189
+ height: HR_SURFACE_SIZE.height,
190
+ },
191
+ });
192
+ const tabletApps = createXrTabletApps({
193
+ hrDiagram,
194
+ renderTargetReadout: createSelectedTargetReadout,
195
+ });
141
196
 
142
197
  const artPlugin = await createConstellationArtPlugin().catch((error) => {
143
198
  debug.recordDiagnostic({
@@ -149,29 +204,30 @@ async function main() {
149
204
  return null;
150
205
  });
151
206
 
152
- const touchPanel = createTouchOsPanelPlugin({
207
+ touchPanel = createTouchOsPanelPlugin({
153
208
  id: 'xr-free-roam-touch-panel',
154
209
  priority: 20,
155
210
  driver: 'pose-anchored',
156
211
  root: createPanelRoot,
157
- surfaceMetrics: { width: 392, height: 430, pixelDensity: 1 },
212
+ surfaceMetrics: XR_PANEL_SURFACE,
213
+ runtimeOptions: {
214
+ theme: XR_PANEL_THEME,
215
+ longPressDelay: 360,
216
+ },
158
217
  pointerSources: [touchPointerSource],
159
218
  anchorPose(frame) {
160
219
  latestPanelFrame = frame;
161
220
  return resolveLeftHandPanelPose(frame, xrRig.xrOrigin);
162
221
  },
163
222
  driverOptions: {
164
- panelWidth: 0.34,
165
- panelHeight: 0.42,
223
+ panelWidth: 0.32,
224
+ panelHeight: 0.44,
166
225
  offset: { x: 0.04, y: 0.02, z: -0.08 },
167
226
  tiltRadians: -0.22,
168
227
  transparent: true,
169
228
  depthTest: false,
170
229
  renderOrder: 50,
171
230
  },
172
- onOutput(output) {
173
- handlePanelOutput(output);
174
- },
175
231
  });
176
232
 
177
233
  const viewer = await createSkykitViewer({
@@ -191,9 +247,11 @@ async function main() {
191
247
  scaleBandedContentRoots: new Map(Object.entries(xrRig.scaleBandedContentRoots)),
192
248
  },
193
249
  view: {
250
+ observerPc: SOL_PC,
251
+ targetPc: ORION_CENTER_PC,
194
252
  limitingMagnitude: DEFAULT_LIMITING_MAGNITUDE,
195
253
  coordinateUnitsPerParsec: DEFAULT_WORLD_SCALE,
196
- orientationIcrs: { x: 0, y: 0, z: 0, w: 1 },
254
+ lookAt: { orientationIcrs: initialOrientation },
197
255
  },
198
256
  plugins: [
199
257
  createSkykitXrSessionPlugin({
@@ -201,6 +259,9 @@ async function main() {
201
259
  referenceSpaceType: 'local-floor',
202
260
  onSessionStarted(handle) {
203
261
  activeXrHandle = handle;
262
+ shipDeck.visible = true;
263
+ preflightController?.setSessionStatus('XR session active');
264
+ preflightController?.sync();
204
265
  invalidatePanel();
205
266
  updateXrDepthRange(handle);
206
267
  },
@@ -208,24 +269,35 @@ async function main() {
208
269
  createSkykitNavigationPlugin(),
209
270
  source,
210
271
  createStreamingStarsPlugin({ id: 'xr-stars', source, renderer: starField }),
272
+ hrDiagram,
211
273
  ...(artPlugin ? [artPlugin] : []),
274
+ createXrFreeRoamFrameSyncPlugin({
275
+ update() {
276
+ selectedTarget.update(camera);
277
+ updateXrDepthRange(activeXrHandle);
278
+ },
279
+ }),
212
280
  createKeyboardNavigationPlugin({ speedPcPerSec: 2, rotationSpeedDegPerSec: 55 }),
213
281
  createSkyGrabPlugin({ target: host, sensitivityRadiansPerPixel: 0.0007 }),
214
282
  touchPanel,
215
283
  createSkykitXrNavigationPlugin({ moveSpeedPcPerSec: 4 }),
284
+ createSkykitXrRayVisualPlugin({
285
+ id: 'xr-free-roam-right-ray',
286
+ raySource: rightRaySource,
287
+ blockers: [touchPanel],
288
+ color: 0x78ffe7,
289
+ opacity: 0.82,
290
+ length: 2000000,
291
+ renderOrder: 60,
292
+ }),
216
293
  createSkykitXrStarPickingPlugin({
217
294
  renderer: starField,
218
295
  source,
219
296
  raySource: rightRaySource,
220
297
  blockers: [touchPanel],
298
+ attributes: ['objectRef', 'pickMeta'],
221
299
  onPick(event) {
222
- panelState.selected = {
223
- label: event.label,
224
- position: { ...event.pick.position },
225
- targetPc: renderPositionToPc(event.pick.position, viewer.getViewState().coordinateUnitsPerParsec),
226
- };
227
- selectedTarget.setPosition(event.pick.position);
228
- invalidatePanel();
300
+ selectPickedStar(event);
229
301
  },
230
302
  }),
231
303
  ],
@@ -238,12 +310,26 @@ async function main() {
238
310
  });
239
311
  viewer.on('xr/session-end', () => {
240
312
  activeXrHandle = null;
313
+ shipDeck.visible = false;
314
+ preflightController?.setSessionStatus('Session ended');
315
+ preflightController?.sync();
241
316
  invalidatePanel();
242
317
  });
243
318
 
244
319
  registerDemoActions(viewer);
245
- wireDomActions(viewer);
246
320
  applyRenderState(viewer, starField, source);
321
+ preflightController = createPreflightController({
322
+ viewer,
323
+ panelState,
324
+ starField,
325
+ source,
326
+ applyRenderState,
327
+ invalidatePanel,
328
+ setConstellationArtEnabled,
329
+ isPresenting: () => activeXrHandle?.presenting === true,
330
+ });
331
+ preflightController.sync();
332
+ void preflightController.refreshXrSupport();
247
333
  resize();
248
334
  window.addEventListener('resize', resize);
249
335
  window.addEventListener('beforeunload', () => {
@@ -251,6 +337,7 @@ async function main() {
251
337
  selectedTarget.dispose();
252
338
  void viewer.dispose();
253
339
  void provider.dispose?.();
340
+ void metaProvider.dispose?.();
254
341
  });
255
342
  loop.start();
256
343
 
@@ -266,10 +353,15 @@ async function main() {
266
353
 
267
354
  async function createConstellationArtPlugin() {
268
355
  const catalog = await createAnchoredImageCatalog({ manifestUrl: WESTERN_SKYCULTURE_MANIFEST_URL });
356
+ debug.recordDiagnostic({
357
+ level: 'info',
358
+ type: 'xr-free-roam/constellation-art-catalog',
359
+ message: `Constellation art catalog loaded with ${catalog.list().length} entries.`,
360
+ });
269
361
  artController = createViewAnchoredImageController({
270
- strategy: 'nearest',
271
- maxAngleDeg: 32,
272
- hysteresisSeconds: 0.25,
362
+ strategy: 'within-angle',
363
+ maxAngleDeg: 34,
364
+ hysteresisSeconds: 0,
273
365
  });
274
366
  return createAnchoredImageSkyPlugin({
275
367
  id: 'xr-constellation-art',
@@ -281,156 +373,202 @@ async function main() {
281
373
  opacity: 0.38,
282
374
  fadeInSeconds: 0.25,
283
375
  fadeOutSeconds: 0.25,
284
- scaleBandId: 'constellation-art',
285
- skipTextureErrors: true,
376
+ skipTextureErrors: false,
377
+ onTextureError(event) {
378
+ debug.recordDiagnostic({
379
+ level: 'warn',
380
+ type: 'xr-free-roam/constellation-art-texture-error',
381
+ message: `Constellation art texture failed for ${event.entry.label}.`,
382
+ data: {
383
+ key: event.entry.key,
384
+ imageUrl: event.imageUrl,
385
+ },
386
+ error: event.error,
387
+ });
388
+ },
286
389
  });
287
390
  }
288
391
 
289
392
  function createPanelRoot() {
290
393
  if (cachedPanelRoot && cachedPanelRevision === panelRevision) return cachedPanelRoot;
291
394
  cachedPanelRevision = panelRevision;
292
- cachedPanelRoot = createColumn('xr-free-roam-panel', {
293
- gap: 8,
294
- padding: 10,
295
- backgroundColor: 'rgba(4, 12, 23, 0.82)',
296
- children: [
297
- createChoiceGroup('xr-panel-page', {
298
- field: 'xrPage',
299
- selectionMode: 'single',
300
- value: panelState.page,
301
- orientation: 'horizontal',
302
- options: PAGE_OPTIONS,
303
- }),
304
- ...createPanelPageChildren(),
305
- ],
395
+ cachedPanelRoot = createSkykitTabletRoot({
396
+ id: 'xr-free-roam-tablet',
397
+ apps: tabletApps,
398
+ appStates: {
399
+ [XR_TABLET_APP_IDS.target]: panelState,
400
+ [XR_TABLET_APP_IDS.rendering]: panelState,
401
+ [XR_TABLET_APP_IDS.hrDiagram]: panelState,
402
+ },
403
+ homeControl: 'button',
404
+ taskSwitcher: 'cards',
405
+ taskCloseControl: 'button',
406
+ launcherLayout: {
407
+ tileWidth: 82,
408
+ tileHeight: 86,
409
+ gap: 9,
410
+ bodyPadding: 9,
411
+ iconMinSize: 36,
412
+ iconMaxSize: 44,
413
+ labelGap: 5,
414
+ },
415
+ onAppEvent: handleTabletAppEvent,
306
416
  });
307
417
  return cachedPanelRoot;
308
418
  }
309
419
 
310
- function createPanelPageChildren() {
311
- if (panelState.page === 'waypoints') {
312
- return WAYPOINTS.map((waypoint) => createButton(`xr-waypoint-${waypoint.id}`, {
313
- label: waypoint.label,
314
- actionId: `${XR_DEMO_ACTIONS.waypointPrefix}${waypoint.id}`,
315
- }));
316
- }
317
- if (panelState.page === 'rendering') {
420
+ function createSelectedTargetReadout(state = panelState) {
421
+ const selected = state.selected;
422
+ if (!selected) {
318
423
  return [
319
- createSlider('xr-mag-limit', {
320
- label: 'Magnitude',
321
- field: 'limitingMagnitude',
322
- value: panelState.limitingMagnitude,
323
- min: 4,
324
- max: 10,
325
- step: 0.1,
326
- valueText: panelState.limitingMagnitude.toFixed(1),
327
- }),
328
- createSlider('xr-exposure', {
329
- label: 'Exposure',
330
- field: 'exposureLog10',
331
- value: panelState.exposureLog10,
332
- min: 3.5,
333
- max: 5.5,
334
- step: 0.05,
335
- valueText: `${Math.round(10 ** panelState.exposureLog10).toLocaleString()}`,
336
- }),
337
- createSlider('xr-world-scale', {
338
- label: 'World scale',
339
- field: 'worldScaleLog10',
340
- value: panelState.worldScaleLog10,
341
- min: -3,
342
- max: 0,
343
- step: 0.05,
344
- valueText: `${(10 ** panelState.worldScaleLog10).toPrecision(2)} u/pc`,
345
- }),
346
- createToggle('xr-near-floor', {
347
- label: 'Near floor',
348
- field: 'nearFloor',
349
- value: panelState.nearFloor,
350
- }),
351
- createToggle('xr-constellation-art', {
352
- label: 'Constellation art',
353
- field: 'constellationArt',
354
- value: panelState.constellationArt,
355
- }),
356
- ];
357
- }
358
- if (panelState.page === 'selected') {
359
- return [
360
- createValueReadout('xr-selected-name', {
361
- label: 'Target',
362
- value: panelState.selected?.label ?? 'None',
363
- }),
364
- createButton('xr-go-selected', {
365
- label: 'Go to selected',
366
- actionId: XR_DEMO_ACTIONS.goSelected,
367
- disabled: !panelState.selected,
424
+ createActionCard('xr-selected-empty', {
425
+ title: 'Target',
426
+ emptyStateText: 'Pick a star or select Sun',
368
427
  }),
369
428
  ];
370
429
  }
371
430
  return [
372
- createValueReadout('xr-home-target', {
373
- label: 'Target',
374
- value: panelState.selected?.label ?? 'None',
375
- }),
376
- createButton('xr-home-waypoints', {
377
- label: 'Waypoints',
378
- actionId: XR_DEMO_ACTIONS.pages.waypoints,
379
- }),
380
- createButton('xr-home-selected', {
381
- label: 'Selected Target',
382
- actionId: XR_DEMO_ACTIONS.pages.selected,
383
- disabled: !panelState.selected,
384
- }),
385
- createButton('xr-home-rendering', {
386
- label: 'Rendering',
387
- actionId: XR_DEMO_ACTIONS.pages.rendering,
388
- }),
389
- createTextLabel('xr-home-mode', {
390
- text: activeXrHandle?.presenting ? 'XR active' : 'Desktop',
391
- tone: 'muted',
431
+ createActionCard('xr-selected-details', {
432
+ title: selected.label || 'Selected star',
433
+ lines: createSelectedIdentifierLines(selected),
434
+ primaryActionId: XR_DEMO_ACTIONS.goSelected,
435
+ primaryActionLabel: 'Fly to',
392
436
  }),
393
437
  ];
394
438
  }
395
439
 
396
- function handlePanelOutput(output) {
397
- if (!output || output.type !== 'change-request') return;
398
- if (output.field === 'xrPage' && typeof output.value === 'string') {
399
- panelState.page = output.value;
400
- invalidatePanel();
440
+ function handleTabletAppEvent(event) {
441
+ if (event.type === 'app-change') {
442
+ if (applyTabletStateChange(event.payload)) {
443
+ applyRenderState(viewer, starField, source);
444
+ preflightController?.sync();
445
+ invalidatePanel();
446
+ }
401
447
  return;
402
448
  }
403
- if (output.field === 'limitingMagnitude') {
404
- panelState.limitingMagnitude = clampNumber(output.value, 4, 10, panelState.limitingMagnitude);
405
- applyRenderState(viewer, starField, source);
406
- invalidatePanel();
407
- return;
449
+
450
+ if (event.type !== 'app-action') return;
451
+ if (event.name === XR_DEMO_ACTIONS.goSelected) {
452
+ if (panelState.selected) {
453
+ goToTarget(viewer, panelState.selected.targetPc, 0.65);
454
+ }
455
+ } else if (event.name === XR_DEMO_ACTIONS.selectSun) {
456
+ selectSunTarget();
408
457
  }
409
- if (output.field === 'exposureLog10') {
410
- panelState.exposureLog10 = clampNumber(output.value, 3.5, 5.5, panelState.exposureLog10);
411
- applyRenderState(viewer, starField, source);
412
- invalidatePanel();
413
- return;
458
+ }
459
+
460
+ function applyTabletStateChange(payload) {
461
+ const field = payload?.field;
462
+ const value = payload?.value;
463
+ if (field === 'limitingMagnitude') {
464
+ panelState.limitingMagnitude = clampNumber(value, 4, 10, panelState.limitingMagnitude);
465
+ return true;
414
466
  }
415
- if (output.field === 'worldScaleLog10') {
416
- panelState.worldScaleLog10 = clampNumber(output.value, -3, 0, panelState.worldScaleLog10);
417
- applyRenderState(viewer, starField, source);
418
- invalidatePanel();
419
- return;
467
+ if (field === 'exposureLog10') {
468
+ panelState.exposureLog10 = clampNumber(value, 3.5, 5.5, panelState.exposureLog10);
469
+ return true;
470
+ }
471
+ if (field === 'worldScaleLog10') {
472
+ panelState.worldScaleLog10 = clampNumber(value, -3, 0, panelState.worldScaleLog10);
473
+ return true;
474
+ }
475
+ if (field === 'nearFloor') {
476
+ panelState.nearFloor = Boolean(value);
477
+ return true;
478
+ }
479
+ if (field === 'constellationArt') {
480
+ setConstellationArtEnabled(Boolean(value));
481
+ return true;
420
482
  }
421
- if (output.field === 'nearFloor') {
422
- panelState.nearFloor = output.value === true;
423
- applyRenderState(viewer, starField, source);
424
- invalidatePanel();
483
+ return false;
484
+ }
485
+
486
+ function setConstellationArtEnabled(enabled) {
487
+ panelState.constellationArt = enabled;
488
+ artController?.setSelection?.(enabled ? undefined : () => false);
489
+ }
490
+
491
+ function selectPickedStar(event) {
492
+ const targetPc = renderPositionToPc(event.pick.position, viewer.getViewState().coordinateUnitsPerParsec);
493
+ const generation = selectTarget({
494
+ label: 'Selected star',
495
+ position: { ...event.pick.position },
496
+ targetPc,
497
+ identifiers: createEmptyIdentifierFields(),
498
+ identifierStatus: 'loading',
499
+ }, event.pick.position);
500
+ void resolveSelectedStarIdentifiers(event.pick, generation);
501
+ }
502
+
503
+ function selectSunTarget() {
504
+ const generation = selectTarget({
505
+ label: 'Sun',
506
+ position: {
507
+ x: SOL_PC.x * viewer.getViewState().coordinateUnitsPerParsec,
508
+ y: SOL_PC.y * viewer.getViewState().coordinateUnitsPerParsec,
509
+ z: SOL_PC.z * viewer.getViewState().coordinateUnitsPerParsec,
510
+ },
511
+ targetPc: { ...SOL_PC },
512
+ identifiers: {
513
+ ...createEmptyIdentifierFields(),
514
+ properName: 'Sol',
515
+ primaryLabel: 'Sun',
516
+ },
517
+ identifierStatus: 'synthetic',
518
+ }, SOL_PC);
519
+ selectionGeneration = generation;
520
+ }
521
+
522
+ function selectTarget(selection, renderPosition) {
523
+ selectionGeneration += 1;
524
+ panelState.selected = selection;
525
+ selectedTarget.setPosition(renderPosition);
526
+ invalidatePanel();
527
+ return selectionGeneration;
528
+ }
529
+
530
+ async function resolveSelectedStarIdentifiers(pick, generation) {
531
+ const ref = pick.objectRef ?? pick.pickMeta ?? null;
532
+ if (!ref) {
533
+ debug.recordDiagnostic({
534
+ level: 'warn',
535
+ type: 'xr-free-roam/star-pick-missing-sidecar-ref',
536
+ message: 'Selected star did not include sidecar lookup metadata.',
537
+ });
538
+ updateSelectedIdentifiers(generation, null, 'unavailable');
425
539
  return;
426
540
  }
427
- if (output.field === 'constellationArt') {
428
- panelState.constellationArt = output.value === true;
429
- artController?.setSelection?.(panelState.constellationArt ? undefined : () => false);
430
- invalidatePanel();
541
+
542
+ try {
543
+ const entry = await metaProvider.getMeta(ref);
544
+ updateSelectedIdentifiers(
545
+ generation,
546
+ metaSidecarEntryDisplayFields(entry),
547
+ entry ? 'ready' : 'unavailable',
548
+ );
549
+ } catch (error) {
550
+ debug.recordDiagnostic({
551
+ level: 'warn',
552
+ type: 'xr-free-roam/star-sidecar-lookup-error',
553
+ message: 'Selected star identifiers could not be loaded from the metadata sidecar.',
554
+ error,
555
+ });
556
+ updateSelectedIdentifiers(generation, null, 'error');
431
557
  }
432
558
  }
433
559
 
560
+ function updateSelectedIdentifiers(generation, fields, status) {
561
+ if (generation !== selectionGeneration || !panelState.selected) return;
562
+ const identifiers = fields ?? createEmptyIdentifierFields();
563
+ panelState.selected = {
564
+ ...panelState.selected,
565
+ label: identifiers.primaryLabel || 'Selected star',
566
+ identifiers,
567
+ identifierStatus: status,
568
+ };
569
+ invalidatePanel();
570
+ }
571
+
434
572
  function applyRenderState(activeViewer, activeStarField, activeSource) {
435
573
  const worldScale = 10 ** panelState.worldScaleLog10;
436
574
  const exposure = 10 ** panelState.exposureLog10;
@@ -452,25 +590,14 @@ async function main() {
452
590
  }
453
591
 
454
592
  function registerDemoActions(activeViewer) {
455
- activeViewer.actions.registerAction(XR_DEMO_ACTIONS.pages.home, () => setPanelPage('home'), { label: 'Panel home' });
456
- activeViewer.actions.registerAction(XR_DEMO_ACTIONS.pages.waypoints, () => setPanelPage('waypoints'), { label: 'Panel waypoints' });
457
- activeViewer.actions.registerAction(XR_DEMO_ACTIONS.pages.selected, () => setPanelPage('selected'), { label: 'Panel selected target' });
458
- activeViewer.actions.registerAction(XR_DEMO_ACTIONS.pages.rendering, () => setPanelPage('rendering'), { label: 'Panel rendering' });
459
593
  activeViewer.actions.registerAction(XR_DEMO_ACTIONS.goSelected, () => {
460
594
  if (panelState.selected) {
461
595
  goToTarget(activeViewer, panelState.selected.targetPc, 0.65);
462
596
  }
463
597
  }, { label: 'Go to selected star' });
464
- for (const waypoint of WAYPOINTS) {
465
- activeViewer.actions.registerAction(`${XR_DEMO_ACTIONS.waypointPrefix}${waypoint.id}`, () => {
466
- goToTarget(activeViewer, waypoint.targetPc, waypoint.approachPc);
467
- }, { label: waypoint.label });
468
- }
469
- }
470
-
471
- function setPanelPage(page) {
472
- panelState.page = page;
473
- invalidatePanel();
598
+ activeViewer.actions.registerAction(XR_DEMO_ACTIONS.selectSun, () => {
599
+ selectSunTarget();
600
+ }, { label: 'Select Sun' });
474
601
  }
475
602
 
476
603
  function goToTarget(activeViewer, targetPc, approachPc) {
@@ -480,7 +607,7 @@ async function main() {
480
607
  view: {
481
608
  observerPc,
482
609
  targetPc,
483
- orientationIcrs: orientationLookingAt(observerPc, targetPc),
610
+ lookAt: { orientationIcrs: orientationLookingAt(observerPc, targetPc) },
484
611
  },
485
612
  durationSecs: 2.4,
486
613
  movement: 'smoothstep',
@@ -491,9 +618,13 @@ async function main() {
491
618
  function updateXrDepthRange(handle) {
492
619
  if (!handle) return;
493
620
  const worldScale = 10 ** panelState.worldScaleLog10;
621
+ const view = viewer.getViewState();
622
+ const visibleBounds = starField.getVisibleBounds({ units: 'parsec' });
494
623
  const range = computeSkykitXrDepthRange({
624
+ observer: view.observerPc,
625
+ visibleBounds,
495
626
  observerCentricSpheres: [
496
- { radiusNavigationUnits: Math.max(500, XR_CONSTELLATION_RADIUS_PC * 2) },
627
+ { radiusNavigationUnits: XR_CONSTELLATION_RADIUS_PC * 2 },
497
628
  ],
498
629
  scale: {
499
630
  navigationUnits: 'pc',
@@ -524,15 +655,306 @@ async function main() {
524
655
  touchPointerSource.getLatestPanelFrame = getLatestPanelFrame;
525
656
  }
526
657
 
527
- function wireDomActions(viewer) {
528
- document.querySelector('[data-action="enter-xr"]')?.addEventListener('click', () => {
529
- void viewer.actions.invoke(SKYKIT_ACTIONS.xr.enter, null, { source: 'xr-free-roam-dom' });
658
+ function createXrFreeRoamFrameSyncPlugin(options) {
659
+ return {
660
+ id: 'xr-free-roam-frame-sync',
661
+ setup(context) {
662
+ context.addPart({
663
+ id: 'xr-free-roam-frame-sync',
664
+ priority: 80,
665
+ update(frame) {
666
+ options.update?.(frame);
667
+ },
668
+ });
669
+ },
670
+ };
671
+ }
672
+
673
+ function createXrTabletApps(options) {
674
+ return [
675
+ createTargetTabletApp(options),
676
+ createRenderingTabletApp(),
677
+ createSkykitSurfaceApp({
678
+ id: XR_TABLET_APP_IDS.hrDiagram,
679
+ name: 'HR',
680
+ icon: { kind: 'symbol', value: 'HR' },
681
+ node: () => options.hrDiagram.getNode(),
682
+ preferredWindow: {
683
+ width: 420,
684
+ height: 526,
685
+ minWidth: 360,
686
+ minHeight: 300,
687
+ resizable: false,
688
+ },
689
+ backgroundColor: '#050c16',
690
+ emptyLabel: 'HR diagram unavailable',
691
+ }),
692
+ ];
693
+ }
694
+
695
+ function createTargetTabletApp(options) {
696
+ return defineTouchApp({
697
+ manifest: {
698
+ id: XR_TABLET_APP_IDS.target,
699
+ name: 'Target',
700
+ version: '1.0.0',
701
+ icon: { kind: 'symbol', value: 'TG' },
702
+ preferredWindow: {
703
+ width: 420,
704
+ height: 526,
705
+ minWidth: 320,
706
+ minHeight: 260,
707
+ resizable: false,
708
+ },
709
+ },
710
+ createApp(ctx) {
711
+ return {
712
+ render(state) {
713
+ return createSurfaceShell('xr-target-app', {
714
+ pointerOpaque: true,
715
+ gap: 6,
716
+ padding: 6,
717
+ bodyGap: 5,
718
+ bodyPadding: 0,
719
+ scrollId: 'xr-target-app-scroll',
720
+ backgroundColor: 'rgba(4, 12, 23, 0.86)',
721
+ children: options.renderTargetReadout(state),
722
+ footer: createButton('xr-selected-sun', {
723
+ label: 'Sun',
724
+ actionId: XR_DEMO_ACTIONS.selectSun,
725
+ }),
726
+ });
727
+ },
728
+ handleOutput(output) {
729
+ emitTabletAppOutput(ctx, output);
730
+ },
731
+ };
732
+ },
530
733
  });
531
- document.querySelector('[data-action="exit-xr"]')?.addEventListener('click', () => {
532
- void viewer.actions.invoke(SKYKIT_ACTIONS.xr.exit, null, { source: 'xr-free-roam-dom' });
734
+ }
735
+
736
+ function createRenderingTabletApp() {
737
+ return defineControlsApp({
738
+ id: XR_TABLET_APP_IDS.rendering,
739
+ name: 'Render',
740
+ icon: { kind: 'symbol', value: 'RD' },
741
+ preferredSurface: {
742
+ width: 420,
743
+ height: 526,
744
+ minWidth: 320,
745
+ minHeight: 280,
746
+ resizable: false,
747
+ },
748
+ controls: ({ section, slider, status, toggle }) => [
749
+ section('Stars', [
750
+ slider('Limit', 'limitingMagnitude', {
751
+ min: 4,
752
+ max: 10,
753
+ step: 0.1,
754
+ valueText: (state) => `Mag ${state.limitingMagnitude.toFixed(1)}`,
755
+ }),
756
+ slider('Exposure', 'exposureLog10', {
757
+ min: 3.5,
758
+ max: 5.5,
759
+ step: 0.1,
760
+ valueText: (state) => Math.round(10 ** state.exposureLog10).toLocaleString(),
761
+ }),
762
+ slider('Scale', 'worldScaleLog10', {
763
+ min: -3,
764
+ max: 0,
765
+ step: 0.1,
766
+ valueText: (state) => formatWorldScale(10 ** state.worldScaleLog10),
767
+ }),
768
+ ]),
769
+ section('Context', [
770
+ toggle('Nearby glow', 'nearFloor'),
771
+ toggle('Constellation art', 'constellationArt'),
772
+ status('Target', (state) => state.selected?.label ?? 'None', { tone: 'muted' }),
773
+ ]),
774
+ ],
533
775
  });
534
776
  }
535
777
 
778
+ function emitTabletAppOutput(ctx, output) {
779
+ if (output?.type === 'action') {
780
+ ctx.actions.emit({
781
+ type: 'app-action',
782
+ appId: ctx.appId,
783
+ instanceId: ctx.instanceId,
784
+ windowId: ctx.windowId,
785
+ name: output.actionId,
786
+ ...(output.payload === undefined ? {} : { payload: output.payload }),
787
+ });
788
+ }
789
+ }
790
+
791
+ function createPreflightController(options) {
792
+ const shell = document.querySelector('.xr-free-roam-shell');
793
+ const enterButton = document.querySelector('[data-action="enter-xr"]');
794
+ const exitButton = document.querySelector('[data-action="exit-xr"]');
795
+ const supportValue = document.querySelector('[data-xr-supported]');
796
+ const sessionStatus = document.querySelector('[data-session-status]');
797
+ const settingInputs = Array.from(document.querySelectorAll('[data-setting]'))
798
+ .filter((input) => input instanceof HTMLInputElement);
799
+ let xrSupported = null;
800
+
801
+ for (const input of settingInputs) {
802
+ input.addEventListener('input', () => {
803
+ readSettingInput(input, options.panelState, options.setConstellationArtEnabled);
804
+ syncSettingOutputs(options.panelState);
805
+ });
806
+ input.addEventListener('change', () => {
807
+ readSettingInput(input, options.panelState, options.setConstellationArtEnabled);
808
+ options.applyRenderState(options.viewer, options.starField, options.source);
809
+ options.invalidatePanel();
810
+ sync();
811
+ });
812
+ }
813
+
814
+ enterButton?.addEventListener('click', async () => {
815
+ setSessionStatus('Opening XR session');
816
+ sync();
817
+ const results = await options.viewer.actions.invoke(SKYKIT_ACTIONS.xr.enter, null, {
818
+ source: 'xr-free-roam-dom',
819
+ });
820
+ const rejected = results.find((result) => result.status === 'rejected');
821
+ if (rejected) {
822
+ const reason = rejected.reason instanceof Error ? rejected.reason.message : String(rejected.reason);
823
+ setSessionStatus(reason || 'XR session failed');
824
+ } else {
825
+ setSessionStatus('XR session requested');
826
+ }
827
+ sync();
828
+ });
829
+
830
+ exitButton?.addEventListener('click', async () => {
831
+ setSessionStatus('Ending XR session');
832
+ await options.viewer.actions.invoke(SKYKIT_ACTIONS.xr.exit, null, {
833
+ source: 'xr-free-roam-dom',
834
+ });
835
+ sync();
836
+ });
837
+
838
+ return {
839
+ sync,
840
+ setSessionStatus,
841
+ refreshXrSupport,
842
+ };
843
+
844
+ function sync() {
845
+ const presenting = options.isPresenting();
846
+ shell?.classList.toggle('is-presenting', presenting);
847
+ if (enterButton instanceof HTMLButtonElement) {
848
+ enterButton.hidden = presenting;
849
+ enterButton.disabled = xrSupported === false || presenting;
850
+ }
851
+ if (exitButton instanceof HTMLButtonElement) {
852
+ exitButton.hidden = !presenting;
853
+ exitButton.disabled = !presenting;
854
+ }
855
+ syncSettingInputs(options.panelState, settingInputs);
856
+ syncSettingOutputs(options.panelState);
857
+ }
858
+
859
+ function setSessionStatus(text) {
860
+ if (sessionStatus) {
861
+ sessionStatus.textContent = text;
862
+ }
863
+ }
864
+
865
+ async function refreshXrSupport() {
866
+ try {
867
+ xrSupported = await globalThis.navigator?.xr?.isSessionSupported?.('immersive-vr') ?? false;
868
+ } catch {
869
+ xrSupported = false;
870
+ }
871
+ if (supportValue) {
872
+ supportValue.textContent = xrSupported ? 'Available' : 'Unavailable';
873
+ }
874
+ sync();
875
+ return xrSupported;
876
+ }
877
+ }
878
+
879
+ function readSettingInput(input, panelState, setConstellationArtEnabled) {
880
+ const field = input.dataset.setting;
881
+ if (field === 'limitingMagnitude') {
882
+ panelState.limitingMagnitude = clampNumber(input.value, 4, 10, panelState.limitingMagnitude);
883
+ } else if (field === 'exposureLog10') {
884
+ panelState.exposureLog10 = clampNumber(input.value, 3.5, 5.5, panelState.exposureLog10);
885
+ } else if (field === 'worldScaleLog10') {
886
+ panelState.worldScaleLog10 = clampNumber(input.value, -3, 0, panelState.worldScaleLog10);
887
+ } else if (field === 'nearFloor') {
888
+ panelState.nearFloor = input.checked;
889
+ } else if (field === 'constellationArt') {
890
+ setConstellationArtEnabled(input.checked);
891
+ }
892
+ }
893
+
894
+ function syncSettingInputs(panelState, inputs) {
895
+ for (const input of inputs) {
896
+ const field = input.dataset.setting;
897
+ if (field === 'limitingMagnitude') {
898
+ input.value = String(panelState.limitingMagnitude);
899
+ } else if (field === 'exposureLog10') {
900
+ input.value = String(panelState.exposureLog10);
901
+ } else if (field === 'worldScaleLog10') {
902
+ input.value = String(panelState.worldScaleLog10);
903
+ } else if (field === 'nearFloor') {
904
+ input.checked = panelState.nearFloor;
905
+ } else if (field === 'constellationArt') {
906
+ input.checked = panelState.constellationArt;
907
+ }
908
+ }
909
+ }
910
+
911
+ function syncSettingOutputs(panelState) {
912
+ for (const output of document.querySelectorAll('[data-setting-value]')) {
913
+ const field = output.dataset.settingValue;
914
+ if (field === 'limitingMagnitude') {
915
+ output.textContent = `Mag ${panelState.limitingMagnitude.toFixed(1)}`;
916
+ } else if (field === 'exposureLog10') {
917
+ output.textContent = Math.round(10 ** panelState.exposureLog10).toLocaleString();
918
+ } else if (field === 'worldScaleLog10') {
919
+ output.textContent = formatWorldScale(10 ** panelState.worldScaleLog10);
920
+ }
921
+ }
922
+ }
923
+
924
+ function formatWorldScale(value) {
925
+ return `${value.toLocaleString('en-US', { maximumSignificantDigits: 3 })} m/pc`;
926
+ }
927
+
928
+ function createEmptyIdentifierFields() {
929
+ return {
930
+ properName: '',
931
+ bayer: '',
932
+ hd: '',
933
+ hip: '',
934
+ gaia: '',
935
+ primaryLabel: '',
936
+ };
937
+ }
938
+
939
+ function createSelectedIdentifierLines(selected) {
940
+ const fields = selected.identifiers ?? createEmptyIdentifierFields();
941
+ const lines = [
942
+ `Proper: ${fields.properName || '-'}`,
943
+ `Bayer: ${fields.bayer || '-'}`,
944
+ `HD: ${fields.hd || '-'}`,
945
+ `HIP: ${fields.hip || '-'}`,
946
+ `Gaia: ${fields.gaia || '-'}`,
947
+ ];
948
+ if (selected.identifierStatus === 'loading') {
949
+ lines.push('Loading identifiers...');
950
+ } else if (selected.identifierStatus === 'error') {
951
+ lines.push('Identifiers unavailable');
952
+ } else if (selected.identifierStatus === 'unavailable') {
953
+ lines.push('No catalog identifiers');
954
+ }
955
+ return lines;
956
+ }
957
+
536
958
  function createRightHandTouchPointerSource(raySource) {
537
959
  const controls = createSkykitXrControlBindings({
538
960
  buttons: {
@@ -557,10 +979,10 @@ function createRightHandTouchPointerSource(raySource) {
557
979
 
558
980
  const select = controls.getButton('select');
559
981
  const phase = select.pressedEdge ? 'down' : select.releasedEdge ? 'up' : 'move';
560
- return {
561
- pointerId: 'right-trigger',
562
- pointerType: 'xr',
563
- handedness: 'right',
982
+ return {
983
+ pointerId: 'right-trigger',
984
+ pointerType: 'ray',
985
+ handedness: 'right',
564
986
  phase,
565
987
  timestamp: skykitFrame.elapsedSeconds * 1000,
566
988
  sourceId: 'right-controller',
@@ -661,20 +1083,12 @@ function createSelectedStarTarget() {
661
1083
  const context = canvas.getContext('2d');
662
1084
  const center = canvas.width / 2;
663
1085
  context.clearRect(0, 0, canvas.width, canvas.height);
664
- context.strokeStyle = 'rgba(255, 218, 136, 0.98)';
665
- context.lineWidth = 5;
666
- context.beginPath();
667
- context.arc(center, center, 34, 0, Math.PI * 2);
668
- context.stroke();
1086
+ context.strokeStyle = 'rgba(84, 255, 172, 0.96)';
1087
+ context.lineWidth = 7;
1088
+ context.shadowColor = 'rgba(84, 255, 172, 0.48)';
1089
+ context.shadowBlur = 10;
669
1090
  context.beginPath();
670
- context.moveTo(center - 52, center);
671
- context.lineTo(center - 22, center);
672
- context.moveTo(center + 22, center);
673
- context.lineTo(center + 52, center);
674
- context.moveTo(center, center - 52);
675
- context.lineTo(center, center - 22);
676
- context.moveTo(center, center + 22);
677
- context.lineTo(center, center + 52);
1091
+ context.arc(center, center, 42, 0, Math.PI * 2);
678
1092
  context.stroke();
679
1093
 
680
1094
  const texture = new THREE.CanvasTexture(canvas);
@@ -683,13 +1097,15 @@ function createSelectedStarTarget() {
683
1097
  transparent: true,
684
1098
  depthTest: false,
685
1099
  depthWrite: false,
686
- sizeAttenuation: false,
1100
+ sizeAttenuation: true,
687
1101
  });
688
1102
  const object3d = new THREE.Sprite(material);
689
1103
  object3d.name = 'xr-selected-star-target';
690
1104
  object3d.visible = false;
691
1105
  object3d.renderOrder = 10_000;
692
- object3d.scale.set(0.085, 0.085, 1);
1106
+ object3d.scale.set(1, 1, 1);
1107
+ const worldPosition = new THREE.Vector3();
1108
+ const cameraPosition = new THREE.Vector3();
693
1109
 
694
1110
  return {
695
1111
  object3d,
@@ -697,6 +1113,17 @@ function createSelectedStarTarget() {
697
1113
  object3d.position.set(position.x, position.y, position.z);
698
1114
  object3d.visible = true;
699
1115
  },
1116
+ clear() {
1117
+ object3d.visible = false;
1118
+ },
1119
+ update(camera) {
1120
+ if (!object3d.visible) return;
1121
+ object3d.getWorldPosition(worldPosition);
1122
+ camera.getWorldPosition(cameraPosition);
1123
+ const distance = Math.max(worldPosition.distanceTo(cameraPosition), 0.001);
1124
+ const diameter = Math.max(distance * 0.032, 0.035);
1125
+ object3d.scale.set(diameter, diameter, 1);
1126
+ },
700
1127
  dispose() {
701
1128
  object3d.parent?.remove(object3d);
702
1129
  material.dispose();