// Tests for the pure partial/segment stroke eraser (P0 engine layer). import 'package:badnote/editor/canvas/pen_stroke.dart'; import 'package:badnote/editor/engine/stroke_eraser.dart'; import 'package:flutter_test/flutter_test.dart'; PenStroke _line(List> xy) => PenStroke( points: [for (final p in xy) PenPoint(p[0], p[1], 0.5)], color: 0xFF000000, width: 0.006, kind: PenStrokeKind.pen, ); void main() { group('strokeHit (whole-stroke proximity)', () { final stroke = _line([ [0.0, 0.5], [0.5, 0.5], [1.0, 0.5], ]); test('hits when a point is inside the radius', () { expect(strokeHit(stroke, 0.5, 0.5, 0.05), isTrue); }); test('misses when every point is outside the radius', () { expect(strokeHit(stroke, 0.5, 0.9, 0.05), isFalse); }); }); group('splitStrokeByCircle (partial erase)', () { test('no hit returns the SAME stroke object (cheap no-change signal)', () { final stroke = _line([ [0.0, 0.0], [0.2, 0.0], ]); final out = splitStrokeByCircle(stroke, 0.9, 0.9, 0.05); expect(out, hasLength(1)); expect(identical(out.first, stroke), isTrue); }); test('erasing the middle of a straight line splits it into two pieces', () { // 5 evenly spaced points along y=0.5; erase the center point only. final stroke = _line([ [0.0, 0.5], [0.25, 0.5], [0.5, 0.5], [0.75, 0.5], [1.0, 0.5], ]); // radius small enough to catch only the x=0.5 point. final out = splitStrokeByCircle(stroke, 0.5, 0.5, 0.1); expect(out, hasLength(2)); expect(out[0].points.map((p) => p.x), [0.0, 0.25]); expect(out[1].points.map((p) => p.x), [0.75, 1.0]); }); test('erasing every point removes the stroke entirely', () { final stroke = _line([ [0.5, 0.5], [0.51, 0.5], [0.52, 0.5], ]); final out = splitStrokeByCircle(stroke, 0.51, 0.5, 0.5); expect(out, isEmpty); }); test('orphan single-survivor runs are dropped (no speckle dots)', () { // erase points 1 and 3 → survivors are isolated singletons at 0 and 2 and 4. final stroke = _line([ [0.0, 0.5], [0.25, 0.5], [0.5, 0.5], [0.75, 0.5], [1.0, 0.5], ]); // Two tiny erase passes won't fit in one circle; instead verify the // single-survivor drop directly: erase the two interior neighbors of a // lone point. Use a circle covering x in (0.1..0.9) except the exact // center is also covered — so all interior gone, endpoints survive as // singletons and must be dropped. final out = splitStrokeByCircle(stroke, 0.5, 0.5, 0.45); // endpoints 0.0 and 1.0 are >0.45 away in x → survive, but each is a lone // point (its neighbor was erased) → both dropped → empty. expect(out, isEmpty); }); test('preserves color / width / kind on the cut pieces', () { final stroke = PenStroke( points: [ const PenPoint(0.0, 0.5, 0.5), const PenPoint(0.25, 0.5, 0.5), const PenPoint(0.5, 0.5, 0.5), const PenPoint(0.75, 0.5, 0.5), const PenPoint(1.0, 0.5, 0.5), ], color: 0xFFFF0000, width: 0.02, kind: PenStrokeKind.highlighter, ); final out = splitStrokeByCircle(stroke, 0.5, 0.5, 0.1); expect(out, hasLength(2)); for (final piece in out) { expect(piece.color, 0xFFFF0000); expect(piece.width, 0.02); expect(piece.kind, PenStrokeKind.highlighter); } }); test('aspect correction amplifies the y delta', () { // A single point offset only in y (dy = 0.2) from the eraser center. final stroke = _line([ [0.5, 0.2], ]); // aspect 1.0 → dy 0.2 < radius 0.25 → hit. expect(strokeHit(stroke, 0.5, 0.0, 0.25, aspect: 1.0), isTrue); // aspect 1.5 → effective dy 0.30 > radius 0.25 → miss. expect(strokeHit(stroke, 0.5, 0.0, 0.25, aspect: 1.5), isFalse); }); }); }