@sakuzu/maplibre-gl-draw 2.1.1 → 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 +54 -2
- 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/click-handler.d.ts +4 -1
- package/dist/modes/select/click-handler.js +17 -9
- 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 +7 -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 +6 -0
- package/dist/view/shaders/initializer.js +10 -2
- package/dist/view/terrain/anchor.js +20 -1
- package/dist/view/ui/selection-ui/renderer.d.ts +3 -1
- package/dist/view/ui/selection-ui/renderer.js +32 -80
- package/dist/view/ui/selection-ui-drawer.js +4 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,13 +6,63 @@ 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
|
+
|
|
36
|
+
## [2.1.2] - 2026-10-02
|
|
37
|
+
|
|
38
|
+
Three fixes: a plugin's fill, the click that finishes a plugin's
|
|
39
|
+
exclusive interaction, and the selection frame of a point on the
|
|
40
|
+
terrain. Nothing else changes.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- A plugin's `fill.draw` drew nothing: the fill shared with the render
|
|
45
|
+
context of custom feature renderers was never given its shader, so a
|
|
46
|
+
renderer that filled an area with it left the area empty. It is now
|
|
47
|
+
prepared each frame with the other renderers.
|
|
48
|
+
- A click outside the element of a plugin's exclusive interaction
|
|
49
|
+
finished the interaction and did nothing else, so it took a second
|
|
50
|
+
click to select a feature. It now goes on as an ordinary click after
|
|
51
|
+
`finish()`: it selects the feature under it, or clears the selection
|
|
52
|
+
on the empty map. A click on the feature already selected does not
|
|
53
|
+
call `onFeatureClick`, so it does not start another interaction.
|
|
54
|
+
- With the terrain on and the camera pitched, the selection frame of a
|
|
55
|
+
point was stretched into a tall rectangle. It now lies flat at the
|
|
56
|
+
elevation of the point and keeps the size of the marker and the margin
|
|
57
|
+
([#12](https://github.com/sakuzu/maplibre-gl-draw/issues/12)).
|
|
58
|
+
|
|
9
59
|
## [2.1.1] - 2026-10-02
|
|
10
60
|
|
|
11
61
|
Three fixes to the lines drawn on the terrain: their thickness on a
|
|
12
62
|
high-DPI display, their thickness between the vertices, and dashed lines
|
|
13
63
|
that sank into the ground. Nothing else changes.
|
|
14
64
|
|
|
15
|
-
### Fixed
|
|
65
|
+
### Fixed in 2.1.1
|
|
16
66
|
|
|
17
67
|
- With the terrain on, a solid line or the outline of a polygon was drawn
|
|
18
68
|
half as thick on a high-DPI display (a device pixel ratio of 2) as
|
|
@@ -1168,7 +1218,9 @@ available from Kasika, Inc.
|
|
|
1168
1218
|
- A read-only mode and an interaction lock.
|
|
1169
1219
|
- Plugins, custom modes and custom feature types.
|
|
1170
1220
|
|
|
1171
|
-
[Unreleased]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.1.
|
|
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
|
|
1223
|
+
[2.1.2]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.1.1...v2.1.2
|
|
1172
1224
|
[2.1.1]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.1.0...v2.1.1
|
|
1173
1225
|
[2.1.0]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.0.0...v2.1.0
|
|
1174
1226
|
[2.0.0]: https://github.com/sakuzu/maplibre-gl-draw/compare/v1.0.0...v2.0.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));
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Single click: the priority order is plugin intercept -> vertex click -> feature selection
|
|
5
5
|
* 1. While a plugin is interacting, clicks inside its container are consumed; anything else
|
|
6
|
-
* finishes it
|
|
6
|
+
* finishes it and then goes on as an ordinary click (it selects the feature under it, or
|
|
7
|
+
* clears the selection on the empty map), except that re-clicking the selected feature is
|
|
8
|
+
* not delegated to plugins.handleFeatureClick (the click that finished an interaction does
|
|
9
|
+
* not start another)
|
|
7
10
|
* 2. A click on a vertex handle of a singly selected feature that supports vertex editing
|
|
8
11
|
* (LineString / Polygon / the Multi types) -> vertex selection
|
|
9
12
|
* 3. Unified z traversal (hitTestTopmost) -> if it is a Store feature, the selection is
|
|
@@ -11,7 +11,8 @@ import { getSelectedFeatures } from '../../view/ui/helper.js';
|
|
|
11
11
|
* The main logic of onClick
|
|
12
12
|
*/
|
|
13
13
|
export function handleSelectClick(event, context, config) {
|
|
14
|
-
|
|
14
|
+
const interaction = handlePluginInteractionClick(event, context);
|
|
15
|
+
if (interaction === 'inside') {
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
17
18
|
const { store } = context;
|
|
@@ -21,7 +22,7 @@ export function handleSelectClick(event, context, config) {
|
|
|
21
22
|
return;
|
|
22
23
|
}
|
|
23
24
|
store.setSelectedVertices(null);
|
|
24
|
-
handleFeatureClick(event, selection, selectedIds, context);
|
|
25
|
+
handleFeatureClick(event, selection, selectedIds, context, interaction === 'finished');
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
28
|
* The main logic of onDoubleClick
|
|
@@ -48,21 +49,21 @@ export function handleSelectDoubleClick(event, context) {
|
|
|
48
49
|
/**
|
|
49
50
|
* Click handling during a plugin interaction
|
|
50
51
|
*
|
|
51
|
-
*
|
|
52
|
+
* Only `inside` consumes the click; after `finished` the caller handles it as an ordinary click.
|
|
52
53
|
*/
|
|
53
54
|
function handlePluginInteractionClick(event, context) {
|
|
54
55
|
const pm = context.plugins;
|
|
55
56
|
if (!pm?.isPluginInteracting())
|
|
56
|
-
return
|
|
57
|
+
return 'none';
|
|
57
58
|
const container = pm.getPluginInteractionContainer();
|
|
58
59
|
if (container) {
|
|
59
60
|
const target = event.originalEvent.target;
|
|
60
61
|
if (container.contains(target)) {
|
|
61
|
-
return
|
|
62
|
+
return 'inside';
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
pm.finishPluginInteraction();
|
|
65
|
-
return
|
|
66
|
+
return 'finished';
|
|
66
67
|
}
|
|
67
68
|
/**
|
|
68
69
|
* Click handling for vertex handles
|
|
@@ -108,8 +109,11 @@ function handleVertexClick(event, selectedIds, context, config) {
|
|
|
108
109
|
}
|
|
109
110
|
/**
|
|
110
111
|
* Feature selection handling
|
|
112
|
+
*
|
|
113
|
+
* @param finishedInteraction The click finished a plugin interaction: re-clicking the selected
|
|
114
|
+
* feature is not delegated to the plugins
|
|
111
115
|
*/
|
|
112
|
-
function handleFeatureClick(event, selection, selectedIds, context) {
|
|
116
|
+
function handleFeatureClick(event, selection, selectedIds, context, finishedInteraction) {
|
|
113
117
|
const { store } = context;
|
|
114
118
|
const orderedFeatures = getDisplayFeatures(store);
|
|
115
119
|
let top = context.hitTestTopmost(event.point, { orderedFeatures });
|
|
@@ -134,8 +138,12 @@ function handleFeatureClick(event, selection, selectedIds, context) {
|
|
|
134
138
|
return;
|
|
135
139
|
}
|
|
136
140
|
const feature = top.feature;
|
|
137
|
-
// Re-clicking an already selected feature -> delegate to the plugins
|
|
138
|
-
|
|
141
|
+
// Re-clicking an already selected feature -> delegate to the plugins (not when the click
|
|
142
|
+
// finished an interaction)
|
|
143
|
+
if (!finishedInteraction &&
|
|
144
|
+
!event.modifiers.shift &&
|
|
145
|
+
selectedIds.length === 1 &&
|
|
146
|
+
selectedIds.includes(feature.id)) {
|
|
139
147
|
if (context.plugins?.handleFeatureClick(feature.id, event)) {
|
|
140
148
|
return;
|
|
141
149
|
}
|
|
@@ -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);
|
|
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,11 +76,13 @@ export function initRenderers(deps) {
|
|
|
74
76
|
tentativeRenderer,
|
|
75
77
|
quadShader,
|
|
76
78
|
strokeRenderer,
|
|
79
|
+
outlineRenderer,
|
|
77
80
|
pointShapeRenderer,
|
|
78
81
|
pointInstanceRenderer,
|
|
79
82
|
sdfLineRenderer,
|
|
80
83
|
polygonBatchRenderer,
|
|
81
84
|
sdfPolygonRenderer,
|
|
85
|
+
fillShaderManager,
|
|
82
86
|
});
|
|
83
87
|
const batchManager = new BatchManager({
|
|
84
88
|
gl,
|
|
@@ -94,6 +98,7 @@ export function initRenderers(deps) {
|
|
|
94
98
|
quadShader,
|
|
95
99
|
imageRenderer,
|
|
96
100
|
strokeRenderer,
|
|
101
|
+
outlineRenderer,
|
|
97
102
|
sdfLineRenderer,
|
|
98
103
|
pointShapeRenderer,
|
|
99
104
|
pointInstanceRenderer,
|
|
@@ -124,6 +129,7 @@ const GPU_OWNERS = [
|
|
|
124
129
|
'textureCache',
|
|
125
130
|
'quadShader',
|
|
126
131
|
'strokeRenderer',
|
|
132
|
+
'outlineRenderer',
|
|
127
133
|
'pointShapeRenderer',
|
|
128
134
|
'pointInstanceRenderer',
|
|
129
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,8 +1,10 @@
|
|
|
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';
|
|
4
5
|
import type { PointShapeRenderer } from '../renderers/point/point-shape.js';
|
|
5
6
|
import type { PolygonBatchRenderer } from '../renderers/polygon/batch.js';
|
|
7
|
+
import type { FillShaderManager } from '../renderers/polygon/fill.js';
|
|
6
8
|
import type { SDFPolygonRenderer } from '../renderers/polygon/sdf-polygon.js';
|
|
7
9
|
import type { StrokeRenderer } from '../renderers/stroke.js';
|
|
8
10
|
import type { TentativeRenderer } from '../ui/tentative.js';
|
|
@@ -15,11 +17,15 @@ export interface ShaderInitializerRenderers {
|
|
|
15
17
|
tentativeRenderer: TentativeRenderer | null;
|
|
16
18
|
quadShader: QuadShader | null;
|
|
17
19
|
strokeRenderer: StrokeRenderer | null;
|
|
20
|
+
/** The outlines laid on the screen around an anchor (the frames of points) */
|
|
21
|
+
outlineRenderer: AnchoredOutlineRenderer | null;
|
|
18
22
|
pointShapeRenderer: PointShapeRenderer | null;
|
|
19
23
|
pointInstanceRenderer: PointInstanceRenderer | null;
|
|
20
24
|
sdfLineRenderer: SDFLineRenderer | null;
|
|
21
25
|
polygonBatchRenderer: PolygonBatchRenderer | null;
|
|
22
26
|
sdfPolygonRenderer: SDFPolygonRenderer | null;
|
|
27
|
+
/** The fill shared with the render context of custom feature renderers */
|
|
28
|
+
fillShaderManager: FillShaderManager | null;
|
|
23
29
|
}
|
|
24
30
|
/**
|
|
25
31
|
* Creates a ShaderInitializer
|
|
@@ -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, } = 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
|
}
|
|
@@ -51,6 +54,7 @@ export class ShaderInitializer {
|
|
|
51
54
|
if (sdfPolygonRenderer) {
|
|
52
55
|
sdfPolygonRenderer.ensureShader(shaderData);
|
|
53
56
|
}
|
|
57
|
+
fillShaderManager?.ensureShader(shaderData);
|
|
54
58
|
}
|
|
55
59
|
/**
|
|
56
60
|
* Sets the projection data on the renderers that need it
|
|
@@ -79,7 +83,7 @@ export class ShaderInitializer {
|
|
|
79
83
|
* uniforms once per frame and applies them for each slot.
|
|
80
84
|
*/
|
|
81
85
|
applyOffsetUniforms(uniforms) {
|
|
82
|
-
const { featureDrawer, tentativeRenderer, quadShader, strokeRenderer, pointShapeRenderer, pointInstanceRenderer, sdfLineRenderer, polygonBatchRenderer, sdfPolygonRenderer, } = this.renderers;
|
|
86
|
+
const { featureDrawer, tentativeRenderer, quadShader, strokeRenderer, outlineRenderer, pointShapeRenderer, pointInstanceRenderer, sdfLineRenderer, polygonBatchRenderer, sdfPolygonRenderer, fillShaderManager, } = this.renderers;
|
|
83
87
|
if (featureDrawer) {
|
|
84
88
|
featureDrawer.setOffsetUniforms(uniforms);
|
|
85
89
|
}
|
|
@@ -92,6 +96,9 @@ export class ShaderInitializer {
|
|
|
92
96
|
if (strokeRenderer) {
|
|
93
97
|
strokeRenderer.setOffsetUniforms(uniforms);
|
|
94
98
|
}
|
|
99
|
+
if (outlineRenderer) {
|
|
100
|
+
outlineRenderer.setOffsetUniforms(uniforms);
|
|
101
|
+
}
|
|
95
102
|
if (pointShapeRenderer) {
|
|
96
103
|
pointShapeRenderer.setOffsetUniforms(uniforms);
|
|
97
104
|
}
|
|
@@ -107,6 +114,7 @@ export class ShaderInitializer {
|
|
|
107
114
|
if (sdfPolygonRenderer) {
|
|
108
115
|
sdfPolygonRenderer.setOffsetUniforms(uniforms);
|
|
109
116
|
}
|
|
117
|
+
fillShaderManager?.setOffsetUniforms(uniforms);
|
|
110
118
|
}
|
|
111
119
|
/**
|
|
112
120
|
* Increments the frame count of SDFLineRenderer
|
|
@@ -140,11 +140,30 @@ export function drawnLongitude(context, lng) {
|
|
|
140
140
|
* @internal
|
|
141
141
|
*/
|
|
142
142
|
export function projectAnchor(context, lng, lat) {
|
|
143
|
+
const frame = context.anchorFrame;
|
|
144
|
+
if (!frame.mainMatrix || !frame.terrain)
|
|
145
|
+
return null;
|
|
146
|
+
return projectAnchorAt(context, lng, lat, anchorElevationMeters(context, lng, lat));
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Maps a position at a given elevation to screen coordinates, with the projection of
|
|
150
|
+
* {@link projectAnchor}
|
|
151
|
+
*
|
|
152
|
+
* Something drawn flat at one elevation, such as the frame of a selected point at the
|
|
153
|
+
* elevation of the point, is projected with this so that it lands where the shader draws it.
|
|
154
|
+
*
|
|
155
|
+
* @param elevationMeters The elevation in meters (the exaggeration included, as
|
|
156
|
+
* {@link anchorElevationMeters} gives it)
|
|
157
|
+
* @returns null when the terrain is disabled, the frame state has not been established or the
|
|
158
|
+
* position is behind the camera
|
|
159
|
+
* @internal
|
|
160
|
+
*/
|
|
161
|
+
export function projectAnchorAt(context, lng, lat, elevationMeters) {
|
|
143
162
|
const frame = context.anchorFrame;
|
|
144
163
|
const { mainMatrix: m, offsetUniforms, terrain, width, height } = frame;
|
|
145
164
|
if (!m || !terrain || width <= 0 || height <= 0)
|
|
146
165
|
return null;
|
|
147
|
-
const elevationMercator =
|
|
166
|
+
const elevationMercator = elevationMeters * frame.elevationScale;
|
|
148
167
|
// The anchor is projected on the copy of the world it is drawn on
|
|
149
168
|
const drawnLng = drawnLongitude(context, lng);
|
|
150
169
|
let clipX;
|
|
@@ -9,12 +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';
|
|
14
|
+
import { TerrainContext } from '../../terrain/context.js';
|
|
13
15
|
import type { SelectionExtensionRegistry } from './extension-registry.js';
|
|
14
16
|
import type { BoundingBoxCoords } from './types.js';
|
|
15
17
|
export declare class SelectionUIRenderer {
|
|
16
18
|
#private;
|
|
17
|
-
constructor(strokeRenderer: StrokeRenderer, config: SelectionUIConfig, extensions: SelectionExtensionRegistry);
|
|
19
|
+
constructor(strokeRenderer: StrokeRenderer, outlineRenderer: AnchoredOutlineRenderer, config: SelectionUIConfig, extensions: SelectionExtensionRegistry, terrain?: TerrainContext);
|
|
18
20
|
/** Compute the BoundingBox of a feature (also usable from outside) */
|
|
19
21
|
computeFeatureBoundingBox(feature: Feature): BoundingBoxCoords | null;
|
|
20
22
|
/** Compute the combined BoundingBox of several features (also usable from outside) */
|
|
@@ -11,21 +11,30 @@ 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_config, _SelectionUIRenderer_transform, _SelectionUIRenderer_projectionData, _SelectionUIRenderer_drawRotatingBoundingBox, _SelectionUIRenderer_drawIndividualBoundingBox,
|
|
15
|
-
import { applyMarginToBoundingBox } from '../../../shared/math/index.js';
|
|
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
|
+
import { applyMarginToBoundingBox, } from '../../../shared/math/index.js';
|
|
16
|
+
import { anchorElevationMeters, isAnchorActive, projectAnchorAt } from '../../terrain/anchor.js';
|
|
17
|
+
import { TerrainContext } from '../../terrain/context.js';
|
|
18
|
+
import { anchorGhostOpacity } from '../../terrain/occlusion.js';
|
|
16
19
|
import { computeBoundingBox, computeCombinedBoundingBox, hasZeroArea, rotateBoundingBox, } from './bounding-box.js';
|
|
17
20
|
export class SelectionUIRenderer {
|
|
18
|
-
constructor(strokeRenderer, config, extensions) {
|
|
21
|
+
constructor(strokeRenderer, outlineRenderer, config, extensions, terrain = new TerrainContext()) {
|
|
19
22
|
_SelectionUIRenderer_instances.add(this);
|
|
20
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);
|
|
21
26
|
/** The Selection UI extension points of the draw instance this renderer belongs to */
|
|
22
27
|
_SelectionUIRenderer_extensions.set(this, void 0);
|
|
28
|
+
/** The terrain state of the draw instance (an inactive one projects with the map) */
|
|
29
|
+
_SelectionUIRenderer_terrain.set(this, void 0);
|
|
23
30
|
_SelectionUIRenderer_config.set(this, void 0);
|
|
24
31
|
_SelectionUIRenderer_transform.set(this, null);
|
|
25
32
|
_SelectionUIRenderer_projectionData.set(this, null);
|
|
26
33
|
__classPrivateFieldSet(this, _SelectionUIRenderer_strokeRenderer, strokeRenderer, "f");
|
|
34
|
+
__classPrivateFieldSet(this, _SelectionUIRenderer_outlineRenderer, outlineRenderer, "f");
|
|
27
35
|
__classPrivateFieldSet(this, _SelectionUIRenderer_config, config, "f");
|
|
28
36
|
__classPrivateFieldSet(this, _SelectionUIRenderer_extensions, extensions, "f");
|
|
37
|
+
__classPrivateFieldSet(this, _SelectionUIRenderer_terrain, terrain, "f");
|
|
29
38
|
}
|
|
30
39
|
/** Compute the BoundingBox of a feature (also usable from outside) */
|
|
31
40
|
computeFeatureBoundingBox(feature) {
|
|
@@ -77,10 +86,10 @@ export class SelectionUIRenderer {
|
|
|
77
86
|
__classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_drawIndividualBoundingBox).call(this, feature, zoom);
|
|
78
87
|
}
|
|
79
88
|
}
|
|
80
|
-
__classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m",
|
|
89
|
+
__classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_drawBoundingBoxWithMargin).call(this, features, zoom);
|
|
81
90
|
}
|
|
82
91
|
}
|
|
83
|
-
_SelectionUIRenderer_strokeRenderer = new WeakMap(), _SelectionUIRenderer_extensions = 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) {
|
|
84
93
|
// When dragState.operation === 'rotate', rotateInfo always exists
|
|
85
94
|
const rotateInfo = dragState.rotateInfo;
|
|
86
95
|
if (!rotateInfo)
|
|
@@ -105,82 +114,32 @@ _SelectionUIRenderer_strokeRenderer = new WeakMap(), _SelectionUIRenderer_extens
|
|
|
105
114
|
if (!bbox)
|
|
106
115
|
return;
|
|
107
116
|
__classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_strokeBoundingBox).call(this, __classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_applyMargin).call(this, bbox, zoom), zoom);
|
|
108
|
-
}, _SelectionUIRenderer_drawCombinedBoundingBox = function _SelectionUIRenderer_drawCombinedBoundingBox(features, zoom) {
|
|
109
|
-
if (!__classPrivateFieldGet(this, _SelectionUIRenderer_transform, "f")) {
|
|
110
|
-
// The fallback when there is no transform (without a margin)
|
|
111
|
-
const bbox = computeCombinedBoundingBox(features, __classPrivateFieldGet(this, _SelectionUIRenderer_extensions, "f"));
|
|
112
|
-
if (bbox)
|
|
113
|
-
__classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_strokeBoundingBox).call(this, bbox, zoom);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const margin = __classPrivateFieldGet(this, _SelectionUIRenderer_config, "f").boundingBox.margin;
|
|
117
|
-
const transform = __classPrivateFieldGet(this, _SelectionUIRenderer_transform, "f");
|
|
118
|
-
let minLng = Number.POSITIVE_INFINITY;
|
|
119
|
-
let maxLng = Number.NEGATIVE_INFINITY;
|
|
120
|
-
let minLat = Number.POSITIVE_INFINITY;
|
|
121
|
-
let maxLat = Number.NEGATIVE_INFINITY;
|
|
122
|
-
for (const feature of features) {
|
|
123
|
-
const bbox = computeBoundingBox(feature, __classPrivateFieldGet(this, _SelectionUIRenderer_extensions, "f"));
|
|
124
|
-
if (!bbox)
|
|
125
|
-
continue;
|
|
126
|
-
// A single-coordinate feature (such as Point) is computed from the extent of the
|
|
127
|
-
// point frame + the margin. The coordinate used is the center of the bbox (features
|
|
128
|
-
// other than Point can be zero-area too, e.g. a MultiPoint with a single point, so
|
|
129
|
-
// coordinates must not be treated as a Coordinate)
|
|
130
|
-
if (hasZeroArea(bbox)) {
|
|
131
|
-
const center = transform.project(bbox.center);
|
|
132
|
-
for (const corner of __classPrivateFieldGet(this, _SelectionUIRenderer_extensions, "f").resolvePointFrameCorners(feature, center, margin)) {
|
|
133
|
-
const at = transform.unproject(corner);
|
|
134
|
-
minLng = Math.min(minLng, at.lng);
|
|
135
|
-
maxLng = Math.max(maxLng, at.lng);
|
|
136
|
-
minLat = Math.min(minLat, at.lat);
|
|
137
|
-
maxLat = Math.max(maxLat, at.lat);
|
|
138
|
-
}
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
const marginedBbox = applyMarginToBoundingBox(bbox, margin, transform, zoom);
|
|
142
|
-
const corners = [
|
|
143
|
-
marginedBbox.topLeft,
|
|
144
|
-
marginedBbox.topRight,
|
|
145
|
-
marginedBbox.bottomRight,
|
|
146
|
-
marginedBbox.bottomLeft,
|
|
147
|
-
];
|
|
148
|
-
for (const corner of corners) {
|
|
149
|
-
minLng = Math.min(minLng, corner[0]);
|
|
150
|
-
maxLng = Math.max(maxLng, corner[0]);
|
|
151
|
-
minLat = Math.min(minLat, corner[1]);
|
|
152
|
-
maxLat = Math.max(maxLat, corner[1]);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (!Number.isFinite(minLng))
|
|
156
|
-
return;
|
|
157
|
-
const coords = [
|
|
158
|
-
[minLng, maxLat],
|
|
159
|
-
[maxLng, maxLat],
|
|
160
|
-
[maxLng, minLat],
|
|
161
|
-
[minLng, minLat],
|
|
162
|
-
[minLng, maxLat],
|
|
163
|
-
];
|
|
164
|
-
__classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_strokePath).call(this, coords, zoom);
|
|
165
117
|
}, _SelectionUIRenderer_drawBoundingBoxWithMargin = function _SelectionUIRenderer_drawBoundingBoxWithMargin(features, zoom) {
|
|
166
118
|
const bbox = computeCombinedBoundingBox(features, __classPrivateFieldGet(this, _SelectionUIRenderer_extensions, "f"));
|
|
167
119
|
if (!bbox)
|
|
168
120
|
return;
|
|
169
121
|
__classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_strokeBoundingBox).call(this, __classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_applyMargin).call(this, bbox, zoom), zoom);
|
|
170
122
|
}, _SelectionUIRenderer_drawPointBoundingBox = function _SelectionUIRenderer_drawPointBoundingBox(feature, coord, zoom) {
|
|
171
|
-
|
|
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)
|
|
172
126
|
return;
|
|
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) {
|
|
173
130
|
const transform = __classPrivateFieldGet(this, _SelectionUIRenderer_transform, "f");
|
|
174
|
-
|
|
131
|
+
if (!transform)
|
|
132
|
+
return null;
|
|
133
|
+
const terrain = __classPrivateFieldGet(this, _SelectionUIRenderer_terrain, "f");
|
|
134
|
+
const elevationMeters = anchorElevationMeters(terrain, coord[0], coord[1]);
|
|
135
|
+
const center = isAnchorActive(terrain)
|
|
136
|
+
? projectAnchorAt(terrain, coord[0], coord[1], elevationMeters)
|
|
137
|
+
: transform.project(coord);
|
|
138
|
+
if (!center)
|
|
139
|
+
return null;
|
|
175
140
|
const margin = __classPrivateFieldGet(this, _SelectionUIRenderer_config, "f").boundingBox.margin;
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
.map((corner) => {
|
|
179
|
-
const at = transform.unproject(corner);
|
|
180
|
-
return [at.lng, at.lat];
|
|
181
|
-
});
|
|
182
|
-
coords.push(coords[0]);
|
|
183
|
-
__classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_strokePath).call(this, coords, zoom);
|
|
141
|
+
const corners = __classPrivateFieldGet(this, _SelectionUIRenderer_extensions, "f").resolvePointFrameCorners(feature, center, margin);
|
|
142
|
+
return { anchor: coord, elevationMeters, center, corners };
|
|
184
143
|
}, _SelectionUIRenderer_applyMargin = function _SelectionUIRenderer_applyMargin(bbox, zoom) {
|
|
185
144
|
if (!__classPrivateFieldGet(this, _SelectionUIRenderer_transform, "f"))
|
|
186
145
|
return bbox;
|
|
@@ -193,12 +152,5 @@ _SelectionUIRenderer_strokeRenderer = new WeakMap(), _SelectionUIRenderer_extens
|
|
|
193
152
|
const corners = [bbox.topLeft, bbox.topRight, bbox.bottomRight, bbox.bottomLeft];
|
|
194
153
|
__classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_strokePath).call(this, [...corners, bbox.topLeft], zoom);
|
|
195
154
|
}, _SelectionUIRenderer_strokePath = function _SelectionUIRenderer_strokePath(coords, zoom) {
|
|
196
|
-
__classPrivateFieldGet(this, _SelectionUIRenderer_strokeRenderer, "f").draw(coords, __classPrivateFieldGet(this, _SelectionUIRenderer_config, "f").boundingBox.stroke, {
|
|
197
|
-
widthUnit: 'pixels',
|
|
198
|
-
closed: true,
|
|
199
|
-
// The frame follows the ground. The elevation comes from the same function as the
|
|
200
|
-
// handles, so the corners of the frame always match the corner handles and never
|
|
201
|
-
// drift from the position that can be grabbed
|
|
202
|
-
followTerrain: true,
|
|
203
|
-
}, zoom, __classPrivateFieldGet(this, _SelectionUIRenderer_projectionData, "f") ?? undefined);
|
|
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);
|
|
204
156
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 SAKAIDA Atsushi
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
3
|
import { THINNING_VIEWPORT_MARGIN_PX } from '../../shared/config/selection.js';
|
|
4
|
+
import { createCoordinateTransform } from '../../shared/math/index.js';
|
|
4
5
|
import { supportsVertexEditing } from '../../shared/utils/coordinates.js';
|
|
5
6
|
import { isGlobeProjection } from '../../shared/utils/map.js';
|
|
6
7
|
import { isLocallyHidden } from '../../store/local-visibility.js';
|
|
@@ -25,14 +26,9 @@ isLocked = false) {
|
|
|
25
26
|
if (pointInstanceRenderer) {
|
|
26
27
|
pointInstanceRenderer.setProjectionData(projectionData);
|
|
27
28
|
}
|
|
28
|
-
// Set the coordinate transform
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
unproject: (point) => {
|
|
32
|
-
const ll = map.unproject([point.x, point.y]);
|
|
33
|
-
return { lng: ll.lng, lat: ll.lat };
|
|
34
|
-
},
|
|
35
|
-
};
|
|
29
|
+
// Set the coordinate transform: the projection the markers are drawn with and the hit
|
|
30
|
+
// testing uses (the anchor projection while the terrain is on, the map's otherwise)
|
|
31
|
+
const coordinateTransform = createCoordinateTransform(map);
|
|
36
32
|
selectionUIRenderer.setTransform(coordinateTransform);
|
|
37
33
|
selectionUIRenderer.setProjectionData(projectionData);
|
|
38
34
|
selectionHandlesRenderer.setTransform(coordinateTransform);
|
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",
|