feat(pen): extensible brush model (4 brushes)
All checks were successful
CI / Windows build (push) Successful in 14m54s

Replace the 2-tool ink system with a data-driven, Krita-style
BrushProfile (lib/editor/engine/brush.dart). Adding a brush is a
const map entry, not render-path branching.

Four presets from the rnote/krita spec:
- fountain pen: quadratic (p^2) pressure, wide dynamic width
- ballpoint:    near-constant width (thinning 0.15)
- highlighter:  flat width, square caps
- pencil:       sqrt(p) pressure, moderate width

Pressure is pre-warped per brush via PressureCurve(gamma) before
perfect_freehand; geometry fields (thinning/streamline/smoothing/
caps) flow through the shared stroke recipe so the PDF overlay and
the note/slide PenCanvas both honor the brush. Brush kind is now
persisted on the stroke model. Picker added to all three toolbars.

Opacity/multiply and pencil grain are carried as data but not yet
composited (TODO brush-opacity / brush-texture); this increment is
width + pressure-curve differentiation. analyze clean, 283 tests.
This commit is contained in:
2026-06-24 11:13:43 +08:00
parent 45a8931b64
commit 0feca74278
21 changed files with 805 additions and 66 deletions

View File

@@ -320,6 +320,36 @@ abstract class AppLocalizations {
/// **'Eraser'**
String get toolEraser;
/// No description provided for @brushPicker.
///
/// In en, this message translates to:
/// **'Brush'**
String get brushPicker;
/// No description provided for @brushFountainPen.
///
/// In en, this message translates to:
/// **'Fountain pen'**
String get brushFountainPen;
/// No description provided for @brushBallpoint.
///
/// In en, this message translates to:
/// **'Ballpoint'**
String get brushBallpoint;
/// No description provided for @brushPencil.
///
/// In en, this message translates to:
/// **'Pencil'**
String get brushPencil;
/// No description provided for @brushHighlighter.
///
/// In en, this message translates to:
/// **'Highlighter'**
String get brushHighlighter;
/// No description provided for @actionUndo.
///
/// In en, this message translates to: