@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 +28 -0
- package/dist/api/impl/engine.js +6 -0
- package/dist/api/impl/extension-host.js +3 -1
- package/dist/api/impl/input.js +12 -3
- package/dist/dispatcher/input-router.js +5 -3
- package/dist/modes/select/mode.js +8 -0
- package/dist/snapping/service.js +16 -0
- package/dist/view/layer/renderers.d.ts +3 -0
- package/dist/view/layer/renderers.js +6 -1
- package/dist/view/renderers/anchored-outline.d.ts +5 -0
- package/dist/view/renderers/anchored-outline.js +352 -0
- package/dist/view/shaders/initializer.d.ts +3 -0
- package/dist/view/shaders/initializer.js +8 -2
- package/dist/view/ui/selection-ui/renderer.d.ts +2 -1
- package/dist/view/ui/selection-ui/renderer.js +20 -131
- package/package.json +1 -1
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
|
package/dist/api/impl/engine.js
CHANGED
|
@@ -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();
|
package/dist/api/impl/input.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
package/dist/snapping/service.js
CHANGED
|
@@ -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,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,
|
|
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",
|
|
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
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|