// 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? _sub; PenHardwareState _current = PenHardwareState.empty; PenHardwareState get current => _current; bool get isActive => _active; bool _active = false; final List _listeners = []; /// 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.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; } }