Skip to content

fix(layout): give an anchor that spans pages the slice on each of them - #672

Merged
DemchaAV merged 1 commit into
developfrom
fix/resolved-anchor-page-slices
Sep 9, 2026
Merged

DemchaAV merged 1 commit into
developfrom
fix/resolved-anchor-page-slices

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Problem

A LayoutAnchorNode wrapping content taller than a page emitted one resolved anchor per
page it occupied — and every one of them repeated the full measured subtree height.

A section spanning five pages, on a 320×150 page with a 20pt margin (content band 20..130):

page reported y reported height reported top
0 20.000 453.250 473.250
1 20.000 453.250 473.250
2 20.000 453.250 473.250
3 20.000 453.250 473.250
4 91.150 453.250 544.400

Every top is hundreds of points off the page. Nothing could use an anchor on content taller
than one page: not "where does this start on page 2", not "how tall is it here". The
per-page occurrences were there, but each carried a number belonging to no page.

This was written down as a limitation rather than fixed, on the grounds that no consumer
needed the per-page extent yet. One does now, and the numbers turned out to be unusable
rather than merely imprecise.

Fix

Each occurrence reports the wrapped node's actual border-box slice on that page, taken
from the FragmentPlacement the compiler had already computed for it.

The placement was always the answer. A composite's fragments are emitted once per page it
occupies, and CompositeDecoration clamps each segment to that page's content band — the
same geometry that puts a spanning section's border on every page it crosses, per-page
margins included. LayoutAnchorDefinition was discarding it and reading
prepared.measureResult() instead. Reading the placement is also what keeps this one
formula rather than a second one drifting beside it.

Same case, after:

page y height top
0 20.000 110.000 130.000
1 20.000 110.000 130.000
2 20.000 110.000 130.000
3 20.000 110.000 130.000
4 91.150 38.850 130.000

Checked against the thing it has to agree with

An anchored section and an accentLeft section of the same shape, laid out across the same
five pages — the border fragment is the independent implementation of "this box, clamped to
this page", so the anchor slices have to match it:

page accentLeft (y, h) anchor (y, h) Δy Δh
0 20.000, 110.000 20.000, 110.000 +0.000000 +0.000000
1 20.000, 110.000 20.000, 110.000 +0.000000 +0.000000
2 20.000, 110.000 20.000, 110.000 +0.000000 +0.000000
3 20.000, 110.000 20.000, 110.000 +0.000000 +0.000000
4 64.200, 65.800 64.200, 65.800 +0.000000 +0.000000

Page 4 includes the section's own 14pt bottom padding inside the border, and the anchor
reports it too.

Margins come off per edge

A margin is part of the flow extent the wrapper has to occupy but no part of the box being
reported, and a slice only meets the edges it actually contains:

  • the top margin is inside the first page's slice;
  • the bottom margin is inside the last page's slice;
  • a middle page holds neither, so it is the whole content band;
  • a node that fits on one page is both first and last, so it loses both — exactly as
    before;
  • horizontally every slice spans the whole box, so left and right always come off.

Unchanged

  • Anchors on content that fits on one page, which is every anchor that exists today.
    The atomic 8×8 marker, the marker carrying an asymmetric margin, and the marker inside a
    table cell all report the same numbers to 1e-9.
  • Border-box semantics: margin excluded, padding included, the wrapped node's box and not
    its container's.
  • One LayoutAnchorId across every slice — same groupKey, kind and index.
  • No path or name inference anywhere.
  • Public API: nothing added, removed or moved; extract-api --check is current
    without regenerating.

Verification

./mvnw -B -ntp clean verify -pl :graph-compose-core,:graph-compose-render-pdf,:graph-compose-render-docx,:graph-compose-render-pptx,:graph-compose-templates,:graph-compose-testing,:graph-compose-qa,:graph-compose-coverage -am
BUILD SUCCESS. javadoc:javadoc green; the knowledge gates green (surfaces current,
stability doc, claims, routes, bundle verify); the backends' no-op handler tests green.

ResolvedLayoutPassTest gains four cases, each written against a measurement rather than
against the implementation:

  • a multi-page zero-margin anchor: first slice node-top → band bottom, last slice band top
    → node bottom, none of them the whole subtree;
  • a middle page is the whole band;
  • an asymmetric margin comes off the edge its slice contains, and horizontally off all of
    them;
  • per-page margins: a page the rule widened to 40pt reports a 40..160 band, not the
    document default.

Proven fail-closed by putting measureResult() back: those four go red and the identity
case stays green, which is right — identity was never the broken part.

Lane: shared-engine — document.layout only. No Timeline file is touched; the word
does not appear in the diff.

A resolved anchor reported the anchored subtree's whole height on every page it
crossed. A section spanning five pages said 453.25pt five times, with tops
hundreds of points outside the page, so nothing could use an anchor on anything
taller than a page. That was written down as a limitation rather than fixed
because no consumer needed the per-page extent; one does now, and the numbers
were unusable rather than merely imprecise.

The placement was already the answer. A composite's fragments are emitted once
per page it occupies, and CompositeDecoration clamps each segment to that page's
content band — the geometry that puts a spanning section's border on every page
it crosses, per-page margins included. The definition was throwing that away and
reading the measurement instead. Reading the placement is also what keeps this
one formula instead of a second one drifting beside it.

The margin comes off per edge, which the slice makes necessary. It is part of
the flow extent the wrapper occupies but no part of the box reported, and a
slice only meets the edges it contains: the top margin is inside the first
page's slice, the bottom margin inside the last page's, a middle page holds
neither, and a node that fits on one page is both so it loses both. Horizontal
margins come off every slice, as before.

Anchors on content that fits on one page are unchanged, which is every anchor
that exists today — the atomic marker, the marker with an asymmetric margin, the
marker in a table cell all report the same numbers to 1e-9, and no snapshot or
pixel baseline moved.

Measured against the thing it has to agree with: an anchored section and an
accentLeft section of the same shape, across five pages, now differ by
dy=0.000000 dh=0.000000 on every one.
@DemchaAV
DemchaAV force-pushed the fix/resolved-anchor-page-slices branch from dd1e61d to 362ebfb Compare September 9, 2026 19:57
@DemchaAV
DemchaAV merged commit 7314323 into develop Sep 9, 2026
12 checks passed
@DemchaAV
DemchaAV deleted the fix/resolved-anchor-page-slices branch September 9, 2026 20:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant