// Proves the absolute pinch-zoom math, focused on the re-baseline case that // produced the on-device flicker: when a finger blips (2→1→2) mid-pinch the // viewer captures a fresh baseline, and the OLD code multiplied that fresh // scaleStart by the recognizer's still-cumulative details.scale — popping the // zoom to a wrong value and snapping back. absolutePinchScale() normalizes // against the baseline so the pop cannot happen. import 'package:flutter_test/flutter_test.dart'; import 'package:badnote/editor/canvas/pinch_scale_solver.dart'; void main() { const min = 0.5, max = 8.0; double solve(double scaleStart, double baseline, double raw) => absolutePinchScale( scaleStart: scaleStart, rawScaleAtBaseline: baseline, rawScale: raw, minScale: min, maxScale: max, ); test('at gesture start (baseline 1.0) target tracks raw directly', () { // scaleStart 1.0, baseline 1.0: pinch out to raw 2.0 → scale 2.0. expect(solve(1.0, 1.0, 2.0), closeTo(2.0, 1e-9)); // pinch in to raw 0.5 → scale 0.5. expect(solve(1.0, 1.0, 0.5), closeTo(0.5, 1e-9)); }); test('a re-baseline at the SAME instant does not change the scale', () { // Pinch in: start(2.0,1.0) → raw 0.6 gives scale 1.2 (well inside clamp). final before = solve(2.0, 1.0, 0.6); expect(before, closeTo(1.2, 1e-9)); // A finger blips: the viewer re-baselines RIGHT HERE — scaleStart becomes // the current scale (1.2) and rawScaleAtBaseline becomes the current raw // (0.6). Re-evaluating the same instant must yield the SAME scale (no pop). final after = solve(1.2, 0.6, 0.6); expect(after, closeTo(before, 1e-9), reason: 're-baseline must be continuous, not a jump'); }); test('after a re-baseline the pinch stays smooth (no pop)', () { // Re-baselined at scale 1.2 / raw 0.6. Continue pinching in: raw 0.54. // Correct: 1.2 * (0.54 / 0.6) = 1.08 — a gentle 10% step, monotonic. final next = solve(1.2, 0.6, 0.54); expect(next, closeTo(1.08, 1e-9)); // The OLD bug multiplied the fresh scaleStart by the un-normalized raw: // 1.2 * 0.54 = 0.648 — a sudden ~46% drop (the flicker). Guard against it. expect(next, greaterThan(1.0), reason: 'must NOT collapse to scaleStart*raw (the old 0.648 pop)'); }); test('result is clamped to [min, max]', () { expect(solve(4.0, 1.0, 4.0), max); // 16 → 8 expect(solve(1.0, 1.0, 0.1), min); // 0.1 → 0.5 }); test('degenerate baseline (0) is treated as no cumulative change', () { expect(solve(2.0, 0.0, 5.0), closeTo(2.0, 1e-9)); }); }