feat(p0.5): PageDocumentSource seam + fit-to-width metrics glue (step 10)
Some checks failed
CI / Windows build (push) Has been cancelled
Some checks failed
CI / Windows build (push) Has been cancelled
Connects the two pure P0.5 pieces: a minimal PageDocumentSource abstraction (pageCount + pageSize, a pdfrx PdfDocument in production) and pageStackMetricsForWidth() which fits every page to a single column width (continuous-single) — height = columnWidth × aspect — feeding PageStackMetrics.visibleRange. Defensive against non-positive page width. This makes the windowing math consumable + unit-testable against a fake source (no pdfium/GPU); the production pdfrx adapter is the thin device-side wrapper added with the page-mounting widget. flutter analyze lib/editor clean; 120/120 tests (+5: fit-to-width, gap, empty, zero-width guard, windowing composition). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
45
lib/editor/pdf/pdf_document_source.dart
Normal file
45
lib/editor/pdf/pdf_document_source.dart
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// lib/editor/pdf/pdf_document_source.dart
|
||||||
|
//
|
||||||
|
// A minimal abstraction over a paginated document (a pdfrx PdfDocument in
|
||||||
|
// production) consumed by the layout + render layer. Keeping the layout math
|
||||||
|
// behind this seam lets continuous-single windowing (layout/page_viewport.dart)
|
||||||
|
// and tiling be unit-tested against a fake source — no pdfium, no GPU, no real
|
||||||
|
// PDF (the production pdfrx adapter is a thin device-side wrapper added with the
|
||||||
|
// viewport widget / page_tile renderer, which are device-gated).
|
||||||
|
|
||||||
|
import 'dart:ui' show Size;
|
||||||
|
|
||||||
|
import '../layout/page_viewport.dart';
|
||||||
|
|
||||||
|
/// Read-only page geometry for a paginated document.
|
||||||
|
abstract class PageDocumentSource {
|
||||||
|
/// Number of pages (>= 0).
|
||||||
|
int get pageCount;
|
||||||
|
|
||||||
|
/// Intrinsic size of page [index] in PDF points (width/height > 0).
|
||||||
|
Size pageSize(int index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds continuous-single stacking metrics by fitting every page to a single
|
||||||
|
/// [columnWidth] (fit-to-width, the continuous-single mode): each page's
|
||||||
|
/// laid-out height is `columnWidth × (pageHeight / pageWidth)`, preserving its
|
||||||
|
/// aspect ratio. [gap] is inserted between pages (content units).
|
||||||
|
///
|
||||||
|
/// Pages reporting a non-positive width are treated as zero-height (defensive;
|
||||||
|
/// real pages always have a positive width) so a malformed page can't throw.
|
||||||
|
///
|
||||||
|
/// Returns the metrics needed by [PageStackMetrics.visibleRange]; pair this with
|
||||||
|
/// the device-gated page-mounting widget.
|
||||||
|
PageStackMetrics pageStackMetricsForWidth(
|
||||||
|
PageDocumentSource source,
|
||||||
|
double columnWidth, {
|
||||||
|
double gap = 0.0,
|
||||||
|
}) {
|
||||||
|
assert(columnWidth >= 0);
|
||||||
|
final heights = List<double>.generate(source.pageCount, (i) {
|
||||||
|
final size = source.pageSize(i);
|
||||||
|
if (size.width <= 0) return 0.0;
|
||||||
|
return columnWidth * (size.height / size.width);
|
||||||
|
});
|
||||||
|
return PageStackMetrics(pageHeights: heights, gap: gap);
|
||||||
|
}
|
||||||
64
test/pdf_document_source_test.dart
Normal file
64
test/pdf_document_source_test.dart
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// Tests for the document→layout glue (P0.5 step 10, automatable slice):
|
||||||
|
// fit-to-width metrics built from a fake PageDocumentSource. No pdfium.
|
||||||
|
|
||||||
|
import 'dart:ui' show Size;
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:badnote/editor/layout/page_viewport.dart';
|
||||||
|
import 'package:badnote/editor/pdf/pdf_document_source.dart';
|
||||||
|
|
||||||
|
class _FakeSource implements PageDocumentSource {
|
||||||
|
_FakeSource(this._sizes);
|
||||||
|
final List<Size> _sizes;
|
||||||
|
@override
|
||||||
|
int get pageCount => _sizes.length;
|
||||||
|
@override
|
||||||
|
Size pageSize(int index) => _sizes[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('fit-to-width scales each page height by its aspect at the column width',
|
||||||
|
() {
|
||||||
|
// A4-ish portrait 600×800 and a square 500×500 at column width 300.
|
||||||
|
final source = _FakeSource([const Size(600, 800), const Size(500, 500)]);
|
||||||
|
final m = pageStackMetricsForWidth(source, 300);
|
||||||
|
expect(m.pageCount, 2);
|
||||||
|
expect(m.heightOf(0), closeTo(300 * 800 / 600, 1e-9)); // 400
|
||||||
|
expect(m.heightOf(1), closeTo(300.0, 1e-9)); // square → 300
|
||||||
|
expect(m.totalExtent, closeTo(700, 1e-9));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gap is threaded into the metrics', () {
|
||||||
|
final source = _FakeSource([const Size(100, 100), const Size(100, 100)]);
|
||||||
|
final m = pageStackMetricsForWidth(source, 100, gap: 25);
|
||||||
|
expect(m.offsetOf(1), closeTo(125, 1e-9)); // 100 + 25 gap
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty document yields empty metrics', () {
|
||||||
|
final m = pageStackMetricsForWidth(_FakeSource(const []), 300);
|
||||||
|
expect(m.pageCount, 0);
|
||||||
|
expect(m.totalExtent, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-positive page width is treated as zero height (no divide-by-zero)',
|
||||||
|
() {
|
||||||
|
final source = _FakeSource([const Size(0, 800), const Size(400, 200)]);
|
||||||
|
final m = pageStackMetricsForWidth(source, 300);
|
||||||
|
expect(m.heightOf(0), 0.0);
|
||||||
|
expect(m.heightOf(1), closeTo(150, 1e-9)); // 300 * 200/400
|
||||||
|
});
|
||||||
|
|
||||||
|
test('windowing composes on top of the fit-to-width metrics', () {
|
||||||
|
// 5 portrait pages 600×800 at width 300 → each 400 tall, total 2000.
|
||||||
|
final source =
|
||||||
|
_FakeSource(List.filled(5, const Size(600, 800)));
|
||||||
|
final m = pageStackMetricsForWidth(source, 300);
|
||||||
|
expect(m.totalExtent, closeTo(2000, 1e-9));
|
||||||
|
// viewport [350, 850) → page1 [400,800) and page2 [800,1200) just touched?
|
||||||
|
// page0 [0,400), page1 [400,800), page2 [800,1200). window [350,850]:
|
||||||
|
// first bottom>350 → page0 bottom 400>350 → 0; last top<850 → page2 top
|
||||||
|
// 800<850 → 2.
|
||||||
|
expect(m.visibleRange(350, 500), const PageWindow(0, 2));
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user