spacemap 0.1.22 → 0.1.24

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);
@@ -18063,12 +18063,6 @@ function onLinkTap(el, handler) {
18063
18063
  }
18064
18064
 
18065
18065
  // 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
18066
  var DEG2RAD$4 = Math.PI / 180;
18073
18067
  var MIN_BODY_SCREEN_RADIUS_PX = 128;
18074
18068
  var LABEL_DISC_FRACTION = .9;
@@ -22390,6 +22384,87 @@ var SimClock = class {
22390
22384
  }
22391
22385
  };
22392
22386
 
22387
+ // src/lib/scene/camera/two-finger.ts
22388
+ var TWO_FINGER_SLOP = 10;
22389
+ var TWO_FINGER_ALONE = 30;
22390
+ var TILT_MAX_SPREAD_SLOPE = Math.tan(MathUtils.degToRad(40));
22391
+ var TILT_MAX_SIDEWAYS = .5;
22392
+ var TILT_MIN_TOGETHER = .3;
22393
+ var TWIST_ENGAGE = MathUtils.degToRad(12);
22394
+ var TwoFingerGesture = class {
22395
+ mode = "map";
22396
+ a0 = {
22397
+ x: 0,
22398
+ y: 0
22399
+ };
22400
+ b0 = {
22401
+ x: 0,
22402
+ y: 0
22403
+ };
22404
+ lastMidY = 0;
22405
+ lastAngle = 0;
22406
+ twisted = 0;
22407
+ twisting = false;
22408
+ begin(a, b, canTilt) {
22409
+ this.mode = canTilt ? "undecided" : "map";
22410
+ this.a0.x = a.x;
22411
+ this.a0.y = a.y;
22412
+ this.b0.x = b.x;
22413
+ this.b0.y = b.y;
22414
+ this.lastMidY = (a.y + b.y) / 2;
22415
+ this.lastAngle = Math.atan2(b.y - a.y, b.x - a.x);
22416
+ this.twisted = 0;
22417
+ this.twisting = false;
22418
+ }
22419
+ move(a, b) {
22420
+ const angle = Math.atan2(b.y - a.y, b.x - a.x);
22421
+ const turn = MathUtils.euclideanModulo(angle - this.lastAngle + Math.PI, 2 * Math.PI) - Math.PI;
22422
+ this.lastAngle = angle;
22423
+ const midY = (a.y + b.y) / 2;
22424
+ let down = midY - this.lastMidY;
22425
+ this.lastMidY = midY;
22426
+ this.twisted += turn;
22427
+ if (this.mode === "undecided") {
22428
+ this.mode = this.decide(a, b);
22429
+ if (this.mode === "tilt") down = midY - (this.a0.y + this.b0.y) / 2;
22430
+ }
22431
+ switch (this.mode) {
22432
+ case "undecided": return {
22433
+ mode: "undecided",
22434
+ down: 0,
22435
+ twist: 0
22436
+ };
22437
+ case "tilt": return {
22438
+ mode: "tilt",
22439
+ down,
22440
+ twist: 0
22441
+ };
22442
+ case "map": {
22443
+ const engaged = this.twisting;
22444
+ this.twisting ||= Math.abs(this.twisted) > TWIST_ENGAGE;
22445
+ return {
22446
+ mode: "map",
22447
+ down: 0,
22448
+ twist: engaged ? turn : 0
22449
+ };
22450
+ }
22451
+ }
22452
+ }
22453
+ decide(a, b) {
22454
+ const ax = a.x - this.a0.x;
22455
+ const ay = a.y - this.a0.y;
22456
+ const bx = b.x - this.b0.x;
22457
+ const by = b.y - this.b0.y;
22458
+ const aTravel = Math.hypot(ax, ay);
22459
+ const bTravel = Math.hypot(bx, by);
22460
+ if (Math.max(aTravel, bTravel) < (aTravel > 0 && bTravel > 0 ? 10 : TWO_FINGER_ALONE)) return "undecided";
22461
+ const sideBySide = Math.abs(this.b0.y - this.a0.y) <= Math.abs(this.b0.x - this.a0.x) * TILT_MAX_SPREAD_SLOPE;
22462
+ const vertical = Math.abs(ax) <= Math.abs(ay) * TILT_MAX_SIDEWAYS && Math.abs(bx) <= Math.abs(by) * TILT_MAX_SIDEWAYS;
22463
+ const together = Math.sign(ay) === Math.sign(by) && Math.min(aTravel, bTravel) >= Math.max(aTravel, bTravel) * TILT_MIN_TOGETHER;
22464
+ return sideBySide && vertical && together ? "tilt" : "map";
22465
+ }
22466
+ };
22467
+
22393
22468
  // src/lib/scene/camera/look-controls.ts
22394
22469
  var MAX_TILT = MathUtils.degToRad(80);
22395
22470
  var LOOK_NONE_HEIGHT = 1;
@@ -22411,14 +22486,15 @@ var _turnBack = new Quaternion();
22411
22486
  var _was = new Quaternion();
22412
22487
  var _pivotGround = new Vector3();
22413
22488
  var _look = new Matrix4();
22489
+ var GLIDE_DONE = 1e-5;
22414
22490
  var ANCHOR_PASSES = 3;
22415
22491
  var SIN_MIN_ANCHOR_ELEVATION = Math.sin(MathUtils.degToRad(20));
22416
22492
  var GROUND_PASSES = 3;
22493
+ var PIVOT_REFINE = 1e-7;
22417
22494
  var orbitHandlers = OrbitControls.prototype;
