Akiba So 3295018ee3
All checks were successful
CI / Windows build (push) Successful in 11m34s
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

BadNote

Local-first Surface Pen note-taking app with PDF/PPT annotation.

All notes, documents, search, and OCR run on your device. No server is required to use the app.

Features

  • Ink notes with Surface Pen (pressure, stabilizer, undo/redo)
  • PDF and PPT import with page-level annotation
  • Full-text search over note titles, typed text, and OCR results
  • Local OCR — pluggable, fully on-device. An embedded ONNX recognition backend (cross-platform, CPU/iGPU) with a graceful fallback to the platform's built-in OCR (Windows). See Local OCR.

Build (Windows)

Prerequisites:

  • Flutter SDK (3.10+)
  • Visual Studio Build Tools with Desktop development with C++
  • Developer Mode enabled (for Flutter plugin symlinks)
flutter pub get
flutter build windows --release

Output: build\windows\x64\runner\Release\badnote.exe

The sqlite3 native binary is vendored under vendor/sqlite3/ (configured via hooks.user_defines in pubspec.yaml), so the build does not download anything from GitHub — it works fully offline / behind a firewall. To add another platform or architecture, drop its official release binary from sqlite3.dart releases into vendor/sqlite3/ (the build validates each file's SHA-256).

In mainland China, point pub/Flutter at the local mirrors:

$env:PUB_HOSTED_URL="https://pub.flutter-io.cn"
$env:FLUTTER_STORAGE_BASE_URL="https://storage.flutter-io.cn"
flutter pub get
flutter build windows --release

Continuous integration

.gitea/workflows/ci.yml builds a Windows release on a self-hosted Windows runner. Format/analyze/test run but are non-blocking, so a usable .exe is produced whenever the compile itself succeeds. It is written for runners behind the GFW: the checkout action comes from the gitea.com mirror, Flutter is expected to be pre-installed on the runner, pub uses flutter-io.cn, and the sqlite3 native binary is vendored.

Self-hosted runner prerequisites

These must hold on the runner machine (they can't be set from the workflow):

  1. Flutter SDK on PATH in the runner's shell (the first build step prints flutter --version and fails fast if it isn't).
  2. Visual Studio Build Tools with Desktop development with C++ (MSVC + Windows SDK) — required to compile the Windows runner and the ONNX Runtime.
  3. The local proxy running (default http://127.0.0.1:7890) so the flutter_onnxruntime build can fetch the ONNX Runtime native lib. Override via repo secrets HTTP_PROXY / HTTPS_PROXY, or install ONNX Runtime system-wide to skip the download.
  4. gitea.com reachable (for the checkout action). If it isn't, switch to the manual-checkout fallback shown in ci.yml (it clones from your own Gitea instance), or set DEFAULT_ACTIONS_URL=https://gitea.com in the runner config and use bare actions/checkout@v4.
  5. Runner in host mode with a sane work directory — a malformed workspace path (e.g. C:\C:\...) is an act_runner config problem, not a workflow one.

Architecture

lib/
├── screens/          # UI (notes, PDF/PPT annotator, search, settings)
├── services/         # Local business logic
│   ├── database_service.dart   # SQLite + FTS5
│   ├── ocr_service.dart        # Local OCR orchestration
│   ├── stroke_rasterizer.dart  # Ink → PNG for OCR
│   ├── ocr_engine.dart         # OCR entry point (delegates to a backend)
│   └── ocr/                    # Pluggable OCR backends
│       ├── ocr_backend.dart           # Backend interface
│       ├── ocr_backends.dart          # Backend selector (ONNX → native)
│       ├── onnx_recognition_backend.dart  # Embedded ONNX (cross-platform)
│       ├── native_ocr_backend.dart    # OS OCR (Windows WinRT)
│       └── ctc_decoder.dart           # Pure-Dart CTC greedy decode
├── providers/        # Riverpod state
└── widgets/          # Ink canvas, toolbars, thumbnails

OCR flow on save:

  1. Extract typed text from text-tool strokes
  2. Rasterize handwriting strokes to PNG
  3. Recognize via the active local OCR backend (embedded ONNX if a model is bundled, otherwise the platform's native OCR)
  4. Merge recognized text into the local FTS index for search

Local OCR

OCR runs entirely on-device through a pluggable backend (lib/services/ocr/). OcrBackends selects, in order:

  1. OnnxRecognitionBackend — embedded, cross-platform recognition via flutter_onnxruntime (CPU/iGPU; suited to low-power APUs). Active only when an ONNX model is bundled.
  2. NativeOcrBackend — the OS built-in OCR (Windows WinRT today).

If no backend is available, OCR is a clean no-op — the app still works.

Enabling the embedded ONNX model

The model is not committed (it is large). Fetch it onto your dev machine before building so it bundles as an asset:

tool/fetch_ocr_model.sh   # downloads PP-OCRv4 rec ONNX + ppocr_keys_v1.txt
                          # into assets/models/ocr/  (proxy hint inside)

See assets/models/ocr/README.md. The recognition geometry / CTC-blank assumptions (PP-OCRv4 mobile rec, 3×48×W, blank=0) are documented in onnx_recognition_backend.dart and should be verified on-device against your exact exported model.

Windows build note: the flutter_onnxruntime plugin downloads the ONNX Runtime native library (v1.22.0) from GitHub at build time. Behind a firewall, set HTTPS_PROXY for the build (CMake honours it), or install ONNX Runtime system-wide and build with -DUSE_SYSTEM_ONNXRUNTIME=ON -DONNXRUNTIME_ROOT_DIR=<path>.

Optional server

The server/ directory contains an experimental FastAPI backend (sync + EasyOCR). It is not required for the desktop app and is kept separately for future multi-device sync experiments. See server/README.md.

Development

flutter run -d windows
flutter test
Description
No description provided
Readme 4.8 MiB
Languages
Dart 90.4%
Python 4.7%
C++ 2.8%
CMake 1.4%
Shell 0.3%
Other 0.2%