spacemap 0.1.17 → 0.1.19

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/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*! spacemap SDK — https://spacemap.co — Mozilla Public License 2.0. Bundles MIT and ISC libraries: THIRD_PARTY_NOTICES.txt in the spacemap npm package. */
2
- import { ACESFilmicToneMapping, AdditiveBlending, AmbientLight, ArrowHelper, BackSide, Box3, BufferAttribute, BufferGeometry, CanvasTexture, ClampToEdgeWrapping, Color, CubeTexture, CustomBlending, DataArrayTexture, DataTexture, DataUtils, DepthTexture, DirectionalLight, DoubleSide, Euler, Float32BufferAttribute, FloatType, FrontSide, Group, HalfFloatType, InstancedBufferAttribute, InstancedMesh, Line, LineBasicMaterial, LineLoop, LinearFilter, LinearMipmapLinearFilter, LinearSRGBColorSpace, MathUtils, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshStandardMaterial, NoBlending, NoColorSpace, NormalBlending, OneFactor, OneMinusSrcAlphaFactor, PCFShadowMap, PCFSoftShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, RGBAFormat, RGFormat, RawShaderMaterial, Raycaster, RedFormat, RepeatWrapping, RingGeometry, SRGBColorSpace, Scene, ShaderChunk, ShaderMaterial, ShadowMaterial, Shape, ShapeGeometry, ShapeUtils, SphereGeometry, Sprite, SpriteMaterial, TOUCH, Texture, TextureLoader, Uint16BufferAttribute, UnsignedByteType, Vector2, Vector3, Vector4, WebGLRenderTarget, WebGLRenderer } from "three";
2
+ import { ACESFilmicToneMapping, AddEquation, AdditiveBlending, AmbientLight, ArrowHelper, BackSide, Box3, BufferAttribute, BufferGeometry, CanvasTexture, ClampToEdgeWrapping, Color, CubeTexture, CustomBlending, DataArrayTexture, DataTexture, DataUtils, DepthTexture, DirectionalLight, DoubleSide, Euler, Float32BufferAttribute, FloatType, FrontSide, Group, HalfFloatType, InstancedBufferAttribute, InstancedMesh, Line, LineBasicMaterial, LineLoop, LinearFilter, LinearMipmapLinearFilter, LinearSRGBColorSpace, MathUtils, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshStandardMaterial, NoBlending, NoColorSpace, NormalBlending, OneFactor, OneMinusDstColorFactor, OneMinusSrcAlphaFactor, PCFShadowMap, PCFSoftShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, RGBAFormat, RGFormat, RawShaderMaterial, Raycaster, RedFormat, RepeatWrapping, RingGeometry, SRGBColorSpace, Scene, ShaderChunk, ShaderMaterial, ShadowMaterial, Shape, ShapeGeometry, ShapeUtils, SphereGeometry, Sprite, SpriteMaterial, TOUCH, Texture, TextureLoader, Uint16BufferAttribute, UnsignedByteType, Vector2, Vector3, Vector4, WebGLRenderTarget, WebGLRenderer } from "three";
3
3
  import { RoomEnvironment } from "three/addons/environments/RoomEnvironment.js";
4
4
  import { CSS2DObject } from "three/addons/renderers/CSS2DRenderer.js";
