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

@@ -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]});