@orbat-mapper/tactical-draw-adapter-maplibre 0.4.4 → 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 +16 -0
- package/dist/index.mjs +141 -19
- package/package.json +2 -2
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
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
449
|
-
|
|
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 ===
|
|
489
|
+
if (previous?.page === page) previous.count += count;
|
|
453
490
|
else ranges.push({
|
|
454
|
-
page
|
|
491
|
+
page,
|
|
455
492
|
first,
|
|
456
|
-
count
|
|
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,
|
|
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
|
-
|
|
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)
|
|
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();
|
|
@@ -1897,8 +2007,14 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1897
2007
|
if (this.map.getSource(handle.sourceId)) this.map.removeSource(handle.sourceId);
|
|
1898
2008
|
}
|
|
1899
2009
|
applyFeatureGroups(layerId, input) {
|
|
2010
|
+
const depthSorted = input.find((group) => group.pin)?.pin.presentation.depthSort ?? false;
|
|
2011
|
+
const sortedPins = [];
|
|
1900
2012
|
const groups = [];
|
|
1901
2013
|
for (const group of input) if (group.pin && this.getPinPresentationCapability().status !== "unsupported") {
|
|
2014
|
+
if (depthSorted) {
|
|
2015
|
+
sortedPins.push(group.pin);
|
|
2016
|
+
continue;
|
|
2017
|
+
}
|
|
1902
2018
|
const previous = groups[groups.length - 1];
|
|
1903
2019
|
if (previous?.pinBatch) previous.pinBatch.push(group.pin);
|
|
1904
2020
|
else groups.push({
|
|
@@ -1911,6 +2027,12 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1911
2027
|
...group,
|
|
1912
2028
|
id: `ordinary:${group.id}`
|
|
1913
2029
|
});
|
|
2030
|
+
if (sortedPins.length) groups.push({
|
|
2031
|
+
id: "pin-depth-sorted",
|
|
2032
|
+
features: [],
|
|
2033
|
+
iconResources: [],
|
|
2034
|
+
pinBatch: sortedPins
|
|
2035
|
+
});
|
|
1914
2036
|
const handle = this.layers.get(layerId);
|
|
1915
2037
|
if (!handle) return;
|
|
1916
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.
|
|
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.
|
|
41
|
+
"@orbat-mapper/tactical-draw": "^0.10.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"maplibre-gl": ">=5.8.0 <7.0.0"
|