5
5
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
@@ -22,6 +22,7 @@ var DEFAULT_HOST = {
22
22
  imagesUrl: "https://images.spacemap.co",
23
23
  liveUrl: "https://live.spacemap.co",
24
24
  tilesUrl: "",
25
+ extrasolarUrl: "",
25
26
  locale: () => "en",
26
27
  messages: {
27
28
  body_note_no_model: () => "no model available",
@@ -31,6 +32,7 @@ var DEFAULT_HOST = {
31
32
  scene_canvas_label: () => "Interactive 3D map of the Solar System",
32
33
  attribution_orbits: () => "Orbits",
33
34
  attribution_imagery: () => "Imagery",
35
+ attribution_stars: () => "Stars",
34
36
  credits_see_all: () => "See full credits",
35
37
  layer_surface: () => "Surface",
36
38
  layer_clouds: () => "Clouds",
@@ -69,6 +71,7 @@ function configureHost(overrides) {
69
71
  current$1.imagesUrl = current$1.imagesUrl.replace(/\/$/, "");
70
72
  current$1.liveUrl = current$1.liveUrl.replace(/\/$/, "");
71
73
  current$1.tilesUrl = current$1.tilesUrl.replace(/\/$/, "");
74
+ current$1.extrasolarUrl = current$1.extrasolarUrl.replace(/\/$/, "");
72
75
  }
73
76
  function host() {
74
77
  return current$1;
@@ -3003,10 +3006,10 @@ function buildVersionedUrl(base, path, cls) {
3003
3006
 
3004
3007
  //#endregion
3005
3008
  //#region src/lib/fetch/position/format.ts
3006
- var MAGIC$2 = 1346456915;
3009
+ var MAGIC$3 = 1346456915;
3007
3010
  var COMMON_HEADER_SIZE = 24;
3008
3011
  var EXTENSION_SIZE = 8;
3009
- var HEADER_SIZE$2 = 24 + 8;
3012
+ var HEADER_SIZE$3 = 24 + 8;
3010
3013
  var labelsUrl = (lang) => `${dataBase()}/v1/labels/${lang}.gz`;
3011
3014
  function zoomSegment(zoom) {
3012
3015
  return zoom === null ? "" : `${zoom}/`;
@@ -3073,6 +3076,9 @@ function buildObjectId(idType, value) {
3073
3076
 
3074
3077
  //#endregion
3075
3078
  //#region src/lib/types/objects.ts
3079
+ function isExtrasolar(body) {
3080
+ return body.data.objectType === ObjectType.EXTRASOLAR;
3081
+ }
3076
3082
  function isSurfaceFeature(body) {
3077
3083
  return body.data.objectType === ObjectType.SURFACE_FEATURE;
3078
3084
  }
@@ -3094,6 +3100,7 @@ var ObjectType = /* @__PURE__ */ function(ObjectType) {
3094
3100
  ObjectType[ObjectType["DEBRIS"] = 14] = "DEBRIS";
3095
3101
  ObjectType[ObjectType["UNDOCUMENTED"] = 15] = "UNDOCUMENTED";
3096
3102
  ObjectType[ObjectType["SURFACE_FEATURE"] = 16] = "SURFACE_FEATURE";
3103
+ ObjectType[ObjectType["EXTRASOLAR"] = 17] = "EXTRASOLAR";
3097
3104
  return ObjectType;
3098
3105
  }({});
3099
3106
  var ZONE_A_RANGE = {
@@ -3177,7 +3184,8 @@ var NOMINAL_RADIUS_KM_BY_SOURCE = {
3177
3184
  var NOMINAL_RADIUS_KM_BY_TYPE = {
3178
3185
  [ObjectType.SPACECRAFT]: .005,
3179
3186
  [ObjectType.DEBRIS]: .005,
3180
- [ObjectType.SURFACE_FEATURE]: .001
3187
+ [ObjectType.SURFACE_FEATURE]: .001,
3188
+ [ObjectType.EXTRASOLAR]: 695700
3181
3189
  };
3182
3190
  var NOMINAL_RADIUS_KM_DEFAULT = .1;
3183
3191
  function effectiveRadiusKm(data) {
@@ -3292,6 +3300,13 @@ var CreditsStore = class {
3292
3300
  set skybox(value) {
3293
3301
  set(this.#skybox, value, true);
3294
3302
  }
3303
+ #stars = state([]);
3304
+ get stars() {
3305
+ return get(this.#stars);
3306
+ }
3307
+ set stars(value) {
3308
+ set(this.#stars, value);
3309
+ }
3295
3310
  #orbitSources = state(proxy(/* @__PURE__ */ new Set()));
3296
3311
  get orbitSources() {
3297
3312
  return get(this.#orbitSources);
@@ -3342,6 +3357,9 @@ var CreditsStore = class {
3342
3357
  imageryOf(layer) {
3343
3358
  return [...this.imagery.values()].filter((c) => c.layer === layer);
3344
3359
  }
3360
+ setStars(credits) {
3361
+ if (this.stars.length === 0) this.stars = credits;
3362
+ }
3345
3363
  registerOrientation(credit) {
3346
3364
  if (this.orientation.has(credit.bodyId)) return;
3347
3365
  this.orientation.set(credit.bodyId, credit);
@@ -3446,7 +3464,7 @@ function rowKey(cols, i) {
3446
3464
  return objectKey(cols.idType, cols.id[i]);
3447
3465
  }
3448
3466
  function parseSharedColumns(buffer, rowCount) {
3449
- let offset = HEADER_SIZE$2;
3467
+ let offset = HEADER_SIZE$3;
3450
3468
  const id = new Int32Array(buffer, offset, rowCount);
3451
3469
  offset += align8(rowCount * 4);
3452
3470
  const objectType = new Uint8Array(buffer, offset, rowCount);
@@ -5315,6 +5333,12 @@ function equatorialToScene(xEq, yEq, zEq, out) {
5315
5333
  target.z = -(yEq * COS_OBL$1 + zEq * SIN_OBL$1);
5316
5334
  return target;
5317
5335
  }
5336
+ function sceneToEquatorial(x, y, z, out) {
5337
+ out.x = x;
5338
+ out.y = -y * SIN_OBL$1 - z * COS_OBL$1;
5339
+ out.z = y * COS_OBL$1 - z * SIN_OBL$1;
5340
+ return out;
5341
+ }
5318
5342
 
5319
5343
  //#endregion
5320
5344
  //#region src/lib/time/jd.ts
@@ -6385,7 +6409,7 @@ function resolveBodyColor(data) {
6385
6409
  const planetId = `naif-${data.id.replace("naif-", "")}99`;
6386
6410
  if (BODY_COLORS[planetId]) return BODY_COLORS[planetId];
6387
6411
  }
6388
- if (objectType === ObjectType.STAR) return TYPE_COLOR_STAR;
6412
+ if (objectType === ObjectType.STAR || objectType === ObjectType.EXTRASOLAR) return TYPE_COLOR_STAR;
6389
6413
  if (objectType === ObjectType.PLANET || objectType === ObjectType.DWARF_PLANET) return TYPE_COLOR_PLANET;
6390
6414
  if (objectType === ObjectType.MOON) return TYPE_COLOR_MOON;
6391
6415
  if (isAsteroid(objectType)) return TYPE_COLOR_ASTEROID;
@@ -6724,12 +6748,14 @@ function adoptRow(held, fresh) {
6724
6748
  held.orbitCenterId = fresh.orbitCenterId ?? held.orbitCenterId;
6725
6749
  held.trailAnchorId = fresh.trailAnchorId;
6726
6750
  }
6751
+ var EXTRASOLAR_KEPT = 64;
6727
6752
  var BodyIndex = class {
6728
6753
  bodiesById = /* @__PURE__ */ new Map();
6729
6754
  majorBodies = [];
6730
6755
  asteroidBodiesByZone = /* @__PURE__ */ new Map();
6731
6756
  spacecraftByParent = /* @__PURE__ */ new Map();
6732
6757
  focusFeature = null;
6758
+ extrasolar = /* @__PURE__ */ new Map();
6733
6759
  dirtyAsteroidZones = /* @__PURE__ */ new Set();
6734
6760
  dirtySpacecraftGroups = /* @__PURE__ */ new Set();
6735
6761
  minorStreaming = false;
@@ -6764,6 +6790,8 @@ var BodyIndex = class {
6764
6790
  const major = this.bodiesById.get(id);
6765
6791
  if (major) return major;
6766
6792
  if (this.focusFeature?.data.id === id) return this.focusFeature;
6793
+ const star = this.extrasolar.get(id);
6794
+ if (star) return star;
6767
6795
  if (zone !== void 0) return this.spacecraftByParent.get(zone)?.get(id) ?? this.asteroidBodiesByZone.get(zone)?.get(id);
6768
6796
  for (const byId of this.spacecraftByParent.values()) {
6769
6797
  const hit = byId.get(id);
@@ -6774,6 +6802,13 @@ var BodyIndex = class {
6774
6802
  if (hit) return hit;
6775
6803
  }
6776
6804
  }
6805
+ addExtrasolar(body, ids = []) {
6806
+ for (const id of [body.data.id, ...ids]) {
6807
+ this.extrasolar.delete(id);
6808
+ this.extrasolar.set(id, body);
6809
+ }
6810
+ while (this.extrasolar.size > EXTRASOLAR_KEPT) this.extrasolar.delete(this.extrasolar.keys().next().value);
6811
+ }
6777
6812
  addBodies(bodies) {
6778
6813
  let added = false;
6779
6814
  for (const b of bodies) {
@@ -7210,6 +7245,7 @@ var MAP_LAYERS = [
7210
7245
  "spacecraft",
7211
7246
  "satellites",
7212
7247
  "debris",
7248
+ "starfield",
7213
7249
  "orbits",
7214
7250
  "labels",
7215
7251
  "halos",
@@ -7358,6 +7394,117 @@ function computeVisibilityFromRatio(ratio, thresholds, focusedMultiplier, isFocu
7358
7394
  return VISIBILITY.HIDE;
7359
7395
  }
7360
7396
 
7397
+ //#endregion
7398
+ //#region src/lib/scene/visibility/switch-fade.ts
7399
+ var SYSTEM_SWITCH_FADE_MS = 300;
7400
+ var SwitchFade = class {
7401
+ progress;
7402
+ target;
7403
+ started;
7404
+ constructor(on, durationMs = 300) {
7405
+ this.durationMs = durationMs;
7406
+ this.started = on !== void 0;
7407
+ this.progress = this.target = on ? 1 : 0;
7408
+ }
7409
+ step(on, dtMs) {
7410
+ this.target = on ? 1 : 0;
7411
+ if (!this.started) {
7412
+ this.started = true;
7413
+ this.progress = this.target;
7414
+ return;
7415
+ }
7416
+ const delta = dtMs / this.durationMs;
7417
+ this.progress = this.target === 1 ? Math.min(this.progress + delta, 1) : Math.max(this.progress - delta, 0);
7418
+ }
7419
+ get share() {
7420
+ const p = this.progress;
7421
+ return p * p * (3 - 2 * p);
7422
+ }
7423
+ get fading() {
7424
+ return this.progress !== this.target;
7425
+ }
7426
+ };
7427
+
7428
+ //#endregion
7429
+ //#region src/lib/scene/visibility/zone-fade.ts
7430
+ var ZoneFades = class {
7431
+ main = new SwitchFade(true);
7432
+ systems = [];
7433
+ stepped = false;
7434
+ step(activeSystemId, dtMs) {
7435
+ if (!this.stepped) {
7436
+ this.stepped = true;
7437
+ this.main = new SwitchFade(activeSystemId === null);
7438
+ if (activeSystemId !== null) this.systems.push({
7439
+ id: activeSystemId,
7440
+ fade: new SwitchFade(true)
7441
+ });
7442
+ return;
7443
+ }
7444
+ this.main.step(activeSystemId === null, dtMs);
7445
+ if (activeSystemId !== null && !this.systems.some((s) => s.id === activeSystemId)) this.systems.push({
7446
+ id: activeSystemId,
7447
+ fade: new SwitchFade(false)
7448
+ });
7449
+ for (let i = this.systems.length - 1; i >= 0; i--) {
7450
+ const { id, fade } = this.systems[i];
7451
+ fade.step(id === activeSystemId, dtMs);
7452
+ if (fade.share === 0 && !fade.fading) this.systems.splice(i, 1);
7453
+ }
7454
+ }
7455
+ get mainShare() {
7456
+ return this.main.share;
7457
+ }
7458
+ get fading() {
7459
+ return this.main.fading || this.systems.some((s) => s.fade.fading);
7460
+ }
7461
+ };
7462
+
7463
+ //#endregion
7464
+ //#region src/lib/scene/visibility/threshold-fade.ts
7465
+ var VisibilityThresholdFades = class {
7466
+ labels = /* @__PURE__ */ new Map();
7467
+ orbits = /* @__PURE__ */ new Map();
7468
+ frame = 0;
7469
+ dtMs = 0;
7470
+ beginFrame(dtMs) {
7471
+ this.frame++;
7472
+ this.dtMs = dtMs;
7473
+ this.prune(this.labels);
7474
+ this.prune(this.orbits);
7475
+ }
7476
+ labelShare(id, shown) {
7477
+ return this.share(this.labels, id, shown);
7478
+ }
7479
+ orbitShare(id, shown) {
7480
+ return this.share(this.orbits, id, shown);
7481
+ }
7482
+ get fading() {
7483
+ return this.anyFading(this.labels) || this.anyFading(this.orbits);
7484
+ }
7485
+ share(entries, id, shown) {
7486
+ let entry = entries.get(id);
7487
+ if (!entry) {
7488
+ entry = {
7489
+ fade: new SwitchFade(shown, 100),
7490
+ frame: this.frame
7491
+ };
7492
+ entries.set(id, entry);
7493
+ } else if (entry.frame !== this.frame) {
7494
+ entry.frame = this.frame;
7495
+ entry.fade.step(shown, this.dtMs);
7496
+ }
7497
+ return entry.fade.share;
7498
+ }
7499
+ anyFading(entries) {
7500
+ for (const entry of entries.values()) if (entry.frame === this.frame && entry.fade.fading) return true;
7501
+ return false;
7502
+ }
7503
+ prune(entries) {
7504
+ for (const [id, entry] of entries) if (entry.frame < this.frame - 1) entries.delete(id);
7505
+ }
7506
+ };
7507
+
7361
7508
  //#endregion
7362
7509
  //#region src/lib/scene/visibility/controller.svelte.ts
7363
7510
  var ALL_LAYERS = new LayerSet();
@@ -7383,6 +7530,10 @@ var VisibilityController = class {
7383
7530
  }
7384
7531
  activeSystemId = null;
7385
7532
  isZoomedIn = false;
7533
+ zones = new ZoneFades();
7534
+ thresholds = new VisibilityThresholdFades();
7535
+ systemShare = 1;
7536
+ starShare = 0;
7386
7537
  focusedBodyIdPlain = SUN_ID;
7387
7538
  focusedSystemIdPlain = null;
7388
7539
  travelSystemActive = false;
@@ -7429,6 +7580,19 @@ var VisibilityController = class {
7429
7580
  this.recomputeFullMoons();
7430
7581
  }
7431
7582
  }
7583
+ stepZones(dtMs) {
7584
+ this.zones.step(this.activeSystemId, dtMs);
7585
+ this.thresholds.beginFrame(dtMs);
7586
+ }
7587
+ labelThresholdShare(id, shown) {
7588
+ return this.thresholds.labelShare(id, shown);
7589
+ }
7590
+ orbitThresholdShare(id, shown) {
7591
+ return this.thresholds.orbitShare(id, shown);
7592
+ }
7593
+ get fading() {
7594
+ return this.zones.fading || this.thresholds.fading;
7595
+ }
7432
7596
  refreshProbeFocusedSystem() {
7433
7597
  const ps = this.getProbeStore();
7434
7598
  if (!ps) return;
@@ -7442,6 +7606,7 @@ var VisibilityController = class {
7442
7606
  this.lastRecomputeDist = -1;
7443
7607
  }
7444
7608
  resolveSystemId(body) {
7609
+ if (isExtrasolar(body)) return null;
7445
7610
  const isSystemBarycenter = body.data.objectType === ObjectType.BARYCENTER && isTopLevelParent(body.data.parentId) && !isTopLevelParent(body.data.id);
7446
7611
  const isTopLevel = body.data.objectType === ObjectType.STAR || !isSystemBarycenter && isTopLevelParent(body.data.parentId);
7447
7612
  if (isSystemBarycenter) return body.data.id;
@@ -7474,6 +7639,7 @@ var VisibilityController = class {
7474
7639
  this.recomputeFullMoons();
7475
7640
  }
7476
7641
  getMoonVisibility(moon) {
7642
+ if (this.systemShare === 0) return VISIBILITY.HIDE;
7477
7643
  if (this.getLayers().hidesBody(moon.data.objectType, moon.data.parentId)) return VISIBILITY.HIDE;
7478
7644
  const cached = this.moonVisibilityCache.get(moon.data.id);
7479
7645
  if (cached !== void 0) return cached;
@@ -7498,6 +7664,7 @@ var VisibilityController = class {
7498
7664
  return this.pinned.has(id);
7499
7665
  }
7500
7666
  getPlanetVisibility(body, camDistThreeJS) {
7667
+ if (this.systemShare === 0 && body.data.objectType !== ObjectType.STAR) return VISIBILITY.HIDE;
7501
7668
  const pinned = this.pinned.has(body.data.id);
7502
7669
  const vis = this.ratioVisibility(body, camDistThreeJS, pinned);
7503
7670
  return pinned && vis !== VISIBILITY.CLOSE ? VISIBILITY.FULL : vis;
@@ -7556,15 +7723,24 @@ var VisibilityController = class {
7556
7723
  return sysNaif !== null && this.isInFocusedSystem(`naif-${sysNaif}`);
7557
7724
  }
7558
7725
  hasFullRendering(body) {
7559
- if (!this.activeSystemId || this.pinned.has(body.data.id)) return true;
7560
- if (this.isInActiveSystem(isTopLevelParent(body.data.parentId) ? body.data.id : body.data.parentId)) return true;
7561
- if (body.data.orbitalSource !== OrbitalSource.SPICE_PROBE) return false;
7562
- const ps = this.getProbeStore();
7563
- if (!ps) return false;
7564
- const sysNaif = ps.containingSystemAt(body.data.id, this.currentJd);
7565
- return sysNaif !== null && this.isInActiveSystem(`naif-${sysNaif}`);
7726
+ return this.fullRenderingShare(body) > 0;
7727
+ }
7728
+ fullRenderingShare(body) {
7729
+ const main = this.zones.mainShare;
7730
+ if (main === 1 || this.pinned.has(body.data.id)) return 1;
7731
+ const home = isTopLevelParent(body.data.parentId) ? body.data.id : body.data.parentId;
7732
+ const probeSystem = this.probeSystemId(body);
7733
+ let share = main;
7734
+ for (const { id, fade } of this.zones.systems) if (this.bodies.isInSystem(home, id) || probeSystem !== null && this.bodies.isInSystem(probeSystem, id)) share += fade.share;
7735
+ return Math.min(share, 1);
7736
+ }
7737
+ probeSystemId(body) {
7738
+ if (body.data.orbitalSource !== OrbitalSource.SPICE_PROBE) return null;
7739
+ const sysNaif = this.getProbeStore()?.containingSystemAt(body.data.id, this.currentJd);
7740
+ return sysNaif === null || sysNaif === void 0 ? null : `naif-${sysNaif}`;
7566
7741
  }
7567
7742
  isMoonGroupVisible(parentId) {
7743
+ if (this.systemShare === 0) return false;
7568
7744
  if (!this.getLayers().isVisible("moons")) return false;
7569
7745
  if (!this.isInFocusedSystem(parentId)) return false;
7570
7746
  const maxA = this.bodies.maxMoonA(parentId);
@@ -7572,19 +7748,25 @@ var VisibilityController = class {
7572
7748
  return this.cameraDistThreeJS / 10 / maxA <= this.scaledPlanetary[VISIBILITY.FAR];
7573
7749
  }
7574
7750
  isSpacecraftGroupVisible(groupParentId) {
7575
- if (this.getLayers().hidesSpacecraftGroup(groupParentId)) return false;
7751
+ return this.spacecraftGroupShare(groupParentId) > 0;
7752
+ }
7753
+ spacecraftGroupShare(groupParentId) {
7754
+ if (this.systemShare === 0) return 0;
7755
+ if (this.getLayers().hidesSpacecraftGroup(groupParentId)) return 0;
7576
7756
  const parent = this.bodies.bodiesById.get(groupParentId);
7577
- if (groupParentId !== "naif-0" && !parent?.position) return false;
7578
- const sysId = this.activeSystemId;
7579
- if (isTopLevelParent(groupParentId)) return !sysId;
7580
- if (parent?.data.objectType === ObjectType.STAR) return !sysId;
7581
- if (!sysId) return false;
7582
- if (groupParentId === sysId) return true;
7583
- return this.bodies.getChildren(sysId)?.has(groupParentId) ?? false;
7757
+ if (groupParentId !== "naif-0" && !parent?.position) return 0;
7758
+ if (isTopLevelParent(groupParentId)) return this.zones.mainShare;
7759
+ if (parent?.data.objectType === ObjectType.STAR) return this.zones.mainShare;
7760
+ for (const { id, fade } of this.zones.systems) if (this.bodies.isInSystem(groupParentId, id)) return fade.share;
7761
+ return 0;
7762
+ }
7763
+ asteroidGroupShare(zone) {
7764
+ return this.isAsteroidGroupVisible(zone) ? this.zones.mainShare : 0;
7584
7765
  }
7585
7766
  isAsteroidGroupVisible(zone) {
7767
+ if (this.systemShare === 0) return false;
7586
7768
  if (this.getLayers().hidesZone(zone)) return false;
7587
- if (this.activeSystemId) return false;
7769
+ if (this.zones.mainShare === 0) return false;
7588
7770
  if (!this.bodies.bodiesById.get("naif-10")?.position) return false;
7589
7771
  const filter = this.getSmallBodyFilter();
7590
7772
  if (filter && zone.startsWith("small_bodies/")) {
@@ -7745,7 +7927,7 @@ function parseChebyshevPayload$1(buffer, startJd, endJd) {
7745
7927
  const float64Coeffs = (view.getUint8(28) & 1) !== 0;
7746
7928
  const coeffBytes = float64Coeffs ? 8 : 4;
7747
7929
  const bodies = [];
7748
- let offset = HEADER_SIZE$2;
7930
+ let offset = HEADER_SIZE$3;
7749
7931
  for (let b = 0; b < bodyCount; b++) {
7750
7932
  const naifId = view.getInt32(offset, true);
7751
7933
  const parentId = view.getInt32(offset + 4, true);
@@ -7905,7 +8087,7 @@ function parseProbesPayload(buffer, startJd, endJd, float64Coeffs) {
7905
8087
  const subchunkS = subchunkDays * SECONDS_PER_DAY$2;
7906
8088
  const chunkStartEt = tdbJdToEt(startJd);
7907
8089
  const probes = [];
7908
- let offset = HEADER_SIZE$2;
8090
+ let offset = HEADER_SIZE$3;
7909
8091
  for (let p = 0; p < probeCount; p++) {
7910
8092
  const objIdValue = view.getInt32(offset, true);
7911
8093
  const idTypeOrdinal = view.getUint8(offset + 4);
@@ -8129,7 +8311,7 @@ var SEC_PER_DAY = 86400;
8129
8311
  var AU_PER_DAY_TO_KM_PER_SEC = AU_KM / SEC_PER_DAY;
8130
8312
  var SMALL_E = 1e-9;
8131
8313
  var SMALL_SIN_I = 1e-9;
8132
- function clamp$3(x, lo, hi) {
8314
+ function clamp$4(x, lo, hi) {
8133
8315
  return x < lo ? lo : x > hi ? hi : x;
8134
8316
  }
8135
8317
  function stateVectorToElements(r, v, muAuDay2, jd) {
@@ -8143,7 +8325,7 @@ function stateVectorToElements(r, v, muAuDay2, jd) {
8143
8325
  const hz = rx * vy - ry * vx;
8144
8326
  const hNorm = Math.hypot(hx, hy, hz);
8145
8327
  if (hNorm === 0) return null;
8146
- const iRad = Math.acos(clamp$3(hz / hNorm, -1, 1));
8328
+ const iRad = Math.acos(clamp$4(hz / hNorm, -1, 1));
8147
8329
  const sinI = Math.sin(iRad);
8148
8330
  const evX = (vy * hz - vz * hy) / muAuDay2 - rx / rNorm;
8149
8331
  const evY = (vz * hx - vx * hz) / muAuDay2 - ry / rNorm;
@@ -8158,7 +8340,7 @@ function stateVectorToElements(r, v, muAuDay2, jd) {
8158
8340
  let omRad;
8159
8341
  if (sinI < SMALL_SIN_I || nNorm === 0) omRad = 0;
8160
8342
  else {
8161
- omRad = Math.acos(clamp$3(nx / nNorm, -1, 1));
8343
+ omRad = Math.acos(clamp$4(nx / nNorm, -1, 1));
8162
8344
  if (ny < 0) omRad = 2 * Math.PI - omRad;
8163
8345
  }
8164
8346
  let wRad;
@@ -8169,7 +8351,7 @@ function stateVectorToElements(r, v, muAuDay2, jd) {
8169
8351
  if (wRad < 0) wRad += 2 * Math.PI;
8170
8352
  } else {
8171
8353
  const cosW = (nx * evX + ny * evY) / (nNorm * e);
8172
- wRad = Math.acos(clamp$3(cosW, -1, 1));
8354
+ wRad = Math.acos(clamp$4(cosW, -1, 1));
8173
8355
  if (evZ < 0) wRad = 2 * Math.PI - wRad;
8174
8356
  }
8175
8357
  let nuRad;
@@ -8180,11 +8362,11 @@ function stateVectorToElements(r, v, muAuDay2, jd) {
8180
8362
  if (nuRad < 0) nuRad += 2 * Math.PI;
8181
8363
  } else if (e < SMALL_E) {
8182
8364
  const cosU = (nx * rx + ny * ry) / (nNorm * rNorm);
8183
- nuRad = Math.acos(clamp$3(cosU, -1, 1));
8365
+ nuRad = Math.acos(clamp$4(cosU, -1, 1));
8184
8366
  if (rz < 0) nuRad = 2 * Math.PI - nuRad;
8185
8367
  } else {
8186
8368
  const cosNu = (evX * rx + evY * ry + evZ * rz) / (e * rNorm);
8187
- nuRad = Math.acos(clamp$3(cosNu, -1, 1));
8369
+ nuRad = Math.acos(clamp$4(cosNu, -1, 1));
8188
8370
  if (rDotV < 0) nuRad = 2 * Math.PI - nuRad;
8189
8371
  }
8190
8372
  let maRad;
@@ -9998,6 +10180,46 @@ async function dataVersionChanged() {
9998
10180
  }
9999
10181
  }
10000
10182
 
10183
+ //#endregion
10184
+ //#region src/lib/fetch/extrasolar/ids.ts
10185
+ var CELL_LEVEL = 10;
10186
+ var CELL_COUNT = 12 * 4 ** 10;
10187
+ var FIRST_TABLE = 1;
10188
+ var LAST_TABLE = 30;
10189
+ var CELL_SPAN = 2 ** 24;
10190
+ var TABLE_SPAN = 32;
10191
+ var SERIAL_SPAN = 2 ** 29;
10192
+ var ID_TEXT = /^[1-9][0-9]{0,15}$/;
10193
+ var EXTRASOLAR_PREFIX = "x-";
10194
+ function decodeId(id) {
10195
+ return {
10196
+ serial: Math.floor(id / SERIAL_SPAN),
10197
+ table: Math.floor(id / CELL_SPAN) % TABLE_SPAN,
10198
+ cell: id % CELL_SPAN
10199
+ };
10200
+ }
10201
+ function cellOf(id) {
10202
+ return id % CELL_SPAN;
10203
+ }
10204
+ function tableOf(id) {
10205
+ return Math.floor(id / CELL_SPAN) % TABLE_SPAN;
10206
+ }
10207
+ function parseExtrasolarId(text, tables) {
10208
+ if (!ID_TEXT.test(text)) return null;
10209
+ const id = Number(text);
10210
+ if (id > Number.MAX_SAFE_INTEGER) return null;
10211
+ const { table, cell } = decodeId(id);
10212
+ if (cell >= CELL_COUNT) return null;
10213
+ if (tables ? !tables.has(table) : table < 1 || table > 30) return null;
10214
+ return id;
10215
+ }
10216
+ function isExtrasolarObjectId(objectId) {
10217
+ return objectId.startsWith("x-");
10218
+ }
10219
+ function extrasolarObjectId(id) {
10220
+ return `${"x-"}${id}`;
10221
+ }
10222
+
10001
10223
  //#endregion
10002
10224
  //#region src/lib/fetch/gz.ts
10003
10225
  function gunzip(res) {
@@ -10100,6 +10322,10 @@ function isViewable(entry) {
10100
10322
  return textureAllowed(entry.distribution);
10101
10323
  }
10102
10324
  function fetchObjectDetail(fileId, fetchLocalized = true, lang = getLocale(), background = false) {
10325
+ if (isExtrasolarObjectId(fileId)) return Promise.resolve({
10326
+ global: null,
10327
+ localized: null
10328
+ });
10103
10329
  return fetchBundlePair(OBJECT_BUNDLES, fileId, lang, fetchLocalized, background);
10104
10330
  }
10105
10331
 
@@ -10135,6 +10361,585 @@ function planRideMarkers(rides, focusedId, sceneName) {
10135
10361
  };
10136
10362
  }
10137
10363
 
10364
+ //#endregion
10365
+ //#region src/lib/fetch/extrasolar/container.ts
10366
+ var MAGIC$2 = "SMXS";
10367
+ var FORMAT_VERSION = 1;
10368
+ var KIND_NODE = 3;
10369
+ var HEADER_SIZE$2 = 32;
10370
+ var TOKEN_AT = 8;
10371
+ var TOKEN_SIZE = 16;
10372
+ var HAS_VALIDITY = 1;
10373
+ var NODE_INFO_SIZE = 80;
10374
+ var TYPE_CODES = {
10375
+ 1: "bool",
10376
+ 2: "int8",
10377
+ 3: "int16",
10378
+ 4: "int32",
10379
+ 5: "uint8",
10380
+ 6: "uint16",
10381
+ 7: "uint32",
10382
+ 8: "float32",
10383
+ 9: "float64",
10384
+ 10: "id",
10385
+ 11: "string",
10386
+ 12: "dict",
10387
+ 13: "cross_ids",
10388
+ 14: "bits"
10389
+ };
10390
+ var ContainerFormatError = class extends Error {
10391
+ constructor(message) {
10392
+ super(message);
10393
+ this.name = "ContainerFormatError";
10394
+ }
10395
+ };
10396
+ var decoder = new TextDecoder();
10397
+ function bitAt(bytes, i) {
10398
+ return (bytes[i >> 3] >> (i & 7) & 1) === 1;
10399
+ }
10400
+ var StringColumn = class {
10401
+ constructor(offsets, bytes, validity) {
10402
+ this.offsets = offsets;
10403
+ this.bytes = bytes;
10404
+ this.validity = validity;
10405
+ }
10406
+ get length() {
10407
+ return this.offsets.length - 1;
10408
+ }
10409
+ get(i) {
10410
+ if (this.validity && !bitAt(this.validity, i)) return null;
10411
+ return decoder.decode(this.bytes.subarray(this.offsets[i], this.offsets[i + 1]));
10412
+ }
10413
+ };
10414
+ var DictColumn = class {
10415
+ constructor(codes, values, validity) {
10416
+ this.codes = codes;
10417
+ this.values = values;
10418
+ this.validity = validity;
10419
+ }
10420
+ get length() {
10421
+ return this.codes.length;
10422
+ }
10423
+ get(i) {
10424
+ if (this.validity && !bitAt(this.validity, i)) return null;
10425
+ return this.values[this.codes[i]];
10426
+ }
10427
+ };
10428
+ var CrossIdsColumn = class {
10429
+ constructor(list, codes, ids, pairs, validity) {
10430
+ this.list = list;
10431
+ this.codes = codes;
10432
+ this.ids = ids;
10433
+ this.pairs = pairs;
10434
+ this.validity = validity;
10435
+ }
10436
+ get length() {
10437
+ return this.list.length - 1;
10438
+ }
10439
+ get(i) {
10440
+ if (this.validity && !bitAt(this.validity, i)) return null;
10441
+ const out = [];
10442
+ for (let j = this.list[i]; j < this.list[i + 1]; j++) {
10443
+ const code = this.codes[j];
10444
+ out.push({
10445
+ catalogue: this.pairs.get(2 * code) ?? "",
10446
+ release: this.pairs.get(2 * code + 1) ?? "",
10447
+ catalogueId: this.ids.get(j) ?? ""
10448
+ });
10449
+ }
10450
+ return out;
10451
+ }
10452
+ };
10453
+ var BitColumn = class {
10454
+ constructor(bytes, length) {
10455
+ this.bytes = bytes;
10456
+ this.length = length;
10457
+ }
10458
+ get(i) {
10459
+ return bitAt(this.bytes, i);
10460
+ }
10461
+ };
10462
+ var Container = class {
10463
+ kind;
10464
+ token;
10465
+ rows;
10466
+ info;
10467
+ columns = /* @__PURE__ */ new Map();
10468
+ constructor(buffer) {
10469
+ this.buffer = buffer;
10470
+ if (buffer.byteLength < HEADER_SIZE$2) throw new ContainerFormatError("payload too short");
10471
+ const view = new DataView(buffer);
10472
+ if (decoder.decode(new Uint8Array(buffer, 0, 4)) !== "SMXS") throw new ContainerFormatError("not an extrasolar file");
10473
+ const version = view.getUint16(4, true);
10474
+ if (version !== 1) throw new ContainerFormatError(`format version ${version}, this code reads ${1}`);
10475
+ this.kind = view.getUint8(6);
10476
+ this.token = decoder.decode(new Uint8Array(buffer, TOKEN_AT, TOKEN_SIZE));
10477
+ this.rows = view.getUint32(24, true);
10478
+ const count = view.getUint16(28, true);
10479
+ const infoSize = view.getUint16(30, true);
10480
+ this.info = new DataView(buffer, HEADER_SIZE$2, infoSize);
10481
+ let at = HEADER_SIZE$2 + infoSize;
10482
+ for (let c = 0; c < count; c++) {
10483
+ const code = view.getUint8(at);
10484
+ const flags = view.getUint8(at + 1);
10485
+ const buffers = view.getUint8(at + 2);
10486
+ const nameSize = view.getUint8(at + 3);
10487
+ at += 4;
10488
+ const name = decoder.decode(new Uint8Array(buffer, at, nameSize));
10489
+ at += nameSize;
10490
+ const places = [];
10491
+ for (let b = 0; b < buffers; b++) {
10492
+ const offset = view.getUint32(at, true);
10493
+ const length = view.getUint32(at + 4, true);
10494
+ if (offset % 8 !== 0 || offset + length > buffer.byteLength) throw new ContainerFormatError(`${name}: buffer ${b} out of place`);
10495
+ places.push({
10496
+ offset,
10497
+ length
10498
+ });
10499
+ at += 8;
10500
+ }
10501
+ const type = TYPE_CODES[code];
10502
+ if (!type) throw new ContainerFormatError(`${name}: unknown type ${code}`);
10503
+ const first = (flags & HAS_VALIDITY) !== 0 ? places.shift() : void 0;
10504
+ this.columns.set(name, {
10505
+ type,
10506
+ validity: first ? new Uint8Array(buffer, first.offset, first.length) : null,
10507
+ places
10508
+ });
10509
+ }
10510
+ }
10511
+ names() {
10512
+ return [...this.columns.keys()];
10513
+ }
10514
+ has(name) {
10515
+ return this.columns.has(name);
10516
+ }
10517
+ type(name) {
10518
+ return this.entry(name).type;
10519
+ }
10520
+ isValid(name, row) {
10521
+ const { validity } = this.entry(name);
10522
+ return validity === null || bitAt(validity, row);
10523
+ }
10524
+ bool(name) {
10525
+ return this.numbers(name, "bool", Uint8Array);
10526
+ }
10527
+ int8(name) {
10528
+ return this.numbers(name, "int8", Int8Array);
10529
+ }
10530
+ int16(name) {
10531
+ return this.numbers(name, "int16", Int16Array);
10532
+ }
10533
+ int32(name) {
10534
+ return this.numbers(name, "int32", Int32Array);
10535
+ }
10536
+ uint8(name) {
10537
+ return this.numbers(name, "uint8", Uint8Array);
10538
+ }
10539
+ uint16(name) {
10540
+ return this.numbers(name, "uint16", Uint16Array);
10541
+ }
10542
+ uint32(name) {
10543
+ return this.numbers(name, "uint32", Uint32Array);
10544
+ }
10545
+ float32(name) {
10546
+ return this.numbers(name, "float32", Float32Array);
10547
+ }
10548
+ float64(name) {
10549
+ return this.numbers(name, "float64", Float64Array);
10550
+ }
10551
+ ids(name) {
10552
+ return this.numbers(name, "id", Float64Array);
10553
+ }
10554
+ strings(name) {
10555
+ const { validity, places } = this.typed(name, "string");
10556
+ return this.stringList(places[0], places[1], validity);
10557
+ }
10558
+ dict(name) {
10559
+ const { validity, places } = this.typed(name, "dict");
10560
+ const values = this.stringList(places[1], places[2], null);
10561
+ const strings = [];
10562
+ for (let k = 0; k < values.length; k++) strings.push(values.get(k) ?? "");
10563
+ return new DictColumn(this.view(places[0], Uint16Array), strings, validity);
10564
+ }
10565
+ crossIds(name) {
10566
+ const { validity, places } = this.typed(name, "cross_ids");
10567
+ return new CrossIdsColumn(this.view(places[0], Uint32Array), this.view(places[1], Uint16Array), this.stringList(places[2], places[3], null), this.stringList(places[4], places[5], null), validity);
10568
+ }
10569
+ bits(name) {
10570
+ const { places } = this.typed(name, "bits");
10571
+ return new BitColumn(this.view(places[0], Uint8Array), this.rows);
10572
+ }
10573
+ entry(name) {
10574
+ const entry = this.columns.get(name);
10575
+ if (!entry) throw new ContainerFormatError(`no column ${name}`);
10576
+ return entry;
10577
+ }
10578
+ typed(name, type) {
10579
+ const entry = this.entry(name);
10580
+ if (entry.type !== type) throw new ContainerFormatError(`${name} is ${entry.type}, not ${type}`);
10581
+ return entry;
10582
+ }
10583
+ numbers(name, type, Kind) {
10584
+ return this.view(this.typed(name, type).places[0], Kind);
10585
+ }
10586
+ view(place, Kind) {
10587
+ return new Kind(this.buffer, place.offset, place.length / Kind.BYTES_PER_ELEMENT);
10588
+ }
10589
+ stringList(offsets, bytes, validity) {
10590
+ return new StringColumn(this.view(offsets, Uint32Array), new Uint8Array(this.buffer, bytes.offset, bytes.length), validity);
10591
+ }
10592
+ };
10593
+ function nodeInfo(container) {
10594
+ const info = container.info;
10595
+ if (container.kind !== 3 || info.byteLength !== NODE_INFO_SIZE) throw new ContainerFormatError("not a render node");
10596
+ const childRange = new Float32Array(8);
10597
+ for (let k = 0; k < 8; k++) childRange[k] = info.getFloat32(40 + 4 * k, true);
10598
+ return {
10599
+ centre: [
10600
+ info.getFloat64(0, true),
10601
+ info.getFloat64(8, true),
10602
+ info.getFloat64(16, true)
10603
+ ],
10604
+ half: info.getFloat64(24, true),
10605
+ minRange: info.getFloat32(32, true),
10606
+ maxRange: info.getFloat32(36, true),
10607
+ childRange,
10608
+ depth: info.getUint8(72),
10609
+ childMask: info.getUint8(73)
10610
+ };
10611
+ }
10612
+
10613
+ //#endregion
10614
+ //#region src/lib/fetch/extrasolar/files.ts
10615
+ var EXTRASOLAR_FORMAT = 1;
10616
+ var ExtrasolarStaleError = class extends Error {
10617
+ constructor(path, found, expected) {
10618
+ super(`${path}: token ${found}, expected ${expected}`);
10619
+ this.name = "ExtrasolarStaleError";
10620
+ }
10621
+ };
10622
+ function extrasolarBase() {
10623
+ return host().extrasolarUrl;
10624
+ }
10625
+ function extrasolarAllowed(entry) {
10626
+ return textureAllowed(entry.distribution);
10627
+ }
10628
+ function extrasolarPath(pattern, fields) {
10629
+ return pattern.replace(/\{(\w+)\}/g, (whole, key) => {
10630
+ const value = fields[key];
10631
+ if (value === void 0) throw new Error(`extrasolarPath: no ${key} for ${pattern}`);
10632
+ return String(value);
10633
+ });
10634
+ }
10635
+ function extrasolarUrl(path, token) {
10636
+ return `${extrasolarBase()}/v1/x/${path}?v=${token}`;
10637
+ }
10638
+ var metadataBase = null;
10639
+ var metadataPending = null;
10640
+ function fetchExtrasolarMetadata() {
10641
+ const base = extrasolarBase();
10642
+ if (metadataPending && metadataBase === base) return metadataPending;
10643
+ metadataBase = base;
10644
+ if (base === "") {
10645
+ metadataPending = Promise.resolve(null);
10646
+ return metadataPending;
10647
+ }
10648
+ const p = (async () => {
10649
+ const res = await fetchWithTimeout(`${base}/v1/x/metadata.json`, { cache: "no-cache" });
10650
+ if (!res.ok) throw new Error(`extrasolar metadata.json returned ${res.status}`);
10651
+ const meta = await res.json();
10652
+ if (meta.format !== 1) throw new Error(`extrasolar format ${meta.format}, this code reads ${1}`);
10653
+ return meta;
10654
+ })();
10655
+ metadataPending = p;
10656
+ p.catch(() => {
10657
+ if (metadataPending === p) metadataPending = null;
10658
+ });
10659
+ return p;
10660
+ }
10661
+ var staleListeners = /* @__PURE__ */ new Set();
10662
+ var staleReported = false;
10663
+ function onExtrasolarStale(listener) {
10664
+ staleListeners.add(listener);
10665
+ return () => staleListeners.delete(listener);
10666
+ }
10667
+ function reportStale() {
10668
+ if (staleReported) return;
10669
+ staleReported = true;
10670
+ for (const listener of staleListeners) listener();
10671
+ }
10672
+ async function fetchExtrasolarFile(path, token, options = {}) {
10673
+ const url = extrasolarUrl(path, token);
10674
+ const res = await fetchWithTimeout(url, {
10675
+ signal: options.signal,
10676
+ priority: options.priority
10677
+ });
10678
+ if (!res.ok) throw new Error(`${url} returned ${res.status}`);
10679
+ const container = new Container(await gunzip(res).arrayBuffer());
10680
+ if (container.token !== token) {
10681
+ reportStale();
10682
+ throw new ExtrasolarStaleError(path, container.token, token);
10683
+ }
10684
+ return container;
10685
+ }
10686
+
10687
+ //#endregion
10688
+ //#region src/lib/fetch/extrasolar/split.ts
10689
+ var ROOTS = 12;
10690
+ var SplitTree = class SplitTree {
10691
+ constructor(first, level) {
10692
+ this.first = first;
10693
+ this.level = level;
10694
+ }
10695
+ static fromBits(bits) {
10696
+ const first = [];
10697
+ const level = [];
10698
+ let at = 0;
10699
+ const visit = (depth, pixel) => {
10700
+ if (at >= bits.length) throw new ContainerFormatError("split table too short");
10701
+ if (!bits.get(at++)) {
10702
+ first.push(pixel * 4 ** (10 - depth));
10703
+ level.push(depth);
10704
+ return;
10705
+ }
10706
+ if (depth === 10) throw new ContainerFormatError("a cell cannot split");
10707
+ for (let k = 0; k < 4; k++) visit(depth + 1, 4 * pixel + k);
10708
+ };
10709
+ for (let root = 0; root < ROOTS; root++) visit(0, root);
10710
+ if (at !== bits.length) throw new ContainerFormatError(`split table has ${bits.length} bits, the tree ${at}`);
10711
+ return new SplitTree(Uint32Array.from(first), Uint8Array.from(level));
10712
+ }
10713
+ get leaves() {
10714
+ return this.first.length;
10715
+ }
10716
+ tileOf(cell) {
10717
+ const first = this.first;
10718
+ let lo = 0;
10719
+ let hi = first.length - 1;
10720
+ while (lo < hi) {
10721
+ const mid = lo + hi + 1 >> 1;
10722
+ if (first[mid] <= cell) lo = mid;
10723
+ else hi = mid - 1;
10724
+ }
10725
+ const level = this.level[lo];
10726
+ return {
10727
+ level,
10728
+ pixel: first[lo] / 4 ** (10 - level)
10729
+ };
10730
+ }
10731
+ };
10732
+ function findId(ids, id) {
10733
+ let lo = 0;
10734
+ let hi = ids.length - 1;
10735
+ while (lo <= hi) {
10736
+ const mid = lo + hi >>> 1;
10737
+ const at = ids[mid];
10738
+ if (at === id) return mid;
10739
+ if (at < id) lo = mid + 1;
10740
+ else hi = mid - 1;
10741
+ }
10742
+ return -1;
10743
+ }
10744
+
10745
+ //#endregion
10746
+ //#region src/lib/fetch/extrasolar/lookup.ts
10747
+ var METADATA_SLUG = "metadata";
10748
+ var TableRow = class {
10749
+ constructor(slug, table, container, row) {
10750
+ this.slug = slug;
10751
+ this.table = table;
10752
+ this.container = container;
10753
+ this.row = row;
10754
+ }
10755
+ has(name) {
10756
+ return this.container.has(name);
10757
+ }
10758
+ number(name) {
10759
+ const c = this.container;
10760
+ if (!c.has(name) || !c.isValid(name, this.row)) return null;
10761
+ let value;
10762
+ switch (c.type(name)) {
10763
+ case "float32":
10764
+ value = c.float32(name)[this.row];
10765
+ break;
10766
+ case "float64":
10767
+ value = c.float64(name)[this.row];
10768
+ break;
10769
+ case "int8":
10770
+ value = c.int8(name)[this.row];
10771
+ break;
10772
+ case "int16":
10773
+ value = c.int16(name)[this.row];
10774
+ break;
10775
+ case "int32":
10776
+ value = c.int32(name)[this.row];
10777
+ break;
10778
+ case "uint8":
10779
+ value = c.uint8(name)[this.row];
10780
+ break;
10781
+ case "uint16":
10782
+ value = c.uint16(name)[this.row];
10783
+ break;
10784
+ case "uint32":
10785
+ value = c.uint32(name)[this.row];
10786
+ break;
10787
+ case "id":
10788
+ value = c.ids(name)[this.row];
10789
+ break;
10790
+ default: throw new Error(`${this.slug}.${name} is not a number column`);
10791
+ }
10792
+ return Number.isNaN(value) ? null : value;
10793
+ }
10794
+ text(name) {
10795
+ const c = this.container;
10796
+ if (!c.has(name)) return null;
10797
+ const type = c.type(name);
10798
+ if (type === "dict") return c.dict(name).get(this.row);
10799
+ if (type === "string") return c.strings(name).get(this.row);
10800
+ throw new Error(`${this.slug}.${name} is not a text column`);
10801
+ }
10802
+ flag(name) {
10803
+ const c = this.container;
10804
+ if (!c.has(name) || !c.isValid(name, this.row)) return null;
10805
+ return c.bool(name)[this.row] === 1;
10806
+ }
10807
+ };
10808
+ var DEFAULT_LIMITS = {
10809
+ tiles: 32,
10810
+ results: 64
10811
+ };
10812
+ var MIN_PARALLAX_SNR = 5;
10813
+ function parallaxDistancePc(row) {
10814
+ const parallax = row.number("parallax");
10815
+ const error = row.number("parallax_error");
10816
+ if (parallax === null || error === null || !(parallax / error > MIN_PARALLAX_SNR)) return null;
10817
+ return 1e3 / parallax;
10818
+ }
10819
+ function homeRow(record) {
10820
+ return record.home && record.rows.get(record.home) || null;
10821
+ }
10822
+ function hasDistance(record) {
10823
+ const home = homeRow(record);
10824
+ return home !== null && parallaxDistancePc(home) !== null;
10825
+ }
10826
+ function readMetadataRow(c, row) {
10827
+ const alias = c.isValid("alias_target", row) ? c.ids("alias_target")[row] : 0;
10828
+ return {
10829
+ id: c.ids("id")[row],
10830
+ name: c.strings("name").get(row),
10831
+ nameSource: c.dict("name_source").get(row),
10832
+ class: c.dict("class").get(row),
10833
+ classSource: c.dict("class_source").get(row),
10834
+ crossIds: c.crossIds("cross_ids").get(row) ?? [],
10835
+ mask: c.uint32("mask")[row],
10836
+ objectFile: c.strings("object_file").get(row),
10837
+ aliasTarget: alias > 0 ? alias : null
10838
+ };
10839
+ }
10840
+ var ExtrasolarLookup = class {
10841
+ splits = /* @__PURE__ */ new Map();
10842
+ tiles;
10843
+ results;
10844
+ constructor(source, limits = DEFAULT_LIMITS) {
10845
+ this.source = source;
10846
+ this.tiles = new LruPromiseCache(limits.tiles);
10847
+ this.results = new LruPromiseCache(limits.results);
10848
+ }
10849
+ async live(id, options = {}) {
10850
+ const first = await this.lookup(id, options);
10851
+ return first.kind === "alias" ? this.lookup(first.target, options) : first;
10852
+ }
10853
+ lookup(id, options = {}) {
10854
+ const data = options.data ?? true;
10855
+ return this.results.getOrCompute(`${id}:${data}`, () => this.resolve(String(id), data));
10856
+ }
10857
+ async resolve(text, data) {
10858
+ const meta = await this.source.metadata();
10859
+ if (!meta) return {
10860
+ kind: "missing",
10861
+ reason: "unconfigured"
10862
+ };
10863
+ const numbered = /* @__PURE__ */ new Map();
10864
+ for (const [slug, table] of Object.entries(meta.tables)) if (table.number !== null) numbered.set(table.number, {
10865
+ slug,
10866
+ table
10867
+ });
10868
+ const id = parseExtrasolarId(text, new Set(numbered.keys()));
10869
+ if (id === null) return {
10870
+ kind: "missing",
10871
+ reason: "id"
10872
+ };
10873
+ const metaTable = meta.tables[METADATA_SLUG];
10874
+ if (!metaTable || !this.source.allowed(metaTable)) return {
10875
+ kind: "missing",
10876
+ reason: "barred"
10877
+ };
10878
+ const cell = cellOf(id);
10879
+ const home = numbered.get(tableOf(id));
10880
+ const homeShown = data && this.source.allowed(home.table);
10881
+ const [metaTile, homeTile] = await Promise.all([this.tile(METADATA_SLUG, metaTable, cell), homeShown ? this.tile(home.slug, home.table, cell) : null]);
10882
+ const metaRow = findId(metaTile.ids("id"), id);
10883
+ if (metaRow < 0) return {
10884
+ kind: "missing",
10885
+ reason: "no-row"
10886
+ };
10887
+ const metadata = readMetadataRow(metaTile, metaRow);
10888
+ if (metadata.aliasTarget !== null) return {
10889
+ kind: "alias",
10890
+ id,
10891
+ target: metadata.aliasTarget
10892
+ };
10893
+ const rows = /* @__PURE__ */ new Map();
10894
+ const adopt = (entry, tile) => {
10895
+ const row = findId(tile.ids("id"), id);
10896
+ if (row >= 0) rows.set(entry.slug, new TableRow(entry.slug, entry.table, tile, row));
10897
+ };
10898
+ if (homeTile) adopt(home, homeTile);
10899
+ if (data) {
10900
+ const others = [];
10901
+ for (let n = 1; n <= 30; n++) {
10902
+ const entry = numbered.get(n);
10903
+ if (!(metadata.mask >>> n & 1) || !entry || entry === home) continue;
10904
+ if (this.source.allowed(entry.table)) others.push(entry);
10905
+ }
10906
+ const tiles = await Promise.all(others.map((entry) => this.tile(entry.slug, entry.table, cell)));
10907
+ others.forEach((entry, i) => adopt(entry, tiles[i]));
10908
+ }
10909
+ return {
10910
+ kind: "live",
10911
+ record: {
10912
+ id,
10913
+ metadata,
10914
+ rows,
10915
+ home: home.slug,
10916
+ credits: meta.credits
10917
+ }
10918
+ };
10919
+ }
10920
+ split(slug, table) {
10921
+ const key = `${slug}?v=${table.token}`;
10922
+ const held = this.splits.get(key);
10923
+ if (held) return held;
10924
+ const p = this.source.file(table.split, table.token).then((c) => SplitTree.fromBits(c.bits("split")));
10925
+ this.splits.set(key, p);
10926
+ p.catch(() => {
10927
+ if (this.splits.get(key) === p) this.splits.delete(key);
10928
+ });
10929
+ return p;
10930
+ }
10931
+ async tile(slug, table, cell) {
10932
+ const split = await this.split(slug, table);
10933
+ const path = extrasolarPath(table.tile, split.tileOf(cell));
10934
+ return this.tiles.getOrCompute(`${path}?v=${table.token}`, () => this.source.file(path, table.token));
10935
+ }
10936
+ };
10937
+ var extrasolarLookup = new ExtrasolarLookup({
10938
+ metadata: fetchExtrasolarMetadata,
10939
+ file: (path, token) => fetchExtrasolarFile(path, token),
10940
+ allowed: extrasolarAllowed
10941
+ });
10942
+
10138
10943
  //#endregion
10139
10944
  //#region src/lib/fetch/coverage.ts
10140
10945
  var EDGE_INSET_JD = 1 / 1440;
@@ -10147,6 +10952,10 @@ async function earthZoneEndJd() {
10147
10952
  return zoom && isDateSegmented(zoom) ? unixMsToJD(lastCoveredMs(zoom)) : Infinity;
10148
10953
  }
10149
10954
  async function coverageOf(id) {
10955
+ if (isExtrasolarObjectId(id)) {
10956
+ const result = await extrasolarLookup.live(id.slice("x-".length));
10957
+ return result.kind === "live" && hasDistance(result.record) ? [[-Infinity, Infinity]] : [];
10958
+ }
10150
10959
  const detail = await fetchObjectDetail(id, false);
10151
10960
  const windows = detail.global?.coverage?.windows ?? [];
10152
10961
  const tle = detail.global?.orbit?.scale === "planet";
@@ -10378,6 +11187,30 @@ function applySouthTowardParent(obj, bodyPos, parentPos) {
10378
11187
  function applyUpVector(obj, dir) {
10379
11188
  obj.quaternion.setFromUnitVectors(LOCAL_NORTH, dir);
10380
11189
  }
11190
+ function landedHeadingAt(headings, jd) {
11191
+ let deg;
11192
+ for (const h of headings ?? []) if (h.jd <= jd) deg = h.deg;
11193
+ return deg;
11194
+ }
11195
+ var LOCAL_FRONT = new Vector3(0, 0, 1);
11196
+ var _up = new Vector3();
11197
+ var _front = new Vector3();
11198
+ var _north = new Vector3();
11199
+ var _east = new Vector3();
11200
+ var _target = new Vector3();
11201
+ var _cross = new Vector3();
11202
+ var _turn = new Quaternion();
11203
+ function turnToHeading(obj, north, headingDeg) {
11204
+ _up.copy(LOCAL_NORTH).applyQuaternion(obj.quaternion);
11205
+ _north.copy(north).addScaledVector(_up, -north.dot(_up)).normalize();
11206
+ _east.crossVectors(_north, _up);
11207
+ const h = headingDeg * DEG2RAD$5;
11208
+ _target.copy(_north).multiplyScalar(Math.cos(h)).addScaledVector(_east, Math.sin(h));
11209
+ _front.copy(LOCAL_FRONT).applyQuaternion(obj.quaternion);
11210
+ _front.addScaledVector(_up, -_front.dot(_up)).normalize();
11211
+ const angle = Math.atan2(_cross.crossVectors(_front, _target).dot(_up), _front.dot(_target));
11212
+ obj.quaternion.premultiply(_turn.setFromAxisAngle(_up, angle));
11213
+ }
10381
11214
  var AXIS_VECTORS = {
10382
11215
  "+x": new Vector3(1, 0, 0),
10383
11216
  "-x": new Vector3(-1, 0, 0),
@@ -11405,7 +12238,7 @@ function isLowEndDevice() {
11405
12238
  return typeof mem === "number" && mem > 0 && mem <= LOW_MEMORY_GIB;
11406
12239
  }
11407
12240
  var coarsePointer;
11408
- function isCoarsePointer() {
12241
+ function isCoarsePointer$1() {
11409
12242
  if (coarsePointer === void 0) coarsePointer = typeof window !== "undefined" && !!window.matchMedia?.("(pointer: coarse)").matches;
11410
12243
  return coarsePointer;
11411
12244
  }
@@ -11526,7 +12359,7 @@ var ATMOSPHERE_QUALITY_PRESETS = {
11526
12359
  }
11527
12360
  };
11528
12361
  function heuristicAtmosphereTier() {
11529
- if (isCoarsePointer()) return isLowEndDevice() ? "low" : "medium";
12362
+ if (isCoarsePointer$1()) return isLowEndDevice() ? "low" : "medium";
11530
12363
  return isLowEndDevice() ? "high" : "ultra";
11531
12364
  }
11532
12365
  function resolveAtmosphereTier(tier) {
@@ -12893,7 +13726,7 @@ function attachCanvasForwarders(el, canvas) {
12893
13726
 
12894
13727
  //#endregion
12895
13728
  //#region src/lib/scene/relief/worker.ts?worker&inline
12896
- var jsContent$1 = "function e(e,t,n,r){return`${e&&`${e}/`}${t}/${n}/${r}.webp`}function t(e,t,n,r,i=1){let a=t.length,o=n.length,s=new Float32Array(a*o*3),c=new Float32Array(a*o*3),l=new Float32Array(a*o*2),u=Float64Array.from(n,Math.sin),d=Float64Array.from(n,Math.cos);for(let f=0;f<a;f++){let a=t[f],p=Math.sin(a),m=Math.cos(a);for(let t=0;t<o;t++){let h=f*o+t,g=-d[t]*p,_=m,v=u[t]*p,y=e+(r?r[h]*i:0),b=h*3;s[b]=y*g,s[b+1]=y*_,s[b+2]=y*v,c[b]=g,c[b+1]=_,c[b+2]=v;let x=h*2;l[x]=n[t]/(2*Math.PI),l[x+1]=1-a/Math.PI}}let f=new Uint32Array((a-1)*(o-1)*6),p=0;for(let e=0;e<a-1;e++){let n=t[e]<=0,r=t[e+1]>=Math.PI;for(let t=0;t<o-1;t++){let i=e*o+t+1,a=e*o+t,s=(e+1)*o+t,c=(e+1)*o+t+1;n||(f[p++]=i,f[p++]=a,f[p++]=c),r||(f[p++]=a,f[p++]=s,f[p++]=c)}}return{positions:s,normals:c,uvs:l,indices:f.subarray(0,p)}}const n=[``,`north`,`south`],r=Math.SQRT2-1;function i(e,t){return e===0?2<<t:1<<t-1}function a(e,t){return e===0?1<<t:1<<t-1}function o(e,t,n,i){if(e===0){let e=Math.atan2(i,-t)/(2*Math.PI);return{x:e<0?e+1:e,y:Math.acos(Math.min(Math.max(n,-1),1))/Math.PI}}let a=e===1?1:-1,o=.5/(r*(1+a*n));return{x:.5+t*o,y:.5+a*i*o}}function s(e,t,n,r){let i=e.max_level-t;return i>=0&&n>=e.x[0]>>i&&n<=e.x[1]>>i&&r>=e.y[0]>>i&&r<=e.y[1]>>i}function c(e,t,n,r){return e?.find(e=>s(e,t,n,r))}const l=Math.sin(60*Math.PI/180),u=2**18,d=2**17;function f(e,t,n,r){return((e*32+t)*u+n)*d+r}function p(e){let t=e%d,n=Math.floor(e/d)%u,r=Math.floor(e/(d*u));return{chart:Math.floor(r/32),level:r%32,x:n,y:t}}const m={chart:0,level:0,x0:0,x1:0,y0:0,y1:0,fx:0,fy:0};function h(e,t,n,r,s){let c=e<1||Math.abs(n)<=l?0:n>0?1:2,u=i(c,e)*s,d=a(c,e)*s,f=o(c,t,n,r),p=f.x*u-.5,h=f.y*d-.5,g=Math.floor(p),_=Math.floor(h),v=e=>Math.min(Math.max(e,0),u-1),y=e=>Math.min(Math.max(e,0),d-1);return m.chart=c,m.level=e,m.fx=p-g,m.fy=h-_,m.x0=c===0?(g+u)%u:v(g),m.x1=c===0?(g+1)%u:v(g+1),m.y0=y(_),m.y1=y(_+1),m}function g(e,t,n){let r=Float64Array.from(t.at,Math.sin),i=Float64Array.from(t.at,Math.cos),a=t.at.length;for(let o=0;o<e.at.length;o++){let s=Math.sin(e.at[o]),c=Math.cos(e.at[o]);for(let l=0;l<a;l++){let u=Math.min(e.level[o],t.level[l]);n(o*a+l,u,-i[l]*s,c,r[l]*s)}}}function _(e,t,n,r=1/0,i=()=>!0){let a=new Set([f(0,0,0,0),f(0,0,1,0)]);return g(e,t,(e,t,o,s,c)=>{for(let e=t;;e--){let t=h(e,o,s,c,n),l=Math.floor(t.x0/n),u=Math.floor(t.x1/n),d=Math.floor(t.y0/n),p=Math.floor(t.y1/n);if(!(e>r&&!(i(t.chart,e,l,d)&&i(t.chart,e,u,d)&&i(t.chart,e,l,p)&&i(t.chart,e,u,p)))){a.add(f(t.chart,e,l,d)),u!==l&&a.add(f(t.chart,e,u,d)),p!==d&&(a.add(f(t.chart,e,l,p)),u!==l&&a.add(f(t.chart,e,u,p)));return}}}),a}function v(e,t,n,r,i,a){for(let o=t;o>=0;o--){let t=h(o,n,r,i,a),s=(n,r)=>{let i=e(f(t.chart,o,Math.floor(n/a),Math.floor(r/a)));return i&&i[r%a*a+n%a]},c=s(t.x0,t.y0),l=s(t.x1,t.y0),u=s(t.x0,t.y1),d=s(t.x1,t.y1);if(c===void 0||l===void 0||u===void 0||d===void 0)continue;let p=c+(l-c)*t.fx;return p+(u+(d-u)*t.fx-p)*t.fy}}function y(e,t,n,r,i,a){let o=new Float32Array(e.at.length*t.at.length),s=!1;return g(e,t,(e,t,c,l,u)=>{let d=v(n,t,c,l,u,r);d===void 0?s=!0:o[e]=a+i*d/65535}),s?null:o}function b(e,t){let n=new Uint8Array(t*t*2);for(let r=0;r<t;r++){let i=(t-1-r)*t*4,a=r*t*2;for(let r=0;r<t;r++,i+=4,a+=2)n[a]=e[i],n[a+1]=e[i+1]}return n}function x(e,t){let n=new Float32Array(t*t);for(let r=0;r<t;r++){let i=(t-1-r)*t*4,a=(r+1)*t;for(let o=r*t;o<a;o++,i+=4)n[o]=(e[i]<<8|e[i+1])+e[i+2]/256}return n}function S(e,t,n){let r=n?new Float32Array(t*t):new Uint16Array(t*t);for(let t=0,i=0;t<r.length;t++,i+=4){let a=e[i]<<8|e[i+1];r[t]=n?a+e[i+2]/256:a}return r}const C=self,w=new Map;let T;async function E(e,t){let n=await fetch(e,{signal:t});if(!n.ok)throw Error(`tile ${n.status}`);let r=await createImageBitmap(await n.blob(),{premultiplyAlpha:`none`,colorSpaceConversion:`none`}),i=r.width;T?.width!==i&&(T=new OffscreenCanvas(i,i));let a=T.getContext(`2d`,{willReadFrequently:!0});if(!a)throw Error(`no 2d canvas in this worker`);return a.globalCompositeOperation=`copy`,a.drawImage(r,0,0),r.close(),{rgba:a.getImageData(0,0,i,i).data,size:i}}async function D(e,t){let{rgba:n,size:r}=await E(e.url,t),i=e.fine?x(n,r):b(n,r);C.postMessage({type:`tile`,id:e.id,size:r,heights:i},[i.buffer])}const O=new Map;let k=``;async function A(r,i){let{rows:a,columns:o,tileSize:s,maxLevel:l,patches:u}=r,d=[r.version,...u.map(e=>e.version)],f=`${r.root}?${d.join()}`;f!==k&&O.clear(),k=f;let m=(e,t,n,r)=>e===0?c(u,t,n,r):void 0,h=_(a,o,s,l,(e,t,n,r)=>m(e,t,n,r)!==void 0),g=[...h].filter(e=>!O.has(e));await Promise.all(Array.from({length:6},async()=>{for(let t=g.pop();t!==void 0;t=g.pop()){let{chart:a,level:o,x:s,y:c}=p(t),u=o>l,d=u?m(a,o,s,c)?.version:r.version,f=`${r.root}${e(n[a],o,s,c)}?v=${d}`;try{let{rgba:e,size:n}=await E(f,i);O.set(t,S(e,n,u))}catch{if(i.aborted)return}}})),i.throwIfAborted();let v=y(a,o,e=>O.get(e),s,r.scaleKm,r.biasKm);for(let e of[...O.keys()])h.has(e)||O.delete(e);if(!v)throw Error(`no height tiles for the grid`);let b=t(r.radius,a.at,o.at,v,r.unitsPerKm),x=b.indices.slice();C.postMessage({type:`grid`,id:r.id,...b,indices:x,riseKm:v},[b.positions.buffer,b.normals.buffer,b.uvs.buffer,x.buffer,v.buffer])}async function j(e){let t=new AbortController;w.set(e.id,t);try{e.type===`tile`?await D(e,t.signal):await A(e,t.signal)}catch{C.postMessage({type:`failed`,id:e.id})}finally{w.delete(e.id)}}self.onmessage=e=>{let t=e.data;t.type===`abort`?w.get(t.id)?.abort():j(t)};";
13729
+ var jsContent$1 = "function e(e,t,n,r){return`${e&&`${e}/`}${t}/${n}/${r}.webp`}function t(e,t,n,r,i=1){let a=t.length,o=n.length,s=new Float32Array(a*o*3),c=new Float32Array(a*o*3),l=new Float32Array(a*o*2),u=Float64Array.from(n,Math.sin),d=Float64Array.from(n,Math.cos);for(let f=0;f<a;f++){let a=t[f],p=Math.sin(a),m=Math.cos(a);for(let t=0;t<o;t++){let h=f*o+t,g=-d[t]*p,_=m,v=u[t]*p,y=e+(r?r[h]*i:0),b=h*3;s[b]=y*g,s[b+1]=y*_,s[b+2]=y*v,c[b]=g,c[b+1]=_,c[b+2]=v;let x=h*2;l[x]=n[t]/(2*Math.PI),l[x+1]=1-a/Math.PI}}let f=new Uint32Array((a-1)*(o-1)*6),p=0;for(let e=0;e<a-1;e++){let n=t[e]<=0,r=t[e+1]>=Math.PI;for(let t=0;t<o-1;t++){let i=e*o+t+1,a=e*o+t,s=(e+1)*o+t,c=(e+1)*o+t+1;n||(f[p++]=i,f[p++]=a,f[p++]=c),r||(f[p++]=a,f[p++]=s,f[p++]=c)}}return{positions:s,normals:c,uvs:l,indices:f.subarray(0,p)}}const n=[``,`north`,`south`],r=Math.SQRT2-1;function i(e,t){return e===0?2<<t:1<<t-1}function a(e,t){return e===0?1<<t:1<<t-1}function o(e,t,n,i){if(e===0){let e=Math.atan2(i,-t)/(2*Math.PI);return{x:e<0?e+1:e,y:Math.acos(Math.min(Math.max(n,-1),1))/Math.PI}}let a=e===1?1:-1,o=.5/(r*(1+a*n));return{x:.5+t*o,y:.5+a*i*o}}function s(e){return e.cap===`north`?1:e.cap===`south`?2:0}function c(e,t,n,r,i){let a=e.max_level-n;return a>=0&&s(e)===t&&r>=e.x[0]>>a&&r<=e.x[1]>>a&&i>=e.y[0]>>a&&i<=e.y[1]>>a}function l(e,t,n,r,i){return e?.find(e=>c(e,t,n,r,i))}const u=Math.sin(60*Math.PI/180),d=2**18,f=2**17;function p(e,t,n,r){return((e*32+t)*d+n)*f+r}function m(e){let t=e%f,n=Math.floor(e/f)%d,r=Math.floor(e/(f*d));return{chart:Math.floor(r/32),level:r%32,x:n,y:t}}const h={chart:0,level:0,x0:0,x1:0,y0:0,y1:0,fx:0,fy:0};function g(e,t){return e<1||Math.abs(t)<=u?0:t>0?1:2}function _(e,t,n,r,s){let c=g(e,n),l=i(c,e)*s,u=a(c,e)*s,d=o(c,t,n,r),f=d.x*l-.5,p=d.y*u-.5,m=Math.floor(f),_=Math.floor(p),v=e=>Math.min(Math.max(e,0),l-1),y=e=>Math.min(Math.max(e,0),u-1);return h.chart=c,h.level=e,h.fx=f-m,h.fy=p-_,h.x0=c===0?(m+l)%l:v(m),h.x1=c===0?(m+1)%l:v(m+1),h.y0=y(_),h.y1=y(_+1),h}function v(e,t,n){let r=Float64Array.from(t.at,Math.sin),i=Float64Array.from(t.at,Math.cos),a=t.at.length;for(let o=0;o<e.at.length;o++){let s=Math.sin(e.at[o]),c=Math.cos(e.at[o]);for(let l=0;l<a;l++){let u=Math.min(e.level[o],t.level[l]);n(o*a+l,u,-i[l]*s,c,r[l]*s)}}}function y(e,t,n,r=1/0,i=()=>!0){let a=new Set([p(0,0,0,0),p(0,0,1,0)]);return v(e,t,(e,t,o,s,c)=>{for(let e=t;;e--){let t=_(e,o,s,c,n),l=Math.floor(t.x0/n),u=Math.floor(t.x1/n),d=Math.floor(t.y0/n),f=Math.floor(t.y1/n);if(!(e>r&&!(i(t.chart,e,l,d)&&i(t.chart,e,u,d)&&i(t.chart,e,l,f)&&i(t.chart,e,u,f)))){a.add(p(t.chart,e,l,d)),u!==l&&a.add(p(t.chart,e,u,d)),f!==d&&(a.add(p(t.chart,e,l,f)),u!==l&&a.add(p(t.chart,e,u,f)));return}}}),a}function b(e,t,n,r,i,a){for(let o=t;o>=0;o--){let t=_(o,n,r,i,a),s=(n,r)=>{let i=e(p(t.chart,o,Math.floor(n/a),Math.floor(r/a)));return i&&i[r%a*a+n%a]},c=s(t.x0,t.y0),l=s(t.x1,t.y0),u=s(t.x0,t.y1),d=s(t.x1,t.y1);if(c===void 0||l===void 0||u===void 0||d===void 0)continue;let f=c+(l-c)*t.fx;return f+(u+(d-u)*t.fx-f)*t.fy}}function x(e,t,n,r,i,a){let o=new Float32Array(e.at.length*t.at.length),s=!1;return v(e,t,(e,t,c,l,u)=>{let d=b(n,t,c,l,u,r);d===void 0?s=!0:o[e]=a+i*d/65535}),s?null:o}function S(e,t){let n=new Uint8Array(t*t*2);for(let r=0;r<t;r++){let i=(t-1-r)*t*4,a=r*t*2;for(let r=0;r<t;r++,i+=4,a+=2)n[a]=e[i],n[a+1]=e[i+1]}return n}function C(e,t){let n=new Float32Array(t*t);for(let r=0;r<t;r++){let i=(t-1-r)*t*4,a=(r+1)*t;for(let o=r*t;o<a;o++,i+=4)n[o]=(e[i]<<8|e[i+1])+e[i+2]/256}return n}function w(e,t,n){let r=n?new Float32Array(t*t):new Uint16Array(t*t);for(let t=0,i=0;t<r.length;t++,i+=4){let a=e[i]<<8|e[i+1];r[t]=n?a+e[i+2]/256:a}return r}const T=self,E=new Map;let D;async function O(e,t){let n=await fetch(e,{signal:t});if(!n.ok)throw Error(`tile ${n.status}`);let r=await createImageBitmap(await n.blob(),{premultiplyAlpha:`none`,colorSpaceConversion:`none`}),i=r.width;D?.width!==i&&(D=new OffscreenCanvas(i,i));let a=D.getContext(`2d`,{willReadFrequently:!0});if(!a)throw Error(`no 2d canvas in this worker`);return a.globalCompositeOperation=`copy`,a.drawImage(r,0,0),r.close(),{rgba:a.getImageData(0,0,i,i).data,size:i}}async function k(e,t){let{rgba:n,size:r}=await O(e.url,t),i=e.fine?C(n,r):S(n,r);T.postMessage({type:`tile`,id:e.id,size:r,heights:i},[i.buffer])}const A=new Map;let j=``;async function M(r,i){let{rows:a,columns:o,tileSize:s,maxLevel:c,patches:u}=r,d=[r.version,...u.map(e=>e.version)],f=`${r.root}?${d.join()}`;f!==j&&A.clear(),j=f;let p=(e,t,n,r)=>l(u,e,t,n,r),h=y(a,o,s,c,(e,t,n,r)=>p(e,t,n,r)!==void 0),g=[...h].filter(e=>!A.has(e));await Promise.all(Array.from({length:6},async()=>{for(let t=g.pop();t!==void 0;t=g.pop()){let{chart:a,level:o,x:s,y:l}=m(t),u=o>c,d=u?p(a,o,s,l)?.version:r.version,f=`${r.root}${e(n[a],o,s,l)}?v=${d}`;try{let{rgba:e,size:n}=await O(f,i);A.set(t,w(e,n,u))}catch{if(i.aborted)return}}})),i.throwIfAborted();let _=x(a,o,e=>A.get(e),s,r.scaleKm,r.biasKm);for(let e of[...A.keys()])h.has(e)||A.delete(e);if(!_)throw Error(`no height tiles for the grid`);let v=t(r.radius,a.at,o.at,_,r.unitsPerKm),b=v.indices.slice();T.postMessage({type:`grid`,id:r.id,...v,indices:b,riseKm:_},[v.positions.buffer,v.normals.buffer,v.uvs.buffer,b.buffer,_.buffer])}async function N(e){let t=new AbortController;E.set(e.id,t);try{e.type===`tile`?await k(e,t.signal):await M(e,t.signal)}catch{T.postMessage({type:`failed`,id:e.id})}finally{E.delete(e.id)}}self.onmessage=e=>{let t=e.data;t.type===`abort`?E.get(t.id)?.abort():N(t)};";
12897
13730
  var blob$1 = typeof self !== "undefined" && self.Blob && new Blob(["URL.revokeObjectURL(import.meta.url);", jsContent$1], { type: "text/javascript;charset=utf-8" });
12898
13731
  function WorkerWrapper$1(options) {
12899
13732
  let objURL;
@@ -13088,34 +13921,40 @@ function placeWindow(chart, level, x, y, previous) {
13088
13921
  ty0
13089
13922
  };
13090
13923
  }
13091
- function inRange(patch, level, x, y) {
13924
+ function patchChart(patch) {
13925
+ return patch.cap === "north" ? 1 : patch.cap === "south" ? 2 : 0;
13926
+ }
13927
+ function inRange(patch, chart, level, x, y) {
13092
13928
  const up = patch.max_level - level;
13093
- return up >= 0 && x >= patch.x[0] >> up && x <= patch.x[1] >> up && y >= patch.y[0] >> up && y <= patch.y[1] >> up;
13929
+ return up >= 0 && patchChart(patch) === chart && x >= patch.x[0] >> up && x <= patch.x[1] >> up && y >= patch.y[0] >> up && y <= patch.y[1] >> up;
13094
13930
  }
13095
- function patchOfTile(patches, level, x, y) {
13096
- return patches?.find((patch) => inRange(patch, level, x, y));
13931
+ function patchOfTile(patches, chart, level, x, y) {
13932
+ return patches?.find((patch) => inRange(patch, chart, level, x, y));
13097
13933
  }
13098
- function mapTileVersion(pyramid, level, x, y) {
13099
- const over = pyramid.patches?.filter((patch) => level > pyramid.max_level - (patch.map_levels ?? 0) && inRange(patch, level, x, y));
13934
+ function tileVersion(pyramid, chart, level, x, y) {
13935
+ const over = pyramid.patches?.filter((patch) => level > pyramid.max_level - (patch.map_levels ?? 0) && inRange(patch, chart, level, x, y));
13100
13936
  return over?.length ? over.map((patch) => patch.version).join(".") : pyramid.version;
13101
13937
  }
13102
13938
  var REACH_TILES = 4 / 2 - .5;
13103
- function deepestLevel(maxLevel, patches, x, y) {
13939
+ function deepestLevel(maxLevel, patches, chart, x, y) {
13104
13940
  let deepest = maxLevel;
13105
13941
  if (!patches) return deepest;
13106
- for (const patch of patches) for (let level = patch.max_level; level > deepest; level--) {
13107
- const up = patch.max_level - level;
13108
- const columns = chartColumns(0, level);
13109
- const x0 = patch.x[0] >> up;
13110
- const y0 = patch.y[0] >> up;
13111
- const halfWidth = ((patch.x[1] >> up) + 1 - x0) / 2;
13112
- const halfHeight = ((patch.y[1] >> up) + 1 - y0) / 2;
13113
- let east = x * columns - (x0 + halfWidth);
13114
- east -= columns * Math.round(east / columns);
13115
- const south = y * chartRows(0, level) - (y0 + halfHeight);
13116
- if (Math.abs(east) - halfWidth <= REACH_TILES && Math.abs(south) - halfHeight <= REACH_TILES) {
13117
- deepest = level;
13118
- break;
13942
+ for (const patch of patches) {
13943
+ if (patchChart(patch) !== chart) continue;
13944
+ for (let level = patch.max_level; level > deepest; level--) {
13945
+ const up = patch.max_level - level;
13946
+ const columns = chartColumns(chart, level);
13947
+ const x0 = patch.x[0] >> up;
13948
+ const y0 = patch.y[0] >> up;
13949
+ const halfWidth = ((patch.x[1] >> up) + 1 - x0) / 2;
13950
+ const halfHeight = ((patch.y[1] >> up) + 1 - y0) / 2;
13951
+ let east = x * columns - (x0 + halfWidth);
13952
+ if (chart === 0) east -= columns * Math.round(east / columns);
13953
+ const south = y * chartRows(chart, level) - (y0 + halfHeight);
13954
+ if (Math.abs(east) - halfWidth <= REACH_TILES && Math.abs(south) - halfHeight <= REACH_TILES) {
13955
+ deepest = level;
13956
+ break;
13957
+ }
13119
13958
  }
13120
13959
  }
13121
13960
  return deepest;
@@ -13123,7 +13962,7 @@ function deepestLevel(maxLevel, patches, x, y) {
13123
13962
  function windowHome(placement, x, y) {
13124
13963
  const columns = chartColumns(placement.chart, placement.level);
13125
13964
  let east = x * columns - placement.tx0;
13126
- east -= columns * Math.round(east / columns);
13965
+ if (placement.chart === 0) east -= columns * Math.round(east / columns);
13127
13966
  return {
13128
13967
  x: east,
13129
13968
  y: y * chartRows(placement.chart, placement.level) - placement.ty0
@@ -13141,6 +13980,7 @@ function slotTile(placement, sx, sy) {
13141
13980
 
13142
13981
  //#endregion
13143
13982
  //#region src/lib/scene/render-tier.ts
13983
+ var MB = 1024 * 1024;
13144
13984
  var RENDER_TIER_PRESETS = {
13145
13985
  low: {
13146
13986
  maxPixelRatio: 1.25,
@@ -13154,7 +13994,10 @@ var RENDER_TIER_PRESETS = {
13154
13994
  reliefWindows: 2,
13155
13995
  reliefShadowSteps: 12,
13156
13996
  terrainBandCells: 16,
13157
- maxSkyboxFace: 2048
13997
+ maxSkyboxFace: 2048,
13998
+ starPoints: 12e5,
13999
+ starBytes: 80 * MB,
14000
+ starFetches: 3
13158
14001
  },
13159
14002
  medium: {
13160
14003
  maxPixelRatio: 1.5,
@@ -13168,7 +14011,10 @@ var RENDER_TIER_PRESETS = {
13168
14011
  reliefWindows: 3,
13169
14012
  reliefShadowSteps: 16,
13170
14013
  terrainBandCells: 24,
13171
- maxSkyboxFace: 2048
14014
+ maxSkyboxFace: 2048,
14015
+ starPoints: 16e5,
14016
+ starBytes: 160 * MB,
14017
+ starFetches: 4
13172
14018
  },
13173
14019
  high: {
13174
14020
  maxPixelRatio: 2,
@@ -13182,7 +14028,10 @@ var RENDER_TIER_PRESETS = {
13182
14028
  reliefWindows: 4,
13183
14029
  reliefShadowSteps: 32,
13184
14030
  terrainBandCells: 32,
13185
- maxSkyboxFace: Infinity
14031
+ maxSkyboxFace: Infinity,
14032
+ starPoints: 25e5,
14033
+ starBytes: 400 * MB,
14034
+ starFetches: 6
13186
14035
  }
13187
14036
  };
13188
14037
  function maxTextureTier() {
@@ -13204,7 +14053,7 @@ function resolveRenderTier() {
13204
14053
  const measured = sceneSettings().atmosphereCalibration?.tier;
13205
14054
  if (measured === "low" || measured === "medium") return measured;
13206
14055
  if (measured === "high" || measured === "ultra") return "high";
13207
- if (isCoarsePointer()) return isLowEndDevice() ? "low" : "medium";
14056
+ if (isCoarsePointer$1()) return isLowEndDevice() ? "low" : "medium";
13208
14057
  return isLowEndDevice() ? "medium" : "high";
13209
14058
  }
13210
14059
  function currentRenderTier() {
@@ -13214,6 +14063,15 @@ function renderPixelRatio() {
13214
14063
  if (typeof window === "undefined") return 1;
13215
14064
  return Math.min(window.devicePixelRatio, currentRenderTier().maxPixelRatio);
13216
14065
  }
14066
+ function starBudget() {
14067
+ const tier = currentRenderTier();
14068
+ const bytes = isLowEndDevice() ? Math.min(tier.starBytes, 64 * MB) : tier.starBytes;
14069
+ return {
14070
+ points: tier.starPoints,
14071
+ bytes,
14072
+ fetches: tier.starFetches
14073
+ };
14074
+ }
13217
14075
 
13218
14076
  //#endregion
13219
14077
  //#region src/lib/scene/objects/surface/tile-chain.ts
@@ -13247,9 +14105,9 @@ void tileCap( vec3 dir, vec3 ddx, vec3 ddy, out float side, out mat3x2 cap ) {
13247
14105
  );
13248
14106
  }
13249
14107
 
13250
- // The point at + off on the map, counted from at, and on the cap of its own
13251
- // hemisphere, which side gives. A position on the sphere has seven digits,
13252
- // and a tile index of a patch takes them all; the offset keeps its own.
14108
+ // The point at + off on the map and on the cap of its own hemisphere, which
14109
+ // side gives, each counted from at. A position on the sphere has seven
14110
+ // digits, and a tile index of a patch takes them all. The offset keeps its own.
13253
14111
  void tileChartsFrom( vec3 at, vec3 off, out mat3x2 map, out mat3x2 cap, out float side ) {
13254
14112
  vec3 pos = at + off;
13255
14113
  vec3 ox = dFdx( off );
@@ -13271,12 +14129,19 @@ void tileChartsFrom( vec3 at, vec3 off, out mat3x2 map, out mat3x2 cap, out floa
13271
14129
  ( pos.z * oy.x - pos.x * oy.z ) * east, ( pos.y * ( pos.x * oy.x + pos.z * oy.z ) / flat1 - flat1 * oy.y ) * south
13272
14130
  );
13273
14131
 
13274
- // A cap goes no deeper than the pyramid, so the direction itself will do.
13275
14132
  float range = length( pos );
13276
14133
  vec3 dir = pos / range;
13277
14134
  vec3 ddx = ( ox - dir * dot( dir, ox ) ) / range;
13278
14135
  vec3 ddy = ( oy - dir * dot( dir, oy ) ) / range;
13279
14136
  tileCap( dir, ddx, ddy, side, cap );
14137
+
14138
+ // The point on the cap, counted from at.
14139
+ float reach = length( at );
14140
+ float foldAt = max( reach + side * at.y, 1e-6 * reach );
14141
+ float grow = ( 2.0 * dot( at, off ) + dot( off, off ) ) / ( range + reach );
14142
+ vec2 planeAt = vec2( at.x, side * at.z );
14143
+ vec2 shift = vec2( off.x, side * off.z ) * foldAt - planeAt * ( grow + side * off.y );
14144
+ cap[ 0 ] = shift / ( ( range + side * pos.y ) * foldAt ) * ( 0.5 / ${CAP_EDGE.toFixed(8)} );
13280
14145
  }
13281
14146
 
13282
14147
  // Long and short half-axes of the footprint whose sides are fx and fy. The
@@ -13319,7 +14184,7 @@ float tileWindowWeight( mat3x2 map, mat3x2 cap, float side, vec4 window, vec4 gr
13319
14184
 
13320
14185
  float rows = ${4 .toFixed(1)};
13321
14186
  // Offset from the window's north-west corner, in tiles.
13322
- vec2 rel = onMap ? home + chart[ 0 ] * grid.xy : chart[ 0 ] * grid.xy - grid.zw;
14187
+ vec2 rel = home + chart[ 0 ] * grid.xy;
13323
14188
  if ( onMap ) {
13324
14189
  // Half a pixel in from a pole of the map: past it the filter would
13325
14190
  // read the far row. The map's longitude wraps.
@@ -13421,8 +14286,8 @@ function release(store, win) {
13421
14286
  store.free.push(win.layer);
13422
14287
  }
13423
14288
  function patchOf(tiles, win, tile) {
13424
- if (win.chart !== 0 || win.level <= tiles.max_level) return void 0;
13425
- return patchOfTile(tiles.patches, win.level, tile.x, tile.y);
14289
+ if (win.level <= tiles.max_level) return void 0;
14290
+ return patchOfTile(tiles.patches, win.chart, win.level, tile.x, tile.y);
13426
14291
  }
13427
14292
  function exists(tiles, win, slot) {
13428
14293
  if (win.level <= tiles.max_level) return true;
@@ -13478,7 +14343,7 @@ function pump(chain, store, tiles, win, x, y, renderer, invalidate) {
13478
14343
  });
13479
14344
  chain.inflight++;
13480
14345
  const folder = CHART_FOLDER[win.chart];
13481
- const version = patchOf(tiles, win, tile)?.version ?? (win.chart === 0 ? mapTileVersion(tiles, win.level, tile.x, tile.y) : tiles.version);
14346
+ const version = patchOf(tiles, win, tile)?.version ?? tileVersion(tiles, win.chart, win.level, tile.x, tile.y);
13482
14347
  const pyramid = {
13483
14348
  id: tiles.id,
13484
14349
  version
@@ -13533,9 +14398,9 @@ function pointUnderCamera(mesh, camera) {
13533
14398
  return over.copy(camera.position).applyMatrix4(inverse.copy(mesh.matrixWorld).invert()).normalize();
13534
14399
  }
13535
14400
  function finestLevelAt$1(chain, tiles, over) {
13536
- if (cameraChart(over.y, chain?.chart) !== 0) return tiles.max_level;
13537
- const point = chartPoint(0, over.x, over.y, over.z);
13538
- return deepestLevel(tiles.max_level, tiles.patches, point.x, point.y);
14401
+ const chart = cameraChart(over.y, chain?.chart);
14402
+ const point = chartPoint(chart, over.x, over.y, over.z);
14403
+ return deepestLevel(tiles.max_level, tiles.patches, chart, point.x, point.y);
13539
14404
  }
13540
14405
  function chainLength(tiles, limit) {
13541
14406
  let finest = tiles.max_level;
@@ -13557,7 +14422,11 @@ function updateChain(chain, tiles, count, level, over, radius, frame, renderer,
13557
14422
  anchor.set(Math.fround(over.x * radius), Math.fround(over.y * radius), Math.fround(over.z * radius));
13558
14423
  chain.uniforms.anchor.value.copy(anchor);
13559
14424
  const range = anchor.length();
13560
- const home = chartPoint(0, anchor.x / range, anchor.y / range, anchor.z / range);
14425
+ const homes = [
14426
+ 0,
14427
+ 1,
14428
+ 2
14429
+ ].map((chart) => chartPoint(chart, anchor.x / range, anchor.y / range, anchor.z / range));
13561
14430
  const windowOf = (link) => store.windows.find((win) => win.chart === link.chart && win.level === link.level);
13562
14431
  store.windows = store.windows.filter((win) => {
13563
14432
  win.rank = links.findIndex((link) => link.chart === win.chart && link.level === win.level);
@@ -13598,10 +14467,9 @@ function updateChain(chain, tiles, count, level, over, radius, frame, renderer,
13598
14467
  continue;
13599
14468
  }
13600
14469
  chain.uniforms.grid.value[i].set(chartColumns(win.chart, win.level), chartRows(win.chart, win.level), win.placement.tx0, win.placement.ty0);
13601
- if (win.chart === 0) {
13602
- const from = windowHome(win.placement, home.x, home.y);
13603
- chain.uniforms.home.value[i].set(from.x, from.y);
13604
- }
14470
+ const home = homes[win.chart];
14471
+ const from = windowHome(win.placement, home.x, home.y);
14472
+ chain.uniforms.home.value[i].set(from.x, from.y);
13605
14473
  link.set(win.chart, win.layer, win.level, 1);
13606
14474
  let loaded = 0;
13607
14475
  for (let slot = 0; slot < SLOTS; slot++) if (isLoaded(store, win.layer, slot)) loaded |= 1 << slot;
@@ -13716,10 +14584,10 @@ void reliefSight( int i, mat3x2 map, mat3x2 cap, float side, vec2 mapStep, vec2
13716
14584
  vec4 grid = uReliefGrid[ i ];
13717
14585
  bool onMap = window.x < 0.5;
13718
14586
  float rows = ${4 .toFixed(1)};
13719
- // In tiles from the window's north-west corner. The map is counted from
13720
- // the point under the camera, so the fragment is on the side of the globe
13721
- // nearest the window.
13722
- vec2 rel = onMap ? uReliefHome[ i ] + map[ 0 ] * grid.xy : cap[ 0 ] * grid.xy - grid.zw;
14587
+ // In tiles from the window's north-west corner, counted from the point
14588
+ // under the camera. On the map the fragment is then on the side of the
14589
+ // globe nearest the window.
14590
+ vec2 rel = uReliefHome[ i ] + ( onMap ? map[ 0 ] : cap[ 0 ] ) * grid.xy;
13723
14591
  vec2 rate = ( onMap ? mapStep : capStep ) * grid.xy;
13724
14592
 
13725
14593
  // Half a pixel in from the window's sides: past it the filter reads the
@@ -15770,6 +16638,7 @@ async function loadSpacecraftModel(bo, modelScene, ctx) {
15770
16638
  try {
15771
16639
  const detail = await fetchObjectDetail(bo.body.data.id, false);
15772
16640
  bo.body.pointing = detail.global?.pointing;
16641
+ bo.body.landedHeadings = landedHeadings(detail.global?.events?.items);
15773
16642
  const attitudeManifest = detail.global?.attitude;
15774
16643
  if (attitudeManifest && (bo.modelLoadEpoch ?? 0) === epoch) {
15775
16644
  const probeId = bo.body.data.id.replace(/^probe-/, "");
@@ -15809,8 +16678,9 @@ async function loadSpacecraftModel(bo, modelScene, ctx) {
15809
16678
  bo.model = root;
15810
16679
  bo.modelName = slug;
15811
16680
  setHaloLoading(bo, false);
15812
- if (meta.scale_meters) {
15813
- const spanKm = meta.scale_meters / 2e3;
16681
+ const spanM = meta.exports[tier]?.scale_meters;
16682
+ if (spanM) {
16683
+ const spanKm = spanM / 2e3;
15814
16684
  bo.body.data.radiusKm = spanKm * (meta.body_span_ratio ?? 1);
15815
16685
  bo.radiusScene = kmToScene(effectiveRadiusKm(bo.body.data));
15816
16686
  bo.modelUnitOverride = meta.body_span_ratio ? kmToScene(spanKm) : void 0;
@@ -15923,6 +16793,7 @@ function unloadBodyModel(bo) {
15923
16793
  bo.model = null;
15924
16794
  bo.modelName = void 0;
15925
16795
  bo.body.pointing = void 0;
16796
+ bo.body.landedHeadings = void 0;
15926
16797
  bo.body.attitudeTrack = void 0;
15927
16798
  bo.body.modelBaseFrame = void 0;
15928
16799
  if (bo.mesh && restoreSphere) bo.mesh.visible = true;
@@ -15942,6 +16813,17 @@ function setHaloLoading(bo, loading) {
15942
16813
  bo.loadingShown = void 0;
15943
16814
  }
15944
16815
  }
16816
+ function landedHeadings(events) {
16817
+ const out = [];
16818
+ for (const e of events ?? []) {
16819
+ const deg = e.type === "landing" ? e.site?.heading_deg : void 0;
16820
+ if (deg !== void 0) out.push({
16821
+ jd: e.jd,
16822
+ deg
16823
+ });
16824
+ }
16825
+ return out.length ? out : void 0;
16826
+ }
15945
16827
  function withBaseFrame(scene, q) {
15946
16828
  if (!q) return scene;
15947
16829
  const wrapper = new Group();
@@ -17318,7 +18200,7 @@ function setLabelName(bo, name, variant, isLarge) {
17318
18200
  }
17319
18201
  function getLabelVariant(body) {
17320
18202
  const t = body.data.objectType;
17321
- if (isMajorBody(t) || t === ObjectType.STAR || isAsteroid(t) || t === ObjectType.COMET || t === ObjectType.BARYCENTER || t === ObjectType.LAGRANGE_POINT) return "major";
18203
+ if (isMajorBody(t) || t === ObjectType.STAR || t === ObjectType.EXTRASOLAR || isAsteroid(t) || t === ObjectType.COMET || t === ObjectType.BARYCENTER || t === ObjectType.LAGRANGE_POINT) return "major";
17322
18204
  if (t === ObjectType.SPACECRAFT) return "spacecraft";
17323
18205
  if (t === ObjectType.DEBRIS) return "debris";
17324
18206
  return "none";
@@ -18277,6 +19159,231 @@ async function loadSkybox(scene, renderer, ctx, disposed = () => false) {
18277
19159
  }
18278
19160
  }
18279
19161
 
19162
+ //#endregion
19163
+ //#region src/lib/scene/starfield/tree.ts
19164
+ var PC_AU = 648e3 / Math.PI;
19165
+ var PC_SCENE = PC_AU * 10;
19166
+ function childCentre(centre, half, k) {
19167
+ const q = half / 2;
19168
+ return [
19169
+ centre[0] + (k & 1 ? q : -q),
19170
+ centre[1] + (k & 2 ? q : -q),
19171
+ centre[2] + (k & 4 ? q : -q)
19172
+ ];
19173
+ }
19174
+ function boxDistance(p, centre, half) {
19175
+ const dx = Math.max(Math.abs(p[0] - centre[0]) - half, 0);
19176
+ const dy = Math.max(Math.abs(p[1] - centre[1]) - half, 0);
19177
+ const dz = Math.max(Math.abs(p[2] - centre[2]) - half, 0);
19178
+ return Math.hypot(dx, dy, dz);
19179
+ }
19180
+ function boxMagnitude(distance, range, rangeLimitMag) {
19181
+ if (distance <= 0) return -Infinity;
19182
+ if (range <= 0) return Infinity;
19183
+ return rangeLimitMag + 5 * Math.log10(distance / range);
19184
+ }
19185
+ function sceneToTree(x, y, z) {
19186
+ const eq = sceneToEquatorial(x, y, z, {
19187
+ x: 0,
19188
+ y: 0,
19189
+ z: 0
19190
+ });
19191
+ return [
19192
+ eq.x / PC_SCENE,
19193
+ eq.y / PC_SCENE,
19194
+ eq.z / PC_SCENE
19195
+ ];
19196
+ }
19197
+ function treeToScene(x, y, z, out) {
19198
+ return equatorialToScene(x * PC_SCENE, y * PC_SCENE, z * PC_SCENE, out);
19199
+ }
19200
+ function icrsToTree(raDeg, decDeg, distancePc) {
19201
+ const ra = raDeg * Math.PI / 180;
19202
+ const dec = decDeg * Math.PI / 180;
19203
+ const c = Math.cos(dec) * distancePc;
19204
+ return [
19205
+ c * Math.cos(ra),
19206
+ c * Math.sin(ra),
19207
+ Math.sin(dec) * distancePc
19208
+ ];
19209
+ }
19210
+ function treeMatrixElements(focus, out) {
19211
+ const x = treeToScene(1, 0, 0, {
19212
+ x: 0,
19213
+ y: 0,
19214
+ z: 0
19215
+ });
19216
+ const y = treeToScene(0, 1, 0, {
19217
+ x: 0,
19218
+ y: 0,
19219
+ z: 0
19220
+ });
19221
+ const z = treeToScene(0, 0, 1, {
19222
+ x: 0,
19223
+ y: 0,
19224
+ z: 0
19225
+ });
19226
+ out.length = 16;
19227
+ out[0] = x.x;
19228
+ out[1] = x.y;
19229
+ out[2] = x.z;
19230
+ out[3] = 0;
19231
+ out[4] = y.x;
19232
+ out[5] = y.y;
19233
+ out[6] = y.z;
19234
+ out[7] = 0;
19235
+ out[8] = z.x;
19236
+ out[9] = z.y;
19237
+ out[10] = z.z;
19238
+ out[11] = 0;
19239
+ out[12] = -focus[0];
19240
+ out[13] = -focus[1];
19241
+ out[14] = -focus[2];
19242
+ out[15] = 1;
19243
+ return out;
19244
+ }
19245
+
19246
+ //#endregion
19247
+ //#region src/lib/scene/starfield/record.ts
19248
+ var RENDER_EPOCH = 2016;
19249
+ var MAG_STEP = 1 / 1024;
19250
+ var G_MINUS_V = [
19251
+ -.02907,
19252
+ -.02385,
19253
+ -.2297,
19254
+ -.001768
19255
+ ];
19256
+ var B_V_RANGE = [-.3, 2.4];
19257
+ var G_MINUS_HP = -.28;
19258
+ var BP_RP_FROM_V_I = [
19259
+ -.01612,
19260
+ 1.274,
19261
+ -.08143
19262
+ ];
19263
+ var V_I_RANGE = [-.5, 3.5];
19264
+ var MAS_PER_RAD = 180 / Math.PI * 36e5;
19265
+ function clamp$3(x, [lo, hi]) {
19266
+ return Math.min(Math.max(x, lo), hi);
19267
+ }
19268
+ function poly(x, coefficients) {
19269
+ let out = 0;
19270
+ for (let i = coefficients.length - 1; i >= 0; i--) out = out * x + coefficients[i];
19271
+ return out;
19272
+ }
19273
+ function propagate(raDeg, decDeg, pmra, pmdec, years) {
19274
+ const ra = raDeg * Math.PI / 180;
19275
+ const dec = decDeg * Math.PI / 180;
19276
+ const rate = years / MAS_PER_RAD;
19277
+ const [cr, sr, cd, sd] = [
19278
+ Math.cos(ra),
19279
+ Math.sin(ra),
19280
+ Math.cos(dec),
19281
+ Math.sin(dec)
19282
+ ];
19283
+ const x = cd * cr + rate * (pmra * -sr + pmdec * -sd * cr);
19284
+ const y = cd * sr + rate * (pmra * cr + pmdec * -sd * sr);
19285
+ const z = sd + rate * (pmdec * cd);
19286
+ const n = Math.hypot(x, y, z);
19287
+ return [(Math.atan2(y / n, x / n) * 180 / Math.PI % 360 + 360) % 360, Math.asin(Math.min(Math.max(z / n, -1), 1)) * 180 / Math.PI];
19288
+ }
19289
+ function starPlace(row) {
19290
+ const distancePc = parallaxDistancePc(row);
19291
+ let ra = row.number("ra");
19292
+ let dec = row.number("dec");
19293
+ if (distancePc === null || ra === null || dec === null) return null;
19294
+ const years = RENDER_EPOCH - (row.number("epoch") ?? 2016);
19295
+ const pmra = row.number("pmra");
19296
+ if (years !== 0 && pmra !== null) [ra, dec] = propagate(ra, dec, pmra, row.number("pmdec") ?? 0, years);
19297
+ const tree = icrsToTree(ra, dec, distancePc);
19298
+ const s = treeToScene(tree[0], tree[1], tree[2], {
19299
+ x: 0,
19300
+ y: 0,
19301
+ z: 0
19302
+ });
19303
+ return {
19304
+ ra,
19305
+ dec,
19306
+ distancePc,
19307
+ tree,
19308
+ scene: [
19309
+ s.x,
19310
+ s.y,
19311
+ s.z
19312
+ ]
19313
+ };
19314
+ }
19315
+ function gEstimate(v, bv, hp) {
19316
+ if (v !== null && bv !== null) return v + poly(clamp$3(bv, B_V_RANGE), G_MINUS_V);
19317
+ return hp === null ? null : hp + G_MINUS_HP;
19318
+ }
19319
+ function apparentG(row) {
19320
+ return row.number("phot_g_mean_mag") ?? gEstimate(row.number("v_mag"), row.number("b_v"), row.number("hp_mag"));
19321
+ }
19322
+ function absoluteG(row) {
19323
+ const g = apparentG(row);
19324
+ const parallax = row.number("parallax");
19325
+ if (g === null || parallaxDistancePc(row) === null || parallax === null) return null;
19326
+ return Math.round((g + 5 * Math.log10(parallax) - 10) / MAG_STEP) * MAG_STEP;
19327
+ }
19328
+ function colourBpRp(row) {
19329
+ const bp = row.number("phot_bp_mean_mag");
19330
+ const rp = row.number("phot_rp_mean_mag");
19331
+ if (bp !== null && rp !== null) return bp - rp;
19332
+ const vi = row.number("v_i");
19333
+ return vi === null ? null : poly(clamp$3(vi, V_I_RANGE), BP_RP_FROM_V_I);
19334
+ }
19335
+
19336
+ //#endregion
19337
+ //#region src/lib/scene/focus/star-focus.ts
19338
+ function makeStarBody(record) {
19339
+ const home = homeRow(record);
19340
+ const place = home ? starPlace(home) : null;
19341
+ return {
19342
+ data: {
19343
+ id: extrasolarObjectId(record.id),
19344
+ name: record.metadata.name,
19345
+ objectType: ObjectType.EXTRASOLAR,
19346
+ parentId: "",
19347
+ radiusKm: NaN,
19348
+ hasLocalized: false,
19349
+ a: 0,
19350
+ e: 0,
19351
+ i: 0,
19352
+ om: 0,
19353
+ w: 0,
19354
+ ma: 0,
19355
+ n: 0,
19356
+ epoch: 0,
19357
+ validityStart: -Infinity,
19358
+ validityEnd: Infinity,
19359
+ orbitalSource: OrbitalSource.UNKNOWN
19360
+ },
19361
+ position: null,
19362
+ extrasolar: {
19363
+ id: record.id,
19364
+ record,
19365
+ scene: place?.scene ?? null,
19366
+ tree: place?.tree ?? null,
19367
+ absMag: home ? absoluteG(home) : null,
19368
+ colour: home ? colourBpRp(home) : null
19369
+ }
19370
+ };
19371
+ }
19372
+
19373
+ //#endregion
19374
+ //#region src/lib/scene/setup/extrasolar.ts
19375
+ async function ensureExtrasolarBody(ctx, objectId) {
19376
+ const text = objectId.slice("x-".length);
19377
+ const first = await extrasolarLookup.lookup(text);
19378
+ const result = first.kind === "alias" ? await extrasolarLookup.live(text) : first;
19379
+ if (result.kind === "live") {
19380
+ const held = ctx.getBody(extrasolarObjectId(result.record.id));
19381
+ const body = held?.extrasolar ? held : makeStarBody(result.record);
19382
+ ctx.bodies.addExtrasolar(body, first.kind === "alias" ? [objectId] : []);
19383
+ }
19384
+ return result;
19385
+ }
19386
+
18280
19387
  //#endregion
18281
19388
  //#region src/lib/scene/setup/scene-load.ts
18282
19389
  var EAGER_ZOOM1_PARTS = 13;
@@ -18385,6 +19492,7 @@ async function loadScene(ctx, date, targetId) {
18385
19492
  });
18386
19493
  const probePromise = metadataPromise.then((metadata) => buildProbeStore(metadata, jd, ctx.layers));
18387
19494
  const passengerPromise = passengerFor(targetId);
19495
+ const extrasolarTarget = targetId && isExtrasolarObjectId(targetId) ? ensureExtrasolarBody(ctx, targetId).catch((e) => console.warn(`scene-load: ${targetId} did not load:`, e)) : null;
18388
19496
  const metadata = await metadataPromise;
18389
19497
  loadProgress.reach("metadata");
18390
19498
  ctx.chebStore = await chebPromise;
@@ -18410,7 +19518,8 @@ async function loadScene(ctx, date, targetId) {
18410
19518
  addedSinceFlush = false;
18411
19519
  ctx.bodies.flushMinor(added);
18412
19520
  };
18413
- if (targetId && !ctx.getBody(targetId)) {
19521
+ if (extrasolarTarget) await extrasolarTarget;
19522
+ else if (targetId && !ctx.getBody(targetId)) {
18414
19523
  const placeholders = await createPlaceholderBody(targetId, ctx);
18415
19524
  if (routePlaceholders(ctx, placeholders, labels) > 0) noteAdded();
18416
19525
  if (placeholders.length > 0) flush();
@@ -18418,7 +19527,7 @@ async function loadScene(ctx, date, targetId) {
18418
19527
  ctx.bodies.majorBodies = major.filter((b) => b.data.objectType !== ObjectType.BARYCENTER && b.data.objectType !== ObjectType.LAGRANGE_POINT && b.data.orbitalSource !== OrbitalSource.SPICE_PROBE);
18419
19528
  try {
18420
19529
  ctx.refresher = new ZoneRefresher(ctx, metadata, loader, date);
18421
- if (targetId) ctx.refresher.want(targetId);
19530
+ if (targetId && !extrasolarTarget) ctx.refresher.want(targetId);
18422
19531
  } catch (e) {
18423
19532
  console.error("scene-load: ZoneRefresher init failed; time-segmented zones stay empty:", e);
18424
19533
  }
@@ -18587,12 +19696,17 @@ var ContextManager = class {
18587
19696
  console.error("[scene-load]", e);
18588
19697
  }
18589
19698
  refreshTick(date) {
19699
+ if (this.visibility.systemShare === 0) return;
18590
19700
  this.refresher?.tick(date);
18591
19701
  }
18592
19702
  async ready(date) {
18593
19703
  await this.refresher?.ready(date);
18594
19704
  }
18595
19705
  async ensureBody(targetId, date) {
19706
+ if (isExtrasolarObjectId(targetId)) {
19707
+ await ensureExtrasolarBody(this, targetId);
19708
+ return;
19709
+ }
18596
19710
  await this.refresher?.ensureBody(targetId, date);
18597
19711
  }
18598
19712
  applyGroupFilter(slug) {
@@ -18713,6 +19827,7 @@ var SimClock = class {
18713
19827
  prevScale = 1;
18714
19828
  stops = null;
18715
19829
  settleTimer = null;
19830
+ arrivals = [];
18716
19831
  constructor(initialJd, live = false) {
18717
19832
  this.jd = initialJd;
18718
19833
  this.settledJd = initialJd;
@@ -18769,6 +19884,19 @@ var SimClock = class {
18769
19884
  this.setJD(jd);
18770
19885
  this.jumps++;
18771
19886
  this.settle();
19887
+ const jumps = this.jumps;
19888
+ return new Promise((resolve) => this.arrivals.push({
19889
+ jumps,
19890
+ resolve
19891
+ }));
19892
+ }
19893
+ placed(jumps) {
19894
+ if (this.arrivals.length === 0) return;
19895
+ this.arrivals = this.arrivals.filter((arrival) => {
19896
+ if (arrival.jumps > jumps) return true;
19897
+ arrival.resolve();
19898
+ return false;
19899
+ });
18772
19900
  }
18773
19901
  settle() {
18774
19902
  if (this.settleTimer !== null) clearTimeout(this.settleTimer);
@@ -18809,7 +19937,7 @@ var SimClock = class {
18809
19937
  return jdToDate(this.jd);
18810
19938
  }
18811
19939
  setDate(date) {
18812
- this.jumpTo(dateToJD(date));
19940
+ return this.jumpTo(dateToJD(date));
18813
19941
  }
18814
19942
  };
18815
19943
 
@@ -19290,6 +20418,9 @@ function installOutputDither(pass) {
19290
20418
  //#endregion
19291
20419
  //#region src/lib/scene/setup/three-boot.ts
19292
20420
  var CAMERA_FAR_DEFAULT = 1e5;
20421
+ var CAMERA_MAX_DISTANCE = 1e5 * PC_SCENE;
20422
+ var SOLAR_MAX_DISTANCE = 31620.5 * 10;
20423
+ var CAMERA_FAR_REVERSED = 1e6 * PC_SCENE;
19293
20424
  function probeClipControl() {
19294
20425
  try {
19295
20426
  const gl = document.createElement("canvas").getContext("webgl2");
@@ -19343,7 +20474,9 @@ function bootThree(canvas, labelContainer, ctx) {
19343
20474
  shadowLight.shadow.bias = -1e-4;
19344
20475
  scene.add(shadowLight);
19345
20476
  scene.add(shadowLight.target);
19346
- const camera = new PerspectiveCamera(60, canvas.clientWidth / canvas.clientHeight, kmToScene(.001), CAMERA_FAR_DEFAULT);
20477
+ const aspect = canvas.clientWidth / canvas.clientHeight;
20478
+ const far = renderer.capabilities.reversedDepthBuffer ? CAMERA_FAR_REVERSED : CAMERA_FAR_DEFAULT;
20479
+ const camera = new PerspectiveCamera(60, aspect, kmToScene(.001), far);
19347
20480
  const composerTarget = new WebGLRenderTarget(canvas.clientWidth, canvas.clientHeight, {
19348
20481
  type: HalfFloatType,
19349
20482
  resolveDepthBuffer: false,
@@ -19375,38 +20508,1006 @@ function bootThree(canvas, labelContainer, ctx) {
19375
20508
  };
19376
20509
  }
19377
20510
 
20511
+ //#endregion
20512
+ //#region src/lib/scene/starfield/band.ts
20513
+ var STARS_IN_AU = 2e3;
20514
+ var STARS_OUT_AU = 1800;
20515
+ function starsTakeSky(distanceAu, taken) {
20516
+ if (distanceAu > 2e3) return true;
20517
+ if (distanceAu < 1800) return false;
20518
+ return taken;
20519
+ }
20520
+
20521
+ //#endregion
20522
+ //#region src/lib/scene/starfield/colour.ts
20523
+ var BP_RP_TEFF = [
20524
+ [-.12, 10700],
20525
+ [-.087, 10400],
20526
+ [-.037, 9700],
20527
+ [.005, 9300],
20528
+ [.068, 8800],
20529
+ [.11, 8600],
20530
+ [.166, 8250],
20531
+ [.194, 8100],
20532
+ [.222, 7910],
20533
+ [.263, 7760],
20534
+ [.32, 7590],
20535
+ [.327, 7400],
20536
+ [.377, 7220],
20537
+ [.434, 7020],
20538
+ [.49, 6820],
20539
+ [.518, 6750],
20540
+ [.546, 6670],
20541
+ [.587, 6550],
20542
+ [.64, 6350],
20543
+ [.67, 6280],
20544
+ [.694, 6180],
20545
+ [.719, 6050],
20546
+ [.767, 5990],
20547
+ [.784, 5930],
20548
+ [.803, 5860],
20549
+ [.823, 5770],
20550
+ [.832, 5720],
20551
+ [.841, 5680],
20552
+ [.85, 5660],
20553
+ [.869, 5600],
20554
+ [.88, 5550],
20555
+ [.9, 5480],
20556
+ [.95, 5380],
20557
+ [.983, 5270],
20558
+ [1.01, 5170],
20559
+ [1.1, 5100],
20560
+ [1.21, 4830],
20561
+ [1.34, 4600],
20562
+ [1.43, 4440],
20563
+ [1.53, 4300],
20564
+ [1.7, 4100],
20565
+ [1.73, 3990],
20566
+ [1.79, 3930],
20567
+ [1.84, 3850],
20568
+ [1.97, 3770],
20569
+ [2.09, 3660],
20570
+ [2.13, 3620],
20571
+ [2.23, 3560],
20572
+ [2.39, 3470],
20573
+ [2.5, 3430],
20574
+ [2.78, 3270],
20575
+ [2.94, 3210],
20576
+ [3.16, 3110],
20577
+ [3.35, 3060],
20578
+ [3.71, 2930],
20579
+ [4.16, 2810],
20580
+ [4.5, 2740],
20581
+ [4.65, 2680],
20582
+ [4.72, 2630],
20583
+ [4.86, 2570],
20584
+ [5.1, 2420]
20585
+ ];
20586
+ function teffFromBpRp(bpRp) {
20587
+ const table = BP_RP_TEFF;
20588
+ if (bpRp <= table[0][0]) return table[0][1];
20589
+ const last = table[table.length - 1];
20590
+ if (bpRp >= last[0]) return last[1];
20591
+ let lo = 0;
20592
+ let hi = table.length - 1;
20593
+ while (hi - lo > 1) {
20594
+ const mid = lo + hi >> 1;
20595
+ if (table[mid][0] <= bpRp) lo = mid;
20596
+ else hi = mid;
20597
+ }
20598
+ const [c0, t0] = table[lo];
20599
+ const [c1, t1] = table[hi];
20600
+ return t0 + (bpRp - c0) / (c1 - c0) * (t1 - t0);
20601
+ }
20602
+ function planckianRgb(teff, out) {
20603
+ const t = Math.min(Math.max(teff, 1667), 25e3);
20604
+ const t1 = 1e3 / t;
20605
+ const t2 = t1 * t1;
20606
+ const t3 = t2 * t1;
20607
+ const x = t <= 4e3 ? -.2661239 * t3 - .2343589 * t2 + .8776956 * t1 + .17991 : -3.0258469 * t3 + 2.1070379 * t2 + .2226347 * t1 + .24039;
20608
+ const x2 = x * x;
20609
+ const x3 = x2 * x;
20610
+ let y;
20611
+ if (t <= 2222) y = -1.1063814 * x3 - 1.3481102 * x2 + 2.18555832 * x - .20219683;
20612
+ else if (t <= 4e3) y = -.9549476 * x3 - 1.37418593 * x2 + 2.09137015 * x - .16748867;
20613
+ else y = 3.081758 * x3 - 5.8733867 * x2 + 3.75112997 * x - .37001483;
20614
+ const X = x / y;
20615
+ const Z = (1 - x - y) / y;
20616
+ const r = Math.max(3.2404542 * X - 1.5371385 - .4985314 * Z, 0);
20617
+ const g = Math.max(-.969266 * X + 1.8760108 + .041556 * Z, 0);
20618
+ const b = Math.max(.0556434 * X - .2040259 + 1.0572252 * Z, 0);
20619
+ const peak = Math.max(r, g, b);
20620
+ out[0] = r / peak;
20621
+ out[1] = g / peak;
20622
+ out[2] = b / peak;
20623
+ return out;
20624
+ }
20625
+ var LUT_LO = BP_RP_TEFF[0][0];
20626
+ var LUT_HI = BP_RP_TEFF[BP_RP_TEFF.length - 1][0];
20627
+ var LUT_SIZE = 512;
20628
+ var lut = null;
20629
+ function buildLut() {
20630
+ const bytes = new Uint8Array(LUT_SIZE * 3);
20631
+ const rgb = [
20632
+ 0,
20633
+ 0,
20634
+ 0
20635
+ ];
20636
+ for (let i = 0; i < LUT_SIZE; i++) {
20637
+ planckianRgb(teffFromBpRp(LUT_LO + (LUT_HI - LUT_LO) * i / (LUT_SIZE - 1)), rgb);
20638
+ bytes[3 * i] = Math.round(rgb[0] * 255);
20639
+ bytes[3 * i + 1] = Math.round(rgb[1] * 255);
20640
+ bytes[3 * i + 2] = Math.round(rgb[2] * 255);
20641
+ }
20642
+ return bytes;
20643
+ }
20644
+ function starColours(bpRp, out) {
20645
+ const table = lut ??= buildLut();
20646
+ const scale = (LUT_SIZE - 1) / (LUT_HI - LUT_LO);
20647
+ for (let i = 0; i < bpRp.length; i++) {
20648
+ const c = bpRp[i];
20649
+ if (Number.isNaN(c)) {
20650
+ out[3 * i] = 255;
20651
+ out[3 * i + 1] = 255;
20652
+ out[3 * i + 2] = 255;
20653
+ continue;
20654
+ }
20655
+ const at = 3 * Math.round(Math.min(Math.max((c - LUT_LO) * scale, 0), LUT_SIZE - 1));
20656
+ out[3 * i] = table[at];
20657
+ out[3 * i + 1] = table[at + 1];
20658
+ out[3 * i + 2] = table[at + 2];
20659
+ }
20660
+ return out;
20661
+ }
20662
+
20663
+ //#endregion
20664
+ //#region src/lib/scene/starfield/material.ts
20665
+ function starUniforms() {
20666
+ return {
20667
+ uLimit: { value: 7 },
20668
+ uPixelRatio: { value: 1 },
20669
+ uGain: { value: 1 }
20670
+ };
20671
+ }
20672
+ var FADE_MAG = 1;
20673
+ var CONTRAST = .75;
20674
+ var CORE_AT_LIMIT = .06;
20675
+ var DOT_PX = 2.5;
20676
+ var MAX_DOT_PX = 14;
20677
+ var starVertexShader = (pick = false) => `
20678
+ uniform float uLimit;
20679
+ uniform float uPixelRatio;
20680
+ uniform float uGain;
20681
+ uniform float uFade;
20682
+ uniform float uFloor;
20683
+ uniform int uHidden;
20684
+ attribute float mag;
20685
+ attribute vec3 starColour;
20686
+ varying vec3 vColour;
20687
+ ${pick ? "uniform highp uint uPickBase;\nflat varying highp uint vPick;" : ""}
20688
+
20689
+ void main() {
20690
+ ${pick ? "vPick = uPickBase + uint(gl_VertexID);" : ""}
20691
+ vec4 mv = modelViewMatrix * vec4(position, 1.0);
20692
+ float pc = length(mv.xyz) / ${PC_SCENE.toFixed(4)};
20693
+ float m = mag + 5.0 * log2(max(pc, 1e-9)) * 0.30103 - 5.0;
20694
+ float above = max(uLimit - m, uFloor);
20695
+ if (above <= 0.0 || mv.z >= 0.0 || gl_VertexID == uHidden) {
20696
+ // Too faint, behind the camera, or drawn on its own: put it off screen.
20697
+ gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
20698
+ gl_PointSize = 0.0;
20699
+ vColour = vec3(0.0);
20700
+ return;
20701
+ }
20702
+ float fade = smoothstep(0.0, ${FADE_MAG.toFixed(1)}, above) * uFade;
20703
+ // Energy relative to a star at the limit. Past a full core, the dot grows.
20704
+ float energy = ${CORE_AT_LIMIT.toFixed(3)} * exp2(above * ${(.4 * CONTRAST * Math.log2(10)).toFixed(5)});
20705
+ float core = min(energy, 1.0);
20706
+ float size = ${DOT_PX.toFixed(2)} * sqrt(max(energy, 1.0));
20707
+ vColour = starColour * core * fade * uGain;
20708
+ gl_Position = projectionMatrix * mv;
20709
+ // Past the far plane a star stays on it, behind everything else.
20710
+ ${isReversedDepth() ? "gl_Position.z = max(gl_Position.z, 0.0);" : "gl_Position.z = min(gl_Position.z, gl_Position.w);"}
20711
+ gl_PointSize = min(size, ${MAX_DOT_PX.toFixed(1)}) * uPixelRatio;
20712
+ }
20713
+ `;
20714
+ var FRAGMENT = `
20715
+ varying vec3 vColour;
20716
+
20717
+ void main() {
20718
+ vec2 c = gl_PointCoord * 2.0 - 1.0;
20719
+ float r2 = dot(c, c);
20720
+ if (r2 > 1.0) discard;
20721
+ gl_FragColor = vec4(vColour * exp(-5.0 * r2), 1.0);
20722
+ }
20723
+ `;
20724
+ var PICK_MIN_LIGHT = 1 / 255;
20725
+ var pickFragment = () => `
20726
+ varying vec3 vColour;
20727
+ flat varying highp uint vPick;
20728
+
20729
+ void main() {
20730
+ vec2 c = gl_PointCoord * 2.0 - 1.0;
20731
+ float r2 = dot(c, c);
20732
+ if (r2 > 1.0) discard;
20733
+ float light = max(vColour.r, max(vColour.g, vColour.b)) * exp(-5.0 * r2);
20734
+ if (light < ${PICK_MIN_LIGHT.toFixed(6)}) discard;
20735
+ gl_FragDepth = ${isReversedDepth() ? "light / (1.0 + light)" : "1.0 / (1.0 + light)"};
20736
+ gl_FragColor = vec4(
20737
+ float(vPick & 255u),
20738
+ float((vPick >> 8) & 255u),
20739
+ float((vPick >> 16) & 255u),
20740
+ float(vPick >> 24)
20741
+ ) / 255.0;
20742
+ }
20743
+ `;
20744
+ var NO_FLOOR = -1e9;
20745
+ function makeStarMaterial(shared, floor = NO_FLOOR) {
20746
+ return new ShaderMaterial({
20747
+ uniforms: {
20748
+ ...shared,
20749
+ uFade: { value: 0 },
20750
+ uFloor: { value: floor },
20751
+ uHidden: { value: -1 }
20752
+ },
20753
+ vertexShader: starVertexShader(),
20754
+ fragmentShader: FRAGMENT,
20755
+ blending: CustomBlending,
20756
+ blendEquation: AddEquation,
20757
+ blendSrc: OneMinusDstColorFactor,
20758
+ blendDst: OneFactor,
20759
+ transparent: true,
20760
+ depthTest: true,
20761
+ depthWrite: false
20762
+ });
20763
+ }
20764
+ function makeStarPickMaterial(draw, base) {
20765
+ return new ShaderMaterial({
20766
+ uniforms: {
20767
+ ...draw.uniforms,
20768
+ uPixelRatio: { value: 1 },
20769
+ uPickBase: { value: base }
20770
+ },
20771
+ vertexShader: starVertexShader(true),
20772
+ fragmentShader: pickFragment(),
20773
+ blending: NoBlending,
20774
+ depthTest: true,
20775
+ depthWrite: true
20776
+ });
20777
+ }
20778
+
20779
+ //#endregion
20780
+ //#region src/lib/scene/starfield/starfield.ts
20781
+ var TREE = "mw";
20782
+ var ROOT = "r";
20783
+ var METADATA = "metadata.json";
20784
+ var BASE_LIMIT_MAG = 7.5;
20785
+ var EXPOSURE_PC = 1;
20786
+ var MUST_LOAD_MAG = 1;
20787
+ var ABORT_MARGIN_MAG = 1;
20788
+ var LIMIT_EASE_MS = 300;
20789
+ var NODE_FADE_MS = 400;
20790
+ var SELECT_EVERY_MS = 100;
20791
+ var ADDS_PER_FRAME = 4;
20792
+ var BOOT_GATE_MS = 1e4;
20793
+ var FOCUS_FLOOR_MAG = 3;
20794
+ var NO_MAGNITUDE = 99;
20795
+ var STAR_RENDER_ORDER = -1;
20796
+ var NODE_PICK_PREFIX = "star:";
20797
+ var FOCUS_PICK_GROUP = "star-focus";
20798
+ var _view = new Vector3();
20799
+ var StarfieldSystem = class {
20800
+ group = new Group();
20801
+ uniforms = starUniforms();
20802
+ tree = null;
20803
+ credits = [];
20804
+ root = null;
20805
+ nodes = /* @__PURE__ */ new Map();
20806
+ drawn = /* @__PURE__ */ new Set();
20807
+ retries = new RetryBackoff(() => this.markDirty());
20808
+ idLoads = /* @__PURE__ */ new Map();
20809
+ loading = 0;
20810
+ cachedBytes = 0;
20811
+ drawable;
20812
+ metadataLoading = false;
20813
+ bootGate = null;
20814
+ stale = false;
20815
+ disposed = false;
20816
+ dirty = true;
20817
+ lastSelectMs = -Infinity;
20818
+ lastFrameMs = 0;
20819
+ targetLimit = BASE_LIMIT_MAG;
20820
+ allowedLimit = BASE_LIMIT_MAG;
20821
+ creditsShown = false;
20822
+ active = false;
20823
+ ready = false;
20824
+ skyTaken = false;
20825
+ sky = new SwitchFade();
20826
+ matrix = [];
20827
+ focusStar = null;
20828
+ focusPoints = null;
20829
+ focusPick = null;
20830
+ constructor(ctx, scene, budget, onChange, pickRegistry) {
20831
+ this.ctx = ctx;
20832
+ this.budget = budget;
20833
+ this.onChange = onChange;
20834
+ this.pickRegistry = pickRegistry;
20835
+ this.group.name = "starfield";
20836
+ this.group.matrixAutoUpdate = false;
20837
+ this.group.visible = false;
20838
+ scene.add(this.group);
20839
+ const open = extrasolarBase() !== "" && !ctx.layers.skipped.has("starfield");
20840
+ this.drawable = open ? "unknown" : "no";
20841
+ }
20842
+ setBudget(budget) {
20843
+ this.budget = budget;
20844
+ this.markDirty();
20845
+ }
20846
+ setFocusStar(star) {
20847
+ const next = star?.tree ? star : null;
20848
+ if (next?.id === this.focusStar?.id) return;
20849
+ this.focusStar = next;
20850
+ if (this.focusPoints) {
20851
+ this.focusPoints.removeFromParent();
20852
+ this.focusPoints.geometry.dispose();
20853
+ this.focusPoints.material.dispose();
20854
+ this.focusPoints = null;
20855
+ }
20856
+ this.dropPick(this.focusPick, FOCUS_PICK_GROUP);
20857
+ this.focusPick = null;
20858
+ if (next) this.focusPoints = this.focusPoint(next);
20859
+ for (const node of this.drawn) if (node.points) this.applyHidden(node);
20860
+ this.onChange();
20861
+ }
20862
+ treeEntry() {
20863
+ return this.tree;
20864
+ }
20865
+ drawnNodes() {
20866
+ const out = [];
20867
+ for (const node of this.drawn) {
20868
+ const view = this.view(node);
20869
+ if (view) out.push(view);
20870
+ }
20871
+ return out;
20872
+ }
20873
+ node(name) {
20874
+ const node = this.nodes.get(name);
20875
+ return node ? this.view(node) : void 0;
20876
+ }
20877
+ pickDraws() {
20878
+ if (!this.group.visible) return [];
20879
+ const out = [];
20880
+ for (const node of this.drawn) {
20881
+ const points = node.points;
20882
+ if (!points?.visible || !node.data) continue;
20883
+ node.pick ??= {
20884
+ base: this.pickRegistry.allocateRows(NODE_PICK_PREFIX + node.name, node.data.container.rows),
20885
+ material: null
20886
+ };
20887
+ out.push({
20888
+ points,
20889
+ material: this.pickMaterial(node.pick, points)
20890
+ });
20891
+ }
20892
+ const focus = this.focusPoints;
20893
+ if (focus) {
20894
+ this.focusPick ??= {
20895
+ base: this.pickRegistry.allocateRows(FOCUS_PICK_GROUP, 1),
20896
+ material: null
20897
+ };
20898
+ out.push({
20899
+ points: focus,
20900
+ material: this.pickMaterial(this.focusPick, focus)
20901
+ });
20902
+ }
20903
+ return out;
20904
+ }
20905
+ pickedStar(pickId) {
20906
+ const at = this.pickRegistry.locate(pickId);
20907
+ if (!at) return null;
20908
+ if (at.groupId === FOCUS_PICK_GROUP) return this.focusStar ? { id: this.focusStar.id } : null;
20909
+ if (!at.groupId.startsWith(NODE_PICK_PREFIX)) return null;
20910
+ return {
20911
+ node: at.groupId.slice(5),
20912
+ row: at.row
20913
+ };
20914
+ }
20915
+ async starId(star) {
20916
+ if ("id" in star) return star.id;
20917
+ const id = (this.nodes.get(star.node)?.ids?.column ?? await this.idsOf(star.node))[star.row];
20918
+ if (!id) throw new Error(`[starfield] row ${star.row} of node ${star.node} has no ID`);
20919
+ return id;
20920
+ }
20921
+ idsOf(name) {
20922
+ let load = this.idLoads.get(name);
20923
+ if (!load) {
20924
+ load = this.loadIds(name).finally(() => this.idLoads.delete(name));
20925
+ this.idLoads.set(name, load);
20926
+ }
20927
+ return load;
20928
+ }
20929
+ async loadIds(name) {
20930
+ const tree = this.tree;
20931
+ if (!tree) throw new Error("[starfield] no tree to read IDs from");
20932
+ const container = await fetchExtrasolarFile(extrasolarPath(tree.ids, { name }), tree.token, { priority: "high" });
20933
+ const node = this.nodes.get(name);
20934
+ const rows = node?.data?.container.rows;
20935
+ if (rows !== void 0 && container.rows !== rows) throw new Error(`[starfield] ID file of node ${name} has ${container.rows} rows, not ${rows}`);
20936
+ const column = container.ids("id");
20937
+ if (node?.data && !node.ids) {
20938
+ node.ids = {
20939
+ column,
20940
+ bytes: container.buffer.byteLength
20941
+ };
20942
+ this.cachedBytes += container.buffer.byteLength;
20943
+ }
20944
+ return column;
20945
+ }
20946
+ pickMaterial(range, points) {
20947
+ range.material ??= makeStarPickMaterial(points.material, range.base);
20948
+ return range.material;
20949
+ }
20950
+ dropPick(range, groupId) {
20951
+ if (!range) return;
20952
+ range.material?.dispose();
20953
+ this.pickRegistry.release(groupId);
20954
+ }
20955
+ rowPosition(name, row) {
20956
+ const data = this.nodes.get(name)?.data;
20957
+ if (!data || row < 0 || row >= data.container.rows) return null;
20958
+ const at = data.geometry.getAttribute("position").array;
20959
+ const [cx, cy, cz] = data.info.centre;
20960
+ const p = treeToScene(cx + at[3 * row], cy + at[3 * row + 1], cz + at[3 * row + 2], {
20961
+ x: 0,
20962
+ y: 0,
20963
+ z: 0
20964
+ });
20965
+ return [
20966
+ p.x,
20967
+ p.y,
20968
+ p.z
20969
+ ];
20970
+ }
20971
+ stats() {
20972
+ let drawnPoints = 0;
20973
+ for (const node of this.drawn) drawnPoints += node.data?.container.rows ?? 0;
20974
+ let cachedNodes = 0;
20975
+ for (const node of this.nodes.values()) if (node.data) cachedNodes++;
20976
+ return {
20977
+ drawnNodes: this.drawn.size,
20978
+ drawnPoints,
20979
+ cachedNodes,
20980
+ cachedBytes: this.cachedBytes,
20981
+ loading: this.loading,
20982
+ limit: this.uniforms.uLimit.value,
20983
+ targetLimit: this.targetLimit,
20984
+ active: this.active,
20985
+ share: this.sky.share
20986
+ };
20987
+ }
20988
+ share() {
20989
+ return this.sky.share;
20990
+ }
20991
+ availability() {
20992
+ return this.drawable;
20993
+ }
20994
+ busy() {
20995
+ if (this.loading > 0) return true;
20996
+ if (this.sky.fading) return true;
20997
+ if (this.drawn.size === 0) return false;
20998
+ if (Math.abs(this.uniforms.uLimit.value - this.allowedLimit) > .01) return true;
20999
+ for (const node of this.drawn) {
21000
+ if (node.fade !== (node.wanted ? 1 : 0)) return true;
21001
+ if (node.wanted && node.data && !node.points) return true;
21002
+ }
21003
+ return false;
21004
+ }
21005
+ update(camera, focus, pixelRatio, gain, nowMs) {
21006
+ const dtMs = this.lastFrameMs ? Math.min(nowMs - this.lastFrameMs, 250) : 0;
21007
+ this.lastFrameMs = nowMs;
21008
+ const wx = camera.position.x + focus[0];
21009
+ const wy = camera.position.y + focus[1];
21010
+ const wz = camera.position.z + focus[2];
21011
+ const au = Math.hypot(wx, wy, wz) / 10;
21012
+ if (!this.active && au > 500) this.active = true;
21013
+ else if (this.active && au < 400) this.stopLoading();
21014
+ if (this.active) this.start();
21015
+ const shown = this.ctx.layers.isVisible("starfield");
21016
+ this.skyTaken = starsTakeSky(au, this.skyTaken);
21017
+ this.sky.step(shown && this.ready && this.root !== null && this.skyTaken, dtMs);
21018
+ const skyShare = this.sky.share;
21019
+ this.group.visible = shown && this.root !== null && skyShare > 0;
21020
+ if (!this.root) return;
21021
+ this.group.matrix.fromArray(treeMatrixElements(focus, this.matrix));
21022
+ this.group.matrixWorldNeedsUpdate = true;
21023
+ const cam = sceneToTree(wx, wy, wz);
21024
+ const fromSunPc = Math.hypot(wx, wy, wz) / PC_SCENE;
21025
+ const target = BASE_LIMIT_MAG + 5 * Math.log10(Math.max(1, fromSunPc / EXPOSURE_PC));
21026
+ if (target !== this.targetLimit) this.dirty = true;
21027
+ this.targetLimit = target;
21028
+ const due = this.dirty || nowMs - this.lastSelectMs >= SELECT_EVERY_MS;
21029
+ if (this.active && !this.stale && due) this.select(cam, nowMs);
21030
+ const limit = this.uniforms.uLimit;
21031
+ const ease = dtMs > 0 ? 1 - Math.exp(-dtMs / LIMIT_EASE_MS) : 1;
21032
+ limit.value += (this.allowedLimit - limit.value) * ease;
21033
+ this.uniforms.uPixelRatio.value = pixelRatio;
21034
+ this.uniforms.uGain.value = gain * skyShare;
21035
+ this.applyFades(dtMs, camera, focus);
21036
+ if (this.group.visible && !this.creditsShown && this.drawn.size > 0) {
21037
+ this.creditsShown = true;
21038
+ this.ctx.credits.setStars(this.credits);
21039
+ }
21040
+ }
21041
+ dispose() {
21042
+ this.disposed = true;
21043
+ for (const node of this.nodes.values()) {
21044
+ node.abort?.abort();
21045
+ this.undraw(node);
21046
+ this.dropPick(node.pick, NODE_PICK_PREFIX + node.name);
21047
+ node.data?.geometry.dispose();
21048
+ }
21049
+ this.nodes.clear();
21050
+ this.setFocusStar(null);
21051
+ this.group.removeFromParent();
21052
+ }
21053
+ stopLoading() {
21054
+ this.active = false;
21055
+ this.ready = false;
21056
+ for (const node of this.nodes.values()) {
21057
+ node.wanted = false;
21058
+ node.abort?.abort();
21059
+ }
21060
+ this.markDirty();
21061
+ }
21062
+ start() {
21063
+ if (this.drawable !== "unknown" || this.metadataLoading) return;
21064
+ if (this.retries.waiting(METADATA)) return;
21065
+ this.metadataLoading = true;
21066
+ this.bootGate ??= Promise.race([eagerMinorsDone, new Promise((resolve) => setTimeout(resolve, BOOT_GATE_MS))]);
21067
+ (async () => {
21068
+ await this.bootGate;
21069
+ const meta = await fetchExtrasolarMetadata();
21070
+ const tree = meta?.trees[TREE];
21071
+ if (this.disposed) return;
21072
+ this.retries.clear(METADATA);
21073
+ if (!meta || !tree || !extrasolarAllowed(tree)) {
21074
+ this.noStars();
21075
+ return;
21076
+ }
21077
+ this.tree = tree;
21078
+ this.credits = meta.credits;
21079
+ this.root = this.record(ROOT, 0, tree.centre, tree.half_size, Infinity, null);
21080
+ this.drawable = "yes";
21081
+ this.markDirty();
21082
+ })().catch((err) => {
21083
+ if (this.disposed) return;
21084
+ console.warn("[starfield] metadata.json did not load:", err);
21085
+ this.retries.fail(METADATA);
21086
+ }).finally(() => {
21087
+ this.metadataLoading = false;
21088
+ });
21089
+ }
21090
+ noStars() {
21091
+ this.drawable = "no";
21092
+ this.onChange();
21093
+ }
21094
+ record(name, depth, centre, half, range, parent) {
21095
+ const node = {
21096
+ name,
21097
+ depth,
21098
+ centre,
21099
+ half,
21100
+ range,
21101
+ parent,
21102
+ children: [],
21103
+ state: "idle",
21104
+ abort: null,
21105
+ data: null,
21106
+ points: null,
21107
+ magnitude: Infinity,
21108
+ wanted: false,
21109
+ lastWantedMs: -Infinity,
21110
+ fade: 0,
21111
+ hidden: null,
21112
+ pick: null,
21113
+ ids: null
21114
+ };
21115
+ this.nodes.set(name, node);
21116
+ return node;
21117
+ }
21118
+ markDirty() {
21119
+ this.dirty = true;
21120
+ this.onChange();
21121
+ }
21122
+ select(cam, nowMs) {
21123
+ const tree = this.tree;
21124
+ const root = this.root;
21125
+ if (!tree || !root) return;
21126
+ this.dirty = false;
21127
+ this.lastSelectMs = nowMs;
21128
+ for (const node of this.nodes.values()) node.wanted = false;
21129
+ const magnitude = (node) => node === root ? -Infinity : boxMagnitude(boxDistance(cam, node.centre, node.half), node.range, tree.range_limit_mag);
21130
+ const heap = new NodeHeap();
21131
+ root.magnitude = magnitude(root);
21132
+ heap.push(root);
21133
+ let points = 0;
21134
+ let allowed = this.targetLimit;
21135
+ const wanted = [];
21136
+ for (let node = heap.pop(); node; node = heap.pop()) {
21137
+ if (node.magnitude > this.targetLimit) break;
21138
+ const rows = node.data?.container.rows ?? tree.node_rows;
21139
+ if (points + rows > this.budget.points && node.magnitude > MUST_LOAD_MAG) {
21140
+ allowed = node.magnitude;
21141
+ break;
21142
+ }
21143
+ points += rows;
21144
+ node.wanted = true;
21145
+ node.lastWantedMs = nowMs;
21146
+ wanted.push(node);
21147
+ for (const child of node.children) {
21148
+ child.magnitude = magnitude(child);
21149
+ heap.push(child);
21150
+ }
21151
+ }
21152
+ this.allowedLimit = allowed;
21153
+ if (wanted.every((node) => node.data || this.retries.waiting(node.name))) this.ready = true;
21154
+ for (const node of wanted) {
21155
+ if (this.loading >= this.budget.fetches) break;
21156
+ if (node.state === "idle" && !this.retries.waiting(node.name)) this.load(node);
21157
+ }
21158
+ for (const node of this.nodes.values()) {
21159
+ if (node.state !== "loading" || node.wanted) continue;
21160
+ if (node.magnitude > allowed + ABORT_MARGIN_MAG || !node.parent?.wanted) node.abort?.abort();
21161
+ }
21162
+ for (const node of wanted) if (node.data) this.drawn.add(node);
21163
+ this.evict();
21164
+ }
21165
+ load(node) {
21166
+ const tree = this.tree;
21167
+ if (!tree) return;
21168
+ node.state = "loading";
21169
+ this.loading++;
21170
+ const abort = new AbortController();
21171
+ node.abort = abort;
21172
+ fetchExtrasolarFile(extrasolarPath(tree.node, { name: node.name }), tree.token, {
21173
+ signal: abort.signal,
21174
+ priority: "low"
21175
+ }).then((container) => {
21176
+ if (this.disposed) return;
21177
+ this.retries.clear(node.name);
21178
+ this.adopt(node, container);
21179
+ }).catch((err) => {
21180
+ node.state = "idle";
21181
+ if (this.disposed || abort.signal.aborted) return;
21182
+ if (err instanceof ExtrasolarStaleError) {
21183
+ this.stale = true;
21184
+ return;
21185
+ }
21186
+ this.retries.fail(node.name);
21187
+ console.warn(`[starfield] node ${node.name} did not load:`, err);
21188
+ }).finally(() => {
21189
+ this.loading--;
21190
+ node.abort = null;
21191
+ this.markDirty();
21192
+ });
21193
+ }
21194
+ adopt(node, container) {
21195
+ const info = nodeInfo(container);
21196
+ const rows = container.rows;
21197
+ const x = container.float32("x");
21198
+ const y = container.float32("y");
21199
+ const z = container.float32("z");
21200
+ const position = new Float32Array(3 * rows);
21201
+ for (let i = 0; i < rows; i++) {
21202
+ position[3 * i] = x[i];
21203
+ position[3 * i + 1] = y[i];
21204
+ position[3 * i + 2] = z[i];
21205
+ }
21206
+ const colour = starColours(container.float32("colour"), new Uint8Array(3 * rows));
21207
+ const geometry = new BufferGeometry();
21208
+ geometry.setAttribute("position", new BufferAttribute(position, 3));
21209
+ geometry.setAttribute("mag", new BufferAttribute(container.float32("mag"), 1));
21210
+ geometry.setAttribute("starColour", new BufferAttribute(colour, 3, true));
21211
+ const bytes = container.buffer.byteLength + position.byteLength + colour.byteLength;
21212
+ node.data = {
21213
+ container,
21214
+ info,
21215
+ geometry,
21216
+ bytes
21217
+ };
21218
+ node.state = "loaded";
21219
+ node.centre = info.centre;
21220
+ node.half = info.half;
21221
+ node.range = info.maxRange;
21222
+ this.cachedBytes += bytes;
21223
+ node.children = [];
21224
+ for (let k = 0; k < 8; k++) {
21225
+ if (!(info.childMask >> k & 1)) continue;
21226
+ node.children.push(this.record(node.name + k, node.depth + 1, childCentre(info.centre, info.half, k), info.half / 2, info.childRange[k], node));
21227
+ }
21228
+ }
21229
+ evict() {
21230
+ if (this.cachedBytes <= this.budget.bytes) return;
21231
+ const candidates = [...this.nodes.values()].filter((node) => node.data && !node.wanted && !node.points && node.children.every((child) => child.state === "idle" && !child.data));
21232
+ candidates.sort((a, b) => a.lastWantedMs - b.lastWantedMs || b.depth - a.depth);
21233
+ for (const node of candidates) {
21234
+ if (this.cachedBytes <= this.budget.bytes) break;
21235
+ if (node.children.some((child) => child.data)) continue;
21236
+ this.release(node);
21237
+ }
21238
+ }
21239
+ release(node) {
21240
+ const data = node.data;
21241
+ if (!data) return;
21242
+ data.geometry.dispose();
21243
+ this.cachedBytes -= data.bytes;
21244
+ node.data = null;
21245
+ node.state = "idle";
21246
+ node.hidden = null;
21247
+ this.dropPick(node.pick, NODE_PICK_PREFIX + node.name);
21248
+ node.pick = null;
21249
+ if (node.ids) this.cachedBytes -= node.ids.bytes;
21250
+ node.ids = null;
21251
+ for (const child of node.children) this.forget(child);
21252
+ node.children = [];
21253
+ }
21254
+ forget(node) {
21255
+ for (const child of node.children) this.forget(child);
21256
+ this.nodes.delete(node.name);
21257
+ }
21258
+ applyFades(dtMs, camera, focus) {
21259
+ const step = dtMs / NODE_FADE_MS;
21260
+ let adds = 0;
21261
+ const view = new ViewCone(camera);
21262
+ for (const node of [...this.drawn]) {
21263
+ const target = node.wanted ? 1 : 0;
21264
+ if (!node.points) {
21265
+ if (!node.wanted || !node.data || adds >= ADDS_PER_FRAME) {
21266
+ if (!node.wanted) this.drawn.delete(node);
21267
+ continue;
21268
+ }
21269
+ adds++;
21270
+ this.draw(node);
21271
+ }
21272
+ if (target > node.fade) node.fade = Math.min(node.fade + step, 1);
21273
+ else if (target < node.fade) node.fade = Math.max(node.fade - step, 0);
21274
+ if (node.fade === 0 && !node.wanted) {
21275
+ this.undraw(node);
21276
+ this.drawn.delete(node);
21277
+ continue;
21278
+ }
21279
+ const points = node.points;
21280
+ points.material.uniforms.uFade.value = node.fade;
21281
+ points.visible = view.sees(node, focus);
21282
+ }
21283
+ }
21284
+ draw(node) {
21285
+ const data = node.data;
21286
+ const points = new Points(data.geometry, makeStarMaterial(this.uniforms));
21287
+ points.position.set(...data.info.centre);
21288
+ points.updateMatrix();
21289
+ points.matrixAutoUpdate = false;
21290
+ points.frustumCulled = false;
21291
+ points.renderOrder = STAR_RENDER_ORDER;
21292
+ points.userData.starNode = node.name;
21293
+ node.points = points;
21294
+ node.fade = 0;
21295
+ this.applyHidden(node);
21296
+ this.group.add(points);
21297
+ }
21298
+ focusPoint(star) {
21299
+ const geometry = new BufferGeometry();
21300
+ geometry.setAttribute("position", new BufferAttribute(new Float32Array(3), 3));
21301
+ const mag = star.absMag ?? NO_MAGNITUDE;
21302
+ geometry.setAttribute("mag", new BufferAttribute(new Float32Array([mag]), 1));
21303
+ const colour = starColours(new Float32Array([star.colour ?? NaN]), new Uint8Array(3));
21304
+ geometry.setAttribute("starColour", new BufferAttribute(colour, 3, true));
21305
+ const material = makeStarMaterial(this.uniforms, FOCUS_FLOOR_MAG);
21306
+ material.uniforms.uFade.value = 1;
21307
+ const points = new Points(geometry, material);
21308
+ points.position.set(...star.tree);
21309
+ points.updateMatrix();
21310
+ points.matrixAutoUpdate = false;
21311
+ points.frustumCulled = false;
21312
+ points.renderOrder = STAR_RENDER_ORDER;
21313
+ points.userData.focusStar = star.id;
21314
+ this.group.add(points);
21315
+ return points;
21316
+ }
21317
+ applyHidden(node) {
21318
+ const star = this.focusStar;
21319
+ let row = -1;
21320
+ if (star && node.data) {
21321
+ if (node.hidden?.id !== star.id) node.hidden = {
21322
+ id: star.id,
21323
+ row: findStarRow(node.data, star)
21324
+ };
21325
+ row = node.hidden.row;
21326
+ }
21327
+ node.points.material.uniforms.uHidden.value = row;
21328
+ }
21329
+ undraw(node) {
21330
+ const points = node.points;
21331
+ if (!points) return;
21332
+ points.removeFromParent();
21333
+ points.material.dispose();
21334
+ if (node.pick) {
21335
+ node.pick.material?.dispose();
21336
+ node.pick.material = null;
21337
+ }
21338
+ node.data?.geometry.dispose();
21339
+ node.points = null;
21340
+ node.fade = 0;
21341
+ }
21342
+ view(node) {
21343
+ const data = node.data;
21344
+ if (!data || !node.points) return void 0;
21345
+ return {
21346
+ name: node.name,
21347
+ rows: data.container.rows,
21348
+ points: node.points,
21349
+ container: data.container,
21350
+ info: data.info
21351
+ };
21352
+ }
21353
+ };
21354
+ var ROW_STEPS = 2 ** -22;
21355
+ var ROW_FLOOR_PC = 1e-9;
21356
+ var ROW_MAG = .01;
21357
+ function findStarRow(data, star) {
21358
+ const tree = star.tree;
21359
+ if (!tree) return -1;
21360
+ const { centre, half } = data.info;
21361
+ const ox = tree[0] - centre[0];
21362
+ const oy = tree[1] - centre[1];
21363
+ const oz = tree[2] - centre[2];
21364
+ if (Math.max(Math.abs(ox), Math.abs(oy), Math.abs(oz)) > half * 1.000001) return -1;
21365
+ const tx = Math.abs(ox) * ROW_STEPS + ROW_FLOOR_PC;
21366
+ const ty = Math.abs(oy) * ROW_STEPS + ROW_FLOOR_PC;
21367
+ const tz = Math.abs(oz) * ROW_STEPS + ROW_FLOOR_PC;
21368
+ const c = data.container;
21369
+ const x = c.float32("x");
21370
+ const y = c.float32("y");
21371
+ const z = c.float32("z");
21372
+ const mag = c.float32("mag");
21373
+ let best = -1;
21374
+ let bestDistance = Infinity;
21375
+ for (let i = 0; i < c.rows; i++) {
21376
+ const dx = Math.abs(x[i] - ox);
21377
+ const dy = Math.abs(y[i] - oy);
21378
+ const dz = Math.abs(z[i] - oz);
21379
+ if (dx > tx || dy > ty || dz > tz) continue;
21380
+ if (star.absMag !== null && Math.abs(mag[i] - star.absMag) > ROW_MAG) continue;
21381
+ const distance = dx + dy + dz;
21382
+ if (distance < bestDistance) {
21383
+ best = i;
21384
+ bestDistance = distance;
21385
+ }
21386
+ }
21387
+ return best;
21388
+ }
21389
+ var ViewCone = class {
21390
+ tanY;
21391
+ tanX;
21392
+ inverse;
21393
+ constructor(camera) {
21394
+ this.tanY = Math.tan(camera.fov / 2 * Math.PI / 180) / camera.zoom;
21395
+ this.tanX = this.tanY * camera.aspect;
21396
+ this.inverse = camera.matrixWorldInverse;
21397
+ }
21398
+ sees(node, focus) {
21399
+ const [cx, cy, cz] = node.centre;
21400
+ const at = treeToScene(cx, cy, cz, _view);
21401
+ _view.set(at.x - focus[0], at.y - focus[1], at.z - focus[2]).applyMatrix4(this.inverse);
21402
+ const r = node.half * Math.sqrt(3) * PC_SCENE;
21403
+ const depth = -_view.z;
21404
+ if (depth < -r) return false;
21405
+ const sideX = (Math.abs(_view.x) - depth * this.tanX) / Math.hypot(1, this.tanX);
21406
+ const sideY = (Math.abs(_view.y) - depth * this.tanY) / Math.hypot(1, this.tanY);
21407
+ return sideX <= r && sideY <= r;
21408
+ }
21409
+ };
21410
+ var NodeHeap = class {
21411
+ items = [];
21412
+ push(node) {
21413
+ const items = this.items;
21414
+ items.push(node);
21415
+ let i = items.length - 1;
21416
+ while (i > 0) {
21417
+ const up = i - 1 >> 1;
21418
+ if (items[up].magnitude <= items[i].magnitude) break;
21419
+ [items[up], items[i]] = [items[i], items[up]];
21420
+ i = up;
21421
+ }
21422
+ }
21423
+ pop() {
21424
+ const items = this.items;
21425
+ const top = items[0];
21426
+ const last = items.pop();
21427
+ if (items.length > 0 && last) {
21428
+ items[0] = last;
21429
+ let i = 0;
21430
+ for (;;) {
21431
+ const l = 2 * i + 1;
21432
+ const r = l + 1;
21433
+ let min = i;
21434
+ if (l < items.length && items[l].magnitude < items[min].magnitude) min = l;
21435
+ if (r < items.length && items[r].magnitude < items[min].magnitude) min = r;
21436
+ if (min === i) break;
21437
+ [items[min], items[i]] = [items[i], items[min]];
21438
+ i = min;
21439
+ }
21440
+ }
21441
+ return top;
21442
+ }
21443
+ };
21444
+
21445
+ //#endregion
21446
+ //#region src/lib/scene/visibility/system-fade.ts
21447
+ var SYSTEM_RADIUS_AU = 30.07;
21448
+ var SYSTEM_OUT_PX = 4.3;
21449
+ var SYSTEM_IN_PX = 4.75;
21450
+ function systemDiameterPx(distanceScene, viewportPx, fovDeg) {
21451
+ const pxPerRad = viewportPx / 2 / Math.tan(fovDeg * Math.PI / 360);
21452
+ return 2 * SYSTEM_RADIUS_AU * 10 / distanceScene * pxPerRad;
21453
+ }
21454
+ function systemShown(distanceScene, viewportPx, fovDeg, shown) {
21455
+ const px = systemDiameterPx(distanceScene, viewportPx, fovDeg);
21456
+ if (px < 4.3) return false;
21457
+ if (px > 4.75) return true;
21458
+ return shown;
21459
+ }
21460
+
19378
21461
  //#endregion
19379
21462
  //#region src/lib/scene/interaction/picking.ts
19380
- function pickMoonDot(pointer, camera, ctx, focusTruePos, canvasWidth, canvasHeight, tmpV3, jd, pointerType, isVisible) {
19381
- const SCREEN_THRESHOLD = pointerType === "touch" || pointerType === "pen" ? 48 : 24;
21463
+ function isCoarsePointer(pointerType) {
21464
+ return pointerType === "touch" || pointerType === "pen";
21465
+ }
21466
+ function pickRadiusPx(pointerType) {
21467
+ return isCoarsePointer(pointerType) ? 48 : 24;
21468
+ }
21469
+ function pickMoonDot(pick) {
21470
+ const { ctx } = pick;
21471
+ return pickDot(ctx.bodies.majorBodies.filter((body) => body.data.objectType === ObjectType.MOON && ctx.visibility.isMoonGroupVisible(body.data.parentId)), pick);
21472
+ }
21473
+ function pickPlanetDot(pick) {
21474
+ const { ctx, camera, focusTruePos } = pick;
21475
+ const [fx, fy, fz] = focusTruePos;
21476
+ const cam = camera.position;
21477
+ return pickDot(ctx.bodies.majorBodies.filter((body) => {
21478
+ const t = body.data.objectType;
21479
+ if (t !== ObjectType.STAR && t !== ObjectType.PLANET && t !== ObjectType.DWARF_PLANET) return false;
21480
+ const at = body.position;
21481
+ if (!at) return false;
21482
+ const dist = Math.hypot(at[0] - fx - cam.x, at[1] - fy - cam.y, at[2] - fz - cam.z);
21483
+ return ctx.visibility.getPlanetVisibility(body, dist) !== VISIBILITY.HIDE;
21484
+ }), pick);
21485
+ }
21486
+ function pickDot(bodies, pick) {
21487
+ const { pointer, camera, ctx, canvasWidth, canvasHeight, jd, isVisible } = pick;
19382
21488
  const px = (pointer.x + 1) * .5 * canvasWidth;
19383
21489
  const py = (1 - pointer.y) * .5 * canvasHeight;
19384
- const v = tmpV3;
19385
- const [fx, fy, fz] = focusTruePos;
21490
+ const v = pick.tmpV3;
21491
+ const [fx, fy, fz] = pick.focusTruePos;
19386
21492
  const cam = camera.position;
19387
21493
  let bestBody;
19388
- let bestScreenDist = SCREEN_THRESHOLD;
21494
+ let bestScreenDist = pickRadiusPx(pick.pointerType);
19389
21495
  let bestWorldDist = Infinity;
19390
- const testBody = (body) => {
21496
+ for (const body of bodies) {
19391
21497
  const at = ctx.place(body, jd);
19392
- if (!at) return;
21498
+ if (!at) continue;
19393
21499
  v.set(at[0] - fx, at[1] - fy, at[2] - fz);
19394
21500
  const worldDist = Math.hypot(v.x - cam.x, v.y - cam.y, v.z - cam.z);
19395
21501
  v.project(camera);
19396
- if (!ndcZVisible(v.z)) return;
21502
+ if (!ndcZVisible(v.z)) continue;
19397
21503
  const sx = (v.x + 1) * .5 * canvasWidth;
19398
21504
  const sy = (1 - v.y) * .5 * canvasHeight;
19399
21505
  const screenDist = Math.hypot(sx - px, sy - py);
19400
- if (!(screenDist < bestScreenDist || screenDist === bestScreenDist && worldDist < bestWorldDist)) return;
19401
- if (isVisible && !isVisible(v.x, v.y, worldDist)) return;
21506
+ if (!(screenDist < bestScreenDist || screenDist === bestScreenDist && worldDist < bestWorldDist)) continue;
21507
+ if (isVisible && !isVisible(v.x, v.y, worldDist)) continue;
19402
21508
  bestScreenDist = screenDist;
19403
21509
  bestWorldDist = worldDist;
19404
21510
  bestBody = body;
19405
- };
19406
- for (const body of ctx.bodies.majorBodies) {
19407
- if (body.data.objectType !== ObjectType.MOON) continue;
19408
- if (!ctx.visibility.isMoonGroupVisible(body.data.parentId)) continue;
19409
- testBody(body);
19410
21511
  }
19411
21512
  if (!bestBody) return null;
19412
21513
  return {
@@ -19418,7 +21519,12 @@ function pickMoonDot(pointer, camera, ctx, focusTruePos, canvasWidth, canvasHeig
19418
21519
 
19419
21520
  //#endregion
19420
21521
  //#region src/lib/scene/interaction/pointer.ts
19421
- var CLICK_DRAG_PX2 = 9;
21522
+ var CLICK_DRAG_PX = 3;
21523
+ var TOUCH_SLOP_PX = 10;
21524
+ function movedTooFar(e, from) {
21525
+ const limit = isCoarsePointer(e.pointerType) ? TOUCH_SLOP_PX : CLICK_DRAG_PX;
21526
+ return Math.hypot(e.clientX - from.x, e.clientY - from.y) > limit;
21527
+ }
19422
21528
  function inHiddenRoot(obj) {
19423
21529
  let top = obj;
19424
21530
  while (top.parent?.parent) top = top.parent;
@@ -19426,11 +21532,13 @@ function inHiddenRoot(obj) {
19426
21532
  }
19427
21533
  var PointerInteraction = class {
19428
21534
  downPos = new Vector2();
21535
+ down = /* @__PURE__ */ new Set();
21536
+ gesture = false;
19429
21537
  pointer = new Vector2();
19430
21538
  _tmpPointer = new Vector2();
19431
21539
  raycaster = new Raycaster();
19432
21540
  _tmpV3 = new Vector3();
19433
- constructor(canvas, camera, ctx, clock, focus, clickables, meshToBody, onPick, modelPick, gpuPick, getClouds, pickRegistry) {
21541
+ constructor(canvas, camera, ctx, clock, focus, clickables, meshToBody, onPick, modelPick, gpuPick, getClouds, pickRegistry, getStars, onStarPick) {
19434
21542
  this.canvas = canvas;
19435
21543
  this.camera = camera;
19436
21544
  this.ctx = ctx;
@@ -19443,22 +21551,39 @@ var PointerInteraction = class {
19443
21551
  this.gpuPick = gpuPick;
19444
21552
  this.getClouds = getClouds;
19445
21553
  this.pickRegistry = pickRegistry;
21554
+ this.getStars = getStars;
21555
+ this.onStarPick = onStarPick;
19446
21556
  }
19447
21557
  attach() {
19448
21558
  this.canvas.addEventListener("pointerdown", this.onDown);
21559
+ this.canvas.addEventListener("pointermove", this.onMove);
19449
21560
  this.canvas.addEventListener("pointerup", this.onUp);
21561
+ this.canvas.addEventListener("pointercancel", this.onCancel);
19450
21562
  }
19451
21563
  detach() {
19452
21564
  this.canvas.removeEventListener("pointerdown", this.onDown);
21565
+ this.canvas.removeEventListener("pointermove", this.onMove);
19453
21566
  this.canvas.removeEventListener("pointerup", this.onUp);
21567
+ this.canvas.removeEventListener("pointercancel", this.onCancel);
19454
21568
  }
19455
21569
  onDown = (e) => {
19456
- this.downPos.set(e.clientX, e.clientY);
21570
+ if (e.isPrimary) {
21571
+ this.down.clear();
21572
+ this.gesture = false;
21573
+ this.downPos.set(e.clientX, e.clientY);
21574
+ } else this.gesture = true;
21575
+ this.down.add(e.pointerId);
21576
+ };
21577
+ onMove = (e) => {
21578
+ if (this.gesture || !this.down.has(e.pointerId)) return;
21579
+ if (movedTooFar(e, this.downPos)) this.gesture = true;
21580
+ };
21581
+ onCancel = (e) => {
21582
+ this.down.delete(e.pointerId);
21583
+ this.gesture = true;
19457
21584
  };
19458
21585
  onUp = (e) => {
19459
- const dx = e.clientX - this.downPos.x;
19460
- const dy = e.clientY - this.downPos.y;
19461
- if (dx * dx + dy * dy > CLICK_DRAG_PX2) return;
21586
+ if (!this.down.delete(e.pointerId) || this.gesture || movedTooFar(e, this.downPos)) return;
19462
21587
  const rect = this.canvas.getBoundingClientRect();
19463
21588
  this.pointer.set((e.clientX - rect.left) / rect.width * 2 - 1, -((e.clientY - rect.top) / rect.height) * 2 + 1);
19464
21589
  const modelBody = this.modelPick(this.pointer.x, this.pointer.y);
@@ -19468,11 +21593,41 @@ var PointerInteraction = class {
19468
21593
  }
19469
21594
  this.raycaster.setFromCamera(this.pointer, this.camera);
19470
21595
  const meshBody = this.resolveMeshHit(this.raycaster.intersectObjects([...this.clickables, ...attachedModelRoots]))?.body;
19471
- const moonHit = pickMoonDot(this.pointer, this.camera, this.ctx, this.focus.focusTruePos, this.canvas.clientWidth, this.canvas.clientHeight, this._tmpV3, this.clock.jd, e.pointerType, this.isDotVisible);
21596
+ const dots = {
21597
+ pointer: this.pointer,
21598
+ camera: this.camera,
21599
+ ctx: this.ctx,
21600
+ focusTruePos: this.focus.focusTruePos,
21601
+ canvasWidth: this.canvas.clientWidth,
21602
+ canvasHeight: this.canvas.clientHeight,
21603
+ tmpV3: this._tmpV3,
21604
+ jd: this.clock.jd,
21605
+ pointerType: e.pointerType,
21606
+ isVisible: this.isDotVisible
21607
+ };
21608
+ const moonHit = pickMoonDot(dots);
19472
21609
  const gpuHit = this.gpuPickCloud(e, rect);
19473
21610
  const bestBody = this.nearer(moonHit, gpuHit)?.body ?? meshBody;
19474
- if (bestBody) this.onPick(bestBody);
21611
+ if (bestBody) {
21612
+ this.onPick(bestBody);
21613
+ return;
21614
+ }
21615
+ this.pickStar(e, rect, dots);
19475
21616
  };
21617
+ pickStar(e, rect, dots) {
21618
+ const stars = this.getStars();
21619
+ if (stars.length === 0) return;
21620
+ const planet = pickPlanetDot(dots);
21621
+ if (planet) {
21622
+ this.onPick(planet.body);
21623
+ return;
21624
+ }
21625
+ const candidates = this.gpuPick.pickDraws(stars, this.camera, e.clientX, e.clientY, rect, pickRadiusPx(e.pointerType));
21626
+ for (const c of candidates) {
21627
+ if (!this.isDotVisible(c.ndcX, c.ndcY, Infinity)) continue;
21628
+ if (this.onStarPick(c.pickId)) return;
21629
+ }
21630
+ }
19476
21631
  nearer(a, b) {
19477
21632
  if (!a) return b;
19478
21633
  if (!b) return a;
@@ -19481,9 +21636,8 @@ var PointerInteraction = class {
19481
21636
  return a;
19482
21637
  }
19483
21638
  gpuPickCloud(e, rect) {
19484
- const radius = e.pointerType === "touch" || e.pointerType === "pen" ? 48 : 24;
19485
21639
  const jd = this.clock.jd;
19486
- const candidates = this.gpuPick.pick(this.getClouds(), this.camera, e.clientX, e.clientY, rect, radius);
21640
+ const candidates = this.gpuPick.pick(this.getClouds(), this.camera, e.clientX, e.clientY, rect, pickRadiusPx(e.pointerType));
19487
21641
  for (const c of candidates) {
19488
21642
  const bodyId = this.pickRegistry.resolve(c.pickId);
19489
21643
  if (!bodyId) continue;
@@ -19566,6 +21720,7 @@ var GpuPickPass = class {
19566
21720
  buffer = new Uint8Array(0);
19567
21721
  prevClear = new Color();
19568
21722
  proxies = [];
21723
+ idleGeometry = new BufferGeometry();
19569
21724
  constructor(renderer, camera) {
19570
21725
  this.renderer = renderer;
19571
21726
  this.target = new WebGLRenderTarget(1, 1, { depthBuffer: true });
@@ -19588,19 +21743,33 @@ var GpuPickPass = class {
19588
21743
  this.target.setSize(px, px);
19589
21744
  this.buffer = new Uint8Array(px * px * 4);
19590
21745
  }
19591
- proxyAt(index, src) {
21746
+ proxyAt(index, src, material) {
19592
21747
  let p = this.proxies[index];
19593
21748
  if (!p) {
19594
- p = new Points(src.geometry, this.material);
21749
+ p = new Points(src.geometry, material);
19595
21750
  p.frustumCulled = false;
19596
21751
  p.matrixAutoUpdate = false;
19597
21752
  p.matrixWorldAutoUpdate = false;
19598
21753
  this.proxies[index] = p;
19599
- } else p.geometry = src.geometry;
21754
+ } else {
21755
+ p.geometry = src.geometry;
21756
+ p.material = material;
21757
+ }
19600
21758
  p.matrixWorld.copy(src.matrixWorld);
19601
21759
  return p;
19602
21760
  }
19603
21761
  pick(clouds, camera, clientX, clientY, rect, radiusCss) {
21762
+ const draws = [];
21763
+ for (const points of clouds) {
21764
+ if (!points.geometry.getAttribute("pickColor")) continue;
21765
+ draws.push({
21766
+ points,
21767
+ material: this.material
21768
+ });
21769
+ }
21770
+ return this.pickDraws(draws, camera, clientX, clientY, rect, radiusCss);
21771
+ }
21772
+ pickDraws(draws, camera, clientX, clientY, rect, radiusCss) {
19604
21773
  const r = Math.max(1, Math.round(radiusCss));
19605
21774
  const boxPx = r * 2 + 1;
19606
21775
  this.ensureSize(boxPx);
@@ -19608,13 +21777,11 @@ var GpuPickPass = class {
19608
21777
  const cy = clientY - rect.top;
19609
21778
  const offsetX = cx - r;
19610
21779
  const offsetY = cy - r;
19611
- this.scene.clear();
19612
21780
  let n = 0;
19613
- for (const pts of clouds) {
19614
- if (!pts.visible) continue;
19615
- if (!pts.geometry.getAttribute("pickColor")) continue;
19616
- if (pts.geometry.drawRange.count === 0) continue;
19617
- this.scene.add(this.proxyAt(n++, pts));
21781
+ for (const { points, material } of draws) {
21782
+ if (!points.visible) continue;
21783
+ if (points.geometry.drawRange.count === 0) continue;
21784
+ this.scene.add(this.proxyAt(n++, points, material));
19618
21785
  }
19619
21786
  if (n === 0) return [];
19620
21787
  this.pickCamera.copy(camera);
@@ -19630,12 +21797,21 @@ var GpuPickPass = class {
19630
21797
  this.renderer.clear(true, true, false);
19631
21798
  this.renderer.render(this.scene, this.pickCamera);
19632
21799
  this.renderer.readRenderTargetPixels(this.target, 0, 0, boxPx, boxPx, this.buffer);
21800
+ this.letGo(n);
19633
21801
  this.renderer.setRenderTarget(prevTarget);
19634
21802
  this.renderer.autoClear = prevAutoClear;
19635
21803
  this.renderer.setClearColor(this.prevClear, prevClearAlpha);
19636
21804
  this.pickCamera.clearViewOffset();
19637
21805
  return this.decode(boxPx, r, rect, offsetX, offsetY);
19638
21806
  }
21807
+ letGo(n) {
21808
+ this.scene.clear();
21809
+ for (let i = 0; i < n; i++) {
21810
+ this.proxies[i].geometry = this.idleGeometry;
21811
+ this.proxies[i].material = this.material;
21812
+ }
21813
+ this.renderer.render(this.scene, this.pickCamera);
21814
+ }
19639
21815
  decode(boxPx, r, rect, offsetX, offsetY) {
19640
21816
  const buf = this.buffer;
19641
21817
  const nearest = /* @__PURE__ */ new Map();
@@ -19665,6 +21841,7 @@ var GpuPickPass = class {
19665
21841
  dispose() {
19666
21842
  this.target.dispose();
19667
21843
  this.material.dispose();
21844
+ this.idleGeometry.dispose();
19668
21845
  this.proxies.length = 0;
19669
21846
  }
19670
21847
  };
@@ -20078,7 +22255,7 @@ function buildMajorBodies(bodies, scene, clickables, meshToBody, bodyObjects, re
20078
22255
  const id = body.data.id;
20079
22256
  const isHalfPromoted = halfPromoteIds?.has(id) ?? false;
20080
22257
  const t = body.data.objectType;
20081
- const isVirtual = isHalfPromoted || t === ObjectType.BARYCENTER || t === ObjectType.LAGRANGE_POINT || t === ObjectType.COMET || isAsteroid(t) || body.data.orbitalSource === OrbitalSource.SPICE_PROBE;
22258
+ const isVirtual = isHalfPromoted || t === ObjectType.EXTRASOLAR || t === ObjectType.BARYCENTER || t === ObjectType.LAGRANGE_POINT || t === ObjectType.COMET || isAsteroid(t) || body.data.orbitalSource === OrbitalSource.SPICE_PROBE;
20082
22259
  const color = resolveBodyColor(body.data);
20083
22260
  const radius = kmToScene(effectiveRadiusKm(body.data));
20084
22261
  const isStar = t === ObjectType.STAR;
@@ -20121,7 +22298,7 @@ function buildMajorBodies(bodies, scene, clickables, meshToBody, bodyObjects, re
20121
22298
  }
20122
22299
  }
20123
22300
  const variant = getLabelVariant(body);
20124
- const isLarge = isStar || t === ObjectType.PLANET;
22301
+ const isLarge = isStar || t === ObjectType.EXTRASOLAR || t === ObjectType.PLANET;
20125
22302
  const isMinor = isHalfPromoted || MINOR_PROMOTED_IDS.has(id) || body.data.isMinor === true;
20126
22303
  const label = createLabel(color, body.data.name ?? "", variant, host().bodyHref(id, body.data.name ?? ""), () => handleFocus(body), isLarge, onHoverChange ? (hovered) => onHoverChange(id, hovered) : void 0, isMinor);
20127
22304
  if (label) {
@@ -21014,7 +23191,7 @@ function buildTrails(bodyObjects, scene, basisPos = [
21014
23191
  for (const [, bo] of bodyObjects) {
21015
23192
  if (bo.trail !== null) continue;
21016
23193
  const { body } = bo;
21017
- if (body.data.objectType === ObjectType.STAR) continue;
23194
+ if (body.data.objectType === ObjectType.STAR || isExtrasolar(body)) continue;
21018
23195
  if (body.data.orbitalSource === OrbitalSource.SPICE_PROBE) {
21019
23196
  if (bo.isMinor && !bo.mesh) continue;
21020
23197
  } else if (!bo.mesh && !bo.focusUpgraded && isMeshUpgradable(body)) continue;
@@ -22130,10 +24307,18 @@ var PickRegistry = class {
22130
24307
  #next = 1;
22131
24308
  #groups = /* @__PURE__ */ new Map();
22132
24309
  allocate(groupId, keys) {
24310
+ return this.add(groupId, keys.length, keys);
24311
+ }
24312
+ allocateRows(groupId, length) {
24313
+ return this.add(groupId, length, null);
24314
+ }
24315
+ add(groupId, length, keys) {
22133
24316
  const base = this.#next;
22134
- this.#next += keys.length;
24317
+ this.#next += length;
22135
24318
  this.#groups.set(groupId, {
24319
+ groupId,
22136
24320
  base,
24321
+ length,
22137
24322
  keys
22138
24323
  });
22139
24324
  return base;
@@ -22142,8 +24327,20 @@ var PickRegistry = class {
22142
24327
  this.#groups.delete(groupId);
22143
24328
  }
22144
24329
  resolve(pickId) {
22145
- if (pickId === 0) return null;
22146
- for (const { base, keys } of this.#groups.values()) if (pickId >= base && pickId < base + keys.length) return keyToId(keys[pickId - base]);
24330
+ const group = this.find(pickId);
24331
+ if (!group?.keys) return null;
24332
+ return keyToId(group.keys[pickId - group.base]);
24333
+ }
24334
+ locate(pickId) {
24335
+ const group = this.find(pickId);
24336
+ if (!group || group.keys) return null;
24337
+ return {
24338
+ groupId: group.groupId,
24339
+ row: pickId - group.base
24340
+ };
24341
+ }
24342
+ find(pickId) {
24343
+ for (const group of this.#groups.values()) if (pickId >= group.base && pickId < group.base + group.length) return group;
22147
24344
  return null;
22148
24345
  }
22149
24346
  clear() {
@@ -22243,6 +24440,8 @@ var PointCloudSystem = class {
22243
24440
  brightMult: 1,
22244
24441
  opacity: EMPHASIS_BASE_OPACITY
22245
24442
  };
24443
+ fade = 1;
24444
+ moonFades = /* @__PURE__ */ new Map();
22246
24445
  constructor(ctx, scene, bodyObjects, circleTexture, focus, mapLayer, onBasisRebuilt) {
22247
24446
  this.ctx = ctx;
22248
24447
  this.scene = scene;
@@ -22262,13 +24461,13 @@ var PointCloudSystem = class {
22262
24461
  }
22263
24462
  applyPointBudget() {
22264
24463
  let total = 0;
22265
- for (const kind of WORKER_CLOUD_KINDS) for (const pts of this.workerPoints[kind].values()) if (pts.visible) total += pts.userData.solvedCount ?? 0;
22266
- const fraction = total > this.pointBudget ? this.pointBudget / total : 1;
22267
- if (fraction === this.drawFraction) return;
22268
- this.drawFraction = fraction;
24464
+ for (const kind of WORKER_CLOUD_KINDS) for (const pts of this.workerPoints[kind].values()) if (pts.visible && !pts.userData.leaving) total += pts.userData.solvedCount ?? 0;
24465
+ this.drawFraction = total > this.pointBudget ? this.pointBudget / total : 1;
22269
24466
  for (const kind of WORKER_CLOUD_KINDS) for (const pts of this.workerPoints[kind].values()) {
24467
+ if (pts.userData.leaving || pts.userData.drawFraction === this.drawFraction) continue;
24468
+ pts.userData.drawFraction = this.drawFraction;
22270
24469
  const n = pts.userData.solvedCount;
22271
- if (n !== void 0) pts.geometry.setDrawRange(0, Math.ceil(n * fraction));
24470
+ if (n !== void 0) pts.geometry.setDrawRange(0, Math.ceil(n * this.drawFraction));
22272
24471
  }
22273
24472
  }
22274
24473
  setEarthSatEmphasis(count) {
@@ -22291,7 +24490,49 @@ var PointCloudSystem = class {
22291
24490
  const { size, dim, opacity } = this.earthSatEmphasis;
22292
24491
  mat.size = size;
22293
24492
  mat.color.setRGB(dim, dim, dim);
22294
- mat.opacity = opacity;
24493
+ this.setOpacity(pts, opacity);
24494
+ }
24495
+ setFade(fade) {
24496
+ if (fade === this.fade) return;
24497
+ this.fade = fade;
24498
+ for (const kind of WORKER_CLOUD_KINDS) for (const pts of this.workerPoints[kind].values()) this.applyOpacity(pts);
24499
+ for (const pts of this.moonPoints.values()) this.applyOpacity(pts);
24500
+ }
24501
+ setOpacity(pts, opacity) {
24502
+ pts.userData.opacity = opacity;
24503
+ this.applyOpacity(pts);
24504
+ }
24505
+ applyOpacity(pts) {
24506
+ pts.material.opacity = pts.userData.opacity * this.fade * (pts.userData.share ?? 1);
24507
+ }
24508
+ updateVisibility(dtMs) {
24509
+ const vis = this.ctx.visibility;
24510
+ for (const [key, pts] of this.workerPoints.asteroid) this.setShare(pts, vis.asteroidGroupShare(parentIdFromSubkey(key)));
24511
+ for (const [key, pts] of this.workerPoints.spacecraft) this.setShare(pts, vis.spacecraftGroupShare(parentIdFromSubkey(key)));
24512
+ for (const [parentId, pts] of this.moonPoints) {
24513
+ let fade = this.moonFades.get(parentId);
24514
+ if (!fade) {
24515
+ fade = new SwitchFade(void 0, 100);
24516
+ this.moonFades.set(parentId, fade);
24517
+ }
24518
+ fade.step(vis.isMoonGroupVisible(parentId), dtMs);
24519
+ this.setShare(pts, fade.share);
24520
+ }
24521
+ }
24522
+ get fading() {
24523
+ for (const fade of this.moonFades.values()) if (fade.fading) return true;
24524
+ return false;
24525
+ }
24526
+ setShare(pts, share) {
24527
+ pts.visible = share > 0;
24528
+ const was = pts.userData.share;
24529
+ if (was === share) return;
24530
+ pts.userData.leaving = was !== void 0 && share < was;
24531
+ pts.userData.share = share;
24532
+ this.applyOpacity(pts);
24533
+ }
24534
+ fadeNewCloud(pts) {
24535
+ this.setOpacity(pts, pts.material.opacity);
22295
24536
  }
22296
24537
  setEmphasizedSmallBodyZone(zone, count) {
22297
24538
  const prev = this.emphasizedSmallBodyZone;
@@ -22323,14 +24564,14 @@ var PointCloudSystem = class {
22323
24564
  const { sizeMult, brightMult, opacity } = this.smallBodyEmphasis;
22324
24565
  mat.size = baseSize * sizeMult;
22325
24566
  mat.color.copy(baseColor).multiplyScalar(brightMult);
22326
- mat.opacity = opacity;
24567
+ this.setOpacity(pts, opacity);
22327
24568
  }
22328
24569
  resetSmallBodyEmphasis(pts) {
22329
24570
  if (pts.userData.smallBodyBaseSize === void 0) return;
22330
24571
  const mat = pts.material;
22331
24572
  mat.size = pts.userData.smallBodyBaseSize;
22332
24573
  mat.color.copy(pts.userData.smallBodyBaseColor);
22333
- mat.opacity = EMPHASIS_BASE_OPACITY;
24574
+ this.setOpacity(pts, EMPHASIS_BASE_OPACITY);
22334
24575
  pts.userData.smallBodyBaseSize = void 0;
22335
24576
  pts.userData.smallBodyBaseColor = void 0;
22336
24577
  }
@@ -22345,8 +24586,10 @@ var PointCloudSystem = class {
22345
24586
  for (const kind of WORKER_CLOUD_KINDS) for (const [key, p] of this.workerPoints[kind]) {
22346
24587
  const bucketKey = parentIdFromSubkey(key);
22347
24588
  p.userData.parentBodyId = this.cloudParentId(kind, bucketKey);
24589
+ this.fadeNewCloud(p);
22348
24590
  this.emphasiseNewCloud(kind, bucketKey, p);
22349
24591
  }
24592
+ for (const p of this.moonPoints.values()) this.fadeNewCloud(p);
22350
24593
  this.assignMapLayer();
22351
24594
  }
22352
24595
  assignMapLayer() {
@@ -22504,6 +24747,7 @@ var PointCloudSystem = class {
22504
24747
  0,
22505
24748
  0
22506
24749
  ];
24750
+ this.fadeNewCloud(pts);
22507
24751
  this.emphasiseNewCloud(kind, bucketKey, pts);
22508
24752
  this.workerPoints[kind].set(key, pts);
22509
24753
  this.pendingSceneAdds.push(pts);
@@ -22561,7 +24805,8 @@ var PointCloudSystem = class {
22561
24805
  posAttr.needsUpdate = true;
22562
24806
  this.bindPickIds(pts, pickIds);
22563
24807
  pts.userData.solvedCount = count;
22564
- pts.geometry.setDrawRange(0, Math.ceil(count * this.drawFraction));
24808
+ const fraction = pts.userData.drawFraction ?? this.drawFraction;
24809
+ pts.geometry.setDrawRange(0, Math.ceil(count * fraction));
22565
24810
  this.lastSolvedJd.set(groupId, jd);
22566
24811
  pts.userData.frontBasis = [
22567
24812
  basisUsed[0],
@@ -22697,7 +24942,7 @@ var PointCloudSystem = class {
22697
24942
  for (const [parentId, moons] of this.moonsByParent()) {
22698
24943
  const pts = this.moonPoints.get(parentId);
22699
24944
  if (!pts) continue;
22700
- if (!this.ctx.visibility.isMoonGroupVisible(parentId)) continue;
24945
+ if (!pts.visible && !this.ctx.visibility.isMoonGroupVisible(parentId)) continue;
22701
24946
  const posAttr = pts.geometry.getAttribute("position");
22702
24947
  const arr = posAttr.array;
22703
24948
  const n = Math.min(moons.length, arr.length / 3);
@@ -23002,9 +25247,16 @@ var _anchorOut = {
23002
25247
  };
23003
25248
  var _modelSphereCenter = new Vector3();
23004
25249
  var _anchorProj = new Vector3();
23005
- function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBodyId, hideCappedMoonLabels, hoveredBodyIds, asteroidPoints, spacecraftPoints, moonPoints, cullFrameCounter, renderer, tmpV3, forceCull, suppressHeliocentricTrails) {
25250
+ function fadeLabel(label, share) {
25251
+ if (label.userData.systemShare === share) return;
25252
+ label.userData.systemShare = share;
25253
+ label.element.style.opacity = share < 1 ? String(share) : "";
25254
+ }
25255
+ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBodyId, hideCappedMoonLabels, hoveredBodyIds, cullFrameCounter, renderer, tmpV3, forceCull, suppressHeliocentricTrails) {
23006
25256
  const layers = ctx.layers;
23007
25257
  const hideOrbits = !layers.isVisible("orbits");
25258
+ const systemShare = ctx.visibility.systemShare;
25259
+ const starShare = ctx.visibility.starShare;
23008
25260
  const showNomenclature = layers.isVisible("nomenclature");
23009
25261
  const fovRad = camera.fov * Math.PI / 180;
23010
25262
  const screenW = renderer.domElement.clientWidth;
@@ -23136,10 +25388,18 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
23136
25388
  let isClose;
23137
25389
  const isFocused = body.data.id === focusedBodyId;
23138
25390
  const isSystemRoot = focusedSystemId !== null && body.data.id === focusedSystemId;
23139
- if (bo.mesh === null && !isMajorBody(body.data.objectType)) {
25391
+ let zoneShare = 1;
25392
+ let labelThresholdShare = 1;
25393
+ let orbitThresholdShare = 1;
25394
+ if (isExtrasolar(body)) {
25395
+ const visible = starShare > 0;
25396
+ group.visible = visible;
25397
+ showLabel = visible;
25398
+ isClose = false;
25399
+ } else if (bo.mesh === null && !isMajorBody(body.data.objectType)) {
23140
25400
  const ot = body.data.objectType;
23141
25401
  if (ot === ObjectType.BARYCENTER || ot === ObjectType.LAGRANGE_POINT) {
23142
- let visible = true;
25402
+ let visible = systemShare > 0;
23143
25403
  const primaryId = !isFocused ? barycenterPrimaryId(body.data.id) : void 0;
23144
25404
  if (primaryId) {
23145
25405
  const primary = bodyObjects.get(primaryId)?.body ?? ctx.getBody(primaryId);
@@ -23152,7 +25412,9 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
23152
25412
  showLabel = visible;
23153
25413
  isClose = false;
23154
25414
  } else {
23155
- const visible = ctx.visibility.getPlanetVisibility(body, dist) !== VISIBILITY.HIDE && ctx.visibility.hasFullRendering(body);
25415
+ const vis = ctx.visibility.getPlanetVisibility(body, dist);
25416
+ zoneShare = ctx.visibility.fullRenderingShare(body);
25417
+ const visible = vis !== VISIBILITY.HIDE && zoneShare > 0;
23156
25418
  group.visible = visible;
23157
25419
  if (trail) trail.visible = visible;
23158
25420
  showLabel = visible;
@@ -23161,18 +25423,32 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
23161
25423
  } else {
23162
25424
  const isMoon = body.data.objectType === ObjectType.MOON;
23163
25425
  const vis = isMoon ? ctx.visibility.getMoonVisibility(body) : ctx.visibility.getPlanetVisibility(body, dist);
23164
- const vf = isMoon ? moonVisFlags(vis, hideCappedMoonLabels, isFocused) : bodyVisFlags(vis, ctx.visibility.hasFullRendering(body), isFocused || isSystemRoot);
25426
+ if (!isMoon) zoneShare = ctx.visibility.fullRenderingShare(body);
25427
+ const vf = isMoon ? moonVisFlags(vis, hideCappedMoonLabels, isFocused) : bodyVisFlags(vis, zoneShare > 0, isFocused || isSystemRoot);
23165
25428
  group.visible = vf.groupVisible;
23166
25429
  if (trail) trail.visible = vf.orbitVisible;
23167
25430
  showLabel = vf.showLabel;
23168
25431
  isClose = vf.isClose;
23169
25432
  }
25433
+ if (focusedSystemId !== null && ctx.visibility.isBodyInSystem(body, focusedSystemId)) {
25434
+ labelThresholdShare = ctx.visibility.labelThresholdShare(body.data.id, showLabel);
25435
+ orbitThresholdShare = ctx.visibility.orbitThresholdShare(body.data.id, !!trail?.visible);
25436
+ if (labelThresholdShare > 0) {
25437
+ group.visible = true;
25438
+ showLabel = true;
25439
+ }
25440
+ if (trail && orbitThresholdShare > 0) trail.visible = true;
25441
+ }
23170
25442
  if (!drawn) {
23171
25443
  group.visible = false;
23172
25444
  showLabel = false;
23173
25445
  isClose = false;
23174
25446
  }
23175
25447
  if (trail && (!drawn || body.orbitCenter === null)) trail.visible = false;
25448
+ if (trail) {
25449
+ trail.userData.zoneShare = zoneShare;
25450
+ trail.userData.thresholdShare = orbitThresholdShare;
25451
+ }
23176
25452
  let starIsPoint = false;
23177
25453
  if (bo.starPoint && drawn) {
23178
25454
  const t = bo.radiusScene / dist * projScale / STAR_POINT_HANDOFF_R;
@@ -23205,12 +25481,10 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
23205
25481
  }
23206
25482
  }
23207
25483
  if (applyLabelDisplay(bo, showLabel, isClose, dist, projScale, focusedBodyId)) newlyShownLabel = true;
25484
+ if (bo.label) fadeLabel(bo.label, bo.starPoint ? 1 : isExtrasolar(body) ? starShare : systemShare * zoneShare * labelThresholdShare);
23208
25485
  const nomScreenR = bo.radiusScene > 0 ? bo.radiusScene / dist * projScale : 0;
23209
25486
  updateNomenclatureVisibility(bo, showNomenclature && body.data.id === nomFocusedBodyId, nomScreenR, camera, screenW, screenH);
23210
25487
  }
23211
- for (const [key, pts] of asteroidPoints) pts.visible = ctx.visibility.isAsteroidGroupVisible(parentIdFromSubkey(key));
23212
- for (const [key, pts] of spacecraftPoints) pts.visible = ctx.visibility.isSpacecraftGroupVisible(parentIdFromSubkey(key));
23213
- for (const [parentId, pts] of moonPoints) pts.visible = ctx.visibility.isMoonGroupVisible(parentId);
23214
25488
  if (screenOccluders.length > 0) {
23215
25489
  const focusedPrimaryId = focusedBodyId ? barycenterPrimaryId(focusedBodyId) : null;
23216
25490
  for (const bo of bodyObjects.values()) {
@@ -23236,7 +25510,8 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
23236
25510
  mat.uniforms.uCenterOffset.value.set(-camera.position.x, -camera.position.y, -camera.position.z);
23237
25511
  const isFocused = bo.body.data.id === focusedBodyId;
23238
25512
  const isHovered = hoveredBodyIds.has(bo.body.data.id);
23239
- mat.uniforms.uAlphaMultiplier.value = isHovered ? 2 : isFocused ? 1.75 : 1;
25513
+ const share = systemShare * (line.userData.zoneShare ?? 1) * (line.userData.thresholdShare ?? 1);
25514
+ mat.uniforms.uAlphaMultiplier.value = (isHovered ? 2 : isFocused ? 1.75 : 1) * share;
23240
25515
  const isClosed = line.userData.isOpenCurve === false;
23241
25516
  mat.uniforms.uAlphaMin.value = isFocused && isClosed ? .15 : 0;
23242
25517
  mat.uniforms.uShowFull.value = isFocused ? 1 : 0;
@@ -24524,6 +26799,7 @@ var OutOfRangeNotifier = class {
24524
26799
  //#region src/lib/scene/position/landed-probe.ts
24525
26800
  var lastSeat = /* @__PURE__ */ new Map();
24526
26801
  var _upScene = new Vector3();
26802
+ var _northScene = new Vector3();
24527
26803
  function renderLandedProbe(d, probe, landed, jd, where, ctx, bodyObjects) {
24528
26804
  const sample = landedPositionAt(landed, jd, landedOpenEnded(probe));
24529
26805
  if (!sample) return null;
@@ -24553,18 +26829,24 @@ function renderLandedProbe(d, probe, landed, jd, where, ctx, bodyObjects) {
24553
26829
  const quat = bodyQuaternion(landingBody.orientation, jd, landingBody.nutPrec);
24554
26830
  const tmp = new Vector3(bx, by, bz).applyQuaternion(quat);
24555
26831
  const up = eff ? _upScene.set(eff.normal[0], eff.normal[1], eff.normal[2]).applyQuaternion(quat) : null;
26832
+ const north = _northScene.set(-Math.sin(latR) * Math.cos(lngR), Math.cos(latR), Math.sin(latR) * Math.sin(lngR)).applyQuaternion(quat);
24556
26833
  d.parentId = bodyKey;
24557
26834
  return {
24558
26835
  x: bodyWorldPos[0] + kmToScene(tmp.x),
24559
26836
  y: bodyWorldPos[1] + kmToScene(tmp.y),
24560
26837
  z: bodyWorldPos[2] + kmToScene(tmp.z),
24561
26838
  parentPos: bodyWorldPos,
24562
- up
26839
+ up,
26840
+ north
24563
26841
  };
24564
26842
  }
24565
26843
 
24566
26844
  //#endregion
24567
26845
  //#region src/lib/scene/position/placer.ts
26846
+ function fixedPlace(body) {
26847
+ const at = body.extrasolar?.scene;
26848
+ return at ? setPlaced(body, at[0], at[1], at[2]) : setUnplaced(body, "never");
26849
+ }
24568
26850
  var newestPosScratch = [
24569
26851
  0,
24570
26852
  0,
@@ -24649,7 +26931,7 @@ var Placer = class {
24649
26931
  return null;
24650
26932
  }
24651
26933
  this.settling.add(id);
24652
- const pos = isSurfaceFeature(body) ? this.seatFeature(body, jd) : this.settle(body, jd);
26934
+ const pos = isSurfaceFeature(body) ? this.seatFeature(body, jd) : isExtrasolar(body) ? fixedPlace(body) : this.settle(body, jd);
24653
26935
  this.settling.delete(id);
24654
26936
  if (jd === this.passJd) this.settled.add(id);
24655
26937
  return pos;
@@ -24810,8 +27092,12 @@ var Placer = class {
24810
27092
  const up = landedRender.up;
24811
27093
  if (bo?.mesh) if (up) applyUpVector(bo.mesh, up);
24812
27094
  else applySouthTowardParent(bo.mesh, pos, landedRender.parentPos);
24813
- if (bo?.model) if (up) applyUpVector(bo.model, up);
24814
- else applySouthTowardParent(bo.model, pos, landedRender.parentPos);
27095
+ if (bo?.model) {
27096
+ if (up) applyUpVector(bo.model, up);
27097
+ else applySouthTowardParent(bo.model, pos, landedRender.parentPos);
27098
+ const heading = landedHeadingAt(body.landedHeadings, jd);
27099
+ if (heading !== void 0) turnToHeading(bo.model, landedRender.north, heading);
27100
+ }
24815
27101
  return pos;
24816
27102
  }
24817
27103
  const primary = resolveProbePrimary(located.probe, jd, located.fitCenterNaifId, ctx.chebStore ?? null, (id) => this.at(id, jd) !== null);
@@ -25374,6 +27660,13 @@ function updateSunShadowLight(bodyObjects, focusTruePos, ctx, shadowLight, sunPo
25374
27660
  shadowCam.updateProjectionMatrix();
25375
27661
  }
25376
27662
 
27663
+ //#endregion
27664
+ //#region src/lib/scene/relief/grid-heights.ts
27665
+ var CAP_SIN = Math.sin(60 * Math.PI / 180);
27666
+ function gridChart(level, y) {
27667
+ return level < 1 || Math.abs(y) <= CAP_SIN ? 0 : y > 0 ? 1 : 2;
27668
+ }
27669
+
25377
27670
  //#endregion
25378
27671
  //#region src/lib/scene/lod/terrain-grid.ts
25379
27672
  var COARSE_SEGS = 128;
@@ -25591,7 +27884,14 @@ function heightTilesOf(bo) {
25591
27884
  return reliefTilesAvailable(tiles) && !tiledStates.get(bo)?.dead ? tiles : void 0;
25592
27885
  }
25593
27886
  function gridLevel(tiles, theta, phi, radius) {
25594
- const deepest = deepestLevel(tiles.max_level, tiles.patches, phi / (2 * Math.PI), theta / Math.PI);
27887
+ const y = Math.cos(theta);
27888
+ const chart = gridChart(tiles.max_level, y);
27889
+ const point = chartPoint(chart, ...[
27890
+ -Math.cos(phi) * Math.sin(theta),
27891
+ y,
27892
+ Math.sin(phi) * Math.sin(theta)
27893
+ ]);
27894
+ const deepest = deepestLevel(tiles.max_level, tiles.patches, chart, point.x, point.y);
25595
27895
  if (deepest === tiles.max_level) return deepest;
25596
27896
  const floatStep = 2 ** (Math.floor(Math.log2(radius)) - 23);
25597
27897
  const cells = 2 * Math.PI * radius / (CELL_FLOAT_STEPS * floatStep);
@@ -25659,7 +27959,8 @@ function updateTiledWindow(bo, mesh, tiles, altitude, centerTheta, centerPhi, in
25659
27959
  root: tileRoot(tiles),
25660
27960
  version: tiles.version,
25661
27961
  maxLevel: tiles.max_level,
25662
- patches: (tiles.patches ?? []).map(({ max_level, x, y, version }) => ({
27962
+ patches: (tiles.patches ?? []).map(({ cap, max_level, x, y, version }) => ({
27963
+ cap,
25663
27964
  max_level,
25664
27965
  x,
25665
27966
  y,
@@ -26230,6 +28531,7 @@ var PromotionRegistry = class {
26230
28531
  autoPromoted = /* @__PURE__ */ new Set();
26231
28532
  pinned = /* @__PURE__ */ new Set();
26232
28533
  pinPromoted = /* @__PURE__ */ new Set();
28534
+ focusPromoted = /* @__PURE__ */ new Set();
26233
28535
  groupTargets = null;
26234
28536
  groupMode = "none";
26235
28537
  smallBodyZone = null;
@@ -26314,10 +28616,15 @@ var PromotionRegistry = class {
26314
28616
  if (!this.buildBodyInstance(body)) return;
26315
28617
  this.finalizeBuilds();
26316
28618
  }
28619
+ releaseFocusPromoted(focusedId) {
28620
+ if (this.focusPromoted.size === 0) return;
28621
+ const ids = [...this.focusPromoted].filter((id) => id !== focusedId);
28622
+ this.tearDownBodies(ids, this.focusPromoted);
28623
+ }
26317
28624
  buildBodyInstance(body, halfPromoteIds, auto = false) {
26318
28625
  const { bodyObjects, ctx, clock, scene, clickables, meshToBody, renderer } = this.deps;
26319
28626
  if (bodyObjects.has(body.data.id)) return false;
26320
- if (!body.orbitElements && !body.data.pageOnly && body.data.orbitalSource !== OrbitalSource.SPICE_PROBE) {
28627
+ if (!body.orbitElements && !body.data.pageOnly && !isExtrasolar(body) && body.data.orbitalSource !== OrbitalSource.SPICE_PROBE) {
26321
28628
  body.orbitElements = body.data;
26322
28629
  body.orbitCenterId = body.data.parentId;
26323
28630
  }
@@ -26331,7 +28638,8 @@ var PromotionRegistry = class {
26331
28638
  if (zone) ctx.bodies.dirtyAsteroidZones.add(zone);
26332
28639
  }
26333
28640
  const id = body.data.id;
26334
- if (this.defaults.has(id)) {} else if (auto) this.autoPromoted.add(id);
28641
+ if (this.defaults.has(id)) {} else if (isExtrasolar(body)) this.focusPromoted.add(id);
28642
+ else if (auto) this.autoPromoted.add(id);
26335
28643
  else {
26336
28644
  this.userPromoted.add(id);
26337
28645
  this.emitUserPromotedCount();
@@ -26524,6 +28832,7 @@ var PromotionRegistry = class {
26524
28832
  var FocusController = class {
26525
28833
  promotion;
26526
28834
  readerFocusFilter = null;
28835
+ focusSets = 0;
26527
28836
  focusedBody;
26528
28837
  _tmpV3 = new Vector3();
26529
28838
  pendingInitialView = null;
@@ -26551,6 +28860,9 @@ var FocusController = class {
26551
28860
  get current() {
26552
28861
  return this.focusedBody;
26553
28862
  }
28863
+ get focusChanges() {
28864
+ return this.focusSets;
28865
+ }
26554
28866
  place(body) {
26555
28867
  return this.deps.ctx.place(body, this.deps.clock.jd);
26556
28868
  }
@@ -26643,6 +28955,7 @@ var FocusController = class {
26643
28955
  ];
26644
28956
  }
26645
28957
  this.focusedBody = void 0;
28958
+ this.promotion.releaseFocusPromoted(void 0);
26646
28959
  prepareFocusTarget(focus, [...position], camera, camWorld, camPos, sceneSettings().resolvedReducedMotion);
26647
28960
  const spherical = cartesianToSpherical(camPos ?? camWorld, position, void 0);
26648
28961
  callbacks.onCameraPosition?.(spherical.latitude, spherical.longitude, spherical.distance);
@@ -26661,6 +28974,8 @@ var FocusController = class {
26661
28974
  }
26662
28975
  setFocusTarget(body, camPos) {
26663
28976
  const { ctx, bodyObjects, controls, callbacks, focus, camera, systemData, loadTexture } = this.deps;
28977
+ this.focusSets++;
28978
+ const drawsItself = !isSurfaceFeature(body) && !isExtrasolar(body);
26664
28979
  if (!isSurfaceFeature(body)) this.promotion.ensureBodyObjects(body);
26665
28980
  const prev = this.focusedBody;
26666
28981
  const prevNomBodyId = nomenclatureBodyId(prev, bodyObjects);
@@ -26678,12 +28993,13 @@ var FocusController = class {
26678
28993
  }
26679
28994
  if (body.data.id !== prev?.data.id) this.pendingInitialView = null;
26680
28995
  this.focusedBody = body;
28996
+ this.promotion.releaseFocusPromoted(body.data.id);
26681
28997
  if (!isSurfaceFeature(body)) ctx.bodies.focusFeature = null;
26682
28998
  controls.minDistance = minCameraDistance(body, modelMinRadiusKm(bodyObjects.get(body.data.id)));
26683
28999
  const featureHost = isSurfaceFeature(body) ? ctx.getBody(body.featureAnchor.hostId) : void 0;
26684
29000
  ctx.visibility.setFocused(featureHost ?? body);
26685
29001
  callbacks.onFocusChange(body);
26686
- if (!isSurfaceFeature(body)) loadTexture(body);
29002
+ if (drawsItself) loadTexture(body);
26687
29003
  if (newNomBodyId && newNomBodyId !== body.data.id) {
26688
29004
  const landingBody = ctx.getBody(newNomBodyId);
26689
29005
  if (landingBody) loadTexture(landingBody);
@@ -26755,6 +29071,30 @@ var FocusController = class {
26755
29071
  callbacks.onCameraPosition?.(settled.latitude, settled.longitude, settled.distance);
26756
29072
  this.pendingInitialView = null;
26757
29073
  }
29074
+ snapToBodyFrom(id, fromId, distance) {
29075
+ const { ctx, camera, controls, callbacks } = this.deps;
29076
+ const body = ctx.getBody(id);
29077
+ const from = ctx.getBody(fromId);
29078
+ const pos = body && this.place(body);
29079
+ const at = from && this.place(from);
29080
+ if (!body || !pos || !at) return;
29081
+ this.settleOnBodyInstant(body, pos);
29082
+ const d = f64dist(at, pos);
29083
+ const offset = [
29084
+ (at[0] - pos[0]) / d * distance,
29085
+ (at[1] - pos[1]) / d * distance,
29086
+ (at[2] - pos[2]) / d * distance
29087
+ ];
29088
+ camera.position.set(offset[0], offset[1], offset[2]);
29089
+ controls.update();
29090
+ const settled = cartesianToSpherical(offset, [
29091
+ 0,
29092
+ 0,
29093
+ 0
29094
+ ]);
29095
+ callbacks.onCameraPosition?.(settled.latitude, settled.longitude, settled.distance);
29096
+ this.pendingInitialView = null;
29097
+ }
26758
29098
  settleOnBodyInstant(body, pos) {
26759
29099
  const { focus, pointClouds } = this.deps;
26760
29100
  this.setFocusTarget(body);
@@ -26775,7 +29115,7 @@ var FocusController = class {
26775
29115
  const body = this.focusedBody;
26776
29116
  if (!body) return;
26777
29117
  const { camera, controls, callbacks } = this.deps;
26778
- const bodyFixed = !isModelBearing(body);
29118
+ const bodyFixed = !isModelBearing(body) && !isExtrasolar(body);
26779
29119
  const quat = bodyFixed ? this.focusedBodyQuat(body) ?? [
26780
29120
  0,
26781
29121
  0,
@@ -27174,6 +29514,7 @@ var TIGHT_FAR_RELEASE = 2.3;
27174
29514
  var SUN_PROXY_FAR_FRACTION = .9;
27175
29515
  var _sunLiftDir = new Vector3();
27176
29516
  var TRAIL_DRIFT_PX = .25;
29517
+ var SKY_GONE_SHARE = .9999;
27177
29518
  var MODEL_SHADOW_DIST = 10;
27178
29519
  var MODEL_SHADOW_EXTENT = 4;
27179
29520
  var MODEL_SHADOW_NEAR = 4;
@@ -27204,6 +29545,7 @@ var SceneRenderer = class SceneRenderer {
27204
29545
  bodyObjects = /* @__PURE__ */ new Map();
27205
29546
  circleTexture = makeCircleTexture();
27206
29547
  pointClouds;
29548
+ starfield;
27207
29549
  systemData;
27208
29550
  positionDiagnostics = new PositionDiagnostics();
27209
29551
  placer;
@@ -27224,6 +29566,8 @@ var SceneRenderer = class SceneRenderer {
27224
29566
  limitsArmed = false;
27225
29567
  bodySelectEnabled = true;
27226
29568
  featureSelectEnabled = true;
29569
+ starSelectEnabled = false;
29570
+ pickSerial = 0;
27227
29571
  modelScene;
27228
29572
  modelCamera;
27229
29573
  modelLight;
@@ -27325,6 +29669,8 @@ var SceneRenderer = class SceneRenderer {
27325
29669
  fpsSamples = [];
27326
29670
  fpsSampleHead = 0;
27327
29671
  stashedSky = null;
29672
+ systemOn = true;
29673
+ systemFade = new SwitchFade();
27328
29674
  namesShown = true;
27329
29675
  halosShown = true;
27330
29676
  textureLoader = new TextureLoader();
@@ -27413,11 +29759,12 @@ var SceneRenderer = class SceneRenderer {
27413
29759
  this.pointClouds = new PointCloudSystem(ctx, this.scene, this.bodyObjects, this.circleTexture, this.focus, SceneRenderer.MAP_LAYER, () => this.rebuildTrailBasis());
27414
29760
  this.pointClouds.seedBasis(focusPos);
27415
29761
  this.pointClouds.setPointBudget(this.tier.pointBudget);
29762
+ this.starfield = new StarfieldSystem(ctx, this.scene, starBudget(), this.invalidate, this.pointClouds.pickRegistry);
27416
29763
  this.controls = new OrbitControls(this.camera, canvas);
27417
29764
  this.controls.enableDamping = true;
27418
29765
  this.orbitDampingFactor = orbitDamping(sceneSettings().resolvedReducedMotion);
27419
29766
  this.controls.minDistance = focusBody ? minCameraDistance(focusBody) : kmToScene(.01);
27420
- this.controls.maxDistance = 31620.5 * 10;
29767
+ Object.defineProperty(this.controls, "maxDistance", { get: () => this.maxCameraDistance() });
27421
29768
  this.controls.target.set(0, 0, 0);
27422
29769
  this.controls.update();
27423
29770
  this.controls.addEventListener("end", this.onControlsEnd);
@@ -27474,7 +29821,7 @@ var SceneRenderer = class SceneRenderer {
27474
29821
  this.systemData.syncToFocus();
27475
29822
  this.focusController.readerFocusFilter = this.readerMayFocus;
27476
29823
  this.gpuPick = new GpuPickPass(this.renderer, this.camera);
27477
- this.pointerInteraction = new PointerInteraction(canvas, this.camera, ctx, clock, this.focus, this.clickables, this.meshToBody, (body) => this.focusController.handleFocus(body), (ndcX, ndcY) => this.pickFocusedModel(ndcX, ndcY), this.gpuPick, () => [...this.pointClouds.asteroids().values(), ...this.pointClouds.spacecraft().values()], this.pointClouds.pickRegistry);
29824
+ this.pointerInteraction = new PointerInteraction(canvas, this.camera, ctx, clock, this.focus, this.clickables, this.meshToBody, this.pickBody, (ndcX, ndcY) => this.pickFocusedModel(ndcX, ndcY), this.gpuPick, () => [...this.pointClouds.asteroids().values(), ...this.pointClouds.spacecraft().values()], this.pointClouds.pickRegistry, () => this.starSelectEnabled && this.bodySelectEnabled ? this.starfield.pickDraws() : [], (pickId) => this.pickStar(pickId));
27478
29825
  this.pointerInteraction.attach();
27479
29826
  this.tick();
27480
29827
  }
@@ -27733,6 +30080,7 @@ var SceneRenderer = class SceneRenderer {
27733
30080
  this.lastRenderedCamPos.copy(this.camera.position);
27734
30081
  this.lastRenderedCamQuat.copy(this.camera.quaternion);
27735
30082
  this.applyJdUpdate(true);
30083
+ if (!this.dateHeld() && !this.seekAwaitsData) this.clock.placed(this.seenJumps);
27736
30084
  this.refreshTravelFocus();
27737
30085
  this.callbacks.onFrame?.(this.sceneJd, renderedDtMs);
27738
30086
  const controlsSettled = this.holdingCamera ? this.applyHeldCamera() : stepFocusAnimation(this.focus, this.camera, this.controls, () => this.repositionAll(), () => this.pointClouds.rebuildBasis());
@@ -27757,7 +30105,9 @@ var SceneRenderer = class SceneRenderer {
27757
30105
  if (this.systemData.hasPendingUnloads() && performance.now() - this.focus.focusStartTime >= this.focus.focusDurationMs) this.systemData.drainPendingUnloads();
27758
30106
  const { distance } = this.getCameraState();
27759
30107
  this.updateTravelCraftSystem();
30108
+ this.updateSystemShare(renderedDtMs);
27760
30109
  this.ctx.visibility.updateCamera(distance, this.sceneJd);
30110
+ this.ctx.visibility.stepZones(renderedDtMs);
27761
30111
  this.updateTightFar(distance);
27762
30112
  this.systemData.syncToFocus();
27763
30113
  this.refreshTravelPath();
@@ -27769,7 +30119,9 @@ var SceneRenderer = class SceneRenderer {
27769
30119
  viewportPx: this.viewportH,
27770
30120
  ctx: this.ctx
27771
30121
  });
27772
- this.cullFrameCounter = updateBodyVisibility(this.bodyObjects, this.camera, this.ctx, this.focus.focusTruePos, this.focusController.current?.data.id, this.hideCappedMoonLabels, this.hoveredBodyIds, this.pointClouds.asteroids(), this.pointClouds.spacecraft(), this.pointClouds.moons(), this.cullFrameCounter, this.renderer, this._tmpV3, !controlsSettled, this.tightFar);
30122
+ this.ctx.visibility.starShare = this.starfield.share();
30123
+ this.cullFrameCounter = updateBodyVisibility(this.bodyObjects, this.camera, this.ctx, this.focus.focusTruePos, this.focusController.current?.data.id, this.hideCappedMoonLabels, this.hoveredBodyIds, this.cullFrameCounter, this.renderer, this._tmpV3, !controlsSettled, this.tightFar);
30124
+ this.pointClouds.updateVisibility(renderedDtMs);
27773
30125
  this.pointClouds.applyPointBudget();
27774
30126
  refreshDeferredTrails(this.bodyObjects, this.focus, this.lastUpdatedJd, this.trailView());
27775
30127
  updateRingShaders(this.bodyObjects, this.focus.focusTruePos, sceneSettings().realisticLighting, sceneSettings().overexposeRings, this.camera.position, this.pxPerRad());
@@ -27779,7 +30131,6 @@ var SceneRenderer = class SceneRenderer {
27779
30131
  occluders.length = 0;
27780
30132
  const atmoState = updateAtmosphereShaders(this.bodyObjects, this.camera.position, sceneSettings().showAtmospheres, sceneSettings().realisticLighting, this.sunIntensityScale, currentAtmosphereConfig(), this.sceneJd);
27781
30133
  recordAtmospherePerf(renderedDtMs, atmoState.shellProminent);
27782
- this.scene.backgroundIntensity = atmoState.skyboxIntensity;
27783
30134
  this.applyLayerChrome();
27784
30135
  const sunBo = this.bodyObjects.get(SUN_ID);
27785
30136
  if (sunBo) applyStarTint(sunBo.corona, sunBo.starPoint, atmoState.sunTint);
@@ -27801,6 +30152,11 @@ var SceneRenderer = class SceneRenderer {
27801
30152
  this.pointClouds.drainOnePendingSceneAdd();
27802
30153
  this.updateDepthFar();
27803
30154
  this.updateSunProxy(atmoState.sunRefraction);
30155
+ this.camera.updateMatrixWorld();
30156
+ const focusedStar = this.focusController.current;
30157
+ this.starfield.setFocusStar(focusedStar && isExtrasolar(focusedStar) ? focusedStar.extrasolar ?? null : null);
30158
+ this.starfield.update(this.camera, this.focus.focusTruePos, this.renderer.getPixelRatio(), atmoState.skyboxIntensity, nowMs);
30159
+ this.updateSky(atmoState.skyboxIntensity);
27804
30160
  if (atmoState.insideShell) this.atmoDepthPass.run(this.renderer, this.scene, this.camera, this.bodyObjects, SceneRenderer.ATMO_DEPTH_LAYER);
27805
30161
  this.syncSceneSamples(atmoState.insideShell);
27806
30162
  this.composer.readBuffer.resolveDepthBuffer = this.overlayActive;
@@ -27826,6 +30182,8 @@ var SceneRenderer = class SceneRenderer {
27826
30182
  if (nowMs - this.focus.focusStartTime < this.focus.focusDurationMs) return true;
27827
30183
  if (this.jumpJd !== null || this.seekAwaitsData) return true;
27828
30184
  if (this.focusLoads > 0 || this.systemData.isLoading()) return true;
30185
+ if (this.systemFade.fading || this.ctx.visibility.fading) return true;
30186
+ if (this.pointClouds.fading) return true;
27829
30187
  for (const bo of this.bodyObjects.values()) if (bo.group.visible && bodyLoading(bo)) return true;
27830
30188
  return false;
27831
30189
  }
@@ -27836,6 +30194,26 @@ var SceneRenderer = class SceneRenderer {
27836
30194
  target.samples = samples;
27837
30195
  target.dispose();
27838
30196
  }
30197
+ maxCameraDistance() {
30198
+ switch (this.starfield.availability()) {
30199
+ case "yes": return CAMERA_MAX_DISTANCE;
30200
+ case "no": return SOLAR_MAX_DISTANCE;
30201
+ case "unknown": return Math.max(SOLAR_MAX_DISTANCE, this.camera.position.distanceTo(this.controls.target));
30202
+ }
30203
+ }
30204
+ updateSystemShare(dtMs) {
30205
+ const [fx, fy, fz] = this.focus.focusTruePos;
30206
+ const cam = this.camera.position;
30207
+ const fromBarycentre = Math.hypot(fx + cam.x, fy + cam.y, fz + cam.z);
30208
+ this.systemOn = systemShown(fromBarycentre, this.viewportH, this.camera.fov, this.systemOn);
30209
+ this.systemFade.step(this.systemOn, dtMs);
30210
+ const share = this.systemFade.share;
30211
+ const visibility = this.ctx.visibility;
30212
+ const wasHidden = visibility.systemShare === 0;
30213
+ visibility.systemShare = share;
30214
+ this.pointClouds.setFade(share);
30215
+ if (wasHidden && share > 0) this.ctx.refreshTick(jdToDate(this.clock.jd));
30216
+ }
27839
30217
  updateTightFar(distance) {
27840
30218
  if (isReversedDepth()) return;
27841
30219
  const focused = this.focusController.current;
@@ -27855,10 +30233,12 @@ var SceneRenderer = class SceneRenderer {
27855
30233
  }
27856
30234
  updateDepthFar() {
27857
30235
  if (isReversedDepth()) return;
27858
- let far = CAMERA_FAR_DEFAULT;
30236
+ const [fx, fy, fz] = this.focus.focusTruePos;
30237
+ const cam = this.camera.position;
30238
+ const span = Math.max(cam.length(), Math.hypot(fx + cam.x, fy + cam.y, fz + cam.z));
30239
+ let far = Math.max(CAMERA_FAR_DEFAULT, span * FAR_MARGIN);
27859
30240
  const sysId = this.ctx.visibility.activeSystemId;
27860
30241
  if (sysId) {
27861
- const [fx, fy, fz] = this.focus.focusTruePos;
27862
30242
  let maxDistSq = this.camera.position.lengthSq();
27863
30243
  const consider = (pos) => {
27864
30244
  const dx = pos[0] - fx;
@@ -27879,7 +30259,7 @@ var SceneRenderer = class SceneRenderer {
27879
30259
  const p = bo.body.position;
27880
30260
  if (p && this.ctx.visibility.hasFullRendering(bo.body)) consider(p);
27881
30261
  }
27882
- far = Math.min(CAMERA_FAR_DEFAULT, Math.max(FAR_MIN, Math.sqrt(maxDistSq) * FAR_MARGIN));
30262
+ far = Math.min(far, Math.max(FAR_MIN, Math.sqrt(maxDistSq) * FAR_MARGIN));
27883
30263
  }
27884
30264
  if (Math.abs(far - this.camera.far) > this.camera.far * FAR_UPDATE_EPS) {
27885
30265
  this.camera.far = far;
@@ -28137,7 +30517,30 @@ var SceneRenderer = class SceneRenderer {
28137
30517
  setFeatureSelectEnabled(enabled) {
28138
30518
  this.featureSelectEnabled = enabled;
28139
30519
  }
30520
+ setStarSelectEnabled(enabled) {
30521
+ this.starSelectEnabled = enabled;
30522
+ }
28140
30523
  readerMayFocus = (body) => this.bodySelectEnabled && (this.focusableBodies?.has(body.data.id) ?? true);
30524
+ pickBody = (body) => {
30525
+ this.pickSerial++;
30526
+ this.focusController.handleFocus(body);
30527
+ };
30528
+ pickStar(pickId) {
30529
+ const star = this.starfield.pickedStar(pickId);
30530
+ if (!star) return false;
30531
+ const serial = ++this.pickSerial;
30532
+ const focusChanges = this.focusController.focusChanges;
30533
+ const current = () => serial === this.pickSerial && focusChanges === this.focusController.focusChanges;
30534
+ (async () => {
30535
+ const objectId = extrasolarObjectId(await this.starfield.starId(star));
30536
+ if (!current()) return;
30537
+ await this.ctx.ensureBody(objectId, jdToDate(this.clock.jd));
30538
+ const body = this.ctx.getBody(objectId);
30539
+ if (!body) throw new Error(`no record for ${objectId}`);
30540
+ if (current()) this.pickBody(body);
30541
+ })().catch((err) => console.warn("[starfield] the picked star did not load:", err));
30542
+ return true;
30543
+ }
28141
30544
  setCooperativeTouch(cooperative) {
28142
30545
  this.controls.touches.ONE = cooperative ? NO_TOUCH_GESTURE : TOUCH.ROTATE;
28143
30546
  this.canvas.style.touchAction = cooperative ? "pan-x pan-y" : "none";
@@ -28226,7 +30629,7 @@ var SceneRenderer = class SceneRenderer {
28226
30629
  }
28227
30630
  shouldRender(nowMs) {
28228
30631
  if (this.holdingCamera) return true;
28229
- if (nowMs - this.focus.focusStartTime < this.focus.focusDurationMs || this.focusKey() !== this.lastRenderedFocusKey || this.ctx.bodies.minorStreaming || this.pointClouds.hasPendingSceneAdds() || this.systemData.hasPendingUnloads()) return true;
30632
+ if (nowMs - this.focus.focusStartTime < this.focus.focusDurationMs || this.focusKey() !== this.lastRenderedFocusKey || this.ctx.bodies.minorStreaming || this.pointClouds.hasPendingSceneAdds() || this.systemData.hasPendingUnloads() || this.systemFade.fading || this.ctx.visibility.fading || this.pointClouds.fading || this.starfield.busy()) return true;
28230
30633
  if (this.covered) return false;
28231
30634
  if (this.frameIndex - this.lastRenderedFrame >= SceneRenderer.IDLE_HEARTBEAT_FRAMES) return true;
28232
30635
  if (this.cameraMovedFrom(this.lastRenderedCamPos, this.lastRenderedCamQuat) || this.frameIndex < this.wakeUntilFrame || this.haloDebug.active) return true;
@@ -28548,6 +30951,10 @@ var SceneRenderer = class SceneRenderer {
28548
30951
  this.disarmLimits();
28549
30952
  this.focusController.snapToBody(id, latitude, longitude, zoom);
28550
30953
  }
30954
+ snapToBodyFrom(id, fromId, distance) {
30955
+ this.disarmLimits();
30956
+ this.focusController.snapToBodyFrom(id, fromId, distance);
30957
+ }
28551
30958
  setFocusTarget(body, camPos) {
28552
30959
  this.disarmLimits();
28553
30960
  this.applyJdUpdate();
@@ -28677,16 +31084,21 @@ var SceneRenderer = class SceneRenderer {
28677
31084
  setSkyDebugMarkersVisible(visible) {
28678
31085
  this.skyDebugMarkers.setVisible(visible);
28679
31086
  }
28680
- applyLayerChrome() {
28681
- if (this.ctx.layers.isVisible("stars")) {
31087
+ updateSky(skyboxIntensity) {
31088
+ const share = this.starfield.share();
31089
+ if (this.ctx.layers.isVisible("stars") && share < SKY_GONE_SHARE) {
28682
31090
  if (this.stashedSky) {
28683
31091
  this.scene.background = this.stashedSky;
28684
31092
  this.stashedSky = null;
28685
31093
  }
28686
31094
  } else if (this.scene.background) {
31095
+ if (this.stashedSky !== this.scene.background) disposeTexture(this.stashedSky);
28687
31096
  this.stashedSky = this.scene.background;
28688
31097
  this.scene.background = null;
28689
31098
  }
31099
+ this.scene.backgroundIntensity = skyboxIntensity * (1 - share);
31100
+ }
31101
+ applyLayerChrome() {
28690
31102
  const names = this.ctx.layers.isVisible("labels");
28691
31103
  if (names !== this.namesShown) {
28692
31104
  this.namesShown = names;
@@ -28753,6 +31165,7 @@ var SceneRenderer = class SceneRenderer {
28753
31165
  this.resize(this.canvas.clientWidth, this.canvas.clientHeight);
28754
31166
  }
28755
31167
  this.pointClouds.setPointBudget(this.tier.pointBudget);
31168
+ this.starfield.setBudget(starBudget());
28756
31169
  }
28757
31170
  resize(width, height) {
28758
31171
  this.invalidate();
@@ -28790,9 +31203,11 @@ var SceneRenderer = class SceneRenderer {
28790
31203
  disposeReliefTiles(bo);
28791
31204
  }
28792
31205
  this.pointClouds.dispose();
31206
+ this.starfield.dispose();
28793
31207
  this.disposeScene(this.scene);
28794
31208
  this.disposeScene(this.modelScene);
28795
31209
  disposeTexture(this.scene.background);
31210
+ disposeTexture(this.stashedSky);
28796
31211
  disposeTexture(this.scene.environment);
28797
31212
  disposeTexture(this.modelScene.environment);
28798
31213
  this.circleTexture.dispose();
@@ -31136,7 +33551,8 @@ var TYPES = {
31136
33551
  [ObjectType.SPACECRAFT]: "spacecraft",
31137
33552
  [ObjectType.DEBRIS]: "debris",
31138
33553
  [ObjectType.UNDOCUMENTED]: "unknown",
31139
- [ObjectType.SURFACE_FEATURE]: "surface-feature"
33554
+ [ObjectType.SURFACE_FEATURE]: "surface-feature",
33555
+ [ObjectType.EXTRASOLAR]: "star"
31140
33556
  };
31141
33557
  var ORBIT_CLASSES = {
31142
33558
  [ObjectType.ASTEROID_INNER]: "inner",
@@ -31265,9 +33681,11 @@ var SpaceMap = class {
31265
33681
  canvas = null;
31266
33682
  labelLayer = null;
31267
33683
  stopWatching = null;
33684
+ stopExtrasolarStale = null;
31268
33685
  controls = null;
31269
33686
  lostPanelTimer;
31270
33687
  cooperative = null;
33688
+ starSelectEnabled = false;
31271
33689
  limits = {};
31272
33690
  initialFocusPending = true;
31273
33691
  pendingFocusId = null;
@@ -31409,6 +33827,7 @@ var SpaceMap = class {
31409
33827
  container.classList.add("sm-map");
31410
33828
  mapsIn.set(container, (mapsIn.get(container) ?? 0) + 1);
31411
33829
  this.controls = new ControlHost(this, container);
33830
+ this.stopExtrasolarStale = onExtrasolarStale(() => this.ctx.onDataStale?.());
31412
33831
  const canvas = document.createElement("canvas");
31413
33832
  canvas.className = "sm-map__canvas";
31414
33833
  canvas.tabIndex = 0;
@@ -31472,6 +33891,8 @@ var SpaceMap = class {
31472
33891
  this.controls = null;
31473
33892
  this.stopWatching?.();
31474
33893
  this.stopWatching = null;
33894
+ this.stopExtrasolarStale?.();
33895
+ this.stopExtrasolarStale = null;
31475
33896
  clearTimeout(this.lostPanelTimer);
31476
33897
  canvas.removeEventListener("keydown", this.onKeyDown);
31477
33898
  canvas.removeEventListener("touchmove", this.onTouchMove);
@@ -31791,20 +34212,38 @@ var SpaceMap = class {
31791
34212
  panTo(target) {
31792
34213
  return this.toFeature(this.targetBody(target), target.feature, "pan");
31793
34214
  }
31794
- jumpTo(target) {
34215
+ async jumpTo(target) {
31795
34216
  const body = this.targetBody(target);
34217
+ if (this.placedNow(body) && target.facing === void 0 || this.placedNow(target.facing)) this.navigation++;
34218
+ else {
34219
+ const loading = this.goTo(body, {
34220
+ clock: "keep",
34221
+ camera: "none"
34222
+ });
34223
+ const token = this.navigation;
34224
+ const arrival = await loading;
34225
+ if (arrival.outcome === "failed") throw arrival.error;
34226
+ if (arrival.outcome !== "placed" || token !== this.navigation) return;
34227
+ }
34228
+ this.snap(target, body);
34229
+ return this.settled(0);
34230
+ }
34231
+ placedNow(id) {
34232
+ const body = this.ctx.getBody(id);
34233
+ return !!body && !!this.renderer?.placeOf(body);
34234
+ }
34235
+ snap(target, body) {
31796
34236
  if (target.facing !== void 0) {
31797
34237
  this.snapToBodyFacing(body, target.facing, target.elevationDeg ?? 0, this.framing(target, body));
31798
- return this;
34238
+ return;
31799
34239
  }
31800
34240
  if (target.feature) {
31801
34241
  const { featureId, lat, lon, diameterM } = target.feature;
31802
34242
  this.focusOnFeature(body, featureId, lat, lon, diameterM, null, "snap");
31803
- return this;
34243
+ return;
31804
34244
  }
31805
34245
  const camera = this.getCamera();
31806
34246
  this.snapToBody(body, target.lat ?? camera?.lat ?? 45, target.lon ?? camera?.lon ?? 0, this.framing(target, body));
31807
- return this;
31808
34247
  }
31809
34248
  getCamera() {
31810
34249
  const renderer = this.renderer;
@@ -31862,8 +34301,13 @@ var SpaceMap = class {
31862
34301
  renderer.setZoomEnabled(this.scrollZoom.isEnabled());
31863
34302
  renderer.setBodySelectEnabled(this.bodySelect.isEnabled());
31864
34303
  renderer.setFeatureSelectEnabled(this.featureSelect.isEnabled());
34304
+ renderer.setStarSelectEnabled(this.starSelectEnabled);
31865
34305
  renderer.setCooperativeTouch(cooperative);
31866
34306
  }
34307
+ setStarSelectEnabled(enabled) {
34308
+ this.starSelectEnabled = enabled;
34309
+ this.renderer?.setStarSelectEnabled(enabled);
34310
+ }
31867
34311
  onWheelCapture = (e) => {
31868
34312
  if (!this.scrollZoom.isEnabled()) return;
31869
34313
  if (this.cooperative && !this.cooperative.allowsWheel(e)) e.stopPropagation();
@@ -31904,6 +34348,9 @@ var SpaceMap = class {
31904
34348
  snapToBody(id, latitude, longitude, zoom) {
31905
34349
  this.renderer?.snapToBody(id, latitude, longitude, zoom);
31906
34350
  }
34351
+ snapToBodyFrom(id, fromId, distance) {
34352
+ this.renderer?.snapToBodyFrom(id, fromId, distance);
34353
+ }
31907
34354
  snapToBodyFacing(id, towardId, elevationDeg, distance) {
31908
34355
  this.renderer?.snapToBodyFacing(id, towardId, elevationDeg, distance);
31909
34356
  }
@@ -32251,7 +34698,7 @@ var MarkerDrawing = class {
32251
34698
  const screen = this.visible ? viewport.project(this.at.lon, this.at.lat) : null;
32252
34699
  this.element.style.display = screen ? "" : "none";
32253
34700
  if (!screen) return;
32254
- const shift = viewport.repeatShifts.find((s) => screen[0] + s >= 0 && screen[0] + s <= viewport.width) ?? 0;
34701
+ const shift = viewport.shiftInFrame(screen[0]);
32255
34702
  this.element.style.transform = `translate(${screen[0] + shift}px, ${screen[1]}px) translate(${-this.align[0] * 100}%, ${-this.align[1] * 100}%)`;
32256
34703
  }
32257
34704
  setPosition(at) {
@@ -32355,14 +34802,15 @@ function stepOfLevel(level, tileSize) {
32355
34802
  return 2 * Math.PI / (2 * tileSize * 2 ** level);
32356
34803
  }
32357
34804
  function finestLevelAt(pyramid, lon, lat) {
32358
- const [across, fy] = chartFraction(0, lon, lat);
32359
- const fx = across - Math.floor(across);
32360
34805
  let finest = pyramid.max_level;
32361
34806
  for (const patch of pyramid.patches ?? []) {
32362
34807
  if (patch.max_level <= finest) continue;
32363
- const x = Math.floor(fx * chartColumns(0, patch.max_level));
32364
- const y = Math.floor(fy * chartRows(0, patch.max_level));
32365
- if (patchOfTile([patch], patch.max_level, x, y)) finest = patch.max_level;
34808
+ const chart = patchChart(patch);
34809
+ const [across, fy] = chartFraction(chart, lon, lat);
34810
+ const fx = chart === 0 ? across - Math.floor(across) : across;
34811
+ const x = Math.floor(fx * chartColumns(chart, patch.max_level));
34812
+ const y = Math.floor(fy * chartRows(chart, patch.max_level));
34813
+ if (patchOfTile([patch], chart, patch.max_level, x, y)) finest = patch.max_level;
32366
34814
  }
32367
34815
  return finest;
32368
34816
  }
@@ -32445,12 +34893,13 @@ function fitted(chart, level, tileSize, extent, margin) {
32445
34893
  }
32446
34894
  }
32447
34895
  function reaches(patch, extent) {
32448
- const columns = chartColumns(0, patch.max_level);
32449
- const rows = chartRows(0, patch.max_level);
34896
+ const chart = patchChart(patch);
34897
+ const columns = chartColumns(chart, patch.max_level);
34898
+ const rows = chartRows(chart, patch.max_level);
32450
34899
  const south = (patch.y[1] + 1) / rows;
32451
34900
  if (patch.y[0] / rows > extent.maxY || south < extent.minY) return false;
32452
34901
  let west = patch.x[0] / columns;
32453
- west -= Math.round(west - (extent.minX + extent.maxX) / 2);
34902
+ if (chart === 0) west -= Math.round(west - (extent.minX + extent.maxX) / 2);
32454
34903
  const east = west + (patch.x[1] + 1 - patch.x[0]) / columns;
32455
34904
  return west <= extent.maxX && east >= extent.minX;
32456
34905
  }
@@ -32464,18 +34913,32 @@ function planWindows(pyramid, frame, firstLevel, moves = true) {
32464
34913
  if (place) places.push(place);
32465
34914
  }
32466
34915
  const onMap = extentOn(0, frame, places, centre);
34916
+ const cap = centre[1] >= 0 ? 1 : 2;
34917
+ const onCap = extentOn(cap, frame, places, centre);
32467
34918
  const wanted = levelForStep(frame.step, pyramid.tile_size);
32468
34919
  const specs = [];
32469
34920
  let finest = pyramid.max_level;
32470
- for (const patch of pyramid.patches ?? []) if (patch.max_level > finest && reaches(patch, onMap)) finest = patch.max_level;
34921
+ let finestOn = {
34922
+ chart: 0,
34923
+ extent: onMap
34924
+ };
34925
+ for (const patch of pyramid.patches ?? []) {
34926
+ const chart = patchChart(patch);
34927
+ const extent = chart === 0 ? onMap : chart === cap ? onCap : null;
34928
+ if (extent && patch.max_level > finest && reaches(patch, extent)) {
34929
+ finest = patch.max_level;
34930
+ finestOn = {
34931
+ chart,
34932
+ extent
34933
+ };
34934
+ }
34935
+ }
32471
34936
  for (let level = Math.min(wanted, finest); level > pyramid.max_level; level--) {
32472
- const spec = fitted(0, level, pyramid.tile_size, onMap, margin);
34937
+ const spec = fitted(finestOn.chart, level, pyramid.tile_size, finestOn.extent, margin);
32473
34938
  if (!spec) continue;
32474
34939
  specs.push(spec);
32475
34940
  break;
32476
34941
  }
32477
- const cap = centre[1] >= 0 ? 1 : 2;
32478
- const onCap = extentOn(cap, frame, places, centre);
32479
34942
  for (let level = Math.min(wanted, pyramid.max_level); level >= firstLevel; level--) {
32480
34943
  const map = fitted(0, level, pyramid.tile_size, onMap, margin);
32481
34944
  const polar = onCap && level >= 1 ? fitted(cap, level, pyramid.tile_size, onCap, margin) : void 0;
@@ -32497,7 +34960,7 @@ function windowTiles(spec, pyramid) {
32497
34960
  for (let ty = Math.max(Math.floor(spec.y0 / size), 0); ty <= lastY; ty++) for (let tx = Math.floor(spec.x0 / size); tx <= lastX; tx++) {
32498
34961
  if (spec.chart !== 0 && (tx < 0 || tx >= columns)) continue;
32499
34962
  const x = (tx % columns + columns) % columns;
32500
- const patch = spec.level > pyramid.max_level ? patchOfTile(pyramid.patches, spec.level, x, ty) : void 0;
34963
+ const patch = spec.level > pyramid.max_level ? patchOfTile(pyramid.patches, spec.chart, spec.level, x, ty) : void 0;
32501
34964
  if (spec.level > pyramid.max_level && !patch) continue;
32502
34965
  tiles.push({
32503
34966
  cap: CHART_FOLDER[spec.chart],
@@ -33167,7 +35630,8 @@ var LayerTiles = class {
33167
35630
  win.ctx = canvas.getContext("2d") ?? void 0;
33168
35631
  } else win.data = new Uint8ClampedArray(spec.width * spec.height * 4);
33169
35632
  for (const tile of windowTiles(spec, this.block)) {
33170
- const version = tile.patch?.version ?? (tile.cap === "" ? mapTileVersion(this.block, tile.level, tile.x, tile.y) : this.block.version);
35633
+ const chart = CHART_FOLDER.indexOf(tile.cap);
35634
+ const version = tile.patch?.version ?? tileVersion(this.block, chart, tile.level, tile.x, tile.y);
33171
35635
  const url = tileUrl({
33172
35636
  id: this.block.id,
33173
35637
  version
@@ -33239,7 +35703,10 @@ var Viewport = class {
33239
35703
  }
33240
35704
  unproject(px, py) {
33241
35705
  const [x, y] = this.fromScreen(px, py);
33242
- return this.projection.inverse(x, y);
35706
+ if (!this.repeatsHorizontally) return this.projection.inverse(x, y);
35707
+ const { minX, maxX } = this.projection.extent;
35708
+ const worldW = maxX - minX;
35709
+ return this.projection.inverse(((x - minX) % worldW + worldW) % worldW + minX, y);
33243
35710
  }
33244
35711
  get worldWidthPx() {
33245
35712
  const { minX, maxX } = this.projection.extent;
@@ -33258,6 +35725,9 @@ var Viewport = class {
33258
35725
  w
33259
35726
  ].filter((shift) => left + shift < this.width && left + shift + w > 0);
33260
35727
  }
35728
+ shiftInFrame(x) {
35729
+ return this.repeatShifts.find((shift) => x + shift >= 0 && x + shift <= this.width) ?? 0;
35730
+ }
33261
35731
  };
33262
35732
  function repeats(projection, worldWidthPx, width) {
33263
35733
  return projection.cyclic && projection.rectangular && worldWidthPx >= width - .5;
@@ -33694,7 +36164,9 @@ var FlatMap = class {
33694
36164
  await this.adopt(bodyId);
33695
36165
  }
33696
36166
  project(lon, lat) {
33697
- return this.viewport().project(lon, lat);
36167
+ const viewport = this.viewport();
36168
+ const screen = viewport.project(lon, lat);
36169
+ return screen ? [screen[0] + viewport.shiftInFrame(screen[0]), screen[1]] : null;
33698
36170
  }
33699
36171
  unproject(px, py) {
33700
36172
  const found = this.viewport().unproject(px, py);
@@ -36600,9 +39072,11 @@ function attributionChips(ctx, labels = ORBIT_ARCHIVE_ORGANISATIONS) {
36600
39072
  const bodyId = ctx.visibility.focusedBodyId;
36601
39073
  const model = bodyId ? credits.model.get(bodyId) : void 0;
36602
39074
  if (model) organisations.add(model.organisation);
39075
+ const stars = [...new Set(credits.stars.filter((c) => c.license).map((c) => c.organisation))];
36603
39076
  return {
36604
39077
  orbits,
36605
- imagery: [...organisations].sort()
39078
+ imagery: [...organisations].sort(),
39079
+ stars
36606
39080
  };
36607
39081
  }
36608
39082
 
@@ -36638,12 +39112,14 @@ var AttributionControl = class {
36638
39112
  const root = bar();
36639
39113
  const orbits = group();
36640
39114
  const imagery = group();
36641
- root.append(link(HOME_URL, "spacemap", "sm-attribution__home"), orbits, imagery, link(CREDITS_URL, host().messages.credits_see_all()));
39115
+ const stars = group();
39116
+ root.append(link(HOME_URL, "spacemap", "sm-attribution__home"), orbits, imagery, stars, link(CREDITS_URL, host().messages.credits_see_all()));
36642
39117
  this.stop = effect_root(() => {
36643
39118
  user_effect(() => {
36644
39119
  const chips = attributionChips(map.ctx);
36645
39120
  fill(orbits, host().messages.attribution_orbits(), chips.orbits);
36646
39121
  fill(imagery, host().messages.attribution_imagery(), chips.imagery);
39122
+ fill(stars, host().messages.attribution_stars(), chips.stars);
36647
39123
  });
36648
39124
  });
36649
39125
  return root;
@@ -37008,13 +39484,14 @@ async function fetchPanoramas(bodyId) {
37008
39484
  //#endregion
37009
39485
  //#region src/sdk/index.ts
37010
39486
  async function createMap(options) {
37011
- const { container, dataUrl, imagesUrl, liveUrl, tilesUrl, locale, messages, includeNonCommercial, controls, events, ...rest } = options;
39487
+ const { container, dataUrl, imagesUrl, liveUrl, tilesUrl, extrasolarUrl, locale, messages, includeNonCommercial, controls, events, ...rest } = options;
37012
39488
  const element = resolveContainer(container);
37013
39489
  applyHost({
37014
39490
  dataUrl,
37015
39491
  imagesUrl,
37016
39492
  liveUrl,
37017
39493
  tilesUrl,
39494
+ extrasolarUrl,
37018
39495
  locale,
37019
39496
  messages,
37020
39497
  includeNonCommercial
@@ -37035,13 +39512,14 @@ async function createMap(options) {
37035
39512
  return map;
37036
39513
  }
37037
39514
  async function createFlatMap(options) {
37038
- const { container, dataUrl, imagesUrl, liveUrl, tilesUrl, locale, messages, includeNonCommercial, controls, events, ...rest } = options;
39515
+ const { container, dataUrl, imagesUrl, liveUrl, tilesUrl, extrasolarUrl, locale, messages, includeNonCommercial, controls, events, ...rest } = options;
37039
39516
  const element = resolveContainer(container);
37040
39517
  applyHost({
37041
39518
  dataUrl,
37042
39519
  imagesUrl,
37043
39520
  liveUrl,
37044
39521
  tilesUrl,
39522
+ extrasolarUrl,
37045
39523
  locale,
37046
39524
  messages,
37047
39525
  includeNonCommercial
@@ -37056,13 +39534,14 @@ async function createFlatMap(options) {
37056
39534
  return map;
37057
39535
  }
37058
39536
  async function createPanorama(options) {
37059
- const { container, dataUrl, imagesUrl, liveUrl, tilesUrl, locale, messages, includeNonCommercial, controls, events, ...rest } = options;
39537
+ const { container, dataUrl, imagesUrl, liveUrl, tilesUrl, extrasolarUrl, locale, messages, includeNonCommercial, controls, events, ...rest } = options;
37060
39538
  const element = resolveContainer(container);
37061
39539
  applyHost({
37062
39540
  dataUrl,
37063
39541
  imagesUrl,
37064
39542
  liveUrl,
37065
39543
  tilesUrl,
39544
+ extrasolarUrl,
37066
39545
  locale,
37067
39546
  messages,
37068
39547
  includeNonCommercial
@@ -37077,13 +39556,14 @@ async function createPanorama(options) {
37077
39556
  return view;
37078
39557
  }
37079
39558
  async function createSystemMap(options) {
37080
- const { container, dataUrl, imagesUrl, liveUrl, tilesUrl, locale, messages, includeNonCommercial, events, ...rest } = options;
39559
+ const { container, dataUrl, imagesUrl, liveUrl, tilesUrl, extrasolarUrl, locale, messages, includeNonCommercial, events, ...rest } = options;
37081
39560
  const element = resolveContainer(container);
37082
39561
  applyHost({
37083
39562
  dataUrl,
37084
39563
  imagesUrl,
37085
39564
  liveUrl,
37086
39565
  tilesUrl,
39566
+ extrasolarUrl,
37087
39567
  locale,
37088
39568
  messages,
37089
39569
  includeNonCommercial
@@ -37111,6 +39591,7 @@ function applyHost(options) {
37111
39591
  if (options.imagesUrl !== void 0) overrides.imagesUrl = options.imagesUrl;
37112
39592
  if (options.liveUrl !== void 0) overrides.liveUrl = options.liveUrl;
37113
39593
  if (options.tilesUrl !== void 0) overrides.tilesUrl = options.tilesUrl;
39594
+ if (options.extrasolarUrl !== void 0) overrides.extrasolarUrl = options.extrasolarUrl;
37114
39595
  if (options.locale !== void 0) overrides.locale = () => options.locale;
37115
39596
  if (options.messages !== void 0) overrides.messages = options.messages;
37116
39597
  if (options.includeNonCommercial) overrides.textures = "non-commercial";