feat(pen): persist brush kind so it survives reload
Some checks failed
CI / Windows build (push) Has been cancelled

Closes TODO(brush-persist). EditorStroke now serializes its brush as
the stable BrushKind name; sidecars written before this field, and any
unknown name, load as fountainPen (back-compat). PenStroke<->EditorStroke
carry brush both ways, so a ballpoint/highlighter/pencil stroke keeps
its opacity/blend after a document is closed and reopened.

Note: InkStroke (the note/scratchpad world-coord format) has no brush
field, so notes derive brush from the tool — highlighter is preserved,
ballpoint/pencil collapse to fountainPen on reload (TODO: extend
InkStroke). PDF documents persist brush fully. analyze clean,
379 tests green.
This commit is contained in:
2026-06-24 23:40:40 +08:00
parent 6c2dd71b82
commit 46589a4c87
8 changed files with 206 additions and 40 deletions

View File

@@ -13,6 +13,12 @@
// * Point x/y are NORMALIZED to the page rectangle, i.e. in [0,1].
// * Stroke `width` is a FRACTION of the page width, so it scales with zoom.
// @JsonKey is applied to freezed factory parameters (e.g. EditorStroke.brush)
// for fine-grained serialization control; freezed re-emits those annotations on
// generated getters where they're valid, so suppress the source-level
// invalid_annotation_target for the whole file (the documented freezed pattern).
// ignore_for_file: invalid_annotation_target
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:uuid/uuid.dart';
@@ -75,13 +81,17 @@ abstract class EditorStroke with _$EditorStroke {
@Default(false) bool filled,
String? textContent,
@Default(14.0) double fontSize,
// Brush the stroke was drawn with. NOT serialized (increment 1 keeps brush
// out of persistence / InkStroke round-trip — see TODO(brush-persist)); it
// is an IN-MEMORY render hint only, so the committed render path can resolve
// each stroke's perfect_freehand geometry. Defaults to fountainPen so loaded
// (deserialized) strokes keep the legacy pen visual.
// ignore: invalid_annotation_target
@JsonKey(includeFromJson: false, includeToJson: false)
// Brush the stroke was drawn with — drives the committed render path's
// perfect_freehand geometry + opacity/blend (resolveStrokePaint). Persisted
// as the stable `BrushKind` @JsonValue name (e.g. "ballpoint") so a
// ballpoint/highlighter/pencil stroke keeps its look across close/reopen.
// BACK-COMPAT: sidecars written before this field existed have no `brush`
// key, and an unknown name (a future brush opened by an older build) is
// tolerated — both fall back to fountainPen via the JsonKey below.
@JsonKey(
defaultValue: BrushKind.fountainPen,
unknownEnumValue: BrushKind.fountainPen,
)
@Default(BrushKind.fountainPen)
BrushKind brush,
}) = _EditorStroke;
@@ -154,6 +164,11 @@ abstract class EditorStroke with _$EditorStroke {
filled: stroke.filled,
textContent: stroke.textContent,
fontSize: stroke.fontSize,
// InkStroke has no brush field; derive from the tool so a loaded
// highlighter keeps the flat highlighter brush (pens → fountainPen).
brush: stroke.tool == PenTool.highlighter
? BrushKind.highlighter
: BrushKind.fountainPen,
);
/// Lossless adapter to the freezed/JSON [InkStroke] model. Null superset