feat(engine): P0 stroke engine + persistence
Per the full-refactor plan §9 (input-independent half of P0): - engine: canonical EditorStroke (lossless InkStroke round-trip) + stroke_geometry (single getStroke outline) + revision-gated StrokeStore - render: static/live ink painters + ink_picture_cache (revision-keyed) + annotation_layer (RepaintBoundary) - persistence: DB v6 (ink, notebook_pages) + editor_repository diff-write (UPSERT changed / DELETE removed in one txn; id-set after commit) + save_scheduler - pdf_service export now FILLS the getStroke outline (R7 hairline fix) Not yet wired into the live editor (input relocation pending pen-pressure diagnostic). 28 new tests pass.
This commit is contained in:
68
lib/editor/engine/stroke_geometry.dart
Normal file
68
lib/editor/engine/stroke_geometry.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
// lib/editor/engine/stroke_geometry.dart
|
||||
//
|
||||
// Single source of stroke outline geometry for both screen render and export.
|
||||
// The recipe is lifted verbatim from the proven live
|
||||
// `ink_painters.buildStrokePath` (lib/editor/canvas/ink_painters.dart): points
|
||||
// are scaled from normalized page coords to pixels, perfect_freehand produces
|
||||
// the outline, and a closed fill Path is built. Keeping ONE implementation here
|
||||
// kills the hairline-export divergence (R7).
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:perfect_freehand/perfect_freehand.dart' as pf;
|
||||
|
||||
import 'stroke_model.dart';
|
||||
|
||||
/// Builds a closed, fillable outline [Path] for one [stroke], scaled into the
|
||||
/// pixel space of [pageSize] (which maps normalized [0,1] coords to pixels).
|
||||
///
|
||||
/// [isComplete] should be false for the in-progress live stroke so freehand
|
||||
/// tapers the trailing end correctly, and true for committed strokes.
|
||||
///
|
||||
/// Returns an empty [Path] when the stroke has no points (or freehand produces
|
||||
/// no outline).
|
||||
Path buildStrokeOutline(
|
||||
EditorStroke stroke,
|
||||
Size pageSize, {
|
||||
required bool isComplete,
|
||||
}) {
|
||||
final path = Path();
|
||||
if (stroke.points.isEmpty) return path;
|
||||
|
||||
final pixelWidth = stroke.width * pageSize.width;
|
||||
|
||||
final hasRealPressure = stroke.points.any((p) => p.pressure != null);
|
||||
final isHighlighter = stroke.tool == EditorTool.highlighter;
|
||||
|
||||
final pfPoints = stroke.points
|
||||
.map(
|
||||
(p) => pf.Point(
|
||||
p.x * pageSize.width,
|
||||
p.y * pageSize.height,
|
||||
p.pressure ?? 0.5,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
final outline = pf.getStroke(
|
||||
pfPoints,
|
||||
size: pixelWidth,
|
||||
// Highlighter keeps a constant width (no thinning); pen thins (0.7),
|
||||
// matching the live recipe.
|
||||
thinning: isHighlighter ? 0.0 : 0.7,
|
||||
smoothing: 0.5,
|
||||
streamline: 0.5,
|
||||
// Real stylus pressure -> don't simulate; no pressure -> let freehand fake
|
||||
// it based on velocity (highlighter never simulates).
|
||||
simulatePressure: !hasRealPressure && !isHighlighter,
|
||||
isComplete: isComplete,
|
||||
);
|
||||
|
||||
if (outline.isEmpty) return path;
|
||||
path.moveTo(outline.first.x, outline.first.y);
|
||||
for (var i = 1; i < outline.length; i++) {
|
||||
path.lineTo(outline[i].x, outline[i].y);
|
||||
}
|
||||
path.close();
|
||||
return path;
|
||||
}
|
||||
182
lib/editor/engine/stroke_model.dart
Normal file
182
lib/editor/engine/stroke_model.dart
Normal file
@@ -0,0 +1,182 @@
|
||||
// lib/editor/engine/stroke_model.dart
|
||||
//
|
||||
// Canonical, persistable stroke model for the BadNote editor engine.
|
||||
//
|
||||
// This is the single source of truth for ink strokes across the new own-canvas
|
||||
// engine (screen render + export + persistence). It is a deliberate SUPERSET of
|
||||
// both the in-memory live `PenStroke`/`PenPoint` (lib/editor/canvas/pen_stroke.dart)
|
||||
// and the freezed/JSON `InkStroke`/`InkPoint` (lib/models/ink_stroke.dart) so the
|
||||
// adapters below round-trip losslessly with `InkStroke` (SF1): `tilt`,
|
||||
// `timestamp` and `pointerDeviceKind` are preserved, never dropped.
|
||||
//
|
||||
// Coordinate semantics (matching the live conventions):
|
||||
// * 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.
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../models/ink_point.dart';
|
||||
import '../../models/ink_stroke.dart';
|
||||
import '../../models/pen_tool.dart';
|
||||
import '../../models/pointer_device_kind.dart';
|
||||
import '../canvas/pen_stroke.dart';
|
||||
|
||||
part 'stroke_model.freezed.dart';
|
||||
part 'stroke_model.g.dart';
|
||||
|
||||
const _uuid = Uuid();
|
||||
|
||||
/// The drawing tools the engine knows about. Extensible; P0 uses these three.
|
||||
enum EditorTool {
|
||||
@JsonValue('pen')
|
||||
pen,
|
||||
@JsonValue('highlighter')
|
||||
highlighter,
|
||||
@JsonValue('eraser')
|
||||
eraser,
|
||||
}
|
||||
|
||||
/// A single captured sample of a stroke.
|
||||
///
|
||||
/// [x]/[y] are normalized to the page rectangle ([0,1]). The remaining fields
|
||||
/// are a superset of [InkPoint] (nullable here so the live capture path can
|
||||
/// leave them unset, while [InkStroke] data round-trips intact through the
|
||||
/// adapters below).
|
||||
@freezed
|
||||
abstract class EditorPoint with _$EditorPoint {
|
||||
const factory EditorPoint({
|
||||
required double x,
|
||||
required double y,
|
||||
double? pressure,
|
||||
double? tilt,
|
||||
int? timestamp,
|
||||
InputDeviceKind? pointerDeviceKind,
|
||||
}) = _EditorPoint;
|
||||
|
||||
factory EditorPoint.fromJson(Map<String, dynamic> json) =>
|
||||
_$EditorPointFromJson(json);
|
||||
}
|
||||
|
||||
/// A committed stroke in normalized page coordinates.
|
||||
///
|
||||
/// [width] is a fraction of page width (matches live `PenStroke.width`).
|
||||
@freezed
|
||||
abstract class EditorStroke with _$EditorStroke {
|
||||
const EditorStroke._();
|
||||
|
||||
factory EditorStroke({
|
||||
required String id,
|
||||
required List<EditorPoint> points,
|
||||
@Default(EditorTool.pen) EditorTool tool,
|
||||
@Default(0xFF000000) int color,
|
||||
@Default(0.003) double width,
|
||||
@Default(false) bool filled,
|
||||
String? textContent,
|
||||
@Default(14.0) double fontSize,
|
||||
}) = _EditorStroke;
|
||||
|
||||
/// Convenience constructor that generates a uuid [id] when none is supplied.
|
||||
factory EditorStroke.create({
|
||||
String? id,
|
||||
required List<EditorPoint> points,
|
||||
EditorTool tool = EditorTool.pen,
|
||||
int color = 0xFF000000,
|
||||
double width = 0.003,
|
||||
bool filled = false,
|
||||
String? textContent,
|
||||
double fontSize = 14.0,
|
||||
}) =>
|
||||
EditorStroke(
|
||||
id: id ?? _uuid.v4(),
|
||||
points: points,
|
||||
tool: tool,
|
||||
color: color,
|
||||
width: width,
|
||||
filled: filled,
|
||||
textContent: textContent,
|
||||
fontSize: fontSize,
|
||||
);
|
||||
|
||||
factory EditorStroke.fromJson(Map<String, dynamic> json) =>
|
||||
_$EditorStrokeFromJson(json);
|
||||
|
||||
// ---- Adapters -----------------------------------------------------------
|
||||
|
||||
/// Adapts an in-memory live [PenStroke] (normalized, no tilt/timestamp/kind).
|
||||
factory EditorStroke.fromPenStroke(PenStroke stroke, {String? id}) =>
|
||||
EditorStroke(
|
||||
id: id ?? _uuid.v4(),
|
||||
points: stroke.points
|
||||
.map((p) => EditorPoint(x: p.x, y: p.y, pressure: p.pressure))
|
||||
.toList(),
|
||||
tool: switch (stroke.kind) {
|
||||
PenStrokeKind.pen => EditorTool.pen,
|
||||
PenStrokeKind.highlighter => EditorTool.highlighter,
|
||||
},
|
||||
color: stroke.color,
|
||||
width: stroke.width,
|
||||
);
|
||||
|
||||
/// Lossless adapter from the freezed/JSON [InkStroke] model.
|
||||
factory EditorStroke.fromInkStroke(InkStroke stroke) => EditorStroke(
|
||||
id: stroke.id,
|
||||
points: stroke.points
|
||||
.map(
|
||||
(p) => EditorPoint(
|
||||
x: p.x,
|
||||
y: p.y,
|
||||
pressure: p.pressure,
|
||||
tilt: p.tilt,
|
||||
timestamp: p.timestamp,
|
||||
pointerDeviceKind: p.pointerDeviceKind,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
tool: _toolFromPenTool(stroke.tool),
|
||||
color: stroke.color,
|
||||
width: stroke.strokeWidth,
|
||||
filled: stroke.filled,
|
||||
textContent: stroke.textContent,
|
||||
fontSize: stroke.fontSize,
|
||||
);
|
||||
|
||||
/// Lossless adapter to the freezed/JSON [InkStroke] model. Null superset
|
||||
/// fields fall back to [InkPoint]'s own defaults so the InkStroke round-trip
|
||||
/// (fromInkStroke → toInkStroke) reproduces the original exactly.
|
||||
InkStroke toInkStroke({DateTime? createdAt}) => InkStroke(
|
||||
id: id,
|
||||
points: points
|
||||
.map(
|
||||
(p) => InkPoint(
|
||||
x: p.x,
|
||||
y: p.y,
|
||||
pressure: p.pressure ?? 0.5,
|
||||
tilt: p.tilt ?? 0.0,
|
||||
timestamp: p.timestamp ?? 0,
|
||||
pointerDeviceKind:
|
||||
p.pointerDeviceKind ?? InputDeviceKind.unknown,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
tool: _toolToPenTool(tool),
|
||||
color: color,
|
||||
strokeWidth: width,
|
||||
createdAt: createdAt ?? DateTime.fromMillisecondsSinceEpoch(0),
|
||||
filled: filled,
|
||||
textContent: textContent,
|
||||
fontSize: fontSize,
|
||||
);
|
||||
|
||||
static EditorTool _toolFromPenTool(PenTool tool) => switch (tool) {
|
||||
PenTool.highlighter => EditorTool.highlighter,
|
||||
PenTool.eraser => EditorTool.eraser,
|
||||
_ => EditorTool.pen,
|
||||
};
|
||||
|
||||
static PenTool _toolToPenTool(EditorTool tool) => switch (tool) {
|
||||
EditorTool.pen => PenTool.pen,
|
||||
EditorTool.highlighter => PenTool.highlighter,
|
||||
EditorTool.eraser => PenTool.eraser,
|
||||
};
|
||||
}
|
||||
618
lib/editor/engine/stroke_model.freezed.dart
Normal file
618
lib/editor/engine/stroke_model.freezed.dart
Normal file
@@ -0,0 +1,618 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'stroke_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
|
||||
);
|
||||
|
||||
EditorPoint _$EditorPointFromJson(Map<String, dynamic> json) {
|
||||
return _EditorPoint.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$EditorPoint {
|
||||
double get x => throw _privateConstructorUsedError;
|
||||
double get y => throw _privateConstructorUsedError;
|
||||
double? get pressure => throw _privateConstructorUsedError;
|
||||
double? get tilt => throw _privateConstructorUsedError;
|
||||
int? get timestamp => throw _privateConstructorUsedError;
|
||||
InputDeviceKind? get pointerDeviceKind => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this EditorPoint to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of EditorPoint
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$EditorPointCopyWith<EditorPoint> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $EditorPointCopyWith<$Res> {
|
||||
factory $EditorPointCopyWith(
|
||||
EditorPoint value,
|
||||
$Res Function(EditorPoint) then,
|
||||
) = _$EditorPointCopyWithImpl<$Res, EditorPoint>;
|
||||
@useResult
|
||||
$Res call({
|
||||
double x,
|
||||
double y,
|
||||
double? pressure,
|
||||
double? tilt,
|
||||
int? timestamp,
|
||||
InputDeviceKind? pointerDeviceKind,
|
||||
});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$EditorPointCopyWithImpl<$Res, $Val extends EditorPoint>
|
||||
implements $EditorPointCopyWith<$Res> {
|
||||
_$EditorPointCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of EditorPoint
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? x = null,
|
||||
Object? y = null,
|
||||
Object? pressure = freezed,
|
||||
Object? tilt = freezed,
|
||||
Object? timestamp = freezed,
|
||||
Object? pointerDeviceKind = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
x: null == x
|
||||
? _value.x
|
||||
: x // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
y: null == y
|
||||
? _value.y
|
||||
: y // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
pressure: freezed == pressure
|
||||
? _value.pressure
|
||||
: pressure // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
tilt: freezed == tilt
|
||||
? _value.tilt
|
||||
: tilt // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
timestamp: freezed == timestamp
|
||||
? _value.timestamp
|
||||
: timestamp // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
pointerDeviceKind: freezed == pointerDeviceKind
|
||||
? _value.pointerDeviceKind
|
||||
: pointerDeviceKind // ignore: cast_nullable_to_non_nullable
|
||||
as InputDeviceKind?,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$EditorPointImplCopyWith<$Res>
|
||||
implements $EditorPointCopyWith<$Res> {
|
||||
factory _$$EditorPointImplCopyWith(
|
||||
_$EditorPointImpl value,
|
||||
$Res Function(_$EditorPointImpl) then,
|
||||
) = __$$EditorPointImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({
|
||||
double x,
|
||||
double y,
|
||||
double? pressure,
|
||||
double? tilt,
|
||||
int? timestamp,
|
||||
InputDeviceKind? pointerDeviceKind,
|
||||
});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$EditorPointImplCopyWithImpl<$Res>
|
||||
extends _$EditorPointCopyWithImpl<$Res, _$EditorPointImpl>
|
||||
implements _$$EditorPointImplCopyWith<$Res> {
|
||||
__$$EditorPointImplCopyWithImpl(
|
||||
_$EditorPointImpl _value,
|
||||
$Res Function(_$EditorPointImpl) _then,
|
||||
) : super(_value, _then);
|
||||
|
||||
/// Create a copy of EditorPoint
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? x = null,
|
||||
Object? y = null,
|
||||
Object? pressure = freezed,
|
||||
Object? tilt = freezed,
|
||||
Object? timestamp = freezed,
|
||||
Object? pointerDeviceKind = freezed,
|
||||
}) {
|
||||
return _then(
|
||||
_$EditorPointImpl(
|
||||
x: null == x
|
||||
? _value.x
|
||||
: x // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
y: null == y
|
||||
? _value.y
|
||||
: y // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
pressure: freezed == pressure
|
||||
? _value.pressure
|
||||
: pressure // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
tilt: freezed == tilt
|
||||
? _value.tilt
|
||||
: tilt // ignore: cast_nullable_to_non_nullable
|
||||
as double?,
|
||||
timestamp: freezed == timestamp
|
||||
? _value.timestamp
|
||||
: timestamp // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
pointerDeviceKind: freezed == pointerDeviceKind
|
||||
? _value.pointerDeviceKind
|
||||
: pointerDeviceKind // ignore: cast_nullable_to_non_nullable
|
||||
as InputDeviceKind?,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$EditorPointImpl implements _EditorPoint {
|
||||
const _$EditorPointImpl({
|
||||
required this.x,
|
||||
required this.y,
|
||||
this.pressure,
|
||||
this.tilt,
|
||||
this.timestamp,
|
||||
this.pointerDeviceKind,
|
||||
});
|
||||
|
||||
factory _$EditorPointImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$EditorPointImplFromJson(json);
|
||||
|
||||
@override
|
||||
final double x;
|
||||
@override
|
||||
final double y;
|
||||
@override
|
||||
final double? pressure;
|
||||
@override
|
||||
final double? tilt;
|
||||
@override
|
||||
final int? timestamp;
|
||||
@override
|
||||
final InputDeviceKind? pointerDeviceKind;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'EditorPoint(x: $x, y: $y, pressure: $pressure, tilt: $tilt, timestamp: $timestamp, pointerDeviceKind: $pointerDeviceKind)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$EditorPointImpl &&
|
||||
(identical(other.x, x) || other.x == x) &&
|
||||
(identical(other.y, y) || other.y == y) &&
|
||||
(identical(other.pressure, pressure) ||
|
||||
other.pressure == pressure) &&
|
||||
(identical(other.tilt, tilt) || other.tilt == tilt) &&
|
||||
(identical(other.timestamp, timestamp) ||
|
||||
other.timestamp == timestamp) &&
|
||||
(identical(other.pointerDeviceKind, pointerDeviceKind) ||
|
||||
other.pointerDeviceKind == pointerDeviceKind));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
x,
|
||||
y,
|
||||
pressure,
|
||||
tilt,
|
||||
timestamp,
|
||||
pointerDeviceKind,
|
||||
);
|
||||
|
||||
/// Create a copy of EditorPoint
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$EditorPointImplCopyWith<_$EditorPointImpl> get copyWith =>
|
||||
__$$EditorPointImplCopyWithImpl<_$EditorPointImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$EditorPointImplToJson(this);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _EditorPoint implements EditorPoint {
|
||||
const factory _EditorPoint({
|
||||
required final double x,
|
||||
required final double y,
|
||||
final double? pressure,
|
||||
final double? tilt,
|
||||
final int? timestamp,
|
||||
final InputDeviceKind? pointerDeviceKind,
|
||||
}) = _$EditorPointImpl;
|
||||
|
||||
factory _EditorPoint.fromJson(Map<String, dynamic> json) =
|
||||
_$EditorPointImpl.fromJson;
|
||||
|
||||
@override
|
||||
double get x;
|
||||
@override
|
||||
double get y;
|
||||
@override
|
||||
double? get pressure;
|
||||
@override
|
||||
double? get tilt;
|
||||
@override
|
||||
int? get timestamp;
|
||||
@override
|
||||
InputDeviceKind? get pointerDeviceKind;
|
||||
|
||||
/// Create a copy of EditorPoint
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$EditorPointImplCopyWith<_$EditorPointImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
EditorStroke _$EditorStrokeFromJson(Map<String, dynamic> json) {
|
||||
return _EditorStroke.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$EditorStroke {
|
||||
String get id => throw _privateConstructorUsedError;
|
||||
List<EditorPoint> get points => throw _privateConstructorUsedError;
|
||||
EditorTool get tool => throw _privateConstructorUsedError;
|
||||
int get color => throw _privateConstructorUsedError;
|
||||
double get width => throw _privateConstructorUsedError;
|
||||
bool get filled => throw _privateConstructorUsedError;
|
||||
String? get textContent => throw _privateConstructorUsedError;
|
||||
double get fontSize => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this EditorStroke to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of EditorStroke
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$EditorStrokeCopyWith<EditorStroke> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $EditorStrokeCopyWith<$Res> {
|
||||
factory $EditorStrokeCopyWith(
|
||||
EditorStroke value,
|
||||
$Res Function(EditorStroke) then,
|
||||
) = _$EditorStrokeCopyWithImpl<$Res, EditorStroke>;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id,
|
||||
List<EditorPoint> points,
|
||||
EditorTool tool,
|
||||
int color,
|
||||
double width,
|
||||
bool filled,
|
||||
String? textContent,
|
||||
double fontSize,
|
||||
});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$EditorStrokeCopyWithImpl<$Res, $Val extends EditorStroke>
|
||||
implements $EditorStrokeCopyWith<$Res> {
|
||||
_$EditorStrokeCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of EditorStroke
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? points = null,
|
||||
Object? tool = null,
|
||||
Object? color = null,
|
||||
Object? width = null,
|
||||
Object? filled = null,
|
||||
Object? textContent = freezed,
|
||||
Object? fontSize = null,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
points: null == points
|
||||
? _value.points
|
||||
: points // ignore: cast_nullable_to_non_nullable
|
||||
as List<EditorPoint>,
|
||||
tool: null == tool
|
||||
? _value.tool
|
||||
: tool // ignore: cast_nullable_to_non_nullable
|
||||
as EditorTool,
|
||||
color: null == color
|
||||
? _value.color
|
||||
: color // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
width: null == width
|
||||
? _value.width
|
||||
: width // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
filled: null == filled
|
||||
? _value.filled
|
||||
: filled // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
textContent: freezed == textContent
|
||||
? _value.textContent
|
||||
: textContent // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
fontSize: null == fontSize
|
||||
? _value.fontSize
|
||||
: fontSize // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$EditorStrokeImplCopyWith<$Res>
|
||||
implements $EditorStrokeCopyWith<$Res> {
|
||||
factory _$$EditorStrokeImplCopyWith(
|
||||
_$EditorStrokeImpl value,
|
||||
$Res Function(_$EditorStrokeImpl) then,
|
||||
) = __$$EditorStrokeImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({
|
||||
String id,
|
||||
List<EditorPoint> points,
|
||||
EditorTool tool,
|
||||
int color,
|
||||
double width,
|
||||
bool filled,
|
||||
String? textContent,
|
||||
double fontSize,
|
||||
});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$EditorStrokeImplCopyWithImpl<$Res>
|
||||
extends _$EditorStrokeCopyWithImpl<$Res, _$EditorStrokeImpl>
|
||||
implements _$$EditorStrokeImplCopyWith<$Res> {
|
||||
__$$EditorStrokeImplCopyWithImpl(
|
||||
_$EditorStrokeImpl _value,
|
||||
$Res Function(_$EditorStrokeImpl) _then,
|
||||
) : super(_value, _then);
|
||||
|
||||
/// Create a copy of EditorStroke
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? points = null,
|
||||
Object? tool = null,
|
||||
Object? color = null,
|
||||
Object? width = null,
|
||||
Object? filled = null,
|
||||
Object? textContent = freezed,
|
||||
Object? fontSize = null,
|
||||
}) {
|
||||
return _then(
|
||||
_$EditorStrokeImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
points: null == points
|
||||
? _value._points
|
||||
: points // ignore: cast_nullable_to_non_nullable
|
||||
as List<EditorPoint>,
|
||||
tool: null == tool
|
||||
? _value.tool
|
||||
: tool // ignore: cast_nullable_to_non_nullable
|
||||
as EditorTool,
|
||||
color: null == color
|
||||
? _value.color
|
||||
: color // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
width: null == width
|
||||
? _value.width
|
||||
: width // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
filled: null == filled
|
||||
? _value.filled
|
||||
: filled // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
textContent: freezed == textContent
|
||||
? _value.textContent
|
||||
: textContent // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
fontSize: null == fontSize
|
||||
? _value.fontSize
|
||||
: fontSize // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$EditorStrokeImpl extends _EditorStroke {
|
||||
_$EditorStrokeImpl({
|
||||
required this.id,
|
||||
required final List<EditorPoint> points,
|
||||
this.tool = EditorTool.pen,
|
||||
this.color = 0xFF000000,
|
||||
this.width = 0.003,
|
||||
this.filled = false,
|
||||
this.textContent,
|
||||
this.fontSize = 14.0,
|
||||
}) : _points = points,
|
||||
super._();
|
||||
|
||||
factory _$EditorStrokeImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$EditorStrokeImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String id;
|
||||
final List<EditorPoint> _points;
|
||||
@override
|
||||
List<EditorPoint> get points {
|
||||
if (_points is EqualUnmodifiableListView) return _points;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_points);
|
||||
}
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final EditorTool tool;
|
||||
@override
|
||||
@JsonKey()
|
||||
final int color;
|
||||
@override
|
||||
@JsonKey()
|
||||
final double width;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool filled;
|
||||
@override
|
||||
final String? textContent;
|
||||
@override
|
||||
@JsonKey()
|
||||
final double fontSize;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'EditorStroke(id: $id, points: $points, tool: $tool, color: $color, width: $width, filled: $filled, textContent: $textContent, fontSize: $fontSize)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$EditorStrokeImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
const DeepCollectionEquality().equals(other._points, _points) &&
|
||||
(identical(other.tool, tool) || other.tool == tool) &&
|
||||
(identical(other.color, color) || other.color == color) &&
|
||||
(identical(other.width, width) || other.width == width) &&
|
||||
(identical(other.filled, filled) || other.filled == filled) &&
|
||||
(identical(other.textContent, textContent) ||
|
||||
other.textContent == textContent) &&
|
||||
(identical(other.fontSize, fontSize) ||
|
||||
other.fontSize == fontSize));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
const DeepCollectionEquality().hash(_points),
|
||||
tool,
|
||||
color,
|
||||
width,
|
||||
filled,
|
||||
textContent,
|
||||
fontSize,
|
||||
);
|
||||
|
||||
/// Create a copy of EditorStroke
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$EditorStrokeImplCopyWith<_$EditorStrokeImpl> get copyWith =>
|
||||
__$$EditorStrokeImplCopyWithImpl<_$EditorStrokeImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$EditorStrokeImplToJson(this);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _EditorStroke extends EditorStroke {
|
||||
factory _EditorStroke({
|
||||
required final String id,
|
||||
required final List<EditorPoint> points,
|
||||
final EditorTool tool,
|
||||
final int color,
|
||||
final double width,
|
||||
final bool filled,
|
||||
final String? textContent,
|
||||
final double fontSize,
|
||||
}) = _$EditorStrokeImpl;
|
||||
_EditorStroke._() : super._();
|
||||
|
||||
factory _EditorStroke.fromJson(Map<String, dynamic> json) =
|
||||
_$EditorStrokeImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get id;
|
||||
@override
|
||||
List<EditorPoint> get points;
|
||||
@override
|
||||
EditorTool get tool;
|
||||
@override
|
||||
int get color;
|
||||
@override
|
||||
double get width;
|
||||
@override
|
||||
bool get filled;
|
||||
@override
|
||||
String? get textContent;
|
||||
@override
|
||||
double get fontSize;
|
||||
|
||||
/// Create a copy of EditorStroke
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$EditorStrokeImplCopyWith<_$EditorStrokeImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
73
lib/editor/engine/stroke_model.g.dart
Normal file
73
lib/editor/engine/stroke_model.g.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'stroke_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$EditorPointImpl _$$EditorPointImplFromJson(Map<String, dynamic> json) =>
|
||||
_$EditorPointImpl(
|
||||
x: (json['x'] as num).toDouble(),
|
||||
y: (json['y'] as num).toDouble(),
|
||||
pressure: (json['pressure'] as num?)?.toDouble(),
|
||||
tilt: (json['tilt'] as num?)?.toDouble(),
|
||||
timestamp: (json['timestamp'] as num?)?.toInt(),
|
||||
pointerDeviceKind: $enumDecodeNullable(
|
||||
_$InputDeviceKindEnumMap,
|
||||
json['pointerDeviceKind'],
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$EditorPointImplToJson(_$EditorPointImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'x': instance.x,
|
||||
'y': instance.y,
|
||||
'pressure': instance.pressure,
|
||||
'tilt': instance.tilt,
|
||||
'timestamp': instance.timestamp,
|
||||
'pointerDeviceKind': _$InputDeviceKindEnumMap[instance.pointerDeviceKind],
|
||||
};
|
||||
|
||||
const _$InputDeviceKindEnumMap = {
|
||||
InputDeviceKind.touch: 'touch',
|
||||
InputDeviceKind.mouse: 'mouse',
|
||||
InputDeviceKind.stylus: 'stylus',
|
||||
InputDeviceKind.invertedStylus: 'invertedStylus',
|
||||
InputDeviceKind.trackpad: 'trackpad',
|
||||
InputDeviceKind.unknown: 'unknown',
|
||||
};
|
||||
|
||||
_$EditorStrokeImpl _$$EditorStrokeImplFromJson(Map<String, dynamic> json) =>
|
||||
_$EditorStrokeImpl(
|
||||
id: json['id'] as String,
|
||||
points: (json['points'] as List<dynamic>)
|
||||
.map((e) => EditorPoint.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
tool:
|
||||
$enumDecodeNullable(_$EditorToolEnumMap, json['tool']) ??
|
||||
EditorTool.pen,
|
||||
color: (json['color'] as num?)?.toInt() ?? 0xFF000000,
|
||||
width: (json['width'] as num?)?.toDouble() ?? 0.003,
|
||||
filled: json['filled'] as bool? ?? false,
|
||||
textContent: json['textContent'] as String?,
|
||||
fontSize: (json['fontSize'] as num?)?.toDouble() ?? 14.0,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$EditorStrokeImplToJson(_$EditorStrokeImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'points': instance.points,
|
||||
'tool': _$EditorToolEnumMap[instance.tool]!,
|
||||
'color': instance.color,
|
||||
'width': instance.width,
|
||||
'filled': instance.filled,
|
||||
'textContent': instance.textContent,
|
||||
'fontSize': instance.fontSize,
|
||||
};
|
||||
|
||||
const _$EditorToolEnumMap = {
|
||||
EditorTool.pen: 'pen',
|
||||
EditorTool.highlighter: 'highlighter',
|
||||
EditorTool.eraser: 'eraser',
|
||||
};
|
||||
56
lib/editor/engine/stroke_store.dart
Normal file
56
lib/editor/engine/stroke_store.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
// lib/editor/engine/stroke_store.dart
|
||||
//
|
||||
// Mutable, revision-tracked store for committed EditorStrokes.
|
||||
//
|
||||
// Every mutation bumps [revision] (monotonic int). Consumers use the revision
|
||||
// as an O(1) repaint gate: if revision has not changed since the last paint,
|
||||
// nothing needs to be redrawn (StaticInkPainter.shouldRepaint).
|
||||
|
||||
import 'stroke_model.dart';
|
||||
|
||||
/// Holds the ordered list of committed [EditorStroke]s for one ink host (e.g.
|
||||
/// a page or annotation layer). Every mutating operation bumps [revision].
|
||||
///
|
||||
/// This class is intentionally NOT a ChangeNotifier / Listenable — callers
|
||||
/// poll the revision number from within CustomPainter.shouldRepaint, so no
|
||||
/// subscription machinery is needed here.
|
||||
class StrokeStore {
|
||||
final List<EditorStroke> _strokes = [];
|
||||
int _revision = 0;
|
||||
|
||||
/// Monotonically increasing counter. Bumped on every mutation.
|
||||
int get revision => _revision;
|
||||
|
||||
/// Unmodifiable ordered list of committed strokes.
|
||||
List<EditorStroke> get committed => List.unmodifiable(_strokes);
|
||||
|
||||
/// Appends [stroke] and bumps the revision.
|
||||
void add(EditorStroke stroke) {
|
||||
_strokes.add(stroke);
|
||||
_revision++;
|
||||
}
|
||||
|
||||
/// Removes the stroke with the given [id] (no-op if not found) and bumps
|
||||
/// the revision only when a stroke was actually removed.
|
||||
void removeById(String id) {
|
||||
final before = _strokes.length;
|
||||
_strokes.removeWhere((s) => s.id == id);
|
||||
if (_strokes.length != before) {
|
||||
_revision++;
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces the entire stroke list and bumps the revision.
|
||||
void replaceAll(List<EditorStroke> strokes) {
|
||||
_strokes
|
||||
..clear()
|
||||
..addAll(strokes);
|
||||
_revision++;
|
||||
}
|
||||
|
||||
/// Clears all strokes and bumps the revision.
|
||||
void clear() {
|
||||
_strokes.clear();
|
||||
_revision++;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user