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:
@@ -10,6 +10,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:badnote/editor/engine/brush.dart';
|
||||
import 'package:badnote/editor/engine/stroke_model.dart';
|
||||
import 'package:badnote/models/bookmark.dart';
|
||||
import 'package:badnote/models/ink_point.dart';
|
||||
@@ -141,6 +142,43 @@ void main() {
|
||||
expect(sp.strokes, original.scratchLinks.single.scratchpad.strokes);
|
||||
});
|
||||
|
||||
test('sidecar with mixed-brush strokes round-trips each brush', () {
|
||||
EditorStroke brushStroke(String id, BrushKind brush) => EditorStroke(
|
||||
id: id,
|
||||
points: const [
|
||||
EditorPoint(x: 0.1, y: 0.2, pressure: 0.6),
|
||||
EditorPoint(x: 0.4, y: 0.5, pressure: 0.6),
|
||||
],
|
||||
color: 0xFF223344,
|
||||
width: 0.004,
|
||||
brush: brush,
|
||||
);
|
||||
|
||||
final original = BadnoteSidecar(strokes: {
|
||||
0: [
|
||||
brushStroke('b-fountain', BrushKind.fountainPen),
|
||||
brushStroke('b-ballpoint', BrushKind.ballpoint),
|
||||
],
|
||||
1: [
|
||||
brushStroke('b-pencil', BrushKind.pencil),
|
||||
brushStroke('b-highlighter', BrushKind.highlighter),
|
||||
],
|
||||
});
|
||||
final reparsed = _roundTrip(original);
|
||||
|
||||
expect(
|
||||
reparsed.strokes[0]!.map((s) => s.brush).toList(),
|
||||
[BrushKind.fountainPen, BrushKind.ballpoint],
|
||||
);
|
||||
expect(
|
||||
reparsed.strokes[1]!.map((s) => s.brush).toList(),
|
||||
[BrushKind.pencil, BrushKind.highlighter],
|
||||
);
|
||||
// Full value equality (brush is part of EditorStroke's freezed equality).
|
||||
expect(reparsed.strokes[0], original.strokes[0]);
|
||||
expect(reparsed.strokes[1], original.strokes[1]);
|
||||
});
|
||||
|
||||
test('strokes JSON is byte-compatible with EditorStroke.toJson', () {
|
||||
final stroke = _editorStroke('s0', EditorTool.pen);
|
||||
final sidecar = BadnoteSidecar(strokes: {2: [stroke]});
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:badnote/editor/engine/brush.dart';
|
||||
import 'package:badnote/editor/engine/stroke_geometry.dart';
|
||||
import 'package:badnote/editor/engine/stroke_model.dart';
|
||||
import 'package:badnote/models/ink_point.dart';
|
||||
@@ -90,6 +91,65 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('EditorStroke brush persistence', () {
|
||||
EditorStroke strokeWithBrush(BrushKind brush) => EditorStroke(
|
||||
id: 'brush-$brush',
|
||||
points: const [
|
||||
EditorPoint(x: 0.1, y: 0.2, pressure: 0.6),
|
||||
EditorPoint(x: 0.4, y: 0.5, pressure: 0.6),
|
||||
],
|
||||
color: 0xFF334455,
|
||||
width: 0.004,
|
||||
brush: brush,
|
||||
);
|
||||
|
||||
test('round-trips each BrushKind through JSON', () {
|
||||
for (final brush in BrushKind.values) {
|
||||
final stroke = strokeWithBrush(brush);
|
||||
final decoded = EditorStroke.fromJson(
|
||||
jsonDecode(jsonEncode(stroke.toJson())) as Map<String, dynamic>,
|
||||
);
|
||||
expect(decoded.brush, brush, reason: 'brush $brush should survive JSON');
|
||||
expect(decoded, stroke);
|
||||
}
|
||||
});
|
||||
|
||||
test('serializes brush as the stable BrushKind.name string', () {
|
||||
final json = strokeWithBrush(BrushKind.ballpoint).toJson();
|
||||
expect(json['brush'], 'ballpoint');
|
||||
});
|
||||
|
||||
test('JSON WITHOUT a brush field loads as fountainPen (back-compat)', () {
|
||||
// Simulate a sidecar written before the brush field existed: serialize
|
||||
// through a real JSON cycle (so nested points are maps), then strip the
|
||||
// `brush` key as an old file would lack it.
|
||||
final json = jsonDecode(jsonEncode(strokeWithBrush(BrushKind.pencil)))
|
||||
as Map<String, dynamic>;
|
||||
json.remove('brush');
|
||||
expect(json.containsKey('brush'), isFalse);
|
||||
|
||||
final decoded = EditorStroke.fromJson(json);
|
||||
expect(decoded.brush, BrushKind.fountainPen);
|
||||
});
|
||||
|
||||
test('unknown brush name falls back to fountainPen (forward-compat)', () {
|
||||
final json = jsonDecode(jsonEncode(strokeWithBrush(BrushKind.ballpoint)))
|
||||
as Map<String, dynamic>
|
||||
..['brush'] = 'someFutureBrush';
|
||||
final decoded = EditorStroke.fromJson(json);
|
||||
expect(decoded.brush, BrushKind.fountainPen);
|
||||
});
|
||||
|
||||
test('fromPenStroke carries brush both ways', () {
|
||||
// EditorStroke -> PenStroke equivalent: fromPenStroke reads PenStroke.brush.
|
||||
final decoded = EditorStroke.fromJson(
|
||||
jsonDecode(jsonEncode(strokeWithBrush(BrushKind.pencil).toJson()))
|
||||
as Map<String, dynamic>,
|
||||
);
|
||||
expect(decoded.brush, BrushKind.pencil);
|
||||
});
|
||||
});
|
||||
|
||||
group('EditorStroke <-> InkStroke round-trip', () {
|
||||
test('InkStroke -> EditorStroke -> InkStroke is lossless', () {
|
||||
final ink = InkStroke(
|
||||
|
||||
Reference in New Issue
Block a user