@orbat-mapper/tactical-draw-adapter-maplibre 0.4.3 → 0.5.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;
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;
@@ -525,21 +580,25 @@ var PinLayer = class {
525
580
  const key = `${position[0]}/${position[1]}`;
526
581
  const cached = this.elevationCache.get(key);
527
582
  if (cached !== void 0) return cached;
528
- const terrain = this.map.terrain;
529
- const manager = terrain.tileManager ?? terrain.sourceCache;
530
- const source = terrain.tileManager ? terrain.tileManager.getSource() : terrain.sourceCache.sourceCache.getSource();
531
- const { tileID, mercatorX, mercatorY } = terrain._getOverscaledTileIDFromLngLatZoom(new LngLat(position[0], position[1]), Math.min(manager.maxzoom, source.maxzoom + manager.deltaZoom));
532
- const x = mercatorX % 8192;
533
- const y = mercatorY % 8192;
534
- const getSampler = terrain.getElevationSampler ?? terrain._getElevationSampler;
535
583
  let value;
536
- if (getSampler) {
537
- const sampler = getSampler.call(terrain, tileID);
538
- if (!sampler) return NaN;
539
- value = sampler(x, y, 8192) * terrain.exaggeration;
540
- } else {
541
- if (!manager.getSourceTile(tileID, true)?.dem) return NaN;
542
- value = terrain.getElevation(tileID, x, y, 8192);
584
+ try {
585
+ const terrain = this.map.terrain;
586
+ const manager = terrain.tileManager ?? terrain.sourceCache;
587
+ const source = terrain.tileManager ? terrain.tileManager.getSource() : terrain.sourceCache.sourceCache.getSource();
588
+ const { tileID, mercatorX, mercatorY } = terrain._getOverscaledTileIDFromLngLatZoom(new LngLat(position[0], position[1]), Math.min(manager.maxzoom, source.maxzoom + manager.deltaZoom));
589
+ const x = mercatorX % 8192;
590
+ const y = mercatorY % 8192;
591
+ const getSampler = terrain.getElevationSampler ?? terrain._getElevationSampler;
592
+ if (getSampler) {
593
+ const sampler = getSampler.call(terrain, tileID);
594
+ if (!sampler) return NaN;
595
+ value = sampler(x, y, 8192) * terrain.exaggeration;
596
+ } else {
597
+ if (!manager.getSourceTile(tileID, true)?.dem) return NaN;
598
+ value = terrain.getElevation(tileID, x, y, 8192);
599
+ }
600
+ } catch {
601
+ return NaN;
543
602
  }
544
603
  this.elevationCache.set(key, value);
545
604
  return value;
@@ -642,6 +701,7 @@ var PinLayer = class {
642
701
  }
643
702
  this.pins = pins;
644
703
  this.prioritizesSelection = pins[0]?.presentation.prioritizePins ?? false;
704
+ this.depthSorted = pins[0]?.presentation.depthSort ?? false;
645
705
  if (this.elevationCache.size > pins.length * 60 + 1e3) this.elevationCache.clear();
646
706
  if (!this.program) {
647
707
  this.onStatus?.({
@@ -813,6 +873,7 @@ var PinLayer = class {
813
873
  }
814
874
  rebuild() {
815
875
  if (!this.ready) return;
876
+ this.sortDirty = true;
816
877
  if (!this.dirty) {
817
878
  const previousPending = new Set(this.pendingElevation);
818
879
  const center = {
@@ -829,8 +890,10 @@ var PinLayer = class {
829
890
  this.dirty = true;
830
891
  break;
831
892
  }
893
+ const patch = new Float32Array(data);
894
+ this.instanceData.set(patch, slice.offset);
832
895
  this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
833
- 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);
834
897
  }
835
898
  if (!this.dirty) {
836
899
  this.changedPins.clear();
@@ -870,7 +933,11 @@ var PinLayer = class {
870
933
  });
871
934
  this.ranges = ranges;
872
935
  gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
873
- 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);
874
941
  this.dirty = false;
875
942
  this.changedPins.clear();
876
943
  }
@@ -889,8 +956,52 @@ var PinLayer = class {
889
956
  if (this.status.status !== "supported") return;
890
957
  this.frame = input;
891
958
  this.projection.update(input, this.origin, this.map.getCenter(), 512 * 2 ** this.map.getZoom(), this.map.getProjection()?.type === "globe");
959
+ this.updatePaintOrder();
892
960
  this.draw(context, false);
893
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
+ }
894
1005
  draw(gl, pick) {
895
1006
  const u = this.uniforms ?? this.resolveUniforms(gl);
896
1007
  gl.useProgram(this.program);
@@ -981,7 +1092,10 @@ var PinLayer = class {
981
1092
  const pixels = new Uint8Array(rw * rh * 4);
982
1093
  gl.readPixels(x, y, rw, rh, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
983
1094
  let id = 0;
984
- 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
+ }
985
1099
  gl.disable(gl.SCISSOR_TEST);
986
1100
  gl.bindFramebuffer(gl.FRAMEBUFFER, previous);
987
1101
  this.map.painter.context.setDirty();
@@ -1893,8 +2007,14 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1893
2007
  if (this.map.getSource(handle.sourceId)) this.map.removeSource(handle.sourceId);
1894
2008
  }
1895
2009
  applyFeatureGroups(layerId, input) {
2010
+ const depthSorted = input.find((group) => group.pin)?.pin.presentation.depthSort ?? false;
2011
+ const sortedPins = [];
1896
2012
  const groups = [];
1897
2013
  for (const group of input) if (group.pin && this.getPinPresentationCapability().status !== "unsupported") {
2014
+ if (depthSorted) {
2015
+ sortedPins.push(group.pin);
2016
+ continue;
2017
+ }
1898
2018
  const previous = groups[groups.length - 1];
1899
2019
  if (previous?.pinBatch) previous.pinBatch.push(group.pin);
1900
2020
  else groups.push({
@@ -1907,6 +2027,12 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1907
2027
  ...group,
1908
2028
  id: `ordinary:${group.id}`
1909
2029
  });
2030
+ if (sortedPins.length) groups.push({
2031
+ id: "pin-depth-sorted",
2032
+ features: [],
2033
+ iconResources: [],
2034
+ pinBatch: sortedPins
2035
+ });
1910
2036
  const handle = this.layers.get(layerId);
1911
2037
  if (!handle) return;
1912
2038
  const allPins = groups.flatMap((group) => group.pinBatch ?? []);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/tactical-draw-adapter-maplibre",
3
- "version": "0.4.3",
3
+ "version": "0.5.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.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "maplibre-gl": ">=5.8.0 <7.0.0"