@tileflow/core 0.1.0-alpha.43 → 0.1.0-alpha.44

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/README.md CHANGED
@@ -267,6 +267,11 @@ under the camera, read chosen layers of a vector source for an area at a fixed z
267
267
  Browser 3D scenes support `camera.adaptivePerspective`: caller override,
268
268
  map default, then `false`. Streets enables it. Depth markers let buildings
269
269
  occlude application markers.
270
+ Custom 3D layers load visible ground from the bottom of the viewport towards the horizon before
271
+ spending tiles offscreen. Buildings fade down at the far boundary, across the view's width, and
272
+ distance haze follows that boundary. Coverage follows the viewport and its projection on resize,
273
+ within each layer's `maxTiles` budget (25 by default); views exceeding that budget still have
274
+ bounded coverage, with the nearest ground taking priority.
270
275
  Model roles use `modelLighting` for independent colours and materials. `colorScale` (0–1,
271
276
  default 1) subtly darkens a role after recolouring. Map-shaded models share the buildings’
272
277
  vertical/contact gradient; physical roles keep their own lighting. Model corner geometry is
@@ -1,11 +1,11 @@
1
1
  # Third-party notices
2
2
 
3
- `@tileflow/core/browser` embeds `maplibre-contour` 0.1.0, `PMTiles` 4.5.0, `fflate` 0.8.3, and
3
+ `@tileflow/core/browser` embeds `maplibre-contour` 0.1.1, `PMTiles` 4.5.0, `fflate` 0.8.3, and
4
4
  `earcut` 3.2.4 so contour, local archive, and 3D building rendering remain self-contained.
5
5
  `@tileflow/core` and `@tileflow/core/browser` embed the decoder of `meshoptimizer` 1.3.0, loaded
6
6
  only to read a compressed 3D model file. The complete upstream licenses follow.
7
7
 
8
- ## maplibre-contour 0.1.0
8
+ ## maplibre-contour 0.1.1
9
9
 
10
10
  BSD 3-Clause License
11
11
 
