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:
69
lib/editor/render/static_ink_painter.dart
Normal file
69
lib/editor/render/static_ink_painter.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
// lib/editor/render/static_ink_painter.dart
|
||||
//
|
||||
// CustomPainter for the committed-stroke (static) layer.
|
||||
//
|
||||
// paint() gets-or-builds a ui.Picture of all committed strokes keyed by
|
||||
// store.revision, then delegates to canvas.drawPicture — so as long as the
|
||||
// revision is unchanged the raster thread replays the same display list at
|
||||
// zero CPU cost.
|
||||
//
|
||||
// shouldRepaint() is O(1): it compares the revision int and pageSize only.
|
||||
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../engine/stroke_geometry.dart';
|
||||
import '../engine/stroke_store.dart';
|
||||
import 'ink_picture_cache.dart';
|
||||
|
||||
/// Paints the committed ink layer by recording strokes into a [ui.Picture]
|
||||
/// once per [StrokeStore.revision] and caching it in [InkPictureCache].
|
||||
///
|
||||
/// Place this inside a [RepaintBoundary] / [CustomPaint] pair. The sibling
|
||||
/// [LiveInkPainter] handles the in-progress stroke in a separate layer.
|
||||
class StaticInkPainter extends CustomPainter {
|
||||
StaticInkPainter({
|
||||
required this.hostId,
|
||||
required this.store,
|
||||
required this.pageSize,
|
||||
required this.cache,
|
||||
}) : revision = store.revision;
|
||||
|
||||
final String hostId;
|
||||
final StrokeStore store;
|
||||
final Size pageSize;
|
||||
final InkPictureCache cache;
|
||||
|
||||
/// Revision snapshot captured at construction time. Used by [shouldRepaint]
|
||||
/// so two painters built at different revisions compare correctly even when
|
||||
/// they share the same [StrokeStore] instance.
|
||||
final int revision;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final picture = cache.getOrBuild(hostId, store.revision, pageSize, () {
|
||||
final recorder = ui.PictureRecorder();
|
||||
final rec = Canvas(recorder);
|
||||
for (final stroke in store.committed) {
|
||||
final path =
|
||||
buildStrokeOutline(stroke, pageSize, isComplete: true);
|
||||
if (path.getBounds().isEmpty) continue;
|
||||
rec.drawPath(
|
||||
path,
|
||||
Paint()
|
||||
..color = Color(stroke.color)
|
||||
..style = PaintingStyle.fill
|
||||
..isAntiAlias = true,
|
||||
);
|
||||
}
|
||||
return recorder.endRecording();
|
||||
});
|
||||
|
||||
canvas.drawPicture(picture);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(StaticInkPainter old) =>
|
||||
old.revision != store.revision || old.pageSize != pageSize;
|
||||
}
|
||||
Reference in New Issue
Block a user