- _compositeIncremental 中不再 dispose strokeImage,因为 _cache 持有同一引用 - 提前 dispose 导致 syncStrokes/clear/dispose 时 double-dispose(use-after-free) - 单笔画 image 生命周期由缓存统一管理:移除/清除/销毁时释放 - 更新 _rebuildComposite 注释,移除过时说明 审计 ID: 8b-R01
299 lines
8.2 KiB
Dart
299 lines
8.2 KiB
Dart
// 笔画光栅化缓存 — 将已完成笔画渲染为 ui.Image,避免每帧重算
|
||
//
|
||
// 核心思路:
|
||
// 1. 每条笔画完成时,通过 ui.PictureRecorder 光栅化为独立 ui.Image
|
||
// 2. 将所有单笔画图像合成为一个 compositeImage(在 saveLayer 中正确处理橡皮擦)
|
||
// 3. CachedStrokesPainter 每帧仅 drawImage(compositeImage),O(1) 操作
|
||
// 4. 新笔画完成 → 光栅化 → 合成到 compositeImage(增量,不需全部重算)
|
||
// 5. 撤销/重做/清除 → 移除对应缓存条目 → 重建 compositeImage
|
||
|
||
import 'dart:ui' as ui;
|
||
|
||
import 'package:flutter/material.dart';
|
||
|
||
import 'stroke_model.dart';
|
||
import 'stroke_renderer.dart';
|
||
|
||
// ===== 缓存条目 =====
|
||
|
||
/// 单笔画光栅化缓存条目
|
||
class _CacheEntry {
|
||
final ui.Image image;
|
||
final Stroke stroke;
|
||
|
||
const _CacheEntry({required this.image, required this.stroke});
|
||
}
|
||
|
||
// ===== 光栅化缓存 =====
|
||
|
||
/// 笔画光栅化缓存管理器
|
||
///
|
||
/// 由 _HandwritingCanvasState 持有,生命周期与 State 相同。
|
||
/// dispose() 时释放所有 ui.Image GPU 资源。
|
||
class StrokeRasterCache {
|
||
final Map<String, _CacheEntry> _cache = {};
|
||
|
||
/// 所有已完成笔画的合成位图
|
||
ui.Image? _compositeImage;
|
||
|
||
/// 合成图版本号,每次重建递增,驱动 CachedStrokesPainter.shouldRepaint
|
||
int _layerVersion = 0;
|
||
|
||
/// 当前画布尺寸
|
||
Size _canvasSize = Size.zero;
|
||
|
||
/// 是否需要重建合成图
|
||
bool _dirty = false;
|
||
|
||
// ===== Getters =====
|
||
|
||
/// 合成位图(可能为 null,表示画布为空)
|
||
ui.Image? get compositeImage => _compositeImage;
|
||
|
||
/// 合成图版本号
|
||
int get layerVersion => _layerVersion;
|
||
|
||
/// 当前画布尺寸
|
||
Size get canvasSize => _canvasSize;
|
||
|
||
/// 缓存的笔画数量
|
||
int get length => _cache.length;
|
||
|
||
/// 已缓存的笔画 ID 集合
|
||
Set<String> get cachedStrokeIds => _cache.keys.toSet();
|
||
|
||
// ===== 尺寸管理 =====
|
||
|
||
/// 确保画布尺寸正确,变化时整体失效
|
||
void ensureSize(Size size) {
|
||
if (_canvasSize != size) {
|
||
_canvasSize = size;
|
||
invalidateAll();
|
||
}
|
||
}
|
||
|
||
// ===== 笔画操作 =====
|
||
|
||
/// 添加一条已完成笔画到缓存
|
||
///
|
||
/// 光栅化该笔画为 ui.Image,然后增量合成到 compositeImage。
|
||
Future<void> addStroke(Stroke stroke) async {
|
||
if (_canvasSize == Size.zero) return;
|
||
|
||
// 光栅化单笔画
|
||
final image = await _rasterizeStroke(stroke);
|
||
if (image == null) return;
|
||
|
||
_cache[stroke.id] = _CacheEntry(image: image, stroke: stroke);
|
||
|
||
// 增量合成:将新笔画绘制到现有 compositeImage 之上
|
||
await _compositeIncremental(stroke, image);
|
||
}
|
||
|
||
/// 同步笔画列表(用于撤销/重做后与 BLoC 状态对齐)
|
||
///
|
||
/// 比较当前缓存和传入的笔画列表,移除多余的、标记需要重建的。
|
||
Future<void> syncStrokes(List<Stroke> strokes) async {
|
||
final currentIds = strokes.map((s) => s.id).toSet();
|
||
final cachedIds = _cache.keys.toSet();
|
||
|
||
// 移除缓存中多余的条目(撤销的笔画)
|
||
final toRemove = cachedIds.difference(currentIds);
|
||
for (final id in toRemove) {
|
||
final entry = _cache.remove(id);
|
||
entry?.image.dispose();
|
||
}
|
||
|
||
// 添加缺失的条目(重做的笔画)
|
||
final toAdd = currentIds.difference(cachedIds);
|
||
for (final stroke in strokes) {
|
||
if (toAdd.contains(stroke.id)) {
|
||
final image = await _rasterizeStroke(stroke);
|
||
if (image != null) {
|
||
_cache[stroke.id] = _CacheEntry(image: image, stroke: stroke);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 任何变更都需要重建合成图
|
||
if (toRemove.isNotEmpty || toAdd.isNotEmpty) {
|
||
_dirty = true;
|
||
await _rebuildComposite();
|
||
}
|
||
}
|
||
|
||
/// 清除所有缓存
|
||
Future<void> clear() async {
|
||
for (final entry in _cache.values) {
|
||
entry.image.dispose();
|
||
}
|
||
_cache.clear();
|
||
_compositeImage?.dispose();
|
||
_compositeImage = null;
|
||
_layerVersion++;
|
||
_dirty = false;
|
||
}
|
||
|
||
/// 整体失效(画布尺寸变化时)
|
||
Future<void> invalidateAll() async {
|
||
final strokes = _cache.values.map((e) => e.stroke).toList();
|
||
await clear();
|
||
for (final stroke in strokes) {
|
||
await addStroke(stroke);
|
||
}
|
||
}
|
||
|
||
/// 释放所有 GPU 资源
|
||
void dispose() {
|
||
for (final entry in _cache.values) {
|
||
entry.image.dispose();
|
||
}
|
||
_cache.clear();
|
||
_compositeImage?.dispose();
|
||
_compositeImage = null;
|
||
}
|
||
|
||
// ===== 光栅化 =====
|
||
|
||
/// 将单条笔画光栅化为 ui.Image
|
||
Future<ui.Image?> _rasterizeStroke(Stroke stroke) async {
|
||
final outlinePoints = pointsToOutline(
|
||
stroke.points,
|
||
stroke.brushType,
|
||
stroke.width,
|
||
isComplete: true,
|
||
);
|
||
if (outlinePoints.isEmpty) return null;
|
||
|
||
final path = buildStrokePath(outlinePoints);
|
||
final paint = createPaintForStroke(stroke);
|
||
|
||
final recorder = ui.PictureRecorder();
|
||
final canvas = Canvas(recorder);
|
||
|
||
// 橡皮擦需要 saveLayer 保护,避免穿透
|
||
if (stroke.brushType == BrushType.eraser) {
|
||
canvas.saveLayer(
|
||
Rect.fromLTWH(0, 0, _canvasSize.width, _canvasSize.height),
|
||
Paint(),
|
||
);
|
||
}
|
||
|
||
canvas.drawPath(path, paint);
|
||
|
||
if (stroke.brushType == BrushType.eraser) {
|
||
canvas.restore();
|
||
}
|
||
|
||
final picture = recorder.endRecording();
|
||
return picture.toImage(
|
||
_canvasSize.width.toInt().clamp(1, 4096),
|
||
_canvasSize.height.toInt().clamp(1, 4096),
|
||
);
|
||
}
|
||
|
||
// ===== 合成 =====
|
||
|
||
/// 增量合成:将新笔画图像绘制到现有 compositeImage 上
|
||
Future<void> _compositeIncremental(Stroke stroke, ui.Image strokeImage) async {
|
||
final recorder = ui.PictureRecorder();
|
||
final canvas = Canvas(recorder);
|
||
|
||
// saveLayer 创建离屏缓冲区,橡皮擦 dstOut 在其中正确合成
|
||
canvas.saveLayer(
|
||
Rect.fromLTWH(0, 0, _canvasSize.width, _canvasSize.height),
|
||
Paint(),
|
||
);
|
||
|
||
// 先绘制现有的合成图
|
||
if (_compositeImage != null) {
|
||
canvas.drawImage(_compositeImage!, Offset.zero, Paint());
|
||
}
|
||
|
||
// 再绘制新笔画(橡皮擦用 dstOut)
|
||
if (stroke.brushType == BrushType.eraser) {
|
||
canvas.drawImage(
|
||
strokeImage,
|
||
Offset.zero,
|
||
Paint()..blendMode = BlendMode.dstOut,
|
||
);
|
||
} else {
|
||
canvas.drawImage(strokeImage, Offset.zero, Paint());
|
||
}
|
||
|
||
canvas.restore();
|
||
|
||
// 释放旧的合成图
|
||
_compositeImage?.dispose();
|
||
|
||
final picture = recorder.endRecording();
|
||
_compositeImage = await picture.toImage(
|
||
_canvasSize.width.toInt().clamp(1, 4096),
|
||
_canvasSize.height.toInt().clamp(1, 4096),
|
||
);
|
||
|
||
// 注意:不在此处 dispose strokeImage!
|
||
// _cache 和此方法持有同一 image 引用,提前 dispose 会导致
|
||
// syncStrokes/clear/dispose 时 use-after-dispose(审计 ID: 8b-R01)。
|
||
// 单笔画 image 由缓存统一管理生命周期(移除/清除/销毁时释放)。
|
||
|
||
_layerVersion++;
|
||
}
|
||
|
||
/// 全量重建合成图(撤销/清除后使用)
|
||
Future<void> _rebuildComposite() async {
|
||
if (!_dirty || _cache.isEmpty) {
|
||
if (_cache.isEmpty) {
|
||
_compositeImage?.dispose();
|
||
_compositeImage = null;
|
||
_layerVersion++;
|
||
}
|
||
_dirty = false;
|
||
return;
|
||
}
|
||
|
||
final recorder = ui.PictureRecorder();
|
||
final canvas = Canvas(recorder);
|
||
|
||
canvas.saveLayer(
|
||
Rect.fromLTWH(0, 0, _canvasSize.width, _canvasSize.height),
|
||
Paint(),
|
||
);
|
||
|
||
// 按笔画顺序重新绘制所有单笔画
|
||
// 直接从 stroke 数据重绘路径,确保与增量合成结果一致
|
||
for (final entry in _cache.values) {
|
||
final stroke = entry.stroke;
|
||
final outlinePoints = pointsToOutline(
|
||
stroke.points,
|
||
stroke.brushType,
|
||
stroke.width,
|
||
isComplete: true,
|
||
);
|
||
if (outlinePoints.isEmpty) continue;
|
||
|
||
final path = buildStrokePath(outlinePoints);
|
||
final paint = createPaintForStroke(stroke);
|
||
|
||
if (stroke.brushType == BrushType.eraser) {
|
||
canvas.drawPath(path, Paint()..blendMode = BlendMode.dstOut);
|
||
} else {
|
||
canvas.drawPath(path, paint);
|
||
}
|
||
}
|
||
|
||
canvas.restore();
|
||
|
||
_compositeImage?.dispose();
|
||
|
||
final picture = recorder.endRecording();
|
||
_compositeImage = await picture.toImage(
|
||
_canvasSize.width.toInt().clamp(1, 4096),
|
||
_canvasSize.height.toInt().clamp(1, 4096),
|
||
);
|
||
|
||
_layerVersion++;
|
||
_dirty = false;
|
||
}
|
||
}
|