spacemap 0.1.22 → 0.1.25

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
@@ -3061,9 +3061,6 @@ function isExtrasolar(body) {
3061
3061
  function isExoplanet(body) {
3062
3062
  return body.data.objectType === ObjectType.EXOPLANET;
3063
3063
  }
3064
- function isSurfaceFeature(body) {
3065
- return body.data.objectType === ObjectType.SURFACE_FEATURE;
3066
- }
3067
3064
  var ObjectType = /* @__PURE__ */ function(ObjectType) {
3068
3065
  ObjectType[ObjectType["BARYCENTER"] = 0] = "BARYCENTER";
3069
3066
  ObjectType[ObjectType["LAGRANGE_POINT"] = 1] = "LAGRANGE_POINT";
@@ -6837,7 +6834,6 @@ var BodyIndex = class {
6837
6834
  majorBodies = [];
6838
6835
  asteroidBodiesByZone = /* @__PURE__ */ new Map();
6839
6836
  spacecraftByParent = /* @__PURE__ */ new Map();
6840
- focusFeature = null;
6841
6837
  extrasolar = /* @__PURE__ */ new Map();
6842
6838
  pinnedExtrasolar = /* @__PURE__ */ new Set();
6843
6839
  tripCentreId = null;
@@ -6884,7 +6880,6 @@ var BodyIndex = class {
6884
6880
  getBody(id, zone) {
6885
6881
  const major = this.bodiesById.get(id);
6886
6882
  if (major) return major;
6887
- if (this.focusFeature?.data.id === id) return this.focusFeature;
6888
6883
  const star = this.extrasolar.get(id) ?? this.exoplanets.get(this.exoplanetAliases.get(id) ?? id);
6889
6884
  if (star) return star;
6890
6885
  if (zone !== void 0) return this.spacecraftByParent.get(zone)?.get(id) ?? this.asteroidBodiesByZone.get(zone)?.get(id);
@@ -7778,6 +7773,7 @@ var VisibilityController = class {
7778
7773
  systemCentres = /* @__PURE__ */ new Map();
7779
7774
  focusedBodyIdPlain = SUN_ID;
7780
7775
  focusedSystemIdPlain = null;
7776
+ subjectIdPlain;
7781
7777
  travelSystemActive = false;
7782
7778
  cameraDistThreeJS = 0;
7783
7779
  lastRecomputeDist = -1;
@@ -7881,11 +7877,15 @@ var VisibilityController = class {
7881
7877
  this.focusedSystemIdPlain = sysId;
7882
7878
  this.lastRecomputeDist = -1;
7883
7879
  }
7880
+ inFocus(id) {
7881
+ return id === this.focusedBodyIdPlain || id === this.subjectIdPlain;
7882
+ }
7884
7883
  clearTravelSystem() {
7885
7884
  this.travelSystemActive = false;
7886
7885
  }
7887
- setFocused(body) {
7886
+ setFocused(body, subject) {
7888
7887
  this.travelSystemActive = false;
7888
+ this.subjectIdPlain = subject?.data.id;
7889
7889
  if (body.data.id === this.focusedBodyIdPlain) return;
7890
7890
  this.focusedBodyId = body.data.id;
7891
7891
  this.focusedBodyIdPlain = body.data.id;
@@ -7910,7 +7910,7 @@ var VisibilityController = class {
7910
7910
  else if (isAsteroidMoon && !this.matchesSmallBodyClass(moon.data.parentId)) vis = VISIBILITY.HIDE;
7911
7911
  else {
7912
7912
  const ratio = this.cameraDistThreeJS / 10 / moon.data.a;
7913
- const isFocused = moon.data.id === this.focusedBodyIdPlain;
7913
+ const isFocused = this.inFocus(moon.data.id);
7914
7914
  vis = computeVisibilityFromRatio(ratio, this.scaledPlanetary, 5, isFocused);
7915
7915
  if (!isAsteroidMoon && vis === VISIBILITY.FULL && !this.fullMoonIds.has(moon.data.id) && !isFocused) vis = VISIBILITY.CAPPED;
7916
7916
  }
@@ -7958,7 +7958,7 @@ var VisibilityController = class {
7958
7958
  if (!this.isInFocusedSystem(body.data.parentId)) return VISIBILITY.HIDE;
7959
7959
  const refA = body.data.a;
7960
7960
  if (!refA || refA <= 0) return VISIBILITY.FULL;
7961
- const isFocused = body.data.id === this.focusedBodyIdPlain;
7961
+ const isFocused = this.inFocus(body.data.id);
7962
7962
  return computeVisibilityFromRatio(camDistThreeJS / 10 / refA, this.scaledPlanetary, 5, isFocused);
7963
7963
  }
7964
7964
  let refA = body.extrasolar?.pairAxis ?? body.data.a;
@@ -7970,7 +7970,7 @@ var VisibilityController = class {
7970
7970
  if (refA === 0 && body.data.e < .9) console.warn(`No semi-major axis available for body ${body.data.id} (${body.data.name}), falling back to FULL visibility`);
7971
7971
  return VISIBILITY.FULL;
7972
7972
  }
7973
- const isFocused = body.data.id === this.focusedBodyIdPlain;
7973
+ const isFocused = this.inFocus(body.data.id);
7974
7974
  const noLine = isExtrasolar(body) && !body.orbitElements;
7975
7975
  const tier = computeVisibilityFromRatio(camDistThreeJS / 10 / refA, this.scaledSystem, 50, isFocused || noLine);
7976
7976
  return tier === VISIBILITY.CLOSE ? VISIBILITY.FULL : tier;
@@ -7978,7 +7978,7 @@ var VisibilityController = class {
7978
7978
  getProbeVisibility(body, camDistThreeJS) {
7979
7979
  const ps = this.getProbeStore();
7980
7980
  if ((ps ? ps.containingSystemAt(body.data.id, this.currentJd) : null) === EARTH_SYSTEM_NAIF && this.getEarthSatGroupFilter()) return VISIBILITY.HIDE;
7981
- const isFocused = body.data.id === this.focusedBodyIdPlain;
7981
+ const isFocused = this.inFocus(body.data.id);
7982
7982
  if (!(ps ? !ps.hasHeliocentricFit(body.data.id, this.currentJd) : false)) {
7983
7983
  const sun = this.bodies.bodiesById.get(SUN_ID);
7984
7984
  const helioDist = sun?.position && body.position ? f64dist(body.position, sun.position) / 10 : 0;
@@ -8043,7 +8043,7 @@ var VisibilityController = class {
8043
8043
  bodyUnclearedShare(body) {
8044
8044
  if (this.clearing.share === 0) return 1;
8045
8045
  const { id, objectType, parentId, orbitalSource } = body.data;
8046
- if (id === this.focusedBodyIdPlain || id === this.focusedSystemIdPlain || this.pinned.has(id) || this.tripEnds.has(id)) return 1;
8046
+ if (this.inFocus(id) || id === this.focusedSystemIdPlain || this.pinned.has(id) || this.tripEnds.has(id)) return 1;
8047
8047
  const layer = bodyLayer(objectType, parentId);
8048
8048
  if (layer === "spacecraft" || orbitalSource === OrbitalSource.SPICE_PROBE) {
8049
8049
  const page = this.getProbePage();
@@ -16768,7 +16768,7 @@ function seedAccepted() {
16768
16768
  for (let i = 0; i < _reservedActive; i++) copyRect(poolSlot(_accepted, i, makeRect), _reserved[i]);
16769
16769
  return _reservedActive;
16770
16770
  }
16771
- function cullOverlappingLabels(bodyObjects, screenWidth, screenHeight, camera, focusedBodyId, ctx, hoveredBodyIds, screenOccluders, focusTruePos = [
16771
+ function cullOverlappingLabels(bodyObjects, screenWidth, screenHeight, camera, ctx, hoveredBodyIds, screenOccluders, focusTruePos = [
16772
16772
  0,
16773
16773
  0,
16774
16774
  0
@@ -16783,7 +16783,7 @@ function cullOverlappingLabels(bodyObjects, screenWidth, screenHeight, camera, f
16783
16783
  if (span && span.offsetWidth > 0) bo.labelTextWidth = span.offsetWidth;
16784
16784
  }
16785
16785
  if (!labelScreenRect(bo, camera, focusTruePos, screenWidth, screenHeight, _projRect)) continue;
16786
- const isFocused = body.data.id === focusedBodyId;
16786
+ const isFocused = ctx.visibility.inFocus(body.data.id);
16787
16787
  const isHovered = hoveredBodyIds.has(body.data.id);
16788
16788
  const c = poolSlot(_candidates, _candidatesActive++, makeCandidate);
16789
16789
  copyRect(c.rect, _projRect);
@@ -17623,19 +17623,60 @@ function isModelBearing(body) {
17623
17623
  var modelLoader = new GLTFLoader();
17624
17624
  modelLoader.setMeshoptDecoder(MeshoptDecoder);
17625
17625
  var _loader = modelLoader;
17626
- var _bundleMetaCache = /* @__PURE__ */ new Map();
17627
- function fetchBundleMeta(slug) {
17628
- let p = _bundleMetaCache.get(slug);
17629
- if (!p) {
17630
- p = fetch(versionedUrl(`/v1/models/${slug}/metadata.json`, "models")).then((r) => r.json());
17631
- _bundleMetaCache.set(slug, p);
17632
- p.catch(() => _bundleMetaCache.delete(slug));
17626
+ function modelUrl(slug, tier) {
17627
+ return versionedUrl(`/v1/models/${slug}/${tier}.glb`, "models");
17628
+ }
17629
+ async function loadModelBundle(slug, tier) {
17630
+ const first = await loadTier(slug, tier);
17631
+ if (!first || textureAllowed(first.meta.exports[tier]?.distribution)) return first;
17632
+ disposeGltf(first.gltf.scene);
17633
+ const other = tier === "high" ? "low" : "high";
17634
+ if (!first.meta.tiers?.includes(other)) return null;
17635
+ if (!textureAllowed(first.meta.exports[other]?.distribution)) return null;
17636
+ const second = await loadTier(slug, other);
17637
+ if (!second || textureAllowed(second.meta.exports[other]?.distribution)) return second;
17638
+ disposeGltf(second.gltf.scene);
17639
+ return null;
17640
+ }
17641
+ async function loadTier(slug, tier) {
17642
+ const gltf = await _loader.loadAsync(modelUrl(slug, tier));
17643
+ const meta = (gltf.asset?.extras)?.spacemap;
17644
+ if (!meta?.exports?.high) {
17645
+ disposeGltf(gltf.scene);
17646
+ console.warn(`models/${slug}/${tier}.glb carries no payload`);
17647
+ return null;
17633
17648
  }
17634
- return p;
17649
+ return {
17650
+ gltf,
17651
+ meta,
17652
+ tier
17653
+ };
17654
+ }
17655
+ var _tiers;
17656
+ function fetchModelTiers() {
17657
+ if (!_tiers) {
17658
+ const p = fetch(versionedUrl("/v1/models/tiers.json", "models")).then((r) => {
17659
+ if (!r.ok) throw new Error(`models/tiers.json: ${r.status}`);
17660
+ return r.json();
17661
+ }).then((d) => d.bundles ?? {}).catch((e) => {
17662
+ console.warn("models/tiers.json did not load:", e);
17663
+ if (_tiers === p) _tiers = void 0;
17664
+ return {};
17665
+ });
17666
+ _tiers = p;
17667
+ }
17668
+ return _tiers;
17669
+ }
17670
+ async function bundleTiers(slug) {
17671
+ return (await fetchModelTiers())[slug];
17672
+ }
17673
+ function tierAllowed(tiers, tier) {
17674
+ const info = tiers[tier];
17675
+ return !!info && textureAllowed(info.distribution);
17635
17676
  }
17636
- function allowedTier(meta) {
17637
- if (textureAllowed(meta.exports.high.distribution)) return "high";
17638
- if (meta.exports.low && textureAllowed(meta.exports.low.distribution)) return "low";
17677
+ function allowedTier(tiers) {
17678
+ if (!tiers) return "high";
17679
+ return ["high", "low"].find((tier) => tierAllowed(tiers, tier));
17639
17680
  }
17640
17681
  function shapeModelCredit(meta) {
17641
17682
  return meta.credit ?? meta.exports.high.credit;
@@ -17700,13 +17741,13 @@ async function loadSpacecraftModel(bo, modelScene, ctx) {
17700
17741
  if ((bo.modelLoadEpoch ?? 0) !== epoch) return;
17701
17742
  if (bo.mesh) bo.mesh.visible = false;
17702
17743
  setHaloLoading(bo, true);
17703
- const meta = await fetchBundleMeta(slug);
17704
- const tier = allowedTier(meta);
17705
- if (!tier) {
17744
+ const wanted = allowedTier(await bundleTiers(slug));
17745
+ const loaded = wanted && await loadModelBundle(slug, wanted);
17746
+ if (!loaded) {
17706
17747
  bo.noPhysical = "model";
17707
17748
  return;
17708
17749
  }
17709
- const gltf = await _loader.loadAsync(versionedUrl(`/v1/models/${slug}/${tier}.glb`, "models"));
17750
+ const { gltf, meta, tier } = loaded;
17710
17751
  if ((bo.modelLoadEpoch ?? 0) !== epoch || !bo.mesh) {
17711
17752
  disposeGltf(gltf.scene);
17712
17753
  return;
@@ -17758,20 +17799,9 @@ async function loadNaturalBodyModel(bo, ctx) {
17758
17799
  return;
17759
17800
  }
17760
17801
  if (detail.global?.orientation && !bo.body.orientation) applyBodyOrientation(bo.body, detail.global.orientation, detail.global.nut_prec, ctx);
17761
- const metaPromise = fetchBundleMeta(slug);
17762
- const gltf = await _loader.loadAsync(versionedUrl(`/v1/models/${slug}/high.glb`, "models"));
17763
- if ((bo.modelLoadEpoch ?? 0) !== epoch) {
17764
- disposeGltf(gltf.scene);
17765
- return;
17766
- }
17767
- let meta;
17768
- try {
17769
- meta = await metaPromise;
17770
- } catch (e) {
17771
- disposeGltf(gltf.scene);
17772
- console.warn(`Failed to load shape-model metadata for ${slug}:`, e);
17773
- return;
17774
- }
17802
+ const loaded = await loadModelBundle(slug, "high");
17803
+ if (!loaded) return;
17804
+ const { gltf, meta } = loaded;
17775
17805
  if ((bo.modelLoadEpoch ?? 0) !== epoch) {
17776
17806
  disposeGltf(gltf.scene);
17777
17807
  return;
@@ -18063,12 +18093,6 @@ function onLinkTap(el, handler) {
18063
18093
  }
18064
18094
 
18065
18095
  // src/lib/scene/objects/surface/nomenclature.ts
18066
- function nomenclatureBodyId(focused, bodyObjects) {
18067
- if (!focused) return void 0;
18068
- if (isSurfaceFeature(focused)) return focused.featureAnchor.hostId;
18069
- if (bodyObjects.get(focused.data.id)?.isLanded) return focused.data.parentId;
18070
- return focused.data.id;
18071
- }
18072
18096
  var DEG2RAD$4 = Math.PI / 180;
18073
18097
  var MIN_BODY_SCREEN_RADIUS_PX = 128;
18074
18098
  var LABEL_DISC_FRACTION = .9;
@@ -22390,6 +22414,87 @@ var SimClock = class {
22390
22414
  }
22391
22415
  };
22392
22416
 
22417
+ // src/lib/scene/camera/two-finger.ts
22418
+ var TWO_FINGER_SLOP = 10;
22419
+ var TWO_FINGER_ALONE = 30;
22420
+ var TILT_MAX_SPREAD_SLOPE = Math.tan(MathUtils.degToRad(40));
22421
+ var TILT_MAX_SIDEWAYS = .5;
22422
+ var TILT_MIN_TOGETHER = .3;
22423
+ var TWIST_ENGAGE = MathUtils.degToRad(12);
22424
+ var TwoFingerGesture = class {
22425
+ mode = "map";
22426
+ a0 = {
22427
+ x: 0,
22428
+ y: 0
22429
+ };
22430
+ b0 = {
22431
+ x: 0,
22432
+ y: 0
22433
+ };
22434
+ lastMidY = 0;
22435
+ lastAngle = 0;
22436
+ twisted = 0;
22437
+ twisting = false;
22438
+ begin(a, b, canTilt) {
22439
+ this.mode = canTilt ? "undecided" : "map";
22440
+ this.a0.x = a.x;
22441
+ this.a0.y = a.y;
22442
+ this.b0.x = b.x;
22443
+ this.b0.y = b.y;
22444
+ this.lastMidY = (a.y + b.y) / 2;
22445
+ this.lastAngle = Math.atan2(b.y - a.y, b.x - a.x);
22446
+ this.twisted = 0;
22447
+ this.twisting = false;
22448
+ }
22449
+ move(a, b) {
22450
+ const angle = Math.atan2(b.y - a.y, b.x - a.x);
22451
+ const turn = MathUtils.euclideanModulo(angle - this.lastAngle + Math.PI, 2 * Math.PI) - Math.PI;
22452
+ this.lastAngle = angle;
22453
+ const midY = (a.y + b.y) / 2;
22454
+ let down = midY - this.lastMidY;
22455
+ this.lastMidY = midY;
22456
+ this.twisted += turn;
22457
+ if (this.mode === "undecided") {
22458
+ this.mode = this.decide(a, b);
22459
+ if (this.mode === "tilt") down = midY - (this.a0.y + this.b0.y) / 2;
22460
+ }
22461
+ switch (this.mode) {
22462
+ case "undecided": return {
22463
+ mode: "undecided",
22464
+ down: 0,
22465
+ twist: 0
22466
+ };
22467
+ case "tilt": return {
22468
+ mode: "tilt",
22469
+ down,
22470
+ twist: 0
22471
+ };
22472
+ case "map": {
22473
+ const engaged = this.twisting;
22474
+ this.twisting ||= Math.abs(this.twisted) > TWIST_ENGAGE;
22475
+ return {
22476
+ mode: "map",
22477
+ down: 0,
22478
+ twist: engaged ? turn : 0
22479
+ };
22480
+ }
22481
+ }
22482
+ }
22483
+ decide(a, b) {
22484
+ const ax = a.x - this.a0.x;
22485
+ const ay = a.y - this.a0.y;
22486
+ const bx = b.x - this.b0.x;
22487
+ const by = b.y - this.b0.y;
22488
+ const aTravel = Math.hypot(ax, ay);
22489
+ const bTravel = Math.hypot(bx, by);
22490
+ if (Math.max(aTravel, bTravel) < (aTravel > 0 && bTravel > 0 ? 10 : TWO_FINGER_ALONE)) return "undecided";
22491
+ const sideBySide = Math.abs(this.b0.y - this.a0.y) <= Math.abs(this.b0.x - this.a0.x) * TILT_MAX_SPREAD_SLOPE;
22492
+ const vertical = Math.abs(ax) <= Math.abs(ay) * TILT_MAX_SIDEWAYS && Math.abs(bx) <= Math.abs(by) * TILT_MAX_SIDEWAYS;
22493
+ const together = Math.sign(ay) === Math.sign(by) && Math.min(aTravel, bTravel) >= Math.max(aTravel, bTravel) * TILT_MIN_TOGETHER;
22494
+ return sideBySide && vertical && together ? "tilt" : "map";
22495
+ }
22496
+ };
22497
+
22393
22498
  // src/lib/scene/camera/look-controls.ts
22394
22499
  var MAX_TILT = MathUtils.degToRad(80);
22395
22500
  var LOOK_NONE_HEIGHT = 1;
@@ -22411,14 +22516,15 @@ var _turnBack = new Quaternion();
22411
22516
  var _was = new Quaternion();
22412
22517
  var _pivotGround = new Vector3();
22413
22518
  var _look = new Matrix4();
22519
+ var GLIDE_DONE = 1e-5;
22414
22520
  var ANCHOR_PASSES = 3;
22415
22521
  var SIN_MIN_ANCHOR_ELEVATION = Math.sin(MathUtils.degToRad(20));
22416
22522
  var GROUND_PASSES = 3;
22523
+ var PIVOT_REFINE = 1e-7;
22417
22524
  var orbitHandlers = OrbitControls.prototype;
22418
22525
  var LookOrbitControls = class extends OrbitControls {
22419
22526
  canvas;
22420
22527
  surfaceRadius;
22421
- standingHeight;
22422
22528
  onTurnSettled;
22423
22529
  heading = 0;
22424
22530
  tilt = 0;
@@ -22426,6 +22532,7 @@ var LookOrbitControls = class extends OrbitControls {
22426
22532
  orbitPos = new Vector3();
22427
22533
  orbitQuat = new Quaternion();
22428
22534
  pivotRadius = 0;
22535
+ placedByName = false;
22429
22536
  lastUp = new Vector3();
22430
22537
  holdsNorth = false;
22431
22538
  northTurning = false;
@@ -22436,16 +22543,22 @@ var LookOrbitControls = class extends OrbitControls {
22436
22543
  dragPointer = null;
22437
22544
  dragX = 0;
22438
22545
  dragY = 0;
22439
- constructor(camera, canvas, surfaceRadius, standingHeight = () => null, onTurnSettled = () => {}) {
22546
+ twoFinger = new TwoFingerGesture();
22547
+ glideHeading = 0;
22548
+ glideTilt = 0;
22549
+ constructor(camera, canvas, surfaceRadius, onTurnSettled = () => {}) {
22440
22550
  super(camera, canvas);
22441
22551
  this.canvas = canvas;
22442
22552
  this.surfaceRadius = surfaceRadius;
22443
- this.standingHeight = standingHeight;
22444
22553
  this.onTurnSettled = onTurnSettled;
22445
22554
  this.mouseButtons.MIDDLE = null;
22446
- this.addEventListener("start", () => this.pendingLook = null);
22555
+ this.addEventListener("start", () => {
22556
+ this.pendingLook = null;
22557
+ this.placedByName = false;
22558
+ });
22447
22559
  canvas.addEventListener("pointerdown", this.onLookDown);
22448
22560
  canvas.addEventListener("mousedown", this.blockMiddle);
22561
+ canvas.addEventListener("pointerdown", this.onCatch);
22449
22562
  }
22450
22563
  get orbitPosition() {
22451
22564
  return this.looking ? this.orbitPos : this.object.position;
@@ -22464,6 +22577,8 @@ var LookOrbitControls = class extends OrbitControls {
22464
22577
  const cam = this.object;
22465
22578
  this.looking = false;
22466
22579
  this.pendingLook = null;
22580
+ this.placedByName = false;
22581
+ this.stopGlide();
22467
22582
  if (tilt === 0 && heading === 0) return;
22468
22583
  if (!this.groundKnown()) {
22469
22584
  this.pendingLook = {
@@ -22484,6 +22599,7 @@ var LookOrbitControls = class extends OrbitControls {
22484
22599
  this.heading = heading;
22485
22600
  this.holdAngles(share);
22486
22601
  this.looking = true;
22602
+ this.placedByName = true;
22487
22603
  this.place();
22488
22604
  }
22489
22605
  toOrbitAxes(x, y) {
@@ -22501,6 +22617,12 @@ var LookOrbitControls = class extends OrbitControls {
22501
22617
  endLook() {
22502
22618
  this.looking = false;
22503
22619
  this.pendingLook = null;
22620
+ this.placedByName = false;
22621
+ this.stopGlide();
22622
+ }
22623
+ stopGlide() {
22624
+ this.glideHeading = 0;
22625
+ this.glideTilt = 0;
22504
22626
  }
22505
22627
  groundKnown() {
22506
22628
  _n.subVectors(this.orbitPosition, this.target).normalize();
@@ -22530,22 +22652,28 @@ var LookOrbitControls = class extends OrbitControls {
22530
22652
  this.placedQuat.copy(cam.quaternion);
22531
22653
  return;
22532
22654
  }
22533
- if (this.pivotRadius === 0) {
22534
- const was = _from$1.subVectors(this.placedPos, this.target);
22535
- const now = _hit.subVectors(cam.position, this.target);
22536
- const stretch = now.length() / was.length();
22537
- _turnBack.setFromUnitVectors(was.normalize(), now.normalize());
22538
- this.orbitPos.sub(this.target).applyQuaternion(_turnBack).multiplyScalar(stretch).add(this.target);
22539
- this.orbitQuat.premultiply(_turnBack);
22540
- this.place();
22541
- return;
22542
- }
22543
22655
  const holds = this.holdsNorth;
22544
22656
  _n.subVectors(this.orbitPos, this.target).normalize();
22545
22657
  this.lookToward(_pivotGround.copy(_n).multiplyScalar(this.pivotRadius));
22546
22658
  this.holdsNorth = holds;
22547
22659
  }
22548
22660
  update(deltaTime = null) {
22661
+ const changed = this.step(deltaTime);
22662
+ return this.glide() || changed;
22663
+ }
22664
+ glide() {
22665
+ if (!this.glideHeading && !this.glideTilt) return false;
22666
+ const f = this.dampingFactor;
22667
+ this.turnNow(this.glideHeading * f, this.glideTilt * f);
22668
+ this.glideHeading *= 1 - f;
22669
+ this.glideTilt *= 1 - f;
22670
+ if (Math.abs(this.glideHeading) < GLIDE_DONE && Math.abs(this.glideTilt) < GLIDE_DONE) {
22671
+ this.stopGlide();
22672
+ this.onTurnSettled();
22673
+ }
22674
+ return true;
22675
+ }
22676
+ step(deltaTime) {
22549
22677
  if (!this.looking) {
22550
22678
  const changed = super.update(deltaTime);
22551
22679
  if (this.pendingLook && this.groundKnown()) this.setLook(this.pendingLook);
@@ -22554,14 +22682,19 @@ var LookOrbitControls = class extends OrbitControls {
22554
22682
  const cam = this.object;
22555
22683
  if (!cam.position.equals(this.placedPos) || !cam.quaternion.equals(this.placedQuat)) {
22556
22684
  this.looking = false;
22685
+ this.stopGlide();
22557
22686
  return super.update(deltaTime);
22558
22687
  }
22688
+ if (!this.followGround()) return super.update(deltaTime);
22559
22689
  cam.position.copy(this.orbitPos);
22560
22690
  cam.quaternion.copy(this.orbitQuat);
22561
22691
  _was.copy(this.orbitQuat);
22562
22692
  const changed = super.update(deltaTime);
22563
22693
  const moved = cam.position.distanceToSquared(this.orbitPos) > this.orbitPos.lengthSq() * 1e-20;
22564
- if (moved) this.orbitMoved = true;
22694
+ if (moved) {
22695
+ this.orbitMoved = true;
22696
+ this.placedByName = false;
22697
+ }
22565
22698
  this.orbitPos.copy(cam.position);
22566
22699
  this.orbitQuat.copy(cam.quaternion);
22567
22700
  if (moved) this.samplePivot();
@@ -22586,6 +22719,7 @@ var LookOrbitControls = class extends OrbitControls {
22586
22719
  super.dispose();
22587
22720
  this.canvas.removeEventListener("pointerdown", this.onLookDown);
22588
22721
  this.canvas.removeEventListener("mousedown", this.blockMiddle);
22722
+ this.canvas.removeEventListener("pointerdown", this.onCatch);
22589
22723
  this.endLookDrag();
22590
22724
  }
22591
22725
  _handleMouseDownRotate(event) {
@@ -22600,6 +22734,28 @@ var LookOrbitControls = class extends OrbitControls {
22600
22734
  _handleTouchMoveRotate(event) {
22601
22735
  orbitHandlers._handleTouchMoveRotate.call(this, this.rotatedTouch(event));
22602
22736
  }
22737
+ _handleTouchStartDollyPan(event) {
22738
+ orbitHandlers._handleTouchStartDollyPan.call(this, event);
22739
+ const [a, b] = this.fingers();
22740
+ this.twoFinger.begin(a, b, this.enableRotate && this.lookShare() > 0);
22741
+ }
22742
+ _handleTouchMoveDollyPan(event) {
22743
+ const [a, b] = this.fingers();
22744
+ const step = this.twoFinger.move(a, b);
22745
+ if (step.mode === "undecided") return;
22746
+ const rate = TURN_PER_HEIGHT / (this.canvas.clientHeight || 1);
22747
+ if (step.mode === "tilt") {
22748
+ if (this.enableRotate) this.turn(0, -step.down * rate);
22749
+ return;
22750
+ }
22751
+ orbitHandlers._handleTouchMoveDollyPan.call(this, event);
22752
+ if (step.twist !== 0 && this.enableRotate) this.turn(step.twist, 0);
22753
+ }
22754
+ fingers() {
22755
+ const touch = this;
22756
+ const [a, b] = touch._pointers;
22757
+ return [touch._pointerPositions[a], touch._pointerPositions[b]];
22758
+ }
22603
22759
  _handleMouseWheel(event) {
22604
22760
  const anchored = this.enableRotate && this.groundAt(event.clientX, event.clientY, _anchor) && -_ray.dot(_hit.copy(_anchor).normalize()) > SIN_MIN_ANCHOR_ELEVATION;
22605
22761
  orbitHandlers._handleMouseWheel.call(this, event);
@@ -22664,6 +22820,7 @@ var LookOrbitControls = class extends OrbitControls {
22664
22820
  const level = offset.addScaledVector(n, -offset.dot(n));
22665
22821
  this.heading = Math.atan2(_ray.crossVectors(down, level).dot(n), down.dot(level));
22666
22822
  this.holdsNorth = false;
22823
+ this.placedByName = false;
22667
22824
  this.looking = true;
22668
22825
  if (!this.holdAngles(this.lookShare())) {
22669
22826
  this.looking = false;
@@ -22712,7 +22869,15 @@ var LookOrbitControls = class extends OrbitControls {
22712
22869
  pageY: 2 * y - other.y
22713
22870
  };
22714
22871
  }
22715
- turn(dx, dy) {
22872
+ turn(heading, tilt) {
22873
+ if (!this.enableDamping) {
22874
+ this.turnNow(heading, tilt);
22875
+ return;
22876
+ }
22877
+ this.glideHeading += heading;
22878
+ this.glideTilt += tilt;
22879
+ }
22880
+ turnNow(heading, tilt) {
22716
22881
  const cam = this.object;
22717
22882
  const share = this.lookShare();
22718
22883
  if (share === 0) return;
@@ -22726,9 +22891,8 @@ var LookOrbitControls = class extends OrbitControls {
22726
22891
  this.lastUp.copy(cam.up);
22727
22892
  }
22728
22893
  this.holdsNorth = false;
22729
- const rate = TURN_PER_HEIGHT / (this.canvas.clientHeight || 1);
22730
- this.heading -= dx * rate;
22731
- this.tilt = Math.max(0, this.tilt - dy * rate);
22894
+ this.heading += heading;
22895
+ this.tilt = Math.max(0, this.tilt + tilt);
22732
22896
  this.holdAngles(share);
22733
22897
  this.place();
22734
22898
  }
@@ -22736,10 +22900,7 @@ var LookOrbitControls = class extends OrbitControls {
22736
22900
  _n.subVectors(this.orbitPosition, this.target);
22737
22901
  const range = _n.length();
22738
22902
  const ground = range > 0 ? this.surfaceRadius(_n.divideScalar(range)) : null;
22739
- if (ground === null) return 0;
22740
- if (ground > 0) return this.shareAt((range - ground) / ground);
22741
- const height = this.standingHeight();
22742
- return height === null ? 0 : this.shareAt(height);
22903
+ return ground ? this.shareAt((range - ground) / ground) : 0;
22743
22904
  }
22744
22905
  shareAt(height) {
22745
22906
  const t = MathUtils.clamp((1 - height) / .5, 0, 1);
@@ -22751,6 +22912,22 @@ var LookOrbitControls = class extends OrbitControls {
22751
22912
  this.heading = MathUtils.clamp(this.heading, -Math.PI * share, Math.PI * share);
22752
22913
  return share > 0;
22753
22914
  }
22915
+ followGround() {
22916
+ _n.subVectors(this.orbitPos, this.target);
22917
+ const range = _n.length();
22918
+ const ground = range > 0 ? this.surfaceRadius(_n.divideScalar(range)) : null;
22919
+ if (ground === null) return true;
22920
+ const pivot = Math.min(ground, range);
22921
+ if (Math.abs(pivot - this.pivotRadius) <= ground * PIVOT_REFINE) return true;
22922
+ if (this.placedByName) {
22923
+ this.pivotRadius = pivot;
22924
+ return true;
22925
+ }
22926
+ const holds = this.holdsNorth;
22927
+ this.lookToward(_pivotGround.copy(_n).multiplyScalar(ground));
22928
+ this.holdsNorth = holds;
22929
+ return this.looking;
22930
+ }
22754
22931
  samplePivot() {
22755
22932
  _n.subVectors(this.orbitPos, this.target);
22756
22933
  const range = _n.length();
@@ -22772,6 +22949,11 @@ var LookOrbitControls = class extends OrbitControls {
22772
22949
  this.placedPos.copy(cam.position);
22773
22950
  this.placedQuat.copy(cam.quaternion);
22774
22951
  }
22952
+ onCatch = () => {
22953
+ if (!this.glideHeading && !this.glideTilt) return;
22954
+ this.stopGlide();
22955
+ this.onTurnSettled();
22956
+ };
22775
22957
  blockMiddle = (event) => {
22776
22958
  if (event.button === 1) event.preventDefault();
22777
22959
  };
@@ -22796,7 +22978,8 @@ var LookOrbitControls = class extends OrbitControls {
22796
22978
  const dy = event.clientY - this.dragY;
22797
22979
  this.dragX = event.clientX;
22798
22980
  this.dragY = event.clientY;
22799
- if (dx !== 0 || dy !== 0) this.turn(dx, dy);
22981
+ const rate = TURN_PER_HEIGHT / (this.canvas.clientHeight || 1);
22982
+ if (dx !== 0 || dy !== 0) this.turn(-dx * rate, -dy * rate);
22800
22983
  };
22801
22984
  onLookUp = (event) => {
22802
22985
  if (event.pointerId !== this.dragPointer) return;
@@ -22815,35 +22998,42 @@ var LookOrbitControls = class extends OrbitControls {
22815
22998
  // src/lib/scene/framing.ts
22816
22999
  var DEFAULT_FOCUS_ID = EARTH_ID;
22817
23000
 
22818
- // src/lib/scene/focus/feature-focus.ts
22819
- function featureBodyId(hostId, featureId) {
22820
- return `feature:${hostId}:${featureId}`;
22821
- }
22822
- function makeFeatureBody(anchor, name) {
22823
- return {
22824
- data: {
22825
- id: featureBodyId(anchor.hostId, anchor.featureId),
22826
- name,
22827
- objectType: ObjectType.SURFACE_FEATURE,
22828
- parentId: anchor.hostId,
22829
- radiusKm: NaN,
22830
- hasLocalized: false,
22831
- a: 0,
22832
- e: 0,
22833
- i: 0,
22834
- om: 0,
22835
- w: 0,
22836
- ma: 0,
22837
- n: 0,
22838
- epoch: 0,
22839
- validityStart: -Infinity,
22840
- validityEnd: Infinity,
22841
- orbitalSource: OrbitalSource.UNKNOWN
22842
- },
22843
- position: null,
22844
- orbitCenterId: anchor.hostId,
22845
- featureAnchor: anchor
22846
- };
23001
+ // src/lib/scene/position/feature-seat.ts
23002
+ var DEG2RAD$3 = Math.PI / 180;
23003
+ var _fixed = new Vector3();
23004
+ var _addend = new Vector3();
23005
+ function featureSeat(anchor, host, hostPos, hostBo, jd) {
23006
+ _addend.set(0, 0, 0);
23007
+ const idx = hostBo?.nomenclatureActiveIndex ?? -1;
23008
+ const label = idx >= 0 ? hostBo?.nomenclatureLabels?.[idx] ?? null : null;
23009
+ if (label && label.element.dataset.featureId === String(anchor.featureId)) {
23010
+ const modelAnchor = hostBo?.nomenclatureAnchor;
23011
+ if (modelAnchor) {
23012
+ _fixed.copy(label.position);
23013
+ _addend.copy(modelAnchor.position);
23014
+ } else {
23015
+ _fixed.copy(label.position);
23016
+ if (hostBo?.mesh) _fixed.multiply(hostBo.mesh.scale);
23017
+ }
23018
+ } else {
23019
+ const radiusKm = host.data.radiusKm;
23020
+ const latR = anchor.lat * DEG2RAD$3;
23021
+ const lngR = anchor.lon * DEG2RAD$3;
23022
+ const seat = host.orientation && Number.isFinite(radiusKm) && radiusKm > 0 ? renderedSeatAt(hostBo, host.data.id, radiusKm, latR, lngR) : null;
23023
+ if (seat) _fixed.set(seat.pointKm[0], seat.pointKm[1], seat.pointKm[2]);
23024
+ else {
23025
+ const r = Number.isFinite(radiusKm) && radiusKm > 0 ? radiusKm : 0;
23026
+ const [nx, ny, nz] = bodyFixedUnit(latR, lngR);
23027
+ _fixed.set(r * nx, r * ny, r * nz);
23028
+ }
23029
+ _fixed.multiplyScalar(kmToScene(1));
23030
+ }
23031
+ if (host.orientation) _fixed.applyQuaternion(bodyQuaternion(host.orientation, jd, host.nutPrec));
23032
+ return [
23033
+ hostPos[0] + _fixed.x + _addend.x,
23034
+ hostPos[1] + _fixed.y + _addend.y,
23035
+ hostPos[2] + _fixed.z + _addend.z
23036
+ ];
22847
23037
  }
22848
23038
 
22849
23039
  // src/lib/scene/label/throttled-renderer.ts
@@ -23045,6 +23235,7 @@ function makeTrailMaterial(color, brightness = TRAIL_DIM) {
23045
23235
  }
23046
23236
  var TRIM_DEPTH = CAMERA_NEAR / 2;
23047
23237
  var TRIM_SHARE = 1e-6;
23238
+ var MITER_LIMIT = 4;
23048
23239
  function makeFatTrailMaterial(color, lineWidth, brightness = TRAIL_DIM, dash) {
23049
23240
  return new ShaderMaterial({
23050
23241
  defines: dash ? { DASHED: "" } : {},
@@ -23070,9 +23261,11 @@ function makeFatTrailMaterial(color, lineWidth, brightness = TRAIL_DIM, dash) {
23070
23261
  uniform float uLineWidth;
23071
23262
  uniform vec2 uResolution;
23072
23263
  uniform vec2 uTrim;
23073
- attribute vec2 corner;
23264
+ attribute vec3 corner;
23265
+ attribute vec3 segPrev;
23074
23266
  attribute vec3 segStart;
23075
23267
  attribute vec3 segEnd;
23268
+ attribute vec3 segNext;
23076
23269
  attribute vec2 alphaStart;
23077
23270
  attribute vec2 alphaEnd;
23078
23271
  varying float vAlpha;
@@ -23082,9 +23275,78 @@ function makeFatTrailMaterial(color, lineWidth, brightness = TRAIL_DIM, dash) {
23082
23275
  varying float vDistW;
23083
23276
  varying float vW;
23084
23277
  #endif
23278
+ vec3 toView(vec3 point) {
23279
+ return mat3(viewMatrix) * (point + uCenterOffset);
23280
+ }
23281
+ vec2 toPx(vec3 view) {
23282
+ vec4 clip = projectionMatrix * vec4(view, 1.0);
23283
+ return clip.xy / clip.w * uResolution * 0.5;
23284
+ }
23285
+ // The unit direction on screen from one point to another, with the
23286
+ // distance in pixels in z.
23287
+ vec3 heading(vec2 from, vec2 to) {
23288
+ vec2 d = to - from;
23289
+ // Scaled before normalising: an end cut just ahead of the camera can
23290
+ // sit 1e20 px off screen, and its square overflows a float.
23291
+ float span = max(abs(d.x), abs(d.y));
23292
+ // Seen end on: any direction will do.
23293
+ if (span < 1e-4) return vec3(1.0, 0.0, 0.0);
23294
+ vec2 u = d / span;
23295
+ return vec3(normalize(u), span * length(u));
23296
+ }
23297
+ // The screen headings into and out of the middle of three points. Both
23298
+ // segments at a turn call this with the same points, so they place
23299
+ // their shared corners alike.
23300
+ void turnAt(vec3 before, vec3 at, vec3 after, out vec3 din, out vec3 dout) {
23301
+ vec2 p = toPx(toView(at));
23302
+ din = heading(toPx(toView(before)), p);
23303
+ dout = heading(p, toPx(toView(after)));
23304
+ }
23305
+ // A mitred corner: its offset, and how far along the quad's own
23306
+ // segment it moves.
23307
+ vec3 mitredAt(vec2 offset, vec2 own) {
23308
+ return vec3(offset, dot(offset, own));
23309
+ }
23310
+ // This corner's offset in pixels from the point where the line turns
23311
+ // from din to dout, and in z how far it moves along the quad's own
23312
+ // segment, own. The quads either side compute it from the same three
23313
+ // points, so they meet exactly. A square end, the fallback, moves
23314
+ // nothing along: exactly nothing, since a dash can end right there.
23315
+ vec3 cornerAt(vec3 din, vec3 dout, vec2 own, bool joins) {
23316
+ float h = uLineWidth * 0.5;
23317
+ vec3 square = vec3(vec2(-own.y, own.x) * corner.x * h, 0.0);
23318
+ bool bevel = corner.z > 0.5;
23319
+ if (!joins || din.z == 0.0 || dout.z == 0.0) return bevel ? vec3(0.0) : square;
23320
+ float inner = din.x * dout.y - din.y * dout.x >= 0.0 ? 1.0 : -1.0;
23321
+ vec2 bisector = din.xy + dout.xy;
23322
+ // The cosine of half the turn.
23323
+ float cosHalf = length(bisector) * 0.5;
23324
+ float safeCos = max(cosHalf, 1e-6);
23325
+ vec2 mitre = vec2(-bisector.y, bisector.x) / (2.0 * safeCos) * (h / safeCos);
23326
+ // How far along each segment the inner corner moves. Past half the
23327
+ // shorter segment it would cross that segment's other corner, and
23328
+ // square ends overlap instead, as with no join.
23329
+ float reach = h * sqrt(max(1.0 - cosHalf * cosHalf, 0.0)) / safeCos;
23330
+ bool fits = cosHalf > 1e-3 && reach <= 0.5 * min(din.z, dout.z);
23331
+ bool mitred = fits && cosHalf * ${MITER_LIMIT.toFixed(1)} >= 1.0;
23332
+ if (bevel) {
23333
+ // Each segment's square outer corner and the inner corner they
23334
+ // share, at the turn's own distance along the line. Flat under a
23335
+ // mitre, which covers the turn by itself, and beside a segment
23336
+ // shorter than the line is wide, where it could reach into the
23337
+ // quad past that segment.
23338
+ if (mitred || (!fits && min(din.z, dout.z) < uLineWidth)) return vec3(0.0);
23339
+ if (corner.z < 1.5) return vec3(-inner * vec2(-din.y, din.x) * h, 0.0);
23340
+ if (corner.z < 2.5) return vec3(-inner * vec2(-dout.y, dout.x) * h, 0.0);
23341
+ return vec3(fits ? inner * mitre : vec2(0.0), 0.0);
23342
+ }
23343
+ if (!fits) return square;
23344
+ if (corner.x == inner) return mitredAt(inner * mitre, own);
23345
+ return mitred ? mitredAt(-inner * mitre, own) : square;
23346
+ }
23085
23347
  void main() {
23086
- vec3 a = mat3(viewMatrix) * (segStart + uCenterOffset);
23087
- vec3 b = mat3(viewMatrix) * (segEnd + uCenterOffset);
23348
+ vec3 a = toView(segStart);
23349
+ vec3 b = toView(segEnd);
23088
23350
  float aheadA = -a.z;
23089
23351
  float aheadB = -b.z;
23090
23352
  float front = max(aheadA, aheadB);
@@ -23106,28 +23368,32 @@ function makeFatTrailMaterial(color, lineWidth, brightness = TRAIL_DIM, dash) {
23106
23368
  float cutB = aheadB < uTrim.x ? (cut - aheadB) / (aheadA - aheadB) : 0.0;
23107
23369
  if (cutA > 0.0) a = vec3(mix(a.xy, b.xy, cutA), -cut);
23108
23370
  if (cutB > 0.0) b = vec3(mix(b.xy, a.xy, cutB), -cut);
23109
- vec4 clipA = projectionMatrix * vec4(a, 1.0);
23110
- vec4 clipB = projectionMatrix * vec4(b, 1.0);
23111
- vec2 ndcA = clipA.xy / clipA.w;
23112
- vec2 ndcB = clipB.xy / clipB.w;
23113
- vec2 dirPx = (ndcB - ndcA) * uResolution * 0.5;
23114
- // Scaled before normalising: an end cut just ahead of the camera can
23115
- // sit 1e20 px off screen, and its square overflows a float.
23116
- float span = max(abs(dirPx.x), abs(dirPx.y));
23117
- // A segment seen end on: any perpendicular will do.
23118
- vec2 dirN = span < 1e-4 ? vec2(1.0, 0.0) : normalize(dirPx / span);
23119
- vec2 offsetNDC = vec2(-dirN.y, dirN.x) * corner.x * uLineWidth / uResolution;
23120
23371
  bool atEnd = corner.y > 0.5;
23121
- vec4 clip = atEnd ? clipB : clipA;
23122
- vec2 ndc = atEnd ? ndcB : ndcA;
23123
- gl_Position = vec4((ndc + offsetNDC) * clip.w, clip.zw);
23372
+ vec3 own = heading(toPx(a), toPx(b));
23373
+ // The turn this corner sits on, read in the line's direction: only
23374
+ // with all three of its points ahead of the camera, so that both
23375
+ // segments at the turn agree on it.
23376
+ bool joins = cutA == 0.0 && cutB == 0.0 && -toView(atEnd ? segNext : segPrev).z >= uTrim.x;
23377
+ vec3 din = own;
23378
+ vec3 dout = own;
23379
+ if (joins) {
23380
+ if (atEnd) turnAt(segStart, segEnd, segNext, din, dout);
23381
+ else turnAt(segPrev, segStart, segEnd, din, dout);
23382
+ own = atEnd ? din : dout;
23383
+ }
23384
+ vec3 offset = cornerAt(din, dout, own.xy, joins);
23385
+ vec4 clip = projectionMatrix * vec4(atEnd ? b : a, 1.0);
23386
+ gl_Position = vec4((clip.xy / clip.w + offset.xy * 2.0 / uResolution) * clip.w, clip.zw);
23124
23387
  vec2 alphas = atEnd ? mix(alphaEnd, alphaStart, cutB) : mix(alphaStart, alphaEnd, cutA);
23125
23388
  vAlpha = mix(alphas.x, alphas.y, uShowFull);
23126
23389
  #ifdef DASHED
23127
23390
  // Carried times w and divided back out per fragment, which undoes
23128
23391
  // the perspective a varying is interpolated with: the dashes are
23129
- // even on screen however much nearer one end is.
23392
+ // even on screen however much nearer one end is. A corner a mitre
23393
+ // moves along the segment counts on by as much, so dash ends stay
23394
+ // square to the line.
23130
23395
  float dist = atEnd ? mix(distEnd, distStart, cutB) : mix(distStart, distEnd, cutA);
23396
+ dist += offset.z;
23131
23397
  vDistW = dist * clip.w;
23132
23398
  vW = clip.w;
23133
23399
  #endif
@@ -25015,7 +25281,7 @@ function clampCameraOutsideBody(camera, body, focusTruePos, surface, clearanceKm
25015
25281
  const dist = Math.hypot(dx, dy, dz);
25016
25282
  if (surface?.seated) {
25017
25283
  if (dist < 1e-9) {
25018
- camera.position.set(cx, cy + focusDist + kmToScene(LANDED_KEEP_AWAY_KM), cz);
25284
+ camera.position.set(cx, cy + kmToScene(radiusKm + LANDED_KEEP_AWAY_KM), cz);
25019
25285
  return;
25020
25286
  }
25021
25287
  const hh = Math.tan(camera.fov * Math.PI / 360) * camera.near;
@@ -25060,7 +25326,7 @@ function clampCameraOutsideBody(camera, body, focusTruePos, surface, clearanceKm
25060
25326
  _dirBF.y,
25061
25327
  _dirBF.z
25062
25328
  ]);
25063
- const wall = (sKm !== null ? kmToScene(sKm) : focusDist) + kmToScene(LANDED_KEEP_AWAY_KM);
25329
+ const wall = kmToScene((sKm ?? radiusKm) + LANDED_KEEP_AWAY_KM);
25064
25330
  if (wall - r > lift) lift = wall - r;
25065
25331
  }
25066
25332
  if (lift <= 0) return;
@@ -25284,7 +25550,6 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
25284
25550
  screenOccluders.length = occluderCount;
25285
25551
  const focusedSystemId = ctx.visibility.focusedSystemId;
25286
25552
  let newlyShownLabel = false;
25287
- const nomFocusedBodyId = focusedBodyId ? nomenclatureBodyId(ctx.getBody(focusedBodyId), bodyObjects) : void 0;
25288
25553
  for (const bo of bodyObjects.values()) {
25289
25554
  const { body, group, trail } = bo;
25290
25555
  const dist = bo.cachedDist;
@@ -25292,7 +25557,7 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
25292
25557
  const drawn = isDrawn(bo);
25293
25558
  bo.root.visible = drawn;
25294
25559
  let showLabel;
25295
- const isFocused = body.data.id === focusedBodyId;
25560
+ const isFocused = ctx.visibility.inFocus(body.data.id);
25296
25561
  const isSystemRoot = focusedSystemId !== null && body.data.id === focusedSystemId;
25297
25562
  const centreShare = ctx.visibility.systemCentres.get(body.data.id);
25298
25563
  const isHeldStar = isFocused || ctx.visibility.isPinned(body.data.id) || centreShare === 1;
@@ -25401,7 +25666,7 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
25401
25666
  if (bo.label) fadeLabel(bo.label, labelShare);
25402
25667
  if (bo.planetDot) bo.planetDot.material.opacity = labelShare;
25403
25668
  const nomScreenR = bo.radiusScene > 0 ? bo.radiusScene / dist * projScale : 0;
25404
- const isNomFocused = showNomenclature && body.data.id === nomFocusedBodyId;
25669
+ const isNomFocused = showNomenclature && body.data.id === focusedBodyId;
25405
25670
  updateNomenclatureVisibility(bo, isNomFocused, nomScreenR, projScale, camera, screenW, screenH);
25406
25671
  }
25407
25672
  if (screenOccluders.length > 0) {
@@ -25413,7 +25678,7 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
25413
25678
  if (isScreenOccluded(_occlusionRect.x, _occlusionRect.y, bo.cachedDist, id, screenOccluders, id === focusedBodyId ? focusedPrimaryId : null)) bo.label.visible = false;
25414
25679
  }
25415
25680
  }
25416
- const groundBo = nomFocusedBodyId ? bodyObjects.get(nomFocusedBodyId) : void 0;
25681
+ const groundBo = focusedBodyId ? bodyObjects.get(focusedBodyId) : void 0;
25417
25682
  const ground = groundBo?.body.position;
25418
25683
  if (groundBo && ground) {
25419
25684
  const relief = reliefRadiiKm(groundBo);
@@ -25424,17 +25689,17 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
25424
25689
  for (const bo of bodyObjects.values()) {
25425
25690
  const at = bo.body.position;
25426
25691
  const id = bo.body.data.id;
25427
- if (!bo.label?.visible || !at || id === nomFocusedBodyId || id === focusedBodyId) continue;
25692
+ if (!bo.label?.visible || !at || ctx.visibility.inFocus(id)) continue;
25428
25693
  if (behindGround(at[0] - ground[0], at[1] - ground[1], at[2] - ground[2], cx, cy, cz, top)) bo.label.visible = false;
25429
25694
  }
25430
25695
  }
25431
25696
  if (forceCull || newlyShownLabel || ++cullFrameCounter >= 3) {
25432
25697
  cullFrameCounter = 0;
25433
- cullOverlappingLabels(bodyObjects, screenW, screenH, camera, focusedBodyId, ctx, hoveredBodyIds, screenOccluders, focusTruePos);
25698
+ cullOverlappingLabels(bodyObjects, screenW, screenH, camera, ctx, hoveredBodyIds, screenOccluders, focusTruePos);
25434
25699
  }
25435
25700
  refreshVisibleBodyLabelRects(bodyObjects, screenW, screenH, camera, focusTruePos, sceneSettings().showConstellations);
25436
- if (nomFocusedBodyId) {
25437
- const focusedBo = bodyObjects.get(nomFocusedBodyId);
25701
+ if (focusedBodyId) {
25702
+ const focusedBo = bodyObjects.get(focusedBodyId);
25438
25703
  if (focusedBo) cullOverlappingNomenclatureLabels(focusedBo);
25439
25704
  }
25440
25705
  for (const bo of bodyObjects.values()) {
@@ -25442,7 +25707,7 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
25442
25707
  if (!line?.visible) continue;
25443
25708
  const mat = line.material;
25444
25709
  mat.uniforms.uCenterOffset.value.set(-camera.position.x, -camera.position.y, -camera.position.z);
25445
- const isFocused = bo.body.data.id === focusedBodyId;
25710
+ const isFocused = ctx.visibility.inFocus(bo.body.data.id);
25446
25711
  const isHovered = hoveredBodyIds.has(bo.body.data.id);
25447
25712
  const share = ctx.visibility.systemShareOf(bo.body) * (line.userData.zoneShare ?? 1) * (line.userData.thresholdShare ?? 1) * (line.userData.haloShare ?? 1);
25448
25713
  mat.uniforms.uAlphaMultiplier.value = (isHovered ? 2 : isFocused ? 1.75 : 1) * share;
@@ -29525,13 +29790,9 @@ var GALACTIC_NORTH_SCENE = (() => {
29525
29790
  function galacticNorthVector(out) {
29526
29791
  return (out ?? new Vector3()).copy(GALACTIC_NORTH_SCENE);
29527
29792
  }
29528
- function bodyNorthVector(body, jd, out, landed = false) {
29793
+ function bodyNorthVector(body, jd, out) {
29529
29794
  const target = out ?? new Vector3();
29530
29795
  if (!body.orientation) {
29531
- if (landed && body.orbitCenter && body.position) {
29532
- target.set(body.position[0] - body.orbitCenter[0], body.position[1] - body.orbitCenter[1], body.position[2] - body.orbitCenter[2]);
29533
- if (target.lengthSq() > 1e-20) return target.normalize();
29534
- }
29535
29796
  target.copy(SCENE_UP);
29536
29797
  if (body.modelBaseFrame) target.applyQuaternion(body.modelBaseFrame).normalize();
29537
29798
  return target;
@@ -29545,7 +29806,6 @@ var CameraUpController = class {
29545
29806
  camera;
29546
29807
  controls;
29547
29808
  ctx;
29548
- isLanded;
29549
29809
  refId = null;
29550
29810
  startVec = new Vector3(0, 1, 0);
29551
29811
  targetVec = new Vector3(0, 1, 0);
@@ -29556,11 +29816,10 @@ var CameraUpController = class {
29556
29816
  plane = null;
29557
29817
  _quatA = new Quaternion();
29558
29818
  _quatB = new Quaternion();
29559
- constructor(camera, controls, ctx, isLanded) {
29819
+ constructor(camera, controls, ctx) {
29560
29820
  this.camera = camera;
29561
29821
  this.controls = controls;
29562
29822
  this.ctx = ctx;
29563
- this.isLanded = isLanded;
29564
29823
  }
29565
29824
  setNorthReference(id) {
29566
29825
  if (id === this.refId) return;
@@ -29579,10 +29838,8 @@ var CameraUpController = class {
29579
29838
  else this.targetVec.copy(SCENE_UP);
29580
29839
  } else {
29581
29840
  const refBody = this.refId ? this.ctx.getBody(this.refId) : void 0;
29582
- if (refBody) {
29583
- const landed = !!refBody.featureAnchor || this.isLanded(refBody.data.id);
29584
- bodyNorthVector(refBody, jd, this.targetVec, landed);
29585
- } else this.targetVec.copy(SCENE_UP);
29841
+ if (refBody) bodyNorthVector(refBody, jd, this.targetVec);
29842
+ else this.targetVec.copy(SCENE_UP);
29586
29843
  }
29587
29844
  if (this.needsDurationCalc) {
29588
29845
  this.needsDurationCalc = false;
@@ -30323,46 +30580,71 @@ function orbitalElementsToCurve(el, numPoints = 512) {
30323
30580
 
30324
30581
  // src/lib/scene/objects/trail/geometry.ts
30325
30582
  var TRAIL_RENDER_ORDER = 3;
30326
- var QUAD_CORNERS = [
30583
+ var CORNERS = [
30327
30584
  -1,
30328
30585
  0,
30586
+ 0,
30329
30587
  1,
30330
30588
  0,
30589
+ 0,
30331
30590
  -1,
30332
30591
  1,
30592
+ 0,
30333
30593
  1,
30334
- 1
30594
+ 1,
30595
+ 0,
30596
+ 0,
30597
+ 0,
30598
+ 1,
30599
+ 0,
30600
+ 0,
30601
+ 2,
30602
+ 0,
30603
+ 0,
30604
+ 3
30335
30605
  ];
30336
- var QUAD_INDICES = [
30606
+ var CORNER_INDICES = [
30337
30607
  0,
30338
30608
  1,
30339
30609
  2,
30340
30610
  2,
30341
30611
  1,
30342
- 3
30612
+ 3,
30613
+ 4,
30614
+ 5,
30615
+ 6
30343
30616
  ];
30344
30617
  function instanced(geometry, name) {
30345
30618
  return geometry.getAttribute(name).data;
30346
30619
  }
30347
- function setSegmentAttributes(geometry, names, capacity, stride) {
30348
- const buffer = new InstancedInterleavedBuffer(new Float32Array(capacity * stride), stride);
30349
- geometry.setAttribute(names[0], new InterleavedBufferAttribute(buffer, stride, 0));
30350
- geometry.setAttribute(names[1], new InterleavedBufferAttribute(buffer, stride, stride));
30620
+ function setPointAttributes(geometry, names, count, stride) {
30621
+ const buffer = new InstancedInterleavedBuffer(new Float32Array(count * stride), stride);
30622
+ names.forEach((name, i) => geometry.setAttribute(name, new InterleavedBufferAttribute(buffer, stride, i * stride)));
30351
30623
  }
30352
30624
  function makeFatTrailGeometry(capacity) {
30353
30625
  const geometry = new InstancedBufferGeometry();
30354
- geometry.setAttribute("corner", new Float32BufferAttribute(QUAD_CORNERS, 2));
30355
- geometry.setIndex(QUAD_INDICES);
30356
- setSegmentAttributes(geometry, ["segStart", "segEnd"], capacity, 3);
30357
- setSegmentAttributes(geometry, ["alphaStart", "alphaEnd"], capacity, 2);
30626
+ geometry.setAttribute("corner", new Float32BufferAttribute(CORNERS, 3));
30627
+ geometry.setIndex(CORNER_INDICES);
30628
+ setPointAttributes(geometry, [
30629
+ "segPrev",
30630
+ "segStart",
30631
+ "segEnd",
30632
+ "segNext"
30633
+ ], capacity + 2, 3);
30634
+ setPointAttributes(geometry, ["alphaStart", "alphaEnd"], capacity, 2);
30358
30635
  geometry.instanceCount = 0;
30359
30636
  return geometry;
30360
30637
  }
30361
30638
  function writeFatTrailVertices(geometry, posSrc, trailSrc, fullSrc, n) {
30362
30639
  const points = instanced(geometry, "segStart");
30363
30640
  const alphas = instanced(geometry, "alphaStart");
30364
- const m = Math.min(n, points.count);
30365
- points.array.set(posSrc.subarray(0, m * 3));
30641
+ const m = Math.min(n, alphas.count);
30642
+ const arr = points.array;
30643
+ arr.set(posSrc.subarray(0, m * 3), 3);
30644
+ const last = (m - 1) * 3;
30645
+ const closed = m > 2 && posSrc[0] === posSrc[last] && posSrc[1] === posSrc[last + 1] && posSrc[2] === posSrc[last + 2];
30646
+ arr.copyWithin(0, closed ? last : 3, closed ? last + 3 : 6);
30647
+ arr.copyWithin((m + 1) * 3, closed ? 6 : m * 3, closed ? 9 : (m + 1) * 3);
30366
30648
  const alphaArr = alphas.array;
30367
30649
  for (let i = 0; i < m; i++) {
30368
30650
  alphaArr[2 * i] = trailSrc[i];
@@ -30371,7 +30653,7 @@ function writeFatTrailVertices(geometry, posSrc, trailSrc, fullSrc, n) {
30371
30653
  points.needsUpdate = true;
30372
30654
  alphas.needsUpdate = true;
30373
30655
  geometry.instanceCount = Math.max(0, m - 1);
30374
- geometry.setDrawRange(0, QUAD_INDICES.length);
30656
+ geometry.setDrawRange(0, CORNER_INDICES.length);
30375
30657
  }
30376
30658
  function makeEmptyTrail() {
30377
30659
  const geometry = new BufferGeometry();
@@ -30401,7 +30683,7 @@ function writeLineDistances(geometry, src, n) {
30401
30683
  }
30402
30684
  function buildFatLineFromThin(capacity, posArr, trailAlphas, fullAlphas, total, color, lineWidth, brightness, dash) {
30403
30685
  const geometry = makeFatTrailGeometry(capacity);
30404
- if (dash) setSegmentAttributes(geometry, ["distStart", "distEnd"], capacity, 1);
30686
+ if (dash) setPointAttributes(geometry, ["distStart", "distEnd"], capacity, 1);
30405
30687
  writeFatTrailVertices(geometry, posArr, trailAlphas, fullAlphas, total);
30406
30688
  const mesh = new Mesh(geometry, makeFatTrailMaterial(color, lineWidth, brightness, dash));
30407
30689
  mesh.matrixAutoUpdate = false;
@@ -30768,7 +31050,7 @@ var SystemDataLoader = class {
30768
31050
  };
30769
31051
 
30770
31052
  // src/lib/scene/debug/skybox-adjust.ts
30771
- var DEG2RAD$3 = Math.PI / 180;
31053
+ var DEG2RAD$2 = Math.PI / 180;
30772
31054
  var SkyboxAdjuster = class {
30773
31055
  scene;
30774
31056
  adjust = {
@@ -30792,15 +31074,15 @@ var SkyboxAdjuster = class {
30792
31074
  this.adjust.rxDeg = rxDeg;
30793
31075
  this.adjust.ryDeg = ryDeg;
30794
31076
  this.adjust.rzDeg = rzDeg;
30795
- this._adjustQ.setFromAxisAngle(this._axisX, rxDeg * DEG2RAD$3).premultiply(this._tmpQ.setFromAxisAngle(this._axisY, ryDeg * DEG2RAD$3)).premultiply(this._tmpQ.setFromAxisAngle(this._axisZ, rzDeg * DEG2RAD$3));
31077
+ this._adjustQ.setFromAxisAngle(this._axisX, rxDeg * DEG2RAD$2).premultiply(this._tmpQ.setFromAxisAngle(this._axisY, ryDeg * DEG2RAD$2)).premultiply(this._tmpQ.setFromAxisAngle(this._axisZ, rzDeg * DEG2RAD$2));
30796
31078
  this._q.copy(SKYBOX_BASE_ROTATION).multiply(this._adjustQ);
30797
31079
  this.scene.backgroundRotation.setFromQuaternion(this._q);
30798
31080
  }
30799
31081
  };
30800
31082
 
30801
31083
  // src/lib/scene/objects/sky/debug-markers.ts
30802
- var DEG2RAD$2 = Math.PI / 180;
30803
- var OBL_RAD = EARTH_OBLIQUITY_DEG * DEG2RAD$2;
31084
+ var DEG2RAD$1 = Math.PI / 180;
31085
+ var OBL_RAD = EARTH_OBLIQUITY_DEG * DEG2RAD$1;
30804
31086
  var COS_OBL = Math.cos(OBL_RAD);
30805
31087
  var SIN_OBL = Math.sin(OBL_RAD);
30806
31088
  var SKY_MARKER_RADIUS = 5e3;
@@ -30916,8 +31198,8 @@ var REFERENCE_CIRCLES = [
30916
31198
  }
30917
31199
  ];
30918
31200
  function eqToScene(raDeg, decDeg, out) {
30919
- const ra = raDeg * DEG2RAD$2;
30920
- const dec = decDeg * DEG2RAD$2;
31201
+ const ra = raDeg * DEG2RAD$1;
31202
+ const dec = decDeg * DEG2RAD$1;
30921
31203
  const cd = Math.cos(dec);
30922
31204
  const xEq = cd * Math.cos(ra);
30923
31205
  const yEq = cd * Math.sin(ra);
@@ -32600,16 +32882,16 @@ function between(points, jds, jd, from = 0, to = points.length) {
32600
32882
  }
32601
32883
 
32602
32884
  // src/lib/math/travel/state.ts
32603
- var DEG2RAD$1 = Math.PI / 180;
32604
- var COS_EPS = Math.cos(EARTH_OBLIQUITY_DEG * DEG2RAD$1);
32605
- var SIN_EPS = Math.sin(EARTH_OBLIQUITY_DEG * DEG2RAD$1);
32885
+ var DEG2RAD = Math.PI / 180;
32886
+ var COS_EPS = Math.cos(EARTH_OBLIQUITY_DEG * DEG2RAD);
32887
+ var SIN_EPS = Math.sin(EARTH_OBLIQUITY_DEG * DEG2RAD);
32606
32888
  function planeRotation(w, i, om, equatorial) {
32607
- const cosW = Math.cos(w * DEG2RAD$1);
32608
- const sinW = Math.sin(w * DEG2RAD$1);
32609
- const cosI = Math.cos(i * DEG2RAD$1);
32610
- const sinI = Math.sin(i * DEG2RAD$1);
32611
- const cosOm = Math.cos(om * DEG2RAD$1);
32612
- const sinOm = Math.sin(om * DEG2RAD$1);
32889
+ const cosW = Math.cos(w * DEG2RAD);
32890
+ const sinW = Math.sin(w * DEG2RAD);
32891
+ const cosI = Math.cos(i * DEG2RAD);
32892
+ const sinI = Math.sin(i * DEG2RAD);
32893
+ const cosOm = Math.cos(om * DEG2RAD);
32894
+ const sinOm = Math.sin(om * DEG2RAD);
32613
32895
  return {
32614
32896
  m11: cosOm * cosW - sinOm * sinW * cosI,
32615
32897
  m12: -cosOm * sinW - sinOm * cosW * cosI,
@@ -32659,10 +32941,10 @@ function elementsToState(el, jd) {
32659
32941
  }
32660
32942
  if (!isFinite(e) || !isFinite(ma)) return null;
32661
32943
  const aKm = a * AU_KM;
32662
- const nRadPerSec = n * DEG2RAD$1 / SEC_PER_DAY$1;
32944
+ const nRadPerSec = n * DEG2RAD / SEC_PER_DAY$1;
32663
32945
  const mu = nRadPerSec * nRadPerSec * Math.abs(aKm) * Math.abs(aKm) * Math.abs(aKm);
32664
32946
  if (!isFinite(mu) || mu <= 0) return null;
32665
- const M = (ma + n * (t - epoch)) * DEG2RAD$1;
32947
+ const M = (ma + n * (t - epoch)) * DEG2RAD;
32666
32948
  let nu;
32667
32949
  let rKm;
32668
32950
  if (e < 1 || a > 0) {
@@ -33658,65 +33940,6 @@ function addVec(out, a, b) {
33658
33940
  out[2] = a[2] + b[2];
33659
33941
  }
33660
33942
 
33661
- // src/lib/scene/position/feature-seat.ts
33662
- var DEG2RAD = Math.PI / 180;
33663
- var SEAT_GLIDE_TAU_MS = 120;
33664
- var SEAT_GLIDE_SNAP_SQ = kmToScene(.001) ** 2;
33665
- var glideState = /* @__PURE__ */ new WeakMap();
33666
- var _fixed = new Vector3();
33667
- var _addend = new Vector3();
33668
- function glideVec(current, target, k) {
33669
- if (current.distanceToSquared(target) <= SEAT_GLIDE_SNAP_SQ) current.copy(target);
33670
- else current.lerp(target, k);
33671
- }
33672
- function seatFeatureBody(fb, host, hostPos, hostBo, jd) {
33673
- const anchor = fb.featureAnchor;
33674
- setOrbitCenter(fb, hostPos);
33675
- _addend.set(0, 0, 0);
33676
- const idx = hostBo?.nomenclatureActiveIndex ?? -1;
33677
- const label = idx >= 0 ? hostBo?.nomenclatureLabels?.[idx] ?? null : null;
33678
- if (label && label.element.dataset.featureId === String(anchor.featureId)) {
33679
- const modelAnchor = hostBo?.nomenclatureAnchor;
33680
- if (modelAnchor) {
33681
- _fixed.copy(label.position);
33682
- _addend.copy(modelAnchor.position);
33683
- } else {
33684
- _fixed.copy(label.position);
33685
- if (hostBo?.mesh) _fixed.multiply(hostBo.mesh.scale);
33686
- }
33687
- } else {
33688
- const radiusKm = host.data.radiusKm;
33689
- const latR = anchor.lat * DEG2RAD;
33690
- const lngR = anchor.lon * DEG2RAD;
33691
- const seat = host.orientation && Number.isFinite(radiusKm) && radiusKm > 0 ? renderedSeatAt(hostBo, host.data.id, radiusKm, latR, lngR) : null;
33692
- if (seat) _fixed.set(seat.pointKm[0], seat.pointKm[1], seat.pointKm[2]);
33693
- else {
33694
- const r = Number.isFinite(radiusKm) && radiusKm > 0 ? radiusKm : 0;
33695
- const [nx, ny, nz] = bodyFixedUnit(latR, lngR);
33696
- _fixed.set(r * nx, r * ny, r * nz);
33697
- }
33698
- _fixed.multiplyScalar(kmToScene(1));
33699
- }
33700
- const now = performance.now();
33701
- let st = glideState.get(fb);
33702
- if (!st) {
33703
- st = {
33704
- fixed: _fixed.clone(),
33705
- addend: _addend.clone(),
33706
- t: now
33707
- };
33708
- glideState.set(fb, st);
33709
- } else {
33710
- const k = 1 - Math.exp(-(now - st.t) / SEAT_GLIDE_TAU_MS);
33711
- st.t = now;
33712
- glideVec(st.fixed, _fixed, k);
33713
- glideVec(st.addend, _addend, k);
33714
- }
33715
- _fixed.copy(st.fixed);
33716
- if (host.orientation) _fixed.applyQuaternion(bodyQuaternion(host.orientation, jd, host.nutPrec));
33717
- return setPlaced(fb, hostPos[0] + _fixed.x + st.addend.x, hostPos[1] + _fixed.y + st.addend.y, hostPos[2] + _fixed.z + st.addend.z);
33718
- }
33719
-
33720
33943
  // src/lib/math/orbit/lagrange.ts
33721
33944
  var EARTH_SUN_MASS_RATIO = 30034e-10;
33722
33945
  var ALONG_MIN = .45;
@@ -34051,7 +34274,7 @@ var Placer = class {
34051
34274
  return null;
34052
34275
  }
34053
34276
  this.settling.add(id);
34054
- const pos = isSurfaceFeature(body) ? this.seatFeature(body, jd) : isExtrasolar(body) && !body.data.parentId ? fixedPlace(body) : this.settle(body, jd);
34277
+ const pos = isExtrasolar(body) && !body.data.parentId ? fixedPlace(body) : this.settle(body, jd);
34055
34278
  this.settling.delete(id);
34056
34279
  if (jd === this.passJd) this.settled.set(id, body);
34057
34280
  return pos;
@@ -34060,13 +34283,6 @@ var Placer = class {
34060
34283
  const why = (this.ctx.getBody(aboveId) ?? this.bodyObjects.get(aboveId)?.body)?.unplaced;
34061
34284
  return why === "loading" || why === "never" ? why : "parent";
34062
34285
  }
34063
- seatFeature(body, jd) {
34064
- const hostId = body.featureAnchor.hostId;
34065
- const host = this.ctx.getBody(hostId);
34066
- const hostPos = host && this.place(host, jd);
34067
- if (!host || !hostPos) return setUnplaced(body, this.under(hostId));
34068
- return seatFeatureBody(body, host, hostPos, this.bodyObjects.get(hostId), jd);
34069
- }
34070
34286
  rideAt(jd) {
34071
34287
  if (jd !== this.rideJd || this.focusedId !== this.rideFocus) {
34072
34288
  this.rideJd = jd;
@@ -35250,14 +35466,13 @@ function updateSphereLOD(bodyObjects, camera, renderer, ctx, focusedId, invalida
35250
35466
  const fovRad = camera.fov * Math.PI / 180;
35251
35467
  const projScale = renderer.domElement.clientHeight / (2 * Math.tan(fovRad / 2));
35252
35468
  const activeSystem = ctx.visibility.activeSystemId;
35253
- const terrainId = focusedId;
35254
35469
  for (const bo of bodyObjects.values()) {
35255
35470
  if (!bo.mesh || !bo.radiusScene || !bo.group.visible) continue;
35256
35471
  if (bo.cachedDist <= 0) continue;
35257
35472
  const isStar = bo.body.data.objectType === ObjectType.STAR;
35258
35473
  const id = bo.body.data.id;
35259
- if (id === terrainId || bo.terrainWindow) {
35260
- if (updateTerrainWindow(bo, camera, id === terrainId, invalidate)) continue;
35474
+ if (id === focusedId || bo.terrainWindow) {
35475
+ if (updateTerrainWindow(bo, camera, id === focusedId, invalidate)) continue;
35261
35476
  }
35262
35477
  const desired = desiredSphereSegments(bo.radiusScene / bo.cachedDist * projScale, isStar, activeSystem ? id === activeSystem || ctx.visibility.isInActiveSystem(bo.body.data.parentId) : id === focusedId, bo.currentSegments ?? 64);
35263
35478
  if (desired === bo.currentSegments) continue;
@@ -35642,8 +35857,11 @@ var PromotionRegistry = class {
35642
35857
  this.finalizeBuilds();
35643
35858
  }
35644
35859
  syncSystemBodies(bodies) {
35645
- const focusedId = this.deps.getFocusedId();
35646
- if (focusedId !== void 0 && this.systemPromoted.delete(focusedId)) this.userPromoted.add(focusedId);
35860
+ for (const id of this.systemPromoted) {
35861
+ if (!this.deps.inFocus(id)) continue;
35862
+ this.systemPromoted.delete(id);
35863
+ this.userPromoted.add(id);
35864
+ }
35647
35865
  const wanted = new Set(bodies.map((b) => b.data.id));
35648
35866
  const dropped = [...this.systemPromoted].filter((id) => !wanted.has(id) && !this.pinned.has(id));
35649
35867
  this.tearDownBodies(dropped, this.systemPromoted);
@@ -35755,11 +35973,10 @@ var PromotionRegistry = class {
35755
35973
  }
35756
35974
  teardownAutoPromoted(forceAll) {
35757
35975
  if (this.autoPromoted.size === 0) return;
35758
- const focusedId = this.deps.getFocusedId();
35759
35976
  const jd = this.deps.clock.jd;
35760
35977
  const toRemove = [];
35761
35978
  for (const id of this.autoPromoted) {
35762
- if (id === focusedId) continue;
35979
+ if (this.deps.inFocus(id)) continue;
35763
35980
  if (forceAll || this.groupTargets && !this.groupTargets.has(id)) {
35764
35981
  toRemove.push(id);
35765
35982
  continue;
@@ -35772,21 +35989,21 @@ var PromotionRegistry = class {
35772
35989
  }
35773
35990
  setPinned(ids) {
35774
35991
  this.pinned = new Set(ids);
35775
- const focusedId = this.deps.getFocusedId();
35776
35992
  const dropped = [...this.pinPromoted].filter((id) => !this.pinned.has(id));
35777
- if (focusedId && dropped.includes(focusedId)) {
35778
- this.pinPromoted.delete(focusedId);
35779
- this.userPromoted.add(focusedId);
35780
- this.emitUserPromotedCount();
35993
+ const kept = dropped.filter(this.deps.inFocus);
35994
+ for (const id of kept) {
35995
+ this.pinPromoted.delete(id);
35996
+ this.userPromoted.add(id);
35781
35997
  }
35782
- this.tearDownBodies(dropped.filter((id) => id !== focusedId), this.pinPromoted);
35998
+ if (kept.length > 0) this.emitUserPromotedCount();
35999
+ this.tearDownBodies(dropped.filter((id) => !this.deps.inFocus(id)), this.pinPromoted);
35783
36000
  this.buildPinned();
35784
36001
  }
35785
36002
  buildPinned() {
35786
36003
  let built = false;
35787
36004
  for (const id of this.pinned) {
35788
36005
  const body = this.deps.ctx.getBody(id);
35789
- if (!body || isSurfaceFeature(body) || body.data.objectType === ObjectType.MOON) continue;
36006
+ if (!body || body.data.objectType === ObjectType.MOON) continue;
35790
36007
  if (!this.buildBodyInstance(body)) continue;
35791
36008
  built = true;
35792
36009
  if (this.userPromoted.delete(id)) this.pinPromoted.add(id);
@@ -35796,8 +36013,7 @@ var PromotionRegistry = class {
35796
36013
  this.finalizeBuilds();
35797
36014
  }
35798
36015
  clearUserPromoted() {
35799
- const focusedId = this.deps.getFocusedId();
35800
- const toRemove = [...this.userPromoted].filter((id) => id !== focusedId);
36016
+ const toRemove = [...this.userPromoted].filter((id) => !this.deps.inFocus(id));
35801
36017
  this.tearDownBodies(toRemove, this.userPromoted);
35802
36018
  this.emitUserPromotedCount();
35803
36019
  }
@@ -35858,15 +36074,61 @@ var PromotionRegistry = class {
35858
36074
  }
35859
36075
  emitUserPromotedCount() {
35860
36076
  if (!this.deps.onUserPromotedChange) return;
35861
- const focusedId = this.deps.getFocusedId();
35862
- let count = this.userPromoted.size;
35863
- if (focusedId && this.userPromoted.has(focusedId)) count--;
36077
+ let count = 0;
36078
+ for (const id of this.userPromoted) if (!this.deps.inFocus(id)) count++;
35864
36079
  this.deps.onUserPromotedChange(count);
35865
36080
  }
35866
36081
  };
35867
36082
 
35868
36083
  // src/lib/scene/focus/controller.ts
36084
+ function surfaceSite(host, point, hostPos, jd) {
36085
+ const s = cartesianToSpherical([...point], [...hostPos], siteQuat(host, jd));
36086
+ return {
36087
+ latitude: s.latitude,
36088
+ longitude: s.longitude,
36089
+ radius: s.distance
36090
+ };
36091
+ }
36092
+ function siteQuat(host, jd) {
36093
+ const q = host.orientation && bodyQuaternion(host.orientation, jd, host.nutPrec);
36094
+ return q ? [
36095
+ q.x,
36096
+ q.y,
36097
+ q.z,
36098
+ q.w
36099
+ ] : void 0;
36100
+ }
36101
+ function underHalfRadius(host, distance) {
36102
+ return distance < kmToScene(effectiveRadiusKm(host.data)) / 2;
36103
+ }
36104
+ function viewOverSite(site, offset, quat) {
36105
+ const { latitude, longitude } = site;
36106
+ const up = new Vector3(...sphericalToCartesian([
36107
+ 0,
36108
+ 0,
36109
+ 0
36110
+ ], latitude, longitude, 1, quat));
36111
+ const down = new Vector3(...sphericalToCartesian([
36112
+ 0,
36113
+ 0,
36114
+ 0
36115
+ ], latitude + 90, longitude, 1, quat)).negate();
36116
+ const range = offset.length();
36117
+ const level = offset.clone().addScaledVector(up, -offset.dot(up));
36118
+ const tilt = Math.acos(MathUtils.clamp(offset.dot(up) / range, -1, 1));
36119
+ const heading = Math.atan2(new Vector3().crossVectors(down, level).dot(up), down.dot(level));
36120
+ return {
36121
+ latitude,
36122
+ longitude,
36123
+ zoom: site.radius + range,
36124
+ look: {
36125
+ tilt,
36126
+ heading
36127
+ }
36128
+ };
36129
+ }
35869
36130
  var _from = new Vector3();
36131
+ var _shift = new Vector3();
35870
36132
  var _forward = new Vector3();
35871
36133
  var _ground = new Vector3();
35872
36134
  var FocusController = class {
@@ -35875,6 +36137,10 @@ var FocusController = class {
35875
36137
  readerFocusFilter = null;
35876
36138
  focusSets = 0;
35877
36139
  focusedBody;
36140
+ subjectBody;
36141
+ onGround = false;
36142
+ subjectSite = null;
36143
+ placedOffSubject = false;
35878
36144
  _tmpV3 = new Vector3();
35879
36145
  pendingInitialView = null;
35880
36146
  heldEpoch = -1;
@@ -35896,7 +36162,7 @@ var FocusController = class {
35896
36162
  onBodyClick: (b) => this.handleFocus(b),
35897
36163
  assignMapLayerToTrails: deps.assignMapLayerToTrails,
35898
36164
  repositionAll: deps.repositionAll,
35899
- getFocusedId: () => this.focusedBody?.data.id,
36165
+ inFocus: (id) => id === this.focusedBody?.data.id || id === this.subjectBody?.data.id,
35900
36166
  onUserPromotedChange: deps.callbacks.onUserPromotedChange,
35901
36167
  onSystemBodies: (bodies) => deps.systemData.loadPlanets(bodies)
35902
36168
  });
@@ -35904,6 +36170,151 @@ var FocusController = class {
35904
36170
  get current() {
35905
36171
  return this.focusedBody;
35906
36172
  }
36173
+ get subject() {
36174
+ return this.subjectBody;
36175
+ }
36176
+ get groundView() {
36177
+ return this.onGround;
36178
+ }
36179
+ onGroundAt(body, jd = this.deps.clock.jd) {
36180
+ if (body.data.objectType !== ObjectType.SPACECRAFT) return false;
36181
+ const probe = this.probeRecord(body, jd);
36182
+ return probe && isLandedAt(probe, jd);
36183
+ }
36184
+ landedSite(body, jd = this.deps.clock.jd) {
36185
+ const probe = this.probeRecord(body, jd);
36186
+ if (!probe?.landed || !isLandedAt(probe, jd)) return void 0;
36187
+ const { ctx } = this.deps;
36188
+ const host = ctx.getBody(`naif-${probe.landed.bodyNaifId}`);
36189
+ const pos = ctx.place(body, jd);
36190
+ const hostPos = host && ctx.place(host, jd);
36191
+ if (!host || !pos || !hostPos) return void 0;
36192
+ return {
36193
+ host,
36194
+ site: surfaceSite(host, pos, hostPos, jd)
36195
+ };
36196
+ }
36197
+ probeRecord(body, jd) {
36198
+ if (body.data.objectType !== ObjectType.SPACECRAFT) return void 0;
36199
+ return this.deps.ctx.probeStore?.probeWithCenter(body.data.id, jd)?.probe;
36200
+ }
36201
+ followSubject(jd) {
36202
+ const subject = this.subjectBody;
36203
+ const host = this.focusedBody;
36204
+ const now = subject && this.landedSite(subject, jd);
36205
+ if (!subject || !host || !now || now.host !== host) return;
36206
+ const was = this.subjectSite;
36207
+ this.subjectSite = now.site;
36208
+ if (!was || !this.placedOffSubject && this.pendingInitialView) return;
36209
+ const q = this.focusedBodyQuat(host);
36210
+ const a = sphericalToCartesian([
36211
+ 0,
36212
+ 0,
36213
+ 0
36214
+ ], was.latitude, was.longitude, was.radius, q);
36215
+ const b = sphericalToCartesian([
36216
+ 0,
36217
+ 0,
36218
+ 0
36219
+ ], now.site.latitude, now.site.longitude, now.site.radius, q);
36220
+ const shift = _shift.set(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
36221
+ if (shift.length() < kmToScene(1e-6)) return;
36222
+ this.deps.controls.moveOrbit((orbit) => orbit.add(shift));
36223
+ const held = this.pendingInitialView;
36224
+ if (held) {
36225
+ const at = sphericalToCartesian([
36226
+ 0,
36227
+ 0,
36228
+ 0
36229
+ ], held.latitude, held.longitude, held.zoom, q);
36230
+ const moved = cartesianToSpherical([
36231
+ at[0] + shift.x,
36232
+ at[1] + shift.y,
36233
+ at[2] + shift.z
36234
+ ], [
36235
+ 0,
36236
+ 0,
36237
+ 0
36238
+ ], q);
36239
+ this.pendingInitialView = {
36240
+ ...held,
36241
+ latitude: moved.latitude,
36242
+ longitude: moved.longitude,
36243
+ zoom: moved.distance
36244
+ };
36245
+ }
36246
+ }
36247
+ leaveGround() {
36248
+ const subject = this.subjectBody;
36249
+ const host = this.focusedBody;
36250
+ const site = this.subjectSite;
36251
+ if (!subject || !host) return;
36252
+ const pos = this.place(subject);
36253
+ if (!pos || !site) {
36254
+ this.setFocusTarget(subject);
36255
+ return;
36256
+ }
36257
+ const seat = sphericalToCartesian([
36258
+ 0,
36259
+ 0,
36260
+ 0
36261
+ ], site.latitude, site.longitude, site.radius, siteQuat(host, this.deps.clock.jd));
36262
+ const cam = this.deps.camera.position;
36263
+ const offset = cartesianToSpherical([
36264
+ cam.x - seat[0],
36265
+ cam.y - seat[1],
36266
+ cam.z - seat[2]
36267
+ ], [
36268
+ 0,
36269
+ 0,
36270
+ 0
36271
+ ]);
36272
+ this.settleOnBodyInstant(subject, pos);
36273
+ this.snapToBodyFrame(offset.latitude, offset.longitude, offset.distance);
36274
+ }
36275
+ panToSurfacePoint(host, site, height, subject) {
36276
+ const { focus, camera, controls, callbacks, pointClouds } = this.deps;
36277
+ const hostPos = this.place(host);
36278
+ if (!hostPos) return 0;
36279
+ const current = this.focusedBody;
36280
+ if (!(current !== void 0 && (current.data.id === host.data.id || this.landedSite(current)?.host.data.id === host.data.id)) && height !== void 0) return this.arriveAbove(host, site, height, subject);
36281
+ const camWorld = this.cameraTruePos();
36282
+ if (f64dist(focus.focusTruePos, hostPos) > 0) {
36283
+ focus.focusTruePos = [...hostPos];
36284
+ camera.position.set(camWorld[0] - hostPos[0], camWorld[1] - hostPos[1], camWorld[2] - hostPos[2]);
36285
+ this.deps.repositionAll();
36286
+ pointClouds.rebuildBasis();
36287
+ }
36288
+ this.setFocusTarget(host, void 0, subject);
36289
+ this.onGround = true;
36290
+ this.placedOffSubject = true;
36291
+ focus.aim = null;
36292
+ focus.aimLooks = false;
36293
+ focus.aimPose = null;
36294
+ const quat = siteQuat(host, this.deps.clock.jd);
36295
+ const ground = new Vector3(...sphericalToCartesian([
36296
+ 0,
36297
+ 0,
36298
+ 0
36299
+ ], site.latitude, site.longitude, site.radius, quat));
36300
+ const from = _from.set(camWorld[0] - hostPos[0], camWorld[1] - hostPos[1], camWorld[2] - hostPos[2]);
36301
+ if (controls.looksOver(from, ground)) {
36302
+ focus.aimPose = controls.lookPose(from, ground, new Quaternion());
36303
+ focus.aim = [
36304
+ ground.x,
36305
+ ground.y,
36306
+ ground.z
36307
+ ];
36308
+ focus.aimLooks = true;
36309
+ focus.focusDurationMs = angularDuration(camera.quaternion.angleTo(focus.aimPose), FOCUS_ROT_PACING);
36310
+ return focus.focusDurationMs;
36311
+ }
36312
+ const distance = from.length();
36313
+ callbacks.onCameraPosition?.(site.latitude, site.longitude, distance);
36314
+ const camPos = sphericalToCartesian([...hostPos], site.latitude, site.longitude, distance, quat);
36315
+ prepareFlyToCamera(focus, camera, camWorld, camPos, sceneSettings().resolvedReducedMotion);
36316
+ return focus.focusDurationMs;
36317
+ }
35907
36318
  get focusChanges() {
35908
36319
  return this.focusSets;
35909
36320
  }
@@ -35946,12 +36357,12 @@ var FocusController = class {
35946
36357
  ];
35947
36358
  }
35948
36359
  holdsModel(id) {
35949
- const focused = this.focusedBody;
35950
- return id === (focused && (isSurfaceFeature(focused) ? focused.featureAnchor.hostId : focused.data.id)) || this.upgradeTargetIds.has(id);
36360
+ return id === this.focusedBody?.data.id || id === this.subjectBody?.data.id || this.upgradeTargetIds.has(id);
35951
36361
  }
35952
36362
  syncUpgradeTargets(body) {
35953
36363
  const { ctx, scene, clickables, meshToBody, bodyObjects, pointClouds, clock, loadTexture, assignMapLayerToTrails } = this.deps;
35954
36364
  const targets = upgradeTargets(body, ctx);
36365
+ if (this.subjectBody && !targets.includes(this.subjectBody)) targets.push(this.subjectBody);
35955
36366
  const newIds = new Set(targets.map((t) => t.data.id));
35956
36367
  for (const id of this.upgradeTargetIds) {
35957
36368
  if (newIds.has(id)) continue;
@@ -36011,6 +36422,8 @@ var FocusController = class {
36011
36422
  ];
36012
36423
  }
36013
36424
  this.focusedBody = void 0;
36425
+ this.subjectBody = void 0;
36426
+ this.onGround = false;
36014
36427
  this.promotion.emitUserPromotedCount();
36015
36428
  prepareFocusTarget(focus, [...position], camera, camWorld, camPos, sceneSettings().resolvedReducedMotion);
36016
36429
  const spherical = cartesianToSpherical(camPos ?? camWorld, position, void 0);
@@ -36051,8 +36464,13 @@ var FocusController = class {
36051
36464
  }
36052
36465
  handleFocus(body) {
36053
36466
  if (this.readerFocusFilter && !this.readerFocusFilter(body)) return;
36054
- if (this.focusedBody?.data.id === body.data.id && !this.deps.focus.aim) {
36055
- this.deps.callbacks.onFocusChange(body);
36467
+ if ((this.subjectBody ?? this.focusedBody)?.data.id === body.data.id && !this.deps.focus.aim) {
36468
+ this.deps.callbacks.onFocusChange(body, this.focusedBody);
36469
+ return;
36470
+ }
36471
+ const landed = this.landedSite(body);
36472
+ if (landed) {
36473
+ this.panToSurfacePoint(landed.host, landed.site, void 0, body);
36056
36474
  return;
36057
36475
  }
36058
36476
  this.setFocusTarget(body);
@@ -36062,36 +36480,28 @@ var FocusController = class {
36062
36480
  const { latitude, longitude, distance } = cartesianToSpherical(camWorld, pos, this.focusedBodyQuat(body));
36063
36481
  this.deps.callbacks.onCameraPosition?.(latitude, longitude, distance);
36064
36482
  }
36065
- setFocusTarget(body, camPos) {
36483
+ setFocusTarget(body, camPos, subject) {
36066
36484
  const { ctx, bodyObjects, callbacks, focus, camera, systemData, loadTexture } = this.deps;
36067
36485
  this.focusSets++;
36068
- const drawsItself = !isSurfaceFeature(body) && !isExtrasolar(body) && !isExoplanet(body);
36069
- if (!isSurfaceFeature(body)) this.promotion.ensureBodyObjects(body);
36486
+ this.subjectBody = subject;
36487
+ this.subjectSite = (subject && this.landedSite(subject)?.site) ?? null;
36488
+ this.placedOffSubject = false;
36489
+ this.onGround = false;
36490
+ const drawsItself = !isExtrasolar(body) && !isExoplanet(body);
36491
+ this.promotion.ensureBodyObjects(body);
36070
36492
  const prev = this.focusedBody;
36071
- const prevNomBodyId = nomenclatureBodyId(prev, bodyObjects);
36072
- const newNomBodyId = nomenclatureBodyId(body, bodyObjects);
36073
- const prevModelId = prev && (isSurfaceFeature(prev) ? prev.featureAnchor.hostId : prev.data.id);
36074
- const newModelId = isSurfaceFeature(body) ? body.featureAnchor.hostId : body.data.id;
36493
+ const prevId = prev?.data.id;
36075
36494
  const newTargetIds = this.syncUpgradeTargets(body);
36076
- if (prevModelId && prevModelId !== newModelId && !newTargetIds.has(prevModelId)) {
36077
- const prevBo = bodyObjects.get(prevModelId);
36078
- if (prevBo) unloadBodyModel(prevBo);
36079
- }
36080
- if (prevNomBodyId && prevNomBodyId !== newNomBodyId) {
36081
- const prevNomBo = bodyObjects.get(prevNomBodyId);
36082
- if (prevNomBo) disposeNomenclatureLabels(prevNomBo);
36495
+ if (prevId && prevId !== body.data.id) {
36496
+ const prevBo = bodyObjects.get(prevId);
36497
+ if (prevBo && !newTargetIds.has(prevId)) unloadBodyModel(prevBo);
36498
+ if (prevBo) disposeNomenclatureLabels(prevBo);
36083
36499
  }
36084
36500
  if (body.data.id !== prev?.data.id) this.pendingInitialView = null;
36085
36501
  this.focusedBody = body;
36086
- if (!isSurfaceFeature(body)) ctx.bodies.focusFeature = null;
36087
- const featureHost = isSurfaceFeature(body) ? ctx.getBody(body.featureAnchor.hostId) : void 0;
36088
- ctx.visibility.setFocused(featureHost ?? body);
36089
- callbacks.onFocusChange(body);
36502
+ ctx.visibility.setFocused(body, subject);
36503
+ callbacks.onFocusChange(subject ?? body, body);
36090
36504
  if (drawsItself) loadTexture(body);
36091
- if (newNomBodyId && newNomBodyId !== body.data.id) {
36092
- const landingBody = ctx.getBody(newNomBodyId);
36093
- if (landingBody) loadTexture(landingBody);
36094
- }
36095
36505
  systemData.syncToFocus();
36096
36506
  const pos = this.place(body);
36097
36507
  if (pos) {
@@ -36109,6 +36519,19 @@ var FocusController = class {
36109
36519
  this.setFocusTarget(body);
36110
36520
  return 0;
36111
36521
  }
36522
+ const landed = this.landedSite(body);
36523
+ if (landed) {
36524
+ const { host, site } = landed;
36525
+ if (zoom === void 0 || underHalfRadius(host, zoom)) return this.panToSurfacePoint(host, site, zoom, body);
36526
+ const view = {
36527
+ latitude: latitude ?? site.latitude,
36528
+ longitude: longitude ?? site.longitude
36529
+ };
36530
+ return this.flyOntoGround(host, {
36531
+ ...view,
36532
+ zoom
36533
+ }, body);
36534
+ }
36112
36535
  let camPos;
36113
36536
  if (zoom !== void 0) {
36114
36537
  if (latitude !== void 0 && longitude !== void 0) camPos = sphericalToCartesian(pos, latitude, longitude, zoom, this.focusedBodyQuat(body));
@@ -36127,7 +36550,7 @@ var FocusController = class {
36127
36550
  callbacks.onCameraPosition?.(spherical.latitude, spherical.longitude, spherical.distance);
36128
36551
  if (zoom !== void 0 && camPos) {
36129
36552
  this.pendingInitialView = null;
36130
- if (this.focusedBody?.data.id === id && f64dist(focus.focusTruePos, pos) < minCameraDistance(body)) {
36553
+ if (this.focusedBody?.data.id === id && !this.subjectBody && !this.onGround && f64dist(focus.focusTruePos, pos) < minCameraDistance(body)) {
36131
36554
  focus.focusTruePos = [...pos];
36132
36555
  this.deps.repositionAll();
36133
36556
  pointClouds.rebuildBasis();
@@ -36143,9 +36566,55 @@ var FocusController = class {
36143
36566
  const body = this.deps.ctx.getBody(id);
36144
36567
  const pos = body && this.place(body);
36145
36568
  if (!body || !pos) return;
36569
+ const landed = this.landedSite(body);
36570
+ if (landed) {
36571
+ const { host, site } = landed;
36572
+ if (underHalfRadius(host, zoom)) this.arriveAbove(host, site, zoom, body, true, look);
36573
+ else this.snapOntoGround(host, {
36574
+ latitude,
36575
+ longitude,
36576
+ zoom,
36577
+ look
36578
+ }, body);
36579
+ return;
36580
+ }
36146
36581
  this.settleOnBodyInstant(body, pos);
36147
36582
  this.snapToBodyFrame(latitude, longitude, zoom, look);
36148
36583
  }
36584
+ arriveAbove(host, site, height, subject, instant = false, look = LEVEL_LOOK) {
36585
+ const view = {
36586
+ latitude: site.latitude,
36587
+ longitude: site.longitude,
36588
+ zoom: site.radius + height
36589
+ };
36590
+ if (!instant) return this.flyOntoGround(host, view, subject);
36591
+ this.snapOntoGround(host, {
36592
+ ...view,
36593
+ look
36594
+ }, subject);
36595
+ this.placedOffSubject = true;
36596
+ return 0;
36597
+ }
36598
+ flyOntoGround(host, view, subject) {
36599
+ const { focus, callbacks } = this.deps;
36600
+ const hostPos = this.place(host);
36601
+ if (!hostPos) return 0;
36602
+ const camPos = sphericalToCartesian([...hostPos], view.latitude, view.longitude, view.zoom, siteQuat(host, this.deps.clock.jd));
36603
+ callbacks.onCameraPosition?.(view.latitude, view.longitude, view.zoom);
36604
+ this.pendingInitialView = null;
36605
+ this.setFocusTarget(host, camPos, subject);
36606
+ focus.orbitFly = true;
36607
+ this.onGround = true;
36608
+ this.placedOffSubject = true;
36609
+ return focus.focusDurationMs;
36610
+ }
36611
+ snapOntoGround(host, view, subject) {
36612
+ const pos = this.place(host);
36613
+ if (!pos) return;
36614
+ this.settleOnBodyInstant(host, pos, subject);
36615
+ this.snapToBodyFrame(view.latitude, view.longitude, view.zoom, view.look);
36616
+ this.onGround = true;
36617
+ }
36149
36618
  snapToBodyFacing(id, towardId, elevationDeg, distance) {
36150
36619
  const { ctx, camera, controls, callbacks } = this.deps;
36151
36620
  const body = ctx.getBody(id);
@@ -36190,9 +36659,9 @@ var FocusController = class {
36190
36659
  callbacks.onCameraPosition?.(settled.latitude, settled.longitude, settled.distance);
36191
36660
  this.pendingInitialView = null;
36192
36661
  }
36193
- settleOnBodyInstant(body, pos) {
36662
+ settleOnBodyInstant(body, pos, subject) {
36194
36663
  const { focus, pointClouds } = this.deps;
36195
- this.setFocusTarget(body);
36664
+ this.setFocusTarget(body, void 0, subject);
36196
36665
  focus.focusTruePos = [...pos];
36197
36666
  focus.focusOriginWorld = [...pos];
36198
36667
  focus.focusTargetWorld = [...pos];
@@ -36203,6 +36672,10 @@ var FocusController = class {
36203
36672
  focus.orbitFly = false;
36204
36673
  focus.arcOrbit = false;
36205
36674
  focus.cameraStaysOnBody = false;
36675
+ focus.aim = null;
36676
+ focus.aimLooks = false;
36677
+ focus.aimPose = null;
36678
+ focus.leavesAim = false;
36206
36679
  this.deps.repositionAll();
36207
36680
  pointClouds.rebuildBasis();
36208
36681
  }
@@ -36272,10 +36745,6 @@ function framesInBodyAxes(b) {
36272
36745
  }
36273
36746
  function upgradeTargets(focus, ctx) {
36274
36747
  if (!focus) return [];
36275
- if (isSurfaceFeature(focus)) {
36276
- const host = ctx.getBody(focus.featureAnchor.hostId);
36277
- return host && isMeshUpgradable(host) ? [host] : [];
36278
- }
36279
36748
  const out = [];
36280
36749
  if (isMeshUpgradable(focus)) out.push(focus);
36281
36750
  if (focus.data.objectType === ObjectType.MOON || focus.data.orbitalSource === OrbitalSource.SPICE_PROBE) {
@@ -36736,7 +37205,7 @@ var SPHERE_GROUND = {
36736
37205
  radiusKm: () => null
36737
37206
  };
36738
37207
  var _toCamera = new Vector3();
36739
- function cameraMotionScale(camera, focusTruePos, focused, parent, seated, position = camera.position, ground = SPHERE_GROUND) {
37208
+ function cameraMotionScale(camera, focusTruePos, focused, parent, position = camera.position, ground = SPHERE_GROUND) {
36740
37209
  const orbitRadius = position.length();
36741
37210
  if (!(orbitRadius > 0)) return {
36742
37211
  rotate: 1,
@@ -36747,7 +37216,7 @@ function cameraMotionScale(camera, focusTruePos, focused, parent, seated, positi
36747
37216
  const floor = focusRadius > 0 ? kmToScene(ground.clearanceKm(sized)) : 0;
36748
37217
  let clearance = orbitRadius;
36749
37218
  let height = Math.max(orbitRadius - focusRadius, floor);
36750
- for (const body of [sized, seated ? void 0 : parent]) {
37219
+ for (const body of [sized, parent]) {
36751
37220
  const at = body?.position;
36752
37221
  if (!body || !at) continue;
36753
37222
  if (!(effectiveRadiusKm(body.data) > 0)) continue;
@@ -36822,7 +37291,7 @@ var TIGHT_FAR_RELEASE = 2.3;
36822
37291
  var SUN_PROXY_FAR_FRACTION = .9;
36823
37292
  var _sunLiftDir = new Vector3();
36824
37293
  var _lookDir = new Vector3();
36825
- var _lookUp = new Vector3();
37294
+ var _groundDir = new Vector3();
36826
37295
  var SPIN_FOLLOW_NONE = 2;
36827
37296
  var _spinNow = new Quaternion();
36828
37297
  var _spinStep = new Quaternion();
@@ -36985,7 +37454,6 @@ var SceneRenderer = class SceneRenderer {
36985
37454
  };
36986
37455
  viewportH = 1;
36987
37456
  tier = currentRenderTier();
36988
- landedNomBodyId = null;
36989
37457
  rafId = 0;
36990
37458
  paused = false;
36991
37459
  covered = false;
@@ -37091,7 +37559,7 @@ var SceneRenderer = class SceneRenderer {
37091
37559
  const dust = new StarDust(this.renderer, dustPlan(), this.invalidate);
37092
37560
  this.starfield = new StarfieldSystem(ctx, this.scene, starBudget(), this.invalidate, this.pointClouds.pickRegistry, dust, new GlowSky(this.renderer, this.scene, glowPlan(), dust, this.invalidate, () => this.skyboxSource(), new StarlessSky(loadStarlessSky, this.invalidate)));
37093
37561
  this.constellations = new ConstellationSystem(ctx, this.scene, SceneRenderer.MAP_LAYER, this.invalidate, canvas, (slug, figure, name) => this.callbacks.onConstellationSelect?.(slug, figure, name), (direction, instant) => this.aimAt(direction, instant), () => this.focus.camOriginWorld !== null);
37094
- this.controls = new LookOrbitControls(this.camera, canvas, this.lookSurfaceRadius, this.standingHeight, () => this.pendingUrlWrite = true);
37562
+ this.controls = new LookOrbitControls(this.camera, canvas, this.lookSurfaceRadius, () => this.pendingUrlWrite = true);
37095
37563
  this.controls.enableDamping = true;
37096
37564
  this.orbitDampingFactor = orbitDamping(sceneSettings().resolvedReducedMotion);
37097
37565
  Object.defineProperty(this.controls, "minDistance", { get: () => this.minCameraDistance() });
@@ -37112,7 +37580,7 @@ var SceneRenderer = class SceneRenderer {
37112
37580
  }
37113
37581
  this.invalidate();
37114
37582
  });
37115
- this.cameraUp = new CameraUpController(this.camera, this.controls, ctx, (id) => this.isLanded(id));
37583
+ this.cameraUp = new CameraUpController(this.camera, this.controls, ctx);
37116
37584
  this.systemData = new SystemDataLoader(this.scene, ctx, this.renderer, this.textureLoader, this.bodyObjects, clock, () => {
37117
37585
  this.focusController.reapplyInitialViewIfPending();
37118
37586
  this.invalidate();
@@ -37165,7 +37633,7 @@ var SceneRenderer = class SceneRenderer {
37165
37633
  ctx.visibility.updateCamera(framing.zoom, this.clock.jd);
37166
37634
  this.viewOpen = !ctx.loading;
37167
37635
  this.updateSystemShare(0);
37168
- callbacks.onFocusChange(focusBody);
37636
+ callbacks.onFocusChange(focusBody, focusBody);
37169
37637
  this.buildScene();
37170
37638
  if (focusBody) this.focusController.promotion.ensureBodyObjects(focusBody);
37171
37639
  if (focusBody) this.focusController.syncUpgradeTargets(focusBody);
@@ -37176,6 +37644,7 @@ var SceneRenderer = class SceneRenderer {
37176
37644
  this.gpuPick = new GpuPickPass(this.renderer, this.camera);
37177
37645
  this.pointerInteraction = new PointerInteraction(canvas, this.camera, ctx, clock, this.focus, this.clickables, this.meshToBody, this.pickBody, (ndcX, ndcY) => this.pickFocusedModel(ndcX, ndcY), this.gpuPick, () => [...this.pointClouds.asteroids().values(), ...this.pointClouds.spacecraft().values()], this.pointClouds.pickRegistry, () => this.starSelectEnabled && this.bodySelectEnabled && this.ctx.visibility.planetarySystemShare() === 0 ? this.starfield.pickDraws() : [], (pickId) => this.pickStar(pickId));
37178
37646
  this.pointerInteraction.attach();
37647
+ if (focusBody && this.focusController.landedSite(focusBody)) this.focusController.snapToBody(focusBody.data.id, framing.latitude, framing.longitude, framing.zoom, framing.look);
37179
37648
  this.tick();
37180
37649
  }
37181
37650
  buildScene() {
@@ -37450,6 +37919,9 @@ var SceneRenderer = class SceneRenderer {
37450
37919
  at[1] + offset[1],
37451
37920
  at[2] + offset[2]
37452
37921
  ];
37922
+ const host = this.focusController.landedSite(body, this.sceneJd)?.host;
37923
+ const hostAt = host && this.whereBody(host);
37924
+ if (host && hostAt) return cartesianToSpherical(camera, hostAt, siteQuat(host, this.sceneJd));
37453
37925
  return cartesianToSpherical(camera, at, this.focusController.focusedBodyQuat(body));
37454
37926
  }
37455
37927
  where(id) {
@@ -37496,7 +37968,7 @@ var SceneRenderer = class SceneRenderer {
37496
37968
  this.controls.target.set(0, 0, 0);
37497
37969
  this.controls.update();
37498
37970
  }
37499
- this.coverageWatch?.sync(this.focusController.current, this.clock.jd);
37971
+ this.coverageWatch?.sync(this.focusController.subject ?? this.focusController.current, this.clock.jd);
37500
37972
  this.clock.tick(performance.now());
37501
37973
  if (!this.shouldRender(nowMs)) {
37502
37974
  this.controls.update();
@@ -37522,13 +37994,14 @@ var SceneRenderer = class SceneRenderer {
37522
37994
  if (!this.dateHeld() && !this.seekAwaitsData) this.clock.placed(this.seenJumps);
37523
37995
  this.refreshTravelFocus();
37524
37996
  this.callbacks.onFrame?.(this.sceneJd, renderedDtMs);
37997
+ this.followLanding(nowMs);
37525
37998
  this.followSpin(nowMs);
37526
37999
  const controlsSettled = this.holdingCamera ? this.applyHeldCamera() : stepFocusAnimation(this.focus, this.camera, this.controls, () => this.repositionAll(), () => this.pointClouds.rebuildBasis());
37527
38000
  this.applyReaderLimits(nowMs);
37528
38001
  const near = this.nearSurfaces();
37529
38002
  if (near) {
37530
38003
  this.clampToSurfaces(near);
37531
- this.setMotionScale(cameraMotionScale(this.camera, this.focus.focusTruePos, near.focused, near.parent, near.seated, this.controls.orbitPosition, this.motionGround));
38004
+ this.setMotionScale(cameraMotionScale(this.camera, this.focus.focusTruePos, near.focused, near.parent, this.controls.orbitPosition, this.motionGround));
37532
38005
  } else this.setMotionScale({
37533
38006
  rotate: 1,
37534
38007
  translate: 1
@@ -37587,7 +38060,7 @@ var SceneRenderer = class SceneRenderer {
37587
38060
  }
37588
38061
  for (const light of this.ambientLights) light.intensity = ambient;
37589
38062
  updateUserLocationOcclusion(this.userLocationMarker, this.bodyObjects, this.camera);
37590
- const focusedIdLod = nomenclatureBodyId(this.focusController.current, this.bodyObjects);
38063
+ const focusedIdLod = this.focusController.current?.data.id;
37591
38064
  updateTextureLOD(this.bodyObjects, this.camera, this.renderer, this.ctx, this.textureLoader, focusedIdLod, this.sceneJd, this.appearances, this.invalidate);
37592
38065
  updateSphereLOD(this.bodyObjects, this.camera, this.renderer, this.ctx, focusedIdLod, this.invalidate);
37593
38066
  updateSunShadowLight(this.bodyObjects, this.focus.focusTruePos, this.ctx, this.shadowLight, starPointLight, distance, this._tmpV3, sceneSettings().realisticLighting && this.starLight.starId === "naif-10", this.sunIntensityScale);
@@ -37644,6 +38117,7 @@ var SceneRenderer = class SceneRenderer {
37644
38117
  if (!body) return POINT_MIN_DISTANCE;
37645
38118
  const bo = this.bodyObjects.get(body.data.id);
37646
38119
  const lowest = bo && (modelMinRadiusKm(bo) ?? reliefRadiiKm(bo)?.min ?? smallestRadiusKm(bo));
38120
+ if (this.focusController.groundView) return kmToScene((lowest ?? effectiveRadiusKm(body.data)) + LANDED_KEEP_AWAY_KM);
37647
38121
  return minCameraDistance(body, lowest, bo && finestTexelKm(bo));
37648
38122
  }
37649
38123
  clearanceKm(body) {
@@ -37651,7 +38125,7 @@ var SceneRenderer = class SceneRenderer {
37651
38125
  return surfaceClearanceKm(body, bo && finestTexelKm(bo));
37652
38126
  }
37653
38127
  followSpin(nowMs) {
37654
- const body = this.spinningBody();
38128
+ const body = this.focusController.current;
37655
38129
  const flying = nowMs - this.focus.focusStartTime < this.focus.focusDurationMs;
37656
38130
  if (!body?.orientation || flying || this.holdingCamera || this.focus.aim) {
37657
38131
  this.spinFrom = null;
@@ -37665,16 +38139,26 @@ var SceneRenderer = class SceneRenderer {
37665
38139
  };
37666
38140
  if (!from || from.id !== body.data.id) return;
37667
38141
  const radiusKm = effectiveRadiusKm(body.data);
37668
- const height = sceneToKm(this.controls.orbitPosition.length()) / radiusKm - (body === this.focusController.current ? 1 : 0);
38142
+ const height = sceneToKm(this.controls.orbitPosition.length()) / radiusKm - 1;
37669
38143
  const t = MathUtils.clamp((SPIN_FOLLOW_NONE - height) / 1.5, 0, 1);
37670
38144
  const share = t * t * (3 - 2 * t);
37671
38145
  if (share === 0) return;
37672
38146
  _spinStep.copy(_spinNow).multiply(from.quat.invert());
37673
38147
  this.controls.carry(_noTurn.identity().slerp(_spinStep, share));
37674
38148
  }
37675
- spinningBody() {
37676
- const focused = this.focusController.current;
37677
- return focused && (this.standingOn(focused) ?? focused);
38149
+ followLanding(nowMs) {
38150
+ if (nowMs - this.focus.focusStartTime < this.focus.focusDurationMs || this.holdingCamera || this.dateHeld()) return;
38151
+ const fc = this.focusController;
38152
+ const subject = fc.subject;
38153
+ if (subject) {
38154
+ const onGround = fc.onGroundAt(subject);
38155
+ if (onGround === false) fc.leaveGround();
38156
+ else if (onGround) fc.followSubject(this.sceneJd);
38157
+ return;
38158
+ }
38159
+ const current = fc.current;
38160
+ const landed = current && fc.landedSite(current);
38161
+ if (landed) fc.panToSurfacePoint(landed.host, landed.site, void 0, current);
37678
38162
  }
37679
38163
  settleCamera() {
37680
38164
  this.settleLook();
@@ -37692,22 +38176,23 @@ var SceneRenderer = class SceneRenderer {
37692
38176
  return {
37693
38177
  focused,
37694
38178
  parent: parentId ? this.ctx.getBody(parentId) : void 0,
37695
- seated: Boolean(this.bodyObjects.get(focused.data.id)?.isLanded) || isSurfaceFeature(focused)
38179
+ onGround: this.focusController.groundView
37696
38180
  };
37697
38181
  }
37698
- clampToSurfaces({ focused, parent, seated }) {
38182
+ clampToSurfaces({ focused, parent, onGround }) {
37699
38183
  const { camera } = this;
37700
38184
  const at = this.focus.focusTruePos;
38185
+ const own = parent?.data.id === focused.data.id;
37701
38186
  if (parent) {
37702
- const surface = this.surfaceClamp(parent, seated);
38187
+ const surface = this.surfaceClamp(parent, own && onGround);
37703
38188
  clampCameraOutsideBody(camera, parent, at, surface, this.clearanceKm(parent));
37704
38189
  }
37705
- if (parent?.data.id === focused.data.id) return;
37706
- const surface = this.surfaceClamp(focused, false);
38190
+ if (own) return;
38191
+ const surface = this.surfaceClamp(focused, onGround);
37707
38192
  if (surface) clampCameraOutsideBody(camera, focused, at, surface, this.clearanceKm(focused));
37708
38193
  }
37709
38194
  motionGround = {
37710
- clearanceKm: (body) => this.clearanceKm(body),
38195
+ clearanceKm: (body) => this.focusController.groundView && body === this.focusController.current ? LANDED_KEEP_AWAY_KM : this.clearanceKm(body),
37711
38196
  radiusKm: (body, direction) => {
37712
38197
  const surface = this.surfaceClamp(body, false);
37713
38198
  if (!surface) return null;
@@ -37783,20 +38268,10 @@ var SceneRenderer = class SceneRenderer {
37783
38268
  }
37784
38269
  updateTightFar(distance) {
37785
38270
  if (isReversedDepth()) return;
37786
- const focused = this.focusController.current;
37787
- const anchorId = focused ? nomenclatureBodyId(focused, this.bodyObjects) : void 0;
37788
- const anchorBo = anchorId ? this.bodyObjects.get(anchorId) : void 0;
37789
- const radius = anchorBo?.radiusScene ?? 0;
37790
- let dist = distance;
37791
- const anchorAt = anchorBo?.body.position;
37792
- if (focused && anchorAt && anchorId !== focused.data.id) {
37793
- const [fx, fy, fz] = this.focus.focusTruePos;
37794
- const [bx, by, bz] = anchorAt;
37795
- const cam = this.camera.position;
37796
- dist = Math.hypot(fx + cam.x - bx, fy + cam.y - by, fz + cam.z - bz);
37797
- }
38271
+ const id = this.focusController.current?.data.id;
38272
+ const radius = id && this.bodyObjects.get(id)?.radiusScene || 0;
37798
38273
  const bound = this.tightFar ? TIGHT_FAR_RELEASE : TIGHT_FAR_ENGAGE;
37799
- this.tightFar = radius > 0 && this.ctx.visibility.activeSystemId !== null && dist < radius * bound;
38274
+ this.tightFar = radius > 0 && this.ctx.visibility.activeSystemId !== null && distance < radius * bound;
37800
38275
  }
37801
38276
  updateDepthFar() {
37802
38277
  if (isReversedDepth()) return;
@@ -38229,34 +38704,23 @@ var SceneRenderer = class SceneRenderer {
38229
38704
  const cam = this.camera;
38230
38705
  return !(cam.position.distanceToSquared(pos) <= cam.position.lengthSq() * 1e-10 && 8 * (1 - Math.abs(cam.quaternion.dot(quat))) <= 1e-8);
38231
38706
  }
38707
+ groundUnderCamera() {
38708
+ const body = this.focusController?.current;
38709
+ if (!body) return null;
38710
+ const down = _groundDir.copy(this.controls.orbitPosition).normalize();
38711
+ return {
38712
+ groundKm: this.motionGround.radiusKm(body, down) ?? effectiveRadiusKm(body.data),
38713
+ clearanceKm: this.motionGround.clearanceKm(body)
38714
+ };
38715
+ }
38232
38716
  lookSurfaceRadius = (direction) => {
38233
38717
  const focused = this.focusController.current;
38234
38718
  if (this.holdingCamera || this.focus.aim || !focused) return null;
38235
- if (this.standingOn(focused)) return 0;
38236
38719
  const bo = this.bodyObjects.get(focused.data.id);
38237
38720
  if (isModelBearing(focused) || !(bo?.mesh || hasModelSurface(bo))) return null;
38238
38721
  const radiusKm = effectiveRadiusKm(focused.data);
38239
38722
  return kmToScene(this.motionGround.radiusKm(focused, direction) ?? radiusKm);
38240
38723
  };
38241
- standingOn(focused) {
38242
- if (isSurfaceFeature(focused)) return this.ctx.getBody(focused.featureAnchor.hostId);
38243
- if (!this.bodyObjects.get(focused.data.id)?.isLanded) return void 0;
38244
- const parentId = collisionParentId(focused.data.parentId);
38245
- return parentId ? this.ctx.getBody(parentId) : void 0;
38246
- }
38247
- standingHeight = () => {
38248
- const focused = this.focusController.current;
38249
- const host = focused && this.standingOn(focused);
38250
- const at = host?.position;
38251
- if (!host || !at) return null;
38252
- const fp = this.focus.focusTruePos;
38253
- const cam = this.controls.orbitPosition;
38254
- const up = _lookUp.set(cam.x + fp[0] - at[0], cam.y + fp[1] - at[1], cam.z + fp[2] - at[2]);
38255
- const dist = up.length();
38256
- const groundKm = this.motionGround.radiusKm(host, up.divideScalar(dist));
38257
- const ground = kmToScene(groundKm ?? effectiveRadiusKm(host.data));
38258
- return ground > 0 ? (dist - ground) / ground : null;
38259
- };
38260
38724
  onControlsEnd = () => {
38261
38725
  this.pendingUrlWrite = true;
38262
38726
  this.focusController.clearPendingInitialView();
@@ -38303,26 +38767,17 @@ var SceneRenderer = class SceneRenderer {
38303
38767
  }
38304
38768
  setSelectedFeature(featureId) {
38305
38769
  this.selectedFeatureId = featureId;
38306
- const nomId = nomenclatureBodyId(this.focusController.current, this.bodyObjects);
38770
+ const nomId = this.focusController.current?.data.id;
38307
38771
  const bo = nomId ? this.bodyObjects.get(nomId) : void 0;
38308
38772
  if (bo) setActiveFeatureLabel(bo, featureId);
38309
38773
  }
38310
- syncLandedNomenclature() {
38311
- const focused = this.focusController.current;
38312
- const landingId = (focused ? this.bodyObjects.get(focused.data.id) : void 0)?.isLanded ? focused.data.parentId : null;
38313
- if (landingId === this.landedNomBodyId) return;
38314
- this.landedNomBodyId = landingId;
38315
- if (!landingId) return;
38316
- const landingBody = this.ctx.getBody(landingId);
38317
- if (landingBody) this.maybeLoadTexture(landingBody);
38318
- }
38319
38774
  clearUserPromoted() {
38320
38775
  this.focusController.promotion.clearUserPromoted();
38321
38776
  }
38322
38777
  focusedSeatConfigKey() {
38323
- const focused = this.focusController.current;
38778
+ const focused = this.focusController.subject ?? this.focusController.current;
38324
38779
  if (!focused) return null;
38325
- if (focused.data.objectType !== ObjectType.SPACECRAFT && !isSurfaceFeature(focused)) return null;
38780
+ if (focused.data.objectType !== ObjectType.SPACECRAFT) return null;
38326
38781
  const parentId = collisionParentId(focused.data.parentId);
38327
38782
  const bo = parentId ? this.bodyObjects.get(parentId) : void 0;
38328
38783
  if (!bo) return null;
@@ -38436,7 +38891,6 @@ var SceneRenderer = class SceneRenderer {
38436
38891
  this.repositionBodies();
38437
38892
  this.pointClouds.maybeRebase();
38438
38893
  }
38439
- this.syncLandedNomenclature();
38440
38894
  this.seekAwaitsData = seeked && result.focusLoading;
38441
38895
  const losingPlace = result.focusUnplaced && !this.focusWasOutOfRange;
38442
38896
  this.focusWasOutOfRange = result.focusUnplaced;
@@ -38454,83 +38908,32 @@ var SceneRenderer = class SceneRenderer {
38454
38908
  this.focusWasOutOfRange = false;
38455
38909
  return this.focusController.focusOnBody(id, zoom, latitude, longitude);
38456
38910
  }
38457
- focusOnFeature(anchor, name, zoom, mode = "frame", view) {
38911
+ focusOnFeature(anchor, zoom, move = "turn", view = null, look = LEVEL_LOOK) {
38458
38912
  this.disarmLimits();
38459
38913
  this.applyJdUpdate();
38460
38914
  this.focusWasOutOfRange = false;
38461
38915
  const host = this.ctx.getBody(anchor.hostId);
38462
38916
  const hostPos = host && this.whereBody(host);
38463
38917
  if (!host || !hostPos) return 0;
38464
- this.focusController.clearPendingInitialView();
38465
- const alreadyOnHost = nomenclatureBodyId(this.focusController.current, this.bodyObjects) === anchor.hostId;
38466
- const fb = makeFeatureBody(anchor, name);
38467
- this.ctx.bodies.focusFeature = fb;
38918
+ const fc = this.focusController;
38919
+ fc.clearPendingInitialView();
38468
38920
  const hostBo = this.bodyObjects.get(host.data.id);
38469
38921
  this.selectedFeatureId = anchor.featureId;
38470
38922
  if (hostBo) setActiveFeatureLabel(hostBo, anchor.featureId);
38471
- const seat = this.whereBody(fb);
38472
- if (!seat) return 0;
38473
- const quat = this.focusController.focusedBodyQuat(fb);
38474
- if (mode === "pan") {
38475
- const s = cartesianToSpherical(this.focusController.cameraTruePos(), seat, quat);
38476
- this.callbacks.onCameraPosition?.(s.latitude, s.longitude, s.distance);
38477
- this.focusController.setFocusTarget(fb);
38478
- return this.focus.focusDurationMs;
38479
- }
38480
- let zx = seat[0] - hostPos[0];
38481
- let zy = seat[1] - hostPos[1];
38482
- let zz = seat[2] - hostPos[2];
38483
- const len = Math.hypot(zx, zy, zz);
38484
- if (len > 1e-9) {
38485
- zx /= len;
38486
- zy /= len;
38487
- zz /= len;
38488
- } else {
38489
- zx = 0;
38490
- zy = 1;
38491
- zz = 0;
38492
- }
38493
- const camPos = mode === "snap" && view ? sphericalToCartesian(seat, view.latitude, view.longitude, view.zoom, quat) : [
38494
- seat[0] + zx * zoom,
38495
- seat[1] + zy * zoom,
38496
- seat[2] + zz * zoom
38497
- ];
38498
- const s = cartesianToSpherical(camPos, seat, quat);
38499
- this.callbacks.onCameraPosition?.(s.latitude, s.longitude, s.distance);
38500
- if (mode === "snap") {
38501
- this.focusController.setFocusTarget(fb, camPos);
38502
- const f = this.focus;
38503
- f.focusTruePos = [...seat];
38504
- f.focusOriginWorld = [...seat];
38505
- f.focusTargetWorld = [...seat];
38506
- f.focusStartTime = -FOCUS_DURATION_MS;
38507
- f.camOriginWorld = null;
38508
- f.camTargetWorld = null;
38509
- f.camTargetOffset = null;
38510
- f.camOriginOffset = null;
38511
- f.flyQ0 = null;
38512
- f.orbitFly = false;
38513
- f.arcOrbit = false;
38514
- f.cameraStaysOnBody = false;
38515
- this.repositionAll();
38516
- this.pointClouds.rebuildBasis();
38517
- this.camera.position.set(camPos[0] - seat[0], camPos[1] - seat[1], camPos[2] - seat[2]);
38518
- this.controls.update();
38519
- return 0;
38520
- }
38521
- if (alreadyOnHost) {
38522
- const camWorld = this.focusController.cameraTruePos();
38523
- this.focusController.setFocusTarget(fb);
38524
- this.focus.focusTruePos = [...seat];
38525
- this.camera.position.set(camWorld[0] - seat[0], camWorld[1] - seat[1], camWorld[2] - seat[2]);
38526
- this.repositionAll();
38527
- this.pointClouds.rebuildBasis();
38528
- prepareFlyToCamera(this.focus, this.camera, camWorld, camPos, sceneSettings().resolvedReducedMotion);
38529
- } else {
38530
- this.focusController.setFocusTarget(fb, camPos);
38531
- this.focus.orbitFly = true;
38923
+ const seat = featureSeat(anchor, host, hostPos, hostBo, this.sceneJd);
38924
+ const site = surfaceSite(host, seat, hostPos, this.sceneJd);
38925
+ if (move === "turn") return fc.panToSurfacePoint(host, site, zoom);
38926
+ if (move === "frame" || !view) return fc.arriveAbove(host, site, zoom, void 0, move === "snap", look);
38927
+ if (underHalfRadius(host, view.zoom)) {
38928
+ const offset = sphericalToCartesian([
38929
+ 0,
38930
+ 0,
38931
+ 0
38932
+ ], view.latitude, view.longitude, view.zoom);
38933
+ view = viewOverSite(site, new Vector3(...offset), siteQuat(host, this.sceneJd));
38532
38934
  }
38533
- return this.focus.focusDurationMs;
38935
+ fc.snapOntoGround(host, view);
38936
+ return 0;
38534
38937
  }
38535
38938
  pickFocusedModel(ndcX, ndcY) {
38536
38939
  if (!this.overlayActive) return null;
@@ -38551,13 +38954,12 @@ var SceneRenderer = class SceneRenderer {
38551
38954
  }
38552
38955
  overlayModelBo() {
38553
38956
  const withModel = (bo) => bo?.model && isDrawn(bo) ? bo : null;
38554
- const focusBody = this.focusController.current;
38957
+ const focusBody = this.focusController.subject ?? this.focusController.current;
38555
38958
  if (!focusBody) {
38556
38959
  const centerId = this.travelFocus?.centerId;
38557
38960
  return withModel(centerId ? this.bodyObjects.get(centerId) : void 0);
38558
38961
  }
38559
- const modelId = isSurfaceFeature(focusBody) ? focusBody.featureAnchor.hostId : focusBody.data.id;
38560
- const bo = this.bodyObjects.get(modelId);
38962
+ const bo = this.bodyObjects.get(focusBody.data.id);
38561
38963
  const own = withModel(bo);
38562
38964
  if (own) return own;
38563
38965
  if (focusBody.data.orbitalSource === OrbitalSource.SPICE_PROBE && !bo?.isLanded) {
@@ -41330,6 +41732,14 @@ var SpaceMap = class {
41330
41732
  set focusedBody(value) {
41331
41733
  set(this.#focusedBody, value);
41332
41734
  }
41735
+ #cameraBody = state();
41736
+ get cameraBody() {
41737
+ return get(this.#cameraBody);
41738
+ }
41739
+ set cameraBody(value) {
41740
+ set(this.#cameraBody, value);
41741
+ }
41742
+ movingToFeature = false;
41333
41743
  attribution = null;
41334
41744
  objects = new MapObjectCollection(() => this.requireRenderer().extensions, () => this.requireCanvas());
41335
41745
  dragRotate;
@@ -41426,9 +41836,7 @@ var SpaceMap = class {
41426
41836
  }
41427
41837
  getFocusedBody() {
41428
41838
  const focused = this.focusedBody;
41429
- if (!focused) return void 0;
41430
- const body = isSurfaceFeature(focused) ? this.ctx.getBody(focused.featureAnchor.hostId) : focused;
41431
- return body && bodyView(body);
41839
+ return focused && bodyView(focused);
41432
41840
  }
41433
41841
  getChildren(id) {
41434
41842
  return [...this.ctx.bodies.getChildren(id) ?? []];
@@ -41652,11 +42060,12 @@ var SpaceMap = class {
41652
42060
  notify: (notice) => this.emit("notice", notice),
41653
42061
  dismiss: (topic) => this.emit("noticedismiss", topic)
41654
42062
  },
41655
- onFocusChange: (body) => {
42063
+ onFocusChange: (body, orbited) => {
41656
42064
  const initial = this.initialFocusPending;
41657
42065
  this.initialFocusPending = false;
41658
42066
  this.focusedBody = body;
41659
- const feature = body !== void 0 && isSurfaceFeature(body);
42067
+ this.cameraBody = orbited;
42068
+ const feature = this.movingToFeature;
41660
42069
  this.emit("focuschange", {
41661
42070
  body: this.getFocusedBody(),
41662
42071
  initial,
@@ -41671,7 +42080,7 @@ var SpaceMap = class {
41671
42080
  },
41672
42081
  onIdle: () => this.emit("idle"),
41673
42082
  onCameraPosition: (latitude, longitude, zoom, look = LEVEL_LOOK) => {
41674
- const body = this.focusedBody?.data.id;
42083
+ const body = this.cameraBody?.data.id;
41675
42084
  if (body === void 0) return;
41676
42085
  this.emit("camera", {
41677
42086
  body,
@@ -41904,7 +42313,7 @@ var SpaceMap = class {
41904
42313
  });
41905
42314
  }
41906
42315
  panTo(target) {
41907
- return this.toFeature(this.targetBody(target), target.feature, "pan");
42316
+ return this.toFeature(this.targetBody(target), target.feature, "turn");
41908
42317
  }
41909
42318
  async jumpTo(target) {
41910
42319
  const body = this.targetBody(target);
@@ -41937,8 +42346,7 @@ var SpaceMap = class {
41937
42346
  }
41938
42347
  if (target.feature) {
41939
42348
  const { featureId, lat, lon, diameterM } = target.feature;
41940
- this.focusOnFeature(body, featureId, lat, lon, diameterM, null, "snap");
41941
- this.renderer?.setLook(lookOf(target));
42349
+ this.focusOnFeature(body, featureId, lat, lon, diameterM, "snap", null, lookOf(target));
41942
42350
  return;
41943
42351
  }
41944
42352
  const camera = this.getCamera();
@@ -41946,7 +42354,7 @@ var SpaceMap = class {
41946
42354
  }
41947
42355
  getCamera() {
41948
42356
  const renderer = this.renderer;
41949
- const body = this.focusedBody?.data.id;
42357
+ const body = this.cameraBody?.data.id;
41950
42358
  if (!renderer || body === void 0) return null;
41951
42359
  const { latitude, longitude, distance } = renderer.getCameraState();
41952
42360
  return {
@@ -41980,6 +42388,9 @@ var SpaceMap = class {
41980
42388
  getLimits() {
41981
42389
  return { ...this.limits };
41982
42390
  }
42391
+ getClosestApproach() {
42392
+ return this.renderer?.groundUnderCamera() ?? null;
42393
+ }
41983
42394
  applyLimits() {
41984
42395
  const l = this.limits;
41985
42396
  const band = {};
@@ -42019,9 +42430,9 @@ var SpaceMap = class {
42019
42430
  onTouchMove = (e) => {
42020
42431
  this.cooperative?.allowsTouchDrag(e.touches.length);
42021
42432
  };
42022
- toFeature(body, feature, mode) {
42433
+ toFeature(body, feature, move) {
42023
42434
  const { featureId, lat, lon, diameterM } = feature;
42024
- return this.settled(this.focusOnFeature(body, featureId, lat, lon, diameterM, null, mode));
42435
+ return this.settled(this.focusOnFeature(body, featureId, lat, lon, diameterM, move));
42025
42436
  }
42026
42437
  settled(ms) {
42027
42438
  return new Promise((resolve) => {
@@ -42071,7 +42482,7 @@ var SpaceMap = class {
42071
42482
  if (!renderer || !vantage || vantage.kept) return null;
42072
42483
  return (body) => renderer.viewFrom(body.data.id, vantage.offset) ?? {};
42073
42484
  }
42074
- focusOnFeature(bodyId, featureId, lat, lon, diameterM, name, mode = "frame", view = null) {
42485
+ focusOnFeature(bodyId, featureId, lat, lon, diameterM, move = "turn", view = null, look) {
42075
42486
  const hostBody = this.ctx.getBody(bodyId);
42076
42487
  if (!hostBody) return 0;
42077
42488
  const idealScene = kmToScene(featureDiameterM(diameterM, effectiveRadiusKm(hostBody.data)) * 4 / 1e3);
@@ -42085,16 +42496,20 @@ var SpaceMap = class {
42085
42496
  lon,
42086
42497
  diameterM
42087
42498
  };
42088
- return this.renderer?.focusOnFeature(anchor, name, zoom, mode, view) ?? 0;
42499
+ this.movingToFeature = true;
42500
+ try {
42501
+ return this.renderer?.focusOnFeature(anchor, zoom, move, view, look) ?? 0;
42502
+ } finally {
42503
+ this.movingToFeature = false;
42504
+ }
42089
42505
  }
42090
42506
  setFocusTarget(body, camPos) {
42091
42507
  this.renderer?.setFocusTarget(body, camPos);
42092
42508
  }
42093
42509
  getPose() {
42094
42510
  const renderer = this.renderer;
42095
- const focused = this.focusedBody;
42096
- if (!renderer || !focused) return null;
42097
- const body = focused.data.id;
42511
+ const body = this.cameraBody?.data.id;
42512
+ if (!renderer || body === void 0) return null;
42098
42513
  return {
42099
42514
  position: {
42100
42515
  body,