spacemap 0.1.18 → 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";
@@ -11429,7 +12238,7 @@ function isLowEndDevice() {
11429
12238
  return typeof mem === "number" && mem > 0 && mem <= LOW_MEMORY_GIB;
11430
12239
  }
11431
12240
  var coarsePointer;
11432
- function isCoarsePointer() {
12241
+ function isCoarsePointer$1() {
11433
12242
  if (coarsePointer === void 0) coarsePointer = typeof window !== "undefined" && !!window.matchMedia?.("(pointer: coarse)").matches;
11434
12243
  return coarsePointer;
11435
12244
  }
@@ -11550,7 +12359,7 @@ var ATMOSPHERE_QUALITY_PRESETS = {
11550
12359
  }
11551
12360
  };
11552
12361
  function heuristicAtmosphereTier() {
11553
- if (isCoarsePointer()) return isLowEndDevice() ? "low" : "medium";
12362
+ if (isCoarsePointer$1()) return isLowEndDevice() ? "low" : "medium";
11554
12363
  return isLowEndDevice() ? "high" : "ultra";
11555
12364
  }
11556
12365
  function resolveAtmosphereTier(tier) {
@@ -12917,7 +13726,7 @@ function attachCanvasForwarders(el, canvas) {
12917
13726
 
12918
13727
  //#endregion
12919
13728
  //#region src/lib/scene/relief/worker.ts?worker&inline
12920
- 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)};";
12921
13730
  var blob$1 = typeof self !== "undefined" && self.Blob && new Blob(["URL.revokeObjectURL(import.meta.url);", jsContent$1], { type: "text/javascript;charset=utf-8" });
12922
13731
  function WorkerWrapper$1(options) {
12923
13732
  let objURL;
@@ -13112,34 +13921,40 @@ function placeWindow(chart, level, x, y, previous) {
13112
13921
  ty0
13113
13922
  };
13114
13923
  }
13115
- 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) {
13116
13928
  const up = patch.max_level - level;
13117
- 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;
13118
13930
  }
13119
- function patchOfTile(patches, level, x, y) {
13120
- 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));
13121
13933
  }
13122
- function mapTileVersion(pyramid, level, x, y) {
13123
- 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));
13124
13936
  return over?.length ? over.map((patch) => patch.version).join(".") : pyramid.version;
13125
13937
  }
13126
13938
  var REACH_TILES = 4 / 2 - .5;
