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/canvas/input_diagnostics.dart
Normal file
90
lib/editor/canvas/input_diagnostics.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
// lib/editor/canvas/input_diagnostics.dart
|
||||
//
|
||||
// Live zoom/pan diagnostics for the pen canvas. PenInteractiveViewer records one
|
||||
// entry per scale-update frame; the editor's diagnostic overlay displays the
|
||||
// accumulated summary + a rolling trace so a SINGLE device session reveals the
|
||||
// nature of any "跳变" (is it a raw-scale spike, a focal/position jump, or a
|
||||
// pointer-count oscillation?). All numbers reset via [reset].
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../input/diagnostic_logger.dart';
|
||||
|
||||
class InputDiagnostics extends ChangeNotifier {
|
||||
InputDiagnostics._();
|
||||
static final InputDiagnostics instance = InputDiagnostics._();
|
||||
|
||||
int frames = 0;
|
||||
int scaleDropped = 0; // frames rejected as a scale glitch
|
||||
int focalDropped = 0; // frames rejected as a focal/position glitch
|
||||
int rebaselines = 0; // pointer-count re-baselines
|
||||
int pointerCountMax = 0;
|
||||
double rawScaleMin = double.infinity, rawScaleMax = 0;
|
||||
double scaleMin = double.infinity, scaleMax = 0;
|
||||
double maxFocalJumpPx = 0; // largest single-frame local focal delta
|
||||
double maxAppliedScaleJump = 1; // largest single-frame applied scale ratio
|
||||
|
||||
final List<String> _trace = <String>[];
|
||||
List<String> get trace => List.unmodifiable(_trace);
|
||||
|
||||
void recordRebaseline() {
|
||||
rebaselines++;
|
||||
DiagnosticLogger.instance.log('ZOOM rebaseline');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void recordScaleFrame({
|
||||
required double rawScale,
|
||||
required int pointerCount,
|
||||
required double currentScale,
|
||||
required double appliedChange, // 1.0 when the frame was dropped
|
||||
required double focalJumpPx,
|
||||
required bool scaleDrop,
|
||||
required bool focalDrop,
|
||||
}) {
|
||||
frames++;
|
||||
if (scaleDrop) scaleDropped++;
|
||||
if (focalDrop) focalDropped++;
|
||||
if (rawScale < rawScaleMin) rawScaleMin = rawScale;
|
||||
if (rawScale > rawScaleMax) rawScaleMax = rawScale;
|
||||
final double resulting = currentScale * appliedChange;
|
||||
if (resulting < scaleMin) scaleMin = resulting;
|
||||
if (resulting > scaleMax) scaleMax = resulting;
|
||||
if (pointerCount > pointerCountMax) pointerCountMax = pointerCount;
|
||||
if (focalJumpPx > maxFocalJumpPx) maxFocalJumpPx = focalJumpPx;
|
||||
final double jump = appliedChange >= 1 ? appliedChange : 1 / appliedChange;
|
||||
if (jump > maxAppliedScaleJump) maxAppliedScaleJump = jump;
|
||||
|
||||
final String line = 'p$pointerCount raw=${rawScale.toStringAsFixed(3)} '
|
||||
'ch=${appliedChange.toStringAsFixed(3)} '
|
||||
'cur=${currentScale.toStringAsFixed(3)} '
|
||||
'fj=${focalJumpPx.toStringAsFixed(0)}'
|
||||
'${scaleDrop ? " SDROP" : ""}${focalDrop ? " FDROP" : ""}';
|
||||
_trace.add(line);
|
||||
if (_trace.length > 24) _trace.removeAt(0);
|
||||
DiagnosticLogger.instance.log('ZOOM $line');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
frames = scaleDropped = focalDropped = rebaselines = pointerCountMax = 0;
|
||||
rawScaleMin = scaleMin = double.infinity;
|
||||
rawScaleMax = scaleMax = 0;
|
||||
maxFocalJumpPx = 0;
|
||||
maxAppliedScaleJump = 1;
|
||||
_trace.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String _f(double v) => v.isFinite ? v.toStringAsFixed(2) : '-';
|
||||
|
||||
String summary() {
|
||||
if (frames == 0) return 'zoom: (pinch to record)';
|
||||
return 'zoom f=$frames sDrop=$scaleDropped fDrop=$focalDropped '
|
||||
'rebase=$rebaselines pMax=$pointerCountMax\n'
|
||||
' raw=${_f(rawScaleMin)}..${_f(rawScaleMax)} '
|
||||
'scale=${_f(scaleMin)}..${_f(scaleMax)}\n'
|
||||
' maxFocalJump=${maxFocalJumpPx.toStringAsFixed(0)}px '
|
||||
'maxScaleJump=${_f(maxAppliedScaleJump)}';
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,14 @@ import '../../services/database_service.dart';
|
||||
import '../engine/stroke_geometry.dart' show kDefaultPenThinning;
|
||||
import '../engine/stroke_model.dart';
|
||||
import '../engine/undo_stack.dart';
|
||||
import '../input/diagnostic_logger.dart';
|
||||
import '../input/pen_config.dart';
|
||||
import '../input/pen_input_service.dart';
|
||||
import '../persistence/editor_repository.dart';
|
||||
import '../persistence/save_scheduler.dart';
|
||||
import '../ui/pen_settings_page.dart';
|
||||
import '../ui/thumbnail_grid.dart';
|
||||
import 'input_diagnostics.dart';
|
||||
import 'pen_canvas.dart';
|
||||
import 'pen_stroke.dart';
|
||||
|
||||
@@ -95,13 +97,6 @@ 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;
|
||||
@@ -126,23 +121,11 @@ 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) {
|
||||
@@ -240,10 +223,10 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
scheduler.dispose();
|
||||
}
|
||||
_document?.dispose();
|
||||
_transform.removeListener(_onTransformDebug);
|
||||
_transform.dispose();
|
||||
_penConfig?.dispose();
|
||||
PenInputService.instance.stop();
|
||||
DiagnosticLogger.instance.stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -453,18 +436,59 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.inverseSurface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 6),
|
||||
child: Text(
|
||||
'${_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,
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 380),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 6),
|
||||
child: ListenableBuilder(
|
||||
listenable: InputDiagnostics.instance,
|
||||
builder: (context, _) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final d = InputDiagnostics.instance;
|
||||
final tail = d.trace.length > 6
|
||||
? d.trace.sublist(d.trace.length - 6)
|
||||
: d.trace;
|
||||
final mono = TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 11,
|
||||
color: cs.onInverseSurface);
|
||||
final monoFaint = mono.copyWith(
|
||||
fontSize: 10,
|
||||
color:
|
||||
cs.onInverseSurface.withValues(alpha: 0.75));
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_penDebug.isEmpty
|
||||
? 'hover / draw with the pen…'
|
||||
: _penDebug,
|
||||
style: mono),
|
||||
const SizedBox(height: 4),
|
||||
Text(d.summary(), style: mono),
|
||||
if (tail.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(tail.join('\n'), style: monoFaint),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'log: ${DiagnosticLogger.instance.path ?? "(developer.log only)"}',
|
||||
style: monoFaint),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: () =>
|
||||
InputDiagnostics.instance.reset(),
|
||||
child: Text('Reset stats',
|
||||
style:
|
||||
TextStyle(color: cs.inversePrimary)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -621,15 +645,17 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
_ToolButton(
|
||||
icon: Icons.bug_report_outlined,
|
||||
selected: _showPenDebug,
|
||||
tooltip: 'Pen + zoom diagnostic',
|
||||
onPressed: () => setState(() {
|
||||
_showPenDebug = !_showPenDebug;
|
||||
if (_showPenDebug) {
|
||||
_zoomMin = double.infinity;
|
||||
_zoomMax = 0.0;
|
||||
_zoomNow = _transform.value.getMaxScaleOnAxis();
|
||||
tooltip: 'Input diagnostic (writes a log file)',
|
||||
onPressed: () {
|
||||
final on = !_showPenDebug;
|
||||
setState(() => _showPenDebug = on);
|
||||
if (on) {
|
||||
InputDiagnostics.instance.reset();
|
||||
DiagnosticLogger.instance.start();
|
||||
} else {
|
||||
DiagnosticLogger.instance.stop();
|
||||
}
|
||||
}),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -31,6 +31,8 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/physics.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'input_diagnostics.dart';
|
||||
|
||||
/// Devices allowed to pan/zoom. Stylus + invertedStylus are excluded so the pen
|
||||
/// is owned exclusively by the drawing `Listener`.
|
||||
const Set<PointerDeviceKind> _kPanZoomDevices = <PointerDeviceKind>{
|
||||
@@ -46,6 +48,11 @@ const Set<PointerDeviceKind> _kPanZoomDevices = <PointerDeviceKind>{
|
||||
const double _kScaleGlitchHi = 1.4;
|
||||
const double _kScaleGlitchLo = 1 / _kScaleGlitchHi;
|
||||
|
||||
/// During a 2-finger gesture the focal point (finger midpoint) should move
|
||||
/// smoothly. A single-frame local jump beyond this is a Windows touch misread,
|
||||
/// and the frame is dropped (position-jump guard).
|
||||
const double _kFocalGlitchPx = 250.0;
|
||||
|
||||
const double _kDrag = 0.0000135;
|
||||
|
||||
enum _GestureType { pan, scale }
|
||||
@@ -162,9 +169,11 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
||||
_lastPointerCount = details.pointerCount;
|
||||
_scaleStart = _transformer.value.getMaxScaleOnAxis();
|
||||
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
|
||||
InputDiagnostics.instance.recordRebaseline();
|
||||
return;
|
||||
}
|
||||
|
||||
final double focalJumpPx = details.focalPointDelta.distance;
|
||||
final Offset focalPointScene = _transformer.toScene(details.localFocalPoint);
|
||||
|
||||
if (_gestureType == _GestureType.pan) {
|
||||
@@ -175,16 +184,36 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
||||
}
|
||||
if (!_gestureIsSupported(_gestureType)) return;
|
||||
|
||||
// Position-jump guard: during a pinch the focal midpoint should move
|
||||
// smoothly; a big single-frame jump is a touch misread → drop the frame.
|
||||
final bool focalDrop =
|
||||
details.pointerCount >= 2 && focalJumpPx > _kFocalGlitchPx;
|
||||
|
||||
void record(double appliedChange, bool scaleDrop, bool focalDropped) {
|
||||
InputDiagnostics.instance.recordScaleFrame(
|
||||
rawScale: details.scale,
|
||||
pointerCount: details.pointerCount,
|
||||
currentScale: scale,
|
||||
appliedChange: appliedChange,
|
||||
focalJumpPx: focalJumpPx,
|
||||
scaleDrop: scaleDrop,
|
||||
focalDrop: focalDropped,
|
||||
);
|
||||
}
|
||||
|
||||
switch (_gestureType!) {
|
||||
case _GestureType.scale:
|
||||
assert(_scaleStart != null);
|
||||
final double desiredScale = _scaleStart! * details.scale;
|
||||
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) {
|
||||
// Drop a frame demanding an implausible per-frame scale jump (Windows
|
||||
// multi-touch glitch) OR an implausible focal jump. Absolute tracking
|
||||
// means the next good frame resumes from the true finger positions, so
|
||||
// the spike never shows.
|
||||
final bool scaleDrop =
|
||||
scaleChange > _kScaleGlitchHi || scaleChange < _kScaleGlitchLo;
|
||||
if (scaleDrop || focalDrop) {
|
||||
record(1.0, scaleDrop, focalDrop);
|
||||
return;
|
||||
}
|
||||
_transformer.value = _matrixScale(_transformer.value, scaleChange);
|
||||
@@ -203,16 +232,23 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
||||
if (_round(_referenceFocalPoint!) != _round(focalPointSceneCheck)) {
|
||||
_referenceFocalPoint = focalPointSceneCheck;
|
||||
}
|
||||
record(scaleChange, false, false);
|
||||
|
||||
case _GestureType.pan:
|
||||
assert(_referenceFocalPoint != null);
|
||||
// Throw away near-scale frames so a stale reference can't jump the pan.
|
||||
if (details.scale != 1.0) return;
|
||||
if (focalDrop) {
|
||||
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
|
||||
record(1.0, false, true);
|
||||
return;
|
||||
}
|
||||
final Offset translationChange =
|
||||
focalPointScene - _referenceFocalPoint!;
|
||||
_transformer.value =
|
||||
_matrixTranslate(_transformer.value, translationChange);
|
||||
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
|
||||
record(1.0, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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