181 lines
5.3 KiB
Dart
181 lines
5.3 KiB
Dart
|
|
// test/editor_stroke_model_test.dart
|
||
|
|
//
|
||
|
|
// Pure unit tests for the engine foundation (NO DB, NO widgets):
|
||
|
|
// (a) EditorStroke -> json -> EditorStroke round-trips losslessly,
|
||
|
|
// including tilt/timestamp/pointerDeviceKind.
|
||
|
|
// (b) EditorStroke <-> InkStroke round-trips losslessly.
|
||
|
|
// (c) buildStrokeOutline returns a non-empty Path for a >=2-point stroke and
|
||
|
|
// an empty Path for 0 points.
|
||
|
|
|
||
|
|
import 'dart:convert';
|
||
|
|
import 'dart:ui';
|
||
|
|
|
||
|
|
import 'package:badnote/editor/engine/stroke_geometry.dart';
|
||
|
|
import 'package:badnote/editor/engine/stroke_model.dart';
|
||
|
|
import 'package:badnote/models/ink_point.dart';
|
||
|
|
import 'package:badnote/models/ink_stroke.dart';
|
||
|
|
import 'package:badnote/models/pen_tool.dart';
|
||
|
|
import 'package:badnote/models/pointer_device_kind.dart';
|
||
|
|
import 'package:flutter_test/flutter_test.dart';
|
||
|
|
|
||
|
|
void main() {
|
||
|
|
group('EditorStroke JSON round-trip', () {
|
||
|
|
test('round-trips losslessly incl. tilt/timestamp/pointerDeviceKind', () {
|
||
|
|
final stroke = EditorStroke(
|
||
|
|
id: 'stroke-1',
|
||
|
|
points: const [
|
||
|
|
EditorPoint(
|
||
|
|
x: 0.1,
|
||
|
|
y: 0.2,
|
||
|
|
pressure: 0.75,
|
||
|
|
tilt: 0.33,
|
||
|
|
timestamp: 1234567,
|
||
|
|
pointerDeviceKind: InputDeviceKind.stylus,
|
||
|
|
),
|
||
|
|
EditorPoint(
|
||
|
|
x: 0.4,
|
||
|
|
y: 0.5,
|
||
|
|
pressure: 0.5,
|
||
|
|
tilt: 0.0,
|
||
|
|
timestamp: 1234600,
|
||
|
|
pointerDeviceKind: InputDeviceKind.invertedStylus,
|
||
|
|
),
|
||
|
|
],
|
||
|
|
tool: EditorTool.highlighter,
|
||
|
|
color: 0xFFAABBCC,
|
||
|
|
width: 0.0123,
|
||
|
|
filled: true,
|
||
|
|
textContent: 'hello',
|
||
|
|
fontSize: 18.0,
|
||
|
|
);
|
||
|
|
|
||
|
|
// Persisted as a JSON string (DB TEXT column); jsonEncode invokes nested
|
||
|
|
// toJson, jsonDecode rebuilds the maps.
|
||
|
|
final decoded = EditorStroke.fromJson(
|
||
|
|
jsonDecode(jsonEncode(stroke.toJson())) as Map<String, dynamic>,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(decoded, stroke);
|
||
|
|
// Spot-check the superset fields explicitly.
|
||
|
|
expect(decoded.points.first.tilt, 0.33);
|
||
|
|
expect(decoded.points.first.timestamp, 1234567);
|
||
|
|
expect(
|
||
|
|
decoded.points.first.pointerDeviceKind,
|
||
|
|
InputDeviceKind.stylus,
|
||
|
|
);
|
||
|
|
expect(
|
||
|
|
decoded.points.last.pointerDeviceKind,
|
||
|
|
InputDeviceKind.invertedStylus,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('preserves null superset fields', () {
|
||
|
|
final stroke = EditorStroke(
|
||
|
|
id: 'stroke-null',
|
||
|
|
points: const [
|
||
|
|
EditorPoint(x: 0.0, y: 0.0),
|
||
|
|
EditorPoint(x: 1.0, y: 1.0, pressure: 0.9),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
|
||
|
|
final decoded = EditorStroke.fromJson(
|
||
|
|
jsonDecode(jsonEncode(stroke.toJson())) as Map<String, dynamic>,
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(decoded, stroke);
|
||
|
|
expect(decoded.points.first.pressure, isNull);
|
||
|
|
expect(decoded.points.first.tilt, isNull);
|
||
|
|
expect(decoded.points.first.timestamp, isNull);
|
||
|
|
expect(decoded.points.first.pointerDeviceKind, isNull);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group('EditorStroke <-> InkStroke round-trip', () {
|
||
|
|
test('InkStroke -> EditorStroke -> InkStroke is lossless', () {
|
||
|
|
final ink = InkStroke(
|
||
|
|
id: 'ink-1',
|
||
|
|
points: const [
|
||
|
|
InkPoint(
|
||
|
|
x: 0.2,
|
||
|
|
y: 0.3,
|
||
|
|
pressure: 0.8,
|
||
|
|
tilt: 0.1,
|
||
|
|
timestamp: 999,
|
||
|
|
pointerDeviceKind: InputDeviceKind.stylus,
|
||
|
|
),
|
||
|
|
InkPoint(
|
||
|
|
x: 0.6,
|
||
|
|
y: 0.7,
|
||
|
|
pressure: 0.4,
|
||
|
|
tilt: 0.2,
|
||
|
|
timestamp: 1050,
|
||
|
|
pointerDeviceKind: InputDeviceKind.touch,
|
||
|
|
),
|
||
|
|
],
|
||
|
|
tool: PenTool.highlighter,
|
||
|
|
color: 0xFF112233,
|
||
|
|
strokeWidth: 0.02,
|
||
|
|
createdAt: DateTime.fromMillisecondsSinceEpoch(42),
|
||
|
|
filled: true,
|
||
|
|
textContent: 'note',
|
||
|
|
fontSize: 22.0,
|
||
|
|
);
|
||
|
|
|
||
|
|
final editor = EditorStroke.fromInkStroke(ink);
|
||
|
|
final back = editor.toInkStroke(createdAt: ink.createdAt);
|
||
|
|
|
||
|
|
expect(back, ink);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('EditorStroke (non-null fields) -> InkStroke -> EditorStroke', () {
|
||
|
|
final editor = EditorStroke(
|
||
|
|
id: 'ink-2',
|
||
|
|
points: const [
|
||
|
|
EditorPoint(
|
||
|
|
x: 0.1,
|
||
|
|
y: 0.1,
|
||
|
|
pressure: 0.5,
|
||
|
|
tilt: 0.0,
|
||
|
|
timestamp: 7,
|
||
|
|
pointerDeviceKind: InputDeviceKind.mouse,
|
||
|
|
),
|
||
|
|
],
|
||
|
|
tool: EditorTool.eraser,
|
||
|
|
color: 0xFF000000,
|
||
|
|
width: 0.004,
|
||
|
|
);
|
||
|
|
|
||
|
|
final ink = editor.toInkStroke();
|
||
|
|
final back = EditorStroke.fromInkStroke(ink);
|
||
|
|
|
||
|
|
expect(back, editor);
|
||
|
|
expect(ink.tool, PenTool.eraser);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group('buildStrokeOutline', () {
|
||
|
|
const pageSize = Size(800, 600);
|
||
|
|
|
||
|
|
test('returns a non-empty Path for a >=2-point stroke', () {
|
||
|
|
final stroke = EditorStroke(
|
||
|
|
id: 's',
|
||
|
|
points: const [
|
||
|
|
EditorPoint(x: 0.1, y: 0.1, pressure: 0.6),
|
||
|
|
EditorPoint(x: 0.5, y: 0.4, pressure: 0.6),
|
||
|
|
EditorPoint(x: 0.8, y: 0.9, pressure: 0.6),
|
||
|
|
],
|
||
|
|
width: 0.01,
|
||
|
|
);
|
||
|
|
|
||
|
|
final path = buildStrokeOutline(stroke, pageSize, isComplete: true);
|
||
|
|
expect(path.getBounds().isEmpty, isFalse);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('returns an empty Path for 0 points', () {
|
||
|
|
final stroke = EditorStroke(id: 'empty', points: const []);
|
||
|
|
final path = buildStrokeOutline(stroke, pageSize, isComplete: true);
|
||
|
|
expect(path.getBounds().isEmpty, isTrue);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|