diff --git a/.changeset/paginate-float32-overlap.md b/.changeset/paginate-float32-overlap.md
new file mode 100644
index 000000000..083ca07d2
--- /dev/null
+++ b/.changeset/paginate-float32-overlap.md
@@ -0,0 +1,5 @@
+---
+'@react-pdf/paginate': patch
+---
+
+Split a column whose children lie past 2^15 pt. Yoga's float32 rounding made touching siblings overlap there, so the column was mistaken for a wrapped one and drawn on a single page.
diff --git a/packages/paginate/src/paginator/toItems.ts b/packages/paginate/src/paginator/toItems.ts
index b17200b46..522fba0f5 100644
--- a/packages/paginate/src/paginator/toItems.ts
+++ b/packages/paginate/src/paginator/toItems.ts
@@ -182,11 +182,18 @@ const rowOf = (node: ContainerNode): Item => {
]);
};
+// Yoga lays out in float32, so a position is only exact to about 2^-23 of its
+// magnitude. Past 2^15 pt that exceeds 0.001 and touching siblings overlap.
+const overlapTolerance = (y: number) => Math.max(0.001, Math.abs(y) * 2 ** -22);
+
// A column whose children overlap vertically wrapped into visual columns:
// its boxes no longer describe a vertical flow, so there is nowhere sane to
// break inside it. Rows are exempt — their cells share a y by design.
const isWrapped = (flow: FlowNode[]) =>
- flow.some((_, index) => gapBefore(flow, index) < -0.001);
+ flow.some(
+ (child, index) =>
+ gapBefore(flow, index) < -overlapTolerance(outerTop(child)),
+ );
const columnOf = (node: ContainerNode): Item => {
const flow = node.children.filter((child) => !isAbsolute(child));
diff --git a/packages/paginate/tests/flow.test.ts b/packages/paginate/tests/flow.test.ts
index 200b604d7..3f60dbc0e 100644
--- a/packages/paginate/tests/flow.test.ts
+++ b/packages/paginate/tests/flow.test.ts
@@ -173,6 +173,35 @@ describe('flow to items', () => {
expect(item.split).toBeUndefined();
});
+ test('float32 rounding past 2^15 pt does not count as overlap', () => {
+ const first = leaf(32_928.015625, Math.fround(47.064));
+ const second = leaf(Math.fround(32_928.015625 + 47.064), 60);
+
+ expect(second.box.top).toBeLessThan(first.box.top + first.box.height);
+
+ const container: ContainerNode = {
+ box: box(32_900, 200),
+ direction: 'column',
+ children: [first, second],
+ };
+
+ const [item] = toItems([container]).children;
+
+ expect(item.kind).toBe('column');
+ });
+
+ test('a column wrapped past 2^15 pt still demotes to a leaf', () => {
+ const container: ContainerNode = {
+ box: box(32_900, 100),
+ direction: 'column',
+ children: [leaf(32_900, 80), leaf(32_930, 80)],
+ };
+
+ const [item] = toItems([container]).children;
+
+ expect(item.kind).toBe('leaf');
+ });
+
test('lazy materializes into converted items', () => {
const node: LazyNode = {
box: box(0, 100),
diff --git a/packages/renderer/tests/pageWrap.test.jsx b/packages/renderer/tests/pageWrap.test.jsx
index b10f5b902..99c4c56fd 100644
--- a/packages/renderer/tests/pageWrap.test.jsx
+++ b/packages/renderer/tests/pageWrap.test.jsx
@@ -5,6 +5,7 @@ import {
Font,
Page,
Text,
+ View,
Image,
StyleSheet,
} from '@react-pdf/renderer';
@@ -262,3 +263,38 @@ describe('pageWrap', () => {
expect(image).toMatchImageSnapshot();
}, 30_000);
});
+
+const ROW_HEIGHTS = [60.096, 47.064, 117.16];
+const ROW_COLORS = ['#a6cee3', '#fdbf6f', '#b2df8a'];
+
+// Past 2 ** 15 pt yoga's float32 positions round enough for adjacent rows to
+// overlap. The auto margin is a gap that collapses at the page break, so the
+// rows get that far down in a few pages.
+const FarDownRows = (pageProps) => (
+
+
+
+
+ {ROW_HEIGHTS.concat(ROW_HEIGHTS).map((height, index) => (
+
+ ))}
+
+
+
+);
+
+describe('pageWrap (next engine)', () => {
+ test('should split a view whose rows lie past 2^15 pt', async () => {
+ expect(
+ await renderToImage(),
+ ).toMatchImageSnapshot();
+ });
+});
diff --git a/packages/renderer/tests/snapshots/page-wrap-test-jsx-tests-page-wrap-test-jsx-page-wrap-next-engine-should-split-a-view-whose-rows-lie-past-2-15-pt-1-snap.png b/packages/renderer/tests/snapshots/page-wrap-test-jsx-tests-page-wrap-test-jsx-page-wrap-next-engine-should-split-a-view-whose-rows-lie-past-2-15-pt-1-snap.png
new file mode 100644
index 000000000..bfba13dc4
Binary files /dev/null and b/packages/renderer/tests/snapshots/page-wrap-test-jsx-tests-page-wrap-test-jsx-page-wrap-next-engine-should-split-a-view-whose-rows-lie-past-2-15-pt-1-snap.png differ