@sakuzu/maplibre-gl-draw 2.1.2 → 2.1.3

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/CHANGELOG.md CHANGED
@@ -6,6 +6,33 @@ the project follows semantic versioning.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.1.3] - 2026-10-04
10
+
11
+ Two fixes: the snap indicator that stayed after its input, and the
12
+ selection frame of a point on the globe and on the terrain at a high
13
+ pitch.
14
+
15
+ ### Fixed in 2.1.3
16
+
17
+ - The snap indicator stayed after the input that made it: after leaving
18
+ the drawing mode, turning snapping off, Escape, or a pan, it went on
19
+ showing the last result, and `snap.changed` did not report the end of
20
+ the snap. The engine now clears the snap result when the mode changes,
21
+ when snapping (or a kind of it) is turned off, when a mode declines to
22
+ snap an input, and on cancel, and `snap.changed` reports `null` once
23
+ ([#22](https://github.com/sakuzu/maplibre-gl-draw/issues/22)).
24
+ - The select mode snapped every input, so hovering near a vertex drew
25
+ the indicator, against the guide ("only while drawing and while moving
26
+ a vertex"). It now snaps its drags only. A select-mode plugin that
27
+ read `event.snapped` on a hover or a click receives the raw position
28
+ ([#22](https://github.com/sakuzu/maplibre-gl-draw/issues/22)).
29
+ - The selection frame of a point is drawn as a figure of the screen,
30
+ like the mark: it keeps the size of the mark and the margin on the
31
+ terrain at any pitch and at the edge of the globe, and a point on the
32
+ far side of the globe gets no frame. The frame of a selection of
33
+ several features is drawn on the box of its handles
34
+ ([#12](https://github.com/sakuzu/maplibre-gl-draw/issues/12)).
35
+
9
36
  ## [2.1.2] - 2026-10-02
10
37
 
11
38
  Three fixes: a plugin's fill, the click that finishes a plugin's
@@ -1192,6 +1219,7 @@ available from Kasika, Inc.
1192
1219
  - Plugins, custom modes and custom feature types.
1193
1220
 
1194
1221
  [Unreleased]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.1.2...HEAD
1222
+ [2.1.3]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.1.2...v2.1.3
1195
1223
  [2.1.2]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.1.1...v2.1.2
1196
1224
  [2.1.1]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.1.0...v2.1.1
1197
1225
  [2.1.0]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.0.0...v2.1.0
@@ -196,6 +196,12 @@ export function createEngine(map, options = {}, build = {}) {
196
196
  const repaintOnSnapChange = () => map.triggerRepaint();
197
197
  eventEmitter.on('snap.change', repaintOnSnapChange);
198
198
  teardown.add(() => eventEmitter.off('snap.change', repaintOnSnapChange));
199
+ // A snap belongs to the input of the mode that made it: leaving the mode clears it (any
200
+ // mode, built-in or added), so neither the indicator nor snap.changed keeps a stale target
201
+ teardown.add(store.subscribe((changes) => {
202
+ if (changes.mode)
203
+ snapService.clear();
204
+ }));
199
205
  // 4. Create the RenderCoordinator (triggers a repaint automatically when the
200
206
  // Store changes)
201
207
  const renderCoordinator = createRenderCoordinator({ map, store });
@@ -83,7 +83,9 @@ export function createExtensionHost(deps) {
83
83
  install: (name, factory) => modeManager.registerMode(name, () => {
84
84
  const handle = createModeContext(modeServices, screen);
85
85
  try {
86
- return bridgeMode(name, factory(handle.context), handle.dispose, handle.entering);
86
+ return bridgeMode(name, factory(handle.context), handle.dispose, handle.entering,
87
+ // An interrupted mode leaves no snap behind (leaving it is cleared by the engine)
88
+ () => context.snapService.clear());
87
89
  }
88
90
  catch (error) {
89
91
  handle.dispose();
@@ -83,9 +83,10 @@ const BRIDGED = Symbol('extension mode');
83
83
  * @param onExit - Called after the mode was left, to end what its context holds
84
84
  * @param entering - Runs the entering of the mode, so that its context knows the cursor the
85
85
  * mode sets then
86
+ * @param onCancelled - Called after the mode was interrupted, to clear what its input left
86
87
  * @internal
87
88
  */
88
- export function bridgeMode(name, handler, onExit, entering = (run) => run()) {
89
+ export function bridgeMode(name, handler, onExit, entering = (run) => run(), onCancelled = () => { }) {
89
90
  const bridged = {
90
91
  [BRIDGED]: handler,
91
92
  modeName: name,
@@ -102,7 +103,15 @@ export function bridgeMode(name, handler, onExit, entering = (run) => run()) {
102
103
  }
103
104
  },
104
105
  onExternalStateChange() {
105
- handler.onCancel?.();
106
+ bridged.cancel();
107
+ },
108
+ cancel() {
109
+ try {
110
+ handler.onCancel?.();
111
+ }
112
+ finally {
113
+ onCancelled();
114
+ }
106
115
  },
107
116
  undoVertex: () => handler.onUndoVertex?.() === true,
108
117
  redoVertex: () => handler.onRedoVertex?.() === true,
@@ -136,7 +145,7 @@ export function routeToMode(handler, event, snapped, snap) {
136
145
  return undefined;
137
146
  const consumed = deliver(mode, event, snapped, snap);
138
147
  if (!consumed && event.type === 'keydown' && event.key === 'Escape' && mode.onCancel) {
139
- mode.onCancel();
148
+ handler.cancel();
140
149
  return true;
141
150
  }
142
151
  return consumed;
@@ -123,12 +123,14 @@ export function createInputRouter(deps) {
123
123
  *
124
124
  * Whether snapping is wanted is asked of the current mode per input type
125
125
  * (isSnapEnabledFor). A mode that does not declare it snaps for every input type
126
- * (as before).
126
+ * (as before). An input that is not snapped clears the previous result, so the
127
+ * indicator of an earlier snap does not stay behind it.
127
128
  */
128
129
  function applySnap(event, inputType) {
129
- if (!snapService || event.snap === false)
130
+ if (!snapService)
130
131
  return { event, result: null };
131
- if (modeManager.getHandler()?.isSnapEnabledFor?.(inputType) === false) {
132
+ if (event.snap === false || modeManager.getHandler()?.isSnapEnabledFor?.(inputType) === false) {
133
+ snapService.clear();
132
134
  return { event, result: null };
133
135
  }
134
136
  const result = snapService.resolve(event.lngLat, event.point, buildSnapContext(event.modifiers));
@@ -80,6 +80,14 @@ export class SelectMode {
80
80
  store.setSelection(null, []);
81
81
  }, 'silent');
82
82
  }
83
+ /**
84
+ * Snapping applies only while a vertex or a feature is dragged: a click or a hover in
85
+ * select is not snapped (and draws no indicator). A mode of the contract that wants the
86
+ * snapped position of a point asks `snap` of its ModeContext.
87
+ */
88
+ isSnapEnabledFor(inputType) {
89
+ return inputType === 'dragstart' || inputType === 'dragmove' || inputType === 'dragend';
90
+ }
83
91
  /**
84
92
  * Called after a state change from the outside (a change applied from outside the mode).
85
93
  * Resets the mode's internal state to keep it consistent.
@@ -110,6 +110,17 @@ export function createSnapService(deps = {}) {
110
110
  }
111
111
  return result;
112
112
  }
113
+ /**
114
+ * Drops the result when it holds a target, emitting the change once without a target (the
115
+ * same event as a snap that comes off). A result without a target is left as it is.
116
+ */
117
+ function clear() {
118
+ if (!lastResult?.target)
119
+ return;
120
+ const { lngLat } = lastResult;
121
+ lastResult = null;
122
+ deps.eventEmitter?.emit('snap.change', { lngLat });
123
+ }
113
124
  /**
114
125
  * Evaluates a single candidate and updates best
115
126
  *
@@ -248,7 +259,10 @@ export function createSnapService(deps = {}) {
248
259
  },
249
260
  setEnabled(value) {
250
261
  enabled = value;
262
+ if (!value)
263
+ clear();
251
264
  },
265
+ clear,
252
266
  setTolerance(px) {
253
267
  options.tolerancePx = px;
254
268
  },
@@ -260,6 +274,8 @@ export function createSnapService(deps = {}) {
260
274
  },
261
275
  setKindEnabled(kind, value) {
262
276
  options.kinds[kind] = value;
277
+ if (!value)
278
+ clear();
263
279
  },
264
280
  isKindEnabled(kind) {
265
281
  return options.kinds[kind];
@@ -12,6 +12,7 @@ import type { PixelRatioInput } from '../../shared/utils/pixel-ratio.js';
12
12
  import type { SpatialIndex } from '../../store/spatial/spatial-index.js';
13
13
  import type { Store } from '../../store/store.js';
14
14
  import { TextureCache } from '../cache/texture.js';
15
+ import { AnchoredOutlineRenderer } from '../renderers/anchored-outline.js';
15
16
  import { BatchManager } from '../renderers/batch-manager.js';
16
17
  import { FeatureDrawer } from '../renderers/drawer.js';
17
18
  import { type ImageErrorFn, ImageRenderer } from '../renderers/image.js';
@@ -37,6 +38,8 @@ export interface Renderers {
37
38
  quadShader: QuadShader;
38
39
  imageRenderer: ImageRenderer;
39
40
  strokeRenderer: StrokeRenderer;
41
+ /** The outlines laid on the screen around an anchor (the frames of points) */
42
+ outlineRenderer: AnchoredOutlineRenderer;
40
43
  sdfLineRenderer: SDFLineRenderer;
41
44
  pointShapeRenderer: PointShapeRenderer;
42
45
  pointInstanceRenderer: PointInstanceRenderer;
@@ -1,6 +1,7 @@
1
1
  // SPDX-FileCopyrightText: 2026 SAKAIDA Atsushi
2
2
  // SPDX-License-Identifier: AGPL-3.0-only
3
3
  import { TextureCache } from '../cache/texture.js';
4
+ import { AnchoredOutlineRenderer } from '../renderers/anchored-outline.js';
4
5
  import { BatchManager } from '../renderers/batch-manager.js';
5
6
  import { FeatureDrawer } from '../renderers/drawer.js';
6
7
  import { ImageRenderer } from '../renderers/image.js';
@@ -27,6 +28,7 @@ export function initRenderers(deps) {
27
28
  const quadShader = new QuadShader(gl).useTerrainState(terrain);
28
29
  const imageRenderer = new ImageRenderer(mapInstance, gl, textureCache, quadShader, (fileId) => store.getFile(fileId), onImageError);
29
30
  const strokeRenderer = new StrokeRenderer(mapInstance, gl, pixelRatio, terrain);
31
+ const outlineRenderer = new AnchoredOutlineRenderer(mapInstance, gl, pixelRatio, terrain);
30
32
  const sdfLineRenderer = new SDFLineRenderer(mapInstance, gl, pixelRatio, terrain);
31
33
  const pointShapeRenderer = new PointShapeRenderer(mapInstance, gl, pixelRatio, terrain);
32
34
  const pointInstanceRenderer = new PointInstanceRenderer(mapInstance, gl, pixelRatio, terrain);
@@ -35,7 +37,7 @@ export function initRenderers(deps) {
35
37
  const sdfPolygonRenderer = new SDFPolygonRenderer(gl, pixelRatio, polygonScope);
36
38
  const fillShaderManager = new FillShaderManager(gl, terrain);
37
39
  const selectionExtensions = scope.selection.extensions;
38
- const selectionUIRenderer = new SelectionUIRenderer(strokeRenderer, selectionConfig, selectionExtensions, terrain);
40
+ const selectionUIRenderer = new SelectionUIRenderer(strokeRenderer, outlineRenderer, selectionConfig, selectionExtensions, terrain);
39
41
  // The tile size of this instance's map (read by the custom bounding box calculators)
40
42
  const tileSize = mapInstance.transform?.tileSize ?? 512;
41
43
  selectionExtensions.setTileSize(tileSize);
@@ -74,6 +76,7 @@ export function initRenderers(deps) {
74
76
  tentativeRenderer,
75
77
  quadShader,
76
78
  strokeRenderer,
79
+ outlineRenderer,
77
80
  pointShapeRenderer,
78
81
  pointInstanceRenderer,
79
82
  sdfLineRenderer,
@@ -95,6 +98,7 @@ export function initRenderers(deps) {
95
98
  quadShader,
96
99
  imageRenderer,
97
100
  strokeRenderer,
101
+ outlineRenderer,
98
102
  sdfLineRenderer,
99
103
  pointShapeRenderer,
100
104
  pointInstanceRenderer,
@@ -125,6 +129,7 @@ const GPU_OWNERS = [
125
129
  'textureCache',
126
130
  'quadShader',
127
131
  'strokeRenderer',
132
+ 'outlineRenderer',
128
133
  'pointShapeRenderer',
129
134
  'pointInstanceRenderer',
130
135
  'sdfLineRenderer',
@@ -0,0 +1,5 @@
1
+ /** A point of the screen relative to the anchor, in CSS px (y points down) */
2
+ export interface OutlineOffset {
3
+ x: number;
4
+ y: number;
5
+ }
@@ -0,0 +1,352 @@
1
+ // SPDX-FileCopyrightText: 2026 SAKAIDA Atsushi
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ import { getDashPattern, RENDERING_DEFAULTS } from '../../shared/math/index.js';
4
+ import { resolvePixelRatio } from '../../shared/utils/pixel-ratio.js';
5
+ import { calculateLngLatOffset, createProgram, getProjectionTransitionUniform, OFFSET_MODE_GLSL, } from '../shaders/helpers.js';
6
+ import { ProjectionUniformManager } from '../shaders/projection.js';
7
+ import { TerrainContext } from '../terrain/context.js';
8
+ import { drawBillboardsWithoutDepth } from './point/billboard-depth.js';
9
+ /** Floats per vertex: the offset (x, y in device px), the signed distance across, the distance along */
10
+ const FLOATS_PER_VERTEX = 4;
11
+ const STRIDE = FLOATS_PER_VERTEX * 4;
12
+ /**
13
+ * The vertices of a closed outline as a triangle strip: two per corner (the two sides of the
14
+ * line) and the first corner again at the end
15
+ *
16
+ * Each vertex is the corner moved along the miter of its two edges by half the width plus one
17
+ * pixel (the band the HHAA coverage fades in), the same join as StrokeRenderer: a miter longer
18
+ * than the limit becomes a bevel along the edge that arrives. The distance along the outline is
19
+ * in CSS px, the unit of the dash patterns.
20
+ *
21
+ * @param corners The corners relative to the anchor, in CSS px, in order (not closed)
22
+ * @param widthPx The width of the line in device px
23
+ * @param pixelRatio Device px per CSS px
24
+ * @param miterLimit The longest miter, in half widths
25
+ * @returns The interleaved vertices (x, y, signed distance, distance), empty for fewer than two
26
+ * distinct corners
27
+ * @internal
28
+ */
29
+ export function buildOutlineStrip(corners, widthPx, pixelRatio, miterLimit = RENDERING_DEFAULTS.MITER_LIMIT) {
30
+ // Repeated corners make no edge
31
+ const ring = corners.filter((corner, i) => {
32
+ const next = corners[(i + 1) % corners.length];
33
+ return corners.length === 1 || corner.x !== next.x || corner.y !== next.y;
34
+ });
35
+ const n = ring.length;
36
+ if (n < 2)
37
+ return new Float32Array(0);
38
+ const expanded = widthPx / 2 + 1;
39
+ const unit = (from, to) => {
40
+ const dx = to.x - from.x;
41
+ const dy = to.y - from.y;
42
+ const length = Math.hypot(dx, dy);
43
+ return { x: dx / length, y: dy / length };
44
+ };
45
+ const data = new Float32Array((n + 1) * 2 * FLOATS_PER_VERTEX);
46
+ let along = 0;
47
+ for (let i = 0; i <= n; i++) {
48
+ const corner = ring[i % n];
49
+ if (i > 0) {
50
+ const before = ring[i - 1];
51
+ along += Math.hypot(corner.x - before.x, corner.y - before.y);
52
+ }
53
+ const arriving = unit(ring[(i + n - 1) % n], corner);
54
+ const leaving = unit(corner, ring[(i + 1) % n]);
55
+ const n1 = { x: -arriving.y, y: arriving.x };
56
+ const n2 = { x: -leaving.y, y: leaving.x };
57
+ let normal = n1;
58
+ let scale = 1;
59
+ const sum = { x: n1.x + n2.x, y: n1.y + n2.y };
60
+ const sumLength = Math.hypot(sum.x, sum.y);
61
+ if (sumLength > 0.001) {
62
+ const miter = { x: sum.x / sumLength, y: sum.y / sumLength };
63
+ const cosHalf = miter.x * n1.x + miter.y * n1.y;
64
+ if (cosHalf > 0.001 && 1 / cosHalf <= miterLimit) {
65
+ normal = miter;
66
+ scale = 1 / cosHalf;
67
+ }
68
+ }
69
+ for (const side of [1, -1]) {
70
+ const base = (i * 2 + (side === 1 ? 0 : 1)) * FLOATS_PER_VERTEX;
71
+ data[base] = corner.x * pixelRatio + normal.x * side * expanded * scale;
72
+ data[base + 1] = corner.y * pixelRatio + normal.y * side * expanded * scale;
73
+ data[base + 2] = side * expanded;
74
+ data[base + 3] = along;
75
+ }
76
+ }
77
+ return data;
78
+ }
79
+ /**
80
+ * Draws closed outlines laid in pixels around an anchor projected like a marker
81
+ *
82
+ * @internal
83
+ */
84
+ export class AnchoredOutlineRenderer {
85
+ constructor(_map, gl, pixelRatio, terrain = new TerrainContext()) {
86
+ Object.defineProperty(this, "gl", {
87
+ enumerable: true,
88
+ configurable: true,
89
+ writable: true,
90
+ value: void 0
91
+ });
92
+ Object.defineProperty(this, "program", {
93
+ enumerable: true,
94
+ configurable: true,
95
+ writable: true,
96
+ value: null
97
+ });
98
+ Object.defineProperty(this, "variantName", {
99
+ enumerable: true,
100
+ configurable: true,
101
+ writable: true,
102
+ value: ''
103
+ });
104
+ Object.defineProperty(this, "projectionUniformManager", {
105
+ enumerable: true,
106
+ configurable: true,
107
+ writable: true,
108
+ value: void 0
109
+ });
110
+ Object.defineProperty(this, "offsetUniforms", {
111
+ enumerable: true,
112
+ configurable: true,
113
+ writable: true,
114
+ value: null
115
+ });
116
+ /** The injected device pixel ratio (read from window each time when not injected) */
117
+ Object.defineProperty(this, "pixelRatio", {
118
+ enumerable: true,
119
+ configurable: true,
120
+ writable: true,
121
+ value: void 0
122
+ });
123
+ Object.defineProperty(this, "colorLoc", {
124
+ enumerable: true,
125
+ configurable: true,
126
+ writable: true,
127
+ value: null
128
+ });
129
+ Object.defineProperty(this, "widthLoc", {
130
+ enumerable: true,
131
+ configurable: true,
132
+ writable: true,
133
+ value: null
134
+ });
135
+ Object.defineProperty(this, "pixelToClipLoc", {
136
+ enumerable: true,
137
+ configurable: true,
138
+ writable: true,
139
+ value: null
140
+ });
141
+ Object.defineProperty(this, "anchorOffsetLoc", {
142
+ enumerable: true,
143
+ configurable: true,
144
+ writable: true,
145
+ value: null
146
+ });
147
+ Object.defineProperty(this, "anchorElevationLoc", {
148
+ enumerable: true,
149
+ configurable: true,
150
+ writable: true,
151
+ value: null
152
+ });
153
+ Object.defineProperty(this, "dashArrayLoc", {
154
+ enumerable: true,
155
+ configurable: true,
156
+ writable: true,
157
+ value: null
158
+ });
159
+ Object.defineProperty(this, "dashEnabledLoc", {
160
+ enumerable: true,
161
+ configurable: true,
162
+ writable: true,
163
+ value: null
164
+ });
165
+ Object.defineProperty(this, "vao", {
166
+ enumerable: true,
167
+ configurable: true,
168
+ writable: true,
169
+ value: void 0
170
+ });
171
+ Object.defineProperty(this, "vertexBuffer", {
172
+ enumerable: true,
173
+ configurable: true,
174
+ writable: true,
175
+ value: void 0
176
+ });
177
+ this.gl = gl;
178
+ this.pixelRatio = pixelRatio;
179
+ // An outline around an anchor is a symbol: it keeps the height of its anchor on the frames
180
+ // that lay the surfaces flat, as the marker does
181
+ this.projectionUniformManager = new ProjectionUniformManager(gl).useTerrainState(terrain);
182
+ this.vertexBuffer = gl.createBuffer();
183
+ this.vao = gl.createVertexArray();
184
+ gl.bindVertexArray(this.vao);
185
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
186
+ gl.enableVertexAttribArray(0);
187
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, STRIDE, 0); // a_offset
188
+ gl.enableVertexAttribArray(1);
189
+ gl.vertexAttribPointer(1, 1, gl.FLOAT, false, STRIDE, 8); // a_side_dist
190
+ gl.enableVertexAttribArray(2);
191
+ gl.vertexAttribPointer(2, 1, gl.FLOAT, false, STRIDE, 12); // a_distance
192
+ gl.bindVertexArray(null);
193
+ }
194
+ /** Makes sure the shader of the projection variant exists */
195
+ ensureShader(shaderData) {
196
+ if (this.variantName === shaderData.variantName)
197
+ return;
198
+ if (this.program)
199
+ this.gl.deleteProgram(this.program);
200
+ this.program = this.createShaderProgram(shaderData.vertexShaderPrelude, shaderData.define);
201
+ this.variantName = shaderData.variantName;
202
+ this.projectionUniformManager.getLocations(this.program);
203
+ const gl = this.gl;
204
+ this.colorLoc = gl.getUniformLocation(this.program, 'u_color');
205
+ this.widthLoc = gl.getUniformLocation(this.program, 'u_width');
206
+ this.pixelToClipLoc = gl.getUniformLocation(this.program, 'u_pixel_to_clip');
207
+ this.anchorOffsetLoc = gl.getUniformLocation(this.program, 'u_anchor_offset');
208
+ this.anchorElevationLoc = gl.getUniformLocation(this.program, 'u_anchor_elevation_m');
209
+ this.dashArrayLoc = gl.getUniformLocation(this.program, 'u_dashArray');
210
+ this.dashEnabledLoc = gl.getUniformLocation(this.program, 'u_dashEnabled');
211
+ }
212
+ /** Sets the uniforms of the offset mode (the camera of the copy of the world being drawn) */
213
+ setOffsetUniforms(uniforms) {
214
+ this.offsetUniforms = uniforms;
215
+ }
216
+ createShaderProgram(prelude, define) {
217
+ const projTransitionUniform = getProjectionTransitionUniform(prelude);
218
+ const vertexSource = `#version 300 es
219
+ ${prelude}
220
+ ${define}
221
+ ${projTransitionUniform}${OFFSET_MODE_GLSL}
222
+ layout(location = 0) in vec2 a_offset; // the vertex around the anchor (device px, y down)
223
+ layout(location = 1) in float a_side_dist; // the signed distance across the line (device px)
224
+ layout(location = 2) in float a_distance; // the distance along the outline (CSS px)
225
+
226
+ uniform vec2 u_anchor_offset; // the anchor relative to the center (64-bit on the CPU)
227
+ uniform float u_anchor_elevation_m; // the height of the anchor, from the CPU as for a marker
228
+ uniform vec2 u_pixel_to_clip; // device px to clip space
229
+ uniform float u_width; // the width of the line (device px)
230
+
231
+ out float v_dist;
232
+ out float v_width;
233
+ out float v_distance;
234
+
235
+ void main() {
236
+ // The height comes from the CPU, the source hit testing and the marker use
237
+ g_anchor_elevation_m = u_anchor_elevation_m;
238
+ g_anchor_elevation_on = 1.0;
239
+ vec4 centerClip = project_position_to_clipspace_from_offset(u_anchor_offset, u_projection_matrix);
240
+ // The depth of the anchor for every vertex: the far side of the globe clips the outline
241
+ // with its anchor, as it clips a marker
242
+ gl_Position = vec4(
243
+ centerClip.xy / centerClip.w + vec2(a_offset.x, -a_offset.y) * u_pixel_to_clip,
244
+ centerClip.z / centerClip.w,
245
+ 1.0
246
+ );
247
+ v_dist = a_side_dist;
248
+ v_width = u_width / 2.0;
249
+ v_distance = a_distance;
250
+ }`;
251
+ const fragmentSource = `#version 300 es
252
+ precision highp float;
253
+
254
+ uniform vec4 u_color;
255
+ uniform vec4 u_dashArray; // [dashLength, gapLength, patternLength, 0]
256
+ uniform float u_dashEnabled;
257
+
258
+ in float v_dist;
259
+ in float v_width;
260
+ in float v_distance;
261
+
262
+ out vec4 fragColor;
263
+
264
+ // HHAA: the part of the pixel on the inner side of a half plane
265
+ float pixelLine(float x) {
266
+ return clamp(x + 0.5, 0.0, 1.0);
267
+ }
268
+
269
+ void main() {
270
+ if (u_dashEnabled > 0.5) {
271
+ float patternLength = u_dashArray.z;
272
+ if (patternLength > 0.0 && mod(v_distance, patternLength) > u_dashArray.x) discard;
273
+ }
274
+ float coverage = pixelLine(v_dist + v_width) - pixelLine(v_dist - v_width);
275
+ // Blending is non-premultiplied, so the coverage goes to the alpha only
276
+ fragColor = vec4(u_color.rgb, u_color.a * coverage);
277
+ }`;
278
+ return createProgram(this.gl, vertexSource, fragmentSource);
279
+ }
280
+ /**
281
+ * Draws a closed outline around an anchor
282
+ *
283
+ * @param anchor The position the outline is laid around, `[lng, lat]`
284
+ * @param elevationMeters The height of the anchor (the height its marker is drawn at)
285
+ * @param corners The corners relative to the anchor's point on the screen, in CSS px
286
+ * @param style The line (its width is in CSS px)
287
+ * @param opacity A factor on the opacity of the line (the terrain's ghost of the marker)
288
+ * @param zoom The zoom of the frame
289
+ * @param projectionData The projection of the copy of the world being drawn
290
+ */
291
+ draw(anchor, elevationMeters, corners, style, opacity, zoom, projectionData) {
292
+ if (!this.program)
293
+ return;
294
+ const gl = this.gl;
295
+ const pixelRatio = resolvePixelRatio(this.pixelRatio);
296
+ const widthPx = style.width * pixelRatio;
297
+ const data = buildOutlineStrip(corners, widthPx, pixelRatio);
298
+ if (data.length === 0)
299
+ return;
300
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
301
+ gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
302
+ gl.bindVertexArray(this.vao);
303
+ gl.useProgram(this.program);
304
+ this.projectionUniformManager.setUniforms(projectionData, zoom, this.offsetUniforms);
305
+ const center = this.offsetUniforms?.centerLngLat ?? [0, 0];
306
+ const offset = calculateLngLatOffset([anchor[0], anchor[1]], center);
307
+ if (this.anchorOffsetLoc)
308
+ gl.uniform2f(this.anchorOffsetLoc, offset[0], offset[1]);
309
+ if (this.anchorElevationLoc)
310
+ gl.uniform1f(this.anchorElevationLoc, elevationMeters);
311
+ if (this.pixelToClipLoc) {
312
+ gl.uniform2f(this.pixelToClipLoc, 2 / gl.drawingBufferWidth, 2 / gl.drawingBufferHeight);
313
+ }
314
+ if (this.widthLoc)
315
+ gl.uniform1f(this.widthLoc, widthPx);
316
+ if (this.colorLoc) {
317
+ gl.uniform4fv(this.colorLoc, [
318
+ style.color[0],
319
+ style.color[1],
320
+ style.color[2],
321
+ style.color[3] * style.opacity * opacity,
322
+ ]);
323
+ }
324
+ const dashPattern = style.dashArray ?? getDashPattern(style.lineStyle);
325
+ const dashed = style.lineStyle !== 'solid' && dashPattern.length >= 2;
326
+ if (this.dashEnabledLoc)
327
+ gl.uniform1f(this.dashEnabledLoc, dashed ? 1 : 0);
328
+ if (dashed && this.dashArrayLoc) {
329
+ const [dash, gap] = dashPattern;
330
+ gl.uniform4fv(this.dashArrayLoc, [dash, gap, dash + gap, 0]);
331
+ }
332
+ drawBillboardsWithoutDepth(gl, () => {
333
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, data.length / FLOATS_PER_VERTEX);
334
+ });
335
+ gl.bindVertexArray(null);
336
+ }
337
+ /** Releases the GPU resources */
338
+ dispose() {
339
+ if (this.program) {
340
+ this.gl.deleteProgram(this.program);
341
+ this.program = null;
342
+ }
343
+ if (this.vao) {
344
+ this.gl.deleteVertexArray(this.vao);
345
+ this.vao = null;
346
+ }
347
+ if (this.vertexBuffer) {
348
+ this.gl.deleteBuffer(this.vertexBuffer);
349
+ this.vertexBuffer = null;
350
+ }
351
+ }
352
+ }
@@ -1,3 +1,4 @@
1
+ import type { AnchoredOutlineRenderer } from '../renderers/anchored-outline.js';
1
2
  import type { FeatureDrawer } from '../renderers/drawer.js';
2
3
  import type { SDFLineRenderer } from '../renderers/line/sdf-line.js';
3
4
  import type { PointInstanceRenderer } from '../renderers/point/point-instance.js';
@@ -16,6 +17,8 @@ export interface ShaderInitializerRenderers {
16
17
  tentativeRenderer: TentativeRenderer | null;
17
18
  quadShader: QuadShader | null;
18
19
  strokeRenderer: StrokeRenderer | null;
20
+ /** The outlines laid on the screen around an anchor (the frames of points) */
21
+ outlineRenderer: AnchoredOutlineRenderer | null;
19
22
  pointShapeRenderer: PointShapeRenderer | null;
20
23
  pointInstanceRenderer: PointInstanceRenderer | null;
21
24
  sdfLineRenderer: SDFLineRenderer | null;
@@ -23,7 +23,7 @@ export class ShaderInitializer {
23
23
  * Makes every renderer ensure its shader
24
24
  */
25
25
  ensureShaders(shaderData) {
26
- const { featureDrawer, tentativeRenderer, quadShader, strokeRenderer, pointShapeRenderer, pointInstanceRenderer, sdfLineRenderer, polygonBatchRenderer, sdfPolygonRenderer, fillShaderManager, } = this.renderers;
26
+ const { featureDrawer, tentativeRenderer, quadShader, strokeRenderer, outlineRenderer, pointShapeRenderer, pointInstanceRenderer, sdfLineRenderer, polygonBatchRenderer, sdfPolygonRenderer, fillShaderManager, } = this.renderers;
27
27
  if (featureDrawer) {
28
28
  featureDrawer.ensureShader(shaderData);
29
29
  }
@@ -36,6 +36,9 @@ export class ShaderInitializer {
36
36
  if (strokeRenderer) {
37
37
  strokeRenderer.ensureShader(shaderData);
38
38
  }
39
+ if (outlineRenderer) {
40
+ outlineRenderer.ensureShader(shaderData);
41
+ }
39
42
  if (pointShapeRenderer) {
40
43
  pointShapeRenderer.ensureShader(shaderData);
41
44
  }
@@ -80,7 +83,7 @@ export class ShaderInitializer {
80
83
  * uniforms once per frame and applies them for each slot.
81
84
  */
82
85
  applyOffsetUniforms(uniforms) {
83
- const { featureDrawer, tentativeRenderer, quadShader, strokeRenderer, pointShapeRenderer, pointInstanceRenderer, sdfLineRenderer, polygonBatchRenderer, sdfPolygonRenderer, fillShaderManager, } = this.renderers;
86
+ const { featureDrawer, tentativeRenderer, quadShader, strokeRenderer, outlineRenderer, pointShapeRenderer, pointInstanceRenderer, sdfLineRenderer, polygonBatchRenderer, sdfPolygonRenderer, fillShaderManager, } = this.renderers;
84
87
  if (featureDrawer) {
85
88
  featureDrawer.setOffsetUniforms(uniforms);
86
89
  }
@@ -93,6 +96,9 @@ export class ShaderInitializer {
93
96
  if (strokeRenderer) {
94
97
  strokeRenderer.setOffsetUniforms(uniforms);
95
98
  }
99
+ if (outlineRenderer) {
100
+ outlineRenderer.setOffsetUniforms(uniforms);
101
+ }
96
102
  if (pointShapeRenderer) {
97
103
  pointShapeRenderer.setOffsetUniforms(uniforms);
98
104
  }
@@ -9,13 +9,14 @@ import type { ProjectionData } from 'maplibre-gl';
9
9
  import type { SelectionUIConfig } from '../../../shared/config/selection.js';
10
10
  import { type CoordinateTransform } from '../../../shared/math/index.js';
11
11
  import type { DragState, Feature } from '../../../store/types.js';
12
+ import type { AnchoredOutlineRenderer } from '../../renderers/anchored-outline.js';
12
13
  import type { StrokeRenderer } from '../../renderers/stroke.js';
13
14
  import { TerrainContext } from '../../terrain/context.js';
14
15
  import type { SelectionExtensionRegistry } from './extension-registry.js';
15
16
  import type { BoundingBoxCoords } from './types.js';
16
17
  export declare class SelectionUIRenderer {
17
18
  #private;
18
- constructor(strokeRenderer: StrokeRenderer, config: SelectionUIConfig, extensions: SelectionExtensionRegistry, terrain?: TerrainContext);
19
+ constructor(strokeRenderer: StrokeRenderer, outlineRenderer: AnchoredOutlineRenderer, config: SelectionUIConfig, extensions: SelectionExtensionRegistry, terrain?: TerrainContext);
19
20
  /** Compute the BoundingBox of a feature (also usable from outside) */
20
21
  computeFeatureBoundingBox(feature: Feature): BoundingBoxCoords | null;
21
22
  /** Compute the combined BoundingBox of several features (also usable from outside) */
@@ -11,20 +11,18 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
11
11
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
12
12
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
13
13
  };
14
- var _SelectionUIRenderer_instances, _SelectionUIRenderer_strokeRenderer, _SelectionUIRenderer_extensions, _SelectionUIRenderer_terrain, _SelectionUIRenderer_config, _SelectionUIRenderer_transform, _SelectionUIRenderer_projectionData, _SelectionUIRenderer_drawRotatingBoundingBox, _SelectionUIRenderer_drawIndividualBoundingBox, _SelectionUIRenderer_drawCombinedBoundingBox, _SelectionUIRenderer_drawBoundingBoxWithMargin, _SelectionUIRenderer_drawPointBoundingBox, _SelectionUIRenderer_pointFrame, _SelectionUIRenderer_applyMargin, _SelectionUIRenderer_strokeBoundingBox, _SelectionUIRenderer_strokePath;
14
+ var _SelectionUIRenderer_instances, _SelectionUIRenderer_strokeRenderer, _SelectionUIRenderer_outlineRenderer, _SelectionUIRenderer_extensions, _SelectionUIRenderer_terrain, _SelectionUIRenderer_config, _SelectionUIRenderer_transform, _SelectionUIRenderer_projectionData, _SelectionUIRenderer_drawRotatingBoundingBox, _SelectionUIRenderer_drawIndividualBoundingBox, _SelectionUIRenderer_drawBoundingBoxWithMargin, _SelectionUIRenderer_drawPointBoundingBox, _SelectionUIRenderer_pointFrame, _SelectionUIRenderer_applyMargin, _SelectionUIRenderer_strokeBoundingBox, _SelectionUIRenderer_strokePath;
15
15
  import { applyMarginToBoundingBox, } from '../../../shared/math/index.js';
16
16
  import { anchorElevationMeters, isAnchorActive, projectAnchorAt } from '../../terrain/anchor.js';
17
17
  import { TerrainContext } from '../../terrain/context.js';
18
+ import { anchorGhostOpacity } from '../../terrain/occlusion.js';
18
19
  import { computeBoundingBox, computeCombinedBoundingBox, hasZeroArea, rotateBoundingBox, } from './bounding-box.js';
19
- /**
20
- * How far the positions differ that the scale of the screen around a point is measured
21
- * between, in pixels at the zoom of the frame
22
- */
23
- const POINT_FRAME_PROBE_PX = 4;
24
20
  export class SelectionUIRenderer {
25
- constructor(strokeRenderer, config, extensions, terrain = new TerrainContext()) {
21
+ constructor(strokeRenderer, outlineRenderer, config, extensions, terrain = new TerrainContext()) {
26
22
  _SelectionUIRenderer_instances.add(this);
27
23
  _SelectionUIRenderer_strokeRenderer.set(this, void 0);
24
+ /** Draws the frames of points, which are figures of the screen */
25
+ _SelectionUIRenderer_outlineRenderer.set(this, void 0);
28
26
  /** The Selection UI extension points of the draw instance this renderer belongs to */
29
27
  _SelectionUIRenderer_extensions.set(this, void 0);
30
28
  /** The terrain state of the draw instance (an inactive one projects with the map) */
@@ -33,6 +31,7 @@ export class SelectionUIRenderer {
33
31
  _SelectionUIRenderer_transform.set(this, null);
34
32
  _SelectionUIRenderer_projectionData.set(this, null);
35
33
  __classPrivateFieldSet(this, _SelectionUIRenderer_strokeRenderer, strokeRenderer, "f");
34
+ __classPrivateFieldSet(this, _SelectionUIRenderer_outlineRenderer, outlineRenderer, "f");
36
35
  __classPrivateFieldSet(this, _SelectionUIRenderer_config, config, "f");
37
36
  __classPrivateFieldSet(this, _SelectionUIRenderer_extensions, extensions, "f");
38
37
  __classPrivateFieldSet(this, _SelectionUIRenderer_terrain, terrain, "f");
@@ -87,10 +86,10 @@ export class SelectionUIRenderer {
87
86
  __classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_drawIndividualBoundingBox).call(this, feature, zoom);
88
87
  }
89
88
  }
90
- __classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_drawCombinedBoundingBox).call(this, features, zoom);
89
+ __classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_drawBoundingBoxWithMargin).call(this, features, zoom);
91
90
  }
92
91
  }
93
- _SelectionUIRenderer_strokeRenderer = new WeakMap(), _SelectionUIRenderer_extensions = new WeakMap(), _SelectionUIRenderer_terrain = new WeakMap(), _SelectionUIRenderer_config = new WeakMap(), _SelectionUIRenderer_transform = new WeakMap(), _SelectionUIRenderer_projectionData = new WeakMap(), _SelectionUIRenderer_instances = new WeakSet(), _SelectionUIRenderer_drawRotatingBoundingBox = function _SelectionUIRenderer_drawRotatingBoundingBox(features, zoom, dragState) {
92
+ _SelectionUIRenderer_strokeRenderer = new WeakMap(), _SelectionUIRenderer_outlineRenderer = new WeakMap(), _SelectionUIRenderer_extensions = new WeakMap(), _SelectionUIRenderer_terrain = new WeakMap(), _SelectionUIRenderer_config = new WeakMap(), _SelectionUIRenderer_transform = new WeakMap(), _SelectionUIRenderer_projectionData = new WeakMap(), _SelectionUIRenderer_instances = new WeakSet(), _SelectionUIRenderer_drawRotatingBoundingBox = function _SelectionUIRenderer_drawRotatingBoundingBox(features, zoom, dragState) {
94
93
  // When dragState.operation === 'rotate', rotateInfo always exists
95
94
  const rotateInfo = dragState.rotateInfo;
96
95
  if (!rotateInfo)
@@ -115,99 +114,32 @@ _SelectionUIRenderer_strokeRenderer = new WeakMap(), _SelectionUIRenderer_extens
115
114
  if (!bbox)
116
115
  return;
117
116
  __classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_strokeBoundingBox).call(this, __classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_applyMargin).call(this, bbox, zoom), zoom);
118
- }, _SelectionUIRenderer_drawCombinedBoundingBox = function _SelectionUIRenderer_drawCombinedBoundingBox(features, zoom) {
119
- if (!__classPrivateFieldGet(this, _SelectionUIRenderer_transform, "f")) {
120
- // The fallback when there is no transform (without a margin)
121
- const bbox = computeCombinedBoundingBox(features, __classPrivateFieldGet(this, _SelectionUIRenderer_extensions, "f"));
122
- if (bbox)
123
- __classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_strokeBoundingBox).call(this, bbox, zoom);
124
- return;
125
- }
126
- const margin = __classPrivateFieldGet(this, _SelectionUIRenderer_config, "f").boundingBox.margin;
127
- const transform = __classPrivateFieldGet(this, _SelectionUIRenderer_transform, "f");
128
- let minLng = Number.POSITIVE_INFINITY;
129
- let maxLng = Number.NEGATIVE_INFINITY;
130
- let minLat = Number.POSITIVE_INFINITY;
131
- let maxLat = Number.NEGATIVE_INFINITY;
132
- for (const feature of features) {
133
- const bbox = computeBoundingBox(feature, __classPrivateFieldGet(this, _SelectionUIRenderer_extensions, "f"));
134
- if (!bbox)
135
- continue;
136
- // A single-coordinate feature (such as Point) is computed from the extent of the
137
- // point frame + the margin, as its own frame is (#pointFrame). The coordinate used is
138
- // the center of the bbox (features other than Point can be zero-area too, e.g. a
139
- // MultiPoint with a single point, so coordinates must not be treated as a Coordinate)
140
- if (hasZeroArea(bbox)) {
141
- const frame = __classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_pointFrame).call(this, feature, bbox.center, zoom);
142
- for (const [lng, lat] of frame?.coords ?? []) {
143
- minLng = Math.min(minLng, lng);
144
- maxLng = Math.max(maxLng, lng);
145
- minLat = Math.min(minLat, lat);
146
- maxLat = Math.max(maxLat, lat);
147
- }
148
- continue;
149
- }
150
- const marginedBbox = applyMarginToBoundingBox(bbox, margin, transform, zoom);
151
- const corners = [
152
- marginedBbox.topLeft,
153
- marginedBbox.topRight,
154
- marginedBbox.bottomRight,
155
- marginedBbox.bottomLeft,
156
- ];
157
- for (const corner of corners) {
158
- minLng = Math.min(minLng, corner[0]);
159
- maxLng = Math.max(maxLng, corner[0]);
160
- minLat = Math.min(minLat, corner[1]);
161
- maxLat = Math.max(maxLat, corner[1]);
162
- }
163
- }
164
- if (!Number.isFinite(minLng))
165
- return;
166
- const coords = [
167
- [minLng, maxLat],
168
- [maxLng, maxLat],
169
- [maxLng, minLat],
170
- [minLng, minLat],
171
- [minLng, maxLat],
172
- ];
173
- __classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_strokePath).call(this, coords, zoom);
174
117
  }, _SelectionUIRenderer_drawBoundingBoxWithMargin = function _SelectionUIRenderer_drawBoundingBoxWithMargin(features, zoom) {
175
118
  const bbox = computeCombinedBoundingBox(features, __classPrivateFieldGet(this, _SelectionUIRenderer_extensions, "f"));
176
119
  if (!bbox)
177
120
  return;
178
121
  __classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_strokeBoundingBox).call(this, __classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_applyMargin).call(this, bbox, zoom), zoom);
179
122
  }, _SelectionUIRenderer_drawPointBoundingBox = function _SelectionUIRenderer_drawPointBoundingBox(feature, coord, zoom) {
180
- const frame = __classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_pointFrame).call(this, feature, coord, zoom);
181
- if (!frame)
123
+ const frame = __classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_pointFrame).call(this, feature, coord);
124
+ const projectionData = __classPrivateFieldGet(this, _SelectionUIRenderer_projectionData, "f");
125
+ if (!frame || !projectionData)
182
126
  return;
183
- __classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_strokePath).call(this, frame.coords, zoom, {
184
- elevationMeters: frame.elevationMeters,
185
- followTerrain: false,
186
- });
187
- }, _SelectionUIRenderer_pointFrame = function _SelectionUIRenderer_pointFrame(feature, coord, zoom) {
127
+ const { x, y } = frame.center;
128
+ __classPrivateFieldGet(this, _SelectionUIRenderer_outlineRenderer, "f").draw([coord[0], coord[1]], frame.elevationMeters, frame.corners.map((corner) => ({ x: corner.x - x, y: corner.y - y })), __classPrivateFieldGet(this, _SelectionUIRenderer_config, "f").boundingBox.stroke, anchorGhostOpacity(__classPrivateFieldGet(this, _SelectionUIRenderer_terrain, "f"), coord[0], coord[1]), zoom, projectionData);
129
+ }, _SelectionUIRenderer_pointFrame = function _SelectionUIRenderer_pointFrame(feature, coord) {
188
130
  const transform = __classPrivateFieldGet(this, _SelectionUIRenderer_transform, "f");
189
131
  if (!transform)
190
132
  return null;
191
133
  const terrain = __classPrivateFieldGet(this, _SelectionUIRenderer_terrain, "f");
192
134
  const elevationMeters = anchorElevationMeters(terrain, coord[0], coord[1]);
193
- const anchored = isAnchorActive(terrain);
194
- const project = (at) => anchored ? projectAnchorAt(terrain, at[0], at[1], elevationMeters) : transform.project(at);
195
- const center = project(coord);
135
+ const center = isAnchorActive(terrain)
136
+ ? projectAnchorAt(terrain, coord[0], coord[1], elevationMeters)
137
+ : transform.project(coord);
196
138
  if (!center)
197
139
  return null;
198
140
  const margin = __classPrivateFieldGet(this, _SelectionUIRenderer_config, "f").boundingBox.margin;
199
141
  const corners = __classPrivateFieldGet(this, _SelectionUIRenderer_extensions, "f").resolvePointFrameCorners(feature, center, margin);
200
- const toPosition = anchored ? screenToPlane(project, coord, center, zoom) : null;
201
- const coords = corners.map((corner) => {
202
- const position = toPosition?.(corner);
203
- if (position)
204
- return position;
205
- // No terrain, or the plane seen edge on: the corner is cast onto the map
206
- const at = transform.unproject(corner);
207
- return [at.lng, at.lat];
208
- });
209
- coords.push(coords[0]);
210
- return { coords, elevationMeters };
142
+ return { anchor: coord, elevationMeters, center, corners };
211
143
  }, _SelectionUIRenderer_applyMargin = function _SelectionUIRenderer_applyMargin(bbox, zoom) {
212
144
  if (!__classPrivateFieldGet(this, _SelectionUIRenderer_transform, "f"))
213
145
  return bbox;
@@ -219,49 +151,6 @@ _SelectionUIRenderer_strokeRenderer = new WeakMap(), _SelectionUIRenderer_extens
219
151
  }, _SelectionUIRenderer_strokeBoundingBox = function _SelectionUIRenderer_strokeBoundingBox(bbox, zoom) {
220
152
  const corners = [bbox.topLeft, bbox.topRight, bbox.bottomRight, bbox.bottomLeft];
221
153
  __classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_strokePath).call(this, [...corners, bbox.topLeft], zoom);
222
- }, _SelectionUIRenderer_strokePath = function _SelectionUIRenderer_strokePath(coords, zoom, placement = { followTerrain: true }) {
223
- __classPrivateFieldGet(this, _SelectionUIRenderer_strokeRenderer, "f").draw(coords, __classPrivateFieldGet(this, _SelectionUIRenderer_config, "f").boundingBox.stroke, {
224
- widthUnit: 'pixels',
225
- closed: true,
226
- followTerrain: placement.followTerrain,
227
- ...(placement.elevationMeters !== undefined && {
228
- elevationMeters: placement.elevationMeters,
229
- }),
230
- }, zoom, __classPrivateFieldGet(this, _SelectionUIRenderer_projectionData, "f") ?? undefined);
154
+ }, _SelectionUIRenderer_strokePath = function _SelectionUIRenderer_strokePath(coords, zoom) {
155
+ __classPrivateFieldGet(this, _SelectionUIRenderer_strokeRenderer, "f").draw(coords, __classPrivateFieldGet(this, _SelectionUIRenderer_config, "f").boundingBox.stroke, { widthUnit: 'pixels', closed: true, followTerrain: true }, zoom, __classPrivateFieldGet(this, _SelectionUIRenderer_projectionData, "f") ?? undefined);
231
156
  };
232
- /**
233
- * The inverse of a projection around a point, to the first order: a point of the screen near
234
- * `center` to the position whose projection it is
235
- *
236
- * The scale of the screen along the longitude and the latitude is measured by projecting
237
- * positions a few pixels to either side of `coord`.
238
- *
239
- * @returns null when the projection is degenerate there (seen edge on, or a probe behind the
240
- * camera)
241
- */
242
- function screenToPlane(project, coord, center, zoom) {
243
- const [lng, lat] = coord;
244
- // Degrees of longitude per pixel at the zoom on a 512 px world, a few pixels' worth
245
- const step = (360 / (512 * 2 ** zoom)) * POINT_FRAME_PROBE_PX;
246
- const latStep = Math.min(step, Math.max(1e-9, (89.9 - Math.abs(lat)) / 2));
247
- const east = project([lng + step, lat]);
248
- const west = project([lng - step, lat]);
249
- const north = project([lng, lat + latStep]);
250
- const south = project([lng, lat - latStep]);
251
- if (!east || !west || !north || !south)
252
- return null;
253
- // The screen per degree of longitude (a, c) and of latitude (b, d)
254
- const a = (east.x - west.x) / (2 * step);
255
- const c = (east.y - west.y) / (2 * step);
256
- const b = (north.x - south.x) / (2 * latStep);
257
- const d = (north.y - south.y) / (2 * latStep);
258
- const det = a * d - b * c;
259
- const scale = Math.max(Math.abs(a), Math.abs(b), Math.abs(c), Math.abs(d));
260
- if (!Number.isFinite(det) || Math.abs(det) <= 1e-12 * scale * scale)
261
- return null;
262
- return (point) => {
263
- const dx = point.x - center.x;
264
- const dy = point.y - center.y;
265
- return [lng + (d * dx - b * dy) / det, lat + (a * dy - c * dx) / det];
266
- };
267
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakuzu/maplibre-gl-draw",
3
- "version": "2.1.2",
3
+ "version": "2.1.3",
4
4
  "description": "WebGL2 drawing and editing engine for MapLibre GL JS: points, lines, and polygons rendered in a custom layer, with selection, hit testing, and geometry utilities.",
5
5
  "keywords": [
6
6
  "maplibre",