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:
@@ -220,7 +220,13 @@ jobs:
|
|||||||
HTTPS_PROXY: http://192.168.31.189:7890
|
HTTPS_PROXY: http://192.168.31.189:7890
|
||||||
http_proxy: http://192.168.31.189:7890
|
http_proxy: http://192.168.31.189:7890
|
||||||
https_proxy: http://192.168.31.189:7890
|
https_proxy: http://192.168.31.189:7890
|
||||||
run: flutter build windows --release
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$sha = if ($env:GITHUB_SHA) { $env:GITHUB_SHA.Substring(0, [Math]::Min(12, $env:GITHUB_SHA.Length)) } else { "unknown" }
|
||||||
|
$built = Get-Date -Format "yyyy-MM-ddTHH:mm:ssK"
|
||||||
|
flutter build windows --release `
|
||||||
|
--dart-define="BADNOTE_GIT_SHA=$sha" `
|
||||||
|
--dart-define="BADNOTE_BUILD_TIME=$built"
|
||||||
|
|
||||||
- name: Show build output
|
- name: Show build output
|
||||||
shell: powershell
|
shell: powershell
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Build a zip diagnostic pack the user can hand back for remote debugging.
|
// Build a zip diagnostic pack the user can hand back for remote debugging.
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
@@ -13,6 +14,18 @@ import 'badnote_log.dart';
|
|||||||
import 'frame_sampler.dart';
|
import 'frame_sampler.dart';
|
||||||
import 'pen_event_ring.dart';
|
import 'pen_event_ring.dart';
|
||||||
|
|
||||||
|
/// Injected at build/export time so Surface packages can be matched to git.
|
||||||
|
/// Override via `--dart-define=BADNOTE_GIT_SHA=...` in CI.
|
||||||
|
const String kBadNoteGitSha = String.fromEnvironment(
|
||||||
|
'BADNOTE_GIT_SHA',
|
||||||
|
defaultValue: 'dev',
|
||||||
|
);
|
||||||
|
|
||||||
|
const String kBadNoteBuildTime = String.fromEnvironment(
|
||||||
|
'BADNOTE_BUILD_TIME',
|
||||||
|
defaultValue: '',
|
||||||
|
);
|
||||||
|
|
||||||
class DiagnosticExportResult {
|
class DiagnosticExportResult {
|
||||||
DiagnosticExportResult({required this.zipPath, required this.bytes});
|
DiagnosticExportResult({required this.zipPath, required this.bytes});
|
||||||
|
|
||||||
@@ -34,6 +47,8 @@ class DiagnosticExport {
|
|||||||
final meta = <String, Object?>{
|
final meta = <String, Object?>{
|
||||||
'exportedAt': DateTime.now().toIso8601String(),
|
'exportedAt': DateTime.now().toIso8601String(),
|
||||||
'sessionId': log.sessionId,
|
'sessionId': log.sessionId,
|
||||||
|
'gitSha': kBadNoteGitSha,
|
||||||
|
'buildTime': kBadNoteBuildTime.isEmpty ? null : kBadNoteBuildTime,
|
||||||
'platform': Platform.operatingSystem,
|
'platform': Platform.operatingSystem,
|
||||||
'osVersion': Platform.operatingSystemVersion,
|
'osVersion': Platform.operatingSystemVersion,
|
||||||
'localHostname': Platform.localHostname,
|
'localHostname': Platform.localHostname,
|
||||||
@@ -48,7 +63,8 @@ class DiagnosticExport {
|
|||||||
'zoom': InputDiagnostics.instance.summary(),
|
'zoom': InputDiagnostics.instance.summary(),
|
||||||
'frames': FrameSampler.instance.summary(),
|
'frames': FrameSampler.instance.summary(),
|
||||||
'instructions':
|
'instructions':
|
||||||
'Reproduce the issue for ~3 minutes with diagnostics on, then share this zip.',
|
'Reproduce the issue for ~3 minutes with diagnostics on, then share this zip. '
|
||||||
|
'Confirm meta.gitSha matches the CI commit you installed.',
|
||||||
};
|
};
|
||||||
|
|
||||||
final archive = Archive();
|
final archive = Archive();
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ class InputDiagnostics extends ChangeNotifier {
|
|||||||
static final InputDiagnostics instance = InputDiagnostics._();
|
static final InputDiagnostics instance = InputDiagnostics._();
|
||||||
|
|
||||||
int frames = 0;
|
int frames = 0;
|
||||||
int scaleDropped = 0; // frames rejected as a scale glitch
|
int scaleDropped = 0; // frames HARD-rejected (legacy; prefer soft-clamp)
|
||||||
|
int softClamped = 0; // frames whose step was soft-clamped (still applied)
|
||||||
int focalDropped = 0; // frames rejected as a focal/position glitch
|
int focalDropped = 0; // frames rejected as a focal/position glitch
|
||||||
int rebaselines = 0; // pointer-count re-baselines
|
int rebaselines = 0; // pointer-count re-baselines
|
||||||
int pointerCountMax = 0;
|
int pointerCountMax = 0;
|
||||||
@@ -42,13 +43,16 @@ class InputDiagnostics extends ChangeNotifier {
|
|||||||
required double focalJumpPx,
|
required double focalJumpPx,
|
||||||
required bool scaleDrop,
|
required bool scaleDrop,
|
||||||
required bool focalDrop,
|
required bool focalDrop,
|
||||||
|
bool softClamped = false,
|
||||||
|
double? liveScale,
|
||||||
}) {
|
}) {
|
||||||
frames++;
|
frames++;
|
||||||
if (scaleDrop) scaleDropped++;
|
if (scaleDrop) scaleDropped++;
|
||||||
|
if (softClamped) this.softClamped++;
|
||||||
if (focalDrop) focalDropped++;
|
if (focalDrop) focalDropped++;
|
||||||
if (rawScale < rawScaleMin) rawScaleMin = rawScale;
|
if (rawScale < rawScaleMin) rawScaleMin = rawScale;
|
||||||
if (rawScale > rawScaleMax) rawScaleMax = rawScale;
|
if (rawScale > rawScaleMax) rawScaleMax = rawScale;
|
||||||
final double resulting = currentScale * appliedChange;
|
final double resulting = currentScale;
|
||||||
if (resulting < scaleMin) scaleMin = resulting;
|
if (resulting < scaleMin) scaleMin = resulting;
|
||||||
if (resulting > scaleMax) scaleMax = resulting;
|
if (resulting > scaleMax) scaleMax = resulting;
|
||||||
if (pointerCount > pointerCountMax) pointerCountMax = pointerCount;
|
if (pointerCount > pointerCountMax) pointerCountMax = pointerCount;
|
||||||
@@ -56,16 +60,21 @@ class InputDiagnostics extends ChangeNotifier {
|
|||||||
final double jump = appliedChange >= 1 ? appliedChange : 1 / appliedChange;
|
final double jump = appliedChange >= 1 ? appliedChange : 1 / appliedChange;
|
||||||
if (jump > maxAppliedScaleJump) maxAppliedScaleJump = jump;
|
if (jump > maxAppliedScaleJump) maxAppliedScaleJump = jump;
|
||||||
|
|
||||||
|
final liveBit = liveScale == null
|
||||||
|
? ''
|
||||||
|
: ' live=${liveScale.toStringAsFixed(3)}';
|
||||||
final String line = 'p$pointerCount raw=${rawScale.toStringAsFixed(3)} '
|
final String line = 'p$pointerCount raw=${rawScale.toStringAsFixed(3)} '
|
||||||
'ch=${appliedChange.toStringAsFixed(3)} '
|
'ch=${appliedChange.toStringAsFixed(3)} '
|
||||||
'cur=${currentScale.toStringAsFixed(3)} '
|
'cur=${currentScale.toStringAsFixed(3)}$liveBit '
|
||||||
'fj=${focalJumpPx.toStringAsFixed(0)}'
|
'fj=${focalJumpPx.toStringAsFixed(0)}'
|
||||||
'${scaleDrop ? " SDROP" : ""}${focalDrop ? " FDROP" : ""}';
|
'${softClamped ? " SCLAMP" : ""}'
|
||||||
|
'${scaleDrop ? " SDROP" : ""}'
|
||||||
|
'${focalDrop ? " FDROP" : ""}';
|
||||||
_trace.add(line);
|
_trace.add(line);
|
||||||
if (_trace.length > 24) _trace.removeAt(0);
|
if (_trace.length > 24) _trace.removeAt(0);
|
||||||
FrameSampler.instance.recordZoom(
|
FrameSampler.instance.recordZoom(
|
||||||
rawScale: rawScale,
|
rawScale: rawScale,
|
||||||
scaleDrop: scaleDrop,
|
scaleDrop: scaleDrop || softClamped,
|
||||||
focalDrop: focalDrop,
|
focalDrop: focalDrop,
|
||||||
focalJumpPx: focalJumpPx,
|
focalJumpPx: focalJumpPx,
|
||||||
);
|
);
|
||||||
@@ -74,7 +83,8 @@ class InputDiagnostics extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
frames = scaleDropped = focalDropped = rebaselines = pointerCountMax = 0;
|
frames = scaleDropped = softClamped = focalDropped = rebaselines =
|
||||||
|
pointerCountMax = 0;
|
||||||
rawScaleMin = scaleMin = double.infinity;
|
rawScaleMin = scaleMin = double.infinity;
|
||||||
rawScaleMax = scaleMax = 0;
|
rawScaleMax = scaleMax = 0;
|
||||||
maxFocalJumpPx = 0;
|
maxFocalJumpPx = 0;
|
||||||
@@ -83,15 +93,15 @@ class InputDiagnostics extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _f(double v) => v.isFinite ? v.toStringAsFixed(2) : '-';
|
|
||||||
|
|
||||||
String summary() {
|
String summary() {
|
||||||
if (frames == 0) return 'zoom: (pinch to record)';
|
final rawLo = rawScaleMin.isFinite ? rawScaleMin.toStringAsFixed(2) : '-';
|
||||||
return 'zoom f=$frames sDrop=$scaleDropped fDrop=$focalDropped '
|
final rawHi = rawScaleMax > 0 ? rawScaleMax.toStringAsFixed(2) : '-';
|
||||||
'rebase=$rebaselines pMax=$pointerCountMax\n'
|
final scLo = scaleMin.isFinite ? scaleMin.toStringAsFixed(2) : '-';
|
||||||
' raw=${_f(rawScaleMin)}..${_f(rawScaleMax)} '
|
final scHi = scaleMax > 0 ? scaleMax.toStringAsFixed(2) : '-';
|
||||||
'scale=${_f(scaleMin)}..${_f(scaleMax)}\n'
|
return 'zoom f=$frames sDrop=$scaleDropped sClamp=$softClamped '
|
||||||
|
'fDrop=$focalDropped rebase=$rebaselines pMax=$pointerCountMax\n'
|
||||||
|
' raw=$rawLo..$rawHi scale=$scLo..$scHi\n'
|
||||||
' maxFocalJump=${maxFocalJumpPx.toStringAsFixed(0)}px '
|
' maxFocalJump=${maxFocalJumpPx.toStringAsFixed(0)}px '
|
||||||
'maxScaleJump=${_f(maxAppliedScaleJump)}';
|
'maxScaleJump=${maxAppliedScaleJump.toStringAsFixed(2)}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
// IS the identity; the old djb2 path-hash document id is gone). Highlights now
|
// 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).
|
// survive reopen, and a stored highlight can be removed (the un-highlight tool).
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart'
|
import 'package:flutter/foundation.dart'
|
||||||
show ValueListenable, visibleForTesting;
|
show ValueListenable, visibleForTesting;
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
@@ -188,6 +190,12 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
|||||||
/// absolute target normalizes against it (see [absolutePinchScale]).
|
/// absolute target normalizes against it (see [absolutePinchScale]).
|
||||||
double _pinchRawScaleAtBaseline = 1.0;
|
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) ────────────────────────────
|
// ── Live stroke state (viewer-level pen capture) ────────────────────────────
|
||||||
|
|
||||||
/// The page index the in-progress stroke belongs to (the page of its first
|
/// 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) {
|
void _onPinchStart(ScaleStartDetails details) {
|
||||||
if (!_controller.isReady) return;
|
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;
|
_pinchPointerCount = details.pointerCount;
|
||||||
_pinchLastAppliedScale = _pinchScaleStart!;
|
_pinchLastAppliedScale = _pinchScaleStart!;
|
||||||
_pinchRawScaleAtBaseline = 1.0;
|
_pinchRawScaleAtBaseline = 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPinchUpdate(ScaleUpdateDetails details) {
|
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;
|
final scaleStart = _pinchScaleStart;
|
||||||
if (scaleStart == null || !_controller.isReady) return;
|
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) {
|
if (details.pointerCount != _pinchPointerCount) {
|
||||||
_pinchPointerCount = details.pointerCount;
|
_pinchPointerCount = details.pointerCount;
|
||||||
_pinchScaleStart = _pinchLastAppliedScale;
|
_pinchScaleStart = _pinchLastAppliedScale;
|
||||||
@@ -928,8 +963,6 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
|||||||
return;
|
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(
|
final step = softClampedPinchStep(
|
||||||
scaleStart: _pinchScaleStart!,
|
scaleStart: _pinchScaleStart!,
|
||||||
rawScaleAtBaseline: _pinchRawScaleAtBaseline,
|
rawScaleAtBaseline: _pinchRawScaleAtBaseline,
|
||||||
@@ -941,7 +974,6 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
|||||||
);
|
);
|
||||||
final focalDrop = details.focalPointDelta.distance > _kFocalGlitchPx;
|
final focalDrop = details.focalPointDelta.distance > _kFocalGlitchPx;
|
||||||
if (focalDrop) {
|
if (focalDrop) {
|
||||||
// Keep scale continuous; only skip the focal jump this frame.
|
|
||||||
if (step.reanchor) {
|
if (step.reanchor) {
|
||||||
_pinchScaleStart = step.appliedScale;
|
_pinchScaleStart = step.appliedScale;
|
||||||
_pinchRawScaleAtBaseline = details.scale;
|
_pinchRawScaleAtBaseline = details.scale;
|
||||||
@@ -953,7 +985,8 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
|||||||
currentScale: _pinchLastAppliedScale,
|
currentScale: _pinchLastAppliedScale,
|
||||||
appliedChange: 1.0,
|
appliedChange: 1.0,
|
||||||
focalJumpPx: details.focalPointDelta.distance,
|
focalJumpPx: details.focalPointDelta.distance,
|
||||||
scaleDrop: step.spiked,
|
scaleDrop: false,
|
||||||
|
softClamped: step.spiked,
|
||||||
focalDrop: true,
|
focalDrop: true,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -971,28 +1004,31 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
|||||||
duration: Duration.zero,
|
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
|
final applied = _pinchLastAppliedScale > 0
|
||||||
? appliedScale / _pinchLastAppliedScale
|
? targetScale / _pinchLastAppliedScale
|
||||||
: 1.0;
|
: 1.0;
|
||||||
InputDiagnostics.instance.recordScaleFrame(
|
InputDiagnostics.instance.recordScaleFrame(
|
||||||
rawScale: details.scale,
|
rawScale: details.scale,
|
||||||
pointerCount: details.pointerCount,
|
pointerCount: details.pointerCount,
|
||||||
currentScale: appliedScale,
|
currentScale: targetScale,
|
||||||
appliedChange: applied,
|
appliedChange: applied,
|
||||||
focalJumpPx: details.focalPointDelta.distance,
|
focalJumpPx: details.focalPointDelta.distance,
|
||||||
scaleDrop: step.spiked,
|
scaleDrop: false,
|
||||||
|
softClamped: step.spiked,
|
||||||
focalDrop: false,
|
focalDrop: false,
|
||||||
|
liveScale: _controller.currentZoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
_pinchLastAppliedScale = appliedScale;
|
_pinchLastAppliedScale = targetScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPinchEnd(ScaleEndDetails details) {
|
void _onPinchEnd(ScaleEndDetails details) {
|
||||||
|
final pending = _pendingPinchUpdate;
|
||||||
|
_pendingPinchUpdate = null;
|
||||||
|
_pinchFlushScheduled = false;
|
||||||
|
if (pending != null && _pinchScaleStart != null) {
|
||||||
|
_applyPinchUpdate(pending);
|
||||||
|
}
|
||||||
_pinchScaleStart = null;
|
_pinchScaleStart = null;
|
||||||
_pinchPointerCount = 0;
|
_pinchPointerCount = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
// because this canvas always uses an infinite boundary, free pan, and no
|
// because this canvas always uses an infinite boundary, free pan, and no
|
||||||
// rotation — so that code was provably a no-op here.
|
// rotation — so that code was provably a no-op here.
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart' show clampDouble;
|
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.
|
/// (the reported flicker). Normalizing kills that pop at the source.
|
||||||
double _rawScaleAtBaseline = 1.0;
|
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) -----------
|
// --- Matrix helpers (infinite boundary → no clamping to bounds) -----------
|
||||||
|
|
||||||
Matrix4 _matrixTranslate(Matrix4 matrix, Offset translation) {
|
Matrix4 _matrixTranslate(Matrix4 matrix, Offset translation) {
|
||||||
@@ -169,6 +176,8 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
|||||||
_scaleAnimation?.removeListener(_handleScaleAnimation);
|
_scaleAnimation?.removeListener(_handleScaleAnimation);
|
||||||
_scaleAnimation = null;
|
_scaleAnimation = null;
|
||||||
}
|
}
|
||||||
|
_pendingScaleUpdate = null;
|
||||||
|
_scaleFlushScheduled = false;
|
||||||
_gestureType = null;
|
_gestureType = null;
|
||||||
_lastPointerCount = details.pointerCount;
|
_lastPointerCount = details.pointerCount;
|
||||||
_scaleStart = _transformer.value.getMaxScaleOnAxis();
|
_scaleStart = _transformer.value.getMaxScaleOnAxis();
|
||||||
@@ -178,6 +187,27 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onScaleUpdate(ScaleUpdateDetails details) {
|
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();
|
final double scale = _transformer.value.getMaxScaleOnAxis();
|
||||||
_scaleAnimationFocalPoint = details.localFocalPoint;
|
_scaleAnimationFocalPoint = details.localFocalPoint;
|
||||||
|
|
||||||
@@ -220,14 +250,20 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
|||||||
final bool focalDrop =
|
final bool focalDrop =
|
||||||
details.pointerCount >= 2 && focalJumpPx > _kFocalGlitchPx;
|
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(
|
InputDiagnostics.instance.recordScaleFrame(
|
||||||
rawScale: details.scale,
|
rawScale: details.scale,
|
||||||
pointerCount: details.pointerCount,
|
pointerCount: details.pointerCount,
|
||||||
currentScale: scale,
|
currentScale: currentScale,
|
||||||
appliedChange: appliedChange,
|
appliedChange: appliedChange,
|
||||||
focalJumpPx: focalJumpPx,
|
focalJumpPx: focalJumpPx,
|
||||||
scaleDrop: scaleDrop,
|
scaleDrop: false,
|
||||||
|
softClamped: softClamped,
|
||||||
focalDrop: focalDropped,
|
focalDrop: focalDropped,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -252,7 +288,7 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
|||||||
_rawScaleAtBaseline = details.scale;
|
_rawScaleAtBaseline = details.scale;
|
||||||
_lastAppliedScale = step.appliedScale;
|
_lastAppliedScale = step.appliedScale;
|
||||||
}
|
}
|
||||||
record(1.0, step.spiked, true);
|
record(_lastAppliedScale, 1.0, step.spiked, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,7 +309,7 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
|||||||
final double applied =
|
final double applied =
|
||||||
_lastAppliedScale > 0 ? targetScale / _lastAppliedScale : 1.0;
|
_lastAppliedScale > 0 ? targetScale / _lastAppliedScale : 1.0;
|
||||||
_lastAppliedScale = targetScale;
|
_lastAppliedScale = targetScale;
|
||||||
record(applied, step.spiked, false);
|
record(targetScale, applied, step.spiked, false);
|
||||||
|
|
||||||
case _GestureType.pan:
|
case _GestureType.pan:
|
||||||
assert(_referenceFocalPoint != null);
|
assert(_referenceFocalPoint != null);
|
||||||
@@ -281,7 +317,7 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
|||||||
if (details.scale != 1.0) return;
|
if (details.scale != 1.0) return;
|
||||||
if (focalDrop) {
|
if (focalDrop) {
|
||||||
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
|
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
|
||||||
record(1.0, false, true);
|
record(scale, 1.0, false, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Offset translationChange =
|
final Offset translationChange =
|
||||||
@@ -289,11 +325,17 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
|||||||
_transformer.value =
|
_transformer.value =
|
||||||
_matrixTranslate(_transformer.value, translationChange);
|
_matrixTranslate(_transformer.value, translationChange);
|
||||||
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
|
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
|
||||||
record(1.0, false, false);
|
record(scale, 1.0, false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onScaleEnd(ScaleEndDetails details) {
|
void _onScaleEnd(ScaleEndDetails details) {
|
||||||
|
final pending = _pendingScaleUpdate;
|
||||||
|
_pendingScaleUpdate = null;
|
||||||
|
_scaleFlushScheduled = false;
|
||||||
|
if (pending != null && _scaleStart != null) {
|
||||||
|
_applyScaleUpdate(pending);
|
||||||
|
}
|
||||||
_scaleStart = null;
|
_scaleStart = null;
|
||||||
_referenceFocalPoint = null;
|
_referenceFocalPoint = null;
|
||||||
_lastPointerCount = 0;
|
_lastPointerCount = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user