From b11209aedb4bc0282dca9cacbf71fc9cc362dea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TATSUNO=20=E2=80=9CTaz=E2=80=9D=20Yasuhiro?= Date: Wed, 7 Oct 2026 10:45:26 +0900 Subject: [PATCH 1/2] test(renderer): snapshot a view whose rows lie past 2^15 pt in the new engine The snapshot records the current, broken output. The new engine does not split the view, so every row is drawn on one page over the header. --- packages/renderer/tests/pageWrap.test.jsx | 36 ++++++++++++++++++ ...iew-whose-rows-lie-past-2-15-pt-1-snap.png | Bin 0 -> 748 bytes 2 files changed, 36 insertions(+) create mode 100644 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 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 0000000000000000000000000000000000000000..1c46c51286fbb340459afc8f92dc76122479266f GIT binary patch literal 748 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yu@pObhHwBu4M$1`kk42gJ=A9v;+!|9?UO$q`WOpT-y2J`k;J$`6mVNh3Fr!HWz!CrRzX7dA^?l_{@ zhp-uw)w6o~^tzp`%#SaB|IXCN{CmFpcM?rN_8F%6B+2%Oub)3Rt-m3(m;Zhp2QYy4 zpFfwL-;lbq*+GDVrI8Fm;`X*m3qHO#_4{Mak{r_iJli+a|M^+=jV)rDvKU$Rlj&@P gHlPQt<8 literal 0 HcmV?d00001 From 923327224a91d1087552aa88305fcfa8704bf693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?TATSUNO=20=E2=80=9CTaz=E2=80=9D=20Yasuhiro?= Date: Wed, 7 Oct 2026 10:47:16 +0900 Subject: [PATCH 2/2] fix(paginate): split columns whose children lie past 2^15 pt Yoga lays out in float32. Past 2^15 pt a position is only exact to about 1/256 pt, so rounding alone can make touching siblings overlap by more than the fixed 0.001 pt tolerance. The column was then treated as wrapped and became one unsplittable leaf. Scale the tolerance with the position. --- .changeset/paginate-float32-overlap.md | 5 +++ packages/paginate/src/paginator/toItems.ts | 9 +++++- packages/paginate/tests/flow.test.ts | 29 ++++++++++++++++++ ...iew-whose-rows-lie-past-2-15-pt-1-snap.png | Bin 748 -> 2377 bytes 4 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 .changeset/paginate-float32-overlap.md 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/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 index 1c46c51286fbb340459afc8f92dc76122479266f..bfba13dc49092ae90880194eeb1fde106e11381c 100644 GIT binary patch literal 2377 zcmeAS@N?(olHy`uVBq!ia0vp^Cm0x*?Ks$gtoUj_b|A%4?Bp530R%N1DIGvQV{wqX z6T`Z5GA|hzI6iy2IEGZrd3$$ZS4g5v`$NmqjEfTtbi}$tS$rSq=6vGPIks2%v)O82 zmV-)GV)_ZkcDOo+(2J{P@T-{!pNpHrvn_ZiH2TJbO8Hsk+Nvnqx&J_-UXOpZhn zi8r^Mt-th;MVzI{L4jkG7@}ctsLDppJ-;DyC-!K+)_?u<3tj`=K^Mz9yYJ>n+cKqX zy|-__%wEQ2Pu0a(8XW{U=ti_W{q%Hl?iY?3S3l+bcT*@h^y~Y(ub`mH7vNxOR2bgG z>4yb3-1-|LaiyBSZ?962YIMZc$NgyFo3mlh-!HzvkgY0yes2(l?CbC6=eq&3&f4ep z`}^)OT#kyj|7%>u>~mV0uBG7``ET}MXT+bA13N=L_KrL#NAMC|mJ?g%(?ngOr3~Ng z7lH=b*vpthyY}yu0(qy2p1B)bjI@0HzJB&a>4d1$o1gb@zo?uLc6wVbQ={-6$01tS d5AB-zko{_7`=`x|89y@sfv2mV%Q~loCIH9w@izbf delta 319 zcmX>p^oDhU7&pTS4mJh`hP4sv=1o+TuVEaktG3V_a!z?jJhPH>Ykv^JBJQS-s zWDYRjh=`2rs(qLEl5tN{Lh91i)=Z|Vh6NMM_9~os&w6M3T+MmUS>it*;yHb8NA;g4 zTNwX;P58zTG1EbTgQW>h9NMva-$z*%zBlpmacAx^oGv=jq#(e-)CeXfOS9-o8qC{Y z_4uKMg+X0yow|U<27B4gm(#cD6Ddwf6Q5k&65RLwE6yd zwr{Ba^Rw(5Tf{VFvB`zZ0h1e;)#QMx!Djts76?%~zopr03)i2 AE&u=k