package/dist/3d.js CHANGED
@@ -2553,11 +2553,11 @@ var growth = `
2553
2553
  uniform float u_scale;
2554
2554
  uniform vec2 u_offset; // this tile's corner from the scene's origin, in tile units
2555
2555
  uniform float u_metres; // metres per tile unit
2556
- // The disc the city's tiles cover whole, in this tile's units: its centre, where heights start to
2557
- // ease down and where they are gone; none while its radius is 0. And the view's direction along the
2558
- // ground, in this tile's axes.
2556
+ // In this tile's units: origin and start/end of the fade along the view, or centre and radii of the
2557
+ // fallback covered disc. The direction is along the ground in this tile's axes.
2559
2558
  uniform vec4 u_ring;
2560
2559
  uniform vec2 u_ring_ahead;
2560
+ uniform bool u_ring_directional;
2561
2561
 
2562
2562
  float progressOf() {
2563
2563
  return clamp(u_scale * 1.25 - a_wave.x * 0.25, 0.0, 1.0);
@@ -2569,9 +2569,11 @@ float elevationOf(float own, float top, float p) {
2569
2569
  }
2570
2570
 
2571
2571
  // Buildings sink into the ground over the last stretch of the city's reach, so it ends without an
2572
- // edge of cut-off blocks. Only its far side does: where the tile limit holds the ring short, the
2573
- // buildings round the camera, nearer than its centre, keep standing.
2572
+ // edge of cut-off blocks. The view-aligned fade leaves the entire near width standing; the legacy
2573
+ // disc fallback also exempts its near side.
2574
2574
  float reachOf(vec2 position) {
2575
+ if (u_ring_directional)
2576
+ return 1.0 - smoothstep(u_ring.z, u_ring.w, dot(position - u_ring.xy, u_ring_ahead));
2575
2577
  if (u_ring.w <= 0.0) return 1.0;
2576
2578
  vec2 from = position - u_ring.xy;
2577
2579
  float far = smoothstep(0.0, u_ring.w - u_ring.z, dot(from, u_ring_ahead));
@@ -5665,7 +5667,8 @@ function createCameraTiles(options) {
5665
5667
  const chosen = cameraTileRing(
5666
5668
  { ...centre, zoom: currentZoom, pitch: map.getPitch(), bearing: map.getBearing() },
5667
5669
  zoom,
5668
- options.maxTiles
5670
+ options.maxTiles,
5671
+ cameraViewport(map, zoom)
5669
5672
  );
5670
5673
  ring = chosen;
5671
5674
  wanted = chosen.tiles.map(({ x: x3, y: y2 }) => `${x3}/${y2}`);
@@ -5677,7 +5680,7 @@ function createCameraTiles(options) {
5677
5680
  return;
5678
5681
  }
5679
5682
  const now = performance.now();
5680
- for (const { x: x3, y: y2, distance } of chosen.tiles) {
5683
+ for (const [priority, { x: x3, y: y2 }] of chosen.tiles.entries()) {
5681
5684
  const key = `${x3}/${y2}`;
5682
5685
  let entry = tiles.get(key);
5683
5686
  if (!entry) {
@@ -5685,7 +5688,7 @@ function createCameraTiles(options) {
5685
5688
  tiles.set(key, entry);
5686
5689
  }
5687
5690
  entry.used = now;
5688
- entry.distance = distance;
5691
+ entry.priority = priority;
5689
5692
  if ((!entry.value && !entry.pending || entry.retryAt !== void 0) && !entry.loading && (entry.retryAt ?? 0) <= now)
5690
5693
  load(source, key, entry, chosen.tileMetres);
5691
5694
  }
@@ -5698,7 +5701,7 @@ function createCameraTiles(options) {
5698
5701
  entry.retryAt = void 0;
5699
5702
  source.read(
5700
5703
  { z: zoom, x: entry.x, y: entry.y },
5701
- { signal: controller.signal, priority: entry.distance ?? 0 }
5704
+ { signal: controller.signal, priority: entry.priority ?? 0 }
5702
5705
  ).then((tile) => {
5703
5706
  if (disposed || tiles.get(key) !== entry || entry.loading !== controller) return;
5704
5707
  if (!tile) {
@@ -5733,14 +5736,14 @@ function createCameraTiles(options) {
5733
5736
  while (!disposed && queue.length && performance.now() - started < buildBudgetMs) {
5734
5737
  let nearest = 0;
5735
5738
  for (let at = 1; at < queue.length; at += 1)
5736
- if ((queue[at].entry.distance ?? 0) < (queue[nearest].entry.distance ?? 0))
5739
+ if ((queue[at].entry.priority ?? 0) < (queue[nearest].entry.priority ?? 0))
5737
5740
  nearest = at;
5738
5741
  const { key, entry, controller, tile, metres: tileMetres } = queue.splice(nearest, 1)[0];
5739
5742
  const current = () => !disposed && tiles.get(key) === entry && entry.loading === controller && !controller.signal.aborted;
5740
5743
  if (!current()) continue;
5741
5744
  let value;
5742
5745
  try {
5743
- value = await options.build(tile, tileMetres, controller.signal, entry.distance ?? 0);
5746
+ value = await options.build(tile, tileMetres, controller.signal, entry.priority ?? 0);
5744
5747
  } catch {
5745
5748
  if (current()) {
5746
5749
  entry.loading = void 0;
@@ -5810,6 +5813,7 @@ function createCameraTiles(options) {
5810
5813
  map.on("sourcedata", onSource);
5811
5814
  map.on("move", onMove);
5812
5815
  map.on("moveend", onMoveEnd);
5816
+ map.on("resize", onMoveEnd);
5813
5817
  return {
5814
5818
  update,
5815
5819
  ready() {
@@ -5864,6 +5868,7 @@ function createCameraTiles(options) {
5864
5868
  map.off("sourcedata", onSource);
5865
5869
  map.off("move", onMove);
5866
5870
  map.off("moveend", onMoveEnd);
5871
+ map.off("resize", onMoveEnd);
5867
5872
  for (const entry of tiles.values()) {
5868
5873
  entry.loading?.abort();
5869
5874
  if (entry.pending && entry.pending !== options.empty) options.release(entry.pending);
@@ -5931,33 +5936,110 @@ function cameraFrame(callback) {
5931
5936
  if (typeof requestAnimationFrame === "function") requestAnimationFrame(callback);
5932
5937
  else setTimeout(callback, 16);
5933
5938
  }
5934
- function cameraTileRing(camera, tileZoom, maxTiles) {
5939
+ function cameraTileRing(camera, tileZoom, maxTiles, viewport) {
5935
5940
  const reach = Math.min(
5936
5941
  5e3,
5937
5942
  Math.max(800, 1600 * 2 ** (16 - camera.zoom) * (1 + camera.pitch / 60))
5938
5943
  );
5939
5944
  const bearing = camera.bearing * Math.PI / 180;
5945
+ const direction = [Math.sin(bearing), -Math.cos(bearing)];
5940
5946
  const ahead = reach * 0.35 * Math.min(1, camera.pitch / 60);
5941
5947
  const latitude = camera.lat + Math.cos(bearing) * ahead / 111320;
5942
5948
  const longitude = camera.lng + Math.sin(bearing) * ahead / (111320 * Math.cos(camera.lat * Math.PI / 180));
5943
5949
  const [cx, cy] = cameraTileAt(longitude, latitude, tileZoom);
5944
- const tileMetres = earth * Math.cos(latitude * Math.PI / 180) / 2 ** tileZoom;
5950
+ const [ox, oy] = cameraTileAt(camera.lng, camera.lat, tileZoom);
5951
+ const tileMetres = earth * Math.cos(camera.lat * Math.PI / 180) / 2 ** tileZoom;
5952
+ const far = (reach + ahead) / tileMetres;
5953
+ const along = ([x3, y2]) => (x3 - ox) * direction[0] + (y2 - oy) * direction[1];
5954
+ const visible = viewport && clipGround(viewport, (point) => far - along(point));
5945
5955
  const span = Math.ceil(reach / tileMetres);
5956
+ const left = Math.floor(Math.min(cx - span, ...visible?.map(([x3]) => x3) ?? []));
5957
+ const right = Math.floor(Math.max(cx + span, ...visible?.map(([x3]) => x3) ?? []));
5958
+ const top = Math.floor(Math.min(cy - span, ...visible?.map(([, y2]) => y2) ?? []));
5959
+ const bottom = Math.floor(Math.max(cy + span, ...visible?.map(([, y2]) => y2) ?? []));
5946
5960
  const candidates = [];
5947
- for (let dx = -span; dx <= span; dx += 1)
5948
- for (let dy = -span; dy <= span; dy += 1) {
5949
- const x3 = Math.floor(cx) + dx;
5950
- const y2 = Math.floor(cy) + dy;
5951
- const distance = Math.hypot(
5952
- Math.max(x3, Math.min(cx, x3 + 1)) - cx,
5953
- Math.max(y2, Math.min(cy, y2 + 1)) - cy
5954
- );
5955
- if (distance * tileMetres <= reach) candidates.push({ x: x3, y: y2, distance });
5961
+ for (let x3 = left; x3 <= right; x3++)
5962
+ for (let y2 = top; y2 <= bottom; y2++) {
5963
+ const distance = Math.hypot(Math.max(x3 - cx, 0, cx - x3 - 1), Math.max(y2 - cy, 0, cy - y2 - 1));
5964
+ let intersection = visible ?? [];
5965
+ for (const boundary of [
5966
+ ([px]) => px - x3,
5967
+ ([px]) => x3 + 1 - px,
5968
+ ([, py]) => py - y2,
5969
+ ([, py]) => y2 + 1 - py
5970
+ ])
5971
+ intersection = clipGround(intersection, boundary);
5972
+ const near2 = Math.min(...intersection.map(along));
5973
+ if (near2 < Infinity || distance * tileMetres <= reach)
5974
+ candidates.push({ x: x3, y: y2, distance, near: near2 });
5975
+ }
5976
+ candidates.sort((a, b2) => a.near - b2.near || a.distance - b2.distance);
5977
+ const tiles = candidates.slice(0, maxTiles).map(({ x: x3, y: y2, distance }) => ({ x: x3, y: y2, distance }));
5978
+ const omitted = candidates.slice(tiles.length);
5979
+ const radius = omitted.reduce(
5980
+ (radius2, tile) => Math.min(radius2, tile.distance),
5981
+ reach / tileMetres
5982
+ );
5983
+ const end = Math.min(far, ...omitted.map((tile) => tile.near));
5984
+ const near = visible?.length ? Math.min(...visible.map(along)) : end;
5985
+ return {
5986
+ tiles,
5987
+ centre: [cx, cy],
5988
+ radius,
5989
+ tileMetres,
5990
+ ...visible?.length ? {
5991
+ fade: {
5992
+ origin: [ox, oy],
5993
+ start: end - Math.max(0.25, (end - near) * 0.2),
5994
+ end
5995
+ }
5996
+ } : {}
5997
+ };
5998
+ }
5999
+ function cameraViewport(map, zoom) {
6000
+ const canvas = map.getCanvas?.();
6001
+ const width = canvas?.clientWidth ?? 0;
6002
+ const height = canvas?.clientHeight ?? 0;
6003
+ if (!map.unproject || width <= 0 || height <= 0) return void 0;
6004
+ const origin = cameraTileAt(map.getCenter().lng, map.getCenter().lat, zoom)[0];
6005
+ const world = 2 ** zoom;
6006
+ const pointAt = (x3, y2) => {
6007
+ const location = map.unproject([x3, y2]);
6008
+ const [tx, ty] = cameraTileAt(location.lng, location.lat, zoom);
6009
+ return [tx + Math.round((origin - tx) / world) * world, ty];
6010
+ };
6011
+ const bearing = map.getBearing() * Math.PI / 180;
6012
+ const points = [];
6013
+ for (const x3 of [0, width]) {
6014
+ const bottom = pointAt(x3, height);
6015
+ const middle = pointAt(x3, height / 2);
6016
+ let top = pointAt(x3, 0);
6017
+ if ((top[0] - middle[0]) * Math.sin(bearing) - (top[1] - middle[1]) * Math.cos(bearing) <= 0) {
6018
+ const stretch = world / Math.hypot(middle[0] - bottom[0], middle[1] - bottom[1]);
6019
+ top = [
6020
+ middle[0] + (middle[0] - bottom[0]) * stretch,
6021
+ middle[1] + (middle[1] - bottom[1]) * stretch
6022
+ ];
6023
+ }
6024
+ points.push(bottom, top);
6025
+ }
6026
+ if (points.some((point) => point.some((value) => !Number.isFinite(value)))) return void 0;
6027
+ return [points[0], points[1], points[3], points[2]];
6028
+ }
6029
+ function clipGround(points, distance) {
6030
+ const result = [];
6031
+ for (let i2 = 0; i2 < points.length; i2++) {
6032
+ const a = points[i2];
6033
+ const b2 = points[(i2 + 1) % points.length];
6034
+ const da = distance(a);
6035
+ const db = distance(b2);
6036
+ if (da >= 0) result.push([a[0], a[1]]);
6037
+ if (da < 0 !== db < 0) {
6038
+ const t = da / (da - db);
6039
+ result.push([a[0] + (b2[0] - a[0]) * t, a[1] + (b2[1] - a[1]) * t]);
5956
6040
  }
5957
- candidates.sort((a, b2) => a.distance - b2.distance);
5958
- const tiles = candidates.slice(0, maxTiles);
5959
- const radius = Math.min(reach / tileMetres, candidates[tiles.length]?.distance ?? Infinity);
5960
- return { tiles, centre: [cx, cy], radius, tileMetres };
6041
+ }
6042
+ return result;
5961
6043
  }
5962
6044
  function cameraTileAt(longitude, latitude, zoom) {
5963
6045
  const phi = Math.min(85.05, Math.max(-85.05, latitude)) * Math.PI / 180;
@@ -8505,6 +8587,7 @@ var tileflowSkyHazeReachFloor = 3;
8505
8587
  var nineTenths = 0.8042;
8506
8588
  function tileflowSkyReachDepth(view, reach) {
8507
8589
  const sin = Math.sin(radians(view.pitch));
8590
+ if (reach.forward !== void 0) return 1 + reach.forward * sin;
8508
8591
  const cos = Math.cos(radians(view.pitch));
8509
8592
  const back = -sin;
8510
8593
  const across = Math.atan(view.aspect * Math.tan(view.fov / 2));
@@ -8577,7 +8660,8 @@ function reachOf(map, distance) {
8577
8660
  return {
8578
8661
  ahead: (east * Math.sin(bearing) + north * Math.cos(bearing)) / metres,
8579
8662
  across: (east * Math.cos(bearing) - north * Math.sin(bearing)) / metres,
8580
- radius: reach.radius / metres
8663
+ radius: reach.radius / metres,
8664
+ ...reach.forward === void 0 ? {} : { forward: reach.forward / metres }
8581
8665
  };
8582
8666
  }
8583
8667
  function fovOf(map, args) {
@@ -9112,6 +9196,7 @@ function createTileflowBuildings3D(options) {
9112
9196
  context.uniform2f(program.uniforms.u_offset, ...offset);
9113
9197
  context.uniform4f(program.uniforms.u_ring ?? null, ...ring);
9114
9198
  context.uniform2f(program.uniforms.u_ring_ahead ?? null, ...ahead);
9199
+ context.uniform1i(program.uniforms.u_ring_directional ?? null, camera.ring()?.fade ? 1 : 0);
9115
9200
  const buffers = value.gl;
9116
9201
  context.bindVertexArray(buffers.vao);
9117
9202
  for (const range of ranges)
@@ -9394,13 +9479,21 @@ function createTileflowBuildings3D(options) {
9394
9479
  return {
9395
9480
  lng: ring.centre[0] / n * 360 - 180,
9396
9481
  lat: Math.atan(Math.sinh(Math.PI * (1 - 2 * ring.centre[1] / n))) * 180 / Math.PI,
9397
- radius: ring.radius * ring.tileMetres
9482
+ radius: ring.radius * ring.tileMetres,
9483
+ ...ring.fade ? { forward: ring.fade.end * ring.tileMetres } : {}
9398
9484
  };
9399
9485
  }
9400
9486
  function ringIn(x3, y2) {
9401
9487
  const ring = camera.ring();
9402
9488
  if (!ring) return [0, 0, 0, 0];
9403
9489
  const extent4 = tileflowBuildingExtent;
9490
+ if (ring.fade)
9491
+ return [
9492
+ (ring.fade.origin[0] - x3) * extent4,
9493
+ (ring.fade.origin[1] - y2) * extent4,
9494
+ ring.fade.start * extent4,
9495
+ ring.fade.end * extent4
9496
+ ];
9404
9497
  const cx = (ring.centre[0] - x3) * extent4;
9405
9498
  const cy = (ring.centre[1] - y2) * extent4;
9406
9499
  const inner = ring.radius * extent4 * 0.8;
@@ -9414,6 +9507,8 @@ function createTileflowBuildings3D(options) {
9414
9507
  return [Math.sin(bearing), -Math.cos(bearing)];
9415
9508
  }
9416
9509
  function insideRing(ring, left, top, right, bottom) {
9510
+ if (camera.ring()?.fade)
9511
+ return Math.min(ahead[0] * (left - ring[0]), ahead[0] * (right - ring[0])) + Math.min(ahead[1] * (top - ring[1]), ahead[1] * (bottom - ring[1])) < ring[3];
9417
9512
  const radius = ring[3];
9418
9513
  if (!(radius > 0)) return true;
9419
9514
  const cx = ring[0];
@@ -9631,6 +9726,7 @@ function createTileflowBuildings3D(options) {
9631
9726
  );
9632
9727
  context.uniform4f(u.u_ring ?? null, ...ringIn(x3, y2));
9633
9728
  context.uniform2f(u.u_ring_ahead ?? null, ...ahead);
9729
+ context.uniform1i(u.u_ring_directional ?? null, camera.ring()?.fade ? 1 : 0);
9634
9730
  const palette = themes ? paletteOf(context, entry, themes.themes) : void 0;
9635
9731
  context.uniform1f(u.u_palette_on, palette ? 1 : 0);
9636
9732
  context.activeTexture(context.TEXTURE1);
package/dist/browser.js CHANGED
@@ -1224,7 +1224,7 @@ var init_meshopt_decoder = __esm({
1224
1224
  }
1225
1225
  });
1226
1226
 
1227
- // ../../node_modules/.pnpm/maplibre-contour@0.1.0/node_modules/maplibre-contour/dist/index.mjs
1227
+ // ../../node_modules/.pnpm/maplibre-contour@0.1.1/node_modules/maplibre-contour/dist/index.mjs
1228
1228
  var dist_exports = {};
1229
1229
  __export(dist_exports, {
1230
1230
  default: () => mlcontour$1
@@ -1248,7 +1248,7 @@ function define(_2, chunk) {
1248
1248
  }
1249
1249
  var shared3, worker, mlcontour, mlcontour$1;
1250
1250
  var init_dist = __esm({
1251
- "../../node_modules/.pnpm/maplibre-contour@0.1.0/node_modules/maplibre-contour/dist/index.mjs"() {
1251
+ "../../node_modules/.pnpm/maplibre-contour@0.1.1/node_modules/maplibre-contour/dist/index.mjs"() {
1252
1252
  "use strict";
1253
1253
  define(["exports"], (function(exports) {
1254
1254
  "use strict";
@@ -1596,13 +1596,10 @@ var init_dist = __esm({
1596
1596
  });
1597
1597
  }
1598
1598
  function prepareContourTile(promise) {
1599
- return promise.then(({ arrayBuffer }) => {
1600
- const clone = copy(arrayBuffer);
1601
- return {
1602
- arrayBuffer: clone,
1603
- transferrables: [clone]
1604
- };
1605
- });
1599
+ return promise.then(({ arrayBuffer }) => ({
1600
+ arrayBuffer,
1601
+ transferrables: [arrayBuffer]
1602
+ }));
1606
1603
  }
1607
1604
  let supportsOffscreenCanvas = null;
1608
1605
  function offscreenCanvasSupported() {
@@ -1665,6 +1662,10 @@ var init_dist = __esm({
1665
1662
  var _a2;
1666
1663
  return Boolean((_a2 = abortController === null || abortController === void 0 ? void 0 : abortController.signal) === null || _a2 === void 0 ? void 0 : _a2.aborted);
1667
1664
  }
1665
+ function parseUrl(url) {
1666
+ const [, z2, x3, y2] = /\/\/(\d+)\/(\d+)\/(\d+)/.exec(url) || [];
1667
+ return [Number(z2), Number(x3), Number(y2)];
1668
+ }
1668
1669
  let num = 0;
1669
1670
  class AsyncCache {
1670
1671
  constructor(maxSize = 100) {
@@ -2644,9 +2645,7 @@ var init_dist = __esm({
2644
2645
  function writeLayer(layer, pbf) {
2645
2646
  if (!pbf)
2646
2647
  throw new Error("pbf undefined");
2647
- pbf.writeVarintField(15, 2);
2648
2648
  pbf.writeStringField(1, layer.id || "");
2649
- pbf.writeVarintField(5, layer.extent || 4096);
2650
2649
  const context = {
2651
2650
  keys: [],
2652
2651
  values: [],
@@ -2663,6 +2662,8 @@ var init_dist = __esm({
2663
2662
  for (const value of context.values) {
2664
2663
  pbf.writeMessage(4, writeValue, value);
2665
2664
  }
2665
+ pbf.writeVarintField(5, layer.extent || 4096);
2666
+ pbf.writeVarintField(15, 2);
2666
2667
  }
2667
2668
  function writeFeature(context, pbf) {
2668
2669
  const feature = context.feature;
@@ -2911,7 +2912,7 @@ var init_dist = __esm({
2911
2912
  }
2912
2913
  fetchDem(z2, x3, y2, options, abortController, timer) {
2913
2914
  return __awaiter(this, void 0, void 0, function* () {
2914
- const zoom = Math.min(z2 - (options.overzoom || 0), this.maxzoom);
2915
+ const zoom = Math.max(0, Math.min(z2 - (options.overzoom || 0), this.maxzoom));
2915
2916
  const subZ = z2 - zoom;
2916
2917
  const div = 1 << subZ;
2917
2918
  const newX = Math.floor(x3 / div);
@@ -2926,7 +2927,7 @@ var init_dist = __esm({
2926
2927
  return Promise.resolve({ arrayBuffer: new ArrayBuffer(0) });
2927
2928
  }
2928
2929
  const key = [z2, x3, y2, encodeIndividualOptions(options)].join("/");
2929
- return this.contourCache.get(key, (_2, childAbortController) => __awaiter(this, void 0, void 0, function* () {
2930
+ const cached = this.contourCache.get(key, (_2, childAbortController) => __awaiter(this, void 0, void 0, function* () {
2930
2931
  const max2 = 1 << z2;
2931
2932
  const neighborPromises = [];
2932
2933
  for (let iy = y2 - 1; iy <= y2 + 1; iy++) {
@@ -2969,8 +2970,11 @@ var init_dist = __esm({
2969
2970
  }
2970
2971
  });
2971
2972
  mark === null || mark === void 0 ? void 0 : mark();
2972
- return { arrayBuffer: result.buffer };
2973
+ return { arrayBuffer: result.slice().buffer };
2973
2974
  }), parentAbortController);
2975
+ return cached.then(({ arrayBuffer }) => ({
2976
+ arrayBuffer: copy(arrayBuffer)
2977
+ }));
2974
2978
  }
2975
2979
  }
2976
2980
  let id = 0;
@@ -3054,13 +3058,14 @@ var init_dist = __esm({
3054
3058
  exports.L = LocalDemManager;
3055
3059
  exports.T = Timer;
3056
3060
  exports._ = __awaiter;
3057
- exports.a = decodeOptions;
3058
- exports.b = generateIsolines;
3061
+ exports.a = parseUrl;
3062
+ exports.b = decodeOptions;
3059
3063
  exports.c = decodeParsedImage;
3060
3064
  exports.d = defaultDecoder;
3061
3065
  exports.e = encodeOptions;
3062
- exports.f = prepareContourTile;
3066
+ exports.f = generateIsolines;
3063
3067
  exports.g = getOptionsForZoom;
3068
+ exports.h = prepareContourTile;
3064
3069
  exports.p = prepareDemTile;
3065
3070
  }));
3066
3071
  define(["./shared"], (function(actor) {
@@ -3083,7 +3088,7 @@ var init_dist = __esm({
3083
3088
  };
3084
3089
  this.fetchContourTile = (managerId, z2, x3, y2, options, abortController, timer) => {
3085
3090
  var _a2;
3086
- return actor.f(((_a2 = this.managers[managerId]) === null || _a2 === void 0 ? void 0 : _a2.fetchContourTile(z2, x3, y2, options, abortController, timer)) || noManager(managerId));
3091
+ return actor.h(((_a2 = this.managers[managerId]) === null || _a2 === void 0 ? void 0 : _a2.fetchContourTile(z2, x3, y2, options, abortController, timer)) || noManager(managerId));
3087
3092
  };
3088
3093
  }
3089
3094
  }
@@ -3152,7 +3157,7 @@ var init_dist = __esm({
3152
3157
  maplibre.addProtocol(this.contourProtocolId, this.contourProtocol);
3153
3158
  };
3154
3159
  this.sharedDemProtocolV4 = (request, abortController) => actor._(this, void 0, void 0, function* () {
3155
- const [z2, x3, y2] = this.parseUrl(request.url);
3160
+ const [z2, x3, y2] = actor.a(request.url);
3156
3161
  const timer = new actor.T("main");
3157
3162
  let timing;
3158
3163
  try {
@@ -3175,8 +3180,8 @@ var init_dist = __esm({
3175
3180
  const timer = new actor.T("main");
3176
3181
  let timing;
3177
3182
  try {
3178
- const [z2, x3, y2] = this.parseUrl(request.url);
3179
- const options = actor.a(request.url);
3183
+ const [z2, x3, y2] = actor.a(request.url);
3184
+ const options = actor.b(request.url);
3180
3185
  const data = yield this.manager.fetchContourTile(z2, x3, y2, actor.g(options, z2), abortController, timer);
3181
3186
  timing = timer.finish(request.url);
3182
3187
  return { data: data.arrayBuffer };
@@ -3213,13 +3218,9 @@ var init_dist = __esm({
3213
3218
  getDemTile(z2, x3, y2, abortController) {
3214
3219
  return this.manager.fetchAndParseTile(z2, x3, y2, abortController || new AbortController());
3215
3220
  }
3216
- parseUrl(url) {
3217
- const [, z2, x3, y2] = /\/\/(\d+)\/(\d+)\/(\d+)/.exec(url) || [];
3218
- return [Number(z2), Number(x3), Number(y2)];
3219
- }
3220
3221
  }
3221
3222
  const exported = {
3222
- generateIsolines: actor.b,
3223
+ generateIsolines: actor.f,
3223
3224
  DemSource,
3224
3225
  HeightTile: actor.H,
3225
3226
  LocalDemManager: actor.L,
@@ -5324,6 +5325,7 @@ var tileflowSkyHazeReachFloor = 3;
5324
5325
  var nineTenths = 0.8042;
5325
5326
  function tileflowSkyReachDepth(view, reach) {
5326
5327
  const sin = Math.sin(radians(view.pitch));
5328
+ if (reach.forward !== void 0) return 1 + reach.forward * sin;
5327
5329
  const cos = Math.cos(radians(view.pitch));
5328
5330
  const back = -sin;
5329
5331
  const across = Math.atan(view.aspect * Math.tan(view.fov / 2));
@@ -5397,7 +5399,8 @@ function reachOf(map, distance) {
5397
5399
  return {
5398
5400
  ahead: (east * Math.sin(bearing) + north * Math.cos(bearing)) / metres,
5399
5401
  across: (east * Math.cos(bearing) - north * Math.sin(bearing)) / metres,
5400
- radius: reach.radius / metres
5402
+ radius: reach.radius / metres,
5403
+ ...reach.forward === void 0 ? {} : { forward: reach.forward / metres }
5401
5404
  };
5402
5405
  }
5403
5406
  function fovOf(map, args) {
@@ -8427,11 +8430,11 @@ var growth = `
8427
8430
  uniform float u_scale;
