Book-like reading the user explicitly wanted: pairIntoRows groups pages into
two-up spread rows (optional coverAlone for a title page; trailing odd page sits
alone), spreadRowHeights fits each page to half the column and takes the tallest
per row (common baseline), and spreadStackMetrics stacks the rows so the
existing PageStackMetrics.visibleRange windows continuous-DOUBLE by ROW.
Pure geometry reusing the continuous-single layer; the row-mounting widget is
device-gated. Zero-rework-risk (not rendering).
flutter analyze lib/editor clean; 172/172 tests (+8).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends PageStackMetrics with the navigation geometry continuous-single needs:
maxScrollExtent (last page bottom rests at viewport bottom, never negative),
clampScroll, and dominantPageAt — the page covering most of the viewport, which
drives the page-number indicator + thumbnail-grid highlight + jump-to-page (F4).
Pure; clamps past both ends; 0 for empty documents.
flutter analyze lib/editor clean; 129/129 tests (+6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PageStackMetrics + PageWindow: the pure geometry that decides which pages are
mounted for a scroll position (windowed lazy hosting → 60fps on a 300-page doc,
R1). Pages stack vertically with cumulative tops (O(log n) binary-search
visibleRange); a viewport [scroll, scroll+extent) grown by cacheExtent on each
side selects the inclusive intersecting page band, half-open at page boundaries,
clamped to valid indices, empty for empty/over-scrolled-past documents, and
gap-aware (a scroll resting inside an inter-page gap shows no page).
Widget-free + pdfrx-free by design: the page-mounting widget and zoom-settle DPI
refresh are device-gated; only the windowing math is automatable, and it is here
with exhaustive unit coverage (boundaries, cache band, clamping, gaps, empty).
flutter analyze lib/editor clean; 115/115 tests (+13).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>