2026-06-23 09:30:41 +08:00
|
|
|
// 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));
|
|
|
|
|
});
|
2026-08-05 20:51:15 +08:00
|
|
|
|
|
|
|
|
group('softClampedPinchStep', () {
|
|
|
|
|
test('normal step passes through unchanged', () {
|
|
|
|
|
final s = softClampedPinchStep(
|
|
|
|
|
scaleStart: 1.0,
|
|
|
|
|
rawScaleAtBaseline: 1.0,
|
|
|
|
|
rawScale: 1.1,
|
|
|
|
|
lastAppliedScale: 1.0,
|
|
|
|
|
minScale: min,
|
|
|
|
|
maxScale: max,
|
|
|
|
|
maxStepRatio: 1.18,
|
|
|
|
|
);
|
|
|
|
|
expect(s.appliedScale, closeTo(1.1, 1e-9));
|
|
|
|
|
expect(s.spiked, isFalse);
|
|
|
|
|
expect(s.reanchor, isFalse);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('spike is soft-clamped and requests reanchor (no hard drop)', () {
|
|
|
|
|
// Ideal would jump 1.0 → 2.0 (ratio 2.0 >> 1.18).
|
|
|
|
|
final s = softClampedPinchStep(
|
|
|
|
|
scaleStart: 1.0,
|
|
|
|
|
rawScaleAtBaseline: 1.0,
|
|
|
|
|
rawScale: 2.0,
|
|
|
|
|
lastAppliedScale: 1.0,
|
|
|
|
|
minScale: min,
|
|
|
|
|
maxScale: max,
|
|
|
|
|
maxStepRatio: 1.18,
|
|
|
|
|
);
|
|
|
|
|
expect(s.appliedScale, closeTo(1.18, 1e-9));
|
|
|
|
|
expect(s.spiked, isTrue);
|
|
|
|
|
expect(s.reanchor, isTrue);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('after reanchor, next frame can continue smoothly', () {
|
|
|
|
|
final spike = softClampedPinchStep(
|
|
|
|
|
scaleStart: 1.0,
|
|
|
|
|
rawScaleAtBaseline: 1.0,
|
|
|
|
|
rawScale: 2.0,
|
|
|
|
|
lastAppliedScale: 1.0,
|
|
|
|
|
minScale: min,
|
|
|
|
|
maxScale: max,
|
|
|
|
|
maxStepRatio: 1.18,
|
|
|
|
|
);
|
|
|
|
|
// Caller re-anchors: scaleStart=1.18, baseline=2.0, last=1.18.
|
|
|
|
|
// Continue toward ideal 2.0 with raw still 2.0 → applied stays 1.18.
|
|
|
|
|
final next = softClampedPinchStep(
|
|
|
|
|
scaleStart: spike.appliedScale,
|
|
|
|
|
rawScaleAtBaseline: 2.0,
|
|
|
|
|
rawScale: 2.0,
|
|
|
|
|
lastAppliedScale: spike.appliedScale,
|
|
|
|
|
minScale: min,
|
|
|
|
|
maxScale: max,
|
|
|
|
|
maxStepRatio: 1.18,
|
|
|
|
|
);
|
|
|
|
|
expect(next.appliedScale, closeTo(1.18, 1e-9));
|
|
|
|
|
expect(next.spiked, isFalse);
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-06-23 09:30:41 +08:00
|
|
|
}
|