spacemap 0.1.9 → 0.1.11

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
@@ -1,5 +1,5 @@
1
1
  /*! spacemap SDK — https://spacemap.co — Mozilla Public License 2.0. Bundles MIT and ISC libraries: THIRD_PARTY_NOTICES.txt in the spacemap npm package. */
2
- import { ACESFilmicToneMapping, AdditiveBlending, AmbientLight, ArrowHelper, BackSide, Box3, BufferAttribute, BufferGeometry, CanvasTexture, Color, CubeTexture, CustomBlending, DataTexture, DepthTexture, DirectionalLight, DoubleSide, Euler, Float32BufferAttribute, FloatType, FrontSide, Group, HalfFloatType, InstancedBufferAttribute, InstancedMesh, Line, LineBasicMaterial, LineLoop, LinearFilter, LinearMipmapLinearFilter, LinearSRGBColorSpace, MathUtils, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshStandardMaterial, NoBlending, NoColorSpace, NormalBlending, OneFactor, OneMinusSrcAlphaFactor, PCFShadowMap, PCFSoftShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, RGBAFormat, RawShaderMaterial, Raycaster, RedFormat, RingGeometry, SRGBColorSpace, Scene, ShaderChunk, ShaderMaterial, ShadowMaterial, Shape, ShapeGeometry, ShapeUtils, SphereGeometry, Sprite, SpriteMaterial, TOUCH, Texture, TextureLoader, Uint16BufferAttribute, UnsignedByteType, Vector2, Vector3, Vector4, WebGLRenderTarget, WebGLRenderer } from "three";
2
+ import { ACESFilmicToneMapping, AdditiveBlending, AmbientLight, ArrowHelper, BackSide, Box3, BufferAttribute, BufferGeometry, CanvasTexture, Color, CubeTexture, CustomBlending, DataArrayTexture, DataTexture, DepthTexture, DirectionalLight, DoubleSide, Euler, Float32BufferAttribute, FloatType, FrontSide, Group, HalfFloatType, InstancedBufferAttribute, InstancedMesh, Line, LineBasicMaterial, LineLoop, LinearFilter, LinearMipmapLinearFilter, LinearSRGBColorSpace, MathUtils, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NoColorSpace, NormalBlending, OneFactor, OneMinusSrcAlphaFactor, PCFShadowMap, PCFSoftShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, RGBAFormat, RawShaderMaterial, Raycaster, RedFormat, RepeatWrapping, RingGeometry, SRGBColorSpace, Scene, ShaderChunk, ShaderMaterial, ShadowMaterial, Shape, ShapeGeometry, ShapeUtils, SphereGeometry, Sprite, SpriteMaterial, TOUCH, Texture, TextureLoader, Uint16BufferAttribute, UnsignedByteType, Vector2, Vector3, Vector4, WebGLRenderTarget, WebGLRenderer } from "three";
3
3
  import { OrbitControls } from "three/addons/controls/OrbitControls.js";
4
4
  import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
5
5
  import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