8428
8431
  uniform vec2 u_offset; // this tile's corner from the scene's origin, in tile units
8429
8432
  uniform float u_metres; // metres per tile unit
8430
- // The disc the city's tiles cover whole, in this tile's units: its centre, where heights start to
8431
- // ease down and where they are gone; none while its radius is 0. And the view's direction along the
8432
- // ground, in this tile's axes.
8433
+ // In this tile's units: origin and start/end of the fade along the view, or centre and radii of the
8434
+ // fallback covered disc. The direction is along the ground in this tile's axes.
8433
8435
  uniform vec4 u_ring;
8434
8436
  uniform vec2 u_ring_ahead;
8437
+ uniform bool u_ring_directional;
8435
8438
 
8436
8439
  float progressOf() {
8437
8440
  return clamp(u_scale * 1.25 - a_wave.x * 0.25, 0.0, 1.0);
@@ -8443,9 +8446,11 @@ float elevationOf(float own, float top, float p) {
8443
8446
  }
8444
8447
 
8445
8448
  // Buildings sink into the ground over the last stretch of the city's reach, so it ends without an
8446
- // edge of cut-off blocks. Only its far side does: where the tile limit holds the ring short, the
8447
- // buildings round the camera, nearer than its centre, keep standing.
8449
+ // edge of cut-off blocks. The view-aligned fade leaves the entire near width standing; the legacy
8450
+ // disc fallback also exempts its near side.
8448
8451
  float reachOf(vec2 position) {
8452
+ if (u_ring_directional)
8453
+ return 1.0 - smoothstep(u_ring.z, u_ring.w, dot(position - u_ring.xy, u_ring_ahead));
8449
8454
  if (u_ring.w <= 0.0) return 1.0;
8450
8455
  vec2 from = position - u_ring.xy;
8451
8456
  float far = smoothstep(0.0, u_ring.w - u_ring.z, dot(from, u_ring_ahead));
@@ -11624,7 +11629,8 @@ function createCameraTiles(options) {
11624
11629
  const chosen = cameraTileRing(
11625
11630
  { ...centre, zoom: currentZoom, pitch: map.getPitch(), bearing: map.getBearing() },
11626
11631
  zoom,
11627
- options.maxTiles
11632
+ options.maxTiles,
11633
+ cameraViewport(map, zoom)
11628
11634
  );
11629
11635
  ring = chosen;
11630
11636
  wanted = chosen.tiles.map(({ x: x3, y: y2 }) => `${x3}/${y2}`);
@@ -11636,7 +11642,7 @@ function createCameraTiles(options) {
11636
11642
  return;
11637
11643
  }
11638
11644
  const now = performance.now();
11639
- for (const { x: x3, y: y2, distance } of chosen.tiles) {
11645
+ for (const [priority, { x: x3, y: y2 }] of chosen.tiles.entries()) {
11640
11646
  const key = `${x3}/${y2}`;
11641
11647
  let entry = tiles.get(key);
11642
11648
  if (!entry) {
@@ -11644,7 +11650,7 @@ function createCameraTiles(options) {
11644
11650
  tiles.set(key, entry);
11645
11651
  }
11646
11652
  entry.used = now;
11647
- entry.distance = distance;
11653
+ entry.priority = priority;
11648
11654
  if ((!entry.value && !entry.pending || entry.retryAt !== void 0) && !entry.loading && (entry.retryAt ?? 0) <= now)
11649
11655
  load(source, key, entry, chosen.tileMetres);
11650
11656
  }
@@ -11657,7 +11663,7 @@ function createCameraTiles(options) {
11657
11663
  entry.retryAt = void 0;
11658
11664
  source.read(
11659
11665
  { z: zoom, x: entry.x, y: entry.y },
11660
- { signal: controller.signal, priority: entry.distance ?? 0 }
11666
+ { signal: controller.signal, priority: entry.priority ?? 0 }
11661
11667
  ).then((tile) => {
11662
11668
  if (disposed || tiles.get(key) !== entry || entry.loading !== controller) return;
11663
11669
  if (!tile) {
@@ -11692,14 +11698,14 @@ function createCameraTiles(options) {
11692
11698
  while (!disposed && queue.length && performance.now() - started < buildBudgetMs) {
11693
11699
  let nearest = 0;
11694
11700
  for (let at = 1; at < queue.length; at += 1)
11695
- if ((queue[at].entry.distance ?? 0) < (queue[nearest].entry.distance ?? 0))
11701
+ if ((queue[at].entry.priority ?? 0) < (queue[nearest].entry.priority ?? 0))
11696
11702
  nearest = at;
11697
11703
  const { key, entry, controller, tile, metres: tileMetres } = queue.splice(nearest, 1)[0];
11698
11704
  const current = () => !disposed && tiles.get(key) === entry && entry.loading === controller && !controller.signal.aborted;
11699
11705
  if (!current()) continue;
11700
11706
  let value;
11701
11707
  try {
11702
- value = await options.build(tile, tileMetres, controller.signal, entry.distance ?? 0);
11708
+ value = await options.build(tile, tileMetres, controller.signal, entry.priority ?? 0);
11703
11709
  } catch {
11704
11710
  if (current()) {
11705
11711
  entry.loading = void 0;
@@ -11769,6 +11775,7 @@ function createCameraTiles(options) {
11769
11775
  map.on("sourcedata", onSource);
11770
11776
  map.on("move", onMove);
11771
11777
  map.on("moveend", onMoveEnd);
11778
+ map.on("resize", onMoveEnd);
11772
11779
  return {
11773
11780
  update,
11774
11781
  ready() {
@@ -11823,6 +11830,7 @@ function createCameraTiles(options) {
11823
11830
  map.off("sourcedata", onSource);
11824
11831
  map.off("move", onMove);
11825
11832
  map.off("moveend", onMoveEnd);
11833
+ map.off("resize", onMoveEnd);
11826
11834
  for (const entry of tiles.values()) {
11827
11835
  entry.loading?.abort();
11828
11836
  if (entry.pending && entry.pending !== options.empty) options.release(entry.pending);
@@ -11890,33 +11898,110 @@ function cameraFrame(callback) {
11890
11898
  if (typeof requestAnimationFrame === "function") requestAnimationFrame(callback);
11891
11899
  else setTimeout(callback, 16);
11892
11900
  }
11893
- function cameraTileRing(camera, tileZoom, maxTiles) {
11901
+ function cameraTileRing(camera, tileZoom, maxTiles, viewport) {
11894
11902
  const reach = Math.min(
11895
11903
  5e3,
11896
11904
  Math.max(800, 1600 * 2 ** (16 - camera.zoom) * (1 + camera.pitch / 60))
11897
11905
  );
11898
11906
  const bearing = camera.bearing * Math.PI / 180;
11907
+ const direction = [Math.sin(bearing), -Math.cos(bearing)];
11899
11908
  const ahead = reach * 0.35 * Math.min(1, camera.pitch / 60);
11900
11909
  const latitude = camera.lat + Math.cos(bearing) * ahead / 111320;
11901
11910
  const longitude = camera.lng + Math.sin(bearing) * ahead / (111320 * Math.cos(camera.lat * Math.PI / 180));
11902
11911
  const [cx, cy] = cameraTileAt(longitude, latitude, tileZoom);
11903
- const tileMetres = earth * Math.cos(latitude * Math.PI / 180) / 2 ** tileZoom;
11912
+ const [ox, oy] = cameraTileAt(camera.lng, camera.lat, tileZoom);
11913
+ const tileMetres = earth * Math.cos(camera.lat * Math.PI / 180) / 2 ** tileZoom;
11914
+ const far = (reach + ahead) / tileMetres;
11915
+ const along = ([x3, y2]) => (x3 - ox) * direction[0] + (y2 - oy) * direction[1];
11916
+ const visible = viewport && clipGround(viewport, (point) => far - along(point));
11904
11917
  const span = Math.ceil(reach / tileMetres);
11918
+ const left = Math.floor(Math.min(cx - span, ...visible?.map(([x3]) => x3) ?? []));
11919
+ const right = Math.floor(Math.max(cx + span, ...visible?.map(([x3]) => x3) ?? []));
11920
+ const top = Math.floor(Math.min(cy - span, ...visible?.map(([, y2]) => y2) ?? []));
11921
+ const bottom = Math.floor(Math.max(cy + span, ...visible?.map(([, y2]) => y2) ?? []));
11905
11922
  const candidates = [];
11906
- for (let dx = -span; dx <= span; dx += 1)
11907
- for (let dy = -span; dy <= span; dy += 1) {
11908
- const x3 = Math.floor(cx) + dx;
11909
- const y2 = Math.floor(cy) + dy;
11910
- const distance = Math.hypot(
11911
- Math.max(x3, Math.min(cx, x3 + 1)) - cx,
11912
- Math.max(y2, Math.min(cy, y2 + 1)) - cy
11913
- );
11914
- if (distance * tileMetres <= reach) candidates.push({ x: x3, y: y2, distance });
11923
+ for (let x3 = left; x3 <= right; x3++)
11924
+ for (let y2 = top; y2 <= bottom; y2++) {
11925
+ const distance = Math.hypot(Math.max(x3 - cx, 0, cx - x3 - 1), Math.max(y2 - cy, 0, cy - y2 - 1));
11926
+ let intersection = visible ?? [];
11927
+ for (const boundary of [
11928
+ ([px]) => px - x3,
11929
+ ([px]) => x3 + 1 - px,
11930
+ ([, py]) => py - y2,
11931
+ ([, py]) => y2 + 1 - py
11932
+ ])
11933
+ intersection = clipGround(intersection, boundary);
11934
+ const near2 = Math.min(...intersection.map(along));
11935
+ if (near2 < Infinity || distance * tileMetres <= reach)
11936
+ candidates.push({ x: x3, y: y2, distance, near: near2 });
11937
+ }
11938
+ candidates.sort((a, b2) => a.near - b2.near || a.distance - b2.distance);
11939
+ const tiles = candidates.slice(0, maxTiles).map(({ x: x3, y: y2, distance }) => ({ x: x3, y: y2, distance }));
11940
+ const omitted = candidates.slice(tiles.length);
11941
+ const radius = omitted.reduce(
11942
+ (radius2, tile) => Math.min(radius2, tile.distance),
11943
+ reach / tileMetres
11944
+ );
11945
+ const end = Math.min(far, ...omitted.map((tile) => tile.near));
11946
+ const near = visible?.length ? Math.min(...visible.map(along)) : end;
11947
+ return {
11948
+ tiles,
11949
+ centre: [cx, cy],
11950
+ radius,
11951
+ tileMetres,
11952
+ ...visible?.length ? {
11953
+ fade: {
11954
+ origin: [ox, oy],
11955
+ start: end - Math.max(0.25, (end - near) * 0.2),
11956
+ end
11957
+ }
11958
+ } : {}
11959
+ };
11960
+ }
11961
+ function cameraViewport(map, zoom) {
11962
+ const canvas = map.getCanvas?.();
11963
+ const width = canvas?.clientWidth ?? 0;
11964
+ const height = canvas?.clientHeight ?? 0;
11965
+ if (!map.unproject || width <= 0 || height <= 0) return void 0;
11966
+ const origin = cameraTileAt(map.getCenter().lng, map.getCenter().lat, zoom)[0];
11967
+ const world = 2 ** zoom;
11968
+ const pointAt = (x3, y2) => {
11969
+ const location = map.unproject([x3, y2]);
11970
+ const [tx, ty] = cameraTileAt(location.lng, location.lat, zoom);
11971
+ return [tx + Math.round((origin - tx) / world) * world, ty];
11972
+ };
11973
+ const bearing = map.getBearing() * Math.PI / 180;
11974
+ const points = [];
11975
+ for (const x3 of [0, width]) {
11976
+ const bottom = pointAt(x3, height);
11977
+ const middle = pointAt(x3, height / 2);
11978
+ let top = pointAt(x3, 0);
11979
+ if ((top[0] - middle[0]) * Math.sin(bearing) - (top[1] - middle[1]) * Math.cos(bearing) <= 0) {
11980
+ const stretch = world / Math.hypot(middle[0] - bottom[0], middle[1] - bottom[1]);
11981
+ top = [
11982
+ middle[0] + (middle[0] - bottom[0]) * stretch,
11983
+ middle[1] + (middle[1] - bottom[1]) * stretch
11984
+ ];
11985
+ }
11986
+ points.push(bottom, top);
11987
+ }
11988
+ if (points.some((point) => point.some((value) => !Number.isFinite(value)))) return void 0;
11989
+ return [points[0], points[1], points[3], points[2]];
11990
+ }
11991
+ function clipGround(points, distance) {
11992
+ const result = [];
11993
+ for (let i2 = 0; i2 < points.length; i2++) {
11994
+ const a = points[i2];
11995
+ const b2 = points[(i2 + 1) % points.length];
11996
+ const da = distance(a);
11997
+ const db = distance(b2);
11998
+ if (da >= 0) result.push([a[0], a[1]]);
11999
+ if (da < 0 !== db < 0) {
12000
+ const t = da / (da - db);
12001
+ result.push([a[0] + (b2[0] - a[0]) * t, a[1] + (b2[1] - a[1]) * t]);
11915
12002
  }
11916
- candidates.sort((a, b2) => a.distance - b2.distance);
11917
- const tiles = candidates.slice(0, maxTiles);
11918
- const radius = Math.min(reach / tileMetres, candidates[tiles.length]?.distance ?? Infinity);
11919
- return { tiles, centre: [cx, cy], radius, tileMetres };
12003
+ }
12004
+ return result;
11920
12005
  }
11921
12006
  function cameraTileAt(longitude, latitude, zoom) {
11922
12007
  const phi = Math.min(85.05, Math.max(-85.05, latitude)) * Math.PI / 180;
@@ -14447,6 +14532,7 @@ function createTileflowBuildings3D(options) {
14447
14532
  context.uniform2f(program.uniforms.u_offset, ...offset);
14448
14533
  context.uniform4f(program.uniforms.u_ring ?? null, ...ring);
14449
14534
  context.uniform2f(program.uniforms.u_ring_ahead ?? null, ...ahead);
14535
+ context.uniform1i(program.uniforms.u_ring_directional ?? null, camera.ring()?.fade ? 1 : 0);
14450
14536
  const buffers = value.gl;
14451
14537
  context.bindVertexArray(buffers.vao);
14452
14538
  for (const range of ranges)
@@ -14729,13 +14815,21 @@ function createTileflowBuildings3D(options) {
14729
14815
  return {
14730
14816
  lng: ring.centre[0] / n * 360 - 180,
14731
14817
  lat: Math.atan(Math.sinh(Math.PI * (1 - 2 * ring.centre[1] / n))) * 180 / Math.PI,
14732
- radius: ring.radius * ring.tileMetres
14818
+ radius: ring.radius * ring.tileMetres,
14819
+ ...ring.fade ? { forward: ring.fade.end * ring.tileMetres } : {}
14733
14820
  };
14734
14821
  }
14735
14822
  function ringIn(x3, y2) {
14736
14823
  const ring = camera.ring();
14737
14824
  if (!ring) return [0, 0, 0, 0];
14738
14825
  const extent5 = tileflowBuildingExtent;
14826
+ if (ring.fade)
14827
+ return [
14828
+ (ring.fade.origin[0] - x3) * extent5,
14829
+ (ring.fade.origin[1] - y2) * extent5,
14830
+ ring.fade.start * extent5,
14831
+ ring.fade.end * extent5
14832
+ ];
14739
14833
  const cx = (ring.centre[0] - x3) * extent5;
14740
14834
  const cy = (ring.centre[1] - y2) * extent5;
14741
14835
  const inner = ring.radius * extent5 * 0.8;
@@ -14749,6 +14843,8 @@ function createTileflowBuildings3D(options) {
14749
14843
  return [Math.sin(bearing), -Math.cos(bearing)];
14750
14844
  }
14751
14845
  function insideRing2(ring, left, top, right, bottom) {
14846
+ if (camera.ring()?.fade)
14847
+ return Math.min(ahead[0] * (left - ring[0]), ahead[0] * (right - ring[0])) + Math.min(ahead[1] * (top - ring[1]), ahead[1] * (bottom - ring[1])) < ring[3];
14752
14848
  const radius = ring[3];
14753
14849
  if (!(radius > 0)) return true;
14754
14850
  const cx = ring[0];
@@ -14966,6 +15062,7 @@ function createTileflowBuildings3D(options) {
14966
15062
  );
14967
15063
  context.uniform4f(u.u_ring ?? null, ...ringIn(x3, y2));
14968
15064
  context.uniform2f(u.u_ring_ahead ?? null, ...ahead);
15065
+ context.uniform1i(u.u_ring_directional ?? null, camera.ring()?.fade ? 1 : 0);
14969
15066
  const palette = themes ? paletteOf(context, entry, themes.themes) : void 0;
14970
15067
  context.uniform1f(u.u_palette_on, palette ? 1 : 0);
14971
15068
  context.activeTexture(context.TEXTURE1);
@@ -20361,11 +20458,11 @@ async function loadPinnedMaplibreContour() {
20361
20458
  }
20362
20459
  function resolveMaplibreContourLibrary(module) {
20363
20460
  if (!module || typeof module !== "object") {
20364
- throw new Error("maplibre-contour 0.1.0 did not expose its browser module.");
20461
+ throw new Error("maplibre-contour 0.1.1 did not expose its browser module.");
20365
20462
  }
20366
20463
  const library = module.default;
20367
20464
  if (!library || typeof library !== "object" || typeof library.DemSource !== "function") {
20368
- throw new Error("maplibre-contour 0.1.0 did not expose DemSource.");
20465
+ throw new Error("maplibre-contour 0.1.1 did not expose DemSource.");
20369
20466
  }
20370
20467
  return library;
20371
20468
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tileflow/core",
3
- "version": "0.1.0-alpha.43",
3
+ "version": "0.1.0-alpha.44",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Typed Tileflow config and MapLibre style compiler.",
6
6
  "type": "module",
@@ -87,7 +87,7 @@
87
87
  "devDependencies": {
88
88
  "@mapbox/vector-tile": "3.0.0",
89
89
  "earcut": "3.2.4",
90
- "maplibre-contour": "0.1.0",
90
+ "maplibre-contour": "0.1.1",
91
91
  "meshoptimizer": "1.3.0",
92
92
  "pbf": "5.1.2",
93
93
  "pmtiles": "4.5.0",