fix(pdf): live ink follows pen + stop zoom jump
All checks were successful
CI / Windows build (push) Successful in 12m51s
All checks were successful
CI / Windows build (push) Successful in 12m51s
Two critical PDF-editor bugs. 1. Live ink only appeared after lifting the pen. The page overlay painter captured the live stroke as a build-time snapshot, so per-move repaints redrew stale (null) data until commit. Route the live stroke through a ValueNotifier the painter reads at paint time (repaint: merge(overlayRepaint, liveStrokeVN)). 2. Pinch-zoom jumped on Windows touch. pdfrx's internal forked InteractiveViewer scales with an unguarded scaleStart*details.scale that pops on a touch-count blip or one-frame spike. Take over the pinch: scaleEnabled:false (pdfrx keeps 1-finger scroll + wheel), a glitch-guarded ScaleGestureRecognizer drives focal zoom via the pdfrx controller, reusing absolutePinchScale + the re-baseline / per-frame-clamp / focal-jump guards already proven on the note canvas. Zoom + pen feel are device-validated. analyze clean, tests green.
This commit is contained in:
126
test/pen_editor_live_stroke_test.dart
Normal file
126
test/pen_editor_live_stroke_test.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
// Regression guard for Bug 1 ("字迹写完才出现"): the PDF editor's per-page overlay
|
||||
// painter must read the in-progress stroke from a ValueListenable at PAINT time,
|
||||
// so a mid-stroke update repaints the live ink WITHOUT re-running
|
||||
// pageOverlaysBuilder (which only re-runs on setState). The old design captured
|
||||
// the live stroke as a build-time snapshot, so _bumpOverlay repainted the
|
||||
// painter but it still drew the stale (null) snapshot — the stroke only appeared
|
||||
// on pointer-up.
|
||||
//
|
||||
// These tests pump the REAL _PageOverlayPainter (via LiveStrokeOverlayHarness)
|
||||
// and assert (1) paint() reflects the CURRENT notifier value, and (2) updating
|
||||
// the notifier alone drives a CustomPaint repaint with no widget rebuild.
|
||||
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:badnote/editor/canvas/pen_editor_screen.dart';
|
||||
import 'package:badnote/editor/canvas/pen_stroke.dart';
|
||||
import 'package:badnote/editor/engine/brush.dart';
|
||||
|
||||
PenStroke _stroke() => PenStroke(
|
||||
points: const [
|
||||
PenPoint(0.2, 0.2, 0.8),
|
||||
PenPoint(0.5, 0.5, 0.8),
|
||||
PenPoint(0.8, 0.8, 0.8),
|
||||
],
|
||||
color: 0xFF000000,
|
||||
width: 0.01,
|
||||
kind: PenStrokeKind.pen,
|
||||
brush: BrushKind.fountainPen,
|
||||
);
|
||||
|
||||
/// Records the number of drawPath calls a painter issues for the given size.
|
||||
int _drawPathCount(CustomPainter painter, Size size) {
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = _CountingCanvas(Canvas(recorder));
|
||||
painter.paint(canvas, size);
|
||||
recorder.endRecording().dispose();
|
||||
return canvas.drawPathCount;
|
||||
}
|
||||
|
||||
void main() {
|
||||
const size = Size(100, 100);
|
||||
|
||||
test('paint() reads the CURRENT live stroke from the notifier at paint time',
|
||||
() {
|
||||
final harness = LiveStrokeOverlayHarness(pageIndex: 0);
|
||||
addTearDown(harness.dispose);
|
||||
final painter = harness.buildPainter();
|
||||
|
||||
// Idle: nothing to draw.
|
||||
expect(_drawPathCount(painter, size), 0);
|
||||
|
||||
// Mid-stroke: pushing a live stroke into the notifier must make the SAME
|
||||
// painter instance draw it on the next paint (no reconstruction).
|
||||
harness.setLiveStroke(0, _stroke());
|
||||
expect(_drawPathCount(painter, size), 1,
|
||||
reason: 'painter must read the live stroke at paint time, not a '
|
||||
'build-time snapshot');
|
||||
|
||||
// Cleared: back to nothing.
|
||||
harness.setLiveStroke(0, null);
|
||||
expect(_drawPathCount(painter, size), 0);
|
||||
});
|
||||
|
||||
test('a live stroke on a DIFFERENT page is not painted here', () {
|
||||
final harness = LiveStrokeOverlayHarness(pageIndex: 0);
|
||||
addTearDown(harness.dispose);
|
||||
final painter = harness.buildPainter();
|
||||
|
||||
harness.setLiveStroke(3, _stroke()); // belongs to page 3, not page 0
|
||||
expect(_drawPathCount(painter, size), 0);
|
||||
});
|
||||
|
||||
testWidgets('updating the live-stroke notifier repaints without a rebuild',
|
||||
(tester) async {
|
||||
final harness = LiveStrokeOverlayHarness(pageIndex: 0);
|
||||
addTearDown(harness.dispose);
|
||||
|
||||
var builds = 0;
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Builder(builder: (context) {
|
||||
builds++;
|
||||
return CustomPaint(
|
||||
painter: harness.buildPainter(),
|
||||
size: size,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(builds, 1);
|
||||
|
||||
// Pushing a live stroke must trigger a repaint of the CustomPaint via its
|
||||
// merged repaint Listenable — WITHOUT rebuilding the widget tree (which is
|
||||
// exactly the path _bumpOverlay/setState would NOT cover mid-stroke).
|
||||
harness.setLiveStroke(0, _stroke());
|
||||
await tester.pump();
|
||||
expect(builds, 1, reason: 'no widget rebuild should be needed');
|
||||
|
||||
// And the painter now draws the live ink.
|
||||
expect(_drawPathCount(harness.buildPainter(), size), 1);
|
||||
});
|
||||
}
|
||||
|
||||
/// A [Canvas] proxy that counts drawPath calls.
|
||||
class _CountingCanvas implements Canvas {
|
||||
_CountingCanvas(this._inner);
|
||||
|
||||
final Canvas _inner;
|
||||
int drawPathCount = 0;
|
||||
|
||||
@override
|
||||
void drawPath(ui.Path path, ui.Paint paint) {
|
||||
drawPathCount++;
|
||||
_inner.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
void noSuchMethod(Invocation invocation) =>
|
||||
_forward(invocation);
|
||||
|
||||
dynamic _forward(Invocation i) => null;
|
||||
}
|
||||
Reference in New Issue
Block a user