Commit Graph

11 Commits

Author SHA1 Message Date
299b9546a8 feat(pen): apply pressure-response curve for natural feel
"手写笔就是一个带压感的手指,没有特殊适配" — correct. The new editor fed
RAW LINEAR stylus pressure into perfect_freehand, and the pressureGamma
config (a slider in pen-settings) was read ONLY by that slider's UI and
NEVER applied to a stroke. Dead wiring, like the rest.

Wire it for real:
- PenCanvas applies PressureCurve(floor, gamma) at capture, so stored
  pressure carries the feel and live + PDF export replay identically.
- Natural defaults: gamma 0.7 (light touches register more width, rnote/
  OneNote-like) + floor 0.12 (thin strokes keep body, not scratchy).
- pen_editor threads PenConfig.pressureGamma into the canvas — the slider
  now actually changes stroke width.
- pen_config: natural default + one-time migration of the legacy inert
  gamma 1.0, guarded by a marker so a deliberate 1.0 still sticks.

Tests: pen_config_gamma_migration (4) + pressure_curve (6) pass; pen
widget regressions green. flutter analyze: 0 issues.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:43:56 +08:00
eca5141372 feat(p0): live canvas renders via revision-gated ui.Picture cache (step 3)
Some checks failed
CI / Windows build (push) Has been cancelled
The live PenCanvas committed-ink layer now uses the relocated render/ painters
(render.StaticInkPainter + InkPictureCache + StrokeStore) instead of the old
canvas/ink_painters versions — the P0.5 perf prerequisite. The committed layer's
ui.Picture is recorded once per StrokeStore.revision and replayed on the raster
thread, so pinch / pan / live-stroke frames no longer re-rasterize committed ink.

- pen_canvas mirrors widget.strokes (PenStroke) into a StrokeStore (EditorStroke)
  on every new-list identity (the parent already replaces the list on each
  commit/erase), bumping the revision → cache invalidates → static layer repaints.
- thinning (PenConfig.pressureSensitivity) is threaded into the render painters
  AND folded into the cache key + shouldRepaint, so a sensitivity change can't
  replay a stale Picture built at the old thinning.
- live layer converts _liveStroke→EditorStroke per frame (correct: it must
  repaint every move); eraser preview keeps the existing canvas painter.
- pen_canvas disposes the InkPictureCache.

Equivalent by construction (both paths call buildStrokeOutline with the same
thinning); device confirms final fidelity. The old canvas Static/LiveInkPainter
are now orphaned (buildStrokePath still used by tests) — P1 deletes them.

flutter analyze lib/editor clean; 90/90 tests (+6: thinning repaint/cache + live).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 02:59:30 +08:00
a48c0e7e56 refactor(p0): extract pure input_arbiter from pen_canvas (step 4) + truth-table test
P0 step 4: the draw-vs-pan/zoom decision (single-pointer + device-kind + palm
rejection + hardware-pan-button suppression) is lifted verbatim out of the
PenCanvas StatefulWidget into pure functions in input/input_arbiter.dart, and
pen_canvas now delegates _shouldDraw/_isStylus to them. Behavior-identical
(same expressions), now decided by ONE unit-tested place.

Adds test/input_arbiter_test.dart pinning the full truth table: stylus/mouse
always draw, finger draws only with the toggle, >=2 pointers never draw (pinch
owns it), hardware pan button suppresses, trackpad/unknown never draw.

flutter analyze clean; 74/74 tests pass (+8). No live-path behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 01:56:13 +08:00
ae9e070b46 fix(pen): eraser lag/stuck-red/reliability; zoom glitch-reject; native input diag
All checks were successful
CI / Windows build (push) Successful in 17m58s
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
7e405453e0 feat(pen): forked InteractiveViewer (stylus-exclusive draw) + zoom diagnostic
All checks were successful
CI / Windows build (push) Successful in 18m35s
Replace stock InteractiveViewer with PenInteractiveViewer, a focused fork of
Flutter 3.44's InteractiveViewer for our config (constrained=false, infinite
boundary, no rotation — that machinery dropped as a no-op here). Two deliberate
changes, grounded in the Rnote/Saber research:

