Files
BadNote/lib/editor/input/pen_input_service.dart

202 lines
6.5 KiB
Dart
Raw Permalink Normal View History

feat(pen): pressure-responsive width, configurable thinning, native Windows pen (tilt/buttons) W1 — Custom pen width + pressure sensitivity (Saber-style): - Root cause of "压感没用": perfect_freehand 1.0.4 IGNORES real stylus pressure (hardcodes radius=size/2 when simulatePressure=false) — width never tracked pen force. Upgraded perfect_freehand ^1.0.0 -> ^2.0.0 (honors real pressure); migrated all 5 getStroke call sites to the 2.x API (PointVector / StrokeOptions / Offset). - De-hardcoded `thinning` into `kDefaultPenThinning` (0.85), single source shared by the on-screen painter and the PDF export path; exposed as PenConfig.pressureSensitivity with a Pressure Sensitivity slider; live-applies via a config listener. W3 — Native Windows pen plugin (tilt + barrel/eraser buttons): - windows/runner/pen_channel.{h,cpp}: observe WM_POINTER at the TOP of MessageHandler (before HandleTopLevelWindowProc, which Flutter uses to consume pen events), read GetPointerPenInfo penFlags + tilt, stream over EventChannel('badnote/pen'); non-consuming. - PenInputService: single latched hardware state (no Win32-pointerId<->event.pointer correlation); graceful no-op off-Windows. - pen_canvas maps barrel/inverted/eraser through PenConfig.sideButton/eraserEnd (eraser/undo/toggleTool/pan) and captures tilt into PenPoint.tilt -> EditorPoint.tilt. W2 — Zoom flicker: page raster isolated in its own RepaintBoundary (safe interim); definitive crisp-on-zoom fix gated on the on-device root-cause probe (plan M3). Plans: ralplan-consensus plan at docs/plans/2026-06-22-badnote-pen-polish.md (Architect APPROVE-WITH-MUST-FIX M1-M4 + Critic ITERATE->APPROVE). Tests: 58/58 pass incl. shared-thinning invariant + thinning-affects-outline + tilt-adapter round-trip. flutter analyze clean; linux debug build OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 02:10:05 +08:00
// 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].
feat(pen): pressure-responsive width, configurable thinning, native Windows pen (tilt/buttons) W1 — Custom pen width + pressure sensitivity (Saber-style): - Root cause of "压感没用": perfect_freehand 1.0.4 IGNORES real stylus pressure (hardcodes radius=size/2 when simulatePressure=false) — width never tracked pen force. Upgraded perfect_freehand ^1.0.0 -> ^2.0.0 (honors real pressure); migrated all 5 getStroke call sites to the 2.x API (PointVector / StrokeOptions / Offset). - De-hardcoded `thinning` into `kDefaultPenThinning` (0.85), single source shared by the on-screen painter and the PDF export path; exposed as PenConfig.pressureSensitivity with a Pressure Sensitivity slider; live-applies via a config listener. W3 — Native Windows pen plugin (tilt + barrel/eraser buttons): - windows/runner/pen_channel.{h,cpp}: observe WM_POINTER at the TOP of MessageHandler (before HandleTopLevelWindowProc, which Flutter uses to consume pen events), read GetPointerPenInfo penFlags + tilt, stream over EventChannel('badnote/pen'); non-consuming. - PenInputService: single latched hardware state (no Win32-pointerId<->event.pointer correlation); graceful no-op off-Windows. - pen_canvas maps barrel/inverted/eraser through PenConfig.sideButton/eraserEnd (eraser/undo/toggleTool/pan) and captures tilt into PenPoint.tilt -> EditorPoint.tilt. W2 — Zoom flicker: page raster isolated in its own RepaintBoundary (safe interim); definitive crisp-on-zoom fix gated on the on-device root-cause probe (plan M3). Plans: ralplan-consensus plan at docs/plans/2026-06-22-badnote-pen-polish.md (Architect APPROVE-WITH-MUST-FIX M1-M4 + Critic ITERATE->APPROVE). Tests: 58/58 pass incl. shared-thinning invariant + thinning-affects-outline + tilt-adapter round-trip. flutter analyze clean; linux debug build OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 02:10:05 +08:00
import 'dart:async';
import 'package:flutter/services.dart';
import '../../diagnostics/badnote_log.dart';
import '../../diagnostics/pen_event_ring.dart';
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
import 'diagnostic_logger.dart';
feat(pen): pressure-responsive width, configurable thinning, native Windows pen (tilt/buttons) W1 — Custom pen width + pressure sensitivity (Saber-style): - Root cause of "压感没用": perfect_freehand 1.0.4 IGNORES real stylus pressure (hardcodes radius=size/2 when simulatePressure=false) — width never tracked pen force. Upgraded perfect_freehand ^1.0.0 -> ^2.0.0 (honors real pressure); migrated all 5 getStroke call sites to the 2.x API (PointVector / StrokeOptions / Offset). - De-hardcoded `thinning` into `kDefaultPenThinning` (0.85), single source shared by the on-screen painter and the PDF export path; exposed as PenConfig.pressureSensitivity with a Pressure Sensitivity slider; live-applies via a config listener. W3 — Native Windows pen plugin (tilt + barrel/eraser buttons): - windows/runner/pen_channel.{h,cpp}: observe WM_POINTER at the TOP of MessageHandler (before HandleTopLevelWindowProc, which Flutter uses to consume pen events), read GetPointerPenInfo penFlags + tilt, stream over EventChannel('badnote/pen'); non-consuming. - PenInputService: single latched hardware state (no Win32-pointerId<->event.pointer correlation); graceful no-op off-Windows. - pen_canvas maps barrel/inverted/eraser through PenConfig.sideButton/eraserEnd (eraser/undo/toggleTool/pan) and captures tilt into PenPoint.tilt -> EditorPoint.tilt. W2 — Zoom flicker: page raster isolated in its own RepaintBoundary (safe interim); definitive crisp-on-zoom fix gated on the on-device root-cause probe (plan M3). Plans: ralplan-consensus plan at docs/plans/2026-06-22-badnote-pen-polish.md (Architect APPROVE-WITH-MUST-FIX M1-M4 + Critic ITERATE->APPROVE). Tests: 58/58 pass incl. shared-thinning invariant + thinning-affects-outline + tilt-adapter round-trip. flutter analyze clean; linux debug build OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 02:10:05 +08:00
/// 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,
feat(pen): pressure-responsive width, configurable thinning, native Windows pen (tilt/buttons) W1 — Custom pen width + pressure sensitivity (Saber-style): - Root cause of "压感没用": perfect_freehand 1.0.4 IGNORES real stylus pressure (hardcodes radius=size/2 when simulatePressure=false) — width never tracked pen force. Upgraded perfect_freehand ^1.0.0 -> ^2.0.0 (honors real pressure); migrated all 5 getStroke call sites to the 2.x API (PointVector / StrokeOptions / Offset). - De-hardcoded `thinning` into `kDefaultPenThinning` (0.85), single source shared by the on-screen painter and the PDF export path; exposed as PenConfig.pressureSensitivity with a Pressure Sensitivity slider; live-applies via a config listener. W3 — Native Windows pen plugin (tilt + barrel/eraser buttons): - windows/runner/pen_channel.{h,cpp}: observe WM_POINTER at the TOP of MessageHandler (before HandleTopLevelWindowProc, which Flutter uses to consume pen events), read GetPointerPenInfo penFlags + tilt, stream over EventChannel('badnote/pen'); non-consuming. - PenInputService: single latched hardware state (no Win32-pointerId<->event.pointer correlation); graceful no-op off-Windows. - pen_canvas maps barrel/inverted/eraser through PenConfig.sideButton/eraserEnd (eraser/undo/toggleTool/pan) and captures tilt into PenPoint.tilt -> EditorPoint.tilt. W2 — Zoom flicker: page raster isolated in its own RepaintBoundary (safe interim); definitive crisp-on-zoom fix gated on the on-device root-cause probe (plan M3). Plans: ralplan-consensus plan at docs/plans/2026-06-22-badnote-pen-polish.md (Architect APPROVE-WITH-MUST-FIX M1-M4 + Critic ITERATE->APPROVE). Tests: 58/58 pass incl. shared-thinning invariant + thinning-affects-outline + tilt-adapter round-trip. flutter analyze clean; linux debug build OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 02:10:05 +08:00
});
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;
feat(pen): pressure-responsive width, configurable thinning, native Windows pen (tilt/buttons) W1 — Custom pen width + pressure sensitivity (Saber-style): - Root cause of "压感没用": perfect_freehand 1.0.4 IGNORES real stylus pressure (hardcodes radius=size/2 when simulatePressure=false) — width never tracked pen force. Upgraded perfect_freehand ^1.0.0 -> ^2.0.0 (honors real pressure); migrated all 5 getStroke call sites to the 2.x API (PointVector / StrokeOptions / Offset). - De-hardcoded `thinning` into `kDefaultPenThinning` (0.85), single source shared by the on-screen painter and the PDF export path; exposed as PenConfig.pressureSensitivity with a Pressure Sensitivity slider; live-applies via a config listener. W3 — Native Windows pen plugin (tilt + barrel/eraser buttons): - windows/runner/pen_channel.{h,cpp}: observe WM_POINTER at the TOP of MessageHandler (before HandleTopLevelWindowProc, which Flutter uses to consume pen events), read GetPointerPenInfo penFlags + tilt, stream over EventChannel('badnote/pen'); non-consuming. - PenInputService: single latched hardware state (no Win32-pointerId<->event.pointer correlation); graceful no-op off-Windows. - pen_canvas maps barrel/inverted/eraser through PenConfig.sideButton/eraserEnd (eraser/undo/toggleTool/pan) and captures tilt into PenPoint.tilt -> EditorPoint.tilt. W2 — Zoom flicker: page raster isolated in its own RepaintBoundary (safe interim); definitive crisp-on-zoom fix gated on the on-device root-cause probe (plan M3). Plans: ralplan-consensus plan at docs/plans/2026-06-22-badnote-pen-polish.md (Architect APPROVE-WITH-MUST-FIX M1-M4 + Critic ITERATE->APPROVE). Tests: 58/58 pass incl. shared-thinning invariant + thinning-affects-outline + tilt-adapter round-trip. flutter analyze clean; linux debug build OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 02:10:05 +08:00
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();
}
}
fix(pen): eraser lag/stuck-red/reliability; zoom glitch-reject; native input diag Eraser (regression from the preview I added): - LAG: the preview did setState on every hover/erase-move (rebuilding the whole canvas) and recomputed perfect_freehand getStroke per overlapped stroke per frame. Now the cursor is a ValueNotifier driving the preview layer's repaint directly (no canvas rebuild), and the highlight is a plain polyline of the point-runs inside the radius (no getStroke). - STUCK RED ("一直红着"): the cursor was never cleared. Preview is now active-erase-only and cleared on pen up/cancel. - "选中了的笔画也不见得能删掉": radius was strokeWidth*2 (tiny) so a pass removed ~2 points and the stroke survived. Now a decisive fixed 0.02 (page-width fraction). The highlight traces exactly the point-run that splitStrokeByCircle removes, so what turns red is what gets deleted. Zoom: replace the per-frame scale CLAMP with glitch REJECTION — drop a frame demanding an implausible per-frame scale jump (>1.4x or <0.71x; a real pinch is ≲1.15x/frame). A dropped frame catches up the next frame (absolute tracking), so no lag, but the Windows multi-touch spike never shows. Pairs with the existing pointer-count re-baseline. Native diagnostic: ObservePenMessage now counts WM_POINTER* / PT_PEN / legacy mouse messages it sees and emits them on the channel; PenInputService exposes `debugSummary` and the overlay shows `native ptr=… pen=… mouse=… msg=0x…`. This will tell us on-device whether WM_POINTER ever reaches the observer (→ buttons recoverable) or Flutter is on a non-pointer path (→ not). 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-22 22:02:17 +08:00
int _diagPtr = 0;
int _diagPen = 0;
int _diagMouse = 0;
int _diagMsg = 0;
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
int _orPtrFlags = 0;
int _orPenFlags = 0;
int _orPenMask = 0;
int _btnChangeLast = 0;
int _tiltAbsMax = 0;
double _pressureMaxSeen = 0;
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
String _hex(int v) => '0x${v.toRadixString(16)}';
fix(pen): eraser lag/stuck-red/reliability; zoom glitch-reject; native input diag Eraser (regression from the preview I added): - LAG: the preview did setState on every hover/erase-move (rebuilding the whole canvas) and recomputed perfect_freehand getStroke per overlapped stroke per frame. Now the cursor is a ValueNotifier driving the preview layer's repaint directly (no canvas rebuild), and the highlight is a plain polyline of the point-runs inside the radius (no getStroke). - STUCK RED ("一直红着"): the cursor was never cleared. Preview is now active-erase-only and cleared on pen up/cancel. - "选中了的笔画也不见得能删掉": radius was strokeWidth*2 (tiny) so a pass removed ~2 points and the stroke survived. Now a decisive fixed 0.02 (page-width fraction). The highlight traces exactly the point-run that splitStrokeByCircle removes, so what turns red is what gets deleted. Zoom: replace the per-frame scale CLAMP with glitch REJECTION — drop a frame demanding an implausible per-frame scale jump (>1.4x or <0.71x; a real pinch is ≲1.15x/frame). A dropped frame catches up the next frame (absolute tracking), so no lag, but the Windows multi-touch spike never shows. Pairs with the existing pointer-count re-baseline. Native diagnostic: ObservePenMessage now counts WM_POINTER* / PT_PEN / legacy mouse messages it sees and emits them on the channel; PenInputService exposes `debugSummary` and the overlay shows `native ptr=… pen=… mouse=… msg=0x…`. This will tell us on-device whether WM_POINTER ever reaches the observer (→ buttons recoverable) or Flutter is on a non-pointer path (→ not). 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-22 22:02:17 +08:00
String get debugSummary => _active
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
? '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)}'
fix(pen): eraser lag/stuck-red/reliability; zoom glitch-reject; native input diag Eraser (regression from the preview I added): - LAG: the preview did setState on every hover/erase-move (rebuilding the whole canvas) and recomputed perfect_freehand getStroke per overlapped stroke per frame. Now the cursor is a ValueNotifier driving the preview layer's repaint directly (no canvas rebuild), and the highlight is a plain polyline of the point-runs inside the radius (no getStroke). - STUCK RED ("一直红着"): the cursor was never cleared. Preview is now active-erase-only and cleared on pen up/cancel. - "选中了的笔画也不见得能删掉": radius was strokeWidth*2 (tiny) so a pass removed ~2 points and the stroke survived. Now a decisive fixed 0.02 (page-width fraction). The highlight traces exactly the point-run that splitStrokeByCircle removes, so what turns red is what gets deleted. Zoom: replace the per-frame scale CLAMP with glitch REJECTION — drop a frame demanding an implausible per-frame scale jump (>1.4x or <0.71x; a real pinch is ≲1.15x/frame). A dropped frame catches up the next frame (absolute tracking), so no lag, but the Windows multi-touch spike never shows. Pairs with the existing pointer-count re-baseline. Native diagnostic: ObservePenMessage now counts WM_POINTER* / PT_PEN / legacy mouse messages it sees and emits them on the channel; PenInputService exposes `debugSummary` and the overlay shows `native ptr=… pen=… mouse=… msg=0x…`. This will tell us on-device whether WM_POINTER ever reaches the observer (→ buttons recoverable) or Flutter is on a non-pointer path (→ not). 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-22 22:02:17 +08:00
: 'native: channel silent (no events)';
feat(pen): pressure-responsive width, configurable thinning, native Windows pen (tilt/buttons) W1 — Custom pen width + pressure sensitivity (Saber-style): - Root cause of "压感没用": perfect_freehand 1.0.4 IGNORES real stylus pressure (hardcodes radius=size/2 when simulatePressure=false) — width never tracked pen force. Upgraded perfect_freehand ^1.0.0 -> ^2.0.0 (honors real pressure); migrated all 5 getStroke call sites to the 2.x API (PointVector / StrokeOptions / Offset). - De-hardcoded `thinning` into `kDefaultPenThinning` (0.85), single source shared by the on-screen painter and the PDF export path; exposed as PenConfig.pressureSensitivity with a Pressure Sensitivity slider; live-applies via a config listener. W3 — Native Windows pen plugin (tilt + barrel/eraser buttons): - windows/runner/pen_channel.{h,cpp}: observe WM_POINTER at the TOP of MessageHandler (before HandleTopLevelWindowProc, which Flutter uses to consume pen events), read GetPointerPenInfo penFlags + tilt, stream over EventChannel('badnote/pen'); non-consuming. - PenInputService: single latched hardware state (no Win32-pointerId<->event.pointer correlation); graceful no-op off-Windows. - pen_canvas maps barrel/inverted/eraser through PenConfig.sideButton/eraserEnd (eraser/undo/toggleTool/pan) and captures tilt into PenPoint.tilt -> EditorPoint.tilt. W2 — Zoom flicker: page raster isolated in its own RepaintBoundary (safe interim); definitive crisp-on-zoom fix gated on the on-device root-cause probe (plan M3). Plans: ralplan-consensus plan at docs/plans/2026-06-22-badnote-pen-polish.md (Architect APPROVE-WITH-MUST-FIX M1-M4 + Critic ITERATE->APPROVE). Tests: 58/58 pass incl. shared-thinning invariant + thinning-affects-outline + tilt-adapter round-trip. flutter analyze clean; linux debug build OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 02:10:05 +08:00
void start() {
if (_sub != null) return;
try {
_sub = _channel.receiveBroadcastStream().listen(
_onEvent,
onError: (Object _) {},
feat(pen): pressure-responsive width, configurable thinning, native Windows pen (tilt/buttons) W1 — Custom pen width + pressure sensitivity (Saber-style): - Root cause of "压感没用": perfect_freehand 1.0.4 IGNORES real stylus pressure (hardcodes radius=size/2 when simulatePressure=false) — width never tracked pen force. Upgraded perfect_freehand ^1.0.0 -> ^2.0.0 (honors real pressure); migrated all 5 getStroke call sites to the 2.x API (PointVector / StrokeOptions / Offset). - De-hardcoded `thinning` into `kDefaultPenThinning` (0.85), single source shared by the on-screen painter and the PDF export path; exposed as PenConfig.pressureSensitivity with a Pressure Sensitivity slider; live-applies via a config listener. W3 — Native Windows pen plugin (tilt + barrel/eraser buttons): - windows/runner/pen_channel.{h,cpp}: observe WM_POINTER at the TOP of MessageHandler (before HandleTopLevelWindowProc, which Flutter uses to consume pen events), read GetPointerPenInfo penFlags + tilt, stream over EventChannel('badnote/pen'); non-consuming. - PenInputService: single latched hardware state (no Win32-pointerId<->event.pointer correlation); graceful no-op off-Windows. - pen_canvas maps barrel/inverted/eraser through PenConfig.sideButton/eraserEnd (eraser/undo/toggleTool/pan) and captures tilt into PenPoint.tilt -> EditorPoint.tilt. W2 — Zoom flicker: page raster isolated in its own RepaintBoundary (safe interim); definitive crisp-on-zoom fix gated on the on-device root-cause probe (plan M3). Plans: ralplan-consensus plan at docs/plans/2026-06-22-badnote-pen-polish.md (Architect APPROVE-WITH-MUST-FIX M1-M4 + Critic ITERATE->APPROVE). Tests: 58/58 pass incl. shared-thinning invariant + thinning-affects-outline + tilt-adapter round-trip. flutter analyze clean; linux debug build OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 02:10:05 +08:00
cancelOnError: false,
);
} catch (_) {}
feat(pen): pressure-responsive width, configurable thinning, native Windows pen (tilt/buttons) W1 — Custom pen width + pressure sensitivity (Saber-style): - Root cause of "压感没用": perfect_freehand 1.0.4 IGNORES real stylus pressure (hardcodes radius=size/2 when simulatePressure=false) — width never tracked pen force. Upgraded perfect_freehand ^1.0.0 -> ^2.0.0 (honors real pressure); migrated all 5 getStroke call sites to the 2.x API (PointVector / StrokeOptions / Offset). - De-hardcoded `thinning` into `kDefaultPenThinning` (0.85), single source shared by the on-screen painter and the PDF export path; exposed as PenConfig.pressureSensitivity with a Pressure Sensitivity slider; live-applies via a config listener. W3 — Native Windows pen plugin (tilt + barrel/eraser buttons): - windows/runner/pen_channel.{h,cpp}: observe WM_POINTER at the TOP of MessageHandler (before HandleTopLevelWindowProc, which Flutter uses to consume pen events), read GetPointerPenInfo penFlags + tilt, stream over EventChannel('badnote/pen'); non-consuming. - PenInputService: single latched hardware state (no Win32-pointerId<->event.pointer correlation); graceful no-op off-Windows. - pen_canvas maps barrel/inverted/eraser through PenConfig.sideButton/eraserEnd (eraser/undo/toggleTool/pan) and captures tilt into PenPoint.tilt -> EditorPoint.tilt. W2 — Zoom flicker: page raster isolated in its own RepaintBoundary (safe interim); definitive crisp-on-zoom fix gated on the on-device root-cause probe (plan M3). Plans: ralplan-consensus plan at docs/plans/2026-06-22-badnote-pen-polish.md (Architect APPROVE-WITH-MUST-FIX M1-M4 + Critic ITERATE->APPROVE). Tests: 58/58 pass incl. shared-thinning invariant + thinning-affects-outline + tilt-adapter round-trip. flutter analyze clean; linux debug build OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 02:10:05 +08:00
}
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;
feat(pen): pressure-responsive width, configurable thinning, native Windows pen (tilt/buttons) W1 — Custom pen width + pressure sensitivity (Saber-style): - Root cause of "压感没用": perfect_freehand 1.0.4 IGNORES real stylus pressure (hardcodes radius=size/2 when simulatePressure=false) — width never tracked pen force. Upgraded perfect_freehand ^1.0.0 -> ^2.0.0 (honors real pressure); migrated all 5 getStroke call sites to the 2.x API (PointVector / StrokeOptions / Offset). - De-hardcoded `thinning` into `kDefaultPenThinning` (0.85), single source shared by the on-screen painter and the PDF export path; exposed as PenConfig.pressureSensitivity with a Pressure Sensitivity slider; live-applies via a config listener. W3 — Native Windows pen plugin (tilt + barrel/eraser buttons): - windows/runner/pen_channel.{h,cpp}: observe WM_POINTER at the TOP of MessageHandler (before HandleTopLevelWindowProc, which Flutter uses to consume pen events), read GetPointerPenInfo penFlags + tilt, stream over EventChannel('badnote/pen'); non-consuming. - PenInputService: single latched hardware state (no Win32-pointerId<->event.pointer correlation); graceful no-op off-Windows. - pen_canvas maps barrel/inverted/eraser through PenConfig.sideButton/eraserEnd (eraser/undo/toggleTool/pan) and captures tilt into PenPoint.tilt -> EditorPoint.tilt. W2 — Zoom flicker: page raster isolated in its own RepaintBoundary (safe interim); definitive crisp-on-zoom fix gated on the on-device root-cause probe (plan M3). Plans: ralplan-consensus plan at docs/plans/2026-06-22-badnote-pen-polish.md (Architect APPROVE-WITH-MUST-FIX M1-M4 + Critic ITERATE->APPROVE). Tests: 58/58 pass incl. shared-thinning invariant + thinning-affects-outline + tilt-adapter round-trip. flutter analyze clean; linux debug build OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 02:10:05 +08:00
_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,
feat(pen): pressure-responsive width, configurable thinning, native Windows pen (tilt/buttons) W1 — Custom pen width + pressure sensitivity (Saber-style): - Root cause of "压感没用": perfect_freehand 1.0.4 IGNORES real stylus pressure (hardcodes radius=size/2 when simulatePressure=false) — width never tracked pen force. Upgraded perfect_freehand ^1.0.0 -> ^2.0.0 (honors real pressure); migrated all 5 getStroke call sites to the 2.x API (PointVector / StrokeOptions / Offset). - De-hardcoded `thinning` into `kDefaultPenThinning` (0.85), single source shared by the on-screen painter and the PDF export path; exposed as PenConfig.pressureSensitivity with a Pressure Sensitivity slider; live-applies via a config listener. W3 — Native Windows pen plugin (tilt + barrel/eraser buttons): - windows/runner/pen_channel.{h,cpp}: observe WM_POINTER at the TOP of MessageHandler (before HandleTopLevelWindowProc, which Flutter uses to consume pen events), read GetPointerPenInfo penFlags + tilt, stream over EventChannel('badnote/pen'); non-consuming. - PenInputService: single latched hardware state (no Win32-pointerId<->event.pointer correlation); graceful no-op off-Windows. - pen_canvas maps barrel/inverted/eraser through PenConfig.sideButton/eraserEnd (eraser/undo/toggleTool/pan) and captures tilt into PenPoint.tilt -> EditorPoint.tilt. W2 — Zoom flicker: page raster isolated in its own RepaintBoundary (safe interim); definitive crisp-on-zoom fix gated on the on-device root-cause probe (plan M3). Plans: ralplan-consensus plan at docs/plans/2026-06-22-badnote-pen-polish.md (Architect APPROVE-WITH-MUST-FIX M1-M4 + Critic ITERATE->APPROVE). Tests: 58/58 pass incl. shared-thinning invariant + thinning-affects-outline + tilt-adapter round-trip. flutter analyze clean; linux debug build OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 02:10:05 +08:00
);
fix(pen): eraser lag/stuck-red/reliability; zoom glitch-reject; native input diag Eraser (regression from the preview I added): - LAG: the preview did setState on every hover/erase-move (rebuilding the whole canvas) and recomputed perfect_freehand getStroke per overlapped stroke per frame. Now the cursor is a ValueNotifier driving the preview layer's repaint directly (no canvas rebuild), and the highlight is a plain polyline of the point-runs inside the radius (no getStroke). - STUCK RED ("一直红着"): the cursor was never cleared. Preview is now active-erase-only and cleared on pen up/cancel. - "选中了的笔画也不见得能删掉": radius was strokeWidth*2 (tiny) so a pass removed ~2 points and the stroke survived. Now a decisive fixed 0.02 (page-width fraction). The highlight traces exactly the point-run that splitStrokeByCircle removes, so what turns red is what gets deleted. Zoom: replace the per-frame scale CLAMP with glitch REJECTION — drop a frame demanding an implausible per-frame scale jump (>1.4x or <0.71x; a real pinch is ≲1.15x/frame). A dropped frame catches up the next frame (absolute tracking), so no lag, but the Windows multi-touch spike never shows. Pairs with the existing pointer-count re-baseline. Native diagnostic: ObservePenMessage now counts WM_POINTER* / PT_PEN / legacy mouse messages it sees and emits them on the channel; PenInputService exposes `debugSummary` and the overlay shows `native ptr=… pen=… mouse=… msg=0x…`. This will tell us on-device whether WM_POINTER ever reaches the observer (→ buttons recoverable) or Flutter is on a non-pointer path (→ not). 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-22 22:02:17 +08:00
_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;
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
_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;
}
feat(pen): pressure-responsive width, configurable thinning, native Windows pen (tilt/buttons) W1 — Custom pen width + pressure sensitivity (Saber-style): - Root cause of "压感没用": perfect_freehand 1.0.4 IGNORES real stylus pressure (hardcodes radius=size/2 when simulatePressure=false) — width never tracked pen force. Upgraded perfect_freehand ^1.0.0 -> ^2.0.0 (honors real pressure); migrated all 5 getStroke call sites to the 2.x API (PointVector / StrokeOptions / Offset). - De-hardcoded `thinning` into `kDefaultPenThinning` (0.85), single source shared by the on-screen painter and the PDF export path; exposed as PenConfig.pressureSensitivity with a Pressure Sensitivity slider; live-applies via a config listener. W3 — Native Windows pen plugin (tilt + barrel/eraser buttons): - windows/runner/pen_channel.{h,cpp}: observe WM_POINTER at the TOP of MessageHandler (before HandleTopLevelWindowProc, which Flutter uses to consume pen events), read GetPointerPenInfo penFlags + tilt, stream over EventChannel('badnote/pen'); non-consuming. - PenInputService: single latched hardware state (no Win32-pointerId<->event.pointer correlation); graceful no-op off-Windows. - pen_canvas maps barrel/inverted/eraser through PenConfig.sideButton/eraserEnd (eraser/undo/toggleTool/pan) and captures tilt into PenPoint.tilt -> EditorPoint.tilt. W2 — Zoom flicker: page raster isolated in its own RepaintBoundary (safe interim); definitive crisp-on-zoom fix gated on the on-device root-cause probe (plan M3). Plans: ralplan-consensus plan at docs/plans/2026-06-22-badnote-pen-polish.md (Architect APPROVE-WITH-MUST-FIX M1-M4 + Critic ITERATE->APPROVE). Tests: 58/58 pass incl. shared-thinning invariant + thinning-affects-outline + tilt-adapter round-trip. flutter analyze clean; linux debug build OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 02:10:05 +08:00
_active = true;
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
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"}';
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
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,
},
);
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
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"} '
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
'msg=0x${_diagMsg.toRadixString(16)} resolved=0x${flags.toRadixString(16)}',
);
}
_notifyListeners();
feat(pen): pressure-responsive width, configurable thinning, native Windows pen (tilt/buttons) W1 — Custom pen width + pressure sensitivity (Saber-style): - Root cause of "压感没用": perfect_freehand 1.0.4 IGNORES real stylus pressure (hardcodes radius=size/2 when simulatePressure=false) — width never tracked pen force. Upgraded perfect_freehand ^1.0.0 -> ^2.0.0 (honors real pressure); migrated all 5 getStroke call sites to the 2.x API (PointVector / StrokeOptions / Offset). - De-hardcoded `thinning` into `kDefaultPenThinning` (0.85), single source shared by the on-screen painter and the PDF export path; exposed as PenConfig.pressureSensitivity with a Pressure Sensitivity slider; live-applies via a config listener. W3 — Native Windows pen plugin (tilt + barrel/eraser buttons): - windows/runner/pen_channel.{h,cpp}: observe WM_POINTER at the TOP of MessageHandler (before HandleTopLevelWindowProc, which Flutter uses to consume pen events), read GetPointerPenInfo penFlags + tilt, stream over EventChannel('badnote/pen'); non-consuming. - PenInputService: single latched hardware state (no Win32-pointerId<->event.pointer correlation); graceful no-op off-Windows. - pen_canvas maps barrel/inverted/eraser through PenConfig.sideButton/eraserEnd (eraser/undo/toggleTool/pan) and captures tilt into PenPoint.tilt -> EditorPoint.tilt. W2 — Zoom flicker: page raster isolated in its own RepaintBoundary (safe interim); definitive crisp-on-zoom fix gated on the on-device root-cause probe (plan M3). Plans: ralplan-consensus plan at docs/plans/2026-06-22-badnote-pen-polish.md (Architect APPROVE-WITH-MUST-FIX M1-M4 + Critic ITERATE->APPROVE). Tests: 58/58 pass incl. shared-thinning invariant + thinning-affects-outline + tilt-adapter round-trip. flutter analyze clean; linux debug build OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 02:10:05 +08:00
}
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
String _lastPenLogKey = '';
feat(pen): pressure-responsive width, configurable thinning, native Windows pen (tilt/buttons) W1 — Custom pen width + pressure sensitivity (Saber-style): - Root cause of "压感没用": perfect_freehand 1.0.4 IGNORES real stylus pressure (hardcodes radius=size/2 when simulatePressure=false) — width never tracked pen force. Upgraded perfect_freehand ^1.0.0 -> ^2.0.0 (honors real pressure); migrated all 5 getStroke call sites to the 2.x API (PointVector / StrokeOptions / Offset). - De-hardcoded `thinning` into `kDefaultPenThinning` (0.85), single source shared by the on-screen painter and the PDF export path; exposed as PenConfig.pressureSensitivity with a Pressure Sensitivity slider; live-applies via a config listener. W3 — Native Windows pen plugin (tilt + barrel/eraser buttons): - windows/runner/pen_channel.{h,cpp}: observe WM_POINTER at the TOP of MessageHandler (before HandleTopLevelWindowProc, which Flutter uses to consume pen events), read GetPointerPenInfo penFlags + tilt, stream over EventChannel('badnote/pen'); non-consuming. - PenInputService: single latched hardware state (no Win32-pointerId<->event.pointer correlation); graceful no-op off-Windows. - pen_canvas maps barrel/inverted/eraser through PenConfig.sideButton/eraserEnd (eraser/undo/toggleTool/pan) and captures tilt into PenPoint.tilt -> EditorPoint.tilt. W2 — Zoom flicker: page raster isolated in its own RepaintBoundary (safe interim); definitive crisp-on-zoom fix gated on the on-device root-cause probe (plan M3). Plans: ralplan-consensus plan at docs/plans/2026-06-22-badnote-pen-polish.md (Architect APPROVE-WITH-MUST-FIX M1-M4 + Critic ITERATE->APPROVE). Tests: 58/58 pass incl. shared-thinning invariant + thinning-affects-outline + tilt-adapter round-trip. flutter analyze clean; linux debug build OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 02:10:05 +08:00
void stop() {
_sub?.cancel();
_sub = null;
_active = false;
_current = PenHardwareState.empty;
}
}