@orbat-mapper/tactical-draw-adapter-maplibre 0.7.3 → 0.8.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
@@ -78,6 +78,18 @@ declare class PinLayer implements CustomLayerInterface {
78
78
  private sortDirty;
79
79
  /** View-wide far-to-near paint order; `false` restores canonical layer order. */
80
80
  private depthSorted;
81
+ /** View-wide presentation while zoom or distance scaling is on, else undefined. */
82
+ private scaling;
83
+ /**
84
+ * Pin scaling reference, derived from the camera pose rather than MapLibre's center
85
+ * point: after a terrain gesture MapLibre re-seats the center on the terrain and
86
+ * recomputes zoom with the camera fixed, which would otherwise snap pin sizes.
87
+ */
88
+ private reference;
89
+ /** Per-frame scratch, so the reference update allocates nothing in flat mode. */
90
+ private cameraScratch;
91
+ private rayScratch;
92
+ private positionScratch;
81
93
  /** Hoisted out of the per-frame sort so no comparator is allocated while panning. */
82
94
  private comparePaintOrder;
83
95
  private version;
@@ -103,6 +115,8 @@ declare class PinLayer implements CustomLayerInterface {
103
115
  private status;
104
116
  private refreshTerrain;
105
117
  private elevation;
118
+ /** Uncached DEM read; NaN while the DEM tile is missing or changing. */
119
+ private sampleElevation;
106
120
  has(id: string): boolean;
107
121
  constructor(id: string, resources: PinResources, onStatus?: ((event: PinPresentationStatusEvent) => void) | undefined);
108
122
  private restoreContext;
@@ -127,6 +141,45 @@ declare class PinLayer implements CustomLayerInterface {
127
141
  * — those stay at the call site, as they aren't part of the shared matrix math.
128
142
  */
129
143
  private projectToPixel;
144
+ /** View-wide, so any pin's value describes the layer (like depthSort). */
145
+ private zoomScale;
146
+ /**
147
+ * Per frame: move the reference elevation toward the terrain under the view-center
148
+ * ray, then take the clip w where that ray crosses it. Pin sizes then depend only
149
+ * on the camera and that plane. The plane adapts only in proportion to camera
150
+ * motion, never over time: MapLibre re-seats its center (and zoom) on the terrain
151
+ * after a gesture with the camera fixed, and a stationary camera must not resize
152
+ * pins. The effective zoom is the zoom that plane would give, invariant under that
153
+ * re-seat (camera fixed, zoom and pixel units rescaled).
154
+ */
155
+ private updateScaleReference;
156
+ /**
157
+ * Camera position as [Mercator x, Mercator y, meters up] written into `out`,
158
+ * independent of zoom and of the layer origin, so it is unchanged when MapLibre
159
+ * re-seats its center. It is the point where clip x, y and w all vanish: rows 0, 1
160
+ * and 3 of the column-major matrix, solved by Cramer's rule. False if singular.
161
+ */
162
+ private cameraPosition;
163
+ private rayDepth;
164
+ /**
165
+ * Where the view-center ray meets elevation `z`: world x, y (layer pixels from the
166
+ * origin) and clip w, in a reused scratch object. Flat projection only; the globe
167
+ * falls back to the origin.
168
+ */
169
+ private rayHit;
170
+ /**
171
+ * Terrain height where the view-center ray meets the current reference plane. Unlike
172
+ * MapLibre's center elevation, which is frozen during gestures and re-seated after
173
+ * them, this follows the camera continuously and is unchanged while it is still.
174
+ */
175
+ private rayTerrainElevation;
176
+ /** Elevation of the distance-scaling reference plane; sea level until one is set. */
177
+ private referenceElevation;
178
+ /**
179
+ * Combined zoom and distance scale for a pin anchored at world (x, y), mirroring the
180
+ * shader: distance is horizontal, the anchor's clip w at the reference elevation.
181
+ */
182
+ private pinScale;
130
183
  footprint(id: string): PinFootprint | null;
131
184
  moveFromPixel(id: string, attachment: PixelCoordinate): number[] | null;
132
185
  onRemove(_map: Map$1, context: WebGLRenderingContext | WebGL2RenderingContext): void;
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as maplibregl from "maplibre-gl";
2
2
  import { LngLat, MercatorCoordinate } from "maplibre-gl";
3
- import { BaseMapAdapter, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, HATCH_PATTERNS, PointerCallbackHub, TEXT_HALO_COLOR, TOUCH_HIT_TOLERANCE_PX, bindAbortable, evaluatePickCandidates, fillPatternImageId, makeFillPatternImage, mergeNativeAndCachedPickFeatures, nearestWorldCopyLng, parseColor, pickHitTolerance, pickPointerType, resolveTextHaloWidth } from "@orbat-mapper/tactical-draw";
3
+ import { BaseMapAdapter, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, HATCH_PATTERNS, PointerCallbackHub, TEXT_HALO_COLOR, TOUCH_HIT_TOLERANCE_PX, bindAbortable, combinePinScale, evaluatePickCandidates, fillPatternImageId, makeFillPatternImage, mergeNativeAndCachedPickFeatures, nearestWorldCopyLng, parseColor, pickHitTolerance, pickPointerType, pinDistanceScale, pinZoomScale, resolveTextHaloWidth } from "@orbat-mapper/tactical-draw";
4
4
  import { EARTH_RADIUS, project, unproject } from "@orbat-mapper/control-measures";
5
5
  //#region src/pin-atlas.ts
6
6
  /**
@@ -219,6 +219,16 @@ vec4 projectPin(vec3 position, out float horizon) {
219
219
  return mix(flatClip,globe,u_globeTransition);
220
220
  }
221
221
  uniform vec2 u_view;
222
+ // Zoom-out shrink for screen-space pixel dimensions (lift, dot, plate, symbol, halos).
223
+ // Stem width is left alone; fragment shading stays in unscaled local units.
224
+ uniform float u_scale;
225
+ // Perspective sizing, mirroring pinDistanceScale(): the exponent, 0 when off;
226
+ // u_depthRef is the reference clip w at u_refElevation.
227
+ uniform float u_distanceStrength;
228
+ uniform float u_depthRef;
229
+ uniform float u_refElevation;
230
+ // Bounds on zoom × distance (sizeLimits in combinePinScale); (0, huge) when both are off.
231
+ uniform vec2 u_sizeLimits;
222
232
  out vec2 v_local;
223
233
  out vec2 v_uv;
224
234
  flat out vec4 v_bounds;
@@ -232,14 +242,26 @@ void main() {
232
242
  float groundHorizon, targetHorizon;
233
243
  vec4 ground = projectPin(a_ground.xyz, groundHorizon);
234
244
  vec4 target = projectPin(a_target.xyz, targetHorizon);
235
- if(a_flags.y < .5) { target = ground; targetHorizon = groundHorizon; target.y += a_ground.w * 2. / u_view.y * target.w; }
245
+ float scale = u_scale;
246
+ if(u_distanceStrength > 0.) {
247
+ // Horizontal distance: measure the anchor at the reference elevation, so relief
248
+ // (a unit in a valley below the reference) never resizes pins while zooming.
249
+ // Flat clip w is affine in z, so only the globe needs another projection.
250
+ float refHorizon, depth;
251
+ if(u_globeTransition == 0.) depth = ground.w + (u_refElevation - a_ground.z) * u_matrix[2][3];
252
+ else depth = projectPin(vec3(a_ground.xy, u_refElevation), refHorizon).w;
253
+ if(depth > 0.)
254
+ scale *= pow(u_depthRef / depth, u_distanceStrength);
255
+ }
256
+ scale = clamp(scale, u_sizeLimits.x, u_sizeLimits.y);
257
+ if(a_flags.y < .5) { target = ground; targetHorizon = groundHorizon; target.y += a_ground.w * scale * 2. / u_view.y * target.w; }
236
258
  v_horizon = a_flags.x < .5 ? groundHorizon : (a_flags.x < 1.5 ? mix(groundHorizon,targetHorizon,c.y) : targetHorizon);
237
259
  vec2 gp = ground.xy / ground.w * u_view * .5;
238
260
  vec2 tp = target.xy / target.w * u_view * .5;
239
261
  float kind = a_flags.x;
240
262
  vec4 clip = target;
241
263
  vec2 offset;
242
- if(kind < .5) { clip = ground; offset = (c - .5) * a_shape.xy; }
264
+ if(kind < .5) { clip = ground; offset = (c - .5) * a_shape.xy * scale; }
243
265
  else if(kind < 1.5) {
244
266
  vec2 delta = tp-gp; float len = length(delta);
245
267
  vec2 dir = len > .0001 ? delta/len : vec2(0,1);
@@ -247,7 +269,7 @@ void main() {
247
269
  vec2 p = mix(gp,tp,c.y) + vec2(-dir.y,dir.x) * (c.x-.5)*(a_shape.x+1.);
248
270
  clip = vec4(p/u_view*2., mix(ground.z/ground.w,target.z/target.w,c.y), 1.);
249
271
  offset = vec2(0);
250
- } else { offset = (c - vec2(.5,0)) * a_shape.xy + a_shape.zw; }
272
+ } else { offset = ((c - vec2(.5,0)) * a_shape.xy + a_shape.zw) * scale; }
251
273
  clip.xy += offset * 2. / u_view * clip.w;
252
274
  if (ground.w <= 0. || target.w <= 0.) clip = vec4(2,2,2,1);
253
275
  gl_Position=clip;
@@ -336,6 +358,10 @@ const PIN_GLOBE_RADIUS = EARTH_RADIUS;
336
358
  function pinMercator(position) {
337
359
  return MercatorCoordinate.fromLngLat([position[0], Math.max(-89.999999, Math.min(89.999999, position[1]))]);
338
360
  }
361
+ /** Latitude in radians of Mercator `y`; allocation-free, unlike MercatorCoordinate.toLngLat. */
362
+ function mercatorLatitude(y) {
363
+ return Math.atan(Math.sinh(Math.PI * (1 - 2 * y)));
364
+ }
339
365
  /** Scratch sphere position reused by the per-pin, per-frame depth() path. */
340
366
  const depthSphere = [
341
367
  0,
@@ -555,6 +581,42 @@ var PinLayer = class {
555
581
  sortDirty = true;
556
582
  /** View-wide far-to-near paint order; `false` restores canonical layer order. */
557
583
  depthSorted = false;
584
+ /** View-wide presentation while zoom or distance scaling is on, else undefined. */
585
+ scaling;
586
+ /**
587
+ * Pin scaling reference, derived from the camera pose rather than MapLibre's center
588
+ * point: after a terrain gesture MapLibre re-seats the center on the terrain and
589
+ * recomputes zoom with the camera fixed, which would otherwise snap pin sizes.
590
+ */
591
+ reference = {
592
+ elevation: NaN,
593
+ /**
594
+ * Clip w where distance scaling is 1. It tracks the terrain height at the center
595
+ * (see updateScaleReference): a sea-level reference drifts from the terrain by an
596
+ * offset that doubles in pixels each zoom level, making pins pulse while zooming.
597
+ */
598
+ depth: NaN,
599
+ zoom: NaN,
600
+ camera: [
601
+ NaN,
602
+ NaN,
603
+ NaN
604
+ ],
605
+ bearing: 0,
606
+ pitch: 0
607
+ };
608
+ /** Per-frame scratch, so the reference update allocates nothing in flat mode. */
609
+ cameraScratch = [
610
+ NaN,
611
+ NaN,
612
+ NaN
613
+ ];
614
+ rayScratch = {
615
+ x: 0,
616
+ y: 0,
617
+ depth: NaN
618
+ };
619
+ positionScratch = [0, 0];
558
620
  /** Hoisted out of the per-frame sort so no comparator is allocated while panning. */
559
621
  comparePaintOrder = (a, b) => (this.depthSorted ? this.depths[b] - this.depths[a] : 0) || a - b;
560
622
  version = 0;
@@ -589,6 +651,13 @@ var PinLayer = class {
589
651
  const key = `${position[0]}/${position[1]}`;
590
652
  const cached = this.elevationCache.get(key);
591
653
  if (cached !== void 0) return cached;
654
+ const value = this.sampleElevation(position);
655
+ if (Number.isFinite(value)) this.elevationCache.set(key, value);
656
+ return value;
657
+ }
658
+ /** Uncached DEM read; NaN while the DEM tile is missing or changing. */
659
+ sampleElevation(position) {
660
+ if (!this.map.getTerrain() || Math.abs(position[1]) >= 85.051129) return 0;
592
661
  let value;
593
662
  try {
594
663
  const terrain = this.map.terrain;
@@ -609,7 +678,6 @@ var PinLayer = class {
609
678
  } catch {
610
679
  return NaN;
611
680
  }
612
- this.elevationCache.set(key, value);
613
681
  return value;
614
682
  }
615
683
  has(id) {
@@ -679,6 +747,11 @@ var PinLayer = class {
679
747
  globeTransition: gl.getUniformLocation(program, "u_globeTransition"),
680
748
  world: gl.getUniformLocation(program, "u_world"),
681
749
  view: gl.getUniformLocation(program, "u_view"),
750
+ scale: gl.getUniformLocation(program, "u_scale"),
751
+ distanceStrength: gl.getUniformLocation(program, "u_distanceStrength"),
752
+ depthRef: gl.getUniformLocation(program, "u_depthRef"),
753
+ refElevation: gl.getUniformLocation(program, "u_refElevation"),
754
+ sizeLimits: gl.getUniformLocation(program, "u_sizeLimits"),
682
755
  buffer: gl.getUniformLocation(program, "u_buffer"),
683
756
  pick: gl.getUniformLocation(program, "u_pick"),
684
757
  atlas: gl.getUniformLocation(program, "u_atlas"),
@@ -711,6 +784,8 @@ var PinLayer = class {
711
784
  this.pins = pins;
712
785
  this.prioritizesSelection = pins[0]?.presentation.prioritizePins ?? false;
713
786
  this.depthSorted = pins[0]?.presentation.depthSort ?? false;
787
+ const view = pins[0]?.presentation;
788
+ this.scaling = view && (view.zoomScale.enabled || view.distanceScale.enabled) ? view : void 0;
714
789
  if (this.elevationCache.size > pins.length * 60 + 1e3) this.elevationCache.clear();
715
790
  if (!this.program) {
716
791
  this.onStatus?.({
@@ -965,6 +1040,8 @@ var PinLayer = class {
965
1040
  if (this.status.status !== "supported") return;
966
1041
  this.frame = input;
967
1042
  this.projection.update(input, this.origin, this.map.getCenter(), 512 * 2 ** this.map.getZoom(), this.map.getProjection()?.type === "globe");
1043
+ if (this.scaling) this.updateScaleReference();
1044
+ else this.reference.elevation = this.reference.depth = this.reference.zoom = NaN;
968
1045
  this.updatePaintOrder();
969
1046
  this.draw(context, false);
970
1047
  }
@@ -1034,6 +1111,12 @@ var PinLayer = class {
1034
1111
  gl.uniform1f(u.globeTransition, this.projection.transition);
1035
1112
  gl.uniform1f(u.world, 512 * 2 ** this.map.getZoom());
1036
1113
  gl.uniform2f(u.view, width, height);
1114
+ gl.uniform1f(u.scale, this.zoomScale());
1115
+ const distance = this.scaling?.distanceScale, limits = this.scaling?.sizeLimits;
1116
+ gl.uniform1f(u.distanceStrength, distance?.enabled ? distance.strength : 0);
1117
+ gl.uniform2f(u.sizeLimits, limits ? Math.min(limits.minScale, limits.maxScale) : 0, limits?.maxScale ?? 1e30);
1118
+ gl.uniform1f(u.depthRef, this.reference.depth);
1119
+ gl.uniform1f(u.refElevation, this.referenceElevation());
1037
1120
  gl.uniform2f(u.buffer, gl.drawingBufferWidth, gl.drawingBufferHeight);
1038
1121
  gl.uniform1i(u.pick, pick ? 1 : 0);
1039
1122
  gl.uniform1i(u.atlas, 0);
@@ -1123,6 +1206,123 @@ var PinLayer = class {
1123
1206
  const width = this.map.getContainer().clientWidth, height = this.map.getContainer().clientHeight;
1124
1207
  return [(c[0] / c[3] + 1) * width / 2, (1 - c[1] / c[3]) * height / 2];
1125
1208
  }
1209
+ /** View-wide, so any pin's value describes the layer (like depthSort). */
1210
+ zoomScale() {
1211
+ if (!this.scaling) return 1;
1212
+ const zoom = Number.isFinite(this.reference.zoom) ? this.reference.zoom : this.map.getZoom();
1213
+ return pinZoomScale(this.scaling, zoom);
1214
+ }
1215
+ /**
1216
+ * Per frame: move the reference elevation toward the terrain under the view-center
1217
+ * ray, then take the clip w where that ray crosses it. Pin sizes then depend only
1218
+ * on the camera and that plane. The plane adapts only in proportion to camera
1219
+ * motion, never over time: MapLibre re-seats its center (and zoom) on the terrain
1220
+ * after a gesture with the camera fixed, and a stationary camera must not resize
1221
+ * pins. The effective zoom is the zoom that plane would give, invariant under that
1222
+ * re-seat (camera fixed, zoom and pixel units rescaled).
1223
+ */
1224
+ updateScaleReference() {
1225
+ const target = this.rayTerrainElevation();
1226
+ const ref = this.reference;
1227
+ const camera = this.cameraPosition(this.cameraScratch) ? this.cameraScratch : ref.camera;
1228
+ const bearing = this.map.getBearing(), pitch = this.map.getPitch();
1229
+ if (!Number.isFinite(ref.elevation)) ref.elevation = target;
1230
+ else {
1231
+ const height = camera[2] - ref.elevation;
1232
+ const lat = mercatorLatitude(camera[1]);
1233
+ const metersPerMercator = 2 * Math.PI * PIN_GLOBE_RADIUS * Math.cos(lat);
1234
+ const travel = Math.hypot((camera[0] - ref.camera[0]) * metersPerMercator, (camera[1] - ref.camera[1]) * metersPerMercator, camera[2] - ref.camera[2]);
1235
+ const turn = Math.abs((bearing - ref.bearing + 540) % 360 - 180) / 90;
1236
+ const motion = (height > 0 ? travel / height : 1) + turn + Math.abs(pitch - ref.pitch) / 30;
1237
+ if (Number.isFinite(motion)) ref.elevation += (target - ref.elevation) * Math.min(1, motion * 2);
1238
+ }
1239
+ if (camera !== ref.camera) {
1240
+ ref.camera[0] = camera[0];
1241
+ ref.camera[1] = camera[1];
1242
+ ref.camera[2] = camera[2];
1243
+ }
1244
+ ref.bearing = bearing;
1245
+ ref.pitch = pitch;
1246
+ const centerDepth = this.rayDepth(this.map.getCenterElevation());
1247
+ const depth = this.rayDepth(ref.elevation);
1248
+ ref.depth = depth > 0 ? depth : centerDepth;
1249
+ ref.zoom = this.map.getZoom() + Math.log2(centerDepth / ref.depth);
1250
+ }
1251
+ /**
1252
+ * Camera position as [Mercator x, Mercator y, meters up] written into `out`,
1253
+ * independent of zoom and of the layer origin, so it is unchanged when MapLibre
1254
+ * re-seats its center. It is the point where clip x, y and w all vanish: rows 0, 1
1255
+ * and 3 of the column-major matrix, solved by Cramer's rule. False if singular.
1256
+ */
1257
+ cameraPosition(out) {
1258
+ const m = this.projection.matrix;
1259
+ const a0 = m[0], a1 = m[4], a2 = m[8], t0 = -m[12];
1260
+ const b0 = m[1], b1 = m[5], b2 = m[9], t1 = -m[13];
1261
+ const c0 = m[3], c1 = m[7], c2 = m[11], t2 = -m[15];
1262
+ const det = a0 * (b1 * c2 - b2 * c1) - a1 * (b0 * c2 - b2 * c0) + a2 * (b0 * c1 - b1 * c0);
1263
+ if (!det || !Number.isFinite(det)) return false;
1264
+ const x = (t0 * (b1 * c2 - b2 * c1) - a1 * (t1 * c2 - b2 * t2) + a2 * (t1 * c1 - b1 * t2)) / det;
1265
+ const y = (a0 * (t1 * c2 - b2 * t2) - t0 * (b0 * c2 - b2 * c0) + a2 * (b0 * t2 - t1 * c0)) / det;
1266
+ const z = (a0 * (b1 * t2 - t1 * c1) - a1 * (b0 * t2 - t1 * c0) + t0 * (b0 * c1 - b1 * c0)) / det;
1267
+ out[0] = x / this.projection.world + this.origin[0];
1268
+ out[1] = y / this.projection.world + this.origin[1];
1269
+ out[2] = z;
1270
+ return true;
1271
+ }
1272
+ rayDepth(z) {
1273
+ return this.rayHit(z)?.depth ?? NaN;
1274
+ }
1275
+ /**
1276
+ * Where the view-center ray meets elevation `z`: world x, y (layer pixels from the
1277
+ * origin) and clip w, in a reused scratch object. Flat projection only; the globe
1278
+ * falls back to the origin.
1279
+ */
1280
+ rayHit(z) {
1281
+ const hit = this.rayScratch;
1282
+ if (this.projection.transition !== 0) {
1283
+ hit.x = hit.y = 0;
1284
+ hit.depth = this.projection.project(0, 0, z).clip[3];
1285
+ return hit;
1286
+ }
1287
+ const m = this.projection.matrix;
1288
+ const a = m[0], b = m[4], c = m[1], d = m[5];
1289
+ const e = -(m[8] * z + m[12]), f = -(m[9] * z + m[13]);
1290
+ const det = a * d - b * c;
1291
+ if (Math.abs(det) < 1e-12) return null;
1292
+ hit.x = (e * d - b * f) / det;
1293
+ hit.y = (a * f - e * c) / det;
1294
+ hit.depth = m[3] * hit.x + m[7] * hit.y + m[11] * z + m[15];
1295
+ return hit;
1296
+ }
1297
+ /**
1298
+ * Terrain height where the view-center ray meets the current reference plane. Unlike
1299
+ * MapLibre's center elevation, which is frozen during gestures and re-seated after
1300
+ * them, this follows the camera continuously and is unchanged while it is still.
1301
+ */
1302
+ rayTerrainElevation() {
1303
+ const ref = this.reference.elevation;
1304
+ const fallback = this.map.getCenterElevation();
1305
+ if (!this.map.getTerrain() || !Number.isFinite(ref)) return fallback;
1306
+ const hit = this.rayHit(ref);
1307
+ if (!hit || !(hit.depth > 0)) return fallback;
1308
+ const world = this.projection.world, position = this.positionScratch;
1309
+ position[0] = (hit.x / world + this.origin[0]) * 360 - 180;
1310
+ position[1] = mercatorLatitude(hit.y / world + this.origin[1]) * 180 / Math.PI;
1311
+ const value = this.sampleElevation(position);
1312
+ return Number.isFinite(value) ? value : fallback;
1313
+ }
1314
+ /** Elevation of the distance-scaling reference plane; sea level until one is set. */
1315
+ referenceElevation() {
1316
+ return Number.isFinite(this.reference.elevation) ? this.reference.elevation : 0;
1317
+ }
1318
+ /**
1319
+ * Combined zoom and distance scale for a pin anchored at world (x, y), mirroring the
1320
+ * shader: distance is horizontal, the anchor's clip w at the reference elevation.
1321
+ */
1322
+ pinScale(presentation, worldX, worldY) {
1323
+ const depth = this.projection.project(worldX, worldY, this.referenceElevation()).clip[3];
1324
+ return combinePinScale(presentation, this.zoomScale(), pinDistanceScale(presentation, depth, this.reference.depth));
1325
+ }
1126
1326
  footprint(id) {
1127
1327
  const i = this.pins.findIndex((p) => p.id === id);
1128
1328
  const pin = this.pins[i];
@@ -1130,10 +1330,13 @@ var PinLayer = class {
1130
1330
  const p = pin.presentation, coord = pinMercator(pin.position);
1131
1331
  coord.x += Math.round(this.origin[0] - coord.x);
1132
1332
  const world = 512 * 2 ** this.map.getZoom();
1133
- const project = (z) => this.projectToPixel((coord.x - this.origin[0]) * world, (coord.y - this.origin[1]) * world, z);
1134
- const ground = project(this.elevations[i] ?? 0), attachment = p.mode === "world" ? project((this.elevations[i] ?? 0) + p.heightMeters) : [ground[0], ground[1] - p.liftPixels];
1333
+ const x = (coord.x - this.origin[0]) * world, y = (coord.y - this.origin[1]) * world;
1334
+ const project = (z) => this.projectToPixel(x, y, z);
1335
+ const ground = project(this.elevations[i] ?? 0);
1336
+ const pinScale = this.pinScale(p, x, y);
1337
+ const attachment = p.mode === "world" ? project((this.elevations[i] ?? 0) + p.heightMeters) : [ground[0], ground[1] - p.liftPixels * pinScale];
1135
1338
  if (!attachment.every(Number.isFinite)) return null;
1136
- const scale = p.sizePixels / pin.octagonSize, padding = p.plate.enabled ? p.plate.padding : 0, halo = p.symbolHalo.opacity > 0 ? p.symbolHalo.width : 0, overhang = Math.max(0, halo - padding);
1339
+ const scale = p.sizePixels / pin.octagonSize * pinScale, padding = (p.plate.enabled ? p.plate.padding : 0) * pinScale, halo = (p.symbolHalo.opacity > 0 ? p.symbolHalo.width : 0) * pinScale, overhang = Math.max(0, halo - padding);
1137
1340
  return {
1138
1341
  ground,
1139
1342
  attachment,
@@ -1149,8 +1352,9 @@ var PinLayer = class {
1149
1352
  const c = pinMercator(ll);
1150
1353
  c.x += Math.round(this.origin[0] - c.x);
1151
1354
  const world = 512 * 2 ** this.map.getZoom();
1152
- const [px, py] = this.projectToPixel((c.x - this.origin[0]) * world, (c.y - this.origin[1]) * world, elevationOrSeaLevel(this.elevation(ll)) + (p.mode === "world" ? p.heightMeters : 0));
1153
- return [px, py - (p.mode === "screen" ? p.liftPixels : 0)];
1355
+ const x = (c.x - this.origin[0]) * world, y = (c.y - this.origin[1]) * world;
1356
+ const [px, py] = this.projectToPixel(x, y, elevationOrSeaLevel(this.elevation(ll)) + (p.mode === "world" ? p.heightMeters : 0));
1357
+ return [px, py - (p.mode === "screen" ? p.liftPixels * this.pinScale(p, x, y) : 0)];
1154
1358
  };
1155
1359
  return invertPinProjection(project, pin.position, attachment, this.map.getProjection()?.type === "globe" ? 90 : void 0);
1156
1360
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/tactical-draw-adapter-maplibre",
3
- "version": "0.7.3",
3
+ "version": "0.8.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",
@@ -37,8 +37,8 @@
37
37
  "access": "public"
38
38
  },
39
39
  "dependencies": {
40
- "@orbat-mapper/control-measures": "^0.30.0",
41
- "@orbat-mapper/tactical-draw": "^0.11.1"
40
+ "@orbat-mapper/control-measures": "^0.30.1",
41
+ "@orbat-mapper/tactical-draw": "^0.12.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "maplibre-gl": ">=5.8.0 <7.0.0"