From 7788e413598debc591f326085937cef74664fe04 Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Sat, 23 May 2026 05:43:35 -0400 Subject: [PATCH] feat(core): skip pinch zoom inertia on touch input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pinch-end inertia projection is driven by velocity = Δlog2(scale)/Δt between the last two frames. On touch, the final-lift frame is almost always noisy, so even a small synthetic velocity gets multiplied by `inertia` and flings the camera well past where the user expected it to land. Gate the inertia branch on `pointerType !== 'touch'`. Trackpad / mouse pinches keep the existing inertia behavior; touch pinches end at the last live zoom. No new constants, no damping math — just one guard. --- modules/core/src/controllers/controller.ts | 12 +++++- .../core/controllers/controllers.spec.ts | 43 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/modules/core/src/controllers/controller.ts b/modules/core/src/controllers/controller.ts index 455c2ca3393..ab91f06e20a 100644 --- a/modules/core/src/controllers/controller.ts +++ b/modules/core/src/controllers/controller.ts @@ -692,7 +692,17 @@ export default abstract class Controller { }); }); +test('MapController skips pinch zoom inertia on touch lift', () => { + // Touch pinches lift with a noisy final frame that can produce a large + // synthetic velocity. The end zoom should equal the last live pinch zoom, + // not the inertia-projected zoom. + const makePinchEvent = ( + type: string, + scale: number, + deltaTime: number, + pointerType: 'touch' | 'mouse' = 'touch' + ) => ({ + type, + offsetCenter: {x: 50, y: 50}, + scale, + rotation: 0, + deltaTime, + srcEvent: {preventDefault() {}, pointerType}, + stopPropagation() {} + }); + + const controller = createTestController({ + view: new MapView({controller: true}), + initialViewState: { + longitude: -122.45, + latitude: 37.78, + zoom: 10, + pitch: 30, + bearing: -45, + inertia: 300 + } + }); + + controller.handleEvent(makePinchEvent('pinchstart', 1, 0) as any); + controller.handleEvent(makePinchEvent('pinchmove', 1.1, 16) as any); + const zoomAfterMove = controller.props.zoom as number; + // 100x scale spike on the lift frame — pre-fix would fling the zoom way past. + controller.handleEvent(makePinchEvent('pinchend', 100, 17) as any); + + expect( + controller.props.zoom, + 'touch pinch end stays at the last live zoom (no inertia projection)' + ).toBeCloseTo(zoomAfterMove); +}); + test('GlobeController', async () => { await testController( GlobeView,