Files
nj/app/lib/features/editor/widgets/stroke_model.dart
iven d0653614e0 feat(diary): 手写引擎 + 日记 CRUD + 同步 API (Phase F3 + B2)
Flutter 手写引擎 (Phase F3):
- stroke_model.dart: 笔画数据模型 (StrokePoint/Stroke/BrushType)
- stroke_renderer.dart: perfect_freehand 渲染管线 + 四画笔参数
- handwriting_canvas.dart: Listener 输入 + 掌心抑制 + 去抖过滤
- editor_bloc.dart: BLoC 状态管理 + 撤销/重做 (50步)

Rust 日记 CRUD + 同步 (Phase B2):
- journal_service.rs: CRUD + 软删除 + 分页列表 + 事件发布
- sync_service.rs: 版本号同步 + 冲突检测
- journal_handler.rs: 5个API端点 + utoipa注解 + 权限守卫
- sync_handler.rs: 同步API端点
- error.rs: From<DiaryError> for AppError + 8个单元测试
- 路由注册: /diary/journals + /diary/sync

验证:
- cargo check: 0 error
- cargo test: 433 测试全通过
- flutter analyze: 1 warning (unused private param)
2026-06-01 00:36:05 +08:00

112 lines
2.6 KiB
Dart

// 笔画数据模型 — 手写不可变类(避免 build_runner 依赖)
import 'dart:collection';
/// 画笔类型
enum BrushType {
pen('pen'),
pencil('pencil'),
marker('marker'),
eraser('eraser');
const BrushType(this.value);
final String value;
}
/// 笔画点
class StrokePoint {
const StrokePoint({
required this.x,
required this.y,
this.pressure = 0.5,
this.timestamp = 0,
});
final double x;
final double y;
final double pressure;
final int timestamp;
StrokePoint copyWith({
double? x,
double? y,
double? pressure,
int? timestamp,
}) =>
StrokePoint(
x: x ?? this.x,
y: y ?? this.y,
pressure: pressure ?? this.pressure,
timestamp: timestamp ?? this.timestamp,
);
Map<String, dynamic> toJson() => {
'x': x,
'y': y,
'pressure': pressure,
'timestamp': timestamp,
};
factory StrokePoint.fromJson(Map<String, dynamic> json) => StrokePoint(
x: (json['x'] as num).toDouble(),
y: (json['y'] as num).toDouble(),
pressure: (json['pressure'] as num?)?.toDouble() ?? 0.5,
timestamp: (json['timestamp'] as int?) ?? 0,
);
}
/// 笔画
class Stroke {
const Stroke({
required this.id,
required this.points,
this.brushType = BrushType.pen,
this.color = '#2D2420',
this.width = 3.0,
});
final String id;
final List<StrokePoint> points;
final BrushType brushType;
final String color;
final double width;
Stroke copyWith({
String? id,
List<StrokePoint>? points,
BrushType? brushType,
String? color,
double? width,
}) =>
Stroke(
id: id ?? this.id,
points: points ?? this.points,
brushType: brushType ?? this.brushType,
color: color ?? this.color,
width: width ?? this.width,
);
Map<String, dynamic> toJson() => {
'id': id,
'points': points.map((p) => p.toJson()).toList(),
'brushType': brushType.value,
'color': color,
'width': width,
};
factory Stroke.fromJson(Map<String, dynamic> json) => Stroke(
id: json['id'] as String,
points: UnmodifiableListView(
(json['points'] as List)
.map((p) => StrokePoint.fromJson(p as Map<String, dynamic>))
.toList(),
),
brushType: BrushType.values.firstWhere(
(b) => b.value == json['brushType'],
orElse: () => BrushType.pen,
),
color: (json['color'] as String?) ?? '#2D2420',
width: (json['width'] as num?)?.toDouble() ?? 3.0,
);
}