@sakuzu/maplibre-gl-draw 2.1.1 → 2.1.2
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 +26 -2
- package/dist/modes/select/click-handler.d.ts +4 -1
- package/dist/modes/select/click-handler.js +17 -9
- package/dist/view/layer/renderers.js +2 -1
- package/dist/view/shaders/initializer.d.ts +3 -0
- package/dist/view/shaders/initializer.js +4 -2
- package/dist/view/terrain/anchor.js +20 -1
- package/dist/view/ui/selection-ui/renderer.d.ts +2 -1
- package/dist/view/ui/selection-ui/renderer.js +88 -25
- package/dist/view/ui/selection-ui-drawer.js +4 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,13 +6,36 @@ the project follows semantic versioning.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.1.2] - 2026-10-02
|
|
10
|
+
|
|
11
|
+
Three fixes: a plugin's fill, the click that finishes a plugin's
|
|
12
|
+
exclusive interaction, and the selection frame of a point on the
|
|
13
|
+
terrain. Nothing else changes.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- A plugin's `fill.draw` drew nothing: the fill shared with the render
|
|
18
|
+
context of custom feature renderers was never given its shader, so a
|
|
19
|
+
renderer that filled an area with it left the area empty. It is now
|
|
20
|
+
prepared each frame with the other renderers.
|
|
21
|
+
- A click outside the element of a plugin's exclusive interaction
|
|
22
|
+
finished the interaction and did nothing else, so it took a second
|
|
23
|
+
click to select a feature. It now goes on as an ordinary click after
|
|
24
|
+
`finish()`: it selects the feature under it, or clears the selection
|
|
25
|
+
on the empty map. A click on the feature already selected does not
|
|
26
|
+
call `onFeatureClick`, so it does not start another interaction.
|
|
27
|
+
- With the terrain on and the camera pitched, the selection frame of a
|
|
28
|
+
point was stretched into a tall rectangle. It now lies flat at the
|
|
29
|
+
elevation of the point and keeps the size of the marker and the margin
|
|
30
|
+
([#12](https://github.com/sakuzu/maplibre-gl-draw/issues/12)).
|
|
31
|
+
|
|
9
32
|
## [2.1.1] - 2026-10-02
|
|
10
33
|
|
|
11
34
|
Three fixes to the lines drawn on the terrain: their thickness on a
|
|
12
35
|
high-DPI display, their thickness between the vertices, and dashed lines
|
|
13
36
|
that sank into the ground. Nothing else changes.
|
|
14
37
|
|
|
15
|
-
### Fixed
|
|
38
|
+
### Fixed in 2.1.1
|
|
16
39
|
|
|
17
40
|
- With the terrain on, a solid line or the outline of a polygon was drawn
|
|
18
41
|
half as thick on a high-DPI display (a device pixel ratio of 2) as
|
|
@@ -1168,7 +1191,8 @@ available from Kasika, Inc.
|
|
|
1168
1191
|
- A read-only mode and an interaction lock.
|
|
1169
1192
|
- Plugins, custom modes and custom feature types.
|
|
1170
1193
|
|
|
1171
|
-
[Unreleased]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.1.
|
|
1194
|
+
[Unreleased]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.1.2...HEAD
|
|
1195
|
+
[2.1.2]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.1.1...v2.1.2
|
|
1172
1196
|
[2.1.1]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.1.0...v2.1.1
|
|
1173
1197
|
[2.1.0]: https://github.com/sakuzu/maplibre-gl-draw/compare/v2.0.0...v2.1.0
|
|
1174
1198
|
[2.0.0]: https://github.com/sakuzu/maplibre-gl-draw/compare/v1.0.0...v2.0.0
|
|
@@ -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
|
}
|
|
@@ -35,7 +35,7 @@ export function initRenderers(deps) {
|
|
|
35
35
|
const sdfPolygonRenderer = new SDFPolygonRenderer(gl, pixelRatio, polygonScope);
|
|
36
36
|
const fillShaderManager = new FillShaderManager(gl, terrain);
|
|
37
37
|
const selectionExtensions = scope.selection.extensions;
|
|
38
|
-
const selectionUIRenderer = new SelectionUIRenderer(strokeRenderer, selectionConfig, selectionExtensions);
|
|
38
|
+
const selectionUIRenderer = new SelectionUIRenderer(strokeRenderer, selectionConfig, selectionExtensions, terrain);
|
|
39
39
|
// The tile size of this instance's map (read by the custom bounding box calculators)
|
|
40
40
|
const tileSize = mapInstance.transform?.tileSize ?? 512;
|
|
41
41
|
selectionExtensions.setTileSize(tileSize);
|
|
@@ -79,6 +79,7 @@ export function initRenderers(deps) {
|
|
|
79
79
|
sdfLineRenderer,
|
|
80
80
|
polygonBatchRenderer,
|
|
81
81
|
sdfPolygonRenderer,
|
|
82
|
+
fillShaderManager,
|
|
82
83
|
});
|
|
83
84
|
const batchManager = new BatchManager({
|
|
84
85
|
gl,
|
|
@@ -3,6 +3,7 @@ import type { SDFLineRenderer } from '../renderers/line/sdf-line.js';
|
|
|
3
3
|
import type { PointInstanceRenderer } from '../renderers/point/point-instance.js';
|
|
4
4
|
import type { PointShapeRenderer } from '../renderers/point/point-shape.js';
|
|
5
5
|
import type { PolygonBatchRenderer } from '../renderers/polygon/batch.js';
|
|
6
|
+
import type { FillShaderManager } from '../renderers/polygon/fill.js';
|
|
6
7
|
import type { SDFPolygonRenderer } from '../renderers/polygon/sdf-polygon.js';
|
|
7
8
|
import type { StrokeRenderer } from '../renderers/stroke.js';
|
|
8
9
|
import type { TentativeRenderer } from '../ui/tentative.js';
|
|
@@ -20,6 +21,8 @@ export interface ShaderInitializerRenderers {
|
|
|
20
21
|
sdfLineRenderer: SDFLineRenderer | null;
|
|
21
22
|
polygonBatchRenderer: PolygonBatchRenderer | null;
|
|
22
23
|
sdfPolygonRenderer: SDFPolygonRenderer | null;
|
|
24
|
+
/** The fill shared with the render context of custom feature renderers */
|
|
25
|
+
fillShaderManager: FillShaderManager | null;
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
25
28
|
* 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, pointShapeRenderer, pointInstanceRenderer, sdfLineRenderer, polygonBatchRenderer, sdfPolygonRenderer, fillShaderManager, } = this.renderers;
|
|
27
27
|
if (featureDrawer) {
|
|
28
28
|
featureDrawer.ensureShader(shaderData);
|
|
29
29
|
}
|
|
@@ -51,6 +51,7 @@ export class ShaderInitializer {
|
|
|
51
51
|
if (sdfPolygonRenderer) {
|
|
52
52
|
sdfPolygonRenderer.ensureShader(shaderData);
|
|
53
53
|
}
|
|
54
|
+
fillShaderManager?.ensureShader(shaderData);
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
56
57
|
* Sets the projection data on the renderers that need it
|
|
@@ -79,7 +80,7 @@ export class ShaderInitializer {
|
|
|
79
80
|
* uniforms once per frame and applies them for each slot.
|
|
80
81
|
*/
|
|
81
82
|
applyOffsetUniforms(uniforms) {
|
|
82
|
-
const { featureDrawer, tentativeRenderer, quadShader, strokeRenderer, pointShapeRenderer, pointInstanceRenderer, sdfLineRenderer, polygonBatchRenderer, sdfPolygonRenderer, } = this.renderers;
|
|
83
|
+
const { featureDrawer, tentativeRenderer, quadShader, strokeRenderer, pointShapeRenderer, pointInstanceRenderer, sdfLineRenderer, polygonBatchRenderer, sdfPolygonRenderer, fillShaderManager, } = this.renderers;
|
|
83
84
|
if (featureDrawer) {
|
|
84
85
|
featureDrawer.setOffsetUniforms(uniforms);
|
|
85
86
|
}
|
|
@@ -107,6 +108,7 @@ export class ShaderInitializer {
|
|
|
107
108
|
if (sdfPolygonRenderer) {
|
|
108
109
|
sdfPolygonRenderer.setOffsetUniforms(uniforms);
|
|
109
110
|
}
|
|
111
|
+
fillShaderManager?.setOffsetUniforms(uniforms);
|
|
110
112
|
}
|
|
111
113
|
/**
|
|
112
114
|
* 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;
|
|
@@ -10,11 +10,12 @@ 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
12
|
import type { StrokeRenderer } from '../../renderers/stroke.js';
|
|
13
|
+
import { TerrainContext } from '../../terrain/context.js';
|
|
13
14
|
import type { SelectionExtensionRegistry } from './extension-registry.js';
|
|
14
15
|
import type { BoundingBoxCoords } from './types.js';
|
|
15
16
|
export declare class SelectionUIRenderer {
|
|
16
17
|
#private;
|
|
17
|
-
constructor(strokeRenderer: StrokeRenderer, config: SelectionUIConfig, extensions: SelectionExtensionRegistry);
|
|
18
|
+
constructor(strokeRenderer: StrokeRenderer, config: SelectionUIConfig, extensions: SelectionExtensionRegistry, terrain?: TerrainContext);
|
|
18
19
|
/** Compute the BoundingBox of a feature (also usable from outside) */
|
|
19
20
|
computeFeatureBoundingBox(feature: Feature): BoundingBoxCoords | null;
|
|
20
21
|
/** Compute the combined BoundingBox of several features (also usable from outside) */
|
|
@@ -11,21 +11,31 @@ 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, _SelectionUIRenderer_drawCombinedBoundingBox, _SelectionUIRenderer_drawBoundingBoxWithMargin, _SelectionUIRenderer_drawPointBoundingBox, _SelectionUIRenderer_applyMargin, _SelectionUIRenderer_strokeBoundingBox, _SelectionUIRenderer_strokePath;
|
|
15
|
-
import { applyMarginToBoundingBox } from '../../../shared/math/index.js';
|
|
14
|
+
var _SelectionUIRenderer_instances, _SelectionUIRenderer_strokeRenderer, _SelectionUIRenderer_extensions, _SelectionUIRenderer_terrain, _SelectionUIRenderer_config, _SelectionUIRenderer_transform, _SelectionUIRenderer_projectionData, _SelectionUIRenderer_drawRotatingBoundingBox, _SelectionUIRenderer_drawIndividualBoundingBox, _SelectionUIRenderer_drawCombinedBoundingBox, _SelectionUIRenderer_drawBoundingBoxWithMargin, _SelectionUIRenderer_drawPointBoundingBox, _SelectionUIRenderer_pointFrame, _SelectionUIRenderer_applyMargin, _SelectionUIRenderer_strokeBoundingBox, _SelectionUIRenderer_strokePath;
|
|
15
|
+
import { applyMarginToBoundingBox, } from '../../../shared/math/index.js';
|
|
16
|
+
import { anchorElevationMeters, isAnchorActive, projectAnchorAt } from '../../terrain/anchor.js';
|
|
17
|
+
import { TerrainContext } from '../../terrain/context.js';
|
|
16
18
|
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;
|
|
17
24
|
export class SelectionUIRenderer {
|
|
18
|
-
constructor(strokeRenderer, config, extensions) {
|
|
25
|
+
constructor(strokeRenderer, config, extensions, terrain = new TerrainContext()) {
|
|
19
26
|
_SelectionUIRenderer_instances.add(this);
|
|
20
27
|
_SelectionUIRenderer_strokeRenderer.set(this, void 0);
|
|
21
28
|
/** The Selection UI extension points of the draw instance this renderer belongs to */
|
|
22
29
|
_SelectionUIRenderer_extensions.set(this, void 0);
|
|
30
|
+
/** The terrain state of the draw instance (an inactive one projects with the map) */
|
|
31
|
+
_SelectionUIRenderer_terrain.set(this, void 0);
|
|
23
32
|
_SelectionUIRenderer_config.set(this, void 0);
|
|
24
33
|
_SelectionUIRenderer_transform.set(this, null);
|
|
25
34
|
_SelectionUIRenderer_projectionData.set(this, null);
|
|
26
35
|
__classPrivateFieldSet(this, _SelectionUIRenderer_strokeRenderer, strokeRenderer, "f");
|
|
27
36
|
__classPrivateFieldSet(this, _SelectionUIRenderer_config, config, "f");
|
|
28
37
|
__classPrivateFieldSet(this, _SelectionUIRenderer_extensions, extensions, "f");
|
|
38
|
+
__classPrivateFieldSet(this, _SelectionUIRenderer_terrain, terrain, "f");
|
|
29
39
|
}
|
|
30
40
|
/** Compute the BoundingBox of a feature (also usable from outside) */
|
|
31
41
|
computeFeatureBoundingBox(feature) {
|
|
@@ -80,7 +90,7 @@ export class SelectionUIRenderer {
|
|
|
80
90
|
__classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_drawCombinedBoundingBox).call(this, features, zoom);
|
|
81
91
|
}
|
|
82
92
|
}
|
|
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) {
|
|
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) {
|
|
84
94
|
// When dragState.operation === 'rotate', rotateInfo always exists
|
|
85
95
|
const rotateInfo = dragState.rotateInfo;
|
|
86
96
|
if (!rotateInfo)
|
|
@@ -124,17 +134,16 @@ _SelectionUIRenderer_strokeRenderer = new WeakMap(), _SelectionUIRenderer_extens
|
|
|
124
134
|
if (!bbox)
|
|
125
135
|
continue;
|
|
126
136
|
// A single-coordinate feature (such as Point) is computed from the extent of the
|
|
127
|
-
// point frame + the margin
|
|
128
|
-
// other than Point can be zero-area too, e.g. a
|
|
129
|
-
// coordinates must not be treated as a Coordinate)
|
|
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)
|
|
130
140
|
if (hasZeroArea(bbox)) {
|
|
131
|
-
const
|
|
132
|
-
for (const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
maxLat = Math.max(maxLat, at.lat);
|
|
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);
|
|
138
147
|
}
|
|
139
148
|
continue;
|
|
140
149
|
}
|
|
@@ -168,19 +177,37 @@ _SelectionUIRenderer_strokeRenderer = new WeakMap(), _SelectionUIRenderer_extens
|
|
|
168
177
|
return;
|
|
169
178
|
__classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_strokeBoundingBox).call(this, __classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_applyMargin).call(this, bbox, zoom), zoom);
|
|
170
179
|
}, _SelectionUIRenderer_drawPointBoundingBox = function _SelectionUIRenderer_drawPointBoundingBox(feature, coord, zoom) {
|
|
171
|
-
|
|
180
|
+
const frame = __classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_pointFrame).call(this, feature, coord, zoom);
|
|
181
|
+
if (!frame)
|
|
172
182
|
return;
|
|
183
|
+
__classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_strokePath).call(this, frame.coords, zoom, {
|
|
184
|
+
elevationMeters: frame.elevationMeters,
|
|
185
|
+
followTerrain: false,
|
|
186
|
+
});
|
|
187
|
+
}, _SelectionUIRenderer_pointFrame = function _SelectionUIRenderer_pointFrame(feature, coord, zoom) {
|
|
173
188
|
const transform = __classPrivateFieldGet(this, _SelectionUIRenderer_transform, "f");
|
|
174
|
-
|
|
189
|
+
if (!transform)
|
|
190
|
+
return null;
|
|
191
|
+
const terrain = __classPrivateFieldGet(this, _SelectionUIRenderer_terrain, "f");
|
|
192
|
+
const elevationMeters = anchorElevationMeters(terrain, coord[0], coord[1]);
|
|
193
|
+
const anchored = isAnchorActive(terrain);
|
|
194
|
+
const project = (at) => anchored ? projectAnchorAt(terrain, at[0], at[1], elevationMeters) : transform.project(at);
|
|
195
|
+
const center = project(coord);
|
|
196
|
+
if (!center)
|
|
197
|
+
return null;
|
|
175
198
|
const margin = __classPrivateFieldGet(this, _SelectionUIRenderer_config, "f").boundingBox.margin;
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
199
|
+
const corners = __classPrivateFieldGet(this, _SelectionUIRenderer_extensions, "f").resolvePointFrameCorners(feature, center, margin);
|
|
200
|
+
const toPosition = anchored ? screenToPlane(project, coord, center, zoom) : null;
|
|
201
|
+
const coords = corners.map((corner) => {
|
|
202
|
+
const position = toPosition?.(corner);
|
|
203
|
+
if (position)
|
|
204
|
+
return position;
|
|
205
|
+
// No terrain, or the plane seen edge on: the corner is cast onto the map
|
|
179
206
|
const at = transform.unproject(corner);
|
|
180
207
|
return [at.lng, at.lat];
|
|
181
208
|
});
|
|
182
209
|
coords.push(coords[0]);
|
|
183
|
-
|
|
210
|
+
return { coords, elevationMeters };
|
|
184
211
|
}, _SelectionUIRenderer_applyMargin = function _SelectionUIRenderer_applyMargin(bbox, zoom) {
|
|
185
212
|
if (!__classPrivateFieldGet(this, _SelectionUIRenderer_transform, "f"))
|
|
186
213
|
return bbox;
|
|
@@ -192,13 +219,49 @@ _SelectionUIRenderer_strokeRenderer = new WeakMap(), _SelectionUIRenderer_extens
|
|
|
192
219
|
}, _SelectionUIRenderer_strokeBoundingBox = function _SelectionUIRenderer_strokeBoundingBox(bbox, zoom) {
|
|
193
220
|
const corners = [bbox.topLeft, bbox.topRight, bbox.bottomRight, bbox.bottomLeft];
|
|
194
221
|
__classPrivateFieldGet(this, _SelectionUIRenderer_instances, "m", _SelectionUIRenderer_strokePath).call(this, [...corners, bbox.topLeft], zoom);
|
|
195
|
-
}, _SelectionUIRenderer_strokePath = function _SelectionUIRenderer_strokePath(coords, zoom) {
|
|
222
|
+
}, _SelectionUIRenderer_strokePath = function _SelectionUIRenderer_strokePath(coords, zoom, placement = { followTerrain: true }) {
|
|
196
223
|
__classPrivateFieldGet(this, _SelectionUIRenderer_strokeRenderer, "f").draw(coords, __classPrivateFieldGet(this, _SelectionUIRenderer_config, "f").boundingBox.stroke, {
|
|
197
224
|
widthUnit: 'pixels',
|
|
198
225
|
closed: true,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
226
|
+
followTerrain: placement.followTerrain,
|
|
227
|
+
...(placement.elevationMeters !== undefined && {
|
|
228
|
+
elevationMeters: placement.elevationMeters,
|
|
229
|
+
}),
|
|
203
230
|
}, zoom, __classPrivateFieldGet(this, _SelectionUIRenderer_projectionData, "f") ?? undefined);
|
|
204
231
|
};
|
|
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
|
+
}
|
|
@@ -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.2",
|
|
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",
|