docs: 项目 Wiki 知识库 — 7 文件覆盖架构/手写/数据/前端/后端/技术债
新增 wiki/ 知识库 (遵循 HMS wiki-methodology.md 5 节结构): - index.md (84 行) — 症状导航 13 条 + 模块索引 + 系统数据流 - architecture.md (120 行) — 基座剥离 7 耦合点 + Feature Flag + PIPL 合规 - handwriting-engine.md (124 行) — 双层 Canvas + O(1) 点缓冲 + 光栅化缓存 - data-layer.md (127 行) — Isar + SyncEngine 离线同步 + 踩坑记录 - frontend.md (118 行) — 16 模块地图 + BLoC 注入链 + 设计系统 - erp-diary.md (101 行) — 15 Entity / 10 Service / 8 Handler + API 端点 新增 docs/: - tech-debt-board.md (110 行) — 10 条技术债 + 偿还优先级排名 其他更新: - .gitignore: 添加 .understand-anything/ (待初始化) - CLAUDE.md §9: 添加 wiki 参考文档链接
This commit is contained in:
124
wiki/handwriting-engine.md
Normal file
124
wiki/handwriting-engine.md
Normal file
@@ -0,0 +1,124 @@
|
||||
---
|
||||
title: 手写引擎
|
||||
updated: 2026-06-01
|
||||
status: active
|
||||
tags: [handwriting, performance, canvas, perfect-freehand]
|
||||
---
|
||||
|
||||
# 手写引擎
|
||||
|
||||
> 从 [[index]] 导航。关联: [[data-layer]] [[frontend]]
|
||||
|
||||
暖记的**核心价值** — 保留用户真实笔迹。目标:<16ms 延迟(p99),在 Samsung Tab A9 / iPad 10th 上流畅运行。
|
||||
|
||||
## 1. 设计决策
|
||||
|
||||
### Q: 为什么用 Listener 不用 GestureDetector?
|
||||
|
||||
GestureDetector 内部有手势竞技场(gesture arena),额外延迟约 5-8ms。Listener 直接接收 PointerDownEvent/PointerMoveEvent,跳过竞技场,延迟最低。
|
||||
|
||||
### Q: 为什么双层 Canvas 架构?
|
||||
|
||||
单层 Canvas 每帧需要重绘全部 N 条笔画,复杂度 O(N×P)。双层架构将已完成笔画光栅化为 ui.Image 位图,每帧只绘制当前活跃笔画,复杂度降至 O(P_current)。
|
||||
|
||||
### Q: 为什么选 perfect_freehand?
|
||||
|
||||
支持压力感知、速度相关的变宽笔画,开箱即用的高质量笔迹渲染。4 种画笔(钢笔/铅笔/马克笔/橡皮擦)通过不同参数配置实现。
|
||||
|
||||
### Q: 笔画存储策略?
|
||||
|
||||
HandwritingStroke 作为独立 SeaORM Entity,大字段(points JSON)隔离,日记列表查询时延迟加载。前端 Isar 中笔画序列化为 handwriting_ref 元素,固定 ID `${journalId}_strokes`。
|
||||
|
||||
## 2. 关键文件 + 数据流
|
||||
|
||||
### 核心文件
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `features/editor/widgets/handwriting_canvas.dart` | 307 | Listener + 双层 Stack + 掌心抑制 |
|
||||
| `features/editor/widgets/stroke_cache.dart` | 303 | StrokeRasterCache 光栅化 + 合成 |
|
||||
| `features/editor/widgets/active_stroke_painter.dart` | 35 | 实时绘制当前笔画 |
|
||||
| `features/editor/widgets/cached_strokes_painter.dart` | 35 | drawImage 绘制缓存位图 |
|
||||
| `features/editor/widgets/stroke_renderer.dart` | ~120 | 4 种画笔渲染(pen/pencil/marker/eraser) |
|
||||
| `features/editor/widgets/stroke_model.dart` | 112 | Stroke/StrokePoint 数据模型 |
|
||||
|
||||
### 数据流
|
||||
|
||||
```
|
||||
PointerDown/Move/Up Events
|
||||
│ (Listener, not GestureDetector)
|
||||
▼
|
||||
StrokePoint (x, y, pressure, timestamp)
|
||||
│ accumulate
|
||||
▼
|
||||
Stroke (id, points[], brushType, color, width)
|
||||
│ ValueNotifier → 触发重绘
|
||||
▼
|
||||
ActiveStrokePainter ──── 实时绘制当前笔画
|
||||
│ onStrokeCompleted
|
||||
▼
|
||||
StrokeRasterCache.addStroke()
|
||||
│ toImage() 光栅化
|
||||
▼
|
||||
compositeImage (ui.Image) ──── 所有完成笔画合成位图
|
||||
│
|
||||
▼
|
||||
CachedStrokesPainter ──── drawImage 绘制缓存
|
||||
```
|
||||
|
||||
## 3. 代码逻辑
|
||||
|
||||
### O(1) 点缓冲
|
||||
|
||||
```dart
|
||||
// 旧方案:O(N²) — 每次 rebuild 都 spread 整个列表
|
||||
final points = [..._allPoints, newPoint];
|
||||
|
||||
// 新方案:O(1) — 可变缓冲区 + ValueNotifier
|
||||
_currentPoints.add(newPoint);
|
||||
_pointsNotifier.value = _currentPoints;
|
||||
```
|
||||
|
||||
### 橡皮擦实现
|
||||
|
||||
```
|
||||
saveLayer() // 保存当前画布状态
|
||||
drawPath(eraserPath, BlendMode.dstOut) // 用 dstOut 混合模式"擦除"
|
||||
restore() // 恢复,不穿透背景
|
||||
```
|
||||
|
||||
### 模式切换
|
||||
|
||||
```
|
||||
旧方案:if (isDrawingMode) Canvas() else ElementLayer() — 销毁重建
|
||||
新方案:IgnorePointer(ignoring: !isDrawingMode) — 不销毁,只控制交互
|
||||
```
|
||||
|
||||
### 不变量
|
||||
|
||||
⚡ **shouldRepaint 守卫** — CachedStrokesPainter 仅在 compositeImage 引用变化时重绘
|
||||
|
||||
⚡ **_currentPoints 可变缓冲** — 不创建新 List,直接 add + ValueNotifier 通知
|
||||
|
||||
⚡ **掌心抑制** — 通过 PointerDeviceKind 过滤,仅处理 touch 事件
|
||||
|
||||
## 4. 活跃问题 + 陷阱
|
||||
|
||||
| 问题 | 级别 | 状态 | 说明 |
|
||||
|------|------|------|------|
|
||||
| toImage() 同步阻塞 | HIGH | 待修 | 光栅化在主线程,大笔画可能卡 UI |
|
||||
| 画布尺寸变化缓存失效 | MEDIUM | 待修 | 屏幕旋转时需平滑过渡 |
|
||||
| 橡皮擦视觉反馈 | LOW | 待优化 | 擦除区域无实时预览 |
|
||||
|
||||
### 历史教训
|
||||
|
||||
- 初版用 GestureDetector,延迟约 20ms → 改用 Listener 降至 <16ms
|
||||
- 初版 if/else 切换模式会销毁 Canvas → IgnorePointer 保持 Widget 树稳定
|
||||
- 单层 Canvas 100+ 笔画时明显卡顿 → 双层 + 光栅化缓存解决
|
||||
|
||||
## 5. 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| 2026-06-01 | 初始创建 — 双层架构、性能优化记录、活跃问题 |
|
||||
| 2026-06-01 | 性能优化提交 (e07da7a):双层 Canvas + 光栅化缓存 + O(1) 点缓冲 |
|
||||
Reference in New Issue
Block a user