Some checks failed
CI / Windows build (push) Has been cancelled
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>
76 lines
2.6 KiB
Dart
76 lines
2.6 KiB
Dart
// lib/editor/layout/double_page_layout.dart
|
|
//
|
|
// Two-up (spread) layout foundation for book-like reading (F2/F3 — the user's
|
|
// explicit "two-page spread" ask). Pages are grouped into spread ROWS; each row
|
|
// then stacks vertically exactly like continuous-single, so windowing reuses
|
|
// PageStackMetrics on the row heights.
|
|
//
|
|
// Pure geometry (no widgets / pdfrx): the row-mounting widget is device-gated;
|
|
// the pairing + row-height math is here, fully unit-tested. Building it ahead of
|
|
// its phase is zero-rework-risk (it is not rendering and the P0.5 perf gate
|
|
// can't invalidate pure geometry).
|
|
|
|
import 'page_viewport.dart';
|
|
import '../pdf/pdf_document_source.dart';
|
|
|
|
/// Groups page indices into two-up spread rows. With [coverAlone], page 0 sits
|
|
/// on its own row (a book cover / title page) and the rest pair 1-2, 3-4, …;
|
|
/// otherwise pages pair from 0. A trailing odd page occupies a single-page row.
|
|
List<List<int>> pairIntoRows(int pageCount, {bool coverAlone = false}) {
|
|
assert(pageCount >= 0);
|
|
final rows = <List<int>>[];
|
|
var i = 0;
|
|
if (coverAlone && pageCount > 0) {
|
|
rows.add(<int>[0]);
|
|
i = 1;
|
|
}
|
|
while (i < pageCount) {
|
|
if (i + 1 < pageCount) {
|
|
rows.add(<int>[i, i + 1]);
|
|
i += 2;
|
|
} else {
|
|
rows.add(<int>[i]);
|
|
i += 1;
|
|
}
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
/// Height of each spread row when each page is fit to HALF of [columnWidth]
|
|
/// (two pages share the column). A row's height is the tallest of its pages so
|
|
/// both pages sit on a common baseline. Non-positive page widths contribute 0.
|
|
List<double> spreadRowHeights(
|
|
PageDocumentSource source,
|
|
double columnWidth,
|
|
List<List<int>> rows,
|
|
) {
|
|
assert(columnWidth >= 0);
|
|
final half = columnWidth / 2;
|
|
return rows.map((row) {
|
|
var tallest = 0.0;
|
|
for (final pageIndex in row) {
|
|
final size = source.pageSize(pageIndex);
|
|
if (size.width <= 0) continue;
|
|
final fit = half * (size.height / size.width);
|
|
if (fit > tallest) tallest = fit;
|
|
}
|
|
return tallest;
|
|
}).toList(growable: false);
|
|
}
|
|
|
|
/// Continuous-DOUBLE stacking metrics: pair pages into spread rows, then stack
|
|
/// the rows vertically. [PageStackMetrics.visibleRange] over the result yields
|
|
/// the visible ROW range; map rows back to pages via the [pairIntoRows] result.
|
|
PageStackMetrics spreadStackMetrics(
|
|
PageDocumentSource source,
|
|
double columnWidth, {
|
|
bool coverAlone = false,
|
|
double gap = 0.0,
|
|
}) {
|
|
final rows = pairIntoRows(source.pageCount, coverAlone: coverAlone);
|
|
return PageStackMetrics(
|
|
pageHeights: spreadRowHeights(source, columnWidth, rows),
|
|
gap: gap,
|
|
);
|
|
}
|