spacemap 0.1.20 → 0.1.21

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
@@ -2623,7 +2623,7 @@ function hydrate(component, options) {
2623
2623
  set_hydrate_node(previous_hydrate_node);
2624
2624
  }
2625
2625
  }
2626
- var listeners = /* @__PURE__ */ new Map();
2626
+ var listeners$1 = /* @__PURE__ */ new Map();
2627
2627
  function _mount(Component, { target, anchor, props = {}, events, context, intro = true, transformError }) {
2628
2628
  init_operations();
2629
2629
  var component = void 0;
@@ -2655,10 +2655,10 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
2655
2655
  registered_events.add(event_name);
2656
2656
  var passive = is_passive_event(event_name);
2657
2657
  for (const node of [target, document]) {
2658
- var counts = listeners.get(node);
2658
+ var counts = listeners$1.get(node);
2659
2659
  if (counts === void 0) {
2660
2660
  counts = /* @__PURE__ */ new Map();
2661
- listeners.set(node, counts);
2661
+ listeners$1.set(node, counts);
2662
2662
  }
2663
2663
  var count = counts.get(event_name);
2664
2664
  if (count === void 0) {
@@ -2672,12 +2672,12 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
2672
2672
  root_event_handles.add(event_handle);
2673
2673
  return () => {
2674
2674
  for (var event_name of registered_events) for (const node of [target, document]) {
2675
- var counts = listeners.get(node);
2675
+ var counts = listeners$1.get(node);
2676
2676
  var count = counts.get(event_name);
2677
2677
  if (--count == 0) {
2678
2678
  node.removeEventListener(event_name, handle_event_propagation);
2679
2679
  counts.delete(event_name);
2680
- if (counts.size === 0) listeners.delete(node);
2680
+ if (counts.size === 0) listeners$1.delete(node);
2681
2681
  } else counts.set(event_name, count);
2682
2682
  }
2683
2683
  root_event_handles.delete(event_handle);
@@ -6709,6 +6709,7 @@ var BodyIndex = class {
6709
6709
  focusFeature = null;
6710
6710
  extrasolar = /* @__PURE__ */ new Map();
6711
6711
  pinnedExtrasolar = /* @__PURE__ */ new Set();
6712
+ tripCentreId = null;
6712
6713
  exoplanets = /* @__PURE__ */ new Map();
6713
6714
  exoplanetAliases = /* @__PURE__ */ new Map();
6714
6715
  dirtyAsteroidZones = /* @__PURE__ */ new Set();
@@ -6730,6 +6731,7 @@ var BodyIndex = class {
6730
6731
  set(this.#orientationVersion, value, true);
6731
6732
  }
6732
6733
  orientationRead = /* @__PURE__ */ new Set();
6734
+ nutationAwaited = /* @__PURE__ */ new Map();
6733
6735
  childrenByParent = /* @__PURE__ */ new Map();
6734
6736
  moonMaxAByParent = /* @__PURE__ */ new Map();
6735
6737
  addListeners = /* @__PURE__ */ new Set();
@@ -6769,10 +6771,13 @@ var BodyIndex = class {
6769
6771
  this.pinnedExtrasolar = ids;
6770
6772
  this.trimExtrasolar();
6771
6773
  }
6774
+ pinTripCentre(id) {
6775
+ this.tripCentreId = id;
6776
+ }
6772
6777
  trimExtrasolar() {
6773
6778
  for (const id of this.extrasolar.keys()) {
6774
6779
  if (this.extrasolar.size - this.pinnedExtrasolar.size <= EXTRASOLAR_KEPT) break;
6775
- if (!this.pinnedExtrasolar.has(id)) this.extrasolar.delete(id);
6780
+ if (id !== this.tripCentreId && !this.pinnedExtrasolar.has(id)) this.extrasolar.delete(id);
6776
6781
  }
6777
6782
  }
6778
6783
  addExoplanets(bodies) {
@@ -7094,15 +7099,70 @@ async function fetchWithTimeout(input, init = {}, timeoutMs = BOOT_FETCH_TIMEOUT
7094
7099
  });
7095
7100
  }
7096
7101
 
7102
+ // src/lib/fetch/online.ts
7103
+ function untilOnline() {
7104
+ if (typeof navigator === "undefined" || navigator.onLine !== false) return Promise.resolve();
7105
+ return new Promise((resolve) => window.addEventListener("online", () => resolve(), { once: true }));
7106
+ }
7107
+
7108
+ // src/lib/fetch/retry-backoff.ts
7109
+ var FIRST_DELAY_MS = 2e3;
7110
+ var MAX_DELAY_MS = 6e4;
7111
+ var JITTER = .25;
7112
+ var RetryBackoff = class {
7113
+ onDue;
7114
+ random;
7115
+ failures = /* @__PURE__ */ new Map();
7116
+ constructor(onDue = () => {}, random = Math.random) {
7117
+ this.onDue = onDue;
7118
+ this.random = random;
7119
+ }
7120
+ waiting(key) {
7121
+ return this.failures.get(key)?.timer !== void 0;
7122
+ }
7123
+ fail(key) {
7124
+ const last = this.failures.get(key);
7125
+ clearTimeout(last?.timer);
7126
+ const failure = {
7127
+ count: (last?.count ?? 0) + 1,
7128
+ timer: void 0
7129
+ };
7130
+ const delay = Math.min(FIRST_DELAY_MS * 2 ** (failure.count - 1), MAX_DELAY_MS);
7131
+ const spread = 1 + JITTER * (2 * this.random() - 1);
7132
+ failure.timer = setTimeout(() => {
7133
+ failure.timer = void 0;
7134
+ this.onDue(key);
7135
+ }, delay * spread);
7136
+ this.failures.set(key, failure);
7137
+ }
7138
+ clear(key) {
7139
+ clearTimeout(this.failures.get(key)?.timer);
7140
+ this.failures.delete(key);
7141
+ }
7142
+ };
7143
+
7097
7144
  // src/lib/fetch/systems-global.ts
7098
7145
  var phases = /* @__PURE__ */ new Map();
7099
7146
  var gmKm3s2 = /* @__PURE__ */ new Map();
7100
7147
  var loadPromise$1 = null;
7148
+ var loaded = false;
7149
+ var absent = false;
7150
+ var listeners = /* @__PURE__ */ new Set();
7151
+ var RETRY_KEY = "global";
7152
+ var retry = new RetryBackoff(() => void askAgain());
7153
+ var StatusError = class extends Error {
7154
+ status;
7155
+ constructor(status) {
7156
+ super(`systems/global.json: ${status}`);
7157
+ this.status = status;
7158
+ }
7159
+ };
7160
+ var permanent = (e) => e instanceof StatusError && e.status >= 400 && e.status < 500 && e.status !== 408 && e.status !== 429;
7101
7161
  function loadSystemsGlobal() {
7102
7162
  if (loadPromise$1) return loadPromise$1;
7103
7163
  const p = (async () => {
7104
7164
  const r = await fetchWithTimeout(`${dataBase()}/v1/systems/global.json`);
7105
- if (!r.ok) throw new Error(`systems/global.json: ${r.status}`);
7165
+ if (!r.ok) throw new StatusError(r.status);
7106
7166
  const raw = await r.json();
7107
7167
  if (raw.gm) for (const [k, v] of Object.entries(raw.gm)) gmKm3s2.set(parseInt(k, 10), v);
7108
7168
  if (raw.nut_prec_angles) for (const [k, v] of Object.entries(raw.nut_prec_angles)) {
@@ -7112,13 +7172,51 @@ function loadSystemsGlobal() {
7112
7172
  phaseDegree
7113
7173
  });
7114
7174
  }
7175
+ loaded = true;
7115
7176
  })();
7116
- p.catch(() => {
7177
+ p.then(() => {
7178
+ retry.clear(RETRY_KEY);
7179
+ settle();
7180
+ }, (e) => {
7117
7181
  if (loadPromise$1 === p) loadPromise$1 = null;
7182
+ if (!permanent(e)) {
7183
+ retry.fail(RETRY_KEY);
7184
+ return;
7185
+ }
7186
+ absent = true;
7187
+ retry.clear(RETRY_KEY);
7188
+ settle();
7118
7189
  });
7119
7190
  loadPromise$1 = p;
7120
7191
  return p;
7121
7192
  }
7193
+ function settle() {
7194
+ const told = [...listeners];
7195
+ listeners.clear();
7196
+ for (const listener of told) try {
7197
+ listener();
7198
+ } catch (e) {
7199
+ console.error("systems-global: a listener failed:", e);
7200
+ }
7201
+ }
7202
+ function onSystemsGlobalSettled(listener) {
7203
+ if (loaded || absent) {
7204
+ listener();
7205
+ return () => {};
7206
+ }
7207
+ listeners.add(listener);
7208
+ askAgain();
7209
+ return () => listeners.delete(listener);
7210
+ }
7211
+ async function askAgain() {
7212
+ if (loaded || absent || listeners.size === 0 || retry.waiting(RETRY_KEY)) return;
7213
+ await untilOnline();
7214
+ if (listeners.size === 0) return;
7215
+ await loadSystemsGlobal().catch(() => {});
7216
+ }
7217
+ function nutPrecAwaited(id, sums) {
7218
+ return !!sums && !loaded && !absent && naifId(id) !== null;
7219
+ }
7122
7220
  function nutPrecFor(id, sums) {
7123
7221
  const naif = naifId(id);
7124
7222
  if (naif === null || !sums) return void 0;
@@ -9046,14 +9144,14 @@ function writeTrailAlphas(fullArr, trailArr, posArr, isOpenCurve, useTrail) {
9046
9144
  // src/lib/fetch/position/chunk.ts
9047
9145
  var SSB_NAIF_ID = 0;
9048
9146
  var SUN_NAIF_ID = 10;
9049
- function chebyshevOsculatingElements(body, parentId, jd, origin) {
9147
+ function chebyshevOsculatingElements(body, parentId, jd, origin, ownGm = 0) {
9050
9148
  const gmKm3s2 = getGmKm3s2(parentId);
9051
9149
  if (!gmKm3s2) return null;
9052
9150
  const state = chebyshevStateKm(body, jd);
9053
9151
  if (!state) return null;
9054
9152
  const from = origin ? chebyshevStateKm(origin, jd) : null;
9055
9153
  if (origin && !from) return null;
9056
- const muAuDay2 = gmKm3s2 * KM3_S2_TO_AU3_DAY2;
9154
+ const muAuDay2 = (gmKm3s2 + ownGm) * KM3_S2_TO_AU3_DAY2;
9057
9155
  const r = [
9058
9156
  (state.position[0] - (from?.position[0] ?? 0)) / AU_KM,
9059
9157
  (state.position[1] - (from?.position[1] ?? 0)) / AU_KM,
@@ -9068,7 +9166,7 @@ function chebyshevOsculatingElements(body, parentId, jd, origin) {
9068
9166
  }
9069
9167
  function chebyshevHelioElements(body, jd, sun) {
9070
9168
  if (!sun || body.parentId !== SSB_NAIF_ID || body.naifId === SUN_NAIF_ID) return null;
9071
- return chebyshevOsculatingElements(body, SUN_NAIF_ID, jd, sun);
9169
+ return chebyshevOsculatingElements(body, SUN_NAIF_ID, jd, sun, getGmKm3s2(body.naifId));
9072
9170
  }
9073
9171
  function elementsUrl(zone, zoom, part, time) {
9074
9172
  return time ? chunkedPartedUrl(zone, zoom, time, part) : partedUrl(zone, zoom, part);
@@ -9716,42 +9814,6 @@ async function fetchChebyshev(zone, zoom, chunk, priority) {
9716
9814
  };
9717
9815
  }
9718
9816
 
9719
- // src/lib/fetch/retry-backoff.ts
9720
- var FIRST_DELAY_MS = 2e3;
9721
- var MAX_DELAY_MS = 6e4;
9722
- var JITTER = .25;
9723
- var RetryBackoff = class {
9724
- onDue;
9725
- random;
9726
- failures = /* @__PURE__ */ new Map();
9727
- constructor(onDue = () => {}, random = Math.random) {
9728
- this.onDue = onDue;
9729
- this.random = random;
9730
- }
9731
- waiting(key) {
9732
- return this.failures.get(key)?.timer !== void 0;
9733
- }
9734
- fail(key) {
9735
- const last = this.failures.get(key);
9736
- clearTimeout(last?.timer);
9737
- const failure = {
9738
- count: (last?.count ?? 0) + 1,
9739
- timer: void 0
9740
- };
9741
- const delay = Math.min(FIRST_DELAY_MS * 2 ** (failure.count - 1), MAX_DELAY_MS);
9742
- const spread = 1 + JITTER * (2 * this.random() - 1);
9743
- failure.timer = setTimeout(() => {
9744
- failure.timer = void 0;
9745
- this.onDue(key);
9746
- }, delay * spread);
9747
- this.failures.set(key, failure);
9748
- }
9749
- clear(key) {
9750
- clearTimeout(this.failures.get(key)?.timer);
9751
- this.failures.delete(key);
9752
- }
9753
- };
9754
-
9755
9817
  // src/lib/fetch/position/chunk-window.ts
9756
9818
  var NEIGHBOR_WINDOW = 1;
9757
9819
  var RESOLVED = Promise.resolve();
@@ -10743,7 +10805,7 @@ function nodeInfo(container) {
10743
10805
  var EXTRASOLAR_FORMAT = 1;
10744
10806
  var LITERATURE_PAGE = "/constants/stars/literature.py";
10745
10807
  function isWikidataCredit(credit) {
10746
- return credit.source.includes("//www.wikidata.org/");
10808
+ return credit.source === "https://www.wikidata.org/";
10747
10809
  }
10748
10810
  function isPaperCredit(credit) {
10749
10811
  return /\b\d{4}$/.test(credit.organisation) || credit.source.includes("/constants/stars/literature.py");
@@ -15927,7 +15989,7 @@ function typePriority(type) {
15927
15989
  }
15928
15990
  }
15929
15991
  function isDrawn(bo) {
15930
- return bo.body.position !== null && !bo.carried;
15992
+ return bo.body.position !== null && !bo.carried && !bo.body.nutPrecAwaited;
15931
15993
  }
15932
15994
 
15933
15995
  // src/lib/scene/label/culling.ts
@@ -16707,16 +16769,33 @@ function shapeModelSkipReason(global) {
16707
16769
 
16708
16770
  // src/lib/scene/objects/body/orientation-apply.ts
16709
16771
  function applyBodyOrientation(body, orientation, nutPrec, ctx, systemId) {
16772
+ const id = body.data.id;
16710
16773
  body.orientation = orientation;
16711
- body.nutPrec = nutPrecFor(body.data.id, nutPrec);
16774
+ body.nutPrec = nutPrecFor(id, nutPrec);
16775
+ body.nutPrecAwaited = nutPrecAwaited(id, nutPrec) ? nutPrec : void 0;
16776
+ if (body.nutPrecAwaited) ctx?.bodies.nutationAwaited.set(id, body);
16777
+ else ctx?.bodies.nutationAwaited.delete(id);
16712
16778
  if (ctx) ctx.bodies.dataVersion++;
16713
16779
  ctx?.credits.registerOrientation({
16714
- bodyId: body.data.id,
16715
- systemId: systemId ?? body.data.id,
16780
+ bodyId: id,
16781
+ systemId: systemId ?? id,
16716
16782
  source: orientation.source,
16717
16783
  reference: orientation.reference
16718
16784
  });
16719
16785
  }
16786
+ function joinAwaitedNutation(ctx) {
16787
+ const awaited = ctx.bodies.nutationAwaited;
16788
+ const turned = [];
16789
+ for (const body of awaited.values()) {
16790
+ body.nutPrec = nutPrecFor(body.data.id, body.nutPrecAwaited);
16791
+ body.nutPrecAwaited = void 0;
16792
+ if (body.nutPrec) turned.push(body);
16793
+ }
16794
+ const settled = awaited.size > 0;
16795
+ awaited.clear();
16796
+ if (settled) ctx.bodies.dataVersion++;
16797
+ return turned;
16798
+ }
16720
16799
 
16721
16800
  // src/lib/scene/objects/body/model-radial.ts
16722
16801
  var LAT_CELLS = 48;
@@ -16979,7 +17058,9 @@ function modelUnitScene(bo) {
16979
17058
  return bo.modelUnitOverride ?? bo.radiusScene;
16980
17059
  }
16981
17060
  var attachedModelRoots = /* @__PURE__ */ new Set();
16982
- function mountModel(bo, root) {
17061
+ function mountModel(bo, root, jd) {
17062
+ if (bo.mesh) root.quaternion.copy(bo.mesh.quaternion);
17063
+ else if (bo.body.orientation && Number.isFinite(jd)) applyOrientation(root, bo.body.orientation, jd, bo.body.nutPrec);
16983
17064
  const wrapper = new Group();
16984
17065
  wrapper.scale.setScalar(modelUnitScene(bo));
16985
17066
  wrapper.userData.pickBody = bo.body;
@@ -17055,6 +17136,7 @@ async function loadSpacecraftModel(bo, modelScene, ctx) {
17055
17136
  root.visible = !bo.isCrashed;
17056
17137
  bo.model = root;
17057
17138
  bo.modelName = slug;
17139
+ if (ctx) ctx.bodies.dataVersion++;
17058
17140
  setHaloLoading(bo, false);
17059
17141
  const spanM = meta.exports[tier]?.scale_meters;
17060
17142
  if (spanM) {
@@ -17101,6 +17183,10 @@ async function loadNaturalBodyModel(bo, ctx) {
17101
17183
  console.warn(`Failed to load shape-model metadata for ${slug}:`, e);
17102
17184
  return;
17103
17185
  }
17186
+ if ((bo.modelLoadEpoch ?? 0) !== epoch) {
17187
+ disposeGltf(gltf.scene);
17188
+ return;
17189
+ }
17104
17190
  if (meta.kind !== "shape_model") {
17105
17191
  disposeGltf(gltf.scene);
17106
17192
  return;
@@ -17128,7 +17214,7 @@ async function loadNaturalBodyModel(bo, ctx) {
17128
17214
  setLabelAnnotation(bo, "missing", null);
17129
17215
  }
17130
17216
  }
17131
- mountModel(bo, root);
17217
+ mountModel(bo, root, ctx?.placer?.jd ?? NaN);
17132
17218
  bo.modelName = slug;
17133
17219
  if (bo.mesh) bo.mesh.visible = false;
17134
17220
  const credit = shapeModelCredit(meta);
@@ -20456,6 +20542,54 @@ function planetBody(planet, elements, orbit, file, hostRecord) {
20456
20542
  };
20457
20543
  }
20458
20544
 
20545
+ // src/lib/math/travel/vec3.ts
20546
+ function add(a, b) {
20547
+ return [
20548
+ a[0] + b[0],
20549
+ a[1] + b[1],
20550
+ a[2] + b[2]
20551
+ ];
20552
+ }
20553
+ function sub(a, b) {
20554
+ return [
20555
+ a[0] - b[0],
20556
+ a[1] - b[1],
20557
+ a[2] - b[2]
20558
+ ];
20559
+ }
20560
+ function scale(a, k) {
20561
+ return [
20562
+ a[0] * k,
20563
+ a[1] * k,
20564
+ a[2] * k
20565
+ ];
20566
+ }
20567
+ function dot$1(a, b) {
20568
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
20569
+ }
20570
+ function cross$1(a, b) {
20571
+ return [
20572
+ a[1] * b[2] - a[2] * b[1],
20573
+ a[2] * b[0] - a[0] * b[2],
20574
+ a[0] * b[1] - a[1] * b[0]
20575
+ ];
20576
+ }
20577
+ function norm(a) {
20578
+ return Math.hypot(a[0], a[1], a[2]);
20579
+ }
20580
+ function normalize(a) {
20581
+ const n = norm(a);
20582
+ return n === 0 ? [
20583
+ 0,
20584
+ 0,
20585
+ 0
20586
+ ] : [
20587
+ a[0] / n,
20588
+ a[1] / n,
20589
+ a[2] / n
20590
+ ];
20591
+ }
20592
+
20459
20593
  // src/lib/scene/exoplanets/pair-orbits.ts
20460
20594
  var MAX_PERIOD_DAYS = 36525e3;
20461
20595
  var POINT_ALONE_PX = 2;
@@ -20486,6 +20620,20 @@ function barycentreOf(first, second, share) {
20486
20620
  first[2] + share * (second[2] * depth - first[2])
20487
20621
  ];
20488
20622
  }
20623
+ function nodePlace(id, pairs, sceneOf) {
20624
+ const pair = pairs.find((p) => p.node.id === id);
20625
+ if (!pair || pair.node.point) {
20626
+ const at = sceneOf(id);
20627
+ return at && [
20628
+ at[0],
20629
+ at[1],
20630
+ at[2]
20631
+ ];
20632
+ }
20633
+ const first = nodePlace(pair.first, pairs, sceneOf);
20634
+ const second = nodePlace(pair.second, pairs, sceneOf);
20635
+ return first && second && barycentreOf(first, second, pair.orbit.barycentre);
20636
+ }
20489
20637
  function drawnPairs(nodes) {
20490
20638
  const draws = ({ orbit, members }) => orbit !== null && orbit.period <= MAX_PERIOD_DAYS && members.length === 2 && members.every((member) => {
20491
20639
  if (member.id === null) return false;
@@ -20686,13 +20834,7 @@ function samePlace(a, b) {
20686
20834
  async function holdStarSystem(ctx, plan) {
20687
20835
  await Promise.all(plan.ids.map((id) => ensureExtrasolarBody(ctx, extrasolarObjectId(id))));
20688
20836
  const anchor = (id) => ctx.getBody(extrasolarObjectId(id))?.extrasolar;
20689
- const placeOf = (id) => {
20690
- const pair = plan.pairs.find((p) => p.node.id === id);
20691
- if (!pair || pair.node.point) return anchor(id)?.scene ?? null;
20692
- const first = placeOf(pair.first);
20693
- const second = placeOf(pair.second);
20694
- return first && second && barycentreOf(first, second, pair.orbit.barycentre);
20695
- };
20837
+ const placeOf = (id) => nodePlace(id, plan.pairs, (at) => anchor(at)?.scene ?? null);
20696
20838
  const pairs = plan.pairs.filter(({ node, first, second }) => placeOf(node.id) !== null && anchor(first) !== void 0 && anchor(second) !== void 0);
20697
20839
  const insidePoints = /* @__PURE__ */ new Map();
20698
20840
  for (const pair of pairs) {
@@ -21207,6 +21349,13 @@ var ExoplanetStore = class {
21207
21349
  this.trim();
21208
21350
  }
21209
21351
  view(objectId) {
21352
+ const frame = this.frameOf(objectId);
21353
+ return frame && systemView(frame.at, frame.normal, frame.outer || 1);
21354
+ }
21355
+ planeNormal(objectId) {
21356
+ return this.frameOf(objectId)?.normal ?? null;
21357
+ }
21358
+ frameOf(objectId) {
21210
21359
  const planet = planetOfObjectId(objectId);
21211
21360
  const hostId = planet?.host ?? extrasolarIdOf(objectId);
21212
21361
  if (hostId === null) return null;
@@ -21217,13 +21366,15 @@ var ExoplanetStore = class {
21217
21366
  if (!held && !pair || !at) return null;
21218
21367
  const planets = held?.file.planets.filter((p) => p.host === hostId) ?? [];
21219
21368
  const plane = planets.find((p) => p.number === planet?.n)?.elements ?? pair?.orbit ?? outermostElements(planets);
21220
- const normal = plane ? orbitNormal(plane.i, plane.om) : [
21221
- 0,
21222
- 1,
21223
- 0
21224
- ];
21225
- const outer = Math.max(pair?.orbit.a ?? 0, outerOrbitAU(planets) ?? 0);
21226
- return systemView(at, normal, outer || 1);
21369
+ return {
21370
+ at,
21371
+ normal: plane ? orbitNormal(plane.i, plane.om) : [
21372
+ 0,
21373
+ 1,
21374
+ 0
21375
+ ],
21376
+ outer: Math.max(pair?.orbit.a ?? 0, outerOrbitAU(planets) ?? 0)
21377
+ };
21227
21378
  }
21228
21379
  pairOf(star) {
21229
21380
  const root = star && systemRoot(star.record);
@@ -27778,54 +27929,6 @@ function refreshTrail(body, line, basisPos, jd, view) {
27778
27929
  recordRewrite(ud, curve, offX, offY, offZ, headX, headY, headZ, posArr, n, view);
27779
27930
  }
27780
27931
 
27781
- // src/lib/math/travel/vec3.ts
27782
- function add(a, b) {
27783
- return [
27784
- a[0] + b[0],
27785
- a[1] + b[1],
27786
- a[2] + b[2]
27787
- ];
27788
- }
27789
- function sub(a, b) {
27790
- return [
27791
- a[0] - b[0],
27792
- a[1] - b[1],
27793
- a[2] - b[2]
27794
- ];
27795
- }
27796
- function scale(a, k) {
27797
- return [
27798
- a[0] * k,
27799
- a[1] * k,
27800
- a[2] * k
27801
- ];
27802
- }
27803
- function dot$1(a, b) {
27804
- return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
27805
- }
27806
- function cross$1(a, b) {
27807
- return [
27808
- a[1] * b[2] - a[2] * b[1],
27809
- a[2] * b[0] - a[0] * b[2],
27810
- a[0] * b[1] - a[1] * b[0]
27811
- ];
27812
- }
27813
- function norm(a) {
27814
- return Math.hypot(a[0], a[1], a[2]);
27815
- }
27816
- function normalize(a) {
27817
- const n = norm(a);
27818
- return n === 0 ? [
27819
- 0,
27820
- 0,
27821
- 0
27822
- ] : [
27823
- a[0] / n,
27824
- a[1] / n,
27825
- a[2] / n
27826
- ];
27827
- }
27828
-
27829
27932
  // src/lib/math/travel/path-sample.ts
27830
27933
  function crossingWindow(path, index) {
27831
27934
  const count = path.arcs[index].points.length;
@@ -27952,7 +28055,7 @@ function eclipticToScene(r) {
27952
28055
  -r[1] * k
27953
28056
  ];
27954
28057
  }
27955
- function elementsToState(el, jd, muKm3S2) {
28058
+ function elementsToState(el, jd) {
27956
28059
  const { a, e, ma, n, epoch, q, tp } = el;
27957
28060
  const t = elementsJd(el, jd);
27958
28061
  const { om, w } = propagateOrbitAngles(el, jd);
@@ -27961,15 +28064,14 @@ function elementsToState(el, jd, muKm3S2) {
27961
28064
  if (q == null || tp == null || !isFinite(q) || !isFinite(tp) || q <= 0) return null;
27962
28065
  const solved = solveBarker(q, tp, t);
27963
28066
  if (!solved) return null;
27964
- const mu = muKm3S2 && muKm3S2 > 0 ? muKm3S2 : GM_SUN_KM3_S2;
27965
28067
  const rKm = solved.r * AU_KM;
27966
28068
  const pKm = 2 * q * AU_KM;
27967
- return buildState(rKm, solved.nu, pKm, 1, mu, w, el.i, om, equatorial);
28069
+ return buildState(rKm, solved.nu, pKm, 1, GM_SUN_KM3_S2, w, el.i, om, equatorial);
27968
28070
  }
27969
28071
  if (!isFinite(e) || !isFinite(ma)) return null;
27970
28072
  const aKm = a * AU_KM;
27971
28073
  const nRadPerSec = n * DEG2RAD$1 / SEC_PER_DAY$1;
27972
- const mu = muKm3S2 && muKm3S2 > 0 ? muKm3S2 : nRadPerSec * nRadPerSec * Math.abs(aKm) * Math.abs(aKm) * Math.abs(aKm);
28074
+ const mu = nRadPerSec * nRadPerSec * Math.abs(aKm) * Math.abs(aKm) * Math.abs(aKm);
27973
28075
  if (!isFinite(mu) || mu <= 0) return null;
27974
28076
  const M = (ma + n * (t - epoch)) * DEG2RAD$1;
27975
28077
  let nu;
@@ -28008,6 +28110,37 @@ function buildState(rKm, nu, pKm, e, mu, w, i, om, equatorial) {
28008
28110
  };
28009
28111
  }
28010
28112
 
28113
+ // src/lib/scene/objects/travel/stop-at-camera.ts
28114
+ function depthAt(positions, i, eye, ahead) {
28115
+ return (positions[i * 3] - eye.x) * ahead.x + (positions[i * 3 + 1] - eye.y) * ahead.y + (positions[i * 3 + 2] - eye.z) * ahead.z;
28116
+ }
28117
+ function collapse(positions, kept, gone, from, to, eye, ahead, clearance) {
28118
+ const keptDepth = depthAt(positions, kept, eye, ahead);
28119
+ const goneDepth = depthAt(positions, gone, eye, ahead);
28120
+ const share = Math.max(0, (keptDepth - clearance) / (keptDepth - goneDepth));
28121
+ const kx = positions[kept * 3];
28122
+ const ky = positions[kept * 3 + 1];
28123
+ const kz = positions[kept * 3 + 2];
28124
+ const x = kx + (positions[gone * 3] - kx) * share;
28125
+ const y = ky + (positions[gone * 3 + 1] - ky) * share;
28126
+ const z = kz + (positions[gone * 3 + 2] - kz) * share;
28127
+ for (let i = from; i < to; i++) {
28128
+ positions[i * 3] = x;
28129
+ positions[i * 3 + 1] = y;
28130
+ positions[i * 3 + 2] = z;
28131
+ }
28132
+ }
28133
+ function stopAtCamera(positions, count, eye, ahead, clearance) {
28134
+ if (depthAt(positions, 0, eye, ahead) > 0 && depthAt(positions, count - 1, eye, ahead) > 0) return;
28135
+ let first = 0;
28136
+ while (first < count && depthAt(positions, first, eye, ahead) <= 0) first++;
28137
+ if (first === count) return;
28138
+ let last = count - 1;
28139
+ while (depthAt(positions, last, eye, ahead) <= 0) last--;
28140
+ if (first > 0) collapse(positions, first, first - 1, 0, first, eye, ahead, clearance);
28141
+ if (last < count - 1) collapse(positions, last, last + 1, last + 1, count, eye, ahead, clearance);
28142
+ }
28143
+
28011
28144
  // src/lib/travel/arc-colors.ts
28012
28145
  var ARC_COLORS = {
28013
28146
  cruise: "#7fdbff",
@@ -28161,6 +28294,7 @@ var SEVERITY_ORDER = [
28161
28294
  var HAZARD_MARKER_SIZE = .03;
28162
28295
  var HAZARD_CHIP_GAP_PX = 16;
28163
28296
  var FADE_FRACTION = .2;
28297
+ var CAMERA_CLEARANCE = 1e-5;
28164
28298
  function dotTexture(color) {
28165
28299
  const size = 64;
28166
28300
  const canvas = document.createElement("canvas");
@@ -28281,6 +28415,7 @@ var TravelPathOverlay = class {
28281
28415
  layer = null;
28282
28416
  offset = new Vector3();
28283
28417
  projected = new Vector3();
28418
+ ahead = new Vector3();
28284
28419
  rects = [];
28285
28420
  order = [];
28286
28421
  hovered = null;
@@ -28504,7 +28639,7 @@ var TravelPathOverlay = class {
28504
28639
  }
28505
28640
  addArcs(path, width, brightness, owner = null, patched = false, renderOrder = PATH_RENDER_ORDER) {
28506
28641
  const parts = patched && path.frame === "planetary";
28507
- const ends = (at) => parts && path.endOrbits.some((orbit) => orbit.at === at && orbit.approach.length > 1);
28642
+ const ends = (at) => parts && path.endOrbits.some((orbit) => orbit.at === at && (orbit.approach.length > 1 || this.crossing && orbit.anchorId !== path.centerId && systemOf(orbit.bodyId) !== orbit.bodyId));
28508
28643
  path.arcs.forEach((arc, index) => {
28509
28644
  const { from, to } = patched ? crossingWindow(path, index) : {
28510
28645
  from: 0,
@@ -28608,8 +28743,11 @@ var TravelPathOverlay = class {
28608
28743
  craft.local = eclipticToScene(at.r);
28609
28744
  craft.anchorId = at.centerId === this.path.centerId ? null : at.centerId;
28610
28745
  }
28611
- reposition(centerScenePos, basis, bodyScenePos) {
28746
+ reposition(centerScenePos, basis, bodyScenePos, camera) {
28612
28747
  if (this.center === null) return;
28748
+ const eye = camera.position;
28749
+ camera.getWorldDirection(this.ahead);
28750
+ const clearance = eye.length() * CAMERA_CLEARANCE;
28613
28751
  const centre = [
28614
28752
  centerScenePos[0] - basis[0],
28615
28753
  centerScenePos[1] - basis[1],
@@ -28631,6 +28769,7 @@ var TravelPathOverlay = class {
28631
28769
  arc.positions[i * 3 + 1] = arc.local[i * 3 + 1] + ay;
28632
28770
  arc.positions[i * 3 + 2] = arc.local[i * 3 + 2] + az;
28633
28771
  }
28772
+ stopAtCamera(arc.positions, arc.count, eye, this.ahead, clearance);
28634
28773
  writeFatTrailVertices(arc.line.geometry, arc.positions, arc.alphas, arc.alphas, arc.count);
28635
28774
  }
28636
28775
  for (const marker of this.markers) {
@@ -29222,6 +29361,14 @@ function estimateVelocity(id, jd, pos) {
29222
29361
  });
29223
29362
  return dir ?? void 0;
29224
29363
  }
29364
+ function turnBody(body, bo, jd) {
29365
+ const orientation = body.orientation;
29366
+ if (!orientation) return;
29367
+ if (bo.mesh) applyOrientation(bo.mesh, orientation, jd, body.nutPrec);
29368
+ if (bo.model) applyOrientation(bo.model, orientation, jd, body.nutPrec);
29369
+ if (bo.nomenclatureAnchor) applyOrientation(bo.nomenclatureAnchor, orientation, jd, body.nutPrec);
29370
+ for (const ring of bo.rings) applyOrientation(ring.mesh, orientation, jd, body.nutPrec);
29371
+ }
29225
29372
  var Placer = class {
29226
29373
  ctx;
29227
29374
  bodyObjects;
@@ -29242,6 +29389,9 @@ var Placer = class {
29242
29389
  this.diagnostics = diagnostics;
29243
29390
  this.held = held;
29244
29391
  }
29392
+ get jd() {
29393
+ return this.passJd;
29394
+ }
29245
29395
  begin(jd, focusedId, frame) {
29246
29396
  this.passJd = jd;
29247
29397
  this.focusedId = focusedId;
@@ -29360,11 +29510,8 @@ var Placer = class {
29360
29510
  const oc = bo.trail.userData.orbitCenter;
29361
29511
  if (oc) oc.set(body.orbitCenter[0], body.orbitCenter[1], body.orbitCenter[2]);
29362
29512
  }
29363
- if (body.orientation && (bo.mesh || bo.model)) {
29364
- if (bo.mesh) applyOrientation(bo.mesh, body.orientation, jd, body.nutPrec);
29365
- if (bo.model) applyOrientation(bo.model, body.orientation, jd, body.nutPrec);
29366
- if (bo.nomenclatureAnchor) applyOrientation(bo.nomenclatureAnchor, body.orientation, jd, body.nutPrec);
29367
- } else if (isModelBearing(body)) {
29513
+ if (body.orientation) turnBody(body, bo, jd);
29514
+ else if (isModelBearing(body)) {
29368
29515
  const track = body.attitudeTrack;
29369
29516
  if (!body.pointingOverride && track && track.orientationAt(jd, attitudeQuat)) {
29370
29517
  if (bo.mesh) bo.mesh.quaternion.copy(attitudeQuat);
@@ -29387,7 +29534,6 @@ var Placer = class {
29387
29534
  }
29388
29535
  }
29389
29536
  }
29390
- if (body.orientation) for (const ring of bo.rings) applyOrientation(ring.mesh, body.orientation, jd, body.nutPrec);
29391
29537
  return pos;
29392
29538
  }
29393
29539
  settleProbe(body, bo, jd) {
@@ -30739,6 +30885,7 @@ var _corner = new Vector3();
30739
30885
  function surfaceClearanceKm(body) {
30740
30886
  return SURFACE_CLEARANCE_KM[body.data.objectType] ?? effectiveRadiusKm(body.data) * .25;
30741
30887
  }
30888
+ var POINT_MIN_DISTANCE = kmToScene(.01);
30742
30889
  function minCameraDistance(body, minRadiusKm) {
30743
30890
  return kmToScene((minRadiusKm ?? effectiveRadiusKm(body.data)) + surfaceClearanceKm(body));
30744
30891
  }
@@ -31305,7 +31452,7 @@ var FocusController = class {
31305
31452
  prepareFocusTarget(focus, [...pos], camera, camWorld, camPos, sceneSettings().resolvedReducedMotion);
31306
31453
  }
31307
31454
  focusOnPoint(position, distance) {
31308
- const { focus, camera, callbacks } = this.deps;
31455
+ const { focus, camera, controls, callbacks } = this.deps;
31309
31456
  const camWorld = this.cameraTruePos();
31310
31457
  let camPos;
31311
31458
  if (distance !== void 0) {
@@ -31317,6 +31464,7 @@ var FocusController = class {
31317
31464
  ];
31318
31465
  }
31319
31466
  this.focusedBody = void 0;
31467
+ controls.minDistance = POINT_MIN_DISTANCE;
31320
31468
  this.promotion.emitUserPromotedCount();
31321
31469
  prepareFocusTarget(focus, [...position], camera, camWorld, camPos, sceneSettings().resolvedReducedMotion);
31322
31470
  const spherical = cartesianToSpherical(camPos ?? camWorld, position, void 0);
@@ -31565,6 +31713,10 @@ var SIDES = 72;
31565
31713
  var LABEL_ROOM_PX = 40;
31566
31714
  var LABEL_WIDTH_PX = 128;
31567
31715
  var SIDE_MIN = .001;
31716
+ var BALL_ELEVATION_RAD = 45 * Math.PI / 180;
31717
+ var BALL_SLACK = 3;
31718
+ var BALL_ROOM_MIN = .25;
31719
+ var RING_SHARE = 1 / 4;
31568
31720
  var NORTH = [
31569
31721
  0,
31570
31722
  1,
@@ -31588,11 +31740,12 @@ function openArea(insets, width, height) {
31588
31740
  left: share(insets.left + LABEL_ROOM_PX, width)
31589
31741
  };
31590
31742
  }
31591
- function tripVantage(target, origin, camera, view) {
31743
+ function tripVantage(target, origin, camera, view, ring = 0) {
31592
31744
  const line = sub(origin, target);
31593
31745
  const length = norm(line);
31594
31746
  const narrow = narrowHalfFov(view.fovDeg, view.aspect);
31595
31747
  if (!(length > 0) || !(narrow > 0)) return null;
31748
+ if (ring >= RING_SHARE * length) return ballVantage(target, target, length + ring, null, camera, view);
31596
31749
  const offset = sub(camera, target);
31597
31750
  if (shows(offset, line, length, view, narrow)) return {
31598
31751
  offset: [
@@ -31634,6 +31787,61 @@ function tripVantage(target, origin, camera, view) {
31634
31787
  kept: false
31635
31788
  };
31636
31789
  }
31790
+ function ballVantage(target, center, radius, normal, camera, view) {
31791
+ if (!(radius > 0)) return null;
31792
+ const pole = normal ?? NORTH;
31793
+ const ball = sub(center, target);
31794
+ const offset = sub(camera, target);
31795
+ const distance = norm(offset);
31796
+ if (distance > 0) {
31797
+ const least = ballDistance(ball, radius, scale(offset, 1 / distance), view);
31798
+ if (least !== null && distance >= least && distance <= BALL_SLACK * least) return {
31799
+ offset: [
31800
+ offset[0],
31801
+ offset[1],
31802
+ offset[2]
31803
+ ],
31804
+ kept: true
31805
+ };
31806
+ }
31807
+ const first = sideOf(offset, pole) ?? sideOf(EAST, pole) ?? sideOf(NORTH, pole);
31808
+ if (!first) return null;
31809
+ const second = cross$1(pole, first);
31810
+ let best = null;
31811
+ for (let i = 0; i < SIDES; i++) {
31812
+ const turn = 2 * Math.PI * i / SIDES;
31813
+ const side = add(scale(first, Math.cos(turn)), scale(second, Math.sin(turn)));
31814
+ const direction = add(scale(side, Math.cos(BALL_ELEVATION_RAD)), scale(pole, Math.sin(BALL_ELEVATION_RAD)));
31815
+ const away = ballDistance(ball, radius, direction, view);
31816
+ if (away !== null && (!best || away < best.distance)) best = {
31817
+ direction,
31818
+ distance: away
31819
+ };
31820
+ }
31821
+ if (!best) return null;
31822
+ const [x, y, z] = scale(best.direction, best.distance);
31823
+ return {
31824
+ offset: [
31825
+ x,
31826
+ y,
31827
+ z
31828
+ ],
31829
+ kept: false
31830
+ };
31831
+ }
31832
+ function ballDistance(center, radius, back, view) {
31833
+ const right = cross$1(view.up, back);
31834
+ const width = norm(right);
31835
+ if (!(width > SIDE_MIN)) return null;
31836
+ const across = scale(right, 1 / width);
31837
+ const up = cross$1(back, across);
31838
+ const tanY = Math.tan(view.fovDeg * Math.PI / 360);
31839
+ const tanX = tanY * view.aspect;
31840
+ const room = (share, tan) => Math.max(share, BALL_ROOM_MIN) * tan;
31841
+ const x = dot$1(center, across);
31842
+ const y = dot$1(center, up);
31843
+ return Math.max((x + radius) / room(view.open.right, tanX), (radius - x) / room(view.open.left, tanX), (y + radius) / room(view.open.top, tanY), (radius - y) / room(view.open.bottom, tanY)) + dot$1(center, back) + radius;
31844
+ }
31637
31845
  function distanceAt(length, tanAngle) {
31638
31846
  return length * (Math.cos(VANTAGE_RAD) + Math.sin(VANTAGE_RAD) / tanAngle);
31639
31847
  }
@@ -32145,6 +32353,7 @@ var SceneRenderer = class SceneRenderer {
32145
32353
  atRest = false;
32146
32354
  focusLoads = 0;
32147
32355
  unsubscribeBodiesAdded = null;
32356
+ unsubscribeNutation = null;
32148
32357
  _trailView = {
32149
32358
  camX: 0,
32150
32359
  camY: 0,
@@ -32261,7 +32470,7 @@ var SceneRenderer = class SceneRenderer {
32261
32470
  this.controls = new OrbitControls(this.camera, canvas);
32262
32471
  this.controls.enableDamping = true;
32263
32472
  this.orbitDampingFactor = orbitDamping(sceneSettings().resolvedReducedMotion);
32264
- this.controls.minDistance = focusBody ? minCameraDistance(focusBody) : kmToScene(.01);
32473
+ this.controls.minDistance = focusBody ? minCameraDistance(focusBody) : POINT_MIN_DISTANCE;
32265
32474
  Object.defineProperty(this.controls, "maxDistance", { get: () => this.maxCameraDistance() });
32266
32475
  this.controls.target.set(0, 0, 0);
32267
32476
  this.controls.update();
@@ -32272,6 +32481,13 @@ var SceneRenderer = class SceneRenderer {
32272
32481
  for (const type of SceneRenderer.INPUT_EVENTS) canvas.addEventListener(type, this.invalidate, { passive: true });
32273
32482
  window.addEventListener("keydown", this.invalidate);
32274
32483
  this.unsubscribeBodiesAdded = ctx.bodies.onBodiesAdded(this.invalidate);
32484
+ this.unsubscribeNutation = onSystemsGlobalSettled(() => {
32485
+ for (const body of joinAwaitedNutation(ctx)) {
32486
+ const bo = this.bodyObjects.get(body.data.id);
32487
+ if (bo) turnBody(body, bo, this.sceneJd);
32488
+ }
32489
+ this.invalidate();
32490
+ });
32275
32491
  this.cameraUp = new CameraUpController(this.camera, this.controls, ctx, (id) => this.isLanded(id));
32276
32492
  this.systemData = new SystemDataLoader(this.scene, ctx, this.renderer, this.textureLoader, this.bodyObjects, clock, () => this.focusController.reapplyInitialViewIfPending());
32277
32493
  this.focusController = new FocusController({
@@ -32341,6 +32557,12 @@ var SceneRenderer = class SceneRenderer {
32341
32557
  }
32342
32558
  setTravelPath(plan, options = [], hazards = [], steps = []) {
32343
32559
  this.travelPath.set(plan, options, hazards, steps);
32560
+ const centerId = this.travelPath.centerId;
32561
+ this.ctx.bodies.pinTripCentre(centerId);
32562
+ if (centerId !== null && !this.ctx.getBody(centerId)) {
32563
+ const at = jdToDate(this.sceneJd);
32564
+ this.ctx.ensureBody(centerId, at).catch((e) => console.warn(`[map] no centre ${centerId} for the trip:`, e));
32565
+ }
32344
32566
  this.refreshTravelPath();
32345
32567
  }
32346
32568
  setTravelHover(id) {
@@ -32387,7 +32609,7 @@ var SceneRenderer = class SceneRenderer {
32387
32609
  }
32388
32610
  this.travelPath.setVisible(true);
32389
32611
  this.travelPath.setClock(this.craftJd);
32390
- this.travelPath.reposition(center, this.focus.focusTruePos, (id) => this.where(id));
32612
+ this.travelPath.reposition(center, this.focus.focusTruePos, (id) => this.where(id), this.camera);
32391
32613
  this.travelPath.updateCameraOffset(this.camera.position);
32392
32614
  this.reserveTravelLabelSpace();
32393
32615
  }
@@ -32554,13 +32776,10 @@ var SceneRenderer = class SceneRenderer {
32554
32776
  this.focusController.panCameraToBody(body, distance);
32555
32777
  return this.focus.focusDurationMs;
32556
32778
  }
32557
- tripVantage(originId, targetId, insets) {
32558
- const origin = this.where(originId);
32559
- const target = this.where(targetId);
32560
- if (!origin || !target) return null;
32779
+ tripCamera(insets) {
32561
32780
  const { clientWidth, clientHeight } = this.renderer.domElement;
32562
32781
  const { up } = this.camera;
32563
- return tripVantage(target, origin, this.focusController.cameraTruePos(), {
32782
+ return {
32564
32783
  fovDeg: this.camera.fov,
32565
32784
  aspect: this.camera.aspect,
32566
32785
  up: [
@@ -32569,7 +32788,22 @@ var SceneRenderer = class SceneRenderer {
32569
32788
  up.z
32570
32789
  ],
32571
32790
  open: openArea(insets, clientWidth || 1, clientHeight || 1)
32572
- });
32791
+ };
32792
+ }
32793
+ tripVantage(originId, targetId, insets, ringKm = 0) {
32794
+ const origin = this.where(originId);
32795
+ const target = this.where(targetId);
32796
+ if (!origin || !target) return null;
32797
+ const camera = this.focusController.cameraTruePos();
32798
+ return tripVantage(target, origin, camera, this.tripCamera(insets), kmToScene(ringKm));
32799
+ }
32800
+ ballVantage(targetId, centerId, radiusKm, normal, insets) {
32801
+ const target = this.where(targetId);
32802
+ const center = this.where(centerId);
32803
+ if (!target || !center) return null;
32804
+ const camera = this.focusController.cameraTruePos();
32805
+ const radius = kmToScene(radiusKm);
32806
+ return ballVantage(target, center, radius, normal, camera, this.tripCamera(insets));
32573
32807
  }
32574
32808
  viewFrom(id, offset) {
32575
32809
  const body = this.ctx.getBody(id);
@@ -32787,9 +33021,26 @@ var SceneRenderer = class SceneRenderer {
32787
33021
  fovDeg: this.camera.fov,
32788
33022
  dtMs,
32789
33023
  outside: 1 - this.starfield.skyboxShare(),
32790
- focused: this.focusController.current
33024
+ focused: this.focusController.current ?? this.tripEndInView()
32791
33025
  });
32792
33026
  }
33027
+ tripEndInView() {
33028
+ if (!this.travelFocus) return void 0;
33029
+ const at = this.focus.focusTruePos;
33030
+ let nearest;
33031
+ let nearestDistance = Infinity;
33032
+ for (const id of this.travelPath.endIds) {
33033
+ const body = this.ctx.getBody(id);
33034
+ const place = body && this.whereBody(body);
33035
+ if (!place) continue;
33036
+ const distance = f64dist(at, place);
33037
+ if (distance < nearestDistance) {
33038
+ nearest = body;
33039
+ nearestDistance = distance;
33040
+ }
33041
+ }
33042
+ return nearest;
33043
+ }
32793
33044
  updateSystemShare(dtMs) {
32794
33045
  const visibility = this.ctx.visibility;
32795
33046
  if (!this.viewOpen) {
@@ -33797,11 +34048,12 @@ var SceneRenderer = class SceneRenderer {
33797
34048
  for (const type of SceneRenderer.INPUT_EVENTS) this.canvas.removeEventListener(type, this.invalidate);
33798
34049
  window.removeEventListener("keydown", this.invalidate);
33799
34050
  this.unsubscribeBodiesAdded?.();
34051
+ this.unsubscribeNutation?.();
33800
34052
  this.controls.dispose();
33801
34053
  this.haloDebug.dispose();
33802
34054
  this.travelPath.dispose();
33803
34055
  this.extensions.dispose();
33804
- for (const bo of this.bodyObjects.values()) if (bo.model) unloadBodyModel(bo);
34056
+ for (const bo of this.bodyObjects.values()) unloadBodyModel(bo);
33805
34057
  for (const bo of this.bodyObjects.values()) {
33806
34058
  disposeTileOverlay(bo);
33807
34059
  disposeReliefTiles(bo);
@@ -35965,7 +36217,9 @@ var MapObjectCollection = class {
35965
36217
  };
35966
36218
 
35967
36219
  // src/lib/scene/extensions/measure.ts
36220
+ var turnsYet = (anchor, ctx) => isSurface(anchor) && ctx.bodies.nutationAwaited.has(anchor.body);
35968
36221
  function anchorOffsetKm(a, b, ctx, jd) {
36222
+ if (turnsYet(a, ctx) || turnsYet(b, ctx)) return null;
35969
36223
  const from = resolveAnchor(a, ctx, jd);
35970
36224
  const to = resolveAnchor(b, ctx, jd);
35971
36225
  if (!from || !to) return null;
@@ -36005,6 +36259,7 @@ function subsolarPoint$1(id, ctx, jd) {
36005
36259
  const sun = ctx.getBody(SUN_ID);
36006
36260
  if (!body || !sun || body === sun) return null;
36007
36261
  if (!body.orientation && !ctx.bodies.orientationRead.has(id)) return null;
36262
+ if (ctx.bodies.nutationAwaited.has(id)) return null;
36008
36263
  const centre = bodyCentre(body, ctx, jd);
36009
36264
  const sunCentre = bodyCentre(sun, ctx, jd);
36010
36265
  return centre && sunCentre && subsolarAt(body, centre, sunCentre, jd);
@@ -36967,9 +37222,16 @@ var SpaceMap = class {
36967
37222
  snapToBodyFacing(id, towardId, elevationDeg, distance) {
36968
37223
  this.renderer?.snapToBodyFacing(id, towardId, elevationDeg, distance);
36969
37224
  }
36970
- tripView(originId, targetId, insets) {
37225
+ tripView(originId, targetId, insets, ringKm = 0) {
37226
+ return this.vantageView(this.renderer?.tripVantage(originId, targetId, insets, ringKm));
37227
+ }
37228
+ tripBallView(targetId, ball, insets) {
37229
+ const normal = this.ctx.exoplanets.planeNormal(targetId);
37230
+ const { centerId, radiusKm } = ball;
37231
+ return this.vantageView(this.renderer?.ballVantage(targetId, centerId, radiusKm, normal, insets));
37232
+ }
37233
+ vantageView(vantage) {
36971
37234
  const renderer = this.renderer;
36972
- const vantage = renderer?.tripVantage(originId, targetId, insets);
36973
37235
  if (!renderer || !vantage || vantage.kept) return null;
36974
37236
  return (body) => renderer.viewFrom(body.data.id, vantage.offset) ?? {};
36975
37237
  }