// R7 regression (P0 step 7/8): the on-screen painter and the PDF export must // build their freehand outline from ONE shared recipe. The export bug was // pdf_service hardcoding `thinning: 0.7, streamline: 0.5` while the screen used // 0.85 / 0.32 — a hairline mismatch. These tests pin the single source so a // future edit to one side can't silently re-diverge. import 'dart:ui'; import 'package:flutter_test/flutter_test.dart'; import 'package:perfect_freehand/perfect_freehand.dart' as pf; import 'package:badnote/editor/engine/stroke_geometry.dart'; import 'package:badnote/editor/engine/stroke_model.dart'; void main() { const size = Size(800, 600); EditorStroke pen() => EditorStroke.create( id: 'a', points: const [ EditorPoint(x: 0.1, y: 0.1, pressure: 0.9), EditorPoint(x: 0.4, y: 0.2, pressure: 0.5), EditorPoint(x: 0.7, y: 0.15, pressure: 0.2), ], width: 0.01, ); test('buildStrokeOutline traces exactly the shared freehandOutlinePoints', () { final stroke = pen(); final pfPoints = stroke.points .map((p) => pf.PointVector( p.x * size.width, p.y * size.height, p.pressure ?? 0.5)) .toList(); final shared = freehandOutlinePoints( pfPoints: pfPoints, size: stroke.width * size.width, isHighlighter: false, hasRealPressure: true, isComplete: true, ); expect(shared, isNotEmpty); final ref = Path()..moveTo(shared.first.dx, shared.first.dy); for (var i = 1; i < shared.length; i++) { ref.lineTo(shared[i].dx, shared[i].dy); } ref.close(); final screen = buildStrokeOutline(stroke, size, isComplete: true); // Same vertices ⇒ identical bounds. (If the screen path used different // options than the shared core, the outline would differ.) expect(screen.getBounds(), ref.getBounds()); }); test('default thinning IS kDefaultPenThinning (export passes the default)', () { final pts = [ pf.PointVector(10, 10, 0.9), pf.PointVector(100, 40, 0.5), pf.PointVector(200, 30, 0.2), ]; final byDefault = freehandOutlinePoints( pfPoints: pts, size: 8, isHighlighter: false, hasRealPressure: true, isComplete: true, ); final explicit = freehandOutlinePoints( pfPoints: pts, size: 8, isHighlighter: false, hasRealPressure: true, isComplete: true, thinning: kDefaultPenThinning, ); expect(byDefault, explicit); }); test('thinning actually changes the outline (param is wired, not ignored)', () { final pts = [ pf.PointVector(10, 10, 0.9), pf.PointVector(100, 40, 0.5), pf.PointVector(200, 30, 0.2), ]; final strong = freehandOutlinePoints( pfPoints: pts, size: 8, isHighlighter: false, hasRealPressure: true, isComplete: true, thinning: kDefaultPenThinning, ); final none = freehandOutlinePoints( pfPoints: pts, size: 8, isHighlighter: false, hasRealPressure: true, isComplete: true, thinning: 0.0, ); expect(strong, isNot(equals(none))); }); test('empty input yields an empty outline (no crash)', () { expect( freehandOutlinePoints( pfPoints: const [], size: 8, isHighlighter: false, hasRealPressure: false, isComplete: true, ), isEmpty, ); }); }