13127
- function deepestLevel(maxLevel, patches, x, y) {
13939
+ function deepestLevel(maxLevel, patches, chart, x, y) {
13128
13940
  let deepest = maxLevel;
13129
13941
  if (!patches) return deepest;
13130
- for (const patch of patches) for (let level = patch.max_level; level > deepest; level--) {
13131
- const up = patch.max_level - level;
13132
- const columns = chartColumns(0, level);
13133
- const x0 = patch.x[0] >> up;
13134
- const y0 = patch.y[0] >> up;
13135
- const halfWidth = ((patch.x[1] >> up) + 1 - x0) / 2;
13136
- const halfHeight = ((patch.y[1] >> up) + 1 - y0) / 2;
13137
- let east = x * columns - (x0 + halfWidth);
13138
- east -= columns * Math.round(east / columns);
13139
- const south = y * chartRows(0, level) - (y0 + halfHeight);
13140
- if (Math.abs(east) - halfWidth <= REACH_TILES && Math.abs(south) - halfHeight <= REACH_TILES) {
13141
- deepest = level;
13142
- 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
+ }
13143
13958
  }
13144
13959
  }
13145
13960
  return deepest;
@@ -13147,7 +13962,7 @@ function deepestLevel(maxLevel, patches, x, y) {
13147
13962
  function windowHome(placement, x, y) {
13148
13963
  const columns = chartColumns(placement.chart, placement.level);
13149
13964
  let east = x * columns - placement.tx0;
13150
- east -= columns * Math.round(east / columns);
13965
+ if (placement.chart === 0) east -= columns * Math.round(east / columns);
13151
13966
  return {
13152
13967
  x: east,
13153
13968
  y: y * chartRows(placement.chart, placement.level) - placement.ty0
@@ -13165,6 +13980,7 @@ function slotTile(placement, sx, sy) {
13165
13980
 
13166
13981
  //#endregion
13167
13982
  //#region src/lib/scene/render-tier.ts
13983
+ var MB = 1024 * 1024;
13168
13984
  var RENDER_TIER_PRESETS = {
13169
13985
  low: {
13170
13986
  maxPixelRatio: 1.25,
@@ -13178,7 +13994,10 @@ var RENDER_TIER_PRESETS = {
13178
13994
  reliefWindows: 2,
13179
13995
  reliefShadowSteps: 12,
13180
13996
  terrainBandCells: 16,
13181
- maxSkyboxFace: 2048
13997
+ maxSkyboxFace: 2048,
13998
+ starPoints: 12e5,
13999
+ starBytes: 80 * MB,
14000
+ starFetches: 3
13182
14001
  },
13183
14002
  medium: {
13184
14003
  maxPixelRatio: 1.5,
@@ -13192,7 +14011,10 @@ var RENDER_TIER_PRESETS = {
13192
14011
  reliefWindows: 3,
13193
14012
  reliefShadowSteps: 16,
13194
14013
  terrainBandCells: 24,
13195
- maxSkyboxFace: 2048
14014
+ maxSkyboxFace: 2048,
14015
+ starPoints: 16e5,
14016
+ starBytes: 160 * MB,
14017
+ starFetches: 4
13196
14018
  },
13197
14019
  high: {
13198
14020
  maxPixelRatio: 2,
@@ -13206,7 +14028,10 @@ var RENDER_TIER_PRESETS = {
13206
14028
  reliefWindows: 4,
13207
14029
  reliefShadowSteps: 32,
13208
14030
  terrainBandCells: 32,
13209
- maxSkyboxFace: Infinity
14031
+ maxSkyboxFace: Infinity,
14032
+ starPoints: 25e5,
14033
+ starBytes: 400 * MB,
14034
+ starFetches: 6
13210
14035
  }
13211
14036
  };
13212
14037
  function maxTextureTier() {
@@ -13228,7 +14053,7 @@ function resolveRenderTier() {
13228
14053
  const measured = sceneSettings().atmosphereCalibration?.tier;
13229
14054
  if (measured === "low" || measured === "medium") return measured;
13230
14055
  if (measured === "high" || measured === "ultra") return "high";
13231
- if (isCoarsePointer()) return isLowEndDevice() ? "low" : "medium";
14056
+ if (isCoarsePointer$1()) return isLowEndDevice() ? "low" : "medium";
13232
14057
  return isLowEndDevice() ? "medium" : "high";
13233
14058
  }
13234
14059
  function currentRenderTier() {
@@ -13238,6 +14063,15 @@ function renderPixelRatio() {
13238
14063
  if (typeof window === "undefined") return 1;
13239
14064
  return Math.min(window.devicePixelRatio, currentRenderTier().maxPixelRatio);
13240
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
+ }
13241
14075
 
13242
14076
  //#endregion
13243
14077
  //#region src/lib/scene/objects/surface/tile-chain.ts
@@ -13271,9 +14105,9 @@ void tileCap( vec3 dir, vec3 ddx, vec3 ddy, out float side, out mat3x2 cap ) {
13271
14105
  );
13272
14106
  }
13273
14107
 
13274
- // The point at + off on the map, counted from at, and on the cap of its own
13275
- // hemisphere, which side gives. A position on the sphere has seven digits,
13276
- // 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.
13277
14111
  void tileChartsFrom( vec3 at, vec3 off, out mat3x2 map, out mat3x2 cap, out float side ) {
13278
14112
  vec3 pos = at + off;
13279
14113
  vec3 ox = dFdx( off );
@@ -13295,12 +14129,19 @@ void tileChartsFrom( vec3 at, vec3 off, out mat3x2 map, out mat3x2 cap, out floa
13295
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
13296
14130
  );
13297
14131
 
13298
- // A cap goes no deeper than the pyramid, so the direction itself will do.
13299
14132
  float range = length( pos );
13300
14133
  vec3 dir = pos / range;
13301
14134
  vec3 ddx = ( ox - dir * dot( dir, ox ) ) / range;
13302
14135
  vec3 ddy = ( oy - dir * dot( dir, oy ) ) / range;
13303
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)} );
13304
14145
  }
13305
14146
 
13306
14147
  // Long and short half-axes of the footprint whose sides are fx and fy. The
@@ -13343,7 +14184,7 @@ float tileWindowWeight( mat3x2 map, mat3x2 cap, float side, vec4 window, vec4 gr
13343
14184
 
13344
14185
  float rows = ${4 .toFixed(1)};
13345
14186
  // Offset from the window's north-west corner, in tiles.
13346
- vec2 rel = onMap ? home + chart[ 0 ] * grid.xy : chart[ 0 ] * grid.xy - grid.zw;
14187
+ vec2 rel = home + chart[ 0 ] * grid.xy;
13347
14188
  if ( onMap ) {
13348
14189
  // Half a pixel in from a pole of the map: past it the filter would
13349
14190
  // read the far row. The map's longitude wraps.
@@ -13445,8 +14286,8 @@ function release(store, win) {
13445
14286
  store.free.push(win.layer);
13446
14287
  }
13447
14288
  function patchOf(tiles, win, tile) {
13448
- if (win.chart !== 0 || win.level <= tiles.max_level) return void 0;
13449
- 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);
13450
14291
  }
13451
14292
  function exists(tiles, win, slot) {
13452
14293
  if (win.level <= tiles.max_level) return true;
@@ -13502,7 +14343,7 @@ function pump(chain, store, tiles, win, x, y, renderer, invalidate) {
13502
14343
  });
13503
14344
  chain.inflight++;
13504
14345
  const folder = CHART_FOLDER[win.chart];
13505
- 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);
13506
14347
  const pyramid = {
13507
14348
  id: tiles.id,
13508
14349
  version
@@ -13557,9 +14398,9 @@ function pointUnderCamera(mesh, camera) {
13557
14398
  return over.copy(camera.position).applyMatrix4(inverse.copy(mesh.matrixWorld).invert()).normalize();
13558
14399
  }
13559
14400
  function finestLevelAt$1(chain, tiles, over) {
13560
- if (cameraChart(over.y, chain?.chart) !== 0) return tiles.max_level;
13561
- const point = chartPoint(0, over.x, over.y, over.z);
13562
- 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);
13563
14404
  }
13564
14405
  function chainLength(tiles, limit) {
13565
14406
  let finest = tiles.max_level;
@@ -13581,7 +14422,11 @@ function updateChain(chain, tiles, count, level, over, radius, frame, renderer,
13581
14422
  anchor.set(Math.fround(over.x * radius), Math.fround(over.y * radius), Math.fround(over.z * radius));
13582
14423
  chain.uniforms.anchor.value.copy(anchor);
13583
14424
  const range = anchor.length();
13584
- 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));
13585
14430
  const windowOf = (link) => store.windows.find((win) => win.chart === link.chart && win.level === link.level);
13586
14431
  store.windows = store.windows.filter((win) => {
13587
14432
  win.rank = links.findIndex((link) => link.chart === win.chart && link.level === win.level);
@@ -13622,10 +14467,9 @@ function updateChain(chain, tiles, count, level, over, radius, frame, renderer,
13622
14467
  continue;
13623
14468
  }
13624
14469
  chain.uniforms.grid.value[i].set(chartColumns(win.chart, win.level), chartRows(win.chart, win.level), win.placement.tx0, win.placement.ty0);
13625
- if (win.chart === 0) {
13626
- const from = windowHome(win.placement, home.x, home.y);
13627
- chain.uniforms.home.value[i].set(from.x, from.y);
13628
- }
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);
13629
14473
  link.set(win.chart, win.layer, win.level, 1);
13630
14474
  let loaded = 0;
13631
14475
  for (let slot = 0; slot < SLOTS; slot++) if (isLoaded(store, win.layer, slot)) loaded |= 1 << slot;
@@ -13740,10 +14584,10 @@ void reliefSight( int i, mat3x2 map, mat3x2 cap, float side, vec2 mapStep, vec2
13740
14584
  vec4 grid = uReliefGrid[ i ];
13741
14585
  bool onMap = window.x < 0.5;
13742
14586
  float rows = ${4 .toFixed(1)};
13743
- // In tiles from the window's north-west corner. The map is counted from
13744
- // the point under the camera, so the fragment is on the side of the globe
13745
- // nearest the window.
13746
- 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;
13747
14591
  vec2 rate = ( onMap ? mapStep : capStep ) * grid.xy;
13748
14592
 
13749
14593
  // Half a pixel in from the window's sides: past it the filter reads the
@@ -17356,7 +18200,7 @@ function setLabelName(bo, name, variant, isLarge) {
17356
18200
  }
17357
18201
  function getLabelVariant(body) {
17358
18202
  const t = body.data.objectType;
17359
- 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";
17360
18204
  if (t === ObjectType.SPACECRAFT) return "spacecraft";
17361
18205
  if (t === ObjectType.DEBRIS) return "debris";
17362
18206
  return "none";
@@ -18315,6 +19159,231 @@ async function loadSkybox(scene, renderer, ctx, disposed = () => false) {
18315
19159
  }
18316
19160
  }
18317
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
+
18318
19387
  //#endregion
18319
19388
  //#region src/lib/scene/setup/scene-load.ts
18320
19389
  var EAGER_ZOOM1_PARTS = 13;
@@ -18423,6 +19492,7 @@ async function loadScene(ctx, date, targetId) {
18423
19492
  });
18424
19493
  const probePromise = metadataPromise.then((metadata) => buildProbeStore(metadata, jd, ctx.layers));
18425
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;
18426
19496
  const metadata = await metadataPromise;
18427
19497
  loadProgress.reach("metadata");
18428
19498
  ctx.chebStore = await chebPromise;
@@ -18448,7 +19518,8 @@ async function loadScene(ctx, date, targetId) {
18448
19518
  addedSinceFlush = false;
18449
19519
  ctx.bodies.flushMinor(added);
18450
19520
  };
18451
- if (targetId && !ctx.getBody(targetId)) {
19521
+ if (extrasolarTarget) await extrasolarTarget;
19522
+ else if (targetId && !ctx.getBody(targetId)) {
18452
19523
  const placeholders = await createPlaceholderBody(targetId, ctx);
18453
19524
  if (routePlaceholders(ctx, placeholders, labels) > 0) noteAdded();
18454
19525
  if (placeholders.length > 0) flush();
@@ -18456,7 +19527,7 @@ async function loadScene(ctx, date, targetId) {
18456
19527
  ctx.bodies.majorBodies = major.filter((b) => b.data.objectType !== ObjectType.BARYCENTER && b.data.objectType !== ObjectType.LAGRANGE_POINT && b.data.orbitalSource !== OrbitalSource.SPICE_PROBE);
18457
19528
  try {
18458
19529
  ctx.refresher = new ZoneRefresher(ctx, metadata, loader, date);
18459
- if (targetId) ctx.refresher.want(targetId);
19530
+ if (targetId && !extrasolarTarget) ctx.refresher.want(targetId);
18460
19531
  } catch (e) {
18461
19532
  console.error("scene-load: ZoneRefresher init failed; time-segmented zones stay empty:", e);
18462
19533
  }
@@ -18625,12 +19696,17 @@ var ContextManager = class {
18625
19696
  console.error("[scene-load]", e);
18626
19697
  }
18627
19698
  refreshTick(date) {
19699
+ if (this.visibility.systemShare === 0) return;
18628
19700
  this.refresher?.tick(date);
18629
19701
  }
18630
19702
  async ready(date) {
18631
19703
  await this.refresher?.ready(date);
18632
19704
  }
18633
19705
  async ensureBody(targetId, date) {
19706
+ if (isExtrasolarObjectId(targetId)) {
19707
+ await ensureExtrasolarBody(this, targetId);
19708
+ return;
19709
+ }
18634
19710
  await this.refresher?.ensureBody(targetId, date);
18635
19711
  }
18636
19712
  applyGroupFilter(slug) {
@@ -19342,6 +20418,9 @@ function installOutputDither(pass) {
19342
20418
  //#endregion
19343
20419
  //#region src/lib/scene/setup/three-boot.ts
19344
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;
19345
20424
  function probeClipControl() {
19346
20425
  try {
19347
20426
  const gl = document.createElement("canvas").getContext("webgl2");
@@ -19395,7 +20474,9 @@ function bootThree(canvas, labelContainer, ctx) {
19395
20474
  shadowLight.shadow.bias = -1e-4;
19396
20475
  scene.add(shadowLight);
19397
20476
  scene.add(shadowLight.target);
19398
- 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);
19399
20480
  const composerTarget = new WebGLRenderTarget(canvas.clientWidth, canvas.clientHeight, {
19400
20481
  type: HalfFloatType,
19401
20482
  resolveDepthBuffer: false,
@@ -19427,38 +20508,1006 @@ function bootThree(canvas, labelContainer, ctx) {
19427
20508
  };
19428
20509
  }
19429
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
+
19430
21461
  //#endregion
19431
21462
  //#region src/lib/scene/interaction/picking.ts
19432
- function pickMoonDot(pointer, camera, ctx, focusTruePos, canvasWidth, canvasHeight, tmpV3, jd, pointerType, isVisible) {
19433
- 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;
19434
21488
  const px = (pointer.x + 1) * .5 * canvasWidth;
19435
21489
  const py = (1 - pointer.y) * .5 * canvasHeight;
19436
- const v = tmpV3;
19437
- const [fx, fy, fz] = focusTruePos;
21490
+ const v = pick.tmpV3;
21491
+ const [fx, fy, fz] = pick.focusTruePos;
19438
21492
  const cam = camera.position;
19439
21493
  let bestBody;
19440
- let bestScreenDist = SCREEN_THRESHOLD;
21494
+ let bestScreenDist = pickRadiusPx(pick.pointerType);
19441
21495
  let bestWorldDist = Infinity;
19442
- const testBody = (body) => {
21496
+ for (const body of bodies) {
19443
21497
  const at = ctx.place(body, jd);
19444
- if (!at) return;
21498
+ if (!at) continue;
19445
21499
  v.set(at[0] - fx, at[1] - fy, at[2] - fz);
19446
21500
  const worldDist = Math.hypot(v.x - cam.x, v.y - cam.y, v.z - cam.z);
19447
21501
  v.project(camera);
19448
- if (!ndcZVisible(v.z)) return;
21502
+ if (!ndcZVisible(v.z)) continue;
19449
21503
  const sx = (v.x + 1) * .5 * canvasWidth;
19450
21504
  const sy = (1 - v.y) * .5 * canvasHeight;
19451
21505
  const screenDist = Math.hypot(sx - px, sy - py);
19452
- if (!(screenDist < bestScreenDist || screenDist === bestScreenDist && worldDist < bestWorldDist)) return;
19453
- 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;
19454
21508
  bestScreenDist = screenDist;
19455
21509
  bestWorldDist = worldDist;
19456
21510
  bestBody = body;
19457
- };
19458
- for (const body of ctx.bodies.majorBodies) {
19459
- if (body.data.objectType !== ObjectType.MOON) continue;
19460
- if (!ctx.visibility.isMoonGroupVisible(body.data.parentId)) continue;
19461
- testBody(body);
19462
21511
  }
19463
21512
  if (!bestBody) return null;
19464
21513
  return {
@@ -19470,7 +21519,12 @@ function pickMoonDot(pointer, camera, ctx, focusTruePos, canvasWidth, canvasHeig
19470
21519
 
19471
21520
  //#endregion
19472
21521
  //#region src/lib/scene/interaction/pointer.ts
19473
- 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
+ }
19474
21528
  function inHiddenRoot(obj) {
19475
21529
  let top = obj;
19476
21530
  while (top.parent?.parent) top = top.parent;
@@ -19478,11 +21532,13 @@ function inHiddenRoot(obj) {
19478
21532
  }
19479
21533
  var PointerInteraction = class {
19480
21534
  downPos = new Vector2();
21535
+ down = /* @__PURE__ */ new Set();
21536
+ gesture = false;
19481
21537
  pointer = new Vector2();
19482
21538
  _tmpPointer = new Vector2();
19483
21539
  raycaster = new Raycaster();
19484
21540
  _tmpV3 = new Vector3();
19485
- 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) {
19486
21542
  this.canvas = canvas;
19487
21543
  this.camera = camera;
19488
21544
  this.ctx = ctx;
@@ -19495,22 +21551,39 @@ var PointerInteraction = class {
19495
21551
  this.gpuPick = gpuPick;
19496
21552
  this.getClouds = getClouds;
19497
21553
  this.pickRegistry = pickRegistry;
21554
+ this.getStars = getStars;
21555
+ this.onStarPick = onStarPick;
19498
21556
  }
19499
21557
  attach() {
19500
21558
  this.canvas.addEventListener("pointerdown", this.onDown);
21559
+ this.canvas.addEventListener("pointermove", this.onMove);
19501
21560
  this.canvas.addEventListener("pointerup", this.onUp);
21561
+ this.canvas.addEventListener("pointercancel", this.onCancel);
19502
21562
  }
19503
21563
  detach() {
19504
21564
  this.canvas.removeEventListener("pointerdown", this.onDown);
21565
+ this.canvas.removeEventListener("pointermove", this.onMove);
19505
21566
  this.canvas.removeEventListener("pointerup", this.onUp);
21567
+ this.canvas.removeEventListener("pointercancel", this.onCancel);
19506
21568
  }
19507
21569
  onDown = (e) => {
19508
- 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;
19509
21584
  };
19510
21585
  onUp = (e) => {
19511
- const dx = e.clientX - this.downPos.x;
19512
- const dy = e.clientY - this.downPos.y;
19513
- if (dx * dx + dy * dy > CLICK_DRAG_PX2) return;
21586
+ if (!this.down.delete(e.pointerId) || this.gesture || movedTooFar(e, this.downPos)) return;
19514
21587
  const rect = this.canvas.getBoundingClientRect();
19515
21588
  this.pointer.set((e.clientX - rect.left) / rect.width * 2 - 1, -((e.clientY - rect.top) / rect.height) * 2 + 1);
19516
21589
  const modelBody = this.modelPick(this.pointer.x, this.pointer.y);
@@ -19520,11 +21593,41 @@ var PointerInteraction = class {
19520
21593
  }
19521
21594
  this.raycaster.setFromCamera(this.pointer, this.camera);
19522
21595
  const meshBody = this.resolveMeshHit(this.raycaster.intersectObjects([...this.clickables, ...attachedModelRoots]))?.body;
19523
- 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);
19524
21609
  const gpuHit = this.gpuPickCloud(e, rect);
19525
21610
  const bestBody = this.nearer(moonHit, gpuHit)?.body ?? meshBody;
19526
- if (bestBody) this.onPick(bestBody);
21611
+ if (bestBody) {
21612
+ this.onPick(bestBody);
21613
+ return;
21614
+ }
21615
+ this.pickStar(e, rect, dots);
19527
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
+ }
19528
21631
  nearer(a, b) {
19529
21632
  if (!a) return b;
19530
21633
  if (!b) return a;
@@ -19533,9 +21636,8 @@ var PointerInteraction = class {
19533
21636
  return a;
19534
21637
  }
19535
21638
  gpuPickCloud(e, rect) {
19536
- const radius = e.pointerType === "touch" || e.pointerType === "pen" ? 48 : 24;
19537
21639
  const jd = this.clock.jd;
19538
- 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));
19539
21641
  for (const c of candidates) {
19540
21642
  const bodyId = this.pickRegistry.resolve(c.pickId);
19541
21643
  if (!bodyId) continue;
@@ -19618,6 +21720,7 @@ var GpuPickPass = class {
19618
21720
  buffer = new Uint8Array(0);
19619
21721
  prevClear = new Color();
19620
21722
  proxies = [];
21723
+ idleGeometry = new BufferGeometry();
19621
21724
  constructor(renderer, camera) {
19622
21725
  this.renderer = renderer;
19623
21726
  this.target = new WebGLRenderTarget(1, 1, { depthBuffer: true });
@@ -19640,19 +21743,33 @@ var GpuPickPass = class {
19640
21743
  this.target.setSize(px, px);
19641
21744
  this.buffer = new Uint8Array(px * px * 4);
19642
21745
  }
19643
- proxyAt(index, src) {
21746
+ proxyAt(index, src, material) {
19644
21747
  let p = this.proxies[index];
19645
21748
  if (!p) {
19646
- p = new Points(src.geometry, this.material);
21749
+ p = new Points(src.geometry, material);
19647
21750
  p.frustumCulled = false;
19648
21751
  p.matrixAutoUpdate = false;
19649
21752
  p.matrixWorldAutoUpdate = false;
19650
21753
  this.proxies[index] = p;
19651
- } else p.geometry = src.geometry;
21754
+ } else {
21755
+ p.geometry = src.geometry;
21756
+ p.material = material;
21757
+ }
19652
21758
  p.matrixWorld.copy(src.matrixWorld);
19653
21759
  return p;
19654
21760
  }
19655
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) {
19656
21773
  const r = Math.max(1, Math.round(radiusCss));
19657
21774
  const boxPx = r * 2 + 1;
19658
21775
  this.ensureSize(boxPx);
@@ -19660,13 +21777,11 @@ var GpuPickPass = class {
19660
21777
  const cy = clientY - rect.top;
19661
21778
  const offsetX = cx - r;
19662
21779
  const offsetY = cy - r;
19663
- this.scene.clear();
19664
21780
  let n = 0;
19665
- for (const pts of clouds) {
19666
- if (!pts.visible) continue;
19667
- if (!pts.geometry.getAttribute("pickColor")) continue;
19668
- if (pts.geometry.drawRange.count === 0) continue;
19669
- 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));
19670
21785
  }
19671
21786
  if (n === 0) return [];
19672
21787
  this.pickCamera.copy(camera);
@@ -19682,12 +21797,21 @@ var GpuPickPass = class {
19682
21797
  this.renderer.clear(true, true, false);
19683
21798
  this.renderer.render(this.scene, this.pickCamera);
19684
21799
  this.renderer.readRenderTargetPixels(this.target, 0, 0, boxPx, boxPx, this.buffer);
21800
+ this.letGo(n);
19685
21801
  this.renderer.setRenderTarget(prevTarget);
19686
21802
  this.renderer.autoClear = prevAutoClear;
19687
21803
  this.renderer.setClearColor(this.prevClear, prevClearAlpha);
19688
21804
  this.pickCamera.clearViewOffset();
19689
21805
  return this.decode(boxPx, r, rect, offsetX, offsetY);
19690
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
+ }
19691
21815
  decode(boxPx, r, rect, offsetX, offsetY) {
19692
21816
  const buf = this.buffer;
19693
21817
  const nearest = /* @__PURE__ */ new Map();
@@ -19717,6 +21841,7 @@ var GpuPickPass = class {
19717
21841
  dispose() {
19718
21842
  this.target.dispose();
19719
21843
  this.material.dispose();
21844
+ this.idleGeometry.dispose();
19720
21845
  this.proxies.length = 0;
19721
21846
  }
19722
21847
  };
@@ -20130,7 +22255,7 @@ function buildMajorBodies(bodies, scene, clickables, meshToBody, bodyObjects, re
20130
22255
  const id = body.data.id;
20131
22256
  const isHalfPromoted = halfPromoteIds?.has(id) ?? false;
20132
22257
  const t = body.data.objectType;
20133
- 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;
20134
22259
  const color = resolveBodyColor(body.data);
20135
22260
  const radius = kmToScene(effectiveRadiusKm(body.data));
20136
22261
  const isStar = t === ObjectType.STAR;
@@ -20173,7 +22298,7 @@ function buildMajorBodies(bodies, scene, clickables, meshToBody, bodyObjects, re
20173
22298
  }
20174
22299
  }
20175
22300
  const variant = getLabelVariant(body);
20176
- const isLarge = isStar || t === ObjectType.PLANET;
22301
+ const isLarge = isStar || t === ObjectType.EXTRASOLAR || t === ObjectType.PLANET;
20177
22302
  const isMinor = isHalfPromoted || MINOR_PROMOTED_IDS.has(id) || body.data.isMinor === true;
20178
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);
20179
22304
  if (label) {
@@ -21066,7 +23191,7 @@ function buildTrails(bodyObjects, scene, basisPos = [
21066
23191
  for (const [, bo] of bodyObjects) {
21067
23192
  if (bo.trail !== null) continue;
21068
23193
  const { body } = bo;
21069
- if (body.data.objectType === ObjectType.STAR) continue;
23194
+ if (body.data.objectType === ObjectType.STAR || isExtrasolar(body)) continue;
21070
23195
  if (body.data.orbitalSource === OrbitalSource.SPICE_PROBE) {
21071
23196
  if (bo.isMinor && !bo.mesh) continue;
21072
23197
  } else if (!bo.mesh && !bo.focusUpgraded && isMeshUpgradable(body)) continue;
@@ -22182,10 +24307,18 @@ var PickRegistry = class {
22182
24307
  #next = 1;
22183
24308
  #groups = /* @__PURE__ */ new Map();
22184
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) {
22185
24316
  const base = this.#next;
22186
- this.#next += keys.length;
24317
+ this.#next += length;
22187
24318
  this.#groups.set(groupId, {
24319
+ groupId,
22188
24320
  base,
24321
+ length,
22189
24322
  keys
22190
24323
  });
22191
24324
  return base;
@@ -22194,8 +24327,20 @@ var PickRegistry = class {
22194
24327
  this.#groups.delete(groupId);
22195
24328
  }
22196
24329
  resolve(pickId) {
22197
- if (pickId === 0) return null;
22198
- 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;
22199
24344
  return null;
22200
24345
  }
22201
24346
  clear() {
@@ -22295,6 +24440,8 @@ var PointCloudSystem = class {
22295
24440
  brightMult: 1,
22296
24441
  opacity: EMPHASIS_BASE_OPACITY
22297
24442
  };
24443
+ fade = 1;
24444
+ moonFades = /* @__PURE__ */ new Map();
22298
24445
  constructor(ctx, scene, bodyObjects, circleTexture, focus, mapLayer, onBasisRebuilt) {
22299
24446
  this.ctx = ctx;
22300
24447
  this.scene = scene;
@@ -22314,13 +24461,13 @@ var PointCloudSystem = class {
22314
24461
  }
22315
24462
  applyPointBudget() {
22316
24463
  let total = 0;
22317
- for (const kind of WORKER_CLOUD_KINDS) for (const pts of this.workerPoints[kind].values()) if (pts.visible) total += pts.userData.solvedCount ?? 0;
22318
- const fraction = total > this.pointBudget ? this.pointBudget / total : 1;
22319
- if (fraction === this.drawFraction) return;
22320
- 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;
22321
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;
22322
24469
  const n = pts.userData.solvedCount;
22323
- 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));
22324
24471
  }
22325
24472
  }
22326
24473
  setEarthSatEmphasis(count) {
@@ -22343,7 +24490,49 @@ var PointCloudSystem = class {
22343
24490
  const { size, dim, opacity } = this.earthSatEmphasis;
22344
24491
  mat.size = size;
22345
24492
  mat.color.setRGB(dim, dim, dim);
22346
- 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);
22347
24536
  }
22348
24537
  setEmphasizedSmallBodyZone(zone, count) {
22349
24538
  const prev = this.emphasizedSmallBodyZone;
@@ -22375,14 +24564,14 @@ var PointCloudSystem = class {
22375
24564
  const { sizeMult, brightMult, opacity } = this.smallBodyEmphasis;
22376
24565
  mat.size = baseSize * sizeMult;
22377
24566
  mat.color.copy(baseColor).multiplyScalar(brightMult);
22378
- mat.opacity = opacity;
24567
+ this.setOpacity(pts, opacity);
22379
24568
  }
22380
24569
  resetSmallBodyEmphasis(pts) {
22381
24570
  if (pts.userData.smallBodyBaseSize === void 0) return;
22382
24571
  const mat = pts.material;
22383
24572
  mat.size = pts.userData.smallBodyBaseSize;
22384
24573
  mat.color.copy(pts.userData.smallBodyBaseColor);
22385
- mat.opacity = EMPHASIS_BASE_OPACITY;
24574
+ this.setOpacity(pts, EMPHASIS_BASE_OPACITY);
22386
24575
  pts.userData.smallBodyBaseSize = void 0;
22387
24576
  pts.userData.smallBodyBaseColor = void 0;
22388
24577
  }
@@ -22397,8 +24586,10 @@ var PointCloudSystem = class {
22397
24586
  for (const kind of WORKER_CLOUD_KINDS) for (const [key, p] of this.workerPoints[kind]) {
22398
24587
  const bucketKey = parentIdFromSubkey(key);
22399
24588
  p.userData.parentBodyId = this.cloudParentId(kind, bucketKey);
24589
+ this.fadeNewCloud(p);
22400
24590
  this.emphasiseNewCloud(kind, bucketKey, p);
22401
24591
  }
24592
+ for (const p of this.moonPoints.values()) this.fadeNewCloud(p);
22402
24593
  this.assignMapLayer();
22403
24594
  }
22404
24595
  assignMapLayer() {
@@ -22556,6 +24747,7 @@ var PointCloudSystem = class {
22556
24747
  0,
22557
24748
  0
22558
24749
  ];
24750
+ this.fadeNewCloud(pts);
22559
24751
  this.emphasiseNewCloud(kind, bucketKey, pts);
22560
24752
  this.workerPoints[kind].set(key, pts);
22561
24753
  this.pendingSceneAdds.push(pts);
@@ -22613,7 +24805,8 @@ var PointCloudSystem = class {
22613
24805
  posAttr.needsUpdate = true;
22614
24806
  this.bindPickIds(pts, pickIds);
22615
24807
  pts.userData.solvedCount = count;
22616
- 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));
22617
24810
  this.lastSolvedJd.set(groupId, jd);
22618
24811
  pts.userData.frontBasis = [
22619
24812
  basisUsed[0],
@@ -22749,7 +24942,7 @@ var PointCloudSystem = class {
22749
24942
  for (const [parentId, moons] of this.moonsByParent()) {
22750
24943
  const pts = this.moonPoints.get(parentId);
22751
24944
  if (!pts) continue;
22752
- if (!this.ctx.visibility.isMoonGroupVisible(parentId)) continue;
24945
+ if (!pts.visible && !this.ctx.visibility.isMoonGroupVisible(parentId)) continue;
22753
24946
  const posAttr = pts.geometry.getAttribute("position");
22754
24947
  const arr = posAttr.array;
22755
24948
  const n = Math.min(moons.length, arr.length / 3);
@@ -23054,9 +25247,16 @@ var _anchorOut = {
23054
25247
  };
23055
25248
  var _modelSphereCenter = new Vector3();
23056
25249
  var _anchorProj = new Vector3();
23057
- 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) {
23058
25256
  const layers = ctx.layers;
23059
25257
  const hideOrbits = !layers.isVisible("orbits");
25258
+ const systemShare = ctx.visibility.systemShare;
25259
+ const starShare = ctx.visibility.starShare;
23060
25260
  const showNomenclature = layers.isVisible("nomenclature");
23061
25261
  const fovRad = camera.fov * Math.PI / 180;
23062
25262
  const screenW = renderer.domElement.clientWidth;
@@ -23188,10 +25388,18 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
23188
25388
  let isClose;
23189
25389
  const isFocused = body.data.id === focusedBodyId;
23190
25390
  const isSystemRoot = focusedSystemId !== null && body.data.id === focusedSystemId;
23191
- 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)) {
23192
25400
  const ot = body.data.objectType;
23193
25401
  if (ot === ObjectType.BARYCENTER || ot === ObjectType.LAGRANGE_POINT) {
23194
- let visible = true;
25402
+ let visible = systemShare > 0;
23195
25403
  const primaryId = !isFocused ? barycenterPrimaryId(body.data.id) : void 0;
23196
25404
  if (primaryId) {
23197
25405
  const primary = bodyObjects.get(primaryId)?.body ?? ctx.getBody(primaryId);
@@ -23204,7 +25412,9 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
23204
25412
  showLabel = visible;
23205
25413
  isClose = false;
23206
25414
  } else {
23207
- 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;
23208
25418
  group.visible = visible;
23209
25419
  if (trail) trail.visible = visible;
23210
25420
  showLabel = visible;
@@ -23213,18 +25423,32 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
23213
25423
  } else {
23214
25424
  const isMoon = body.data.objectType === ObjectType.MOON;
23215
25425
  const vis = isMoon ? ctx.visibility.getMoonVisibility(body) : ctx.visibility.getPlanetVisibility(body, dist);
23216
- 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);
23217
25428
  group.visible = vf.groupVisible;
23218
25429
  if (trail) trail.visible = vf.orbitVisible;
23219
25430
  showLabel = vf.showLabel;
23220
25431
  isClose = vf.isClose;
23221
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
+ }
23222
25442
  if (!drawn) {
23223
25443
  group.visible = false;
23224
25444
  showLabel = false;
23225
25445
  isClose = false;
23226
25446
  }
23227
25447
  if (trail && (!drawn || body.orbitCenter === null)) trail.visible = false;
25448
+ if (trail) {
25449
+ trail.userData.zoneShare = zoneShare;
25450
+ trail.userData.thresholdShare = orbitThresholdShare;
25451
+ }
23228
25452
  let starIsPoint = false;
23229
25453
  if (bo.starPoint && drawn) {
23230
25454
  const t = bo.radiusScene / dist * projScale / STAR_POINT_HANDOFF_R;
@@ -23257,12 +25481,10 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
23257
25481
  }
23258
25482
  }
23259
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);
23260
25485
  const nomScreenR = bo.radiusScene > 0 ? bo.radiusScene / dist * projScale : 0;
23261
25486
  updateNomenclatureVisibility(bo, showNomenclature && body.data.id === nomFocusedBodyId, nomScreenR, camera, screenW, screenH);
23262
25487
  }
23263
- for (const [key, pts] of asteroidPoints) pts.visible = ctx.visibility.isAsteroidGroupVisible(parentIdFromSubkey(key));
23264
- for (const [key, pts] of spacecraftPoints) pts.visible = ctx.visibility.isSpacecraftGroupVisible(parentIdFromSubkey(key));
23265
- for (const [parentId, pts] of moonPoints) pts.visible = ctx.visibility.isMoonGroupVisible(parentId);
23266
25488
  if (screenOccluders.length > 0) {
23267
25489
  const focusedPrimaryId = focusedBodyId ? barycenterPrimaryId(focusedBodyId) : null;
23268
25490
  for (const bo of bodyObjects.values()) {
@@ -23288,7 +25510,8 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
23288
25510
  mat.uniforms.uCenterOffset.value.set(-camera.position.x, -camera.position.y, -camera.position.z);
23289
25511
  const isFocused = bo.body.data.id === focusedBodyId;
23290
25512
  const isHovered = hoveredBodyIds.has(bo.body.data.id);
23291
- 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;
23292
25515
  const isClosed = line.userData.isOpenCurve === false;
23293
25516
  mat.uniforms.uAlphaMin.value = isFocused && isClosed ? .15 : 0;
23294
25517
  mat.uniforms.uShowFull.value = isFocused ? 1 : 0;
@@ -24620,6 +26843,10 @@ function renderLandedProbe(d, probe, landed, jd, where, ctx, bodyObjects) {
24620
26843
 
24621
26844
  //#endregion
24622
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
+ }
24623
26850
  var newestPosScratch = [
24624
26851
  0,
24625
26852
  0,
@@ -24704,7 +26931,7 @@ var Placer = class {
24704
26931
  return null;
24705
26932
  }
24706
26933
  this.settling.add(id);
24707
- 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);
24708
26935
  this.settling.delete(id);
24709
26936
  if (jd === this.passJd) this.settled.add(id);
24710
26937
  return pos;
@@ -25433,6 +27660,13 @@ function updateSunShadowLight(bodyObjects, focusTruePos, ctx, shadowLight, sunPo
25433
27660
  shadowCam.updateProjectionMatrix();
25434
27661
  }
25435
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
+
25436
27670
  //#endregion
25437
27671
  //#region src/lib/scene/lod/terrain-grid.ts
25438
27672
  var COARSE_SEGS = 128;
@@ -25650,7 +27884,14 @@ function heightTilesOf(bo) {
25650
27884
  return reliefTilesAvailable(tiles) && !tiledStates.get(bo)?.dead ? tiles : void 0;
25651
27885
  }
25652
27886
  function gridLevel(tiles, theta, phi, radius) {
25653
- 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);
25654
27895
  if (deepest === tiles.max_level) return deepest;
25655
27896
  const floatStep = 2 ** (Math.floor(Math.log2(radius)) - 23);
25656
27897
  const cells = 2 * Math.PI * radius / (CELL_FLOAT_STEPS * floatStep);
@@ -25718,7 +27959,8 @@ function updateTiledWindow(bo, mesh, tiles, altitude, centerTheta, centerPhi, in
25718
27959
  root: tileRoot(tiles),
25719
27960
  version: tiles.version,
25720
27961
  maxLevel: tiles.max_level,
25721
- patches: (tiles.patches ?? []).map(({ max_level, x, y, version }) => ({
27962
+ patches: (tiles.patches ?? []).map(({ cap, max_level, x, y, version }) => ({
27963
+ cap,
25722
27964
  max_level,
25723
27965
  x,
25724
27966
  y,
@@ -26289,6 +28531,7 @@ var PromotionRegistry = class {
26289
28531
  autoPromoted = /* @__PURE__ */ new Set();
26290
28532
  pinned = /* @__PURE__ */ new Set();
26291
28533
  pinPromoted = /* @__PURE__ */ new Set();
28534
+ focusPromoted = /* @__PURE__ */ new Set();
26292
28535
  groupTargets = null;
26293
28536
  groupMode = "none";
26294
28537
  smallBodyZone = null;
@@ -26373,10 +28616,15 @@ var PromotionRegistry = class {
26373
28616
  if (!this.buildBodyInstance(body)) return;
26374
28617
  this.finalizeBuilds();
26375
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
+ }
26376
28624
  buildBodyInstance(body, halfPromoteIds, auto = false) {
26377
28625
  const { bodyObjects, ctx, clock, scene, clickables, meshToBody, renderer } = this.deps;
26378
28626
  if (bodyObjects.has(body.data.id)) return false;
26379
- 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) {
26380
28628
  body.orbitElements = body.data;
26381
28629
  body.orbitCenterId = body.data.parentId;
26382
28630
  }
@@ -26390,7 +28638,8 @@ var PromotionRegistry = class {
26390
28638
  if (zone) ctx.bodies.dirtyAsteroidZones.add(zone);
26391
28639
  }
26392
28640
  const id = body.data.id;
26393
- 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);
26394
28643
  else {
26395
28644
  this.userPromoted.add(id);
26396
28645
  this.emitUserPromotedCount();
@@ -26583,6 +28832,7 @@ var PromotionRegistry = class {
26583
28832
  var FocusController = class {
26584
28833
  promotion;
26585
28834
  readerFocusFilter = null;
28835
+ focusSets = 0;
26586
28836
  focusedBody;
26587
28837
  _tmpV3 = new Vector3();
26588
28838
  pendingInitialView = null;
@@ -26610,6 +28860,9 @@ var FocusController = class {
26610
28860
  get current() {
26611
28861
  return this.focusedBody;
26612
28862
  }
28863
+ get focusChanges() {
28864
+ return this.focusSets;
28865
+ }
26613
28866
  place(body) {
26614
28867
  return this.deps.ctx.place(body, this.deps.clock.jd);
26615
28868
  }
@@ -26702,6 +28955,7 @@ var FocusController = class {
26702
28955
  ];
26703
28956
  }
26704
28957
  this.focusedBody = void 0;
28958
+ this.promotion.releaseFocusPromoted(void 0);
26705
28959
  prepareFocusTarget(focus, [...position], camera, camWorld, camPos, sceneSettings().resolvedReducedMotion);
26706
28960
  const spherical = cartesianToSpherical(camPos ?? camWorld, position, void 0);
26707
28961
  callbacks.onCameraPosition?.(spherical.latitude, spherical.longitude, spherical.distance);
@@ -26720,6 +28974,8 @@ var FocusController = class {
26720
28974
  }
26721
28975
  setFocusTarget(body, camPos) {
26722
28976
  const { ctx, bodyObjects, controls, callbacks, focus, camera, systemData, loadTexture } = this.deps;
28977
+ this.focusSets++;
28978
+ const drawsItself = !isSurfaceFeature(body) && !isExtrasolar(body);
26723
28979
  if (!isSurfaceFeature(body)) this.promotion.ensureBodyObjects(body);
26724
28980
  const prev = this.focusedBody;
26725
28981
  const prevNomBodyId = nomenclatureBodyId(prev, bodyObjects);
@@ -26737,12 +28993,13 @@ var FocusController = class {
26737
28993
  }
26738
28994
  if (body.data.id !== prev?.data.id) this.pendingInitialView = null;
26739
28995
  this.focusedBody = body;
28996
+ this.promotion.releaseFocusPromoted(body.data.id);
26740
28997
  if (!isSurfaceFeature(body)) ctx.bodies.focusFeature = null;
26741
28998
  controls.minDistance = minCameraDistance(body, modelMinRadiusKm(bodyObjects.get(body.data.id)));
26742
28999
  const featureHost = isSurfaceFeature(body) ? ctx.getBody(body.featureAnchor.hostId) : void 0;
26743
29000
  ctx.visibility.setFocused(featureHost ?? body);
26744
29001
  callbacks.onFocusChange(body);
26745
- if (!isSurfaceFeature(body)) loadTexture(body);
29002
+ if (drawsItself) loadTexture(body);
26746
29003
  if (newNomBodyId && newNomBodyId !== body.data.id) {
26747
29004
  const landingBody = ctx.getBody(newNomBodyId);
26748
29005
  if (landingBody) loadTexture(landingBody);
@@ -26814,6 +29071,30 @@ var FocusController = class {
26814
29071
  callbacks.onCameraPosition?.(settled.latitude, settled.longitude, settled.distance);
26815
29072
  this.pendingInitialView = null;
26816
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
+ }
26817
29098
  settleOnBodyInstant(body, pos) {
26818
29099
  const { focus, pointClouds } = this.deps;
26819
29100
  this.setFocusTarget(body);
@@ -26834,7 +29115,7 @@ var FocusController = class {
26834
29115
  const body = this.focusedBody;
26835
29116
  if (!body) return;
26836
29117
  const { camera, controls, callbacks } = this.deps;
26837
- const bodyFixed = !isModelBearing(body);
29118
+ const bodyFixed = !isModelBearing(body) && !isExtrasolar(body);
26838
29119
  const quat = bodyFixed ? this.focusedBodyQuat(body) ?? [
26839
29120
  0,
26840
29121
  0,
@@ -27233,6 +29514,7 @@ var TIGHT_FAR_RELEASE = 2.3;
27233
29514
  var SUN_PROXY_FAR_FRACTION = .9;
27234
29515
  var _sunLiftDir = new Vector3();
27235
29516
  var TRAIL_DRIFT_PX = .25;
29517
+ var SKY_GONE_SHARE = .9999;
27236
29518
  var MODEL_SHADOW_DIST = 10;
27237
29519
  var MODEL_SHADOW_EXTENT = 4;
27238
29520
  var MODEL_SHADOW_NEAR = 4;
@@ -27263,6 +29545,7 @@ var SceneRenderer = class SceneRenderer {
27263
29545
  bodyObjects = /* @__PURE__ */ new Map();
27264
29546
  circleTexture = makeCircleTexture();
27265
29547
  pointClouds;
29548
+ starfield;
27266
29549
  systemData;
27267
29550
  positionDiagnostics = new PositionDiagnostics();
27268
29551
  placer;
@@ -27283,6 +29566,8 @@ var SceneRenderer = class SceneRenderer {
27283
29566
  limitsArmed = false;
27284
29567
  bodySelectEnabled = true;
27285
29568
  featureSelectEnabled = true;
29569
+ starSelectEnabled = false;
29570
+ pickSerial = 0;
27286
29571
  modelScene;
27287
29572
  modelCamera;
27288
29573
  modelLight;
@@ -27384,6 +29669,8 @@ var SceneRenderer = class SceneRenderer {
27384
29669
  fpsSamples = [];
27385
29670
  fpsSampleHead = 0;
27386
29671
  stashedSky = null;
29672
+ systemOn = true;
29673
+ systemFade = new SwitchFade();
27387
29674
  namesShown = true;
27388
29675
  halosShown = true;
27389
29676
  textureLoader = new TextureLoader();
@@ -27472,11 +29759,12 @@ var SceneRenderer = class SceneRenderer {
27472
29759
  this.pointClouds = new PointCloudSystem(ctx, this.scene, this.bodyObjects, this.circleTexture, this.focus, SceneRenderer.MAP_LAYER, () => this.rebuildTrailBasis());
27473
29760
  this.pointClouds.seedBasis(focusPos);
27474
29761
  this.pointClouds.setPointBudget(this.tier.pointBudget);
29762
+ this.starfield = new StarfieldSystem(ctx, this.scene, starBudget(), this.invalidate, this.pointClouds.pickRegistry);
27475
29763
  this.controls = new OrbitControls(this.camera, canvas);
27476
29764
  this.controls.enableDamping = true;
27477
29765
  this.orbitDampingFactor = orbitDamping(sceneSettings().resolvedReducedMotion);
27478
29766
  this.controls.minDistance = focusBody ? minCameraDistance(focusBody) : kmToScene(.01);
27479
- this.controls.maxDistance = 31620.5 * 10;
29767
+ Object.defineProperty(this.controls, "maxDistance", { get: () => this.maxCameraDistance() });
27480
29768
  this.controls.target.set(0, 0, 0);
27481
29769
  this.controls.update();
27482
29770
  this.controls.addEventListener("end", this.onControlsEnd);
@@ -27533,7 +29821,7 @@ var SceneRenderer = class SceneRenderer {
27533
29821
  this.systemData.syncToFocus();
27534
29822
  this.focusController.readerFocusFilter = this.readerMayFocus;
27535
29823
  this.gpuPick = new GpuPickPass(this.renderer, this.camera);
27536
- 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));
27537
29825
  this.pointerInteraction.attach();
27538
29826
  this.tick();
27539
29827
  }
@@ -27817,7 +30105,9 @@ var SceneRenderer = class SceneRenderer {
27817
30105
  if (this.systemData.hasPendingUnloads() && performance.now() - this.focus.focusStartTime >= this.focus.focusDurationMs) this.systemData.drainPendingUnloads();
27818
30106
  const { distance } = this.getCameraState();
27819
30107
  this.updateTravelCraftSystem();
30108
+ this.updateSystemShare(renderedDtMs);
27820
30109
  this.ctx.visibility.updateCamera(distance, this.sceneJd);
30110
+ this.ctx.visibility.stepZones(renderedDtMs);
27821
30111
  this.updateTightFar(distance);
27822
30112
  this.systemData.syncToFocus();
27823
30113
  this.refreshTravelPath();
@@ -27829,7 +30119,9 @@ var SceneRenderer = class SceneRenderer {
27829
30119
  viewportPx: this.viewportH,
27830
30120
  ctx: this.ctx
27831
30121
  });
27832
- 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);
27833
30125
  this.pointClouds.applyPointBudget();
27834
30126
  refreshDeferredTrails(this.bodyObjects, this.focus, this.lastUpdatedJd, this.trailView());
27835
30127
  updateRingShaders(this.bodyObjects, this.focus.focusTruePos, sceneSettings().realisticLighting, sceneSettings().overexposeRings, this.camera.position, this.pxPerRad());
@@ -27839,7 +30131,6 @@ var SceneRenderer = class SceneRenderer {
27839
30131
  occluders.length = 0;
27840
30132
  const atmoState = updateAtmosphereShaders(this.bodyObjects, this.camera.position, sceneSettings().showAtmospheres, sceneSettings().realisticLighting, this.sunIntensityScale, currentAtmosphereConfig(), this.sceneJd);
27841
30133
  recordAtmospherePerf(renderedDtMs, atmoState.shellProminent);
27842
- this.scene.backgroundIntensity = atmoState.skyboxIntensity;
27843
30134
  this.applyLayerChrome();
27844
30135
  const sunBo = this.bodyObjects.get(SUN_ID);
27845
30136
  if (sunBo) applyStarTint(sunBo.corona, sunBo.starPoint, atmoState.sunTint);
@@ -27861,6 +30152,11 @@ var SceneRenderer = class SceneRenderer {
27861
30152
  this.pointClouds.drainOnePendingSceneAdd();
27862
30153
  this.updateDepthFar();
27863
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);
27864
30160
  if (atmoState.insideShell) this.atmoDepthPass.run(this.renderer, this.scene, this.camera, this.bodyObjects, SceneRenderer.ATMO_DEPTH_LAYER);
27865
30161
  this.syncSceneSamples(atmoState.insideShell);
27866
30162
  this.composer.readBuffer.resolveDepthBuffer = this.overlayActive;
@@ -27886,6 +30182,8 @@ var SceneRenderer = class SceneRenderer {
27886
30182
  if (nowMs - this.focus.focusStartTime < this.focus.focusDurationMs) return true;
27887
30183
  if (this.jumpJd !== null || this.seekAwaitsData) return true;
27888
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;
27889
30187
  for (const bo of this.bodyObjects.values()) if (bo.group.visible && bodyLoading(bo)) return true;
27890
30188
  return false;
27891
30189
  }
@@ -27896,6 +30194,26 @@ var SceneRenderer = class SceneRenderer {
27896
30194
  target.samples = samples;
27897
30195
  target.dispose();
27898
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
+ }
27899
30217
  updateTightFar(distance) {
27900
30218
  if (isReversedDepth()) return;
27901
30219
  const focused = this.focusController.current;
@@ -27915,10 +30233,12 @@ var SceneRenderer = class SceneRenderer {
27915
30233
  }
27916
30234
  updateDepthFar() {
27917
30235
  if (isReversedDepth()) return;
27918
- 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);
27919
30240
  const sysId = this.ctx.visibility.activeSystemId;
27920
30241
  if (sysId) {
27921
- const [fx, fy, fz] = this.focus.focusTruePos;
27922
30242
  let maxDistSq = this.camera.position.lengthSq();
27923
30243
  const consider = (pos) => {
27924
30244
  const dx = pos[0] - fx;
@@ -27939,7 +30259,7 @@ var SceneRenderer = class SceneRenderer {
27939
30259
  const p = bo.body.position;
27940
30260
  if (p && this.ctx.visibility.hasFullRendering(bo.body)) consider(p);
27941
30261
  }
27942
- 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));
27943
30263
  }
27944
30264
  if (Math.abs(far - this.camera.far) > this.camera.far * FAR_UPDATE_EPS) {
27945
30265
  this.camera.far = far;
@@ -28197,7 +30517,30 @@ var SceneRenderer = class SceneRenderer {
28197
30517
  setFeatureSelectEnabled(enabled) {
28198
30518
  this.featureSelectEnabled = enabled;
28199
30519
  }
30520
+ setStarSelectEnabled(enabled) {
30521
+ this.starSelectEnabled = enabled;
30522
+ }
28200
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
+ }
28201
30544
  setCooperativeTouch(cooperative) {
28202
30545
  this.controls.touches.ONE = cooperative ? NO_TOUCH_GESTURE : TOUCH.ROTATE;
28203
30546
  this.canvas.style.touchAction = cooperative ? "pan-x pan-y" : "none";
@@ -28286,7 +30629,7 @@ var SceneRenderer = class SceneRenderer {
28286
30629
  }
28287
30630
  shouldRender(nowMs) {
28288
30631
  if (this.holdingCamera) return true;
28289
- 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;
28290
30633
  if (this.covered) return false;
28291
30634
  if (this.frameIndex - this.lastRenderedFrame >= SceneRenderer.IDLE_HEARTBEAT_FRAMES) return true;
28292
30635
  if (this.cameraMovedFrom(this.lastRenderedCamPos, this.lastRenderedCamQuat) || this.frameIndex < this.wakeUntilFrame || this.haloDebug.active) return true;
@@ -28608,6 +30951,10 @@ var SceneRenderer = class SceneRenderer {
28608
30951
  this.disarmLimits();
28609
30952
  this.focusController.snapToBody(id, latitude, longitude, zoom);
28610
30953
  }
30954
+ snapToBodyFrom(id, fromId, distance) {
30955
+ this.disarmLimits();
30956
+ this.focusController.snapToBodyFrom(id, fromId, distance);
30957
+ }
28611
30958
  setFocusTarget(body, camPos) {
28612
30959
  this.disarmLimits();
28613
30960
  this.applyJdUpdate();
@@ -28737,16 +31084,21 @@ var SceneRenderer = class SceneRenderer {
28737
31084
  setSkyDebugMarkersVisible(visible) {
28738
31085
  this.skyDebugMarkers.setVisible(visible);
28739
31086
  }
28740
- applyLayerChrome() {
28741
- 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) {
28742
31090
  if (this.stashedSky) {
28743
31091
  this.scene.background = this.stashedSky;
28744
31092
  this.stashedSky = null;
28745
31093
  }
28746
31094
  } else if (this.scene.background) {
31095
+ if (this.stashedSky !== this.scene.background) disposeTexture(this.stashedSky);
28747
31096
  this.stashedSky = this.scene.background;
28748
31097
  this.scene.background = null;
28749
31098
  }
31099
+ this.scene.backgroundIntensity = skyboxIntensity * (1 - share);
31100
+ }
31101
+ applyLayerChrome() {
28750
31102
  const names = this.ctx.layers.isVisible("labels");
28751
31103
  if (names !== this.namesShown) {
28752
31104
  this.namesShown = names;
@@ -28813,6 +31165,7 @@ var SceneRenderer = class SceneRenderer {
28813
31165
  this.resize(this.canvas.clientWidth, this.canvas.clientHeight);
28814
31166
  }
28815
31167
  this.pointClouds.setPointBudget(this.tier.pointBudget);
31168
+ this.starfield.setBudget(starBudget());
28816
31169
  }
28817
31170
  resize(width, height) {
28818
31171
  this.invalidate();
@@ -28850,9 +31203,11 @@ var SceneRenderer = class SceneRenderer {
28850
31203
  disposeReliefTiles(bo);
28851
31204
  }
28852
31205
  this.pointClouds.dispose();
31206
+ this.starfield.dispose();
28853
31207
  this.disposeScene(this.scene);
28854
31208
  this.disposeScene(this.modelScene);
28855
31209
  disposeTexture(this.scene.background);
31210
+ disposeTexture(this.stashedSky);
28856
31211
  disposeTexture(this.scene.environment);
28857
31212
  disposeTexture(this.modelScene.environment);
28858
31213
  this.circleTexture.dispose();
@@ -31196,7 +33551,8 @@ var TYPES = {
31196
33551
  [ObjectType.SPACECRAFT]: "spacecraft",
31197
33552
  [ObjectType.DEBRIS]: "debris",
31198
33553
  [ObjectType.UNDOCUMENTED]: "unknown",
31199
- [ObjectType.SURFACE_FEATURE]: "surface-feature"
33554
+ [ObjectType.SURFACE_FEATURE]: "surface-feature",
33555
+ [ObjectType.EXTRASOLAR]: "star"
31200
33556
  };
31201
33557
  var ORBIT_CLASSES = {
31202
33558
  [ObjectType.ASTEROID_INNER]: "inner",
@@ -31325,9 +33681,11 @@ var SpaceMap = class {
31325
33681
  canvas = null;
31326
33682
  labelLayer = null;
31327
33683
  stopWatching = null;
33684
+ stopExtrasolarStale = null;
31328
33685
  controls = null;
31329
33686
  lostPanelTimer;
31330
33687
  cooperative = null;
33688
+ starSelectEnabled = false;
31331
33689
  limits = {};
31332
33690
  initialFocusPending = true;
31333
33691
  pendingFocusId = null;
@@ -31469,6 +33827,7 @@ var SpaceMap = class {
31469
33827
  container.classList.add("sm-map");
31470
33828
  mapsIn.set(container, (mapsIn.get(container) ?? 0) + 1);
31471
33829
  this.controls = new ControlHost(this, container);
33830
+ this.stopExtrasolarStale = onExtrasolarStale(() => this.ctx.onDataStale?.());
31472
33831
  const canvas = document.createElement("canvas");
31473
33832
  canvas.className = "sm-map__canvas";
31474
33833
  canvas.tabIndex = 0;
@@ -31532,6 +33891,8 @@ var SpaceMap = class {
31532
33891
  this.controls = null;
31533
33892
  this.stopWatching?.();
31534
33893
  this.stopWatching = null;
33894
+ this.stopExtrasolarStale?.();
33895
+ this.stopExtrasolarStale = null;
31535
33896
  clearTimeout(this.lostPanelTimer);
31536
33897
  canvas.removeEventListener("keydown", this.onKeyDown);
31537
33898
  canvas.removeEventListener("touchmove", this.onTouchMove);
@@ -31940,8 +34301,13 @@ var SpaceMap = class {
31940
34301
  renderer.setZoomEnabled(this.scrollZoom.isEnabled());
31941
34302
  renderer.setBodySelectEnabled(this.bodySelect.isEnabled());
31942
34303
  renderer.setFeatureSelectEnabled(this.featureSelect.isEnabled());
34304
+ renderer.setStarSelectEnabled(this.starSelectEnabled);
31943
34305
  renderer.setCooperativeTouch(cooperative);
31944
34306
  }
34307
+ setStarSelectEnabled(enabled) {
34308
+ this.starSelectEnabled = enabled;
34309
+ this.renderer?.setStarSelectEnabled(enabled);
34310
+ }
31945
34311
  onWheelCapture = (e) => {
31946
34312
  if (!this.scrollZoom.isEnabled()) return;
31947
34313
  if (this.cooperative && !this.cooperative.allowsWheel(e)) e.stopPropagation();
@@ -31982,6 +34348,9 @@ var SpaceMap = class {
31982
34348
  snapToBody(id, latitude, longitude, zoom) {
31983
34349
  this.renderer?.snapToBody(id, latitude, longitude, zoom);
31984
34350
  }
34351
+ snapToBodyFrom(id, fromId, distance) {
34352
+ this.renderer?.snapToBodyFrom(id, fromId, distance);
34353
+ }
31985
34354
  snapToBodyFacing(id, towardId, elevationDeg, distance) {
31986
34355
  this.renderer?.snapToBodyFacing(id, towardId, elevationDeg, distance);
31987
34356
  }
@@ -32329,7 +34698,7 @@ var MarkerDrawing = class {
32329
34698
  const screen = this.visible ? viewport.project(this.at.lon, this.at.lat) : null;
32330
34699
  this.element.style.display = screen ? "" : "none";
32331
34700
  if (!screen) return;
32332
- const shift = viewport.repeatShifts.find((s) => screen[0] + s >= 0 && screen[0] + s <= viewport.width) ?? 0;
34701
+ const shift = viewport.shiftInFrame(screen[0]);
32333
34702
  this.element.style.transform = `translate(${screen[0] + shift}px, ${screen[1]}px) translate(${-this.align[0] * 100}%, ${-this.align[1] * 100}%)`;
32334
34703
  }
32335
34704
  setPosition(at) {
@@ -32433,14 +34802,15 @@ function stepOfLevel(level, tileSize) {
32433
34802
  return 2 * Math.PI / (2 * tileSize * 2 ** level);
32434
34803
  }
32435
34804
  function finestLevelAt(pyramid, lon, lat) {
32436
- const [across, fy] = chartFraction(0, lon, lat);
32437
- const fx = across - Math.floor(across);
32438
34805
  let finest = pyramid.max_level;
32439
34806
  for (const patch of pyramid.patches ?? []) {
32440
34807
  if (patch.max_level <= finest) continue;
32441
- const x = Math.floor(fx * chartColumns(0, patch.max_level));
32442
- const y = Math.floor(fy * chartRows(0, patch.max_level));
32443
- 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;
32444
34814
  }
32445
34815
  return finest;
32446
34816
  }
@@ -32523,12 +34893,13 @@ function fitted(chart, level, tileSize, extent, margin) {
32523
34893
  }
32524
34894
  }
32525
34895
  function reaches(patch, extent) {
32526
- const columns = chartColumns(0, patch.max_level);
32527
- 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);
32528
34899
  const south = (patch.y[1] + 1) / rows;
32529
34900
  if (patch.y[0] / rows > extent.maxY || south < extent.minY) return false;
32530
34901
  let west = patch.x[0] / columns;
32531
- west -= Math.round(west - (extent.minX + extent.maxX) / 2);
34902
+ if (chart === 0) west -= Math.round(west - (extent.minX + extent.maxX) / 2);
32532
34903
  const east = west + (patch.x[1] + 1 - patch.x[0]) / columns;
32533
34904
  return west <= extent.maxX && east >= extent.minX;
32534
34905
  }
@@ -32542,18 +34913,32 @@ function planWindows(pyramid, frame, firstLevel, moves = true) {
32542
34913
  if (place) places.push(place);
32543
34914
  }
32544
34915
  const onMap = extentOn(0, frame, places, centre);
34916
+ const cap = centre[1] >= 0 ? 1 : 2;
34917
+ const onCap = extentOn(cap, frame, places, centre);
32545
34918
  const wanted = levelForStep(frame.step, pyramid.tile_size);
32546
34919
  const specs = [];
32547
34920
  let finest = pyramid.max_level;
32548
- 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
+ }
32549
34936
  for (let level = Math.min(wanted, finest); level > pyramid.max_level; level--) {
32550
- const spec = fitted(0, level, pyramid.tile_size, onMap, margin);
34937
+ const spec = fitted(finestOn.chart, level, pyramid.tile_size, finestOn.extent, margin);
32551
34938
  if (!spec) continue;
32552
34939
  specs.push(spec);
32553
34940
  break;
32554
34941
  }
32555
- const cap = centre[1] >= 0 ? 1 : 2;
32556
- const onCap = extentOn(cap, frame, places, centre);
32557
34942
  for (let level = Math.min(wanted, pyramid.max_level); level >= firstLevel; level--) {
32558
34943
  const map = fitted(0, level, pyramid.tile_size, onMap, margin);
32559
34944
  const polar = onCap && level >= 1 ? fitted(cap, level, pyramid.tile_size, onCap, margin) : void 0;
@@ -32575,7 +34960,7 @@ function windowTiles(spec, pyramid) {
32575
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++) {
32576
34961
  if (spec.chart !== 0 && (tx < 0 || tx >= columns)) continue;
32577
34962
  const x = (tx % columns + columns) % columns;
32578
- 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;
32579
34964
  if (spec.level > pyramid.max_level && !patch) continue;
32580
34965
  tiles.push({
32581
34966
  cap: CHART_FOLDER[spec.chart],
@@ -33245,7 +35630,8 @@ var LayerTiles = class {
33245
35630
  win.ctx = canvas.getContext("2d") ?? void 0;
33246
35631
  } else win.data = new Uint8ClampedArray(spec.width * spec.height * 4);
33247
35632
  for (const tile of windowTiles(spec, this.block)) {
33248
- 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);
33249
35635
  const url = tileUrl({
33250
35636
  id: this.block.id,
33251
35637
  version
@@ -33317,7 +35703,10 @@ var Viewport = class {
33317
35703
  }
33318
35704
  unproject(px, py) {
33319
35705
  const [x, y] = this.fromScreen(px, py);
33320
- 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);
33321
35710
  }
33322
35711
  get worldWidthPx() {
33323
35712
  const { minX, maxX } = this.projection.extent;
@@ -33336,6 +35725,9 @@ var Viewport = class {
33336
35725
  w
33337
35726
  ].filter((shift) => left + shift < this.width && left + shift + w > 0);
33338
35727
  }
35728
+ shiftInFrame(x) {
35729
+ return this.repeatShifts.find((shift) => x + shift >= 0 && x + shift <= this.width) ?? 0;
35730
+ }
33339
35731
  };
33340
35732
  function repeats(projection, worldWidthPx, width) {
33341
35733
  return projection.cyclic && projection.rectangular && worldWidthPx >= width - .5;
@@ -33772,7 +36164,9 @@ var FlatMap = class {
33772
36164
  await this.adopt(bodyId);
33773
36165
  }
33774
36166
  project(lon, lat) {
33775
- 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;
33776
36170
  }
33777
36171
  unproject(px, py) {
33778
36172
  const found = this.viewport().unproject(px, py);
@@ -36678,9 +39072,11 @@ function attributionChips(ctx, labels = ORBIT_ARCHIVE_ORGANISATIONS) {
36678
39072
  const bodyId = ctx.visibility.focusedBodyId;
36679
39073
  const model = bodyId ? credits.model.get(bodyId) : void 0;
36680
39074
  if (model) organisations.add(model.organisation);
39075
+ const stars = [...new Set(credits.stars.filter((c) => c.license).map((c) => c.organisation))];
36681
39076
  return {
36682
39077
  orbits,
36683
- imagery: [...organisations].sort()
39078
+ imagery: [...organisations].sort(),
39079
+ stars
36684
39080
  };
36685
39081
  }
36686
39082
 
@@ -36716,12 +39112,14 @@ var AttributionControl = class {
36716
39112
  const root = bar();
36717
39113
  const orbits = group();
36718
39114
  const imagery = group();
36719
- 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()));
36720
39117
  this.stop = effect_root(() => {
36721
39118
  user_effect(() => {
36722
39119
  const chips = attributionChips(map.ctx);
36723
39120
  fill(orbits, host().messages.attribution_orbits(), chips.orbits);
36724
39121
  fill(imagery, host().messages.attribution_imagery(), chips.imagery);
39122
+ fill(stars, host().messages.attribution_stars(), chips.stars);
36725
39123
  });
36726
39124
  });
36727
39125
  return root;
@@ -37086,13 +39484,14 @@ async function fetchPanoramas(bodyId) {
37086
39484
  //#endregion
37087
39485
  //#region src/sdk/index.ts
37088
39486
  async function createMap(options) {
37089
- 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;
37090
39488
  const element = resolveContainer(container);
37091
39489
  applyHost({
37092
39490
  dataUrl,
37093
39491
  imagesUrl,
37094
39492
  liveUrl,
37095
39493
  tilesUrl,
39494
+ extrasolarUrl,
37096
39495
  locale,
37097
39496
  messages,
37098
39497
  includeNonCommercial
@@ -37113,13 +39512,14 @@ async function createMap(options) {
37113
39512
  return map;
37114
39513
  }
37115
39514
  async function createFlatMap(options) {
37116
- 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;
37117
39516
  const element = resolveContainer(container);
37118
39517
  applyHost({
37119
39518
  dataUrl,
37120
39519
  imagesUrl,
37121
39520
  liveUrl,
37122
39521
  tilesUrl,
39522
+ extrasolarUrl,
37123
39523
  locale,
37124
39524
  messages,
37125
39525
  includeNonCommercial
@@ -37134,13 +39534,14 @@ async function createFlatMap(options) {
37134
39534
  return map;
37135
39535
  }
37136
39536
  async function createPanorama(options) {
37137
- 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;
37138
39538
  const element = resolveContainer(container);
37139
39539
  applyHost({
37140
39540
  dataUrl,
37141
39541
  imagesUrl,
37142
39542
  liveUrl,
37143
39543
  tilesUrl,
39544
+ extrasolarUrl,
37144
39545
  locale,
37145
39546
  messages,
37146
39547
  includeNonCommercial
@@ -37155,13 +39556,14 @@ async function createPanorama(options) {
37155
39556
  return view;
37156
39557
  }
37157
39558
  async function createSystemMap(options) {
37158
- 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;
37159
39560
  const element = resolveContainer(container);
37160
39561
  applyHost({
37161
39562
  dataUrl,
37162
39563
  imagesUrl,
37163
39564
  liveUrl,
37164
39565
  tilesUrl,
39566
+ extrasolarUrl,
37165
39567
  locale,
37166
39568
  messages,
37167
39569
  includeNonCommercial
@@ -37189,6 +39591,7 @@ function applyHost(options) {
37189
39591
  if (options.imagesUrl !== void 0) overrides.imagesUrl = options.imagesUrl;
37190
39592
  if (options.liveUrl !== void 0) overrides.liveUrl = options.liveUrl;
37191
39593
  if (options.tilesUrl !== void 0) overrides.tilesUrl = options.tilesUrl;
39594
+ if (options.extrasolarUrl !== void 0) overrides.extrasolarUrl = options.extrasolarUrl;
37192
39595
  if (options.locale !== void 0) overrides.locale = () => options.locale;
37193
39596
  if (options.messages !== void 0) overrides.messages = options.messages;
37194
39597
  if (options.includeNonCommercial) overrides.textures = "non-commercial";