spacemap 0.1.2 → 0.1.4

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
@@ -24,6 +24,7 @@ var DEFAULT_HOST = {
24
24
  messages: {
25
25
  body_note_no_model: () => "no model available",
26
26
  body_note_no_radius: () => "no size data available",
27
+ body_note_crash_site: () => "crash site, no wreckage model",
27
28
  carried_by_scene_label: ({ carrier }) => `Carried by ${carrier}`,
28
29
  scene_canvas_label: () => "Interactive 3D map of the Solar System",
29
30
  attribution_orbits: () => "Orbits",
@@ -36,7 +37,17 @@ var DEFAULT_HOST = {
36
37
  layer_nomenclature: () => "Named features",
37
38
  cooperative_wheel: () => "Use ctrl + scroll to zoom the map",
38
39
  cooperative_wheel_mac: () => "Use ⌘ + scroll to zoom the map",
39
- cooperative_touch: () => "Use two fingers to move the map"
40
+ cooperative_touch: () => "Use two fingers to move the map",
41
+ planetary_system_primary: () => "primary",
42
+ planetary_system_retrograde: () => "retrograde",
43
+ planetary_system_axis_unit: () => "primary radii · log",
44
+ tab_rings: () => "Rings",
45
+ system_map_solar_system: () => "Solar System",
46
+ system_map_moons: ({ count }) => count === 1 ? "1 moon" : `${count} moons`,
47
+ system_map_zone_inner: () => "Inner asteroids & comets",
48
+ system_map_zone_outer: () => "Outer asteroids & comets",
49
+ unit_symbol_astronomical_unit: () => "AU",
50
+ system_map_axis_au: () => "AU · log"
40
51
  },
41
52
  bodyHref: () => "",
42
53
  textures: "open"
@@ -6212,6 +6223,9 @@ var BodyIndex = class {
6212
6223
  set orientationVersion(value) {
6213
6224
  set(this.#orientationVersion, value, true);
6214
6225
  }
6226
+ orientationRead = /* @__PURE__ */ new Set();
6227
+ hostRead = /* @__PURE__ */ new Map();
6228
+ hostReadVersion = 0;
6215
6229
  childrenByParent = /* @__PURE__ */ new Map();
6216
6230
  moonMaxAByParent = /* @__PURE__ */ new Map();
6217
6231
  addListeners = /* @__PURE__ */ new Set();
@@ -6434,8 +6448,8 @@ var loadProgress = new LoadProgress();
6434
6448
  //#endregion
6435
6449
  //#region src/lib/fetch/fetch-timeout.ts
6436
6450
  var BOOT_FETCH_TIMEOUT_MS = 3e4;
6437
- function countBootBytes(res) {
6438
- if (!loadProgress.active || !res.body) return res;
6451
+ function countBootBytes(res, background) {
6452
+ if (background || !loadProgress.active || !res.body) return res;
6439
6453
  const total = Number(res.headers.get("content-length"));
6440
6454
  if (!Number.isFinite(total) || total <= 0) return res;
6441
6455
  loadProgress.announce(total);
@@ -6458,7 +6472,7 @@ async function fetchWithTimeout(input, init = {}, timeoutMs = BOOT_FETCH_TIMEOUT
6458
6472
  return countBootBytes(await fetch(input, {
6459
6473
  ...init,
6460
6474
  signal: controller.signal
6461
- }));
6475
+ }), init.priority === "low");
6462
6476
  } catch (e) {
6463
6477
  if (controller.signal.aborted && !init.signal?.aborted) throw new Error(`Request timed out after ${timeoutMs} ms: ${input}`, { cause: e });
6464
6478
  throw e;
@@ -7307,6 +7321,7 @@ function parseKeplerPayload(buffer, offset, method, float64) {
7307
7321
  var LANDED_LATLNG_SCALE = 1e-7;
7308
7322
  var LANDED_ALT_MM_SCALE = .001;
7309
7323
  var LANDED_FLAG_STATIC = 1;
7324
+ var LANDED_FLAG_DESTROYED = 2;
7310
7325
  var LANDED_SAMPLE_SIZE = 16;
7311
7326
  var LANDED_HEADER_SIZE = 32;
7312
7327
  function parseLandedPayload(buffer, offset, chunkStartEt) {
@@ -7336,6 +7351,7 @@ function parseLandedPayload(buffer, offset, chunkStartEt) {
7336
7351
  return {
7337
7352
  bodyNaifId,
7338
7353
  isStatic: (flagsByte & LANDED_FLAG_STATIC) !== 0,
7354
+ isDestroyed: (flagsByte & LANDED_FLAG_DESTROYED) !== 0,
7339
7355
  startEt: chunkStartEt + startOffsetS,
7340
7356
  endEt: chunkStartEt + endOffsetS,
7341
7357
  latRefDeg,
@@ -9549,7 +9565,7 @@ function memoizedGzJson(url, opts = {}) {
9549
9565
  //#region src/lib/fetch/bundle-cache.ts
9550
9566
  var MAX_BUNDLES = 24;
9551
9567
  var cache$2 = /* @__PURE__ */ new Map();
9552
- function fetchGzipBundle(url) {
9568
+ function fetchGzipBundle(url, init) {
9553
9569
  let p = cache$2.get(url);
9554
9570
  if (p) {
9555
9571
  cache$2.delete(url);
@@ -9557,7 +9573,7 @@ function fetchGzipBundle(url) {
9557
9573
  }
9558
9574
  if (!p) {
9559
9575
  p = (async () => {
9560
- const res = await fetchWithTimeout(url);
9576
+ const res = await fetchWithTimeout(url, init);
9561
9577
  if (!res.ok) {
9562
9578
  if (res.status === 404) return {};
9563
9579
  throw new Error(`fetchGzipBundle: ${url} returned ${res.status} ${res.statusText}`);
@@ -10308,20 +10324,35 @@ async function loadTierBitmaps(id, tier, priority) {
10308
10324
  }));
10309
10325
  }
10310
10326
  var tierPrefetch = /* @__PURE__ */ new Map();
10327
+ var tierClaims = /* @__PURE__ */ new Map();
10311
10328
  function tierKey(id, tier) {
10312
10329
  return `${id}:${tier}`;
10313
10330
  }
10314
- function tierBitmaps(id, tier, priority) {
10331
+ function tierBitmaps(id, tier, priority, claim = true) {
10315
10332
  const key = tierKey(id, tier);
10316
10333
  let p = tierPrefetch.get(key);
10317
10334
  if (!p) {
10318
- p = loadTierBitmaps(id, tier, priority);
10335
+ const loading = loadTierBitmaps(id, tier, priority);
10336
+ p = loading;
10319
10337
  tierPrefetch.set(key, p);
10338
+ loading.catch(() => {
10339
+ if (tierPrefetch.get(key) !== loading) return;
10340
+ tierPrefetch.delete(key);
10341
+ tierClaims.delete(key);
10342
+ });
10320
10343
  }
10344
+ if (claim) tierClaims.set(key, (tierClaims.get(key) ?? 0) + 1);
10321
10345
  return p;
10322
10346
  }
10323
10347
  function releaseTierBitmaps(id, tier, bitmaps) {
10324
- tierPrefetch.delete(tierKey(id, tier));
10348
+ const key = tierKey(id, tier);
10349
+ const left = (tierClaims.get(key) ?? 1) - 1;
10350
+ if (left > 0) {
10351
+ tierClaims.set(key, left);
10352
+ return;
10353
+ }
10354
+ tierClaims.delete(key);
10355
+ tierPrefetch.delete(key);
10325
10356
  for (const b of bitmaps) b.close();
10326
10357
  }
10327
10358
  function makeCube(bitmaps) {
@@ -10336,10 +10367,10 @@ function pickLowTier(meta) {
10336
10367
  }
10337
10368
  function prefetchSkyboxTiers(meta) {
10338
10369
  const low = pickLowTier(meta);
10339
- if (low) tierBitmaps(meta.id, low).catch(() => tierPrefetch.delete(tierKey(meta.id, low)));
10370
+ if (low) tierBitmaps(meta.id, low, void 0, false).catch(() => {});
10340
10371
  }
10341
10372
  var FULL_TIER_GATE_TIMEOUT_MS = 12e3;
10342
- async function loadFromMeta(scene, renderer, meta) {
10373
+ async function loadFromMeta(scene, renderer, meta, disposed) {
10343
10374
  const tier = pickTier$1(meta, Math.min(renderer.capabilities.maxTextureSize, currentRenderTier().maxSkyboxFace));
10344
10375
  if (!tier) {
10345
10376
  console.warn(`Skybox ${meta.id}: no tiers declared`);
@@ -10352,7 +10383,7 @@ async function loadFromMeta(scene, renderer, meta) {
10352
10383
  };
10353
10384
  let fullInstalled = false;
10354
10385
  if (lowTier && lowTier !== tier) tierBitmaps(meta.id, lowTier).then((bitmaps) => {
10355
- if (fullInstalled) {
10386
+ if (fullInstalled || disposed()) {
10356
10387
  releaseTierBitmaps(meta.id, lowTier, bitmaps);
10357
10388
  return;
10358
10389
  }
@@ -10360,11 +10391,38 @@ async function loadFromMeta(scene, renderer, meta) {
10360
10391
  lowRef.bitmaps = bitmaps;
10361
10392
  scene.background = lowRef.cube;
10362
10393
  performance.mark("sm-skybox-low");
10394
+ if (fullFailed) keepLow();
10363
10395
  }).catch(() => {});
10396
+ let fullFailed = false;
10397
+ const keepLow = () => {
10398
+ if (!lowTier || !lowRef.cube || !lowRef.bitmaps) return;
10399
+ renderer.initTexture(lowRef.cube);
10400
+ releaseTierBitmaps(meta.id, lowTier, lowRef.bitmaps);
10401
+ lowRef.bitmaps = null;
10402
+ };
10364
10403
  if (tier !== lowTier) await Promise.race([eagerMinorsDone, new Promise((resolve) => setTimeout(resolve, FULL_TIER_GATE_TIMEOUT_MS))]);
10365
- const fullBitmaps = await tierBitmaps(meta.id, tier, tier === lowTier ? void 0 : "low");
10366
- const full = makeCube(fullBitmaps);
10404
+ let fullBitmaps;
10405
+ try {
10406
+ fullBitmaps = await tierBitmaps(meta.id, tier, tier === lowTier ? void 0 : "low");
10407
+ } catch (err) {
10408
+ fullFailed = true;
10409
+ if (!disposed()) keepLow();
10410
+ else if (lowTier && lowRef.bitmaps) {
10411
+ lowRef.cube?.dispose();
10412
+ releaseTierBitmaps(meta.id, lowTier, lowRef.bitmaps);
10413
+ lowRef.bitmaps = null;
10414
+ }
10415
+ throw err;
10416
+ }
10367
10417
  await new Promise((resolve) => "requestIdleCallback" in window ? requestIdleCallback(() => resolve(), { timeout: 2e3 }) : setTimeout(resolve, 500));
10418
+ if (disposed()) {
10419
+ fullInstalled = true;
10420
+ lowRef.cube?.dispose();
10421
+ releaseTierBitmaps(meta.id, tier, fullBitmaps);
10422
+ if (lowTier && lowTier !== tier && lowRef.bitmaps) releaseTierBitmaps(meta.id, lowTier, lowRef.bitmaps);
10423
+ return;
10424
+ }
10425
+ const full = makeCube(fullBitmaps);
10368
10426
  renderer.initTexture(full);
10369
10427
  fullInstalled = true;
10370
10428
  const prevBackground = scene.background;
@@ -10375,7 +10433,7 @@ async function loadFromMeta(scene, renderer, meta) {
10375
10433
  releaseTierBitmaps(meta.id, tier, fullBitmaps);
10376
10434
  if (lowTier && lowTier !== tier && lowRef.bitmaps) releaseTierBitmaps(meta.id, lowTier, lowRef.bitmaps);
10377
10435
  }
10378
- async function loadSkybox(scene, renderer, ctx) {
10436
+ async function loadSkybox(scene, renderer, ctx, disposed = () => false) {
10379
10437
  if (ctx?.layers.skipped.has("stars")) return;
10380
10438
  try {
10381
10439
  const meta = await fetchMetadata();
@@ -10387,7 +10445,7 @@ async function loadSkybox(scene, renderer, ctx) {
10387
10445
  attribution: meta.skybox.attribution,
10388
10446
  description: meta.skybox.description
10389
10447
  };
10390
- await loadFromMeta(scene, renderer, meta.skybox);
10448
+ await loadFromMeta(scene, renderer, meta.skybox, disposed);
10391
10449
  } catch (err) {
10392
10450
  console.warn("Failed to load skybox:", err);
10393
10451
  }
@@ -12390,6 +12448,7 @@ function bootThree(canvas, labelContainer, ctx) {
12390
12448
  //#region src/lib/scene/minor-body-position.ts
12391
12449
  function refreshMinorBodyPosition(body, jd, ctx) {
12392
12450
  const d = body.data;
12451
+ body.placedJd = jd;
12393
12452
  const isParabolic = d.q != null;
12394
12453
  let parentPos;
12395
12454
  if (d.parentId === "naif-0") parentPos = [
@@ -12428,6 +12487,47 @@ function refreshMinorBodyPosition(body, jd, ctx) {
12428
12487
  }
12429
12488
  body.positionUnknown = false;
12430
12489
  }
12490
+ function refreshMoonPosition(body, jd, ctx) {
12491
+ const d = body.data;
12492
+ body.placedJd = jd;
12493
+ body.positionUnknown = true;
12494
+ if (d.unplaceable) return;
12495
+ if (d.visibleFromDays !== void 0 && jd - 2451545 < d.visibleFromDays) return;
12496
+ const parent = ctx.getBody(d.parentId);
12497
+ if (!parent || !placeForRead(parent, jd, ctx)) return;
12498
+ const parentPos = parent.position;
12499
+ const offset = ctx.chebStore?.has(d.id) ? ctx.chebStore.positionScene(d.id, jd) : d.a === 0 ? [
12500
+ 0,
12501
+ 0,
12502
+ 0
12503
+ ] : orbitalElementsToPositionJD(d, jd);
12504
+ if (!offset) return;
12505
+ body.position[0] = parentPos[0] + offset[0];
12506
+ body.position[1] = parentPos[1] + offset[1];
12507
+ body.position[2] = parentPos[2] + offset[2];
12508
+ if (body.orbitCenter) {
12509
+ body.orbitCenter[0] = parentPos[0];
12510
+ body.orbitCenter[1] = parentPos[1];
12511
+ body.orbitCenter[2] = parentPos[2];
12512
+ }
12513
+ body.positionUnknown = false;
12514
+ }
12515
+ var HOST_READ_MS = 2e3;
12516
+ function placeForRead(body, jd, ctx) {
12517
+ const d = body.data;
12518
+ const stale = body.placedJd !== jd;
12519
+ if (d.orbitalSource === OrbitalSource.SPICE_PROBE) {
12520
+ const { hostRead } = ctx.bodies;
12521
+ if (!stale && !hostRead.has(d.id)) return !body.positionUnknown;
12522
+ if (!hostRead.has(d.id)) ctx.bodies.hostReadVersion++;
12523
+ hostRead.set(d.id, performance.now());
12524
+ if (stale) return false;
12525
+ } else if (stale) {
12526
+ if (d.objectType === ObjectType.MOON) refreshMoonPosition(body, jd, ctx);
12527
+ else if (!ctx.bodies.bodiesById.has(d.id) && !isSurfaceFeature(body)) refreshMinorBodyPosition(body, jd, ctx);
12528
+ }
12529
+ return !body.positionUnknown;
12530
+ }
12431
12531
 
12432
12532
  //#endregion
12433
12533
  //#region src/lib/fetch/attitude/parse.ts
@@ -13101,6 +13201,7 @@ async function loadSpacecraftModel(bo, modelScene, ctx) {
13101
13201
  fitToUnitRadius(root, anchor && baseFrame ? anchor.applyQuaternion(baseFrame) : anchor);
13102
13202
  enableShadows(root);
13103
13203
  modelScene.add(root);
13204
+ root.visible = !bo.isCrashed;
13104
13205
  bo.model = root;
13105
13206
  bo.modelName = slug;
13106
13207
  setHaloLoading(bo, false);
@@ -14121,11 +14222,16 @@ function refreshVisibleBodyLabelRects(bodyObjects, screenWidth, screenHeight, ca
14121
14222
  //#region src/lib/scene/label/factory.ts
14122
14223
  var SPACECRAFT_ICON_D = "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z";
14123
14224
  var LANDED_PROBE_ICON_D = "M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z";
14124
- function setSpacecraftLanded(halo, landed) {
14225
+ var SPACECRAFT_GLYPH_D = {
14226
+ flying: SPACECRAFT_ICON_D,
14227
+ landed: LANDED_PROBE_ICON_D,
14228
+ crashed: LANDED_PROBE_ICON_D + "M15 9l-6 6M9 9l6 6"
14229
+ };
14230
+ function setSpacecraftGlyph(halo, glyph) {
14125
14231
  if (!halo) return;
14126
14232
  const path = halo.querySelector("path");
14127
14233
  if (!path) return;
14128
- path.setAttribute("d", landed ? LANDED_PROBE_ICON_D : SPACECRAFT_ICON_D);
14234
+ path.setAttribute("d", SPACECRAFT_GLYPH_D[glyph]);
14129
14235
  }
14130
14236
  function addLabelNameSpan(el, name, variant, isLarge) {
14131
14237
  const span = document.createElement("span");
@@ -16334,6 +16440,8 @@ function attachRingBundles(bodyId, systemId, rings, radii, bo, scene, maxTexture
16334
16440
  return;
16335
16441
  }
16336
16442
  node.mesh.userData.ringBundle = ringMeta.bundle;
16443
+ const host = bo.mesh ?? bo.model;
16444
+ if (host) node.mesh.quaternion.copy(host.quaternion);
16337
16445
  bo.rings.push(node);
16338
16446
  scene.add(node.mesh);
16339
16447
  bo.extraObjects.push(node.mesh);
@@ -16755,6 +16863,7 @@ function applyRadiiToMesh(bo, radii) {
16755
16863
  async function loadBodyTexture(bo, textureLoader, currentJd, scene, maxTextureSize, ctx) {
16756
16864
  if (bo.textureTier || bo.textureLoading) return;
16757
16865
  const detail = await fetchObjectDetail(bo.body.data.id, false);
16866
+ ctx?.bodies.orientationRead.add(bo.body.data.id);
16758
16867
  if (!detail.global) return;
16759
16868
  if (detail.global.orientation && !bo.body.orientation) applyBodyOrientation(bo, detail.global.orientation, ctx);
16760
16869
  if (detail.global.radii && bo.radiusScene > 0 && !bo.radiiApplied && !detail.global.displacement?.absolute_radius) applyRadiiToMesh(bo, detail.global.radii);
@@ -17923,6 +18032,7 @@ async function loadSystemData(barycenterId, bodyObjects, scene, textureLoader, c
17923
18032
  if (surface) ctx?.credits.registerImagery("surface", bodyId, barycenterId, surface);
17924
18033
  const bo = bodyObjects.get(bodyId);
17925
18034
  if (!bo?.mesh) continue;
18035
+ ctx?.bodies.orientationRead.add(bodyId);
17926
18036
  if (bodyMeta.orientation) {
17927
18037
  applyBodyOrientation(bo, bodyMeta.orientation, ctx, barycenterId);
17928
18038
  if (bodyMeta.nut_prec) {
@@ -19087,6 +19197,7 @@ var SMALL_BODY_MAX_OPACITY = EMPHASIS_MAX_OPACITY;
19087
19197
  var PointCloudSystem = class {
19088
19198
  orbitPool = new OrbitWorkerPool();
19089
19199
  pickRegistry = new PickRegistry();
19200
+ disposed = false;
19090
19201
  workerPoints = {
19091
19202
  asteroid: /* @__PURE__ */ new Map(),
19092
19203
  spacecraft: /* @__PURE__ */ new Map()
@@ -19272,7 +19383,7 @@ var PointCloudSystem = class {
19272
19383
  return true;
19273
19384
  }
19274
19385
  async rebuildMinorPass() {
19275
- if (this.ctx.bodies.dirtyAsteroidZones.size === 0 && this.ctx.bodies.dirtySpacecraftGroups.size === 0) return;
19386
+ if (this.disposed || this.ctx.bodies.dirtyAsteroidZones.size === 0 && this.ctx.bodies.dirtySpacecraftGroups.size === 0) return;
19276
19387
  const seedBasis = [
19277
19388
  this.basisPos[0],
19278
19389
  this.basisPos[1],
@@ -19292,6 +19403,7 @@ var PointCloudSystem = class {
19292
19403
  groups: [],
19293
19404
  baseWorker: 0
19294
19405
  };
19406
+ if (this.disposed) return;
19295
19407
  const cloudColor = bucket && groups.length > 0 ? bucket.cloudColor() : "#888888";
19296
19408
  for (let i = 0; i < k; i++) {
19297
19409
  const key = `${zone}#${i}`;
@@ -19319,6 +19431,7 @@ var PointCloudSystem = class {
19319
19431
  this.ctx.bodies.dirtySpacecraftGroups.delete(gid);
19320
19432
  drainedGroups.push(gid);
19321
19433
  const { buckets, baseWorker } = await partitionForWorkersSliced(gid, drawnSpacecraft(this.ctx.layers, gid, bucket ? Array.from(bucket.values()) : []), k);
19434
+ if (this.disposed) return;
19322
19435
  const skip = new Set(this.bodyObjects.keys());
19323
19436
  for (let i = 0; i < k; i++) {
19324
19437
  const key = `${gid}#${i}`;
@@ -19640,6 +19753,7 @@ var PointCloudSystem = class {
19640
19753
  return this.moonPoints;
19641
19754
  }
19642
19755
  dispose() {
19756
+ this.disposed = true;
19643
19757
  this.orbitPool.destroy();
19644
19758
  this.pickRegistry.clear();
19645
19759
  for (const kind of WORKER_CLOUD_KINDS) this.disposeClouds(this.workerPoints[kind]);
@@ -19910,9 +20024,15 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
19910
20024
  const cameraInverse = camera.matrixWorldInverse;
19911
20025
  for (const bo of bodyObjects.values()) {
19912
20026
  bo.cachedDist = f64dist(camTrue, bo.body.position);
19913
- if (bo.noPhysical) {
20027
+ if (bo.isCrashed || bo.noPhysical) {
19914
20028
  const isModel = bo.noPhysical === "model";
19915
- setLabelAnnotation(bo, "missing", sceneToKm(bo.cachedDist) < (isModel ? .1 : 1) ? isModel ? host().messages.body_note_no_model() : host().messages.body_note_no_radius() : null);
20029
+ const near = sceneToKm(bo.cachedDist) < (bo.isCrashed || isModel ? .1 : 1);
20030
+ let text = null;
20031
+ if (near) {
20032
+ const m = host().messages;
20033
+ text = bo.isCrashed ? m.body_note_crash_site() : isModel ? m.body_note_no_model() : m.body_note_no_radius();
20034
+ }
20035
+ setLabelAnnotation(bo, "missing", text);
19916
20036
  }
19917
20037
  const label = bo.label;
19918
20038
  if (!label) continue;
@@ -21254,18 +21374,32 @@ function updatePositions(params) {
21254
21374
  for (const body of ctx.bodies.bodiesById.values()) positionMap.set(body.data.id, body.position);
21255
21375
  for (const bo of bodyObjects.values()) if (!ctx.bodies.bodiesById.has(bo.body.data.id)) positionMap.set(bo.body.data.id, bo.body.position);
21256
21376
  const computed = /* @__PURE__ */ new Set();
21377
+ const placeFirst = (id) => {
21378
+ if (computed.has(id)) return;
21379
+ const body = ctx.bodies.bodiesById.get(id) ?? bodyObjects.get(id)?.body;
21380
+ if (body) computePosition(body);
21381
+ };
21382
+ const now = performance.now();
21383
+ const hostReads = (id) => {
21384
+ const at = ctx.bodies.hostRead.get(id);
21385
+ if (at === void 0) return false;
21386
+ if (now - at < 2e3) return true;
21387
+ ctx.bodies.hostRead.delete(id);
21388
+ return false;
21389
+ };
21257
21390
  const computePosition = (body) => {
21258
21391
  if (computed.has(body.data.id)) return;
21259
21392
  computed.add(body.data.id);
21260
21393
  const d = body.data;
21261
21394
  const bo = bodyObjects.get(d.id);
21262
- if (bo && frame !== void 0 && d.id !== focusedId && !bo.group.visible && !bo.trail?.visible && (d.orbitalSource === OrbitalSource.SPICE_PROBE || !isMajorBody(d.objectType))) {
21395
+ if (bo && frame !== void 0 && d.id !== focusedId && !hostReads(d.id) && !bo.group.visible && !bo.trail?.visible && (d.orbitalSource === OrbitalSource.SPICE_PROBE || !isMajorBody(d.objectType))) {
21263
21396
  bo.staggerSlot ??= nextStaggerSlot++ % HIDDEN_STRIDE;
21264
21397
  if (frame % HIDDEN_STRIDE !== bo.staggerSlot) return;
21265
21398
  }
21266
21399
  const hide = (notForNotice = false) => {
21267
21400
  if (bo) bo.outOfRange = true;
21268
21401
  body.positionUnknown = true;
21402
+ body.placedJd = jd;
21269
21403
  if (!notForNotice && d.id === focusedId) oorState.focusedOutOfRange = true;
21270
21404
  };
21271
21405
  if (d.unplaceable) {
@@ -21346,6 +21480,7 @@ function updatePositions(params) {
21346
21480
  const probeLanded = located.probe.landed;
21347
21481
  if (probeLanded && isLandedAt(located.probe, jd)) {
21348
21482
  const flyingFrameId = d.parentId;
21483
+ placeFirst(`naif-${probeLanded.bodyNaifId}`);
21349
21484
  const landedRender = renderLandedProbe(d, located.probe, probeLanded, jd, positionMap, ctx, bodyObjects);
21350
21485
  if (!landedRender) {
21351
21486
  hide();
@@ -21353,13 +21488,17 @@ function updatePositions(params) {
21353
21488
  }
21354
21489
  diagnostics.clear("probe-unavailable", d.id);
21355
21490
  body.positionUnknown = false;
21491
+ body.placedJd = jd;
21356
21492
  if (flyingFrameId !== d.parentId) body.trailBuffer?.clear();
21357
21493
  if (bo) {
21358
21494
  bo.outOfRange = false;
21359
- if (!bo.isLanded) {
21360
- setSpacecraftLanded(bo.labelHalo, true);
21495
+ const crashed = probeLanded.isDestroyed;
21496
+ if (!bo.isLanded || bo.isCrashed !== crashed) {
21497
+ setSpacecraftGlyph(bo.labelHalo, crashed ? "crashed" : "landed");
21361
21498
  bo.isLanded = true;
21499
+ bo.isCrashed = crashed;
21362
21500
  }
21501
+ if (bo.model) bo.model.visible = !crashed;
21363
21502
  }
21364
21503
  body.position[0] = landedRender.x;
21365
21504
  body.position[1] = landedRender.y;
@@ -21398,8 +21537,13 @@ function updatePositions(params) {
21398
21537
  }
21399
21538
  diagnostics.clear("probe-unavailable", d.id);
21400
21539
  if (bo?.isLanded) {
21401
- setSpacecraftLanded(bo.labelHalo, false);
21540
+ setSpacecraftGlyph(bo.labelHalo, "flying");
21402
21541
  bo.isLanded = false;
21542
+ if (bo.isCrashed) {
21543
+ bo.isCrashed = false;
21544
+ if (bo.model) bo.model.visible = true;
21545
+ if (!bo.noPhysical) setLabelAnnotation(bo, "missing", null);
21546
+ }
21403
21547
  }
21404
21548
  const earthPos = positionMap.get(EARTH_ID);
21405
21549
  const sunPos = positionMap.get(SUN_ID);
@@ -21424,10 +21568,7 @@ function updatePositions(params) {
21424
21568
  populateProbeTrailBuffer(buf, ctx.probeStore, ctx.chebStore ?? null, d.id, probeParentKey, jd);
21425
21569
  }
21426
21570
  if (probeParentChanged) d.parentId = probeParentKey;
21427
- if (!computed.has(probeParentKey)) {
21428
- const parentBody = ctx.bodies.bodiesById.get(probeParentKey) ?? bodyObjects.get(probeParentKey)?.body;
21429
- if (parentBody) computePosition(parentBody);
21430
- }
21571
+ placeFirst(probeParentKey);
21431
21572
  const probeParentPos = positionMap.get(probeParentKey);
21432
21573
  if (!probeParentPos) {
21433
21574
  hide();
@@ -21482,6 +21623,7 @@ function updatePositions(params) {
21482
21623
  }
21483
21624
  if (bo) bo.outOfRange = false;
21484
21625
  body.positionUnknown = false;
21626
+ body.placedJd = jd;
21485
21627
  body.position[0] = x;
21486
21628
  body.position[1] = y;
21487
21629
  body.position[2] = z;
@@ -23155,7 +23297,7 @@ function upgradeTargets(focus, ctx) {
23155
23297
  //#endregion
23156
23298
  //#region src/lib/scene/probe-coverage-watch.ts
23157
23299
  var STOP_INSET_JD = 1e-6;
23158
- function displayName(body) {
23300
+ function displayName$1(body) {
23159
23301
  return body.data.name ?? body.data.id;
23160
23302
  }
23161
23303
  var ProbeCoverageWatch = class {
@@ -23198,7 +23340,7 @@ var ProbeCoverageWatch = class {
23198
23340
  onHit(body, hitJd, forwardJd) {
23199
23341
  this.notices.notify({
23200
23342
  topic: "coverage-pause",
23201
- name: displayName(body),
23343
+ name: displayName$1(body),
23202
23344
  direction: hitJd >= forwardJd ? "forward" : "backward",
23203
23345
  jd: hitJd
23204
23346
  });
@@ -23274,10 +23416,14 @@ function rotateByQuaternion(q, x, y, z, out, at) {
23274
23416
  out[at + 1] = y + q.w * ty + (q.z * tx - q.x * tz);
23275
23417
  out[at + 2] = z + q.w * tz + (q.x * ty - q.y * tx);
23276
23418
  }
23419
+ function bodyCentre(body, ctx, jd) {
23420
+ return placeForRead(body, jd, ctx) ? body.position : null;
23421
+ }
23277
23422
  function resolveAnchor(anchor, ctx, jd) {
23278
23423
  const body = ctx.getBody(anchor.body);
23279
- if (!body) return null;
23280
- const [cx, cy, cz] = body.position;
23424
+ const centre = body && bodyCentre(body, ctx, jd);
23425
+ if (!centre) return null;
23426
+ const [cx, cy, cz] = centre;
23281
23427
  if (isSurface(anchor)) {
23282
23428
  const distance = kmToScene(effectiveRadiusKm(body.data) + (anchor.altitudeKm ?? 0));
23283
23429
  const [dx, dy, dz] = surfaceDirection(anchor.latitude, anchor.longitude);
@@ -23313,12 +23459,13 @@ function resolveAnchor(anchor, ctx, jd) {
23313
23459
  cz + oz
23314
23460
  ];
23315
23461
  }
23316
- function anchorNormal(anchor, world, ctx) {
23462
+ function anchorNormal(anchor, world, ctx, jd) {
23317
23463
  const body = ctx.getBody(anchor.body);
23318
- if (!body) return null;
23319
- const dx = world[0] - body.position[0];
23320
- const dy = world[1] - body.position[1];
23321
- const dz = world[2] - body.position[2];
23464
+ const centre = body && bodyCentre(body, ctx, jd);
23465
+ if (!centre) return null;
23466
+ const dx = world[0] - centre[0];
23467
+ const dy = world[1] - centre[1];
23468
+ const dz = world[2] - centre[2];
23322
23469
  const length = Math.hypot(dx, dy, dz);
23323
23470
  if (length === 0) return null;
23324
23471
  return [
@@ -23362,6 +23509,9 @@ function resolvePose(pose, ctx, jd) {
23362
23509
  //#region src/lib/scene/camera/motion-scale.ts
23363
23510
  var MIN_TRANSLATE_SCALE = .02;
23364
23511
  var MIN_ROTATE_SCALE = 1e-4;
23512
+ function orbitDamping(reducedMotion) {
23513
+ return reducedMotion ? .4 : .05;
23514
+ }
23365
23515
  function cameraMotionScale(camera, focusTruePos, focused, parent, seated) {
23366
23516
  const orbitRadius = camera.position.length();
23367
23517
  if (!(orbitRadius > 0)) return {
@@ -23437,8 +23587,6 @@ function updateUserLocationOcclusion(marker, bodyObjects, camera) {
23437
23587
  //#endregion
23438
23588
  //#region src/lib/scene/renderer.ts
23439
23589
  var NO_TOUCH_GESTURE = -1;
23440
- var DEFAULT_DAMPING = .05;
23441
- var REDUCED_MOTION_DAMPING = .4;
23442
23590
  var FAR_MARGIN = 1.2;
23443
23591
  var FAR_MIN = kmToScene(1e6);
23444
23592
  var FAR_UPDATE_EPS = .02;
@@ -23542,9 +23690,14 @@ var SceneRenderer = class SceneRenderer {
23542
23690
  focusDurationMs: FOCUS_DURATION_MS
23543
23691
  };
23544
23692
  lastUpdatedJd = NaN;
23693
+ get placedJd() {
23694
+ return this.lastUpdatedJd;
23695
+ }
23545
23696
  lastUpdatedSystemId = null;
23546
23697
  lastSeatConfigKey = null;
23547
23698
  lastProbeVersion = 0;
23699
+ disposed = false;
23700
+ lastHostReadVersion = 0;
23548
23701
  focusWasOutOfRange = false;
23549
23702
  _positionMapScratch = /* @__PURE__ */ new Map();
23550
23703
  frameIndex = 0;
@@ -23647,7 +23800,7 @@ var SceneRenderer = class SceneRenderer {
23647
23800
  this.orbitPreview = new OrbitPreviewOverlay(this.scene);
23648
23801
  this.orbitPreview.setLayer(SceneRenderer.MAP_LAYER);
23649
23802
  this.skyboxAdjuster.set(0, 0, 0);
23650
- loadSkybox(this.scene, this.renderer, ctx);
23803
+ loadSkybox(this.scene, this.renderer, ctx, () => this.disposed);
23651
23804
  const sunBody = ctx.bodies.majorBodies.find((b) => b.data.id === SUN_ID);
23652
23805
  const resident = ctx.getBody(initialView.id);
23653
23806
  const matchedBody = resident && !resident.positionUnknown ? resident : void 0;
@@ -23667,7 +23820,7 @@ var SceneRenderer = class SceneRenderer {
23667
23820
  this.pointClouds.setPointBudget(this.tier.pointBudget);
23668
23821
  this.controls = new OrbitControls(this.camera, canvas);
23669
23822
  this.controls.enableDamping = true;
23670
- this.controls.dampingFactor = sceneSettings().resolvedReducedMotion ? REDUCED_MOTION_DAMPING : DEFAULT_DAMPING;
23823
+ this.controls.dampingFactor = orbitDamping(sceneSettings().resolvedReducedMotion);
23671
23824
  this.controls.minDistance = focusBody ? minCameraDistance(focusBody) : kmToScene(.01);
23672
23825
  this.controls.maxDistance = 31620.5 * 10;
23673
23826
  this.controls.target.set(0, 0, 0);
@@ -24249,7 +24402,7 @@ var SceneRenderer = class SceneRenderer {
24249
24402
  if (this.modelShadowPlane) {
24250
24403
  this._tmpUp.set(0, 1, 0).applyQuaternion(bo.model.quaternion);
24251
24404
  const daytime = this._tmpSun.dot(this._tmpUp) > .03;
24252
- this.modelShadowPlane.visible = Boolean(bo.isLanded) && daytime;
24405
+ this.modelShadowPlane.visible = Boolean(bo.isLanded) && !bo.isCrashed && daytime;
24253
24406
  if (this.modelShadowPlane.visible) this.modelShadowPlane.quaternion.setFromUnitVectors(this._planeNormal, this._tmpUp);
24254
24407
  }
24255
24408
  this._tmpV3.set(0, 0, 0);
@@ -24516,7 +24669,8 @@ var SceneRenderer = class SceneRenderer {
24516
24669
  const systemId = this.ctx.visibility.focusedSystemId;
24517
24670
  const seatKey = this.focusedSeatConfigKey();
24518
24671
  const probeVersion = this.ctx.probeStore?.version ?? 0;
24519
- if (this.clock.jd === this.lastUpdatedJd && systemId === this.lastUpdatedSystemId && seatKey === this.lastSeatConfigKey && probeVersion === this.lastProbeVersion) {
24672
+ const hostReadVersion = this.ctx.bodies.hostReadVersion;
24673
+ if (this.clock.jd === this.lastUpdatedJd && systemId === this.lastUpdatedSystemId && seatKey === this.lastSeatConfigKey && probeVersion === this.lastProbeVersion && hostReadVersion === this.lastHostReadVersion) {
24520
24674
  this.clock.seeked = false;
24521
24675
  return;
24522
24676
  }
@@ -24526,6 +24680,7 @@ var SceneRenderer = class SceneRenderer {
24526
24680
  this.lastUpdatedSystemId = systemId;
24527
24681
  this.lastSeatConfigKey = seatKey;
24528
24682
  this.lastProbeVersion = probeVersion;
24683
+ this.lastHostReadVersion = hostReadVersion;
24529
24684
  this.ctx.refreshTick(jdToDate(this.clock.jd));
24530
24685
  this.focusController.promotion.onSimTimeChanged();
24531
24686
  const result = updatePositions({
@@ -24555,7 +24710,7 @@ var SceneRenderer = class SceneRenderer {
24555
24710
  }
24556
24711
  }
24557
24712
  setReducedMotion(on) {
24558
- this.controls.dampingFactor = on ? REDUCED_MOTION_DAMPING : DEFAULT_DAMPING;
24713
+ this.controls.dampingFactor = orbitDamping(on);
24559
24714
  }
24560
24715
  focusOnBody(id, zoom, latitude, longitude) {
24561
24716
  this.disarmLimits();
@@ -24861,7 +25016,7 @@ var SceneRenderer = class SceneRenderer {
24861
25016
  this.renderer.setPixelRatio(renderPixelRatio());
24862
25017
  this.composer.setPixelRatio(renderPixelRatio());
24863
25018
  this.resize(this.canvas.clientWidth, this.canvas.clientHeight);
24864
- loadSkybox(this.scene, this.renderer, this.ctx);
25019
+ loadSkybox(this.scene, this.renderer, this.ctx, () => this.disposed);
24865
25020
  this.resume();
24866
25021
  }
24867
25022
  recoverWorkersIfDead(timeoutMs) {
@@ -24892,6 +25047,7 @@ var SceneRenderer = class SceneRenderer {
24892
25047
  setTrailResolution(width, height);
24893
25048
  }
24894
25049
  dispose() {
25050
+ this.disposed = true;
24895
25051
  cancelAnimationFrame(this.rafId);
24896
25052
  this.pointerInteraction.detach();
24897
25053
  this.gpuPick.dispose();
@@ -25313,8 +25469,8 @@ var MarkerExtension = class {
25313
25469
  this.label.visible = this.occlude ? this.facesCamera(world, frame) : true;
25314
25470
  this.object.updateMatrixWorld(true);
25315
25471
  }
25316
- facesCamera(world, { camera, ctx }) {
25317
- const normal = anchorNormal(this.anchor, world, ctx);
25472
+ facesCamera(world, { camera, ctx, jd }) {
25473
+ const normal = anchorNormal(this.anchor, world, ctx, jd);
25318
25474
  if (!normal) return true;
25319
25475
  const { position } = this.label;
25320
25476
  const dx = camera.position.x - position.x;
@@ -26676,16 +26832,17 @@ var SurfaceShapeExtension = class {
26676
26832
  }
26677
26833
  update({ jd, basis, camera, ctx }) {
26678
26834
  const body = this.wanted && this.count >= 2 ? ctx.getBody(this.body) : void 0;
26679
- if (this.line) this.line.visible = body !== void 0;
26680
- if (this.fill) this.fill.visible = body !== void 0;
26681
- if (!body) return;
26835
+ const centre = body && bodyCentre(body, ctx, jd);
26836
+ if (this.line) this.line.visible = !!centre;
26837
+ if (this.fill) this.fill.visible = !!centre;
26838
+ if (!body || !centre) return;
26682
26839
  const radiusKm = effectiveRadiusKm(body.data);
26683
26840
  const scale = kmToScene(radiusKm + (this.altitudeKm ?? radiusKm * DEFAULT_LIFT));
26684
26841
  const q = body.orientation ? bodyQuaternion(body.orientation, jd, body.nutPrec) : null;
26685
26842
  const origin = [
26686
- body.position[0] - basis[0],
26687
- body.position[1] - basis[1],
26688
- body.position[2] - basis[2]
26843
+ centre[0] - basis[0],
26844
+ centre[1] - basis[1],
26845
+ centre[2] - basis[2]
26689
26846
  ];
26690
26847
  if (this.line) {
26691
26848
  this.place(this.dirs, this.count, q, scale, origin, this.scratch);
@@ -26915,6 +27072,50 @@ var MapObjectCollection = class {
26915
27072
  }
26916
27073
  };
26917
27074
 
27075
+ //#endregion
27076
+ //#region src/lib/scene/extensions/measure.ts
27077
+ function anchorOffsetKm(a, b, ctx, jd) {
27078
+ const from = resolveAnchor(a, ctx, jd);
27079
+ const to = resolveAnchor(b, ctx, jd);
27080
+ if (!from || !to) return null;
27081
+ return sceneToEcliptic([
27082
+ to[0] - from[0],
27083
+ to[1] - from[1],
27084
+ to[2] - from[2]
27085
+ ]);
27086
+ }
27087
+ function anchorDistanceKm(a, b, ctx, jd) {
27088
+ const offset = anchorOffsetKm(a, b, ctx, jd);
27089
+ return offset && Math.hypot(...offset);
27090
+ }
27091
+ function subsolarPoint(id, ctx, jd) {
27092
+ const body = ctx.getBody(id);
27093
+ const sun = ctx.getBody(SUN_ID);
27094
+ if (!body || !sun || body === sun) return null;
27095
+ if (!body.orientation && !ctx.bodies.orientationRead.has(id)) return null;
27096
+ const centre = bodyCentre(body, ctx, jd);
27097
+ if (!centre) return null;
27098
+ const turn = body.orientation ? bodyQuaternion(body.orientation, jd, body.nutPrec).toArray() : [
27099
+ 0,
27100
+ 0,
27101
+ 0,
27102
+ 1
27103
+ ];
27104
+ const { latitude, longitude, distance } = cartesianToSpherical([
27105
+ sun.position[0],
27106
+ sun.position[1],
27107
+ sun.position[2]
27108
+ ], [
27109
+ centre[0],
27110
+ centre[1],
27111
+ centre[2]
27112
+ ], turn);
27113
+ return distance > 0 ? {
27114
+ lon: longitude,
27115
+ lat: latitude
27116
+ } : null;
27117
+ }
27118
+
26918
27119
  //#endregion
26919
27120
  //#region src/lib/interaction/gesture.ts
26920
27121
  var GestureHandler = class {
@@ -27095,7 +27296,8 @@ var KEY_ROTATE_RAD = .05;
27095
27296
  var KEY_ZOOM_FACTOR = 1.15;
27096
27297
  var CONTEXT_LOST_PANEL_DELAY_MS = 2e3;
27097
27298
  var RESTORE_PING_TIMEOUT_MS = 4e3;
27098
- var onceTarget$1 = /* @__PURE__ */ new WeakMap();
27299
+ var mapsIn = /* @__PURE__ */ new WeakMap();
27300
+ var onceTarget$2 = /* @__PURE__ */ new WeakMap();
27099
27301
  var SpaceMap = class {
27100
27302
  ctx = new ContextManager();
27101
27303
  clock;
@@ -27196,14 +27398,14 @@ var SpaceMap = class {
27196
27398
  off();
27197
27399
  listener(...args);
27198
27400
  };
27199
- onceTarget$1.set(wrapped, listener);
27401
+ onceTarget$2.set(wrapped, listener);
27200
27402
  const off = this.on(event, wrapped);
27201
27403
  return off;
27202
27404
  }
27203
27405
  off(event, listener) {
27204
27406
  const set = this.listeners[event];
27205
27407
  set.delete(listener);
27206
- for (const held of set) if (onceTarget$1.get(held) === listener) set.delete(held);
27408
+ for (const held of set) if (onceTarget$2.get(held) === listener) set.delete(held);
27207
27409
  }
27208
27410
  emit(event, ...args) {
27209
27411
  for (const listener of this.listeners[event]) listener(...args);
@@ -27221,6 +27423,19 @@ var SpaceMap = class {
27221
27423
  getChildren(id) {
27222
27424
  return [...this.ctx.bodies.getChildren(id) ?? []];
27223
27425
  }
27426
+ get placedJd() {
27427
+ const jd = this.renderer?.placedJd;
27428
+ return jd === void 0 || Number.isNaN(jd) ? this.clock.jd : jd;
27429
+ }
27430
+ distanceKm(from, to) {
27431
+ return anchorDistanceKm(from, to, this.ctx, this.placedJd);
27432
+ }
27433
+ offsetKm(from, to) {
27434
+ return anchorOffsetKm(from, to, this.ctx, this.placedJd);
27435
+ }
27436
+ getSubsolarPoint(id) {
27437
+ return subsolarPoint(id, this.ctx, this.placedJd);
27438
+ }
27224
27439
  load(targetId = this.initialView.id) {
27225
27440
  return this.ctx.load(jdToDate(this.clock.jd), targetId);
27226
27441
  }
@@ -27252,6 +27467,7 @@ var SpaceMap = class {
27252
27467
  if (this.container) throw new Error("SpaceMap is already mounted");
27253
27468
  this.container = container;
27254
27469
  container.classList.add("sm-map");
27470
+ mapsIn.set(container, (mapsIn.get(container) ?? 0) + 1);
27255
27471
  this.controls = new ControlHost(this, container);
27256
27472
  const canvas = document.createElement("canvas");
27257
27473
  canvas.className = "sm-map__canvas";
@@ -27331,7 +27547,9 @@ var SpaceMap = class {
27331
27547
  this.renderer = null;
27332
27548
  canvas.remove();
27333
27549
  this.labelLayer?.remove();
27334
- container.classList.remove("sm-map");
27550
+ const left = (mapsIn.get(container) ?? 1) - 1;
27551
+ mapsIn.set(container, left);
27552
+ if (left === 0) container.classList.remove("sm-map");
27335
27553
  this.container = this.canvas = this.labelLayer = null;
27336
27554
  }
27337
27555
  watch(renderer) {
@@ -27752,7 +27970,7 @@ var SpaceMap = class {
27752
27970
 
27753
27971
  //#endregion
27754
27972
  //#region src/lib/flatmap/layers.ts
27755
- var SVG_NS = "http://www.w3.org/2000/svg";
27973
+ var SVG_NS$1 = "http://www.w3.org/2000/svg";
27756
27974
  function layerInfo(layer) {
27757
27975
  return {
27758
27976
  id: layer.id,
@@ -27770,7 +27988,7 @@ function creditOf(bundle) {
27770
27988
  };
27771
27989
  }
27772
27990
  function pathElement(d, className) {
27773
- const path = document.createElementNS(SVG_NS, "path");
27991
+ const path = document.createElementNS(SVG_NS$1, "path");
27774
27992
  path.setAttribute("d", d);
27775
27993
  path.setAttribute("class", className);
27776
27994
  return path;
@@ -27789,7 +28007,7 @@ function graticuleLayer(label, stepDeg = 30) {
27789
28007
  if (viewport.projection.azimuthal) {
27790
28008
  const centre = viewport.toScreen(0, 0);
27791
28009
  const edge = viewport.toScreen(viewport.projection.extent.maxX, 0);
27792
- const circle = document.createElementNS(SVG_NS, "circle");
28010
+ const circle = document.createElementNS(SVG_NS$1, "circle");
27793
28011
  circle.setAttribute("cx", String(centre[0]));
27794
28012
  circle.setAttribute("cy", String(centre[1]));
27795
28013
  circle.setAttribute("r", String(Math.abs(edge[0] - centre[0])));
@@ -27826,12 +28044,12 @@ function nomenclatureLayer(bodyId, label) {
27826
28044
  for (const shift of shifts) {
27827
28045
  const x = at[0] + shift;
27828
28046
  if (x < 0 || x > viewport.width) continue;
27829
- const dot = document.createElementNS(SVG_NS, "circle");
28047
+ const dot = document.createElementNS(SVG_NS$1, "circle");
27830
28048
  dot.setAttribute("cx", x.toFixed(1));
27831
28049
  dot.setAttribute("cy", at[1].toFixed(1));
27832
28050
  dot.setAttribute("r", "1.6");
27833
28051
  dot.setAttribute("class", "sm-flat__feature-dot");
27834
- const text = document.createElementNS(SVG_NS, "text");
28052
+ const text = document.createElementNS(SVG_NS$1, "text");
27835
28053
  text.setAttribute("x", (x + 4).toFixed(1));
27836
28054
  text.setAttribute("y", (at[1] + 3).toFixed(1));
27837
28055
  text.setAttribute("class", "sm-flat__feature-label");
@@ -27878,12 +28096,12 @@ var PathDrawing = class {
27878
28096
  this.onRemove = onRemove;
27879
28097
  this.onChange = onChange;
27880
28098
  this.points = points;
27881
- this.node = document.createElementNS(SVG_NS, "g");
28099
+ this.node = document.createElementNS(SVG_NS$1, "g");
27882
28100
  applyStyle(this.node, style);
27883
28101
  bindEvents(this.node, style);
27884
- this.area = closed ? document.createElementNS(SVG_NS, "path") : null;
28102
+ this.area = closed ? document.createElementNS(SVG_NS$1, "path") : null;
27885
28103
  this.area?.setAttribute("stroke", "none");
27886
- this.edge = document.createElementNS(SVG_NS, "path");
28104
+ this.edge = document.createElementNS(SVG_NS$1, "path");
27887
28105
  this.edge.setAttribute("fill", "none");
27888
28106
  if (this.area) this.node.append(this.area);
27889
28107
  this.node.append(this.edge);
@@ -28204,27 +28422,33 @@ async function fetchJson(url) {
28204
28422
  return null;
28205
28423
  }
28206
28424
  }
28207
- async function fromBundle(bodyId) {
28208
- const meta = await fetchJson(versionedUrl(`/v1/textures/${bodyId}/metadata.json`, "textures"));
28209
- if (!meta) return {};
28210
- if (!textureAllowed(meta.distribution)) return {};
28211
- const tiers = Object.keys(meta.exports ?? {}).filter((k) => [
28212
- "low",
28213
- "medium",
28214
- "high"
28215
- ].includes(k));
28216
- if (tiers.length === 0) return {};
28217
- return { surface: {
28425
+ async function tiersOf(bundleId) {
28426
+ const found = await Promise.all(TIER_ORDER.map((tier) => fetch(versionedUrl(`/v1/textures/${bundleId}/${tier}.webp`, "textures"), { method: "HEAD" }).then((response) => response.ok).catch(() => false)));
28427
+ return TIER_ORDER.filter((_, i) => found[i]);
28428
+ }
28429
+ async function fromRecord(bodyId, record, tiers = tiersOf) {
28430
+ if (!record?.map_texture_available || !record.texture) return {};
28431
+ const surface = pickTexture([{
28432
+ ...record.texture,
28218
28433
  id: bodyId,
28219
- tiers,
28220
- source: String(meta.source ?? ""),
28221
- organisation: String(meta.organisation ?? ""),
28222
- type: String(meta.type ?? "cylindrical"),
28223
- attribution: meta.attribution,
28224
- description: meta.description,
28225
- license: meta.license,
28226
- distribution: meta.distribution
28227
- } };
28434
+ tiers: void 0
28435
+ }, ...record.alternates ?? []]);
28436
+ if (!surface) return {};
28437
+ const written = surface.tiers ?? await tiers(surface.id);
28438
+ if (written.length === 0) return {};
28439
+ const { frames, ...bundle } = surface;
28440
+ const radii = record.radii;
28441
+ return {
28442
+ surface: {
28443
+ ...bundle,
28444
+ tiers: written,
28445
+ monthlyFrames: frames
28446
+ },
28447
+ radiusKm: radii ? (radii.a + radii.b + radii.c) / 3 : void 0
28448
+ };
28449
+ }
28450
+ async function fromBundle(bodyId) {
28451
+ return fromRecord(bodyId, (await fetchObjectDetail(bodyId, false).catch(() => null))?.global);
28228
28452
  }
28229
28453
  async function loadBodySources(bodyId) {
28230
28454
  const systemId = systemIdFor(bodyId);
@@ -28525,14 +28749,14 @@ var RASTER_LAYERS = [
28525
28749
  function isRaster(layer) {
28526
28750
  return layer.kind === "raster";
28527
28751
  }
28528
- var onceTarget = /* @__PURE__ */ new WeakMap();
28752
+ var onceTarget$1 = /* @__PURE__ */ new WeakMap();
28529
28753
  var FlatMap = class {
28530
28754
  container = null;
28531
28755
  root = document.createElement("div");
28532
28756
  canvas = document.createElement("canvas");
28533
- svg = document.createElementNS(SVG_NS, "svg");
28534
- layerGroup = document.createElementNS(SVG_NS, "g");
28535
- shapeGroup = document.createElementNS(SVG_NS, "g");
28757
+ svg = document.createElementNS(SVG_NS$1, "svg");
28758
+ layerGroup = document.createElementNS(SVG_NS$1, "g");
28759
+ shapeGroup = document.createElementNS(SVG_NS$1, "g");
28536
28760
  markerLayer = document.createElement("div");
28537
28761
  ctx;
28538
28762
  overlay;
@@ -28903,7 +29127,7 @@ var FlatMap = class {
28903
29127
  off();
28904
29128
  listener(...args);
28905
29129
  });
28906
- onceTarget.set(wrapped, listener);
29130
+ onceTarget$1.set(wrapped, listener);
28907
29131
  const off = this.on(event, wrapped);
28908
29132
  return off;
28909
29133
  }
@@ -28911,7 +29135,7 @@ var FlatMap = class {
28911
29135
  const set = this.listeners.get(event);
28912
29136
  if (!set) return;
28913
29137
  set.delete(listener);
28914
- for (const held of set) if (onceTarget.get(held) === listener) set.delete(held);
29138
+ for (const held of set) if (onceTarget$1.get(held) === listener) set.delete(held);
28915
29139
  }
28916
29140
  emit(event, ...args) {
28917
29141
  const set = this.listeners.get(event);
@@ -29115,7 +29339,7 @@ var FlatMap = class {
29115
29339
  this.layerGroup.replaceChildren();
29116
29340
  for (const layer of this.layerList) {
29117
29341
  if (isRaster(layer) || !layer.visible) continue;
29118
- const group = document.createElementNS(SVG_NS, "g");
29342
+ const group = document.createElementNS(SVG_NS$1, "g");
29119
29343
  group.setAttribute("data-layer", layer.id);
29120
29344
  layer.render(viewport, group);
29121
29345
  this.layerGroup.append(group);
@@ -30048,6 +30272,1455 @@ var PanoramaView = class {
30048
30272
  };
30049
30273
  };
30050
30274
 
30275
+ //#endregion
30276
+ //#region src/lib/fetch/groups/planetary-systems-map.ts
30277
+ var fetchPlanetarySystemsMap = memoizedGzJson(() => `${dataBase()}/v1/groups/__planetary_systems_map__.json.gz`, { error: (res) => `Failed to fetch planetary systems map: ${res.status}` });
30278
+
30279
+ //#endregion
30280
+ //#region src/lib/fetch/groups/solar-system-map.ts
30281
+ var fetchSolarSystemMap = memoizedGzJson(() => `${dataBase()}/v1/groups/__solar_system_map__.json.gz`, { error: (res) => `Failed to fetch solar system map: ${res.status}` });
30282
+
30283
+ //#endregion
30284
+ //#region src/lib/charts/scrub.ts
30285
+ var DRAG_SLOP = 8;
30286
+ function createScrub({ onScrub, onEnd }) {
30287
+ let downX = null;
30288
+ let downY = 0;
30289
+ let scrubbing = false;
30290
+ function end() {
30291
+ if (scrubbing) onEnd();
30292
+ downX = null;
30293
+ scrubbing = false;
30294
+ }
30295
+ return {
30296
+ onpointerdown(e) {
30297
+ if (e.pointerType === "mouse") return;
30298
+ downX = e.clientX;
30299
+ downY = e.clientY;
30300
+ scrubbing = false;
30301
+ },
30302
+ onpointermove(e) {
30303
+ if (e.pointerType === "mouse" || downX === null) return;
30304
+ if (!scrubbing && Math.hypot(e.clientX - downX, e.clientY - downY) < 8) return;
30305
+ scrubbing = true;
30306
+ onScrub(e.clientX, e.clientY);
30307
+ },
30308
+ onpointerup: end,
30309
+ onpointercancel: end,
30310
+ onpointerleave: end
30311
+ };
30312
+ }
30313
+
30314
+ //#endregion
30315
+ //#region src/lib/systemmap/layout.ts
30316
+ var VIEW_H = 240;
30317
+ var X_LEFT = 44;
30318
+ var X_RIGHT = 700;
30319
+ var CY = 120;
30320
+ var MIN_R = 2.2;
30321
+ var MAX_OFFSET = 84;
30322
+ var PRIMARY_LIMB_X = 44 - 4;
30323
+ var MOON_GAP = 4;
30324
+ var MOON_SPACING = 3;
30325
+ var HIT_MARGIN = 16;
30326
+ var INSIDE = 1e4;
30327
+ var MOON_ZONE_W = 34;
30328
+ var MOON_ZONE_PAD = 7;
30329
+ var RING_FORESHORTEN = .42;
30330
+ var BAND_Y = 26;
30331
+ var BAND_H = 240 - 60;
30332
+ var BAND_MIN_HIT = 30;
30333
+ function minDistance(cx, cy, placed) {
30334
+ let min = Infinity;
30335
+ for (const p of placed) min = Math.min(min, Math.hypot(p.cx - cx, p.cy - cy));
30336
+ return min;
30337
+ }
30338
+ function foldTilt(tiltDeg) {
30339
+ return tiltDeg > 90 ? 180 - tiltDeg : tiltDeg;
30340
+ }
30341
+ function layout(model) {
30342
+ const logLo = Math.log10(model.domain[0]);
30343
+ const logSpan = Math.log10(model.domain[1]) - logLo;
30344
+ const xOf = (km) => {
30345
+ return 44 + (Math.log10(Math.max(km / model.unitKm, 1e-9)) - logLo) / logSpan * (700 - 44);
30346
+ };
30347
+ const bodies = [];
30348
+ for (const b of [...model.bodies].sort((a, b) => a.aKm - b.aKm)) {
30349
+ const cx = xOf(b.aKm);
30350
+ const r = Math.max(MIN_R, b.radiusKm * model.pxPerKm);
30351
+ let cy = 120;
30352
+ if (b.tiltDeg > 0) {
30353
+ const offset = Math.min(foldTilt(b.tiltDeg) * model.pxPerDeg, MAX_OFFSET);
30354
+ const up = 120 - offset;
30355
+ const down = 120 + offset;
30356
+ cy = minDistance(cx, up, bodies) >= minDistance(cx, down, bodies) ? up : down;
30357
+ }
30358
+ bodies.push({
30359
+ ...b,
30360
+ cx,
30361
+ cy,
30362
+ r
30363
+ });
30364
+ }
30365
+ const moonZones = [];
30366
+ for (const parent of bodies) {
30367
+ if (!parent.satellites?.length) continue;
30368
+ const ringTop = parent.rings ? parent.r * parent.rings.outer * RING_FORESHORTEN : 0;
30369
+ let y = parent.cy - Math.max(parent.r, ringTop) - MOON_GAP;
30370
+ const moons = [];
30371
+ for (const s of [...parent.satellites].sort((a, b) => b.radiusKm - a.radiusKm)) {
30372
+ const r = Math.max(MIN_R, s.radiusKm * model.pxPerKm);
30373
+ y -= r;
30374
+ moons.push({
30375
+ ...s,
30376
+ cx: parent.cx,
30377
+ cy: y,
30378
+ r,
30379
+ hitY: 0,
30380
+ hitHeight: 0
30381
+ });
30382
+ y -= r + MOON_SPACING;
30383
+ }
30384
+ const top = Math.min(...moons.map((mn) => mn.cy - mn.r)) - MOON_ZONE_PAD;
30385
+ const bottom = Math.max(...moons.map((mn) => mn.cy + mn.r)) + MOON_ZONE_PAD;
30386
+ moons.forEach((mn, idx) => {
30387
+ const below = idx === 0 ? bottom : (moons[idx - 1].cy + mn.cy) / 2;
30388
+ const above = idx === moons.length - 1 ? top : (moons[idx + 1].cy + mn.cy) / 2;
30389
+ mn.hitY = above;
30390
+ mn.hitHeight = below - above;
30391
+ });
30392
+ moonZones.push({
30393
+ parent,
30394
+ moons,
30395
+ x: parent.cx - MOON_ZONE_W / 2,
30396
+ y: top,
30397
+ width: MOON_ZONE_W,
30398
+ height: bottom - top,
30399
+ perMoon: !parent.grouped && !parent.satellitesLink && moons.some((mn) => mn.link)
30400
+ });
30401
+ }
30402
+ const bands = model.bands.map((b) => {
30403
+ const x = xOf(b.innerKm);
30404
+ const width = xOf(b.outerKm) - x;
30405
+ const hitWidth = Math.max(width, BAND_MIN_HIT);
30406
+ return {
30407
+ ...b,
30408
+ x,
30409
+ width,
30410
+ hitX: x + (width - hitWidth) / 2,
30411
+ hitWidth
30412
+ };
30413
+ }).sort((a, b) => b.width - a.width);
30414
+ let cloudPath = "";
30415
+ model.cloud?.points.forEach((p, idx) => {
30416
+ const aUnits = p.aKm / model.unitKm;
30417
+ if (aUnits < model.domain[0] || aUnits > model.domain[1]) return;
30418
+ const offset = Math.min(Math.max(foldTilt(p.tiltDeg), 0) * model.pxPerDeg, MAX_OFFSET);
30419
+ const cy = 120 + (idx % 2 === 0 ? -offset : offset);
30420
+ cloudPath += `M${xOf(p.aKm).toFixed(1)} ${cy.toFixed(1)}h0.01`;
30421
+ });
30422
+ const primaryR = model.primary.radiusKm * model.pxPerKm;
30423
+ return {
30424
+ bodies,
30425
+ moonZones,
30426
+ bands,
30427
+ cloudPath,
30428
+ primaryR,
30429
+ primaryCx: PRIMARY_LIMB_X - primaryR,
30430
+ xOf
30431
+ };
30432
+ }
30433
+ function inside(z, vx, vy) {
30434
+ return vx >= z.x && vx <= z.x + z.width && vy >= z.y && vy <= z.y + z.height;
30435
+ }
30436
+ function nearestBody(l, vx, vy, reach) {
30437
+ for (const pickable of [true, false]) {
30438
+ let nearest = null;
30439
+ let gap = reach;
30440
+ for (const b of l.bodies) {
30441
+ if (!!b.link !== pickable) continue;
30442
+ const centre = Math.hypot(vx - b.cx, vy - b.cy);
30443
+ const edge = centre <= b.r ? centre - INSIDE : centre - b.r;
30444
+ if (edge > gap) continue;
30445
+ nearest = b;
30446
+ gap = edge;
30447
+ }
30448
+ if (nearest) return nearest;
30449
+ }
30450
+ return null;
30451
+ }
30452
+ function hitTest(l, vx, vy) {
30453
+ const on = nearestBody(l, vx, vy, 0);
30454
+ if (on) return {
30455
+ kind: "body",
30456
+ id: on.id
30457
+ };
30458
+ for (const z of l.moonZones) {
30459
+ if (!inside(z, vx, vy)) continue;
30460
+ if (z.parent.grouped) return {
30461
+ kind: "body",
30462
+ id: z.parent.id
30463
+ };
30464
+ if (!z.perMoon) return {
30465
+ kind: "stack",
30466
+ id: z.parent.id
30467
+ };
30468
+ const moon = z.moons.find((mn) => vy >= mn.hitY && vy <= mn.hitY + mn.hitHeight);
30469
+ if (moon) return {
30470
+ kind: "moon",
30471
+ parent: z.parent.id,
30472
+ id: moon.id
30473
+ };
30474
+ }
30475
+ const near = nearestBody(l, vx, vy, 16);
30476
+ if (near) return {
30477
+ kind: "body",
30478
+ id: near.id
30479
+ };
30480
+ for (let i = l.bands.length - 1; i >= 0; i--) {
30481
+ const band = l.bands[i];
30482
+ if (vx >= band.hitX && vx <= band.hitX + band.hitWidth && vy >= 26 && vy <= 26 + 180) return {
30483
+ kind: "band",
30484
+ key: band.key
30485
+ };
30486
+ }
30487
+ if (Math.hypot(vx - l.primaryCx, vy - 120) <= l.primaryR) return { kind: "primary" };
30488
+ return null;
30489
+ }
30490
+ function tipFor(model, l, hover) {
30491
+ if (!hover) return null;
30492
+ const { text } = model;
30493
+ switch (hover.kind) {
30494
+ case "primary": return {
30495
+ cx: Math.max(44, l.primaryCx),
30496
+ cy: 120 - Math.min(l.primaryR, 120 - 40),
30497
+ title: model.primary.name,
30498
+ sub: text.primary
30499
+ };
30500
+ case "body": {
30501
+ const b = l.bodies.find((p) => p.id === hover.id);
30502
+ if (!b) return null;
30503
+ const parts = [text.distance(b.aKm)];
30504
+ if (b.tiltDeg > 90) parts.push(text.retrograde);
30505
+ if (b.grouped && b.satellites?.length) parts.push(text.moons(b.satelliteCount ?? b.satellites.length));
30506
+ return {
30507
+ cx: b.cx,
30508
+ cy: b.cy - b.r - 4,
30509
+ title: b.name,
30510
+ sub: parts.join(" · ")
30511
+ };
30512
+ }
30513
+ case "stack": {
30514
+ const zone = l.moonZones.find((z) => z.parent.id === hover.id);
30515
+ if (!zone) return null;
30516
+ const cx = zone.x + zone.width / 2;
30517
+ return zone.parent.satellitesTab ? {
30518
+ cx,
30519
+ cy: zone.y,
30520
+ title: zone.parent.name,
30521
+ sub: text.moons(zone.parent.satelliteCount ?? zone.moons.length)
30522
+ } : {
30523
+ cx,
30524
+ cy: zone.y,
30525
+ title: zone.moons[0].name,
30526
+ sub: zone.parent.name
30527
+ };
30528
+ }
30529
+ case "moon": {
30530
+ const zone = l.moonZones.find((z) => z.parent.id === hover.parent);
30531
+ const moon = zone?.moons.find((mn) => mn.id === hover.id);
30532
+ if (!zone || !moon) return null;
30533
+ return {
30534
+ cx: moon.cx,
30535
+ cy: moon.cy - moon.r - 4,
30536
+ title: moon.name,
30537
+ sub: zone.parent.name
30538
+ };
30539
+ }
30540
+ case "band": {
30541
+ const band = l.bands.find((bd) => bd.key === hover.key);
30542
+ if (!band) return null;
30543
+ return {
30544
+ cx: band.x + band.width / 2,
30545
+ cy: 44,
30546
+ title: band.name ?? band.label,
30547
+ sub: band.sub ?? `${text.distance(band.innerKm)}–${text.distance(band.outerKm)}`
30548
+ };
30549
+ }
30550
+ }
30551
+ }
30552
+
30553
+ //#endregion
30554
+ //#region src/lib/systemmap/chart.ts
30555
+ var SVG_NS = "http://www.w3.org/2000/svg";
30556
+ var PRIMARY_RIM = 10;
30557
+ var RING_TILT = -18;
30558
+ var CLOUD_R = 1.2;
30559
+ var TIP_H = 28;
30560
+ var TIP_MARGIN = 6;
30561
+ var LABEL_MARGIN = 4;
30562
+ var LABEL_SIZE = 18;
30563
+ var LABEL_MIN_SIZE = 12;
30564
+ var LABEL_MAX_W = 190;
30565
+ function svg(tag, attrs = {}, ...children) {
30566
+ const el = document.createElementNS(SVG_NS, tag);
30567
+ for (const [name, value] of Object.entries(attrs)) if (value !== void 0) el.setAttribute(name, String(value));
30568
+ for (const child of children) if (child) el.append(child);
30569
+ return el;
30570
+ }
30571
+ function sameHover(a, b) {
30572
+ return JSON.stringify(a) === JSON.stringify(b);
30573
+ }
30574
+ function linksOf(model, l) {
30575
+ const links = /* @__PURE__ */ new Map();
30576
+ const add = (hover, link) => {
30577
+ if (link?.href || link?.onclick) links.set(JSON.stringify(hover), link);
30578
+ };
30579
+ for (const band of l.bands) add({
30580
+ kind: "band",
30581
+ key: band.key
30582
+ }, band);
30583
+ add({ kind: "primary" }, model.primary.link);
30584
+ for (const b of l.bodies) add({
30585
+ kind: "body",
30586
+ id: b.id
30587
+ }, b.link);
30588
+ for (const z of l.moonZones) {
30589
+ if (z.parent.grouped) continue;
30590
+ if (!z.perMoon) add({
30591
+ kind: "stack",
30592
+ id: z.parent.id
30593
+ }, z.parent.satellitesLink);
30594
+ else for (const mn of z.moons) add({
30595
+ kind: "moon",
30596
+ parent: z.parent.id,
30597
+ id: mn.id
30598
+ }, mn.link);
30599
+ }
30600
+ return links;
30601
+ }
30602
+ var nextUid = 0;
30603
+ var SystemMapChart = class {
30604
+ root = document.createElement("div");
30605
+ svg = svg("svg");
30606
+ tip = document.createElement("div");
30607
+ tipTitle = document.createElement("span");
30608
+ tipSub = document.createElement("span");
30609
+ uid = `sm-system-map-${nextUid++}`;
30610
+ resize = new ResizeObserver(() => {
30611
+ this.fitLabels();
30612
+ this.placeTip();
30613
+ });
30614
+ model = null;
30615
+ placed = null;
30616
+ background = false;
30617
+ hover = null;
30618
+ highlights = /* @__PURE__ */ new Map();
30619
+ targets = /* @__PURE__ */ new Map();
30620
+ links = /* @__PURE__ */ new Map();
30621
+ shape = "";
30622
+ labels = [];
30623
+ constructor() {
30624
+ this.tip.className = "sm-system-map__tip";
30625
+ this.tipTitle.className = "sm-system-map__tip-title";
30626
+ this.tipSub.className = "sm-system-map__tip-sub";
30627
+ this.tip.append(this.tipTitle, this.tipSub);
30628
+ this.tip.hidden = true;
30629
+ this.root.append(this.svg, this.tip);
30630
+ this.resize.observe(this.root);
30631
+ const scrub = createScrub({
30632
+ onScrub: (x, y) => this.setHover(this.hitAt(x, y)),
30633
+ onEnd: () => this.setHover(null)
30634
+ });
30635
+ this.svg.addEventListener("pointerdown", scrub.onpointerdown);
30636
+ this.svg.addEventListener("pointermove", scrub.onpointermove);
30637
+ this.svg.addEventListener("pointerup", scrub.onpointerup);
30638
+ this.svg.addEventListener("pointercancel", scrub.onpointercancel);
30639
+ this.svg.addEventListener("pointerleave", scrub.onpointerleave);
30640
+ let at = [0, 0];
30641
+ const track = (e) => {
30642
+ at = [e.clientX, e.clientY];
30643
+ };
30644
+ this.svg.addEventListener("pointerdown", track);
30645
+ this.svg.addEventListener("pointerup", track);
30646
+ this.svg.addEventListener("pointermove", (e) => {
30647
+ track(e);
30648
+ if (e.pointerType === "mouse") this.setHover(this.hitAt(e.clientX, e.clientY));
30649
+ });
30650
+ this.svg.addEventListener("pointerleave", (e) => {
30651
+ if (e.pointerType === "mouse") this.setHover(null);
30652
+ });
30653
+ this.svg.addEventListener("click", (e) => {
30654
+ if (e.detail === 0) return;
30655
+ const hit = Math.abs(at[0] - e.clientX) < 1 && Math.abs(at[1] - e.clientY) < 1 ? this.hitAt(...at) : this.hitAt(e.clientX, e.clientY);
30656
+ if (!hit) return;
30657
+ const meant = this.targets.get(JSON.stringify(hit));
30658
+ if (meant?.contains(e.target)) return;
30659
+ e.stopPropagation();
30660
+ e.preventDefault();
30661
+ meant?.dispatchEvent(new MouseEvent("click", {
30662
+ bubbles: true,
30663
+ cancelable: true,
30664
+ ctrlKey: e.ctrlKey,
30665
+ metaKey: e.metaKey,
30666
+ shiftKey: e.shiftKey,
30667
+ altKey: e.altKey,
30668
+ button: e.button
30669
+ }));
30670
+ }, true);
30671
+ }
30672
+ destroy() {
30673
+ this.resize.disconnect();
30674
+ this.root.remove();
30675
+ }
30676
+ update(model, options) {
30677
+ this.model = model;
30678
+ this.placed = layout(model);
30679
+ const links = linksOf(model, this.placed);
30680
+ const shape = JSON.stringify([
30681
+ JSON.stringify(model, (key, value) => key === "href" ? void 0 : value),
30682
+ options,
30683
+ [...links].map(([key, link]) => [
30684
+ key,
30685
+ !!link.href,
30686
+ !!link.onclick
30687
+ ])
30688
+ ]);
30689
+ this.links = links;
30690
+ if (shape === this.shape) {
30691
+ for (const [key, el] of this.targets) {
30692
+ const href = links.get(key)?.href;
30693
+ if (href) el.setAttribute("href", href);
30694
+ }
30695
+ return;
30696
+ }
30697
+ this.shape = shape;
30698
+ this.background = options.variant === "background";
30699
+ this.hover = null;
30700
+ this.root.className = this.background ? "sm-system-map sm-system-map--background" : "sm-system-map";
30701
+ this.draw(model, this.placed, options.ariaLabel);
30702
+ this.fitLabels();
30703
+ this.placeTip();
30704
+ }
30705
+ draw(model, l, ariaLabel) {
30706
+ const bg = this.background;
30707
+ this.highlights = /* @__PURE__ */ new Map();
30708
+ this.targets = /* @__PURE__ */ new Map();
30709
+ this.labels = [];
30710
+ const root = this.svg;
30711
+ root.replaceChildren();
30712
+ root.setAttribute("viewBox", bg && model.backgroundView || `0 0 ${720} ${240}`);
30713
+ root.setAttribute("preserveAspectRatio", bg ? model.backgroundFit === "fit" ? "xMidYMid meet" : model.backgroundView ? "xMinYMid slice" : "xMidYMid slice" : "xMidYMid meet");
30714
+ root.setAttribute("class", "sm-system-map__svg");
30715
+ root.setAttribute("role", "group");
30716
+ root.setAttribute("aria-label", ariaLabel);
30717
+ root.setAttribute("data-vaul-no-drag", "");
30718
+ const stop = (offset, opacity) => svg("stop", {
30719
+ offset,
30720
+ "stop-color": model.primary.color,
30721
+ "stop-opacity": opacity
30722
+ });
30723
+ root.append(svg("defs", {}, svg("radialGradient", {
30724
+ id: `${this.uid}-primary`,
30725
+ gradientUnits: "userSpaceOnUse",
30726
+ cx: l.primaryCx,
30727
+ cy: 120,
30728
+ r: l.primaryR
30729
+ }, stop("0%", 1), stop(Math.max(0, 1 - PRIMARY_RIM / l.primaryR), .85), stop("100%", .35)), svg("pattern", {
30730
+ id: `${this.uid}-band`,
30731
+ width: 9,
30732
+ height: 9,
30733
+ patternUnits: "userSpaceOnUse"
30734
+ }, svg("circle", {
30735
+ cx: 2,
30736
+ cy: 2,
30737
+ r: .9,
30738
+ fill: "currentColor",
30739
+ opacity: .55
30740
+ }), svg("circle", {
30741
+ cx: 6.5,
30742
+ cy: 6,
30743
+ r: .9,
30744
+ fill: "currentColor",
30745
+ opacity: .55
30746
+ }))));
30747
+ if (!bg) this.drawAxis(model, l);
30748
+ if (l.cloudPath) root.append(svg("path", {
30749
+ d: l.cloudPath,
30750
+ stroke: model.cloud?.color,
30751
+ "stroke-width": CLOUD_R * 2,
30752
+ "stroke-linecap": "round",
30753
+ fill: "none",
30754
+ opacity: .45,
30755
+ "pointer-events": "none"
30756
+ }));
30757
+ for (const band of l.bands) {
30758
+ const fill = svg("rect", {
30759
+ x: band.x,
30760
+ y: 26,
30761
+ width: band.width,
30762
+ height: 180,
30763
+ fill: `url(#${this.uid}-band)`,
30764
+ class: `sm-system-map__band sm-system-map__band--${band.tone}`
30765
+ });
30766
+ if (bg) {
30767
+ root.append(fill);
30768
+ continue;
30769
+ }
30770
+ const label = svg("text", {
30771
+ x: band.x + band.width / 2,
30772
+ y: 240 - 8,
30773
+ "text-anchor": "middle",
30774
+ "font-size": LABEL_SIZE,
30775
+ class: `sm-system-map__band-label sm-system-map__band-label--${band.tone}`
30776
+ });
30777
+ label.textContent = band.label;
30778
+ this.labels.push(label);
30779
+ root.append(this.target(band, band.name ?? band.label, {
30780
+ kind: "band",
30781
+ key: band.key
30782
+ }, fill, svg("rect", {
30783
+ x: band.hitX,
30784
+ y: 26,
30785
+ width: band.hitWidth,
30786
+ height: 180,
30787
+ fill: "transparent"
30788
+ }), label));
30789
+ }
30790
+ const disc = svg("circle", {
30791
+ cx: l.primaryCx,
30792
+ cy: 120,
30793
+ r: l.primaryR,
30794
+ fill: `url(#${this.uid}-primary)`
30795
+ });
30796
+ root.append(bg ? disc : this.target(model.primary.link, model.primary.name, { kind: "primary" }, disc));
30797
+ for (const b of l.bodies) {
30798
+ if (!b.rings) continue;
30799
+ const mid = b.r * (b.rings.inner + b.rings.outer) / 2;
30800
+ root.append(svg("ellipse", {
30801
+ cx: b.cx,
30802
+ cy: b.cy,
30803
+ rx: mid,
30804
+ ry: mid * RING_FORESHORTEN,
30805
+ transform: `rotate(${RING_TILT} ${b.cx} ${b.cy})`,
30806
+ fill: "none",
30807
+ stroke: "#d9c89f",
30808
+ "stroke-width": b.r * (b.rings.outer - b.rings.inner),
30809
+ opacity: .45
30810
+ }));
30811
+ }
30812
+ const zoneOf = new Map(l.moonZones.map((z) => [z.parent.id, z]));
30813
+ for (const b of l.bodies) root.append(this.body(b, zoneOf.get(b.id)));
30814
+ for (const z of l.moonZones) if (!z.parent.grouped) this.stack(z);
30815
+ }
30816
+ drawAxis(model, l) {
30817
+ for (const t of model.ticks) {
30818
+ const x = l.xOf(t * model.unitKm);
30819
+ const tick = svg("text", {
30820
+ x,
30821
+ y: 16,
30822
+ "text-anchor": "middle",
30823
+ "font-size": 16,
30824
+ fill: "currentColor",
30825
+ opacity: .85
30826
+ });
30827
+ tick.textContent = String(t);
30828
+ this.svg.append(svg("line", {
30829
+ x1: x,
30830
+ x2: x,
30831
+ y1: 20,
30832
+ y2: 240 - 28,
30833
+ stroke: "currentColor",
30834
+ "stroke-width": .5,
30835
+ opacity: .12
30836
+ }), tick);
30837
+ }
30838
+ const unit = svg("text", {
30839
+ x: 700,
30840
+ y: 16,
30841
+ "text-anchor": "end",
30842
+ "font-size": 14,
30843
+ fill: "currentColor",
30844
+ opacity: .65
30845
+ });
30846
+ unit.textContent = model.axisLabel;
30847
+ this.svg.append(unit);
30848
+ }
30849
+ body(b, zone) {
30850
+ const dot = b.reference ? svg("circle", {
30851
+ cx: b.cx,
30852
+ cy: b.cy,
30853
+ r: b.r + 1,
30854
+ fill: "none",
30855
+ stroke: b.color,
30856
+ "stroke-width": 1.2,
30857
+ class: "sm-system-map__reference"
30858
+ }) : svg("circle", {
30859
+ cx: b.cx,
30860
+ cy: b.cy,
30861
+ r: b.r,
30862
+ fill: b.color
30863
+ });
30864
+ if (this.background) {
30865
+ if (!zone || !b.grouped) return dot;
30866
+ return svg("g", {}, dot, ...this.moonDots(zone));
30867
+ }
30868
+ const hover = {
30869
+ kind: "body",
30870
+ id: b.id
30871
+ };
30872
+ const ring = this.highlight(hover, svg("circle", {
30873
+ cx: b.cx,
30874
+ cy: b.cy,
30875
+ r: b.r + 3.5,
30876
+ fill: "none",
30877
+ stroke: "currentColor",
30878
+ "stroke-width": 1,
30879
+ opacity: .8
30880
+ }));
30881
+ const hit = svg("circle", {
30882
+ cx: b.cx,
30883
+ cy: b.cy,
30884
+ r: b.r + 16,
30885
+ fill: "transparent"
30886
+ });
30887
+ if (!zone || !b.grouped) return this.target(b.link, b.name, hover, ring, dot, hit);
30888
+ return this.target(b.link, b.name, hover, this.highlight(hover, this.zoneWash(zone)), ring, dot, hit, ...this.moonDots(zone), this.zoneHit(zone));
30889
+ }
30890
+ stack(z) {
30891
+ if (this.background) {
30892
+ this.svg.append(...this.moonDots(z));
30893
+ return;
30894
+ }
30895
+ const p = z.parent;
30896
+ if (z.perMoon) {
30897
+ this.svg.append(...this.moonDots(z));
30898
+ for (const mn of z.moons) {
30899
+ const hover = {
30900
+ kind: "moon",
30901
+ parent: p.id,
30902
+ id: mn.id
30903
+ };
30904
+ this.svg.append(this.target(mn.link, mn.name, hover, this.highlight(hover, svg("circle", {
30905
+ cx: mn.cx,
30906
+ cy: mn.cy,
30907
+ r: mn.r + 2.5,
30908
+ fill: "none",
30909
+ stroke: "currentColor",
30910
+ "stroke-width": 1,
30911
+ opacity: .8
30912
+ })), svg("rect", {
30913
+ x: z.x,
30914
+ y: mn.hitY,
30915
+ width: z.width,
30916
+ height: mn.hitHeight,
30917
+ fill: "transparent"
30918
+ })));
30919
+ }
30920
+ return;
30921
+ }
30922
+ const hover = {
30923
+ kind: "stack",
30924
+ id: p.id
30925
+ };
30926
+ const first = z.moons[0];
30927
+ this.svg.append(this.highlight(hover, this.zoneWash(z)), ...this.moonDots(z), this.target(p.satellitesLink, p.satellitesTab ? `${p.name} · ${this.model.text.moons(p.satelliteCount ?? z.moons.length)}` : first.name, hover, this.zoneHit(z)));
30928
+ }
30929
+ moonDots(z) {
30930
+ return z.moons.map((mn) => svg("circle", {
30931
+ cx: mn.cx,
30932
+ cy: mn.cy,
30933
+ r: mn.r,
30934
+ fill: mn.color
30935
+ }));
30936
+ }
30937
+ zoneWash(z) {
30938
+ return svg("rect", {
30939
+ x: z.x,
30940
+ y: z.y,
30941
+ width: z.width,
30942
+ height: z.height,
30943
+ rx: 4,
30944
+ fill: "currentColor",
30945
+ opacity: .1,
30946
+ "pointer-events": "none"
30947
+ });
30948
+ }
30949
+ zoneHit(z) {
30950
+ return svg("rect", {
30951
+ x: z.x,
30952
+ y: z.y,
30953
+ width: z.width,
30954
+ height: z.height,
30955
+ fill: "transparent"
30956
+ });
30957
+ }
30958
+ highlight(hover, el) {
30959
+ const key = JSON.stringify(hover);
30960
+ const list = this.highlights.get(key);
30961
+ if (list) list.push(el);
30962
+ else this.highlights.set(key, [el]);
30963
+ el.style.display = "none";
30964
+ return el;
30965
+ }
30966
+ target(link, label, hover, ...children) {
30967
+ const linked = !!(link?.href || link?.onclick);
30968
+ const el = link?.href ? svg("a", {
30969
+ href: link.href,
30970
+ "aria-label": label
30971
+ }) : linked ? svg("g", {
30972
+ role: "button",
30973
+ tabindex: 0,
30974
+ "aria-label": label,
30975
+ class: "sm-system-map__button"
30976
+ }) : svg("g");
30977
+ el.append(...children);
30978
+ if (!linked) return el;
30979
+ const key = JSON.stringify(hover);
30980
+ this.targets.set(key, el);
30981
+ el.addEventListener("focus", () => this.setHover(hover));
30982
+ el.addEventListener("blur", () => {
30983
+ if (sameHover(this.hover, hover)) this.setHover(null);
30984
+ });
30985
+ el.addEventListener("click", (e) => this.links.get(key)?.onclick?.(e));
30986
+ if (!link?.href) el.addEventListener("keydown", (e) => {
30987
+ const key = e.key;
30988
+ if (key !== "Enter" && key !== " ") return;
30989
+ e.preventDefault();
30990
+ el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
30991
+ });
30992
+ return el;
30993
+ }
30994
+ setHover(hover) {
30995
+ if (sameHover(this.hover, hover)) return;
30996
+ for (const el of this.highlights.get(JSON.stringify(this.hover)) ?? []) el.style.display = "none";
30997
+ this.targets.get(JSON.stringify(this.hover))?.removeAttribute("data-near");
30998
+ this.hover = hover;
30999
+ for (const el of this.highlights.get(JSON.stringify(hover)) ?? []) el.style.display = "";
31000
+ const near = this.targets.get(JSON.stringify(hover));
31001
+ near?.setAttribute("data-near", "");
31002
+ this.svg.classList.toggle("sm-system-map__svg--near", !!near);
31003
+ this.placeTip();
31004
+ }
31005
+ hitAt(clientX, clientY) {
31006
+ if (!this.placed || this.background) return null;
31007
+ const rect = this.svg.getBoundingClientRect();
31008
+ if (!rect.width || !rect.height) return null;
31009
+ const vx = (clientX - rect.left) / rect.width * 720;
31010
+ const vy = (clientY - rect.top) / rect.height * 240;
31011
+ return hitTest(this.placed, vx, vy);
31012
+ }
31013
+ fitLabels() {
31014
+ for (const label of this.labels) {
31015
+ let width = 0;
31016
+ label.setAttribute("font-size", String(LABEL_SIZE));
31017
+ try {
31018
+ width = label.getComputedTextLength();
31019
+ } catch {}
31020
+ if (!width) continue;
31021
+ if (width > LABEL_MAX_W) {
31022
+ const size = Math.max(LABEL_MIN_SIZE, LABEL_SIZE * LABEL_MAX_W / width);
31023
+ label.setAttribute("font-size", String(size));
31024
+ width *= size / LABEL_SIZE;
31025
+ }
31026
+ const centre = Number(label.dataset.centre ?? label.getAttribute("x"));
31027
+ label.dataset.centre = String(centre);
31028
+ const half = width / 2;
31029
+ const x = Math.min(Math.max(centre, half + LABEL_MARGIN), Math.max(half, 720 - half - LABEL_MARGIN));
31030
+ label.setAttribute("x", String(x));
31031
+ }
31032
+ }
31033
+ placeTip() {
31034
+ const tip = this.model && this.placed && !this.background ? tipFor(this.model, this.placed, this.hover) : null;
31035
+ this.tip.hidden = !tip;
31036
+ if (!tip) return;
31037
+ this.tipTitle.textContent = tip.title;
31038
+ this.tipSub.textContent = tip.sub ? ` · ${tip.sub}` : "";
31039
+ const containerW = this.root.clientWidth;
31040
+ const scale = containerW / 720;
31041
+ this.tip.style.maxWidth = `${Math.max(0, containerW - 2 * TIP_MARGIN)}px`;
31042
+ const half = this.tip.offsetWidth / 2;
31043
+ const left = Math.min(Math.max(tip.cx * scale, half + TIP_MARGIN), Math.max(half, containerW - half - TIP_MARGIN));
31044
+ this.tip.style.left = `${left}px`;
31045
+ this.tip.style.top = `${Math.max(4, tip.cy * scale - TIP_H)}px`;
31046
+ }
31047
+ };
31048
+
31049
+ //#endregion
31050
+ //#region src/lib/systemmap/planetary.ts
31051
+ function systemFromMapEntry(entry, name) {
31052
+ return {
31053
+ planetId: entry.primary.id,
31054
+ planetName: name,
31055
+ planetRadiusKm: entry.primary.radius_km,
31056
+ planetColor: BODY_COLORS[entry.primary.id] ?? "#cccccc",
31057
+ moons: entry.moons.map((mn) => ({
31058
+ id: mn.id,
31059
+ name: mn.id,
31060
+ aRp: mn.a_rp,
31061
+ tiltDeg: mn.tilt_deg,
31062
+ radiusKm: mn.radius_km,
31063
+ color: BODY_COLORS[mn.id] ?? mn.color ?? "#cccccc"
31064
+ })),
31065
+ rings: entry.rings ? {
31066
+ innerRp: entry.rings.inner_rp,
31067
+ outerRp: entry.rings.outer_rp
31068
+ } : null,
31069
+ moonCount: entry.moon_count
31070
+ };
31071
+ }
31072
+ var TOP_MOON_R = 14;
31073
+ var AT_SURFACE = 1.001;
31074
+ var PX_PER_DEG = .62;
31075
+ var TICK_LADDER = [
31076
+ 1,
31077
+ 2,
31078
+ 3,
31079
+ 5,
31080
+ 10,
31081
+ 20,
31082
+ 30,
31083
+ 50,
31084
+ 100,
31085
+ 200,
31086
+ 300,
31087
+ 500,
31088
+ 1e3,
31089
+ 2e3,
31090
+ 5e3
31091
+ ];
31092
+ var BG_VIEW = "0 96 320 144";
31093
+ var BG_VIEW_WIDE = "0 96 720 144";
31094
+ var EMPTY_DOMAIN = [1, 100];
31095
+ function domainOf(system, bands) {
31096
+ let lo = Math.min(...system.moons.map((mn) => mn.aRp));
31097
+ let hi = Math.max(...system.moons.map((mn) => mn.aRp));
31098
+ for (const b of bands) {
31099
+ lo = Math.min(lo, b.innerKm / system.planetRadiusKm);
31100
+ hi = Math.max(hi, b.outerKm / system.planetRadiusKm);
31101
+ }
31102
+ if (!Number.isFinite(lo) || !Number.isFinite(hi)) return EMPTY_DOMAIN;
31103
+ if (hi <= lo) {
31104
+ lo /= 2;
31105
+ hi *= 2;
31106
+ }
31107
+ lo = lo <= AT_SURFACE ? 1 : lo;
31108
+ return [lo > 1 ? 10 ** (Math.log10(lo) - .12) : 1, 10 ** (Math.log10(hi) + .12)];
31109
+ }
31110
+ function planetarySystemModel(system, parts) {
31111
+ const rKm = system.planetRadiusKm;
31112
+ const domain = domainOf(system, parts.bands);
31113
+ return {
31114
+ primary: {
31115
+ id: system.planetId,
31116
+ name: system.planetName,
31117
+ radiusKm: rKm,
31118
+ color: system.planetColor,
31119
+ link: parts.primaryLink
31120
+ },
31121
+ bodies: system.moons.map((mn) => ({
31122
+ id: mn.id,
31123
+ name: mn.name,
31124
+ aKm: mn.aRp * rKm,
31125
+ tiltDeg: mn.tiltDeg,
31126
+ radiusKm: mn.radiusKm,
31127
+ color: mn.color,
31128
+ link: parts.moonLink?.(mn)
31129
+ })),
31130
+ bands: parts.bands,
31131
+ cloud: parts.cloud,
31132
+ unitKm: rKm,
31133
+ domain,
31134
+ ticks: TICK_LADDER.filter((t) => t >= domain[0] && t <= domain[1]),
31135
+ axisLabel: parts.axisLabel,
31136
+ pxPerKm: TOP_MOON_R / Math.max(...system.moons.map((mn) => mn.radiusKm).filter((r) => r > 0), 1),
31137
+ pxPerDeg: PX_PER_DEG,
31138
+ text: parts.text,
31139
+ backgroundView: parts.wide ? BG_VIEW_WIDE : BG_VIEW,
31140
+ backgroundFit: parts.wide ? "fit" : "slice"
31141
+ };
31142
+ }
31143
+
31144
+ //#endregion
31145
+ //#region src/lib/systemmap/solar.ts
31146
+ var SOLAR_PX_PER_KM = 224e-6;
31147
+ var SOLAR_PX_PER_DEG = 1.8;
31148
+ var DOMAIN = [.3, 50];
31149
+ var TICKS = [
31150
+ .3,
31151
+ 1,
31152
+ 3,
31153
+ 10,
31154
+ 30
31155
+ ];
31156
+ var SUN_COLOR = "#ffdd44";
31157
+ function displayName(name) {
31158
+ const stripped = name.replace(/^\d+\s+/, "");
31159
+ if (stripped === stripped.toUpperCase() && /[A-Z]/.test(stripped)) return stripped.charAt(0) + stripped.slice(1).toLowerCase();
31160
+ return stripped;
31161
+ }
31162
+ function solarColor(o) {
31163
+ return o.color || BODY_COLORS[o.id] || "#cccccc";
31164
+ }
31165
+ function fileStacks(file, name) {
31166
+ const stacks = /* @__PURE__ */ new Map();
31167
+ const planets = new Map(file.objects.map((o) => [o.id, o]));
31168
+ for (const o of file.objects) {
31169
+ if (o.kind !== "moon" || !o.parent) continue;
31170
+ const stack = stacks.get(o.parent) ?? {
31171
+ satellites: [],
31172
+ count: planets.get(o.parent)?.moon_count,
31173
+ tab: false
31174
+ };
31175
+ stack.satellites.push({
31176
+ id: o.id,
31177
+ name: name(o),
31178
+ radiusKm: o.diameter_km / 2,
31179
+ color: solarColor(o)
31180
+ });
31181
+ stack.tab ||= !!o.link_parent;
31182
+ stacks.set(o.parent, stack);
31183
+ }
31184
+ return stacks;
31185
+ }
31186
+ function solarSystemModel(file, parts) {
31187
+ const sun = file.objects.find((o) => o.kind === "star");
31188
+ if (!sun) return null;
31189
+ const listed = fileStacks(file, parts.name);
31190
+ const stackOf = parts.stack ?? ((o) => listed.get(o.id));
31191
+ const bodies = file.objects.filter((o) => o.kind !== "moon" && o.kind !== "star" && (parts.keep?.(o) ?? true)).map((o) => {
31192
+ const stack = stackOf(o);
31193
+ return {
31194
+ id: o.id,
31195
+ name: parts.name(o),
31196
+ aKm: o.a * AU_KM,
31197
+ tiltDeg: o.i,
31198
+ radiusKm: o.diameter_km / 2,
31199
+ color: solarColor(o),
31200
+ rings: o.rings,
31201
+ satellites: stack?.satellites,
31202
+ satelliteCount: stack?.count,
31203
+ satellitesTab: stack?.tab ?? false,
31204
+ ...parts.links(o, stack)
31205
+ };
31206
+ });
31207
+ return {
31208
+ primary: {
31209
+ id: sun.id,
31210
+ name: parts.name(sun),
31211
+ radiusKm: sun.diameter_km / 2,
31212
+ color: SUN_COLOR,
31213
+ link: parts.primaryLink
31214
+ },
31215
+ bodies,
31216
+ bands: file.belts.map((b) => ({
31217
+ key: b.slug,
31218
+ innerKm: b.inner_au * AU_KM,
31219
+ outerKm: b.outer_au * AU_KM,
31220
+ tone: b.kind === "kuiper_belt" ? "sky" : "muted",
31221
+ ...parts.band(b)
31222
+ })),
31223
+ unitKm: AU_KM,
31224
+ domain: DOMAIN,
31225
+ ticks: TICKS,
31226
+ axisLabel: parts.text.axisAu,
31227
+ pxPerKm: SOLAR_PX_PER_KM,
31228
+ pxPerDeg: SOLAR_PX_PER_DEG,
31229
+ text: parts.text
31230
+ };
31231
+ }
31232
+
31233
+ //#endregion
31234
+ //#region src/lib/systemmap/views.ts
31235
+ function extraFromPlace(place) {
31236
+ return {
31237
+ id: place.id,
31238
+ name: place.name ?? place.id,
31239
+ moon: place.moon ?? false,
31240
+ aAu: place.aAu ?? 0,
31241
+ tiltDeg: place.tiltDeg ?? 0,
31242
+ radiusKm: place.radiusKm ?? 0,
31243
+ color: place.color ?? BODY_COLORS[place.id] ?? "#cccccc",
31244
+ parent: place.parent
31245
+ };
31246
+ }
31247
+ var MAX_STACK = 4;
31248
+ var ZONE_TOP_R = 10;
31249
+ var ZONE_DOMAIN = {
31250
+ inner: [.6, 5.5],
31251
+ outer: [4.5, 55]
31252
+ };
31253
+ var ZONE_TICKS = [
31254
+ .1,
31255
+ .2,
31256
+ .3,
31257
+ .5,
31258
+ 1,
31259
+ 2,
31260
+ 3,
31261
+ 5,
31262
+ 10,
31263
+ 20,
31264
+ 30,
31265
+ 50,
31266
+ 100,
31267
+ 200,
31268
+ 500,
31269
+ 1e3
31270
+ ];
31271
+ var LAST_AU_TICK_AT = .9;
31272
+ var LAST_RADII_TICK_AT = .76;
31273
+ function tickAt(tick, domain) {
31274
+ return Math.log(tick / domain[0]) / Math.log(domain[1] / domain[0]);
31275
+ }
31276
+ var JUPITER_ID = "naif-599";
31277
+ function unplacedIds(solar, systems, bodies, placed = []) {
31278
+ if (!bodies) return [];
31279
+ const known = new Set([...solar.objects.map((o) => o.id), ...placed]);
31280
+ for (const entry of Object.values(systems)) {
31281
+ known.add(entry.primary.id);
31282
+ for (const moon of entry.moons) known.add(moon.id);
31283
+ }
31284
+ return [...bodies].filter((id) => !known.has(id));
31285
+ }
31286
+ function extraFromBundle(global) {
31287
+ const moon = global.type === "moon";
31288
+ const parent = global.orbit?.parent_id;
31289
+ const a = parent === void 0 || parent === "naif-10" || parent === "naif-0" ? global.orbit?.a ?? global.orbit?.q : void 0;
31290
+ if (a === void 0 && !moon) return null;
31291
+ const radii = global.radii;
31292
+ return {
31293
+ id: global.id,
31294
+ name: displayName(global.name ?? global.id),
31295
+ moon,
31296
+ aAu: a ?? 0,
31297
+ tiltDeg: global.orbit?.i ?? 0,
31298
+ radiusKm: radii ? (radii.a + radii.b + radii.c) / 3 : (global.sbdb?.diameter ?? 0) / 2,
31299
+ color: BODY_COLORS[global.id] ?? global.sbdb?.color ?? global.color ?? "#cccccc",
31300
+ parent: global.orbit?.parent_id
31301
+ };
31302
+ }
31303
+ var ZONE_CUT = .92;
31304
+ function zoneOf(aAu, jupiterAu) {
31305
+ return aAu < jupiterAu * ZONE_CUT ? "inner" : "outer";
31306
+ }
31307
+ function systemEntry(systems, primaryId) {
31308
+ return Object.values(systems).find((entry) => entry.primary.id === primaryId);
31309
+ }
31310
+ function targetKey(target) {
31311
+ return target.kind === "zone" ? `zone:${target.zone}` : `${target.kind}:${target.id}`;
31312
+ }
31313
+ var Scope = class {
31314
+ targets = /* @__PURE__ */ new Map();
31315
+ constructor(data, options, pick) {
31316
+ this.data = data;
31317
+ this.options = options;
31318
+ this.pick = pick;
31319
+ }
31320
+ has(id) {
31321
+ return !this.options.bodies || this.options.bodies.has(id);
31322
+ }
31323
+ name(id, exported) {
31324
+ return this.options.names[id] ?? (exported ? displayName(exported) : id);
31325
+ }
31326
+ named(id, name) {
31327
+ return this.options.names[id] ?? name;
31328
+ }
31329
+ link(target) {
31330
+ this.targets.set(targetKey(target), target);
31331
+ return { onclick: () => this.pick(target) };
31332
+ }
31333
+ body(id, name) {
31334
+ return this.has(id) ? this.link({
31335
+ kind: "body",
31336
+ id,
31337
+ name
31338
+ }) : void 0;
31339
+ }
31340
+ stack(primaryId, listed) {
31341
+ if (listed && !this.options.bodies) return listed;
31342
+ const moons = (systemEntry(this.data.systems, primaryId)?.moons ?? []).filter((moon) => this.has(moon.id)).map((moon) => ({
31343
+ id: moon.id,
31344
+ name: this.name(moon.id),
31345
+ radiusKm: moon.radius_km,
31346
+ color: BODY_COLORS[moon.id] ?? moon.color ?? "#cccccc"
31347
+ }));
31348
+ if (!moons.length) return void 0;
31349
+ moons.sort((a, b) => b.radiusKm - a.radiusKm);
31350
+ return {
31351
+ satellites: moons.slice(0, MAX_STACK),
31352
+ count: moons.length,
31353
+ tab: true
31354
+ };
31355
+ }
31356
+ links(id, name, stack) {
31357
+ if (!stack) return { link: this.body(id, name) };
31358
+ const system = () => this.link({
31359
+ kind: "system",
31360
+ id,
31361
+ name
31362
+ });
31363
+ if (this.options.grouping === "systems") return {
31364
+ link: system(),
31365
+ grouped: true
31366
+ };
31367
+ const largest = stack.satellites[0];
31368
+ return {
31369
+ link: this.body(id, name),
31370
+ satellitesLink: stack.tab ? system() : this.body(largest.id, largest.name)
31371
+ };
31372
+ }
31373
+ built(model, label) {
31374
+ return {
31375
+ model,
31376
+ targets: [...this.targets.values()],
31377
+ label
31378
+ };
31379
+ }
31380
+ };
31381
+ function buildSolarView(data, options, text, pick) {
31382
+ const scope = new Scope(data, options, pick);
31383
+ const name = (o) => scope.name(o.id, o.name);
31384
+ const listed = fileStacks(data.solar, name);
31385
+ const stacks = new Map(data.solar.objects.map((o) => [o.id, scope.stack(o.id, listed.get(o.id))]));
31386
+ const sun = data.solar.objects.find((o) => o.kind === "star");
31387
+ const zoneBand = (zone) => {
31388
+ const label = zone === "inner" ? text.zoneInner : text.zoneOuter;
31389
+ return {
31390
+ label,
31391
+ sub: "",
31392
+ ...scope.link({
31393
+ kind: "zone",
31394
+ zone,
31395
+ name: label
31396
+ })
31397
+ };
31398
+ };
31399
+ const model = solarSystemModel(data.solar, {
31400
+ text,
31401
+ name,
31402
+ primaryLink: sun && scope.body(sun.id, name(sun)),
31403
+ keep: (o) => scope.has(o.id) || !!stacks.get(o.id),
31404
+ stack: (o) => stacks.get(o.id),
31405
+ links: (o, stack) => scope.links(o.id, name(o), stack),
31406
+ band: (belt) => options.zones === "belts" ? { label: belt.label } : zoneBand(belt.kind === "kuiper_belt" ? "outer" : "inner")
31407
+ });
31408
+ if (!model) throw new Error("spacemap: the Solar System map names no Sun");
31409
+ return scope.built(model, text.solarSystem);
31410
+ }
31411
+ function buildSystemView(data, primaryId, exported, options, text, pick) {
31412
+ const entry = systemEntry(data.systems, primaryId);
31413
+ if (!entry) throw new Error(`spacemap: no planetary system around ${primaryId}`);
31414
+ const scope = new Scope(data, options, pick);
31415
+ const system = systemFromMapEntry(entry, scope.name(primaryId, exported[primaryId]));
31416
+ system.moons = system.moons.filter((moon) => scope.has(moon.id)).map((moon) => ({
31417
+ ...moon,
31418
+ name: scope.name(moon.id, exported[moon.id])
31419
+ }));
31420
+ const rKm = system.planetRadiusKm;
31421
+ const bands = system.rings ? [{
31422
+ key: "rings",
31423
+ label: text.rings,
31424
+ innerKm: system.rings.innerRp * rKm,
31425
+ outerKm: system.rings.outerRp * rKm,
31426
+ tone: "amber"
31427
+ }] : [];
31428
+ const model = planetarySystemModel(system, {
31429
+ text,
31430
+ axisLabel: text.axisRadii,
31431
+ bands,
31432
+ primaryLink: scope.body(primaryId, system.planetName),
31433
+ moonLink: (moon) => scope.body(moon.id, moon.name)
31434
+ });
31435
+ model.ticks = model.ticks.filter((t) => tickAt(t, model.domain) < LAST_RADII_TICK_AT);
31436
+ return scope.built(model, system.planetName);
31437
+ }
31438
+ function buildZoneView(data, zone, options, text, pick) {
31439
+ const scope = new Scope(data, options, pick);
31440
+ const { solar, extras } = data;
31441
+ const sun = solar.objects.find((o) => o.kind === "star");
31442
+ const jupiterAu = solar.objects.find((o) => o.id === JUPITER_ID)?.a;
31443
+ if (!sun || jupiterAu === void 0) throw new Error("spacemap: the Solar System map names no Sun or no Jupiter");
31444
+ const small = solar.objects.filter((o) => o.kind === "dwarf" || o.kind === "asteroid").map((o) => ({
31445
+ id: o.id,
31446
+ name: scope.name(o.id, o.name),
31447
+ aAu: o.a,
31448
+ tiltDeg: o.i,
31449
+ radiusKm: o.diameter_km / 2,
31450
+ color: solarColor(o)
31451
+ }));
31452
+ for (const extra of extras) {
31453
+ if (extra.moon || small.some((s) => s.id === extra.id)) continue;
31454
+ if (!(extra.aAu > 0)) continue;
31455
+ small.push({
31456
+ ...extra,
31457
+ name: scope.named(extra.id, extra.name)
31458
+ });
31459
+ }
31460
+ const bodies = [];
31461
+ for (const s of small) {
31462
+ if (zoneOf(s.aAu, jupiterAu) !== zone) continue;
31463
+ const base = {
31464
+ ...s,
31465
+ aKm: s.aAu * AU_KM
31466
+ };
31467
+ const stack = scope.stack(s.id);
31468
+ if (stack) {
31469
+ bodies.push({
31470
+ ...base,
31471
+ satellites: stack.satellites,
31472
+ satelliteCount: stack.count,
31473
+ satellitesTab: true,
31474
+ ...scope.links(s.id, s.name, stack)
31475
+ });
31476
+ continue;
31477
+ }
31478
+ const moons = extras.filter((extra) => extra.moon && extra.parent === s.id && scope.has(extra.id)).map((extra) => {
31479
+ const name = scope.named(extra.id, extra.name);
31480
+ return {
31481
+ ...extra,
31482
+ name,
31483
+ link: scope.body(extra.id, name)
31484
+ };
31485
+ });
31486
+ if (!scope.has(s.id) && !moons.length) continue;
31487
+ bodies.push({
31488
+ ...base,
31489
+ link: scope.body(s.id, s.name),
31490
+ satellites: moons.length ? moons : void 0
31491
+ });
31492
+ }
31493
+ const [lo, hi] = ZONE_DOMAIN[zone];
31494
+ const domain = [Math.min(lo, ...bodies.map((b) => b.aKm / AU_KM * .85)), Math.max(hi, ...bodies.map((b) => b.aKm / AU_KM * 1.15))];
31495
+ const within = (au) => au >= domain[0] && au <= domain[1];
31496
+ const pxPerKm = ZONE_TOP_R / Math.max(...bodies.map((b) => b.radiusKm), 1);
31497
+ for (const o of solar.objects) {
31498
+ if (o.kind !== "planet" || !within(o.a)) continue;
31499
+ bodies.push({
31500
+ id: o.id,
31501
+ name: scope.name(o.id, o.name),
31502
+ aKm: o.a * AU_KM,
31503
+ tiltDeg: 0,
31504
+ radiusKm: 0,
31505
+ color: solarColor(o),
31506
+ reference: true
31507
+ });
31508
+ }
31509
+ const label = zone === "inner" ? text.zoneInner : text.zoneOuter;
31510
+ return scope.built({
31511
+ primary: {
31512
+ id: sun.id,
31513
+ name: scope.name(sun.id, sun.name),
31514
+ radiusKm: sun.diameter_km / 2 * SOLAR_PX_PER_KM / pxPerKm,
31515
+ color: SUN_COLOR
31516
+ },
31517
+ bodies,
31518
+ bands: solar.belts.filter((belt) => within(belt.inner_au) && within(belt.outer_au)).map((belt) => ({
31519
+ key: belt.slug,
31520
+ label: belt.label,
31521
+ innerKm: belt.inner_au * AU_KM,
31522
+ outerKm: belt.outer_au * AU_KM,
31523
+ tone: belt.kind === "kuiper_belt" ? "sky" : "muted"
31524
+ })),
31525
+ unitKm: AU_KM,
31526
+ domain,
31527
+ ticks: ZONE_TICKS.filter((t) => within(t) && tickAt(t, domain) < LAST_AU_TICK_AT),
31528
+ axisLabel: text.axisAu,
31529
+ pxPerKm,
31530
+ pxPerDeg: SOLAR_PX_PER_DEG,
31531
+ text
31532
+ }, label);
31533
+ }
31534
+
31535
+ //#endregion
31536
+ //#region src/lib/systemmap/system-map.ts
31537
+ var onceTarget = /* @__PURE__ */ new WeakMap();
31538
+ var AU_TO_KM_THRESHOLD = .01;
31539
+ function viewText() {
31540
+ const { messages, locale } = host();
31541
+ const number = new Intl.NumberFormat(locale(), { maximumSignificantDigits: 3 });
31542
+ const kilometres = new Intl.NumberFormat(locale(), {
31543
+ style: "unit",
31544
+ unit: "kilometer",
31545
+ maximumSignificantDigits: 3
31546
+ });
31547
+ const au = messages.unit_symbol_astronomical_unit();
31548
+ return {
31549
+ primary: messages.planetary_system_primary(),
31550
+ retrograde: messages.planetary_system_retrograde(),
31551
+ moons: (count) => messages.system_map_moons({ count }),
31552
+ distance: (km) => {
31553
+ const distance = km / AU_KM;
31554
+ return distance > 0 && distance < AU_TO_KM_THRESHOLD ? kilometres.format(km) : `${number.format(distance)} ${au}`;
31555
+ },
31556
+ axisAu: messages.system_map_axis_au(),
31557
+ solarSystem: messages.system_map_solar_system(),
31558
+ rings: messages.tab_rings(),
31559
+ axisRadii: messages.planetary_system_axis_unit(),
31560
+ zoneInner: messages.system_map_zone_inner(),
31561
+ zoneOuter: messages.system_map_zone_outer()
31562
+ };
31563
+ }
31564
+ var SystemMap = class {
31565
+ chart = new SystemMapChart();
31566
+ listeners = /* @__PURE__ */ new Map();
31567
+ view;
31568
+ bodies;
31569
+ names;
31570
+ grouping;
31571
+ zones;
31572
+ places;
31573
+ targets = [];
31574
+ extras = /* @__PURE__ */ new Map();
31575
+ queue = Promise.resolve();
31576
+ disposed = false;
31577
+ constructor(options = {}) {
31578
+ this.view = options.view ?? { kind: "solar-system" };
31579
+ this.bodies = options.bodies ? new Set(options.bodies) : null;
31580
+ this.names = options.names ?? {};
31581
+ this.grouping = options.grouping ?? "bodies";
31582
+ this.zones = options.zones ?? "belts";
31583
+ this.places = (options.places ?? []).map(extraFromPlace);
31584
+ }
31585
+ mount(container) {
31586
+ container.append(this.chart.root);
31587
+ }
31588
+ remove() {
31589
+ this.disposed = true;
31590
+ this.chart.destroy();
31591
+ }
31592
+ load() {
31593
+ return this.draw();
31594
+ }
31595
+ async setView(view) {
31596
+ await this.draw(view);
31597
+ }
31598
+ getView() {
31599
+ return this.view;
31600
+ }
31601
+ setBodies(ids) {
31602
+ const bodies = ids ? new Set(ids) : null;
31603
+ return this.draw(void 0, bodies).catch(() => {});
31604
+ }
31605
+ getTargets() {
31606
+ return [...this.targets];
31607
+ }
31608
+ draw(view, bodies) {
31609
+ const turn = this.queue.then(() => {
31610
+ if (bodies !== void 0) this.bodies = bodies;
31611
+ return this.drawNow(view ?? this.view);
31612
+ });
31613
+ this.queue = turn.catch(() => {});
31614
+ return turn;
31615
+ }
31616
+ async drawNow(view) {
31617
+ let built;
31618
+ try {
31619
+ built = await this.build(view);
31620
+ } catch (cause) {
31621
+ const error = cause instanceof Error ? cause : new Error(String(cause));
31622
+ if (!this.disposed) this.emit("error", error);
31623
+ throw error;
31624
+ }
31625
+ if (this.disposed) return;
31626
+ const changed = JSON.stringify(view) !== JSON.stringify(this.view);
31627
+ this.view = view;
31628
+ this.targets = built.targets;
31629
+ this.chart.update(built.model, { ariaLabel: built.label });
31630
+ if (changed) this.emit("viewchange", view);
31631
+ }
31632
+ async build(view) {
31633
+ const [solar, systems] = await Promise.all([fetchSolarSystemMap(), fetchPlanetarySystemsMap()]);
31634
+ const options = {
31635
+ bodies: this.bodies,
31636
+ names: this.names,
31637
+ grouping: this.grouping,
31638
+ zones: this.zones
31639
+ };
31640
+ const text = viewText();
31641
+ const pick = (target) => this.emit("select", target);
31642
+ const data = {
31643
+ solar,
31644
+ systems,
31645
+ extras: []
31646
+ };
31647
+ switch (view.kind) {
31648
+ case "solar-system": return buildSolarView(data, options, text, pick);
31649
+ case "system": return buildSystemView(data, view.id, await this.exportedNames(view.id, systems), options, text, pick);
31650
+ case "zone": {
31651
+ const ids = unplacedIds(solar, systems, this.bodies, this.places.map((place) => place.id));
31652
+ const read = (await Promise.all(ids.map((id) => this.extra(id)))).filter((extra) => extra !== null);
31653
+ const placed = [...this.places, ...read];
31654
+ const parents = unplacedIds(solar, systems, new Set(placed.flatMap((extra) => extra.moon && extra.parent ? [extra.parent] : [])), placed.map((extra) => extra.id));
31655
+ const hosts = await Promise.all(parents.map((id) => this.extra(id)));
31656
+ const extras = [...placed, ...hosts.filter((extra) => extra !== null)];
31657
+ return buildZoneView({
31658
+ ...data,
31659
+ extras
31660
+ }, view.zone, options, text, pick);
31661
+ }
31662
+ }
31663
+ }
31664
+ extra(id) {
31665
+ let pending = this.extras.get(id);
31666
+ if (!pending) {
31667
+ pending = fetchObjectDetail(id, false).then((detail) => {
31668
+ if (!detail.global) throw new Error(`spacemap: no such body as ${id}`);
31669
+ return extraFromBundle(detail.global);
31670
+ }).catch((cause) => {
31671
+ this.extras.delete(id);
31672
+ this.emit("error", cause instanceof Error ? cause : new Error(String(cause)));
31673
+ return null;
31674
+ });
31675
+ this.extras.set(id, pending);
31676
+ }
31677
+ return pending;
31678
+ }
31679
+ async exportedNames(primaryId, systems) {
31680
+ if (![primaryId, ...(systemEntry(systems, primaryId)?.moons ?? []).map((m) => m.id)].some((id) => !(id in this.names) && (id === primaryId || !this.bodies || this.bodies.has(id)))) return {};
31681
+ try {
31682
+ const { global, localized } = await fetchObjectDetail(primaryId, true, host().locale());
31683
+ const names = {};
31684
+ for (const moon of global?.notable_moons ?? []) if (moon.id) names[moon.id] = moon.name;
31685
+ Object.assign(names, localized?.notable_moon_names);
31686
+ const name = localized?.name ?? global?.name;
31687
+ if (name) names[primaryId] = name;
31688
+ return names;
31689
+ } catch {
31690
+ return {};
31691
+ }
31692
+ }
31693
+ on(event, listener) {
31694
+ let set = this.listeners.get(event);
31695
+ if (!set) {
31696
+ set = /* @__PURE__ */ new Set();
31697
+ this.listeners.set(event, set);
31698
+ }
31699
+ set.add(listener);
31700
+ return () => set.delete(listener);
31701
+ }
31702
+ once(event, listener) {
31703
+ const wrapped = ((...args) => {
31704
+ off();
31705
+ listener(...args);
31706
+ });
31707
+ onceTarget.set(wrapped, listener);
31708
+ const off = this.on(event, wrapped);
31709
+ return off;
31710
+ }
31711
+ off(event, listener) {
31712
+ const set = this.listeners.get(event);
31713
+ if (!set) return;
31714
+ set.delete(listener);
31715
+ for (const held of set) if (onceTarget.get(held) === listener) set.delete(held);
31716
+ }
31717
+ emit(event, ...args) {
31718
+ const set = this.listeners.get(event);
31719
+ if (!set) return;
31720
+ for (const listener of [...set]) listener(...args);
31721
+ }
31722
+ };
31723
+
30051
31724
  //#endregion
30052
31725
  //#region src/lib/scene/state/attribution.ts
30053
31726
  var DEFAULT_ORBIT_LABELS = Object.fromEntries(ORBIT_SOURCE_ORDER.map((s) => [s, ORBIT_SOURCES[s].label]));
@@ -30308,10 +31981,16 @@ function kmToAu(km) {
30308
31981
 
30309
31982
  //#endregion
30310
31983
  //#region src/lib/fetch/panoramas.ts
30311
- async function fetchPanoramaIndex(fetcher = fetchWithTimeout) {
30312
- const res = await fetcher(`${dataBase()}/v1/panoramas.json`);
30313
- if (!res.ok) throw new Error(`Failed to load panoramas.json: ${res.status}`);
30314
- return (await res.json()).bodies ?? [];
31984
+ var index = null;
31985
+ function fetchPanoramaIndex(fetcher = fetchWithTimeout) {
31986
+ if (index) return index;
31987
+ index = (async () => {
31988
+ const res = await fetcher(`${dataBase()}/v1/panoramas.json`);
31989
+ if (!res.ok) throw new Error(`Failed to load panoramas.json: ${res.status}`);
31990
+ return (await res.json()).bodies ?? [];
31991
+ })();
31992
+ index.catch(() => index = null);
31993
+ return index;
30315
31994
  }
30316
31995
  async function fetchPanoramas(bodyId) {
30317
31996
  return (await fetchObjectDetail(bodyId, false)).global?.panoramas ?? [];
@@ -30382,6 +32061,27 @@ async function createPanorama(options) {
30382
32061
  await view.load();
30383
32062
  return view;
30384
32063
  }
32064
+ async function createSystemMap(options) {
32065
+ const { container, dataUrl, imagesUrl, locale, messages, includeNonCommercial, events, ...rest } = options;
32066
+ const element = resolveContainer(container);
32067
+ applyHost({
32068
+ dataUrl,
32069
+ imagesUrl,
32070
+ locale,
32071
+ messages,
32072
+ includeNonCommercial
32073
+ });
32074
+ const map = new SystemMap(rest);
32075
+ map.mount(element);
32076
+ for (const [event, listener] of Object.entries(events ?? {})) map.on(event, listener);
32077
+ try {
32078
+ await map.load();
32079
+ } catch (error) {
32080
+ map.remove();
32081
+ throw error;
32082
+ }
32083
+ return map;
32084
+ }
30385
32085
  function resolveContainer(container) {
30386
32086
  if (typeof container !== "string") return container;
30387
32087
  const element = document.querySelector(container);
@@ -30399,5 +32099,5 @@ function applyHost(options) {
30399
32099
  }
30400
32100
 
30401
32101
  //#endregion
30402
- export { AttributionControl, FlatAttributionControl, FlatMap, GestureHandler, MAP_LAYERS, PROJECTION_IDS, PanoramaAttributionControl, PanoramaView, SpaceMap, Viewport, bearingDeg, boxRing, circlePoints, configureHost, createFlatMap, createMap, createPanorama, createProjection, dateToJD, defaultSceneSettings, elements, fetchPanoramaIndex, fetchPanoramas, findPanorama, fixed, graticule, groundDistanceM, jdToDate, kmToAu, neighboursOf, panoramaAt, pathFor, planeBasis, projectionAspect, samples, smallCircle, tle };
30403
- (function(){if(typeof document==='undefined')return;var s=document.createElement('style');s.textContent=".scene-label{pointer-events:auto;cursor:pointer;-webkit-user-select:none;user-select:none;color:inherit;touch-action:none;z-index:1;width:32px;height:32px;text-decoration:none;display:block;position:relative;left:0}.scene-label__halo{border-radius:50%;width:100%;height:100%;transition:transform .1s}.scene-label__halo--spacecraft,.scene-label__halo--debris{overflow:visible}.scene-label__loader{box-sizing:border-box;pointer-events:none;border:2px solid #fff;border-color:#fff #fff #fff3 #fff3;border-radius:50%;width:32px;height:32px;margin:auto;animation:.9s linear infinite scene-label-loader-spin;position:fixed;inset:0}@keyframes scene-label-loader-spin{to{transform:rotate(360deg)}}.reduce-motion .scene-label__loader{animation-duration:.9s!important;animation-iteration-count:infinite!important}.scene-label__name{white-space:nowrap;pointer-events:auto;cursor:pointer;color:#fff;text-shadow:0 0 4px #000,0 0 4px #000;padding-left:8px;font-size:14px;font-weight:700;text-decoration:none;position:absolute;top:50%;left:32px;transform:translateY(-50%)}.scene-label__name--large{font-size:16px}.scene-label__annotations{top:28px;pointer-events:none;flex-direction:column;padding-inline-start:8px;display:flex;position:absolute;inset-inline-start:32px}.scene-label__annotation{opacity:0;white-space:nowrap;color:#fff9;text-shadow:0 0 3px #000,0 0 3px #000;transition:opacity .15s, display .15s allow-discrete;font-size:11px;line-height:1.3;display:none}.scene-label__annotation--visible{opacity:1;display:block}@starting-style{.scene-label__annotation--visible{opacity:0}}.scene-feature-label{color:#ffffffb3;white-space:nowrap;pointer-events:auto;cursor:pointer;-webkit-user-select:none;user-select:none;text-shadow:0 0 2px #000,0 0 2px #000;z-index:0;font-size:12px;transition:color .1s;left:0}.scene-feature-label:hover{color:#fff;text-shadow:0 0 3px #000,0 0 3px #000}.scene-feature-label--active{color:#fff;font-size:14px;font-weight:600}.scene-path-label{color:#fff;font:inherit;white-space:nowrap;text-align:start;text-shadow:0 0 4px #000,0 0 4px #000;pointer-events:auto;cursor:pointer;-webkit-user-select:none;user-select:none;touch-action:none;z-index:2;background:0 0;border:0;align-items:center;gap:8px;padding:0;line-height:1.15;display:flex;left:0}.scene-path-label--static{pointer-events:none;cursor:default}.scene-path-label__halo{border-radius:50%;flex-shrink:0;width:32px;height:32px;margin-inline-start:-16px;transition:transform .1s}.scene-path-label:hover .scene-path-label__halo{transform:scale(1.15)}.scene-path-label__text{flex-direction:column;gap:1px;display:flex}.scene-path-label__name{font-size:14px;font-weight:700}.scene-path-label__when{opacity:.8;font-variant-numeric:tabular-nums;font-size:12px}.scene-path-label--dim .scene-path-label__text{display:none}.scene-path-label--dim .scene-path-label__halo{transform:scale(.55)}.scene-path-label--dim:hover .scene-path-label__halo{transform:scale(.75)}.scene-path-label--step .scene-path-label__halo{background:0 0;border:0}.scene-path-label--step:hover .scene-path-label__halo{background:#ffffff1f}.scene-path-label--step .scene-path-label__name{font-size:12px;font-weight:600}.scene-path-label--step .scene-path-label__when{font-size:11px}.scene-path-label__hazard{font-size:11px;font-weight:700}.scene-path-label--lit{color:#fff}.scene-path-label--lit .scene-path-label__halo{transform:scale(1.15)}.scene-path-label--lit.scene-path-label--dim .scene-path-label__halo{transform:scale(.75)}.scene-path-label--faint .scene-path-label__text{display:none}.scene-path-label--faint .scene-path-label__halo{opacity:.45;transform:scale(.5)}.scene-path-label--faint:hover .scene-path-label__halo{opacity:1;transform:scale(.75)}.scene-overlay--no-names .scene-label__name,.scene-overlay--no-names .scene-label__annotations{display:none}.scene-hazard-label{white-space:nowrap;pointer-events:none;-webkit-user-select:none;user-select:none;text-shadow:0 0 4px #000,0 0 4px #000;margin-inline-start:16px;font-size:12px;font-weight:700;position:relative;left:0}.user-location-marker{pointer-events:none;background:#1a73e8;border:2px solid #fff;border-radius:50%;width:16px;height:16px;box-shadow:0 0 6px #0006}.sm-drawing-label{white-space:nowrap;color:#fff;text-shadow:0 0 4px #000,0 0 4px #000;-webkit-user-select:none;user-select:none;pointer-events:none;font:700 14px/1.2 system-ui,sans-serif}.sm-drawing-icon{-webkit-user-select:none;user-select:none;pointer-events:none;display:block}.sm-drawing--interactive{pointer-events:auto;cursor:pointer;touch-action:none}.sm-cooperative-hint{z-index:4;color:#fff;text-align:center;pointer-events:none;opacity:0;background:#0000008c;place-content:center;padding:16px;font:15px/1.4 system-ui,sans-serif;transition:opacity .2s;display:grid;position:absolute;inset:0}.sm-cooperative-hint--shown{opacity:1}@media (prefers-reduced-motion:reduce){.sm-cooperative-hint{transition:none}}.sm-ctrl{z-index:2;pointer-events:none;flex-direction:column;gap:6px;max-width:100%;padding:6px;display:flex;position:absolute}.sm-ctrl>*{pointer-events:auto}.sm-ctrl--top-left{align-items:flex-start;top:0;left:0}.sm-ctrl--top-right{align-items:flex-end;top:0;right:0}.sm-ctrl--bottom-left{align-items:flex-start;bottom:0;left:0}.sm-ctrl--bottom-right{align-items:flex-end;bottom:0;right:0}.sm-map{-webkit-user-select:none;user-select:none;width:100%;height:100%;position:relative}.sm-map__canvas{pointer-events:auto;touch-action:none;width:100%;height:100%;display:block}.sm-map__canvas:focus-visible{outline:2px solid var(--ring,Highlight)}.sm-map .scene-overlay{pointer-events:none;z-index:0;position:absolute;inset:0}.sm-flat{touch-action:none;background:#05070e;width:100%;height:100%;position:relative;overflow:hidden}.sm-flat__raster,.sm-flat__vector,.sm-flat__markers{width:100%;height:100%;position:absolute;inset:0}.sm-flat__raster{display:block}.sm-flat__vector,.sm-flat__markers{pointer-events:none}.sm-flat--interactive{cursor:grab}.sm-flat--cooperative{touch-action:pan-x pan-y}.sm-flat--dragging{cursor:grabbing}.sm-flat__graticule{fill:none;stroke:#ffffff47;stroke-width:1px}.sm-flat__frame{fill:none;stroke:#ffffff73;stroke-width:1px}.sm-flat__feature-dot{fill:#fffc}.sm-flat__feature-label{fill:#ffffffd9;paint-order:stroke;stroke:#000000a6;stroke-width:2.5px;font:10px/1 system-ui,sans-serif}.sm-flat__marker-dot{background:#4ea3ff;border:2px solid #fff;border-radius:50%;width:10px;height:10px;box-shadow:0 0 0 1px #00000073}.sm-ctrl:has(>.sm-attribution:only-child){padding:0}.sm-ctrl--top-left:has(>.sm-attribution:only-child)>.sm-attribution{border-radius:0 0 var(--sm-attribution-radius) 0}.sm-ctrl--top-right:has(>.sm-attribution:only-child)>.sm-attribution{border-radius:0 0 0 var(--sm-attribution-radius)}.sm-ctrl--bottom-left:has(>.sm-attribution:only-child)>.sm-attribution{border-radius:0 var(--sm-attribution-radius) 0 0}.sm-ctrl--bottom-right:has(>.sm-attribution:only-child)>.sm-attribution{border-radius:var(--sm-attribution-radius) 0 0 0}.sm-attribution{--sm-attribution-radius:3px;border-radius:var(--sm-attribution-radius);color:#ffffffbf;white-space:nowrap;background:#0006;align-items:center;gap:.5em;max-width:100%;padding:1px 6px;font:11px/1.45 system-ui,sans-serif;display:flex}.sm-attribution a{color:inherit;text-decoration:none}.sm-attribution a:hover{color:#fff;text-decoration:underline}.sm-attribution__home{font-weight:600}.sm-attribution__group{text-overflow:ellipsis;overflow:hidden}.sm-attribution__label{color:#ffffff80}\n/*$vite$:1*/";document.head.appendChild(s);})();
32102
+ export { AttributionControl, FlatAttributionControl, FlatMap, GestureHandler, MAP_LAYERS, PROJECTION_IDS, PanoramaAttributionControl, PanoramaView, SpaceMap, SystemMap, Viewport, bearingDeg, boxRing, circlePoints, configureHost, createFlatMap, createMap, createPanorama, createProjection, createSystemMap, dateToJD, defaultSceneSettings, elements, fetchPanoramaIndex, fetchPanoramas, findPanorama, fixed, graticule, groundDistanceM, isViewable, jdToDate, kmToAu, neighboursOf, panoramaAt, pathFor, planeBasis, projectionAspect, samples, smallCircle, tle };
32103
+ (function(){if(typeof document==='undefined')return;var s=document.createElement('style');s.textContent=".scene-label{pointer-events:auto;cursor:pointer;-webkit-user-select:none;user-select:none;color:inherit;touch-action:none;z-index:1;width:32px;height:32px;text-decoration:none;display:block;position:relative;left:0}.scene-label__halo{border-radius:50%;width:100%;height:100%;transition:transform .1s}.scene-label__halo--spacecraft,.scene-label__halo--debris{overflow:visible}.scene-label__loader{box-sizing:border-box;pointer-events:none;border:2px solid #fff;border-color:#fff #fff #fff3 #fff3;border-radius:50%;width:32px;height:32px;margin:auto;animation:.9s linear infinite scene-label-loader-spin;position:fixed;inset:0}@keyframes scene-label-loader-spin{to{transform:rotate(360deg)}}.reduce-motion .scene-label__loader{animation-duration:.9s!important;animation-iteration-count:infinite!important}.scene-label__name{white-space:nowrap;pointer-events:auto;cursor:pointer;color:#fff;text-shadow:0 0 4px #000,0 0 4px #000;padding-left:8px;font-size:14px;font-weight:700;text-decoration:none;position:absolute;top:50%;left:32px;transform:translateY(-50%)}.scene-label__name--large{font-size:16px}.scene-label__annotations{top:28px;pointer-events:none;flex-direction:column;padding-inline-start:8px;display:flex;position:absolute;inset-inline-start:32px}.scene-label__annotation{opacity:0;white-space:nowrap;color:#fff9;text-shadow:0 0 3px #000,0 0 3px #000;transition:opacity .15s, display .15s allow-discrete;font-size:11px;line-height:1.3;display:none}.scene-label__annotation--visible{opacity:1;display:block}@starting-style{.scene-label__annotation--visible{opacity:0}}.scene-feature-label{color:#ffffffb3;white-space:nowrap;pointer-events:auto;cursor:pointer;-webkit-user-select:none;user-select:none;text-shadow:0 0 2px #000,0 0 2px #000;z-index:0;font-size:12px;transition:color .1s;left:0}.scene-feature-label:hover{color:#fff;text-shadow:0 0 3px #000,0 0 3px #000}.scene-feature-label--active{color:#fff;font-size:14px;font-weight:600}.scene-path-label{color:#fff;font:inherit;white-space:nowrap;text-align:start;text-shadow:0 0 4px #000,0 0 4px #000;pointer-events:auto;cursor:pointer;-webkit-user-select:none;user-select:none;touch-action:none;z-index:2;background:0 0;border:0;align-items:center;gap:8px;padding:0;line-height:1.15;display:flex;left:0}.scene-path-label--static{pointer-events:none;cursor:default}.scene-path-label__halo{border-radius:50%;flex-shrink:0;width:32px;height:32px;margin-inline-start:-16px;transition:transform .1s}.scene-path-label:hover .scene-path-label__halo{transform:scale(1.15)}.scene-path-label__text{flex-direction:column;gap:1px;display:flex}.scene-path-label__name{font-size:14px;font-weight:700}.scene-path-label__when{opacity:.8;font-variant-numeric:tabular-nums;font-size:12px}.scene-path-label--dim .scene-path-label__text{display:none}.scene-path-label--dim .scene-path-label__halo{transform:scale(.55)}.scene-path-label--dim:hover .scene-path-label__halo{transform:scale(.75)}.scene-path-label--step .scene-path-label__halo{background:0 0;border:0}.scene-path-label--step:hover .scene-path-label__halo{background:#ffffff1f}.scene-path-label--step .scene-path-label__name{font-size:12px;font-weight:600}.scene-path-label--step .scene-path-label__when{font-size:11px}.scene-path-label__hazard{font-size:11px;font-weight:700}.scene-path-label--lit{color:#fff}.scene-path-label--lit .scene-path-label__halo{transform:scale(1.15)}.scene-path-label--lit.scene-path-label--dim .scene-path-label__halo{transform:scale(.75)}.scene-path-label--faint .scene-path-label__text{display:none}.scene-path-label--faint .scene-path-label__halo{opacity:.45;transform:scale(.5)}.scene-path-label--faint:hover .scene-path-label__halo{opacity:1;transform:scale(.75)}.scene-overlay--no-names .scene-label__name,.scene-overlay--no-names .scene-label__annotations{display:none}.scene-hazard-label{white-space:nowrap;pointer-events:none;-webkit-user-select:none;user-select:none;text-shadow:0 0 4px #000,0 0 4px #000;margin-inline-start:16px;font-size:12px;font-weight:700;position:relative;left:0}.user-location-marker{pointer-events:none;background:#1a73e8;border:2px solid #fff;border-radius:50%;width:16px;height:16px;box-shadow:0 0 6px #0006}.sm-drawing-label{white-space:nowrap;color:#fff;text-shadow:0 0 4px #000,0 0 4px #000;-webkit-user-select:none;user-select:none;pointer-events:none;font:700 14px/1.2 system-ui,sans-serif}.sm-drawing-icon{-webkit-user-select:none;user-select:none;pointer-events:none;display:block}.sm-drawing--interactive{pointer-events:auto;cursor:pointer;touch-action:none}.sm-cooperative-hint{z-index:4;color:#fff;text-align:center;pointer-events:none;opacity:0;background:#0000008c;place-content:center;padding:16px;font:15px/1.4 system-ui,sans-serif;transition:opacity .2s;display:grid;position:absolute;inset:0}.sm-cooperative-hint--shown{opacity:1}@media (prefers-reduced-motion:reduce){.sm-cooperative-hint{transition:none}}.sm-ctrl{z-index:2;pointer-events:none;flex-direction:column;gap:6px;max-width:100%;padding:6px;display:flex;position:absolute}.sm-ctrl>*{pointer-events:auto}.sm-ctrl--top-left{align-items:flex-start;top:0;left:0}.sm-ctrl--top-right{align-items:flex-end;top:0;right:0}.sm-ctrl--bottom-left{align-items:flex-start;bottom:0;left:0}.sm-ctrl--bottom-right{align-items:flex-end;bottom:0;right:0}.sm-map{-webkit-user-select:none;user-select:none}:where(.sm-map){width:100%;height:100%;position:relative}.sm-map__canvas{pointer-events:auto;touch-action:none;width:100%;height:100%;display:block}.sm-map__canvas:focus-visible{outline:2px solid var(--ring,Highlight)}.sm-map .scene-overlay{pointer-events:none;z-index:0;position:absolute;inset:0}.sm-flat{touch-action:none;background:#05070e;width:100%;height:100%;position:relative;overflow:hidden}.sm-flat__raster,.sm-flat__vector,.sm-flat__markers{width:100%;height:100%;position:absolute;inset:0}.sm-flat__raster{display:block}.sm-flat__vector,.sm-flat__markers{pointer-events:none}.sm-flat--interactive{cursor:grab}.sm-flat--cooperative{touch-action:pan-x pan-y}.sm-flat--dragging{cursor:grabbing}.sm-flat__graticule{fill:none;stroke:#ffffff47;stroke-width:1px}.sm-flat__frame{fill:none;stroke:#ffffff73;stroke-width:1px}.sm-flat__feature-dot{fill:#fffc}.sm-flat__feature-label{fill:#ffffffd9;paint-order:stroke;stroke:#000000a6;stroke-width:2.5px;font:10px/1 system-ui,sans-serif}.sm-flat__marker-dot{background:#4ea3ff;border:2px solid #fff;border-radius:50%;width:10px;height:10px;box-shadow:0 0 0 1px #00000073}.sm-system-map{border-radius:var(--sm-system-map-radius,6px);background:var(--sm-system-map-background,#ffffff0d);width:100%;color:var(--sm-system-map-ink,#ffffff9e);position:relative;overflow:hidden}.sm-system-map--background{pointer-events:none;background:0 0;border-radius:0;height:100%}.sm-system-map__svg{touch-action:pan-y;width:100%;height:auto;display:block}.sm-system-map--background .sm-system-map__svg{height:100%}.sm-system-map__svg--near :is(a,.sm-system-map__button):not([data-near]){pointer-events:none}.sm-system-map__button{cursor:pointer}.sm-system-map__button:focus{outline:none}.sm-system-map__reference{opacity:.6}.sm-system-map__band--muted{color:var(--sm-system-map-ink,#ffffff9e)}.sm-system-map__band--sky{color:var(--sm-system-map-sky,oklch(74.6% .16 232.661/.7))}.sm-system-map__band--amber{color:var(--sm-system-map-amber,oklch(92.4% .12 95.746/.8))}.sm-system-map__band-label{font-weight:600}.sm-system-map__band-label--muted{fill:var(--sm-system-map-ink,#ffffff9e)}.sm-system-map__band-label--sky{fill:var(--sm-system-map-sky-label,oklch(74.6% .16 232.661))}.sm-system-map__band-label--amber{fill:var(--sm-system-map-amber-label,oklch(92.4% .12 95.746/.9))}.sm-system-map__tip{z-index:10;background:var(--sm-system-map-tip-background,#0a0c14e6);width:max-content;color:var(--sm-system-map-tip-ink,#fffffff2);white-space:nowrap;pointer-events:none;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-radius:.25rem;padding:.25rem .5rem;font-size:.75rem;line-height:1.33333;position:absolute;transform:translate(-50%);box-shadow:0 1px 3px #0000001a,0 1px 2px -1px #0000001a}.sm-system-map__tip[hidden]{display:none}.sm-system-map__tip-title{font-weight:500}.sm-system-map__tip-sub{color:var(--sm-system-map-ink,#ffffff9e)}.sm-ctrl:has(>.sm-attribution:only-child){padding:0}.sm-ctrl--top-left:has(>.sm-attribution:only-child)>.sm-attribution{border-radius:0 0 var(--sm-attribution-radius) 0}.sm-ctrl--top-right:has(>.sm-attribution:only-child)>.sm-attribution{border-radius:0 0 0 var(--sm-attribution-radius)}.sm-ctrl--bottom-left:has(>.sm-attribution:only-child)>.sm-attribution{border-radius:0 var(--sm-attribution-radius) 0 0}.sm-ctrl--bottom-right:has(>.sm-attribution:only-child)>.sm-attribution{border-radius:var(--sm-attribution-radius) 0 0 0}.sm-attribution{--sm-attribution-radius:3px;border-radius:var(--sm-attribution-radius);color:#ffffffbf;white-space:nowrap;background:#0006;align-items:center;gap:.5em;max-width:100%;padding:1px 6px;font:11px/1.45 system-ui,sans-serif;display:flex}.sm-attribution a{color:inherit;text-decoration:none}.sm-attribution a:hover{color:#fff;text-decoration:underline}.sm-attribution__home{font-weight:600}.sm-attribution__group{text-overflow:ellipsis;overflow:hidden}.sm-attribution__label{color:#ffffff80}\n/*$vite$:1*/";document.head.appendChild(s);})();