@orbat-mapper/tactical-draw-adapter-maplibre 0.4.4 → 0.6.0

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/dist/index.d.mts CHANGED
@@ -62,6 +62,20 @@ declare class PinLayer implements CustomLayerInterface {
62
62
  private atlas;
63
63
  private pins;
64
64
  private ranges;
65
+ private instanceData;
66
+ private sortedData;
67
+ private paintOrder;
68
+ private gpuOffsets;
69
+ private depths;
70
+ private sortMatrix;
71
+ private sortGlobeMatrix;
72
+ private sortTransition;
73
+ private sortWorld;
74
+ private sortDirty;
75
+ /** View-wide far-to-near paint order; `false` restores canonical layer order. */
76
+ private depthSorted;
77
+ /** Hoisted out of the per-frame sort so no comparator is allocated while panning. */
78
+ private comparePaintOrder;
65
79
  private version;
66
80
  private resourceRevision;
67
81
  private needsDepth;
@@ -98,6 +112,8 @@ declare class PinLayer implements CustomLayerInterface {
98
112
  private buildPin;
99
113
  private rebuild;
100
114
  render(context: WebGLRenderingContext | WebGL2RenderingContext, input: CustomRenderMethodInput): void;
115
+ /** Reorder existing instances only when camera/data changes, preserving atlas batches. */
116
+ private updatePaintOrder;
101
117
  private draw;
102
118
  /** On-demand GPU pick shares the visual shader including alpha and terrain mask. */
103
119
  pick(pixel: PixelCoordinate, tolerance?: number): string | null;
@@ -127,6 +143,13 @@ interface MapLibreLayerHandle {
127
143
  sourceId: string;
128
144
  /** MapLibre layer ids created for this logical layer (fill, line, circle, symbol) */
129
145
  mlLayerIds: string[];
146
+ /**
147
+ * Anchor for overlay-band bundle layers while terrain is active — this
148
+ * handle's own `-circle` layer (ADR-0057). Stored explicitly rather than
149
+ * derived from `mlLayerIds` by position/suffix so the terrain-banding code
150
+ * doesn't depend on `createLayer`'s layer-creation order.
151
+ */
152
+ overlayAnchorId: string;
130
153
  groupBundles: Map<string, MapLibreGroupBundle>;
131
154
  groupOrder: string[];
132
155
  nextGroupBundleId: number;
@@ -198,13 +221,33 @@ interface MapLibreAdapterOptions {
198
221
  * the refit until the animation ends.
199
222
  */
200
223
  viewChangeMode?: "continuous" | "settle";
224
+ /**
225
+ * While MapLibre terrain is active, paint fill/line layers in one band below
226
+ * circle/icon/text/pin layers so terrain draping stays in few passes
227
+ * (ADR-0057). `false` keeps exact per-graphic paint order under terrain, at
228
+ * one draping pass per graphic. @default true
229
+ */
230
+ terrainBanding?: boolean;
201
231
  }
202
232
  declare class MapLibreAdapter extends BaseMapAdapter<MapLibreLayerHandle, MapLibreEventToken, MapLibreViewToken> {
203
233
  private map;
204
234
  private readonly viewChangeMode;
205
235
  private readonly pendingIconImages;
206
236
  readonly scalesGroundPointSymbolsNatively = true;
237
+ private terrainActive;
238
+ private terrainBanding;
239
+ private readonly handleTerrainChange;
207
240
  constructor(map: maplibregl.Map, options?: MapLibreAdapterOptions);
241
+ /** Switches `terrainBanding` at runtime, re-banding every group if the effective state changes. */
242
+ setTerrainBanding(enabled: boolean): void;
243
+ protected destroyEngineResources(): void;
244
+ /**
245
+ * Insertion anchor for a new bundle layer: below the handle's layers, or —
246
+ * for an overlay-band layer under terrain — below the overlay anchor, so it
247
+ * never lands between draped layers and fragments a render-to-texture stack
248
+ * (ADR-0057).
249
+ */
250
+ private insertionAnchor;
208
251
  private toContinuousLng;
209
252
  protected createLayer(layerId: LayerId, style?: StyleOptions): MapLibreLayerHandle;
210
253
  protected destroyLayer(handle: MapLibreLayerHandle): void;
@@ -218,6 +261,21 @@ declare class MapLibreAdapter extends BaseMapAdapter<MapLibreLayerHandle, MapLib
218
261
  private updateSurfaceSizing;
219
262
  private buildGroupLayer;
220
263
  private updateGroupBundleSource;
264
+ /**
265
+ * Reorders bundle layers below `handle.mlLayerIds`. Walks groups back to
266
+ * front with a `beforeId` cursor, the same LIS-based stable-subset
267
+ * optimization as before.
268
+ *
269
+ * While terrain is active (ADR-0057), bundle layers instead split into two
270
+ * independently-tracked bands so every graphic contributes at most two
271
+ * render-to-texture stacks instead of one per graphic: a draped band
272
+ * (image overlays, fill/fill-pattern/line surfaces) anchored below the
273
+ * handle's own `-fill` layer, and an overlay band (the pin layer,
274
+ * circle/icon/text surfaces) anchored below the handle's own `-circle`
275
+ * layer. Without terrain, both cursors stay equal throughout, collapsing
276
+ * back to the single-cursor algorithm and the exact atomic per-graphic
277
+ * order (ADR-0039).
278
+ */
221
279
  private orderGroupBundles;
222
280
  private nativeLayerIds;
223
281
  /**
package/dist/index.mjs CHANGED
@@ -327,6 +327,12 @@ const PIN_GLOBE_RADIUS = EARTH_RADIUS;
327
327
  function pinMercator(position) {
328
328
  return MercatorCoordinate.fromLngLat([position[0], Math.max(-89.999999, Math.min(89.999999, position[1]))]);
329
329
  }
330
+ /** Scratch sphere position reused by the per-pin, per-frame depth() path. */
331
+ const depthSphere = [
332
+ 0,
333
+ 0,
334
+ 0
335
+ ];
330
336
  const multiply = (m, v) => [
331
337
  0,
332
338
  1,
@@ -367,6 +373,34 @@ var PinProjection = class {
367
373
  this.matrix[12 + row] = fallback[12 + row] + fallback[row] * xyScale * origin[0] * world + fallback[4 + row] * xyScale * origin[1] * world;
368
374
  }
369
375
  }
376
+ /**
377
+ * Globe-sphere position for a world/elevation anchor, written into `out` so the
378
+ * per-pin depth path stays allocation-free. Single source of the sphere maths
379
+ * shared with project().
380
+ */
381
+ sphere(worldX, worldY, elevation, out) {
382
+ const angle = (worldX / this.world + this.origin[0]) * Math.PI * 2 + Math.PI;
383
+ const t = Math.exp(Math.PI - (worldY / this.world + this.origin[1]) * Math.PI * 2);
384
+ const scale = 1 + elevation / PIN_GLOBE_RADIUS;
385
+ out[0] = Math.sin(angle) * 2 * t / (t * t + 1) * scale;
386
+ out[1] = (t * t - 1) / (t * t + 1) * scale;
387
+ out[2] = Math.cos(angle) * 2 * t / (t * t + 1) * scale;
388
+ return out;
389
+ }
390
+ /** Allocation-free depth-only projection for sorting large pin batches. */
391
+ depth(worldX, worldY, elevation) {
392
+ const m = this.matrix;
393
+ let z = m[2] * worldX + m[6] * worldY + m[10] * elevation + m[14];
394
+ let w = m[3] * worldX + m[7] * worldY + m[11] * elevation + m[15];
395
+ if (this.transition !== 0) {
396
+ const s = this.sphere(worldX, worldY, elevation, depthSphere);
397
+ const g = this.globeMatrix;
398
+ const blend = this.transition;
399
+ z = z * (1 - blend) + (g[2] * s[0] + g[6] * s[1] + g[10] * s[2] + g[14]) * blend;
400
+ w = w * (1 - blend) + (g[3] * s[0] + g[7] * s[1] + g[11] * s[2] + g[15]) * blend;
401
+ }
402
+ return z / w;
403
+ }
370
404
  project(worldX, worldY, elevation) {
371
405
  const flat = multiply(this.matrix, [
372
406
  worldX,
@@ -378,16 +412,11 @@ var PinProjection = class {
378
412
  clip: flat,
379
413
  horizon: 1
380
414
  };
381
- const x = worldX / this.world + this.origin[0];
382
- const y = worldY / this.world + this.origin[1];
383
- const angle = x * Math.PI * 2 + Math.PI;
384
- const t = Math.exp(Math.PI - y * Math.PI * 2);
385
- const scale = 1 + elevation / PIN_GLOBE_RADIUS;
386
- const sphere = [
387
- Math.sin(angle) * 2 * t / (t * t + 1),
388
- (t * t - 1) / (t * t + 1),
389
- Math.cos(angle) * 2 * t / (t * t + 1)
390
- ].map((n) => n * scale);
415
+ const sphere = this.sphere(worldX, worldY, elevation, [
416
+ 0,
417
+ 0,
418
+ 0
419
+ ]);
391
420
  const globe = multiply(this.globeMatrix, [...sphere, 1]);
392
421
  const horizon = 1 - (1 - sphere.reduce((sum, n, i) => sum + n * this.clippingPlane[i], this.clippingPlane[3])) * Math.max(0, (this.transition - .2) / .8);
393
422
  return {
@@ -439,22 +468,34 @@ function shader(gl, type, source) {
439
468
  }
440
469
  return s;
441
470
  }
471
+ /** Copies `source` into `cache`, reporting whether any element differed. */
472
+ function cacheChanged(cache, source) {
473
+ let changed = false;
474
+ for (let i = 0; i < cache.length; i++) {
475
+ if (cache[i] !== source[i]) changed = true;
476
+ cache[i] = source[i];
477
+ }
478
+ return changed;
479
+ }
442
480
  const elevationOrSeaLevel = (value) => Number.isFinite(value) ? value : 0;
443
481
  /**
444
482
  * Appends one instance's worth of vertex-shader input to `data` and extends `ranges`
445
483
  * accordingly. Hoisted out of rebuild()'s per-pin loop (ADR n/a) so pushing 10k+ pins
446
484
  * never allocates a fresh closure per pin — every varying value is an explicit param.
447
485
  */
448
- function appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionTerrain, kind, shape, color, a, target, mode) {
449
- if (highlightOnly && kind !== PIN_KIND.HIGHLIGHT) return;
450
- const first = data.length / STRIDE;
486
+ /** Extends the trailing draw range when it shares an atlas page, else starts a new one. */
487
+ function pushRange(ranges, page, first, count) {
451
488
  const previous = ranges[ranges.length - 1];
452
- if (previous?.page === image.page) previous.count++;
489
+ if (previous?.page === page) previous.count += count;
453
490
  else ranges.push({
454
- page: image.page,
491
+ page,
455
492
  first,
456
- count: 1
493
+ count
457
494
  });
495
+ }
496
+ function appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionTerrain, kind, shape, color, a, target, mode) {
497
+ if (highlightOnly && kind !== PIN_KIND.HIGHLIGHT) return;
498
+ pushRange(ranges, image.page, data.length / STRIDE, 1);
458
499
  data.push(...a, ...target, ...shape, ...color, ...image.uv, kind, mode, occlusionTerrain, instanceId);
459
500
  }
460
501
  /**
@@ -493,6 +534,20 @@ var PinLayer = class {
493
534
  atlas = new PinAtlas();
494
535
  pins = [];
495
536
  ranges = [];
537
+ instanceData = new Float32Array(0);
538
+ sortedData = new Float32Array(0);
539
+ paintOrder = [];
540
+ gpuOffsets = [];
541
+ depths = new Float64Array(0);
542
+ sortMatrix = new Float64Array(16).fill(NaN);
543
+ sortGlobeMatrix = new Float64Array(16).fill(NaN);
544
+ sortTransition = NaN;
545
+ sortWorld = NaN;
546
+ sortDirty = true;
547
+ /** View-wide far-to-near paint order; `false` restores canonical layer order. */
548
+ depthSorted = false;
549
+ /** Hoisted out of the per-frame sort so no comparator is allocated while panning. */
550
+ comparePaintOrder = (a, b) => (this.depthSorted ? this.depths[b] - this.depths[a] : 0) || a - b;
496
551
  version = 0;
497
552
  resourceRevision = -1;
498
553
  needsDepth = false;
@@ -646,6 +701,7 @@ var PinLayer = class {
646
701
  }
647
702
  this.pins = pins;
648
703
  this.prioritizesSelection = pins[0]?.presentation.prioritizePins ?? false;
704
+ this.depthSorted = pins[0]?.presentation.depthSort ?? false;
649
705
  if (this.elevationCache.size > pins.length * 60 + 1e3) this.elevationCache.clear();
650
706
  if (!this.program) {
651
707
  this.onStatus?.({
@@ -817,6 +873,7 @@ var PinLayer = class {
817
873
  }
818
874
  rebuild() {
819
875
  if (!this.ready) return;
876
+ this.sortDirty = true;
820
877
  if (!this.dirty) {
821
878
  const previousPending = new Set(this.pendingElevation);
822
879
  const center = {
@@ -833,8 +890,10 @@ var PinLayer = class {
833
890
  this.dirty = true;
834
891
  break;
835
892
  }
893
+ const patch = new Float32Array(data);
894
+ this.instanceData.set(patch, slice.offset);
836
895
  this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
837
- this.gl.bufferSubData(this.gl.ARRAY_BUFFER, slice.offset * Float32Array.BYTES_PER_ELEMENT, new Float32Array(data));
896
+ this.gl.bufferSubData(this.gl.ARRAY_BUFFER, this.gpuOffsets[i] * Float32Array.BYTES_PER_ELEMENT, patch);
838
897
  }
839
898
  if (!this.dirty) {
840
899
  this.changedPins.clear();
@@ -874,7 +933,11 @@ var PinLayer = class {
874
933
  });
875
934
  this.ranges = ranges;
876
935
  gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
877
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.DYNAMIC_DRAW);
936
+ this.instanceData = new Float32Array(data);
937
+ this.paintOrder = this.pins.map((_, i) => i);
938
+ this.gpuOffsets = this.slices.map((slice) => slice.offset);
939
+ this.depths = new Float64Array(this.pins.length);
940
+ gl.bufferData(gl.ARRAY_BUFFER, this.instanceData, gl.DYNAMIC_DRAW);
878
941
  this.dirty = false;
879
942
  this.changedPins.clear();
880
943
  }
@@ -893,8 +956,52 @@ var PinLayer = class {
893
956
  if (this.status.status !== "supported") return;
894
957
  this.frame = input;
895
958
  this.projection.update(input, this.origin, this.map.getCenter(), 512 * 2 ** this.map.getZoom(), this.map.getProjection()?.type === "globe");
959
+ this.updatePaintOrder();
896
960
  this.draw(context, false);
897
961
  }
962
+ /** Reorder existing instances only when camera/data changes, preserving atlas batches. */
963
+ updatePaintOrder() {
964
+ const sorted = this.depthSorted;
965
+ let changed = this.sortDirty;
966
+ if (sorted) {
967
+ if (cacheChanged(this.sortMatrix, this.matrix)) changed = true;
968
+ if (cacheChanged(this.sortGlobeMatrix, this.projection.globeMatrix)) changed = true;
969
+ if (this.sortTransition !== this.projection.transition) changed = true;
970
+ if (this.sortWorld !== this.projection.world) changed = true;
971
+ this.sortTransition = this.projection.transition;
972
+ this.sortWorld = this.projection.world;
973
+ }
974
+ if (!changed) return;
975
+ this.sortDirty = false;
976
+ if (sorted) for (let i = 0; i < this.pins.length; i++) {
977
+ const offset = this.slices[i].offset;
978
+ const data = this.instanceData;
979
+ const depth = this.projection.depth(data[offset] * this.projection.world, data[offset + 1] * this.projection.world, this.elevations[i]);
980
+ this.depths[i] = Number.isFinite(depth) ? depth : -Infinity;
981
+ }
982
+ const compare = this.comparePaintOrder;
983
+ let ordered = true;
984
+ for (let i = 1; i < this.paintOrder.length; i++) if (compare(this.paintOrder[i - 1], this.paintOrder[i]) > 0) {
985
+ ordered = false;
986
+ break;
987
+ }
988
+ if (ordered) return;
989
+ this.paintOrder.sort(compare);
990
+ if (this.sortedData.length !== this.instanceData.length) this.sortedData = new Float32Array(this.instanceData.length);
991
+ const ranges = [];
992
+ let offset = 0;
993
+ for (const i of this.paintOrder) {
994
+ const slice = this.slices[i];
995
+ this.sortedData.set(this.instanceData.subarray(slice.offset, slice.offset + slice.length), offset);
996
+ this.gpuOffsets[i] = offset;
997
+ const count = slice.length / STRIDE;
998
+ if (count) pushRange(ranges, slice.page, offset / STRIDE, count);
999
+ offset += slice.length;
1000
+ }
1001
+ this.ranges = ranges;
1002
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
1003
+ this.gl.bufferSubData(this.gl.ARRAY_BUFFER, 0, this.sortedData);
1004
+ }
898
1005
  draw(gl, pick) {
899
1006
  const u = this.uniforms ?? this.resolveUniforms(gl);
900
1007
  gl.useProgram(this.program);
@@ -985,7 +1092,10 @@ var PinLayer = class {
985
1092
  const pixels = new Uint8Array(rw * rh * 4);
986
1093
  gl.readPixels(x, y, rw, rh, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
987
1094
  let id = 0;
988
- for (let i = 0; i < pixels.length; i += 4) id = Math.max(id, pixels[i] + pixels[i + 1] * 256 + pixels[i + 2] * 65536);
1095
+ for (let i = 0; i < pixels.length; i += 4) {
1096
+ const candidate = pixels[i] + pixels[i + 1] * 256 + pixels[i + 2] * 65536;
1097
+ if (candidate && (!id || this.gpuOffsets[candidate - 1] > this.gpuOffsets[id - 1])) id = candidate;
1098
+ }
989
1099
  gl.disable(gl.SCISSOR_TEST);
990
1100
  gl.bindFramebuffer(gl.FRAMEBUFFER, previous);
991
1101
  this.map.painter.context.setDirty();
@@ -1195,13 +1305,44 @@ function coordinatesEqual(a, b) {
1195
1305
  }
1196
1306
  return true;
1197
1307
  }
1198
- function firstBundleLayerId(bundle) {
1199
- for (const overlay of bundle.overlays.values()) return overlay.layerId;
1308
+ /**
1309
+ * Paint channels MapLibre renders into a terrain render-to-texture stack
1310
+ * (background/fill/line/raster/hillshade) rather than directly onto the
1311
+ * screen. See ADR-0057.
1312
+ */
1313
+ const DRAPED_CHANNELS = new Set([
1314
+ "fill",
1315
+ "fill-pattern",
1316
+ "line"
1317
+ ]);
1318
+ function isDrapedChannel(channel) {
1319
+ return DRAPED_CHANNELS.has(channel);
1320
+ }
1321
+ /**
1322
+ * Bottom-most layer of a bundle, used as the next (earlier) group's anchor in
1323
+ * `orderGroupBundles`. With a `band` (terrain active, ADR-0057), only that
1324
+ * band's layers count: image overlays are always draped, the pin layer is
1325
+ * always overlay, and paint surfaces follow `isDrapedChannel`.
1326
+ */
1327
+ function firstBundleLayerId(bundle, band) {
1328
+ if (bundle.pinLayer && band !== "draped") return bundle.pinLayer.id;
1329
+ if (band !== "overlay") for (const overlay of bundle.overlays.values()) return overlay.layerId;
1200
1330
  for (const key of bundle.paintOrder) {
1201
1331
  const surface = bundle.paintSurfaces.get(key);
1202
- if (surface) return surface.layerId;
1332
+ if (!surface) continue;
1333
+ if (!band || isDrapedChannel(surface.channel) === (band === "draped")) return surface.layerId;
1203
1334
  }
1204
1335
  }
1336
+ /** Hides an always-empty handle-level anchor layer; see `createLayer`. */
1337
+ function hiddenLayer(layer) {
1338
+ return {
1339
+ ...layer,
1340
+ layout: {
1341
+ ..."layout" in layer ? layer.layout : {},
1342
+ visibility: "none"
1343
+ }
1344
+ };
1345
+ }
1205
1346
  function sameOrder(a, b) {
1206
1347
  if (a.length !== b.length) return false;
1207
1348
  for (let index = 0; index < a.length; index++) if (a[index] !== b[index]) return false;
@@ -1805,10 +1946,38 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1805
1946
  viewChangeMode;
1806
1947
  pendingIconImages = /* @__PURE__ */ new Set();
1807
1948
  scalesGroundPointSymbolsNatively = true;
1949
+ terrainActive = false;
1950
+ terrainBanding;
1951
+ handleTerrainChange = () => {
1952
+ const active = this.terrainBanding && !!this.map.getTerrain?.();
1953
+ if (active === this.terrainActive) return;
1954
+ this.terrainActive = active;
1955
+ for (const handle of this.layers.values()) this.orderGroupBundles(handle, handle.groupOrder, handle.groupOrder, new Set(handle.groupOrder));
1956
+ };
1808
1957
  constructor(map, options = {}) {
1809
1958
  super();
1810
1959
  this.map = map;
1811
1960
  this.viewChangeMode = options.viewChangeMode ?? "continuous";
1961
+ this.terrainBanding = options.terrainBanding ?? true;
1962
+ this.handleTerrainChange();
1963
+ this.map.on("terrain", this.handleTerrainChange);
1964
+ }
1965
+ /** Switches `terrainBanding` at runtime, re-banding every group if the effective state changes. */
1966
+ setTerrainBanding(enabled) {
1967
+ this.terrainBanding = enabled;
1968
+ this.handleTerrainChange();
1969
+ }
1970
+ destroyEngineResources() {
1971
+ this.map.off("terrain", this.handleTerrainChange);
1972
+ }
1973
+ /**
1974
+ * Insertion anchor for a new bundle layer: below the handle's layers, or —
1975
+ * for an overlay-band layer under terrain — below the overlay anchor, so it
1976
+ * never lands between draped layers and fragments a render-to-texture stack
1977
+ * (ADR-0057).
1978
+ */
1979
+ insertionAnchor(handle, draped) {
1980
+ return this.terrainActive && !draped ? handle.overlayAnchorId : handle.mlLayerIds[0];
1812
1981
  }
1813
1982
  toContinuousLng(lng) {
1814
1983
  return nearestWorldCopyLng(lng, this.map.getCenter().lng);
@@ -1822,25 +1991,25 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1822
1991
  });
1823
1992
  const mlLayerIds = [];
1824
1993
  const fillId = `${layerId}-fill`;
1825
- this.map.addLayer({
1994
+ this.map.addLayer(hiddenLayer({
1826
1995
  id: fillId,
1827
1996
  type: "fill",
1828
1997
  source: sourceId,
1829
1998
  filter: buildFillFilter(false),
1830
1999
  paint: buildFillPaint(style)
1831
- });
2000
+ }));
1832
2001
  mlLayerIds.push(fillId);
1833
2002
  const fillPatternId = `${layerId}-fill-pattern`;
1834
- this.map.addLayer({
2003
+ this.map.addLayer(hiddenLayer({
1835
2004
  id: fillPatternId,
1836
2005
  type: "fill",
1837
2006
  source: sourceId,
1838
2007
  filter: buildFillFilter(true),
1839
2008
  paint: FILL_PATTERN_PAINT
1840
- });
2009
+ }));
1841
2010
  mlLayerIds.push(fillPatternId);
1842
2011
  const lineId = `${layerId}-line`;
1843
- this.map.addLayer({
2012
+ this.map.addLayer(hiddenLayer({
1844
2013
  id: lineId,
1845
2014
  type: "line",
1846
2015
  source: sourceId,
@@ -1862,28 +2031,29 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1862
2031
  "line-cap": "round",
1863
2032
  "line-join": "round"
1864
2033
  }
1865
- });
2034
+ }));
1866
2035
  mlLayerIds.push(lineId);
1867
2036
  const circleId = `${layerId}-circle`;
1868
- this.map.addLayer({
2037
+ this.map.addLayer(hiddenLayer({
1869
2038
  id: circleId,
1870
2039
  type: "circle",
1871
2040
  source: sourceId,
1872
2041
  filter: buildCirclePointFilter(style),
1873
2042
  paint: buildCirclePaint(style)
1874
- });
2043
+ }));
1875
2044
  mlLayerIds.push(circleId);
1876
2045
  const iconId = `${layerId}-icon`;
1877
- this.map.addLayer(buildIconLayer(iconId, sourceId));
2046
+ this.map.addLayer(hiddenLayer(buildIconLayer(iconId, sourceId)));
1878
2047
  mlLayerIds.push(iconId);
1879
2048
  if (style?.text) {
1880
2049
  const symbolId = `${layerId}-symbol`;
1881
- this.map.addLayer(buildSymbolLayer(symbolId, sourceId, style.text));
2050
+ this.map.addLayer(hiddenLayer(buildSymbolLayer(symbolId, sourceId, style.text)));
1882
2051
  mlLayerIds.push(symbolId);
1883
2052
  }
1884
2053
  return {
1885
2054
  sourceId,
1886
2055
  mlLayerIds,
2056
+ overlayAnchorId: circleId,
1887
2057
  groupBundles: /* @__PURE__ */ new Map(),
1888
2058
  groupOrder: [],
1889
2059
  nextGroupBundleId: 0,
@@ -1897,8 +2067,14 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1897
2067
  if (this.map.getSource(handle.sourceId)) this.map.removeSource(handle.sourceId);
1898
2068
  }
1899
2069
  applyFeatureGroups(layerId, input) {
2070
+ const depthSorted = input.find((group) => group.pin)?.pin.presentation.depthSort ?? false;
2071
+ const sortedPins = [];
1900
2072
  const groups = [];
1901
2073
  for (const group of input) if (group.pin && this.getPinPresentationCapability().status !== "unsupported") {
2074
+ if (depthSorted) {
2075
+ sortedPins.push(group.pin);
2076
+ continue;
2077
+ }
1902
2078
  const previous = groups[groups.length - 1];
1903
2079
  if (previous?.pinBatch) previous.pinBatch.push(group.pin);
1904
2080
  else groups.push({
@@ -1911,6 +2087,12 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1911
2087
  ...group,
1912
2088
  id: `ordinary:${group.id}`
1913
2089
  });
2090
+ if (sortedPins.length) groups.push({
2091
+ id: "pin-depth-sorted",
2092
+ features: [],
2093
+ iconResources: [],
2094
+ pinBatch: sortedPins
2095
+ });
1914
2096
  const handle = this.layers.get(layerId);
1915
2097
  if (!handle) return;
1916
2098
  const allPins = groups.flatMap((group) => group.pinBatch ?? []);
@@ -1940,7 +2122,7 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1940
2122
  if (group.pinBatch) {
1941
2123
  if (!bundle.pinLayer) {
1942
2124
  const custom = new PinLayer(`${bundle.sourceId}-pins`, handle.pinResources, this.emitPinStatus);
1943
- this.map.addLayer(custom, handle.mlLayerIds[0]);
2125
+ this.map.addLayer(custom, this.insertionAnchor(handle, false));
1944
2126
  bundle.pinLayer = custom;
1945
2127
  bundlesNeedingOrder.add(group.id);
1946
2128
  }
@@ -2114,7 +2296,7 @@ var MapLibreAdapter = class extends BaseMapAdapter {
2114
2296
  featureId
2115
2297
  ]
2116
2298
  };
2117
- this.map.addLayer(layer, handle.mlLayerIds[0]);
2299
+ this.map.addLayer(layer, this.insertionAnchor(handle, isDrapedChannel(channel)));
2118
2300
  bundle.paintSurfaces.set(key, {
2119
2301
  layerId,
2120
2302
  channel,
@@ -2210,23 +2392,44 @@ var MapLibreAdapter = class extends BaseMapAdapter {
2210
2392
  bundle.sourceInitialized = true;
2211
2393
  if (isPromiseLike(result)) Promise.resolve(result).catch(() => {});
2212
2394
  }
2395
+ /**
2396
+ * Reorders bundle layers below `handle.mlLayerIds`. Walks groups back to
2397
+ * front with a `beforeId` cursor, the same LIS-based stable-subset
2398
+ * optimization as before.
2399
+ *
2400
+ * While terrain is active (ADR-0057), bundle layers instead split into two
2401
+ * independently-tracked bands so every graphic contributes at most two
2402
+ * render-to-texture stacks instead of one per graphic: a draped band
2403
+ * (image overlays, fill/fill-pattern/line surfaces) anchored below the
2404
+ * handle's own `-fill` layer, and an overlay band (the pin layer,
2405
+ * circle/icon/text surfaces) anchored below the handle's own `-circle`
2406
+ * layer. Without terrain, both cursors stay equal throughout, collapsing
2407
+ * back to the single-cursor algorithm and the exact atomic per-graphic
2408
+ * order (ADR-0039).
2409
+ */
2213
2410
  orderGroupBundles(handle, previousGroupIds, desiredGroupIds, forcedGroupIds) {
2214
- let beforeId = handle.mlLayerIds[0];
2215
- if (!beforeId) return;
2411
+ const rootBeforeId = handle.mlLayerIds[0];
2412
+ if (!rootBeforeId) return;
2413
+ let drapedBeforeId = rootBeforeId;
2414
+ let overlayBeforeId = this.terrainActive ? handle.overlayAnchorId : rootBeforeId;
2216
2415
  const stableGroupIds = sameOrder(previousGroupIds, desiredGroupIds) ? null : longestStableGroupIds(previousGroupIds, desiredGroupIds);
2217
2416
  for (let index = desiredGroupIds.length - 1; index >= 0; index--) {
2218
2417
  const groupId = desiredGroupIds[index];
2219
2418
  const bundle = handle.groupBundles.get(groupId);
2220
2419
  if (!bundle) continue;
2221
2420
  if (forcedGroupIds.has(groupId) || stableGroupIds !== null && !stableGroupIds.has(groupId)) {
2222
- if (bundle.pinLayer && this.map.getLayer(bundle.pinLayer.id)) this.map.moveLayer(bundle.pinLayer.id, beforeId);
2223
- for (const overlay of bundle.overlays.values()) if (this.map.getLayer(overlay.layerId)) this.map.moveLayer(overlay.layerId, beforeId);
2421
+ if (bundle.pinLayer && this.map.getLayer(bundle.pinLayer.id)) this.map.moveLayer(bundle.pinLayer.id, overlayBeforeId);
2422
+ for (const overlay of bundle.overlays.values()) if (this.map.getLayer(overlay.layerId)) this.map.moveLayer(overlay.layerId, drapedBeforeId);
2224
2423
  for (const key of bundle.paintOrder) {
2225
2424
  const surface = bundle.paintSurfaces.get(key);
2226
- if (surface && this.map.getLayer(surface.layerId)) this.map.moveLayer(surface.layerId, beforeId);
2425
+ if (!surface || !this.map.getLayer(surface.layerId)) continue;
2426
+ this.map.moveLayer(surface.layerId, isDrapedChannel(surface.channel) ? drapedBeforeId : overlayBeforeId);
2227
2427
  }
2228
2428
  }
2229
- beforeId = bundle.pinLayer?.id ?? firstBundleLayerId(bundle) ?? beforeId;
2429
+ if (this.terrainActive) {
2430
+ drapedBeforeId = firstBundleLayerId(bundle, "draped") ?? drapedBeforeId;
2431
+ overlayBeforeId = firstBundleLayerId(bundle, "overlay") ?? overlayBeforeId;
2432
+ } else drapedBeforeId = overlayBeforeId = firstBundleLayerId(bundle) ?? drapedBeforeId;
2230
2433
  }
2231
2434
  }
2232
2435
  nativeLayerIds(handle) {
@@ -2467,7 +2670,7 @@ var MapLibreAdapter = class extends BaseMapAdapter {
2467
2670
  for (const bundle of handle.groupBundles.values()) for (const surface of bundle.paintSurfaces.values()) this.applyChannelStyle(surface.layerId, surface.channel, style, resolvedStyle, surface.featureId);
2468
2671
  if (style.text && !handle.mlLayerIds.some((id) => id.endsWith("-symbol"))) {
2469
2672
  const symbolId = `${layerId}-symbol`;
2470
- this.map.addLayer(buildSymbolLayer(symbolId, handle.sourceId, resolvedStyle.text));
2673
+ this.map.addLayer(hiddenLayer(buildSymbolLayer(symbolId, handle.sourceId, resolvedStyle.text)));
2471
2674
  handle.mlLayerIds.push(symbolId);
2472
2675
  }
2473
2676
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/tactical-draw-adapter-maplibre",
3
- "version": "0.4.4",
3
+ "version": "0.6.0",
4
4
  "description": "MapLibre GL JS engine adapter for @orbat-mapper/tactical-draw. Draw and edit tactical control measures (MIL-STD-2525 / APP-6) on a MapLibre map.",
5
5
  "license": "MIT",
6
6
  "author": "Orbat Mapper",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@orbat-mapper/control-measures": "^0.27.0",
41
- "@orbat-mapper/tactical-draw": "^0.9.2"
41
+ "@orbat-mapper/tactical-draw": "^0.10.1"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "maplibre-gl": ">=5.8.0 <7.0.0"