Files
BadNote/lib/editor/render/static_ink_painter.dart
Akiba So eca5141372
Some checks failed
CI / Windows build (push) Has been cancelled
feat(p0): live canvas renders via revision-gated ui.Picture cache (step 3)
The live PenCanvas committed-ink layer now uses the relocated render/ painters
(render.StaticInkPainter + InkPictureCache + StrokeStore) instead of the old
canvas/ink_painters versions — the P0.5 perf prerequisite. The committed layer's
ui.Picture is recorded once per StrokeStore.revision and replayed on the raster
thread, so pinch / pan / live-stroke frames no longer re-rasterize committed ink.

- pen_canvas mirrors widget.strokes (PenStroke) into a StrokeStore (EditorStroke)
  on every new-list identity (the parent already replaces the list on each
  commit/erase), bumping the revision → cache invalidates → static layer repaints.
- thinning (PenConfig.pressureSensitivity) is threaded into the render painters
  AND folded into the cache key + shouldRepaint, so a sensitivity change can't
  replay a stale Picture built at the old thinning.
- live layer converts _liveStroke→EditorStroke per frame (correct: it must
  repaint every move); eraser preview keeps the existing canvas painter.
- pen_canvas disposes the InkPictureCache.

Equivalent by construction (both paths call buildStrokeOutline with the same
thinning); device confirms final fidelity. The old canvas Static/LiveInkPainter
are now orphaned (buildStrokePath still used by tests) — P1 deletes them.

flutter analyze lib/editor clean; 90/90 tests (+6: thinning repaint/cache + live).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 02:59:30 +08:00

81 lines
2.8 KiB
Dart

// 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;
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 ||
old.thinning != thinning;
}