From a0a56b4ce26977b0c0ebac47a85e06c0819edf2b Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sat, 11 Jul 2026 01:12:20 +0100 Subject: [PATCH] refactor(engine): extract composite bleed-box geometry into CompositeDecoration compileComposite resolved the decoration (fill / border) box inline: four coordinates, each adjusted per bled edge against the page dimensions. That is pure geometry and belongs with the decoration owner. Move it onto CompositeDecoration.resolveBleedBox, returning a BleedBox record; compileComposite now resolves the box in one call and passes it to fill, and drops the now-unused DocumentEdge import. Byte-identical placement, and the per-edge math gets direct unit coverage in CompositeDecorationTest. --- .../document/layout/CompositeDecoration.java | 56 +++++++++++++++++++ .../document/layout/LayoutCompiler.java | 39 +++---------- .../layout/CompositeDecorationTest.java | 52 +++++++++++++++++ 3 files changed, 117 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/demcha/compose/document/layout/CompositeDecoration.java b/src/main/java/com/demcha/compose/document/layout/CompositeDecoration.java index 9f68e598..0b7d31b8 100644 --- a/src/main/java/com/demcha/compose/document/layout/CompositeDecoration.java +++ b/src/main/java/com/demcha/compose/document/layout/CompositeDecoration.java @@ -30,6 +30,62 @@ final class CompositeDecoration { private CompositeDecoration() { } + /** + * Resolves the decoration (fill / border) box for a composite. Without bleed + * the box is the in-margin content geometry; each declared bleed edge instead + * snaps that side of the box to the trimmed page edge (left/right adjust the x + * span, top/bottom the y span) while the children stay in the content region. + * + * @param bleed the node's bleed edges + * @param x content-box left (in-margin) + * @param width content-box width (in-margin) + * @param topY content-box top y (in-margin) + * @param bottomY content-box bottom y (in-margin) + * @param canvas the page canvas, for the trimmed page-edge coordinates + * @return the resolved decoration box + */ + static BleedBox resolveBleedBox(DocumentBleed bleed, + double x, + double width, + double topY, + double bottomY, + LayoutCanvas canvas) { + double decorX = x; + double decorWidth = width; + double decorTopY = topY; + double decorBottomY = bottomY; + if (bleed.any()) { + double pageWidth = canvas.width(); + double pageHeight = canvas.height(); + if (bleed.bleeds(DocumentEdge.LEFT)) { + decorWidth += decorX; + decorX = 0.0; + } + if (bleed.bleeds(DocumentEdge.RIGHT)) { + decorWidth = Math.max(0.0, pageWidth - decorX); + } + if (bleed.bleeds(DocumentEdge.TOP)) { + decorTopY = pageHeight; + } + if (bleed.bleeds(DocumentEdge.BOTTOM)) { + decorBottomY = 0.0; + } + } + return new BleedBox(decorX, decorWidth, decorTopY, decorBottomY); + } + + /** + * A resolved composite decoration box — the fill / border extent after content + * bleed, in page space. + * + * @param x decoration box left + * @param width decoration box width + * @param topY decoration box top y + * @param bottomY decoration box bottom y + */ + record BleedBox(double x, double width, double topY, double bottomY) { + } + static List fill(PreparedNode prepared, NodeDefinition definition, String path, diff --git a/src/main/java/com/demcha/compose/document/layout/LayoutCompiler.java b/src/main/java/com/demcha/compose/document/layout/LayoutCompiler.java index ee196167..40700f17 100644 --- a/src/main/java/com/demcha/compose/document/layout/LayoutCompiler.java +++ b/src/main/java/com/demcha/compose/document/layout/LayoutCompiler.java @@ -8,7 +8,6 @@ import com.demcha.compose.document.node.RowNode; import com.demcha.compose.document.node.RowVerticalAlign; import com.demcha.compose.document.style.DocumentBleed; -import com.demcha.compose.document.style.DocumentEdge; import com.demcha.compose.engine.components.style.Margin; import com.demcha.compose.engine.components.style.Padding; import org.slf4j.Logger; @@ -320,32 +319,12 @@ private void compileComposite(PreparedNode prepared, int endPage = state.pageIndex; double endPageBottomY = state.pageTop() - state.usedHeight + margin.bottom(); - // Content bleed: the decoration box (fill/border) extends to the trimmed - // page edge on the declared edges, while children stay in the content - // region (so text never runs off the page). Byte-identical when the node - // does not bleed — every value below collapses to the in-margin geometry. + // Content bleed extends the decoration box to the trimmed page edge on the + // declared edges while children stay in the content region; without bleed the + // box is the in-margin geometry (see CompositeDecoration#resolveBleedBox). DocumentBleed bleed = node.bleed(); - double decorX = placementX; - double decorWidth = naturalMeasure.width(); - double decorTopY = placementTopY; - double decorBottomY = endPageBottomY; - if (bleed.any()) { - double pageWidth = state.canvas.width(); - double pageHeight = state.canvas.height(); - if (bleed.bleeds(DocumentEdge.LEFT)) { - decorWidth += decorX; - decorX = 0.0; - } - if (bleed.bleeds(DocumentEdge.RIGHT)) { - decorWidth = Math.max(0.0, pageWidth - decorX); - } - if (bleed.bleeds(DocumentEdge.TOP)) { - decorTopY = pageHeight; - } - if (bleed.bleeds(DocumentEdge.BOTTOM)) { - decorBottomY = 0.0; - } - } + CompositeDecoration.BleedBox decor = CompositeDecoration.resolveBleedBox( + bleed, placementX, naturalMeasure.width(), placementTopY, endPageBottomY, state.canvas); List decorationFragments = CompositeDecoration.fill( prepared, definition, @@ -353,10 +332,10 @@ private void compileComposite(PreparedNode prepared, parentPath, childIndex, depth, - decorX, - decorTopY, - decorBottomY, - decorWidth, + decor.x(), + decor.topY(), + decor.bottomY(), + decor.width(), startPage, endPage, margin, diff --git a/src/test/java/com/demcha/compose/document/layout/CompositeDecorationTest.java b/src/test/java/com/demcha/compose/document/layout/CompositeDecorationTest.java index 5d96de73..dc09cd4c 100644 --- a/src/test/java/com/demcha/compose/document/layout/CompositeDecorationTest.java +++ b/src/test/java/com/demcha/compose/document/layout/CompositeDecorationTest.java @@ -174,4 +174,56 @@ void toPlacedFragmentsReindexesFragmentIndexPerPlacement() { assertThat(f.pageIndex()).isEqualTo(3); }); } + + // resolveBleedBox: content box x=20 w=100, top=80 bottom=30, on the 200×100 canvas. + + @Test + void resolveBleedBoxWithoutBleedReturnsTheInMarginBox() { + CompositeDecoration.BleedBox box = CompositeDecoration.resolveBleedBox( + DocumentBleed.none(), 20, 100, 80, 30, canvas); + assertThat(box.x()).isCloseTo(20, within(EPS)); + assertThat(box.width()).isCloseTo(100, within(EPS)); + assertThat(box.topY()).isCloseTo(80, within(EPS)); + assertThat(box.bottomY()).isCloseTo(30, within(EPS)); + } + + @Test + void resolveBleedBoxLeftSnapsOriginToZeroAndAbsorbsTheLeftGap() { + CompositeDecoration.BleedBox box = CompositeDecoration.resolveBleedBox( + DocumentBleed.of(DocumentEdge.LEFT), 20, 100, 80, 30, canvas); + assertThat(box.x()).isCloseTo(0, within(EPS)); + assertThat(box.width()).isCloseTo(120, within(EPS)); // 100 + 20 + assertThat(box.topY()).isCloseTo(80, within(EPS)); + assertThat(box.bottomY()).isCloseTo(30, within(EPS)); + } + + @Test + void resolveBleedBoxRightStretchesWidthToThePageEdge() { + CompositeDecoration.BleedBox box = CompositeDecoration.resolveBleedBox( + DocumentBleed.of(DocumentEdge.RIGHT), 20, 100, 80, 30, canvas); + assertThat(box.x()).isCloseTo(20, within(EPS)); + assertThat(box.width()).isCloseTo(180, within(EPS)); // pageWidth 200 - x 20 + assertThat(box.topY()).isCloseTo(80, within(EPS)); + assertThat(box.bottomY()).isCloseTo(30, within(EPS)); + } + + @Test + void resolveBleedBoxTopAndBottomSnapToThePageEdges() { + CompositeDecoration.BleedBox box = CompositeDecoration.resolveBleedBox( + DocumentBleed.of(DocumentEdge.TOP, DocumentEdge.BOTTOM), 20, 100, 80, 30, canvas); + assertThat(box.x()).isCloseTo(20, within(EPS)); + assertThat(box.width()).isCloseTo(100, within(EPS)); + assertThat(box.topY()).isCloseTo(100, within(EPS)); // pageHeight + assertThat(box.bottomY()).isCloseTo(0, within(EPS)); + } + + @Test + void resolveBleedBoxAllSnapsToTheFullPage() { + CompositeDecoration.BleedBox box = CompositeDecoration.resolveBleedBox( + DocumentBleed.all(), 20, 100, 80, 30, canvas); + assertThat(box.x()).isCloseTo(0, within(EPS)); + assertThat(box.width()).isCloseTo(200, within(EPS)); // full page width + assertThat(box.topY()).isCloseTo(100, within(EPS)); + assertThat(box.bottomY()).isCloseTo(0, within(EPS)); + } }