All checks were successful
CI / Windows build (push) Successful in 8m19s
Wire Win32 pressure into Dart, tighten pinch guards, use geometric shape strokes, expand the ink palette, and replace scratch-link split view with an on-page sticky that shares the sidecar repo. Co-authored-by: Cursor <cursoragent@cursor.com>
202 lines
6.5 KiB
Dart
202 lines
6.5 KiB
Dart
// lib/editor/input/pen_input_service.dart
|
|
//
|
|
// Dart side of the native Windows pen observer (`windows/runner/pen_channel.cpp`).
|
|
//
|
|
// Streams barrel / eraser / tilt / PRESSURE from WM_POINTER + GetPointerPenInfo.
|
|
// Flutter's PointerEvent.pressure on Windows is unreliable (often flat); native
|
|
// pressure (0..1024 → [0,1]) is preferred when [PenHardwareState.pressureValid].
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
import '../../diagnostics/badnote_log.dart';
|
|
import '../../diagnostics/pen_event_ring.dart';
|
|
import 'diagnostic_logger.dart';
|
|
|
|
/// Latest hardware pen state delivered by the native observer.
|
|
class PenHardwareState {
|
|
const PenHardwareState({
|
|
this.barrel = false,
|
|
this.inverted = false,
|
|
this.eraser = false,
|
|
this.tiltX = 0.0,
|
|
this.tiltY = 0.0,
|
|
this.pressure = 0.0,
|
|
this.pressureValid = false,
|
|
});
|
|
|
|
final bool barrel;
|
|
final bool inverted;
|
|
final bool eraser;
|
|
final double tiltX;
|
|
final double tiltY;
|
|
|
|
/// Normalized stylus pressure in [0,1] when [pressureValid] is true.
|
|
final double pressure;
|
|
final bool pressureValid;
|
|
|
|
double get tiltMagnitude {
|
|
final t = tiltX * tiltX + tiltY * tiltY;
|
|
return t <= 0 ? 0.0 : _sqrt(t);
|
|
}
|
|
|
|
static const empty = PenHardwareState();
|
|
}
|
|
|
|
double _sqrt(double v) {
|
|
if (v <= 0) return 0;
|
|
var x = v;
|
|
var last = 0.0;
|
|
for (var i = 0; i < 12 && x != last; i++) {
|
|
last = x;
|
|
x = 0.5 * (x + v / x);
|
|
}
|
|
return x;
|
|
}
|
|
|
|
class PenInputService {
|
|
PenInputService._();
|
|
|
|
static final PenInputService instance = PenInputService._();
|
|
|
|
static const EventChannel _channel = EventChannel('badnote/pen');
|
|
|
|
StreamSubscription<dynamic>? _sub;
|
|
PenHardwareState _current = PenHardwareState.empty;
|
|
|
|
PenHardwareState get current => _current;
|
|
|
|
bool get isActive => _active;
|
|
bool _active = false;
|
|
|
|
final List<VoidCallback> _listeners = <VoidCallback>[];
|
|
|
|
/// Notify when native hardware state changes (barrel / pressure / tilt).
|
|
void addListener(VoidCallback listener) => _listeners.add(listener);
|
|
|
|
void removeListener(VoidCallback listener) => _listeners.remove(listener);
|
|
|
|
void _notifyListeners() {
|
|
for (final l in List<VoidCallback>.of(_listeners)) {
|
|
l();
|
|
}
|
|
}
|
|
|
|
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;
|
|
double _pressureMaxSeen = 0;
|
|
|
|
String _hex(int v) => '0x${v.toRadixString(16)}';
|
|
|
|
String get debugSummary => _active
|
|
? 'native ptr=$_diagPtr pen=$_diagPen mouse=$_diagMouse msg=${_hex(_diagMsg)}'
|
|
'\n orPtrFlags=${_hex(_orPtrFlags)} orPenFlags=${_hex(_orPenFlags)}'
|
|
' mask=${_hex(_orPenMask)} btnChg=$_btnChangeLast tiltMax=$_tiltAbsMax'
|
|
'\n pressure=${_current.pressureValid ? _current.pressure.toStringAsFixed(3) : "n/a"}'
|
|
' maxSeen=${_pressureMaxSeen.toStringAsFixed(3)}'
|
|
: 'native: channel silent (no events)';
|
|
|
|
void start() {
|
|
if (_sub != null) return;
|
|
try {
|
|
_sub = _channel.receiveBroadcastStream().listen(
|
|
_onEvent,
|
|
onError: (Object _) {},
|
|
cancelOnError: false,
|
|
);
|
|
} catch (_) {}
|
|
}
|
|
|
|
void _onEvent(dynamic event) {
|
|
if (event is! Map) return;
|
|
final flags = (event['flags'] as num?)?.toInt() ?? 0;
|
|
final pressureValid = ((event['pressureValid'] as num?)?.toInt() ?? 0) != 0;
|
|
final pressure = (event['pressure'] as num?)?.toDouble() ?? 0.0;
|
|
_current = PenHardwareState(
|
|
barrel: flags & 0x1 != 0,
|
|
inverted: flags & 0x2 != 0,
|
|
eraser: flags & 0x4 != 0,
|
|
tiltX: (event['tiltX'] as num?)?.toDouble() ?? 0.0,
|
|
tiltY: (event['tiltY'] as num?)?.toDouble() ?? 0.0,
|
|
pressure: pressure.clamp(0.0, 1.0),
|
|
pressureValid: pressureValid,
|
|
);
|
|
_diagPtr = (event['diagPtr'] as num?)?.toInt() ?? _diagPtr;
|
|
_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;
|
|
_pressureMaxSeen =
|
|
(event['pressureMaxSeen'] as num?)?.toDouble() ?? _pressureMaxSeen;
|
|
if (pressureValid && pressure > _pressureMaxSeen) {
|
|
_pressureMaxSeen = pressure;
|
|
}
|
|
_active = true;
|
|
|
|
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},'
|
|
'${pressureValid ? pressure.toStringAsFixed(2) : "x"}';
|
|
if (key != _lastPenLogKey) {
|
|
_lastPenLogKey = key;
|
|
PenEventRing.instance.recordHardware(
|
|
barrel: _current.barrel,
|
|
eraser: _current.eraser,
|
|
inverted: _current.inverted,
|
|
tiltX: _current.tiltX,
|
|
tiltY: _current.tiltY,
|
|
);
|
|
BadNoteLog.instance.debug(
|
|
LogSubsystem.penNative,
|
|
'pen_hw',
|
|
fields: {
|
|
'ptrFlags': '0x${rawPtr.toRadixString(16)}',
|
|
'penFlags': '0x${rawPen.toRadixString(16)}',
|
|
'mask': '0x${rawMask.toRadixString(16)}',
|
|
'btnChg': btnChange,
|
|
'tiltX': _current.tiltX,
|
|
'tiltY': _current.tiltY,
|
|
'pressure': pressureValid ? pressure : null,
|
|
'pressureValid': pressureValid,
|
|
'barrel': _current.barrel,
|
|
'eraser': _current.eraser,
|
|
'inverted': _current.inverted,
|
|
},
|
|
);
|
|
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)} '
|
|
'p=${pressureValid ? pressure.toStringAsFixed(3) : "n/a"} '
|
|
'msg=0x${_diagMsg.toRadixString(16)} resolved=0x${flags.toRadixString(16)}',
|
|
);
|
|
}
|
|
_notifyListeners();
|
|
}
|
|
|
|
String _lastPenLogKey = '';
|
|
|
|
void stop() {
|
|
_sub?.cancel();
|
|
_sub = null;
|
|
_active = false;
|
|
_current = PenHardwareState.empty;
|
|
}
|
|
}
|