fix: soft-clamp pinch zoom and Krita-inspired brush opacity
All checks were successful
CI / Windows build (push) Successful in 10m28s
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>
This commit is contained in:
@@ -48,7 +48,6 @@ const Set<PointerDeviceKind> _kPanZoomDevices = <PointerDeviceKind>{
|
||||
/// intent — that frame is dropped so the zoom can't pop and snap back.
|
||||
/// Device logs showed spikes ~1.30; keep the band under that so jumps die.
|
||||
const double _kScaleGlitchHi = 1.18;
|
||||
const double _kScaleGlitchLo = 1 / _kScaleGlitchHi;
|
||||
|
||||
/// During a 2-finger gesture the focal point (finger midpoint) should move
|
||||
/// smoothly. A single-frame local jump beyond this is a Windows touch misread,
|
||||
@@ -107,15 +106,8 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
||||
/// applying a frame whose scale/focal still refer to the old finger set.
|
||||
int _lastPointerCount = 0;
|
||||
|
||||
/// The recognizer's cumulative `details.scale` and the absolute scale we last
|
||||
/// APPLIED, both as of the previous accepted frame. The pinch is driven
|
||||
/// absolutely from these + the gesture-start snapshot — we never read the live
|
||||
/// matrix back into the per-frame scale change. (Re-reading
|
||||
/// `getMaxScaleOnAxis()` per frame was the flicker source: a single transient
|
||||
/// mis-read/interleaved write made `desiredScale/liveScale` demand a ~1.3–1.4x
|
||||
/// jump for one frame and snap back. The glitch guard missed it because the
|
||||
/// spike sat just under the 1.4 threshold.)
|
||||
double _lastRawScale = 1.0;
|
||||
/// The absolute scale we last APPLIED. Soft-clamp limits the step from this
|
||||
/// value; we never read the live matrix back into the per-frame scale change.
|
||||
double _lastAppliedScale = 1.0;
|
||||
|
||||
/// The recognizer's cumulative `details.scale` AT THE CURRENT BASELINE (the
|
||||
@@ -181,7 +173,6 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
||||
_lastPointerCount = details.pointerCount;
|
||||
_scaleStart = _transformer.value.getMaxScaleOnAxis();
|
||||
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
|
||||
_lastRawScale = 1.0;
|
||||
_lastAppliedScale = _scaleStart!;
|
||||
_rawScaleAtBaseline = 1.0;
|
||||
}
|
||||
@@ -206,7 +197,6 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
||||
// any transient in the live matrix.
|
||||
_scaleStart = _lastAppliedScale;
|
||||
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
|
||||
_lastRawScale = details.scale;
|
||||
// Re-anchor the cumulative scale to THIS frame's details.scale so the next
|
||||
// good frame resumes from _scaleStart (not _scaleStart × a stale ratio).
|
||||
_rawScaleAtBaseline = details.scale;
|
||||
@@ -245,39 +235,32 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
||||
switch (_gestureType!) {
|
||||
case _GestureType.scale:
|
||||
assert(_scaleStart != null);
|
||||
// Per-frame finger-motion ratio from the recognizer's OWN cumulative
|
||||
// scale — the clean, monotonic signal (verified against device logs).
|
||||
// Crucially we do NOT divide by the live matrix scale here: feeding
|
||||
// getMaxScaleOnAxis() back in is what let a single mis-read pop the zoom
|
||||
// and snap back. A ratio outside the glitch band is a real multi-touch
|
||||
// spike → drop the frame; absolute tracking means the next good frame
|
||||
// resumes from the true finger span, so the spike never shows.
|
||||
final double rawRatio =
|
||||
_lastRawScale > 0 ? details.scale / _lastRawScale : 1.0;
|
||||
final bool scaleDrop =
|
||||
rawRatio > _kScaleGlitchHi || rawRatio < _kScaleGlitchLo;
|
||||
if (scaleDrop || focalDrop) {
|
||||
record(1.0, scaleDrop, focalDrop);
|
||||
return;
|
||||
}
|
||||
|
||||
// Drive the transform ABSOLUTELY from the gesture-start snapshot: the
|
||||
// target scale is `_scaleStart * details.scale`, and we re-anchor so the
|
||||
// scene point that was under the focal at gesture start stays under the
|
||||
// CURRENT focal (which also yields 2-finger pan for free). Closed form
|
||||
// for a pure scale+translate matrix — no inversion, no live read-back —
|
||||
// so an interleaved/transient matrix write can't survive into the next
|
||||
// frame: every frame is fully re-derived from clean inputs.
|
||||
// Absolute target scale, normalized against the baseline so a
|
||||
// mid-gesture re-baseline (finger blip) can't pop the zoom. See
|
||||
// pinch_scale_solver.dart for the full rationale.
|
||||
final double targetScale = absolutePinchScale(
|
||||
// Soft-clamp per-step change instead of hard-dropping (Surface diag:
|
||||
// hard SDROP froze lastRaw and avalanched while the matrix still moved).
|
||||
final SoftPinchStep step = softClampedPinchStep(
|
||||
scaleStart: _scaleStart!,
|
||||
rawScaleAtBaseline: _rawScaleAtBaseline,
|
||||
rawScale: details.scale,
|
||||
lastAppliedScale: _lastAppliedScale,
|
||||
minScale: widget.minScale,
|
||||
maxScale: widget.maxScale,
|
||||
maxStepRatio: _kScaleGlitchHi,
|
||||
);
|
||||
if (focalDrop) {
|
||||
if (step.reanchor) {
|
||||
_scaleStart = step.appliedScale;
|
||||
_rawScaleAtBaseline = details.scale;
|
||||
_lastAppliedScale = step.appliedScale;
|
||||
}
|
||||
record(1.0, step.spiked, true);
|
||||
return;
|
||||
}
|
||||
|
||||
final double targetScale = step.appliedScale;
|
||||
if (step.reanchor) {
|
||||
_scaleStart = targetScale;
|
||||
_rawScaleAtBaseline = details.scale;
|
||||
}
|
||||
final Offset focal = details.localFocalPoint;
|
||||
final double tx = focal.dx - targetScale * _referenceFocalPoint!.dx;
|
||||
final double ty = focal.dy - targetScale * _referenceFocalPoint!.dy;
|
||||
@@ -289,9 +272,8 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
||||
|
||||
final double applied =
|
||||
_lastAppliedScale > 0 ? targetScale / _lastAppliedScale : 1.0;
|
||||
_lastRawScale = details.scale;
|
||||
_lastAppliedScale = targetScale;
|
||||
record(applied, false, false);
|
||||
record(applied, step.spiked, false);
|
||||
|
||||
case _GestureType.pan:
|
||||
assert(_referenceFocalPoint != null);
|
||||
|
||||
Reference in New Issue
Block a user