feat(p0): live canvas renders via revision-gated ui.Picture cache (step 3)
Some checks failed
CI / Windows build (push) Has been cancelled
Some checks failed
CI / Windows build (push) Has been cancelled
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>
This commit is contained in:
@@ -23,10 +23,15 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../engine/stroke_eraser.dart';
|
||||
import '../engine/stroke_geometry.dart' show kDefaultPenThinning;
|
||||
import '../engine/stroke_model.dart';
|
||||
import '../engine/stroke_store.dart';
|
||||
import '../input/input_arbiter.dart' as arbiter;
|
||||
import '../input/pen_config.dart';
|
||||
import '../input/pen_input_service.dart';
|
||||
import 'ink_painters.dart';
|
||||
import '../render/ink_picture_cache.dart';
|
||||
import '../render/live_ink_painter.dart' as render;
|
||||
import '../render/static_ink_painter.dart' as render;
|
||||
import 'ink_painters.dart' show EraserPreviewPainter;
|
||||
import 'pen_interactive_viewer.dart';
|
||||
import 'pen_stroke.dart';
|
||||
|
||||
@@ -136,6 +141,28 @@ class _PenCanvasState extends State<PenCanvas> {
|
||||
/// (the old per-move setState was the eraser-lag source).
|
||||
final ValueNotifier<PenPoint?> _eraserCursor = ValueNotifier<PenPoint?>(null);
|
||||
|
||||
/// Committed ink mirrored as the canonical [EditorStroke] model, driving the
|
||||
/// revision-gated [render.StaticInkPainter] + [InkPictureCache] (P0 step 3).
|
||||
/// The cache replays a recorded ui.Picture for the committed layer, so pinch /
|
||||
/// pan / live-stroke frames never re-rasterize the committed ink — the
|
||||
/// P0.5 perf prerequisite. Kept in sync with [PenCanvas.strokes] (which the
|
||||
/// parent replaces with a fresh list identity on every commit/erase).
|
||||
final StrokeStore _store = StrokeStore();
|
||||
final InkPictureCache _inkCache = InkPictureCache();
|
||||
List<PenStroke>? _syncedStrokesRef;
|
||||
static const String _inkHostId = 'pen-canvas';
|
||||
|
||||
/// Re-mirror [PenCanvas.strokes] into [_store] when the parent hands us a new
|
||||
/// list (identity change ⇒ a commit/erase happened). Bumping the store
|
||||
/// revision invalidates the cached Picture so the committed layer repaints.
|
||||
void _syncStore() {
|
||||
if (identical(_syncedStrokesRef, widget.strokes)) return;
|
||||
_syncedStrokesRef = widget.strokes;
|
||||
_store.replaceAll(
|
||||
widget.strokes.map((s) => EditorStroke.fromPenStroke(s)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// True when the eraser would act (eraser tool selected, or a barrel/inverted
|
||||
/// eraser signal is live).
|
||||
bool get _isEraserMode =>
|
||||
@@ -462,6 +489,7 @@ class _PenCanvasState extends State<PenCanvas> {
|
||||
@override
|
||||
void dispose() {
|
||||
_eraserCursor.dispose();
|
||||
_inkCache.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -474,6 +502,12 @@ class _PenCanvasState extends State<PenCanvas> {
|
||||
// so a pinch re-enables pan/zoom immediately.
|
||||
final panEnabled = _drawPointer == null;
|
||||
|
||||
// Mirror committed strokes into the revision-tracked store (only re-mirrors
|
||||
// when the parent handed us a new list identity).
|
||||
_syncStore();
|
||||
final liveEditorStroke =
|
||||
_liveStroke == null ? null : EditorStroke.fromPenStroke(_liveStroke!);
|
||||
|
||||
return Listener(
|
||||
onPointerHover: _onPointerHover,
|
||||
onPointerDown: _onPointerDown,
|
||||
@@ -500,13 +534,17 @@ class _PenCanvasState extends State<PenCanvas> {
|
||||
Positioned.fill(
|
||||
child: RepaintBoundary(child: widget.pageWidget),
|
||||
),
|
||||
// Committed ink (static layer, isolated repaint).
|
||||
// Committed ink (static layer, isolated repaint). Backed by the
|
||||
// revision-gated ui.Picture cache (P0 step 3): unchanged across
|
||||
// pinch/pan/live-move frames ⇒ cache hit ⇒ zero re-raster.
|
||||
Positioned.fill(
|
||||
child: RepaintBoundary(
|
||||
child: CustomPaint(
|
||||
painter: StaticInkPainter(
|
||||
strokes: widget.strokes,
|
||||
painter: render.StaticInkPainter(
|
||||
hostId: _inkHostId,
|
||||
store: _store,
|
||||
pageSize: widget.pageSize,
|
||||
cache: _inkCache,
|
||||
thinning: widget.thinning,
|
||||
),
|
||||
),
|
||||
@@ -532,8 +570,8 @@ class _PenCanvasState extends State<PenCanvas> {
|
||||
Positioned.fill(
|
||||
child: RepaintBoundary(
|
||||
child: CustomPaint(
|
||||
painter: LiveInkPainter(
|
||||
stroke: _liveStroke,
|
||||
painter: render.LiveInkPainter(
|
||||
live: liveEditorStroke,
|
||||
pageSize: widget.pageSize,
|
||||
thinning: widget.thinning,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user