feat(note): rebuild note editor on the pen-first canvas
Some checks failed
CI / Windows build (push) Has been cancelled
Some checks failed
CI / Windows build (push) Has been cancelled
Notes now use the single performant inking engine (PenCanvas) instead of the old ink_canvas, per "all note features on the pen-first canvas". - ink_stroke_adapter: pure InkStroke<->PenStroke bridge (normalize against a logical note page; drop non-freehand shapes/text). Round-trip tested. - pen_palette_widgets: shared M3 ToolButton/PaletteDivider/RoundIconButton so PDF + note editors use identical chrome (PenEditorScreen migrated to them; its private copies deleted). - PenNoteScreen: PenCanvas over a white logical page, undo/redo, title, save -> Note.strokes (+ local OCR for search). Pressure curve, eraser size/mode and palm rejection all inherited from the shared canvas. - Route home (new/open) + search note hits -> PenNoteScreen; remove the now-redundant "Pen Canvas (beta)" spike button; delete the dead old note_editor_screen. Tests: ink_stroke_adapter (5) + pen_note_screen widget (load + commit, 2). flutter analyze: 0 issues. Full suite: 265/265.
This commit is contained in:
98
test/ink_stroke_adapter_test.dart
Normal file
98
test/ink_stroke_adapter_test.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
76
test/pen_note_screen_widget_test.dart
Normal file
76
test/pen_note_screen_widget_test.dart
Normal file
@@ -0,0 +1,76 @@
|
||||
// Screen-level guard for the pen-first note editor: an existing note's ink
|
||||
// strokes load onto the PenCanvas via the adapter, and a stylus pass commits a
|
||||
// new stroke. Exercises the real PenNoteScreen (no DB needed — persistence only
|
||||
// runs on save).
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:badnote/editor/canvas/pen_canvas.dart';
|
||||
import 'package:badnote/editor/canvas/pen_note_screen.dart';
|
||||
import 'package:badnote/models/ink_point.dart';
|
||||
import 'package:badnote/models/ink_stroke.dart';
|
||||
import 'package:badnote/models/note.dart';
|
||||
import 'package:badnote/models/pen_tool.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
Note noteWith(List<InkStroke> strokes) => Note(
|
||||
id: 'n1',
|
||||
title: 'Test',
|
||||
strokes: strokes,
|
||||
createdAt: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
updatedAt: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
);
|
||||
|
||||
InkStroke freehand() => InkStroke(
|
||||
id: 's1',
|
||||
points: const [
|
||||
InkPoint(x: 100, y: 200, timestamp: 0),
|
||||
InkPoint(x: 300, y: 400, timestamp: 0),
|
||||
InkPoint(x: 500, y: 700, timestamp: 0),
|
||||
],
|
||||
tool: PenTool.pen,
|
||||
createdAt: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
);
|
||||
|
||||
int canvasStrokeCount(WidgetTester tester) =>
|
||||
tester.widget<PenCanvas>(find.byType(PenCanvas)).strokes.length;
|
||||
|
||||
testWidgets('loads an existing note\'s ink strokes onto the canvas',
|
||||
(tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
await tester.pumpWidget(ProviderScope(
|
||||
child: MaterialApp(home: PenNoteScreen(note: noteWith([freehand()]))),
|
||||
));
|
||||
await tester.pump(); // let PenConfig load
|
||||
|
||||
expect(find.byType(PenCanvas), findsOneWidget);
|
||||
expect(canvasStrokeCount(tester), 1,
|
||||
reason: 'the note\'s freehand stroke should load via the adapter');
|
||||
});
|
||||
|
||||
testWidgets('a stylus pass commits a new stroke', (tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
await tester.pumpWidget(ProviderScope(
|
||||
child: MaterialApp(home: PenNoteScreen(note: noteWith([]))),
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
expect(canvasStrokeCount(tester), 0);
|
||||
|
||||
final center = tester.getCenter(find.byType(PenCanvas));
|
||||
final g = await tester.startGesture(center, kind: PointerDeviceKind.stylus);
|
||||
await g.moveBy(const Offset(40, 30));
|
||||
await g.moveBy(const Offset(30, 20));
|
||||
await g.up();
|
||||
await tester.pump();
|
||||
|
||||
expect(canvasStrokeCount(tester), 1,
|
||||
reason: 'the drawn stroke should be committed to the canvas');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user