// 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, this.thinning = kDefaultPenThinning, }) : revision = store.revision; final String hostId; final StrokeStore store; final Size pageSize; final InkPictureCache cache; /// perfect_freehand pressure→width response (from `PenConfig.pressureSensitivity`). /// Folded into the cache key + [shouldRepaint] so a sensitivity change can't /// replay a stale Picture built at the old thinning. final double thinning; /// 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) { // thinning is part of the cache identity (different thinning ⇒ different // outline) so it MUST be in the key, not just shouldRepaint. final cacheKey = '$hostId#${thinning.toStringAsFixed(4)}'; final picture = cache.getOrBuild(cacheKey, store.revision, pageSize, () { final recorder = ui.PictureRecorder(); final rec = Canvas(recorder); for (final stroke in store.committed) { final path = buildStrokeOutline(stroke, pageSize, isComplete: true, thinning: thinning); if (path.getBounds().isEmpty) continue; // One drawPath per stroke ⇒ highlighter self-overlap never darkens; // cross-stroke overlap darkens via BlendMode.multiply (closes // TODO(brush-opacity); shared resolver with the live + PenCanvas paths). rec.drawPath(path, paintForEditorStroke(stroke)); } return recorder.endRecording(); }); canvas.drawPicture(picture); } @override bool shouldRepaint(StaticInkPainter old) => old.revision != store.revision || old.pageSize != pageSize || old.thinning != thinning; }