feat(pen): extensible brush model (4 brushes)
All checks were successful
CI / Windows build (push) Successful in 14m54s
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:
@@ -26,6 +26,7 @@ import 'package:pdfrx/pdfrx.dart';
|
||||
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../../services/database_service.dart';
|
||||
import '../engine/brush.dart';
|
||||
import '../engine/stroke_eraser.dart';
|
||||
import '../engine/stroke_geometry.dart' show kDefaultPenThinning;
|
||||
import '../engine/stroke_model.dart';
|
||||
@@ -35,7 +36,7 @@ import '../input/diagnostic_logger.dart';
|
||||
import '../input/pen_config.dart';
|
||||
import '../input/pen_input_service.dart';
|
||||
import '../input/pressure_curve.dart'
|
||||
show PressureCurve, kNaturalPressureGamma, kNaturalPressureFloor;
|
||||
show PressureCurve, kNaturalPressureFloor;
|
||||
import '../pdf/pen_capture_region.dart';
|
||||
import '../persistence/editor_repository.dart';
|
||||
import '../persistence/save_scheduler.dart';
|
||||
@@ -146,6 +147,11 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
// Tool state.
|
||||
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 — TODO(brush-persist-selection)).
|
||||
BrushKind _penBrush = BrushKind.fountainPen;
|
||||
|
||||
/// When true the "select text" tool is active: pen capture is disabled so the
|
||||
/// pen falls through to pdfrx for native text selection.
|
||||
bool _selectTextMode = false;
|
||||
@@ -235,6 +241,13 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
kind: es.tool == EditorTool.highlighter
|
||||
? PenStrokeKind.highlighter
|
||||
: PenStrokeKind.pen,
|
||||
// Brush isn't persisted yet (TODO(brush-persist)); derive it
|
||||
// from the tool so a loaded highlighter still renders with the
|
||||
// highlighter brush (flat width), and pens fall back to the
|
||||
// fountainPen default.
|
||||
brush: es.tool == EditorTool.highlighter
|
||||
? BrushKind.highlighter
|
||||
: BrushKind.fountainPen,
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
@@ -358,9 +371,11 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
final raw = _rawNormalizedPressure(event);
|
||||
if (raw == null) return null;
|
||||
// PenConfig exposes gamma but not floor; use the shared natural floor (the
|
||||
// bitmap editor did the same — it never sourced floor from config).
|
||||
// bitmap editor did the same — it never sourced floor from config). The
|
||||
// gamma is the BRUSH's pressure warp (fountain p² / pencil √p / linear),
|
||||
// superseding the legacy config gamma — see TODO(brush-pressure-knob).
|
||||
const floor = kNaturalPressureFloor;
|
||||
final gamma = _penConfig?.value.pressureGamma ?? kNaturalPressureGamma;
|
||||
final gamma = brushProfileFor(_currentBrush()).pressureGamma;
|
||||
return PressureCurve(floor: floor, gamma: gamma).apply(raw);
|
||||
}
|
||||
|
||||
@@ -440,6 +455,7 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
color: _currentColor().toARGB32(),
|
||||
width: _currentStrokeWidth(),
|
||||
kind: _currentKind(),
|
||||
brush: _currentBrush(),
|
||||
);
|
||||
_bumpOverlay();
|
||||
}
|
||||
@@ -456,6 +472,7 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
color: _currentColor().toARGB32(),
|
||||
width: _currentStrokeWidth(),
|
||||
kind: _currentKind(),
|
||||
brush: _currentBrush(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -510,6 +527,12 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
? PenStrokeKind.highlighter
|
||||
: PenStrokeKind.pen;
|
||||
|
||||
/// Brush in effect: highlighter tool ⇒ highlighter brush, else the selected
|
||||
/// pen brush. Drives both the capture-time pressure warp and render geometry.
|
||||
BrushKind _currentBrush() => _tool == CanvasTool.highlighter
|
||||
? BrushKind.highlighter
|
||||
: _penBrush;
|
||||
|
||||
Color _currentColor() => _tool == CanvasTool.highlighter
|
||||
? _color.withAlpha(0x80)
|
||||
: _color;
|
||||
@@ -754,11 +777,15 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ToolButton(
|
||||
icon: Icons.edit_outlined,
|
||||
selected: _tool == CanvasTool.pen && !_selectTextMode,
|
||||
tooltip: l.toolPen,
|
||||
onPressed: () => _setTool(CanvasTool.pen),
|
||||
BrushPickerButton(
|
||||
selected: _penBrush,
|
||||
active: _tool == CanvasTool.pen && !_selectTextMode,
|
||||
tooltip: l.brushPicker,
|
||||
labelFor: (b) => brushLabel(b, l),
|
||||
onSelected: (b) {
|
||||
setState(() => _penBrush = b);
|
||||
_setTool(CanvasTool.pen);
|
||||
},
|
||||
),
|
||||
ToolButton(
|
||||
icon: Icons.brush_outlined,
|
||||
|
||||
Reference in New Issue
Block a user