All checks were successful
CI / Windows build (push) Successful in 10m28s
Hard SDROP avalanches froze lastRaw while zoom still crawled; soft-clamp and re-anchor instead. Ballpoint is near-solid, pencil uses soft √p without multiply stacking; PDF ink falls back to nearest page during zoom settle. Co-authored-by: Cursor <cursoragent@cursor.com>
104 lines
4.1 KiB
Dart
104 lines
4.1 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.
|
|
//
|
|
// Soft-clamp (Surface 2026-08-05 diag): a HARD drop of frames whose per-step
|
|
// ratio exceeds the glitch band caused an avalanche — lastRaw never advanced,
|
|
// so every subsequent frame also dropped while pdfrx/live zoom still crawled.
|
|
// [softClampedPinchStep] always returns an applied scale, clamping the step,
|
|
// and tells the caller to re-anchor when a spike was clipped.
|
|
|
|
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);
|
|
}
|
|
|
|
/// Result of one soft-clamped pinch step.
|
|
class SoftPinchStep {
|
|
const SoftPinchStep({
|
|
required this.appliedScale,
|
|
required this.reanchor,
|
|
required this.spiked,
|
|
});
|
|
|
|
/// Scale to write into the matrix / controller this frame.
|
|
final double appliedScale;
|
|
|
|
/// When true the caller must set `scaleStart = appliedScale` and
|
|
/// `rawScaleAtBaseline = rawScale` so absolute tracking does not keep
|
|
/// fighting the clamp on later frames.
|
|
final bool reanchor;
|
|
|
|
/// True when the ideal absolute target was clipped by the per-step band.
|
|
final bool spiked;
|
|
}
|
|
|
|
/// Soft-clamp the per-frame scale change instead of dropping the frame.
|
|
///
|
|
/// Ideal scale comes from [absolutePinchScale]. The step from
|
|
/// [lastAppliedScale] is then limited to `[1/maxStepRatio, maxStepRatio]`.
|
|
/// Spikes still get partially applied (smooth catch-up) and the caller
|
|
/// re-anchors so the next frame starts clean.
|
|
SoftPinchStep softClampedPinchStep({
|
|
required double scaleStart,
|
|
required double rawScaleAtBaseline,
|
|
required double rawScale,
|
|
required double lastAppliedScale,
|
|
required double minScale,
|
|
required double maxScale,
|
|
required double maxStepRatio,
|
|
}) {
|
|
final ideal = absolutePinchScale(
|
|
scaleStart: scaleStart,
|
|
rawScaleAtBaseline: rawScaleAtBaseline,
|
|
rawScale: rawScale,
|
|
minScale: minScale,
|
|
maxScale: maxScale,
|
|
);
|
|
if (lastAppliedScale <= 0 || maxStepRatio <= 1.0) {
|
|
return SoftPinchStep(appliedScale: ideal, reanchor: false, spiked: false);
|
|
}
|
|
final lo = lastAppliedScale / maxStepRatio;
|
|
final hi = lastAppliedScale * maxStepRatio;
|
|
final applied = clampDouble(ideal, lo, hi);
|
|
final spiked = applied != ideal;
|
|
return SoftPinchStep(
|
|
appliedScale: clampDouble(applied, minScale, maxScale),
|
|
reanchor: spiked,
|
|
spiked: spiked,
|
|
);
|
|
}
|