2026-06-23 10:21:40 +08:00
|
|
|
// Proves the InkStroke<->PenStroke bridge used to host notes/slides on the
|
|
|
|
|
// pen-first canvas: normalization round-trips within the logical page, the
|
|
|
|
|
// tool<->kind mapping is correct, and non-freehand strokes (shapes/text) are
|
|
|
|
|
// dropped because the pen canvas cannot render them.
|
|
|
|
|
|
|
|
|
|
import 'dart:ui' show Size;
|
|
|
|
|
|
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
|
|
|
|
|
|
import 'package:badnote/editor/canvas/pen_stroke.dart';
|
|
|
|
|
import 'package:badnote/editor/notebook/ink_stroke_adapter.dart';
|
|
|
|
|
import 'package:badnote/models/ink_point.dart';
|
|
|
|
|
import 'package:badnote/models/ink_stroke.dart';
|
|
|
|
|
import 'package:badnote/models/pen_tool.dart';
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
const page = Size(1000, 1414);
|
|
|
|
|
final t0 = DateTime.fromMillisecondsSinceEpoch(0);
|
|
|
|
|
|
|
|
|
|
InkStroke ink(
|
|
|
|
|
List<InkPoint> pts, {
|
|
|
|
|
PenTool tool = PenTool.pen,
|
|
|
|
|
double width = 2.0,
|
|
|
|
|
int color = 0xFF112233,
|
|
|
|
|
}) =>
|
|
|
|
|
InkStroke(
|
|
|
|
|
id: 'a',
|
|
|
|
|
points: pts,
|
|
|
|
|
tool: tool,
|
|
|
|
|
color: color,
|
|
|
|
|
strokeWidth: width,
|
|
|
|
|
createdAt: t0,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
InkPoint ip(double x, double y, {double pressure = 0.7}) =>
|
|
|
|
|
InkPoint(x: x, y: y, pressure: pressure, timestamp: 0);
|
|
|
|
|
|
|
|
|
|
test('ink -> pen normalizes against the page', () {
|
|
|
|
|
final pen = penStrokeFromInk(
|
|
|
|
|
ink([ip(250, 707), ip(500, 1414)], width: 10), page)!;
|
|
|
|
|
expect(pen.points.first.x, closeTo(0.25, 1e-9));
|
|
|
|
|
expect(pen.points.first.y, closeTo(0.5, 1e-9));
|
|
|
|
|
expect(pen.points[1].x, closeTo(0.5, 1e-9));
|
|
|
|
|
expect(pen.points[1].y, closeTo(1.0, 1e-9));
|
|
|
|
|
expect(pen.width, closeTo(10 / 1000, 1e-9));
|
|
|
|
|
expect(pen.color, 0xFF112233);
|
|
|
|
|
expect(pen.kind, PenStrokeKind.pen);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('round-trip ink -> pen -> ink preserves coords, width, color', () {
|
|
|
|
|
final original = ink([ip(123, 456), ip(789, 1000)], width: 7, color: 0xFFABCDEF);
|
|
|
|
|
final pen = penStrokeFromInk(original, page)!;
|
|
|
|
|
final back = inkStrokeFromPen(pen, page, id: 'b', createdAt: t0);
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < original.points.length; i++) {
|
|
|
|
|
expect(back.points[i].x, closeTo(original.points[i].x, 1e-6));
|
|
|
|
|
expect(back.points[i].y, closeTo(original.points[i].y, 1e-6));
|
|
|
|
|
}
|
|
|
|
|
expect(back.strokeWidth, closeTo(7, 1e-6));
|
|
|
|
|
expect(back.color, 0xFFABCDEF);
|
|
|
|
|
expect(back.tool, PenTool.pen);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('highlighter maps to the highlighter kind both ways', () {
|
|
|
|
|
final pen = penStrokeFromInk(
|
|
|
|
|
ink([ip(10, 10), ip(20, 20)], tool: PenTool.highlighter), page)!;
|
|
|
|
|
expect(pen.kind, PenStrokeKind.highlighter);
|
|
|
|
|
final back = inkStrokeFromPen(pen, page, id: 'c', createdAt: t0);
|
|
|
|
|
expect(back.tool, PenTool.highlighter);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('non-freehand strokes (shapes/text) are dropped', () {
|
|
|
|
|
for (final tool in [
|
|
|
|
|
PenTool.rectangle,
|
|
|
|
|
PenTool.ellipse,
|
|
|
|
|
PenTool.line,
|
|
|
|
|
PenTool.arrow,
|
|
|
|
|
PenTool.text,
|
|
|
|
|
]) {
|
|
|
|
|
expect(penStrokeFromInk(ink([ip(0, 0), ip(5, 5)], tool: tool), page),
|
|
|
|
|
isNull,
|
|
|
|
|
reason: '$tool has no pen-canvas representation');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-23 15:52:52 +08:00
|
|
|
test('world coords are stable when the scratchpad world expands', () {
|
|
|
|
|
// A stroke drawn at the center of a 4000-world: normalized 0.5 -> world
|
|
|
|
|
// 2000. This is how the split-view scratchpad stores ink.
|
|
|
|
|
final pen0 = PenStroke(
|
|
|
|
|
points: const [PenPoint(0.5, 0.5, 0.6), PenPoint(0.6, 0.6, 0.6)],
|
|
|
|
|
color: 0xFF000000,
|
|
|
|
|
width: 0.002,
|
|
|
|
|
kind: PenStrokeKind.pen,
|
|
|
|
|
);
|
|
|
|
|
final stored = inkStrokeFromPen(pen0, const Size(4000, 4000),
|
|
|
|
|
id: 'w', createdAt: t0);
|
|
|
|
|
expect(stored.points.first.x, closeTo(2000, 1e-6));
|
|
|
|
|
expect(stored.points.first.y, closeTo(2000, 1e-6));
|
|
|
|
|
|
|
|
|
|
// The world auto-expands to 8000. The STORED world coords are untouched;
|
|
|
|
|
// only the render-time normalization divisor changes.
|
|
|
|
|
final pen1 = penStrokeFromInk(stored, const Size(8000, 8000))!;
|
|
|
|
|
expect(pen1.points.first.x, closeTo(0.25, 1e-9)); // 2000 / 8000
|
|
|
|
|
expect(pen1.points.first.y, closeTo(0.25, 1e-9));
|
|
|
|
|
|
|
|
|
|
// Crucially, the ABSOLUTE on-page position is unchanged: normalized *
|
|
|
|
|
// pageWidth = 0.25 * 8000 = 2000 == the original world x. Zero drift.
|
|
|
|
|
expect(pen1.points.first.x * 8000, closeTo(2000, 1e-6));
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-23 10:21:40 +08:00
|
|
|
test('penStrokesFromInk preserves order and filters non-freehand', () {
|
|
|
|
|
final strokes = [
|
|
|
|
|
ink([ip(0, 0), ip(1, 1)], tool: PenTool.pen, color: 0xFF000001),
|
|
|
|
|
ink([ip(2, 2), ip(3, 3)], tool: PenTool.rectangle),
|
|
|
|
|
ink([ip(4, 4), ip(5, 5)], tool: PenTool.highlighter, color: 0xFF000002),
|
|
|
|
|
];
|
|
|
|
|
final pens = penStrokesFromInk(strokes, page);
|
|
|
|
|
expect(pens, hasLength(2));
|
|
|
|
|
expect(pens[0].color, 0xFF000001);
|
|
|
|
|
expect(pens[1].color, 0xFF000002);
|
|
|
|
|
expect(pens[1].kind, PenStrokeKind.highlighter);
|
|
|
|
|
});
|
|
|
|
|
}
|