1. The pan/zoom ScaleGestureRecognizer excludes stylus/invertedStylus via
   `supportedDevices`. The pen never reaches it, so a stylus stroke can no longer
   be stolen as a pan on its first frame (the "写字识别成单击" feel bug, caused by
   stock IV's panEnabled updating a frame after the stroke began). Drawing is
   owned solely by the canvas Listener; no arena fight, no panEnabled lag. The
   prior _lastStylus hover hack is removed (superseded).

2. Per-frame scale change is clamped (×0.74..×1.35). Stock IV already damps focal
   jitter and guards the pan branch, but a single-frame multi-touch glitch could
   still spike details.scale, popping the zoom bigger/smaller and snapping back
   (the reported pinch flicker). Clamping swallows the spike; a real (gradual)
   pinch is unaffected since scale tracks absolutely from gesture start. Cap is
   far above any real pinch (~1.1-1.2x/frame), so no felt lag.

Everything else (scale-about-focal, pan, fling inertia, mouse-wheel zoom) is
Flutter's proven logic verbatim.

Also add an on-device input diagnostic (bug-report toggle): the existing pen
readout already prints kind/pressure/buttons; now it also shows live
zoom=now/min/max so the next device test captures (a) whether the side/eraser
button arrives as buttons/invertedStylus, and (b) the value any residual pinch
flash jumps to. 66/66 tests, analyze clean, linux build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:27:56 +08:00
25400c8b82 feat(pen): eraser delete-preview + pre-disable IV pan on stylus hover
All checks were successful
CI / Windows build (push) Successful in 14m2s
Eraser preview (user request, "加一个淡一点的描边"): new EraserPreviewPainter
shows the eraser circle and a faint red outline over the committed strokes the
eraser currently overlaps, so you can see what is about to be deleted. Mounted
only in eraser mode (tool or barrel/inverted signal) with a live cursor that
follows the hovering/erasing pen; shares _eraserRadius/_pageAspect with the live
erase so preview and action always agree. Kept in its own RepaintBoundary.

Pen-feel fix (the "写字识别成单击" pan-steal): panEnabled now also requires the
last pointer to not be a stylus. Windows fires stylus HOVER before contact, so
_lastStylus is already true when the pen touches down -> the InteractiveViewer's
pan is disabled BEFORE the stroke's first move, instead of one frame late. A
2+ pointer pinch still always pans (focal translation); a finger/mouse down
flips _lastStylus back so finger-pan keeps working.

Grounded in Rnote + Saber research: Saber uses the same button detection we have
(buttons==kSecondaryButton || invertedStylus); the deeper zoom-flash / draw-vs-
pan robustness wants a Saber-style forked InteractiveViewer (single recognizer,
decide-at-start) -- scoped as the next step, not done here. 66/66 tests, linux
build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:03:50 +08:00
c045fdd3ca feat(pen): partial/segment erase + fix side-button while drawing
All checks were successful
CI / Windows build (push) Successful in 16m23s
W4/P0 engine: add engine/stroke_eraser.dart (pure, aspect-corrected) with
whole-stroke `strokeHit` + partial `splitStrokeByCircle`. Grazing a long
stroke now CUTS it into surviving pieces instead of deleting it whole.
Wired through PenCanvas.onEraseStroke (now (index, replacements)) →
pen_editor_screen._eraseStroke (replaceRange); undo/persistence unchanged
(whole-page snapshot). 8 new unit tests; 66/66 pass.

Fix side-button (侧键): _isEraserSignal used `buttons == kSecondaryButton`,
but tip-down + barrel = kStylusContact|kPrimaryStylusButton = 0x03, so the
side button only registered on hover, never while drawing. Now a bitmask
test. (Eraser-end/tilt remain blocked on the silent native badnote/pen
channel — needs on-device native logging.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 12:42:26 +08:00
3295018ee3 feat(pen): pressure-responsive width, configurable thinning, native Windows pen (tilt/buttons)
All checks were successful
CI / Windows build (push) Successful in 11m34s
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
1e2a83b0b9 fix(canvas): compact page pill + pen diagnostic
All checks were successful
CI / Windows build (push) Successful in 10m45s
Page slider is no longer persistent: a compact prev/'n/total'/next pill;
tapping the label reveals the slider (collapses again), so it stops
blocking the page. Add a pen-pressure diagnostic toggle (bug icon) that
shows the live kind/pressure/min/max Windows delivers — to pin down why
pressure reads flat on the Surface Pen.
2026-06-21 23:13:05 +08:00
0c40a456e8 fix(canvas): persist strokes, pressure, slider
All checks were successful
CI / Windows build (push) Successful in 8m33s
Strokes vanished on pen-up: StaticInkPainter aliased the same mutable
list so shouldRepaint saw no change. Commit/erase now replace the list.
Finger-drawing toggle wins over palm-rejection; pressure surfaces even
when the pen reports no min/max range; pages recenter after a flip; the
keyboard page-jump (unreliable on Windows) is now a drag slider. Also
register the dynamic_color plugin in generated registrants.
2026-06-21 22:33:18 +08:00
3febbd1431 feat(editor): pen canvas + Material You UI
All checks were successful
CI / Windows build (push) Successful in 8m22s
Clean-room reimplementation of Saber's input model: we own the gesture
pipeline so pen draws with real pressure (perfect_freehand), two-finger
pinch zooms/pans, and palm is rejected (stylus-priority, 2nd-pointer
cancels stroke). pdfrx renders one page at a time (PdfPageView, no
gestures) under a shared transform; page-based nav. Material You theme
via dynamic_color (system accent + seed fallback) and a floating tonal
tool palette + page pill. Old pdfrx-overlay spike no longer wired.
2026-06-21 22:04:49 +08:00