Skip to content

Fix terrain pan/zoom gestures losing the grabbed terrain point - #8067

Merged
HarelM merged 14 commits into
maplibre:mainfrom
StrawberryJam22:fix/terrain-gesture-anchor-parallax
Aug 8, 2026
Merged

HarelM merged 14 commits into
maplibre:mainfrom
StrawberryJam22:fix/terrain-gesture-anchor-parallax

Conversation

@StrawberryJam22

@StrawberryJam22 StrawberryJam22 commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

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: handleMapControlsRollPitchBearingZoom has already applied the zoom about the screen center, and the drag branch in HandlerManager._handleMapControls applies 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 TouchEvent sequences 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 via setLocationAtPoint once 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 through MapControlsDeltas.aroundElevation into setLocationAtPoint's new elevation parameter). 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 at elevation (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 the a − b difference; 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 old z = mercatorZfromAltitude(...) value was mercator-units in a meters-typed slot, i.e. numerically ~0; the new code states the units it actually uses.)
  • HandlerManager captures _terrainGestureAnchorElevation at gesture start and clears it when movement finishes (same lifetime as the existing elevation freeze); preZoomAroundLoc is computed on the anchor plane via the new ITransform.screenPointToLocationAtElevation.
  • When the anchor is unavailable, the code falls back to the previous behavior: raycast returns null (terrain not loaded / sky) → previous path for the whole gesture; around is, 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.
  • Globe controls are untouched (useGlobeControls branch; 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:

cell (default touch mode, terrain on) stock fixed
moving pinch-in, pitch 66 125.8 7.5
moving pinch-in, pitch 0 136.8 7.8
moving pinch-out, pitch 66 44.0 0.5
moving pinch-out, pitch 0 46.3 0.7
fixed-centroid pinch (control) 4.4 / 7.2 4.4 / 7.2 (unchanged)

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 around is 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 main the 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 on main — 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 the tan(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)

  • The inertia fling after release still re-anchors on the center plane (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.
  • The rotation drift of rotateCamera does not work properly with terrain exaggeration #2937 (rotation over terrain, which shares this gesture math) is a separate follow-up, and should become easier after this lands.
  • A gesture chain that never fully ends (e.g. wheel → drag inside the scroll-zoom timeout) keeps the first sub-gesture's anchor plane — the same lifetime the elevation freeze already has; re-capturing mid-movement would reintroduce plane jumps.

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

  • Confirm your changes do not include backports from Mapbox projects (unless with compliant license) — original work.
  • Briefly describe the changes in this PR.
  • Link to related issues.
  • Include before/after visuals or gifs if this PR includes visual changes — before/after drift table and manual repro above.
  • Write tests for all new functionality.
  • Document any changes to public APIs — the new screenPointToLocationAtElevation is @internal-documented; the new optional parameter is documented on the existing setLocationAtPoint interface doc.
  • Post benchmark scores — no performance claim: the gesture path does one extra plane intersection per frame and one raycast per gesture start (the same raycast recalculateZoomAndCenter already does at gesture end).
  • Add an entry to CHANGELOG.md under the ## main section.
  • Confirm you have read our AI policy here.

Assisted-By: Claude Fable 5 (claude-fable-5)

StrawberryJam22 and others added 2 commits July 31, 2026 18:01
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>
@StrawberryJam22
StrawberryJam22 marked this pull request as ready for review July 31, 2026 15:18
Assisted-By: Claude Fable 5 (claude-fable-5)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@StrawberryJam22
StrawberryJam22 force-pushed the fix/terrain-gesture-anchor-parallax branch from 94f872d to 7189bfd Compare July 31, 2026 16:14
@codecov

codecov Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.12%. Comparing base (1a66c2d) to head (a2d5216).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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>
@HarelM

HarelM commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the write up! Can you share a few before-after videos maybe?

Comment thread src/geo/projection/camera_helper.ts
Comment thread src/geo/transform_interface.ts Outdated
Comment thread src/ui/handler_manager.test.ts Outdated
Comment thread src/ui/handler_manager.test.ts Outdated
Comment thread src/ui/handler_manager.test.ts Outdated
@HarelM

HarelM commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

There's a need to tone down the comments in this PR, it's too verbose.
I'll do a second sweep after that. Thanks!!

@HarelM

HarelM commented Jul 31, 2026 •

Copy link
Copy Markdown
Collaborator

@StrawberryJam22

Copy link
Copy Markdown
Contributor Author

Agreed — trimmed the source and test comments in 29afe96.

@StrawberryJam22

Copy link
Copy Markdown
Contributor Author

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 (recalculateZoomAndCenter after the elevation freeze lifts), which is untouched — one adjacent improvement is that DEM tiles loading mid-gesture no longer influence the in-gesture solve, since the anchor's elevation is captured once at gesture start, but the dragend→moveend jump itself remains. #4688 is in the jumpTo/easeTo path, also untouched — easeTo still re-anchors on the center-elevation plane (the 'inertia' item under known scope edges). Both should get easier on top of this: the plane-at-elevation plumbing (setLocationAtPoint with elevation, screenPointToLocationAtElevation) is what a settle/ease fix would reuse.

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>
@StrawberryJam22
StrawberryJam22 force-pushed the fix/terrain-gesture-anchor-parallax branch from 29afe96 to 735ba42 Compare August 1, 2026 09:29
@StrawberryJam22

Copy link
Copy Markdown
Contributor Author

Thanks for the write up! Can you share a few before-after videos maybe?

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.MOV
trim.BF7730E7-7989-4B8A-87D0-F14FF715653C.MOV

Comment thread src/geo/projection/mercator_transform.ts Outdated
Comment thread src/ui/handler_manager.ts Outdated
@HarelM

HarelM commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the video! Added a few more comments.

Comment thread src/ui/handler_manager.test.ts
…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>

@HarelM HarelM left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, THANKS!

@HarelM
HarelM enabled auto-merge (squash) August 7, 2026 11:41
Comment thread src/ui/handler_manager.test.ts Outdated
@HarelM
HarelM disabled auto-merge August 7, 2026 11:53
HarelM and others added 4 commits August 7, 2026 14:53
…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>
@HarelM
HarelM enabled auto-merge (squash) August 7, 2026 14:35
@HarelM

HarelM commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

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>
auto-merge was automatically disabled August 8, 2026 09:10

Head branch was pushed to by a user without write access

@HarelM
HarelM merged commit 871bb78 into maplibre:main Aug 8, 2026
24 checks passed
renovate Bot added a commit to cigaleapp/cigale that referenced this pull request Aug 24, 2026
##### [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))
@HarelM

HarelM commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

@StrawberryJam22 is this still solved in latest version?
When I pan a pitched map that has terrain on, the map jumps a bit when I release the grab. Was this solved as part of this PR?

@StrawberryJam22

Copy link
Copy Markdown
Contributor Author

@StrawberryJam22 is this still solved in latest version?

When I pan a pitched map that has terrain on, the map jumps a bit when I release the grab. Was this solved as part of this PR?

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.

@HarelM

HarelM commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

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.
Thanks for looking into this.
Feel free to review it.

renovate Bot added a commit to cigaleapp/cigale that referenced this pull request Aug 25, 2026
##### [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))
gwennlbh pushed a commit to cigaleapp/cigale that referenced this pull request Aug 25, 2026
##### [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>
johncarmack1984 added a commit to johncarmack1984/maplibre-gl-js that referenced this pull request Sep 16, 2026
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.
HarelM pushed a commit that referenced this pull request Sep 23, 2026
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants