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

@@ -13,6 +13,7 @@ import '../../models/ink_stroke.dart';
import '../../models/note.dart';
import '../../providers/note_provider.dart';
import '../../providers/ocr_provider.dart';
import '../engine/brush.dart';
import '../input/pen_config.dart';
import '../input/pen_input_service.dart';
import '../input/pressure_curve.dart' show kNaturalPressureGamma;
@@ -45,6 +46,12 @@ class _PenNoteScreenState extends ConsumerState<PenNoteScreen> {
final List<List<PenStroke>> _redo = [];
CanvasTool _tool = CanvasTool.pen;
/// Selected brush for the PEN tool (fountain/ballpoint/pencil). The
/// highlighter tool always uses [BrushKind.highlighter]; local state only for
/// this increment (not persisted — see TODO(brush-persist-selection)).
BrushKind _penBrush = BrushKind.fountainPen;
Color _color = Colors.black;
bool _allowFingerDrawing = false;
bool _dirty = false;
@@ -318,6 +325,7 @@ class _PenNoteScreenState extends ConsumerState<PenNoteScreen> {
strokes: _strokes,
transformationController: _transform,
tool: _tool,
brush: _penBrush,
color: _color,
strokeWidth: _strokeWidth,
pressureGamma:
@@ -359,11 +367,16 @@ class _PenNoteScreenState extends ConsumerState<PenNoteScreen> {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
ToolButton(
icon: Icons.edit_outlined,
selected: _tool == CanvasTool.pen,
tooltip: 'Pen',
onPressed: () => setState(() => _tool = CanvasTool.pen),
// Pen tool with brush picker (fountain / ballpoint / pencil).
BrushPickerButton(
selected: _penBrush,
active: _tool == CanvasTool.pen,
tooltip: 'Brush',
labelFor: brushLabelEn,
onSelected: (b) => setState(() {
_penBrush = b;
_tool = CanvasTool.pen;
}),
),
ToolButton(
icon: Icons.brush_outlined,