From 9024c78de1c14d492ec413272c46dd73d7a37c9a Mon Sep 17 00:00:00 2001 From: Abdullah <89297042+AzazelSensei@users.noreply.github.com> Date: Thu, 13 Aug 2026 12:33:17 +0300 Subject: [PATCH] fix: don't pass a non-positive pinch scale to math.log The pinch path checked details.scale > 0, but the value actually logged is details.scale + _scaleCorrector. After a gesture-race win that corrector is 1 - lastScale, so the sum can go <= 0. math.log of that is NaN and it ends up in the camera. Skip the zoom update for that frame, and make zoomForScale return the start zoom when the scale isn't usable. Fixes #2237 --- lib/src/gestures/map_interactive_viewer.dart | 22 ++++++++--- .../gestures/map_interactive_viewer_test.dart | 38 +++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/lib/src/gestures/map_interactive_viewer.dart b/lib/src/gestures/map_interactive_viewer.dart index 252080994..a8ddc43b3 100644 --- a/lib/src/gestures/map_interactive_viewer.dart +++ b/lib/src/gestures/map_interactive_viewer.dart @@ -647,10 +647,13 @@ class MapInteractiveViewerState extends State var newZoom = _camera.zoom; // Handle pinch zoom. - if (hasPinchZoom && details.scale > 0.0) { + // After a gesture-race win, _scaleCorrector is 1 - lastScale, so this + // sum can be <= 0 even when details.scale itself is positive. + final scale = details.scale + _scaleCorrector; + if (hasPinchZoom && scale > 0.0) { newZoom = _getZoomForScale( _mapZoomStart, - details.scale + _scaleCorrector, + scale, ); // Handle starting of pinch zoom. @@ -1401,10 +1404,19 @@ class MapInteractiveViewerState extends State // Utilities + /// Converts a pinch [scale] into a zoom level starting from [startZoom]. + /// + /// Returns [startZoom] when [scale] is not a positive finite number. + /// `math.log` is undefined for those inputs and would produce `NaN` or + /// `-Infinity`, which then poison the camera. + @visibleForTesting + static double zoomForScale(double startZoom, double scale) { + if (scale <= 0.0 || !scale.isFinite) return startZoom; + return scale == 1.0 ? startZoom : startZoom + math.log(scale) / math.ln2; + } + double _getZoomForScale(double startZoom, double scale) { - final resultZoom = - scale == 1.0 ? startZoom : startZoom + math.log(scale) / math.ln2; - return _camera.clampZoom(resultZoom); + return _camera.clampZoom(zoomForScale(startZoom, scale)); } Offset _rotateOffset(Offset offset) { diff --git a/test/gestures/map_interactive_viewer_test.dart b/test/gestures/map_interactive_viewer_test.dart index b7ee28728..1dce29355 100644 --- a/test/gestures/map_interactive_viewer_test.dart +++ b/test/gestures/map_interactive_viewer_test.dart @@ -46,4 +46,42 @@ void main() { }, ); }); + + group('MapInteractiveViewerState.zoomForScale', () { + test('adds log2(scale) to the start zoom', () { + expect(MapInteractiveViewerState.zoomForScale(13, 2), 14); + expect(MapInteractiveViewerState.zoomForScale(13, 1), 13); + expect(MapInteractiveViewerState.zoomForScale(13, 0.5), 12); + }); + + test( + 'returns the start zoom when the corrected pinch scale is not ' + 'positive (regression: details.scale + _scaleCorrector can be ' + '<= 0 after a gesture-race win, and math.log of that is NaN - ' + 'https://github.com/fleaflet/flutter_map/issues/2237)', + () { + // _scaleCorrector = 1.0 - _lastScale, with _lastScale == 2.5 + const scaleCorrector = 1.0 - 2.5; + const detailsScale = 1; + final scale = detailsScale + scaleCorrector; + + expect(scale, lessThanOrEqualTo(0)); + + final zoom = MapInteractiveViewerState.zoomForScale(13, scale); + + expect(zoom, 13); + expect(zoom.isFinite, isTrue); + expect(zoom.isNaN, isFalse); + }, + ); + + test('returns the start zoom for zero, NaN, and infinite scale', () { + expect(MapInteractiveViewerState.zoomForScale(13, 0), 13); + expect(MapInteractiveViewerState.zoomForScale(13, double.nan), 13); + expect( + MapInteractiveViewerState.zoomForScale(13, double.negativeInfinity), + 13, + ); + }); + }); }