feat(diag): full input logging + read barrel from pointerFlags; focal-jump reject
All checks were successful
CI / Windows build (push) Successful in 12m44s
All checks were successful
CI / Windows build (push) Successful in 12m44s
Buttons (likely fix + ground truth): device diag showed ptr=12577 pen=10056 — WM_POINTER reaches the observer and GetPointerPenInfo succeeds, so the buttons were just read from the wrong field. Native now resolves the barrel from BOTH penFlags(PEN_FLAG_BARREL) AND pointerInfo.pointerFlags(POINTER_FLAG_SECONDBUTTON) — many pens use the latter. It also emits the full raw set (pointerFlags, penFlags, penMask, ButtonChangeType, tilt) plus OR-accumulated flags so a single session reveals exactly which field each button sets. Comprehensive logging (per user request "用好用的log库 / 我手动开启日志再记录"): new DiagnosticLogger emits through dart:developer log(name 'badnote.input') — capturable via `flutter run` / DevTools / `flutter logs` — AND mirrors to a file (path shown in the overlay) for the packaged GUI build that has no console. Manually enabled by the toolbar diagnostic toggle; off by default. PEN lines log on raw-field change; ZOOM lines log every scale frame + rebaselines. Zoom: scale-only glitch rejection didn't stop the jumping, so add focal/position glitch rejection — drop a 2-finger frame whose focal jumps >250px (a touch misread). The full per-frame trace (raw scale, pointerCount, applied change, focal jump, drops) is now logged so the residual cause is unambiguous. InputDiagnostics singleton accumulates the stats; the overlay shows summary + last trace lines + log path + reset. Removed the ad-hoc inline zoom min/max. 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:
90
lib/editor/input/diagnostic_logger.dart
Normal file
90
lib/editor/input/diagnostic_logger.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
// lib/editor/input/diagnostic_logger.dart
|
||||
//
|
||||
// On-device input diagnostics. When the user manually enables the diagnostic
|
||||
// (the toolbar toggle), every native pen event (raw button/flag/tilt fields)
|
||||
// and every zoom frame is emitted through the standard `dart:developer` log
|
||||
// channel (name 'badnote.input') — capturable via `flutter run`, DevTools, or
|
||||
// any log tool — AND mirrored to a text file as a fallback for the packaged
|
||||
// GUI build, which has no attached console. Disabled by default (no overhead).
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class DiagnosticLogger {
|
||||
DiagnosticLogger._();
|
||||
static final DiagnosticLogger instance = DiagnosticLogger._();
|
||||
|
||||
final List<String> _buffer = <String>[];
|
||||
File? _file;
|
||||
Timer? _timer;
|
||||
int _epochMs = 0;
|
||||
|
||||
bool _active = false;
|
||||
bool get isActive => _active;
|
||||
|
||||
/// Absolute path of the current log file (shown in the overlay), or null.
|
||||
String? path;
|
||||
|
||||
/// Begin a session. Enables the `dart:developer` log channel immediately and
|
||||
/// opens the fallback file (best-effort). Safe to call repeatedly.
|
||||
Future<void> start() async {
|
||||
if (_active) return;
|
||||
_active = true; // developer.log works even if the file can't be opened
|
||||
_epochMs = DateTime.now().millisecondsSinceEpoch;
|
||||
developer.log('--- session start ${DateTime.now().toIso8601String()} ---',
|
||||
name: 'badnote.input');
|
||||
try {
|
||||
Directory dir;
|
||||
try {
|
||||
dir = await getApplicationDocumentsDirectory();
|
||||
} catch (_) {
|
||||
dir = await getTemporaryDirectory();
|
||||
}
|
||||
final file = File('${dir.path}${Platform.pathSeparator}badnote_input_log.txt');
|
||||
await file.writeAsString(
|
||||
'# BadNote input diagnostic log\n'
|
||||
'# started ${DateTime.now().toIso8601String()}\n'
|
||||
'# columns: <ms> <kind> <fields...>\n',
|
||||
flush: true,
|
||||
);
|
||||
_file = file;
|
||||
path = file.path;
|
||||
_buffer.clear();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (_) => _flush());
|
||||
} catch (_) {
|
||||
// File is a fallback; never break the app over it.
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit one diagnostic line through the standard log channel and the file.
|
||||
void log(String line) {
|
||||
if (!_active) return;
|
||||
developer.log(line, name: 'badnote.input');
|
||||
if (_file == null) return;
|
||||
final t = DateTime.now().millisecondsSinceEpoch - _epochMs;
|
||||
_buffer.add('$t $line');
|
||||
if (_buffer.length >= 1000) _flush();
|
||||
}
|
||||
|
||||
Future<void> _flush() async {
|
||||
final file = _file;
|
||||
if (file == null || _buffer.isEmpty) return;
|
||||
final chunk = '${_buffer.join('\n')}\n';
|
||||
_buffer.clear();
|
||||
try {
|
||||
await file.writeAsString(chunk, mode: FileMode.append, flush: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/// Flush and stop. The file remains on disk for retrieval.
|
||||
Future<void> stop() async {
|
||||
if (!_active) return;
|
||||
_active = false;
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
await _flush();
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,8 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'diagnostic_logger.dart';
|
||||
|
||||
/// Latest hardware pen state delivered by the native observer.
|
||||
class PenHardwareState {
|
||||
const PenHardwareState({
|
||||
@@ -94,17 +96,27 @@ class PenInputService {
|
||||
bool get isActive => _active;
|
||||
bool _active = false;
|
||||
|
||||
// Native-side diagnostics (see windows/runner/pen_channel.cpp): how many
|
||||
// WM_POINTER / PT_PEN / legacy-mouse messages the observer has seen. Lets the
|
||||
// on-device overlay tell us WHICH layer is failing for buttons/tilt.
|
||||
// Native-side diagnostics (see windows/runner/pen_channel.cpp).
|
||||
int _diagPtr = 0;
|
||||
int _diagPen = 0;
|
||||
int _diagMouse = 0;
|
||||
int _diagMsg = 0;
|
||||
int _orPtrFlags = 0;
|
||||
int _orPenFlags = 0;
|
||||
int _orPenMask = 0;
|
||||
int _btnChangeLast = 0;
|
||||
int _tiltAbsMax = 0;
|
||||
|
||||
/// One-line native readout for the diagnostic overlay.
|
||||
String _hex(int v) => '0x${v.toRadixString(16)}';
|
||||
|
||||
/// Multi-line native readout for the diagnostic overlay. The OR-accumulated
|
||||
/// flag fields are the ground truth for which field carries the side/eraser
|
||||
/// button: e.g. orPtrFlags with bit 0x20 (POINTER_FLAG_SECONDBUTTON) set means
|
||||
/// the barrel button IS detectable.
|
||||
String get debugSummary => _active
|
||||
? 'native ptr=$_diagPtr pen=$_diagPen mouse=$_diagMouse msg=0x${_diagMsg.toRadixString(16)}'
|
||||
? 'native ptr=$_diagPtr pen=$_diagPen mouse=$_diagMouse msg=${_hex(_diagMsg)}'
|
||||
'\n orPtrFlags=${_hex(_orPtrFlags)} orPenFlags=${_hex(_orPenFlags)}'
|
||||
' mask=${_hex(_orPenMask)} btnChg=$_btnChangeLast tiltMax=$_tiltAbsMax'
|
||||
: 'native: channel silent (no events)';
|
||||
|
||||
/// Begins listening to the native channel. Idempotent; safe on any platform
|
||||
@@ -140,9 +152,35 @@ class PenInputService {
|
||||
_diagPen = (event['diagPen'] as num?)?.toInt() ?? _diagPen;
|
||||
_diagMouse = (event['diagMouse'] as num?)?.toInt() ?? _diagMouse;
|
||||
_diagMsg = (event['diagMsg'] as num?)?.toInt() ?? _diagMsg;
|
||||
_orPtrFlags = (event['orPtrFlags'] as num?)?.toInt() ?? _orPtrFlags;
|
||||
_orPenFlags = (event['orPenFlags'] as num?)?.toInt() ?? _orPenFlags;
|
||||
_orPenMask = (event['orPenMask'] as num?)?.toInt() ?? _orPenMask;
|
||||
_btnChangeLast = (event['btnChangeLast'] as num?)?.toInt() ?? _btnChangeLast;
|
||||
_tiltAbsMax = (event['tiltAbsMax'] as num?)?.toInt() ?? _tiltAbsMax;
|
||||
_active = true;
|
||||
|
||||
// Log a PEN line whenever the raw per-event button/flag fields change, so
|
||||
// the file captures exactly which field a button press sets (without
|
||||
// flooding on every high-rate WM_POINTERUPDATE).
|
||||
final rawPtr = (event['rawPtrFlags'] as num?)?.toInt() ?? 0;
|
||||
final rawPen = (event['rawPenFlags'] as num?)?.toInt() ?? 0;
|
||||
final rawMask = (event['rawPenMask'] as num?)?.toInt() ?? 0;
|
||||
final btnChange = (event['btnChange'] as num?)?.toInt() ?? 0;
|
||||
final key = '$rawPtr,$rawPen,$rawMask,$btnChange,${_current.tiltX},${_current.tiltY}';
|
||||
if (key != _lastPenLogKey) {
|
||||
_lastPenLogKey = key;
|
||||
DiagnosticLogger.instance.log(
|
||||
'PEN ptrFlags=0x${rawPtr.toRadixString(16)} '
|
||||
'penFlags=0x${rawPen.toRadixString(16)} '
|
||||
'mask=0x${rawMask.toRadixString(16)} btnChg=$btnChange '
|
||||
'tilt=${_current.tiltX.toStringAsFixed(0)},${_current.tiltY.toStringAsFixed(0)} '
|
||||
'msg=0x${_diagMsg.toRadixString(16)} resolved=0x${flags.toRadixString(16)}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _lastPenLogKey = '';
|
||||
|
||||
/// Stops listening and resets state.
|
||||
void stop() {
|
||||
_sub?.cancel();
|
||||
|
||||
Reference in New Issue
Block a user