Files
BadNote/lib/editor/input/diagnostic_logger.dart
Akiba So f9ec04fe86
All checks were successful
CI / Windows build (push) Successful in 12m44s
feat(diag): full input logging + read barrel from pointerFlags; focal-jump reject
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>
2026-06-23 01:10:17 +08:00

91 lines
2.9 KiB
Dart

// 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();
}
}