feat(pen): persist brush kind so it survives reload
Some checks failed
CI / Windows build (push) Has been cancelled
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:
@@ -20,19 +20,31 @@
|
||||
|
||||
import 'dart:ui' show Color, BlendMode;
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
/// The four selectable brushes. Extensible: add a kind here + a preset in
|
||||
/// [kBrushPresets]. The eraser is NOT a brush — it stays a separate tool.
|
||||
///
|
||||
/// The `@JsonValue` names are the STABLE on-disk identifiers persisted in the
|
||||
/// sidecar (`EditorStroke.brush`); they are decoupled from the Dart enum
|
||||
/// identifiers so renaming a constant here never breaks existing sidecars. A
|
||||
/// brush whose stored name is unknown (e.g. a future brush opened by an older
|
||||
/// build) is read back as [fountainPen] (see `EditorStroke.brush`'s JsonKey).
|
||||
enum BrushKind {
|
||||
/// Strong pressure→width (rnote Pow2 / quadratic), soft taper, solid ink.
|
||||
@JsonValue('fountainPen')
|
||||
fountainPen,
|
||||
|
||||
/// Near-constant thin width; pressure carries OPACITY (the ballpoint "tell").
|
||||
@JsonValue('ballpoint')
|
||||
ballpoint,
|
||||
|
||||
/// Broad, flat width, translucent, square (uncapped) ends.
|
||||
@JsonValue('highlighter')
|
||||
highlighter,
|
||||
|
||||
/// Moderate width + opacity from pressure (rnote Sqrt / √p), scratchy.
|
||||
@JsonValue('pencil')
|
||||
pencil,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -52,6 +52,13 @@ _$EditorStrokeImpl _$$EditorStrokeImplFromJson(Map<String, dynamic> json) =>
|
||||
filled: json['filled'] as bool? ?? false,
|
||||
textContent: json['textContent'] as String?,
|
||||
fontSize: (json['fontSize'] as num?)?.toDouble() ?? 14.0,
|
||||
brush:
|
||||
$enumDecodeNullable(
|
||||
_$BrushKindEnumMap,
|
||||
json['brush'],
|
||||
unknownValue: BrushKind.fountainPen,
|
||||
) ??
|
||||
BrushKind.fountainPen,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$EditorStrokeImplToJson(_$EditorStrokeImpl instance) =>
|
||||
@@ -64,6 +71,7 @@ Map<String, dynamic> _$$EditorStrokeImplToJson(_$EditorStrokeImpl instance) =>
|
||||
'filled': instance.filled,
|
||||
'textContent': instance.textContent,
|
||||
'fontSize': instance.fontSize,
|
||||
'brush': _$BrushKindEnumMap[instance.brush]!,
|
||||
};
|
||||
|
||||
const _$EditorToolEnumMap = {
|
||||
@@ -71,3 +79,10 @@ const _$EditorToolEnumMap = {
|
||||
EditorTool.highlighter: 'highlighter',
|
||||
EditorTool.eraser: 'eraser',
|
||||
};
|
||||
|
||||
const _$BrushKindEnumMap = {
|
||||
BrushKind.fountainPen: 'fountainPen',
|
||||
BrushKind.ballpoint: 'ballpoint',
|
||||
BrushKind.highlighter: 'highlighter',
|
||||
BrushKind.pencil: 'pencil',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user