diff --git a/modules/core/src/controllers/controller.ts b/modules/core/src/controllers/controller.ts index 455c2ca3393..57c8d605e0a 100644 --- a/modules/core/src/controllers/controller.ts +++ b/modules/core/src/controllers/controller.ts @@ -20,6 +20,10 @@ const NO_TRANSITION_PROPS = { const DEFAULT_INERTIA = 300; const INERTIA_EASING = t => 1 - (1 - t) * (1 - t); +// Cap how much the smoothed log-scale can change per pinch event. 0.18 log2 +// units ≈ a 1.13× zoom per frame, well above any realistic pinch rate but +// low enough to swallow the spike from a single noisy final-lift frame. +const MAX_PINCH_ZOOM_DELTA_PER_EVENT = 0.18; const EVENT_TYPES = { WHEEL: ['wheel'], @@ -112,6 +116,10 @@ export type ViewStateChangeParameters = { const pinchEventWorkaround: any = {}; +function clampPinchZoomDelta(delta: number): number { + return Math.max(-MAX_PINCH_ZOOM_DELTA_PER_EVENT, Math.min(MAX_PINCH_ZOOM_DELTA_PER_EVENT, delta)); +} + export default abstract class Controller> { abstract get ControllerState(): ConstructorOf; abstract get transition(): TransitionProps; @@ -650,6 +658,7 @@ export default abstract class Controller { }); }); +test('MapController clamps a noisy final pinch frame instead of jumping', () => { + // Simulate a normal pinch ending with one sensor-noise spike. Without the + // per-event log-scale clamp the spike would propagate straight into the zoom. + const makePinchEvent = (type: string, scale: number, deltaTime: number) => ({ + type, + offsetCenter: {x: 50, y: 50}, + scale, + rotation: 0, + deltaTime, + srcEvent: {preventDefault() {}}, + stopPropagation() {} + }); + + const controller = createTestController({ + view: new MapView({controller: true}), + initialViewState: { + longitude: -122.45, + latitude: 37.78, + zoom: 10, + pitch: 30, + bearing: -45 + } + }); + + controller.handleEvent(makePinchEvent('pinchstart', 1, 0) as any); + controller.handleEvent(makePinchEvent('pinchmove', 1.05, 16) as any); + controller.handleEvent(makePinchEvent('pinchmove', 1.1, 32) as any); + const zoomBeforeSpike = controller.props.zoom as number; + // 100x scale on a single frame is the kind of spike a noisy lift produces. + controller.handleEvent(makePinchEvent('pinchmove', 100, 48) as any); + + const delta = (controller.props.zoom as number) - zoomBeforeSpike; + // The per-event log2 delta cap is 0.18. + expect( + delta, + 'noisy final pinch frame is clamped to the per-event log-scale cap' + ).toBeLessThanOrEqual(0.18 + 1e-6); +}); + test('GlobeController', async () => { await testController( GlobeView,