Files
BadNote/lib/editor/canvas/pinch_scale_solver.dart
Akiba So f48e43f13d fix(zoom): kill re-baseline pinch pop
Device log showed a single-frame scale pop (cur 0.504->0.694, a
+38% jump UP while the pinch was still shrinking).

Root cause: the absolute mapping targetScale = scaleStart *
details.scale is only valid when details.scale is 1.0 at the
moment scaleStart is captured. That holds at gesture start, but
on a mid-gesture re-baseline (a finger blips 2->1->2, routine on
Windows touch) a fresh scaleStart got multiplied by the
recognizer's still-cumulative details.scale, popping the zoom
then snapping back.

Fix: track rawScaleAtBaseline and normalize details.scale against
it so the cumulative reads 1.0 at every baseline. Extracted
absolutePinchScale() pure solver + 5 unit tests covering the
exact re-baseline scenario.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:30:41 +08:00

42 lines
1.9 KiB
Dart

// lib/editor/canvas/pinch_scale_solver.dart
//
// Pure math for the pen canvas's absolute pinch-zoom. Extracted so the
// re-baseline behavior (the subtle part) can be unit-tested without simulating
// a flaky multi-pointer gesture.
//
// The pinch is driven ABSOLUTELY: the scale shown is always
// scaleStart * (rawScale / rawScaleAtBaseline)
// where `scaleStart` is the matrix scale captured at the current baseline and
// `rawScaleAtBaseline` is the recognizer's cumulative `details.scale` at that
// same baseline. Dividing by `rawScaleAtBaseline` re-normalizes the cumulative
// scale so it reads 1.0 at the baseline instant.
//
// Why this matters: a baseline is captured at gesture start AND on every
// pointer-count change (a finger blips 2→1→2, routine on Windows touch). At
// gesture start `details.scale` is 1.0, so a naive `scaleStart * rawScale` is
// correct. But at a MID-GESTURE re-baseline `details.scale` is whatever the
// pinch has accumulated (e.g. 0.40) — multiplying the fresh `scaleStart` by
// that stale 0.40 popped the zoom to a wrong scale and snapped back (the
// reported flicker). Normalizing against `rawScaleAtBaseline` removes the pop.
import 'package:flutter/foundation.dart' show clampDouble;
/// Returns the absolute target scale for a pinch frame.
///
/// [scaleStart] — matrix scale captured at the current baseline.
/// [rawScaleAtBaseline] — recognizer cumulative `details.scale` at that
/// baseline (1.0 at gesture start; the live value at a re-baseline).
/// [rawScale] — the recognizer's current cumulative `details.scale`.
/// Result is clamped to [minScale, maxScale].
double absolutePinchScale({
required double scaleStart,
required double rawScaleAtBaseline,
required double rawScale,
required double minScale,
required double maxScale,
}) {
final double cumulative =
rawScaleAtBaseline > 0 ? rawScale / rawScaleAtBaseline : 1.0;
return clampDouble(scaleStart * cumulative, minScale, maxScale);
}