spacemap 0.1.13 → 0.1.14

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, 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";
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, NoBlending, NoColorSpace, NormalBlending, OneFactor, OneMinusSrcAlphaFactor, PCFShadowMap, PCFSoftShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, RGBAFormat, RGFormat, 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";
@@ -2960,6 +2960,12 @@ function get_custom_elements_slots(element) {
2960
2960
  return result;
2961
2961
  }
2962
2962
 
2963
+ //#endregion
2964
+ //#region src/lib/fetch/tile-path.ts
2965
+ function tilePath(cap, level, x, y) {
2966
+ return `${cap && `${cap}/`}${level}/${x}/${y}.webp`;
2967
+ }
2968
+
2963
2969
  //#endregion
2964
2970
  //#region src/lib/fetch/data-base.ts
2965
2971
  function dataBase() {
@@ -2968,10 +2974,12 @@ function dataBase() {
2968
2974
  function tilesBase() {
2969
2975
  return host().tilesUrl;
2970
2976
  }
2977
+ function tileRoot(tiles) {
2978
+ return `${tilesBase()}/v1/tiles/${tiles.id}/`;
2979
+ }
2971
2980
  function tileUrl(tiles, cap, level, x, y, frame) {
2972
2981
  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}`;
2982
+ return `${tileRoot(tiles)}${month}${tilePath(cap, level, x, y)}?v=${tiles.version}`;
2975
2983
  }
2976
2984
  var versions = {};
2977
2985
  function setDataVersions(v) {
@@ -3240,6 +3248,11 @@ function tileCredit(map) {
3240
3248
  if (!tiles || tilesBase() === "" || !textureAllowed(tiles.distribution)) return void 0;
3241
3249
  return tiles.source === map.source && tiles.attribution === map.attribution ? void 0 : tiles;
3242
3250
  }
3251
+ function patchCredits(map) {
3252
+ const tiles = map.tiles;
3253
+ if (!tiles?.patches || tilesBase() === "" || !textureAllowed(tiles.distribution)) return [];
3254
+ return [...new Map(tiles.patches.map((patch) => [patch.source, patch])).values()];
3255
+ }
3243
3256
 
3244
3257
  //#endregion
3245
3258
  //#region src/lib/scene/state/credits.svelte.ts
@@ -3298,8 +3311,9 @@ var CreditsStore = class {
3298
3311
  }
3299
3312
  registerImagery(layer, bodyId, systemId, meta) {
3300
3313
  this.addImagery(layer, bodyId, systemId, meta, layer === "rings");
3301
- const tiles = layer === "surface" ? tileCredit(meta) : void 0;
3302
- if (tiles) this.addImagery(layer, bodyId, systemId, tiles, true);
3314
+ if (layer !== "surface" && layer !== "topography") return;
3315
+ const tiles = tileCredit(meta);
3316
+ for (const work of [...tiles ? [tiles] : [], ...patchCredits(meta)]) this.addImagery(layer, bodyId, systemId, work, true);
3303
3317
  }
3304
3318
  addImagery(layer, bodyId, systemId, meta, bySource) {
3305
3319
  const key = [
@@ -6700,18 +6714,18 @@ function smallBodyFiltersEqual(a, b) {
6700
6714
  if (a.kind === "flag" && b.kind === "flag") return a.flag === b.flag;
6701
6715
  return false;
6702
6716
  }
6703
- var pending$2 = null;
6717
+ var pending$3 = null;
6704
6718
  function fetchGroupIndex() {
6705
- if (pending$2) return pending$2;
6719
+ if (pending$3) return pending$3;
6706
6720
  const p = fetchWithTimeout(`${dataBase()}/v1/groups/__index__.json`).then((r) => {
6707
6721
  if (!r.ok) throw new Error(`Failed to fetch group index: ${r.status}`);
6708
6722
  return r.json();
6709
6723
  });
6710
- pending$2 = p;
6724
+ pending$3 = p;
6711
6725
  p.catch(() => {
6712
- if (pending$2 === p) pending$2 = null;
6726
+ if (pending$3 === p) pending$3 = null;
6713
6727
  });
6714
- return pending$2;
6728
+ return pending$3;
6715
6729
  }
6716
6730
 
6717
6731
  //#endregion
@@ -8912,9 +8926,9 @@ function partsForDate(zoom, isoDate, cap = 0) {
8912
8926
  const parts = zoom.parts_by_date[isoDate] ?? zoom.parts;
8913
8927
  return cap > 0 ? Math.min(parts, cap) : parts;
8914
8928
  }
8915
- var pending$1 = null;
8929
+ var pending$2 = null;
8916
8930
  function fetchMetadata() {
8917
- if (pending$1) return pending$1;
8931
+ if (pending$2) return pending$2;
8918
8932
  const p = fetchWithTimeout(`${dataBase()}/v1/metadata.json`).then((r) => {
8919
8933
  if (!r.ok) throw new Error(`Failed to fetch metadata: ${r.status}`);
8920
8934
  return r.json();
@@ -8922,11 +8936,11 @@ function fetchMetadata() {
8922
8936
  setDataVersions(meta.versions);
8923
8937
  return meta;
8924
8938
  });
8925
- pending$1 = p;
8939
+ pending$2 = p;
8926
8940
  p.catch(() => {
8927
- if (pending$1 === p) pending$1 = null;
8941
+ if (pending$2 === p) pending$2 = null;
8928
8942
  });
8929
- return pending$1;
8943
+ return pending$2;
8930
8944
  }
8931
8945
  function chebyshevZoneParams(meta) {
8932
8946
  const out = /* @__PURE__ */ new Map();
@@ -9088,7 +9102,8 @@ var ChunkWindow = class {
9088
9102
  request(jd) {
9089
9103
  for (const [zone, params] of this.zoneParams) {
9090
9104
  const idx = chunkIndexForJd(params, jd);
9091
- if (this.isLoadable(params, idx)) this.loadChunk(zone, params, idx, "high")?.catch(() => {});
9105
+ if (!this.isLoadable(params, idx)) continue;
9106
+ this.loadChunk(zone, params, idx, "high", true)?.catch(() => {});
9092
9107
  }
9093
9108
  }
9094
9109
  loadingAt(jd, zone) {
@@ -9099,6 +9114,14 @@ var ChunkWindow = class {
9099
9114
  }
9100
9115
  return false;
9101
9116
  }
9117
+ awaitedAt(jd, zone) {
9118
+ for (const [name, params] of this.zoneParams) {
9119
+ if (zone !== void 0 && name !== zone) continue;
9120
+ const key = chunkKey(name, chunkIndexForJd(params, jd));
9121
+ if (this.inflight.has(key) || this.retries.waiting(key)) return true;
9122
+ }
9123
+ return false;
9124
+ }
9102
9125
  isResident(zone, chunkIdx) {
9103
9126
  return this.chunks.get(zone)?.has(chunkIdx) ?? false;
9104
9127
  }
@@ -9122,7 +9145,10 @@ var ChunkWindow = class {
9122
9145
  if (ok) this.retries.clear(key);
9123
9146
  else this.retries.fail(key);
9124
9147
  };
9125
- job.then(() => settle(true), () => settle(false));
9148
+ job.then(() => settle(true), (error) => {
9149
+ console.warn(`[position] chunk ${key} did not load:`, error);
9150
+ settle(false);
9151
+ });
9126
9152
  return job;
9127
9153
  }
9128
9154
  async fetchAndStore(zone, params, chunkIdx, priority) {
@@ -9161,9 +9187,9 @@ var ChebyshevStore = class extends ChunkWindow {
9161
9187
  end: params.end_jd
9162
9188
  };
9163
9189
  }
9164
- isLoading(objectId, jd) {
9190
+ isAwaited(objectId, jd) {
9165
9191
  const zone = this.idToZone.get(objectId);
9166
- return zone !== void 0 && this.loadingAt(jd, zone);
9192
+ return zone !== void 0 && this.awaitedAt(jd, zone);
9167
9193
  }
9168
9194
  resolve(objectId, jd) {
9169
9195
  const zone = this.idToZone.get(objectId);
@@ -9548,6 +9574,7 @@ var GROUP_BUNDLES = {
9548
9574
  localizedPath: (lang, b) => `/v1/groups/${lang}/${b}.json.gz`
9549
9575
  };
9550
9576
  var clientFetcher = (path, versionClass) => fetchGzipBundle(versionClass ? versionedUrl(path, versionClass) : `${dataBase()}${path}`);
9577
+ var prefetchFetcher = (path, versionClass) => fetchGzipBundle(versionClass ? versionedUrl(path, versionClass) : `${dataBase()}${path}`, { priority: "low" });
9551
9578
  async function loadBundlePair(desc, key, { meta, lang, fetchBundle, localized = true }) {
9552
9579
  const counts = meta[desc.counts];
9553
9580
  const nGlobal = counts?.global ?? 0;
@@ -9559,22 +9586,22 @@ async function loadBundlePair(desc, key, { meta, lang, fetchBundle, localized =
9559
9586
  localized: localizedBundle?.[key] ?? null
9560
9587
  };
9561
9588
  }
9562
- async function fetchBundlePair(desc, key, lang, localized = true) {
9589
+ async function fetchBundlePair(desc, key, lang, localized = true, background = false) {
9563
9590
  return loadBundlePair(desc, key, {
9564
9591
  meta: await fetchMetadata(),
9565
9592
  lang,
9566
9593
  localized,
9567
- fetchBundle: clientFetcher
9594
+ fetchBundle: background ? prefetchFetcher : clientFetcher
9568
9595
  });
9569
9596
  }
9570
9597
 
9571
9598
  //#endregion
9572
9599
  //#region src/lib/fetch/objects/object-data.ts
9573
9600
  function isViewable(entry) {
9574
- return entry.imagery !== "withheld" && textureAllowed(entry.distribution);
9601
+ return textureAllowed(entry.distribution);
9575
9602
  }
9576
- function fetchObjectDetail(fileId, fetchLocalized = true, lang = getLocale()) {
9577
- return fetchBundlePair(OBJECT_BUNDLES, fileId, lang, fetchLocalized);
9603
+ function fetchObjectDetail(fileId, fetchLocalized = true, lang = getLocale(), background = false) {
9604
+ return fetchBundlePair(OBJECT_BUNDLES, fileId, lang, fetchLocalized, background);
9578
9605
  }
9579
9606
 
9580
9607
  //#endregion
@@ -9937,6 +9964,7 @@ var ZoneRefresher = class {
9937
9964
  if (z.zone !== zoneName || z.kind !== "time") continue;
9938
9965
  z.currentTime = "";
9939
9966
  z.lastLoadStartMs = -Infinity;
9967
+ this.retries.clear(loadKey(z, snapshotDate(z.zoomData, this.latestDate)));
9940
9968
  }
9941
9969
  this.tick(this.latestDate);
9942
9970
  }
@@ -10006,7 +10034,11 @@ var ZoneRefresher = class {
10006
10034
  this.wanted.add(id);
10007
10035
  }
10008
10036
  async ensureBody(targetId, date) {
10009
- if (targetId.startsWith("probe-")) await this.ensureProbe(targetId, date);
10037
+ const anchorFailure = targetId.startsWith("probe-") ? await this.ensureProbe(targetId, date) : null;
10038
+ await this.ensureListed(targetId, date);
10039
+ if (anchorFailure) throw anchorFailure.error;
10040
+ }
10041
+ async ensureListed(targetId, date) {
10010
10042
  if (this.ctx.getBody(targetId)) return;
10011
10043
  if (targetId.startsWith("norad_satcat-") && !this.wanted.has(targetId)) {
10012
10044
  this.want(targetId);
@@ -10020,20 +10052,22 @@ var ZoneRefresher = class {
10020
10052
  }
10021
10053
  async ensureProbe(targetId, date) {
10022
10054
  const store = this.ctx.probeStore;
10023
- if (!store) return;
10055
+ if (!store) return null;
10024
10056
  const jd = dateToJD(date);
10025
10057
  const passengerPromise = passengerFor(targetId);
10026
10058
  await store.load(jd).catch(() => {});
10027
10059
  const passenger = await passengerPromise;
10028
10060
  if (passenger) store.registerCarried(passenger);
10029
10061
  const fcId = store.stampedFitCenterAt(targetId, jd);
10030
- if (fcId) await ensureTargetStreamed(this.ctx, fcId);
10031
- if (this.ctx.getBody(targetId)) return;
10062
+ const anchorFailure = fcId ? await ensureTargetStreamed(this.ctx, fcId).then(() => null, (error) => ({ error })) : null;
10063
+ if (this.ctx.getBody(targetId)) return anchorFailure;
10032
10064
  const labels = await fetchLabels();
10033
10065
  const target = this.loader.processProbes(store, date, labels).find((b) => b.data.id === targetId);
10034
- if (!target) return;
10035
- this.ctx.bodies.addBodies([target]);
10036
- this.ctx.credits.recordOrbitSources([target]);
10066
+ if (target) {
10067
+ this.ctx.bodies.addBodies([target]);
10068
+ this.ctx.credits.recordOrbitSources([target]);
10069
+ }
10070
+ return anchorFailure;
10037
10071
  }
10038
10072
  async loadTime(z, time, date) {
10039
10073
  const fromTime = z.currentTime;
@@ -10212,6 +10246,9 @@ var RENDER_TIER_PRESETS = {
10212
10246
  pointBudget: 4e5,
10213
10247
  maxTextureTier: "medium",
10214
10248
  tileWindows: 3,
10249
+ reliefWindows: 2,
10250
+ reliefShadowSteps: 12,
10251
+ terrainBandCells: 16,
10215
10252
  maxSkyboxFace: 2048
10216
10253
  },
10217
10254
  medium: {
@@ -10222,6 +10259,9 @@ var RENDER_TIER_PRESETS = {
10222
10259
  pointBudget: 9e5,
10223
10260
  maxTextureTier: "medium",
10224
10261
  tileWindows: 4,
10262
+ reliefWindows: 3,
10263
+ reliefShadowSteps: 16,
10264
+ terrainBandCells: 24,
10225
10265
  maxSkyboxFace: 2048
10226
10266
  },
10227
10267
  high: {
@@ -10232,6 +10272,9 @@ var RENDER_TIER_PRESETS = {
10232
10272
  pointBudget: Infinity,
10233
10273
  maxTextureTier: "high",
10234
10274
  tileWindows: 6,
10275
+ reliefWindows: 4,
10276
+ reliefShadowSteps: 32,
10277
+ terrainBandCells: 32,
10235
10278
  maxSkyboxFace: Infinity
10236
10279
  }
10237
10280
  };
@@ -10243,6 +10286,13 @@ function maxTileWindows() {
10243
10286
  if (isLowEndDevice()) return 0;
10244
10287
  return currentRenderTier().tileWindows;
10245
10288
  }
10289
+ function maxReliefWindows() {
10290
+ if (isLowEndDevice()) return 0;
10291
+ return currentRenderTier().reliefWindows;
10292
+ }
10293
+ function reliefShadowSteps() {
10294
+ return currentRenderTier().reliefShadowSteps;
10295
+ }
10246
10296
  function resolveRenderTier() {
10247
10297
  const measured = sceneSettings().atmosphereCalibration?.tier;
10248
10298
  if (measured === "low" || measured === "medium") return measured;
@@ -10955,7 +11005,7 @@ var SimClock = class {
10955
11005
  //#endregion
10956
11006
  //#region src/lib/math/spherical.ts
10957
11007
  var { PI, sin, cos, asin, atan2, sqrt } = Math;
10958
- var DEG$8 = 180 / PI;
11008
+ var DEG$9 = 180 / PI;
10959
11009
  var RAD$5 = PI / 180;
10960
11010
  function applyQuat(v, q, inverse) {
10961
11011
  const [x, y, z] = v;
@@ -10986,8 +11036,8 @@ function cartesianToSpherical(cam, target, bodyQuat) {
10986
11036
  dz
10987
11037
  ], bodyQuat, true);
10988
11038
  return {
10989
- latitude: asin(dy / distance) * DEG$8,
10990
- longitude: bodyQuat ? atan2(-dz, dx) * DEG$8 : atan2(dx, dz) * DEG$8,
11039
+ latitude: asin(dy / distance) * DEG$9,
11040
+ longitude: bodyQuat ? atan2(-dz, dx) * DEG$9 : atan2(dx, dz) * DEG$9,
10991
11041
  distance
10992
11042
  };
10993
11043
  }
@@ -11945,7 +11995,7 @@ function tagShaderModifier(material, mod) {
11945
11995
  const mods = tagged.userData.shaderMods ??= [];
11946
11996
  if (mods.includes(mod)) return;
11947
11997
  mods.push(mod);
11948
- material.customProgramCacheKey = () => mods.join("|");
11998
+ material.customProgramCacheKey = () => mods.map((name) => typeof name === "string" ? name : name()).join("|");
11949
11999
  }
11950
12000
  function chainShaderHook(material, mod, patch) {
11951
12001
  const prev = material.onBeforeCompile;
@@ -15551,159 +15601,6 @@ function setStarLimbAlpha(material, alpha) {
15551
15601
  if (alpha && uniforms?.uLimbAlpha) uniforms.uLimbAlpha.value.set(alpha[0], alpha[1], alpha[2]);
15552
15602
  }
15553
15603
 
15554
- //#endregion
15555
- //#region src/lib/scene/objects/surface/self-shadow.ts
15556
- var STEPS = 32;
15557
- var NORMAL_STRENGTH = 1;
15558
- var GRADIENT_STENCIL = 2;
15559
- var SLOPE_CLAMP = 1.5;
15560
- var SHADOW_REACH = 4;
15561
- var SHADOW_SOFT = .04;
15562
- var TERMINATOR_GATE_LIT = .02;
15563
- var TERMINATOR_GATE_DARK = -.08;
15564
- var MARCH_GRAZE_FADE = .18;
15565
- function attachSelfShadowToBody(material, heightMap, scaleScene, sunDir) {
15566
- const tex = heightMap.image;
15567
- const texel = new Vector2(1 / (tex?.width ?? 4096), 1 / (tex?.height ?? 2048));
15568
- const existing = material.userData.selfShadow;
15569
- if (existing) {
15570
- existing.uSelfHeightMap.value = heightMap;
15571
- existing.uSelfScale.value = scaleScene;
15572
- existing.uSelfTexel.value.copy(texel);
15573
- return existing;
15574
- }
15575
- const uniforms = {
15576
- uSelfHeightMap: { value: heightMap },
15577
- uSelfScale: { value: scaleScene },
15578
- uSelfTexel: { value: texel }
15579
- };
15580
- material.userData.selfShadow = uniforms;
15581
- const sun = sunDir ?? getEclipseSceneUniforms().uSunDir;
15582
- chainShaderHook(material, "selfShadow", (shader) => {
15583
- Object.assign(shader.uniforms, uniforms, { uSelfSunDir: sun });
15584
- shader.vertexShader = shader.vertexShader.replace("#include <common>", `#include <common>
15585
- varying vec3 vSelfWorldNormal;
15586
- varying vec2 vSelfUv;
15587
- varying vec3 vSelfEastW;
15588
- varying vec3 vSelfNorthW;
15589
- varying float vSelfRadiusW;
15590
- varying float vSelfCosLat;`).replace("#include <beginnormal_vertex>", `#include <beginnormal_vertex>
15591
- vSelfWorldNormal = normalize(mat3(modelMatrix) * objectNormal);
15592
- vSelfUv = uv;
15593
- // Analytic equirect tangent basis: smooth per fragment, so the
15594
- // relief normal doesn't inherit mesh facets.
15595
- vec3 nObj = normalize(position);
15596
- vec3 ec = cross(vec3(0.0, 1.0, 0.0), nObj);
15597
- float cl = length(ec);
15598
- vec3 eastObj = cl > 1e-5 ? ec / cl : vec3(1.0, 0.0, 0.0);
15599
- vSelfEastW = normalize(mat3(modelMatrix) * eastObj);
15600
- vSelfNorthW = normalize(mat3(modelMatrix) * cross(nObj, eastObj));
15601
- vSelfRadiusW = length(mat3(modelMatrix) * position);
15602
- vSelfCosLat = cl;`);
15603
- shader.fragmentShader = shader.fragmentShader.replace("#include <common>", `#include <common>
15604
- #define SELF_STEPS ${STEPS}
15605
- uniform sampler2D uSelfHeightMap;
15606
- uniform float uSelfScale;
15607
- uniform vec2 uSelfTexel;
15608
- uniform vec3 uSelfSunDir;
15609
- varying vec3 vSelfWorldNormal;
15610
- varying vec2 vSelfUv;
15611
- varying vec3 vSelfEastW;
15612
- varying vec3 vSelfNorthW;
15613
- varying float vSelfRadiusW;
15614
- varying float vSelfCosLat;
15615
-
15616
- float selfHeight(vec2 uv) {
15617
- return texture2D(uSelfHeightMap, uv).r * uSelfScale;
15618
- }
15619
-
15620
- // Analytic equirect frame (Tu=∂pos/∂u east, Tv=∂pos/∂v south). Screen-
15621
- // derivative tangents were piecewise-constant per triangle, tiling the
15622
- // relief normal with mesh facets. |Tu|=2πR·cos(lat), |Tv|=πR is the true metric.
15623
- void selfTangents(out vec3 Tu, out vec3 Tv) {
15624
- float R = vSelfRadiusW;
15625
- Tu = vSelfEastW * (6.2831853 * R * vSelfCosLat);
15626
- Tv = -vSelfNorthW * (3.14159265 * R);
15627
- }`).replace("#include <normal_fragment_maps>", `#include <normal_fragment_maps>
15628
- {
15629
- vec3 Tu, Tv;
15630
- selfTangents(Tu, Tv);
15631
- float lenTu = length(Tu);
15632
- float lenTv = length(Tv);
15633
- vec3 Nw = normalize(vSelfWorldNormal);
15634
- if (uSelfScale > 0.0 && lenTu > 1e-12 && lenTv > 1e-12) {
15635
- // Wider-than-one-texel stencil averages out 8-bit terracing, floored
15636
- // at the low tier's texel so a higher-res map keeps the same
15637
- // physical smoothing width instead of flattening the relief.
15638
- vec2 d = max(uSelfTexel, vec2(1.0 / 2048.0, 1.0 / 1024.0)) * ${GRADIENT_STENCIL.toFixed(3)};
15639
- float hu = selfHeight(vSelfUv + vec2(d.x, 0.0))
15640
- - selfHeight(vSelfUv - vec2(d.x, 0.0));
15641
- float hv = selfHeight(vSelfUv + vec2(0.0, d.y))
15642
- - selfHeight(vSelfUv - vec2(0.0, d.y));
15643
- // Slope = Δheight / Δworld-distance along each axis.
15644
- float su = ${NORMAL_STRENGTH.toFixed(3)} * hu / (2.0 * d.x * lenTu);
15645
- float sv = ${NORMAL_STRENGTH.toFixed(3)} * hv / (2.0 * d.y * lenTv);
15646
- // |Tu|/|Tv|=2·cos(lat): fade relief at the poles, where |Tu|→0 blows up east-west slope into a starburst.
15647
- float poleFade = smoothstep(0.0, 0.12, lenTu / (2.0 * lenTv));
15648
- su = clamp(su * poleFade, -${SLOPE_CLAMP.toFixed(3)}, ${SLOPE_CLAMP.toFixed(3)});
15649
- sv = clamp(sv * poleFade, -${SLOPE_CLAMP.toFixed(3)}, ${SLOPE_CLAMP.toFixed(3)});
15650
- vec3 reliefN = cross(Tu / lenTu + Nw * su, Tv / lenTv + Nw * sv);
15651
- float rl = length(reliefN);
15652
- if (rl > 1e-12) {
15653
- reliefN /= rl;
15654
- if (dot(reliefN, Nw) < 0.0) reliefN = -reliefN;
15655
- normal = normalize((viewMatrix * vec4(reliefN, 0.0)).xyz);
15656
- }
15657
- }
15658
- }`).replace("#include <lights_fragment_end>", `#include <lights_fragment_end>
15659
- {
15660
- vec3 Nw = normalize(vSelfWorldNormal);
15661
- vec3 L = normalize(uSelfSunDir);
15662
- float gN = dot(L, Nw);
15663
- // Gate on the GEOMETRIC normal: the relief normal can tilt sunward
15664
- // past the terminator and light the night side. Fading direct light
15665
- // by geometric N·L keeps the night dark regardless.
15666
- float shadow = smoothstep(${TERMINATOR_GATE_DARK.toFixed(3)}, ${TERMINATOR_GATE_LIT.toFixed(3)}, gN);
15667
- vec3 Tu, Tv;
15668
- selfTangents(Tu, Tv);
15669
- float lenTu = length(Tu);
15670
- float lenTv = length(Tv);
15671
- // Local crater self-shadow, lit hemisphere only — the macro gate owns
15672
- // the night side; the short march can't see that curvature.
15673
- if (shadow > 0.0 && uSelfScale > 0.0 && lenTu > 1e-12 && lenTv > 1e-12 && gN > 0.0) {
15674
- float lu = dot(L, Tu / lenTu);
15675
- float lv = dot(L, Tv / lenTv);
15676
- float lh = sqrt(lu * lu + lv * lv);
15677
- if (lh > 1e-4) {
15678
- float ds = ${SHADOW_REACH.toFixed(3)} * uSelfScale / float(SELF_STEPS);
15679
- // World step → uv step via the per-axis metric (|Tu|, |Tv|).
15680
- vec2 stepUv = ds * vec2((lu / lh) / lenTu, (lv / lh) / lenTv);
15681
- float h0 = selfHeight(vSelfUv);
15682
- float tanElev = gN / lh;
15683
- float block = 0.0;
15684
- for (int i = 1; i <= SELF_STEPS; i++) {
15685
- vec2 uv = vSelfUv + float(i) * stepUv;
15686
- float rayH = h0 + float(i) * ds * tanElev;
15687
- block = max(block, selfHeight(uv) - rayH);
15688
- }
15689
- // Fade the march out at grazing angles: a near-horizontal ray
15690
- // occludes every away-facing slope, banding the terminator.
15691
- float marchFade = smoothstep(0.0, ${MARCH_GRAZE_FADE.toFixed(3)}, gN);
15692
- shadow *= 1.0 - marchFade * clamp(block / (${SHADOW_SOFT.toFixed(3)} * uSelfScale), 0.0, 1.0);
15693
- }
15694
- }
15695
- reflectedLight.directDiffuse *= shadow;
15696
- reflectedLight.directSpecular *= shadow;
15697
- }`);
15698
- });
15699
- return uniforms;
15700
- }
15701
- function detachSelfShadow(uniforms) {
15702
- if (!uniforms) return;
15703
- uniforms.uSelfHeightMap.value = null;
15704
- uniforms.uSelfScale.value = 0;
15705
- }
15706
-
15707
15604
  //#endregion
15708
15605
  //#region src/lib/scene/lod/tile-window.ts
15709
15606
  var WINDOW_TILES = 4;
@@ -15717,7 +15614,7 @@ var CHART_FOLDER = [
15717
15614
  ];
15718
15615
  var FIRST_TILE_LEVEL = 3;
15719
15616
  var HOLD_TILES = .75;
15720
- var BASE_WIDTH_PX = 2048;
15617
+ var BASE_WIDTH_PX$1 = 2048;
15721
15618
  var CAP_EDGE = Math.SQRT2 - 1;
15722
15619
  var CAP_ENTER_DEG = 62;
15723
15620
  var CAP_LEAVE_DEG = 58;
@@ -15731,7 +15628,7 @@ function chartRows(chart, level) {
15731
15628
  function neededLevel(radiusScene, dist, projScale, maxLevel) {
15732
15629
  const height = Math.max(dist - radiusScene, radiusScene * 1e-6);
15733
15630
  const widthPx = 2 * Math.PI * radiusScene * projScale / height;
15734
- return Math.min(Math.ceil(Math.log2(widthPx / BASE_WIDTH_PX)), maxLevel);
15631
+ return Math.min(Math.ceil(Math.log2(widthPx / BASE_WIDTH_PX$1)), maxLevel);
15735
15632
  }
15736
15633
  function chartPoint(chart, x, y, z) {
15737
15634
  if (chart === 0) {
@@ -15752,12 +15649,17 @@ function cameraChart(y, previous) {
15752
15649
  const cap = y >= 0 ? 1 : 2;
15753
15650
  return Math.asin(Math.min(Math.abs(y), 1)) * 180 / Math.PI > (previous === cap ? CAP_LEAVE_DEG : CAP_ENTER_DEG) ? cap : 0;
15754
15651
  }
15755
- function windowChain(level, count, chart, y) {
15652
+ function windowChain(level, count, chart, y, maxLevel) {
15756
15653
  const chain = [];
15757
- for (let z = level; z >= 3 && chain.length < count; z--) chain.push({
15758
- chart,
15759
- level: z
15760
- });
15654
+ const own = Math.max(1, Math.floor(count / 3));
15655
+ let z = level;
15656
+ while (z >= 3 && chain.length < count) {
15657
+ chain.push({
15658
+ chart,
15659
+ level: z
15660
+ });
15661
+ z = z > maxLevel && chain.length >= count - own ? Math.min(z - 1, maxLevel) : z - 1;
15662
+ }
15761
15663
  const reached = chain.length > 0 && chain[chain.length - 1].level === 3;
15762
15664
  const both = Math.abs(y) > Math.sin(BOTH_CHARTS_DEG * Math.PI / 180);
15763
15665
  if (reached && both && chain.length < count) {
@@ -15793,6 +15695,42 @@ function placeWindow(chart, level, x, y, previous) {
15793
15695
  ty0
15794
15696
  };
15795
15697
  }
15698
+ function patchOfTile(patches, level, x, y) {
15699
+ return patches?.find((patch) => {
15700
+ const up = patch.max_level - level;
15701
+ return up >= 0 && x >= patch.x[0] >> up && x <= patch.x[1] >> up && y >= patch.y[0] >> up && y <= patch.y[1] >> up;
15702
+ });
15703
+ }
15704
+ var REACH_TILES = 4 / 2 - .5;
15705
+ function deepestLevel(maxLevel, patches, x, y) {
15706
+ let deepest = maxLevel;
15707
+ if (!patches) return deepest;
15708
+ for (const patch of patches) for (let level = patch.max_level; level > deepest; level--) {
15709
+ const up = patch.max_level - level;
15710
+ const columns = chartColumns(0, level);
15711
+ const x0 = patch.x[0] >> up;
15712
+ const y0 = patch.y[0] >> up;
15713
+ const halfWidth = ((patch.x[1] >> up) + 1 - x0) / 2;
15714
+ const halfHeight = ((patch.y[1] >> up) + 1 - y0) / 2;
15715
+ let east = x * columns - (x0 + halfWidth);
15716
+ east -= columns * Math.round(east / columns);
15717
+ const south = y * chartRows(0, level) - (y0 + halfHeight);
15718
+ if (Math.abs(east) - halfWidth <= REACH_TILES && Math.abs(south) - halfHeight <= REACH_TILES) {
15719
+ deepest = level;
15720
+ break;
15721
+ }
15722
+ }
15723
+ return deepest;
15724
+ }
15725
+ function windowHome(placement, x, y) {
15726
+ const columns = chartColumns(placement.chart, placement.level);
15727
+ let east = x * columns - placement.tx0;
15728
+ east -= columns * Math.round(east / columns);
15729
+ return {
15730
+ x: east,
15731
+ y: y * chartRows(placement.chart, placement.level) - placement.ty0
15732
+ };
15733
+ }
15796
15734
  function slotTile(placement, sx, sy) {
15797
15735
  const columns = chartColumns(placement.chart, placement.level);
15798
15736
  const dx = ((sx - placement.tx0) % 4 + 4) % 4;
@@ -15804,24 +15742,190 @@ function slotTile(placement, sx, sy) {
15804
15742
  }
15805
15743
 
15806
15744
  //#endregion
15807
- //#region src/lib/scene/objects/surface/tile-overlay.ts
15745
+ //#region src/lib/scene/relief/worker.ts?worker&inline
15746
+ var jsContent$1 = "function e(e,t,n,r){return`${e&&`${e}/`}${t}/${n}/${r}.webp`}function t(e,t,n,r,i=1){let a=t.length,o=n.length,s=new Float32Array(a*o*3),c=new Float32Array(a*o*3),l=new Float32Array(a*o*2),u=Float64Array.from(n,Math.sin),d=Float64Array.from(n,Math.cos);for(let f=0;f<a;f++){let a=t[f],p=Math.sin(a),m=Math.cos(a);for(let t=0;t<o;t++){let h=f*o+t,g=-d[t]*p,_=m,v=u[t]*p,y=e+(r?r[h]*i:0),b=h*3;s[b]=y*g,s[b+1]=y*_,s[b+2]=y*v,c[b]=g,c[b+1]=_,c[b+2]=v;let x=h*2;l[x]=n[t]/(2*Math.PI),l[x+1]=1-a/Math.PI}}let f=new Uint32Array((a-1)*(o-1)*6),p=0;for(let e=0;e<a-1;e++){let n=t[e]<=0,r=t[e+1]>=Math.PI;for(let t=0;t<o-1;t++){let i=e*o+t+1,a=e*o+t,s=(e+1)*o+t,c=(e+1)*o+t+1;n||(f[p++]=i,f[p++]=a,f[p++]=c),r||(f[p++]=a,f[p++]=s,f[p++]=c)}}return{positions:s,normals:c,uvs:l,indices:f.subarray(0,p)}}const n=[``,`north`,`south`],r=Math.SQRT2-1;function i(e,t){return e===0?2<<t:1<<t-1}function a(e,t){return e===0?1<<t:1<<t-1}function o(e,t,n,i){if(e===0){let e=Math.atan2(i,-t)/(2*Math.PI);return{x:e<0?e+1:e,y:Math.acos(Math.min(Math.max(n,-1),1))/Math.PI}}let a=e===1?1:-1,o=.5/(r*(1+a*n));return{x:.5+t*o,y:.5+a*i*o}}const s=Math.sin(60*Math.PI/180);function c(e,t,n,r){return((e*16+t)*8192+n)*8192+r}function l(e){let t=e%8192,n=Math.floor(e/8192)%8192,r=Math.floor(e/(8192*8192));return{chart:Math.floor(r/16),level:r%16,x:n,y:t}}const u={chart:0,level:0,x0:0,x1:0,y0:0,y1:0,fx:0,fy:0};function d(e,t,n,r,c){let l=e<1||Math.abs(n)<=s?0:n>0?1:2,d=i(l,e)*c,f=a(l,e)*c,p=o(l,t,n,r),m=p.x*d-.5,h=p.y*f-.5,g=Math.floor(m),_=Math.floor(h),v=e=>Math.min(Math.max(e,0),d-1),y=e=>Math.min(Math.max(e,0),f-1);return u.chart=l,u.level=e,u.fx=m-g,u.fy=h-_,u.x0=l===0?(g+d)%d:v(g),u.x1=l===0?(g+1)%d:v(g+1),u.y0=y(_),u.y1=y(_+1),u}function f(e,t,n){let r=Float64Array.from(t.at,Math.sin),i=Float64Array.from(t.at,Math.cos),a=t.at.length;for(let o=0;o<e.at.length;o++){let s=Math.sin(e.at[o]),c=Math.cos(e.at[o]);for(let l=0;l<a;l++){let u=Math.min(e.level[o],t.level[l]);n(o*a+l,u,-i[l]*s,c,r[l]*s)}}}function p(e,t,n){let r=new Set([c(0,0,0,0),c(0,0,1,0)]);return f(e,t,(e,t,i,a,o)=>{let s=d(t,i,a,o,n),l=Math.floor(s.x0/n),u=Math.floor(s.x1/n),f=Math.floor(s.y0/n),p=Math.floor(s.y1/n);r.add(c(s.chart,s.level,l,f)),u!==l&&r.add(c(s.chart,s.level,u,f)),p!==f&&(r.add(c(s.chart,s.level,l,p)),u!==l&&r.add(c(s.chart,s.level,u,p)))}),r}function m(e,t,n,r,i,a){for(let o=t;o>=0;o--){let t=d(o,n,r,i,a),s=(n,r)=>{let i=e(c(t.chart,o,Math.floor(n/a),Math.floor(r/a)));return i&&i[r%a*a+n%a]},l=s(t.x0,t.y0),u=s(t.x1,t.y0),f=s(t.x0,t.y1),p=s(t.x1,t.y1);if(l===void 0||u===void 0||f===void 0||p===void 0)continue;let m=l+(u-l)*t.fx;return m+(f+(p-f)*t.fx-m)*t.fy}}function h(e,t,n,r,i,a){let o=new Float32Array(e.at.length*t.at.length),s=!1;return f(e,t,(e,t,c,l,u)=>{let d=m(n,t,c,l,u,r);d===void 0?s=!0:o[e]=a+i*d/65535}),s?null:o}function g(e,t){let n=new Uint8Array(t*t*2);for(let r=0;r<t;r++){let i=(t-1-r)*t*4,a=r*t*2;for(let r=0;r<t;r++,i+=4,a+=2)n[a]=e[i],n[a+1]=e[i+1]}return n}function _(e,t){let n=new Uint16Array(t*t);for(let t=0,r=0;t<n.length;t++,r+=4)n[t]=e[r]<<8|e[r+1];return n}const v=self,y=new Map;let b;async function x(e,t){let n=await fetch(e,{signal:t});if(!n.ok)throw Error(`tile ${n.status}`);let r=await createImageBitmap(await n.blob(),{premultiplyAlpha:`none`,colorSpaceConversion:`none`}),i=r.width;b?.width!==i&&(b=new OffscreenCanvas(i,i));let a=b.getContext(`2d`,{willReadFrequently:!0});if(!a)throw Error(`no 2d canvas in this worker`);return a.globalCompositeOperation=`copy`,a.drawImage(r,0,0),r.close(),{rgba:a.getImageData(0,0,i,i).data,size:i}}async function S(e,t){let{rgba:n,size:r}=await x(e.url,t),i=g(n,r);v.postMessage({type:`tile`,id:e.id,size:r,bytes:i},[i.buffer])}const C=new Map;let w=``;async function T(r,i){let{rows:a,columns:o,tileSize:s}=r,c=`${r.root}?${r.version}`;c!==w&&C.clear(),w=c;let u=p(a,o,s),d=[...u].filter(e=>!C.has(e));await Promise.all(Array.from({length:6},async()=>{for(let t=d.pop();t!==void 0;t=d.pop()){let{chart:a,level:o,x:s,y:c}=l(t),u=`${r.root}${e(n[a],o,s,c)}?v=${r.version}`;try{let{rgba:e,size:n}=await x(u,i);C.set(t,_(e,n))}catch{if(i.aborted)return}}})),i.throwIfAborted();let f=h(a,o,e=>C.get(e),s,r.scaleKm,r.biasKm);for(let e of[...C.keys()])u.has(e)||C.delete(e);if(!f)throw Error(`no height tiles for the grid`);let m=t(r.radius,a.at,o.at,f,r.unitsPerKm),g=m.indices.slice();v.postMessage({type:`grid`,id:r.id,...m,indices:g,riseKm:f},[m.positions.buffer,m.normals.buffer,m.uvs.buffer,g.buffer,f.buffer])}async function E(e){let t=new AbortController;y.set(e.id,t);try{e.type===`tile`?await S(e,t.signal):await T(e,t.signal)}catch{v.postMessage({type:`failed`,id:e.id})}finally{y.delete(e.id)}}self.onmessage=e=>{let t=e.data;t.type===`abort`?y.get(t.id)?.abort():E(t)};";
15747
+ var blob$1 = typeof self !== "undefined" && self.Blob && new Blob(["URL.revokeObjectURL(import.meta.url);", jsContent$1], { type: "text/javascript;charset=utf-8" });
15748
+ function WorkerWrapper$1(options) {
15749
+ let objURL;
15750
+ try {
15751
+ objURL = blob$1 && (self.URL || self.webkitURL).createObjectURL(blob$1);
15752
+ if (!objURL) throw "";
15753
+ const worker = new Worker(objURL, {
15754
+ type: "module",
15755
+ name: options?.name
15756
+ });
15757
+ worker.addEventListener("error", () => {
15758
+ (self.URL || self.webkitURL).revokeObjectURL(objURL);
15759
+ });
15760
+ return worker;
15761
+ } catch (e) {
15762
+ return new Worker("data:text/javascript;charset=utf-8," + encodeURIComponent(jsContent$1), {
15763
+ type: "module",
15764
+ name: options?.name
15765
+ });
15766
+ }
15767
+ }
15768
+
15769
+ //#endregion
15770
+ //#region src/lib/scene/relief/client.ts
15771
+ var worker = null;
15772
+ var nextId = 1;
15773
+ var pending$1 = /* @__PURE__ */ new Map();
15774
+ function drop(reason) {
15775
+ worker?.terminate();
15776
+ worker = null;
15777
+ const lost = [...pending$1.values()];
15778
+ pending$1.clear();
15779
+ for (const { reject } of lost) reject(new Error(reason));
15780
+ }
15781
+ function ensureWorker() {
15782
+ if (worker) return worker;
15783
+ worker = new WorkerWrapper$1();
15784
+ worker.onmessage = (ev) => {
15785
+ const msg = ev.data;
15786
+ const request = pending$1.get(msg.id);
15787
+ pending$1.delete(msg.id);
15788
+ if (!request) return;
15789
+ if (msg.type === "failed") request.reject(/* @__PURE__ */ new Error("relief worker could not answer"));
15790
+ else request.resolve(msg);
15791
+ };
15792
+ worker.onerror = (ev) => drop(`relief worker error: ${ev.message || "unknown"}`);
15793
+ worker.onmessageerror = () => drop("relief worker sent a message that could not be read");
15794
+ return worker;
15795
+ }
15796
+ function ask(build, signal) {
15797
+ return new Promise((resolve, reject) => {
15798
+ if (typeof Worker === "undefined") {
15799
+ reject(/* @__PURE__ */ new Error("no workers here"));
15800
+ return;
15801
+ }
15802
+ const id = nextId++;
15803
+ pending$1.set(id, {
15804
+ resolve,
15805
+ reject
15806
+ });
15807
+ signal.addEventListener("abort", () => {
15808
+ if (!pending$1.delete(id)) return;
15809
+ ensureWorker().postMessage({
15810
+ type: "abort",
15811
+ id
15812
+ });
15813
+ reject(new DOMException("aborted", "AbortError"));
15814
+ }, { once: true });
15815
+ ensureWorker().postMessage(build(id));
15816
+ });
15817
+ }
15818
+ function absolute(url) {
15819
+ return new URL(url, location.href).href;
15820
+ }
15821
+ function loadHeightTile(url, signal) {
15822
+ return ask((id) => ({
15823
+ type: "tile",
15824
+ id,
15825
+ url: absolute(url)
15826
+ }), signal);
15827
+ }
15828
+ function buildTerrainGrid(spec, signal) {
15829
+ return ask((id) => ({
15830
+ ...spec,
15831
+ type: "grid",
15832
+ id,
15833
+ root: absolute(spec.root)
15834
+ }), signal);
15835
+ }
15836
+
15837
+ //#endregion
15838
+ //#region src/lib/scene/objects/surface/tile-chain.ts
15808
15839
  var MAX_WINDOWS = 6;
15809
- var MAX_DRAWN = MAX_WINDOWS + 1;
15840
+ var MAX_DRAWN = 6 + 1;
15810
15841
  var MAX_INFLIGHT = 6;
15811
- var MAX_FAILURES = 4;
15842
+ var MAX_FAILURES$1 = 4;
15812
15843
  var FETCH_TIMEOUT_S = 30;
15813
15844
  var IDLE_DISPOSE_S = 5;
15814
15845
  var FEATHER_TILES = .5;
15815
15846
  var MAX_ANISOTROPY = 16;
15816
15847
  var SLOTS = 4 * 4;
15817
- var PARS = `
15818
- varying vec3 vTileDir;
15819
- uniform sampler2DArray uTileMap;
15820
- uniform sampler2DArray uTileMask;
15821
- uniform vec4 uTileGrid[ ${MAX_DRAWN} ];
15822
- uniform vec4 uTileWindow[ ${MAX_DRAWN} ];
15823
- // Most taps of the anisotropic filter, and a tile's side in pixels.
15824
- uniform vec2 uTileFilter;
15848
+ var TILE_CHAIN_GLSL = `
15849
+ #ifndef TILE_CHAIN
15850
+ #define TILE_CHAIN
15851
+
15852
+ // Where a fragment is on the map and on the cap of its own hemisphere, as
15853
+ // fractions of the chart, and what a screen pixel spans there. The place on
15854
+ // the cap is from the chart's north-west corner. The place on the map is from
15855
+ // the point that the maker of the charts counts from.
15856
+ struct TileCharts {
15857
+ vec2 mapAt;
15858
+ vec2 mapDx;
15859
+ vec2 mapDy;
15860
+ vec2 capAt;
15861
+ vec2 capDx;
15862
+ vec2 capDy;
15863
+ float side;
15864
+ };
15865
+
15866
+ // The cap of the hemisphere of dir, seen from above its pole. ddx and ddy are
15867
+ // what a screen pixel adds to dir.
15868
+ void tileCap( vec3 dir, vec3 ddx, vec3 ddy, out float side, out vec2 capAt, out vec2 capDx, out vec2 capDy ) {
15869
+ side = dir.y < 0.0 ? - 1.0 : 1.0;
15870
+ float fold = 1.0 + side * dir.y;
15871
+ vec2 plane = vec2( dir.x, side * dir.z );
15872
+ float capScale = 0.5 / ( ${CAP_EDGE.toFixed(8)} * fold );
15873
+ capAt = 0.5 + plane * capScale;
15874
+ capDx = ( vec2( ddx.x, side * ddx.z ) - plane * ( side * ddx.y / fold ) ) * capScale;
15875
+ capDy = ( vec2( ddy.x, side * ddy.z ) - plane * ( side * ddy.y / fold ) ) * capScale;
15876
+ }
15877
+
15878
+ // The charts of a direction, the map counted from its north-west corner. The
15879
+ // direction is the fragment's own rather than its interpolated coordinates:
15880
+ // those have one slope per triangle, which near a pole differs enough between
15881
+ // the two triangles of a grid cell to pick different levels, and they jump at
15882
+ // the 180° meridian.
15883
+ TileCharts tileCharts( vec3 dir ) {
15884
+ TileCharts charts;
15885
+ vec3 ddx = dFdx( dir );
15886
+ vec3 ddy = dFdy( dir );
15887
+
15888
+ float flat2 = max( dir.x * dir.x + dir.z * dir.z, 1e-12 );
15889
+ float east = 1.0 / ( 2.0 * PI * flat2 );
15890
+ float south = - 1.0 / ( PI * sqrt( flat2 ) );
15891
+ charts.mapAt = vec2( fract( atan( dir.z, - dir.x ) / ( 2.0 * PI ) ), acos( clamp( dir.y, - 1.0, 1.0 ) ) / PI );
15892
+ charts.mapDx = vec2( ( dir.z * ddx.x - dir.x * ddx.z ) * east, ddx.y * south );
15893
+ charts.mapDy = vec2( ( dir.z * ddy.x - dir.x * ddy.z ) * east, ddy.y * south );
15894
+ tileCap( dir, ddx, ddy, charts.side, charts.capAt, charts.capDx, charts.capDy );
15895
+ return charts;
15896
+ }
15897
+
15898
+ // The charts of the point at + off, the map counted from at. A position on
15899
+ // the sphere has seven digits, and a tile index of a patch takes them all;
15900
+ // the offset keeps its own.
15901
+ TileCharts tileChartsFrom( vec3 at, vec3 off ) {
15902
+ TileCharts charts;
15903
+ vec3 pos = at + off;
15904
+ vec3 ox = dFdx( off );
15905
+ vec3 oy = dFdy( off );
15906
+
15907
+ // The turn about the axis and the drop from the north pole, each as the
15908
+ // angle between at and the point.
15909
+ float flat2 = max( pos.x * pos.x + pos.z * pos.z, 1e-30 );
15910
+ float flat1 = sqrt( flat2 );
15911
+ float flat0 = sqrt( at.x * at.x + at.z * at.z );
15912
+ float turn = atan( at.z * off.x - at.x * off.z, at.x * pos.x + at.z * pos.z );
15913
+ float out1 = ( 2.0 * ( at.x * off.x + at.z * off.z ) + off.x * off.x + off.z * off.z ) / ( flat1 + flat0 );
15914
+ float drop = atan( out1 * at.y - off.y * flat0, pos.y * at.y + flat1 * flat0 );
15915
+ charts.mapAt = vec2( turn / ( 2.0 * PI ), drop / PI );
15916
+ float east = 1.0 / ( 2.0 * PI * flat2 );
15917
+ float south = 1.0 / ( PI * dot( pos, pos ) );
15918
+ charts.mapDx = vec2( ( pos.z * ox.x - pos.x * ox.z ) * east, ( pos.y * ( pos.x * ox.x + pos.z * ox.z ) / flat1 - flat1 * ox.y ) * south );
15919
+ charts.mapDy = vec2( ( pos.z * oy.x - pos.x * oy.z ) * east, ( pos.y * ( pos.x * oy.x + pos.z * oy.z ) / flat1 - flat1 * oy.y ) * south );
15920
+
15921
+ // A cap goes no deeper than the pyramid, so the direction itself will do.
15922
+ float range = length( pos );
15923
+ vec3 dir = pos / range;
15924
+ vec3 ddx = ( ox - dir * dot( dir, ox ) ) / range;
15925
+ vec3 ddy = ( oy - dir * dot( dir, oy ) ) / range;
15926
+ tileCap( dir, ddx, ddy, charts.side, charts.capAt, charts.capDx, charts.capDy );
15927
+ return charts;
15928
+ }
15825
15929
 
15826
15930
  // Long and short half-axes of the footprint whose sides are fx and fy. The
15827
15931
  // two sides alone would do where the stretch runs along the screen's axes;
@@ -15840,105 +15944,61 @@ void tileAxes( vec2 fx, vec2 fy, out vec2 major, out vec2 minor ) {
15840
15944
  minor = vec2( - axis.y, axis.x ) * sqrt( lo );
15841
15945
  }
15842
15946
 
15843
- vec3 tileColour( vec3 below ) {
15844
- if ( uTileWindow[ 0 ].w < 0.5 ) return below;
15845
-
15846
- // Where the fragment is on each chart, as fractions from the chart's
15847
- // north-west corner, and what a screen pixel spans there. Both come from
15848
- // the direction of the fragment rather than its interpolated coordinates:
15849
- // those have one slope per triangle, which near a pole differs enough
15850
- // between the two triangles of a grid cell to pick different levels, and
15851
- // they jump at the 180° meridian.
15852
- vec3 dir = normalize( vTileDir );
15853
- vec3 ddx = dFdx( dir );
15854
- vec3 ddy = dFdy( dir );
15855
-
15856
- float flat2 = max( dir.x * dir.x + dir.z * dir.z, 1e-12 );
15857
- float east = 1.0 / ( 2.0 * PI * flat2 );
15858
- float south = - 1.0 / ( PI * sqrt( flat2 ) );
15859
- vec2 mapAt = vec2( fract( atan( dir.z, - dir.x ) / ( 2.0 * PI ) ), acos( clamp( dir.y, - 1.0, 1.0 ) ) / PI );
15860
- vec2 mapDx = vec2( ( dir.z * ddx.x - dir.x * ddx.z ) * east, ddx.y * south );
15861
- vec2 mapDy = vec2( ( dir.z * ddy.x - dir.x * ddy.z ) * east, ddy.y * south );
15862
-
15863
- // The cap of the fragment's own hemisphere, seen from above its pole.
15864
- float side = dir.y < 0.0 ? - 1.0 : 1.0;
15865
- float fold = 1.0 + side * dir.y;
15866
- vec2 plane = vec2( dir.x, side * dir.z );
15867
- float capScale = 0.5 / ( ${CAP_EDGE.toFixed(8)} * fold );
15868
- vec2 capAt = 0.5 + plane * capScale;
15869
- vec2 capDx = ( vec2( ddx.x, side * ddx.z ) - plane * ( side * ddx.y / fold ) ) * capScale;
15870
- vec2 capDy = ( vec2( ddy.x, side * ddy.z ) - plane * ( side * ddy.y / fold ) ) * capScale;
15947
+ // How much of a window shows at the fragment, before the mask of its tiles.
15948
+ // home is the point that the charts count the map from, in tiles from the
15949
+ // window's north-west corner. Gives the fragment's place in the window's
15950
+ // texture, in tiles, and the axes of its footprint, in the window's pixels.
15951
+ float tileWindowWeight( TileCharts charts, vec4 window, vec4 grid, vec2 home, vec2 tileFilter, out vec2 spot, out vec2 major, out vec2 minor ) {
15952
+ bool onMap = window.x < 0.5;
15953
+ // The other pole's cap.
15954
+ if ( ! onMap && ( window.x < 1.5 ) != ( charts.side > 0.0 ) ) return 0.0;
15955
+ vec2 dx = ( onMap ? charts.mapDx : charts.capDx ) * grid.xy * tileFilter.y;
15956
+ vec2 dy = ( onMap ? charts.mapDy : charts.capDy ) * grid.xy * tileFilter.y;
15957
+
15958
+ // Level this fragment asks of the window, from the short axis of its
15959
+ // footprint in the window's pixels: the long axis is the anisotropic
15960
+ // filter's to cover, as far as its taps reach. The window fades in
15961
+ // over the level below its own, as a mipmap would.
15962
+ tileAxes( dx, dy, major, minor );
15963
+ float foot = max( length( minor ), length( major ) / tileFilter.x );
15964
+ float weight = clamp( 1.0 - log2( max( foot, 1e-6 ) ), 0.0, 1.0 );
15965
+ if ( weight <= 0.0 ) return 0.0;
15871
15966
 
15872
15967
  float rows = ${4 .toFixed(1)};
15873
- vec3 colour = vec3( 0.0 );
15874
- float covered = 0.0;
15875
- for ( int i = 0; i < ${MAX_DRAWN}; i ++ ) {
15876
- vec4 window = uTileWindow[ i ];
15877
- if ( window.w < 0.5 ) break;
15878
- vec4 grid = uTileGrid[ i ];
15879
- bool onMap = window.x < 0.5;
15880
- // The other pole's cap.
15881
- if ( ! onMap && ( window.x < 1.5 ) != ( side > 0.0 ) ) continue;
15882
- vec2 tile = ( onMap ? mapAt : capAt ) * grid.xy;
15883
- vec2 dx = ( onMap ? mapDx : capDx ) * grid.xy * uTileFilter.y;
15884
- vec2 dy = ( onMap ? mapDy : capDy ) * grid.xy * uTileFilter.y;
15885
-
15886
- // Level this fragment asks of the window, from the short axis of its
15887
- // footprint in the window's pixels: the long axis is the anisotropic
15888
- // filter's to cover, as far as its taps reach. The window fades in
15889
- // over the level below its own, as a mipmap would.
15890
- vec2 major;
15891
- vec2 minor;
15892
- tileAxes( dx, dy, major, minor );
15893
- float foot = max( length( minor ), length( major ) / uTileFilter.x );
15894
- float weight = clamp( 1.0 - log2( max( foot, 1e-6 ) ), 0.0, 1.0 );
15895
- if ( weight <= 0.0 ) continue;
15896
-
15968
+ // Offset from the window's north-west corner, in tiles.
15969
+ vec2 rel = onMap ? home + charts.mapAt * grid.xy : charts.capAt * grid.xy - grid.zw;
15970
+ if ( onMap ) {
15897
15971
  // Half a pixel in from a pole of the map: past it the filter would
15898
- // read the far row.
15899
- if ( onMap ) tile.y = clamp( tile.y, 0.0005, grid.y - 0.0005 );
15900
- // Offset from the window's north-west corner; the map's longitude wraps.
15901
- vec2 rel = tile - grid.zw;
15902
- if ( onMap ) rel.x = mod( rel.x, grid.x );
15903
- if ( rel.x < 0.0 || rel.x > rows || rel.y < 0.0 || rel.y > rows ) continue;
15904
- // Distance to the window's edge, in tiles. A pole of the map is no
15905
- // edge: nothing lies past it.
15906
- vec2 near = min( rel, rows - rel );
15907
- if ( onMap && grid.w < 0.5 ) near.y = rows - rel.y;
15908
- if ( onMap && grid.w + rows > grid.y - 0.5 ) near.y = grid.w < 0.5 ? rows : rel.y;
15909
- float edge = min( near.x, near.y );
15910
-
15911
- ivec3 slot = ivec3( int( mod( tile.x, rows ) ), int( mod( tile.y, rows ) ), int( window.y ) );
15912
- float alpha = weight * texelFetch( uTileMask, slot, 0 ).r * smoothstep( 0.0, ${FEATHER_TILES.toFixed(2)}, edge );
15913
- if ( alpha <= 0.0 ) continue;
15914
-
15915
- // The texture repeats every window: a tile sits in the slot its own
15916
- // column and row pick, north up.
15917
- vec3 st = vec3( tile.x / rows, - tile.y / rows, window.y );
15918
- vec2 texel = vec2( 1.0, - 1.0 ) / ( rows * uTileFilter.y );
15919
- colour += ( 1.0 - covered ) * alpha * textureGrad( uTileMap, st, major * texel, minor * texel ).rgb;
15920
- covered += ( 1.0 - covered ) * alpha;
15921
- if ( covered > 0.995 ) break;
15922
- }
15923
- return colour + ( 1.0 - covered ) * below;
15972
+ // read the far row. The map's longitude wraps.
15973
+ rel.y = clamp( rel.y, 0.0005 - grid.w, grid.y - 0.0005 - grid.w );
15974
+ rel.x = mod( rel.x, grid.x );
15975
+ }
15976
+ if ( rel.x < 0.0 || rel.x > rows || rel.y < 0.0 || rel.y > rows ) return 0.0;
15977
+ // Distance to the window's edge, in tiles. A pole of the map is no
15978
+ // edge: nothing lies past it.
15979
+ vec2 near = min( rel, rows - rel );
15980
+ if ( onMap && grid.w < 0.5 ) near.y = rows - rel.y;
15981
+ if ( onMap && grid.w + rows > grid.y - 0.5 ) near.y = grid.w < 0.5 ? rows : rel.y;
15982
+ // The texture repeats every window: a tile sits in the slot its own
15983
+ // column and row pick, north up.
15984
+ spot = rel + mod( grid.zw, rows );
15985
+ return weight * smoothstep( 0.0, ${FEATHER_TILES.toFixed(2)}, min( near.x, near.y ) );
15924
15986
  }
15925
- `;
15926
- var MAP_FRAGMENT = `
15927
- #ifdef USE_MAP
15928
- vec4 sampledDiffuseColor = texture2D( map, vMapUv );
15929
- sampledDiffuseColor.rgb = tileColour( sampledDiffuseColor.rgb );
15930
- diffuseColor *= sampledDiffuseColor;
15987
+
15931
15988
  #endif
15932
15989
  `;
15933
- function createOverlay(material) {
15934
- const overlay = {
15935
- material,
15990
+ function createChain(kind) {
15991
+ return {
15992
+ kind,
15936
15993
  uniforms: {
15937
- uTileMap: { value: null },
15938
- uTileMask: { value: null },
15939
- uTileGrid: { value: Array.from({ length: MAX_DRAWN }, () => new Vector4()) },
15940
- uTileWindow: { value: Array.from({ length: MAX_DRAWN }, () => new Vector4()) },
15941
- uTileFilter: { value: new Vector2(1, 1) }
15994
+ map: { value: null },
15995
+ mask: { value: null },
15996
+ grid: { value: Array.from({ length: 7 }, () => new Vector4()) },
15997
+ window: { value: Array.from({ length: 7 }, () => new Vector4()) },
15998
+ home: { value: Array.from({ length: 7 }, () => new Vector2()) },
15999
+ anchor: { value: new Vector3(1, 0, 0) },
16000
+ loaded: { value: new Int32Array(7) },
16001
+ filter: { value: new Vector2(1, 1) }
15942
16002
  },
15943
16003
  inflight: 0,
15944
16004
  failures: 0,
@@ -15946,20 +16006,14 @@ function createOverlay(material) {
15946
16006
  dead: false,
15947
16007
  lastUsed: 0
15948
16008
  };
15949
- chainShaderHook(material, "surfaceTiles", (shader) => {
15950
- Object.assign(shader.uniforms, overlay.uniforms);
15951
- shader.vertexShader = shader.vertexShader.replace("#include <common>", "#include <common>\nvarying vec3 vTileDir;").replace("#include <begin_vertex>", "#include <begin_vertex>\nvTileDir = position;");
15952
- shader.fragmentShader = shader.fragmentShader.replace("#include <map_pars_fragment>", `#include <map_pars_fragment>\n${PARS}`).replace("#include <map_fragment>", MAP_FRAGMENT);
15953
- });
15954
- return overlay;
15955
16009
  }
15956
- function createStore(tileSize, layers, renderer) {
16010
+ function createStore(kind, tileSize, layers, renderer) {
15957
16011
  const size = tileSize * 4;
15958
16012
  const texture = new DataArrayTexture(null, size, size, layers);
15959
16013
  texture.source.dataReady = false;
15960
- texture.format = RGBAFormat;
16014
+ texture.format = kind.format;
15961
16015
  texture.type = UnsignedByteType;
15962
- texture.colorSpace = SRGBColorSpace;
16016
+ texture.colorSpace = kind.colorSpace;
15963
16017
  texture.wrapS = texture.wrapT = RepeatWrapping;
15964
16018
  texture.magFilter = LinearFilter;
15965
16019
  texture.minFilter = LinearMipmapLinearFilter;
@@ -15969,7 +16023,8 @@ function createStore(tileSize, layers, renderer) {
15969
16023
  const mask = new DataArrayTexture(new Uint8Array(SLOTS * layers), 4, 4, layers);
15970
16024
  mask.format = RedFormat;
15971
16025
  mask.type = UnsignedByteType;
15972
- mask.minFilter = mask.magFilter = NearestFilter;
16026
+ mask.minFilter = mask.magFilter = LinearFilter;
16027
+ mask.wrapS = mask.wrapT = RepeatWrapping;
15973
16028
  mask.unpackAlignment = 1;
15974
16029
  mask.needsUpdate = true;
15975
16030
  return {
@@ -16010,26 +16065,27 @@ function release(store, win) {
16010
16065
  for (let slot = 0; slot < SLOTS; slot++) setLoaded(store, win.layer, slot, false);
16011
16066
  store.free.push(win.layer);
16012
16067
  }
16013
- function settled(overlay, store, win) {
16068
+ function patchOf(tiles, win, tile) {
16069
+ if (win.chart !== 0 || win.level <= tiles.max_level) return void 0;
16070
+ return patchOfTile(tiles.patches, win.level, tile.x, tile.y);
16071
+ }
16072
+ function exists(tiles, win, slot) {
16073
+ if (win.level <= tiles.max_level) return true;
16074
+ if (!win.placement) return false;
16075
+ return patchOf(tiles, win, slotTile(win.placement, slot % 4, Math.floor(slot / 4))) !== void 0;
16076
+ }
16077
+ function settled(chain, store, tiles, win) {
16014
16078
  for (let slot = 0; slot < SLOTS; slot++) {
16015
16079
  const key = win.wanted[slot];
16016
16080
  if (!key) return false;
16017
- if (!isLoaded(store, win.layer, slot) && !overlay.failed.has(key)) return false;
16081
+ if (isLoaded(store, win.layer, slot) || chain.failed.has(key)) continue;
16082
+ if (exists(tiles, win, slot)) return false;
16018
16083
  }
16019
16084
  return true;
16020
16085
  }
16021
- async function fetchTile(url, signal) {
16022
- const response = await fetch(url, { signal });
16023
- if (!response.ok) throw new Error(`tile ${response.status}`);
16024
- return createImageBitmap(await response.blob(), {
16025
- imageOrientation: "flipY",
16026
- premultiplyAlpha: "none",
16027
- colorSpaceConversion: "none"
16028
- });
16029
- }
16030
16086
  var copyAt = new Vector3();
16031
16087
  var queue$1 = [];
16032
- function pump(overlay, store, tiles, win, x, y, renderer, invalidate) {
16088
+ function pump(chain, store, tiles, win, x, y, renderer, invalidate) {
16033
16089
  const placement = win.placement;
16034
16090
  if (!placement) return;
16035
16091
  const columns = chartColumns(win.chart, win.level);
@@ -16038,8 +16094,8 @@ function pump(overlay, store, tiles, win, x, y, renderer, invalidate) {
16038
16094
  queue$1.length = 0;
16039
16095
  for (let slot = 0; slot < SLOTS; slot++) {
16040
16096
  const key = win.wanted[slot];
16041
- if (!key || win.pending.has(slot) || overlay.failed.has(key)) continue;
16042
- if (isLoaded(store, win.layer, slot)) continue;
16097
+ if (!key || win.pending.has(slot) || chain.failed.has(key)) continue;
16098
+ if (isLoaded(store, win.layer, slot) || !exists(tiles, win, slot)) continue;
16043
16099
  const tile = slotTile(placement, slot % 4, Math.floor(slot / 4));
16044
16100
  let dx = tile.x + .5 - cx;
16045
16101
  if (win.chart === 0) dx -= columns * Math.round(dx / columns);
@@ -16050,7 +16106,7 @@ function pump(overlay, store, tiles, win, x, y, renderer, invalidate) {
16050
16106
  }
16051
16107
  queue$1.sort((a, b) => a.far - b.far);
16052
16108
  for (const { slot } of queue$1) {
16053
- if (overlay.inflight >= MAX_INFLIGHT) return;
16109
+ if (chain.inflight >= MAX_INFLIGHT) return;
16054
16110
  const key = win.wanted[slot];
16055
16111
  const sx = slot % 4;
16056
16112
  const sy = Math.floor(slot / 4);
@@ -16065,129 +16121,785 @@ function pump(overlay, store, tiles, win, x, y, renderer, invalidate) {
16065
16121
  key,
16066
16122
  abort
16067
16123
  });
16068
- overlay.inflight++;
16124
+ chain.inflight++;
16069
16125
  const folder = CHART_FOLDER[win.chart];
16070
- fetchTile(tileUrl(tiles, folder, win.level, tile.x, tile.y, win.frame), abort.signal).then((bitmap) => {
16126
+ const version = patchOf(tiles, win, tile)?.version ?? tiles.version;
16127
+ const pyramid = {
16128
+ id: tiles.id,
16129
+ version
16130
+ };
16131
+ chain.kind.load(tileUrl(pyramid, folder, win.level, tile.x, tile.y, win.frame), abort.signal).then((loaded) => {
16071
16132
  if (win.wanted[slot] === key && !abort.signal.aborted) {
16072
- const source = new Texture(bitmap);
16073
- source.colorSpace = SRGBColorSpace;
16074
- source.flipY = false;
16075
- source.generateMipmaps = false;
16076
16133
  copyAt.set(sx * store.tileSize, (4 - 1 - sy) * store.tileSize, win.layer);
16077
- renderer.copyTextureToTexture(source, store.texture, null, copyAt);
16078
- source.dispose();
16134
+ renderer.copyTextureToTexture(loaded.texture, store.texture, null, copyAt);
16079
16135
  setLoaded(store, win.layer, slot, true);
16080
- overlay.failures = 0;
16136
+ chain.failures = 0;
16081
16137
  invalidate();
16082
16138
  }
16083
- bitmap.close();
16139
+ loaded.release();
16084
16140
  }).catch(() => {
16085
16141
  if (abort.signal.aborted && !timedOut) return;
16086
- overlay.failed.add(key);
16087
- if (++overlay.failures >= MAX_FAILURES) overlay.dead = true;
16142
+ chain.failed.add(key);
16143
+ if (++chain.failures >= MAX_FAILURES$1) chain.dead = true;
16088
16144
  }).finally(() => {
16089
16145
  clearTimeout(timer);
16090
- overlay.inflight--;
16146
+ chain.inflight--;
16091
16147
  if (win.pending.get(slot)?.key === key) win.pending.delete(slot);
16092
16148
  });
16093
16149
  }
16094
16150
  }
16095
- function switchOff(overlay) {
16096
- for (const link of overlay.uniforms.uTileWindow.value) link.w = 0;
16151
+ function switchOff(chain) {
16152
+ for (const link of chain.uniforms.window.value) link.w = 0;
16153
+ }
16154
+ function disposeChain(chain) {
16155
+ switchOff(chain);
16156
+ const store = chain.store;
16157
+ if (!store) return;
16158
+ for (const win of store.windows) for (const { abort } of win.pending.values()) abort.abort();
16159
+ store.texture.dispose();
16160
+ store.mask.dispose();
16161
+ chain.store = void 0;
16162
+ chain.uniforms.map.value = null;
16163
+ chain.uniforms.mask.value = null;
16164
+ }
16165
+ function restChain(chain, now) {
16166
+ switchOff(chain);
16167
+ if (now - chain.lastUsed > IDLE_DISPOSE_S * 1e3) disposeChain(chain);
16168
+ }
16169
+ var inverse = new Matrix4();
16170
+ var over = new Vector3();
16171
+ var anchor = new Vector3();
16172
+ function pointUnderCamera(mesh, camera) {
16173
+ return over.copy(camera.position).applyMatrix4(inverse.copy(mesh.matrixWorld).invert()).normalize();
16174
+ }
16175
+ function finestLevelAt$1(chain, tiles, over) {
16176
+ if (cameraChart(over.y, chain?.chart) !== 0) return tiles.max_level;
16177
+ const point = chartPoint(0, over.x, over.y, over.z);
16178
+ return deepestLevel(tiles.max_level, tiles.patches, point.x, point.y);
16179
+ }
16180
+ function chainLength(tiles, limit) {
16181
+ let finest = tiles.max_level;
16182
+ if (tiles.patches) for (const patch of tiles.patches) finest = Math.max(finest, patch.max_level);
16183
+ const levels = finest - 3 + 1;
16184
+ return Math.min(limit, 6, levels + 1);
16185
+ }
16186
+ function updateChain(chain, tiles, count, level, over, radius, frame, renderer, invalidate) {
16187
+ chain.lastUsed = performance.now();
16188
+ if (chain.store && chain.store.texture.image.depth !== count + 1) disposeChain(chain);
16189
+ const store = chain.store ??= createStore(chain.kind, tiles.tile_size, count + 1, renderer);
16190
+ chain.uniforms.map.value = store.texture;
16191
+ chain.uniforms.mask.value = store.mask;
16192
+ chain.uniforms.filter.value.set(store.texture.anisotropy, tiles.tile_size);
16193
+ const cap = over.y >= 0 ? 1 : 2;
16194
+ chain.chart = cameraChart(over.y, chain.chart);
16195
+ const links = windowChain(level, count, chain.chart, over.y, tiles.max_level);
16196
+ anchor.set(Math.fround(over.x * radius), Math.fround(over.y * radius), Math.fround(over.z * radius));
16197
+ chain.uniforms.anchor.value.copy(anchor);
16198
+ const range = anchor.length();
16199
+ const home = chartPoint(0, anchor.x / range, anchor.y / range, anchor.z / range);
16200
+ const held = (link) => store.windows.find((win) => win.chart === link.chart && win.level === link.level);
16201
+ store.windows = store.windows.filter((win) => {
16202
+ win.rank = links.findIndex((link) => link.chart === win.chart && link.level === win.level);
16203
+ if (win.rank >= 0) return true;
16204
+ win.rank = links.length;
16205
+ const heir = links.find((link) => link.level === win.level);
16206
+ const heirWindow = heir && held(heir);
16207
+ const near = win.chart === 0 || win.chart === cap;
16208
+ if (heir && near && !(heirWindow && settled(chain, store, tiles, heirWindow))) return true;
16209
+ release(store, win);
16210
+ return false;
16211
+ });
16212
+ for (let i = links.length - 1; i >= 0; i--) {
16213
+ const link = links[i];
16214
+ let win = held(link);
16215
+ if (!win) {
16216
+ const layer = store.free.pop();
16217
+ if (layer === void 0) continue;
16218
+ win = {
16219
+ layer,
16220
+ ...link,
16221
+ rank: i,
16222
+ wanted: new Array(SLOTS),
16223
+ pending: /* @__PURE__ */ new Map()
16224
+ };
16225
+ store.windows.push(win);
16226
+ }
16227
+ const point = chartPoint(link.chart, over.x, over.y, over.z);
16228
+ retarget(store, win, placeWindow(link.chart, link.level, point.x, point.y, win.placement), frame);
16229
+ pump(chain, store, tiles, win, point.x, point.y, renderer, invalidate);
16230
+ }
16231
+ store.windows.sort((a, b) => b.level - a.level || a.rank - b.rank);
16232
+ for (let i = 0; i < 7; i++) {
16233
+ const win = store.windows[i];
16234
+ const link = chain.uniforms.window.value[i];
16235
+ if (!win?.placement) {
16236
+ link.w = 0;
16237
+ continue;
16238
+ }
16239
+ chain.uniforms.grid.value[i].set(chartColumns(win.chart, win.level), chartRows(win.chart, win.level), win.placement.tx0, win.placement.ty0);
16240
+ if (win.chart === 0) {
16241
+ const from = windowHome(win.placement, home.x, home.y);
16242
+ chain.uniforms.home.value[i].set(from.x, from.y);
16243
+ }
16244
+ link.set(win.chart, win.layer, win.level, 1);
16245
+ let loaded = 0;
16246
+ for (let slot = 0; slot < SLOTS; slot++) if (isLoaded(store, win.layer, slot)) loaded |= 1 << slot;
16247
+ chain.uniforms.loaded.value[i] = loaded;
16248
+ }
16249
+ }
16250
+
16251
+ //#endregion
16252
+ //#region src/lib/scene/objects/surface/relief-tiles.ts
16253
+ var MARCH_REACH = 8;
16254
+ var STEP_PIXELS = 1.5;
16255
+ var REFINE_STEPS = 3;
16256
+ var STEEPEST = 1;
16257
+ var MIN_SUN_SIZE = .006;
16258
+ var SLOPE_GLSL = `
16259
+ uniform sampler2DArray uReliefMap;
16260
+ uniform sampler2DArray uReliefMask;
16261
+ uniform vec4 uReliefGrid[ ${7} ];
16262
+ uniform vec4 uReliefWindow[ ${7} ];
16263
+ uniform int uReliefLoaded[ ${7} ];
16264
+ uniform vec2 uReliefFilter;
16265
+ uniform float uReliefScale;
16266
+ ${TILE_CHAIN_GLSL}
16267
+ // Each byte is filtered alone, and the sum of the two is the filtered height.
16268
+ float reliefValue( vec4 texel ) {
16269
+ return dot( texel.rg, vec2( 65280.0, 255.0 ) ) / 65535.0;
16270
+ }
16271
+
16272
+ // How the coordinates of each chart change along the sphere at a direction:
16273
+ // the dot product with a step along the sphere is that step on the chart.
16274
+ struct ReliefRates {
16275
+ vec3 mapX;
16276
+ vec3 mapY;
16277
+ vec3 capX;
16278
+ vec3 capY;
16279
+ };
16280
+
16281
+ ReliefRates reliefRates( vec3 dir, float side ) {
16282
+ ReliefRates rates;
16283
+ float flat2 = max( dir.x * dir.x + dir.z * dir.z, 1e-12 );
16284
+ rates.mapX = vec3( dir.z, 0.0, - dir.x ) / ( 2.0 * PI * flat2 );
16285
+ rates.mapY = ( dir * dir.y - vec3( 0.0, 1.0, 0.0 ) ) / ( PI * sqrt( flat2 ) );
16286
+ float fold = 1.0 + side * dir.y;
16287
+ float capScale = 0.5 / ( ${CAP_EDGE.toFixed(8)} * fold );
16288
+ vec2 plane = vec2( dir.x, side * dir.z ) * side / fold;
16289
+ rates.capX = vec3( 1.0, - plane.x, 0.0 ) * capScale;
16290
+ rates.capY = vec3( 0.0, - plane.y, side ) * capScale;
16291
+ rates.capX -= dir * dot( rates.capX, dir );
16292
+ rates.capY -= dir * dot( rates.capY, dir );
16293
+ return rates;
16294
+ }
16295
+
16296
+ // Slope of the ground at a direction of the body's frame: a vector along the
16297
+ // unit sphere that points uphill, as long as the rise over the run. Also
16298
+ // gives the part of the fragment that the tiles answer for, and the finest
16299
+ // window that does, or -1.
16300
+ vec3 reliefTileSlope( vec3 dir, TileCharts charts, out float covered, out int first ) {
16301
+ covered = 0.0;
16302
+ first = - 1;
16303
+ vec3 slope = vec3( 0.0 );
16304
+ if ( uReliefWindow[ 0 ].w < 0.5 ) return slope;
16305
+ ReliefRates rates = reliefRates( dir, charts.side );
16306
+
16307
+ float rows = ${4 .toFixed(1)};
16308
+ for ( int i = 0; i < ${7}; i ++ ) {
16309
+ vec4 window = uReliefWindow[ i ];
16310
+ if ( window.w < 0.5 ) break;
16311
+ vec4 grid = uReliefGrid[ i ];
16312
+ vec2 spot;
16313
+ vec2 major;
16314
+ vec2 minor;
16315
+ float alpha = tileWindowWeight( charts, window, grid, - grid.zw, uReliefFilter, spot, major, minor );
16316
+ if ( alpha <= 0.0 ) continue;
16317
+ // Filtered, the mask falls towards a missing tile. The pixels beside
16318
+ // one are left out: their slope would read its blank slot.
16319
+ alpha *= smoothstep( 0.51, 0.55, texture( uReliefMask, vec3( spot / rows, window.y ) ).r );
16320
+ if ( alpha <= 0.0 ) continue;
16321
+ if ( first < 0 ) first = i;
16322
+
16323
+ vec3 st = vec3( spot.x / rows, - spot.y / rows, window.y );
16324
+ vec2 texel = vec2( 1.0, - 1.0 ) / ( rows * uReliefFilter.y );
16325
+ vec2 gx = major * texel;
16326
+ vec2 gy = minor * texel;
16327
+ vec3 east = vec3( texel.x, 0.0, 0.0 );
16328
+ vec3 south = vec3( 0.0, texel.y, 0.0 );
16329
+ // Rise over two pixels of the window, then over the whole chart.
16330
+ vec2 rise = vec2(
16331
+ reliefValue( textureGrad( uReliefMap, st + east, gx, gy ) ) - reliefValue( textureGrad( uReliefMap, st - east, gx, gy ) ),
16332
+ reliefValue( textureGrad( uReliefMap, st + south, gx, gy ) ) - reliefValue( textureGrad( uReliefMap, st - south, gx, gy ) )
16333
+ ) * 0.5 * grid.xy * uReliefFilter.y;
16334
+ bool onMap = window.x < 0.5;
16335
+ vec3 uphill = rise.x * ( onMap ? rates.mapX : rates.capX ) + rise.y * ( onMap ? rates.mapY : rates.capY );
16336
+ slope += ( 1.0 - covered ) * alpha * uphill;
16337
+ covered += ( 1.0 - covered ) * alpha;
16338
+ if ( covered > 0.995 ) break;
16339
+ }
16340
+ return slope * uReliefScale;
16341
+ }
16342
+ `;
16343
+ function shadowGlsl(steps) {
16344
+ return `
16345
+ // One window as a shadow ray reads it. The ray is a line on the window's
16346
+ // chart, so where it is inside the window is worked out once, not at each
16347
+ // sample.
16348
+ struct ReliefSight {
16349
+ // The fragment in the window's texture, with its layer, and how far the
16350
+ // ray goes there in a radian.
16351
+ vec3 from;
16352
+ vec2 rate;
16353
+ // The part of the ray inside the window, in radians along it.
16354
+ float enters;
16355
+ float leaves;
16356
+ // The longest step that the window's pixels still hold.
16357
+ float widest;
16358
+ int loaded;
16359
+ };
16360
+
16361
+ ReliefSight reliefSight( int i, TileCharts charts, vec2 mapStep, vec2 capStep ) {
16362
+ vec4 window = uReliefWindow[ i ];
16363
+ vec4 grid = uReliefGrid[ i ];
16364
+ bool onMap = window.x < 0.5;
16365
+ float rows = ${4 .toFixed(1)};
16366
+ // In tiles from the window's north-west corner. The map goes round: the
16367
+ // fragment is taken on the side nearest the window.
16368
+ vec2 rel = ( onMap ? charts.mapAt : charts.capAt ) * grid.xy - grid.zw;
16369
+ if ( onMap ) rel.x = mod( rel.x - 0.5 * rows + 0.5 * grid.x, grid.x ) + 0.5 * rows - 0.5 * grid.x;
16370
+ vec2 rate = ( onMap ? mapStep : capStep ) * grid.xy;
16371
+
16372
+ // Half a pixel in from the window's sides: past it the filter reads the
16373
+ // tile of the far side.
16374
+ float inset = 0.5 / uReliefFilter.y;
16375
+ vec2 run = mix( vec2( 1e-20 ), rate, greaterThan( abs( rate ), vec2( 1e-20 ) ) );
16376
+ vec2 a = ( inset - rel ) / run;
16377
+ vec2 b = ( rows - inset - rel ) / run;
16378
+ vec2 near = min( a, b );
16379
+ vec2 far = max( a, b );
16380
+
16381
+ ReliefSight sight;
16382
+ sight.from = vec3( ( rel.x + grid.z ) / rows, - ( rel.y + grid.w ) / rows, window.y );
16383
+ sight.rate = vec2( rate.x, - rate.y ) / rows;
16384
+ sight.enters = max( near.x, near.y );
16385
+ sight.leaves = min( far.x, far.y );
16386
+ // The other pole's cap holds none of the ray.
16387
+ if ( ! onMap && ( window.x < 1.5 ) != ( charts.side > 0.0 ) ) sight.leaves = - 1.0;
16388
+ sight.widest = ${STEP_PIXELS.toFixed(2)} * PI / ( uReliefFilter.y * exp2( window.z ) );
16389
+ sight.loaded = uReliefLoaded[ i ];
16390
+ return sight;
16391
+ }
16392
+
16393
+ // Height of the ground 't' radians along the ray, as a part of the pyramid's
16394
+ // range. Negative where the window does not hold that ground.
16395
+ float reliefSightValue( ReliefSight sight, float t ) {
16396
+ if ( t < sight.enters || t > sight.leaves ) return - 1.0;
16397
+ vec2 st = sight.from.xy + t * sight.rate;
16398
+ // Both tiles that the filter can read must be in.
16399
+ if ( sight.loaded != ${(1 << 4 * 4) - 1} ) {
16400
+ vec2 tile = vec2( st.x, - st.y ) * ${4 .toFixed(1)};
16401
+ float inset = 0.5 / uReliefFilter.y;
16402
+ ivec2 a = ivec2( floor( tile - inset ) ) & ${4 - 1};
16403
+ ivec2 b = ivec2( floor( tile + inset ) ) & ${4 - 1};
16404
+ int both = ( sight.loaded >> ( a.x + ${4} * a.y ) ) & ( sight.loaded >> ( b.x + ${4} * b.y ) );
16405
+ if ( ( both & 1 ) == 0 ) return - 1.0;
16406
+ }
16407
+ return reliefValue( textureLod( uReliefMap, vec3( st, sight.from.z ), 0.0 ) );
16408
+ }
16409
+
16410
+ // Part of the Sun's disc that the ground leaves in sight of the fragment at
16411
+ // 'dir', or -1 where no window holds the fragment. The Sun is 'toSun' along
16412
+ // the sphere, 'tanElev' above it, and 'sunSize' is the radius of its disc.
16413
+ // 'first' is the fragment's window, as the slope gave it.
16414
+ //
16415
+ // A ray is marched towards the Sun. Each step is longer than the last by one
16416
+ // ratio, from a pixel of the fragment's window to the end of the ray, and is
16417
+ // read from the window whose pixels are about as wide as the step. A shadow
16418
+ // is then sharp beside what casts it, at a cost that does not grow with its
16419
+ // length.
16420
+ float reliefTileShadow( vec3 dir, TileCharts charts, vec3 toSun, float tanElev, float sunSize, int first ) {
16421
+ ReliefRates rates = reliefRates( dir, charts.side );
16422
+ vec2 mapStep = vec2( dot( rates.mapX, toSun ), dot( rates.mapY, toSun ) );
16423
+ vec2 capStep = vec2( dot( rates.capX, toSun ), dot( rates.capY, toSun ) );
16424
+
16425
+ int drawn = 0;
16426
+ for ( int i = 0; i < ${7}; i ++ ) {
16427
+ if ( uReliefWindow[ i ].w > 0.5 ) drawn = i + 1;
16428
+ }
16429
+ int w = first;
16430
+ ReliefSight sight;
16431
+ float own = - 1.0;
16432
+ for ( int i = 0; i < ${7}; i ++ ) {
16433
+ if ( i < first ) continue;
16434
+ if ( i >= drawn ) break;
16435
+ w = i;
16436
+ sight = reliefSight( i, charts, mapStep, capStep );
16437
+ own = reliefSightValue( sight, 0.0 );
16438
+ if ( own >= 0.0 ) break;
16439
+ }
16440
+ if ( own < 0.0 ) return - 1.0;
16441
+ int start = w;
16442
+
16443
+ // A ray rises above the sphere by its slope, and by the sphere's own
16444
+ // curve under it. Past where it clears the pyramid's range, nothing
16445
+ // can stand in it.
16446
+ float clear = sqrt( tanElev * tanElev + 2.0 * uReliefScale * ( 1.0 - own ) ) - tanElev;
16447
+ float reach = min( clear, ${MARCH_REACH.toFixed(1)} * uReliefScale );
16448
+ if ( reach < 1e-7 ) return 1.0;
16449
+
16450
+ // The ratio that takes steps from one pixel to the reach: 'rate' is its
16451
+ // logarithm times the count, and solves rate / ( exp( rate ) - 1 ) = part.
16452
+ float count = ${steps.toFixed(1)};
16453
+ float pixel = sight.widest / ${STEP_PIXELS.toFixed(2)};
16454
+ float part = count * pixel / reach;
16455
+ float rate = 0.0;
16456
+ if ( part < 1.0 ) {
16457
+ rate = - log( part );
16458
+ for ( int k = 0; k < 4; k ++ ) rate = log( 1.0 + rate / part );
16459
+ }
16460
+ // Each sample is the one before times the ratio, plus the first step.
16461
+ bool even = rate < 1e-3;
16462
+ float ratio = even ? 1.0 : exp( rate / count );
16463
+ float head = even ? reach / count : reach * ( ratio - 1.0 ) / ( exp( rate ) - 1.0 );
16464
+
16465
+ // How far the ground stands into the ray is counted in discs of the Sun.
16466
+ // The disc is an angle, and the ray's rise is a slope.
16467
+ float perDisc = 1.0 / ( max( sunSize, ${MIN_SUN_SIZE.toFixed(4)} ) * ( 1.0 + tanElev * tanElev ) );
16468
+ float t = 0.0;
16469
+ float hidden = - 1e9;
16470
+ // The sample that hides the most, the step around it and its window.
16471
+ float worst = 0.0;
16472
+ float around = 0.0;
16473
+ int worstWindow = w;
16474
+ for ( int i = 1; i <= ${steps}; i ++ ) {
16475
+ float before = t;
16476
+ t = t * ratio + head;
16477
+ if ( w + 1 < drawn && t - before > sight.widest ) {
16478
+ w ++;
16479
+ sight = reliefSight( w, charts, mapStep, capStep );
16480
+ }
16481
+ float value = reliefSightValue( sight, t );
16482
+ // The ray left this window, or a tile of it is missing.
16483
+ if ( value < 0.0 && w + 1 < drawn ) {
16484
+ w ++;
16485
+ sight = reliefSight( w, charts, mapStep, capStep );
16486
+ value = reliefSightValue( sight, t );
16487
+ }
16488
+ if ( value < 0.0 ) continue;
16489
+ float stands = ( ( value - own ) * uReliefScale / t - tanElev - 0.5 * t ) * perDisc;
16490
+ if ( stands > hidden ) {
16491
+ hidden = stands;
16492
+ worst = t;
16493
+ around = t - before;
16494
+ worstWindow = w;
16495
+ // The whole disc is hidden.
16496
+ if ( hidden >= 1.0 ) break;
16497
+ }
16498
+ }
16499
+
16500
+ // The crest that hides the most lies between two samples, and far out
16501
+ // those are far apart. Look for it around the best one, each time at half
16502
+ // the distance and in a finer window. Not where the best one is so clear
16503
+ // of the ray that no crest beside it can reach the Sun's disc.
16504
+ float crest = ${(STEEPEST * .5).toFixed(2)} * around / max( worst, 1e-9 ) * perDisc;
16505
+ if ( hidden < 1.0 && hidden + crest > - 1.0 ) {
16506
+ w = worstWindow;
16507
+ sight = reliefSight( w, charts, mapStep, capStep );
16508
+ for ( int k = 0; k < ${REFINE_STEPS}; k ++ ) {
16509
+ around *= 0.5;
16510
+ if ( w > start && around < sight.widest ) {
16511
+ w --;
16512
+ sight = reliefSight( w, charts, mapStep, capStep );
16513
+ }
16514
+ float centre = worst;
16515
+ for ( int e = 0; e < 2; e ++ ) {
16516
+ float at = centre + ( e == 0 ? - around : around );
16517
+ float value = reliefSightValue( sight, at );
16518
+ if ( value < 0.0 ) continue;
16519
+ float stands = ( ( value - own ) * uReliefScale / at - tanElev - 0.5 * at ) * perDisc;
16520
+ if ( stands > hidden ) {
16521
+ hidden = stands;
16522
+ worst = at;
16523
+ }
16524
+ }
16525
+ }
16526
+ }
16527
+ return 1.0 - smoothstep( - 1.0, 1.0, hidden );
16528
+ }
16529
+ `;
16530
+ }
16531
+ function reliefTilesGlsl(steps) {
16532
+ return steps > 0 ? SLOPE_GLSL + shadowGlsl(steps) : SLOPE_GLSL;
16533
+ }
16534
+ async function loadTile(url, signal) {
16535
+ const { bytes, size } = await loadHeightTile(url, signal);
16536
+ const texture = new DataTexture(bytes, size, size, RGFormat, UnsignedByteType);
16537
+ return {
16538
+ texture,
16539
+ release: () => texture.dispose()
16540
+ };
16541
+ }
16542
+ var HEIGHT_TILES = {
16543
+ format: RGFormat,
16544
+ colorSpace: NoColorSpace,
16545
+ load: loadTile
16546
+ };
16547
+ function reliefTilesAvailable(tiles) {
16548
+ return !!tiles && tiles.scale_km !== void 0 && tiles.max_level >= 3 && tilesBase() !== "" && maxReliefWindows() > 0 && textureAllowed(tiles.distribution);
16549
+ }
16550
+ function reliefTilesOf(material, tiles, radiusKm) {
16551
+ if (!reliefTilesAvailable(tiles)) return void 0;
16552
+ const held = material.userData.reliefTiles;
16553
+ if (held) return held;
16554
+ const chain = createChain(HEIGHT_TILES);
16555
+ const relief = {
16556
+ tiles,
16557
+ chain,
16558
+ shadowSteps: reliefShadowSteps(),
16559
+ uniforms: {
16560
+ uReliefMap: chain.uniforms.map,
16561
+ uReliefMask: chain.uniforms.mask,
16562
+ uReliefGrid: chain.uniforms.grid,
16563
+ uReliefWindow: chain.uniforms.window,
16564
+ uReliefLoaded: chain.uniforms.loaded,
16565
+ uReliefFilter: chain.uniforms.filter,
16566
+ uReliefScale: { value: (tiles.scale_km ?? 0) / radiusKm }
16567
+ }
16568
+ };
16569
+ material.userData.reliefTiles = relief;
16570
+ return relief;
16571
+ }
16572
+ function reliefTilesServe(bo) {
16573
+ return !!bo.reliefTiles && !bo.reliefTiles.chain.dead;
16574
+ }
16575
+ function disposeReliefTiles(bo) {
16576
+ if (bo.reliefTiles) disposeChain(bo.reliefTiles.chain);
16577
+ }
16578
+ function updateReliefTiles(bo, camera, renderer, projScale, invalidate) {
16579
+ const relief = bo.reliefTiles;
16580
+ if (!relief) return;
16581
+ const mesh = bo.mesh;
16582
+ if (mesh && relief.shadowSteps !== reliefShadowSteps()) {
16583
+ relief.shadowSteps = reliefShadowSteps();
16584
+ mesh.material.needsUpdate = true;
16585
+ }
16586
+ const level = mesh && bo.selfShadow && !relief.chain.dead ? neededLevel(bo.radiusScene, bo.cachedDist, projScale, relief.tiles.max_level) : -1;
16587
+ if (!mesh || level < 3) {
16588
+ restChain(relief.chain, performance.now());
16589
+ return;
16590
+ }
16591
+ const levels = relief.tiles.max_level - 3 + 1;
16592
+ updateChain(relief.chain, relief.tiles, Math.min(maxReliefWindows(), levels), level, pointUnderCamera(mesh, camera), bo.radiusScene, void 0, renderer, invalidate);
16593
+ }
16594
+
16595
+ //#endregion
16596
+ //#region src/lib/scene/objects/surface/self-shadow.ts
16597
+ var STEPS = 32;
16598
+ var NORMAL_STRENGTH = 1;
16599
+ var GRADIENT_STENCIL = 2;
16600
+ var SLOPE_CLAMP = 1.5;
16601
+ var SHADOW_REACH = 4;
16602
+ var SHADOW_SOFT = .04;
16603
+ var TERMINATOR_GATE_LIT = .02;
16604
+ var TERMINATOR_GATE_DARK = -.08;
16605
+ var MARCH_GRAZE_FADE = .18;
16606
+ var TILE_LIGHT = `
16607
+ if (byTiles > 0.005 && dot(normal, mat3(viewMatrix) * L) > 0.0) {
16608
+ vec3 sunward = normalize(L * mat3(modelMatrix));
16609
+ float up = dot(sunward, selfDir);
16610
+ vec3 along = sunward - selfDir * up;
16611
+ float run = length(along);
16612
+ if (run > 1e-4) tileLight = reliefTileShadow(selfDir, selfCharts, along / run, up / run, uSelfSunSize, selfFirst);
16613
+ if (tileLight < 0.0) byTiles = 0.0;
16614
+ }`;
16615
+ function attachSelfShadowToBody(material, heightMap, scaleScene, sunDir, tiles) {
16616
+ const tex = heightMap.image;
16617
+ const texel = new Vector2(1 / (tex?.width ?? 4096), 1 / (tex?.height ?? 2048));
16618
+ const existing = material.userData.selfShadow;
16619
+ if (existing) {
16620
+ existing.uSelfHeightMap.value = heightMap;
16621
+ existing.uSelfScale.value = scaleScene;
16622
+ existing.uSelfTexel.value.copy(texel);
16623
+ return existing;
16624
+ }
16625
+ const uniforms = {
16626
+ uSelfHeightMap: { value: heightMap },
16627
+ uSelfScale: { value: scaleScene },
16628
+ uSelfTexel: { value: texel }
16629
+ };
16630
+ material.userData.selfShadow = uniforms;
16631
+ const sun = sunDir ?? getEclipseSceneUniforms().uSunDir;
16632
+ chainShaderHook(material, tiles ? () => `selfShadowTiles${reliefShadowSteps()}` : "selfShadow", (shader) => {
16633
+ const steps = tiles ? reliefShadowSteps() : 0;
16634
+ Object.assign(shader.uniforms, uniforms, { uSelfSunDir: sun }, tiles?.uniforms);
16635
+ if (tiles) shader.uniforms.uSelfSunSize = getEclipseSceneUniforms().uSunAngularRadius;
16636
+ shader.vertexShader = shader.vertexShader.replace("#include <common>", `#include <common>
16637
+ varying vec3 vSelfWorldNormal;
16638
+ varying vec2 vSelfUv;
16639
+ varying vec3 vSelfDir;
16640
+ varying vec3 vSelfEastW;
16641
+ varying vec3 vSelfNorthW;
16642
+ varying float vSelfRadiusW;
16643
+ varying float vSelfCosLat;`).replace("#include <beginnormal_vertex>", `#include <beginnormal_vertex>
16644
+ vSelfWorldNormal = normalize(mat3(modelMatrix) * objectNormal);
16645
+ vSelfUv = uv;
16646
+ vSelfDir = position;
16647
+ // Analytic equirect tangent basis: smooth per fragment, so the
16648
+ // relief normal doesn't inherit mesh facets.
16649
+ vec3 nObj = normalize(position);
16650
+ vec3 ec = cross(vec3(0.0, 1.0, 0.0), nObj);
16651
+ float cl = length(ec);
16652
+ vec3 eastObj = cl > 1e-5 ? ec / cl : vec3(1.0, 0.0, 0.0);
16653
+ vSelfEastW = normalize(mat3(modelMatrix) * eastObj);
16654
+ vSelfNorthW = normalize(mat3(modelMatrix) * cross(nObj, eastObj));
16655
+ vSelfRadiusW = length(mat3(modelMatrix) * position);
16656
+ vSelfCosLat = cl;`);
16657
+ shader.fragmentShader = shader.fragmentShader.replace("#include <common>", `#include <common>
16658
+ #define SELF_STEPS ${STEPS}
16659
+ uniform sampler2D uSelfHeightMap;
16660
+ uniform float uSelfScale;
16661
+ uniform vec2 uSelfTexel;
16662
+ uniform vec3 uSelfSunDir;
16663
+ uniform mat3 normalMatrix;
16664
+ ${tiles ? "uniform mat4 modelMatrix;\n uniform float uSelfSunSize;" : ""}
16665
+ varying vec3 vSelfWorldNormal;
16666
+ varying vec2 vSelfUv;
16667
+ varying vec3 vSelfDir;
16668
+ varying vec3 vSelfEastW;
16669
+ varying vec3 vSelfNorthW;
16670
+ varying float vSelfRadiusW;
16671
+ varying float vSelfCosLat;
16672
+ ${tiles ? reliefTilesGlsl(steps) : ""}
16673
+
16674
+ float selfHeight(vec2 uv) {
16675
+ return texture2D(uSelfHeightMap, uv).r * uSelfScale;
16676
+ }
16677
+
16678
+ // Slope of the whole-globe height map at a direction of the
16679
+ // body's frame: a vector along the unit sphere that points
16680
+ // uphill, as long as the rise over the run.
16681
+ vec3 selfMapSlope(vec3 dir) {
16682
+ float cosLat = sqrt(max(1.0 - dir.y * dir.y, 0.0));
16683
+ if (cosLat < 1e-5) return vec3(0.0);
16684
+ vec3 east = vec3(dir.z, 0.0, -dir.x) / cosLat;
16685
+ vec3 north = cross(dir, east);
16686
+ // Wider-than-one-texel stencil averages out 8-bit terracing, floored
16687
+ // at the low tier's texel so a higher-res map keeps the same
16688
+ // physical smoothing width instead of flattening the relief.
16689
+ vec2 d = max(uSelfTexel, vec2(1.0 / 2048.0, 1.0 / 1024.0)) * ${GRADIENT_STENCIL.toFixed(3)};
16690
+ float hu = selfHeight(vSelfUv + vec2(d.x, 0.0))
16691
+ - selfHeight(vSelfUv - vec2(d.x, 0.0));
16692
+ float hv = selfHeight(vSelfUv + vec2(0.0, d.y))
16693
+ - selfHeight(vSelfUv - vec2(0.0, d.y));
16694
+ // The map's u runs east over 2π·cos(lat) radii, its v north over π radii.
16695
+ float su = hu / (2.0 * d.x * 6.2831853 * cosLat * vSelfRadiusW);
16696
+ float sv = hv / (2.0 * d.y * 3.14159265 * vSelfRadiusW);
16697
+ // Fade at the poles, where the columns of the map close in and
16698
+ // blow the east-west slope up into a starburst.
16699
+ float gain = ${NORMAL_STRENGTH.toFixed(3)} * smoothstep(0.0, 0.12, cosLat);
16700
+ su = clamp(su * gain, -${SLOPE_CLAMP.toFixed(3)}, ${SLOPE_CLAMP.toFixed(3)});
16701
+ sv = clamp(sv * gain, -${SLOPE_CLAMP.toFixed(3)}, ${SLOPE_CLAMP.toFixed(3)});
16702
+ return east * su + north * sv;
16703
+ }
16704
+
16705
+ // Analytic equirect frame for the shadow march: Tu=∂pos/∂u runs east and
16706
+ // Tv=∂pos/∂v north, as the map's v does. |Tu|=2πR·cos(lat), |Tv|=πR.
16707
+ void selfTangents(out vec3 Tu, out vec3 Tv) {
16708
+ float R = vSelfRadiusW;
16709
+ Tu = vSelfEastW * (6.2831853 * R * vSelfCosLat);
16710
+ Tv = vSelfNorthW * (3.14159265 * R);
16711
+ }
16712
+
16713
+ // Part of the Sun's light that the ground of the whole-globe map
16714
+ // lets through, for a Sun towards L that is gN above the sphere.
16715
+ float selfMapLight(vec3 L, float gN) {
16716
+ vec3 Tu, Tv;
16717
+ selfTangents(Tu, Tv);
16718
+ float lenTu = length(Tu);
16719
+ float lenTv = length(Tv);
16720
+ if (lenTu <= 1e-12 || lenTv <= 1e-12) return 1.0;
16721
+ float lu = dot(L, Tu / lenTu);
16722
+ float lv = dot(L, Tv / lenTv);
16723
+ float lh = sqrt(lu * lu + lv * lv);
16724
+ if (lh <= 1e-4) return 1.0;
16725
+ float ds = ${SHADOW_REACH.toFixed(3)} * uSelfScale / float(SELF_STEPS);
16726
+ // World step → uv step via the per-axis metric (|Tu|, |Tv|).
16727
+ vec2 stepUv = ds * vec2((lu / lh) / lenTu, (lv / lh) / lenTv);
16728
+ float h0 = selfHeight(vSelfUv);
16729
+ float tanElev = gN / lh;
16730
+ float block = 0.0;
16731
+ for (int i = 1; i <= SELF_STEPS; i++) {
16732
+ vec2 uv = vSelfUv + float(i) * stepUv;
16733
+ float rayH = h0 + float(i) * ds * tanElev;
16734
+ block = max(block, selfHeight(uv) - rayH);
16735
+ }
16736
+ // Fade the march out at grazing angles: a near-horizontal ray
16737
+ // occludes every away-facing slope, banding the terminator.
16738
+ float marchFade = smoothstep(0.0, ${MARCH_GRAZE_FADE.toFixed(3)}, gN);
16739
+ return 1.0 - marchFade * clamp(block / (${SHADOW_SOFT.toFixed(3)} * uSelfScale), 0.0, 1.0);
16740
+ }`).replace("#include <normal_fragment_maps>", `#include <normal_fragment_maps>
16741
+ vec3 selfDir = normalize(vSelfDir);
16742
+ // The part of the fragment that the tiles answer for.
16743
+ float selfCovered = 0.0;
16744
+ ${tiles ? "TileCharts selfCharts = tileCharts(selfDir);\n int selfFirst = -1;" : ""}
16745
+ {
16746
+ vec3 slope = vec3(0.0);
16747
+ ${tiles ? "slope = reliefTileSlope(selfDir, selfCharts, selfCovered, selfFirst);" : ""}
16748
+ if (selfCovered < 0.995 && uSelfScale > 0.0) slope += (1.0 - selfCovered) * selfMapSlope(selfDir);
16749
+ float steep = length(slope);
16750
+ if (steep > ${SLOPE_CLAMP.toFixed(3)}) slope *= ${SLOPE_CLAMP.toFixed(3)} / steep;
16751
+ normal = normalize(normalMatrix * (selfDir - slope));
16752
+ }`).replace("#include <lights_fragment_end>", `#include <lights_fragment_end>
16753
+ {
16754
+ vec3 Nw = normalize(vSelfWorldNormal);
16755
+ vec3 L = normalize(uSelfSunDir);
16756
+ float gN = dot(L, Nw);
16757
+ // Gate on the GEOMETRIC normal: the relief normal can tilt sunward
16758
+ // past the terminator and light the night side. Fading direct light
16759
+ // by geometric N·L keeps the night dark regardless.
16760
+ float shadow = smoothstep(${TERMINATOR_GATE_DARK.toFixed(3)}, ${TERMINATOR_GATE_LIT.toFixed(3)}, gN);
16761
+ // Local crater self-shadow, lit hemisphere only — the macro gate owns
16762
+ // the night side; the short march can't see that curvature.
16763
+ if (shadow > 0.0 && gN > 0.0) {
16764
+ // The part of the fragment whose shadow the tiles cast.
16765
+ float byTiles = ${steps > 0 ? "selfCovered" : "0.0"};
16766
+ float tileLight = 1.0;
16767
+ ${steps > 0 ? TILE_LIGHT : ""}
16768
+ float mapLight = 1.0;
16769
+ if (byTiles < 0.995 && uSelfScale > 0.0) mapLight = selfMapLight(L, gN);
16770
+ shadow *= mix(mapLight, tileLight, byTiles);
16771
+ }
16772
+ reflectedLight.directDiffuse *= shadow;
16773
+ reflectedLight.directSpecular *= shadow;
16774
+ }`);
16775
+ });
16776
+ return uniforms;
16777
+ }
16778
+ function detachSelfShadow(uniforms) {
16779
+ if (!uniforms) return;
16780
+ uniforms.uSelfHeightMap.value = null;
16781
+ uniforms.uSelfScale.value = 0;
16782
+ }
16783
+
16784
+ //#endregion
16785
+ //#region src/lib/scene/objects/surface/tile-overlay.ts
16786
+ var PARS = `
16787
+ varying vec3 vTileOff;
16788
+ uniform sampler2DArray uTileMap;
16789
+ uniform sampler2DArray uTileMask;
16790
+ uniform vec4 uTileGrid[ ${7} ];
16791
+ uniform vec4 uTileWindow[ ${7} ];
16792
+ uniform vec2 uTileHome[ ${7} ];
16793
+ uniform vec3 uTileAnchor;
16794
+ uniform vec2 uTileFilter;
16795
+ ${TILE_CHAIN_GLSL}
16796
+ vec3 tileColour( vec3 below ) {
16797
+ if ( uTileWindow[ 0 ].w < 0.5 ) return below;
16798
+ TileCharts charts = tileChartsFrom( uTileAnchor, vTileOff );
16799
+
16800
+ float rows = ${4 .toFixed(1)};
16801
+ vec3 colour = vec3( 0.0 );
16802
+ float covered = 0.0;
16803
+ for ( int i = 0; i < ${7}; i ++ ) {
16804
+ vec4 window = uTileWindow[ i ];
16805
+ if ( window.w < 0.5 ) break;
16806
+ vec2 spot;
16807
+ vec2 major;
16808
+ vec2 minor;
16809
+ float alpha = tileWindowWeight( charts, window, uTileGrid[ i ], uTileHome[ i ], uTileFilter, spot, major, minor );
16810
+ if ( alpha <= 0.0 ) continue;
16811
+ ivec3 slot = ivec3( int( mod( spot.x, rows ) ), int( mod( spot.y, rows ) ), int( window.y ) );
16812
+ alpha *= texelFetch( uTileMask, slot, 0 ).r;
16813
+ if ( alpha <= 0.0 ) continue;
16814
+
16815
+ vec3 st = vec3( spot.x / rows, - spot.y / rows, window.y );
16816
+ vec2 texel = vec2( 1.0, - 1.0 ) / ( rows * uTileFilter.y );
16817
+ colour += ( 1.0 - covered ) * alpha * textureGrad( uTileMap, st, major * texel, minor * texel ).rgb;
16818
+ covered += ( 1.0 - covered ) * alpha;
16819
+ if ( covered > 0.995 ) break;
16820
+ }
16821
+ return colour + ( 1.0 - covered ) * below;
16822
+ }
16823
+ `;
16824
+ var MAP_FRAGMENT = `
16825
+ #ifdef USE_MAP
16826
+ vec4 sampledDiffuseColor = texture2D( map, vMapUv );
16827
+ sampledDiffuseColor.rgb = tileColour( sampledDiffuseColor.rgb );
16828
+ diffuseColor *= sampledDiffuseColor;
16829
+ #endif
16830
+ `;
16831
+ async function loadColourTile(url, signal) {
16832
+ const response = await fetch(url, { signal });
16833
+ if (!response.ok) throw new Error(`tile ${response.status}`);
16834
+ const bitmap = await createImageBitmap(await response.blob(), {
16835
+ imageOrientation: "flipY",
16836
+ premultiplyAlpha: "none",
16837
+ colorSpaceConversion: "none"
16838
+ });
16839
+ const texture = new Texture(bitmap);
16840
+ texture.colorSpace = SRGBColorSpace;
16841
+ texture.flipY = false;
16842
+ texture.generateMipmaps = false;
16843
+ return {
16844
+ texture,
16845
+ release() {
16846
+ texture.dispose();
16847
+ bitmap.close();
16848
+ }
16849
+ };
16097
16850
  }
16098
- function rest(bo, overlay, now) {
16099
- switchOff(overlay);
16100
- if (now - overlay.lastUsed > IDLE_DISPOSE_S * 1e3) disposeTileOverlay(bo);
16851
+ var COLOUR_TILES = {
16852
+ format: RGBAFormat,
16853
+ colorSpace: SRGBColorSpace,
16854
+ load: loadColourTile
16855
+ };
16856
+ function createOverlay(material) {
16857
+ const chain = createChain(COLOUR_TILES);
16858
+ chainShaderHook(material, "surfaceTiles", (shader) => {
16859
+ Object.assign(shader.uniforms, {
16860
+ uTileMap: chain.uniforms.map,
16861
+ uTileMask: chain.uniforms.mask,
16862
+ uTileGrid: chain.uniforms.grid,
16863
+ uTileWindow: chain.uniforms.window,
16864
+ uTileHome: chain.uniforms.home,
16865
+ uTileAnchor: chain.uniforms.anchor,
16866
+ uTileFilter: chain.uniforms.filter
16867
+ });
16868
+ shader.vertexShader = shader.vertexShader.replace("#include <common>", "#include <common>\nvarying vec3 vTileOff;\nuniform vec3 uTileAnchor;").replace("#include <begin_vertex>", "#include <begin_vertex>\nvTileOff = position - uTileAnchor;");
16869
+ shader.fragmentShader = shader.fragmentShader.replace("#include <map_pars_fragment>", `#include <map_pars_fragment>\n${PARS}`).replace("#include <map_fragment>", MAP_FRAGMENT);
16870
+ });
16871
+ return {
16872
+ material,
16873
+ chain
16874
+ };
16101
16875
  }
16102
16876
  function disposeTileOverlay(bo) {
16103
- const overlay = bo.tileOverlay;
16104
- if (!overlay) return;
16105
- switchOff(overlay);
16106
- const store = overlay.store;
16107
- if (!store) return;
16108
- for (const win of store.windows) for (const { abort } of win.pending.values()) abort.abort();
16109
- store.texture.dispose();
16110
- store.mask.dispose();
16111
- overlay.store = void 0;
16112
- overlay.uniforms.uTileMap.value = null;
16113
- overlay.uniforms.uTileMask.value = null;
16877
+ if (bo.tileOverlay) disposeChain(bo.tileOverlay.chain);
16114
16878
  }
16115
16879
  function tilesServe(bo) {
16116
16880
  const tiles = bo.surfaceTiles;
16117
- return !!tiles && !bo.model && tiles.max_level >= 3 && tilesBase() !== "" && maxTileWindows() > 0 && !bo.tileOverlay?.dead && textureAllowed(tiles.distribution);
16881
+ return !!tiles && !bo.model && tiles.max_level >= 3 && tilesBase() !== "" && maxTileWindows() > 0 && !bo.tileOverlay?.chain.dead && textureAllowed(tiles.distribution);
16118
16882
  }
16119
- var inverse = new Matrix4();
16120
- var over = new Vector3();
16121
16883
  function updateTileOverlay(bo, camera, renderer, projScale, drawable, invalidate) {
16122
16884
  const tiles = bo.surfaceTiles;
16123
16885
  const mesh = bo.mesh;
16124
16886
  const material = mesh?.material;
16125
- const level = tiles && mesh && material?.map && drawable && tilesServe(bo) ? neededLevel(bo.radiusScene, bo.cachedDist, projScale, tiles.max_level) : -1;
16126
- const now = performance.now();
16127
- if (!tiles || !mesh || !material || level < 3) {
16128
- if (bo.tileOverlay) rest(bo, bo.tileOverlay, now);
16887
+ let over;
16888
+ let level = -1;
16889
+ if (tiles && mesh && material?.map && drawable && tilesServe(bo)) {
16890
+ over = pointUnderCamera(mesh, camera);
16891
+ const finest = finestLevelAt$1(bo.tileOverlay?.chain, tiles, over);
16892
+ level = neededLevel(bo.radiusScene, bo.cachedDist, projScale, finest);
16893
+ }
16894
+ if (!tiles || !over || !material || level < 3) {
16895
+ if (bo.tileOverlay) restChain(bo.tileOverlay.chain, performance.now());
16129
16896
  return;
16130
16897
  }
16131
16898
  if (bo.tileOverlay && bo.tileOverlay.material !== material) {
16132
16899
  disposeTileOverlay(bo);
16133
16900
  bo.tileOverlay = void 0;
16134
16901
  }
16135
- const overlay = bo.tileOverlay ??= createOverlay(material);
16136
- overlay.lastUsed = now;
16137
- const levels = tiles.max_level - 3 + 1;
16138
- const count = Math.min(maxTileWindows(), MAX_WINDOWS, levels + 1);
16139
- if (overlay.store && overlay.store.texture.image.depth !== count + 1) disposeTileOverlay(bo);
16140
- const store = overlay.store ??= createStore(tiles.tile_size, count + 1, renderer);
16141
- overlay.uniforms.uTileMap.value = store.texture;
16142
- overlay.uniforms.uTileMask.value = store.mask;
16143
- overlay.uniforms.uTileFilter.value.set(store.texture.anisotropy, tiles.tile_size);
16144
- over.copy(camera.position).applyMatrix4(inverse.copy(mesh.matrixWorld).invert()).normalize();
16145
- const cap = over.y >= 0 ? 1 : 2;
16146
- overlay.chart = cameraChart(over.y, overlay.chart);
16147
- const chain = windowChain(level, count, overlay.chart, over.y);
16148
- const frame = tiles.frames ? bo.textureFrame : void 0;
16149
- const held = (link) => store.windows.find((win) => win.chart === link.chart && win.level === link.level);
16150
- store.windows = store.windows.filter((win) => {
16151
- win.rank = chain.findIndex((link) => link.chart === win.chart && link.level === win.level);
16152
- if (win.rank >= 0) return true;
16153
- win.rank = chain.length;
16154
- const heir = chain.find((link) => link.level === win.level);
16155
- const heirWindow = heir && held(heir);
16156
- const near = win.chart === 0 || win.chart === cap;
16157
- if (heir && near && !(heirWindow && settled(overlay, store, heirWindow))) return true;
16158
- release(store, win);
16159
- return false;
16160
- });
16161
- for (let i = chain.length - 1; i >= 0; i--) {
16162
- const link = chain[i];
16163
- let win = held(link);
16164
- if (!win) {
16165
- const layer = store.free.pop();
16166
- if (layer === void 0) continue;
16167
- win = {
16168
- layer,
16169
- ...link,
16170
- rank: i,
16171
- wanted: new Array(SLOTS),
16172
- pending: /* @__PURE__ */ new Map()
16173
- };
16174
- store.windows.push(win);
16175
- }
16176
- const point = chartPoint(link.chart, over.x, over.y, over.z);
16177
- retarget(store, win, placeWindow(link.chart, link.level, point.x, point.y, win.placement), frame);
16178
- pump(overlay, store, tiles, win, point.x, point.y, renderer, invalidate);
16179
- }
16180
- store.windows.sort((a, b) => b.level - a.level || a.rank - b.rank);
16181
- for (let i = 0; i < MAX_DRAWN; i++) {
16182
- const win = store.windows[i];
16183
- const link = overlay.uniforms.uTileWindow.value[i];
16184
- if (!win?.placement) {
16185
- link.w = 0;
16186
- continue;
16187
- }
16188
- overlay.uniforms.uTileGrid.value[i].set(chartColumns(win.chart, win.level), chartRows(win.chart, win.level), win.placement.tx0, win.placement.ty0);
16189
- link.set(win.chart, win.layer, win.level, 1);
16190
- }
16902
+ updateChain((bo.tileOverlay ??= createOverlay(material)).chain, tiles, chainLength(tiles, maxTileWindows()), level, over, bo.radiusScene, tiles.frames ? bo.textureFrame : void 0, renderer, invalidate);
16191
16903
  }
16192
16904
 
16193
16905
  //#endregion
@@ -16325,6 +17037,8 @@ function downgradeBodyMesh(bo, scene, clickables, meshToBody) {
16325
17037
  }
16326
17038
  disposeTileOverlay(bo);
16327
17039
  bo.tileOverlay = void 0;
17040
+ disposeReliefTiles(bo);
17041
+ bo.reliefTiles = void 0;
16328
17042
  bo.terrainWindow = null;
16329
17043
  mesh.removeFromParent();
16330
17044
  mesh.geometry.dispose();
@@ -16613,11 +17327,14 @@ async function attachDisplacementMap(material, dispMeta, tier, textureLoader, sp
16613
17327
  }
16614
17328
  texture.colorSpace = NoColorSpace;
16615
17329
  material.displacementMap = texture;
16616
- material.displacementScale = dispMeta.scale_km * kmToLocal;
16617
- material.displacementBias = dispMeta.bias_km * kmToLocal - (dispMeta.absolute_radius ? sphereRadius : 0);
17330
+ setDisplacementScale(material, dispMeta, sphereRadius, kmToLocal);
16618
17331
  material.needsUpdate = true;
16619
17332
  return texture;
16620
17333
  }
17334
+ function setDisplacementScale(material, dispMeta, sphereRadius, kmToLocal = kmToScene(1)) {
17335
+ material.displacementScale = dispMeta.scale_km * kmToLocal;
17336
+ material.displacementBias = dispMeta.bias_km * kmToLocal - (dispMeta.absolute_radius ? sphereRadius : 0);
17337
+ }
16621
17338
  function displacementTierUrl(dispMeta, tier) {
16622
17339
  return versionedUrl(`/v1/textures/${dispMeta.id}/${tier}.webp`, "textures");
16623
17340
  }
@@ -16870,7 +17587,12 @@ var SIBLING_LAYERS = {
16870
17587
  bo.displacementMap = tex;
16871
17588
  bo.displacementMeta = meta;
16872
17589
  bo.displacementTier = "low";
16873
- bo.selfShadow = sceneSettings().showSelfShadow ? attachSelfShadowToBody(material, tex, kmToScene(meta.scale_km)) : null;
17590
+ if (!sceneSettings().showSelfShadow) {
17591
+ bo.selfShadow = null;
17592
+ return;
17593
+ }
17594
+ bo.reliefTiles = reliefTilesOf(material, meta.tiles, effectiveRadiusKm(bo.body.data));
17595
+ bo.selfShadow = attachSelfShadowToBody(material, tex, kmToScene(meta.scale_km), void 0, bo.reliefTiles);
16874
17596
  },
16875
17597
  unload(bo, mesh) {
16876
17598
  if (!mesh) return;
@@ -16879,6 +17601,8 @@ var SIBLING_LAYERS = {
16879
17601
  bo.displacementTier = void 0;
16880
17602
  detachSelfShadow(bo.selfShadow);
16881
17603
  bo.selfShadow = null;
17604
+ disposeReliefTiles(bo);
17605
+ bo.reliefTiles = void 0;
16882
17606
  }
16883
17607
  },
16884
17608
  clouds: {
@@ -17288,6 +18012,7 @@ function fetchBodyNomenclature(bodyId, lang = getLocale()) {
17288
18012
  let p = cache$1.get(key);
17289
18013
  if (!p) {
17290
18014
  p = (async () => {
18015
+ await fetchMetadata();
17291
18016
  const [records, labels] = await Promise.all([fetchPositions(bodyId), fetchLabelText(bodyId, lang)]);
17292
18017
  return nameFeatures(records, labels);
17293
18018
  })();
@@ -17351,7 +18076,7 @@ function gridCell(grid, latRad, lonRad) {
17351
18076
  ty
17352
18077
  };
17353
18078
  }
17354
- const { thetas, phis } = grid;
18079
+ const { thetas, phis, riseKm } = grid;
17355
18080
  const theta = Math.PI / 2 - latRad;
17356
18081
  let phi = (lonRad - Math.PI) % (2 * Math.PI);
17357
18082
  if (phi < 0) phi += 2 * Math.PI;
@@ -17367,7 +18092,8 @@ function gridCell(grid, latRad, lonRad) {
17367
18092
  [iy0 + 1, ix0 + 1]
17368
18093
  ].map(([iy, ix]) => ({
17369
18094
  latRad: Math.PI / 2 - thetas[iy],
17370
- lonRad: phis[ix] + Math.PI
18095
+ lonRad: phis[ix] + Math.PI,
18096
+ riseKm: riseKm?.[iy * phis.length + ix]
17371
18097
  })),
17372
18098
  tx,
17373
18099
  ty
@@ -17543,7 +18269,8 @@ function gridForBody(bo) {
17543
18269
  return tw ? {
17544
18270
  kind: "window",
17545
18271
  thetas: tw.thetas,
17546
- phis: tw.phis
18272
+ phis: tw.phis,
18273
+ riseKm: tw.riseKm
17547
18274
  } : {
17548
18275
  kind: "uniform",
17549
18276
  segs: bo?.currentSegments ?? 128
@@ -17552,20 +18279,22 @@ function gridForBody(bo) {
17552
18279
  function renderedCell(bo, bodyId, radiusKm, latRad, lonRad) {
17553
18280
  const radii = radiiFor(bodyId, radiusKm);
17554
18281
  if (!radii) return null;
18282
+ const grid = gridForBody(bo);
17555
18283
  let hf = null;
17556
18284
  let scaleKm = 0;
17557
18285
  let biasKm = 0;
17558
18286
  const meta = bo?.displacementMeta;
17559
- if (meta && bo?.displacementMap) {
18287
+ const ownHeights = grid.kind === "window" && !!grid.riseKm;
18288
+ if (meta && bo?.displacementMap && !ownHeights) {
17560
18289
  hf = heightFieldFor(bo, bodyId);
17561
18290
  if (!hf) return null;
17562
18291
  scaleKm = meta.scale_km;
17563
18292
  biasKm = meta.bias_km - (meta.absolute_radius ? radiusKm : 0);
17564
18293
  }
17565
- const { corners, tx, ty } = gridCell(gridForBody(bo), latRad, lonRad);
18294
+ const { corners, tx, ty } = gridCell(grid, latRad, lonRad);
17566
18295
  return {
17567
18296
  pts: corners.map((corner) => {
17568
- const dispKm = hf ? sampleHeightTexel(hf, corner.latRad, corner.lonRad) * scaleKm + biasKm : 0;
18297
+ const dispKm = corner.riseKm ?? (hf ? sampleHeightTexel(hf, corner.latRad, corner.lonRad) * scaleKm + biasKm : 0);
17569
18298
  return displacedPoint(corner.latRad, corner.lonRad, dispKm, radiusKm, radii.a, radii.b, radii.c);
17570
18299
  }),
17571
18300
  tx,
@@ -20292,14 +21021,14 @@ var PointCloudSystem = class {
20292
21021
 
20293
21022
  //#endregion
20294
21023
  //#region src/lib/scene/objects/trail/lagrange-frame.ts
20295
- var DEG$7 = Math.PI / 180;
21024
+ var DEG$8 = Math.PI / 180;
20296
21025
  var J2000 = 2451545;
20297
21026
  var A = 1.00000261;
20298
21027
  var E = .01671123;
20299
- var L0 = 100.46457166 * DEG$7;
20300
- var L_RATE = 35999.37244981 / 36525 * DEG$7;
20301
- var PERI0 = 102.93768193 * DEG$7;
20302
- var PERI_RATE = .32327364 / 36525 * DEG$7;
21028
+ var L0 = 100.46457166 * DEG$8;
21029
+ var L_RATE = 35999.37244981 / 36525 * DEG$8;
21030
+ var PERI0 = 102.93768193 * DEG$8;
21031
+ var PERI_RATE = .32327364 / 36525 * DEG$8;
20303
21032
  function earthHeliocentric(jd) {
20304
21033
  const t = jd - J2000;
20305
21034
  const peri = PERI0 + PERI_RATE * t;
@@ -22225,7 +22954,7 @@ var Placer = class {
22225
22954
  }
22226
22955
  chebMiss(id, jd) {
22227
22956
  const store = this.ctx.chebStore;
22228
- if (store.isLoading(id, jd)) return "loading";
22957
+ if (store.isAwaited(id, jd)) return "loading";
22229
22958
  const coverage = store.zoneCoverage(id);
22230
22959
  if (coverage !== null && (jd < coverage.start || jd > coverage.end) && jd === this.passJd) {
22231
22960
  this.noData.count++;
@@ -22299,7 +23028,7 @@ var Placer = class {
22299
23028
  const zonePref = activeSysId ? (fitCenterNaif) => ctx.bodies.isInSystem(`naif-${fitCenterNaif}`, activeSysId) : void 0;
22300
23029
  const located = ctx.probeStore?.probeWithCenter(d.id, jd, zonePref) ?? null;
22301
23030
  if (!located) {
22302
- if (ctx.probeStore?.loadingAt(jd)) return setUnplaced(body, "loading");
23031
+ if (ctx.probeStore?.awaitedAt(jd)) return setUnplaced(body, "loading");
22303
23032
  setUnplaced(body, "no-data");
22304
23033
  diagnostics.warnOnce("probe-unavailable", d.id, () => {
22305
23034
  const reason = !ctx.probeStore ? "no ProbeStore" : "no zone has both a loaded chunk and a sub-chunk covering this jd";
@@ -22510,11 +23239,11 @@ function updateRingShaders(bodyObjects, focusTruePos, realistic, overexpose, cam
22510
23239
 
22511
23240
  //#endregion
22512
23241
  //#region src/lib/scene/objects/surface/atmosphere-season.ts
22513
- var DEG$6 = Math.PI / 180;
23242
+ var DEG$7 = Math.PI / 180;
22514
23243
  var J2000_JD = 2451545;
22515
23244
  function marsSolarLongitudeDeg(jd) {
22516
23245
  const d = jd - J2000_JD;
22517
- const m = (19.3871 + .52402073 * d) * DEG$6;
23246
+ const m = (19.3871 + .52402073 * d) * DEG$7;
22518
23247
  return ((270.3871 + .524038496 * d + ((10.691 + 3e-7 * d) * Math.sin(m) + .623 * Math.sin(2 * m) + .05 * Math.sin(3 * m) + .005 * Math.sin(4 * m) + 5e-4 * Math.sin(5 * m))) % 360 + 360) % 360;
22519
23248
  }
22520
23249
  function sampleSeason(lsGrid, values, lsDeg) {
@@ -22798,66 +23527,134 @@ function updateSunShadowLight(bodyObjects, focusTruePos, ctx, shadowLight, sunPo
22798
23527
  }
22799
23528
 
22800
23529
  //#endregion
22801
- //#region src/lib/scene/lod/terrain-window.ts
23530
+ //#region src/lib/scene/lod/terrain-grid.ts
22802
23531
  var COARSE_SEGS = 128;
22803
- var CELL_BUDGET = 256;
22804
- var ENTER_ALT = .25;
22805
- var EXIT_ALT = .32;
22806
- var RECENTER_FRAC = .35;
22807
- var REBUILD_MIN_MS = 300;
22808
- var MARGIN = 1.25;
23532
+ var MARGIN$1 = 1.25;
22809
23533
  var SIN_FLOOR = .05;
22810
- var MAX_VERTS = 4e5;
22811
- var _inv = new Matrix4();
22812
- var _camLocal = new Vector3();
22813
- function mergeAxis(max, intervals, step) {
22814
- const inFine = (p) => intervals.some(([lo, hi]) => p > lo + 1e-9 && p < hi - 1e-9);
22815
- const coarseStep = max / COARSE_SEGS;
22816
- const pts = [];
22817
- for (let j = 0; j <= COARSE_SEGS; j++) {
22818
- const p = j === COARSE_SEGS ? max : j * coarseStep;
22819
- if (j === 0 || j === COARSE_SEGS || !inFine(p)) pts.push(p);
22820
- }
22821
- for (const [lo, hi] of intervals) {
22822
- const n0 = Math.ceil(Math.max(lo, 0) / step - 1e-9);
22823
- const n1 = Math.floor(Math.min(hi, max) / step + 1e-9);
22824
- for (let n = n0; n <= n1; n++) pts.push(n * step);
23534
+ var BASE_WIDTH_PX = 2048;
23535
+ function mergeBands(max, bands, coarseStep) {
23536
+ const inside = (band, p) => !!band && band.intervals.some(([lo, hi]) => p > lo + 1e-9 && p < hi - 1e-9);
23537
+ const points = [];
23538
+ const segments = Math.round(max / coarseStep);
23539
+ for (let j = 0; j <= segments; j++) {
23540
+ const p = j === segments ? max : j * coarseStep;
23541
+ if (j === 0 || j === segments || !inside(bands[bands.length - 1], p)) points.push({
23542
+ p,
23543
+ level: 0
23544
+ });
22825
23545
  }
22826
- pts.sort((a, b) => a - b);
22827
- const tol = step * .25;
22828
- const out = [];
22829
- for (const p of pts) {
22830
- if (out.length && p - out[out.length - 1] < tol) continue;
22831
- out.push(p);
23546
+ bands.forEach((band, i) => {
23547
+ for (const [lo, hi] of band.intervals) {
23548
+ const n0 = Math.ceil(Math.max(lo, 0) / band.step - 1e-9);
23549
+ const n1 = Math.floor(Math.min(hi, max) / band.step + 1e-9);
23550
+ for (let n = n0; n <= n1; n++) {
23551
+ const p = n * band.step;
23552
+ if (!inside(bands[i - 1], p)) points.push({
23553
+ p,
23554
+ level: band.level
23555
+ });
23556
+ }
23557
+ }
23558
+ });
23559
+ points.sort((a, b) => a.p - b.p || b.level - a.level);
23560
+ const tolerance = (bands[0]?.step ?? coarseStep) * .25;
23561
+ const kept = [];
23562
+ for (const point of points) {
23563
+ const last = kept[kept.length - 1];
23564
+ if (last && point.p - last.p < tolerance) last.level = Math.max(last.level, point.level);
23565
+ else kept.push(point);
23566
+ }
23567
+ kept[0].p = 0;
23568
+ kept[kept.length - 1].p = max;
23569
+ return {
23570
+ at: Float64Array.from(kept, (point) => point.p),
23571
+ level: Int8Array.from(kept, (point) => point.level)
23572
+ };
23573
+ }
23574
+ function arcIntervals(centre, half) {
23575
+ const full = 2 * Math.PI;
23576
+ if (half >= Math.PI) return [[0, full]];
23577
+ const lo = centre - half;
23578
+ const hi = centre + half;
23579
+ if (lo < 0) return [[0, hi], [lo + full, full]];
23580
+ if (hi > full) return [[0, hi - full], [lo, full]];
23581
+ return [[lo, hi]];
23582
+ }
23583
+ function finestPixel(maxLevel) {
23584
+ return 2 * Math.PI / (BASE_WIDTH_PX * 2 ** maxLevel);
23585
+ }
23586
+ function nestedStepLevel(altitude, maxLevel, bandCells, previous) {
23587
+ const pixel = finestPixel(maxLevel);
23588
+ const raw = Math.log2(Math.max(altitude / bandCells, pixel) / pixel);
23589
+ if (previous !== void 0 && raw > previous - .85 && raw < previous + .15) return previous;
23590
+ return Math.max(0, Math.ceil(raw - 1e-9));
23591
+ }
23592
+ function planNestedGrid(centreTheta, centrePhi, altitude, peak, maxLevel, bandCells, stepLevel) {
23593
+ const pixel = finestPixel(maxLevel);
23594
+ const rowStep = Math.PI / 128;
23595
+ const columnStep = 2 * Math.PI / 128;
23596
+ const horizon = (height) => Math.acos(1 / (1 + Math.max(height, 0)));
23597
+ const reach = MARGIN$1 * (horizon(altitude) + horizon(peak));
23598
+ const sinTheta = Math.max(Math.sin(centreTheta), SIN_FLOOR);
23599
+ const widen = Math.round(Math.log2(1 / sinTheta));
23600
+ const rowBands = [];
23601
+ const columnBands = [];
23602
+ let radius = 0;
23603
+ for (let exponent = stepLevel; radius < reach; exponent++) {
23604
+ const step = pixel * 2 ** exponent;
23605
+ if (step >= rowStep) break;
23606
+ radius += bandCells * step;
23607
+ const level = Math.max(maxLevel - exponent, 0);
23608
+ rowBands.push({
23609
+ intervals: [[Math.max(centreTheta - radius, 0), Math.min(centreTheta + radius, Math.PI)]],
23610
+ step,
23611
+ level
23612
+ });
23613
+ const half = Math.min(radius / sinTheta, Math.PI);
23614
+ const fit = Math.ceil(Math.log2(2 * half / (4 * bandCells) / pixel) - 1e-9);
23615
+ const columnExponent = Math.max(exponent + widen, fit);
23616
+ const columnWidth = pixel * 2 ** columnExponent;
23617
+ if (columnWidth >= columnStep) continue;
23618
+ columnBands.push({
23619
+ intervals: arcIntervals(centrePhi, half),
23620
+ step: columnWidth,
23621
+ level: Math.max(maxLevel - (columnExponent - widen), 0)
23622
+ });
22832
23623
  }
22833
- out[0] = 0;
22834
- out[out.length - 1] = max;
22835
- return Float64Array.from(out);
23624
+ return {
23625
+ rows: mergeBands(Math.PI, rowBands, rowStep),
23626
+ columns: mergeBands(2 * Math.PI, columnBands, columnStep),
23627
+ stepLevel,
23628
+ innerRadius: bandCells * pixel * 2 ** stepLevel
23629
+ };
22836
23630
  }
22837
- function buildGridGeometry(radius, thetas, phis) {
23631
+ function buildGridArrays(radius, thetas, phis, riseKm, unitsPerKm = 1) {
22838
23632
  const rows = thetas.length;
22839
23633
  const cols = phis.length;
22840
23634
  const positions = new Float32Array(rows * cols * 3);
22841
23635
  const normals = new Float32Array(rows * cols * 3);
22842
23636
  const uvs = new Float32Array(rows * cols * 2);
23637
+ const sinPhi = Float64Array.from(phis, Math.sin);
23638
+ const cosPhi = Float64Array.from(phis, Math.cos);
22843
23639
  for (let iy = 0; iy < rows; iy++) {
22844
23640
  const theta = thetas[iy];
22845
23641
  const sinT = Math.sin(theta);
22846
23642
  const cosT = Math.cos(theta);
22847
23643
  for (let ix = 0; ix < cols; ix++) {
22848
- const phi = phis[ix];
22849
- const nx = -Math.cos(phi) * sinT;
23644
+ const vertex = iy * cols + ix;
23645
+ const nx = -cosPhi[ix] * sinT;
22850
23646
  const ny = cosT;
22851
- const nz = Math.sin(phi) * sinT;
22852
- const i3 = (iy * cols + ix) * 3;
22853
- positions[i3] = radius * nx;
22854
- positions[i3 + 1] = radius * ny;
22855
- positions[i3 + 2] = radius * nz;
23647
+ const nz = sinPhi[ix] * sinT;
23648
+ const r = radius + (riseKm ? riseKm[vertex] * unitsPerKm : 0);
23649
+ const i3 = vertex * 3;
23650
+ positions[i3] = r * nx;
23651
+ positions[i3 + 1] = r * ny;
23652
+ positions[i3 + 2] = r * nz;
22856
23653
  normals[i3] = nx;
22857
23654
  normals[i3 + 1] = ny;
22858
23655
  normals[i3 + 2] = nz;
22859
- const i2 = (iy * cols + ix) * 2;
22860
- uvs[i2] = phi / (2 * Math.PI);
23656
+ const i2 = vertex * 2;
23657
+ uvs[i2] = phis[ix] / (2 * Math.PI);
22861
23658
  uvs[i2 + 1] = 1 - theta / Math.PI;
22862
23659
  }
22863
23660
  }
@@ -22883,19 +23680,155 @@ function buildGridGeometry(radius, thetas, phis) {
22883
23680
  }
22884
23681
  }
22885
23682
  }
23683
+ return {
23684
+ positions,
23685
+ normals,
23686
+ uvs,
23687
+ indices: indices.subarray(0, k)
23688
+ };
23689
+ }
23690
+
23691
+ //#endregion
23692
+ //#region src/lib/scene/lod/terrain-window.ts
23693
+ var CELL_BUDGET = 256;
23694
+ var ENTER_ALT = .25;
23695
+ var EXIT_ALT = .32;
23696
+ var RECENTER_FRAC = .35;
23697
+ var REBUILD_MIN_MS = 300;
23698
+ var MAX_VERTS = 4e5;
23699
+ var _inv = new Matrix4();
23700
+ var _camLocal = new Vector3();
23701
+ function mergeAxis(max, intervals, step) {
23702
+ const inFine = (p) => intervals.some(([lo, hi]) => p > lo + 1e-9 && p < hi - 1e-9);
23703
+ const coarseStep = max / 128;
23704
+ const pts = [];
23705
+ for (let j = 0; j <= 128; j++) {
23706
+ const p = j === 128 ? max : j * coarseStep;
23707
+ if (j === 0 || j === 128 || !inFine(p)) pts.push(p);
23708
+ }
23709
+ for (const [lo, hi] of intervals) {
23710
+ const n0 = Math.ceil(Math.max(lo, 0) / step - 1e-9);
23711
+ const n1 = Math.floor(Math.min(hi, max) / step + 1e-9);
23712
+ for (let n = n0; n <= n1; n++) pts.push(n * step);
23713
+ }
23714
+ pts.sort((a, b) => a - b);
23715
+ const tol = step * .25;
23716
+ const out = [];
23717
+ for (const p of pts) {
23718
+ if (out.length && p - out[out.length - 1] < tol) continue;
23719
+ out.push(p);
23720
+ }
23721
+ out[0] = 0;
23722
+ out[out.length - 1] = max;
23723
+ return Float64Array.from(out);
23724
+ }
23725
+ function gridGeometry(grid) {
22886
23726
  const geometry = new BufferGeometry();
22887
- geometry.setAttribute("position", new BufferAttribute(positions, 3));
22888
- geometry.setAttribute("normal", new BufferAttribute(normals, 3));
22889
- geometry.setAttribute("uv", new BufferAttribute(uvs, 2));
22890
- geometry.setIndex(new BufferAttribute(indices.subarray(0, k), 1));
23727
+ geometry.setAttribute("position", new BufferAttribute(grid.positions, 3));
23728
+ geometry.setAttribute("normal", new BufferAttribute(grid.normals, 3));
23729
+ geometry.setAttribute("uv", new BufferAttribute(grid.uvs, 2));
23730
+ geometry.setIndex(new BufferAttribute(grid.indices, 1));
22891
23731
  return geometry;
22892
23732
  }
23733
+ var tiledStates = /* @__PURE__ */ new WeakMap();
23734
+ var MAX_BUILD_FAILURES = 2;
23735
+ function heightTilesOf(bo) {
23736
+ const tiles = bo.displacementMeta?.tiles;
23737
+ return reliefTilesAvailable(tiles) && !tiledStates.get(bo)?.dead ? tiles : void 0;
23738
+ }
23739
+ function restoreDisplacement(bo) {
23740
+ if (!bo.mesh || !bo.displacementMeta) return;
23741
+ setDisplacementScale(bo.mesh.material, bo.displacementMeta, bo.radiusScene);
23742
+ }
22893
23743
  function exitWindow(bo) {
23744
+ const state = tiledStates.get(bo);
23745
+ state?.abort?.abort();
23746
+ if (state) state.abort = void 0;
22894
23747
  if (!bo.terrainWindow) return;
23748
+ if (bo.terrainWindow.riseKm) restoreDisplacement(bo);
22895
23749
  bo.terrainWindow = null;
22896
23750
  bo.currentSegments = void 0;
22897
23751
  }
22898
- function updateTerrainWindow(bo, camera, eligible) {
23752
+ function drift(thetaA, phiA, thetaB, phiB) {
23753
+ const cos = Math.sin(thetaA) * Math.sin(thetaB) * Math.cos(phiA - phiB) + Math.cos(thetaA) * Math.cos(thetaB);
23754
+ return Math.acos(Math.min(1, Math.max(-1, cos)));
23755
+ }
23756
+ function updateTiledWindow(bo, mesh, tiles, altitude, centerTheta, centerPhi, invalidate) {
23757
+ const held = bo.terrainWindow?.riseKm ? bo.terrainWindow : void 0;
23758
+ const material = mesh.material;
23759
+ if (held) {
23760
+ material.displacementScale = 0;
23761
+ material.displacementBias = 0;
23762
+ }
23763
+ const state = tiledStates.get(bo) ?? {
23764
+ failures: 0,
23765
+ dead: false
23766
+ };
23767
+ tiledStates.set(bo, state);
23768
+ if (state.abort) return !!held;
23769
+ const meta = bo.displacementMeta;
23770
+ if (!meta) return !!held;
23771
+ const bandCells = currentRenderTier().terrainBandCells;
23772
+ const stepLevel = nestedStepLevel(altitude, tiles.max_level, bandCells, held?.stepLevel);
23773
+ if (held) {
23774
+ const still = drift(centerTheta, centerPhi, held.centerTheta, held.centerPhi);
23775
+ if (held.stepLevel === stepLevel && still < RECENTER_FRAC * held.angRadius) return true;
23776
+ if (performance.now() - held.builtAt < REBUILD_MIN_MS) return true;
23777
+ }
23778
+ const radiusKm = effectiveRadiusKm(bo.body.data);
23779
+ const grid = planNestedGrid(centerTheta, centerPhi, altitude, .5 * meta.scale_km / radiusKm, tiles.max_level, bandCells, stepLevel);
23780
+ const thetas = grid.rows.at;
23781
+ const phis = grid.columns.at;
23782
+ if (thetas.length * phis.length > MAX_VERTS) {
23783
+ console.warn(`terrain grid for ${bo.body.data.id} would need ${thetas.length}×${phis.length} vertices: left to the shader`);
23784
+ state.dead = true;
23785
+ return !!held;
23786
+ }
23787
+ const abort = new AbortController();
23788
+ state.abort = abort;
23789
+ const asked = performance.now();
23790
+ buildTerrainGrid({
23791
+ root: tileRoot(tiles),
23792
+ version: tiles.version,
23793
+ tileSize: tiles.tile_size,
23794
+ scaleKm: tiles.scale_km ?? 0,
23795
+ biasKm: (tiles.bias_km ?? 0) - (meta.absolute_radius ? radiusKm : 0),
23796
+ radius: kmToScene(radiusKm),
23797
+ unitsPerKm: kmToScene(1),
23798
+ rows: grid.rows,
23799
+ columns: grid.columns
23800
+ }, abort.signal).then((built) => {
23801
+ if (state.abort !== abort) return;
23802
+ state.abort = void 0;
23803
+ state.failures = 0;
23804
+ if (bo.mesh !== mesh || !bo.displacementMap) return;
23805
+ const old = mesh.geometry;
23806
+ mesh.geometry = gridGeometry(built);
23807
+ old.dispose();
23808
+ material.displacementScale = 0;
23809
+ material.displacementBias = 0;
23810
+ bo.terrainWindow = {
23811
+ thetas,
23812
+ phis,
23813
+ centerTheta,
23814
+ centerPhi,
23815
+ angRadius: grid.innerRadius,
23816
+ stepLevel,
23817
+ texWidth: 2048 * 2 ** tiles.max_level,
23818
+ builtAt: performance.now(),
23819
+ riseKm: built.riseKm
23820
+ };
23821
+ bo.currentSegments = 128;
23822
+ console.debug(`terrain grid ${bo.body.data.id}: ${thetas.length}×${phis.length}, level ${stepLevel}, ${Math.round(performance.now() - asked)} ms`);
23823
+ invalidate();
23824
+ }).catch(() => {
23825
+ if (state.abort !== abort) return;
23826
+ state.abort = void 0;
23827
+ if (++state.failures >= MAX_BUILD_FAILURES) state.dead = true;
23828
+ });
23829
+ return !!held;
23830
+ }
23831
+ function updateTerrainWindow(bo, camera, eligible, invalidate) {
22899
23832
  const mesh = bo.mesh;
22900
23833
  const tex = bo.displacementMap;
22901
23834
  if (!eligible || !mesh || !tex) {
@@ -22914,23 +23847,25 @@ function updateTerrainWindow(bo, camera, eligible) {
22914
23847
  exitWindow(bo);
22915
23848
  return false;
22916
23849
  }
23850
+ const centerTheta = Math.acos(Math.min(1, Math.max(-1, _camLocal.y / dist)));
23851
+ let centerPhi = Math.atan2(_camLocal.z, -_camLocal.x);
23852
+ if (centerPhi < 0) centerPhi += 2 * Math.PI;
23853
+ const tiles = heightTilesOf(bo);
23854
+ if (tiles) return updateTiledWindow(bo, mesh, tiles, alt, centerTheta, centerPhi, invalidate);
23855
+ if (bo.terrainWindow?.riseKm) restoreDisplacement(bo);
22917
23856
  const texWidth = tex.image?.width ?? 2048;
22918
23857
  const texelAng = 2 * Math.PI / texWidth;
22919
23858
  const horizon = Math.acos(1 / (1 + alt));
22920
- const rawStep = Math.max(texelAng, 2 * horizon * MARGIN / CELL_BUDGET);
23859
+ const rawStep = Math.max(texelAng, 2 * horizon * MARGIN$1 / CELL_BUDGET);
22921
23860
  const rawLevel = Math.log2(rawStep / texelAng);
22922
23861
  let stepLevel = Math.max(0, Math.ceil(rawLevel - 1e-9));
22923
23862
  const w = bo.terrainWindow;
22924
23863
  if (w && w.texWidth === texWidth && rawLevel > w.stepLevel - .85 && rawLevel < w.stepLevel + .15) stepLevel = w.stepLevel;
22925
23864
  const step = texelAng * 2 ** stepLevel;
22926
23865
  const angRadius = step * CELL_BUDGET / 2;
22927
- const centerTheta = Math.acos(Math.min(1, Math.max(-1, _camLocal.y / dist)));
22928
- let centerPhi = Math.atan2(_camLocal.z, -_camLocal.x);
22929
- if (centerPhi < 0) centerPhi += 2 * Math.PI;
22930
23866
  if (w && w.texWidth === texWidth) {
22931
23867
  if (w.stepLevel === stepLevel) {
22932
- const cosDrift = Math.sin(centerTheta) * Math.sin(w.centerTheta) * Math.cos(centerPhi - w.centerPhi) + Math.cos(centerTheta) * Math.cos(w.centerTheta);
22933
- if (Math.acos(Math.min(1, Math.max(-1, cosDrift))) < RECENTER_FRAC * w.angRadius) return true;
23868
+ if (drift(centerTheta, centerPhi, w.centerTheta, w.centerPhi) < RECENTER_FRAC * w.angRadius) return true;
22934
23869
  }
22935
23870
  if (performance.now() - w.builtAt < REBUILD_MIN_MS) return true;
22936
23871
  }
@@ -22950,7 +23885,7 @@ function updateTerrainWindow(bo, camera, eligible) {
22950
23885
  return false;
22951
23886
  }
22952
23887
  const old = mesh.geometry;
22953
- mesh.geometry = buildGridGeometry(geomRadius, thetas, phis);
23888
+ mesh.geometry = gridGeometry(buildGridArrays(geomRadius, thetas, phis));
22954
23889
  old.dispose();
22955
23890
  console.debug(`terrain window ${bo.body.data.id}: ${thetas.length}×${phis.length} grid, level ${stepLevel} (tex ${texWidth})`);
22956
23891
  bo.terrainWindow = {
@@ -22963,7 +23898,7 @@ function updateTerrainWindow(bo, camera, eligible) {
22963
23898
  texWidth,
22964
23899
  builtAt: performance.now()
22965
23900
  };
22966
- bo.currentSegments = COARSE_SEGS;
23901
+ bo.currentSegments = 128;
22967
23902
  return true;
22968
23903
  }
22969
23904
 
@@ -22997,7 +23932,7 @@ function desiredSphereSegments(screenR, isStar, inSystem, current) {
22997
23932
  }
22998
23933
  return target;
22999
23934
  }
23000
- function updateSphereLOD(bodyObjects, camera, renderer, ctx, focusedId) {
23935
+ function updateSphereLOD(bodyObjects, camera, renderer, ctx, focusedId, invalidate) {
23001
23936
  const fovRad = camera.fov * Math.PI / 180;
23002
23937
  const projScale = renderer.domElement.clientHeight / (2 * Math.tan(fovRad / 2));
23003
23938
  const activeSystem = ctx.visibility.activeSystemId;
@@ -23008,7 +23943,7 @@ function updateSphereLOD(bodyObjects, camera, renderer, ctx, focusedId) {
23008
23943
  const isStar = bo.body.data.objectType === ObjectType.STAR;
23009
23944
  const id = bo.body.data.id;
23010
23945
  if (id === terrainId || bo.terrainWindow) {
23011
- if (updateTerrainWindow(bo, camera, id === terrainId)) continue;
23946
+ if (updateTerrainWindow(bo, camera, id === terrainId, invalidate)) continue;
23012
23947
  }
23013
23948
  const desired = desiredSphereSegments(bo.radiusScene / bo.cachedDist * projScale, isStar, activeSystem ? id === activeSystem || ctx.visibility.isInActiveSystem(bo.body.data.parentId) : id === focusedId, bo.currentSegments ?? 64);
23014
23949
  if (desired === bo.currentSegments) continue;
@@ -23062,11 +23997,13 @@ function updateTextureLOD(bodyObjects, camera, renderer, ctx, textureLoader, foc
23062
23997
  const target = wantsUpgrade ? highestAvailableTier(desiredRank, bo.availableTiers) : bo.textureTier;
23063
23998
  if (target) loadBodyTextureTier(bo, target, desiredFrame, textureLoader);
23064
23999
  }
23065
- updateTileOverlay(bo, camera, renderer, projScale * renderer.getPixelRatio(), settings.showSurfaceTexture && !bo.appearance?.surfaceOwned, invalidate);
24000
+ const devicePx = projScale * renderer.getPixelRatio();
24001
+ updateTileOverlay(bo, camera, renderer, devicePx, settings.showSurfaceTexture && !bo.appearance?.surfaceOwned, invalidate);
24002
+ updateReliefTiles(bo, camera, renderer, devicePx, invalidate);
23066
24003
  const dispTiers = bo.displacementMeta?.tiers;
23067
24004
  if (bo.displacementMap && dispTiers?.length && !bo.displacementLoading) {
23068
24005
  const maxTex = renderer.capabilities.maxTextureSize;
23069
- const capRank = maxTex >= 16383 ? 2 : maxTex >= 8192 ? 1 : 0;
24006
+ const capRank = maxTex >= 16383 && !reliefTilesServe(bo) ? 2 : maxTex >= 8192 ? 1 : 0;
23070
24007
  const current = bo.displacementTier ?? "low";
23071
24008
  const target = highestAvailableTier(Math.min(tierRank(desiredDemTier(altitudeRadii, current)), capRank, tierCap), dispTiers);
23072
24009
  if (target && target !== current) swapDisplacementTier(bo, target);
@@ -24908,7 +25845,7 @@ var SceneRenderer = class SceneRenderer {
24908
25845
  updateUserLocationOcclusion(this.userLocationMarker, this.bodyObjects, this.camera);
24909
25846
  const focusedIdLod = nomenclatureBodyId(this.focusController.current, this.bodyObjects);
24910
25847
  updateTextureLOD(this.bodyObjects, this.camera, this.renderer, this.ctx, this.textureLoader, focusedIdLod, this.sceneJd, this.appearances, this.invalidate);
24911
- updateSphereLOD(this.bodyObjects, this.camera, this.renderer, this.ctx, focusedIdLod);
25848
+ updateSphereLOD(this.bodyObjects, this.camera, this.renderer, this.ctx, focusedIdLod, this.invalidate);
24912
25849
  updateSunShadowLight(this.bodyObjects, this.focus.focusTruePos, this.ctx, this.shadowLight, this.sunPointLight, distance, this._tmpV3, sceneSettings().realisticLighting, this.sunIntensityScale);
24913
25850
  this.updateModelMounts();
24914
25851
  this.pointClouds.drainOnePendingSceneAdd();
@@ -25380,6 +26317,10 @@ var SceneRenderer = class SceneRenderer {
25380
26317
  }
25381
26318
  lastProbeTargetCheck = null;
25382
26319
  requestedProbeTargets = /* @__PURE__ */ new Set();
26320
+ probeTargetRetries = new RetryBackoff(() => {
26321
+ this.lastProbeTargetCheck = null;
26322
+ this.invalidate();
26323
+ });
25383
26324
  askedProbeRides = /* @__PURE__ */ new Set();
25384
26325
  ensureFocusedProbeTarget() {
25385
26326
  const focused = this.focusController.current;
@@ -25397,9 +26338,13 @@ var SceneRenderer = class SceneRenderer {
25397
26338
  if (!fcId) return;
25398
26339
  const body = this.ctx.getBody(fcId);
25399
26340
  if (!body) {
25400
- if (this.requestedProbeTargets.has(fcId)) return;
26341
+ if (this.requestedProbeTargets.has(fcId) || this.probeTargetRetries.waiting(fcId)) return;
25401
26342
  this.requestedProbeTargets.add(fcId);
25402
- this.ctx.ensureBody(fcId, jdToDate(this.clock.jd));
26343
+ this.ctx.ensureBody(fcId, jdToDate(this.clock.jd)).then(() => this.probeTargetRetries.clear(fcId), (error) => {
26344
+ console.warn(`[renderer] the body ${fcId} a craft is fit to did not load:`, error);
26345
+ this.requestedProbeTargets.delete(fcId);
26346
+ this.probeTargetRetries.fail(fcId);
26347
+ });
25403
26348
  return;
25404
26349
  }
25405
26350
  this.focusController.promotion.ensureBodyObjects(body);
@@ -25845,6 +26790,10 @@ var SceneRenderer = class SceneRenderer {
25845
26790
  this.travelPath.dispose();
25846
26791
  this.extensions.dispose();
25847
26792
  for (const bo of this.bodyObjects.values()) if (bo.model) unloadBodyModel(bo);
26793
+ for (const bo of this.bodyObjects.values()) {
26794
+ disposeTileOverlay(bo);
26795
+ disposeReliefTiles(bo);
26796
+ }
25848
26797
  this.pointClouds.dispose();
25849
26798
  this.disposeScene(this.scene);
25850
26799
  this.disposeScene(this.modelScene);
@@ -26903,7 +27852,7 @@ var IconExtension = class extends MarkerExtension {
26903
27852
 
26904
27853
  //#endregion
26905
27854
  //#region src/lib/flatmap/projection.ts
26906
- var DEG$5 = Math.PI / 180;
27855
+ var DEG$6 = Math.PI / 180;
26907
27856
  var RAD$2 = 180 / Math.PI;
26908
27857
  function wrapLon(lon) {
26909
27858
  if (lon >= -180 && lon < 180) return lon;
@@ -26968,7 +27917,7 @@ function equirectangular(options) {
26968
27917
  maxX: Math.PI,
26969
27918
  maxY: Math.PI / 2
26970
27919
  },
26971
- forward: (lon, lat) => [wrapLon(lon - lon0) * DEG$5, lat * DEG$5],
27920
+ forward: (lon, lat) => [wrapLon(lon - lon0) * DEG$6, lat * DEG$6],
26972
27921
  rowInverse,
26973
27922
  inverse: rowToInverse(rowInverse, lon0)
26974
27923
  };
@@ -26983,8 +27932,8 @@ var eeDy = (t) => 9 * EE_A4 * t ** 8 + 7 * EE_A3 * t ** 6 + 3 * EE_A2 * t ** 2 +
26983
27932
  function equalEarth(options) {
26984
27933
  const lon0 = options.centerLon ?? 0;
26985
27934
  const forward = (lon, lat) => {
26986
- const theta = Math.asin(clamp1(EE_SQRT3 / 2 * Math.sin(lat * DEG$5)));
26987
- const dl = wrapLon(lon - lon0) * DEG$5;
27935
+ const theta = Math.asin(clamp1(EE_SQRT3 / 2 * Math.sin(lat * DEG$6)));
27936
+ const dl = wrapLon(lon - lon0) * DEG$6;
26988
27937
  return [2 * EE_SQRT3 * dl * Math.cos(theta) / (3 * eeDy(theta)), eeY(theta)];
26989
27938
  };
26990
27939
  const thetaFor = (y) => {
@@ -27054,7 +28003,7 @@ function mollweide(options) {
27054
28003
  maxY: MW_SQRT2
27055
28004
  },
27056
28005
  forward: (lon, lat) => {
27057
- const phi = lat * DEG$5;
28006
+ const phi = lat * DEG$6;
27058
28007
  let theta = phi;
27059
28008
  if (Math.abs(Math.abs(phi) - Math.PI / 2) < 1e-9) theta = Math.sign(phi) * (Math.PI / 2);
27060
28009
  else {
@@ -27065,7 +28014,7 @@ function mollweide(options) {
27065
28014
  if (Math.abs(step) < 1e-12) break;
27066
28015
  }
27067
28016
  }
27068
- const dl = wrapLon(lon - lon0) * DEG$5;
28017
+ const dl = wrapLon(lon - lon0) * DEG$6;
27069
28018
  return [2 * MW_SQRT2 / Math.PI * dl * Math.cos(theta), MW_SQRT2 * Math.sin(theta)];
27070
28019
  },
27071
28020
  rowInverse,
@@ -27101,8 +28050,8 @@ function sinusoidal(options) {
27101
28050
  maxY: Math.PI / 2
27102
28051
  },
27103
28052
  forward: (lon, lat) => {
27104
- const phi = lat * DEG$5;
27105
- return [wrapLon(lon - lon0) * DEG$5 * Math.cos(phi), phi];
28053
+ const phi = lat * DEG$6;
28054
+ return [wrapLon(lon - lon0) * DEG$6 * Math.cos(phi), phi];
27106
28055
  },
27107
28056
  rowInverse,
27108
28057
  inverse: rowToInverse(rowInverse, lon0)
@@ -27224,7 +28173,7 @@ function robinson(options) {
27224
28173
  const [i, f] = robinsonSegment(Math.abs(lat) / ROBINSON_STEP);
27225
28174
  const length = splineAt(splineSegment(ROBINSON_LENGTH, i), f);
27226
28175
  const height = splineAt(splineSegment(ROBINSON_HEIGHT, i), f);
27227
- return [ROBINSON_KX * length * wrapLon(lon - lon0) * DEG$5, ROBINSON_KY * height * Math.sign(lat)];
28176
+ return [ROBINSON_KX * length * wrapLon(lon - lon0) * DEG$6, ROBINSON_KY * height * Math.sign(lat)];
27228
28177
  },
27229
28178
  rowInverse,
27230
28179
  inverse: rowToInverse(rowInverse, lon0)
@@ -27246,14 +28195,14 @@ function discExtent(r) {
27246
28195
  };
27247
28196
  }
27248
28197
  function azimuthalDirection(lon, lat, lon0, phi0) {
27249
- const dl = wrapLon(lon - lon0) * DEG$5;
27250
- const phi = lat * DEG$5;
28198
+ const dl = wrapLon(lon - lon0) * DEG$6;
28199
+ const phi = lat * DEG$6;
27251
28200
  return [Math.cos(phi) * Math.sin(dl), Math.cos(phi0) * Math.sin(phi) - Math.sin(phi0) * Math.cos(phi) * Math.cos(dl)];
27252
28201
  }
27253
28202
  function orthographic(options) {
27254
28203
  const lon0 = options.centerLon ?? 0;
27255
- const phi0 = (options.centerLat ?? 0) * DEG$5;
27256
- const clip = Math.min(options.clipAngle ?? 90, 90) * DEG$5;
28204
+ const phi0 = (options.centerLat ?? 0) * DEG$6;
28205
+ const clip = Math.min(options.clipAngle ?? 90, 90) * DEG$6;
27257
28206
  const cosClip = Math.cos(clip);
27258
28207
  const radius = Math.sin(clip);
27259
28208
  const sinPhi0 = Math.sin(phi0);
@@ -27266,8 +28215,8 @@ function orthographic(options) {
27266
28215
  azimuthal: true,
27267
28216
  extent: discExtent(radius),
27268
28217
  forward: (lon, lat) => {
27269
- const dl = wrapLon(lon - lon0) * DEG$5;
27270
- const phi = lat * DEG$5;
28218
+ const dl = wrapLon(lon - lon0) * DEG$6;
28219
+ const phi = lat * DEG$6;
27271
28220
  if (Math.sin(phi0) * Math.sin(phi) + Math.cos(phi0) * Math.cos(phi) * Math.cos(dl) < cosClip) return null;
27272
28221
  return azimuthalDirection(lon, lat, lon0, phi0);
27273
28222
  },
@@ -27281,8 +28230,8 @@ function orthographic(options) {
27281
28230
  var STEREOGRAPHIC_CLIP = 120;
27282
28231
  function stereographic(options) {
27283
28232
  const lon0 = options.centerLon ?? 0;
27284
- const phi0 = (options.centerLat ?? 90) * DEG$5;
27285
- const clip = Math.min(options.clipAngle ?? STEREOGRAPHIC_CLIP, 170) * DEG$5;
28233
+ const phi0 = (options.centerLat ?? 90) * DEG$6;
28234
+ const clip = Math.min(options.clipAngle ?? STEREOGRAPHIC_CLIP, 170) * DEG$6;
27286
28235
  const cosClip = Math.cos(clip);
27287
28236
  const radius = 2 * Math.tan(clip / 2);
27288
28237
  const sinPhi0 = Math.sin(phi0);
@@ -27295,8 +28244,8 @@ function stereographic(options) {
27295
28244
  azimuthal: true,
27296
28245
  extent: discExtent(radius),
27297
28246
  forward: (lon, lat) => {
27298
- const dl = wrapLon(lon - lon0) * DEG$5;
27299
- const phi = lat * DEG$5;
28247
+ const dl = wrapLon(lon - lon0) * DEG$6;
28248
+ const phi = lat * DEG$6;
27300
28249
  const cosC = Math.sin(phi0) * Math.sin(phi) + Math.cos(phi0) * Math.cos(phi) * Math.cos(dl);
27301
28250
  if (cosC < cosClip) return null;
27302
28251
  const k = 2 / (1 + cosC);
@@ -27329,30 +28278,30 @@ function createProjection(id, options = {}) {
27329
28278
 
27330
28279
  //#endregion
27331
28280
  //#region src/lib/flatmap/geometry.ts
27332
- var DEG$4 = Math.PI / 180;
28281
+ var DEG$5 = Math.PI / 180;
27333
28282
  var TAU = 2 * Math.PI;
27334
- var RIM_STEP = 2 * DEG$4;
28283
+ var RIM_STEP = 2 * DEG$5;
27335
28284
  var RAD$1 = 180 / Math.PI;
27336
28285
  var MAX_STEPS = 512;
27337
28286
  var DEFAULT_STEP_DEG = 2;
27338
28287
  function angularDistance(a, b) {
27339
- const φ1 = a.lat * DEG$4;
27340
- const φ2 = b.lat * DEG$4;
28288
+ const φ1 = a.lat * DEG$5;
28289
+ const φ2 = b.lat * DEG$5;
27341
28290
  const dφ = φ2 - φ1;
27342
- const dλ = wrapLon(b.lon - a.lon) * DEG$4;
28291
+ const dλ = wrapLon(b.lon - a.lon) * DEG$5;
27343
28292
  const h = Math.sin(dφ / 2) ** 2 + Math.cos(φ1) * Math.cos(φ2) * Math.sin(dλ / 2) ** 2;
27344
28293
  return 2 * Math.asin(Math.min(1, Math.sqrt(h))) * RAD$1;
27345
28294
  }
27346
28295
  function slerp(a, b, t) {
27347
- const d = angularDistance(a, b) * DEG$4;
28296
+ const d = angularDistance(a, b) * DEG$5;
27348
28297
  if (d < 1e-12) return a;
27349
28298
  const sinD = Math.sin(d);
27350
28299
  const ka = Math.sin((1 - t) * d) / sinD;
27351
28300
  const kb = Math.sin(t * d) / sinD;
27352
- const φ1 = a.lat * DEG$4;
27353
- const λ1 = a.lon * DEG$4;
27354
- const φ2 = b.lat * DEG$4;
27355
- const λ2 = (a.lon + wrapLon(b.lon - a.lon)) * DEG$4;
28301
+ const φ1 = a.lat * DEG$5;
28302
+ const λ1 = a.lon * DEG$5;
28303
+ const φ2 = b.lat * DEG$5;
28304
+ const λ2 = (a.lon + wrapLon(b.lon - a.lon)) * DEG$5;
27356
28305
  const x = ka * Math.cos(φ1) * Math.cos(λ1) + kb * Math.cos(φ2) * Math.cos(λ2);
27357
28306
  const y = ka * Math.cos(φ1) * Math.sin(λ1) + kb * Math.cos(φ2) * Math.sin(λ2);
27358
28307
  const z = ka * Math.sin(φ1) + kb * Math.sin(φ2);
@@ -27586,7 +28535,7 @@ function turnsLeft(loop) {
27586
28535
  for (let i = 0; i < loop.length; i++) {
27587
28536
  const a = loop[i];
27588
28537
  const b = loop[(i + 1) % loop.length];
27589
- sum += wrapLon(b.lon - a.lon) * DEG$4 * (2 + Math.sin(a.lat * DEG$4) + Math.sin(b.lat * DEG$4));
28538
+ sum += wrapLon(b.lon - a.lon) * DEG$5 * (2 + Math.sin(a.lat * DEG$5) + Math.sin(b.lat * DEG$5));
27590
28539
  }
27591
28540
  const area = -sum / 2;
27592
28541
  return area > 0 ? area < TAU : area < -TAU;
@@ -27622,8 +28571,8 @@ function boxRing(latMin, latMax, lonMin, lonSpan) {
27622
28571
  return ring;
27623
28572
  }
27624
28573
  function smallCircle(center, radiusDeg, steps = 128) {
27625
- const φ0 = center.lat * DEG$4;
27626
- const λ0 = center.lon * DEG$4;
28574
+ const φ0 = center.lat * DEG$5;
28575
+ const λ0 = center.lon * DEG$5;
27627
28576
  const c = [
27628
28577
  Math.cos(φ0) * Math.cos(λ0),
27629
28578
  Math.cos(φ0) * Math.sin(λ0),
@@ -27649,7 +28598,7 @@ function smallCircle(center, radiusDeg, steps = 128) {
27649
28598
  c[2] * e[0] - c[0] * e[2],
27650
28599
  c[0] * e[1] - c[1] * e[0]
27651
28600
  ];
27652
- const r = radiusDeg * DEG$4;
28601
+ const r = radiusDeg * DEG$5;
27653
28602
  const sinR = Math.sin(r);
27654
28603
  const cosR = Math.cos(r);
27655
28604
  const ring = [];
@@ -29379,8 +30328,202 @@ var Overlay = class {
29379
30328
  }
29380
30329
  };
29381
30330
 
30331
+ //#endregion
30332
+ //#region src/lib/flatmap/tile-plan.ts
30333
+ var DEG$4 = Math.PI / 180;
30334
+ var MAX_STRETCH = 1.25;
30335
+ var MAX_WINDOW_PIXELS = 8e6;
30336
+ var GRID = 17;
30337
+ var MARGIN = .15;
30338
+ var CAP_GAIN = .75;
30339
+ function chartFraction(chart, lon, lat) {
30340
+ if (chart === 0) return [(lon + 180) / 360, (90 - lat) / 180];
30341
+ const side = chart === 1 ? 1 : -1;
30342
+ const r = .5 * Math.tan((90 - side * lat) * DEG$4 / 2) / CAP_EDGE;
30343
+ return [.5 + r * Math.cos(lon * DEG$4), .5 - side * r * Math.sin(lon * DEG$4)];
30344
+ }
30345
+ function levelForStep(step, tileSize) {
30346
+ return Math.ceil(Math.log2(2 * Math.PI / (step * MAX_STRETCH * 2 * tileSize)));
30347
+ }
30348
+ function stepOfLevel(level, tileSize) {
30349
+ return 2 * Math.PI / (2 * tileSize * 2 ** level);
30350
+ }
30351
+ function finestLevelAt(pyramid, lon, lat) {
30352
+ const [across, fy] = chartFraction(0, lon, lat);
30353
+ const fx = across - Math.floor(across);
30354
+ let finest = pyramid.max_level;
30355
+ for (const patch of pyramid.patches ?? []) {
30356
+ if (patch.max_level <= finest) continue;
30357
+ const x = Math.floor(fx * chartColumns(0, patch.max_level));
30358
+ const y = Math.floor(fy * chartRows(0, patch.max_level));
30359
+ if (patchOfTile([patch], patch.max_level, x, y)) finest = patch.max_level;
30360
+ }
30361
+ return finest;
30362
+ }
30363
+ function extentOn(chart, frame, places, centre) {
30364
+ const [cx] = chartFraction(0, centre[0], centre[1]);
30365
+ const extent = {
30366
+ minX: Infinity,
30367
+ maxX: -Infinity,
30368
+ minY: Infinity,
30369
+ maxY: -Infinity
30370
+ };
30371
+ for (const [lon, lat] of places) {
30372
+ const [across, y] = chartFraction(chart, lon, lat);
30373
+ if (chart !== 0 && (across < 0 || across > 1 || y < 0 || y > 1)) return null;
30374
+ const x = chart === 0 ? across - Math.round(across - cx) : across;
30375
+ extent.minX = Math.min(extent.minX, x);
30376
+ extent.maxX = Math.max(extent.maxX, x);
30377
+ extent.minY = Math.min(extent.minY, y);
30378
+ extent.maxY = Math.max(extent.maxY, y);
30379
+ }
30380
+ const padX = (extent.maxX - extent.minX) / (GRID - 1);
30381
+ const padY = (extent.maxY - extent.minY) / (GRID - 1);
30382
+ extent.minX -= padX;
30383
+ extent.maxX += padX;
30384
+ extent.minY -= padY;
30385
+ extent.maxY += padY;
30386
+ if (chart === 0 && (frame.north || frame.south)) {
30387
+ extent.minX = cx - .5;
30388
+ extent.maxX = cx + .5;
30389
+ if (frame.north) extent.minY = 0;
30390
+ if (frame.south) extent.maxY = 1;
30391
+ }
30392
+ return extent;
30393
+ }
30394
+ function windowOver(chart, level, tileSize, extent, margin) {
30395
+ const chartWidth = chartColumns(chart, level) * tileSize;
30396
+ const chartHeight = chartRows(chart, level) * tileSize;
30397
+ const rect = (grow) => {
30398
+ const growX = (extent.maxX - extent.minX) * grow;
30399
+ const growY = (extent.maxY - extent.minY) * grow;
30400
+ let x0 = Math.floor((extent.minX - growX) * chartWidth);
30401
+ let x1 = Math.ceil((extent.maxX + growX) * chartWidth);
30402
+ if (chart !== 0) {
30403
+ x0 = Math.max(x0, 0);
30404
+ x1 = Math.min(x1, chartWidth);
30405
+ } else if (x1 - x0 > chartWidth) {
30406
+ x0 = Math.round((extent.minX + extent.maxX) / 2 * chartWidth - chartWidth / 2);
30407
+ x1 = x0 + chartWidth;
30408
+ }
30409
+ const y0 = Math.max(Math.floor((extent.minY - growY) * chartHeight), 0);
30410
+ const y1 = Math.min(Math.ceil((extent.maxY + growY) * chartHeight), chartHeight);
30411
+ return [
30412
+ x0,
30413
+ y0,
30414
+ x1,
30415
+ y1
30416
+ ];
30417
+ };
30418
+ const [x0, y0, x1, y1] = rect(margin);
30419
+ const inner = rect(0);
30420
+ return {
30421
+ chart,
30422
+ level,
30423
+ x0,
30424
+ y0,
30425
+ width: Math.max(x1 - x0, 1),
30426
+ height: Math.max(y1 - y0, 1),
30427
+ inner: [
30428
+ inner[0],
30429
+ inner[1],
30430
+ inner[2] - 1,
30431
+ inner[3] - 1
30432
+ ]
30433
+ };
30434
+ }
30435
+ function fitted(chart, level, tileSize, extent, margin) {
30436
+ for (const grow of [margin, 0]) {
30437
+ const spec = windowOver(chart, level, tileSize, extent, grow);
30438
+ if (spec.width * spec.height <= 8e6) return spec;
30439
+ }
30440
+ }
30441
+ function reaches(patch, extent) {
30442
+ const columns = chartColumns(0, patch.max_level);
30443
+ const rows = chartRows(0, patch.max_level);
30444
+ const south = (patch.y[1] + 1) / rows;
30445
+ if (patch.y[0] / rows > extent.maxY || south < extent.minY) return false;
30446
+ let west = patch.x[0] / columns;
30447
+ west -= Math.round(west - (extent.minX + extent.maxX) / 2);
30448
+ const east = west + (patch.x[1] + 1 - patch.x[0]) / columns;
30449
+ return west <= extent.maxX && east >= extent.minX;
30450
+ }
30451
+ function planWindows(pyramid, frame, firstLevel, moves = true) {
30452
+ const margin = moves ? MARGIN : 0;
30453
+ const centre = frame.place(.5, .5);
30454
+ if (!centre || !(frame.step > 0)) return [];
30455
+ const places = [];
30456
+ for (let j = 0; j < GRID; j++) for (let i = 0; i < GRID; i++) {
30457
+ const place = frame.place(i / (GRID - 1), j / (GRID - 1));
30458
+ if (place) places.push(place);
30459
+ }
30460
+ const onMap = extentOn(0, frame, places, centre);
30461
+ const wanted = levelForStep(frame.step, pyramid.tile_size);
30462
+ const specs = [];
30463
+ let finest = pyramid.max_level;
30464
+ for (const patch of pyramid.patches ?? []) if (patch.max_level > finest && reaches(patch, onMap)) finest = patch.max_level;
30465
+ for (let level = Math.min(wanted, finest); level > pyramid.max_level; level--) {
30466
+ const spec = fitted(0, level, pyramid.tile_size, onMap, margin);
30467
+ if (!spec) continue;
30468
+ specs.push(spec);
30469
+ break;
30470
+ }
30471
+ const cap = centre[1] >= 0 ? 1 : 2;
30472
+ const onCap = extentOn(cap, frame, places, centre);
30473
+ for (let level = Math.min(wanted, pyramid.max_level); level >= firstLevel; level--) {
30474
+ const map = fitted(0, level, pyramid.tile_size, onMap, margin);
30475
+ const polar = onCap && level >= 1 ? fitted(cap, level, pyramid.tile_size, onCap, margin) : void 0;
30476
+ const mapPixels = map ? map.width * map.height : Infinity;
30477
+ const spec = polar && polar.width * polar.height <= mapPixels * CAP_GAIN ? polar : map ?? polar;
30478
+ if (!spec) continue;
30479
+ specs.push(spec);
30480
+ break;
30481
+ }
30482
+ return specs;
30483
+ }
30484
+ function windowTiles(spec, pyramid) {
30485
+ const size = pyramid.tile_size;
30486
+ const columns = chartColumns(spec.chart, spec.level);
30487
+ const rows = chartRows(spec.chart, spec.level);
30488
+ const tiles = [];
30489
+ const lastY = Math.min(Math.floor((spec.y0 + spec.height - 1) / size), rows - 1);
30490
+ const lastX = Math.floor((spec.x0 + spec.width - 1) / size);
30491
+ for (let ty = Math.max(Math.floor(spec.y0 / size), 0); ty <= lastY; ty++) for (let tx = Math.floor(spec.x0 / size); tx <= lastX; tx++) {
30492
+ if (spec.chart !== 0 && (tx < 0 || tx >= columns)) continue;
30493
+ const x = (tx % columns + columns) % columns;
30494
+ const patch = spec.level > pyramid.max_level ? patchOfTile(pyramid.patches, spec.level, x, ty) : void 0;
30495
+ if (spec.level > pyramid.max_level && !patch) continue;
30496
+ tiles.push({
30497
+ cap: CHART_FOLDER[spec.chart],
30498
+ level: spec.level,
30499
+ x,
30500
+ y: ty,
30501
+ dx: tx * size - spec.x0,
30502
+ dy: ty * size - spec.y0,
30503
+ patch
30504
+ });
30505
+ }
30506
+ const midX = spec.width / 2 - size / 2;
30507
+ const midY = spec.height / 2 - size / 2;
30508
+ const off = (tile) => Math.hypot(tile.dx - midX, tile.dy - midY);
30509
+ return tiles.sort((a, b) => off(a) - off(b));
30510
+ }
30511
+ function covers(held, spec, tileSize) {
30512
+ if (held.chart !== spec.chart || held.level !== spec.level) return false;
30513
+ let turn = 0;
30514
+ if (spec.chart === 0) {
30515
+ const chartWidth = chartColumns(0, spec.level) * tileSize;
30516
+ turn = chartWidth * Math.round((spec.inner[0] - held.x0) / chartWidth);
30517
+ }
30518
+ return held.x0 + turn <= spec.inner[0] && held.x0 + turn + held.width > spec.inner[2] && held.y0 <= spec.inner[1] && held.y0 + held.height > spec.inner[3];
30519
+ }
30520
+
29382
30521
  //#endregion
29383
30522
  //#region src/lib/flatmap/raster.ts
30523
+ var CAP_GRID = 8;
30524
+ function chartWidth(win) {
30525
+ return chartColumns(win.chart, win.level) * win.tileSize;
30526
+ }
29384
30527
  function sourceSize(source) {
29385
30528
  const width = "width" in source ? source.width : source.codedWidth;
29386
30529
  const height = "height" in source ? source.height : source.codedHeight;
@@ -29413,9 +30556,12 @@ function imageToRaster(source, maxWidth) {
29413
30556
  var InverseLookup = class {
29414
30557
  width;
29415
30558
  height;
30559
+ refU;
30560
+ refV;
29416
30561
  u;
29417
30562
  v;
29418
30563
  inside;
30564
+ caps = /* @__PURE__ */ new Map();
29419
30565
  constructor(viewport, width, height) {
29420
30566
  this.width = width;
29421
30567
  this.height = height;
@@ -29424,6 +30570,8 @@ var InverseLookup = class {
29424
30570
  this.v = new Float32Array(count);
29425
30571
  this.inside = new Uint8Array(count);
29426
30572
  const { projection } = viewport;
30573
+ const middle = projection.inverse(viewport.view.centerX, viewport.view.centerY);
30574
+ [this.refU, this.refV] = middle ? chartFraction(0, middle[0], middle[1]) : [.5, .5];
29427
30575
  const step = 1 / viewport.scale;
29428
30576
  const x0 = viewport.view.centerX - viewport.width / 2 * step;
29429
30577
  const y0 = viewport.view.centerY + viewport.height / 2 * step;
@@ -29437,8 +30585,8 @@ var InverseLookup = class {
29437
30585
  fillRow(projection, row, y, x0, step, viewport) {
29438
30586
  const line = projection.rowInverse(y);
29439
30587
  if (!line) return;
29440
- const { width } = this;
29441
- const v = (90 - line.lat) / 180;
30588
+ const { width, refU } = this;
30589
+ const v = (90 - line.lat) / 180 - this.refV;
29442
30590
  const { minX, maxX } = projection.extent;
29443
30591
  const worldWidth = maxX - minX;
29444
30592
  const repeating = viewport.repeatsHorizontally;
@@ -29447,9 +30595,9 @@ var InverseLookup = class {
29447
30595
  if (repeating) x = ((x - minX) % worldWidth + worldWidth) % worldWidth + minX;
29448
30596
  else if (x < minX || x > maxX) continue;
29449
30597
  if (x < -line.maxAbsX || x > line.maxAbsX) continue;
29450
- const turns = (x * line.lonPerX + projection.centerLon + 180) / 360;
30598
+ const turns = (x * line.lonPerX + projection.centerLon + 180) / 360 - refU;
29451
30599
  const i = row + px;
29452
- this.u[i] = turns - Math.floor(turns);
30600
+ this.u[i] = turns - Math.round(turns);
29453
30601
  this.v[i] = v;
29454
30602
  this.inside[i] = 1;
29455
30603
  }
@@ -29470,31 +30618,154 @@ var InverseLookup = class {
29470
30618
  const place = projection.inverse(x, y);
29471
30619
  if (!place) continue;
29472
30620
  const i = row + px;
29473
- this.u[i] = (place[0] + 180) / 360;
29474
- this.v[i] = (90 - place[1]) / 180;
30621
+ const turns = (place[0] + 180) / 360 - this.refU;
30622
+ this.u[i] = turns - Math.round(turns);
30623
+ this.v[i] = (90 - place[1]) / 180 - this.refV;
29475
30624
  this.inside[i] = 1;
29476
30625
  }
29477
30626
  }
30627
+ offsets(chart) {
30628
+ if (chart === 0) return {
30629
+ refX: this.refU,
30630
+ refY: this.refV,
30631
+ x: this.u,
30632
+ y: this.v
30633
+ };
30634
+ let cap = this.caps.get(chart);
30635
+ if (!cap) {
30636
+ cap = this.capOffsets(chart);
30637
+ this.caps.set(chart, cap);
30638
+ }
30639
+ return cap;
30640
+ }
30641
+ capOffsets(chart) {
30642
+ const { u, v, inside, refU, refV, width, height } = this;
30643
+ const side = chart === 1 ? 1 : -1;
30644
+ const [refX, refY] = chartFraction(chart, refU * 360 - 180, 90 - refV * 180);
30645
+ const x = new Float32Array(u.length);
30646
+ const y = new Float32Array(u.length);
30647
+ const exact = (i) => {
30648
+ const fromPole = side > 0 ? refV + v[i] : 1 - refV - v[i];
30649
+ const radius = .5 * Math.tan(fromPole * Math.PI / 2) / CAP_EDGE;
30650
+ const lon = (refU + u[i]) * 2 * Math.PI - Math.PI;
30651
+ x[i] = .5 + radius * Math.cos(lon) - refX;
30652
+ y[i] = .5 - side * radius * Math.sin(lon) - refY;
30653
+ };
30654
+ const columns = gridLines(width);
30655
+ const rows = gridLines(height);
30656
+ if (columns.length < 2 || rows.length < 2) {
30657
+ for (let i = 0; i < u.length; i++) if (inside[i]) exact(i);
30658
+ return {
30659
+ refX,
30660
+ refY,
30661
+ x,
30662
+ y
30663
+ };
30664
+ }
30665
+ for (const py of rows) for (const px of columns) if (inside[py * width + px]) exact(py * width + px);
30666
+ for (let r = 0; r < rows.length - 1; r++) {
30667
+ const top = rows[r];
30668
+ const bottom = rows[r + 1];
30669
+ const lastRow = r === rows.length - 2 ? bottom : bottom - 1;
30670
+ for (let c = 0; c < columns.length - 1; c++) {
30671
+ const left = columns[c];
30672
+ const right = columns[c + 1];
30673
+ const lastColumn = c === columns.length - 2 ? right : right - 1;
30674
+ const nw = top * width + left;
30675
+ const ne = top * width + right;
30676
+ const sw = bottom * width + left;
30677
+ const se = bottom * width + right;
30678
+ if (!(inside[nw] && inside[ne] && inside[sw] && inside[se])) {
30679
+ for (let py = top; py <= lastRow; py++) for (let i = py * width + left; i <= py * width + lastColumn; i++) if (inside[i]) exact(i);
30680
+ continue;
30681
+ }
30682
+ for (let py = top; py <= lastRow; py++) {
30683
+ const fy = (py - top) / (bottom - top);
30684
+ let atX = x[nw] + fy * (x[sw] - x[nw]);
30685
+ let atY = y[nw] + fy * (y[sw] - y[nw]);
30686
+ const stepX = (x[ne] + fy * (x[se] - x[ne]) - atX) / (right - left);
30687
+ const stepY = (y[ne] + fy * (y[se] - y[ne]) - atY) / (right - left);
30688
+ const skip = py === top || py === bottom;
30689
+ for (let px = left; px <= lastColumn; px++) {
30690
+ if (!(skip && (px === left || px === right))) {
30691
+ x[py * width + px] = atX;
30692
+ y[py * width + px] = atY;
30693
+ }
30694
+ atX += stepX;
30695
+ atY += stepY;
30696
+ }
30697
+ }
30698
+ }
30699
+ }
30700
+ return {
30701
+ refX,
30702
+ refY,
30703
+ x,
30704
+ y
30705
+ };
30706
+ }
29478
30707
  };
30708
+ function gridLines(length) {
30709
+ const lines = [];
30710
+ for (let at = 0; at < length - 1; at += CAP_GRID) lines.push(at);
30711
+ lines.push(length - 1);
30712
+ return lines;
30713
+ }
29479
30714
  function compositeLayers(lookup, layers, target) {
29480
30715
  const out = target.data;
29481
30716
  out.fill(0);
29482
- const { width, height, u, v, inside } = lookup;
30717
+ const { width, height, u, v, inside, refU, refV } = lookup;
29483
30718
  const count = width * height;
29484
30719
  for (const layer of layers) {
29485
30720
  const { image, opacity, blend } = layer;
29486
30721
  if (opacity <= 0) continue;
29487
- const src = image.data;
30722
+ const base = image.data;
29488
30723
  const sw = image.width;
29489
30724
  const sh = image.height;
29490
30725
  const add = blend === "add";
30726
+ const windows = (layer.detail ?? []).map((win) => {
30727
+ const offsets = lookup.offsets(win.chart);
30728
+ const across = chartWidth(win);
30729
+ const down = win.chart === 0 ? across / 2 : across;
30730
+ let left = offsets.refX * across - win.x0;
30731
+ if (win.chart === 0) left -= across * Math.round((left - win.width / 2) / across);
30732
+ return {
30733
+ xs: offsets.x,
30734
+ ys: offsets.y,
30735
+ left,
30736
+ top: offsets.refY * down - win.y0,
30737
+ across,
30738
+ down,
30739
+ width: win.width,
30740
+ height: win.height,
30741
+ data: win.data
30742
+ };
30743
+ });
29491
30744
  for (let i = 0; i < count; i++) {
29492
30745
  if (!inside[i]) continue;
29493
- let sx = Math.floor(u[i] * sw);
29494
- let sy = Math.floor(v[i] * sh);
29495
- sx = sx < 0 ? sx + sw : sx >= sw ? sx - sw : sx;
29496
- sy = sy < 0 ? 0 : sy >= sh ? sh - 1 : sy;
29497
- const s = (sy * sw + sx) * 4;
30746
+ let src = base;
30747
+ let s = -1;
30748
+ for (let k = 0; k < windows.length; k++) {
30749
+ const win = windows[k];
30750
+ const tx = win.left + win.xs[i] * win.across;
30751
+ if (!(tx >= 0 && tx < win.width)) continue;
30752
+ const ty = win.top + win.ys[i] * win.down;
30753
+ if (!(ty >= 0 && ty < win.height)) continue;
30754
+ const t = ((ty | 0) * win.width + (tx | 0)) * 4;
30755
+ if (win.data[t + 3] === 0) continue;
30756
+ src = win.data;
30757
+ s = t;
30758
+ break;
30759
+ }
30760
+ if (s < 0) {
30761
+ let across = refU + u[i];
30762
+ across -= Math.floor(across);
30763
+ let sx = Math.floor(across * sw);
30764
+ let sy = Math.floor((refV + v[i]) * sh);
30765
+ sx = sx < 0 ? sx + sw : sx >= sw ? sx - sw : sx;
30766
+ sy = sy < 0 ? 0 : sy >= sh ? sh - 1 : sy;
30767
+ s = (sy * sw + sx) * 4;
30768
+ }
29498
30769
  const alpha = src[s + 3] / 255 * opacity;
29499
30770
  if (alpha <= 0) continue;
29500
30771
  const d = i * 4;
@@ -29528,22 +30799,56 @@ function drawEquirect(ctx, viewport, layers) {
29528
30799
  const [right, bottom] = viewport.toScreen(maxX - meridian, minY);
29529
30800
  const from = viewport.repeatsHorizontally ? 0 : viewport.toScreen(minX, maxY)[0];
29530
30801
  const to = viewport.repeatsHorizontally ? ctx.canvas.width : viewport.toScreen(maxX, maxY)[0];
30802
+ const paint = (target, layer, size) => {
30803
+ const pattern = target.createPattern(layer.image, "repeat-x");
30804
+ if (!pattern) return;
30805
+ pattern.setTransform(new DOMMatrix().translateSelf(left, top).scaleSelf((right - left) / size[0], (bottom - top) / size[1]));
30806
+ target.save();
30807
+ target.imageSmoothingEnabled = true;
30808
+ target.imageSmoothingQuality = "high";
30809
+ target.fillStyle = pattern;
30810
+ target.fillRect(from, top, to - from, bottom - top);
30811
+ const detail = layer.detail ?? [];
30812
+ if (detail.length) {
30813
+ target.beginPath();
30814
+ target.rect(from, top, to - from, bottom - top);
30815
+ target.clip();
30816
+ }
30817
+ for (let k = detail.length - 1; k >= 0; k--) {
30818
+ const win = detail[k];
30819
+ const scale = (right - left) / chartWidth(win);
30820
+ let x = left + win.x0 * scale;
30821
+ const middle = x + win.width * scale / 2;
30822
+ x += (right - left) * Math.round((target.canvas.width / 2 - middle) / (right - left));
30823
+ target.drawImage(win.canvas, x, top + win.y0 * scale, win.width * scale, win.height * scale);
30824
+ }
30825
+ target.restore();
30826
+ };
29531
30827
  ctx.save();
29532
- ctx.imageSmoothingEnabled = true;
29533
- ctx.imageSmoothingQuality = "high";
29534
30828
  for (const layer of layers) {
29535
30829
  if (layer.opacity <= 0) continue;
29536
30830
  const size = sourceSize(layer.image);
29537
- const pattern = size && ctx.createPattern(layer.image, "repeat-x");
29538
- if (!pattern) continue;
29539
- pattern.setTransform(new DOMMatrix().translateSelf(left, top).scaleSelf((right - left) / size[0], (bottom - top) / size[1]));
30831
+ if (!size) continue;
29540
30832
  ctx.globalAlpha = layer.opacity;
29541
30833
  ctx.globalCompositeOperation = layer.blend === "add" ? "lighter" : "source-over";
29542
- ctx.fillStyle = pattern;
29543
- ctx.fillRect(from, top, to - from, bottom - top);
30834
+ const scratch = layer.detail?.length && (layer.opacity < 1 || layer.blend === "add") ? scratchContext(ctx.canvas.width, ctx.canvas.height) : null;
30835
+ if (scratch) {
30836
+ paint(scratch, layer, size);
30837
+ ctx.drawImage(scratch.canvas, 0, 0);
30838
+ } else paint(ctx, layer, size);
29544
30839
  }
29545
30840
  ctx.restore();
29546
30841
  }
30842
+ var scratch$1 = null;
30843
+ function scratchContext(width, height) {
30844
+ scratch$1 ??= document.createElement("canvas").getContext("2d");
30845
+ if (!scratch$1) return null;
30846
+ if (scratch$1.canvas.width !== width || scratch$1.canvas.height !== height) {
30847
+ scratch$1.canvas.width = width;
30848
+ scratch$1.canvas.height = height;
30849
+ } else scratch$1.clearRect(0, 0, width, height);
30850
+ return scratch$1;
30851
+ }
29547
30852
 
29548
30853
  //#endregion
29549
30854
  //#region src/lib/flatmap/sources.ts
@@ -29682,10 +30987,14 @@ var Store = class {
29682
30987
  this.evict();
29683
30988
  return value;
29684
30989
  }
29685
- release(key) {
30990
+ release(key, keep = true) {
29686
30991
  const entry = this.entries.get(key);
29687
30992
  if (!entry) return;
29688
30993
  entry.claims = Math.max(0, entry.claims - 1);
30994
+ if (!keep && !entry.claims) {
30995
+ this.entries.delete(key);
30996
+ this.free(entry.value);
30997
+ }
29689
30998
  this.evict();
29690
30999
  }
29691
31000
  evict() {
@@ -29723,7 +31032,8 @@ async function acquireBitmap(url, decodeWidth) {
29723
31032
  if (!work) {
29724
31033
  work = decode(url, decodeWidth);
29725
31034
  pending.set(key, work);
29726
- work.finally(() => pending.delete(key));
31035
+ const done = () => pending.delete(key);
31036
+ work.then(done, done);
29727
31037
  }
29728
31038
  const bitmap = await work;
29729
31039
  if (!bitmap) return null;
@@ -29746,8 +31056,8 @@ async function decode(url, decodeWidth) {
29746
31056
  return createImageBitmap(blob);
29747
31057
  }
29748
31058
  }
29749
- function releaseBitmap(key) {
29750
- bitmaps.release(key);
31059
+ function releaseBitmap(key, keep = true) {
31060
+ bitmaps.release(key, keep);
29751
31061
  }
29752
31062
  function acquireRaster(url, workingWidth, source) {
29753
31063
  const key = textureKey(url, workingWidth);
@@ -29767,6 +31077,129 @@ function releaseRaster(key) {
29767
31077
  rasters.release(key);
29768
31078
  }
29769
31079
 
31080
+ //#endregion
31081
+ //#region src/lib/flatmap/tile-windows.ts
31082
+ var MAX_FAILURES = 4;
31083
+ var MAX_STALE = 2;
31084
+ var scratch = null;
31085
+ function copyTile(win, tile, dx, dy) {
31086
+ const x0 = Math.max(dx, 0);
31087
+ const y0 = Math.max(dy, 0);
31088
+ const width = Math.min(dx + tile.width, win.width) - x0;
31089
+ const height = Math.min(dy + tile.height, win.height) - y0;
31090
+ if (width <= 0 || height <= 0) return;
31091
+ scratch ??= document.createElement("canvas").getContext("2d", { willReadFrequently: true });
31092
+ if (!scratch) return;
31093
+ if (scratch.canvas.width < tile.width || scratch.canvas.height < tile.height) {
31094
+ scratch.canvas.width = tile.width;
31095
+ scratch.canvas.height = tile.height;
31096
+ }
31097
+ scratch.globalCompositeOperation = "copy";
31098
+ scratch.drawImage(tile, x0 - dx, y0 - dy, width, height, 0, 0, width, height);
31099
+ const pixels = scratch.getImageData(0, 0, width, height).data;
31100
+ const row = width * 4;
31101
+ for (let y = 0; y < height; y++) win.data.set(pixels.subarray(y * row, (y + 1) * row), ((y0 + y) * win.width + x0) * 4);
31102
+ }
31103
+ var LayerTiles = class {
31104
+ windows = [];
31105
+ kind = "pixels";
31106
+ failures = 0;
31107
+ failed = /* @__PURE__ */ new Set();
31108
+ dead = false;
31109
+ constructor(block, changed, moves) {
31110
+ this.block = block;
31111
+ this.changed = changed;
31112
+ this.moves = moves;
31113
+ }
31114
+ plan(specs, kind, frame) {
31115
+ if (kind !== this.kind) {
31116
+ this.clear();
31117
+ this.kind = kind;
31118
+ }
31119
+ for (const win of this.windows) win.wanted = false;
31120
+ for (const spec of this.dead ? [] : specs) {
31121
+ const held = this.windows.find((win) => win.frame === frame && covers(win, spec, this.block.tile_size));
31122
+ if (held) held.wanted = true;
31123
+ else this.windows.push(this.open(spec, frame));
31124
+ }
31125
+ this.prune();
31126
+ }
31127
+ rasters() {
31128
+ return this.ordered().filter((win) => !!win.data);
31129
+ }
31130
+ canvases() {
31131
+ return this.ordered().flatMap((win) => win.ctx ? [{
31132
+ ...win,
31133
+ canvas: win.ctx.canvas
31134
+ }] : []);
31135
+ }
31136
+ clear() {
31137
+ for (const win of this.windows) this.drop(win);
31138
+ this.windows = [];
31139
+ }
31140
+ drop(win) {
31141
+ win.dropped = true;
31142
+ win.data = void 0;
31143
+ if (win.ctx) win.ctx.canvas.width = 0;
31144
+ }
31145
+ ordered() {
31146
+ return [...this.windows].sort((a, b) => b.level - a.level || Number(b.wanted) - Number(a.wanted));
31147
+ }
31148
+ open(spec, frame) {
31149
+ const win = {
31150
+ ...spec,
31151
+ tileSize: this.block.tile_size,
31152
+ frame,
31153
+ pending: 0,
31154
+ wanted: true,
31155
+ dropped: false
31156
+ };
31157
+ if (this.kind === "canvas") {
31158
+ const canvas = document.createElement("canvas");
31159
+ canvas.width = spec.width;
31160
+ canvas.height = spec.height;
31161
+ win.ctx = canvas.getContext("2d") ?? void 0;
31162
+ } else win.data = new Uint8ClampedArray(spec.width * spec.height * 4);
31163
+ for (const tile of windowTiles(spec, this.block)) {
31164
+ const url = tileUrl({
31165
+ id: this.block.id,
31166
+ version: tile.patch?.version ?? this.block.version
31167
+ }, tile.cap, tile.level, tile.x, tile.y, frame);
31168
+ if (this.failed.has(url)) continue;
31169
+ win.pending++;
31170
+ this.load(win, url, tile.dx, tile.dy);
31171
+ }
31172
+ return win;
31173
+ }
31174
+ async load(win, url, dx, dy) {
31175
+ try {
31176
+ const held = await acquireBitmap(url, null);
31177
+ if (held) try {
31178
+ if (win.ctx && !win.dropped) win.ctx.drawImage(held.bitmap, dx, dy);
31179
+ else if (!win.dropped) copyTile(win, held.bitmap, dx, dy);
31180
+ } finally {
31181
+ releaseBitmap(held.key, this.moves());
31182
+ }
31183
+ this.failures = 0;
31184
+ } catch {
31185
+ this.failed.add(url);
31186
+ if (++this.failures >= MAX_FAILURES) this.dead = true;
31187
+ }
31188
+ win.pending--;
31189
+ if (this.dead) this.clear();
31190
+ else if (win.dropped) return;
31191
+ else if (win.pending === 0) this.prune();
31192
+ this.changed();
31193
+ }
31194
+ prune() {
31195
+ const loading = this.windows.some((win) => win.wanted && win.pending > 0);
31196
+ const stale = this.windows.filter((win) => !win.wanted);
31197
+ const kept = loading ? stale.slice(-MAX_STALE) : [];
31198
+ for (const win of stale) if (!kept.includes(win)) this.drop(win);
31199
+ this.windows = this.windows.filter((win) => !win.dropped);
31200
+ }
31201
+ };
31202
+
29770
31203
  //#endregion
29771
31204
  //#region src/lib/flatmap/view.ts
29772
31205
  function clampToBand(lon, lat, limits) {
@@ -29865,6 +31298,9 @@ var MOVING_BUDGET_MS = 11;
29865
31298
  var MIN_INTERACTION_SCALE = .4;
29866
31299
  var WORTH_COARSENING = .8;
29867
31300
  var SETTLE_MS = 140;
31301
+ var TILED_BASE_TIER = "medium";
31302
+ var MOVING_PLAN_MS = 250;
31303
+ var MAX_TILE_STRETCH = 2;
29868
31304
  var DEFAULT_BODY = "naif-399";
29869
31305
  var RASTER_LAYERS = [
29870
31306
  {
@@ -29911,6 +31347,7 @@ var FlatMap = class {
29911
31347
  projectionOptions;
29912
31348
  view;
29913
31349
  limits;
31350
+ ownMaxZoom;
29914
31351
  limitsArmed = false;
29915
31352
  jd;
29916
31353
  cooperative = null;
@@ -29919,6 +31356,8 @@ var FlatMap = class {
29919
31356
  rasters = /* @__PURE__ */ new Map();
29920
31357
  pending = /* @__PURE__ */ new Set();
29921
31358
  missing = /* @__PURE__ */ new Set();
31359
+ tiles = /* @__PURE__ */ new Map();
31360
+ plannedAt = 0;
29922
31361
  generation = 0;
29923
31362
  controls = null;
29924
31363
  attribution = null;
@@ -29952,6 +31391,7 @@ var FlatMap = class {
29952
31391
  maxZoom: 16,
29953
31392
  ...options.limits
29954
31393
  };
31394
+ this.ownMaxZoom = options.limits?.maxZoom === void 0;
29955
31395
  this.view = {
29956
31396
  zoom: options.zoom ?? 1,
29957
31397
  centerX: 0,
@@ -30008,6 +31448,7 @@ var FlatMap = class {
30008
31448
  for (const layer of this.layerList) if (!isRaster(layer)) layer.dispose?.();
30009
31449
  this.overlay.clear();
30010
31450
  this.dropTextures();
31451
+ this.dropTiles();
30011
31452
  this.root.remove();
30012
31453
  this.container = null;
30013
31454
  }
@@ -30035,16 +31476,18 @@ var FlatMap = class {
30035
31476
  release() {
30036
31477
  this.generation++;
30037
31478
  this.dropTextures();
31479
+ this.dropTiles();
30038
31480
  this.missing.clear();
30039
31481
  for (const layer of this.layerList) if (!isRaster(layer) && !this.customLayers.includes(layer)) layer.dispose?.();
30040
31482
  }
30041
31483
  buildLayers() {
30042
31484
  const { messages } = host();
30043
31485
  const layers = [];
31486
+ this.dropTiles();
30044
31487
  for (const spec of RASTER_LAYERS) {
30045
31488
  const bundle = this.sources[spec.kind];
30046
31489
  if (!bundle) continue;
30047
- layers.push({
31490
+ const layer = {
30048
31491
  kind: "raster",
30049
31492
  id: spec.kind,
30050
31493
  label: spec.label(messages),
@@ -30054,7 +31497,17 @@ var FlatMap = class {
30054
31497
  credit: creditOf(bundle),
30055
31498
  tiers: bundle.tiers,
30056
31499
  url: (jd, tier) => bundleUrl(bundle, tier, jd)
30057
- });
31500
+ };
31501
+ layers.push(layer);
31502
+ if (!bundle.tiles) continue;
31503
+ const tiles = new LayerTiles(bundle.tiles, () => {
31504
+ if (tiles.dead) {
31505
+ this.loadVisible();
31506
+ this.emit("layerschange", this.layers);
31507
+ }
31508
+ this.render();
31509
+ }, () => this.moves());
31510
+ this.tiles.set(layer, tiles);
30058
31511
  }
30059
31512
  layers.push(graticuleLayer(messages.layer_graticule()));
30060
31513
  layers.push(nomenclatureLayer(this.bodyId, messages.layer_nomenclature()));
@@ -30078,11 +31531,23 @@ var FlatMap = class {
30078
31531
  get credits() {
30079
31532
  const seen = /* @__PURE__ */ new Set();
30080
31533
  const out = [];
31534
+ const add = (credit) => {
31535
+ if (seen.has(credit.organisation)) return;
31536
+ seen.add(credit.organisation);
31537
+ out.push(credit);
31538
+ };
30081
31539
  for (const layer of this.layerList) {
30082
31540
  if (!layer.visible || !layer.credit) continue;
30083
- if (seen.has(layer.credit.organisation)) continue;
30084
- seen.add(layer.credit.organisation);
30085
- out.push(layer.credit);
31541
+ add(layer.credit);
31542
+ const tiles = this.tilesOf(layer);
31543
+ if (!tiles) continue;
31544
+ const map = {
31545
+ ...layer.credit,
31546
+ tiles: tiles.block
31547
+ };
31548
+ const pyramid = tileCredit(map);
31549
+ if (pyramid) add(pyramid);
31550
+ for (const patch of patchCredits(map)) add(patch);
30086
31551
  }
30087
31552
  return out;
30088
31553
  }
@@ -30201,11 +31666,15 @@ var FlatMap = class {
30201
31666
  maxZoom: 16,
30202
31667
  ...limits
30203
31668
  };
31669
+ this.ownMaxZoom = limits.maxZoom === void 0;
30204
31670
  this.render();
30205
31671
  return this;
30206
31672
  }
30207
31673
  getLimits() {
30208
- return { ...this.limits };
31674
+ return this.ownMaxZoom ? {
31675
+ ...this.limits,
31676
+ maxZoom: this.pictureMaxZoom()
31677
+ } : { ...this.limits };
30209
31678
  }
30210
31679
  setTime(jd) {
30211
31680
  this.jd = jd;
@@ -30285,8 +31754,10 @@ var FlatMap = class {
30285
31754
  if (!set) return;
30286
31755
  for (const listener of [...set]) listener(...args);
30287
31756
  }
30288
- tier(tiers) {
30289
- return bestTier(tiers, lowerTier(TIER_BY_ZOOM.find(([zoom]) => this.view.zoom >= zoom)?.[1] ?? "low", tierForWidth(this.usableSourceWidth())));
31757
+ tier(layer) {
31758
+ let wanted = TIER_BY_ZOOM.find(([zoom]) => this.view.zoom >= zoom)?.[1] ?? "low";
31759
+ if (this.tilesOf(layer)) wanted = lowerTier(wanted, TILED_BASE_TIER);
31760
+ return bestTier(layer.tiers, lowerTier(wanted, tierForWidth(this.usableSourceWidth())));
30290
31761
  }
30291
31762
  usableSourceWidth() {
30292
31763
  return this.projectionId === "equirectangular" ? this.deviceWidth() * this.view.zoom : this.workingWidth();
@@ -30304,7 +31775,7 @@ var FlatMap = class {
30304
31775
  }
30305
31776
  picturesInUse() {
30306
31777
  return this.layerList.filter((l) => isRaster(l) && l.visible).map((layer) => {
30307
- const tier = this.tier(layer.tiers);
31778
+ const tier = this.tier(layer);
30308
31779
  return {
30309
31780
  url: layer.url(this.jd, tier),
30310
31781
  tier
@@ -30371,10 +31842,98 @@ var FlatMap = class {
30371
31842
  this.bitmaps.clear();
30372
31843
  this.rasters.clear();
30373
31844
  }
31845
+ tilesOf(layer) {
31846
+ const tiles = this.tiles.get(layer);
31847
+ if (!tiles || !isRaster(layer) || tiles.dead || tilesBase() === "") return void 0;
31848
+ if (!textureAllowed(tiles.block.distribution)) return void 0;
31849
+ return Math.max(tiles.block.max_level, ...(tiles.block.patches ?? []).map((patch) => patch.max_level)) >= this.firstTileLevel(layer, tiles) ? tiles : void 0;
31850
+ }
31851
+ firstTileLevel(layer, tiles) {
31852
+ const whole = TIER_WIDTH[bestTier(layer.tiers, TILED_BASE_TIER)] ?? MAX_WORKING_WIDTH;
31853
+ const width = this.projectionId === "equirectangular" ? whole : Math.min(whole, MAX_WORKING_WIDTH);
31854
+ return Math.floor(Math.log2(width / (2 * tiles.block.tile_size))) + 1;
31855
+ }
31856
+ frameSample(viewport, density) {
31857
+ const { width, height, projection } = viewport;
31858
+ const at = (px, py) => viewport.unproject(px, py);
31859
+ const middle = at(width / 2, height / 2);
31860
+ const beside = at(width / 2 + 1, height / 2);
31861
+ const below = at(width / 2, height / 2 + 1);
31862
+ if (!middle || !beside || !below) return void 0;
31863
+ const from = {
31864
+ lon: middle[0],
31865
+ lat: middle[1]
31866
+ };
31867
+ const step = Math.min(angularDistance(from, {
31868
+ lon: beside[0],
31869
+ lat: beside[1]
31870
+ }), angularDistance(from, {
31871
+ lon: below[0],
31872
+ lat: below[1]
31873
+ }));
31874
+ const shows = (lat) => {
31875
+ const pole = projection.azimuthal ? viewport.project(0, lat) : null;
31876
+ return !!pole && pole[0] >= 0 && pole[0] <= width && pole[1] >= 0 && pole[1] <= height;
31877
+ };
31878
+ return {
31879
+ place: (fx, fy) => at(fx * width, fy * height),
31880
+ step: step * Math.PI / 180 / density,
31881
+ north: shows(90),
31882
+ south: shows(-90)
31883
+ };
31884
+ }
31885
+ planTiles(viewport, density) {
31886
+ const kind = this.projectionId === "equirectangular" ? "canvas" : "pixels";
31887
+ let frame;
31888
+ for (const [layer, tiles] of this.tiles) {
31889
+ const drawn = isRaster(layer) && layer.visible && layer.opacity > 0 && this.tilesOf(layer);
31890
+ frame ??= drawn ? this.frameSample(viewport, density) : void 0;
31891
+ const first = drawn && frame ? this.firstTileLevel(layer, tiles) : 0;
31892
+ const specs = drawn && frame ? planWindows(tiles.block, frame, first, this.moves()) : [];
31893
+ tiles.plan(specs, kind, textureFrameForJd(this.jd, tiles.block.frames));
31894
+ }
31895
+ }
31896
+ moves() {
31897
+ return this.dragPan.isEnabled() || this.scrollZoom.isEnabled();
31898
+ }
31899
+ dropTiles() {
31900
+ for (const tiles of this.tiles.values()) tiles.clear();
31901
+ this.tiles.clear();
31902
+ }
31903
+ pictureMaxZoom() {
31904
+ let zoom = 16;
31905
+ const { clientWidth, clientHeight } = this.root;
31906
+ if (!clientWidth || !clientHeight) return zoom;
31907
+ const viewport = new Viewport(this.projection, clientWidth, clientHeight, this.view);
31908
+ const frame = this.frameSample(viewport, 1);
31909
+ const middle = frame?.place(.5, .5);
31910
+ if (!frame || !middle) return zoom;
31911
+ for (const layer of this.layerList) {
31912
+ const tiles = layer.visible ? this.tilesOf(layer) : void 0;
31913
+ if (!tiles) continue;
31914
+ const finest = stepOfLevel(finestLevelAt(tiles.block, middle[0], middle[1]), tiles.block.tile_size) / MAX_TILE_STRETCH;
31915
+ zoom = Math.max(zoom, this.view.zoom * frame.step / finest);
31916
+ }
31917
+ return zoom;
31918
+ }
31919
+ heldLimits() {
31920
+ if (!this.ownMaxZoom) return this.limits;
31921
+ return this.layerList.some((layer) => layer.visible && this.tilesOf(layer)) ? {
31922
+ ...this.limits,
31923
+ maxZoom: void 0
31924
+ } : this.limits;
31925
+ }
31926
+ tileStepDeg(lon, lat) {
31927
+ const surface = this.layerList.find((layer) => layer.id === "surface");
31928
+ const tiles = surface && this.tilesOf(surface);
31929
+ if (!tiles) return null;
31930
+ return stepOfLevel(finestLevelAt(tiles.block, lon, lat), tiles.block.tile_size) * 180 / Math.PI;
31931
+ }
30374
31932
  viewport(width, height) {
30375
31933
  const w = width ?? this.root.clientWidth ?? 1;
30376
31934
  const h = height ?? this.root.clientHeight ?? 1;
30377
- this.view = clampView(this.view, this.projection, w, h, this.limitsArmed ? this.limits : {});
31935
+ const limits = this.limitsArmed ? this.heldLimits() : {};
31936
+ this.view = clampView(this.view, this.projection, w, h, limits);
30378
31937
  return new Viewport(this.projection, Math.max(1, w), Math.max(1, h), this.view);
30379
31938
  }
30380
31939
  render() {
@@ -30415,6 +31974,11 @@ var FlatMap = class {
30415
31974
  }
30416
31975
  const cssViewport = this.viewport(cssWidth, cssHeight);
30417
31976
  const deviceViewport = new Viewport(this.projection, width, height, this.view);
31977
+ const now = performance.now();
31978
+ if (!this.moving || now - this.plannedAt > MOVING_PLAN_MS) {
31979
+ this.plannedAt = now;
31980
+ this.planTiles(cssViewport, sharp);
31981
+ }
30418
31982
  this.clipToDisc(cssViewport);
30419
31983
  this.drawRaster(deviceViewport, width, height);
30420
31984
  this.drawVector(cssViewport);
@@ -30451,18 +32015,20 @@ var FlatMap = class {
30451
32015
  if (layers.length === 0) return;
30452
32016
  if (this.projectionId === "equirectangular") {
30453
32017
  const drawable = layers.map((layer) => ({
30454
- image: this.bitmaps.get(layer.url(this.jd, this.tier(layer.tiers)))?.bitmap,
32018
+ image: this.bitmaps.get(layer.url(this.jd, this.tier(layer)))?.bitmap,
30455
32019
  opacity: layer.opacity,
30456
- blend: layer.blend
32020
+ blend: layer.blend,
32021
+ detail: this.tilesOf(layer)?.canvases()
30457
32022
  })).filter((l) => Boolean(l.image));
30458
32023
  if (drawable.length) drawEquirect(ctx, viewport, drawable);
30459
32024
  return;
30460
32025
  }
30461
32026
  const workingWidth = this.workingWidth();
30462
32027
  const stack = layers.map((layer) => ({
30463
- image: this.rasterFor(layer.url(this.jd, this.tier(layer.tiers)), workingWidth),
32028
+ image: this.rasterFor(layer.url(this.jd, this.tier(layer)), workingWidth),
30464
32029
  opacity: layer.opacity,
30465
- blend: layer.blend
32030
+ blend: layer.blend,
32031
+ detail: this.tilesOf(layer)?.rasters()
30466
32032
  })).filter((l) => Boolean(l.image));
30467
32033
  if (stack.length === 0) return;
30468
32034
  if (!this.buffer || this.buffer.width !== width || this.buffer.height !== height) this.buffer = ctx.createImageData(width, height);
@@ -30620,7 +32186,8 @@ var FlatMap = class {
30620
32186
  zoomBy(factor, px, py) {
30621
32187
  this.limitsArmed = true;
30622
32188
  const anchor = this.viewport().fromScreen(px, py);
30623
- const zoom = Math.min(this.limits.maxZoom ?? Number.POSITIVE_INFINITY, Math.max(1, this.limits.minZoom ?? 1, this.view.zoom * factor));
32189
+ const furthest = this.ownMaxZoom ? Math.max(this.pictureMaxZoom(), this.view.zoom) : this.limits.maxZoom ?? Number.POSITIVE_INFINITY;
32190
+ const zoom = Math.min(furthest, Math.max(1, this.limits.minZoom ?? 1, this.view.zoom * factor));
30624
32191
  if (zoom === this.view.zoom) return;
30625
32192
  this.view = {
30626
32193
  ...this.view,
@@ -30753,7 +32320,7 @@ function wrap(deg) {
30753
32320
  //#region src/lib/panorama/minimap.ts
30754
32321
  var DEG$2 = Math.PI / 180;
30755
32322
  function hasHeading(entry) {
30756
- return entry.orientation !== "unknown" && entry.north_offset_deg !== void 0;
32323
+ return entry.orientation !== "unknown";
30757
32324
  }
30758
32325
 
30759
32326
  //#endregion
@@ -31029,7 +32596,7 @@ var PanoramaView = class {
31029
32596
  key: a.key,
31030
32597
  bearingDeg: a.n.bearingDeg
31031
32598
  })) : []);
31032
- await this.loadTexture(versionedUrl(`/v1/panoramas/${entry.id}.webp`, "panoramas"), entry.north_offset_deg ?? 0, !opening);
32599
+ await this.loadTexture(versionedUrl(`/v1/panoramas/${entry.id}.webp`, "panoramas"), entry.north_offset_deg, !opening);
31033
32600
  if (this.current === entry) this.emit("load", entry);
31034
32601
  }
31035
32602
  async step(key) {