feat(split): scratchpad inks on the pen-first canvas
All checks were successful
CI / Windows build (push) Successful in 20m37s

The split-view scratchpad was the last surface on the old ink_canvas. Move
its inking engine to the performant PenCanvas while keeping the infinite
auto-expanding world.

Key idea: store strokes in absolute WORLD pixels (InkStroke — unchanged
saveScratchpad format) and render through PenCanvas by normalizing against
the CURRENT world size. When the world auto-expands, stored world coords do
not move — only the normalization divisor grows — so ink stays put with zero
drift (proven by the world-expand-stability test).

- Replace InteractiveViewer+SizedBox+InkCanvas with PenCanvas (own pan/zoom,
  minScale 0.1 to survey the big world); keep the AnnotationToolbar.
- Stroke callbacks go through ink_stroke_adapter; load filters to freehand
  so the canvas list stays 1:1 with the undo manager.
- Pen/highlighter/eraser map from the toolbar's PenTool; width is world px.

The left PDF-reference pane (SfPdfViewer, read-only) is unchanged.

Tests: world-expand stability added. flutter analyze: 0. Suite: 270/270.
This commit is contained in:
2026-06-23 15:52:52 +08:00
parent ffb9e35755
commit 0ae2671f9b
2 changed files with 86 additions and 30 deletions

View File

@@ -83,6 +83,31 @@ void main() {
}
});
test('world coords are stable when the scratchpad world expands', () {
// A stroke drawn at the center of a 4000-world: normalized 0.5 -> world
// 2000. This is how the split-view scratchpad stores ink.
final pen0 = PenStroke(
points: const [PenPoint(0.5, 0.5, 0.6), PenPoint(0.6, 0.6, 0.6)],
color: 0xFF000000,
width: 0.002,
kind: PenStrokeKind.pen,
);
final stored = inkStrokeFromPen(pen0, const Size(4000, 4000),
id: 'w', createdAt: t0);
expect(stored.points.first.x, closeTo(2000, 1e-6));
expect(stored.points.first.y, closeTo(2000, 1e-6));
// The world auto-expands to 8000. The STORED world coords are untouched;
// only the render-time normalization divisor changes.
final pen1 = penStrokeFromInk(stored, const Size(8000, 8000))!;
expect(pen1.points.first.x, closeTo(0.25, 1e-9)); // 2000 / 8000
expect(pen1.points.first.y, closeTo(0.25, 1e-9));
// Crucially, the ABSOLUTE on-page position is unchanged: normalized *
// pageWidth = 0.25 * 8000 = 2000 == the original world x. Zero drift.
expect(pen1.points.first.x * 8000, closeTo(2000, 1e-6));
});
test('penStrokesFromInk preserves order and filters non-freehand', () {
final strokes = [
ink([ip(0, 0), ip(1, 1)], tool: PenTool.pen, color: 0xFF000001),