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

@@ -303,12 +303,17 @@ mixin _$EditorStroke {
bool get filled => throw _privateConstructorUsedError;
String? get textContent => throw _privateConstructorUsedError;
double get fontSize =>
throw _privateConstructorUsedError; // 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.
@JsonKey(includeFromJson: false, includeToJson: false)
throw _privateConstructorUsedError; // 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,
)
BrushKind get brush => throw _privateConstructorUsedError;
/// Serializes this EditorStroke to a JSON map.
@@ -337,7 +342,11 @@ abstract class $EditorStrokeCopyWith<$Res> {
bool filled,
String? textContent,
double fontSize,
@JsonKey(includeFromJson: false, includeToJson: false) BrushKind brush,
@JsonKey(
defaultValue: BrushKind.fountainPen,
unknownEnumValue: BrushKind.fountainPen,
)
BrushKind brush,
});
}
@@ -428,7 +437,11 @@ abstract class _$$EditorStrokeImplCopyWith<$Res>
bool filled,
String? textContent,
double fontSize,
@JsonKey(includeFromJson: false, includeToJson: false) BrushKind brush,
@JsonKey(
defaultValue: BrushKind.fountainPen,
unknownEnumValue: BrushKind.fountainPen,
)
BrushKind brush,
});
}
@@ -511,7 +524,10 @@ class _$EditorStrokeImpl extends _EditorStroke {
this.filled = false,
this.textContent,
this.fontSize = 14.0,
@JsonKey(includeFromJson: false, includeToJson: false)
@JsonKey(
defaultValue: BrushKind.fountainPen,
unknownEnumValue: BrushKind.fountainPen,
)
this.brush = BrushKind.fountainPen,
}) : _points = points,
super._();
@@ -546,13 +562,18 @@ class _$EditorStrokeImpl extends _EditorStroke {
@override
@JsonKey()
final 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.
// 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.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
@JsonKey(
defaultValue: BrushKind.fountainPen,
unknownEnumValue: BrushKind.fountainPen,
)
final BrushKind brush;
@override
@@ -617,7 +638,10 @@ abstract class _EditorStroke extends EditorStroke {
final bool filled,
final String? textContent,
final double fontSize,
@JsonKey(includeFromJson: false, includeToJson: false)
@JsonKey(
defaultValue: BrushKind.fountainPen,
unknownEnumValue: BrushKind.fountainPen,
)
final BrushKind brush,
}) = _$EditorStrokeImpl;
_EditorStroke._() : super._();
@@ -640,13 +664,18 @@ abstract class _EditorStroke extends EditorStroke {
@override
String? get textContent;
@override
double get 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.
double get fontSize; // 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.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
@JsonKey(
defaultValue: BrushKind.fountainPen,
unknownEnumValue: BrushKind.fountainPen,
)
BrushKind get brush;
/// Create a copy of EditorStroke