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