spacemap 0.1.9 → 0.1.10

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,14 @@ var ChunkWindow = class {
8950
8967
  done
8951
8968
  };
8952
8969
  }
8970
+ loadingAt(jd, zone) {
8971
+ if (this.inflight.size === 0) return false;
8972
+ for (const [name, params] of this.zoneParams) {
8973
+ if (zone !== void 0 && name !== zone) continue;
8974
+ if (this.inflight.has(chunkKey(name, chunkIndexForJd(params, jd)))) return true;
8975
+ }
8976
+ return false;
8977
+ }
8953
8978
  pendingCurrent(jd) {
8954
8979
  if (this.inflight.size === 0) return RESOLVED;
8955
8980
  let pending = null;
@@ -9021,6 +9046,10 @@ var ChebyshevStore = class extends ChunkWindow {
9021
9046
  end: params.end_jd
9022
9047
  };
9023
9048
  }
9049
+ isLoading(objectId, jd) {
9050
+ const zone = this.idToZone.get(objectId);
9051
+ return zone !== void 0 && this.loadingAt(jd, zone);
9052
+ }
9024
9053
  resolve(objectId, jd) {
9025
9054
  const zone = this.idToZone.get(objectId);
9026
9055
  if (zone === void 0) return null;
@@ -10059,6 +10088,7 @@ var RENDER_TIER_PRESETS = {
10059
10088
  bloomScale: .5,
10060
10089
  pointBudget: 4e5,
10061
10090
  maxTextureTier: "medium",
10091
+ tileWindows: 3,
10062
10092
  maxSkyboxFace: 2048
10063
10093
  },
10064
10094
  medium: {
@@ -10068,6 +10098,7 @@ var RENDER_TIER_PRESETS = {
10068
10098
  bloomScale: .75,
10069
10099
  pointBudget: 9e5,
10070
10100
  maxTextureTier: "medium",
10101
+ tileWindows: 4,
10071
10102
  maxSkyboxFace: 2048
10072
10103
  },
10073
10104
  high: {
@@ -10077,6 +10108,7 @@ var RENDER_TIER_PRESETS = {
10077
10108
  bloomScale: 1,
10078
10109
  pointBudget: Infinity,
10079
10110
  maxTextureTier: "high",
10111
+ tileWindows: 6,
10080
10112
  maxSkyboxFace: Infinity
10081
10113
  }
10082
10114
  };
@@ -10084,6 +10116,10 @@ function maxTextureTier() {
10084
10116
  if (isLowEndDevice()) return "low";
10085
10117
  return currentRenderTier().maxTextureTier;
10086
10118
  }
10119
+ function maxTileWindows() {
10120
+ if (isLowEndDevice()) return 0;
10121
+ return currentRenderTier().tileWindows;
10122
+ }
10087
10123
  function resolveRenderTier() {
10088
10124
  const measured = sceneSettings().atmosphereCalibration?.tier;
10089
10125
  if (measured === "low" || measured === "medium") return measured;
@@ -15429,6 +15465,491 @@ function detachSelfShadow(uniforms) {
15429
15465
  uniforms.uSelfScale.value = 0;
15430
15466
  }
15431
15467
 
15468
+ //#endregion
15469
+ //#region src/lib/scene/lod/tile-window.ts
15470
+ var WINDOW_TILES = 4;
15471
+ var MAP = 0;
15472
+ var NORTH = 1;
15473
+ var SOUTH = 2;
15474
+ var CHART_FOLDER = [
15475
+ "",
15476
+ "north",
15477
+ "south"
15478
+ ];
15479
+ var FIRST_TILE_LEVEL = 3;
15480
+ var HOLD_TILES = .75;
15481
+ var BASE_WIDTH_PX = 2048;
15482
+ var CAP_EDGE = Math.SQRT2 - 1;
15483
+ var CAP_ENTER_DEG = 62;
15484
+ var CAP_LEAVE_DEG = 58;
15485
+ var BOTH_CHARTS_DEG = 45;
15486
+ function chartColumns(chart, level) {
15487
+ return chart === 0 ? 2 << level : 1 << level - 1;
15488
+ }
15489
+ function chartRows(chart, level) {
15490
+ return chart === 0 ? 1 << level : 1 << level - 1;
15491
+ }
15492
+ function neededLevel(radiusScene, dist, projScale, maxLevel) {
15493
+ const height = Math.max(dist - radiusScene, radiusScene * 1e-6);
15494
+ const widthPx = 2 * Math.PI * radiusScene * projScale / height;
15495
+ return Math.min(Math.ceil(Math.log2(widthPx / BASE_WIDTH_PX)), maxLevel);
15496
+ }
15497
+ function chartPoint(chart, x, y, z) {
15498
+ if (chart === 0) {
15499
+ const east = Math.atan2(z, -x) / (2 * Math.PI);
15500
+ return {
15501
+ x: east < 0 ? east + 1 : east,
15502
+ y: Math.acos(Math.min(Math.max(y, -1), 1)) / Math.PI
15503
+ };
15504
+ }
15505
+ const side = chart === 1 ? 1 : -1;
15506
+ const scale = .5 / (CAP_EDGE * (1 + side * y));
15507
+ return {
15508
+ x: .5 + x * scale,
15509
+ y: .5 + side * z * scale
15510
+ };
15511
+ }
15512
+ function cameraChart(y, previous) {
15513
+ const cap = y >= 0 ? 1 : 2;
15514
+ return Math.asin(Math.min(Math.abs(y), 1)) * 180 / Math.PI > (previous === cap ? CAP_LEAVE_DEG : CAP_ENTER_DEG) ? cap : 0;
15515
+ }
15516
+ function windowChain(level, count, chart, y) {
15517
+ const chain = [];
15518
+ for (let z = level; z >= 3 && chain.length < count; z--) chain.push({
15519
+ chart,
15520
+ level: z
15521
+ });
15522
+ const reached = chain.length > 0 && chain[chain.length - 1].level === 3;
15523
+ const both = Math.abs(y) > Math.sin(BOTH_CHARTS_DEG * Math.PI / 180);
15524
+ if (reached && both && chain.length < count) {
15525
+ const cap = y >= 0 ? 1 : 2;
15526
+ chain.push({
15527
+ chart: chart === 0 ? cap : 0,
15528
+ level: 3
15529
+ });
15530
+ }
15531
+ return chain;
15532
+ }
15533
+ function placeAxis(at, count, wraps, kept) {
15534
+ const last = Math.max(count - 4, 0);
15535
+ if (kept !== void 0) {
15536
+ let off = at - (kept + 4 / 2);
15537
+ if (wraps) off -= count * Math.round(off / count);
15538
+ if (!wraps && (kept === 0 && off < 0 || kept === last && off > 0) || Math.abs(off) <= HOLD_TILES) return kept;
15539
+ }
15540
+ const first = Math.round(at - 4 / 2);
15541
+ return wraps ? (first % count + count) % count : Math.min(Math.max(first, 0), last);
15542
+ }
15543
+ function placeWindow(chart, level, x, y, previous) {
15544
+ const columns = chartColumns(chart, level);
15545
+ const rows = chartRows(chart, level);
15546
+ const kept = previous?.chart === chart && previous.level === level ? previous : void 0;
15547
+ const tx0 = placeAxis(x * columns, columns, chart === 0, kept?.tx0);
15548
+ const ty0 = placeAxis(y * rows, rows, false, kept?.ty0);
15549
+ if (kept && kept.tx0 === tx0 && kept.ty0 === ty0) return kept;
15550
+ return {
15551
+ chart,
15552
+ level,
15553
+ tx0,
15554
+ ty0
15555
+ };
15556
+ }
15557
+ function slotTile(placement, sx, sy) {
15558
+ const columns = chartColumns(placement.chart, placement.level);
15559
+ const dx = ((sx - placement.tx0) % 4 + 4) % 4;
15560
+ const dy = ((sy - placement.ty0) % 4 + 4) % 4;
15561
+ return {
15562
+ x: (placement.tx0 + dx) % columns,
15563
+ y: placement.ty0 + dy
15564
+ };
15565
+ }
15566
+
15567
+ //#endregion
15568
+ //#region src/lib/scene/objects/surface/tile-overlay.ts
15569
+ var MAX_WINDOWS = 6;
15570
+ var MAX_DRAWN = MAX_WINDOWS + 1;
15571
+ var MAX_INFLIGHT = 6;
15572
+ var MAX_FAILURES = 4;
15573
+ var FETCH_TIMEOUT_S = 30;
15574
+ var IDLE_DISPOSE_S = 5;
15575
+ var FEATHER_TILES = .5;
15576
+ var MAX_ANISOTROPY = 16;
15577
+ var SLOTS = 4 * 4;
15578
+ var PARS = `
15579
+ varying vec3 vTileDir;
15580
+ uniform sampler2DArray uTileMap;
15581
+ uniform sampler2DArray uTileMask;
15582
+ uniform vec4 uTileGrid[ ${MAX_DRAWN} ];
15583
+ uniform vec4 uTileWindow[ ${MAX_DRAWN} ];
15584
+ // Most taps of the anisotropic filter, and a tile's side in pixels.
15585
+ uniform vec2 uTileFilter;
15586
+
15587
+ // Long and short half-axes of the footprint whose sides are fx and fy. The
15588
+ // two sides alone would do where the stretch runs along the screen's axes;
15589
+ // around a pole it runs every way, and each side then holds most of it.
15590
+ void tileAxes( vec2 fx, vec2 fy, out vec2 major, out vec2 minor ) {
15591
+ float p = fx.x * fx.x + fy.x * fy.x;
15592
+ float q = fx.y * fx.y + fy.y * fy.y;
15593
+ float r = fx.x * fx.y + fy.x * fy.y;
15594
+ float det = fx.x * fy.y - fx.y * fy.x;
15595
+ float hi = 0.5 * ( p + q ) + sqrt( 0.25 * ( p - q ) * ( p - q ) + r * r );
15596
+ float lo = det * det / max( hi, 1e-30 );
15597
+ vec2 axis = p >= q ? vec2( hi - q, r ) : vec2( r, hi - p );
15598
+ float len = length( axis );
15599
+ axis = len > 0.0 ? axis / len : vec2( 1.0, 0.0 );
15600
+ major = axis * sqrt( hi );
15601
+ minor = vec2( - axis.y, axis.x ) * sqrt( lo );
15602
+ }
15603
+
15604
+ vec3 tileColour( vec3 below ) {
15605
+ if ( uTileWindow[ 0 ].w < 0.5 ) return below;
15606
+
15607
+ // Where the fragment is on each chart, as fractions from the chart's
15608
+ // north-west corner, and what a screen pixel spans there. Both come from
15609
+ // the direction of the fragment rather than its interpolated coordinates:
15610
+ // those have one slope per triangle, which near a pole differs enough
15611
+ // between the two triangles of a grid cell to pick different levels, and
15612
+ // they jump at the 180° meridian.
15613
+ vec3 dir = normalize( vTileDir );
15614
+ vec3 ddx = dFdx( dir );
15615
+ vec3 ddy = dFdy( dir );
15616
+
15617
+ float flat2 = max( dir.x * dir.x + dir.z * dir.z, 1e-12 );
15618
+ float east = 1.0 / ( 2.0 * PI * flat2 );
15619
+ float south = - 1.0 / ( PI * sqrt( flat2 ) );
15620
+ vec2 mapAt = vec2( fract( atan( dir.z, - dir.x ) / ( 2.0 * PI ) ), acos( clamp( dir.y, - 1.0, 1.0 ) ) / PI );
15621
+ vec2 mapDx = vec2( ( dir.z * ddx.x - dir.x * ddx.z ) * east, ddx.y * south );
15622
+ vec2 mapDy = vec2( ( dir.z * ddy.x - dir.x * ddy.z ) * east, ddy.y * south );
15623
+
15624
+ // The cap of the fragment's own hemisphere, seen from above its pole.
15625
+ float side = dir.y < 0.0 ? - 1.0 : 1.0;
15626
+ float fold = 1.0 + side * dir.y;
15627
+ vec2 plane = vec2( dir.x, side * dir.z );
15628
+ float capScale = 0.5 / ( ${CAP_EDGE.toFixed(8)} * fold );
15629
+ vec2 capAt = 0.5 + plane * capScale;
15630
+ vec2 capDx = ( vec2( ddx.x, side * ddx.z ) - plane * ( side * ddx.y / fold ) ) * capScale;
15631
+ vec2 capDy = ( vec2( ddy.x, side * ddy.z ) - plane * ( side * ddy.y / fold ) ) * capScale;
15632
+
15633
+ float rows = ${4 .toFixed(1)};
15634
+ vec3 colour = vec3( 0.0 );
15635
+ float covered = 0.0;
15636
+ for ( int i = 0; i < ${MAX_DRAWN}; i ++ ) {
15637
+ vec4 window = uTileWindow[ i ];
15638
+ if ( window.w < 0.5 ) break;
15639
+ vec4 grid = uTileGrid[ i ];
15640
+ bool onMap = window.x < 0.5;
15641
+ // The other pole's cap.
15642
+ if ( ! onMap && ( window.x < 1.5 ) != ( side > 0.0 ) ) continue;
15643
+ vec2 tile = ( onMap ? mapAt : capAt ) * grid.xy;
15644
+ vec2 dx = ( onMap ? mapDx : capDx ) * grid.xy * uTileFilter.y;
15645
+ vec2 dy = ( onMap ? mapDy : capDy ) * grid.xy * uTileFilter.y;
15646
+
15647
+ // Level this fragment asks of the window, from the short axis of its
15648
+ // footprint in the window's pixels: the long axis is the anisotropic
15649
+ // filter's to cover, as far as its taps reach. The window fades in
15650
+ // over the level below its own, as a mipmap would.
15651
+ vec2 major;
15652
+ vec2 minor;
15653
+ tileAxes( dx, dy, major, minor );
15654
+ float foot = max( length( minor ), length( major ) / uTileFilter.x );
15655
+ float weight = clamp( 1.0 - log2( max( foot, 1e-6 ) ), 0.0, 1.0 );
15656
+ if ( weight <= 0.0 ) continue;
15657
+
15658
+ // Half a pixel in from a pole of the map: past it the filter would
15659
+ // read the far row.
15660
+ if ( onMap ) tile.y = clamp( tile.y, 0.0005, grid.y - 0.0005 );
15661
+ // Offset from the window's north-west corner; the map's longitude wraps.
15662
+ vec2 rel = tile - grid.zw;
15663
+ if ( onMap ) rel.x = mod( rel.x, grid.x );
15664
+ if ( rel.x < 0.0 || rel.x > rows || rel.y < 0.0 || rel.y > rows ) continue;
15665
+ // Distance to the window's edge, in tiles. A pole of the map is no
15666
+ // edge: nothing lies past it.
15667
+ vec2 near = min( rel, rows - rel );
15668
+ if ( onMap && grid.w < 0.5 ) near.y = rows - rel.y;
15669
+ if ( onMap && grid.w + rows > grid.y - 0.5 ) near.y = grid.w < 0.5 ? rows : rel.y;
15670
+ float edge = min( near.x, near.y );
15671
+
15672
+ ivec3 slot = ivec3( int( mod( tile.x, rows ) ), int( mod( tile.y, rows ) ), int( window.y ) );
15673
+ float alpha = weight * texelFetch( uTileMask, slot, 0 ).r * smoothstep( 0.0, ${FEATHER_TILES.toFixed(2)}, edge );
15674
+ if ( alpha <= 0.0 ) continue;
15675
+
15676
+ // The texture repeats every window: a tile sits in the slot its own
15677
+ // column and row pick, north up.
15678
+ vec3 st = vec3( tile.x / rows, - tile.y / rows, window.y );
15679
+ vec2 texel = vec2( 1.0, - 1.0 ) / ( rows * uTileFilter.y );
15680
+ colour += ( 1.0 - covered ) * alpha * textureGrad( uTileMap, st, major * texel, minor * texel ).rgb;
15681
+ covered += ( 1.0 - covered ) * alpha;
15682
+ if ( covered > 0.995 ) break;
15683
+ }
15684
+ return colour + ( 1.0 - covered ) * below;
15685
+ }
15686
+ `;
15687
+ var MAP_FRAGMENT = `
15688
+ #ifdef USE_MAP
15689
+ vec4 sampledDiffuseColor = texture2D( map, vMapUv );
15690
+ sampledDiffuseColor.rgb = tileColour( sampledDiffuseColor.rgb );
15691
+ diffuseColor *= sampledDiffuseColor;
15692
+ #endif
15693
+ `;
15694
+ function createOverlay(material) {
15695
+ const overlay = {
15696
+ material,
15697
+ uniforms: {
15698
+ uTileMap: { value: null },
15699
+ uTileMask: { value: null },
15700
+ uTileGrid: { value: Array.from({ length: MAX_DRAWN }, () => new Vector4()) },
15701
+ uTileWindow: { value: Array.from({ length: MAX_DRAWN }, () => new Vector4()) },
15702
+ uTileFilter: { value: new Vector2(1, 1) }
15703
+ },
15704
+ inflight: 0,
15705
+ failures: 0,
15706
+ failed: /* @__PURE__ */ new Set(),
15707
+ dead: false,
15708
+ lastUsed: 0
15709
+ };
15710
+ chainShaderHook(material, "surfaceTiles", (shader) => {
15711
+ Object.assign(shader.uniforms, overlay.uniforms);
15712
+ shader.vertexShader = shader.vertexShader.replace("#include <common>", "#include <common>\nvarying vec3 vTileDir;").replace("#include <begin_vertex>", "#include <begin_vertex>\nvTileDir = position;");
15713
+ shader.fragmentShader = shader.fragmentShader.replace("#include <map_pars_fragment>", `#include <map_pars_fragment>\n${PARS}`).replace("#include <map_fragment>", MAP_FRAGMENT);
15714
+ });
15715
+ return overlay;
15716
+ }
15717
+ function createStore(tileSize, layers, renderer) {
15718
+ const size = tileSize * 4;
15719
+ const texture = new DataArrayTexture(null, size, size, layers);
15720
+ texture.source.dataReady = false;
15721
+ texture.format = RGBAFormat;
15722
+ texture.type = UnsignedByteType;
15723
+ texture.colorSpace = SRGBColorSpace;
15724
+ texture.wrapS = texture.wrapT = RepeatWrapping;
15725
+ texture.minFilter = texture.magFilter = LinearFilter;
15726
+ texture.generateMipmaps = false;
15727
+ texture.anisotropy = Math.min(MAX_ANISOTROPY, renderer.capabilities.getMaxAnisotropy());
15728
+ texture.needsUpdate = true;
15729
+ const mask = new DataArrayTexture(new Uint8Array(SLOTS * layers), 4, 4, layers);
15730
+ mask.format = RedFormat;
15731
+ mask.type = UnsignedByteType;
15732
+ mask.minFilter = mask.magFilter = NearestFilter;
15733
+ mask.unpackAlignment = 1;
15734
+ mask.needsUpdate = true;
15735
+ return {
15736
+ texture,
15737
+ mask,
15738
+ windows: [],
15739
+ free: Array.from({ length: layers }, (_, layer) => layer),
15740
+ tileSize
15741
+ };
15742
+ }
15743
+ function setLoaded(store, layer, slot, loaded) {
15744
+ store.mask.image.data[layer * SLOTS + slot] = loaded ? 255 : 0;
15745
+ store.mask.needsUpdate = true;
15746
+ }
15747
+ function isLoaded(store, layer, slot) {
15748
+ return store.mask.image.data[layer * SLOTS + slot] !== 0;
15749
+ }
15750
+ function retarget(store, win, placement, frame) {
15751
+ if (win.placement === placement && win.frame === frame) return;
15752
+ win.placement = placement;
15753
+ win.frame = frame;
15754
+ for (let slot = 0; slot < SLOTS; slot++) {
15755
+ const { x, y } = slotTile(placement, slot % 4, Math.floor(slot / 4));
15756
+ const key = `${frame ?? 0}/${win.chart}/${win.level}/${x}/${y}`;
15757
+ if (win.wanted[slot] === key) continue;
15758
+ win.wanted[slot] = key;
15759
+ setLoaded(store, win.layer, slot, false);
15760
+ const stale = win.pending.get(slot);
15761
+ if (stale) {
15762
+ stale.abort.abort();
15763
+ win.pending.delete(slot);
15764
+ }
15765
+ }
15766
+ }
15767
+ function release(store, win) {
15768
+ for (const { abort } of win.pending.values()) abort.abort();
15769
+ win.pending.clear();
15770
+ for (let slot = 0; slot < SLOTS; slot++) setLoaded(store, win.layer, slot, false);
15771
+ store.free.push(win.layer);
15772
+ }
15773
+ function settled(overlay, store, win) {
15774
+ for (let slot = 0; slot < SLOTS; slot++) {
15775
+ const key = win.wanted[slot];
15776
+ if (!key) return false;
15777
+ if (!isLoaded(store, win.layer, slot) && !overlay.failed.has(key)) return false;
15778
+ }
15779
+ return true;
15780
+ }
15781
+ async function fetchTile(url, signal) {
15782
+ const response = await fetch(url, { signal });
15783
+ if (!response.ok) throw new Error(`tile ${response.status}`);
15784
+ return createImageBitmap(await response.blob(), {
15785
+ imageOrientation: "flipY",
15786
+ premultiplyAlpha: "none",
15787
+ colorSpaceConversion: "none"
15788
+ });
15789
+ }
15790
+ var copyAt = new Vector3();
15791
+ var queue$1 = [];
15792
+ function pump(overlay, store, tiles, win, x, y, renderer, invalidate) {
15793
+ const placement = win.placement;
15794
+ if (!placement) return;
15795
+ const columns = chartColumns(win.chart, win.level);
15796
+ const cx = x * columns;
15797
+ const cy = y * chartRows(win.chart, win.level);
15798
+ queue$1.length = 0;
15799
+ for (let slot = 0; slot < SLOTS; slot++) {
15800
+ const key = win.wanted[slot];
15801
+ if (!key || win.pending.has(slot) || overlay.failed.has(key)) continue;
15802
+ if (isLoaded(store, win.layer, slot)) continue;
15803
+ const tile = slotTile(placement, slot % 4, Math.floor(slot / 4));
15804
+ let dx = tile.x + .5 - cx;
15805
+ if (win.chart === 0) dx -= columns * Math.round(dx / columns);
15806
+ queue$1.push({
15807
+ slot,
15808
+ far: Math.hypot(dx, tile.y + .5 - cy)
15809
+ });
15810
+ }
15811
+ queue$1.sort((a, b) => a.far - b.far);
15812
+ for (const { slot } of queue$1) {
15813
+ if (overlay.inflight >= MAX_INFLIGHT) return;
15814
+ const key = win.wanted[slot];
15815
+ const sx = slot % 4;
15816
+ const sy = Math.floor(slot / 4);
15817
+ const tile = slotTile(placement, sx, sy);
15818
+ const abort = new AbortController();
15819
+ let timedOut = false;
15820
+ const timer = setTimeout(() => {
15821
+ timedOut = true;
15822
+ abort.abort();
15823
+ }, FETCH_TIMEOUT_S * 1e3);
15824
+ win.pending.set(slot, {
15825
+ key,
15826
+ abort
15827
+ });
15828
+ overlay.inflight++;
15829
+ const folder = CHART_FOLDER[win.chart];
15830
+ fetchTile(tileUrl(tiles, folder, win.level, tile.x, tile.y, win.frame), abort.signal).then((bitmap) => {
15831
+ if (win.wanted[slot] === key && !abort.signal.aborted) {
15832
+ const source = new Texture(bitmap);
15833
+ source.colorSpace = SRGBColorSpace;
15834
+ source.flipY = false;
15835
+ source.generateMipmaps = false;
15836
+ copyAt.set(sx * store.tileSize, (4 - 1 - sy) * store.tileSize, win.layer);
15837
+ renderer.copyTextureToTexture(source, store.texture, null, copyAt);
15838
+ source.dispose();
15839
+ setLoaded(store, win.layer, slot, true);
15840
+ overlay.failures = 0;
15841
+ invalidate();
15842
+ }
15843
+ bitmap.close();
15844
+ }).catch(() => {
15845
+ if (abort.signal.aborted && !timedOut) return;
15846
+ overlay.failed.add(key);
15847
+ if (++overlay.failures >= MAX_FAILURES) overlay.dead = true;
15848
+ }).finally(() => {
15849
+ clearTimeout(timer);
15850
+ overlay.inflight--;
15851
+ if (win.pending.get(slot)?.key === key) win.pending.delete(slot);
15852
+ });
15853
+ }
15854
+ }
15855
+ function switchOff(overlay) {
15856
+ for (const link of overlay.uniforms.uTileWindow.value) link.w = 0;
15857
+ }
15858
+ function rest(bo, overlay, now) {
15859
+ switchOff(overlay);
15860
+ if (now - overlay.lastUsed > IDLE_DISPOSE_S * 1e3) disposeTileOverlay(bo);
15861
+ }
15862
+ function disposeTileOverlay(bo) {
15863
+ const overlay = bo.tileOverlay;
15864
+ if (!overlay) return;
15865
+ switchOff(overlay);
15866
+ const store = overlay.store;
15867
+ if (!store) return;
15868
+ for (const win of store.windows) for (const { abort } of win.pending.values()) abort.abort();
15869
+ store.texture.dispose();
15870
+ store.mask.dispose();
15871
+ overlay.store = void 0;
15872
+ overlay.uniforms.uTileMap.value = null;
15873
+ overlay.uniforms.uTileMask.value = null;
15874
+ }
15875
+ function tilesServe(bo) {
15876
+ const tiles = bo.surfaceTiles;
15877
+ return !!tiles && tiles.max_level >= 3 && tilesBase() !== "" && maxTileWindows() > 0 && !bo.tileOverlay?.dead && textureAllowed(tiles.distribution);
15878
+ }
15879
+ var inverse = new Matrix4();
15880
+ var over = new Vector3();
15881
+ function updateTileOverlay(bo, camera, renderer, projScale, drawable, invalidate) {
15882
+ const tiles = bo.surfaceTiles;
15883
+ const mesh = bo.mesh;
15884
+ const material = mesh?.material;
15885
+ const level = tiles && mesh && material?.map && drawable && tilesServe(bo) ? neededLevel(bo.radiusScene, bo.cachedDist, projScale, tiles.max_level) : -1;
15886
+ const now = performance.now();
15887
+ if (!tiles || !mesh || !material || level < 3) {
15888
+ if (bo.tileOverlay) rest(bo, bo.tileOverlay, now);
15889
+ return;
15890
+ }
15891
+ if (bo.tileOverlay && bo.tileOverlay.material !== material) {
15892
+ disposeTileOverlay(bo);
15893
+ bo.tileOverlay = void 0;
15894
+ }
15895
+ const overlay = bo.tileOverlay ??= createOverlay(material);
15896
+ overlay.lastUsed = now;
15897
+ const levels = tiles.max_level - 3 + 1;
15898
+ const count = Math.min(maxTileWindows(), MAX_WINDOWS, levels + 1);
15899
+ if (overlay.store && overlay.store.texture.image.depth !== count + 1) disposeTileOverlay(bo);
15900
+ const store = overlay.store ??= createStore(tiles.tile_size, count + 1, renderer);
15901
+ overlay.uniforms.uTileMap.value = store.texture;
15902
+ overlay.uniforms.uTileMask.value = store.mask;
15903
+ overlay.uniforms.uTileFilter.value.set(store.texture.anisotropy, tiles.tile_size);
15904
+ over.copy(camera.position).applyMatrix4(inverse.copy(mesh.matrixWorld).invert()).normalize();
15905
+ const cap = over.y >= 0 ? 1 : 2;
15906
+ overlay.chart = cameraChart(over.y, overlay.chart);
15907
+ const chain = windowChain(level, count, overlay.chart, over.y);
15908
+ const frame = tiles.frames ? bo.textureFrame : void 0;
15909
+ const held = (link) => store.windows.find((win) => win.chart === link.chart && win.level === link.level);
15910
+ store.windows = store.windows.filter((win) => {
15911
+ win.rank = chain.findIndex((link) => link.chart === win.chart && link.level === win.level);
15912
+ if (win.rank >= 0) return true;
15913
+ win.rank = chain.length;
15914
+ const heir = chain.find((link) => link.level === win.level);
15915
+ const heirWindow = heir && held(heir);
15916
+ const near = win.chart === 0 || win.chart === cap;
15917
+ if (heir && near && !(heirWindow && settled(overlay, store, heirWindow))) return true;
15918
+ release(store, win);
15919
+ return false;
15920
+ });
15921
+ for (let i = chain.length - 1; i >= 0; i--) {
15922
+ const link = chain[i];
15923
+ let win = held(link);
15924
+ if (!win) {
15925
+ const layer = store.free.pop();
15926
+ if (layer === void 0) continue;
15927
+ win = {
15928
+ layer,
15929
+ ...link,
15930
+ rank: i,
15931
+ wanted: new Array(SLOTS),
15932
+ pending: /* @__PURE__ */ new Map()
15933
+ };
15934
+ store.windows.push(win);
15935
+ }
15936
+ const point = chartPoint(link.chart, over.x, over.y, over.z);
15937
+ retarget(store, win, placeWindow(link.chart, link.level, point.x, point.y, win.placement), frame);
15938
+ pump(overlay, store, tiles, win, point.x, point.y, renderer, invalidate);
15939
+ }
15940
+ store.windows.sort((a, b) => b.level - a.level || a.rank - b.rank);
15941
+ for (let i = 0; i < MAX_DRAWN; i++) {
15942
+ const win = store.windows[i];
15943
+ const link = overlay.uniforms.uTileWindow.value[i];
15944
+ if (!win?.placement) {
15945
+ link.w = 0;
15946
+ continue;
15947
+ }
15948
+ overlay.uniforms.uTileGrid.value[i].set(chartColumns(win.chart, win.level), chartRows(win.chart, win.level), win.placement.tx0, win.placement.ty0);
15949
+ link.set(win.chart, win.layer, win.level, 1);
15950
+ }
15951
+ }
15952
+
15432
15953
  //#endregion
15433
15954
  //#region src/lib/scene/objects/body/lifecycle.ts
15434
15955
  function disposeMaterial(mat) {
@@ -15562,6 +16083,8 @@ function downgradeBodyMesh(bo, scene, clickables, meshToBody) {
15562
16083
  detachSelfShadow(bo.selfShadow);
15563
16084
  bo.selfShadow = null;
15564
16085
  }
16086
+ disposeTileOverlay(bo);
16087
+ bo.tileOverlay = void 0;
15565
16088
  bo.terrainWindow = null;
15566
16089
  mesh.removeFromParent();
15567
16090
  mesh.geometry.dispose();
@@ -16269,6 +16792,7 @@ async function loadBodyTexture(bo, textureLoader, currentJd, scene, maxTextureSi
16269
16792
  if (!sceneSettings().showSurfaceTexture) return;
16270
16793
  if (!surface) return;
16271
16794
  bo.textureBundleId = surface.id === bodyId ? void 0 : surface.id;
16795
+ bo.surfaceTiles = surface.tiles;
16272
16796
  bo.availableTiers ??= surface.tiers ?? [...TIER_NAMES];
16273
16797
  bo.availableFrames = surface.frames;
16274
16798
  await swapBodyTexture(bo, "low", textureFrameForJd(currentJd, bo.availableFrames), textureLoader);
@@ -16288,6 +16812,7 @@ function unloadBodyTexture(bo) {
16288
16812
  bo.textureLoadGen = (bo.textureLoadGen ?? 0) + 1;
16289
16813
  const material = bo.mesh.material;
16290
16814
  unloadSiblingLayer("topography", bo);
16815
+ disposeTileOverlay(bo);
16291
16816
  if (!material.map) return;
16292
16817
  material.map.dispose();
16293
16818
  material.map = null;
@@ -17631,9 +18156,11 @@ function makeTrail(body, color, basisPos = [
17631
18156
  const { orbitElements, orbitCenter, data } = body;
17632
18157
  let curve;
17633
18158
  let isOpenCurve;
18159
+ let start;
17634
18160
  if (data.satrec) {
17635
18161
  curve = sgp4Curve(data.satrec, jd, data.n / 360, 512);
17636
18162
  isOpenCurve = true;
18163
+ start = curve.length - 1;
17637
18164
  } else {
17638
18165
  if (!orbitElements) throw new Error("makeTrail called without orbitElements");
17639
18166
  curve = null;
@@ -17643,7 +18170,7 @@ function makeTrail(body, color, basisPos = [
17643
18170
  const cy = orbitCenter?.[1] ?? 0;
17644
18171
  const cz = orbitCenter?.[2] ?? 0;
17645
18172
  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) : [];
18173
+ const validPoints = curve ? buildTrailPoints(body, curve, isOpenCurve, cx, cy, cz, start) : [];
17647
18174
  if (curve && validPoints.length < 2) return makeEmptyTrail();
17648
18175
  const bufferCapacity = curve ? Math.max(validPoints.length, curve.length) : 512 + 1;
17649
18176
  const fullAlphas = new Float32Array(bufferCapacity);
@@ -17823,6 +18350,7 @@ async function loadSystemData(barycenterId, bodyObjects, scene, textureLoader, c
17823
18350
  bo.availableTiers = surface.tiers;
17824
18351
  bo.availableFrames = surface.frames;
17825
18352
  bo.textureBundleId = surface.id === bodyId ? void 0 : surface.id;
18353
+ bo.surfaceTiles = surface.tiles;
17826
18354
  if (!bo.textureTier) {
17827
18355
  const frame = textureFrameForJd(currentJd, bo.availableFrames);
17828
18356
  promises.push(loadBodyTextureTier(bo, "low", frame, textureLoader));
@@ -19650,15 +20178,17 @@ function refreshTrail(body, line, basisPos, jd, view) {
19650
20178
  const useTrail = line.userData.useTrail;
19651
20179
  const oc = line.userData.orbitCenter;
19652
20180
  const cx = oc.x, cy = oc.y, cz = oc.z;
20181
+ let windowStart;
19653
20182
  if (body.data.satrec) {
19654
20183
  const meanMotion = body.data.n / 360;
19655
20184
  const stepDays = meanMotion > 0 ? 1 / meanMotion / 512 : 0;
19656
- const curveJd = line.userData.curveJd;
20185
+ let curveJd = line.userData.curveJd;
19657
20186
  if (curve.length === 0 || curveJd === void 0 || Math.abs(jd - curveJd) >= stepDays) {
19658
20187
  curve = sgp4Curve(body.data.satrec, jd, meanMotion, 512);
19659
20188
  line.userData.sourceCurve = curve;
19660
- line.userData.curveJd = jd;
20189
+ line.userData.curveJd = curveJd = jd;
19661
20190
  }
20191
+ windowStart = curve.length - (jd < curveJd ? 2 : 1);
19662
20192
  } else if (body.orbitElements) {
19663
20193
  if (body.rederiveElements) {
19664
20194
  const elementsJd = line.userData.elementsJd ?? jd;
@@ -19697,7 +20227,7 @@ function refreshTrail(body, line, basisPos, jd, view) {
19697
20227
  const headY = anchor[1] - cy;
19698
20228
  const headZ = anchor[2] - cz;
19699
20229
  if (withinTolerance(ud, curve, offX, offY, offZ, headX, headY, headZ, view)) return;
19700
- const validPoints = buildTrailPoints(body, curve, isOpenCurve, cx, cy, cz);
20230
+ const validPoints = buildTrailPoints(body, curve, isOpenCurve, cx, cy, cz, windowStart);
19701
20231
  if (validPoints.length < 2) return;
19702
20232
  const { posArr, trailArr, fullArr, capacity } = getTrailWorkingArrays(line);
19703
20233
  const bx = cx - basisPos[0], by = cy - basisPos[1], bz = cz - basisPos[2];
@@ -20998,11 +21528,10 @@ function updatePositions(params) {
20998
21528
  setVec(focus.focusTargetWorld, p);
20999
21529
  const camOff = focus.camTargetOffset;
21000
21530
  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);
21531
+ } else if (a.oldPos) for (let k = 0; k < 3; k++) {
21532
+ const moved = p[k] - a.oldPos[k];
21533
+ focus.focusTruePos[k] += moved;
21534
+ focus.focusTargetWorld[k] += moved;
21006
21535
  }
21007
21536
  break;
21008
21537
  }
@@ -21025,8 +21554,10 @@ function updatePositions(params) {
21025
21554
  refreshTrail(bo.body, line, basis, jd, trailView);
21026
21555
  line.userData.refreshDeferred = false;
21027
21556
  }
21557
+ const focusLoading = !focusPos && why === "loading";
21028
21558
  return {
21029
- focusUnplaced: focusedBody !== void 0 && !focusPos,
21559
+ focusUnplaced: focusedBody !== void 0 && !focusPos && !focusLoading,
21560
+ focusLoading,
21030
21561
  anchorId
21031
21562
  };
21032
21563
  }
@@ -21403,11 +21934,14 @@ var Placer = class {
21403
21934
  if (jd === this.passJd) this.settled.add(id);
21404
21935
  return pos;
21405
21936
  }
21937
+ under(aboveId) {
21938
+ return (this.ctx.getBody(aboveId) ?? this.bodyObjects.get(aboveId)?.body)?.unplaced === "loading" ? "loading" : "parent";
21939
+ }
21406
21940
  seatFeature(body, jd) {
21407
21941
  const hostId = body.featureAnchor.hostId;
21408
21942
  const host = this.ctx.getBody(hostId);
21409
21943
  const hostPos = host && this.place(host, jd);
21410
- if (!host || !hostPos) return setUnplaced(body, "parent");
21944
+ if (!host || !hostPos) return setUnplaced(body, this.under(hostId));
21411
21945
  return seatFeatureBody(body, host, hostPos, this.bodyObjects.get(hostId), jd);
21412
21946
  }
21413
21947
  rideAt(jd) {
@@ -21438,8 +21972,9 @@ var Placer = class {
21438
21972
  if (d.orbitalSource === OrbitalSource.SPICE_PROBE) return this.settleProbe(body, bo, jd);
21439
21973
  const parentPos = this.at(d.parentId, jd);
21440
21974
  if (!parentPos) {
21441
- diagnostics.warnOnce("missing-parent", d.id, () => `place[${d.id}]: parent ${d.parentId} has no place`);
21442
- return setUnplaced(body, "parent");
21975
+ const why = this.under(d.parentId);
21976
+ if (why === "parent") diagnostics.warnOnce("missing-parent", d.id, () => `place[${d.id}]: parent ${d.parentId} has no place`);
21977
+ return setUnplaced(body, why);
21443
21978
  }
21444
21979
  diagnostics.clear("missing-parent", d.id);
21445
21980
  let offset;
@@ -21449,9 +21984,10 @@ var Placer = class {
21449
21984
  return this.finish(body, bo, parentPos[0] + offset[0], parentPos[1] + offset[1], parentPos[2] + offset[2], parentPos, jd);
21450
21985
  }
21451
21986
  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) {
21987
+ const store = this.ctx.chebStore;
21988
+ if (store.isLoading(id, jd)) return "loading";
21989
+ const coverage = store.zoneCoverage(id);
21990
+ if (coverage !== null && (jd < coverage.start || jd > coverage.end) && jd === this.passJd) {
21455
21991
  this.noData.count++;
21456
21992
  if (coverage.start < this.noData.earliestStart) this.noData.earliestStart = coverage.start;
21457
21993
  if (coverage.end > this.noData.latestEnd) this.noData.latestEnd = coverage.end;
@@ -21460,7 +21996,7 @@ var Placer = class {
21460
21996
  const cov = coverage ? `[${coverage.start.toFixed(1)},${coverage.end.toFixed(1)}]` : "unknown";
21461
21997
  return `chebStore.positionScene[${id}] returned null at jd=${jd.toFixed(3)} (coverage=${cov})`;
21462
21998
  });
21463
- return outside ? "no-data" : "loading";
21999
+ return "no-data";
21464
22000
  }
21465
22001
  finish(body, bo, x, y, z, parentPos, jd) {
21466
22002
  const d = body.data;
@@ -21523,6 +22059,7 @@ var Placer = class {
21523
22059
  const zonePref = activeSysId ? (fitCenterNaif) => ctx.bodies.isInSystem(`naif-${fitCenterNaif}`, activeSysId) : void 0;
21524
22060
  const located = ctx.probeStore?.probeWithCenter(d.id, jd, zonePref) ?? null;
21525
22061
  if (!located) {
22062
+ if (ctx.probeStore?.loadingAt(jd)) return setUnplaced(body, "loading");
21526
22063
  setUnplaced(body, "no-data");
21527
22064
  diagnostics.warnOnce("probe-unavailable", d.id, () => {
21528
22065
  const reason = !ctx.probeStore ? "no ProbeStore" : "no zone has both a loaded chunk and a sub-chunk covering this jd";
@@ -21534,7 +22071,7 @@ var Placer = class {
21534
22071
  if (probeLanded && isLandedAt(located.probe, jd)) {
21535
22072
  const flyingFrameId = d.parentId;
21536
22073
  const landedRender = renderLandedProbe(d, located.probe, probeLanded, jd, (id) => this.at(id, jd), ctx, bodyObjects);
21537
- if (!landedRender) return setUnplaced(body, "parent");
22074
+ if (!landedRender) return setUnplaced(body, this.under(`naif-${probeLanded.bodyNaifId}`));
21538
22075
  diagnostics.clear("probe-unavailable", d.id);
21539
22076
  if (flyingFrameId !== d.parentId) body.trailBuffer?.clear();
21540
22077
  if (bo) {
@@ -21559,7 +22096,8 @@ var Placer = class {
21559
22096
  }
21560
22097
  const primary = resolveProbePrimary(located.probe, jd, located.fitCenterNaifId, ctx.chebStore ?? null, (id) => this.at(id, jd) !== null);
21561
22098
  if (!primary) {
21562
- setUnplaced(body, "parent");
22099
+ const center = located.probe.fitCenter?.id;
22100
+ setUnplaced(body, center ? this.under(center) : "parent");
21563
22101
  diagnostics.warnOnce("probe-unavailable", d.id, () => `probe ${d.id} (${d.name ?? "unnamed"}): hidden — stamped fit center is not placeable this frame`);
21564
22102
  return null;
21565
22103
  }
@@ -21606,7 +22144,7 @@ var Placer = class {
21606
22144
  if (probeParentChanged) d.parentId = probeParentKey;
21607
22145
  const probeParentPos = this.at(probeParentKey, jd);
21608
22146
  if (!probeParentPos) {
21609
- setUnplaced(body, "parent");
22147
+ setUnplaced(body, this.under(probeParentKey));
21610
22148
  diagnostics.warnOnce("probe-unavailable", d.id, () => `probe ${d.id} (${d.name ?? "unnamed"}): hidden — fit center ${probeParentKey} has no position this frame`);
21611
22149
  return null;
21612
22150
  }
@@ -21883,11 +22421,11 @@ function updateAtmosphereShaders(bodyObjects, cameraPosition, visible, realistic
21883
22421
  spinAxis.set(0, 1, 0).applyQuaternion(bo.mesh.quaternion);
21884
22422
  uniforms.uSpinAxis.value.copy(spinAxis);
21885
22423
  }
21886
- const atmoMesh = bo.atmosphere.mesh;
21887
- const shellRadius = bo.atmosphere.geometryRadiusScene * atmoMesh.scale.x;
21888
- const camDist = cameraPosition.distanceTo(atmoMesh.position);
22424
+ const shellCenter = bo.root.position;
22425
+ const shellRadius = bo.atmosphere.geometryRadiusScene * bo.atmosphere.mesh.scale.x;
22426
+ const camDist = cameraPosition.distanceTo(shellCenter);
21889
22427
  if (shellRadius > camDist * .1) state.shellProminent = true;
21890
- if (quality.eclipseShadows) cullShellOccluders(uniforms, atmoMesh.position, sunVec, shellRadius);
22428
+ if (quality.eclipseShadows) cullShellOccluders(uniforms, shellCenter, sunVec, shellRadius);
21891
22429
  const inside = quality.insideView && camDist < shellRadius;
21892
22430
  const referenceKm = bo.atmosphere.planetRadiusKm;
21893
22431
  const altKm = camDist * ((referenceKm + params.topAltitudeKm) / shellRadius) - referenceKm;
@@ -21901,7 +22439,7 @@ function updateAtmosphereShaders(bodyObjects, cameraPosition, visible, realistic
21901
22439
  }
21902
22440
  if (inside) {
21903
22441
  state.insideShell = true;
21904
- const sinSunElev = camUp.copy(cameraPosition).sub(atmoMesh.position).divideScalar(camDist).dot(sunVec);
22442
+ const sinSunElev = camUp.copy(cameraPosition).sub(shellCenter).divideScalar(camDist).dot(sunVec);
21905
22443
  const dim = skyboxDimFactor(params, altKm, sinSunElev);
21906
22444
  state.skyboxIntensity *= dim * (1 - deckBlend);
21907
22445
  if (quality.refraction && params.refractivity && !underDeck) {
@@ -21915,7 +22453,7 @@ function updateAtmosphereShaders(bodyObjects, cameraPosition, visible, realistic
21915
22453
  const shellRatio = camDist / shellRadius;
21916
22454
  if (quality.sunTint && shellRatio < SUN_TINT_MAX_RATIO) {
21917
22455
  const planetRadiusScene = shellRadius / (1 + params.topAltitudeKm / referenceKm) * (bo.atmosphere.surfaceRadiusKm / referenceKm);
21918
- camRel.copy(cameraPosition).sub(atmoMesh.position);
22456
+ camRel.copy(cameraPosition).sub(shellCenter);
21919
22457
  const shellSpinAxis = uniforms.uSpinAxis.value;
21920
22458
  const shellStretch = uniforms.uStretch.value;
21921
22459
  if (sunPathTransmittance(params, camRel, sunVec, planetRadiusScene, bo.atmosphere.surfaceRadiusKm, sunT, shellSpinAxis, shellStretch)) {
@@ -21924,7 +22462,7 @@ function updateAtmosphereShaders(bodyObjects, cameraPosition, visible, realistic
21924
22462
  }
21925
22463
  if (sunViewTint && shellRatio < bestViewTintRatio) {
21926
22464
  bestViewTintRatio = shellRatio;
21927
- bindViewTint(sunViewTint, params, atmoMesh.position, planetRadiusScene, bo.atmosphere.surfaceRadiusKm, shellSpinAxis, shellStretch);
22465
+ bindViewTint(sunViewTint, params, shellCenter, planetRadiusScene, bo.atmosphere.surfaceRadiusKm, shellSpinAxis, shellStretch);
21928
22466
  }
21929
22467
  }
21930
22468
  }
@@ -22251,7 +22789,7 @@ function desiredDemTier(altitudeRadii, current) {
22251
22789
  if (tierRank(down) < tierRank(current)) return down;
22252
22790
  return current;
22253
22791
  }
22254
- function updateTextureLOD(bodyObjects, camera, renderer, ctx, textureLoader, focusedId, jd, appearances) {
22792
+ function updateTextureLOD(bodyObjects, camera, renderer, ctx, textureLoader, focusedId, jd, appearances, invalidate) {
22255
22793
  appearances.apply(bodyObjects, textureLoader);
22256
22794
  const fovRad = camera.fov * Math.PI / 180;
22257
22795
  const projScale = renderer.domElement.clientHeight / (2 * Math.tan(fovRad / 2));
@@ -22275,7 +22813,8 @@ function updateTextureLOD(bodyObjects, camera, renderer, ctx, textureLoader, foc
22275
22813
  else if (screenR < 1024 && altitudeRadii > 2) desired = "medium";
22276
22814
  else desired = "high";
22277
22815
  const currentRank = tierRank(bo.textureTier);
22278
- const desiredRank = Math.min(tierRank(desired), tierCap);
22816
+ const bodyCap = tilesServe(bo) ? Math.min(tierCap, tierRank("medium")) : tierCap;
22817
+ const desiredRank = Math.min(tierRank(desired), bodyCap);
22279
22818
  const desiredFrame = textureFrameForJd(jd, bo.availableFrames);
22280
22819
  const frameChanged = desiredFrame !== bo.textureFrame;
22281
22820
  const wantsUpgrade = desiredRank > currentRank;
@@ -22283,6 +22822,7 @@ function updateTextureLOD(bodyObjects, camera, renderer, ctx, textureLoader, foc
22283
22822
  const target = wantsUpgrade ? highestAvailableTier(desiredRank, bo.availableTiers) : bo.textureTier;
22284
22823
  if (target) loadBodyTextureTier(bo, target, desiredFrame, textureLoader);
22285
22824
  }
22825
+ updateTileOverlay(bo, camera, renderer, projScale * renderer.getPixelRatio(), settings.showSurfaceTexture && !bo.appearance?.surfaceOwned && !bo.model, invalidate);
22286
22826
  const dispTiers = bo.displacementMeta?.tiers;
22287
22827
  if (bo.displacementMap && dispTiers?.length && !bo.displacementLoading) {
22288
22828
  const maxTex = renderer.capabilities.maxTextureSize;
@@ -23630,6 +24170,7 @@ var SceneRenderer = class SceneRenderer {
23630
24170
  lastDataVersion = -1;
23631
24171
  dateHolds = 0;
23632
24172
  focusWasOutOfRange = false;
24173
+ seekAwaitsData = false;
23633
24174
  frameIndex = 0;
23634
24175
  static WAKE_FRAMES = 30;
23635
24176
  static IDLE_HEARTBEAT_FRAMES = 10;
@@ -24124,7 +24665,7 @@ var SceneRenderer = class SceneRenderer {
24124
24665
  for (const light of this.ambientLights) light.intensity = ambient;
24125
24666
  updateUserLocationOcclusion(this.userLocationMarker, this.bodyObjects, this.camera);
24126
24667
  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);
24668
+ updateTextureLOD(this.bodyObjects, this.camera, this.renderer, this.ctx, this.textureLoader, focusedIdLod, this.clock.jd, this.appearances, this.invalidate);
24128
24669
  updateSphereLOD(this.bodyObjects, this.camera, this.renderer, this.ctx, focusedIdLod);
24129
24670
  updateSunShadowLight(this.bodyObjects, this.focus.focusTruePos, this.ctx, this.shadowLight, this.sunPointLight, distance, this._tmpV3, sceneSettings().realisticLighting, this.sunIntensityScale);
24130
24671
  this.updateModelMounts();
@@ -24641,7 +25182,7 @@ var SceneRenderer = class SceneRenderer {
24641
25182
  this.clock.seeked = false;
24642
25183
  return;
24643
25184
  }
24644
- const seeked = this.clock.seeked;
25185
+ const seeked = this.clock.seeked || this.seekAwaitsData;
24645
25186
  this.clock.seeked = false;
24646
25187
  this.lastUpdatedJd = this.clock.jd;
24647
25188
  this.lastUpdatedSystemId = systemId;
@@ -24668,6 +25209,7 @@ var SceneRenderer = class SceneRenderer {
24668
25209
  this.pointClouds.maybeRebase();
24669
25210
  }
24670
25211
  this.syncLandedNomenclature();
25212
+ this.seekAwaitsData = seeked && result.focusLoading;
24671
25213
  const losingPlace = result.focusUnplaced && !this.focusWasOutOfRange;
24672
25214
  this.focusWasOutOfRange = result.focusUnplaced;
24673
25215
  if (allowOorRefocus && seeked && losingPlace && result.anchorId) {