fix: coalesce pinch updates and stop live zoom write-back
All checks were successful
CI / Windows build (push) Successful in 9m55s

Surface Aug6 diag showed sDrop=0 but ~220 same-ms dual ZOOM frames and √2 cur ping-pong from reading currentZoom back into pinch state. Flush once per microtask and embed gitSha in diagnostic meta.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-06 03:44:10 +08:00
parent 4f6fb69dee
commit 85af037b7d
5 changed files with 150 additions and 40 deletions

View File

@@ -24,6 +24,7 @@
// because this canvas always uses an infinite boundary, free pan, and no
// rotation — so that code was provably a no-op here.
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart' show clampDouble;
@@ -122,6 +123,12 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
/// (the reported flicker). Normalizing kills that pop at the source.
double _rawScaleAtBaseline = 1.0;
/// Windows ScaleGestureRecognizer emits one onUpdate per finger move in the
/// same event-loop turn. Applying both mutates the matrix twice with an
/// intermediate state (Surface diag: √2-ish cur ping-pong). Keep latest only.
ScaleUpdateDetails? _pendingScaleUpdate;
bool _scaleFlushScheduled = false;
// --- Matrix helpers (infinite boundary → no clamping to bounds) -----------
Matrix4 _matrixTranslate(Matrix4 matrix, Offset translation) {
@@ -169,6 +176,8 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
_scaleAnimation?.removeListener(_handleScaleAnimation);
_scaleAnimation = null;
}
_pendingScaleUpdate = null;
_scaleFlushScheduled = false;
_gestureType = null;
_lastPointerCount = details.pointerCount;
_scaleStart = _transformer.value.getMaxScaleOnAxis();
@@ -178,6 +187,27 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
}
void _onScaleUpdate(ScaleUpdateDetails details) {
// Pointer-count change must apply immediately (re-baseline), not coalesce.
if (details.pointerCount != _lastPointerCount) {
_pendingScaleUpdate = null;
_scaleFlushScheduled = false;
_applyScaleUpdate(details);
return;
}
_pendingScaleUpdate = details;
if (_scaleFlushScheduled) return;
_scaleFlushScheduled = true;
scheduleMicrotask(() {
_scaleFlushScheduled = false;
final pending = _pendingScaleUpdate;
_pendingScaleUpdate = null;
if (pending != null && mounted && _scaleStart != null) {
_applyScaleUpdate(pending);
}
});
}
void _applyScaleUpdate(ScaleUpdateDetails details) {
final double scale = _transformer.value.getMaxScaleOnAxis();
_scaleAnimationFocalPoint = details.localFocalPoint;
@@ -220,14 +250,20 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
final bool focalDrop =
details.pointerCount >= 2 && focalJumpPx > _kFocalGlitchPx;
void record(double appliedChange, bool scaleDrop, bool focalDropped) {
void record(
double currentScale,
double appliedChange,
bool softClamped,
bool focalDropped,
) {
InputDiagnostics.instance.recordScaleFrame(
rawScale: details.scale,
pointerCount: details.pointerCount,
currentScale: scale,
currentScale: currentScale,
appliedChange: appliedChange,
focalJumpPx: focalJumpPx,
scaleDrop: scaleDrop,
scaleDrop: false,
softClamped: softClamped,
focalDrop: focalDropped,
);
}
@@ -252,7 +288,7 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
_rawScaleAtBaseline = details.scale;
_lastAppliedScale = step.appliedScale;
}
record(1.0, step.spiked, true);
record(_lastAppliedScale, 1.0, step.spiked, true);
return;
}
@@ -273,7 +309,7 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
final double applied =
_lastAppliedScale > 0 ? targetScale / _lastAppliedScale : 1.0;
_lastAppliedScale = targetScale;
record(applied, step.spiked, false);
record(targetScale, applied, step.spiked, false);
case _GestureType.pan:
assert(_referenceFocalPoint != null);
@@ -281,7 +317,7 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
if (details.scale != 1.0) return;
if (focalDrop) {
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
record(1.0, false, true);
record(scale, 1.0, false, true);
return;
}
final Offset translationChange =
@@ -289,11 +325,17 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
_transformer.value =
_matrixTranslate(_transformer.value, translationChange);
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
record(1.0, false, false);
record(scale, 1.0, false, false);
}
}
void _onScaleEnd(ScaleEndDetails details) {
final pending = _pendingScaleUpdate;
_pendingScaleUpdate = null;
_scaleFlushScheduled = false;
if (pending != null && _scaleStart != null) {
_applyScaleUpdate(pending);
}
_scaleStart = null;
_referenceFocalPoint = null;
_lastPointerCount = 0;