feat(eraser): configurable size + stroke-eraser mode
Some checks failed
CI / Windows build (push) Has been cancelled
Some checks failed
CI / Windows build (push) Has been cancelled
"优化橡皮擦工具,你优化在哪" — 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>
This commit is contained in:
101
test/pen_eraser_mode_widget_test.dart
Normal file
101
test/pen_eraser_mode_widget_test.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
// 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');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user