The MapLibre map. Terrain may be enabled before or after this call.
A one-shot disposer; the original prototype method is restored when the last outstanding hold on this map releases.
import * as maplibregl from 'maplibre-gl';
import { prepareMapLibre, createEsriTerrain, preventTerrainGestureSnap } from '@bitruvius/sdk-maplibre';
await prepareMapLibre(maplibregl); // v6: wires the worker module · v5: no-op
const map = new maplibregl.Map({ container: 'map', style, pitch: 60 });
await createEsriTerrain(map, { maplibregl });
preventTerrainGestureSnap(map); // camera follows terrain, no gesture-end jump
Suppress MapLibre's terrain gesture-end camera snap-back while keeping the camera following the ground every frame — the fix for the camera "jumping backward" when you pan or scroll-zoom over terrain at a tilt.
Why the jump happens (MapLibre GL 5.x)
During a terrain gesture the camera elevation is FROZEN (so the camera holds a constant height while you drag). On gesture end MapLibre reconciles that frozen elevation against a FRESH DEM sample by calling
transform.recalculateZoomAndCenter, which holds the camera's 3D position fixed and re-derives center + zoom. At a steep pitch a tiny frozen-vs-fresh elevation deltaΔeslides the center along the view ray byΔe · tan(pitch)— e.g. ~11× at 85° — so even the couple-metre delta over flat terrain reads as a visible center-shift + zoom "jump back". ProgrammaticeaseTo/flyToare unaffected: withfreezeElevationoff they update elevation every frame and never finalize.Why shadowing the METHOD (not the public setter)
Both snap sites — the synchronous gesture end (
HandlerManager) and the inertial finalize (Camera._finalizeElevation) — are gated on the methodgetCenterClampedToGround(). The publicsetCenterClampedToGround(false)would flip the underlying field too, which stops the per-frame terrain-follow — the camera then floats and off-centre geometry parallax-drifts on pan (the regression we must avoid). So the fix shadows the method and leaves the field alone.The patch goes on the clamp-GATE objects (
resolveClampGates), never on the transform — the transform object is swapped out on a mercator↔globe projection change, which would silently drop a transform-level patch. On maplibre v5 the single gate is the map itself (Map extends Camera: both snap sites read the map's method). On v6 (Mapcomposes aCameraatmap._camera) the two snap sites read DIFFERENT objects —HandlerManagercalls the Map's thin delegate whileCamera._finalizeElevationcalls the Camera's own method — so BOTH get the shadow; patching only the map would let the inertial snap come back.Permanent shadow on v5, gesture-window shadow on v6
Verified against
maplibre-gl5.0.0–5.24.0: the method is read at exactly the two snap gates and the field drives every follow path — a permanent shadow is safe there. Verified against the 6.0.0 bundle: the per-frame_renderterrain-follow and thesetTerrainelevation resets ALSO call the METHOD, so on the composed-camera shape the shadows exist only inside the movement window (movestart→moveend; both snap sites run beforemoveendis emitted). At rest the real method is in place and the follow/reset paths are untouched.gesture-snap-fix.test.tsasserts both regimes so a breaking bump fails loudly.Refcounted
The SDK terrain entry points (
addEsriTerrain/createEsriTerrain) apply this automatically while they own the map's terrain, and a consumer may ALSO call it manually — acquisitions are counted per map, so releasing one hold never strips the shadow while another still needs it. Each disposer is one-shot.