22418
22495
  var LookOrbitControls = class extends OrbitControls {
22419
22496
  canvas;
22420
22497
  surfaceRadius;
22421
- standingHeight;
22422
22498
  onTurnSettled;
22423
22499
  heading = 0;
22424
22500
  tilt = 0;
@@ -22426,6 +22502,7 @@ var LookOrbitControls = class extends OrbitControls {
22426
22502
  orbitPos = new Vector3();
22427
22503
  orbitQuat = new Quaternion();
22428
22504
  pivotRadius = 0;
22505
+ placedByName = false;
22429
22506
  lastUp = new Vector3();
22430
22507
  holdsNorth = false;
22431
22508
  northTurning = false;
@@ -22436,16 +22513,22 @@ var LookOrbitControls = class extends OrbitControls {
22436
22513
  dragPointer = null;
22437
22514
  dragX = 0;
22438
22515
  dragY = 0;
22439
- constructor(camera, canvas, surfaceRadius, standingHeight = () => null, onTurnSettled = () => {}) {
22516
+ twoFinger = new TwoFingerGesture();
22517
+ glideHeading = 0;
22518
+ glideTilt = 0;
22519
+ constructor(camera, canvas, surfaceRadius, onTurnSettled = () => {}) {
22440
22520
  super(camera, canvas);
22441
22521
  this.canvas = canvas;
22442
22522
  this.surfaceRadius = surfaceRadius;
22443
- this.standingHeight = standingHeight;
22444
22523
  this.onTurnSettled = onTurnSettled;
22445
22524
  this.mouseButtons.MIDDLE = null;
22446
- this.addEventListener("start", () => this.pendingLook = null);
22525
+ this.addEventListener("start", () => {
22526
+ this.pendingLook = null;
22527
+ this.placedByName = false;
22528
+ });
22447
22529
  canvas.addEventListener("pointerdown", this.onLookDown);
22448
22530
  canvas.addEventListener("mousedown", this.blockMiddle);
22531
+ canvas.addEventListener("pointerdown", this.onCatch);
22449
22532
  }
22450
22533
  get orbitPosition() {
22451
22534
  return this.looking ? this.orbitPos : this.object.position;
@@ -22464,6 +22547,8 @@ var LookOrbitControls = class extends OrbitControls {
22464
22547
  const cam = this.object;
22465
22548
  this.looking = false;
22466
22549
  this.pendingLook = null;
22550
+ this.placedByName = false;
22551
+ this.stopGlide();
22467
22552
  if (tilt === 0 && heading === 0) return;
22468
22553
  if (!this.groundKnown()) {
22469
22554
  this.pendingLook = {
@@ -22484,6 +22569,7 @@ var LookOrbitControls = class extends OrbitControls {
22484
22569
  this.heading = heading;
22485
22570
  this.holdAngles(share);
22486
22571
  this.looking = true;
22572
+ this.placedByName = true;
22487
22573
  this.place();
22488
22574
  }
22489
22575
  toOrbitAxes(x, y) {
@@ -22501,6 +22587,12 @@ var LookOrbitControls = class extends OrbitControls {
22501
22587
  endLook() {
22502
22588
  this.looking = false;
22503
22589
  this.pendingLook = null;
22590
+ this.placedByName = false;
22591
+ this.stopGlide();
22592
+ }
22593
+ stopGlide() {
22594
+ this.glideHeading = 0;
22595
+ this.glideTilt = 0;
22504
22596
  }
22505
22597
  groundKnown() {
22506
22598
  _n.subVectors(this.orbitPosition, this.target).normalize();
@@ -22530,22 +22622,28 @@ var LookOrbitControls = class extends OrbitControls {
22530
22622
  this.placedQuat.copy(cam.quaternion);
22531
22623
  return;
22532
22624
  }
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
22625
  const holds = this.holdsNorth;
22544
22626
  _n.subVectors(this.orbitPos, this.target).normalize();
22545
22627
  this.lookToward(_pivotGround.copy(_n).multiplyScalar(this.pivotRadius));
22546
22628
  this.holdsNorth = holds;
22547
22629
  }
22548
22630
  update(deltaTime = null) {
22631
+ const changed = this.step(deltaTime);
22632
+ return this.glide() || changed;
22633
+ }
22634
+ glide() {
22635
+ if (!this.glideHeading && !this.glideTilt) return false;
22636
+ const f = this.dampingFactor;
22637
+ this.turnNow(this.glideHeading * f, this.glideTilt * f);
22638
+ this.glideHeading *= 1 - f;
22639
+ this.glideTilt *= 1 - f;
22640
+ if (Math.abs(this.glideHeading) < GLIDE_DONE && Math.abs(this.glideTilt) < GLIDE_DONE) {
22641
+ this.stopGlide();
22642
+ this.onTurnSettled();
22643
+ }
22644
+ return true;
22645
+ }
22646
+ step(deltaTime) {
22549
22647
  if (!this.looking) {
22550
22648
  const changed = super.update(deltaTime);
22551
22649
  if (this.pendingLook && this.groundKnown()) this.setLook(this.pendingLook);
@@ -22554,14 +22652,19 @@ var LookOrbitControls = class extends OrbitControls {
22554
22652
  const cam = this.object;
22555
22653
  if (!cam.position.equals(this.placedPos) || !cam.quaternion.equals(this.placedQuat)) {
22556
22654
  this.looking = false;
22655
+ this.stopGlide();
22557
22656
  return super.update(deltaTime);
22558
22657
  }
22658
+ if (!this.followGround()) return super.update(deltaTime);
22559
22659
  cam.position.copy(this.orbitPos);
22560
22660
  cam.quaternion.copy(this.orbitQuat);
22561
22661
  _was.copy(this.orbitQuat);
22562
22662
  const changed = super.update(deltaTime);
22563
22663
  const moved = cam.position.distanceToSquared(this.orbitPos) > this.orbitPos.lengthSq() * 1e-20;
22564
- if (moved) this.orbitMoved = true;
22664
+ if (moved) {
22665
+ this.orbitMoved = true;
22666
+ this.placedByName = false;
22667
+ }
22565
22668
  this.orbitPos.copy(cam.position);
22566
22669
  this.orbitQuat.copy(cam.quaternion);
22567
22670
  if (moved) this.samplePivot();
@@ -22586,6 +22689,7 @@ var LookOrbitControls = class extends OrbitControls {
22586
22689
  super.dispose();
22587
22690
  this.canvas.removeEventListener("pointerdown", this.onLookDown);
22588
22691
  this.canvas.removeEventListener("mousedown", this.blockMiddle);
22692
+ this.canvas.removeEventListener("pointerdown", this.onCatch);
22589
22693
  this.endLookDrag();
22590
22694
  }
22591
22695
  _handleMouseDownRotate(event) {
@@ -22600,6 +22704,28 @@ var LookOrbitControls = class extends OrbitControls {
22600
22704
  _handleTouchMoveRotate(event) {
22601
22705
  orbitHandlers._handleTouchMoveRotate.call(this, this.rotatedTouch(event));
22602
22706
  }
22707
+ _handleTouchStartDollyPan(event) {
22708
+ orbitHandlers._handleTouchStartDollyPan.call(this, event);
22709
+ const [a, b] = this.fingers();
22710
+ this.twoFinger.begin(a, b, this.enableRotate && this.lookShare() > 0);
22711
+ }
22712
+ _handleTouchMoveDollyPan(event) {
22713
+ const [a, b] = this.fingers();
22714
+ const step = this.twoFinger.move(a, b);
22715
+ if (step.mode === "undecided") return;
22716
+ const rate = TURN_PER_HEIGHT / (this.canvas.clientHeight || 1);
22717
+ if (step.mode === "tilt") {
22718
+ if (this.enableRotate) this.turn(0, -step.down * rate);
22719
+ return;
22720
+ }
22721
+ orbitHandlers._handleTouchMoveDollyPan.call(this, event);
22722
+ if (step.twist !== 0 && this.enableRotate) this.turn(step.twist, 0);
22723
+ }
22724
+ fingers() {
22725
+ const touch = this;
22726
+ const [a, b] = touch._pointers;
22727
+ return [touch._pointerPositions[a], touch._pointerPositions[b]];
22728
+ }
22603
22729
  _handleMouseWheel(event) {
22604
22730
  const anchored = this.enableRotate && this.groundAt(event.clientX, event.clientY, _anchor) && -_ray.dot(_hit.copy(_anchor).normalize()) > SIN_MIN_ANCHOR_ELEVATION;
22605
22731
  orbitHandlers._handleMouseWheel.call(this, event);
@@ -22664,6 +22790,7 @@ var LookOrbitControls = class extends OrbitControls {
22664
22790
  const level = offset.addScaledVector(n, -offset.dot(n));
22665
22791
  this.heading = Math.atan2(_ray.crossVectors(down, level).dot(n), down.dot(level));
22666
22792
  this.holdsNorth = false;
22793
+ this.placedByName = false;
22667
22794
  this.looking = true;
22668
22795
  if (!this.holdAngles(this.lookShare())) {
22669
22796
  this.looking = false;
@@ -22712,7 +22839,15 @@ var LookOrbitControls = class extends OrbitControls {
22712
22839
  pageY: 2 * y - other.y
22713
22840
  };
22714
22841
  }
22715
- turn(dx, dy) {
22842
+ turn(heading, tilt) {
22843
+ if (!this.enableDamping) {
22844
+ this.turnNow(heading, tilt);
22845
+ return;
22846
+ }
22847
+ this.glideHeading += heading;
22848
+ this.glideTilt += tilt;
22849
+ }
22850
+ turnNow(heading, tilt) {
22716
22851
  const cam = this.object;
22717
22852
  const share = this.lookShare();
22718
22853
  if (share === 0) return;
@@ -22726,9 +22861,8 @@ var LookOrbitControls = class extends OrbitControls {
22726
22861
  this.lastUp.copy(cam.up);
22727
22862
  }
22728
22863
  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);
22864
+ this.heading += heading;
22865
+ this.tilt = Math.max(0, this.tilt + tilt);
22732
22866
  this.holdAngles(share);
22733
22867
  this.place();
22734
22868
  }
@@ -22736,10 +22870,7 @@ var LookOrbitControls = class extends OrbitControls {
22736
22870
  _n.subVectors(this.orbitPosition, this.target);
22737
22871
  const range = _n.length();
22738
22872
  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);
22873
+ return ground ? this.shareAt((range - ground) / ground) : 0;
22743
22874
  }
22744
22875
  shareAt(height) {
22745
22876
  const t = MathUtils.clamp((1 - height) / .5, 0, 1);
@@ -22751,6 +22882,22 @@ var LookOrbitControls = class extends OrbitControls {
22751
22882
  this.heading = MathUtils.clamp(this.heading, -Math.PI * share, Math.PI * share);
22752
22883
  return share > 0;
22753
22884
  }
22885
+ followGround() {
22886
+ _n.subVectors(this.orbitPos, this.target);
22887
+ const range = _n.length();
22888
+ const ground = range > 0 ? this.surfaceRadius(_n.divideScalar(range)) : null;
22889
+ if (ground === null) return true;
22890
+ const pivot = Math.min(ground, range);
22891
+ if (Math.abs(pivot - this.pivotRadius) <= ground * PIVOT_REFINE) return true;
22892
+ if (this.placedByName) {
22893
+ this.pivotRadius = pivot;
22894
+ return true;
22895
+ }
22896
+ const holds = this.holdsNorth;
22897
+ this.lookToward(_pivotGround.copy(_n).multiplyScalar(ground));
22898
+ this.holdsNorth = holds;
22899
+ return this.looking;
22900
+ }
22754
22901
  samplePivot() {
22755
22902
  _n.subVectors(this.orbitPos, this.target);
22756
22903
  const range = _n.length();
@@ -22772,6 +22919,11 @@ var LookOrbitControls = class extends OrbitControls {
22772
22919
  this.placedPos.copy(cam.position);
22773
22920
  this.placedQuat.copy(cam.quaternion);
22774
22921
  }
22922
+ onCatch = () => {
22923
+ if (!this.glideHeading && !this.glideTilt) return;
22924
+ this.stopGlide();
22925
+ this.onTurnSettled();
22926
+ };
22775
22927
  blockMiddle = (event) => {
22776
22928
  if (event.button === 1) event.preventDefault();
22777
22929
  };
@@ -22796,7 +22948,8 @@ var LookOrbitControls = class extends OrbitControls {
22796
22948
  const dy = event.clientY - this.dragY;
22797
22949
  this.dragX = event.clientX;
22798
22950
  this.dragY = event.clientY;
22799
- if (dx !== 0 || dy !== 0) this.turn(dx, dy);
22951
+ const rate = TURN_PER_HEIGHT / (this.canvas.clientHeight || 1);
22952
+ if (dx !== 0 || dy !== 0) this.turn(-dx * rate, -dy * rate);
22800
22953
  };
22801
22954
  onLookUp = (event) => {
22802
22955
  if (event.pointerId !== this.dragPointer) return;
@@ -22815,35 +22968,42 @@ var LookOrbitControls = class extends OrbitControls {
22815
22968
  // src/lib/scene/framing.ts
22816
22969
  var DEFAULT_FOCUS_ID = EARTH_ID;
22817
22970
 
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
- };
22971
+ // src/lib/scene/position/feature-seat.ts
22972
+ var DEG2RAD$3 = Math.PI / 180;
22973
+ var _fixed = new Vector3();
22974
+ var _addend = new Vector3();
22975
+ function featureSeat(anchor, host, hostPos, hostBo, jd) {
22976
+ _addend.set(0, 0, 0);
22977
+ const idx = hostBo?.nomenclatureActiveIndex ?? -1;
22978
+ const label = idx >= 0 ? hostBo?.nomenclatureLabels?.[idx] ?? null : null;
22979
+ if (label && label.element.dataset.featureId === String(anchor.featureId)) {
22980
+ const modelAnchor = hostBo?.nomenclatureAnchor;
22981
+ if (modelAnchor) {
22982
+ _fixed.copy(label.position);
22983
+ _addend.copy(modelAnchor.position);
22984
+ } else {
22985
+ _fixed.copy(label.position);
22986
+ if (hostBo?.mesh) _fixed.multiply(hostBo.mesh.scale);
22987
+ }
22988
+ } else {
22989
+ const radiusKm = host.data.radiusKm;
22990
+ const latR = anchor.lat * DEG2RAD$3;
22991
+ const lngR = anchor.lon * DEG2RAD$3;
22992
+ const seat = host.orientation && Number.isFinite(radiusKm) && radiusKm > 0 ? renderedSeatAt(hostBo, host.data.id, radiusKm, latR, lngR) : null;
22993
+ if (seat) _fixed.set(seat.pointKm[0], seat.pointKm[1], seat.pointKm[2]);
22994
+ else {
22995
+ const r = Number.isFinite(radiusKm) && radiusKm > 0 ? radiusKm : 0;
22996
+ const [nx, ny, nz] = bodyFixedUnit(latR, lngR);
22997
+ _fixed.set(r * nx, r * ny, r * nz);
22998
+ }
22999
+ _fixed.multiplyScalar(kmToScene(1));
23000
+ }
23001
+ if (host.orientation) _fixed.applyQuaternion(bodyQuaternion(host.orientation, jd, host.nutPrec));
23002
+ return [
23003
+ hostPos[0] + _fixed.x + _addend.x,
23004
+ hostPos[1] + _fixed.y + _addend.y,
23005
+ hostPos[2] + _fixed.z + _addend.z
23006
+ ];
22847
23007
  }
22848
23008
 
22849
23009
  // src/lib/scene/label/throttled-renderer.ts
@@ -23045,6 +23205,7 @@ function makeTrailMaterial(color, brightness = TRAIL_DIM) {
23045
23205
  }
23046
23206
  var TRIM_DEPTH = CAMERA_NEAR / 2;
23047
23207
  var TRIM_SHARE = 1e-6;
23208
+ var MITER_LIMIT = 4;
23048
23209
  function makeFatTrailMaterial(color, lineWidth, brightness = TRAIL_DIM, dash) {
23049
23210
  return new ShaderMaterial({
23050
23211
  defines: dash ? { DASHED: "" } : {},
@@ -23070,9 +23231,11 @@ function makeFatTrailMaterial(color, lineWidth, brightness = TRAIL_DIM, dash) {
23070
23231
  uniform float uLineWidth;
23071
23232
  uniform vec2 uResolution;
23072
23233
  uniform vec2 uTrim;
23073
- attribute vec2 corner;
23234
+ attribute vec3 corner;
23235
+ attribute vec3 segPrev;
23074
23236
  attribute vec3 segStart;
23075
23237
  attribute vec3 segEnd;
23238
+ attribute vec3 segNext;
23076
23239
  attribute vec2 alphaStart;
23077
23240
  attribute vec2 alphaEnd;
23078
23241
  varying float vAlpha;
@@ -23082,9 +23245,78 @@ function makeFatTrailMaterial(color, lineWidth, brightness = TRAIL_DIM, dash) {
23082
23245
  varying float vDistW;
23083
23246
  varying float vW;
23084
23247
  #endif
23248
+ vec3 toView(vec3 point) {
23249
+ return mat3(viewMatrix) * (point + uCenterOffset);
23250
+ }
23251
+ vec2 toPx(vec3 view) {
23252
+ vec4 clip = projectionMatrix * vec4(view, 1.0);
23253
+ return clip.xy / clip.w * uResolution * 0.5;
23254
+ }
23255
+ // The unit direction on screen from one point to another, with the
23256
+ // distance in pixels in z.
23257
+ vec3 heading(vec2 from, vec2 to) {
23258
+ vec2 d = to - from;
23259
+ // Scaled before normalising: an end cut just ahead of the camera can
23260
+ // sit 1e20 px off screen, and its square overflows a float.
23261
+ float span = max(abs(d.x), abs(d.y));
23262
+ // Seen end on: any direction will do.
23263
+ if (span < 1e-4) return vec3(1.0, 0.0, 0.0);
23264
+ vec2 u = d / span;
23265
+ return vec3(normalize(u), span * length(u));
23266
+ }
23267
+ // The screen headings into and out of the middle of three points. Both
23268
+ // segments at a turn call this with the same points, so they place
23269
+ // their shared corners alike.
23270
+ void turnAt(vec3 before, vec3 at, vec3 after, out vec3 din, out vec3 dout) {
23271
+ vec2 p = toPx(toView(at));
23272
+ din = heading(toPx(toView(before)), p);
23273
+ dout = heading(p, toPx(toView(after)));
23274
+ }
23275
+ // A mitred corner: its offset, and how far along the quad's own
23276
+ // segment it moves.
23277
+ vec3 mitredAt(vec2 offset, vec2 own) {
23278
+ return vec3(offset, dot(offset, own));
23279
+ }
23280
+ // This corner's offset in pixels from the point where the line turns
23281
+ // from din to dout, and in z how far it moves along the quad's own
23282
+ // segment, own. The quads either side compute it from the same three
23283
+ // points, so they meet exactly. A square end, the fallback, moves
23284
+ // nothing along: exactly nothing, since a dash can end right there.
23285
+ vec3 cornerAt(vec3 din, vec3 dout, vec2 own, bool joins) {
23286
+ float h = uLineWidth * 0.5;
23287
+ vec3 square = vec3(vec2(-own.y, own.x) * corner.x * h, 0.0);
23288
+ bool bevel = corner.z > 0.5;
23289
+ if (!joins || din.z == 0.0 || dout.z == 0.0) return bevel ? vec3(0.0) : square;
23290
+ float inner = din.x * dout.y - din.y * dout.x >= 0.0 ? 1.0 : -1.0;
23291
+ vec2 bisector = din.xy + dout.xy;
23292
+ // The cosine of half the turn.
23293
+ float cosHalf = length(bisector) * 0.5;
23294
+ float safeCos = max(cosHalf, 1e-6);
23295
+ vec2 mitre = vec2(-bisector.y, bisector.x) / (2.0 * safeCos) * (h / safeCos);
23296
+ // How far along each segment the inner corner moves. Past half the
23297
+ // shorter segment it would cross that segment's other corner, and
23298
+ // square ends overlap instead, as with no join.
23299
+ float reach = h * sqrt(max(1.0 - cosHalf * cosHalf, 0.0)) / safeCos;
23300
+ bool fits = cosHalf > 1e-3 && reach <= 0.5 * min(din.z, dout.z);
23301
+ bool mitred = fits && cosHalf * ${MITER_LIMIT.toFixed(1)} >= 1.0;
23302
+ if (bevel) {
23303
+ // Each segment's square outer corner and the inner corner they
23304
+ // share, at the turn's own distance along the line. Flat under a
23305
+ // mitre, which covers the turn by itself, and beside a segment
23306
+ // shorter than the line is wide, where it could reach into the
23307
+ // quad past that segment.
23308
+ if (mitred || (!fits && min(din.z, dout.z) < uLineWidth)) return vec3(0.0);
23309
+ if (corner.z < 1.5) return vec3(-inner * vec2(-din.y, din.x) * h, 0.0);
23310
+ if (corner.z < 2.5) return vec3(-inner * vec2(-dout.y, dout.x) * h, 0.0);
23311
+ return vec3(fits ? inner * mitre : vec2(0.0), 0.0);
23312
+ }
23313
+ if (!fits) return square;
23314
+ if (corner.x == inner) return mitredAt(inner * mitre, own);
23315
+ return mitred ? mitredAt(-inner * mitre, own) : square;
23316
+ }
23085
23317
  void main() {
23086
- vec3 a = mat3(viewMatrix) * (segStart + uCenterOffset);
23087
- vec3 b = mat3(viewMatrix) * (segEnd + uCenterOffset);
23318
+ vec3 a = toView(segStart);
23319
+ vec3 b = toView(segEnd);
23088
23320
  float aheadA = -a.z;
23089
23321
  float aheadB = -b.z;
23090
23322
  float front = max(aheadA, aheadB);
@@ -23106,28 +23338,32 @@ function makeFatTrailMaterial(color, lineWidth, brightness = TRAIL_DIM, dash) {
23106
23338
  float cutB = aheadB < uTrim.x ? (cut - aheadB) / (aheadA - aheadB) : 0.0;
23107
23339
  if (cutA > 0.0) a = vec3(mix(a.xy, b.xy, cutA), -cut);
23108
23340
  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
23341
  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);
23342
+ vec3 own = heading(toPx(a), toPx(b));
23343
+ // The turn this corner sits on, read in the line's direction: only
23344
+ // with all three of its points ahead of the camera, so that both
23345
+ // segments at the turn agree on it.
23346
+ bool joins = cutA == 0.0 && cutB == 0.0 && -toView(atEnd ? segNext : segPrev).z >= uTrim.x;
23347
+ vec3 din = own;
23348
+ vec3 dout = own;
23349
+ if (joins) {
23350
+ if (atEnd) turnAt(segStart, segEnd, segNext, din, dout);
23351
+ else turnAt(segPrev, segStart, segEnd, din, dout);
23352
+ own = atEnd ? din : dout;
23353
+ }
23354
+ vec3 offset = cornerAt(din, dout, own.xy, joins);
23355
+ vec4 clip = projectionMatrix * vec4(atEnd ? b : a, 1.0);
23356
+ gl_Position = vec4((clip.xy / clip.w + offset.xy * 2.0 / uResolution) * clip.w, clip.zw);
23124
23357
  vec2 alphas = atEnd ? mix(alphaEnd, alphaStart, cutB) : mix(alphaStart, alphaEnd, cutA);
23125
23358
  vAlpha = mix(alphas.x, alphas.y, uShowFull);
23126
23359
  #ifdef DASHED
23127
23360
  // Carried times w and divided back out per fragment, which undoes
23128
23361
  // the perspective a varying is interpolated with: the dashes are
23129
- // even on screen however much nearer one end is.
23362
+ // even on screen however much nearer one end is. A corner a mitre
23363
+ // moves along the segment counts on by as much, so dash ends stay
23364
+ // square to the line.
23130
23365
  float dist = atEnd ? mix(distEnd, distStart, cutB) : mix(distStart, distEnd, cutA);
23366
+ dist += offset.z;
23131
23367
  vDistW = dist * clip.w;
23132
23368
  vW = clip.w;
23133
23369
  #endif
@@ -25015,7 +25251,7 @@ function clampCameraOutsideBody(camera, body, focusTruePos, surface, clearanceKm
25015
25251
  const dist = Math.hypot(dx, dy, dz);
25016
25252
  if (surface?.seated) {
25017
25253
  if (dist < 1e-9) {
25018
- camera.position.set(cx, cy + focusDist + kmToScene(LANDED_KEEP_AWAY_KM), cz);
25254
+ camera.position.set(cx, cy + kmToScene(radiusKm + LANDED_KEEP_AWAY_KM), cz);
25019
25255
  return;
25020
25256
  }
25021
25257
  const hh = Math.tan(camera.fov * Math.PI / 360) * camera.near;
@@ -25060,7 +25296,7 @@ function clampCameraOutsideBody(camera, body, focusTruePos, surface, clearanceKm
25060
25296
  _dirBF.y,
25061
25297
  _dirBF.z
25062
25298
  ]);
25063
- const wall = (sKm !== null ? kmToScene(sKm) : focusDist) + kmToScene(LANDED_KEEP_AWAY_KM);
25299
+ const wall = kmToScene((sKm ?? radiusKm) + LANDED_KEEP_AWAY_KM);
25064
25300
  if (wall - r > lift) lift = wall - r;
25065
25301
  }
25066
25302
  if (lift <= 0) return;
@@ -25284,7 +25520,6 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
25284
25520
  screenOccluders.length = occluderCount;
25285
25521
  const focusedSystemId = ctx.visibility.focusedSystemId;
25286
25522
  let newlyShownLabel = false;
25287
- const nomFocusedBodyId = focusedBodyId ? nomenclatureBodyId(ctx.getBody(focusedBodyId), bodyObjects) : void 0;
25288
25523
  for (const bo of bodyObjects.values()) {
25289
25524
  const { body, group, trail } = bo;
25290
25525
  const dist = bo.cachedDist;
@@ -25292,7 +25527,7 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
25292
25527
  const drawn = isDrawn(bo);
25293
25528
  bo.root.visible = drawn;
25294
25529
  let showLabel;
25295
- const isFocused = body.data.id === focusedBodyId;
25530
+ const isFocused = ctx.visibility.inFocus(body.data.id);
25296
25531
  const isSystemRoot = focusedSystemId !== null && body.data.id === focusedSystemId;
25297
25532
  const centreShare = ctx.visibility.systemCentres.get(body.data.id);
25298
25533
  const isHeldStar = isFocused || ctx.visibility.isPinned(body.data.id) || centreShare === 1;
@@ -25401,7 +25636,7 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
25401
25636
  if (bo.label) fadeLabel(bo.label, labelShare);
25402
25637
  if (bo.planetDot) bo.planetDot.material.opacity = labelShare;
25403
25638
  const nomScreenR = bo.radiusScene > 0 ? bo.radiusScene / dist * projScale : 0;
25404
- const isNomFocused = showNomenclature && body.data.id === nomFocusedBodyId;
25639
+ const isNomFocused = showNomenclature && body.data.id === focusedBodyId;
25405
25640
  updateNomenclatureVisibility(bo, isNomFocused, nomScreenR, projScale, camera, screenW, screenH);
25406
25641
  }
25407
25642
  if (screenOccluders.length > 0) {
@@ -25413,7 +25648,7 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
25413
25648
  if (isScreenOccluded(_occlusionRect.x, _occlusionRect.y, bo.cachedDist, id, screenOccluders, id === focusedBodyId ? focusedPrimaryId : null)) bo.label.visible = false;
25414
25649
  }
25415
25650
  }
25416
- const groundBo = nomFocusedBodyId ? bodyObjects.get(nomFocusedBodyId) : void 0;
25651
+ const groundBo = focusedBodyId ? bodyObjects.get(focusedBodyId) : void 0;
25417
25652
  const ground = groundBo?.body.position;
25418
25653
  if (groundBo && ground) {
25419
25654
  const relief = reliefRadiiKm(groundBo);
@@ -25424,17 +25659,17 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
25424
25659
  for (const bo of bodyObjects.values()) {
25425
25660
  const at = bo.body.position;
25426
25661
  const id = bo.body.data.id;
25427
- if (!bo.label?.visible || !at || id === nomFocusedBodyId || id === focusedBodyId) continue;
25662
+ if (!bo.label?.visible || !at || ctx.visibility.inFocus(id)) continue;
25428
25663
  if (behindGround(at[0] - ground[0], at[1] - ground[1], at[2] - ground[2], cx, cy, cz, top)) bo.label.visible = false;
25429
25664
  }
25430
25665
  }
25431
25666
  if (forceCull || newlyShownLabel || ++cullFrameCounter >= 3) {
25432
25667
  cullFrameCounter = 0;
25433
- cullOverlappingLabels(bodyObjects, screenW, screenH, camera, focusedBodyId, ctx, hoveredBodyIds, screenOccluders, focusTruePos);
25668
+ cullOverlappingLabels(bodyObjects, screenW, screenH, camera, ctx, hoveredBodyIds, screenOccluders, focusTruePos);
25434
25669
  }
25435
25670
  refreshVisibleBodyLabelRects(bodyObjects, screenW, screenH, camera, focusTruePos, sceneSettings().showConstellations);
25436
- if (nomFocusedBodyId) {
25437
- const focusedBo = bodyObjects.get(nomFocusedBodyId);
25671
+ if (focusedBodyId) {
25672
+ const focusedBo = bodyObjects.get(focusedBodyId);
25438
25673
  if (focusedBo) cullOverlappingNomenclatureLabels(focusedBo);
25439
25674
  }
25440
25675
  for (const bo of bodyObjects.values()) {
@@ -25442,7 +25677,7 @@ function updateBodyVisibility(bodyObjects, camera, ctx, focusTruePos, focusedBod
25442
25677
  if (!line?.visible) continue;
25443
25678
  const mat = line.material;
25444
25679
  mat.uniforms.uCenterOffset.value.set(-camera.position.x, -camera.position.y, -camera.position.z);
25445
- const isFocused = bo.body.data.id === focusedBodyId;
25680
+ const isFocused = ctx.visibility.inFocus(bo.body.data.id);
25446
25681
  const isHovered = hoveredBodyIds.has(bo.body.data.id);
25447
25682
  const share = ctx.visibility.systemShareOf(bo.body) * (line.userData.zoneShare ?? 1) * (line.userData.thresholdShare ?? 1) * (line.userData.haloShare ?? 1);
25448
25683
  mat.uniforms.uAlphaMultiplier.value = (isHovered ? 2 : isFocused ? 1.75 : 1) * share;
@@ -29525,13 +29760,9 @@ var GALACTIC_NORTH_SCENE = (() => {
29525
29760
  function galacticNorthVector(out) {
29526
29761
  return (out ?? new Vector3()).copy(GALACTIC_NORTH_SCENE);
29527
29762
  }
29528
- function bodyNorthVector(body, jd, out, landed = false) {
29763
+ function bodyNorthVector(body, jd, out) {
29529
29764
  const target = out ?? new Vector3();
29530
29765
  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
29766
  target.copy(SCENE_UP);
29536
29767
  if (body.modelBaseFrame) target.applyQuaternion(body.modelBaseFrame).normalize();
29537
29768
  return target;
@@ -29545,7 +29776,6 @@ var CameraUpController = class {
29545
29776
  camera;
29546
29777
  controls;
29547
29778
  ctx;
29548
- isLanded;
29549
29779
  refId = null;
29550
29780
  startVec = new Vector3(0, 1, 0);
29551
29781
  targetVec = new Vector3(0, 1, 0);
@@ -29556,11 +29786,10 @@ var CameraUpController = class {
29556
29786
  plane = null;
29557
29787
  _quatA = new Quaternion();
29558
29788
  _quatB = new Quaternion();
29559
- constructor(camera, controls, ctx, isLanded) {
29789
+ constructor(camera, controls, ctx) {
29560
29790
  this.camera = camera;
29561
29791
  this.controls = controls;
29562
29792
  this.ctx = ctx;
29563
- this.isLanded = isLanded;
29564
29793
  }
29565
29794
  setNorthReference(id) {
29566
29795
  if (id === this.refId) return;
@@ -29579,10 +29808,8 @@ var CameraUpController = class {
29579
29808
  else this.targetVec.copy(SCENE_UP);
29580
29809
  } else {
29581
29810
  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);
29811
+ if (refBody) bodyNorthVector(refBody, jd, this.targetVec);
29812
+ else this.targetVec.copy(SCENE_UP);
29586
29813
  }
29587
29814
  if (this.needsDurationCalc) {
29588
29815
  this.needsDurationCalc = false;
@@ -30323,46 +30550,71 @@ function orbitalElementsToCurve(el, numPoints = 512) {
30323
30550
 
30324
30551
  // src/lib/scene/objects/trail/geometry.ts
30325
30552
  var TRAIL_RENDER_ORDER = 3;
30326
- var QUAD_CORNERS = [
30553
+ var CORNERS = [
30327
30554
  -1,
30328
30555
  0,
30556
+ 0,
30329
30557
  1,
30330
30558
  0,
30559
+ 0,
30331
30560
  -1,
30332
30561
  1,
30562
+ 0,
30333
30563
  1,
30334
- 1
30564
+ 1,
30565
+ 0,
30566
+ 0,
30567
+ 0,
30568
+ 1,
30569
+ 0,
30570
+ 0,
30571
+ 2,
30572
+ 0,
30573
+ 0,
30574
+ 3
30335
30575
  ];
30336
- var QUAD_INDICES = [
30576
+ var CORNER_INDICES = [
30337
30577
  0,
30338
30578
  1,
30339
30579
  2,
30340
30580
  2,
30341
30581
  1,
30342
- 3
30582
+ 3,
30583
+ 4,
30584
+ 5,
30585
+ 6
30343
30586
  ];
30344
30587
  function instanced(geometry, name) {
30345
30588
  return geometry.getAttribute(name).data;
30346
30589
  }
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));
30590
+ function setPointAttributes(geometry, names, count, stride) {
30591
+ const buffer = new InstancedInterleavedBuffer(new Float32Array(count * stride), stride);
30592
+ names.forEach((name, i) => geometry.setAttribute(name, new InterleavedBufferAttribute(buffer, stride, i * stride)));
30351
30593
  }
30352
30594
  function makeFatTrailGeometry(capacity) {
30353
30595
  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);
30596
+ geometry.setAttribute("corner", new Float32BufferAttribute(CORNERS, 3));
30597
+ geometry.setIndex(CORNER_INDICES);
30598
+ setPointAttributes(geometry, [
30599
+ "segPrev",
30600
+ "segStart",
30601
+ "segEnd",
30602
+ "segNext"
30603
+ ], capacity + 2, 3);
30604
+ setPointAttributes(geometry, ["alphaStart", "alphaEnd"], capacity, 2);
30358
30605
  geometry.instanceCount = 0;
30359
30606
  return geometry;
30360
30607
  }
30361
30608
  function writeFatTrailVertices(geometry, posSrc, trailSrc, fullSrc, n) {
30362
30609
  const points = instanced(geometry, "segStart");
30363
30610
  const alphas = instanced(geometry, "alphaStart");
30364
- const m = Math.min(n, points.count);
30365
- points.array.set(posSrc.subarray(0, m * 3));
30611
+ const m = Math.min(n, alphas.count);
30612
+ const arr = points.array;
30613
+ arr.set(posSrc.subarray(0, m * 3), 3);
30614
+ const last = (m - 1) * 3;
30615
+ const closed = m > 2 && posSrc[0] === posSrc[last] && posSrc[1] === posSrc[last + 1] && posSrc[2] === posSrc[last + 2];
30616
+ arr.copyWithin(0, closed ? last : 3, closed ? last + 3 : 6);
30617
+ arr.copyWithin((m + 1) * 3, closed ? 6 : m * 3, closed ? 9 : (m + 1) * 3);
30366
30618
  const alphaArr = alphas.array;
30367
30619
  for (let i = 0; i < m; i++) {
30368
30620
  alphaArr[2 * i] = trailSrc[i];
@@ -30371,7 +30623,7 @@ function writeFatTrailVertices(geometry, posSrc, trailSrc, fullSrc, n) {
30371
30623
  points.needsUpdate = true;
30372
30624
  alphas.needsUpdate = true;
30373
30625
  geometry.instanceCount = Math.max(0, m - 1);
30374
- geometry.setDrawRange(0, QUAD_INDICES.length);
30626
+ geometry.setDrawRange(0, CORNER_INDICES.length);
30375
30627
  }
30376
30628
  function makeEmptyTrail() {
30377
30629
  const geometry = new BufferGeometry();
@@ -30401,7 +30653,7 @@ function writeLineDistances(geometry, src, n) {
30401
30653
  }
30402
30654
  function buildFatLineFromThin(capacity, posArr, trailAlphas, fullAlphas, total, color, lineWidth, brightness, dash) {
30403
30655
  const geometry = makeFatTrailGeometry(capacity);
30404
- if (dash) setSegmentAttributes(geometry, ["distStart", "distEnd"], capacity, 1);
30656
+ if (dash) setPointAttributes(geometry, ["distStart", "distEnd"], capacity, 1);
30405
30657
  writeFatTrailVertices(geometry, posArr, trailAlphas, fullAlphas, total);
30406
30658
  const mesh = new Mesh(geometry, makeFatTrailMaterial(color, lineWidth, brightness, dash));
30407
30659
  mesh.matrixAutoUpdate = false;
@@ -30768,7 +31020,7 @@ var SystemDataLoader = class {
30768
31020
  };
30769
31021
 
30770
31022
  // src/lib/scene/debug/skybox-adjust.ts
30771
- var DEG2RAD$3 = Math.PI / 180;
31023
+ var DEG2RAD$2 = Math.PI / 180;
30772
31024
  var SkyboxAdjuster = class {
30773
31025
  scene;
30774
31026
  adjust = {
@@ -30792,15 +31044,15 @@ var SkyboxAdjuster = class {
30792
31044
  this.adjust.rxDeg = rxDeg;
30793
31045
  this.adjust.ryDeg = ryDeg;
30794
31046
  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));
31047
+ 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
31048
  this._q.copy(SKYBOX_BASE_ROTATION).multiply(this._adjustQ);
30797
31049
  this.scene.backgroundRotation.setFromQuaternion(this._q);
30798
31050
  }
30799
31051
  };
30800
31052
 
30801
31053
  // src/lib/scene/objects/sky/debug-markers.ts
30802
- var DEG2RAD$2 = Math.PI / 180;
30803
- var OBL_RAD = EARTH_OBLIQUITY_DEG * DEG2RAD$2;
31054
+ var DEG2RAD$1 = Math.PI / 180;
31055
+ var OBL_RAD = EARTH_OBLIQUITY_DEG * DEG2RAD$1;
30804
31056
  var COS_OBL = Math.cos(OBL_RAD);
30805
31057
  var SIN_OBL = Math.sin(OBL_RAD);
30806
31058
  var SKY_MARKER_RADIUS = 5e3;
@@ -30916,8 +31168,8 @@ var REFERENCE_CIRCLES = [
30916
31168
  }
30917
31169
  ];
30918
31170
  function eqToScene(raDeg, decDeg, out) {
30919
- const ra = raDeg * DEG2RAD$2;
30920
- const dec = decDeg * DEG2RAD$2;
31171
+ const ra = raDeg * DEG2RAD$1;
31172
+ const dec = decDeg * DEG2RAD$1;
30921
31173
  const cd = Math.cos(dec);
30922
31174
  const xEq = cd * Math.cos(ra);
30923
31175
  const yEq = cd * Math.sin(ra);
@@ -32600,16 +32852,16 @@ function between(points, jds, jd, from = 0, to = points.length) {
32600
32852
  }
32601
32853
 
32602
32854
  // 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);
32855
+ var DEG2RAD = Math.PI / 180;
32856
+ var COS_EPS = Math.cos(EARTH_OBLIQUITY_DEG * DEG2RAD);
32857
+ var SIN_EPS = Math.sin(EARTH_OBLIQUITY_DEG * DEG2RAD);
32606
32858
  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);
32859
+ const cosW = Math.cos(w * DEG2RAD);
32860
+ const sinW = Math.sin(w * DEG2RAD);
32861
+ const cosI = Math.cos(i * DEG2RAD);
32862
+ const sinI = Math.sin(i * DEG2RAD);
32863
+ const cosOm = Math.cos(om * DEG2RAD);
32864
+ const sinOm = Math.sin(om * DEG2RAD);
32613
32865
  return {
32614
32866
  m11: cosOm * cosW - sinOm * sinW * cosI,
32615
32867
  m12: -cosOm * sinW - sinOm * cosW * cosI,
@@ -32659,10 +32911,10 @@ function elementsToState(el, jd) {
32659
32911
  }
32660
32912
  if (!isFinite(e) || !isFinite(ma)) return null;
32661
32913
  const aKm = a * AU_KM;
32662
- const nRadPerSec = n * DEG2RAD$1 / SEC_PER_DAY$1;
32914
+ const nRadPerSec = n * DEG2RAD / SEC_PER_DAY$1;
32663
32915
  const mu = nRadPerSec * nRadPerSec * Math.abs(aKm) * Math.abs(aKm) * Math.abs(aKm);
32664
32916
  if (!isFinite(mu) || mu <= 0) return null;
32665
- const M = (ma + n * (t - epoch)) * DEG2RAD$1;
32917
+ const M = (ma + n * (t - epoch)) * DEG2RAD;
32666
32918
  let nu;
32667
32919
  let rKm;
32668
32920
  if (e < 1 || a > 0) {
@@ -33658,65 +33910,6 @@ function addVec(out, a, b) {
33658
33910
  out[2] = a[2] + b[2];
33659
33911
  }
33660
33912
 
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
33913
  // src/lib/math/orbit/lagrange.ts
33721
33914
  var EARTH_SUN_MASS_RATIO = 30034e-10;
33722
33915
  var ALONG_MIN = .45;
@@ -34051,7 +34244,7 @@ var Placer = class {
34051
34244
  return null;
34052
34245
  }
34053
34246
  this.settling.add(id);
34054
- const pos = isSurfaceFeature(body) ? this.seatFeature(body, jd) : isExtrasolar(body) && !body.data.parentId ? fixedPlace(body) : this.settle(body, jd);
34247
+ const pos = isExtrasolar(body) && !body.data.parentId ? fixedPlace(body) : this.settle(body, jd);
34055
34248
  this.settling.delete(id);
34056
34249
  if (jd === this.passJd) this.settled.set(id, body);
34057
34250
  return pos;
@@ -34060,13 +34253,6 @@ var Placer = class {
34060
34253
  const why = (this.ctx.getBody(aboveId) ?? this.bodyObjects.get(aboveId)?.body)?.unplaced;
34061
34254
  return why === "loading" || why === "never" ? why : "parent";
34062
34255
  }
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
34256
  rideAt(jd) {
34071
34257
  if (jd !== this.rideJd || this.focusedId !== this.rideFocus) {
34072
34258
  this.rideJd = jd;
@@ -35250,14 +35436,13 @@ function updateSphereLOD(bodyObjects, camera, renderer, ctx, focusedId, invalida
35250
35436
  const fovRad = camera.fov * Math.PI / 180;
35251
35437
  const projScale = renderer.domElement.clientHeight / (2 * Math.tan(fovRad / 2));
35252
35438
  const activeSystem = ctx.visibility.activeSystemId;
35253
- const terrainId = focusedId;
35254
35439
  for (const bo of bodyObjects.values()) {
35255
35440
  if (!bo.mesh || !bo.radiusScene || !bo.group.visible) continue;
35256
35441
  if (bo.cachedDist <= 0) continue;
35257
35442
  const isStar = bo.body.data.objectType === ObjectType.STAR;
35258
35443
  const id = bo.body.data.id;
35259
- if (id === terrainId || bo.terrainWindow) {
35260
- if (updateTerrainWindow(bo, camera, id === terrainId, invalidate)) continue;
35444
+ if (id === focusedId || bo.terrainWindow) {
35445
+ if (updateTerrainWindow(bo, camera, id === focusedId, invalidate)) continue;
35261
35446
  }
35262
35447
  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
35448
  if (desired === bo.currentSegments) continue;
@@ -35642,8 +35827,11 @@ var PromotionRegistry = class {
35642
35827
  this.finalizeBuilds();
35643
35828
  }
35644
35829
  syncSystemBodies(bodies) {
35645
- const focusedId = this.deps.getFocusedId();
35646
- if (focusedId !== void 0 && this.systemPromoted.delete(focusedId)) this.userPromoted.add(focusedId);
35830
+ for (const id of this.systemPromoted) {
35831
+ if (!this.deps.inFocus(id)) continue;
35832
+ this.systemPromoted.delete(id);
35833
+ this.userPromoted.add(id);
35834
+ }
35647
35835
  const wanted = new Set(bodies.map((b) => b.data.id));
35648
35836
  const dropped = [...this.systemPromoted].filter((id) => !wanted.has(id) && !this.pinned.has(id));
35649
35837
  this.tearDownBodies(dropped, this.systemPromoted);
@@ -35755,11 +35943,10 @@ var PromotionRegistry = class {
35755
35943
  }
35756
35944
  teardownAutoPromoted(forceAll) {
35757
35945
  if (this.autoPromoted.size === 0) return;
35758
- const focusedId = this.deps.getFocusedId();
35759
35946
  const jd = this.deps.clock.jd;
35760
35947
  const toRemove = [];
35761
35948
  for (const id of this.autoPromoted) {
35762
- if (id === focusedId) continue;
35949
+ if (this.deps.inFocus(id)) continue;
35763
35950
  if (forceAll || this.groupTargets && !this.groupTargets.has(id)) {
35764
35951
  toRemove.push(id);
35765
35952
  continue;
@@ -35772,21 +35959,21 @@ var PromotionRegistry = class {
35772
35959
  }
35773
35960
  setPinned(ids) {
35774
35961
  this.pinned = new Set(ids);
35775
- const focusedId = this.deps.getFocusedId();
35776
35962
  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();
35963
+ const kept = dropped.filter(this.deps.inFocus);
35964
+ for (const id of kept) {
35965
+ this.pinPromoted.delete(id);
35966
+ this.userPromoted.add(id);
35781
35967
  }
35782
- this.tearDownBodies(dropped.filter((id) => id !== focusedId), this.pinPromoted);
35968
+ if (kept.length > 0) this.emitUserPromotedCount();
35969
+ this.tearDownBodies(dropped.filter((id) => !this.deps.inFocus(id)), this.pinPromoted);
35783
35970
  this.buildPinned();
35784
35971
  }
35785
35972
  buildPinned() {
35786
35973
  let built = false;
35787
35974
  for (const id of this.pinned) {
35788
35975
  const body = this.deps.ctx.getBody(id);
35789
- if (!body || isSurfaceFeature(body) || body.data.objectType === ObjectType.MOON) continue;
35976
+ if (!body || body.data.objectType === ObjectType.MOON) continue;
35790
35977
  if (!this.buildBodyInstance(body)) continue;
35791
35978
  built = true;
35792
35979
  if (this.userPromoted.delete(id)) this.pinPromoted.add(id);
@@ -35796,8 +35983,7 @@ var PromotionRegistry = class {
35796
35983
  this.finalizeBuilds();
35797
35984
  }
35798
35985
  clearUserPromoted() {
35799
- const focusedId = this.deps.getFocusedId();
35800
- const toRemove = [...this.userPromoted].filter((id) => id !== focusedId);
35986
+ const toRemove = [...this.userPromoted].filter((id) => !this.deps.inFocus(id));
35801
35987
  this.tearDownBodies(toRemove, this.userPromoted);
35802
35988
  this.emitUserPromotedCount();
35803
35989
  }
@@ -35858,15 +36044,61 @@ var PromotionRegistry = class {
35858
36044
  }
35859
36045
  emitUserPromotedCount() {
35860
36046
  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--;
36047
+ let count = 0;
36048
+ for (const id of this.userPromoted) if (!this.deps.inFocus(id)) count++;
35864
36049
  this.deps.onUserPromotedChange(count);
35865
36050
  }
35866
36051
  };
35867
36052
 
35868
36053
  // src/lib/scene/focus/controller.ts
36054
+ function surfaceSite(host, point, hostPos, jd) {
36055
+ const s = cartesianToSpherical([...point], [...hostPos], siteQuat(host, jd));
36056
+ return {
36057
+ latitude: s.latitude,
36058
+ longitude: s.longitude,
36059
+ radius: s.distance
36060
+ };
36061
+ }
36062
+ function siteQuat(host, jd) {
36063
+ const q = host.orientation && bodyQuaternion(host.orientation, jd, host.nutPrec);
36064
+ return q ? [
36065
+ q.x,
36066
+ q.y,
36067
+ q.z,
36068
+ q.w
36069
+ ] : void 0;
36070
+ }
36071
+ function underHalfRadius(host, distance) {
36072
+ return distance < kmToScene(effectiveRadiusKm(host.data)) / 2;
36073
+ }
36074
+ function viewOverSite(site, offset, quat) {
36075
+ const { latitude, longitude } = site;
36076
+ const up = new Vector3(...sphericalToCartesian([
36077
+ 0,
36078
+ 0,
36079
+ 0
36080
+ ], latitude, longitude, 1, quat));
36081
+ const down = new Vector3(...sphericalToCartesian([
36082
+ 0,
36083
+ 0,
36084
+ 0
36085
+ ], latitude + 90, longitude, 1, quat)).negate();
36086
+ const range = offset.length();
36087
+ const level = offset.clone().addScaledVector(up, -offset.dot(up));
36088
+ const tilt = Math.acos(MathUtils.clamp(offset.dot(up) / range, -1, 1));
36089
+ const heading = Math.atan2(new Vector3().crossVectors(down, level).dot(up), down.dot(level));
36090
+ return {
36091
+ latitude,
36092
+ longitude,
36093
+ zoom: site.radius + range,
36094
+ look: {
36095
+ tilt,
36096
+ heading
36097
+ }
36098
+ };
36099
+ }
35869
36100
  var _from = new Vector3();
36101
+ var _shift = new Vector3();
35870
36102
  var _forward = new Vector3();
35871
36103
  var _ground = new Vector3();
35872
36104
  var FocusController = class {
@@ -35875,6 +36107,10 @@ var FocusController = class {
35875
36107
  readerFocusFilter = null;
35876
36108
  focusSets = 0;
35877
36109
  focusedBody;
36110
+ subjectBody;
36111
+ onGround = false;
36112
+ subjectSite = null;
36113
+ placedOffSubject = false;
35878
36114
  _tmpV3 = new Vector3();
35879
36115
  pendingInitialView = null;
35880
36116
  heldEpoch = -1;
@@ -35896,7 +36132,7 @@ var FocusController = class {
35896
36132
  onBodyClick: (b) => this.handleFocus(b),
35897
36133
  assignMapLayerToTrails: deps.assignMapLayerToTrails,
35898
36134
  repositionAll: deps.repositionAll,
35899
- getFocusedId: () => this.focusedBody?.data.id,
36135
+ inFocus: (id) => id === this.focusedBody?.data.id || id === this.subjectBody?.data.id,
35900
36136
  onUserPromotedChange: deps.callbacks.onUserPromotedChange,
35901
36137
  onSystemBodies: (bodies) => deps.systemData.loadPlanets(bodies)
35902
36138
  });
@@ -35904,6 +36140,151 @@ var FocusController = class {
35904
36140
  get current() {
35905
36141
  return this.focusedBody;
35906
36142
  }
36143
+ get subject() {
36144
+ return this.subjectBody;
36145
+ }
36146
+ get groundView() {
36147
+ return this.onGround;
36148
+ }
36149
+ onGroundAt(body, jd = this.deps.clock.jd) {
36150
+ if (body.data.objectType !== ObjectType.SPACECRAFT) return false;
36151
+ const probe = this.probeRecord(body, jd);
36152
+ return probe && isLandedAt(probe, jd);
36153
+ }
36154
+ landedSite(body, jd = this.deps.clock.jd) {
36155
+ const probe = this.probeRecord(body, jd);
36156
+ if (!probe?.landed || !isLandedAt(probe, jd)) return void 0;
36157
+ const { ctx } = this.deps;
36158
+ const host = ctx.getBody(`naif-${probe.landed.bodyNaifId}`);
36159
+ const pos = ctx.place(body, jd);
36160
+ const hostPos = host && ctx.place(host, jd);
36161
+ if (!host || !pos || !hostPos) return void 0;
36162
+ return {
36163
+ host,
36164
+ site: surfaceSite(host, pos, hostPos, jd)
36165
+ };
36166
+ }
36167
+ probeRecord(body, jd) {
36168
+ if (body.data.objectType !== ObjectType.SPACECRAFT) return void 0;
36169
+ return this.deps.ctx.probeStore?.probeWithCenter(body.data.id, jd)?.probe;
36170
+ }
36171
+ followSubject(jd) {
36172
+ const subject = this.subjectBody;
36173
+ const host = this.focusedBody;
36174
+ const now = subject && this.landedSite(subject, jd);
36175
+ if (!subject || !host || !now || now.host !== host) return;
36176
+ const was = this.subjectSite;
36177
+ this.subjectSite = now.site;
36178
+ if (!was || !this.placedOffSubject && this.pendingInitialView) return;
36179
+ const q = this.focusedBodyQuat(host);
36180
+ const a = sphericalToCartesian([
36181
+ 0,
36182
+ 0,
36183
+ 0
36184
+ ], was.latitude, was.longitude, was.radius, q);
36185
+ const b = sphericalToCartesian([
36186
+ 0,
36187
+ 0,
36188
+ 0
36189
+ ], now.site.latitude, now.site.longitude, now.site.radius, q);
36190
+ const shift = _shift.set(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
36191
+ if (shift.length() < kmToScene(1e-6)) return;
36192
+ this.deps.controls.moveOrbit((orbit) => orbit.add(shift));
36193
+ const held = this.pendingInitialView;
36194
+ if (held) {
36195
+ const at = sphericalToCartesian([
36196
+ 0,
36197
+ 0,
36198
+ 0
36199
+ ], held.latitude, held.longitude, held.zoom, q);
36200
+ const moved = cartesianToSpherical([
36201
+ at[0] + shift.x,
36202
+ at[1] + shift.y,
36203
+ at[2] + shift.z
36204
+ ], [
36205
+ 0,
36206
+ 0,
36207
+ 0
36208
+ ], q);
36209
+ this.pendingInitialView = {
36210
+ ...held,
36211
+ latitude: moved.latitude,
36212
+ longitude: moved.longitude,
36213
+ zoom: moved.distance
36214
+ };
36215
+ }
36216
+ }
36217
+ leaveGround() {
36218
+ const subject = this.subjectBody;
36219
+ const host = this.focusedBody;
36220
+ const site = this.subjectSite;
36221
+ if (!subject || !host) return;
36222
+ const pos = this.place(subject);
36223
+ if (!pos || !site) {
36224
+ this.setFocusTarget(subject);
36225
+ return;
36226
+ }
36227
+ const seat = sphericalToCartesian([
36228
+ 0,
36229
+ 0,
36230
+ 0
36231
+ ], site.latitude, site.longitude, site.radius, siteQuat(host, this.deps.clock.jd));
36232
+ const cam = this.deps.camera.position;
36233
+ const offset = cartesianToSpherical([
36234
+ cam.x - seat[0],
36235
+ cam.y - seat[1],
36236
+ cam.z - seat[2]
36237
+ ], [
36238
+ 0,
36239
+ 0,
36240
+ 0
36241
+ ]);
36242
+ this.settleOnBodyInstant(subject, pos);
36243
+ this.snapToBodyFrame(offset.latitude, offset.longitude, offset.distance);
36244
+ }
36245
+ panToSurfacePoint(host, site, height, subject) {
36246
+ const { focus, camera, controls, callbacks, pointClouds } = this.deps;
36247
+ const hostPos = this.place(host);
36248
+ if (!hostPos) return 0;
36249
+ const current = this.focusedBody;
36250
+ 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);
36251
+ const camWorld = this.cameraTruePos();
36252
+ if (f64dist(focus.focusTruePos, hostPos) > 0) {
36253
+ focus.focusTruePos = [...hostPos];
36254
+ camera.position.set(camWorld[0] - hostPos[0], camWorld[1] - hostPos[1], camWorld[2] - hostPos[2]);
36255
+ this.deps.repositionAll();
36256
+ pointClouds.rebuildBasis();
36257
+ }
36258
+ this.setFocusTarget(host, void 0, subject);
36259
+ this.onGround = true;
36260
+ this.placedOffSubject = true;
36261
+ focus.aim = null;
36262
+ focus.aimLooks = false;
36263
+ focus.aimPose = null;
36264
+ const quat = siteQuat(host, this.deps.clock.jd);
36265
+ const ground = new Vector3(...sphericalToCartesian([
36266
+ 0,
36267
+ 0,
36268
+ 0
36269
+ ], site.latitude, site.longitude, site.radius, quat));
36270
+ const from = _from.set(camWorld[0] - hostPos[0], camWorld[1] - hostPos[1], camWorld[2] - hostPos[2]);
36271
+ if (controls.looksOver(from, ground)) {
36272
+ focus.aimPose = controls.lookPose(from, ground, new Quaternion());
36273
+ focus.aim = [
36274
+ ground.x,
36275
+ ground.y,
36276
+ ground.z
36277
+ ];
36278
+ focus.aimLooks = true;
36279
+ focus.focusDurationMs = angularDuration(camera.quaternion.angleTo(focus.aimPose), FOCUS_ROT_PACING);
36280
+ return focus.focusDurationMs;
36281
+ }
36282
+ const distance = from.length();
36283
+ callbacks.onCameraPosition?.(site.latitude, site.longitude, distance);
36284
+ const camPos = sphericalToCartesian([...hostPos], site.latitude, site.longitude, distance, quat);
36285
+ prepareFlyToCamera(focus, camera, camWorld, camPos, sceneSettings().resolvedReducedMotion);
36286
+ return focus.focusDurationMs;
36287
+ }
35907
36288
  get focusChanges() {
35908
36289
  return this.focusSets;
35909
36290
  }
@@ -35946,12 +36327,12 @@ var FocusController = class {
35946
36327
  ];
35947
36328
  }
35948
36329
  holdsModel(id) {
35949
- const focused = this.focusedBody;
35950
- return id === (focused && (isSurfaceFeature(focused) ? focused.featureAnchor.hostId : focused.data.id)) || this.upgradeTargetIds.has(id);
36330
+ return id === this.focusedBody?.data.id || id === this.subjectBody?.data.id || this.upgradeTargetIds.has(id);
35951
36331
  }
35952
36332
  syncUpgradeTargets(body) {
35953
36333
  const { ctx, scene, clickables, meshToBody, bodyObjects, pointClouds, clock, loadTexture, assignMapLayerToTrails } = this.deps;
35954
36334
  const targets = upgradeTargets(body, ctx);
36335
+ if (this.subjectBody && !targets.includes(this.subjectBody)) targets.push(this.subjectBody);
35955
36336
  const newIds = new Set(targets.map((t) => t.data.id));
35956
36337
  for (const id of this.upgradeTargetIds) {
35957
36338
  if (newIds.has(id)) continue;
@@ -36011,6 +36392,8 @@ var FocusController = class {
36011
36392
  ];
36012
36393
  }
36013
36394
  this.focusedBody = void 0;
36395
+ this.subjectBody = void 0;
36396
+ this.onGround = false;
36014
36397
  this.promotion.emitUserPromotedCount();
36015
36398
  prepareFocusTarget(focus, [...position], camera, camWorld, camPos, sceneSettings().resolvedReducedMotion);
36016
36399
  const spherical = cartesianToSpherical(camPos ?? camWorld, position, void 0);
@@ -36051,8 +36434,13 @@ var FocusController = class {
36051
36434
  }
36052
36435
  handleFocus(body) {
36053
36436
  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);
36437
+ if ((this.subjectBody ?? this.focusedBody)?.data.id === body.data.id && !this.deps.focus.aim) {
36438
+ this.deps.callbacks.onFocusChange(body, this.focusedBody);
36439
+ return;
36440
+ }
36441
+ const landed = this.landedSite(body);
36442
+ if (landed) {
36443
+ this.panToSurfacePoint(landed.host, landed.site, void 0, body);
36056
36444
  return;
36057
36445
  }
36058
36446
  this.setFocusTarget(body);
@@ -36062,36 +36450,28 @@ var FocusController = class {
36062
36450
  const { latitude, longitude, distance } = cartesianToSpherical(camWorld, pos, this.focusedBodyQuat(body));
36063
36451
  this.deps.callbacks.onCameraPosition?.(latitude, longitude, distance);
36064
36452
  }
36065
- setFocusTarget(body, camPos) {
36453
+ setFocusTarget(body, camPos, subject) {
36066
36454
  const { ctx, bodyObjects, callbacks, focus, camera, systemData, loadTexture } = this.deps;
36067
36455
  this.focusSets++;
36068
- const drawsItself = !isSurfaceFeature(body) && !isExtrasolar(body) && !isExoplanet(body);
36069
- if (!isSurfaceFeature(body)) this.promotion.ensureBodyObjects(body);
36456
+ this.subjectBody = subject;
36457
+ this.subjectSite = (subject && this.landedSite(subject)?.site) ?? null;
36458
+ this.placedOffSubject = false;
36459
+ this.onGround = false;
36460
+ const drawsItself = !isExtrasolar(body) && !isExoplanet(body);
36461
+ this.promotion.ensureBodyObjects(body);
36070
36462
  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;
36463
+ const prevId = prev?.data.id;
36075
36464
  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);
36465
+ if (prevId && prevId !== body.data.id) {
36466
+ const prevBo = bodyObjects.get(prevId);
36467
+ if (prevBo && !newTargetIds.has(prevId)) unloadBodyModel(prevBo);
36468
+ if (prevBo) disposeNomenclatureLabels(prevBo);
36083
36469
  }
36084
36470
  if (body.data.id !== prev?.data.id) this.pendingInitialView = null;
36085
36471
  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);
36472
+ ctx.visibility.setFocused(body, subject);
36473
+ callbacks.onFocusChange(subject ?? body, body);
36090
36474
  if (drawsItself) loadTexture(body);
36091
- if (newNomBodyId && newNomBodyId !== body.data.id) {
36092
- const landingBody = ctx.getBody(newNomBodyId);
36093
- if (landingBody) loadTexture(landingBody);
36094
- }
36095
36475
  systemData.syncToFocus();
36096
36476
  const pos = this.place(body);
36097
36477
  if (pos) {
@@ -36109,6 +36489,19 @@ var FocusController = class {
36109
36489
  this.setFocusTarget(body);
36110
36490
  return 0;
36111
36491
  }
36492
+ const landed = this.landedSite(body);
36493
+ if (landed) {
36494
+ const { host, site } = landed;
36495
+ if (zoom === void 0 || underHalfRadius(host, zoom)) return this.panToSurfacePoint(host, site, zoom, body);
36496
+ const view = {
36497
+ latitude: latitude ?? site.latitude,
36498
+ longitude: longitude ?? site.longitude
36499
+ };
36500
+ return this.flyOntoGround(host, {
36501
+ ...view,
36502
+ zoom
36503
+ }, body);
36504
+ }
36112
36505
  let camPos;
36113
36506
  if (zoom !== void 0) {
36114
36507
  if (latitude !== void 0 && longitude !== void 0) camPos = sphericalToCartesian(pos, latitude, longitude, zoom, this.focusedBodyQuat(body));
@@ -36127,7 +36520,7 @@ var FocusController = class {
36127
36520
  callbacks.onCameraPosition?.(spherical.latitude, spherical.longitude, spherical.distance);
36128
36521
  if (zoom !== void 0 && camPos) {
36129
36522
  this.pendingInitialView = null;
36130
- if (this.focusedBody?.data.id === id && f64dist(focus.focusTruePos, pos) < minCameraDistance(body)) {
36523
+ if (this.focusedBody?.data.id === id && !this.subjectBody && !this.onGround && f64dist(focus.focusTruePos, pos) < minCameraDistance(body)) {
36131
36524
  focus.focusTruePos = [...pos];
36132
36525
  this.deps.repositionAll();
36133
36526
  pointClouds.rebuildBasis();
@@ -36143,9 +36536,55 @@ var FocusController = class {
36143
36536
  const body = this.deps.ctx.getBody(id);
36144
36537
  const pos = body && this.place(body);
36145
36538
  if (!body || !pos) return;
36539
+ const landed = this.landedSite(body);
36540
+ if (landed) {
36541
+ const { host, site } = landed;
36542
+ if (underHalfRadius(host, zoom)) this.arriveAbove(host, site, zoom, body, true, look);
36543
+ else this.snapOntoGround(host, {
36544
+ latitude,
36545
+ longitude,
36546
+ zoom,
36547
+ look
36548
+ }, body);
36549
+ return;
36550
+ }
36146
36551
  this.settleOnBodyInstant(body, pos);
36147
36552
  this.snapToBodyFrame(latitude, longitude, zoom, look);
36148
36553
  }
36554
+ arriveAbove(host, site, height, subject, instant = false, look = LEVEL_LOOK) {
36555
+ const view = {
36556
+ latitude: site.latitude,
36557
+ longitude: site.longitude,
36558
+ zoom: site.radius + height
36559
+ };
36560
+ if (!instant) return this.flyOntoGround(host, view, subject);
36561
+ this.snapOntoGround(host, {
36562
+ ...view,
36563
+ look
36564
+ }, subject);
36565
+ this.placedOffSubject = true;
36566
+ return 0;
36567
+ }
36568
+ flyOntoGround(host, view, subject) {
36569
+ const { focus, callbacks } = this.deps;
36570
+ const hostPos = this.place(host);
36571
+ if (!hostPos) return 0;
36572
+ const camPos = sphericalToCartesian([...hostPos], view.latitude, view.longitude, view.zoom, siteQuat(host, this.deps.clock.jd));
36573
+ callbacks.onCameraPosition?.(view.latitude, view.longitude, view.zoom);
36574
+ this.pendingInitialView = null;
36575
+ this.setFocusTarget(host, camPos, subject);
36576
+ focus.orbitFly = true;
36577
+ this.onGround = true;
36578
+ this.placedOffSubject = true;
36579
+ return focus.focusDurationMs;
36580
+ }
36581
+ snapOntoGround(host, view, subject) {
36582
+ const pos = this.place(host);
36583
+ if (!pos) return;
36584
+ this.settleOnBodyInstant(host, pos, subject);
36585
+ this.snapToBodyFrame(view.latitude, view.longitude, view.zoom, view.look);
36586
+ this.onGround = true;
36587
+ }
36149
36588
  snapToBodyFacing(id, towardId, elevationDeg, distance) {
36150
36589
  const { ctx, camera, controls, callbacks } = this.deps;
36151
36590
  const body = ctx.getBody(id);
@@ -36190,9 +36629,9 @@ var FocusController = class {
36190
36629
  callbacks.onCameraPosition?.(settled.latitude, settled.longitude, settled.distance);
36191
36630
  this.pendingInitialView = null;
36192
36631
  }
36193
- settleOnBodyInstant(body, pos) {
36632
+ settleOnBodyInstant(body, pos, subject) {
36194
36633
  const { focus, pointClouds } = this.deps;
36195
- this.setFocusTarget(body);
36634
+ this.setFocusTarget(body, void 0, subject);
36196
36635
  focus.focusTruePos = [...pos];
36197
36636
  focus.focusOriginWorld = [...pos];
36198
36637
  focus.focusTargetWorld = [...pos];
@@ -36203,6 +36642,10 @@ var FocusController = class {
36203
36642
  focus.orbitFly = false;
36204
36643
  focus.arcOrbit = false;
36205
36644
  focus.cameraStaysOnBody = false;
36645
+ focus.aim = null;
36646
+ focus.aimLooks = false;
36647
+ focus.aimPose = null;
36648
+ focus.leavesAim = false;
36206
36649
  this.deps.repositionAll();
36207
36650
  pointClouds.rebuildBasis();
36208
36651
  }
@@ -36272,10 +36715,6 @@ function framesInBodyAxes(b) {
36272
36715
  }
36273
36716
  function upgradeTargets(focus, ctx) {
36274
36717
  if (!focus) return [];
36275
- if (isSurfaceFeature(focus)) {
36276
- const host = ctx.getBody(focus.featureAnchor.hostId);
36277
- return host && isMeshUpgradable(host) ? [host] : [];
36278
- }
36279
36718
  const out = [];
36280
36719
  if (isMeshUpgradable(focus)) out.push(focus);
36281
36720
  if (focus.data.objectType === ObjectType.MOON || focus.data.orbitalSource === OrbitalSource.SPICE_PROBE) {
@@ -36736,7 +37175,7 @@ var SPHERE_GROUND = {
36736
37175
  radiusKm: () => null
36737
37176
  };
36738
37177
  var _toCamera = new Vector3();
36739
- function cameraMotionScale(camera, focusTruePos, focused, parent, seated, position = camera.position, ground = SPHERE_GROUND) {
37178
+ function cameraMotionScale(camera, focusTruePos, focused, parent, position = camera.position, ground = SPHERE_GROUND) {
36740
37179
  const orbitRadius = position.length();
36741
37180
  if (!(orbitRadius > 0)) return {
36742
37181
  rotate: 1,
@@ -36747,7 +37186,7 @@ function cameraMotionScale(camera, focusTruePos, focused, parent, seated, positi
36747
37186
  const floor = focusRadius > 0 ? kmToScene(ground.clearanceKm(sized)) : 0;
36748
37187
  let clearance = orbitRadius;
36749
37188
  let height = Math.max(orbitRadius - focusRadius, floor);
36750
- for (const body of [sized, seated ? void 0 : parent]) {
37189
+ for (const body of [sized, parent]) {
36751
37190
  const at = body?.position;
36752
37191
  if (!body || !at) continue;
36753
37192
  if (!(effectiveRadiusKm(body.data) > 0)) continue;
@@ -36822,7 +37261,6 @@ var TIGHT_FAR_RELEASE = 2.3;
36822
37261
  var SUN_PROXY_FAR_FRACTION = .9;
36823
37262
  var _sunLiftDir = new Vector3();
36824
37263
  var _lookDir = new Vector3();
36825
- var _lookUp = new Vector3();
36826
37264
  var SPIN_FOLLOW_NONE = 2;
36827
37265
  var _spinNow = new Quaternion();
36828
37266
  var _spinStep = new Quaternion();
@@ -36985,7 +37423,6 @@ var SceneRenderer = class SceneRenderer {
36985
37423
  };
36986
37424
  viewportH = 1;
36987
37425
  tier = currentRenderTier();
36988
- landedNomBodyId = null;
36989
37426
  rafId = 0;
36990
37427
  paused = false;
36991
37428
  covered = false;
@@ -37091,7 +37528,7 @@ var SceneRenderer = class SceneRenderer {
37091
37528
  const dust = new StarDust(this.renderer, dustPlan(), this.invalidate);
37092
37529
  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
37530
  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);
37531
+ this.controls = new LookOrbitControls(this.camera, canvas, this.lookSurfaceRadius, () => this.pendingUrlWrite = true);
37095
37532
  this.controls.enableDamping = true;
37096
37533
  this.orbitDampingFactor = orbitDamping(sceneSettings().resolvedReducedMotion);
37097
37534
  Object.defineProperty(this.controls, "minDistance", { get: () => this.minCameraDistance() });
@@ -37112,7 +37549,7 @@ var SceneRenderer = class SceneRenderer {
37112
37549
  }
37113
37550
  this.invalidate();
37114
37551
  });
37115
- this.cameraUp = new CameraUpController(this.camera, this.controls, ctx, (id) => this.isLanded(id));
37552
+ this.cameraUp = new CameraUpController(this.camera, this.controls, ctx);
37116
37553
  this.systemData = new SystemDataLoader(this.scene, ctx, this.renderer, this.textureLoader, this.bodyObjects, clock, () => {
37117
37554
  this.focusController.reapplyInitialViewIfPending();
37118
37555
  this.invalidate();
@@ -37165,7 +37602,7 @@ var SceneRenderer = class SceneRenderer {
37165
37602
  ctx.visibility.updateCamera(framing.zoom, this.clock.jd);
37166
37603
  this.viewOpen = !ctx.loading;
37167
37604
  this.updateSystemShare(0);
37168
- callbacks.onFocusChange(focusBody);
37605
+ callbacks.onFocusChange(focusBody, focusBody);
37169
37606
  this.buildScene();
37170
37607
  if (focusBody) this.focusController.promotion.ensureBodyObjects(focusBody);
37171
37608
  if (focusBody) this.focusController.syncUpgradeTargets(focusBody);
@@ -37176,6 +37613,7 @@ var SceneRenderer = class SceneRenderer {
37176
37613
  this.gpuPick = new GpuPickPass(this.renderer, this.camera);
37177
37614
  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
37615
  this.pointerInteraction.attach();
37616
+ if (focusBody && this.focusController.landedSite(focusBody)) this.focusController.snapToBody(focusBody.data.id, framing.latitude, framing.longitude, framing.zoom, framing.look);
37179
37617
  this.tick();
37180
37618
  }
37181
37619
  buildScene() {
@@ -37450,6 +37888,9 @@ var SceneRenderer = class SceneRenderer {
37450
37888
  at[1] + offset[1],
37451
37889
  at[2] + offset[2]
37452
37890
  ];
37891
+ const host = this.focusController.landedSite(body, this.sceneJd)?.host;
37892
+ const hostAt = host && this.whereBody(host);
37893
+ if (host && hostAt) return cartesianToSpherical(camera, hostAt, siteQuat(host, this.sceneJd));
37453
37894
  return cartesianToSpherical(camera, at, this.focusController.focusedBodyQuat(body));
37454
37895
  }
37455
37896
  where(id) {
@@ -37496,7 +37937,7 @@ var SceneRenderer = class SceneRenderer {
37496
37937
  this.controls.target.set(0, 0, 0);
37497
37938
  this.controls.update();
37498
37939
  }
37499
- this.coverageWatch?.sync(this.focusController.current, this.clock.jd);
37940
+ this.coverageWatch?.sync(this.focusController.subject ?? this.focusController.current, this.clock.jd);
37500
37941
  this.clock.tick(performance.now());
37501
37942
  if (!this.shouldRender(nowMs)) {
37502
37943
  this.controls.update();
@@ -37522,13 +37963,14 @@ var SceneRenderer = class SceneRenderer {
37522
37963
  if (!this.dateHeld() && !this.seekAwaitsData) this.clock.placed(this.seenJumps);
37523
37964
  this.refreshTravelFocus();
37524
37965
  this.callbacks.onFrame?.(this.sceneJd, renderedDtMs);
37966
+ this.followLanding(nowMs);
37525
37967
  this.followSpin(nowMs);
37526
37968
  const controlsSettled = this.holdingCamera ? this.applyHeldCamera() : stepFocusAnimation(this.focus, this.camera, this.controls, () => this.repositionAll(), () => this.pointClouds.rebuildBasis());
37527
37969
  this.applyReaderLimits(nowMs);
37528
37970
  const near = this.nearSurfaces();
37529
37971
  if (near) {
37530
37972
  this.clampToSurfaces(near);
37531
- this.setMotionScale(cameraMotionScale(this.camera, this.focus.focusTruePos, near.focused, near.parent, near.seated, this.controls.orbitPosition, this.motionGround));
37973
+ this.setMotionScale(cameraMotionScale(this.camera, this.focus.focusTruePos, near.focused, near.parent, this.controls.orbitPosition, this.motionGround));
37532
37974
  } else this.setMotionScale({
37533
37975
  rotate: 1,
37534
37976
  translate: 1
@@ -37587,7 +38029,7 @@ var SceneRenderer = class SceneRenderer {
37587
38029
  }
37588
38030
  for (const light of this.ambientLights) light.intensity = ambient;
37589
38031
  updateUserLocationOcclusion(this.userLocationMarker, this.bodyObjects, this.camera);
37590
- const focusedIdLod = nomenclatureBodyId(this.focusController.current, this.bodyObjects);
38032
+ const focusedIdLod = this.focusController.current?.data.id;
37591
38033
  updateTextureLOD(this.bodyObjects, this.camera, this.renderer, this.ctx, this.textureLoader, focusedIdLod, this.sceneJd, this.appearances, this.invalidate);
37592
38034
  updateSphereLOD(this.bodyObjects, this.camera, this.renderer, this.ctx, focusedIdLod, this.invalidate);
37593
38035
  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 +38086,7 @@ var SceneRenderer = class SceneRenderer {
37644
38086
  if (!body) return POINT_MIN_DISTANCE;
37645
38087
  const bo = this.bodyObjects.get(body.data.id);
37646
38088
  const lowest = bo && (modelMinRadiusKm(bo) ?? reliefRadiiKm(bo)?.min ?? smallestRadiusKm(bo));
38089
+ if (this.focusController.groundView) return kmToScene((lowest ?? effectiveRadiusKm(body.data)) + LANDED_KEEP_AWAY_KM);
37647
38090
  return minCameraDistance(body, lowest, bo && finestTexelKm(bo));
37648
38091
  }
37649
38092
  clearanceKm(body) {
@@ -37651,7 +38094,7 @@ var SceneRenderer = class SceneRenderer {
37651
38094
  return surfaceClearanceKm(body, bo && finestTexelKm(bo));
37652
38095
  }
37653
38096
  followSpin(nowMs) {
37654
- const body = this.spinningBody();
38097
+ const body = this.focusController.current;
37655
38098
  const flying = nowMs - this.focus.focusStartTime < this.focus.focusDurationMs;
37656
38099
  if (!body?.orientation || flying || this.holdingCamera || this.focus.aim) {
37657
38100
  this.spinFrom = null;
@@ -37665,16 +38108,26 @@ var SceneRenderer = class SceneRenderer {
37665
38108
  };
37666
38109
  if (!from || from.id !== body.data.id) return;
37667
38110
  const radiusKm = effectiveRadiusKm(body.data);
37668
- const height = sceneToKm(this.controls.orbitPosition.length()) / radiusKm - (body === this.focusController.current ? 1 : 0);
38111
+ const height = sceneToKm(this.controls.orbitPosition.length()) / radiusKm - 1;
37669
38112
  const t = MathUtils.clamp((SPIN_FOLLOW_NONE - height) / 1.5, 0, 1);
37670
38113
  const share = t * t * (3 - 2 * t);
37671
38114
  if (share === 0) return;
37672
38115
  _spinStep.copy(_spinNow).multiply(from.quat.invert());
37673
38116
  this.controls.carry(_noTurn.identity().slerp(_spinStep, share));
37674
38117
  }
37675
- spinningBody() {
37676
- const focused = this.focusController.current;
37677
- return focused && (this.standingOn(focused) ?? focused);
38118
+ followLanding(nowMs) {
38119
+ if (nowMs - this.focus.focusStartTime < this.focus.focusDurationMs || this.holdingCamera || this.dateHeld()) return;
38120
+ const fc = this.focusController;
38121
+ const subject = fc.subject;
38122
+ if (subject) {
38123
+ const onGround = fc.onGroundAt(subject);
38124
+ if (onGround === false) fc.leaveGround();
38125
+ else if (onGround) fc.followSubject(this.sceneJd);
38126
+ return;
38127
+ }
38128
+ const current = fc.current;
38129
+ const landed = current && fc.landedSite(current);
38130
+ if (landed) fc.panToSurfacePoint(landed.host, landed.site, void 0, current);
37678
38131
  }
37679
38132
  settleCamera() {
37680
38133
  this.settleLook();
@@ -37692,22 +38145,23 @@ var SceneRenderer = class SceneRenderer {
37692
38145
  return {
37693
38146
  focused,
37694
38147
  parent: parentId ? this.ctx.getBody(parentId) : void 0,
37695
- seated: Boolean(this.bodyObjects.get(focused.data.id)?.isLanded) || isSurfaceFeature(focused)
38148
+ onGround: this.focusController.groundView
37696
38149
  };
37697
38150
  }
37698
- clampToSurfaces({ focused, parent, seated }) {
38151
+ clampToSurfaces({ focused, parent, onGround }) {
37699
38152
  const { camera } = this;
37700
38153
  const at = this.focus.focusTruePos;
38154
+ const own = parent?.data.id === focused.data.id;
37701
38155
  if (parent) {
37702
- const surface = this.surfaceClamp(parent, seated);
38156
+ const surface = this.surfaceClamp(parent, own && onGround);
37703
38157
  clampCameraOutsideBody(camera, parent, at, surface, this.clearanceKm(parent));
37704
38158
  }
37705
- if (parent?.data.id === focused.data.id) return;
37706
- const surface = this.surfaceClamp(focused, false);
38159
+ if (own) return;
38160
+ const surface = this.surfaceClamp(focused, onGround);
37707
38161
  if (surface) clampCameraOutsideBody(camera, focused, at, surface, this.clearanceKm(focused));
37708
38162
  }
37709
38163
  motionGround = {
37710
- clearanceKm: (body) => this.clearanceKm(body),
38164
+ clearanceKm: (body) => this.focusController.groundView && body === this.focusController.current ? LANDED_KEEP_AWAY_KM : this.clearanceKm(body),
37711
38165
  radiusKm: (body, direction) => {
37712
38166
  const surface = this.surfaceClamp(body, false);
37713
38167
  if (!surface) return null;
@@ -37783,20 +38237,10 @@ var SceneRenderer = class SceneRenderer {
37783
38237
  }
37784
38238
  updateTightFar(distance) {
37785
38239
  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
- }
38240
+ const id = this.focusController.current?.data.id;
38241
+ const radius = id && this.bodyObjects.get(id)?.radiusScene || 0;
37798
38242
  const bound = this.tightFar ? TIGHT_FAR_RELEASE : TIGHT_FAR_ENGAGE;
37799
- this.tightFar = radius > 0 && this.ctx.visibility.activeSystemId !== null && dist < radius * bound;
38243
+ this.tightFar = radius > 0 && this.ctx.visibility.activeSystemId !== null && distance < radius * bound;
37800
38244
  }
37801
38245
  updateDepthFar() {
37802
38246
  if (isReversedDepth()) return;
@@ -38232,31 +38676,11 @@ var SceneRenderer = class SceneRenderer {
38232
38676
  lookSurfaceRadius = (direction) => {
38233
38677
  const focused = this.focusController.current;
38234
38678
  if (this.holdingCamera || this.focus.aim || !focused) return null;
38235
- if (this.standingOn(focused)) return 0;
38236
38679
  const bo = this.bodyObjects.get(focused.data.id);
38237
38680
  if (isModelBearing(focused) || !(bo?.mesh || hasModelSurface(bo))) return null;
38238
38681
  const radiusKm = effectiveRadiusKm(focused.data);
38239
38682
  return kmToScene(this.motionGround.radiusKm(focused, direction) ?? radiusKm);
38240
38683
  };
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
38684
  onControlsEnd = () => {
38261
38685
  this.pendingUrlWrite = true;
38262
38686
  this.focusController.clearPendingInitialView();
@@ -38303,26 +38727,17 @@ var SceneRenderer = class SceneRenderer {
38303
38727
  }
38304
38728
  setSelectedFeature(featureId) {
38305
38729
  this.selectedFeatureId = featureId;
38306
- const nomId = nomenclatureBodyId(this.focusController.current, this.bodyObjects);
38730
+ const nomId = this.focusController.current?.data.id;
38307
38731
  const bo = nomId ? this.bodyObjects.get(nomId) : void 0;
38308
38732
  if (bo) setActiveFeatureLabel(bo, featureId);
38309
38733
  }
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
38734
  clearUserPromoted() {
38320
38735
  this.focusController.promotion.clearUserPromoted();
38321
38736
  }
38322
38737
  focusedSeatConfigKey() {
38323
- const focused = this.focusController.current;
38738
+ const focused = this.focusController.subject ?? this.focusController.current;
38324
38739
  if (!focused) return null;
38325
- if (focused.data.objectType !== ObjectType.SPACECRAFT && !isSurfaceFeature(focused)) return null;
38740
+ if (focused.data.objectType !== ObjectType.SPACECRAFT) return null;
38326
38741
  const parentId = collisionParentId(focused.data.parentId);
38327
38742
  const bo = parentId ? this.bodyObjects.get(parentId) : void 0;
38328
38743
  if (!bo) return null;
@@ -38436,7 +38851,6 @@ var SceneRenderer = class SceneRenderer {
38436
38851
  this.repositionBodies();
38437
38852
  this.pointClouds.maybeRebase();
38438
38853
  }
38439
- this.syncLandedNomenclature();
38440
38854
  this.seekAwaitsData = seeked && result.focusLoading;
38441
38855
  const losingPlace = result.focusUnplaced && !this.focusWasOutOfRange;
38442
38856
  this.focusWasOutOfRange = result.focusUnplaced;
@@ -38454,83 +38868,32 @@ var SceneRenderer = class SceneRenderer {
38454
38868
  this.focusWasOutOfRange = false;
38455
38869
  return this.focusController.focusOnBody(id, zoom, latitude, longitude);
38456
38870
  }
38457
- focusOnFeature(anchor, name, zoom, mode = "frame", view) {
38871
+ focusOnFeature(anchor, zoom, move = "turn", view = null, look = LEVEL_LOOK) {
38458
38872
  this.disarmLimits();
38459
38873
  this.applyJdUpdate();
38460
38874
  this.focusWasOutOfRange = false;
38461
38875
  const host = this.ctx.getBody(anchor.hostId);
38462
38876
  const hostPos = host && this.whereBody(host);
38463
38877
  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;
38878
+ const fc = this.focusController;
38879
+ fc.clearPendingInitialView();
38468
38880
  const hostBo = this.bodyObjects.get(host.data.id);
38469
38881
  this.selectedFeatureId = anchor.featureId;
38470
38882
  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;
38883
+ const seat = featureSeat(anchor, host, hostPos, hostBo, this.sceneJd);
38884
+ const site = surfaceSite(host, seat, hostPos, this.sceneJd);
38885
+ if (move === "turn") return fc.panToSurfacePoint(host, site, zoom);
38886
+ if (move === "frame" || !view) return fc.arriveAbove(host, site, zoom, void 0, move === "snap", look);
38887
+ if (underHalfRadius(host, view.zoom)) {
38888
+ const offset = sphericalToCartesian([
38889
+ 0,
38890
+ 0,
38891
+ 0
38892
+ ], view.latitude, view.longitude, view.zoom);
38893
+ view = viewOverSite(site, new Vector3(...offset), siteQuat(host, this.sceneJd));
38532
38894
  }
38533
- return this.focus.focusDurationMs;
38895
+ fc.snapOntoGround(host, view);
38896
+ return 0;
38534
38897
  }
38535
38898
  pickFocusedModel(ndcX, ndcY) {
38536
38899
  if (!this.overlayActive) return null;
@@ -38551,13 +38914,12 @@ var SceneRenderer = class SceneRenderer {
38551
38914
  }
38552
38915
  overlayModelBo() {
38553
38916
  const withModel = (bo) => bo?.model && isDrawn(bo) ? bo : null;
38554
- const focusBody = this.focusController.current;
38917
+ const focusBody = this.focusController.subject ?? this.focusController.current;
38555
38918
  if (!focusBody) {
38556
38919
  const centerId = this.travelFocus?.centerId;
38557
38920
  return withModel(centerId ? this.bodyObjects.get(centerId) : void 0);
38558
38921
  }
38559
- const modelId = isSurfaceFeature(focusBody) ? focusBody.featureAnchor.hostId : focusBody.data.id;
38560
- const bo = this.bodyObjects.get(modelId);
38922
+ const bo = this.bodyObjects.get(focusBody.data.id);
38561
38923
  const own = withModel(bo);
38562
38924
  if (own) return own;
38563
38925
  if (focusBody.data.orbitalSource === OrbitalSource.SPICE_PROBE && !bo?.isLanded) {
@@ -41330,6 +41692,14 @@ var SpaceMap = class {
41330
41692
  set focusedBody(value) {
41331
41693
  set(this.#focusedBody, value);
41332
41694
  }
41695
+ #cameraBody = state();
41696
+ get cameraBody() {
41697
+ return get(this.#cameraBody);
41698
+ }
41699
+ set cameraBody(value) {
41700
+ set(this.#cameraBody, value);
41701
+ }
41702
+ movingToFeature = false;
41333
41703
  attribution = null;
41334
41704
  objects = new MapObjectCollection(() => this.requireRenderer().extensions, () => this.requireCanvas());
41335
41705
  dragRotate;
@@ -41426,9 +41796,7 @@ var SpaceMap = class {
41426
41796
  }
41427
41797
  getFocusedBody() {
41428
41798
  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);
41799
+ return focused && bodyView(focused);
41432
41800
  }
41433
41801
  getChildren(id) {
41434
41802
  return [...this.ctx.bodies.getChildren(id) ?? []];
@@ -41652,11 +42020,12 @@ var SpaceMap = class {
41652
42020
  notify: (notice) => this.emit("notice", notice),
41653
42021
  dismiss: (topic) => this.emit("noticedismiss", topic)
41654
42022
  },
41655
- onFocusChange: (body) => {
42023
+ onFocusChange: (body, orbited) => {
41656
42024
  const initial = this.initialFocusPending;
41657
42025
  this.initialFocusPending = false;
41658
42026
  this.focusedBody = body;
41659
- const feature = body !== void 0 && isSurfaceFeature(body);
42027
+ this.cameraBody = orbited;
42028
+ const feature = this.movingToFeature;
41660
42029
  this.emit("focuschange", {
41661
42030
  body: this.getFocusedBody(),
41662
42031
  initial,
@@ -41671,7 +42040,7 @@ var SpaceMap = class {
41671
42040
  },
41672
42041
  onIdle: () => this.emit("idle"),
41673
42042
  onCameraPosition: (latitude, longitude, zoom, look = LEVEL_LOOK) => {
41674
- const body = this.focusedBody?.data.id;
42043
+ const body = this.cameraBody?.data.id;
41675
42044
  if (body === void 0) return;
41676
42045
  this.emit("camera", {
41677
42046
  body,
@@ -41904,7 +42273,7 @@ var SpaceMap = class {
41904
42273
  });
41905
42274
  }
41906
42275
  panTo(target) {
41907
- return this.toFeature(this.targetBody(target), target.feature, "pan");
42276
+ return this.toFeature(this.targetBody(target), target.feature, "turn");
41908
42277
  }
41909
42278
  async jumpTo(target) {
41910
42279
  const body = this.targetBody(target);
@@ -41937,8 +42306,7 @@ var SpaceMap = class {
41937
42306
  }
41938
42307
  if (target.feature) {
41939
42308
  const { featureId, lat, lon, diameterM } = target.feature;
41940
- this.focusOnFeature(body, featureId, lat, lon, diameterM, null, "snap");
41941
- this.renderer?.setLook(lookOf(target));
42309
+ this.focusOnFeature(body, featureId, lat, lon, diameterM, "snap", null, lookOf(target));
41942
42310
  return;
41943
42311
  }
41944
42312
  const camera = this.getCamera();
@@ -41946,7 +42314,7 @@ var SpaceMap = class {
41946
42314
  }
41947
42315
  getCamera() {
41948
42316
  const renderer = this.renderer;
41949
- const body = this.focusedBody?.data.id;
42317
+ const body = this.cameraBody?.data.id;
41950
42318
  if (!renderer || body === void 0) return null;
41951
42319
  const { latitude, longitude, distance } = renderer.getCameraState();
41952
42320
  return {
@@ -42019,9 +42387,9 @@ var SpaceMap = class {
42019
42387
  onTouchMove = (e) => {
42020
42388
  this.cooperative?.allowsTouchDrag(e.touches.length);
42021
42389
  };
42022
- toFeature(body, feature, mode) {
42390
+ toFeature(body, feature, move) {
42023
42391
  const { featureId, lat, lon, diameterM } = feature;
42024
- return this.settled(this.focusOnFeature(body, featureId, lat, lon, diameterM, null, mode));
42392
+ return this.settled(this.focusOnFeature(body, featureId, lat, lon, diameterM, move));
42025
42393
  }
42026
42394
  settled(ms) {
42027
42395
  return new Promise((resolve) => {
@@ -42071,7 +42439,7 @@ var SpaceMap = class {
42071
42439
  if (!renderer || !vantage || vantage.kept) return null;
42072
42440
  return (body) => renderer.viewFrom(body.data.id, vantage.offset) ?? {};
42073
42441
  }
42074
- focusOnFeature(bodyId, featureId, lat, lon, diameterM, name, mode = "frame", view = null) {
42442
+ focusOnFeature(bodyId, featureId, lat, lon, diameterM, move = "turn", view = null, look) {
42075
42443
  const hostBody = this.ctx.getBody(bodyId);
42076
42444
  if (!hostBody) return 0;
42077
42445
  const idealScene = kmToScene(featureDiameterM(diameterM, effectiveRadiusKm(hostBody.data)) * 4 / 1e3);
@@ -42085,16 +42453,20 @@ var SpaceMap = class {
42085
42453
  lon,
42086
42454
  diameterM
42087
42455
  };
42088
- return this.renderer?.focusOnFeature(anchor, name, zoom, mode, view) ?? 0;
42456
+ this.movingToFeature = true;
42457
+ try {
42458
+ return this.renderer?.focusOnFeature(anchor, zoom, move, view, look) ?? 0;
42459
+ } finally {
42460
+ this.movingToFeature = false;
42461
+ }
42089
42462
  }
42090
42463
  setFocusTarget(body, camPos) {
42091
42464
  this.renderer?.setFocusTarget(body, camPos);
42092
42465
  }
42093
42466
  getPose() {
42094
42467
  const renderer = this.renderer;
42095
- const focused = this.focusedBody;
42096
- if (!renderer || !focused) return null;
42097
- const body = focused.data.id;
42468
+ const body = this.cameraBody?.data.id;
42469
+ if (!renderer || body === void 0) return null;
42098
42470
  return {
42099
42471
  position: {
42100
42472
  body,