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.
127 lines
4.1 KiB
Dart
127 lines
4.1 KiB
Dart
// 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;
|
|
}
|