Files
BadNote/test/pen_eraser_mode_widget_test.dart
Akiba So 4fb431727e
Some checks failed
CI / Windows build (push) Has been cancelled
feat(eraser): configurable size + stroke-eraser mode
"优化橡皮擦工具,你优化在哪" — the eraser already did segment erase, but
the radius was a hardcoded const with no size control and no whole-stroke
mode. Add both, OneNote/Notability-style:

- PenConfig: eraserRadius (0.005-0.1, default 0.02) + eraserWholeStroke
  bool, with copyWith / JSON / setters.
- PenCanvas: uses widget.eraserRadius for the live hit area AND the cursor
  preview (they stay in sync); eraserWholeStroke=true removes the whole
  stroke on contact, false keeps the segment-split behavior.
- pen_editor threads both from PenConfig.
- pen-settings: new Eraser section — size slider + "Stroke Eraser" switch.

Tests: pen_eraser_mode_widget proves point-eraser keeps the untouched ends
while stroke-eraser deletes the whole stroke from the same pass.

flutter analyze: 0 issues.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:47:58 +08:00

102 lines
3.4 KiB
Dart

// Guards the two eraser modes (the "优化橡皮擦工具" work): the default point
// eraser cuts a stroke into surviving segments, while the new stroke-eraser
// mode removes the entire stroke on contact. Both are driven by the same
// stylus pass; only the eraserWholeStroke flag differs.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:badnote/editor/canvas/pen_canvas.dart';
import 'package:badnote/editor/canvas/pen_stroke.dart';
void main() {
const pageSize = Size(400, 600);
// A long horizontal stroke spanning the page through the vertical center.
PenStroke longStroke() => PenStroke(
points: const [
PenPoint(0.1, 0.5, 0.5),
PenPoint(0.3, 0.5, 0.5),
PenPoint(0.5, 0.5, 0.5),
PenPoint(0.7, 0.5, 0.5),
PenPoint(0.9, 0.5, 0.5),
],
color: 0xFF000000,
width: 0.004,
kind: PenStrokeKind.pen,
);
Widget host({
required void Function(int, List<PenStroke>) onErase,
required bool wholeStroke,
}) {
final controller = TransformationController();
addTearDown(controller.dispose);
return MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
width: pageSize.width,
height: pageSize.height,
child: PenCanvas(
pageWidget: Container(color: const Color(0xFFEEEEEE)),
pageSize: pageSize,
strokes: [longStroke()],
transformationController: controller,
tool: CanvasTool.eraser,
color: const Color(0xFF000000),
strokeWidth: 0.004,
eraserWholeStroke: wholeStroke,
onStrokeComplete: (_) {},
onEraseStroke: onErase,
),
),
),
),
);
}
// Drag the eraser vertically through the page center (normalized 0.5, 0.5),
// grazing the MIDDLE of the long horizontal stroke.
Future<void> erasePass(WidgetTester tester) async {
final center = tester.getCenter(find.byType(PenCanvas));
final g = await tester.startGesture(center + const Offset(0, -15),
kind: PointerDeviceKind.stylus);
await g.moveBy(const Offset(0, 15));
await g.moveBy(const Offset(0, 15));
await g.up();
await tester.pump();
}
testWidgets('point eraser (default) splits the stroke into surviving pieces',
(tester) async {
List<PenStroke>? replacement;
await tester.pumpWidget(host(
onErase: (_, pieces) => replacement = pieces,
wholeStroke: false,
));
await erasePass(tester);
expect(replacement, isNotNull, reason: 'an erase should have fired');
// Grazing the middle leaves the two ends as surviving sub-strokes.
expect(replacement!.length, greaterThanOrEqualTo(1));
expect(replacement, isNotEmpty,
reason: 'point eraser keeps the untouched ends');
});
testWidgets('stroke eraser removes the entire stroke on contact',
(tester) async {
List<PenStroke>? replacement;
await tester.pumpWidget(host(
onErase: (_, pieces) => replacement = pieces,
wholeStroke: true,
));
await erasePass(tester);
expect(replacement, isNotNull, reason: 'an erase should have fired');
expect(replacement, isEmpty,
reason: 'stroke eraser deletes the whole stroke, leaving no pieces');
});
}