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:
196
lib/editor/engine/brush.dart
Normal file
196
lib/editor/engine/brush.dart
Normal file
@@ -0,0 +1,196 @@
|
||||
// lib/editor/engine/brush.dart
|
||||
//
|
||||
// Data-driven, Krita-compatible brush model — the extensibility seam for the
|
||||
// pen engine. Each [BrushKind] maps to an immutable [BrushProfile] that fully
|
||||
// describes how a stroke is captured (pressure pre-warp) and rendered
|
||||
// (perfect_freehand geometry params + caps/taper). Adding a brush = adding one
|
||||
// const entry to [kBrushPresets]; no render-path branching.
|
||||
//
|
||||
// Mirrors Krita's sensor→curve design (Pixel brush: each property is driven by
|
||||
// a sensor through a response curve). Here the response curve is a pure power
|
||||
// law `p^gamma` applied to pressure BEFORE perfect_freehand (rnote's
|
||||
// `PressureCurve`: Pow2 = quadratic, Sqrt = √p), and the geometry knobs are
|
||||
// perfect_freehand's `thinning`/`streamline`/`smoothing`/caps. A future `.kpp`
|
||||
// (Krita brush preset) importer can produce [BrushProfile]s from the same
|
||||
// fields — see TODO(brush-kpp-import).
|
||||
//
|
||||
// Source spec: docs/research/pen-brush-spec.md §1 (rnote pressure curve) and §4
|
||||
// (per-brush perfect_freehand option tables). The numbers below are lifted from
|
||||
// that spec verbatim.
|
||||
|
||||
/// The four selectable brushes. Extensible: add a kind here + a preset in
|
||||
/// [kBrushPresets]. The eraser is NOT a brush — it stays a separate tool.
|
||||
enum BrushKind {
|
||||
/// Strong pressure→width (rnote Pow2 / quadratic), soft taper, solid ink.
|
||||
fountainPen,
|
||||
|
||||
/// Near-constant thin width; pressure carries opacity in a later increment.
|
||||
ballpoint,
|
||||
|
||||
/// Broad, flat width, translucent, square (uncapped) ends.
|
||||
highlighter,
|
||||
|
||||
/// Moderate width + (later) opacity from pressure (rnote Sqrt / √p), scratchy.
|
||||
pencil,
|
||||
}
|
||||
|
||||
/// Immutable, const description of one brush.
|
||||
///
|
||||
/// The capture path reads [pressureGamma] (the rnote power-law warp applied via
|
||||
/// `PressureCurve(gamma: pressureGamma)` BEFORE perfect_freehand) and the render
|
||||
/// path reads the perfect_freehand geometry fields ([pfThinning], [pfStreamline],
|
||||
/// [pfSmoothing], [simulatePressure]) plus the cap/taper flags.
|
||||
///
|
||||
/// [opacity] / [blendMultiply] are carried NOW for the later compositing
|
||||
/// increment but are NOT yet applied to rendered geometry — see
|
||||
/// TODO(brush-opacity) at the render sites.
|
||||
class BrushProfile {
|
||||
const BrushProfile({
|
||||
required this.kind,
|
||||
required this.baseWidthFraction,
|
||||
required this.pressureGamma,
|
||||
required this.pfThinning,
|
||||
required this.pfStreamline,
|
||||
required this.pfSmoothing,
|
||||
required this.simulatePressure,
|
||||
required this.capStart,
|
||||
required this.capEnd,
|
||||
required this.taper,
|
||||
required this.opacity,
|
||||
required this.blendMultiply,
|
||||
});
|
||||
|
||||
/// Which brush this profile is for.
|
||||
final BrushKind kind;
|
||||
|
||||
/// Suggested base stroke width as a fraction of page width (so it scales with
|
||||
/// zoom, matching `PenStroke.width`). The editors may override with their own
|
||||
/// configured pen/highlighter widths; this is the spec's nominal default
|
||||
/// (spec §4 diameters, expressed as a page-width fraction).
|
||||
final double baseWidthFraction;
|
||||
|
||||
/// rnote `PressureCurve` exponent applied to raw pressure at CAPTURE, before
|
||||
/// perfect_freehand. `2.0` = Pow2 (quadratic, fountain pen); `0.5` = Sqrt
|
||||
/// (pencil); `1.0` = Linear (ballpoint / highlighter). Fed through the
|
||||
/// existing `PressureCurve(gamma: …)` — no new pow function (spec §1).
|
||||
final double pressureGamma;
|
||||
|
||||
/// perfect_freehand `thinning`: how strongly (pre-warped) pressure modulates
|
||||
/// width. `0.0` = constant width (highlighter); high = wide dynamic range
|
||||
/// (fountain pen) (spec §4).
|
||||
final double pfThinning;
|
||||
|
||||
/// perfect_freehand `streamline`: EMA low-pass on input positions (spec §4).
|
||||
final double pfStreamline;
|
||||
|
||||
/// perfect_freehand `smoothing`: outline corner-softening (spec §4).
|
||||
final double pfSmoothing;
|
||||
|
||||
/// perfect_freehand `simulatePressure`: when true, fakes pressure from
|
||||
/// velocity. All four presets ship `false` so REAL stylus pressure (already
|
||||
/// pre-warped by [pressureGamma]) drives width (spec §4). The render path
|
||||
/// still falls back to simulation when the device reports NO usable pressure.
|
||||
final bool simulatePressure;
|
||||
|
||||
/// Round cap on the start of the stroke (false = square end, highlighter).
|
||||
final bool capStart;
|
||||
|
||||
/// Round cap on the end of the stroke (false = square end, highlighter).
|
||||
final bool capEnd;
|
||||
|
||||
/// Whether the ends taper to a point (fountain pen) (spec §4).
|
||||
final bool taper;
|
||||
|
||||
/// Per-stroke opacity hint in [0,1]. CARRIED NOW, applied in a later
|
||||
/// increment — see TODO(brush-opacity). `1.0` = solid.
|
||||
final double opacity;
|
||||
|
||||
/// Whether the brush should composite with `BlendMode.multiply` (highlighter
|
||||
/// build-up / marker feel). CARRIED NOW, applied later — TODO(brush-opacity).
|
||||
final bool blendMultiply;
|
||||
}
|
||||
|
||||
/// The 4 brush presets, populated from the spec §4 tables.
|
||||
///
|
||||
/// Widths are the spec's logical-px diameters re-expressed as page-width
|
||||
/// fractions against the project's ~1000px logical page (the existing
|
||||
/// pen/highlighter widths are 0.006 / 0.02). Fountain pen ≈ pen (0.006),
|
||||
/// highlighter ≈ 0.02 so the existing pen/highlighter visuals are PRESERVED as
|
||||
/// the fountainPen/highlighter presets (no regression).
|
||||
const Map<BrushKind, BrushProfile> kBrushPresets = {
|
||||
// Fountain pen — spec §4: size~6, thinning 0.9, smoothing 0.55,
|
||||
// streamline 0.45, simulatePressure false, taper on, pressure pre-warped to
|
||||
// p² (Pow2 / quadratic = pressureGamma 2.0). Solid ink (opacity 1.0).
|
||||
BrushKind.fountainPen: BrushProfile(
|
||||
kind: BrushKind.fountainPen,
|
||||
baseWidthFraction: 0.006,
|
||||
pressureGamma: 2.0,
|
||||
pfThinning: 0.9,
|
||||
pfStreamline: 0.45,
|
||||
pfSmoothing: 0.55,
|
||||
simulatePressure: false,
|
||||
capStart: true,
|
||||
capEnd: true,
|
||||
taper: true,
|
||||
opacity: 1.0,
|
||||
blendMultiply: false,
|
||||
),
|
||||
// Ballpoint — spec §4: size~2.2, thinning 0.15, smoothing 0.5,
|
||||
// streamline 0.55, near-constant width, linear pressure (gamma 1.0). Pressure
|
||||
// → opacity is deferred (TODO(brush-opacity)); opacity hint carried at 1.0
|
||||
// until then so geometry-only ballpoint is solid (not invisible).
|
||||
BrushKind.ballpoint: BrushProfile(
|
||||
kind: BrushKind.ballpoint,
|
||||
baseWidthFraction: 0.0022,
|
||||
pressureGamma: 1.0,
|
||||
pfThinning: 0.15,
|
||||
pfStreamline: 0.55,
|
||||
pfSmoothing: 0.5,
|
||||
simulatePressure: false,
|
||||
capStart: true,
|
||||
capEnd: true,
|
||||
taper: false,
|
||||
opacity: 1.0,
|
||||
blendMultiply: false,
|
||||
),
|
||||
// Highlighter — spec §4: size~22, thinning 0.0 (constant width),
|
||||
// smoothing 0.4, streamline 0.5, square (uncapped) ends, translucent +
|
||||
// multiply build-up. opacity 0.35 / blendMultiply true carried now; the
|
||||
// existing highlighter ships a 0x80 (50%) color alpha at capture, so the
|
||||
// 0.35 hint is NOT yet applied — see TODO(brush-opacity).
|
||||
BrushKind.highlighter: BrushProfile(
|
||||
kind: BrushKind.highlighter,
|
||||
baseWidthFraction: 0.02,
|
||||
pressureGamma: 1.0,
|
||||
pfThinning: 0.0,
|
||||
pfStreamline: 0.5,
|
||||
pfSmoothing: 0.4,
|
||||
simulatePressure: false,
|
||||
capStart: false,
|
||||
capEnd: false,
|
||||
taper: false,
|
||||
opacity: 0.35,
|
||||
blendMultiply: true,
|
||||
),
|
||||
// Pencil — spec §4: size~3, thinning 0.5, smoothing 0.5, streamline 0.4,
|
||||
// pressure pre-warped to √p (Sqrt = pressureGamma 0.5). Pressure→opacity and
|
||||
// paper grain are deferred (TODO(brush-opacity) / TODO(brush-texture));
|
||||
// opacity hint 0.9 carried now, not yet applied.
|
||||
BrushKind.pencil: BrushProfile(
|
||||
kind: BrushKind.pencil,
|
||||
baseWidthFraction: 0.003,
|
||||
pressureGamma: 0.5,
|
||||
pfThinning: 0.5,
|
||||
pfStreamline: 0.4,
|
||||
pfSmoothing: 0.5,
|
||||
simulatePressure: false,
|
||||
capStart: true,
|
||||
capEnd: true,
|
||||
taper: false,
|
||||
opacity: 0.9,
|
||||
blendMultiply: false,
|
||||
),
|
||||
};
|
||||
|
||||
/// Resolve the [BrushProfile] for [kind] (always present; const map).
|
||||
BrushProfile brushProfileFor(BrushKind kind) => kBrushPresets[kind]!;
|
||||
@@ -11,6 +11,7 @@ import 'dart:ui';
|
||||
|
||||
import 'package:perfect_freehand/perfect_freehand.dart' as pf;
|
||||
|
||||
import 'brush.dart';
|
||||
import 'stroke_model.dart';
|
||||
|
||||
/// Canonical default for perfect_freehand's `thinning` (how strongly pressure
|
||||
@@ -51,23 +52,69 @@ List<Offset> freehandOutlinePoints({
|
||||
required bool hasRealPressure,
|
||||
required bool isComplete,
|
||||
double thinning = kDefaultPenThinning,
|
||||
BrushProfile? brush,
|
||||
}) {
|
||||
if (pfPoints.isEmpty) return const <Offset>[];
|
||||
return pf.getStroke(
|
||||
pfPoints,
|
||||
options: pf.StrokeOptions(
|
||||
size: size,
|
||||
// Highlighter keeps a constant width (no thinning); pen uses the
|
||||
// configurable [thinning] so Surface-Pen pressure changes width.
|
||||
thinning: isHighlighter ? 0.0 : thinning,
|
||||
smoothing: kPenSmoothing,
|
||||
streamline: kPenStreamline,
|
||||
// Real stylus pressure -> don't simulate; no pressure -> let freehand
|
||||
// fake it based on velocity (highlighter never simulates). perfect_freehand
|
||||
// 2.x honors real pressure when simulatePressure is false.
|
||||
simulatePressure: !hasRealPressure && !isHighlighter,
|
||||
isComplete: isComplete,
|
||||
options: brush != null
|
||||
// Brush-driven path: every geometry knob (thinning / streamline /
|
||||
// smoothing / caps / taper / simulatePressure) comes from the
|
||||
// BrushProfile so each brush renders distinctly. Pressure was already
|
||||
// pre-warped by the brush's gamma at CAPTURE (PressureCurve), so the
|
||||
// pre-warp is baked into pfPoints — perfect_freehand stays linear here.
|
||||
// simulatePressure is forced true only when the device gave us NO real
|
||||
// pressure, so velocity-thinning still kicks in for mice/trackpads.
|
||||
? _optionsFromBrush(brush,
|
||||
size: size,
|
||||
isComplete: isComplete,
|
||||
hasRealPressure: hasRealPressure)
|
||||
: pf.StrokeOptions(
|
||||
size: size,
|
||||
// Highlighter keeps a constant width (no thinning); pen uses the
|
||||
// configurable [thinning] so Surface-Pen pressure changes width.
|
||||
thinning: isHighlighter ? 0.0 : thinning,
|
||||
smoothing: kPenSmoothing,
|
||||
streamline: kPenStreamline,
|
||||
// Real stylus pressure -> don't simulate; no pressure -> let
|
||||
// freehand fake it based on velocity (highlighter never simulates).
|
||||
// perfect_freehand 2.x honors real pressure when simulatePressure
|
||||
// is false.
|
||||
simulatePressure: !hasRealPressure && !isHighlighter,
|
||||
isComplete: isComplete,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build perfect_freehand [pf.StrokeOptions] from a [BrushProfile] (spec §4).
|
||||
///
|
||||
/// TODO(brush-opacity): [BrushProfile.opacity] / [BrushProfile.blendMultiply]
|
||||
/// are NOT consumed here — geometry only this increment. The caller still paints
|
||||
/// fill color (with its own alpha) and BlendMode.srcOver; per-stroke opacity /
|
||||
/// BlendMode.multiply for ballpoint/pencil/highlighter lands later.
|
||||
pf.StrokeOptions _optionsFromBrush(
|
||||
BrushProfile brush, {
|
||||
required double size,
|
||||
required bool isComplete,
|
||||
required bool hasRealPressure,
|
||||
}) {
|
||||
return pf.StrokeOptions(
|
||||
size: size,
|
||||
thinning: brush.pfThinning,
|
||||
smoothing: brush.pfSmoothing,
|
||||
streamline: brush.pfStreamline,
|
||||
// Honor REAL pressure (already gamma-pre-warped at capture). Only fall back
|
||||
// to velocity simulation when the device reported no usable pressure.
|
||||
simulatePressure: brush.simulatePressure || !hasRealPressure,
|
||||
start: pf.StrokeEndOptions.start(
|
||||
cap: brush.capStart,
|
||||
taperEnabled: brush.taper,
|
||||
),
|
||||
end: pf.StrokeEndOptions.end(
|
||||
cap: brush.capEnd,
|
||||
taperEnabled: brush.taper,
|
||||
),
|
||||
isComplete: isComplete,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -101,6 +148,11 @@ Path buildStrokeOutline(
|
||||
)
|
||||
.toList();
|
||||
|
||||
// Resolve the brush so each stroke renders with its own geometry. The
|
||||
// pressure pre-warp ([BrushProfile.pressureGamma]) was already applied at
|
||||
// capture, so it is baked into the points here.
|
||||
final brush = brushProfileFor(stroke.brush);
|
||||
|
||||
final outline = freehandOutlinePoints(
|
||||
pfPoints: pfPoints,
|
||||
size: stroke.width * pageSize.width,
|
||||
@@ -108,6 +160,7 @@ Path buildStrokeOutline(
|
||||
hasRealPressure: stroke.points.any((p) => p.pressure != null),
|
||||
isComplete: isComplete,
|
||||
thinning: thinning,
|
||||
brush: brush,
|
||||
);
|
||||
|
||||
if (outline.isEmpty) return path;
|
||||
|
||||
@@ -21,6 +21,7 @@ import '../../models/ink_stroke.dart';
|
||||
import '../../models/pen_tool.dart';
|
||||
import '../../models/pointer_device_kind.dart';
|
||||
import '../canvas/pen_stroke.dart';
|
||||
import 'brush.dart';
|
||||
|
||||
part 'stroke_model.freezed.dart';
|
||||
part 'stroke_model.g.dart';
|
||||
@@ -74,6 +75,15 @@ 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)
|
||||
@Default(BrushKind.fountainPen)
|
||||
BrushKind brush,
|
||||
}) = _EditorStroke;
|
||||
|
||||
/// Convenience constructor that generates a uuid [id] when none is supplied.
|
||||
@@ -86,6 +96,7 @@ abstract class EditorStroke with _$EditorStroke {
|
||||
bool filled = false,
|
||||
String? textContent,
|
||||
double fontSize = 14.0,
|
||||
BrushKind brush = BrushKind.fountainPen,
|
||||
}) =>
|
||||
EditorStroke(
|
||||
id: id ?? _uuid.v4(),
|
||||
@@ -96,6 +107,7 @@ abstract class EditorStroke with _$EditorStroke {
|
||||
filled: filled,
|
||||
textContent: textContent,
|
||||
fontSize: fontSize,
|
||||
brush: brush,
|
||||
);
|
||||
|
||||
factory EditorStroke.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -118,6 +130,7 @@ abstract class EditorStroke with _$EditorStroke {
|
||||
},
|
||||
color: stroke.color,
|
||||
width: stroke.width,
|
||||
brush: stroke.brush,
|
||||
);
|
||||
|
||||
/// Lossless adapter from the freezed/JSON [InkStroke] model.
|
||||
|
||||
@@ -302,7 +302,14 @@ mixin _$EditorStroke {
|
||||
double get width => throw _privateConstructorUsedError;
|
||||
bool get filled => throw _privateConstructorUsedError;
|
||||
String? get textContent => throw _privateConstructorUsedError;
|
||||
double get fontSize => 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)
|
||||
BrushKind get brush => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this EditorStroke to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@@ -330,6 +337,7 @@ abstract class $EditorStrokeCopyWith<$Res> {
|
||||
bool filled,
|
||||
String? textContent,
|
||||
double fontSize,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false) BrushKind brush,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -356,6 +364,7 @@ class _$EditorStrokeCopyWithImpl<$Res, $Val extends EditorStroke>
|
||||
Object? filled = null,
|
||||
Object? textContent = freezed,
|
||||
Object? fontSize = null,
|
||||
Object? brush = null,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
@@ -391,6 +400,10 @@ class _$EditorStrokeCopyWithImpl<$Res, $Val extends EditorStroke>
|
||||
? _value.fontSize
|
||||
: fontSize // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
brush: null == brush
|
||||
? _value.brush
|
||||
: brush // ignore: cast_nullable_to_non_nullable
|
||||
as BrushKind,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
@@ -415,6 +428,7 @@ abstract class _$$EditorStrokeImplCopyWith<$Res>
|
||||
bool filled,
|
||||
String? textContent,
|
||||
double fontSize,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false) BrushKind brush,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -440,6 +454,7 @@ class __$$EditorStrokeImplCopyWithImpl<$Res>
|
||||
Object? filled = null,
|
||||
Object? textContent = freezed,
|
||||
Object? fontSize = null,
|
||||
Object? brush = null,
|
||||
}) {
|
||||
return _then(
|
||||
_$EditorStrokeImpl(
|
||||
@@ -475,6 +490,10 @@ class __$$EditorStrokeImplCopyWithImpl<$Res>
|
||||
? _value.fontSize
|
||||
: fontSize // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
brush: null == brush
|
||||
? _value.brush
|
||||
: brush // ignore: cast_nullable_to_non_nullable
|
||||
as BrushKind,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -492,6 +511,8 @@ class _$EditorStrokeImpl extends _EditorStroke {
|
||||
this.filled = false,
|
||||
this.textContent,
|
||||
this.fontSize = 14.0,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
this.brush = BrushKind.fountainPen,
|
||||
}) : _points = points,
|
||||
super._();
|
||||
|
||||
@@ -525,10 +546,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.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
final BrushKind brush;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'EditorStroke(id: $id, points: $points, tool: $tool, color: $color, width: $width, filled: $filled, textContent: $textContent, fontSize: $fontSize)';
|
||||
return 'EditorStroke(id: $id, points: $points, tool: $tool, color: $color, width: $width, filled: $filled, textContent: $textContent, fontSize: $fontSize, brush: $brush)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -545,7 +574,8 @@ class _$EditorStrokeImpl extends _EditorStroke {
|
||||
(identical(other.textContent, textContent) ||
|
||||
other.textContent == textContent) &&
|
||||
(identical(other.fontSize, fontSize) ||
|
||||
other.fontSize == fontSize));
|
||||
other.fontSize == fontSize) &&
|
||||
(identical(other.brush, brush) || other.brush == brush));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -560,6 +590,7 @@ class _$EditorStrokeImpl extends _EditorStroke {
|
||||
filled,
|
||||
textContent,
|
||||
fontSize,
|
||||
brush,
|
||||
);
|
||||
|
||||
/// Create a copy of EditorStroke
|
||||
@@ -586,6 +617,8 @@ abstract class _EditorStroke extends EditorStroke {
|
||||
final bool filled,
|
||||
final String? textContent,
|
||||
final double fontSize,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
final BrushKind brush,
|
||||
}) = _$EditorStrokeImpl;
|
||||
_EditorStroke._() : super._();
|
||||
|
||||
@@ -607,7 +640,14 @@ abstract class _EditorStroke extends EditorStroke {
|
||||
@override
|
||||
String? get textContent;
|
||||
@override
|
||||
double get fontSize;
|
||||
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.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
BrushKind get brush;
|
||||
|
||||
/// Create a copy of EditorStroke
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
|
||||
Reference in New Issue
Block a user