Repository navigation
Fix terrain pan/zoom gestures losing the grabbed terrain point - #8067
Conversation
With terrain enabled, a moving-centroid pinch (and a plain touch drag over terrain whose elevation differs from the center's) does not keep the terrain point grabbed at gesture start under the fingers: the pan is solved against the frozen center-elevation plane and the pinch zoom is applied about the screen center, so terrain under the fingers slips by roughly the centroid travel. The identical gesture with terrain disabled holds the grabbed point exactly (control test). Related: maplibre#2937 (rotation float), maplibre#7989/maplibre#4688 (elevation refreeze snap). Assisted-By: Claude Fable 5 (claude-fable-5) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oint With terrain enabled, pan/zoom gestures were solved against the horizontal plane at the frozen center elevation, and while a drag was in progress the pinch anchor was dropped entirely (zoom about the screen center plus a raw center shift). Terrain under the fingers at a different elevation moves at a different pixels-per-meter rate, so the grabbed terrain point slipped by roughly the centroid travel. Now the elevation of the terrain point under the gesture is captured once at gesture start (from the terrain coords-framebuffer raycast) and the whole gesture - pan and pinch zoom alike - is solved against the horizontal plane at that elevation, mirroring the flat-map gesture path. The anchor is fixed geography, so its elevation stays stable for the whole gesture even when terrain tiles keep loading. If the terrain under the gesture is not loaded at gesture start, the raycast returns null and the previous behavior is kept unchanged. Assisted-By: Claude Fable 5 (claude-fable-5) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Assisted-By: Claude Fable 5 (claude-fable-5) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
94f872d to
7189bfd
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #8067 +/- ##
==========================================
+ Coverage 93.09% 93.12% +0.02%
==========================================
Files 298 298
Lines 24784 24816 +32
Branches 6506 6521 +15
==========================================
+ Hits 23073 23110 +37
+ Misses 1711 1706 -5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Codecov flagged the near-camera fallback guard and the globe/vertical- perspective elevation-parameter paths as uncovered. Assisted-By: Claude Fable 5 (claude-fable-5) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks for the write up! Can you share a few before-after videos maybe? |
|
There's a need to tone down the comments in this PR, it's too verbose. |
|
Agreed — trimmed the source and test comments in 29afe96. |
|
No — deliberately neither, to keep this reviewable. The PR only changes the reference surface used while a gesture is in progress. #7989 lives in the gesture-end settle ( |
Tone down the code comments, describe the elevation parameter in plain terms, drop the test-header comment in favor of the test names, use function declarations for the test helpers, and create the map once per test instead of replacing the shared one. Assisted-By: Claude Fable 5 (claude-fable-5) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
29afe96 to
735ba42
Compare
Sure thing! Here are two videos of continuous zoom-in-and-out gestures in the same location in the alps before and after the changes. trim.D26556A2-3978-410B-9B95-3A005476FC6A.MOVtrim.BF7730E7-7989-4B8A-87D0-F14FF715653C.MOV |
|
Thanks for the video! Added a few more comments. |
…e tests _updateMapTransform shrinks from ~90 to ~50 lines by extracting _resolveAround, _terrainGestureElevation and _computePreZoomAroundLoc; the setLocationAtPoint solve note moves into its tsdoc; the gesture tests consolidate from nine to six cases. Assisted-By: Claude Fable 5 (claude-fable-5) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…allback test The refactor left the terrain gesture describe using 'it', which is no longer imported, so the whole file failed to load. Also split the fallback test.each into two plain tests per review. Assisted-By: Claude Fable 5 (claude-fable-5) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Assisted-By: Claude Fable 5 (claude-fable-5) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The locally built bundle compresses a few hundred bytes smaller than the CI build; the recorded sizes must be the CI ones for the gzip quota to hold on both. Assisted-By: Claude Fable 5 (claude-fable-5) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Sorry for the extra work on bundle size... Hopefully this is the last time. |
Assisted-By: Claude Fable 5 (claude-fable-5) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Head branch was pushed to by a user without write access
##### [v6.4.0](https://github.com/maplibre/maplibre-gl-js/blob/HEAD/CHANGELOG.md#640) ##### ✨ Features and improvements - Avoid a per-query `Array.sort()` in cross-tile symbol matching (`TileLayerIndex.findMatches`), claiming the lowest-index unclaimed candidate in a single pass instead; reduces main-thread symbol-placement cost on dense/coincident symbol layers ([#7797](maplibre/maplibre-gl-js#7797)) (by [@pholmstr](https://github.com/pholmstr)) - Use `texelFetch` for exact DEM and color-relief elevation stop lookups instead of normalized texture coordinate arithmetic ([#7640](maplibre/maplibre-gl-js#7640)) (by [@johncarmack1984](https://github.com/johncarmack1984)) - Make default draggable markers keyboard-focusable and movable with the arrow keys (1 px per press, 10 px with Shift); custom marker elements stay application-owned ([#8020](maplibre/maplibre-gl-js#8020)) (by [@smmariquit](https://github.com/smmariquit)) ##### 🐞 Bug fixes - Fix a permanent frame rate degradation after switching styles: every sprite reload marked its images as updated forever, making every in-view tile re-check and re-upload them on every frame. Also stop leaking the images of a replaced sprite, which were never removed from the image manager ([#8052](maplibre/maplibre-gl-js#8052)) (by [@HarelM](https://github.com/HarelM)) - Prevent a rejected missing style image resolver from blocking successfully resolved images in the same batch ([#8146](maplibre/maplibre-gl-js#8146)) (by [@birkskyum](https://github.com/birkskyum)) - Explicitly request no browser color management when decoding raster-DEM tiles so their RGB-encoded elevation values are not changed (what would otherwise happen with `gfx.color_management.mode = 1` in Firefox) ([#8125](maplibre/maplibre-gl-js#8125)) (by [@tnikkel](https://github.com/tnikkel)) - Let an abort reach an image or raster tile load that is still awaiting its `transformRequest`, so `ImageSource.updateImage` no longer loses the image it was just handed and an aborted tile is no longer fetched ([#8071](maplibre/maplibre-gl-js#8071)) (by [@mondsichtung](https://github.com/mondsichtung)) - Fix raster tiles fading in again when they are reloaded, briefly flashing the map background, most visibly when switching projection ([#8106](maplibre/maplibre-gl-js#8106)) (by [@mondsichtung](https://github.com/mondsichtung)) - Fix globe panning inverting and stalling near and across the poles by rotating the globe with a versor, keeping the drag direction consistent at every latitude. Panning also eases off as the cursor approaches the edge of the globe and continues past it, instead of stopping. The bearing is preserved while panning, as before ([#5296](maplibre/maplibre-gl-js#5296)) (by [@jcolot](https://github.com/jcolot)) - Fix `fill-extrusion-rounded-corner-distance` producing spikes: corner arcs now land on the integer tile grid, and corners created by tile clipping are left sharp ([#8153](maplibre/maplibre-gl-js#8153)) (by [@HarelM](https://github.com/HarelM)) - Fix a gesture which was held still before being released still flinging the map ([#1303](maplibre/maplibre-gl-js#1303)) (by [@zdila](https://github.com/zdila)) ##### [v6.3.0](https://github.com/maplibre/maplibre-gl-js/blob/HEAD/CHANGELOG.md#630) ##### ✨ Features and improvements - Make fired/listened map events typed. This means that `map.on("something", ...)` (and `once`, `listens`) will now give you an typescript error and better autocomplete. If you relied on firing/listening custom events via the map, this still works via the escape hatches `map.fire("something" as any)` -> `map.on("something" as any, ...)` ([#8072](maplibre/maplibre-gl-js#8072)) (by [@CommanderStorm](https://github.com/CommanderStorm)) - Let a `StyleImageInterface` give a `{renderWithWebGL}` callback as its `data`, an escape hatch for plugin developers and advanced users that renders a style image on the GPU instead of moving its pixels through the CPU. Nothing new is possible that pixels could not already express, but an image that changes often, such as an animated icon, gets more performant ([#7954](maplibre/maplibre-gl-js#7954)) (by [@lucaswoj](https://github.com/lucaswoj)) - Use integer vertex attributes for packed line data instead of float conversion ([#7640](maplibre/maplibre-gl-js#7640)) (by [@johncarmack1984](https://github.com/johncarmack1984)) - Use integer vertex attributes for packed circle, heatmap, symbol, and fill-extrusion data instead of float conversion ([#7640](maplibre/maplibre-gl-js#7640), [#8143](maplibre/maplibre-gl-js#8143)) (by [@johncarmack1984](https://github.com/johncarmack1984)) - Redesign benchmarks to use vitest bench capabilities and remove custom build for benchmarks code ([#982](maplibre/maplibre-gl-js#982)) (by [@johncarmack1984](https://github.com/johncarmack1984)) ##### 🐞 Bug fixes - Fix terrain pan/zoom gestures losing the grabbed terrain point: gestures are now solved against the elevation of the terrain under the gesture instead of the frozen center elevation, so terrain under the pointer/fingers no longer slips during moving-centroid pinches and drags ([#8067](maplibre/maplibre-gl-js#8067)) (by [@StrawberryJam22](https://github.com/StrawberryJam22)) - Fix `ImageSource`, `VideoSource` and `CanvasSource` leaking a GPU texture on every image update and on removal, and a resized texture losing its wrap and filter settings ([#8094](maplibre/maplibre-gl-js#8094)) (by [@mondsichtung](https://github.com/mondsichtung)) - Fix `map.queryRenderedFeatures()` sometimes causing "Out of bounds" error due to race condition while loading tile data ([#8064](maplibre/maplibre-gl-js#8064)) (by [@smvjohansenbouvet](https://github.com/smvjohansenbouvet)) - Fix zooming the globe with the scroll wheel or a two-finger pinch drifting away from the pointer while the globe is small on screen, instead of keeping the location under the pointer as it does when zoomed in ([#8095](maplibre/maplibre-gl-js#8095)) (by [@mondsichtung](https://github.com/mondsichtung)) - Fix projective rendering for non-parallelogram image source quads ([#7887](maplibre/maplibre-gl-js#7887)) (by [@i4innovationnet](https://github.com/i4innovationnet))
|
@StrawberryJam22 is this still solved in latest version? |
The in-gesture anchoring from this PR is intact on latest, its gesture tests enforce the "grabbed point stays under the pointer" invariant and pass on current main. The jump at release is a separate mechanism this PR deliberately didn't touch: while the pointer is down the center elevation is frozen; on release the freeze lifts and recalculateZoomAndCenter re-derives zoom/center/elevation. In a clean synthetic repro that settle is visually invariant (0.0 px even after dragging the center onto ground 1500 m higher, with a smooth inertia decay after it). Where it visibly jumps is when the terrain data itself changed during the drag, DEM tiles that loaded mid-gesture get applied all at once at the freeze lift. That's the #7989 family. One thing worth checking: whether the jump is new in 6.5/6.6, #8158 moved the picking to a CPU raycast, and the release settle goes through that path. If 6.3/6.4 doesn't jump in the same spot, that points there rather than at the gesture code. |
|
I still see jumps in terrain in 6.3/6.4 when releasing the mouse, I thought this PR fixed it. Oh well, there's another PR to address this opened now, just wanted to make sure this didn't broke with the changes to ray case etc. |
##### [v6.4.1](https://github.com/maplibre/maplibre-gl-js/blob/HEAD/CHANGELOG.md#641) ##### 🐞 Bug fixes - Fix `DOM.sanitize` leaving dangerous attributes behind when multiple consecutive attributes are present. Iterating the live `NamedNodeMap` from `elem.attributes` while calling `removeAttribute` skipped the attribute directly after a removed one, so a second dangerous attribute (for example an `ontoggle` on a `<details open>` element) could survive sanitisation and later execute ([#8189](maplibre/maplibre-gl-js#8189)) (by [@0xKirisame](https://github.com/0xKirisame)) - Give custom layers the live globe transition in `CustomRenderMethodInput.defaultProjectionData.projectionTransition`, which was hardcoded to 1 for the whole globe/mercator transition, so a custom layer jumped straight to the fully bent globe while every other layer eased ([#8169](maplibre/maplibre-gl-js#8169)) (by [@mondsichtung](https://github.com/mondsichtung)) ##### [v6.4.0](https://github.com/maplibre/maplibre-gl-js/blob/HEAD/CHANGELOG.md#640) ##### ✨ Features and improvements - Avoid a per-query `Array.sort()` in cross-tile symbol matching (`TileLayerIndex.findMatches`), claiming the lowest-index unclaimed candidate in a single pass instead; reduces main-thread symbol-placement cost on dense/coincident symbol layers ([#7797](maplibre/maplibre-gl-js#7797)) (by [@pholmstr](https://github.com/pholmstr)) - Use `texelFetch` for exact DEM and color-relief elevation stop lookups instead of normalized texture coordinate arithmetic ([#7640](maplibre/maplibre-gl-js#7640)) (by [@johncarmack1984](https://github.com/johncarmack1984)) - Make default draggable markers keyboard-focusable and movable with the arrow keys (1 px per press, 10 px with Shift); custom marker elements stay application-owned ([#8020](maplibre/maplibre-gl-js#8020)) (by [@smmariquit](https://github.com/smmariquit)) ##### 🐞 Bug fixes - Fix a permanent frame rate degradation after switching styles: every sprite reload marked its images as updated forever, making every in-view tile re-check and re-upload them on every frame. Also stop leaking the images of a replaced sprite, which were never removed from the image manager ([#8052](maplibre/maplibre-gl-js#8052)) (by [@HarelM](https://github.com/HarelM)) - Prevent a rejected missing style image resolver from blocking successfully resolved images in the same batch ([#8146](maplibre/maplibre-gl-js#8146)) (by [@birkskyum](https://github.com/birkskyum)) - Explicitly request no browser color management when decoding raster-DEM tiles so their RGB-encoded elevation values are not changed (what would otherwise happen with `gfx.color_management.mode = 1` in Firefox) ([#8125](maplibre/maplibre-gl-js#8125)) (by [@tnikkel](https://github.com/tnikkel)) - Let an abort reach an image or raster tile load that is still awaiting its `transformRequest`, so `ImageSource.updateImage` no longer loses the image it was just handed and an aborted tile is no longer fetched ([#8071](maplibre/maplibre-gl-js#8071)) (by [@mondsichtung](https://github.com/mondsichtung)) - Fix raster tiles fading in again when they are reloaded, briefly flashing the map background, most visibly when switching projection ([#8106](maplibre/maplibre-gl-js#8106)) (by [@mondsichtung](https://github.com/mondsichtung)) - Fix globe panning inverting and stalling near and across the poles by rotating the globe with a versor, keeping the drag direction consistent at every latitude. Panning also eases off as the cursor approaches the edge of the globe and continues past it, instead of stopping. The bearing is preserved while panning, as before ([#5296](maplibre/maplibre-gl-js#5296)) (by [@jcolot](https://github.com/jcolot)) - Fix `fill-extrusion-rounded-corner-distance` producing spikes: corner arcs now land on the integer tile grid, and corners created by tile clipping are left sharp ([#8153](maplibre/maplibre-gl-js#8153)) (by [@HarelM](https://github.com/HarelM)) - Fix a gesture which was held still before being released still flinging the map ([#1303](maplibre/maplibre-gl-js#1303)) (by [@zdila](https://github.com/zdila)) ##### [v6.3.0](https://github.com/maplibre/maplibre-gl-js/blob/HEAD/CHANGELOG.md#630) ##### ✨ Features and improvements - Make fired/listened map events typed. This means that `map.on("something", ...)` (and `once`, `listens`) will now give you an typescript error and better autocomplete. If you relied on firing/listening custom events via the map, this still works via the escape hatches `map.fire("something" as any)` -> `map.on("something" as any, ...)` ([#8072](maplibre/maplibre-gl-js#8072)) (by [@CommanderStorm](https://github.com/CommanderStorm)) - Let a `StyleImageInterface` give a `{renderWithWebGL}` callback as its `data`, an escape hatch for plugin developers and advanced users that renders a style image on the GPU instead of moving its pixels through the CPU. Nothing new is possible that pixels could not already express, but an image that changes often, such as an animated icon, gets more performant ([#7954](maplibre/maplibre-gl-js#7954)) (by [@lucaswoj](https://github.com/lucaswoj)) - Use integer vertex attributes for packed line data instead of float conversion ([#7640](maplibre/maplibre-gl-js#7640)) (by [@johncarmack1984](https://github.com/johncarmack1984)) - Use integer vertex attributes for packed circle, heatmap, symbol, and fill-extrusion data instead of float conversion ([#7640](maplibre/maplibre-gl-js#7640), [#8143](maplibre/maplibre-gl-js#8143)) (by [@johncarmack1984](https://github.com/johncarmack1984)) - Redesign benchmarks to use vitest bench capabilities and remove custom build for benchmarks code ([#982](maplibre/maplibre-gl-js#982)) (by [@johncarmack1984](https://github.com/johncarmack1984)) ##### 🐞 Bug fixes - Fix terrain pan/zoom gestures losing the grabbed terrain point: gestures are now solved against the elevation of the terrain under the gesture instead of the frozen center elevation, so terrain under the pointer/fingers no longer slips during moving-centroid pinches and drags ([#8067](maplibre/maplibre-gl-js#8067)) (by [@StrawberryJam22](https://github.com/StrawberryJam22)) - Fix `ImageSource`, `VideoSource` and `CanvasSource` leaking a GPU texture on every image update and on removal, and a resized texture losing its wrap and filter settings ([#8094](maplibre/maplibre-gl-js#8094)) (by [@mondsichtung](https://github.com/mondsichtung)) - Fix `map.queryRenderedFeatures()` sometimes causing "Out of bounds" error due to race condition while loading tile data ([#8064](maplibre/maplibre-gl-js#8064)) (by [@smvjohansenbouvet](https://github.com/smvjohansenbouvet)) - Fix zooming the globe with the scroll wheel or a two-finger pinch drifting away from the pointer while the globe is small on screen, instead of keeping the location under the pointer as it does when zoomed in ([#8095](maplibre/maplibre-gl-js#8095)) (by [@mondsichtung](https://github.com/mondsichtung)) - Fix projective rendering for non-parallelogram image source quads ([#7887](maplibre/maplibre-gl-js#7887)) (by [@i4innovationnet](https://github.com/i4innovationnet))
##### [v6.4.1](https://github.com/maplibre/maplibre-gl-js/blob/HEAD/CHANGELOG.md#641) ##### 🐞 Bug fixes - Fix `DOM.sanitize` leaving dangerous attributes behind when multiple consecutive attributes are present. Iterating the live `NamedNodeMap` from `elem.attributes` while calling `removeAttribute` skipped the attribute directly after a removed one, so a second dangerous attribute (for example an `ontoggle` on a `<details open>` element) could survive sanitisation and later execute ([#8189](maplibre/maplibre-gl-js#8189)) (by [@0xKirisame](https://github.com/0xKirisame)) - Give custom layers the live globe transition in `CustomRenderMethodInput.defaultProjectionData.projectionTransition`, which was hardcoded to 1 for the whole globe/mercator transition, so a custom layer jumped straight to the fully bent globe while every other layer eased ([#8169](maplibre/maplibre-gl-js#8169)) (by [@mondsichtung](https://github.com/mondsichtung)) ##### [v6.4.0](https://github.com/maplibre/maplibre-gl-js/blob/HEAD/CHANGELOG.md#640) ##### ✨ Features and improvements - Avoid a per-query `Array.sort()` in cross-tile symbol matching (`TileLayerIndex.findMatches`), claiming the lowest-index unclaimed candidate in a single pass instead; reduces main-thread symbol-placement cost on dense/coincident symbol layers ([#7797](maplibre/maplibre-gl-js#7797)) (by [@pholmstr](https://github.com/pholmstr)) - Use `texelFetch` for exact DEM and color-relief elevation stop lookups instead of normalized texture coordinate arithmetic ([#7640](maplibre/maplibre-gl-js#7640)) (by [@johncarmack1984](https://github.com/johncarmack1984)) - Make default draggable markers keyboard-focusable and movable with the arrow keys (1 px per press, 10 px with Shift); custom marker elements stay application-owned ([#8020](maplibre/maplibre-gl-js#8020)) (by [@smmariquit](https://github.com/smmariquit)) ##### 🐞 Bug fixes - Fix a permanent frame rate degradation after switching styles: every sprite reload marked its images as updated forever, making every in-view tile re-check and re-upload them on every frame. Also stop leaking the images of a replaced sprite, which were never removed from the image manager ([#8052](maplibre/maplibre-gl-js#8052)) (by [@HarelM](https://github.com/HarelM)) - Prevent a rejected missing style image resolver from blocking successfully resolved images in the same batch ([#8146](maplibre/maplibre-gl-js#8146)) (by [@birkskyum](https://github.com/birkskyum)) - Explicitly request no browser color management when decoding raster-DEM tiles so their RGB-encoded elevation values are not changed (what would otherwise happen with `gfx.color_management.mode = 1` in Firefox) ([#8125](maplibre/maplibre-gl-js#8125)) (by [@tnikkel](https://github.com/tnikkel)) - Let an abort reach an image or raster tile load that is still awaiting its `transformRequest`, so `ImageSource.updateImage` no longer loses the image it was just handed and an aborted tile is no longer fetched ([#8071](maplibre/maplibre-gl-js#8071)) (by [@mondsichtung](https://github.com/mondsichtung)) - Fix raster tiles fading in again when they are reloaded, briefly flashing the map background, most visibly when switching projection ([#8106](maplibre/maplibre-gl-js#8106)) (by [@mondsichtung](https://github.com/mondsichtung)) - Fix globe panning inverting and stalling near and across the poles by rotating the globe with a versor, keeping the drag direction consistent at every latitude. Panning also eases off as the cursor approaches the edge of the globe and continues past it, instead of stopping. The bearing is preserved while panning, as before ([#5296](maplibre/maplibre-gl-js#5296)) (by [@jcolot](https://github.com/jcolot)) - Fix `fill-extrusion-rounded-corner-distance` producing spikes: corner arcs now land on the integer tile grid, and corners created by tile clipping are left sharp ([#8153](maplibre/maplibre-gl-js#8153)) (by [@HarelM](https://github.com/HarelM)) - Fix a gesture which was held still before being released still flinging the map ([#1303](maplibre/maplibre-gl-js#1303)) (by [@zdila](https://github.com/zdila)) ##### [v6.3.0](https://github.com/maplibre/maplibre-gl-js/blob/HEAD/CHANGELOG.md#630) ##### ✨ Features and improvements - Make fired/listened map events typed. This means that `map.on("something", ...)` (and `once`, `listens`) will now give you an typescript error and better autocomplete. If you relied on firing/listening custom events via the map, this still works via the escape hatches `map.fire("something" as any)` -> `map.on("something" as any, ...)` ([#8072](maplibre/maplibre-gl-js#8072)) (by [@CommanderStorm](https://github.com/CommanderStorm)) - Let a `StyleImageInterface` give a `{renderWithWebGL}` callback as its `data`, an escape hatch for plugin developers and advanced users that renders a style image on the GPU instead of moving its pixels through the CPU. Nothing new is possible that pixels could not already express, but an image that changes often, such as an animated icon, gets more performant ([#7954](maplibre/maplibre-gl-js#7954)) (by [@lucaswoj](https://github.com/lucaswoj)) - Use integer vertex attributes for packed line data instead of float conversion ([#7640](maplibre/maplibre-gl-js#7640)) (by [@johncarmack1984](https://github.com/johncarmack1984)) - Use integer vertex attributes for packed circle, heatmap, symbol, and fill-extrusion data instead of float conversion ([#7640](maplibre/maplibre-gl-js#7640), [#8143](maplibre/maplibre-gl-js#8143)) (by [@johncarmack1984](https://github.com/johncarmack1984)) - Redesign benchmarks to use vitest bench capabilities and remove custom build for benchmarks code ([#982](maplibre/maplibre-gl-js#982)) (by [@johncarmack1984](https://github.com/johncarmack1984)) ##### 🐞 Bug fixes - Fix terrain pan/zoom gestures losing the grabbed terrain point: gestures are now solved against the elevation of the terrain under the gesture instead of the frozen center elevation, so terrain under the pointer/fingers no longer slips during moving-centroid pinches and drags ([#8067](maplibre/maplibre-gl-js#8067)) (by [@StrawberryJam22](https://github.com/StrawberryJam22)) - Fix `ImageSource`, `VideoSource` and `CanvasSource` leaking a GPU texture on every image update and on removal, and a resized texture losing its wrap and filter settings ([#8094](maplibre/maplibre-gl-js#8094)) (by [@mondsichtung](https://github.com/mondsichtung)) - Fix `map.queryRenderedFeatures()` sometimes causing "Out of bounds" error due to race condition while loading tile data ([#8064](maplibre/maplibre-gl-js#8064)) (by [@smvjohansenbouvet](https://github.com/smvjohansenbouvet)) - Fix zooming the globe with the scroll wheel or a two-finger pinch drifting away from the pointer while the globe is small on screen, instead of keeping the location under the pointer as it does when zoomed in ([#8095](maplibre/maplibre-gl-js#8095)) (by [@mondsichtung](https://github.com/mondsichtung)) - Fix projective rendering for non-parallelogram image source quads ([#7887](maplibre/maplibre-gl-js#7887)) (by [@i4innovationnet](https://github.com/i4innovationnet)) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
The hold flag, the maplibre#8067 anchor and its sampled flag live in one `_terrainGesture` object with one tsdoc block, reset when the movement ends. The globe check in `_handleMapControls` only guarded the mercator pixel-delta fallback, so it is folded into that condition. The hold is tested through the real handler stack and the public API, on mercator and on globe, instead of a mocked `_handleMapControls` scenario.
* fix: hold the center elevation for every gesture over terrain, not only drags and zooms A two-finger pitch or rotation never set the terrain movement flag, so the center elevation kept following the DEM while the fingers were down. When a DEM tile landed under the center mid-gesture, as it does the moment an app turns terrain on from a pitch handler, the camera lifted by the exaggerated elevation under the user's fingers. Every gesture over terrain now holds the elevation until it ends and the existing end-of-gesture re-solve puts the camera back onto the terrain without moving it. The gesture anchor is still sampled on the first drag or zoom frame. * docs: link the changelog entry to the PR * test: assert the pitch-only elevation hold through the camera target elevation * refactor: keep the terrain gesture state in one object The hold flag, the #8067 anchor and its sampled flag live in one `_terrainGesture` object with one tsdoc block, reset when the movement ends. The globe check in `_handleMapControls` only guarded the mercator pixel-delta fallback, so it is folded into that condition. The hold is tested through the real handler stack and the public API, on mercator and on globe, instead of a mocked `_handleMapControls` scenario. * fix: measure the mouse rotate and roll pointer against the screen center `map.project(map.getCenter())` projects the center through the terrain, so while a gesture holds the transform at the old elevation and a landed DEM tile says otherwise, that point drifts off the screen center and the rotate handler flips into its angle mode mid-drag. The transform's own centerPoint is the same point in every steady state. * refactor: carry the terrain gesture anchor state in one field anchorElevation is null until the gesture's first drag or zoom frame and undefined when the terrain under the pointer was not loaded on that frame, which keeps the gesture on the center's elevation to its end. The idle literal is inlined, and a test pins the drag speed when that terrain loads mid-drag. * test: move the terrain gesture hold tests to the map terrain tests A right-drag over a stub terrain replaces the two-finger pitch over a loading DEM source, so the tests need no setup helpers. * docs: list the three states of the terrain gesture anchor elevation * fix: keep the camera in place when a DEM tile lands after a gesture over terrain * fix: keep the camera in place when terrain is removed from a view a gesture left * fix: keep the camera in place for terrain set or landing after any gesture The camera now owns the response to a terrain change: `Camera.terrain` is a setter and `_applyTerrainChange` decides whether the camera or the center stays in place, so `Map.setTerrain` and the terrain data handler only hand the change over. The flag is set when a gesture starts instead of when a gesture over terrain ends, and `setTerrain` honors it instead of clearing it. * refactor: drop the terrain clause from the handler's no-change early return A frame with no change does nothing past that return: zero deltas are no-ops in the camera helpers, and the end of the gesture runs in `_fireEvents` on both paths. * test: say what the drag tests measure, drop the globe hold test "Speed" becomes the quantity each test compares: bearing per pixel for the rotate drag, distance per pixel for the pan. The globe hold test ran the same hold as the mercator one, so it goes. The at-rest cases get their own describe block. * fix: release the elevation freeze at the end of easeTo and flyTo Since #3944 an ease over terrain without freezeElevation set the freeze in _prepareElevation and never released it, so DEM tiles landing afterwards were ignored until the next gesture over terrain ended, which then applied the whole delta at once (#8514). The finish of an unfrozen ease now releases the freeze, and an unfrozen ease places the center the way jumpTo does: it clears _terrainChangeKeepsCamera, so a tile landing after it keeps the center and moves the camera. The handler's inertia ease still runs frozen and keeps the flag. * refactor: hand the camera its terrain through setTerrain, and name what the map and handlers call on it without underscores * refactor: apply a terrain change through the requested camera state * test: consolidate the terrain gesture tests * docs: one changelog line for the terrain camera fixes * fix: keep the center on every terrain change, and apply a DEM tile that lands during a gesture at once * fix: fire gesture events through the camera so it drops its requested state at moveend * test: build the terrain gesture tests over a real terrain instead of cast stubs * fix: leave the camera alone while a gesture or ease holds the elevation * fix: leave no requested camera state behind after a handler frame without a change
With terrain enabled on the mercator projection, pan and pinch-zoom gestures lose the terrain point grabbed at gesture start. This PR fixes the pan/zoom case; the rotation drift family of #2937 is a separate follow-up.
The bug
In-gesture solves intersect the horizontal plane at the frozen center elevation (
elevationFreeze), and on frames where a drag is in progress the pinch anchor is dropped entirely:handleMapControlsRollPitchBearingZoomhas already applied the zoom about the screen center, and the drag branch inHandlerManager._handleMapControlsapplies the pan as a raw center pixel-shift. Terrain under the fingers at a different elevation moves at a different pixels-per-meter rate, so the grabbed terrain point slips out from under the fingers by roughly the centroid travel — while the identical gesture with terrain disabled is exact.We hit this in an iOS app embedding GL JS in a WKWebView over high-relief terrain, and measured it with synthetic two-finger
TouchEventsequences driven through the real handler stack over Chamonix (terrarium raster-dem, zoom ~12.6): a moving-centroid pinch-in with ~103 px of centroid travel slips the grabbed mountain 125.8–136.8 px per gesture (release-time drift against the live finger midpoint), pinch-out 44–46 px, pitch-independent (as bad at nadir as at pitch 66), and ~0 px with terrain off in the identical scenario. A fixed-centroid pinch stays clean (4.4–7.2 px), which is why scripted symmetric pinches miss it. Re-pinning the grabbed point from outside viasetLocationAtPointonce per frame does not work — the handler re-derives its own center-plane solution every frame while fingers are down, so the correction only lands at release; the fix has to live in the gesture path.The fix
Capture the elevation of the terrain point under the gesture once at gesture start (from the terrain coords-framebuffer raycast,
terrain.pointCoordinate), and solve the whole gesture — pan and pinch zoom alike — against the horizontal plane at that elevation, using the same anchored code path the flat map uses (handleMapControlsPan→setLocationAtPoint, with the anchor plane threaded throughMapControlsDeltas.aroundElevationintosetLocationAtPoint's newelevationparameter). The anchor is fixed geography, so its elevation is stable for the whole gesture even while terrain tiles keep loading mid-gesture — unlike the center's elevation, which changes as the map moves under a frozen screen point.MercatorTransform.setLocationAtPoint(lnglat, point, elevation?)solves the pick ray on the plane atelevation(default: previous behavior). The center term stays a ray ∩ z=0 solve through the same inverse pixel-matrix, which keeps the matrix-inversion error cancelling out of thea − bdifference; it must not be intersected with the elevated plane, since that point sits(elevation − centerElevation)·tan(pitch)away from the center and would make the solve run away at pitch. (The oldz = mercatorZfromAltitude(...)value was mercator-units in a meters-typed slot, i.e. numerically ~0; the new code states the units it actually uses.)HandlerManagercaptures_terrainGestureAnchorElevationat gesture start and clears it when movement finishes (same lifetime as the existing elevation freeze);preZoomAroundLocis computed on the anchor plane via the newITransform.screenPointToLocationAtElevation.aroundis, or collapses to, the center point (center-anchored pinches, fingers past the horizon guard) → previous path for that frame; anchor plane within 10% of the camera altitude (extreme zoom-in toward a peak) → previous path for that frame.useGlobeControlsbranch; the vertical-perspective transform ignores the elevation parameter — there is no flat ground plane to offset); a globe style rendering mercator inherits the fix through the delegating transform.Measurements (before/after)
Same harness as above, v6.1.0 stock vs this branch — a 24-cell matrix ({terrain on/off} × {pitch 66/0} × {around: touch/center} × {fixed-centroid in / moving in / moving out}) plus 7 shim-instrumented diagnostic cells. Drift in px of the grabbed terrain point against the live finger midpoint at release:
The residual 7.5–7.8 px is comparable to the fixed-centroid control (4.4–7.2 px), i.e. at the harness noise level. Release drift is bit-identical in every terrain-off cell; center-anchored cells change by at most 0.04 px (the moving pinch-out cells — pan-component frames where
aroundis the centroid now use the anchored solve). Wheel zoom over terrain also anchors to the terrain elevation under the cursor now.Reproducing manually
Run a terrain example (e.g. the 3D terrain example) over high relief, pitch and zoom to a mountainside, then do a two-finger pinch whose midpoint travels across the slope (or the same with a drag). On
mainthe terrain feature you grabbed slides away from your fingers by roughly the distance your fingers travel; on this branch it stays under them. Terrain off is unaffected.Tests
The first commit adds three gesture tests (full synthetic-touch sequences through the real handler stack, terrain stubbed with
createTerrain()): the two terrain cases fail onmain— the grabbed point slips 7.7 px and 20.1 px for these small synthetic gestures, the unit-scale analogue of the slips above — while the flat control passes. The fix commit makes them pass, tightens the tolerance to 0.5 px, and adds five more cases: a pitched (60°) pinch (pins thetan(pitch)failure mode of a naive plane solve), a drag over a 2000 m ridge with a non-zero frozen center elevation (pins the relative-plane math), capture-once-per-gesture while terrain "loads" mid-gesture, wheel zoom capturing the terrain elevation under the cursor as the gesture anchor, the center-anchored-pinch pan path, and the raycast-null fallback. A follow-up commit adds three small tests covering the near-camera fallback guard and the globe/vertical-perspective delegation paths (flagged by Codecov). Existing suite: all green.Known scope edges (deliberate)
handler_inertia→easeTo); measured effect in our harness is a few px between release and settle. Candidate follow-up, kept out to keep this reviewable.Related issues: #2937 (rotation over terrain), #7989 (DEM loads mid-drag), #4688 (terrain camera-math nondeterminism), #1024 (closed; the wheel-zoom-over-terrain case) — same terrain-gesture family; none of them describes this moving-centroid slip, so no existing issue is closed by this PR.
Launch Checklist
screenPointToLocationAtElevationis@internal-documented; the new optional parameter is documented on the existingsetLocationAtPointinterface doc.recalculateZoomAndCenteralready does at gesture end).CHANGELOG.mdunder the## mainsection.Assisted-By: Claude Fable 5 (claude-fable-5)