feat(pen): forked InteractiveViewer (stylus-exclusive draw) + zoom diagnostic
All checks were successful
CI / Windows build (push) Successful in 18m35s

Replace stock InteractiveViewer with PenInteractiveViewer, a focused fork of
Flutter 3.44's InteractiveViewer for our config (constrained=false, infinite
boundary, no rotation — that machinery dropped as a no-op here). Two deliberate
changes, grounded in the Rnote/Saber research:

1. The pan/zoom ScaleGestureRecognizer excludes stylus/invertedStylus via
   `supportedDevices`. The pen never reaches it, so a stylus stroke can no longer
   be stolen as a pan on its first frame (the "写字识别成单击" feel bug, caused by
   stock IV's panEnabled updating a frame after the stroke began). Drawing is
   owned solely by the canvas Listener; no arena fight, no panEnabled lag. The
   prior _lastStylus hover hack is removed (superseded).

2. Per-frame scale change is clamped (×0.74..×1.35). Stock IV already damps focal
   jitter and guards the pan branch, but a single-frame multi-touch glitch could
   still spike details.scale, popping the zoom bigger/smaller and snapping back
   (the reported pinch flicker). Clamping swallows the spike; a real (gradual)
   pinch is unaffected since scale tracks absolutely from gesture start. Cap is
   far above any real pinch (~1.1-1.2x/frame), so no felt lag.

Everything else (scale-about-focal, pan, fling inertia, mouse-wheel zoom) is
Flutter's proven logic verbatim.

Also add an on-device input diagnostic (bug-report toggle): the existing pen
readout already prints kind/pressure/buttons; now it also shows live
zoom=now/min/max so the next device test captures (a) whether the side/eraser
button arrives as buttons/invertedStylus, and (b) the value any residual pinch
flash jumps to. 66/66 tests, analyze clean, linux build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 20:27:56 +08:00
parent 25400c8b82
commit 7e405453e0
3 changed files with 452 additions and 33 deletions

View File

@@ -95,6 +95,13 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
String _penDebug = '';
bool _showPenDebug = false;
/// Live zoom-scale diagnostic (only tracked while the overlay is on), so we
/// can capture the value the pinch flash jumps to. `_zoomMin/_zoomMax` record
/// the extremes seen since the overlay was last enabled.
double _zoomNow = 1.0;
double _zoomMin = double.infinity;
double _zoomMax = 0.0;
// Tool state.
CanvasTool _tool = CanvasTool.pen;
Color _color = Colors.black;
@@ -119,11 +126,23 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
// Begin listening to the native Windows pen plugin (barrel/eraser/tilt).
// No-op on platforms without the plugin (W3).
PenInputService.instance.start();
_transform.addListener(_onTransformDebug);
_initPersistence();
_initPenConfig();
_open();
}
/// Track the live zoom scale for the diagnostic overlay (no-op when off).
void _onTransformDebug() {
if (!_showPenDebug) return;
final s = _transform.value.getMaxScaleOnAxis();
setState(() {
_zoomNow = s;
if (s < _zoomMin) _zoomMin = s;
if (s > _zoomMax) _zoomMax = s;
});
}
Future<void> _initPenConfig() async {
final controller = await PenConfigController.load();
if (!mounted) {
@@ -221,6 +240,7 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
scheduler.dispose();
}
_document?.dispose();
_transform.removeListener(_onTransformDebug);
_transform.dispose();
_penConfig?.dispose();
PenInputService.instance.stop();
@@ -437,9 +457,10 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
child: Text(
_penDebug.isEmpty
? 'hover / draw with the pen…'
: _penDebug,
'${_penDebug.isEmpty ? 'hover / draw with the pen…' : _penDebug}'
'\nzoom=${_zoomNow.toStringAsFixed(2)} '
'min=${_zoomMin.isFinite ? _zoomMin.toStringAsFixed(2) : '-'} '
'max=${_zoomMax.toStringAsFixed(2)}',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12,
@@ -600,8 +621,15 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
_ToolButton(
icon: Icons.bug_report_outlined,
selected: _showPenDebug,
tooltip: 'Pen pressure diagnostic',
onPressed: () => setState(() => _showPenDebug = !_showPenDebug),
tooltip: 'Pen + zoom diagnostic',
onPressed: () => setState(() {
_showPenDebug = !_showPenDebug;
if (_showPenDebug) {
_zoomMin = double.infinity;
_zoomMax = 0.0;
_zoomNow = _transform.value.getMaxScaleOnAxis();
}
}),
),
],
),