spacemap 0.1.3 → 0.1.5

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
@@ -37,7 +37,17 @@ var DEFAULT_HOST = {
37
37
  layer_nomenclature: () => "Named features",
38
38
  cooperative_wheel: () => "Use ctrl + scroll to zoom the map",
39
39
  cooperative_wheel_mac: () => "Use ⌘ + scroll to zoom the map",
40
- 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"
41
51
  },
42
52
  bodyHref: () => "",
43
53
  textures: "open"
@@ -6213,6 +6223,9 @@ var BodyIndex = class {
6213
6223
  set orientationVersion(value) {
6214
6224
  set(this.#orientationVersion, value, true);
6215
6225
  }
6226
+ orientationRead = /* @__PURE__ */ new Set();
6227
+ hostRead = /* @__PURE__ */ new Map();
6228
+ hostReadVersion = 0;
6216
6229
  childrenByParent = /* @__PURE__ */ new Map();
6217
6230
  moonMaxAByParent = /* @__PURE__ */ new Map();
6218
6231
  addListeners = /* @__PURE__ */ new Set();
@@ -6776,6 +6789,7 @@ var VisibilityController = class {
6776
6789
  scaledSystem = SYSTEM_DISTANCE_RATIO_THRESHOLDS;
6777
6790
  fullMoonIds = /* @__PURE__ */ new Set();
6778
6791
  moonVisibilityCache = /* @__PURE__ */ new Map();
6792
+ pinned = /* @__PURE__ */ new Set();
6779
6793
  constructor(bodies, getProbeStore = () => null, getEarthSatGroupFilter = () => null, getSmallBodyFilter = () => null, getLayers = () => ALL_LAYERS) {
6780
6794
  this.bodies = bodies;
6781
6795
  this.getProbeStore = getProbeStore;
@@ -6872,8 +6886,19 @@ var VisibilityController = class {
6872
6886
  this.moonVisibilityCache.set(moon.data.id, vis);
6873
6887
  return vis;
6874
6888
  }
6889
+ setPinned(ids) {
6890
+ this.pinned = new Set(ids);
6891
+ }
6892
+ isPinned(id) {
6893
+ return this.pinned.has(id);
6894
+ }
6875
6895
  getPlanetVisibility(body, camDistThreeJS) {
6876
- if (this.getLayers().hidesBody(body.data.objectType, body.data.parentId)) return VISIBILITY.HIDE;
6896
+ const pinned = this.pinned.has(body.data.id);
6897
+ const vis = this.ratioVisibility(body, camDistThreeJS, pinned);
6898
+ return pinned && vis !== VISIBILITY.CLOSE ? VISIBILITY.FULL : vis;
6899
+ }
6900
+ ratioVisibility(body, camDistThreeJS, pinned) {
6901
+ if (!pinned && this.getLayers().hidesBody(body.data.objectType, body.data.parentId)) return VISIBILITY.HIDE;
6877
6902
  if (body.data.orbitalSource === OrbitalSource.SPICE_PROBE) return this.getProbeVisibility(body);
6878
6903
  const earthFilter = this.getEarthSatGroupFilter();
6879
6904
  if (earthFilter && body.data.parentId === "naif-399" && !earthFilter.has(body.data.id)) return VISIBILITY.HIDE;
@@ -6926,7 +6951,7 @@ var VisibilityController = class {
6926
6951
  return sysNaif !== null && this.isInFocusedSystem(`naif-${sysNaif}`);
6927
6952
  }
6928
6953
  hasFullRendering(body) {
6929
- if (!this.activeSystemId) return true;
6954
+ if (!this.activeSystemId || this.pinned.has(body.data.id)) return true;
6930
6955
  if (this.isInActiveSystem(isTopLevelParent(body.data.parentId) ? body.data.id : body.data.parentId)) return true;
6931
6956
  if (body.data.orbitalSource !== OrbitalSource.SPICE_PROBE) return false;
6932
6957
  const ps = this.getProbeStore();
@@ -10311,20 +10336,35 @@ async function loadTierBitmaps(id, tier, priority) {
10311
10336
  }));
10312
10337
  }
10313
10338
  var tierPrefetch = /* @__PURE__ */ new Map();
10339
+ var tierClaims = /* @__PURE__ */ new Map();
10314
10340
  function tierKey(id, tier) {
10315
10341
  return `${id}:${tier}`;
10316
10342
  }
10317
- function tierBitmaps(id, tier, priority) {
10343
+ function tierBitmaps(id, tier, priority, claim = true) {
10318
10344
  const key = tierKey(id, tier);
10319
10345
  let p = tierPrefetch.get(key);
10320
10346
  if (!p) {
10321
- p = loadTierBitmaps(id, tier, priority);
10347
+ const loading = loadTierBitmaps(id, tier, priority);
10348
+ p = loading;
10322
10349
  tierPrefetch.set(key, p);
10350
+ loading.catch(() => {
10351
+ if (tierPrefetch.get(key) !== loading) return;
10352
+ tierPrefetch.delete(key);
10353
+ tierClaims.delete(key);
10354
+ });
10323
10355
  }
10356
+ if (claim) tierClaims.set(key, (tierClaims.get(key) ?? 0) + 1);
10324
10357
  return p;
10325
10358
  }
10326
10359
  function releaseTierBitmaps(id, tier, bitmaps) {
10327
- tierPrefetch.delete(tierKey(id, tier));
10360
+ const key = tierKey(id, tier);
10361
+ const left = (tierClaims.get(key) ?? 1) - 1;
10362
+ if (left > 0) {
10363
+ tierClaims.set(key, left);
10364
+ return;
10365
+ }
10366
+ tierClaims.delete(key);
10367
+ tierPrefetch.delete(key);
10328
10368
  for (const b of bitmaps) b.close();
10329
10369
  }
10330
10370
  function makeCube(bitmaps) {
@@ -10339,10 +10379,10 @@ function pickLowTier(meta) {
10339
10379
  }
10340
10380
  function prefetchSkyboxTiers(meta) {
10341
10381
  const low = pickLowTier(meta);
10342
- if (low) tierBitmaps(meta.id, low).catch(() => tierPrefetch.delete(tierKey(meta.id, low)));
10382
+ if (low) tierBitmaps(meta.id, low, void 0, false).catch(() => {});
10343
10383
  }
10344
10384
  var FULL_TIER_GATE_TIMEOUT_MS = 12e3;
10345
- async function loadFromMeta(scene, renderer, meta) {
10385
+ async function loadFromMeta(scene, renderer, meta, disposed) {
10346
10386
  const tier = pickTier$1(meta, Math.min(renderer.capabilities.maxTextureSize, currentRenderTier().maxSkyboxFace));
10347
10387
  if (!tier) {
10348
10388
  console.warn(`Skybox ${meta.id}: no tiers declared`);
@@ -10355,7 +10395,7 @@ async function loadFromMeta(scene, renderer, meta) {
10355
10395
  };
10356
10396
  let fullInstalled = false;
10357
10397
  if (lowTier && lowTier !== tier) tierBitmaps(meta.id, lowTier).then((bitmaps) => {
10358
- if (fullInstalled) {
10398
+ if (fullInstalled || disposed()) {
10359
10399
  releaseTierBitmaps(meta.id, lowTier, bitmaps);
10360
10400
  return;
10361
10401
  }
@@ -10363,11 +10403,38 @@ async function loadFromMeta(scene, renderer, meta) {
10363
10403
  lowRef.bitmaps = bitmaps;
10364
10404
  scene.background = lowRef.cube;
10365
10405
  performance.mark("sm-skybox-low");
10406
+ if (fullFailed) keepLow();
10366
10407
  }).catch(() => {});
10408
+ let fullFailed = false;
10409
+ const keepLow = () => {
10410
+ if (!lowTier || !lowRef.cube || !lowRef.bitmaps) return;
10411
+ renderer.initTexture(lowRef.cube);
10412
+ releaseTierBitmaps(meta.id, lowTier, lowRef.bitmaps);
10413
+ lowRef.bitmaps = null;
10414
+ };
10367
10415
  if (tier !== lowTier) await Promise.race([eagerMinorsDone, new Promise((resolve) => setTimeout(resolve, FULL_TIER_GATE_TIMEOUT_MS))]);
10368
- const fullBitmaps = await tierBitmaps(meta.id, tier, tier === lowTier ? void 0 : "low");
10369
- const full = makeCube(fullBitmaps);
10416
+ let fullBitmaps;
10417
+ try {
10418
+ fullBitmaps = await tierBitmaps(meta.id, tier, tier === lowTier ? void 0 : "low");
10419
+ } catch (err) {
10420
+ fullFailed = true;
10421
+ if (!disposed()) keepLow();
10422
+ else if (lowTier && lowRef.bitmaps) {
10423
+ lowRef.cube?.dispose();
10424
+ releaseTierBitmaps(meta.id, lowTier, lowRef.bitmaps);
10425
+ lowRef.bitmaps = null;
10426
+ }
10427
+ throw err;
10428
+ }
10370
10429
  await new Promise((resolve) => "requestIdleCallback" in window ? requestIdleCallback(() => resolve(), { timeout: 2e3 }) : setTimeout(resolve, 500));
10430
+ if (disposed()) {
10431
+ fullInstalled = true;
10432
+ lowRef.cube?.dispose();
10433
+ releaseTierBitmaps(meta.id, tier, fullBitmaps);
10434
+ if (lowTier && lowTier !== tier && lowRef.bitmaps) releaseTierBitmaps(meta.id, lowTier, lowRef.bitmaps);
10435
+ return;
10436
+ }
10437
+ const full = makeCube(fullBitmaps);
10371
10438
  renderer.initTexture(full);
10372
10439
  fullInstalled = true;
10373
10440
  const prevBackground = scene.background;
@@ -10378,7 +10445,7 @@ async function loadFromMeta(scene, renderer, meta) {
10378
10445
  releaseTierBitmaps(meta.id, tier, fullBitmaps);
10379
10446
  if (lowTier && lowTier !== tier && lowRef.bitmaps) releaseTierBitmaps(meta.id, lowTier, lowRef.bitmaps);
10380
10447
  }
10381
- async function loadSkybox(scene, renderer, ctx) {
10448
+ async function loadSkybox(scene, renderer, ctx, disposed = () => false) {
10382
10449
  if (ctx?.layers.skipped.has("stars")) return;
10383
10450
  try {
10384
10451
  const meta = await fetchMetadata();
@@ -10390,7 +10457,7 @@ async function loadSkybox(scene, renderer, ctx) {
10390
10457
  attribution: meta.skybox.attribution,
10391
10458
  description: meta.skybox.description
10392
10459
  };
10393
- await loadFromMeta(scene, renderer, meta.skybox);
10460
+ await loadFromMeta(scene, renderer, meta.skybox, disposed);
10394
10461
  } catch (err) {
10395
10462
  console.warn("Failed to load skybox:", err);
10396
10463
  }
@@ -12231,8 +12298,9 @@ function makeTrailMaterial(color, brightness = TRAIL_DIM) {
12231
12298
  `
12232
12299
  });
12233
12300
  }
12234
- function makeFatTrailMaterial(color, lineWidth, brightness = TRAIL_DIM) {
12301
+ function makeFatTrailMaterial(color, lineWidth, brightness = TRAIL_DIM, dash) {
12235
12302
  return new ShaderMaterial({
12303
+ defines: dash ? { DASHED: "" } : {},
12236
12304
  transparent: true,
12237
12305
  depthWrite: false,
12238
12306
  side: DoubleSide,
@@ -12243,7 +12311,8 @@ function makeFatTrailMaterial(color, lineWidth, brightness = TRAIL_DIM) {
12243
12311
  uAlphaMin: { value: 0 },
12244
12312
  uShowFull: { value: 0 },
12245
12313
  uLineWidth: { value: lineWidth },
12246
- uResolution: { value: TRAIL_RESOLUTION }
12314
+ uResolution: { value: TRAIL_RESOLUTION },
12315
+ uDash: { value: new Vector2(dash?.[0] ?? 0, dash?.[1] ?? 0) }
12247
12316
  },
12248
12317
  vertexShader: `
12249
12318
  #include <common>
@@ -12257,6 +12326,11 @@ function makeFatTrailMaterial(color, lineWidth, brightness = TRAIL_DIM) {
12257
12326
  attribute float trailAlpha;
12258
12327
  attribute float fullAlpha;
12259
12328
  varying float vAlpha;
12329
+ #ifdef DASHED
12330
+ attribute float lineDistance;
12331
+ varying float vDistW;
12332
+ varying float vW;
12333
+ #endif
12260
12334
  void main() {
12261
12335
  vAlpha = mix(trailAlpha, fullAlpha, uShowFull);
12262
12336
  vec3 currRel = position + uCenterOffset;
@@ -12273,6 +12347,13 @@ function makeFatTrailMaterial(color, lineWidth, brightness = TRAIL_DIM) {
12273
12347
  vec2 perp = vec2(-dirN.y, dirN.x) * side * uLineWidth * 0.5;
12274
12348
  vec2 offsetNDC = perp / (uResolution * 0.5);
12275
12349
  gl_Position = vec4((currNDC + offsetNDC) * currClip.w, currClip.zw);
12350
+ #ifdef DASHED
12351
+ // Carried times w and divided back out per fragment, which undoes
12352
+ // the perspective a varying is interpolated with: the dashes are
12353
+ // even on screen however much nearer one end is.
12354
+ vDistW = lineDistance * currClip.w;
12355
+ vW = currClip.w;
12356
+ #endif
12276
12357
  #include <logdepthbuf_vertex>
12277
12358
  }
12278
12359
  `,
@@ -12282,7 +12363,15 @@ function makeFatTrailMaterial(color, lineWidth, brightness = TRAIL_DIM) {
12282
12363
  uniform float uAlphaMultiplier;
12283
12364
  uniform float uAlphaMin;
12284
12365
  varying float vAlpha;
12366
+ #ifdef DASHED
12367
+ uniform vec2 uDash;
12368
+ varying float vDistW;
12369
+ varying float vW;
12370
+ #endif
12285
12371
  void main() {
12372
+ #ifdef DASHED
12373
+ if (mod(vDistW / vW, uDash.x + uDash.y) > uDash.x) discard;
12374
+ #endif
12286
12375
  gl_FragColor = vec4(uColor, clamp(max(vAlpha * uAlphaMultiplier, uAlphaMin), 0.0, 1.0));
12287
12376
  #include <logdepthbuf_fragment>
12288
12377
  }
@@ -12393,6 +12482,7 @@ function bootThree(canvas, labelContainer, ctx) {
12393
12482
  //#region src/lib/scene/minor-body-position.ts
12394
12483
  function refreshMinorBodyPosition(body, jd, ctx) {
12395
12484
  const d = body.data;
12485
+ body.placedJd = jd;
12396
12486
  const isParabolic = d.q != null;
12397
12487
  let parentPos;
12398
12488
  if (d.parentId === "naif-0") parentPos = [
@@ -12431,6 +12521,47 @@ function refreshMinorBodyPosition(body, jd, ctx) {
12431
12521
  }
12432
12522
  body.positionUnknown = false;
12433
12523
  }
12524
+ function refreshMoonPosition(body, jd, ctx) {
12525
+ const d = body.data;
12526
+ body.placedJd = jd;
12527
+ body.positionUnknown = true;
12528
+ if (d.unplaceable) return;
12529
+ if (d.visibleFromDays !== void 0 && jd - 2451545 < d.visibleFromDays) return;
12530
+ const parent = ctx.getBody(d.parentId);
12531
+ if (!parent || !placeForRead(parent, jd, ctx)) return;
12532
+ const parentPos = parent.position;
12533
+ const offset = ctx.chebStore?.has(d.id) ? ctx.chebStore.positionScene(d.id, jd) : d.a === 0 ? [
12534
+ 0,
12535
+ 0,
12536
+ 0
12537
+ ] : orbitalElementsToPositionJD(d, jd);
12538
+ if (!offset) return;
12539
+ body.position[0] = parentPos[0] + offset[0];
12540
+ body.position[1] = parentPos[1] + offset[1];
12541
+ body.position[2] = parentPos[2] + offset[2];
12542
+ if (body.orbitCenter) {
12543
+ body.orbitCenter[0] = parentPos[0];
12544
+ body.orbitCenter[1] = parentPos[1];
12545
+ body.orbitCenter[2] = parentPos[2];
12546
+ }
12547
+ body.positionUnknown = false;
12548
+ }
12549
+ var HOST_READ_MS = 2e3;
12550
+ function placeForRead(body, jd, ctx) {
12551
+ const d = body.data;
12552
+ const stale = body.placedJd !== jd;
12553
+ if (d.orbitalSource === OrbitalSource.SPICE_PROBE) {
12554
+ const { hostRead } = ctx.bodies;
12555
+ if (!stale && !hostRead.has(d.id)) return !body.positionUnknown;
12556
+ if (!hostRead.has(d.id)) ctx.bodies.hostReadVersion++;
12557
+ hostRead.set(d.id, performance.now());
12558
+ if (stale) return false;
12559
+ } else if (stale) {
12560
+ if (d.objectType === ObjectType.MOON) refreshMoonPosition(body, jd, ctx);
12561
+ else if (!ctx.bodies.bodiesById.has(d.id) && !isSurfaceFeature(body)) refreshMinorBodyPosition(body, jd, ctx);
12562
+ }
12563
+ return !body.positionUnknown;
12564
+ }
12434
12565
 
12435
12566
  //#endregion
12436
12567
  //#region src/lib/fetch/attitude/parse.ts
@@ -14045,7 +14176,7 @@ function cullOverlappingLabels(bodyObjects, screenWidth, screenHeight, camera, f
14045
14176
  c.isCapped = body.data.objectType === ObjectType.MOON ? ctx.visibility.getMoonVisibility(body) === VISIBILITY.CAPPED : false;
14046
14177
  c.isMinor = bo.isMinor;
14047
14178
  c.isFocused = isFocused;
14048
- c.isSelected = isFocused || isHovered;
14179
+ c.isSelected = isFocused || isHovered || ctx.visibility.isPinned(body.data.id);
14049
14180
  c.dist = bo.cachedDist;
14050
14181
  }
14051
14182
  _candidates.length = _candidatesActive;
@@ -16766,6 +16897,7 @@ function applyRadiiToMesh(bo, radii) {
16766
16897
  async function loadBodyTexture(bo, textureLoader, currentJd, scene, maxTextureSize, ctx) {
16767
16898
  if (bo.textureTier || bo.textureLoading) return;
16768
16899
  const detail = await fetchObjectDetail(bo.body.data.id, false);
16900
+ ctx?.bodies.orientationRead.add(bo.body.data.id);
16769
16901
  if (!detail.global) return;
16770
16902
  if (detail.global.orientation && !bo.body.orientation) applyBodyOrientation(bo, detail.global.orientation, ctx);
16771
16903
  if (detail.global.radii && bo.radiusScene > 0 && !bo.radiiApplied && !detail.global.displacement?.absolute_radius) applyRadiiToMesh(bo, detail.global.radii);
@@ -17670,10 +17802,18 @@ function buildThinLineFromArrays(posArr, trailAlphas, fullAlphas, total, color)
17670
17802
  line.renderOrder = TRAIL_RENDER_ORDER;
17671
17803
  return line;
17672
17804
  }
17673
- function buildFatLineFromThin(capacity, posArr, trailAlphas, fullAlphas, total, color, lineWidth, brightness) {
17805
+ function writeLineDistances(geometry, src, n) {
17806
+ const attr = geometry.getAttribute("lineDistance");
17807
+ const arr = attr.array;
17808
+ const m = Math.min(n, arr.length / 2);
17809
+ for (let i = 0; i < m; i++) arr[2 * i] = arr[2 * i + 1] = src[i];
17810
+ attr.needsUpdate = true;
17811
+ }
17812
+ function buildFatLineFromThin(capacity, posArr, trailAlphas, fullAlphas, total, color, lineWidth, brightness, dash) {
17674
17813
  const geometry = makeFatTrailGeometry(capacity);
17814
+ if (dash) geometry.setAttribute("lineDistance", new Float32BufferAttribute(new Float32Array(2 * capacity), 1));
17675
17815
  writeFatTrailVertices(geometry, posArr, trailAlphas, fullAlphas, total);
17676
- const mesh = new Mesh(geometry, makeFatTrailMaterial(color, lineWidth, brightness));
17816
+ const mesh = new Mesh(geometry, makeFatTrailMaterial(color, lineWidth, brightness, dash));
17677
17817
  mesh.matrixAutoUpdate = false;
17678
17818
  mesh.renderOrder = TRAIL_RENDER_ORDER;
17679
17819
  return mesh;
@@ -17934,6 +18074,7 @@ async function loadSystemData(barycenterId, bodyObjects, scene, textureLoader, c
17934
18074
  if (surface) ctx?.credits.registerImagery("surface", bodyId, barycenterId, surface);
17935
18075
  const bo = bodyObjects.get(bodyId);
17936
18076
  if (!bo?.mesh) continue;
18077
+ ctx?.bodies.orientationRead.add(bodyId);
17937
18078
  if (bodyMeta.orientation) {
17938
18079
  applyBodyOrientation(bo, bodyMeta.orientation, ctx, barycenterId);
17939
18080
  if (bodyMeta.nut_prec) {
@@ -19098,6 +19239,7 @@ var SMALL_BODY_MAX_OPACITY = EMPHASIS_MAX_OPACITY;
19098
19239
  var PointCloudSystem = class {
19099
19240
  orbitPool = new OrbitWorkerPool();
19100
19241
  pickRegistry = new PickRegistry();
19242
+ disposed = false;
19101
19243
  workerPoints = {
19102
19244
  asteroid: /* @__PURE__ */ new Map(),
19103
19245
  spacecraft: /* @__PURE__ */ new Map()
@@ -19283,7 +19425,7 @@ var PointCloudSystem = class {
19283
19425
  return true;
19284
19426
  }
19285
19427
  async rebuildMinorPass() {
19286
- if (this.ctx.bodies.dirtyAsteroidZones.size === 0 && this.ctx.bodies.dirtySpacecraftGroups.size === 0) return;
19428
+ if (this.disposed || this.ctx.bodies.dirtyAsteroidZones.size === 0 && this.ctx.bodies.dirtySpacecraftGroups.size === 0) return;
19287
19429
  const seedBasis = [
19288
19430
  this.basisPos[0],
19289
19431
  this.basisPos[1],
@@ -19303,6 +19445,7 @@ var PointCloudSystem = class {
19303
19445
  groups: [],
19304
19446
  baseWorker: 0
19305
19447
  };
19448
+ if (this.disposed) return;
19306
19449
  const cloudColor = bucket && groups.length > 0 ? bucket.cloudColor() : "#888888";
19307
19450
  for (let i = 0; i < k; i++) {
19308
19451
  const key = `${zone}#${i}`;
@@ -19330,6 +19473,7 @@ var PointCloudSystem = class {
19330
19473
  this.ctx.bodies.dirtySpacecraftGroups.delete(gid);
19331
19474
  drainedGroups.push(gid);
19332
19475
  const { buckets, baseWorker } = await partitionForWorkersSliced(gid, drawnSpacecraft(this.ctx.layers, gid, bucket ? Array.from(bucket.values()) : []), k);
19476
+ if (this.disposed) return;
19333
19477
  const skip = new Set(this.bodyObjects.keys());
19334
19478
  for (let i = 0; i < k; i++) {
19335
19479
  const key = `${gid}#${i}`;
@@ -19651,6 +19795,7 @@ var PointCloudSystem = class {
19651
19795
  return this.moonPoints;
19652
19796
  }
19653
19797
  dispose() {
19798
+ this.disposed = true;
19654
19799
  this.orbitPool.destroy();
19655
19800
  this.pickRegistry.clear();
19656
19801
  for (const kind of WORKER_CLOUD_KINDS) this.disposeClouds(this.workerPoints[kind]);
@@ -20080,7 +20225,7 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
20080
20225
  isClose = !subPixel;
20081
20226
  showLabel = !isClose;
20082
20227
  }
20083
- if (bo.isMinor && !isFocused && trail) trail.visible = false;
20228
+ if (bo.isMinor && !isFocused && trail && !ctx.visibility.isPinned(body.data.id)) trail.visible = false;
20084
20229
  if (suppressHeliocentricTrails && trail?.visible) {
20085
20230
  const oc = trail.userData.orbitCenter;
20086
20231
  if (oc && oc.distanceToSquared(_focusV3.set(fp[0], fp[1], fp[2])) > HELIO_CENTER_DIST_SQ) trail.visible = false;
@@ -21271,18 +21416,32 @@ function updatePositions(params) {
21271
21416
  for (const body of ctx.bodies.bodiesById.values()) positionMap.set(body.data.id, body.position);
21272
21417
  for (const bo of bodyObjects.values()) if (!ctx.bodies.bodiesById.has(bo.body.data.id)) positionMap.set(bo.body.data.id, bo.body.position);
21273
21418
  const computed = /* @__PURE__ */ new Set();
21419
+ const placeFirst = (id) => {
21420
+ if (computed.has(id)) return;
21421
+ const body = ctx.bodies.bodiesById.get(id) ?? bodyObjects.get(id)?.body;
21422
+ if (body) computePosition(body);
21423
+ };
21424
+ const now = performance.now();
21425
+ const hostReads = (id) => {
21426
+ const at = ctx.bodies.hostRead.get(id);
21427
+ if (at === void 0) return false;
21428
+ if (now - at < 2e3) return true;
21429
+ ctx.bodies.hostRead.delete(id);
21430
+ return false;
21431
+ };
21274
21432
  const computePosition = (body) => {
21275
21433
  if (computed.has(body.data.id)) return;
21276
21434
  computed.add(body.data.id);
21277
21435
  const d = body.data;
21278
21436
  const bo = bodyObjects.get(d.id);
21279
- if (bo && frame !== void 0 && d.id !== focusedId && !bo.group.visible && !bo.trail?.visible && (d.orbitalSource === OrbitalSource.SPICE_PROBE || !isMajorBody(d.objectType))) {
21437
+ 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))) {
21280
21438
  bo.staggerSlot ??= nextStaggerSlot++ % HIDDEN_STRIDE;
21281
21439
  if (frame % HIDDEN_STRIDE !== bo.staggerSlot) return;
21282
21440
  }
21283
21441
  const hide = (notForNotice = false) => {
21284
21442
  if (bo) bo.outOfRange = true;
21285
21443
  body.positionUnknown = true;
21444
+ body.placedJd = jd;
21286
21445
  if (!notForNotice && d.id === focusedId) oorState.focusedOutOfRange = true;
21287
21446
  };
21288
21447
  if (d.unplaceable) {
@@ -21363,6 +21522,7 @@ function updatePositions(params) {
21363
21522
  const probeLanded = located.probe.landed;
21364
21523
  if (probeLanded && isLandedAt(located.probe, jd)) {
21365
21524
  const flyingFrameId = d.parentId;
21525
+ placeFirst(`naif-${probeLanded.bodyNaifId}`);
21366
21526
  const landedRender = renderLandedProbe(d, located.probe, probeLanded, jd, positionMap, ctx, bodyObjects);
21367
21527
  if (!landedRender) {
21368
21528
  hide();
@@ -21370,6 +21530,7 @@ function updatePositions(params) {
21370
21530
  }
21371
21531
  diagnostics.clear("probe-unavailable", d.id);
21372
21532
  body.positionUnknown = false;
21533
+ body.placedJd = jd;
21373
21534
  if (flyingFrameId !== d.parentId) body.trailBuffer?.clear();
21374
21535
  if (bo) {
21375
21536
  bo.outOfRange = false;
@@ -21449,10 +21610,7 @@ function updatePositions(params) {
21449
21610
  populateProbeTrailBuffer(buf, ctx.probeStore, ctx.chebStore ?? null, d.id, probeParentKey, jd);
21450
21611
  }
21451
21612
  if (probeParentChanged) d.parentId = probeParentKey;
21452
- if (!computed.has(probeParentKey)) {
21453
- const parentBody = ctx.bodies.bodiesById.get(probeParentKey) ?? bodyObjects.get(probeParentKey)?.body;
21454
- if (parentBody) computePosition(parentBody);
21455
- }
21613
+ placeFirst(probeParentKey);
21456
21614
  const probeParentPos = positionMap.get(probeParentKey);
21457
21615
  if (!probeParentPos) {
21458
21616
  hide();
@@ -21507,6 +21665,7 @@ function updatePositions(params) {
21507
21665
  }
21508
21666
  if (bo) bo.outOfRange = false;
21509
21667
  body.positionUnknown = false;
21668
+ body.placedJd = jd;
21510
21669
  body.position[0] = x;
21511
21670
  body.position[1] = y;
21512
21671
  body.position[2] = z;
@@ -22615,6 +22774,8 @@ var PromotionRegistry = class {
22615
22774
  defaults = /* @__PURE__ */ new Set();
22616
22775
  userPromoted = /* @__PURE__ */ new Set();
22617
22776
  autoPromoted = /* @__PURE__ */ new Set();
22777
+ pinned = /* @__PURE__ */ new Set();
22778
+ pinPromoted = /* @__PURE__ */ new Set();
22618
22779
  groupTargets = null;
22619
22780
  groupMode = "none";
22620
22781
  smallBodyZone = null;
@@ -22624,6 +22785,7 @@ var PromotionRegistry = class {
22624
22785
  this.deps = deps;
22625
22786
  deps.ctx.bodies.onBodiesAdded(() => {
22626
22787
  this.onBodiesAdded();
22788
+ this.buildPinned();
22627
22789
  this.autoPromoteAsteroidMoons();
22628
22790
  this.reevaluateEarthSatMode();
22629
22791
  if (this.smallBodyZone !== null) this.refreshSmallBodyEmphasis();
@@ -22813,6 +22975,31 @@ var PromotionRegistry = class {
22813
22975
  if (toRemove.length === 0) return;
22814
22976
  this.tearDownBodies(toRemove, this.autoPromoted);
22815
22977
  }
22978
+ setPinned(ids) {
22979
+ this.pinned = new Set(ids);
22980
+ const focusedId = this.deps.getFocusedId();
22981
+ const dropped = [...this.pinPromoted].filter((id) => !this.pinned.has(id));
22982
+ if (focusedId && dropped.includes(focusedId)) {
22983
+ this.pinPromoted.delete(focusedId);
22984
+ this.userPromoted.add(focusedId);
22985
+ this.emitUserPromotedCount();
22986
+ }
22987
+ this.tearDownBodies(dropped.filter((id) => id !== focusedId), this.pinPromoted);
22988
+ this.buildPinned();
22989
+ }
22990
+ buildPinned() {
22991
+ let built = false;
22992
+ for (const id of this.pinned) {
22993
+ const body = this.deps.ctx.getBody(id);
22994
+ if (!body || isSurfaceFeature(body) || body.data.objectType === ObjectType.MOON) continue;
22995
+ if (!this.buildBodyInstance(body)) continue;
22996
+ built = true;
22997
+ if (this.userPromoted.delete(id)) this.pinPromoted.add(id);
22998
+ }
22999
+ if (!built) return;
23000
+ this.emitUserPromotedCount();
23001
+ this.finalizeBuilds();
23002
+ }
22816
23003
  clearUserPromoted() {
22817
23004
  const focusedId = this.deps.getFocusedId();
22818
23005
  const toRemove = [...this.userPromoted].filter((id) => id !== focusedId);
@@ -23180,7 +23367,7 @@ function upgradeTargets(focus, ctx) {
23180
23367
  //#endregion
23181
23368
  //#region src/lib/scene/probe-coverage-watch.ts
23182
23369
  var STOP_INSET_JD = 1e-6;
23183
- function displayName(body) {
23370
+ function displayName$1(body) {
23184
23371
  return body.data.name ?? body.data.id;
23185
23372
  }
23186
23373
  var ProbeCoverageWatch = class {
@@ -23223,7 +23410,7 @@ var ProbeCoverageWatch = class {
23223
23410
  onHit(body, hitJd, forwardJd) {
23224
23411
  this.notices.notify({
23225
23412
  topic: "coverage-pause",
23226
- name: displayName(body),
23413
+ name: displayName$1(body),
23227
23414
  direction: hitJd >= forwardJd ? "forward" : "backward",
23228
23415
  jd: hitJd
23229
23416
  });
@@ -23299,10 +23486,14 @@ function rotateByQuaternion(q, x, y, z, out, at) {
23299
23486
  out[at + 1] = y + q.w * ty + (q.z * tx - q.x * tz);
23300
23487
  out[at + 2] = z + q.w * tz + (q.x * ty - q.y * tx);
23301
23488
  }
23489
+ function bodyCentre(body, ctx, jd) {
23490
+ return placeForRead(body, jd, ctx) ? body.position : null;
23491
+ }
23302
23492
  function resolveAnchor(anchor, ctx, jd) {
23303
23493
  const body = ctx.getBody(anchor.body);
23304
- if (!body) return null;
23305
- const [cx, cy, cz] = body.position;
23494
+ const centre = body && bodyCentre(body, ctx, jd);
23495
+ if (!centre) return null;
23496
+ const [cx, cy, cz] = centre;
23306
23497
  if (isSurface(anchor)) {
23307
23498
  const distance = kmToScene(effectiveRadiusKm(body.data) + (anchor.altitudeKm ?? 0));
23308
23499
  const [dx, dy, dz] = surfaceDirection(anchor.latitude, anchor.longitude);
@@ -23338,12 +23529,13 @@ function resolveAnchor(anchor, ctx, jd) {
23338
23529
  cz + oz
23339
23530
  ];
23340
23531
  }
23341
- function anchorNormal(anchor, world, ctx) {
23532
+ function anchorNormal(anchor, world, ctx, jd) {
23342
23533
  const body = ctx.getBody(anchor.body);
23343
- if (!body) return null;
23344
- const dx = world[0] - body.position[0];
23345
- const dy = world[1] - body.position[1];
23346
- const dz = world[2] - body.position[2];
23534
+ const centre = body && bodyCentre(body, ctx, jd);
23535
+ if (!centre) return null;
23536
+ const dx = world[0] - centre[0];
23537
+ const dy = world[1] - centre[1];
23538
+ const dz = world[2] - centre[2];
23347
23539
  const length = Math.hypot(dx, dy, dz);
23348
23540
  if (length === 0) return null;
23349
23541
  return [
@@ -23568,9 +23760,14 @@ var SceneRenderer = class SceneRenderer {
23568
23760
  focusDurationMs: FOCUS_DURATION_MS
23569
23761
  };
23570
23762
  lastUpdatedJd = NaN;
23763
+ get placedJd() {
23764
+ return this.lastUpdatedJd;
23765
+ }
23571
23766
  lastUpdatedSystemId = null;
23572
23767
  lastSeatConfigKey = null;
23573
23768
  lastProbeVersion = 0;
23769
+ disposed = false;
23770
+ lastHostReadVersion = 0;
23574
23771
  focusWasOutOfRange = false;
23575
23772
  _positionMapScratch = /* @__PURE__ */ new Map();
23576
23773
  frameIndex = 0;
@@ -23673,7 +23870,7 @@ var SceneRenderer = class SceneRenderer {
23673
23870
  this.orbitPreview = new OrbitPreviewOverlay(this.scene);
23674
23871
  this.orbitPreview.setLayer(SceneRenderer.MAP_LAYER);
23675
23872
  this.skyboxAdjuster.set(0, 0, 0);
23676
- loadSkybox(this.scene, this.renderer, ctx);
23873
+ loadSkybox(this.scene, this.renderer, ctx, () => this.disposed);
23677
23874
  const sunBody = ctx.bodies.majorBodies.find((b) => b.data.id === SUN_ID);
23678
23875
  const resident = ctx.getBody(initialView.id);
23679
23876
  const matchedBody = resident && !resident.positionUnknown ? resident : void 0;
@@ -24542,7 +24739,8 @@ var SceneRenderer = class SceneRenderer {
24542
24739
  const systemId = this.ctx.visibility.focusedSystemId;
24543
24740
  const seatKey = this.focusedSeatConfigKey();
24544
24741
  const probeVersion = this.ctx.probeStore?.version ?? 0;
24545
- if (this.clock.jd === this.lastUpdatedJd && systemId === this.lastUpdatedSystemId && seatKey === this.lastSeatConfigKey && probeVersion === this.lastProbeVersion) {
24742
+ const hostReadVersion = this.ctx.bodies.hostReadVersion;
24743
+ if (this.clock.jd === this.lastUpdatedJd && systemId === this.lastUpdatedSystemId && seatKey === this.lastSeatConfigKey && probeVersion === this.lastProbeVersion && hostReadVersion === this.lastHostReadVersion) {
24546
24744
  this.clock.seeked = false;
24547
24745
  return;
24548
24746
  }
@@ -24552,6 +24750,7 @@ var SceneRenderer = class SceneRenderer {
24552
24750
  this.lastUpdatedSystemId = systemId;
24553
24751
  this.lastSeatConfigKey = seatKey;
24554
24752
  this.lastProbeVersion = probeVersion;
24753
+ this.lastHostReadVersion = hostReadVersion;
24555
24754
  this.ctx.refreshTick(jdToDate(this.clock.jd));
24556
24755
  this.focusController.promotion.onSimTimeChanged();
24557
24756
  const result = updatePositions({
@@ -24737,6 +24936,10 @@ var SceneRenderer = class SceneRenderer {
24737
24936
  setBodyAppearance(id, appearance) {
24738
24937
  this.appearances.set(id, appearance);
24739
24938
  }
24939
+ setPinnedBodies(ids) {
24940
+ this.ctx.visibility.setPinned(ids);
24941
+ this.focusController.promotion.setPinned(ids);
24942
+ }
24740
24943
  setNorthReference(id) {
24741
24944
  this.cameraUp.setNorthReference(id);
24742
24945
  }
@@ -24887,7 +25090,7 @@ var SceneRenderer = class SceneRenderer {
24887
25090
  this.renderer.setPixelRatio(renderPixelRatio());
24888
25091
  this.composer.setPixelRatio(renderPixelRatio());
24889
25092
  this.resize(this.canvas.clientWidth, this.canvas.clientHeight);
24890
- loadSkybox(this.scene, this.renderer, this.ctx);
25093
+ loadSkybox(this.scene, this.renderer, this.ctx, () => this.disposed);
24891
25094
  this.resume();
24892
25095
  }
24893
25096
  recoverWorkersIfDead(timeoutMs) {
@@ -24918,6 +25121,7 @@ var SceneRenderer = class SceneRenderer {
24918
25121
  setTrailResolution(width, height);
24919
25122
  }
24920
25123
  dispose() {
25124
+ this.disposed = true;
24921
25125
  cancelAnimationFrame(this.rafId);
24922
25126
  this.pointerInteraction.detach();
24923
25127
  this.gpuPick.dispose();
@@ -25339,8 +25543,8 @@ var MarkerExtension = class {
25339
25543
  this.label.visible = this.occlude ? this.facesCamera(world, frame) : true;
25340
25544
  this.object.updateMatrixWorld(true);
25341
25545
  }
25342
- facesCamera(world, { camera, ctx }) {
25343
- const normal = anchorNormal(this.anchor, world, ctx);
25546
+ facesCamera(world, { camera, ctx, jd }) {
25547
+ const normal = anchorNormal(this.anchor, world, ctx, jd);
25344
25548
  if (!normal) return true;
25345
25549
  const { position } = this.label;
25346
25550
  const dx = camera.position.x - position.x;
@@ -25354,6 +25558,153 @@ var MarkerExtension = class {
25354
25558
  }
25355
25559
  };
25356
25560
 
25561
+ //#endregion
25562
+ //#region src/lib/scene/extensions/screen.ts
25563
+ var REACH = 3;
25564
+ var right = new Vector3();
25565
+ var up = new Vector3();
25566
+ var forward = new Vector3();
25567
+ function parseDash(pattern) {
25568
+ const [dash, gap = dash] = (pattern ?? "").split(/[\s,]+/).filter(Boolean).map(Number);
25569
+ return dash > 0 && gap > 0 ? [dash, gap] : null;
25570
+ }
25571
+ function aim(camera, viewportPx) {
25572
+ right.set(1, 0, 0).applyQuaternion(camera.quaternion);
25573
+ up.set(0, 1, 0).applyQuaternion(camera.quaternion);
25574
+ forward.set(0, 0, -1).applyQuaternion(camera.quaternion);
25575
+ return viewportPx / (2 * Math.tan(camera.getEffectiveFOV() * Math.PI / 360));
25576
+ }
25577
+ function view(points, i, camera, out) {
25578
+ const x = points[i * 3] - camera.position.x;
25579
+ const y = points[i * 3 + 1] - camera.position.y;
25580
+ const z = points[i * 3 + 2] - camera.position.z;
25581
+ out[0] = x * right.x + y * right.y + z * right.z;
25582
+ out[1] = x * up.x + y * up.y + z * up.z;
25583
+ out[2] = x * forward.x + y * forward.y + z * forward.z;
25584
+ }
25585
+ function reachesBehind(points, count, camera) {
25586
+ forward.set(0, 0, -1).applyQuaternion(camera.quaternion);
25587
+ const { x, y, z } = camera.position;
25588
+ for (let i = 0; i < count; i++) if ((points[i * 3] - x) * forward.x + (points[i * 3 + 1] - y) * forward.y + (points[i * 3 + 2] - z) * forward.z < camera.near * 2) return true;
25589
+ return false;
25590
+ }
25591
+ function screenLengths(points, count, camera, viewportPx, out) {
25592
+ const focal = aim(camera, viewportPx);
25593
+ const at = [
25594
+ 0,
25595
+ 0,
25596
+ 0
25597
+ ];
25598
+ let length = 0;
25599
+ let lastX = 0;
25600
+ let lastY = 0;
25601
+ let seen = false;
25602
+ for (let i = 0; i < count; i++) {
25603
+ view(points, i, camera, at);
25604
+ if (at[2] > camera.near) {
25605
+ const px = at[0] / at[2] * focal;
25606
+ const py = at[1] / at[2] * focal;
25607
+ if (seen) length += Math.hypot(px - lastX, py - lastY);
25608
+ lastX = px;
25609
+ lastY = py;
25610
+ seen = true;
25611
+ }
25612
+ out[i] = length;
25613
+ }
25614
+ }
25615
+ var SeenLine = class {
25616
+ points = new Float32Array(0);
25617
+ alphas = new Float32Array(0);
25618
+ lengths = new Float32Array(0);
25619
+ count = 0;
25620
+ a = [
25621
+ 0,
25622
+ 0,
25623
+ 0
25624
+ ];
25625
+ b = [
25626
+ 0,
25627
+ 0,
25628
+ 0
25629
+ ];
25630
+ see(points, alphas, count, camera, viewportPx) {
25631
+ if (this.alphas.length < count * 4) {
25632
+ this.points = new Float32Array(count * 12);
25633
+ this.alphas = new Float32Array(count * 4);
25634
+ this.lengths = new Float32Array(count * 4);
25635
+ }
25636
+ this.count = 0;
25637
+ const focal = aim(camera, viewportPx);
25638
+ const halfW = viewportPx * camera.aspect * REACH / 2;
25639
+ const halfH = viewportPx * REACH / 2;
25640
+ const near = camera.near * 2;
25641
+ let { a, b } = this;
25642
+ let joined = false;
25643
+ let length = 0;
25644
+ let lastX = 0;
25645
+ let lastY = 0;
25646
+ if (count > 0) view(points, 0, camera, a);
25647
+ for (let i = 0; i + 1 < count; i++, [a, b] = [b, a]) {
25648
+ view(points, i + 1, camera, b);
25649
+ let from = 0;
25650
+ let to = 1;
25651
+ for (let plane = 0; plane < 5 && from <= to; plane++) {
25652
+ const ga = edge(plane, a, focal, halfW, halfH, near);
25653
+ const gb = edge(plane, b, focal, halfW, halfH, near);
25654
+ if (ga < 0 && gb < 0) to = -1;
25655
+ else if (ga < 0) from = Math.max(from, ga / (ga - gb));
25656
+ else if (gb < 0) to = Math.min(to, ga / (ga - gb));
25657
+ }
25658
+ if (from > to) {
25659
+ joined = false;
25660
+ continue;
25661
+ }
25662
+ const depth = a[2] + (b[2] - a[2]) * from;
25663
+ const x = (a[0] + (b[0] - a[0]) * from) / depth * focal;
25664
+ const y = (a[1] + (b[1] - a[1]) * from) / depth * focal;
25665
+ if (!joined || from > 0) {
25666
+ if (this.count > 0) {
25667
+ this.copy(this.count - 1, 0);
25668
+ this.put(points, alphas, i, from, length, 0);
25669
+ }
25670
+ this.put(points, alphas, i, from, length, 1);
25671
+ lastX = x;
25672
+ lastY = y;
25673
+ }
25674
+ const endDepth = a[2] + (b[2] - a[2]) * to;
25675
+ const endX = (a[0] + (b[0] - a[0]) * to) / endDepth * focal;
25676
+ const endY = (a[1] + (b[1] - a[1]) * to) / endDepth * focal;
25677
+ length += Math.hypot(endX - lastX, endY - lastY);
25678
+ lastX = endX;
25679
+ lastY = endY;
25680
+ this.put(points, alphas, i, to, length, 1);
25681
+ joined = to === 1;
25682
+ }
25683
+ }
25684
+ put(points, alphas, i, t, length, shown) {
25685
+ const at = this.count++;
25686
+ for (let k = 0; k < 3; k++) {
25687
+ const from = points[i * 3 + k];
25688
+ this.points[at * 3 + k] = from + (points[i * 3 + 3 + k] - from) * t;
25689
+ }
25690
+ this.alphas[at] = (alphas[i] + (alphas[i + 1] - alphas[i]) * t) * shown;
25691
+ this.lengths[at] = length;
25692
+ }
25693
+ copy(from, shown) {
25694
+ const at = this.count++;
25695
+ this.points.copyWithin(at * 3, from * 3, from * 3 + 3);
25696
+ this.alphas[at] = this.alphas[from] * shown;
25697
+ this.lengths[at] = this.lengths[from];
25698
+ }
25699
+ };
25700
+ function edge(plane, at, focal, halfW, halfH, near) {
25701
+ if (plane === 0) return at[2] - near;
25702
+ if (plane === 1) return halfW * at[2] - focal * at[0];
25703
+ if (plane === 2) return halfW * at[2] + focal * at[0];
25704
+ if (plane === 3) return halfH * at[2] - focal * at[1];
25705
+ return halfH * at[2] + focal * at[1];
25706
+ }
25707
+
25357
25708
  //#endregion
25358
25709
  //#region src/lib/scene/extensions/polyline.ts
25359
25710
  var REDRAW_EPSILON$1 = 1e-9;
@@ -25363,6 +25714,7 @@ var PolylineExtension = class {
25363
25714
  color;
25364
25715
  widthPx;
25365
25716
  opacity;
25717
+ dash;
25366
25718
  fade;
25367
25719
  closed;
25368
25720
  local;
@@ -25370,6 +25722,7 @@ var PolylineExtension = class {
25370
25722
  mesh = null;
25371
25723
  scratch = new Float32Array(0);
25372
25724
  alphas = new Float32Array(0);
25725
+ seen = new SeenLine();
25373
25726
  capacity = 0;
25374
25727
  lastOrigin = [
25375
25728
  0,
@@ -25384,6 +25737,7 @@ var PolylineExtension = class {
25384
25737
  this.color = options.color ?? "#ffffff";
25385
25738
  this.widthPx = options.widthPx ?? 2;
25386
25739
  this.opacity = options.opacity ?? 1;
25740
+ this.dash = parseDash(options.dash);
25387
25741
  this.fade = options.fade ?? false;
25388
25742
  this.closed = options.closed ?? false;
25389
25743
  this.local = new Float64Array(0);
@@ -25404,6 +25758,10 @@ var PolylineExtension = class {
25404
25758
  }
25405
25759
  if (count > points.length) this.local.copyWithin(points.length * 3, 0, 3);
25406
25760
  this.count = count;
25761
+ if (this.alphas.length < count) {
25762
+ this.scratch = new Float32Array(count * 3);
25763
+ this.alphas = new Float32Array(count);
25764
+ }
25407
25765
  if (count > this.capacity) this.rebuild(count);
25408
25766
  this.fillAlphas();
25409
25767
  this.dirty = true;
@@ -25422,38 +25780,35 @@ var PolylineExtension = class {
25422
25780
  this.removeSelf?.();
25423
25781
  this.removeSelf = null;
25424
25782
  }
25425
- update({ jd, basis, camera, ctx }) {
25426
- const mesh = this.mesh;
25427
- if (!mesh) return;
25783
+ update({ jd, basis, camera, viewportPx, ctx }) {
25428
25784
  const world = this.wanted && this.count >= 2 ? resolveAnchor(this.anchor, ctx, jd) : null;
25429
- mesh.visible = world !== null;
25430
- if (!world) return;
25785
+ if (this.mesh) this.mesh.visible = world !== null;
25786
+ if (!this.mesh || !world) return;
25431
25787
  const origin = [
25432
25788
  world[0] - basis[0],
25433
25789
  world[1] - basis[1],
25434
25790
  world[2] - basis[2]
25435
25791
  ];
25436
- if (this.dirty || Math.abs(origin[0] - this.lastOrigin[0]) > REDRAW_EPSILON$1 || Math.abs(origin[1] - this.lastOrigin[1]) > REDRAW_EPSILON$1 || Math.abs(origin[2] - this.lastOrigin[2]) > REDRAW_EPSILON$1) {
25437
- this.writeVertices(mesh, origin);
25792
+ const moved = this.dirty || Math.abs(origin[0] - this.lastOrigin[0]) > REDRAW_EPSILON$1 || Math.abs(origin[1] - this.lastOrigin[1]) > REDRAW_EPSILON$1 || Math.abs(origin[2] - this.lastOrigin[2]) > REDRAW_EPSILON$1;
25793
+ if (moved) {
25794
+ for (let i = 0; i < this.count * 3; i++) this.scratch[i] = this.local[i] + origin[i % 3];
25438
25795
  this.lastOrigin = origin;
25439
- this.dirty = false;
25440
25796
  }
25441
- mesh.material.uniforms.uCenterOffset.value.copy(camera.position).negate();
25442
- }
25443
- writeVertices(mesh, origin) {
25444
- for (let i = 0; i < this.count; i++) {
25445
- this.scratch[i * 3] = this.local[i * 3] + origin[0];
25446
- this.scratch[i * 3 + 1] = this.local[i * 3 + 1] + origin[1];
25447
- this.scratch[i * 3 + 2] = this.local[i * 3 + 2] + origin[2];
25448
- }
25449
- writeFatTrailVertices(mesh.geometry, this.scratch, this.alphas, this.alphas, this.count);
25797
+ const cut = this.dash !== null || reachesBehind(this.scratch, this.count, camera);
25798
+ if (cut) {
25799
+ const { seen } = this;
25800
+ seen.see(this.scratch, this.alphas, this.count, camera, viewportPx);
25801
+ if (seen.count > this.capacity) this.rebuild(seen.count);
25802
+ writeFatTrailVertices(this.mesh.geometry, seen.points, seen.alphas, seen.alphas, seen.count);
25803
+ if (this.dash) writeLineDistances(this.mesh.geometry, seen.lengths, seen.count);
25804
+ } else if (moved) writeFatTrailVertices(this.mesh.geometry, this.scratch, this.alphas, this.alphas, this.count);
25805
+ this.dirty = cut;
25806
+ this.mesh.material.uniforms.uCenterOffset.value.copy(camera.position).negate();
25450
25807
  }
25451
25808
  rebuild(capacity) {
25452
25809
  this.disposeMesh();
25453
25810
  this.dirty = true;
25454
- this.scratch = new Float32Array(capacity * 3);
25455
- this.alphas = new Float32Array(capacity);
25456
- const mesh = buildFatLineFromThin(capacity, this.scratch, this.alphas, this.alphas, capacity, this.color, this.widthPx, 1);
25811
+ const mesh = buildFatLineFromThin(capacity, this.scratch, this.alphas, this.alphas, 0, this.color, this.widthPx, 1, this.dash ?? void 0);
25457
25812
  mesh.frustumCulled = false;
25458
25813
  this.object.add(mesh);
25459
25814
  this.mesh = mesh;
@@ -26642,6 +26997,7 @@ var SurfaceShapeExtension = class {
26642
26997
  color;
26643
26998
  widthPx;
26644
26999
  opacity;
27000
+ dash;
26645
27001
  fillColor;
26646
27002
  fillOpacity;
26647
27003
  dirs = new Float64Array(0);
@@ -26649,6 +27005,7 @@ var SurfaceShapeExtension = class {
26649
27005
  line = null;
26650
27006
  scratch = new Float32Array(0);
26651
27007
  alphas = new Float32Array(0);
27008
+ lengths = new Float32Array(0);
26652
27009
  fanDirs = new Float64Array(0);
26653
27010
  fill = null;
26654
27011
  fillScratch = new Float32Array(0);
@@ -26668,6 +27025,7 @@ var SurfaceShapeExtension = class {
26668
27025
  this.color = options.color ?? "#ffffff";
26669
27026
  this.widthPx = options.widthPx ?? 2;
26670
27027
  this.opacity = options.opacity ?? 1;
27028
+ this.dash = parseDash(options.dash);
26671
27029
  this.fillColor = options.fill ?? null;
26672
27030
  this.fillOpacity = options.fillOpacity ?? .25;
26673
27031
  this.object.matrixAutoUpdate = false;
@@ -26700,22 +27058,27 @@ var SurfaceShapeExtension = class {
26700
27058
  this.removeSelf?.();
26701
27059
  this.removeSelf = null;
26702
27060
  }
26703
- update({ jd, basis, camera, ctx }) {
27061
+ update({ jd, basis, camera, viewportPx, ctx }) {
26704
27062
  const body = this.wanted && this.count >= 2 ? ctx.getBody(this.body) : void 0;
26705
- if (this.line) this.line.visible = body !== void 0;
26706
- if (this.fill) this.fill.visible = body !== void 0;
26707
- if (!body) return;
27063
+ const centre = body && bodyCentre(body, ctx, jd);
27064
+ if (this.line) this.line.visible = !!centre;
27065
+ if (this.fill) this.fill.visible = !!centre;
27066
+ if (!body || !centre) return;
26708
27067
  const radiusKm = effectiveRadiusKm(body.data);
26709
27068
  const scale = kmToScene(radiusKm + (this.altitudeKm ?? radiusKm * DEFAULT_LIFT));
26710
27069
  const q = body.orientation ? bodyQuaternion(body.orientation, jd, body.nutPrec) : null;
26711
27070
  const origin = [
26712
- body.position[0] - basis[0],
26713
- body.position[1] - basis[1],
26714
- body.position[2] - basis[2]
27071
+ centre[0] - basis[0],
27072
+ centre[1] - basis[1],
27073
+ centre[2] - basis[2]
26715
27074
  ];
26716
27075
  if (this.line) {
26717
27076
  this.place(this.dirs, this.count, q, scale, origin, this.scratch);
26718
27077
  writeFatTrailVertices(this.line.geometry, this.scratch, this.alphas, this.alphas, this.count);
27078
+ if (this.dash) {
27079
+ screenLengths(this.scratch, this.count, camera, viewportPx, this.lengths);
27080
+ writeLineDistances(this.line.geometry, this.lengths, this.count);
27081
+ }
26719
27082
  this.line.material.uniforms.uCenterOffset.value.copy(camera.position).negate();
26720
27083
  }
26721
27084
  if (this.fill) {
@@ -26744,7 +27107,8 @@ var SurfaceShapeExtension = class {
26744
27107
  if (this.count < 2 || this.widthPx <= 0) return;
26745
27108
  this.scratch = new Float32Array(this.count * 3);
26746
27109
  this.alphas = new Float32Array(this.count).fill(this.opacity);
26747
- const line = buildFatLineFromThin(this.count, this.scratch, this.alphas, this.alphas, this.count, this.color, this.widthPx, 1);
27110
+ this.lengths = new Float32Array(this.count);
27111
+ const line = buildFatLineFromThin(this.count, this.scratch, this.alphas, this.alphas, this.count, this.color, this.widthPx, 1, this.dash ?? void 0);
26748
27112
  line.frustumCulled = false;
26749
27113
  line.renderOrder = SURFACE_RENDER_ORDER;
26750
27114
  this.object.add(line);
@@ -26941,6 +27305,50 @@ var MapObjectCollection = class {
26941
27305
  }
26942
27306
  };
26943
27307
 
27308
+ //#endregion
27309
+ //#region src/lib/scene/extensions/measure.ts
27310
+ function anchorOffsetKm(a, b, ctx, jd) {
27311
+ const from = resolveAnchor(a, ctx, jd);
27312
+ const to = resolveAnchor(b, ctx, jd);
27313
+ if (!from || !to) return null;
27314
+ return sceneToEcliptic([
27315
+ to[0] - from[0],
27316
+ to[1] - from[1],
27317
+ to[2] - from[2]
27318
+ ]);
27319
+ }
27320
+ function anchorDistanceKm(a, b, ctx, jd) {
27321
+ const offset = anchorOffsetKm(a, b, ctx, jd);
27322
+ return offset && Math.hypot(...offset);
27323
+ }
27324
+ function subsolarPoint(id, ctx, jd) {
27325
+ const body = ctx.getBody(id);
27326
+ const sun = ctx.getBody(SUN_ID);
27327
+ if (!body || !sun || body === sun) return null;
27328
+ if (!body.orientation && !ctx.bodies.orientationRead.has(id)) return null;
27329
+ const centre = bodyCentre(body, ctx, jd);
27330
+ if (!centre) return null;
27331
+ const turn = body.orientation ? bodyQuaternion(body.orientation, jd, body.nutPrec).toArray() : [
27332
+ 0,
27333
+ 0,
27334
+ 0,
27335
+ 1
27336
+ ];
27337
+ const { latitude, longitude, distance } = cartesianToSpherical([
27338
+ sun.position[0],
27339
+ sun.position[1],
27340
+ sun.position[2]
27341
+ ], [
27342
+ centre[0],
27343
+ centre[1],
27344
+ centre[2]
27345
+ ], turn);
27346
+ return distance > 0 ? {
27347
+ lon: longitude,
27348
+ lat: latitude
27349
+ } : null;
27350
+ }
27351
+
26944
27352
  //#endregion
26945
27353
  //#region src/lib/interaction/gesture.ts
26946
27354
  var GestureHandler = class {
@@ -27121,7 +27529,8 @@ var KEY_ROTATE_RAD = .05;
27121
27529
  var KEY_ZOOM_FACTOR = 1.15;
27122
27530
  var CONTEXT_LOST_PANEL_DELAY_MS = 2e3;
27123
27531
  var RESTORE_PING_TIMEOUT_MS = 4e3;
27124
- var onceTarget$1 = /* @__PURE__ */ new WeakMap();
27532
+ var mapsIn = /* @__PURE__ */ new WeakMap();
27533
+ var onceTarget$2 = /* @__PURE__ */ new WeakMap();
27125
27534
  var SpaceMap = class {
27126
27535
  ctx = new ContextManager();
27127
27536
  clock;
@@ -27222,14 +27631,14 @@ var SpaceMap = class {
27222
27631
  off();
27223
27632
  listener(...args);
27224
27633
  };
27225
- onceTarget$1.set(wrapped, listener);
27634
+ onceTarget$2.set(wrapped, listener);
27226
27635
  const off = this.on(event, wrapped);
27227
27636
  return off;
27228
27637
  }
27229
27638
  off(event, listener) {
27230
27639
  const set = this.listeners[event];
27231
27640
  set.delete(listener);
27232
- for (const held of set) if (onceTarget$1.get(held) === listener) set.delete(held);
27641
+ for (const held of set) if (onceTarget$2.get(held) === listener) set.delete(held);
27233
27642
  }
27234
27643
  emit(event, ...args) {
27235
27644
  for (const listener of this.listeners[event]) listener(...args);
@@ -27247,6 +27656,19 @@ var SpaceMap = class {
27247
27656
  getChildren(id) {
27248
27657
  return [...this.ctx.bodies.getChildren(id) ?? []];
27249
27658
  }
27659
+ get placedJd() {
27660
+ const jd = this.renderer?.placedJd;
27661
+ return jd === void 0 || Number.isNaN(jd) ? this.clock.jd : jd;
27662
+ }
27663
+ distanceKm(from, to) {
27664
+ return anchorDistanceKm(from, to, this.ctx, this.placedJd);
27665
+ }
27666
+ offsetKm(from, to) {
27667
+ return anchorOffsetKm(from, to, this.ctx, this.placedJd);
27668
+ }
27669
+ getSubsolarPoint(id) {
27670
+ return subsolarPoint(id, this.ctx, this.placedJd);
27671
+ }
27250
27672
  load(targetId = this.initialView.id) {
27251
27673
  return this.ctx.load(jdToDate(this.clock.jd), targetId);
27252
27674
  }
@@ -27278,6 +27700,7 @@ var SpaceMap = class {
27278
27700
  if (this.container) throw new Error("SpaceMap is already mounted");
27279
27701
  this.container = container;
27280
27702
  container.classList.add("sm-map");
27703
+ mapsIn.set(container, (mapsIn.get(container) ?? 0) + 1);
27281
27704
  this.controls = new ControlHost(this, container);
27282
27705
  const canvas = document.createElement("canvas");
27283
27706
  canvas.className = "sm-map__canvas";
@@ -27357,7 +27780,9 @@ var SpaceMap = class {
27357
27780
  this.renderer = null;
27358
27781
  canvas.remove();
27359
27782
  this.labelLayer?.remove();
27360
- container.classList.remove("sm-map");
27783
+ const left = (mapsIn.get(container) ?? 1) - 1;
27784
+ mapsIn.set(container, left);
27785
+ if (left === 0) container.classList.remove("sm-map");
27361
27786
  this.container = this.canvas = this.labelLayer = null;
27362
27787
  }
27363
27788
  watch(renderer) {
@@ -27722,6 +28147,10 @@ var SpaceMap = class {
27722
28147
  this.requireRenderer().setBodyAppearance(id, appearance);
27723
28148
  return this;
27724
28149
  }
28150
+ setPinnedBodies(ids) {
28151
+ this.requireRenderer().setPinnedBodies(ids);
28152
+ return this;
28153
+ }
27725
28154
  capRadiusDeg(options) {
27726
28155
  if (options.radiusDeg !== void 0) return options.radiusDeg;
27727
28156
  const bodyRadiusKm = this.getBody(options.body)?.radiusKm;
@@ -27778,7 +28207,7 @@ var SpaceMap = class {
27778
28207
 
27779
28208
  //#endregion
27780
28209
  //#region src/lib/flatmap/layers.ts
27781
- var SVG_NS = "http://www.w3.org/2000/svg";
28210
+ var SVG_NS$1 = "http://www.w3.org/2000/svg";
27782
28211
  function layerInfo(layer) {
27783
28212
  return {
27784
28213
  id: layer.id,
@@ -27796,7 +28225,7 @@ function creditOf(bundle) {
27796
28225
  };
27797
28226
  }
27798
28227
  function pathElement(d, className) {
27799
- const path = document.createElementNS(SVG_NS, "path");
28228
+ const path = document.createElementNS(SVG_NS$1, "path");
27800
28229
  path.setAttribute("d", d);
27801
28230
  path.setAttribute("class", className);
27802
28231
  return path;
@@ -27815,7 +28244,7 @@ function graticuleLayer(label, stepDeg = 30) {
27815
28244
  if (viewport.projection.azimuthal) {
27816
28245
  const centre = viewport.toScreen(0, 0);
27817
28246
  const edge = viewport.toScreen(viewport.projection.extent.maxX, 0);
27818
- const circle = document.createElementNS(SVG_NS, "circle");
28247
+ const circle = document.createElementNS(SVG_NS$1, "circle");
27819
28248
  circle.setAttribute("cx", String(centre[0]));
27820
28249
  circle.setAttribute("cy", String(centre[1]));
27821
28250
  circle.setAttribute("r", String(Math.abs(edge[0] - centre[0])));
@@ -27852,12 +28281,12 @@ function nomenclatureLayer(bodyId, label) {
27852
28281
  for (const shift of shifts) {
27853
28282
  const x = at[0] + shift;
27854
28283
  if (x < 0 || x > viewport.width) continue;
27855
- const dot = document.createElementNS(SVG_NS, "circle");
28284
+ const dot = document.createElementNS(SVG_NS$1, "circle");
27856
28285
  dot.setAttribute("cx", x.toFixed(1));
27857
28286
  dot.setAttribute("cy", at[1].toFixed(1));
27858
28287
  dot.setAttribute("r", "1.6");
27859
28288
  dot.setAttribute("class", "sm-flat__feature-dot");
27860
- const text = document.createElementNS(SVG_NS, "text");
28289
+ const text = document.createElementNS(SVG_NS$1, "text");
27861
28290
  text.setAttribute("x", (x + 4).toFixed(1));
27862
28291
  text.setAttribute("y", (at[1] + 3).toFixed(1));
27863
28292
  text.setAttribute("class", "sm-flat__feature-label");
@@ -27904,12 +28333,12 @@ var PathDrawing = class {
27904
28333
  this.onRemove = onRemove;
27905
28334
  this.onChange = onChange;
27906
28335
  this.points = points;
27907
- this.node = document.createElementNS(SVG_NS, "g");
28336
+ this.node = document.createElementNS(SVG_NS$1, "g");
27908
28337
  applyStyle(this.node, style);
27909
28338
  bindEvents(this.node, style);
27910
- this.area = closed ? document.createElementNS(SVG_NS, "path") : null;
28339
+ this.area = closed ? document.createElementNS(SVG_NS$1, "path") : null;
27911
28340
  this.area?.setAttribute("stroke", "none");
27912
- this.edge = document.createElementNS(SVG_NS, "path");
28341
+ this.edge = document.createElementNS(SVG_NS$1, "path");
27913
28342
  this.edge.setAttribute("fill", "none");
27914
28343
  if (this.area) this.node.append(this.area);
27915
28344
  this.node.append(this.edge);
@@ -28230,27 +28659,33 @@ async function fetchJson(url) {
28230
28659
  return null;
28231
28660
  }
28232
28661
  }
28233
- async function fromBundle(bodyId) {
28234
- const meta = await fetchJson(versionedUrl(`/v1/textures/${bodyId}/metadata.json`, "textures"));
28235
- if (!meta) return {};
28236
- if (!textureAllowed(meta.distribution)) return {};
28237
- const tiers = Object.keys(meta.exports ?? {}).filter((k) => [
28238
- "low",
28239
- "medium",
28240
- "high"
28241
- ].includes(k));
28242
- if (tiers.length === 0) return {};
28243
- return { surface: {
28662
+ async function tiersOf(bundleId) {
28663
+ 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)));
28664
+ return TIER_ORDER.filter((_, i) => found[i]);
28665
+ }
28666
+ async function fromRecord(bodyId, record, tiers = tiersOf) {
28667
+ if (!record?.map_texture_available || !record.texture) return {};
28668
+ const surface = pickTexture([{
28669
+ ...record.texture,
28244
28670
  id: bodyId,
28245
- tiers,
28246
- source: String(meta.source ?? ""),
28247
- organisation: String(meta.organisation ?? ""),
28248
- type: String(meta.type ?? "cylindrical"),
28249
- attribution: meta.attribution,
28250
- description: meta.description,
28251
- license: meta.license,
28252
- distribution: meta.distribution
28253
- } };
28671
+ tiers: void 0
28672
+ }, ...record.alternates ?? []]);
28673
+ if (!surface) return {};
28674
+ const written = surface.tiers ?? await tiers(surface.id);
28675
+ if (written.length === 0) return {};
28676
+ const { frames, ...bundle } = surface;
28677
+ const radii = record.radii;
28678
+ return {
28679
+ surface: {
28680
+ ...bundle,
28681
+ tiers: written,
28682
+ monthlyFrames: frames
28683
+ },
28684
+ radiusKm: radii ? (radii.a + radii.b + radii.c) / 3 : void 0
28685
+ };
28686
+ }
28687
+ async function fromBundle(bodyId) {
28688
+ return fromRecord(bodyId, (await fetchObjectDetail(bodyId, false).catch(() => null))?.global);
28254
28689
  }
28255
28690
  async function loadBodySources(bodyId) {
28256
28691
  const systemId = systemIdFor(bodyId);
@@ -28551,14 +28986,14 @@ var RASTER_LAYERS = [
28551
28986
  function isRaster(layer) {
28552
28987
  return layer.kind === "raster";
28553
28988
  }
28554
- var onceTarget = /* @__PURE__ */ new WeakMap();
28989
+ var onceTarget$1 = /* @__PURE__ */ new WeakMap();
28555
28990
  var FlatMap = class {
28556
28991
  container = null;
28557
28992
  root = document.createElement("div");
28558
28993
  canvas = document.createElement("canvas");
28559
- svg = document.createElementNS(SVG_NS, "svg");
28560
- layerGroup = document.createElementNS(SVG_NS, "g");
28561
- shapeGroup = document.createElementNS(SVG_NS, "g");
28994
+ svg = document.createElementNS(SVG_NS$1, "svg");
28995
+ layerGroup = document.createElementNS(SVG_NS$1, "g");
28996
+ shapeGroup = document.createElementNS(SVG_NS$1, "g");
28562
28997
  markerLayer = document.createElement("div");
28563
28998
  ctx;
28564
28999
  overlay;
@@ -28929,7 +29364,7 @@ var FlatMap = class {
28929
29364
  off();
28930
29365
  listener(...args);
28931
29366
  });
28932
- onceTarget.set(wrapped, listener);
29367
+ onceTarget$1.set(wrapped, listener);
28933
29368
  const off = this.on(event, wrapped);
28934
29369
  return off;
28935
29370
  }
@@ -28937,7 +29372,7 @@ var FlatMap = class {
28937
29372
  const set = this.listeners.get(event);
28938
29373
  if (!set) return;
28939
29374
  set.delete(listener);
28940
- for (const held of set) if (onceTarget.get(held) === listener) set.delete(held);
29375
+ for (const held of set) if (onceTarget$1.get(held) === listener) set.delete(held);
28941
29376
  }
28942
29377
  emit(event, ...args) {
28943
29378
  const set = this.listeners.get(event);
@@ -29141,7 +29576,7 @@ var FlatMap = class {
29141
29576
  this.layerGroup.replaceChildren();
29142
29577
  for (const layer of this.layerList) {
29143
29578
  if (isRaster(layer) || !layer.visible) continue;
29144
- const group = document.createElementNS(SVG_NS, "g");
29579
+ const group = document.createElementNS(SVG_NS$1, "g");
29145
29580
  group.setAttribute("data-layer", layer.id);
29146
29581
  layer.render(viewport, group);
29147
29582
  this.layerGroup.append(group);
@@ -30074,6 +30509,1455 @@ var PanoramaView = class {
30074
30509
  };
30075
30510
  };
30076
30511
 
30512
+ //#endregion
30513
+ //#region src/lib/fetch/groups/planetary-systems-map.ts
30514
+ var fetchPlanetarySystemsMap = memoizedGzJson(() => `${dataBase()}/v1/groups/__planetary_systems_map__.json.gz`, { error: (res) => `Failed to fetch planetary systems map: ${res.status}` });
30515
+
30516
+ //#endregion
30517
+ //#region src/lib/fetch/groups/solar-system-map.ts
30518
+ var fetchSolarSystemMap = memoizedGzJson(() => `${dataBase()}/v1/groups/__solar_system_map__.json.gz`, { error: (res) => `Failed to fetch solar system map: ${res.status}` });
30519
+
30520
+ //#endregion
30521
+ //#region src/lib/charts/scrub.ts
30522
+ var DRAG_SLOP = 8;
30523
+ function createScrub({ onScrub, onEnd }) {
30524
+ let downX = null;
30525
+ let downY = 0;
30526
+ let scrubbing = false;
30527
+ function end() {
30528
+ if (scrubbing) onEnd();
30529
+ downX = null;
30530
+ scrubbing = false;
30531
+ }
30532
+ return {
30533
+ onpointerdown(e) {
30534
+ if (e.pointerType === "mouse") return;
30535
+ downX = e.clientX;
30536
+ downY = e.clientY;
30537
+ scrubbing = false;
30538
+ },
30539
+ onpointermove(e) {
30540
+ if (e.pointerType === "mouse" || downX === null) return;
30541
+ if (!scrubbing && Math.hypot(e.clientX - downX, e.clientY - downY) < 8) return;
30542
+ scrubbing = true;
30543
+ onScrub(e.clientX, e.clientY);
30544
+ },
30545
+ onpointerup: end,
30546
+ onpointercancel: end,
30547
+ onpointerleave: end
30548
+ };
30549
+ }
30550
+
30551
+ //#endregion
30552
+ //#region src/lib/systemmap/layout.ts
30553
+ var VIEW_H = 240;
30554
+ var X_LEFT = 44;
30555
+ var X_RIGHT = 700;
30556
+ var CY = 120;
30557
+ var MIN_R = 2.2;
30558
+ var MAX_OFFSET = 84;
30559
+ var PRIMARY_LIMB_X = 44 - 4;
30560
+ var MOON_GAP = 4;
30561
+ var MOON_SPACING = 3;
30562
+ var HIT_MARGIN = 16;
30563
+ var INSIDE = 1e4;
30564
+ var MOON_ZONE_W = 34;
30565
+ var MOON_ZONE_PAD = 7;
30566
+ var RING_FORESHORTEN = .42;
30567
+ var BAND_Y = 26;
30568
+ var BAND_H = 240 - 60;
30569
+ var BAND_MIN_HIT = 30;
30570
+ function minDistance(cx, cy, placed) {
30571
+ let min = Infinity;
30572
+ for (const p of placed) min = Math.min(min, Math.hypot(p.cx - cx, p.cy - cy));
30573
+ return min;
30574
+ }
30575
+ function foldTilt(tiltDeg) {
30576
+ return tiltDeg > 90 ? 180 - tiltDeg : tiltDeg;
30577
+ }
30578
+ function layout(model) {
30579
+ const logLo = Math.log10(model.domain[0]);
30580
+ const logSpan = Math.log10(model.domain[1]) - logLo;
30581
+ const xOf = (km) => {
30582
+ return 44 + (Math.log10(Math.max(km / model.unitKm, 1e-9)) - logLo) / logSpan * (700 - 44);
30583
+ };
30584
+ const bodies = [];
30585
+ for (const b of [...model.bodies].sort((a, b) => a.aKm - b.aKm)) {
30586
+ const cx = xOf(b.aKm);
30587
+ const r = Math.max(MIN_R, b.radiusKm * model.pxPerKm);
30588
+ let cy = 120;
30589
+ if (b.tiltDeg > 0) {
30590
+ const offset = Math.min(foldTilt(b.tiltDeg) * model.pxPerDeg, MAX_OFFSET);
30591
+ const up = 120 - offset;
30592
+ const down = 120 + offset;
30593
+ cy = minDistance(cx, up, bodies) >= minDistance(cx, down, bodies) ? up : down;
30594
+ }
30595
+ bodies.push({
30596
+ ...b,
30597
+ cx,
30598
+ cy,
30599
+ r
30600
+ });
30601
+ }
30602
+ const moonZones = [];
30603
+ for (const parent of bodies) {
30604
+ if (!parent.satellites?.length) continue;
30605
+ const ringTop = parent.rings ? parent.r * parent.rings.outer * RING_FORESHORTEN : 0;
30606
+ let y = parent.cy - Math.max(parent.r, ringTop) - MOON_GAP;
30607
+ const moons = [];
30608
+ for (const s of [...parent.satellites].sort((a, b) => b.radiusKm - a.radiusKm)) {
30609
+ const r = Math.max(MIN_R, s.radiusKm * model.pxPerKm);
30610
+ y -= r;
30611
+ moons.push({
30612
+ ...s,
30613
+ cx: parent.cx,
30614
+ cy: y,
30615
+ r,
30616
+ hitY: 0,
30617
+ hitHeight: 0
30618
+ });
30619
+ y -= r + MOON_SPACING;
30620
+ }
30621
+ const top = Math.min(...moons.map((mn) => mn.cy - mn.r)) - MOON_ZONE_PAD;
30622
+ const bottom = Math.max(...moons.map((mn) => mn.cy + mn.r)) + MOON_ZONE_PAD;
30623
+ moons.forEach((mn, idx) => {
30624
+ const below = idx === 0 ? bottom : (moons[idx - 1].cy + mn.cy) / 2;
30625
+ const above = idx === moons.length - 1 ? top : (moons[idx + 1].cy + mn.cy) / 2;
30626
+ mn.hitY = above;
30627
+ mn.hitHeight = below - above;
30628
+ });
30629
+ moonZones.push({
30630
+ parent,
30631
+ moons,
30632
+ x: parent.cx - MOON_ZONE_W / 2,
30633
+ y: top,
30634
+ width: MOON_ZONE_W,
30635
+ height: bottom - top,
30636
+ perMoon: !parent.grouped && !parent.satellitesLink && moons.some((mn) => mn.link)
30637
+ });
30638
+ }
30639
+ const bands = model.bands.map((b) => {
30640
+ const x = xOf(b.innerKm);
30641
+ const width = xOf(b.outerKm) - x;
30642
+ const hitWidth = Math.max(width, BAND_MIN_HIT);
30643
+ return {
30644
+ ...b,
30645
+ x,
30646
+ width,
30647
+ hitX: x + (width - hitWidth) / 2,
30648
+ hitWidth
30649
+ };
30650
+ }).sort((a, b) => b.width - a.width);
30651
+ let cloudPath = "";
30652
+ model.cloud?.points.forEach((p, idx) => {
30653
+ const aUnits = p.aKm / model.unitKm;
30654
+ if (aUnits < model.domain[0] || aUnits > model.domain[1]) return;
30655
+ const offset = Math.min(Math.max(foldTilt(p.tiltDeg), 0) * model.pxPerDeg, MAX_OFFSET);
30656
+ const cy = 120 + (idx % 2 === 0 ? -offset : offset);
30657
+ cloudPath += `M${xOf(p.aKm).toFixed(1)} ${cy.toFixed(1)}h0.01`;
30658
+ });
30659
+ const primaryR = model.primary.radiusKm * model.pxPerKm;
30660
+ return {
30661
+ bodies,
30662
+ moonZones,
30663
+ bands,
30664
+ cloudPath,
30665
+ primaryR,
30666
+ primaryCx: PRIMARY_LIMB_X - primaryR,
30667
+ xOf
30668
+ };
30669
+ }
30670
+ function inside(z, vx, vy) {
30671
+ return vx >= z.x && vx <= z.x + z.width && vy >= z.y && vy <= z.y + z.height;
30672
+ }
30673
+ function nearestBody(l, vx, vy, reach) {
30674
+ for (const pickable of [true, false]) {
30675
+ let nearest = null;
30676
+ let gap = reach;
30677
+ for (const b of l.bodies) {
30678
+ if (!!b.link !== pickable) continue;
30679
+ const centre = Math.hypot(vx - b.cx, vy - b.cy);
30680
+ const edge = centre <= b.r ? centre - INSIDE : centre - b.r;
30681
+ if (edge > gap) continue;
30682
+ nearest = b;
30683
+ gap = edge;
30684
+ }
30685
+ if (nearest) return nearest;
30686
+ }
30687
+ return null;
30688
+ }
30689
+ function hitTest(l, vx, vy) {
30690
+ const on = nearestBody(l, vx, vy, 0);
30691
+ if (on) return {
30692
+ kind: "body",
30693
+ id: on.id
30694
+ };
30695
+ for (const z of l.moonZones) {
30696
+ if (!inside(z, vx, vy)) continue;
30697
+ if (z.parent.grouped) return {
30698
+ kind: "body",
30699
+ id: z.parent.id
30700
+ };
30701
+ if (!z.perMoon) return {
30702
+ kind: "stack",
30703
+ id: z.parent.id
30704
+ };
30705
+ const moon = z.moons.find((mn) => vy >= mn.hitY && vy <= mn.hitY + mn.hitHeight);
30706
+ if (moon) return {
30707
+ kind: "moon",
30708
+ parent: z.parent.id,
30709
+ id: moon.id
30710
+ };
30711
+ }
30712
+ const near = nearestBody(l, vx, vy, 16);
30713
+ if (near) return {
30714
+ kind: "body",
30715
+ id: near.id
30716
+ };
30717
+ for (let i = l.bands.length - 1; i >= 0; i--) {
30718
+ const band = l.bands[i];
30719
+ if (vx >= band.hitX && vx <= band.hitX + band.hitWidth && vy >= 26 && vy <= 26 + 180) return {
30720
+ kind: "band",
30721
+ key: band.key
30722
+ };
30723
+ }
30724
+ if (Math.hypot(vx - l.primaryCx, vy - 120) <= l.primaryR) return { kind: "primary" };
30725
+ return null;
30726
+ }
30727
+ function tipFor(model, l, hover) {
30728
+ if (!hover) return null;
30729
+ const { text } = model;
30730
+ switch (hover.kind) {
30731
+ case "primary": return {
30732
+ cx: Math.max(44, l.primaryCx),
30733
+ cy: 120 - Math.min(l.primaryR, 120 - 40),
30734
+ title: model.primary.name,
30735
+ sub: text.primary
30736
+ };
30737
+ case "body": {
30738
+ const b = l.bodies.find((p) => p.id === hover.id);
30739
+ if (!b) return null;
30740
+ const parts = [text.distance(b.aKm)];
30741
+ if (b.tiltDeg > 90) parts.push(text.retrograde);
30742
+ if (b.grouped && b.satellites?.length) parts.push(text.moons(b.satelliteCount ?? b.satellites.length));
30743
+ return {
30744
+ cx: b.cx,
30745
+ cy: b.cy - b.r - 4,
30746
+ title: b.name,
30747
+ sub: parts.join(" · ")
30748
+ };
30749
+ }
30750
+ case "stack": {
30751
+ const zone = l.moonZones.find((z) => z.parent.id === hover.id);
30752
+ if (!zone) return null;
30753
+ const cx = zone.x + zone.width / 2;
30754
+ return zone.parent.satellitesTab ? {
30755
+ cx,
30756
+ cy: zone.y,
30757
+ title: zone.parent.name,
30758
+ sub: text.moons(zone.parent.satelliteCount ?? zone.moons.length)
30759
+ } : {
30760
+ cx,
30761
+ cy: zone.y,
30762
+ title: zone.moons[0].name,
30763
+ sub: zone.parent.name
30764
+ };
30765
+ }
30766
+ case "moon": {
30767
+ const zone = l.moonZones.find((z) => z.parent.id === hover.parent);
30768
+ const moon = zone?.moons.find((mn) => mn.id === hover.id);
30769
+ if (!zone || !moon) return null;
30770
+ return {
30771
+ cx: moon.cx,
30772
+ cy: moon.cy - moon.r - 4,
30773
+ title: moon.name,
30774
+ sub: zone.parent.name
30775
+ };
30776
+ }
30777
+ case "band": {
30778
+ const band = l.bands.find((bd) => bd.key === hover.key);
30779
+ if (!band) return null;
30780
+ return {
30781
+ cx: band.x + band.width / 2,
30782
+ cy: 44,
30783
+ title: band.name ?? band.label,
30784
+ sub: band.sub ?? `${text.distance(band.innerKm)}–${text.distance(band.outerKm)}`
30785
+ };
30786
+ }
30787
+ }
30788
+ }
30789
+
30790
+ //#endregion
30791
+ //#region src/lib/systemmap/chart.ts
30792
+ var SVG_NS = "http://www.w3.org/2000/svg";
30793
+ var PRIMARY_RIM = 10;
30794
+ var RING_TILT = -18;
30795
+ var CLOUD_R = 1.2;
30796
+ var TIP_H = 28;
30797
+ var TIP_MARGIN = 6;
30798
+ var LABEL_MARGIN = 4;
30799
+ var LABEL_SIZE = 18;
30800
+ var LABEL_MIN_SIZE = 12;
30801
+ var LABEL_MAX_W = 190;
30802
+ function svg(tag, attrs = {}, ...children) {
30803
+ const el = document.createElementNS(SVG_NS, tag);
30804
+ for (const [name, value] of Object.entries(attrs)) if (value !== void 0) el.setAttribute(name, String(value));
30805
+ for (const child of children) if (child) el.append(child);
30806
+ return el;
30807
+ }
30808
+ function sameHover(a, b) {
30809
+ return JSON.stringify(a) === JSON.stringify(b);
30810
+ }
30811
+ function linksOf(model, l) {
30812
+ const links = /* @__PURE__ */ new Map();
30813
+ const add = (hover, link) => {
30814
+ if (link?.href || link?.onclick) links.set(JSON.stringify(hover), link);
30815
+ };
30816
+ for (const band of l.bands) add({
30817
+ kind: "band",
30818
+ key: band.key
30819
+ }, band);
30820
+ add({ kind: "primary" }, model.primary.link);
30821
+ for (const b of l.bodies) add({
30822
+ kind: "body",
30823
+ id: b.id
30824
+ }, b.link);
30825
+ for (const z of l.moonZones) {
30826
+ if (z.parent.grouped) continue;
30827
+ if (!z.perMoon) add({
30828
+ kind: "stack",
30829
+ id: z.parent.id
30830
+ }, z.parent.satellitesLink);
30831
+ else for (const mn of z.moons) add({
30832
+ kind: "moon",
30833
+ parent: z.parent.id,
30834
+ id: mn.id
30835
+ }, mn.link);
30836
+ }
30837
+ return links;
30838
+ }
30839
+ var nextUid = 0;
30840
+ var SystemMapChart = class {
30841
+ root = document.createElement("div");
30842
+ svg = svg("svg");
30843
+ tip = document.createElement("div");
30844
+ tipTitle = document.createElement("span");
30845
+ tipSub = document.createElement("span");
30846
+ uid = `sm-system-map-${nextUid++}`;
30847
+ resize = new ResizeObserver(() => {
30848
+ this.fitLabels();
30849
+ this.placeTip();
30850
+ });
30851
+ model = null;
30852
+ placed = null;
30853
+ background = false;
30854
+ hover = null;
30855
+ highlights = /* @__PURE__ */ new Map();
30856
+ targets = /* @__PURE__ */ new Map();
30857
+ links = /* @__PURE__ */ new Map();
30858
+ shape = "";
30859
+ labels = [];
30860
+ constructor() {
30861
+ this.tip.className = "sm-system-map__tip";
30862
+ this.tipTitle.className = "sm-system-map__tip-title";
30863
+ this.tipSub.className = "sm-system-map__tip-sub";
30864
+ this.tip.append(this.tipTitle, this.tipSub);
30865
+ this.tip.hidden = true;
30866
+ this.root.append(this.svg, this.tip);
30867
+ this.resize.observe(this.root);
30868
+ const scrub = createScrub({
30869
+ onScrub: (x, y) => this.setHover(this.hitAt(x, y)),
30870
+ onEnd: () => this.setHover(null)
30871
+ });
30872
+ this.svg.addEventListener("pointerdown", scrub.onpointerdown);
30873
+ this.svg.addEventListener("pointermove", scrub.onpointermove);
30874
+ this.svg.addEventListener("pointerup", scrub.onpointerup);
30875
+ this.svg.addEventListener("pointercancel", scrub.onpointercancel);
30876
+ this.svg.addEventListener("pointerleave", scrub.onpointerleave);
30877
+ let at = [0, 0];
30878
+ const track = (e) => {
30879
+ at = [e.clientX, e.clientY];
30880
+ };
30881
+ this.svg.addEventListener("pointerdown", track);
30882
+ this.svg.addEventListener("pointerup", track);
30883
+ this.svg.addEventListener("pointermove", (e) => {
30884
+ track(e);
30885
+ if (e.pointerType === "mouse") this.setHover(this.hitAt(e.clientX, e.clientY));
30886
+ });
30887
+ this.svg.addEventListener("pointerleave", (e) => {
30888
+ if (e.pointerType === "mouse") this.setHover(null);
30889
+ });
30890
+ this.svg.addEventListener("click", (e) => {
30891
+ if (e.detail === 0) return;
30892
+ 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);
30893
+ if (!hit) return;
30894
+ const meant = this.targets.get(JSON.stringify(hit));
30895
+ if (meant?.contains(e.target)) return;
30896
+ e.stopPropagation();
30897
+ e.preventDefault();
30898
+ meant?.dispatchEvent(new MouseEvent("click", {
30899
+ bubbles: true,
30900
+ cancelable: true,
30901
+ ctrlKey: e.ctrlKey,
30902
+ metaKey: e.metaKey,
30903
+ shiftKey: e.shiftKey,
30904
+ altKey: e.altKey,
30905
+ button: e.button
30906
+ }));
30907
+ }, true);
30908
+ }
30909
+ destroy() {
30910
+ this.resize.disconnect();
30911
+ this.root.remove();
30912
+ }
30913
+ update(model, options) {
30914
+ this.model = model;
30915
+ this.placed = layout(model);
30916
+ const links = linksOf(model, this.placed);
30917
+ const shape = JSON.stringify([
30918
+ JSON.stringify(model, (key, value) => key === "href" ? void 0 : value),
30919
+ options,
30920
+ [...links].map(([key, link]) => [
30921
+ key,
30922
+ !!link.href,
30923
+ !!link.onclick
30924
+ ])
30925
+ ]);
30926
+ this.links = links;
30927
+ if (shape === this.shape) {
30928
+ for (const [key, el] of this.targets) {
30929
+ const href = links.get(key)?.href;
30930
+ if (href) el.setAttribute("href", href);
30931
+ }
30932
+ return;
30933
+ }
30934
+ this.shape = shape;
30935
+ this.background = options.variant === "background";
30936
+ this.hover = null;
30937
+ this.root.className = this.background ? "sm-system-map sm-system-map--background" : "sm-system-map";
30938
+ this.draw(model, this.placed, options.ariaLabel);
30939
+ this.fitLabels();
30940
+ this.placeTip();
30941
+ }
30942
+ draw(model, l, ariaLabel) {
30943
+ const bg = this.background;
30944
+ this.highlights = /* @__PURE__ */ new Map();
30945
+ this.targets = /* @__PURE__ */ new Map();
30946
+ this.labels = [];
30947
+ const root = this.svg;
30948
+ root.replaceChildren();
30949
+ root.setAttribute("viewBox", bg && model.backgroundView || `0 0 ${720} ${240}`);
30950
+ root.setAttribute("preserveAspectRatio", bg ? model.backgroundFit === "fit" ? "xMidYMid meet" : model.backgroundView ? "xMinYMid slice" : "xMidYMid slice" : "xMidYMid meet");
30951
+ root.setAttribute("class", "sm-system-map__svg");
30952
+ root.setAttribute("role", "group");
30953
+ root.setAttribute("aria-label", ariaLabel);
30954
+ root.setAttribute("data-vaul-no-drag", "");
30955
+ const stop = (offset, opacity) => svg("stop", {
30956
+ offset,
30957
+ "stop-color": model.primary.color,
30958
+ "stop-opacity": opacity
30959
+ });
30960
+ root.append(svg("defs", {}, svg("radialGradient", {
30961
+ id: `${this.uid}-primary`,
30962
+ gradientUnits: "userSpaceOnUse",
30963
+ cx: l.primaryCx,
30964
+ cy: 120,
30965
+ r: l.primaryR
30966
+ }, stop("0%", 1), stop(Math.max(0, 1 - PRIMARY_RIM / l.primaryR), .85), stop("100%", .35)), svg("pattern", {
30967
+ id: `${this.uid}-band`,
30968
+ width: 9,
30969
+ height: 9,
30970
+ patternUnits: "userSpaceOnUse"
30971
+ }, svg("circle", {
30972
+ cx: 2,
30973
+ cy: 2,
30974
+ r: .9,
30975
+ fill: "currentColor",
30976
+ opacity: .55
30977
+ }), svg("circle", {
30978
+ cx: 6.5,
30979
+ cy: 6,
30980
+ r: .9,
30981
+ fill: "currentColor",
30982
+ opacity: .55
30983
+ }))));
30984
+ if (!bg) this.drawAxis(model, l);
30985
+ if (l.cloudPath) root.append(svg("path", {
30986
+ d: l.cloudPath,
30987
+ stroke: model.cloud?.color,
30988
+ "stroke-width": CLOUD_R * 2,
30989
+ "stroke-linecap": "round",
30990
+ fill: "none",
30991
+ opacity: .45,
30992
+ "pointer-events": "none"
30993
+ }));
30994
+ for (const band of l.bands) {
30995
+ const fill = svg("rect", {
30996
+ x: band.x,
30997
+ y: 26,
30998
+ width: band.width,
30999
+ height: 180,
31000
+ fill: `url(#${this.uid}-band)`,
31001
+ class: `sm-system-map__band sm-system-map__band--${band.tone}`
31002
+ });
31003
+ if (bg) {
31004
+ root.append(fill);
31005
+ continue;
31006
+ }
31007
+ const label = svg("text", {
31008
+ x: band.x + band.width / 2,
31009
+ y: 240 - 8,
31010
+ "text-anchor": "middle",
31011
+ "font-size": LABEL_SIZE,
31012
+ class: `sm-system-map__band-label sm-system-map__band-label--${band.tone}`
31013
+ });
31014
+ label.textContent = band.label;
31015
+ this.labels.push(label);
31016
+ root.append(this.target(band, band.name ?? band.label, {
31017
+ kind: "band",
31018
+ key: band.key
31019
+ }, fill, svg("rect", {
31020
+ x: band.hitX,
31021
+ y: 26,
31022
+ width: band.hitWidth,
31023
+ height: 180,
31024
+ fill: "transparent"
31025
+ }), label));
31026
+ }
31027
+ const disc = svg("circle", {
31028
+ cx: l.primaryCx,
31029
+ cy: 120,
31030
+ r: l.primaryR,
31031
+ fill: `url(#${this.uid}-primary)`
31032
+ });
31033
+ root.append(bg ? disc : this.target(model.primary.link, model.primary.name, { kind: "primary" }, disc));
31034
+ for (const b of l.bodies) {
31035
+ if (!b.rings) continue;
31036
+ const mid = b.r * (b.rings.inner + b.rings.outer) / 2;
31037
+ root.append(svg("ellipse", {
31038
+ cx: b.cx,
31039
+ cy: b.cy,
31040
+ rx: mid,
31041
+ ry: mid * RING_FORESHORTEN,
31042
+ transform: `rotate(${RING_TILT} ${b.cx} ${b.cy})`,
31043
+ fill: "none",
31044
+ stroke: "#d9c89f",
31045
+ "stroke-width": b.r * (b.rings.outer - b.rings.inner),
31046
+ opacity: .45
31047
+ }));
31048
+ }
31049
+ const zoneOf = new Map(l.moonZones.map((z) => [z.parent.id, z]));
31050
+ for (const b of l.bodies) root.append(this.body(b, zoneOf.get(b.id)));
31051
+ for (const z of l.moonZones) if (!z.parent.grouped) this.stack(z);
31052
+ }
31053
+ drawAxis(model, l) {
31054
+ for (const t of model.ticks) {
31055
+ const x = l.xOf(t * model.unitKm);
31056
+ const tick = svg("text", {
31057
+ x,
31058
+ y: 16,
31059
+ "text-anchor": "middle",
31060
+ "font-size": 16,
31061
+ fill: "currentColor",
31062
+ opacity: .85
31063
+ });
31064
+ tick.textContent = String(t);
31065
+ this.svg.append(svg("line", {
31066
+ x1: x,
31067
+ x2: x,
31068
+ y1: 20,
31069
+ y2: 240 - 28,
31070
+ stroke: "currentColor",
31071
+ "stroke-width": .5,
31072
+ opacity: .12
31073
+ }), tick);
31074
+ }
31075
+ const unit = svg("text", {
31076
+ x: 700,
31077
+ y: 16,
31078
+ "text-anchor": "end",
31079
+ "font-size": 14,
31080
+ fill: "currentColor",
31081
+ opacity: .65
31082
+ });
31083
+ unit.textContent = model.axisLabel;
31084
+ this.svg.append(unit);
31085
+ }
31086
+ body(b, zone) {
31087
+ const dot = b.reference ? svg("circle", {
31088
+ cx: b.cx,
31089
+ cy: b.cy,
31090
+ r: b.r + 1,
31091
+ fill: "none",
31092
+ stroke: b.color,
31093
+ "stroke-width": 1.2,
31094
+ class: "sm-system-map__reference"
31095
+ }) : svg("circle", {
31096
+ cx: b.cx,
31097
+ cy: b.cy,
31098
+ r: b.r,
31099
+ fill: b.color
31100
+ });
31101
+ if (this.background) {
31102
+ if (!zone || !b.grouped) return dot;
31103
+ return svg("g", {}, dot, ...this.moonDots(zone));
31104
+ }
31105
+ const hover = {
31106
+ kind: "body",
31107
+ id: b.id
31108
+ };
31109
+ const ring = this.highlight(hover, svg("circle", {
31110
+ cx: b.cx,
31111
+ cy: b.cy,
31112
+ r: b.r + 3.5,
31113
+ fill: "none",
31114
+ stroke: "currentColor",
31115
+ "stroke-width": 1,
31116
+ opacity: .8
31117
+ }));
31118
+ const hit = svg("circle", {
31119
+ cx: b.cx,
31120
+ cy: b.cy,
31121
+ r: b.r + 16,
31122
+ fill: "transparent"
31123
+ });
31124
+ if (!zone || !b.grouped) return this.target(b.link, b.name, hover, ring, dot, hit);
31125
+ return this.target(b.link, b.name, hover, this.highlight(hover, this.zoneWash(zone)), ring, dot, hit, ...this.moonDots(zone), this.zoneHit(zone));
31126
+ }
31127
+ stack(z) {
31128
+ if (this.background) {
31129
+ this.svg.append(...this.moonDots(z));
31130
+ return;
31131
+ }
31132
+ const p = z.parent;
31133
+ if (z.perMoon) {
31134
+ this.svg.append(...this.moonDots(z));
31135
+ for (const mn of z.moons) {
31136
+ const hover = {
31137
+ kind: "moon",
31138
+ parent: p.id,
31139
+ id: mn.id
31140
+ };
31141
+ this.svg.append(this.target(mn.link, mn.name, hover, this.highlight(hover, svg("circle", {
31142
+ cx: mn.cx,
31143
+ cy: mn.cy,
31144
+ r: mn.r + 2.5,
31145
+ fill: "none",
31146
+ stroke: "currentColor",
31147
+ "stroke-width": 1,
31148
+ opacity: .8
31149
+ })), svg("rect", {
31150
+ x: z.x,
31151
+ y: mn.hitY,
31152
+ width: z.width,
31153
+ height: mn.hitHeight,
31154
+ fill: "transparent"
31155
+ })));
31156
+ }
31157
+ return;
31158
+ }
31159
+ const hover = {
31160
+ kind: "stack",
31161
+ id: p.id
31162
+ };
31163
+ const first = z.moons[0];
31164
+ 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)));
31165
+ }
31166
+ moonDots(z) {
31167
+ return z.moons.map((mn) => svg("circle", {
31168
+ cx: mn.cx,
31169
+ cy: mn.cy,
31170
+ r: mn.r,
31171
+ fill: mn.color
31172
+ }));
31173
+ }
31174
+ zoneWash(z) {
31175
+ return svg("rect", {
31176
+ x: z.x,
31177
+ y: z.y,
31178
+ width: z.width,
31179
+ height: z.height,
31180
+ rx: 4,
31181
+ fill: "currentColor",
31182
+ opacity: .1,
31183
+ "pointer-events": "none"
31184
+ });
31185
+ }
31186
+ zoneHit(z) {
31187
+ return svg("rect", {
31188
+ x: z.x,
31189
+ y: z.y,
31190
+ width: z.width,
31191
+ height: z.height,
31192
+ fill: "transparent"
31193
+ });
31194
+ }
31195
+ highlight(hover, el) {
31196
+ const key = JSON.stringify(hover);
31197
+ const list = this.highlights.get(key);
31198
+ if (list) list.push(el);
31199
+ else this.highlights.set(key, [el]);
31200
+ el.style.display = "none";
31201
+ return el;
31202
+ }
31203
+ target(link, label, hover, ...children) {
31204
+ const linked = !!(link?.href || link?.onclick);
31205
+ const el = link?.href ? svg("a", {
31206
+ href: link.href,
31207
+ "aria-label": label
31208
+ }) : linked ? svg("g", {
31209
+ role: "button",
31210
+ tabindex: 0,
31211
+ "aria-label": label,
31212
+ class: "sm-system-map__button"
31213
+ }) : svg("g");
31214
+ el.append(...children);
31215
+ if (!linked) return el;
31216
+ const key = JSON.stringify(hover);
31217
+ this.targets.set(key, el);
31218
+ el.addEventListener("focus", () => this.setHover(hover));
31219
+ el.addEventListener("blur", () => {
31220
+ if (sameHover(this.hover, hover)) this.setHover(null);
31221
+ });
31222
+ el.addEventListener("click", (e) => this.links.get(key)?.onclick?.(e));
31223
+ if (!link?.href) el.addEventListener("keydown", (e) => {
31224
+ const key = e.key;
31225
+ if (key !== "Enter" && key !== " ") return;
31226
+ e.preventDefault();
31227
+ el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
31228
+ });
31229
+ return el;
31230
+ }
31231
+ setHover(hover) {
31232
+ if (sameHover(this.hover, hover)) return;
31233
+ for (const el of this.highlights.get(JSON.stringify(this.hover)) ?? []) el.style.display = "none";
31234
+ this.targets.get(JSON.stringify(this.hover))?.removeAttribute("data-near");
31235
+ this.hover = hover;
31236
+ for (const el of this.highlights.get(JSON.stringify(hover)) ?? []) el.style.display = "";
31237
+ const near = this.targets.get(JSON.stringify(hover));
31238
+ near?.setAttribute("data-near", "");
31239
+ this.svg.classList.toggle("sm-system-map__svg--near", !!near);
31240
+ this.placeTip();
31241
+ }
31242
+ hitAt(clientX, clientY) {
31243
+ if (!this.placed || this.background) return null;
31244
+ const rect = this.svg.getBoundingClientRect();
31245
+ if (!rect.width || !rect.height) return null;
31246
+ const vx = (clientX - rect.left) / rect.width * 720;
31247
+ const vy = (clientY - rect.top) / rect.height * 240;
31248
+ return hitTest(this.placed, vx, vy);
31249
+ }
31250
+ fitLabels() {
31251
+ for (const label of this.labels) {
31252
+ let width = 0;
31253
+ label.setAttribute("font-size", String(LABEL_SIZE));
31254
+ try {
31255
+ width = label.getComputedTextLength();
31256
+ } catch {}
31257
+ if (!width) continue;
31258
+ if (width > LABEL_MAX_W) {
31259
+ const size = Math.max(LABEL_MIN_SIZE, LABEL_SIZE * LABEL_MAX_W / width);
31260
+ label.setAttribute("font-size", String(size));
31261
+ width *= size / LABEL_SIZE;
31262
+ }
31263
+ const centre = Number(label.dataset.centre ?? label.getAttribute("x"));
31264
+ label.dataset.centre = String(centre);
31265
+ const half = width / 2;
31266
+ const x = Math.min(Math.max(centre, half + LABEL_MARGIN), Math.max(half, 720 - half - LABEL_MARGIN));
31267
+ label.setAttribute("x", String(x));
31268
+ }
31269
+ }
31270
+ placeTip() {
31271
+ const tip = this.model && this.placed && !this.background ? tipFor(this.model, this.placed, this.hover) : null;
31272
+ this.tip.hidden = !tip;
31273
+ if (!tip) return;
31274
+ this.tipTitle.textContent = tip.title;
31275
+ this.tipSub.textContent = tip.sub ? ` · ${tip.sub}` : "";
31276
+ const containerW = this.root.clientWidth;
31277
+ const scale = containerW / 720;
31278
+ this.tip.style.maxWidth = `${Math.max(0, containerW - 2 * TIP_MARGIN)}px`;
31279
+ const half = this.tip.offsetWidth / 2;
31280
+ const left = Math.min(Math.max(tip.cx * scale, half + TIP_MARGIN), Math.max(half, containerW - half - TIP_MARGIN));
31281
+ this.tip.style.left = `${left}px`;
31282
+ this.tip.style.top = `${Math.max(4, tip.cy * scale - TIP_H)}px`;
31283
+ }
31284
+ };
31285
+
31286
+ //#endregion
31287
+ //#region src/lib/systemmap/planetary.ts
31288
+ function systemFromMapEntry(entry, name) {
31289
+ return {
31290
+ planetId: entry.primary.id,
31291
+ planetName: name,
31292
+ planetRadiusKm: entry.primary.radius_km,
31293
+ planetColor: BODY_COLORS[entry.primary.id] ?? "#cccccc",
31294
+ moons: entry.moons.map((mn) => ({
31295
+ id: mn.id,
31296
+ name: mn.id,
31297
+ aRp: mn.a_rp,
31298
+ tiltDeg: mn.tilt_deg,
31299
+ radiusKm: mn.radius_km,
31300
+ color: BODY_COLORS[mn.id] ?? mn.color ?? "#cccccc"
31301
+ })),
31302
+ rings: entry.rings ? {
31303
+ innerRp: entry.rings.inner_rp,
31304
+ outerRp: entry.rings.outer_rp
31305
+ } : null,
31306
+ moonCount: entry.moon_count
31307
+ };
31308
+ }
31309
+ var TOP_MOON_R = 14;
31310
+ var AT_SURFACE = 1.001;
31311
+ var PX_PER_DEG = .62;
31312
+ var TICK_LADDER = [
31313
+ 1,
31314
+ 2,
31315
+ 3,
31316
+ 5,
31317
+ 10,
31318
+ 20,
31319
+ 30,
31320
+ 50,
31321
+ 100,
31322
+ 200,
31323
+ 300,
31324
+ 500,
31325
+ 1e3,
31326
+ 2e3,
31327
+ 5e3
31328
+ ];
31329
+ var BG_VIEW = "0 96 320 144";
31330
+ var BG_VIEW_WIDE = "0 96 720 144";
31331
+ var EMPTY_DOMAIN = [1, 100];
31332
+ function domainOf(system, bands) {
31333
+ let lo = Math.min(...system.moons.map((mn) => mn.aRp));
31334
+ let hi = Math.max(...system.moons.map((mn) => mn.aRp));
31335
+ for (const b of bands) {
31336
+ lo = Math.min(lo, b.innerKm / system.planetRadiusKm);
31337
+ hi = Math.max(hi, b.outerKm / system.planetRadiusKm);
31338
+ }
31339
+ if (!Number.isFinite(lo) || !Number.isFinite(hi)) return EMPTY_DOMAIN;
31340
+ if (hi <= lo) {
31341
+ lo /= 2;
31342
+ hi *= 2;
31343
+ }
31344
+ lo = lo <= AT_SURFACE ? 1 : lo;
31345
+ return [lo > 1 ? 10 ** (Math.log10(lo) - .12) : 1, 10 ** (Math.log10(hi) + .12)];
31346
+ }
31347
+ function planetarySystemModel(system, parts) {
31348
+ const rKm = system.planetRadiusKm;
31349
+ const domain = domainOf(system, parts.bands);
31350
+ return {
31351
+ primary: {
31352
+ id: system.planetId,
31353
+ name: system.planetName,
31354
+ radiusKm: rKm,
31355
+ color: system.planetColor,
31356
+ link: parts.primaryLink
31357
+ },
31358
+ bodies: system.moons.map((mn) => ({
31359
+ id: mn.id,
31360
+ name: mn.name,
31361
+ aKm: mn.aRp * rKm,
31362
+ tiltDeg: mn.tiltDeg,
31363
+ radiusKm: mn.radiusKm,
31364
+ color: mn.color,
31365
+ link: parts.moonLink?.(mn)
31366
+ })),
31367
+ bands: parts.bands,
31368
+ cloud: parts.cloud,
31369
+ unitKm: rKm,
31370
+ domain,
31371
+ ticks: TICK_LADDER.filter((t) => t >= domain[0] && t <= domain[1]),
31372
+ axisLabel: parts.axisLabel,
31373
+ pxPerKm: TOP_MOON_R / Math.max(...system.moons.map((mn) => mn.radiusKm).filter((r) => r > 0), 1),
31374
+ pxPerDeg: PX_PER_DEG,
31375
+ text: parts.text,
31376
+ backgroundView: parts.wide ? BG_VIEW_WIDE : BG_VIEW,
31377
+ backgroundFit: parts.wide ? "fit" : "slice"
31378
+ };
31379
+ }
31380
+
31381
+ //#endregion
31382
+ //#region src/lib/systemmap/solar.ts
31383
+ var SOLAR_PX_PER_KM = 224e-6;
31384
+ var SOLAR_PX_PER_DEG = 1.8;
31385
+ var DOMAIN = [.3, 50];
31386
+ var TICKS = [
31387
+ .3,
31388
+ 1,
31389
+ 3,
31390
+ 10,
31391
+ 30
31392
+ ];
31393
+ var SUN_COLOR = "#ffdd44";
31394
+ function displayName(name) {
31395
+ const stripped = name.replace(/^\d+\s+/, "");
31396
+ if (stripped === stripped.toUpperCase() && /[A-Z]/.test(stripped)) return stripped.charAt(0) + stripped.slice(1).toLowerCase();
31397
+ return stripped;
31398
+ }
31399
+ function solarColor(o) {
31400
+ return o.color || BODY_COLORS[o.id] || "#cccccc";
31401
+ }
31402
+ function fileStacks(file, name) {
31403
+ const stacks = /* @__PURE__ */ new Map();
31404
+ const planets = new Map(file.objects.map((o) => [o.id, o]));
31405
+ for (const o of file.objects) {
31406
+ if (o.kind !== "moon" || !o.parent) continue;
31407
+ const stack = stacks.get(o.parent) ?? {
31408
+ satellites: [],
31409
+ count: planets.get(o.parent)?.moon_count,
31410
+ tab: false
31411
+ };
31412
+ stack.satellites.push({
31413
+ id: o.id,
31414
+ name: name(o),
31415
+ radiusKm: o.diameter_km / 2,
31416
+ color: solarColor(o)
31417
+ });
31418
+ stack.tab ||= !!o.link_parent;
31419
+ stacks.set(o.parent, stack);
31420
+ }
31421
+ return stacks;
31422
+ }
31423
+ function solarSystemModel(file, parts) {
31424
+ const sun = file.objects.find((o) => o.kind === "star");
31425
+ if (!sun) return null;
31426
+ const listed = fileStacks(file, parts.name);
31427
+ const stackOf = parts.stack ?? ((o) => listed.get(o.id));
31428
+ const bodies = file.objects.filter((o) => o.kind !== "moon" && o.kind !== "star" && (parts.keep?.(o) ?? true)).map((o) => {
31429
+ const stack = stackOf(o);
31430
+ return {
31431
+ id: o.id,
31432
+ name: parts.name(o),
31433
+ aKm: o.a * AU_KM,
31434
+ tiltDeg: o.i,
31435
+ radiusKm: o.diameter_km / 2,
31436
+ color: solarColor(o),
31437
+ rings: o.rings,
31438
+ satellites: stack?.satellites,
31439
+ satelliteCount: stack?.count,
31440
+ satellitesTab: stack?.tab ?? false,
31441
+ ...parts.links(o, stack)
31442
+ };
31443
+ });
31444
+ return {
31445
+ primary: {
31446
+ id: sun.id,
31447
+ name: parts.name(sun),
31448
+ radiusKm: sun.diameter_km / 2,
31449
+ color: SUN_COLOR,
31450
+ link: parts.primaryLink
31451
+ },
31452
+ bodies,
31453
+ bands: file.belts.map((b) => ({
31454
+ key: b.slug,
31455
+ innerKm: b.inner_au * AU_KM,
31456
+ outerKm: b.outer_au * AU_KM,
31457
+ tone: b.kind === "kuiper_belt" ? "sky" : "muted",
31458
+ ...parts.band(b)
31459
+ })),
31460
+ unitKm: AU_KM,
31461
+ domain: DOMAIN,
31462
+ ticks: TICKS,
31463
+ axisLabel: parts.text.axisAu,
31464
+ pxPerKm: SOLAR_PX_PER_KM,
31465
+ pxPerDeg: SOLAR_PX_PER_DEG,
31466
+ text: parts.text
31467
+ };
31468
+ }
31469
+
31470
+ //#endregion
31471
+ //#region src/lib/systemmap/views.ts
31472
+ function extraFromPlace(place) {
31473
+ return {
31474
+ id: place.id,
31475
+ name: place.name ?? place.id,
31476
+ moon: place.moon ?? false,
31477
+ aAu: place.aAu ?? 0,
31478
+ tiltDeg: place.tiltDeg ?? 0,
31479
+ radiusKm: place.radiusKm ?? 0,
31480
+ color: place.color ?? BODY_COLORS[place.id] ?? "#cccccc",
31481
+ parent: place.parent
31482
+ };
31483
+ }
31484
+ var MAX_STACK = 4;
31485
+ var ZONE_TOP_R = 10;
31486
+ var ZONE_DOMAIN = {
31487
+ inner: [.6, 5.5],
31488
+ outer: [4.5, 55]
31489
+ };
31490
+ var ZONE_TICKS = [
31491
+ .1,
31492
+ .2,
31493
+ .3,
31494
+ .5,
31495
+ 1,
31496
+ 2,
31497
+ 3,
31498
+ 5,
31499
+ 10,
31500
+ 20,
31501
+ 30,
31502
+ 50,
31503
+ 100,
31504
+ 200,
31505
+ 500,
31506
+ 1e3
31507
+ ];
31508
+ var LAST_AU_TICK_AT = .9;
31509
+ var LAST_RADII_TICK_AT = .76;
31510
+ function tickAt(tick, domain) {
31511
+ return Math.log(tick / domain[0]) / Math.log(domain[1] / domain[0]);
31512
+ }
31513
+ var JUPITER_ID = "naif-599";
31514
+ function unplacedIds(solar, systems, bodies, placed = []) {
31515
+ if (!bodies) return [];
31516
+ const known = new Set([...solar.objects.map((o) => o.id), ...placed]);
31517
+ for (const entry of Object.values(systems)) {
31518
+ known.add(entry.primary.id);
31519
+ for (const moon of entry.moons) known.add(moon.id);
31520
+ }
31521
+ return [...bodies].filter((id) => !known.has(id));
31522
+ }
31523
+ function extraFromBundle(global) {
31524
+ const moon = global.type === "moon";
31525
+ const parent = global.orbit?.parent_id;
31526
+ const a = parent === void 0 || parent === "naif-10" || parent === "naif-0" ? global.orbit?.a ?? global.orbit?.q : void 0;
31527
+ if (a === void 0 && !moon) return null;
31528
+ const radii = global.radii;
31529
+ return {
31530
+ id: global.id,
31531
+ name: displayName(global.name ?? global.id),
31532
+ moon,
31533
+ aAu: a ?? 0,
31534
+ tiltDeg: global.orbit?.i ?? 0,
31535
+ radiusKm: radii ? (radii.a + radii.b + radii.c) / 3 : (global.sbdb?.diameter ?? 0) / 2,
31536
+ color: BODY_COLORS[global.id] ?? global.sbdb?.color ?? global.color ?? "#cccccc",
31537
+ parent: global.orbit?.parent_id
31538
+ };
31539
+ }
31540
+ var ZONE_CUT = .92;
31541
+ function zoneOf(aAu, jupiterAu) {
31542
+ return aAu < jupiterAu * ZONE_CUT ? "inner" : "outer";
31543
+ }
31544
+ function systemEntry(systems, primaryId) {
31545
+ return Object.values(systems).find((entry) => entry.primary.id === primaryId);
31546
+ }
31547
+ function targetKey(target) {
31548
+ return target.kind === "zone" ? `zone:${target.zone}` : `${target.kind}:${target.id}`;
31549
+ }
31550
+ var Scope = class {
31551
+ targets = /* @__PURE__ */ new Map();
31552
+ constructor(data, options, pick) {
31553
+ this.data = data;
31554
+ this.options = options;
31555
+ this.pick = pick;
31556
+ }
31557
+ has(id) {
31558
+ return !this.options.bodies || this.options.bodies.has(id);
31559
+ }
31560
+ name(id, exported) {
31561
+ return this.options.names[id] ?? (exported ? displayName(exported) : id);
31562
+ }
31563
+ named(id, name) {
31564
+ return this.options.names[id] ?? name;
31565
+ }
31566
+ link(target) {
31567
+ this.targets.set(targetKey(target), target);
31568
+ return { onclick: () => this.pick(target) };
31569
+ }
31570
+ body(id, name) {
31571
+ return this.has(id) ? this.link({
31572
+ kind: "body",
31573
+ id,
31574
+ name
31575
+ }) : void 0;
31576
+ }
31577
+ stack(primaryId, listed) {
31578
+ if (listed && !this.options.bodies) return listed;
31579
+ const moons = (systemEntry(this.data.systems, primaryId)?.moons ?? []).filter((moon) => this.has(moon.id)).map((moon) => ({
31580
+ id: moon.id,
31581
+ name: this.name(moon.id),
31582
+ radiusKm: moon.radius_km,
31583
+ color: BODY_COLORS[moon.id] ?? moon.color ?? "#cccccc"
31584
+ }));
31585
+ if (!moons.length) return void 0;
31586
+ moons.sort((a, b) => b.radiusKm - a.radiusKm);
31587
+ return {
31588
+ satellites: moons.slice(0, MAX_STACK),
31589
+ count: moons.length,
31590
+ tab: true
31591
+ };
31592
+ }
31593
+ links(id, name, stack) {
31594
+ if (!stack) return { link: this.body(id, name) };
31595
+ const system = () => this.link({
31596
+ kind: "system",
31597
+ id,
31598
+ name
31599
+ });
31600
+ if (this.options.grouping === "systems") return {
31601
+ link: system(),
31602
+ grouped: true
31603
+ };
31604
+ const largest = stack.satellites[0];
31605
+ return {
31606
+ link: this.body(id, name),
31607
+ satellitesLink: stack.tab ? system() : this.body(largest.id, largest.name)
31608
+ };
31609
+ }
31610
+ built(model, label) {
31611
+ return {
31612
+ model,
31613
+ targets: [...this.targets.values()],
31614
+ label
31615
+ };
31616
+ }
31617
+ };
31618
+ function buildSolarView(data, options, text, pick) {
31619
+ const scope = new Scope(data, options, pick);
31620
+ const name = (o) => scope.name(o.id, o.name);
31621
+ const listed = fileStacks(data.solar, name);
31622
+ const stacks = new Map(data.solar.objects.map((o) => [o.id, scope.stack(o.id, listed.get(o.id))]));
31623
+ const sun = data.solar.objects.find((o) => o.kind === "star");
31624
+ const zoneBand = (zone) => {
31625
+ const label = zone === "inner" ? text.zoneInner : text.zoneOuter;
31626
+ return {
31627
+ label,
31628
+ sub: "",
31629
+ ...scope.link({
31630
+ kind: "zone",
31631
+ zone,
31632
+ name: label
31633
+ })
31634
+ };
31635
+ };
31636
+ const model = solarSystemModel(data.solar, {
31637
+ text,
31638
+ name,
31639
+ primaryLink: sun && scope.body(sun.id, name(sun)),
31640
+ keep: (o) => scope.has(o.id) || !!stacks.get(o.id),
31641
+ stack: (o) => stacks.get(o.id),
31642
+ links: (o, stack) => scope.links(o.id, name(o), stack),
31643
+ band: (belt) => options.zones === "belts" ? { label: belt.label } : zoneBand(belt.kind === "kuiper_belt" ? "outer" : "inner")
31644
+ });
31645
+ if (!model) throw new Error("spacemap: the Solar System map names no Sun");
31646
+ return scope.built(model, text.solarSystem);
31647
+ }
31648
+ function buildSystemView(data, primaryId, exported, options, text, pick) {
31649
+ const entry = systemEntry(data.systems, primaryId);
31650
+ if (!entry) throw new Error(`spacemap: no planetary system around ${primaryId}`);
31651
+ const scope = new Scope(data, options, pick);
31652
+ const system = systemFromMapEntry(entry, scope.name(primaryId, exported[primaryId]));
31653
+ system.moons = system.moons.filter((moon) => scope.has(moon.id)).map((moon) => ({
31654
+ ...moon,
31655
+ name: scope.name(moon.id, exported[moon.id])
31656
+ }));
31657
+ const rKm = system.planetRadiusKm;
31658
+ const bands = system.rings ? [{
31659
+ key: "rings",
31660
+ label: text.rings,
31661
+ innerKm: system.rings.innerRp * rKm,
31662
+ outerKm: system.rings.outerRp * rKm,
31663
+ tone: "amber"
31664
+ }] : [];
31665
+ const model = planetarySystemModel(system, {
31666
+ text,
31667
+ axisLabel: text.axisRadii,
31668
+ bands,
31669
+ primaryLink: scope.body(primaryId, system.planetName),
31670
+ moonLink: (moon) => scope.body(moon.id, moon.name)
31671
+ });
31672
+ model.ticks = model.ticks.filter((t) => tickAt(t, model.domain) < LAST_RADII_TICK_AT);
31673
+ return scope.built(model, system.planetName);
31674
+ }
31675
+ function buildZoneView(data, zone, options, text, pick) {
31676
+ const scope = new Scope(data, options, pick);
31677
+ const { solar, extras } = data;
31678
+ const sun = solar.objects.find((o) => o.kind === "star");
31679
+ const jupiterAu = solar.objects.find((o) => o.id === JUPITER_ID)?.a;
31680
+ if (!sun || jupiterAu === void 0) throw new Error("spacemap: the Solar System map names no Sun or no Jupiter");
31681
+ const small = solar.objects.filter((o) => o.kind === "dwarf" || o.kind === "asteroid").map((o) => ({
31682
+ id: o.id,
31683
+ name: scope.name(o.id, o.name),
31684
+ aAu: o.a,
31685
+ tiltDeg: o.i,
31686
+ radiusKm: o.diameter_km / 2,
31687
+ color: solarColor(o)
31688
+ }));
31689
+ for (const extra of extras) {
31690
+ if (extra.moon || small.some((s) => s.id === extra.id)) continue;
31691
+ if (!(extra.aAu > 0)) continue;
31692
+ small.push({
31693
+ ...extra,
31694
+ name: scope.named(extra.id, extra.name)
31695
+ });
31696
+ }
31697
+ const bodies = [];
31698
+ for (const s of small) {
31699
+ if (zoneOf(s.aAu, jupiterAu) !== zone) continue;
31700
+ const base = {
31701
+ ...s,
31702
+ aKm: s.aAu * AU_KM
31703
+ };
31704
+ const stack = scope.stack(s.id);
31705
+ if (stack) {
31706
+ bodies.push({
31707
+ ...base,
31708
+ satellites: stack.satellites,
31709
+ satelliteCount: stack.count,
31710
+ satellitesTab: true,
31711
+ ...scope.links(s.id, s.name, stack)
31712
+ });
31713
+ continue;
31714
+ }
31715
+ const moons = extras.filter((extra) => extra.moon && extra.parent === s.id && scope.has(extra.id)).map((extra) => {
31716
+ const name = scope.named(extra.id, extra.name);
31717
+ return {
31718
+ ...extra,
31719
+ name,
31720
+ link: scope.body(extra.id, name)
31721
+ };
31722
+ });
31723
+ if (!scope.has(s.id) && !moons.length) continue;
31724
+ bodies.push({
31725
+ ...base,
31726
+ link: scope.body(s.id, s.name),
31727
+ satellites: moons.length ? moons : void 0
31728
+ });
31729
+ }
31730
+ const [lo, hi] = ZONE_DOMAIN[zone];
31731
+ 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))];
31732
+ const within = (au) => au >= domain[0] && au <= domain[1];
31733
+ const pxPerKm = ZONE_TOP_R / Math.max(...bodies.map((b) => b.radiusKm), 1);
31734
+ for (const o of solar.objects) {
31735
+ if (o.kind !== "planet" || !within(o.a)) continue;
31736
+ bodies.push({
31737
+ id: o.id,
31738
+ name: scope.name(o.id, o.name),
31739
+ aKm: o.a * AU_KM,
31740
+ tiltDeg: 0,
31741
+ radiusKm: 0,
31742
+ color: solarColor(o),
31743
+ reference: true
31744
+ });
31745
+ }
31746
+ const label = zone === "inner" ? text.zoneInner : text.zoneOuter;
31747
+ return scope.built({
31748
+ primary: {
31749
+ id: sun.id,
31750
+ name: scope.name(sun.id, sun.name),
31751
+ radiusKm: sun.diameter_km / 2 * SOLAR_PX_PER_KM / pxPerKm,
31752
+ color: SUN_COLOR
31753
+ },
31754
+ bodies,
31755
+ bands: solar.belts.filter((belt) => within(belt.inner_au) && within(belt.outer_au)).map((belt) => ({
31756
+ key: belt.slug,
31757
+ label: belt.label,
31758
+ innerKm: belt.inner_au * AU_KM,
31759
+ outerKm: belt.outer_au * AU_KM,
31760
+ tone: belt.kind === "kuiper_belt" ? "sky" : "muted"
31761
+ })),
31762
+ unitKm: AU_KM,
31763
+ domain,
31764
+ ticks: ZONE_TICKS.filter((t) => within(t) && tickAt(t, domain) < LAST_AU_TICK_AT),
31765
+ axisLabel: text.axisAu,
31766
+ pxPerKm,
31767
+ pxPerDeg: SOLAR_PX_PER_DEG,
31768
+ text
31769
+ }, label);
31770
+ }
31771
+
31772
+ //#endregion
31773
+ //#region src/lib/systemmap/system-map.ts
31774
+ var onceTarget = /* @__PURE__ */ new WeakMap();
31775
+ var AU_TO_KM_THRESHOLD = .01;
31776
+ function viewText() {
31777
+ const { messages, locale } = host();
31778
+ const number = new Intl.NumberFormat(locale(), { maximumSignificantDigits: 3 });
31779
+ const kilometres = new Intl.NumberFormat(locale(), {
31780
+ style: "unit",
31781
+ unit: "kilometer",
31782
+ maximumSignificantDigits: 3
31783
+ });
31784
+ const au = messages.unit_symbol_astronomical_unit();
31785
+ return {
31786
+ primary: messages.planetary_system_primary(),
31787
+ retrograde: messages.planetary_system_retrograde(),
31788
+ moons: (count) => messages.system_map_moons({ count }),
31789
+ distance: (km) => {
31790
+ const distance = km / AU_KM;
31791
+ return distance > 0 && distance < AU_TO_KM_THRESHOLD ? kilometres.format(km) : `${number.format(distance)} ${au}`;
31792
+ },
31793
+ axisAu: messages.system_map_axis_au(),
31794
+ solarSystem: messages.system_map_solar_system(),
31795
+ rings: messages.tab_rings(),
31796
+ axisRadii: messages.planetary_system_axis_unit(),
31797
+ zoneInner: messages.system_map_zone_inner(),
31798
+ zoneOuter: messages.system_map_zone_outer()
31799
+ };
31800
+ }
31801
+ var SystemMap = class {
31802
+ chart = new SystemMapChart();
31803
+ listeners = /* @__PURE__ */ new Map();
31804
+ view;
31805
+ bodies;
31806
+ names;
31807
+ grouping;
31808
+ zones;
31809
+ places;
31810
+ targets = [];
31811
+ extras = /* @__PURE__ */ new Map();
31812
+ queue = Promise.resolve();
31813
+ disposed = false;
31814
+ constructor(options = {}) {
31815
+ this.view = options.view ?? { kind: "solar-system" };
31816
+ this.bodies = options.bodies ? new Set(options.bodies) : null;
31817
+ this.names = options.names ?? {};
31818
+ this.grouping = options.grouping ?? "bodies";
31819
+ this.zones = options.zones ?? "belts";
31820
+ this.places = (options.places ?? []).map(extraFromPlace);
31821
+ }
31822
+ mount(container) {
31823
+ container.append(this.chart.root);
31824
+ }
31825
+ remove() {
31826
+ this.disposed = true;
31827
+ this.chart.destroy();
31828
+ }
31829
+ load() {
31830
+ return this.draw();
31831
+ }
31832
+ async setView(view) {
31833
+ await this.draw(view);
31834
+ }
31835
+ getView() {
31836
+ return this.view;
31837
+ }
31838
+ setBodies(ids) {
31839
+ const bodies = ids ? new Set(ids) : null;
31840
+ return this.draw(void 0, bodies).catch(() => {});
31841
+ }
31842
+ getTargets() {
31843
+ return [...this.targets];
31844
+ }
31845
+ draw(view, bodies) {
31846
+ const turn = this.queue.then(() => {
31847
+ if (bodies !== void 0) this.bodies = bodies;
31848
+ return this.drawNow(view ?? this.view);
31849
+ });
31850
+ this.queue = turn.catch(() => {});
31851
+ return turn;
31852
+ }
31853
+ async drawNow(view) {
31854
+ let built;
31855
+ try {
31856
+ built = await this.build(view);
31857
+ } catch (cause) {
31858
+ const error = cause instanceof Error ? cause : new Error(String(cause));
31859
+ if (!this.disposed) this.emit("error", error);
31860
+ throw error;
31861
+ }
31862
+ if (this.disposed) return;
31863
+ const changed = JSON.stringify(view) !== JSON.stringify(this.view);
31864
+ this.view = view;
31865
+ this.targets = built.targets;
31866
+ this.chart.update(built.model, { ariaLabel: built.label });
31867
+ if (changed) this.emit("viewchange", view);
31868
+ }
31869
+ async build(view) {
31870
+ const [solar, systems] = await Promise.all([fetchSolarSystemMap(), fetchPlanetarySystemsMap()]);
31871
+ const options = {
31872
+ bodies: this.bodies,
31873
+ names: this.names,
31874
+ grouping: this.grouping,
31875
+ zones: this.zones
31876
+ };
31877
+ const text = viewText();
31878
+ const pick = (target) => this.emit("select", target);
31879
+ const data = {
31880
+ solar,
31881
+ systems,
31882
+ extras: []
31883
+ };
31884
+ switch (view.kind) {
31885
+ case "solar-system": return buildSolarView(data, options, text, pick);
31886
+ case "system": return buildSystemView(data, view.id, await this.exportedNames(view.id, systems), options, text, pick);
31887
+ case "zone": {
31888
+ const ids = unplacedIds(solar, systems, this.bodies, this.places.map((place) => place.id));
31889
+ const read = (await Promise.all(ids.map((id) => this.extra(id)))).filter((extra) => extra !== null);
31890
+ const placed = [...this.places, ...read];
31891
+ const parents = unplacedIds(solar, systems, new Set(placed.flatMap((extra) => extra.moon && extra.parent ? [extra.parent] : [])), placed.map((extra) => extra.id));
31892
+ const hosts = await Promise.all(parents.map((id) => this.extra(id)));
31893
+ const extras = [...placed, ...hosts.filter((extra) => extra !== null)];
31894
+ return buildZoneView({
31895
+ ...data,
31896
+ extras
31897
+ }, view.zone, options, text, pick);
31898
+ }
31899
+ }
31900
+ }
31901
+ extra(id) {
31902
+ let pending = this.extras.get(id);
31903
+ if (!pending) {
31904
+ pending = fetchObjectDetail(id, false).then((detail) => {
31905
+ if (!detail.global) throw new Error(`spacemap: no such body as ${id}`);
31906
+ return extraFromBundle(detail.global);
31907
+ }).catch((cause) => {
31908
+ this.extras.delete(id);
31909
+ this.emit("error", cause instanceof Error ? cause : new Error(String(cause)));
31910
+ return null;
31911
+ });
31912
+ this.extras.set(id, pending);
31913
+ }
31914
+ return pending;
31915
+ }
31916
+ async exportedNames(primaryId, systems) {
31917
+ 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 {};
31918
+ try {
31919
+ const { global, localized } = await fetchObjectDetail(primaryId, true, host().locale());
31920
+ const names = {};
31921
+ for (const moon of global?.notable_moons ?? []) if (moon.id) names[moon.id] = moon.name;
31922
+ Object.assign(names, localized?.notable_moon_names);
31923
+ const name = localized?.name ?? global?.name;
31924
+ if (name) names[primaryId] = name;
31925
+ return names;
31926
+ } catch {
31927
+ return {};
31928
+ }
31929
+ }
31930
+ on(event, listener) {
31931
+ let set = this.listeners.get(event);
31932
+ if (!set) {
31933
+ set = /* @__PURE__ */ new Set();
31934
+ this.listeners.set(event, set);
31935
+ }
31936
+ set.add(listener);
31937
+ return () => set.delete(listener);
31938
+ }
31939
+ once(event, listener) {
31940
+ const wrapped = ((...args) => {
31941
+ off();
31942
+ listener(...args);
31943
+ });
31944
+ onceTarget.set(wrapped, listener);
31945
+ const off = this.on(event, wrapped);
31946
+ return off;
31947
+ }
31948
+ off(event, listener) {
31949
+ const set = this.listeners.get(event);
31950
+ if (!set) return;
31951
+ set.delete(listener);
31952
+ for (const held of set) if (onceTarget.get(held) === listener) set.delete(held);
31953
+ }
31954
+ emit(event, ...args) {
31955
+ const set = this.listeners.get(event);
31956
+ if (!set) return;
31957
+ for (const listener of [...set]) listener(...args);
31958
+ }
31959
+ };
31960
+
30077
31961
  //#endregion
30078
31962
  //#region src/lib/scene/state/attribution.ts
30079
31963
  var DEFAULT_ORBIT_LABELS = Object.fromEntries(ORBIT_SOURCE_ORDER.map((s) => [s, ORBIT_SOURCES[s].label]));
@@ -30414,6 +32298,27 @@ async function createPanorama(options) {
30414
32298
  await view.load();
30415
32299
  return view;
30416
32300
  }
32301
+ async function createSystemMap(options) {
32302
+ const { container, dataUrl, imagesUrl, locale, messages, includeNonCommercial, events, ...rest } = options;
32303
+ const element = resolveContainer(container);
32304
+ applyHost({
32305
+ dataUrl,
32306
+ imagesUrl,
32307
+ locale,
32308
+ messages,
32309
+ includeNonCommercial
32310
+ });
32311
+ const map = new SystemMap(rest);
32312
+ map.mount(element);
32313
+ for (const [event, listener] of Object.entries(events ?? {})) map.on(event, listener);
32314
+ try {
32315
+ await map.load();
32316
+ } catch (error) {
32317
+ map.remove();
32318
+ throw error;
32319
+ }
32320
+ return map;
32321
+ }
30417
32322
  function resolveContainer(container) {
30418
32323
  if (typeof container !== "string") return container;
30419
32324
  const element = document.querySelector(container);
@@ -30431,5 +32336,5 @@ function applyHost(options) {
30431
32336
  }
30432
32337
 
30433
32338
  //#endregion
30434
- 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 };
30435
- (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);})();
32339
+ 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 };
32340
+ (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);})();