@@ -20,6 +20,7 @@ var DEFAULT_HOST = {
20
20
  dataUrl: "https://static.spacemap.co",
21
21
  imagesUrl: "https://images.spacemap.co",
22
22
  liveUrl: "https://live.spacemap.co",
23
+ tilesUrl: "",
23
24
  locale: () => "en",
24
25
  messages: {
25
26
  body_note_no_model: () => "no model available",
@@ -66,6 +67,7 @@ function configureHost(overrides) {
66
67
  current$1.dataUrl = current$1.dataUrl.replace(/\/$/, "");
67
68
  current$1.imagesUrl = current$1.imagesUrl.replace(/\/$/, "");
68
69
  current$1.liveUrl = current$1.liveUrl.replace(/\/$/, "");
70
+ current$1.tilesUrl = current$1.tilesUrl.replace(/\/$/, "");
69
71
  }
70
72
  function host() {
71
73
  return current$1;
@@ -2963,6 +2965,14 @@ function get_custom_elements_slots(element) {
2963
2965
  function dataBase() {
2964
2966
  return host().dataUrl;
2965
2967
  }
2968
+ function tilesBase() {
2969
+ return host().tilesUrl;
2970
+ }
2971
+ function tileUrl(tiles, cap, level, x, y, frame) {
2972
+ const month = frame === void 0 ? "" : `${String(frame).padStart(2, "0")}/`;
2973
+ const chart = cap && `${cap}/`;
2974
+ return `${tilesBase()}/v1/tiles/${tiles.id}/${month}${chart}${level}/${x}/${y}.webp?v=${tiles.version}`;
2975
+ }
2966
2976
  var versions = {};
2967
2977
  function setDataVersions(v) {
2968
2978
  versions = v ?? {};
@@ -6933,7 +6943,7 @@ var VisibilityController = class {
6933
6943
  }
6934
6944
  ratioVisibility(body, camDistThreeJS, pinned) {
6935
6945
  if (!pinned && this.getLayers().hidesBody(body.data.objectType, body.data.parentId)) return VISIBILITY.HIDE;
6936
- if (body.data.orbitalSource === OrbitalSource.SPICE_PROBE) return this.getProbeVisibility(body);
6946
+ if (body.data.orbitalSource === OrbitalSource.SPICE_PROBE) return this.getProbeVisibility(body, camDistThreeJS);
6937
6947
  const earthFilter = this.getEarthSatGroupFilter();
6938
6948
  if (earthFilter && body.data.parentId === "naif-399" && !earthFilter.has(body.data.id)) return VISIBILITY.HIDE;
6939
6949
  const ot = body.data.objectType;
@@ -6958,7 +6968,7 @@ var VisibilityController = class {
6958
6968
  const tier = computeVisibilityFromRatio(camDistThreeJS / 10 / refA, this.scaledSystem, 50, isFocused);
6959
6969
  return tier === VISIBILITY.CLOSE ? VISIBILITY.FULL : tier;
6960
6970
  }
6961
- getProbeVisibility(body) {
6971
+ getProbeVisibility(body, camDistThreeJS) {
6962
6972
  const ps = this.getProbeStore();
6963
6973
  if ((ps ? ps.containingSystemAt(body.data.id, this.currentJd) : null) === EARTH_SYSTEM_NAIF && this.getEarthSatGroupFilter()) return VISIBILITY.HIDE;
6964
6974
  const isFocused = body.data.id === this.focusedBodyIdPlain;
@@ -6974,7 +6984,7 @@ var VisibilityController = class {
6974
6984
  if (!parent?.position || !body.position) return VISIBILITY.FULL;
6975
6985
  const distToParent = f64dist(body.position, parent.position) / 10;
6976
6986
  if (distToParent === 0) return VISIBILITY.FULL;
6977
- return this.cameraDistThreeJS / 10 / distToParent <= this.scaledPlanetary[VISIBILITY.FULL] * (isFocused ? 5 : 1) ? VISIBILITY.FULL : VISIBILITY.HIDE;
6987
+ return camDistThreeJS / 10 / distToParent <= this.scaledPlanetary[VISIBILITY.FULL] * (isFocused ? 5 : 1) ? VISIBILITY.FULL : VISIBILITY.HIDE;
6978
6988
  }
6979
6989
  isProbeInFocusedSystem(body) {
6980
6990
  const parentId = body.data.parentId;
@@ -8179,19 +8189,11 @@ function extendProbeTrailBuffer(buf, sample, lastJd, lastPos, headJd) {
8179
8189
  //#endregion
8180
8190
  //#region src/lib/scene/objects/trail/points.ts
8181
8191
  var NUM_TRAIL_POINTS = 512;
8182
- function buildTrailPoints(body, curve, isOpenCurve, cx, cy, cz) {
8183
- if (curve.length === 0) return [];
8184
- const anchor = body.trailAnchor ?? body.position;
8185
- if (!anchor) return [];
8186
- const bodyLocal = [
8187
- anchor[0] - cx,
8188
- anchor[1] - cy,
8189
- anchor[2] - cz
8190
- ];
8192
+ function nearestStart(curve, at) {
8191
8193
  let nearest = 0;
8192
8194
  let best = Infinity;
8193
8195
  for (let j = 0; j < curve.length; j++) {
8194
- const d = (curve[j][0] - bodyLocal[0]) ** 2 + (curve[j][1] - bodyLocal[1]) ** 2 + (curve[j][2] - bodyLocal[2]) ** 2;
8196
+ const d = (curve[j][0] - at[0]) ** 2 + (curve[j][1] - at[1]) ** 2 + (curve[j][2] - at[2]) ** 2;
8195
8197
  if (d < best) {
8196
8198
  best = d;
8197
8199
  nearest = j;
@@ -8199,7 +8201,18 @@ function buildTrailPoints(body, curve, isOpenCurve, cx, cy, cz) {
8199
8201
  }
8200
8202
  const prev = Math.max(nearest - 1, 0);
8201
8203
  const next = Math.min(nearest + 1, curve.length - 1);
8202
- const trailStart = (curve[prev][0] - bodyLocal[0]) ** 2 + (curve[prev][1] - bodyLocal[1]) ** 2 + (curve[prev][2] - bodyLocal[2]) ** 2 < (curve[next][0] - bodyLocal[0]) ** 2 + (curve[next][1] - bodyLocal[1]) ** 2 + (curve[next][2] - bodyLocal[2]) ** 2 ? prev : nearest;
8204
+ return (curve[prev][0] - at[0]) ** 2 + (curve[prev][1] - at[1]) ** 2 + (curve[prev][2] - at[2]) ** 2 < (curve[next][0] - at[0]) ** 2 + (curve[next][1] - at[1]) ** 2 + (curve[next][2] - at[2]) ** 2 ? prev : nearest;
8205
+ }
8206
+ function buildTrailPoints(body, curve, isOpenCurve, cx, cy, cz, start) {
8207
+ if (curve.length === 0) return [];
8208
+ const anchor = body.trailAnchor ?? body.position;
8209
+ if (!anchor) return [];
8210
+ const bodyLocal = [
8211
+ anchor[0] - cx,
8212
+ anchor[1] - cy,
8213
+ anchor[2] - cz
8214
+ ];
8215
+ const trailStart = start ?? nearestStart(curve, bodyLocal);
8203
8216
  const points = [bodyLocal];
8204
8217
  if (isOpenCurve) for (let k = 0; k < 512 - 1; k++) {
8205
8218
  const idx = Math.max(trailStart - k, 0);
@@ -8295,12 +8308,16 @@ var ChunkLoader = class {
8295
8308
  const parentKey = `naif-${body.parentId}`;
8296
8309
  const objType = body.objectType;
8297
8310
  const primaryId = objType === ObjectType.MOON && body.parentId >= 1 && body.parentId <= 9 ? `naif-${body.parentId * 100 + 99}` : null;
8298
- const elements = chebyshevOsculatingElements(body, body.parentId, jd, primaryId ? cheb.body(primaryId, jd) ?? void 0 : void 0);
8311
+ const elementsAt = (record, at) => {
8312
+ if (!primaryId) return chebyshevOsculatingElements(record, record.parentId, at);
8313
+ const primary = cheb.body(primaryId, at);
8314
+ return primary ? chebyshevOsculatingElements(record, record.parentId, at, primary) : null;
8315
+ };
8316
+ const elements = elementsAt(body, jd);
8299
8317
  const ownId = body.id;
8300
8318
  const ownRederive = (newJd) => {
8301
8319
  const fresh = cheb.body(ownId, newJd);
8302
- if (!fresh) return null;
8303
- return chebyshevOsculatingElements(fresh, fresh.parentId, newJd, primaryId ? cheb.body(primaryId, newJd) ?? void 0 : void 0);
8320
+ return fresh ? elementsAt(fresh, newJd) : null;
8304
8321
  };
8305
8322
  const fallbackA = Math.hypot(offset[0], offset[1], offset[2]) / 10;
8306
8323
  const data = {
@@ -8950,6 +8967,20 @@ var ChunkWindow = class {
8950
8967
  done
8951
8968
  };
8952
8969
  }
8970
+ request(jd) {
8971
+ for (const [zone, params] of this.zoneParams) {
8972
+ const idx = chunkIndexForJd(params, jd);
8973
+ if (this.isLoadable(params, idx)) this.loadChunk(zone, params, idx, "high")?.catch(() => {});
8974
+ }
8975
+ }
8976
+ loadingAt(jd, zone) {
8977
+ if (this.inflight.size === 0) return false;
8978
+ for (const [name, params] of this.zoneParams) {
8979
+ if (zone !== void 0 && name !== zone) continue;
8980
+ if (this.inflight.has(chunkKey(name, chunkIndexForJd(params, jd)))) return true;
8981
+ }
8982
+ return false;
8983
+ }
8953
8984
  pendingCurrent(jd) {
8954
8985
  if (this.inflight.size === 0) return RESOLVED;
8955
8986
  let pending = null;
@@ -9021,6 +9052,10 @@ var ChebyshevStore = class extends ChunkWindow {
9021
9052
  end: params.end_jd
9022
9053
  };
9023
9054
  }
9055
+ isLoading(objectId, jd) {
9056
+ const zone = this.idToZone.get(objectId);
9057
+ return zone !== void 0 && this.loadingAt(jd, zone);
9058
+ }
9024
9059
  resolve(objectId, jd) {
9025
9060
  const zone = this.idToZone.get(objectId);
9026
9061
  if (zone === void 0) return null;
@@ -10059,6 +10094,7 @@ var RENDER_TIER_PRESETS = {
10059
10094
  bloomScale: .5,
10060
10095
  pointBudget: 4e5,
10061
10096
  maxTextureTier: "medium",
10097
+ tileWindows: 3,
10062
10098
  maxSkyboxFace: 2048
10063
10099
  },
10064
10100
  medium: {
@@ -10068,6 +10104,7 @@ var RENDER_TIER_PRESETS = {
10068
10104
  bloomScale: .75,
10069
10105
  pointBudget: 9e5,
10070
10106
  maxTextureTier: "medium",
10107
+ tileWindows: 4,
10071
10108
  maxSkyboxFace: 2048
10072
10109
  },
10073
10110
  high: {
@@ -10077,6 +10114,7 @@ var RENDER_TIER_PRESETS = {
10077
10114
  bloomScale: 1,
10078
10115
  pointBudget: Infinity,
10079
10116
  maxTextureTier: "high",
10117
+ tileWindows: 6,
10080
10118
  maxSkyboxFace: Infinity
10081
10119
  }
10082
10120
  };
@@ -10084,6 +10122,10 @@ function maxTextureTier() {
10084
10122
  if (isLowEndDevice()) return "low";
10085
10123
  return currentRenderTier().maxTextureTier;
10086
10124
  }
10125
+ function maxTileWindows() {
10126
+ if (isLowEndDevice()) return 0;
10127
+ return currentRenderTier().tileWindows;
10128
+ }
10087
10129
  function resolveRenderTier() {
10088
10130
  const measured = sceneSettings().atmosphereCalibration?.tier;
10089
10131
  if (measured === "low" || measured === "medium") return measured;
@@ -10342,7 +10384,7 @@ function dedupeById(bodies) {
10342
10384
  async function loadScene(ctx, date, targetId) {
10343
10385
  performance.mark("sm-load-start");
10344
10386
  loadProgress.reset();
10345
- loadSystemsGlobal().catch((e) => console.warn("scene-load: systems-global (GMs/nutation) failed to load:", e));
10387
+ const systemsGlobalPromise = loadSystemsGlobal().catch((e) => console.warn("scene-load: systems-global (GMs/nutation) failed to load:", e));
10346
10388
  const atmospheresPromise = loadAtmospheres().catch((e) => console.warn("scene-load: atmospheres failed to load — rendering without shells:", e));
10347
10389
  const jd = dateToJD(date);
10348
10390
  const metadataPromise = fetchMetadata();
@@ -10374,6 +10416,7 @@ async function loadScene(ctx, date, targetId) {
10374
10416
  loadProgress.reach("ephemeris");
10375
10417
  const loader = new ChunkLoader(ctx.chebStore);
10376
10418
  if (ctx.probeStore) ctx.probeStore.fitCenterUsable = (id) => (ctx.chebStore?.has(id) ?? false) || ctx.getBody(id) !== void 0;
10419
+ await systemsGlobalPromise;
10377
10420
  const major = await loadMajorBodies(ctx, loader, metadata, date, jd, await passengerPromise);
10378
10421
  loadProgress.reach("majors");
10379
10422
  await atmospheresPromise;
@@ -15429,6 +15472,491 @@ function detachSelfShadow(uniforms) {
15429
15472
  uniforms.uSelfScale.value = 0;
15430
15473
  }
15431
15474
 
15475
+ //#endregion
15476
+ //#region src/lib/scene/lod/tile-window.ts
15477
+ var WINDOW_TILES = 4;
15478
+ var MAP = 0;
15479
+ var NORTH = 1;
15480
+ var SOUTH = 2;
15481
+ var CHART_FOLDER = [
15482
+ "",
15483
+ "north",
15484
+ "south"
15485
+ ];
15486
+ var FIRST_TILE_LEVEL = 3;
15487
+ var HOLD_TILES = .75;
15488
+ var BASE_WIDTH_PX = 2048;
15489
+ var CAP_EDGE = Math.SQRT2 - 1;
15490
+ var CAP_ENTER_DEG = 62;
15491
+ var CAP_LEAVE_DEG = 58;
15492
+ var BOTH_CHARTS_DEG = 45;
15493
+ function chartColumns(chart, level) {
15494
+ return chart === 0 ? 2 << level : 1 << level - 1;
15495
+ }
15496
+ function chartRows(chart, level) {
15497
+ return chart === 0 ? 1 << level : 1 << level - 1;
15498
+ }
15499
+ function neededLevel(radiusScene, dist, projScale, maxLevel) {
15500
+ const height = Math.max(dist - radiusScene, radiusScene * 1e-6);
15501
+ const widthPx = 2 * Math.PI * radiusScene * projScale / height;
15502
+ return Math.min(Math.ceil(Math.log2(widthPx / BASE_WIDTH_PX)), maxLevel);
15503
+ }
15504
+ function chartPoint(chart, x, y, z) {
15505
+ if (chart === 0) {
15506
+ const east = Math.atan2(z, -x) / (2 * Math.PI);
15507
+ return {
15508
+ x: east < 0 ? east + 1 : east,
15509
+ y: Math.acos(Math.min(Math.max(y, -1), 1)) / Math.PI
15510
+ };
15511
+ }
15512
+ const side = chart === 1 ? 1 : -1;
15513
+ const scale = .5 / (CAP_EDGE * (1 + side * y));
15514
+ return {
15515
+ x: .5 + x * scale,
15516
+ y: .5 + side * z * scale
15517
+ };
15518
+ }
15519
+ function cameraChart(y, previous) {
15520
+ const cap = y >= 0 ? 1 : 2;
15521
+ return Math.asin(Math.min(Math.abs(y), 1)) * 180 / Math.PI > (previous === cap ? CAP_LEAVE_DEG : CAP_ENTER_DEG) ? cap : 0;
15522
+ }
15523
+ function windowChain(level, count, chart, y) {
15524
+ const chain = [];
15525
+ for (let z = level; z >= 3 && chain.length < count; z--) chain.push({
15526
+ chart,
15527
+ level: z
15528
+ });
15529
+ const reached = chain.length > 0 && chain[chain.length - 1].level === 3;
15530
+ const both = Math.abs(y) > Math.sin(BOTH_CHARTS_DEG * Math.PI / 180);
15531
+ if (reached && both && chain.length < count) {
15532
+ const cap = y >= 0 ? 1 : 2;
15533
+ chain.push({
15534
+ chart: chart === 0 ? cap : 0,
15535
+ level: 3
15536
+ });
15537
+ }
15538
+ return chain;
15539
+ }
15540
+ function placeAxis(at, count, wraps, kept) {
15541
+ const last = Math.max(count - 4, 0);
15542
+ if (kept !== void 0) {
15543
+ let off = at - (kept + 4 / 2);
15544
+ if (wraps) off -= count * Math.round(off / count);
15545
+ if (!wraps && (kept === 0 && off < 0 || kept === last && off > 0) || Math.abs(off) <= HOLD_TILES) return kept;
15546
+ }
15547
+ const first = Math.round(at - 4 / 2);
15548
+ return wraps ? (first % count + count) % count : Math.min(Math.max(first, 0), last);
15549
+ }
15550
+ function placeWindow(chart, level, x, y, previous) {
15551
+ const columns = chartColumns(chart, level);
15552
+ const rows = chartRows(chart, level);
15553
+ const kept = previous?.chart === chart && previous.level === level ? previous : void 0;
15554
+ const tx0 = placeAxis(x * columns, columns, chart === 0, kept?.tx0);
15555
+ const ty0 = placeAxis(y * rows, rows, false, kept?.ty0);
15556
+ if (kept && kept.tx0 === tx0 && kept.ty0 === ty0) return kept;
15557
+ return {
15558
+ chart,
15559
+ level,
15560
+ tx0,
15561
+ ty0
15562
+ };
15563
+ }
15564
+ function slotTile(placement, sx, sy) {
15565
+ const columns = chartColumns(placement.chart, placement.level);
15566
+ const dx = ((sx - placement.tx0) % 4 + 4) % 4;
15567
+ const dy = ((sy - placement.ty0) % 4 + 4) % 4;
15568
+ return {
15569
+ x: (placement.tx0 + dx) % columns,
15570
+ y: placement.ty0 + dy
15571
+ };
15572
+ }
15573
+
15574
+ //#endregion
15575
+ //#region src/lib/scene/objects/surface/tile-overlay.ts
15576
+ var MAX_WINDOWS = 6;
15577
+ var MAX_DRAWN = MAX_WINDOWS + 1;
15578
+ var MAX_INFLIGHT = 6;
15579
+ var MAX_FAILURES = 4;
15580
+ var FETCH_TIMEOUT_S = 30;
15581
+ var IDLE_DISPOSE_S = 5;
15582
+ var FEATHER_TILES = .5;
15583
+ var MAX_ANISOTROPY = 16;
15584
+ var SLOTS = 4 * 4;
15585
+ var PARS = `
15586
+ varying vec3 vTileDir;
15587
+ uniform sampler2DArray uTileMap;
15588
+ uniform sampler2DArray uTileMask;
15589
+ uniform vec4 uTileGrid[ ${MAX_DRAWN} ];
15590
+ uniform vec4 uTileWindow[ ${MAX_DRAWN} ];
15591
+ // Most taps of the anisotropic filter, and a tile's side in pixels.
15592
+ uniform vec2 uTileFilter;
15593
+
15594
+ // Long and short half-axes of the footprint whose sides are fx and fy. The
15595
+ // two sides alone would do where the stretch runs along the screen's axes;
15596
+ // around a pole it runs every way, and each side then holds most of it.
15597
+ void tileAxes( vec2 fx, vec2 fy, out vec2 major, out vec2 minor ) {
15598
+ float p = fx.x * fx.x + fy.x * fy.x;
15599
+ float q = fx.y * fx.y + fy.y * fy.y;
15600
+ float r = fx.x * fx.y + fy.x * fy.y;
15601
+ float det = fx.x * fy.y - fx.y * fy.x;
15602
+ float hi = 0.5 * ( p + q ) + sqrt( 0.25 * ( p - q ) * ( p - q ) + r * r );
15603
+ float lo = det * det / max( hi, 1e-30 );
15604
+ vec2 axis = p >= q ? vec2( hi - q, r ) : vec2( r, hi - p );
15605
+ float len = length( axis );
15606
+ axis = len > 0.0 ? axis / len : vec2( 1.0, 0.0 );
15607
+ major = axis * sqrt( hi );
15608
+ minor = vec2( - axis.y, axis.x ) * sqrt( lo );
15609
+ }
15610
+
15611
+ vec3 tileColour( vec3 below ) {
15612
+ if ( uTileWindow[ 0 ].w < 0.5 ) return below;
15613
+
15614
+ // Where the fragment is on each chart, as fractions from the chart's
15615
+ // north-west corner, and what a screen pixel spans there. Both come from
15616
+ // the direction of the fragment rather than its interpolated coordinates:
15617
+ // those have one slope per triangle, which near a pole differs enough
15618
+ // between the two triangles of a grid cell to pick different levels, and
15619
+ // they jump at the 180° meridian.
15620
+ vec3 dir = normalize( vTileDir );
15621
+ vec3 ddx = dFdx( dir );
15622
+ vec3 ddy = dFdy( dir );
15623
+
15624
+ float flat2 = max( dir.x * dir.x + dir.z * dir.z, 1e-12 );
15625
+ float east = 1.0 / ( 2.0 * PI * flat2 );
15626
+ float south = - 1.0 / ( PI * sqrt( flat2 ) );
15627
+ vec2 mapAt = vec2( fract( atan( dir.z, - dir.x ) / ( 2.0 * PI ) ), acos( clamp( dir.y, - 1.0, 1.0 ) ) / PI );
15628
+ vec2 mapDx = vec2( ( dir.z * ddx.x - dir.x * ddx.z ) * east, ddx.y * south );
15629
+ vec2 mapDy = vec2( ( dir.z * ddy.x - dir.x * ddy.z ) * east, ddy.y * south );
15630
+
15631
+ // The cap of the fragment's own hemisphere, seen from above its pole.
15632
+ float side = dir.y < 0.0 ? - 1.0 : 1.0;
15633
+ float fold = 1.0 + side * dir.y;
15634
+ vec2 plane = vec2( dir.x, side * dir.z );
15635
+ float capScale = 0.5 / ( ${CAP_EDGE.toFixed(8)} * fold );
15636
+ vec2 capAt = 0.5 + plane * capScale;
15637
+ vec2 capDx = ( vec2( ddx.x, side * ddx.z ) - plane * ( side * ddx.y / fold ) ) * capScale;
15638
+ vec2 capDy = ( vec2( ddy.x, side * ddy.z ) - plane * ( side * ddy.y / fold ) ) * capScale;
15639
+
15640
+ float rows = ${4 .toFixed(1)};
15641
+ vec3 colour = vec3( 0.0 );
15642
+ float covered = 0.0;
15643
+ for ( int i = 0; i < ${MAX_DRAWN}; i ++ ) {
15644
+ vec4 window = uTileWindow[ i ];
15645
+ if ( window.w < 0.5 ) break;
15646
+ vec4 grid = uTileGrid[ i ];
15647
+ bool onMap = window.x < 0.5;
15648
+ // The other pole's cap.
15649
+ if ( ! onMap && ( window.x < 1.5 ) != ( side > 0.0 ) ) continue;
15650
+ vec2 tile = ( onMap ? mapAt : capAt ) * grid.xy;
15651
+ vec2 dx = ( onMap ? mapDx : capDx ) * grid.xy * uTileFilter.y;
15652
+ vec2 dy = ( onMap ? mapDy : capDy ) * grid.xy * uTileFilter.y;
15653
+
15654
+ // Level this fragment asks of the window, from the short axis of its
15655
+ // footprint in the window's pixels: the long axis is the anisotropic
15656
+ // filter's to cover, as far as its taps reach. The window fades in
15657
+ // over the level below its own, as a mipmap would.
15658
+ vec2 major;
15659
+ vec2 minor;
15660
+ tileAxes( dx, dy, major, minor );
15661
+ float foot = max( length( minor ), length( major ) / uTileFilter.x );
15662
+ float weight = clamp( 1.0 - log2( max( foot, 1e-6 ) ), 0.0, 1.0 );
15663
+ if ( weight <= 0.0 ) continue;
15664
+
15665
+ // Half a pixel in from a pole of the map: past it the filter would
15666
+ // read the far row.
15667
+ if ( onMap ) tile.y = clamp( tile.y, 0.0005, grid.y - 0.0005 );
15668
+ // Offset from the window's north-west corner; the map's longitude wraps.
15669
+ vec2 rel = tile - grid.zw;
15670
+ if ( onMap ) rel.x = mod( rel.x, grid.x );
15671
+ if ( rel.x < 0.0 || rel.x > rows || rel.y < 0.0 || rel.y > rows ) continue;
15672
+ // Distance to the window's edge, in tiles. A pole of the map is no
15673
+ // edge: nothing lies past it.
15674
+ vec2 near = min( rel, rows - rel );
15675
+ if ( onMap && grid.w < 0.5 ) near.y = rows - rel.y;
15676
+ if ( onMap && grid.w + rows > grid.y - 0.5 ) near.y = grid.w < 0.5 ? rows : rel.y;
15677
+ float edge = min( near.x, near.y );
15678
+
15679
+ ivec3 slot = ivec3( int( mod( tile.x, rows ) ), int( mod( tile.y, rows ) ), int( window.y ) );
15680
+ float alpha = weight * texelFetch( uTileMask, slot, 0 ).r * smoothstep( 0.0, ${FEATHER_TILES.toFixed(2)}, edge );
15681
+ if ( alpha <= 0.0 ) continue;
15682
+
15683
+ // The texture repeats every window: a tile sits in the slot its own
15684
+ // column and row pick, north up.
15685
+ vec3 st = vec3( tile.x / rows, - tile.y / rows, window.y );
15686
+ vec2 texel = vec2( 1.0, - 1.0 ) / ( rows * uTileFilter.y );
15687
+ colour += ( 1.0 - covered ) * alpha * textureGrad( uTileMap, st, major * texel, minor * texel ).rgb;
15688
+ covered += ( 1.0 - covered ) * alpha;
15689
+ if ( covered > 0.995 ) break;
15690
+ }
15691
+ return colour + ( 1.0 - covered ) * below;
15692
+ }
15693
+ `;
15694
+ var MAP_FRAGMENT = `
15695
+ #ifdef USE_MAP
15696
+ vec4 sampledDiffuseColor = texture2D( map, vMapUv );
15697
+ sampledDiffuseColor.rgb = tileColour( sampledDiffuseColor.rgb );
15698
+ diffuseColor *= sampledDiffuseColor;
15699
+ #endif
15700
+ `;
15701
+ function createOverlay(material) {
15702
+ const overlay = {
15703
+ material,
15704
+ uniforms: {
15705
+ uTileMap: { value: null },
15706
+ uTileMask: { value: null },
15707
+ uTileGrid: { value: Array.from({ length: MAX_DRAWN }, () => new Vector4()) },
15708
+ uTileWindow: { value: Array.from({ length: MAX_DRAWN }, () => new Vector4()) },
15709
+ uTileFilter: { value: new Vector2(1, 1) }
15710
+ },
15711
+ inflight: 0,
15712
+ failures: 0,
15713
+ failed: /* @__PURE__ */ new Set(),
15714
+ dead: false,
15715
+ lastUsed: 0
15716
+ };
15717
+ chainShaderHook(material, "surfaceTiles", (shader) => {
15718
+ Object.assign(shader.uniforms, overlay.uniforms);
15719
+ shader.vertexShader = shader.vertexShader.replace("#include <common>", "#include <common>\nvarying vec3 vTileDir;").replace("#include <begin_vertex>", "#include <begin_vertex>\nvTileDir = position;");
15720
+ shader.fragmentShader = shader.fragmentShader.replace("#include <map_pars_fragment>", `#include <map_pars_fragment>\n${PARS}`).replace("#include <map_fragment>", MAP_FRAGMENT);
15721
+ });
15722
+ return overlay;
15723
+ }
15724
+ function createStore(tileSize, layers, renderer) {
15725
+ const size = tileSize * 4;
15726
+ const texture = new DataArrayTexture(null, size, size, layers);
15727
+ texture.source.dataReady = false;
15728
+ texture.format = RGBAFormat;
15729
+ texture.type = UnsignedByteType;
15730
+ texture.colorSpace = SRGBColorSpace;
15731
+ texture.wrapS = texture.wrapT = RepeatWrapping;
15732
+ texture.minFilter = texture.magFilter = LinearFilter;
15733
+ texture.generateMipmaps = false;
15734
+ texture.anisotropy = Math.min(MAX_ANISOTROPY, renderer.capabilities.getMaxAnisotropy());
15735
+ texture.needsUpdate = true;
15736
+ const mask = new DataArrayTexture(new Uint8Array(SLOTS * layers), 4, 4, layers);
15737
+ mask.format = RedFormat;
15738
+ mask.type = UnsignedByteType;
15739
+ mask.minFilter = mask.magFilter = NearestFilter;
15740
+ mask.unpackAlignment = 1;
15741
+ mask.needsUpdate = true;
15742
+ return {
15743
+ texture,
15744
+ mask,
15745
+ windows: [],
15746
+ free: Array.from({ length: layers }, (_, layer) => layer),
15747
+ tileSize
15748
+ };
15749
+ }
15750
+ function setLoaded(store, layer, slot, loaded) {
15751
+ store.mask.image.data[layer * SLOTS + slot] = loaded ? 255 : 0;
15752
+ store.mask.needsUpdate = true;
15753
+ }
15754
+ function isLoaded(store, layer, slot) {
15755
+ return store.mask.image.data[layer * SLOTS + slot] !== 0;
15756
+ }
15757
+ function retarget(store, win, placement, frame) {
15758
+ if (win.placement === placement && win.frame === frame) return;
15759
+ win.placement = placement;
15760
+ win.frame = frame;
15761
+ for (let slot = 0; slot < SLOTS; slot++) {
15762
+ const { x, y } = slotTile(placement, slot % 4, Math.floor(slot / 4));
15763
+ const key = `${frame ?? 0}/${win.chart}/${win.level}/${x}/${y}`;
15764
+ if (win.wanted[slot] === key) continue;
15765
+ win.wanted[slot] = key;
15766
+ setLoaded(store, win.layer, slot, false);
15767
+ const stale = win.pending.get(slot);
15768
+ if (stale) {
15769
+ stale.abort.abort();
15770
+ win.pending.delete(slot);
15771
+ }
15772
+ }
15773
+ }
15774
+ function release(store, win) {
15775
+ for (const { abort } of win.pending.values()) abort.abort();
15776
+ win.pending.clear();
15777
+ for (let slot = 0; slot < SLOTS; slot++) setLoaded(store, win.layer, slot, false);
15778
+ store.free.push(win.layer);
15779
+ }
15780
+ function settled(overlay, store, win) {
15781
+ for (let slot = 0; slot < SLOTS; slot++) {
15782
+ const key = win.wanted[slot];
15783
+ if (!key) return false;
15784
+ if (!isLoaded(store, win.layer, slot) && !overlay.failed.has(key)) return false;
15785
+ }
15786
+ return true;
15787
+ }
15788
+ async function fetchTile(url, signal) {
15789
+ const response = await fetch(url, { signal });
15790
+ if (!response.ok) throw new Error(`tile ${response.status}`);
15791
+ return createImageBitmap(await response.blob(), {
15792
+ imageOrientation: "flipY",
15793
+ premultiplyAlpha: "none",
15794
+ colorSpaceConversion: "none"
15795
+ });
15796
+ }
15797
+ var copyAt = new Vector3();
15798
+ var queue$1 = [];
15799
+ function pump(overlay, store, tiles, win, x, y, renderer, invalidate) {
15800
+ const placement = win.placement;
15801
+ if (!placement) return;
15802
+ const columns = chartColumns(win.chart, win.level);
15803
+ const cx = x * columns;
15804
+ const cy = y * chartRows(win.chart, win.level);
15805
+ queue$1.length = 0;
15806
+ for (let slot = 0; slot < SLOTS; slot++) {
15807
+ const key = win.wanted[slot];
15808
+ if (!key || win.pending.has(slot) || overlay.failed.has(key)) continue;
15809
+ if (isLoaded(store, win.layer, slot)) continue;
15810
+ const tile = slotTile(placement, slot % 4, Math.floor(slot / 4));
15811
+ let dx = tile.x + .5 - cx;
15812
+ if (win.chart === 0) dx -= columns * Math.round(dx / columns);
15813
+ queue$1.push({
15814
+ slot,
15815
+ far: Math.hypot(dx, tile.y + .5 - cy)
15816
+ });
15817
+ }
15818
+ queue$1.sort((a, b) => a.far - b.far);
15819
+ for (const { slot } of queue$1) {
15820
+ if (overlay.inflight >= MAX_INFLIGHT) return;
15821
+ const key = win.wanted[slot];
15822
+ const sx = slot % 4;
15823
+ const sy = Math.floor(slot / 4);
15824
+ const tile = slotTile(placement, sx, sy);
15825
+ const abort = new AbortController();
15826
+ let timedOut = false;
15827
+ const timer = setTimeout(() => {
15828
+ timedOut = true;
15829
+ abort.abort();
15830
+ }, FETCH_TIMEOUT_S * 1e3);
15831
+ win.pending.set(slot, {
15832
+ key,
15833
+ abort
15834
+ });
15835
+ overlay.inflight++;
15836
+ const folder = CHART_FOLDER[win.chart];
15837
+ fetchTile(tileUrl(tiles, folder, win.level, tile.x, tile.y, win.frame), abort.signal).then((bitmap) => {
15838
+ if (win.wanted[slot] === key && !abort.signal.aborted) {
15839
+ const source = new Texture(bitmap);
15840
+ source.colorSpace = SRGBColorSpace;
15841
+ source.flipY = false;
15842
+ source.generateMipmaps = false;
15843
+ copyAt.set(sx * store.tileSize, (4 - 1 - sy) * store.tileSize, win.layer);
15844
+ renderer.copyTextureToTexture(source, store.texture, null, copyAt);
15845
+ source.dispose();
15846
+ setLoaded(store, win.layer, slot, true);
15847
+ overlay.failures = 0;
15848
+ invalidate();
15849
+ }
15850
+ bitmap.close();
15851
+ }).catch(() => {
15852
+ if (abort.signal.aborted && !timedOut) return;
15853
+ overlay.failed.add(key);
15854
+ if (++overlay.failures >= MAX_FAILURES) overlay.dead = true;
15855
+ }).finally(() => {
15856
+ clearTimeout(timer);
15857
+ overlay.inflight--;
15858
+ if (win.pending.get(slot)?.key === key) win.pending.delete(slot);
15859
+ });
15860
+ }
15861
+ }
15862
+ function switchOff(overlay) {
15863
+ for (const link of overlay.uniforms.uTileWindow.value) link.w = 0;
15864
+ }
15865
+ function rest(bo, overlay, now) {
15866
+ switchOff(overlay);
15867
+ if (now - overlay.lastUsed > IDLE_DISPOSE_S * 1e3) disposeTileOverlay(bo);
15868
+ }
15869
+ function disposeTileOverlay(bo) {
15870
+ const overlay = bo.tileOverlay;
15871
+ if (!overlay) return;
15872
+ switchOff(overlay);
15873
+ const store = overlay.store;
15874
+ if (!store) return;
15875
+ for (const win of store.windows) for (const { abort } of win.pending.values()) abort.abort();
15876
+ store.texture.dispose();
15877
+ store.mask.dispose();
15878
+ overlay.store = void 0;
15879
+ overlay.uniforms.uTileMap.value = null;
15880
+ overlay.uniforms.uTileMask.value = null;
15881
+ }
15882
+ function tilesServe(bo) {
15883
+ const tiles = bo.surfaceTiles;
15884
+ return !!tiles && tiles.max_level >= 3 && tilesBase() !== "" && maxTileWindows() > 0 && !bo.tileOverlay?.dead && textureAllowed(tiles.distribution);
15885
+ }
15886
+ var inverse = new Matrix4();
15887
+ var over = new Vector3();
15888
+ function updateTileOverlay(bo, camera, renderer, projScale, drawable, invalidate) {
15889
+ const tiles = bo.surfaceTiles;
15890
+ const mesh = bo.mesh;
15891
+ const material = mesh?.material;
15892
+ const level = tiles && mesh && material?.map && drawable && tilesServe(bo) ? neededLevel(bo.radiusScene, bo.cachedDist, projScale, tiles.max_level) : -1;
15893
+ const now = performance.now();
15894
+ if (!tiles || !mesh || !material || level < 3) {
15895
+ if (bo.tileOverlay) rest(bo, bo.tileOverlay, now);
15896
+ return;
15897
+ }
15898
+ if (bo.tileOverlay && bo.tileOverlay.material !== material) {
15899
+ disposeTileOverlay(bo);
15900
+ bo.tileOverlay = void 0;
15901
+ }
15902
+ const overlay = bo.tileOverlay ??= createOverlay(material);
15903
+ overlay.lastUsed = now;
15904
+ const levels = tiles.max_level - 3 + 1;
15905
+ const count = Math.min(maxTileWindows(), MAX_WINDOWS, levels + 1);
15906
+ if (overlay.store && overlay.store.texture.image.depth !== count + 1) disposeTileOverlay(bo);
15907
+ const store = overlay.store ??= createStore(tiles.tile_size, count + 1, renderer);
15908
+ overlay.uniforms.uTileMap.value = store.texture;
15909
+ overlay.uniforms.uTileMask.value = store.mask;
15910
+ overlay.uniforms.uTileFilter.value.set(store.texture.anisotropy, tiles.tile_size);
15911
+ over.copy(camera.position).applyMatrix4(inverse.copy(mesh.matrixWorld).invert()).normalize();
15912
+ const cap = over.y >= 0 ? 1 : 2;
15913
+ overlay.chart = cameraChart(over.y, overlay.chart);
15914
+ const chain = windowChain(level, count, overlay.chart, over.y);
15915
+ const frame = tiles.frames ? bo.textureFrame : void 0;
15916
+ const held = (link) => store.windows.find((win) => win.chart === link.chart && win.level === link.level);
15917
+ store.windows = store.windows.filter((win) => {
15918
+ win.rank = chain.findIndex((link) => link.chart === win.chart && link.level === win.level);
15919
+ if (win.rank >= 0) return true;
15920
+ win.rank = chain.length;
15921
+ const heir = chain.find((link) => link.level === win.level);
15922
+ const heirWindow = heir && held(heir);
15923
+ const near = win.chart === 0 || win.chart === cap;
15924
+ if (heir && near && !(heirWindow && settled(overlay, store, heirWindow))) return true;
15925
+ release(store, win);
15926
+ return false;
15927
+ });
15928
+ for (let i = chain.length - 1; i >= 0; i--) {
15929
+ const link = chain[i];
15930
+ let win = held(link);
15931
+ if (!win) {
15932
+ const layer = store.free.pop();
15933
+ if (layer === void 0) continue;
15934
+ win = {
15935
+ layer,
15936
+ ...link,
15937
+ rank: i,
15938
+ wanted: new Array(SLOTS),
15939
+ pending: /* @__PURE__ */ new Map()
15940
+ };
15941
+ store.windows.push(win);
15942
+ }
15943
+ const point = chartPoint(link.chart, over.x, over.y, over.z);
15944
+ retarget(store, win, placeWindow(link.chart, link.level, point.x, point.y, win.placement), frame);
15945
+ pump(overlay, store, tiles, win, point.x, point.y, renderer, invalidate);
15946
+ }
15947
+ store.windows.sort((a, b) => b.level - a.level || a.rank - b.rank);
15948
+ for (let i = 0; i < MAX_DRAWN; i++) {
15949
+ const win = store.windows[i];
15950
+ const link = overlay.uniforms.uTileWindow.value[i];
15951
+ if (!win?.placement) {
15952
+ link.w = 0;
15953
+ continue;
15954
+ }
15955
+ overlay.uniforms.uTileGrid.value[i].set(chartColumns(win.chart, win.level), chartRows(win.chart, win.level), win.placement.tx0, win.placement.ty0);
15956
+ link.set(win.chart, win.layer, win.level, 1);
15957
+ }
15958
+ }
15959
+
15432
15960
  //#endregion
15433
15961
  //#region src/lib/scene/objects/body/lifecycle.ts
15434
15962
  function disposeMaterial(mat) {
@@ -15562,6 +16090,8 @@ function downgradeBodyMesh(bo, scene, clickables, meshToBody) {
15562
16090
  detachSelfShadow(bo.selfShadow);
15563
16091
  bo.selfShadow = null;
15564
16092
  }
16093
+ disposeTileOverlay(bo);
16094
+ bo.tileOverlay = void 0;
15565
16095
  bo.terrainWindow = null;
15566
16096
  mesh.removeFromParent();
15567
16097
  mesh.geometry.dispose();
@@ -16269,6 +16799,7 @@ async function loadBodyTexture(bo, textureLoader, currentJd, scene, maxTextureSi
16269
16799
  if (!sceneSettings().showSurfaceTexture) return;
16270
16800
  if (!surface) return;
16271
16801
  bo.textureBundleId = surface.id === bodyId ? void 0 : surface.id;
16802
+ bo.surfaceTiles = surface.tiles;
16272
16803
  bo.availableTiers ??= surface.tiers ?? [...TIER_NAMES];
16273
16804
  bo.availableFrames = surface.frames;
16274
16805
  await swapBodyTexture(bo, "low", textureFrameForJd(currentJd, bo.availableFrames), textureLoader);
@@ -16288,6 +16819,7 @@ function unloadBodyTexture(bo) {
16288
16819
  bo.textureLoadGen = (bo.textureLoadGen ?? 0) + 1;
16289
16820
  const material = bo.mesh.material;
16290
16821
  unloadSiblingLayer("topography", bo);
16822
+ disposeTileOverlay(bo);
16291
16823
  if (!material.map) return;
16292
16824
  material.map.dispose();
16293
16825
  material.map = null;
@@ -17631,9 +18163,11 @@ function makeTrail(body, color, basisPos = [
17631
18163
  const { orbitElements, orbitCenter, data } = body;
17632
18164
  let curve;
17633
18165
  let isOpenCurve;
18166
+ let start;
17634
18167
  if (data.satrec) {
17635
18168
  curve = sgp4Curve(data.satrec, jd, data.n / 360, 512);
17636
18169
  isOpenCurve = true;
18170
+ start = curve.length - 1;
17637
18171
  } else {
17638
18172
  if (!orbitElements) throw new Error("makeTrail called without orbitElements");
17639
18173
  curve = null;
@@ -17643,7 +18177,7 @@ function makeTrail(body, color, basisPos = [
17643
18177
  const cy = orbitCenter?.[1] ?? 0;
17644
18178
  const cz = orbitCenter?.[2] ?? 0;
17645
18179
  const useTrail = isOpenCurve || data.objectType === ObjectType.DWARF_PLANET || data.objectType === ObjectType.MOON || data.objectType === ObjectType.SPACECRAFT || data.objectType === ObjectType.COMET || isAsteroid(data.objectType);
17646
- const validPoints = curve ? buildTrailPoints(body, curve, isOpenCurve, cx, cy, cz) : [];
18180
+ const validPoints = curve ? buildTrailPoints(body, curve, isOpenCurve, cx, cy, cz, start) : [];
17647
18181
  if (curve && validPoints.length < 2) return makeEmptyTrail();
17648
18182
  const bufferCapacity = curve ? Math.max(validPoints.length, curve.length) : 512 + 1;
17649
18183
  const fullAlphas = new Float32Array(bufferCapacity);
@@ -17823,6 +18357,7 @@ async function loadSystemData(barycenterId, bodyObjects, scene, textureLoader, c
17823
18357
  bo.availableTiers = surface.tiers;
17824
18358
  bo.availableFrames = surface.frames;
17825
18359
  bo.textureBundleId = surface.id === bodyId ? void 0 : surface.id;
18360
+ bo.surfaceTiles = surface.tiles;
17826
18361
  if (!bo.textureTier) {
17827
18362
  const frame = textureFrameForJd(currentJd, bo.availableFrames);
17828
18363
  promises.push(loadBodyTextureTier(bo, "low", frame, textureLoader));
@@ -19650,15 +20185,17 @@ function refreshTrail(body, line, basisPos, jd, view) {
19650
20185
  const useTrail = line.userData.useTrail;
19651
20186
  const oc = line.userData.orbitCenter;
19652
20187
  const cx = oc.x, cy = oc.y, cz = oc.z;
20188
+ let windowStart;
19653
20189
  if (body.data.satrec) {
19654
20190
  const meanMotion = body.data.n / 360;
19655
20191
  const stepDays = meanMotion > 0 ? 1 / meanMotion / 512 : 0;
19656
- const curveJd = line.userData.curveJd;
20192
+ let curveJd = line.userData.curveJd;
19657
20193
  if (curve.length === 0 || curveJd === void 0 || Math.abs(jd - curveJd) >= stepDays) {
19658
20194
  curve = sgp4Curve(body.data.satrec, jd, meanMotion, 512);
19659
20195
  line.userData.sourceCurve = curve;
19660
- line.userData.curveJd = jd;
20196
+ line.userData.curveJd = curveJd = jd;
19661
20197
  }
20198
+ windowStart = curve.length - (jd < curveJd ? 2 : 1);
19662
20199
  } else if (body.orbitElements) {
19663
20200
  if (body.rederiveElements) {
19664
20201
  const elementsJd = line.userData.elementsJd ?? jd;
@@ -19697,7 +20234,7 @@ function refreshTrail(body, line, basisPos, jd, view) {
19697
20234
  const headY = anchor[1] - cy;
19698
20235
  const headZ = anchor[2] - cz;
19699
20236
  if (withinTolerance(ud, curve, offX, offY, offZ, headX, headY, headZ, view)) return;
19700
- const validPoints = buildTrailPoints(body, curve, isOpenCurve, cx, cy, cz);
20237
+ const validPoints = buildTrailPoints(body, curve, isOpenCurve, cx, cy, cz, windowStart);
19701
20238
  if (validPoints.length < 2) return;
19702
20239
  const { posArr, trailArr, fullArr, capacity } = getTrailWorkingArrays(line);
19703
20240
  const bx = cx - basisPos[0], by = cy - basisPos[1], bz = cz - basisPos[2];
@@ -20998,11 +21535,10 @@ function updatePositions(params) {
20998
21535
  setVec(focus.focusTargetWorld, p);
20999
21536
  const camOff = focus.camTargetOffset;
21000
21537
  if (camOff && focus.camTargetWorld) addVec(focus.camTargetWorld, p, camOff);
21001
- } else if (a.oldPos) {
21002
- focus.focusTruePos[0] += p[0] - a.oldPos[0];
21003
- focus.focusTruePos[1] += p[1] - a.oldPos[1];
21004
- focus.focusTruePos[2] += p[2] - a.oldPos[2];
21005
- setVec(focus.focusTargetWorld, focus.focusTruePos);
21538
+ } else if (a.oldPos) for (let k = 0; k < 3; k++) {
21539
+ const moved = p[k] - a.oldPos[k];
21540
+ focus.focusTruePos[k] += moved;
21541
+ focus.focusTargetWorld[k] += moved;
21006
21542
  }
21007
21543
  break;
21008
21544
  }
@@ -21025,8 +21561,10 @@ function updatePositions(params) {
21025
21561
  refreshTrail(bo.body, line, basis, jd, trailView);
21026
21562
  line.userData.refreshDeferred = false;
21027
21563
  }
21564
+ const focusLoading = !focusPos && why === "loading";
21028
21565
  return {
21029
- focusUnplaced: focusedBody !== void 0 && !focusPos,
21566
+ focusUnplaced: focusedBody !== void 0 && !focusPos && !focusLoading,
21567
+ focusLoading,
21030
21568
  anchorId
21031
21569
  };
21032
21570
  }
@@ -21364,10 +21902,11 @@ var Placer = class {
21364
21902
  ride = planRideMarkers([], void 0, () => void 0);
21365
21903
  rideJd = NaN;
21366
21904
  rideFocus;
21367
- constructor(ctx, bodyObjects, diagnostics) {
21905
+ constructor(ctx, bodyObjects, diagnostics, held = () => false) {
21368
21906
  this.ctx = ctx;
21369
21907
  this.bodyObjects = bodyObjects;
21370
21908
  this.diagnostics = diagnostics;
21909
+ this.held = held;
21371
21910
  }
21372
21911
  begin(jd, focusedId, frame) {
21373
21912
  this.passJd = jd;
@@ -21391,6 +21930,7 @@ var Placer = class {
21391
21930
  this.place(body, this.passJd);
21392
21931
  }
21393
21932
  place(body, jd) {
21933
+ if (jd !== this.passJd && this.held()) jd = this.passJd;
21394
21934
  const id = body.data.id;
21395
21935
  if (jd === this.passJd && this.settled.has(id)) return body.position;
21396
21936
  if (this.settling.has(id)) {
@@ -21403,11 +21943,14 @@ var Placer = class {
21403
21943
  if (jd === this.passJd) this.settled.add(id);
21404
21944
  return pos;
21405
21945
  }
21946
+ under(aboveId) {
21947
+ return (this.ctx.getBody(aboveId) ?? this.bodyObjects.get(aboveId)?.body)?.unplaced === "loading" ? "loading" : "parent";
21948
+ }
21406
21949
  seatFeature(body, jd) {
21407
21950
  const hostId = body.featureAnchor.hostId;
21408
21951
  const host = this.ctx.getBody(hostId);
21409
21952
  const hostPos = host && this.place(host, jd);
21410
- if (!host || !hostPos) return setUnplaced(body, "parent");
21953
+ if (!host || !hostPos) return setUnplaced(body, this.under(hostId));
21411
21954
  return seatFeatureBody(body, host, hostPos, this.bodyObjects.get(hostId), jd);
21412
21955
  }
21413
21956
  rideAt(jd) {
@@ -21438,8 +21981,9 @@ var Placer = class {
21438
21981
  if (d.orbitalSource === OrbitalSource.SPICE_PROBE) return this.settleProbe(body, bo, jd);
21439
21982
  const parentPos = this.at(d.parentId, jd);
21440
21983
  if (!parentPos) {
21441
- diagnostics.warnOnce("missing-parent", d.id, () => `place[${d.id}]: parent ${d.parentId} has no place`);
21442
- return setUnplaced(body, "parent");
21984
+ const why = this.under(d.parentId);
21985
+ if (why === "parent") diagnostics.warnOnce("missing-parent", d.id, () => `place[${d.id}]: parent ${d.parentId} has no place`);
21986
+ return setUnplaced(body, why);
21443
21987
  }
21444
21988
  diagnostics.clear("missing-parent", d.id);
21445
21989
  let offset;
@@ -21449,9 +21993,10 @@ var Placer = class {
21449
21993
  return this.finish(body, bo, parentPos[0] + offset[0], parentPos[1] + offset[1], parentPos[2] + offset[2], parentPos, jd);
21450
21994
  }
21451
21995
  chebMiss(id, jd) {
21452
- const coverage = this.ctx.chebStore.zoneCoverage(id);
21453
- const outside = coverage !== null && (jd < coverage.start || jd > coverage.end);
21454
- if (outside && jd === this.passJd) {
21996
+ const store = this.ctx.chebStore;
21997
+ if (store.isLoading(id, jd)) return "loading";
21998
+ const coverage = store.zoneCoverage(id);
21999
+ if (coverage !== null && (jd < coverage.start || jd > coverage.end) && jd === this.passJd) {
21455
22000
  this.noData.count++;
21456
22001
  if (coverage.start < this.noData.earliestStart) this.noData.earliestStart = coverage.start;
21457
22002
  if (coverage.end > this.noData.latestEnd) this.noData.latestEnd = coverage.end;
@@ -21460,7 +22005,7 @@ var Placer = class {
21460
22005
  const cov = coverage ? `[${coverage.start.toFixed(1)},${coverage.end.toFixed(1)}]` : "unknown";
21461
22006
  return `chebStore.positionScene[${id}] returned null at jd=${jd.toFixed(3)} (coverage=${cov})`;
21462
22007
  });
21463
- return outside ? "no-data" : "loading";
22008
+ return "no-data";
21464
22009
  }
21465
22010
  finish(body, bo, x, y, z, parentPos, jd) {
21466
22011
  const d = body.data;
@@ -21523,6 +22068,7 @@ var Placer = class {
21523
22068
  const zonePref = activeSysId ? (fitCenterNaif) => ctx.bodies.isInSystem(`naif-${fitCenterNaif}`, activeSysId) : void 0;
21524
22069
  const located = ctx.probeStore?.probeWithCenter(d.id, jd, zonePref) ?? null;
21525
22070
  if (!located) {
22071
+ if (ctx.probeStore?.loadingAt(jd)) return setUnplaced(body, "loading");
21526
22072
  setUnplaced(body, "no-data");
21527
22073
  diagnostics.warnOnce("probe-unavailable", d.id, () => {
21528
22074
  const reason = !ctx.probeStore ? "no ProbeStore" : "no zone has both a loaded chunk and a sub-chunk covering this jd";
@@ -21534,7 +22080,7 @@ var Placer = class {
21534
22080
  if (probeLanded && isLandedAt(located.probe, jd)) {
21535
22081
  const flyingFrameId = d.parentId;
21536
22082
  const landedRender = renderLandedProbe(d, located.probe, probeLanded, jd, (id) => this.at(id, jd), ctx, bodyObjects);
21537
- if (!landedRender) return setUnplaced(body, "parent");
22083
+ if (!landedRender) return setUnplaced(body, this.under(`naif-${probeLanded.bodyNaifId}`));
21538
22084
  diagnostics.clear("probe-unavailable", d.id);
21539
22085
  if (flyingFrameId !== d.parentId) body.trailBuffer?.clear();
21540
22086
  if (bo) {
@@ -21559,7 +22105,8 @@ var Placer = class {
21559
22105
  }
21560
22106
  const primary = resolveProbePrimary(located.probe, jd, located.fitCenterNaifId, ctx.chebStore ?? null, (id) => this.at(id, jd) !== null);
21561
22107
  if (!primary) {
21562
- setUnplaced(body, "parent");
22108
+ const center = located.probe.fitCenter?.id;
22109
+ setUnplaced(body, center ? this.under(center) : "parent");
21563
22110
  diagnostics.warnOnce("probe-unavailable", d.id, () => `probe ${d.id} (${d.name ?? "unnamed"}): hidden — stamped fit center is not placeable this frame`);
21564
22111
  return null;
21565
22112
  }
@@ -21606,7 +22153,7 @@ var Placer = class {
21606
22153
  if (probeParentChanged) d.parentId = probeParentKey;
21607
22154
  const probeParentPos = this.at(probeParentKey, jd);
21608
22155
  if (!probeParentPos) {
21609
- setUnplaced(body, "parent");
22156
+ setUnplaced(body, this.under(probeParentKey));
21610
22157
  diagnostics.warnOnce("probe-unavailable", d.id, () => `probe ${d.id} (${d.name ?? "unnamed"}): hidden — fit center ${probeParentKey} has no position this frame`);
21611
22158
  return null;
21612
22159
  }
@@ -21883,11 +22430,11 @@ function updateAtmosphereShaders(bodyObjects, cameraPosition, visible, realistic
21883
22430
  spinAxis.set(0, 1, 0).applyQuaternion(bo.mesh.quaternion);
21884
22431
  uniforms.uSpinAxis.value.copy(spinAxis);
21885
22432
  }
21886
- const atmoMesh = bo.atmosphere.mesh;
21887
- const shellRadius = bo.atmosphere.geometryRadiusScene * atmoMesh.scale.x;
21888
- const camDist = cameraPosition.distanceTo(atmoMesh.position);
22433
+ const shellCenter = bo.root.position;
22434
+ const shellRadius = bo.atmosphere.geometryRadiusScene * bo.atmosphere.mesh.scale.x;
22435
+ const camDist = cameraPosition.distanceTo(shellCenter);
21889
22436
  if (shellRadius > camDist * .1) state.shellProminent = true;
21890
- if (quality.eclipseShadows) cullShellOccluders(uniforms, atmoMesh.position, sunVec, shellRadius);
22437
+ if (quality.eclipseShadows) cullShellOccluders(uniforms, shellCenter, sunVec, shellRadius);
21891
22438
  const inside = quality.insideView && camDist < shellRadius;
21892
22439
  const referenceKm = bo.atmosphere.planetRadiusKm;
21893
22440
  const altKm = camDist * ((referenceKm + params.topAltitudeKm) / shellRadius) - referenceKm;
@@ -21901,7 +22448,7 @@ function updateAtmosphereShaders(bodyObjects, cameraPosition, visible, realistic
21901
22448
  }
21902
22449
  if (inside) {
21903
22450
  state.insideShell = true;
21904
- const sinSunElev = camUp.copy(cameraPosition).sub(atmoMesh.position).divideScalar(camDist).dot(sunVec);
22451
+ const sinSunElev = camUp.copy(cameraPosition).sub(shellCenter).divideScalar(camDist).dot(sunVec);
21905
22452
  const dim = skyboxDimFactor(params, altKm, sinSunElev);
21906
22453
  state.skyboxIntensity *= dim * (1 - deckBlend);
21907
22454
  if (quality.refraction && params.refractivity && !underDeck) {
@@ -21915,7 +22462,7 @@ function updateAtmosphereShaders(bodyObjects, cameraPosition, visible, realistic
21915
22462
  const shellRatio = camDist / shellRadius;
21916
22463
  if (quality.sunTint && shellRatio < SUN_TINT_MAX_RATIO) {
21917
22464
  const planetRadiusScene = shellRadius / (1 + params.topAltitudeKm / referenceKm) * (bo.atmosphere.surfaceRadiusKm / referenceKm);
21918
- camRel.copy(cameraPosition).sub(atmoMesh.position);
22465
+ camRel.copy(cameraPosition).sub(shellCenter);
21919
22466
  const shellSpinAxis = uniforms.uSpinAxis.value;
21920
22467
  const shellStretch = uniforms.uStretch.value;
21921
22468
  if (sunPathTransmittance(params, camRel, sunVec, planetRadiusScene, bo.atmosphere.surfaceRadiusKm, sunT, shellSpinAxis, shellStretch)) {
@@ -21924,7 +22471,7 @@ function updateAtmosphereShaders(bodyObjects, cameraPosition, visible, realistic
21924
22471
  }
21925
22472
  if (sunViewTint && shellRatio < bestViewTintRatio) {
21926
22473
  bestViewTintRatio = shellRatio;
21927
- bindViewTint(sunViewTint, params, atmoMesh.position, planetRadiusScene, bo.atmosphere.surfaceRadiusKm, shellSpinAxis, shellStretch);
22474
+ bindViewTint(sunViewTint, params, shellCenter, planetRadiusScene, bo.atmosphere.surfaceRadiusKm, shellSpinAxis, shellStretch);
21928
22475
  }
21929
22476
  }
21930
22477
  }
@@ -22251,7 +22798,7 @@ function desiredDemTier(altitudeRadii, current) {
22251
22798
  if (tierRank(down) < tierRank(current)) return down;
22252
22799
  return current;
22253
22800
  }
22254
- function updateTextureLOD(bodyObjects, camera, renderer, ctx, textureLoader, focusedId, jd, appearances) {
22801
+ function updateTextureLOD(bodyObjects, camera, renderer, ctx, textureLoader, focusedId, jd, appearances, invalidate) {
22255
22802
  appearances.apply(bodyObjects, textureLoader);
22256
22803
  const fovRad = camera.fov * Math.PI / 180;
22257
22804
  const projScale = renderer.domElement.clientHeight / (2 * Math.tan(fovRad / 2));
@@ -22275,7 +22822,8 @@ function updateTextureLOD(bodyObjects, camera, renderer, ctx, textureLoader, foc
22275
22822
  else if (screenR < 1024 && altitudeRadii > 2) desired = "medium";
22276
22823
  else desired = "high";
22277
22824
  const currentRank = tierRank(bo.textureTier);
22278
- const desiredRank = Math.min(tierRank(desired), tierCap);
22825
+ const bodyCap = tilesServe(bo) ? Math.min(tierCap, tierRank("medium")) : tierCap;
22826
+ const desiredRank = Math.min(tierRank(desired), bodyCap);
22279
22827
  const desiredFrame = textureFrameForJd(jd, bo.availableFrames);
22280
22828
  const frameChanged = desiredFrame !== bo.textureFrame;
22281
22829
  const wantsUpgrade = desiredRank > currentRank;
@@ -22283,6 +22831,7 @@ function updateTextureLOD(bodyObjects, camera, renderer, ctx, textureLoader, foc
22283
22831
  const target = wantsUpgrade ? highestAvailableTier(desiredRank, bo.availableTiers) : bo.textureTier;
22284
22832
  if (target) loadBodyTextureTier(bo, target, desiredFrame, textureLoader);
22285
22833
  }
22834
+ updateTileOverlay(bo, camera, renderer, projScale * renderer.getPixelRatio(), settings.showSurfaceTexture && !bo.appearance?.surfaceOwned && !bo.model, invalidate);
22286
22835
  const dispTiers = bo.displacementMeta?.tiers;
22287
22836
  if (bo.displacementMap && dispTiers?.length && !bo.displacementLoading) {
22288
22837
  const maxTex = renderer.capabilities.maxTextureSize;
@@ -23629,7 +24178,10 @@ var SceneRenderer = class SceneRenderer {
23629
24178
  disposed = false;
23630
24179
  lastDataVersion = -1;
23631
24180
  dateHolds = 0;
24181
+ seenJumps = 0;
24182
+ jumpJd = null;
23632
24183
  focusWasOutOfRange = false;
24184
+ seekAwaitsData = false;
23633
24185
  frameIndex = 0;
23634
24186
  static WAKE_FRAMES = 30;
23635
24187
  static IDLE_HEARTBEAT_FRAMES = 10;
@@ -23683,6 +24235,7 @@ var SceneRenderer = class SceneRenderer {
23683
24235
  this.ctx = ctx;
23684
24236
  ctx.hasMeshBody = (id) => this.bodyObjects.has(id);
23685
24237
  this.clock = clock;
24238
+ this.seenJumps = clock.jumps;
23686
24239
  this.callbacks = callbacks;
23687
24240
  this.outOfRange = new OutOfRangeNotifier(callbacks.notices);
23688
24241
  this.labelContainer = labelContainer;
@@ -23732,7 +24285,7 @@ var SceneRenderer = class SceneRenderer {
23732
24285
  this.orbitPreview.setLayer(SceneRenderer.MAP_LAYER);
23733
24286
  this.skyboxAdjuster.set(0, 0, 0);
23734
24287
  loadSkybox(this.scene, this.renderer, ctx, () => this.disposed);
23735
- this.placer = new Placer(ctx, this.bodyObjects, this.positionDiagnostics);
24288
+ this.placer = new Placer(ctx, this.bodyObjects, this.positionDiagnostics, () => this.dateHeld());
23736
24289
  ctx.placer = this.placer;
23737
24290
  const focusBody = [
23738
24291
  ctx.getBody(initialView.id),
@@ -23872,7 +24425,7 @@ var SceneRenderer = class SceneRenderer {
23872
24425
  return;
23873
24426
  }
23874
24427
  this.travelPath.setVisible(true);
23875
- this.travelPath.setClock(this.clock.jd);
24428
+ this.travelPath.setClock(this.sceneJd);
23876
24429
  this.travelPath.reposition(center, this.focus.focusTruePos, (id) => this.where(id));
23877
24430
  this.travelPath.updateCameraOffset(this.camera.position);
23878
24431
  this.reserveTravelLabelSpace();
@@ -24004,10 +24557,10 @@ var SceneRenderer = class SceneRenderer {
24004
24557
  return this.focus.focusDurationMs;
24005
24558
  }
24006
24559
  where(id) {
24007
- return this.placer.at(id, this.clock.jd);
24560
+ return this.placer.at(id, this.sceneJd);
24008
24561
  }
24009
24562
  whereBody(body) {
24010
- return this.placer.place(body, this.clock.jd);
24563
+ return this.placer.place(body, this.sceneJd);
24011
24564
  }
24012
24565
  repositionBodies() {
24013
24566
  const [fx, fy, fz] = this.focus.focusTruePos;
@@ -24025,7 +24578,7 @@ var SceneRenderer = class SceneRenderer {
24025
24578
  if (!line) continue;
24026
24579
  const trailBuffer = line.userData.trailBuffer;
24027
24580
  if (trailBuffer) {
24028
- refreshBufferTrail(bo.body, line, trailBuffer, basis, this.clock.jd);
24581
+ refreshBufferTrail(bo.body, line, trailBuffer, basis, this.sceneJd);
24029
24582
  continue;
24030
24583
  }
24031
24584
  const localPositions = line.userData.trailLocalPositions;
@@ -24040,7 +24593,7 @@ var SceneRenderer = class SceneRenderer {
24040
24593
  const nowMs = performance.now();
24041
24594
  const frameDtMs = this.lastTickMs ? nowMs - this.lastTickMs : 0;
24042
24595
  this.lastTickMs = nowMs;
24043
- if (!this.holdingCamera) this.cameraUp.update(this.clock.jd);
24596
+ if (!this.holdingCamera) this.cameraUp.update(this.sceneJd);
24044
24597
  if (this.firstFrame) {
24045
24598
  this.firstFrame = false;
24046
24599
  this.controls.target.set(0, 0, 0);
@@ -24067,7 +24620,7 @@ var SceneRenderer = class SceneRenderer {
24067
24620
  this.lastRenderedCamQuat.copy(this.camera.quaternion);
24068
24621
  this.applyJdUpdate(true);
24069
24622
  this.refreshTravelFocus();
24070
- this.callbacks.onFrame?.(this.clock.jd, renderedDtMs);
24623
+ this.callbacks.onFrame?.(this.sceneJd, renderedDtMs);
24071
24624
  const controlsSettled = this.holdingCamera ? this.applyHeldCamera() : stepFocusAnimation(this.focus, this.camera, this.controls, () => this.repositionAll(), () => this.pointClouds.rebuildBasis());
24072
24625
  this.applyReaderLimits(nowMs);
24073
24626
  const focused = this.holdingCamera ? void 0 : this.focusController.current;
@@ -24090,13 +24643,13 @@ var SceneRenderer = class SceneRenderer {
24090
24643
  if (this.systemData.hasPendingUnloads() && performance.now() - this.focus.focusStartTime >= this.focus.focusDurationMs) this.systemData.drainPendingUnloads();
24091
24644
  const { distance } = this.getCameraState();
24092
24645
  this.updateTravelCraftSystem();
24093
- this.ctx.visibility.updateCamera(distance, this.clock.jd);
24646
+ this.ctx.visibility.updateCamera(distance, this.sceneJd);
24094
24647
  this.updateTightFar(distance);
24095
24648
  this.systemData.syncToFocus();
24096
24649
  this.refreshTravelPath();
24097
24650
  this.refreshOrbitPreview();
24098
24651
  this.extensions.update({
24099
- jd: this.clock.jd,
24652
+ jd: this.sceneJd,
24100
24653
  basis: this.focus.focusTruePos,
24101
24654
  camera: this.camera,
24102
24655
  viewportPx: this.viewportH,
@@ -24107,7 +24660,7 @@ var SceneRenderer = class SceneRenderer {
24107
24660
  refreshDeferredTrails(this.bodyObjects, this.focus, this.lastUpdatedJd, this.trailView());
24108
24661
  updateRingShaders(this.bodyObjects, this.focus.focusTruePos, sceneSettings().realisticLighting, sceneSettings().overexposeRings, this.camera.position, this.pxPerRad());
24109
24662
  updateEclipseUniforms(this.bodyObjects, this.focus.focusTruePos);
24110
- const atmoState = updateAtmosphereShaders(this.bodyObjects, this.camera.position, sceneSettings().showAtmospheres, sceneSettings().realisticLighting, this.sunIntensityScale, currentAtmosphereConfig(), this.clock.jd);
24663
+ const atmoState = updateAtmosphereShaders(this.bodyObjects, this.camera.position, sceneSettings().showAtmospheres, sceneSettings().realisticLighting, this.sunIntensityScale, currentAtmosphereConfig(), this.sceneJd);
24111
24664
  recordAtmospherePerf(renderedDtMs, atmoState.shellProminent);
24112
24665
  this.scene.backgroundIntensity = atmoState.skyboxIntensity;
24113
24666
  this.applyLayerChrome();
@@ -24124,7 +24677,7 @@ var SceneRenderer = class SceneRenderer {
24124
24677
  for (const light of this.ambientLights) light.intensity = ambient;
24125
24678
  updateUserLocationOcclusion(this.userLocationMarker, this.bodyObjects, this.camera);
24126
24679
  const focusedIdLod = nomenclatureBodyId(this.focusController.current, this.bodyObjects);
24127
- updateTextureLOD(this.bodyObjects, this.camera, this.renderer, this.ctx, this.textureLoader, focusedIdLod, this.clock.jd, this.appearances);
24680
+ updateTextureLOD(this.bodyObjects, this.camera, this.renderer, this.ctx, this.textureLoader, focusedIdLod, this.sceneJd, this.appearances, this.invalidate);
24128
24681
  updateSphereLOD(this.bodyObjects, this.camera, this.renderer, this.ctx, focusedIdLod);
24129
24682
  updateSunShadowLight(this.bodyObjects, this.focus.focusTruePos, this.ctx, this.shadowLight, this.sunPointLight, distance, this._tmpV3, sceneSettings().realisticLighting, this.sunIntensityScale);
24130
24683
  this.updateModelMounts();
@@ -24251,7 +24804,7 @@ var SceneRenderer = class SceneRenderer {
24251
24804
  };
24252
24805
  if (!seated || !body.orientation) return void 0;
24253
24806
  return {
24254
- invQuat: bodyQuaternion(body.orientation, this.clock.jd, body.nutPrec).invert(),
24807
+ invQuat: bodyQuaternion(body.orientation, this.sceneJd, body.nutPrec).invert(),
24255
24808
  seated,
24256
24809
  radialKm: (dir) => renderedSurfaceRadialKm(bo, body.data.id, body.data.radiusKm, dir)
24257
24810
  };
@@ -24507,7 +25060,7 @@ var SceneRenderer = class SceneRenderer {
24507
25060
  }
24508
25061
  applyHeldCamera() {
24509
25062
  const pose = this.cameraHold;
24510
- const resolved = pose && resolvePose(pose, this.ctx, this.clock.jd);
25063
+ const resolved = pose && resolvePose(pose, this.ctx, this.sceneJd);
24511
25064
  if (!resolved) return true;
24512
25065
  const basis = this.focus.focusTruePos;
24513
25066
  this.camera.up.set(resolved.up[0], resolved.up[1], resolved.up[2]).normalize();
@@ -24540,10 +25093,10 @@ var SceneRenderer = class SceneRenderer {
24540
25093
  maybeLoadTexture(body) {
24541
25094
  const bo = this.bodyObjects.get(body.data.id);
24542
25095
  if (!bo) return;
24543
- loadBodyTexture(bo, this.textureLoader, this.clock.jd, this.scene, this.renderer.capabilities.maxTextureSize, this.ctx).then(() => {
25096
+ loadBodyTexture(bo, this.textureLoader, this.sceneJd, this.scene, this.renderer.capabilities.maxTextureSize, this.ctx).then(() => {
24544
25097
  if (this.focusController.current?.data.id !== body.data.id) return;
24545
25098
  if (body.orientation && bo.mesh) {
24546
- applyOrientation(bo.mesh, body.orientation, this.clock.jd, body.nutPrec);
25099
+ applyOrientation(bo.mesh, body.orientation, this.sceneJd, body.nutPrec);
24547
25100
  this.focusController.reapplyInitialViewIfPending();
24548
25101
  }
24549
25102
  });
@@ -24630,8 +25183,32 @@ var SceneRenderer = class SceneRenderer {
24630
25183
  held = false;
24631
25184
  };
24632
25185
  }
25186
+ jumpLoads() {
25187
+ const { clock, ctx } = this;
25188
+ if (clock.jumps !== this.seenJumps) {
25189
+ this.seenJumps = clock.jumps;
25190
+ this.jumpJd = clock.jd;
25191
+ ctx.refreshTick(jdToDate(clock.jd));
25192
+ ctx.chebStore?.request(clock.jd);
25193
+ ctx.probeStore?.request(clock.jd);
25194
+ }
25195
+ const jd = this.jumpJd;
25196
+ if (jd === null) return false;
25197
+ if (!ctx.chebStore?.loadingAt(jd) && !ctx.probeStore?.loadingAt(jd)) {
25198
+ this.jumpJd = null;
25199
+ return false;
25200
+ }
25201
+ this.invalidate();
25202
+ return true;
25203
+ }
25204
+ dateHeld() {
25205
+ return !Number.isNaN(this.lastUpdatedJd) && (this.dateHolds > 0 || this.jumpLoads());
25206
+ }
25207
+ get sceneJd() {
25208
+ return this.dateHeld() ? this.lastUpdatedJd : this.clock.jd;
25209
+ }
24633
25210
  applyJdUpdate(allowOorRefocus = false) {
24634
- if (this.dateHolds > 0) return;
25211
+ if (this.dateHolds > 0 || this.jumpLoads()) return;
24635
25212
  this.ensureFocusedProbeTarget();
24636
25213
  const systemId = this.ctx.visibility.focusedSystemId;
24637
25214
  const seatKey = this.focusedSeatConfigKey();
@@ -24641,7 +25218,7 @@ var SceneRenderer = class SceneRenderer {
24641
25218
  this.clock.seeked = false;
24642
25219
  return;
24643
25220
  }
24644
- const seeked = this.clock.seeked;
25221
+ const seeked = this.clock.seeked || this.seekAwaitsData;
24645
25222
  this.clock.seeked = false;
24646
25223
  this.lastUpdatedJd = this.clock.jd;
24647
25224
  this.lastUpdatedSystemId = systemId;
@@ -24668,6 +25245,7 @@ var SceneRenderer = class SceneRenderer {
24668
25245
  this.pointClouds.maybeRebase();
24669
25246
  }
24670
25247
  this.syncLandedNomenclature();
25248
+ this.seekAwaitsData = seeked && result.focusLoading;
24671
25249
  const losingPlace = result.focusUnplaced && !this.focusWasOutOfRange;
24672
25250
  this.focusWasOutOfRange = result.focusUnplaced;
24673
25251
  if (allowOorRefocus && seeked && losingPlace && result.anchorId) {