fix(pen): eraser lag/stuck-red/reliability; zoom glitch-reject; native input diag
All checks were successful
CI / Windows build (push) Successful in 17m58s

Eraser (regression from the preview I added):
- LAG: the preview did setState on every hover/erase-move (rebuilding the whole
  canvas) and recomputed perfect_freehand getStroke per overlapped stroke per
  frame. Now the cursor is a ValueNotifier driving the preview layer's repaint
  directly (no canvas rebuild), and the highlight is a plain polyline of the
  point-runs inside the radius (no getStroke).
- STUCK RED ("一直红着"): the cursor was never cleared. Preview is now
  active-erase-only and cleared on pen up/cancel.
- "选中了的笔画也不见得能删掉": radius was strokeWidth*2 (tiny) so a pass removed
  ~2 points and the stroke survived. Now a decisive fixed 0.02 (page-width
  fraction). The highlight traces exactly the point-run that splitStrokeByCircle
  removes, so what turns red is what gets deleted.

Zoom: replace the per-frame scale CLAMP with glitch REJECTION — drop a frame
demanding an implausible per-frame scale jump (>1.4x or <0.71x; a real pinch is
≲1.15x/frame). A dropped frame catches up the next frame (absolute tracking), so
no lag, but the Windows multi-touch spike never shows. Pairs with the existing
pointer-count re-baseline.

Native diagnostic: ObservePenMessage now counts WM_POINTER* / PT_PEN / legacy
mouse messages it sees and emits them on the channel; PenInputService exposes
`debugSummary` and the overlay shows `native ptr=… pen=… mouse=… msg=0x…`. This
will tell us on-device whether WM_POINTER ever reaches the observer (→ buttons
recoverable) or Flutter is on a non-pointer path (→ not).

Dart: analyze clean, 66/66 tests, linux build green. Native compiles on CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 22:02:17 +08:00
parent 45d89b7790
commit ae9e070b46
5 changed files with 147 additions and 89 deletions

View File

@@ -40,9 +40,11 @@ const Set<PointerDeviceKind> _kPanZoomDevices = <PointerDeviceKind>{
PointerDeviceKind.unknown,
};
/// Per-frame multiplicative scale-change clamp (flicker guard).
const double _kMaxScaleChangePerFrame = 1.35;
const double _kMinScaleChangePerFrame = 1 / _kMaxScaleChangePerFrame;
/// A real pinch changes scale only modestly per frame (≲1.15x at 60fps). A frame
/// demanding far more than this is a Windows multi-touch position glitch, not
/// intent — that frame is dropped so the zoom can't pop and snap back.
const double _kScaleGlitchHi = 1.4;
const double _kScaleGlitchLo = 1 / _kScaleGlitchHi;
const double _kDrag = 0.0000135;
@@ -177,14 +179,14 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
case _GestureType.scale:
assert(_scaleStart != null);
final double desiredScale = _scaleStart! * details.scale;
// Flicker guard: clamp the per-frame change so a single-frame touch
// jitter can't pop the zoom and snap back. Absolute tracking means a
// real pinch just resumes next frame.
final double scaleChange = clampDouble(
desiredScale / scale,
_kMinScaleChangePerFrame,
_kMaxScaleChangePerFrame,
);
final double scaleChange = desiredScale / scale;
// Glitch rejection: drop a frame that demands an implausible per-frame
// scale jump (a Windows multi-touch position glitch). The next good
// frame resumes from the true finger positions, so the spike never
// shows — unlike clamping, which still applied a visible partial jump.
if (scaleChange > _kScaleGlitchHi || scaleChange < _kScaleGlitchLo) {
return;
}
_transformer.value = _matrixScale(_transformer.value, scaleChange);
// Keep the focal point anchored under the fingers across the scale.