fix: coalesce pinch updates and stop live zoom write-back
All checks were successful
CI / Windows build (push) Successful in 9m55s
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:
@@ -22,6 +22,8 @@
|
||||
// IS the identity; the old djb2 path-hash document id is gone). Highlights now
|
||||
// survive reopen, and a stored highlight can be removed (the un-highlight tool).
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart'
|
||||
show ValueListenable, visibleForTesting;
|
||||
import 'package:flutter/gestures.dart';
|
||||
@@ -188,6 +190,12 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
/// absolute target normalizes against it (see [absolutePinchScale]).
|
||||
double _pinchRawScaleAtBaseline = 1.0;
|
||||
|
||||
/// Latest ScaleUpdate pending coalesce (Windows fires one update per finger
|
||||
/// move → two onUpdates in the same event turn; applying both causes √2-ish
|
||||
/// zoom ping-pong via intermediate matrices).
|
||||
ScaleUpdateDetails? _pendingPinchUpdate;
|
||||
bool _pinchFlushScheduled = false;
|
||||
|
||||
// ── Live stroke state (viewer-level pen capture) ────────────────────────────
|
||||
|
||||
/// The page index the in-progress stroke belongs to (the page of its first
|
||||
@@ -907,19 +915,46 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
|
||||
void _onPinchStart(ScaleStartDetails details) {
|
||||
if (!_controller.isReady) return;
|
||||
_pinchScaleStart = _controller.currentZoom;
|
||||
_pendingPinchUpdate = null;
|
||||
_pinchFlushScheduled = false;
|
||||
// Seed ONLY at gesture start. Never mid-gesture (live read-back caused the
|
||||
// Aug6 √2 cur ping-pong when dual finger updates interleaved).
|
||||
final live = _controller.currentZoom;
|
||||
_pinchScaleStart = live > 0 ? live : 1.0;
|
||||
_pinchPointerCount = details.pointerCount;
|
||||
_pinchLastAppliedScale = _pinchScaleStart!;
|
||||
_pinchRawScaleAtBaseline = 1.0;
|
||||
}
|
||||
|
||||
void _onPinchUpdate(ScaleUpdateDetails details) {
|
||||
if (_pinchScaleStart == null || !_controller.isReady) return;
|
||||
// Pointer-count change must apply immediately (re-baseline), not coalesce.
|
||||
if (details.pointerCount != _pinchPointerCount) {
|
||||
_pendingPinchUpdate = null;
|
||||
_pinchFlushScheduled = false;
|
||||
_applyPinchUpdate(details);
|
||||
return;
|
||||
}
|
||||
// Coalesce: Windows ScaleGestureRecognizer fires one onUpdate per finger
|
||||
// move → two applies in the same turn with an intermediate matrix → √2-ish
|
||||
// zoom bounce. Keep only the latest details and flush once per microtask.
|
||||
_pendingPinchUpdate = details;
|
||||
if (_pinchFlushScheduled) return;
|
||||
_pinchFlushScheduled = true;
|
||||
scheduleMicrotask(() {
|
||||
_pinchFlushScheduled = false;
|
||||
final pending = _pendingPinchUpdate;
|
||||
_pendingPinchUpdate = null;
|
||||
if (pending != null && mounted && _pinchScaleStart != null) {
|
||||
_applyPinchUpdate(pending);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _applyPinchUpdate(ScaleUpdateDetails details) {
|
||||
final scaleStart = _pinchScaleStart;
|
||||
if (scaleStart == null || !_controller.isReady) return;
|
||||
|
||||
// Re-baseline on any pointer-count change (a finger lands/lifts, or a
|
||||
// Windows touch 2↔1↔2 dropout). Anchor to the CLEAN tracked scale, not a
|
||||
// matrix read-back, so the displayed scale is continuous; skip this frame.
|
||||
if (details.pointerCount != _pinchPointerCount) {
|
||||
_pinchPointerCount = details.pointerCount;
|
||||
_pinchScaleStart = _pinchLastAppliedScale;
|
||||
@@ -928,8 +963,6 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Soft-clamp per-step change (Surface diag: hard SDROP avalanche when
|
||||
// lastRaw froze while live zoom still crawled). Always apply + advance.
|
||||
final step = softClampedPinchStep(
|
||||
scaleStart: _pinchScaleStart!,
|
||||
rawScaleAtBaseline: _pinchRawScaleAtBaseline,
|
||||
@@ -941,7 +974,6 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
);
|
||||
final focalDrop = details.focalPointDelta.distance > _kFocalGlitchPx;
|
||||
if (focalDrop) {
|
||||
// Keep scale continuous; only skip the focal jump this frame.
|
||||
if (step.reanchor) {
|
||||
_pinchScaleStart = step.appliedScale;
|
||||
_pinchRawScaleAtBaseline = details.scale;
|
||||
@@ -953,7 +985,8 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
currentScale: _pinchLastAppliedScale,
|
||||
appliedChange: 1.0,
|
||||
focalJumpPx: details.focalPointDelta.distance,
|
||||
scaleDrop: step.spiked,
|
||||
scaleDrop: false,
|
||||
softClamped: step.spiked,
|
||||
focalDrop: true,
|
||||
);
|
||||
return;
|
||||
@@ -971,28 +1004,31 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
duration: Duration.zero,
|
||||
);
|
||||
|
||||
// Prefer controller read-back so we stay locked to what pdfrx actually
|
||||
// applied (guards against a second consumer nudging zoom).
|
||||
final live = _controller.currentZoom;
|
||||
final appliedScale = live > 0 ? live : targetScale;
|
||||
|
||||
final applied = _pinchLastAppliedScale > 0
|
||||
? appliedScale / _pinchLastAppliedScale
|
||||
? targetScale / _pinchLastAppliedScale
|
||||
: 1.0;
|
||||
InputDiagnostics.instance.recordScaleFrame(
|
||||
rawScale: details.scale,
|
||||
pointerCount: details.pointerCount,
|
||||
currentScale: appliedScale,
|
||||
currentScale: targetScale,
|
||||
appliedChange: applied,
|
||||
focalJumpPx: details.focalPointDelta.distance,
|
||||
scaleDrop: step.spiked,
|
||||
scaleDrop: false,
|
||||
softClamped: step.spiked,
|
||||
focalDrop: false,
|
||||
liveScale: _controller.currentZoom,
|
||||
);
|
||||
|
||||
_pinchLastAppliedScale = appliedScale;
|
||||
_pinchLastAppliedScale = targetScale;
|
||||
}
|
||||
|
||||
void _onPinchEnd(ScaleEndDetails details) {
|
||||
final pending = _pendingPinchUpdate;
|
||||
_pendingPinchUpdate = null;
|
||||
_pinchFlushScheduled = false;
|
||||
if (pending != null && _pinchScaleStart != null) {
|
||||
_applyPinchUpdate(pending);
|
||||
}
|
||||
_pinchScaleStart = null;
|
||||
_pinchPointerCount = 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user