diff --git a/CHANGELOG.md b/CHANGELOG.md index d40fec785..c67bdb26c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,13 @@ PDF `GoTo` actions. External links are unchanged. ### Public API +- **`LineBuilder.lineCap(DocumentLineCap)`** (`@since 1.9.0`). Lines gain the + round / square end-caps `PathBuilder` already exposed. Pairing `ROUND` with a + short dash draws a dotted line — `line.dashed(0.1, 4).lineCap(DocumentLineCap.ROUND)` + renders round dots (the standard table-of-contents leader / separator style). + The `BUTT` default emits no cap operator, so existing line output is + byte-identical. + - **Content bleed: `DocumentBleed` / `DocumentEdge`** (`@since 1.9.0`). Flow builders gain `bleed(DocumentBleed)` and `bleedToEdge(DocumentEdge...)`, so a section's background fill extends to the trimmed physical page edge on the diff --git a/assets/readme/examples/line-cap.pdf b/assets/readme/examples/line-cap.pdf new file mode 100644 index 000000000..1c0a99dfc Binary files /dev/null and b/assets/readme/examples/line-cap.pdf differ diff --git a/examples/README.md b/examples/README.md index 5f66f26e9..2b7054cdc 100644 --- a/examples/README.md +++ b/examples/README.md @@ -92,6 +92,7 @@ are with the canonical DSL, then jump to its detailed section below. |---|---|---| | [Shape containers](#shape-containers) | Circles, ellipses, rounded cards with `ClipPolicy.CLIP_PATH` | [PDF](../assets/readme/examples/shape-container.pdf) · [Source](src/main/java/com/demcha/examples/features/shapes/ShapeContainerExample.java) | | [Vector paths (Bézier)](#vector-paths-bézier) | `addPath(...)` + `SvgPath.parse(...)` — design shapes and imported SVG icons as native curves; zero tessellation | [PDF](../assets/readme/examples/vector-path.pdf) · [Source](src/main/java/com/demcha/examples/features/shapes/VectorPathExample.java) | +| [Line caps & dotted lines](#line-caps--dotted-lines) | `line.lineCap(ROUND)` — round / square caps for plain lines; `dashed(0.1, 4).lineCap(ROUND)` draws a dotted leader | [PDF](../assets/readme/examples/line-cap.pdf) · [Source](src/main/java/com/demcha/examples/features/shapes/LineCapExample.java) | | [SVG icon gallery](#svg-icon-gallery) | 34 real-world multicolour svgrepo icons via `SvgIcon.parse` — up to 19 layers each, the whole set 156 KB of sources | [PDF](../assets/readme/examples/svg-icon-gallery.pdf) · [Source](src/main/java/com/demcha/examples/features/svg/SvgIconGalleryExample.java) | | [Advanced tables](#advanced-tables) | Row span, zebra rows, totals, repeating header on page break | [PDF](../assets/readme/examples/table-advanced.pdf) · [Source](src/main/java/com/demcha/examples/features/tables/TableAdvancedExample.java) | | [Barcodes](#barcodes) | QR, Code 128, Code 39, EAN-13, EAN-8, branded QR with theme colours | [PDF](../assets/readme/examples/barcode-showcase.pdf) · [Source](src/main/java/com/demcha/examples/features/barcodes/BarcodeShowcaseExample.java) | @@ -385,6 +386,22 @@ flow.addPath(path -> path [📄 View PDF](../assets/readme/examples/vector-path.pdf) · [📜 Full source](src/main/java/com/demcha/examples/features/shapes/VectorPathExample.java) +### Line caps & dotted lines + +`LineBuilder.lineCap(DocumentLineCap)` brings the round / square end-caps +`PathBuilder` already had to plain lines. The headline use is a dotted line: a +`ROUND` cap on a near-zero dash draws round dots — the classic table-of-contents +leader / separator. The `BUTT` default emits no cap operator, so existing line +output stays byte-identical. + +```java +flow.addLine(l -> l.horizontal(w).stroke(stroke) + .dashed(0.1, 4).lineCap(DocumentLineCap.ROUND)); // round dots +``` + +[📄 View PDF](../assets/readme/examples/line-cap.pdf) · +[📜 Full source](src/main/java/com/demcha/examples/features/shapes/LineCapExample.java) + ### SVG icon gallery A stress-test sheet for the beta SVG reader: 34 real-world multicolour diff --git a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java index bfc3adf4c..36835c1fb 100644 --- a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java +++ b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java @@ -9,6 +9,7 @@ import com.demcha.examples.features.layout.BleedExample; import com.demcha.examples.features.layout.BlockAlignExample; import com.demcha.examples.features.lists.NestedListExample; +import com.demcha.examples.features.shapes.LineCapExample; import com.demcha.examples.features.shapes.PhotoClipExample; import com.demcha.examples.features.shapes.ShapeContainerExample; import com.demcha.examples.features.shapes.VectorPathExample; @@ -143,6 +144,7 @@ public static void main(String[] args) throws Exception { // v1.5 visual primitives System.out.println("Generated: " + ShapeContainerExample.generate()); System.out.println("Generated: " + VectorPathExample.generate()); + System.out.println("Generated: " + LineCapExample.generate()); System.out.println("Generated: " + PhotoClipExample.generate()); System.out.println("Generated: " + SvgIconGalleryExample.generate()); System.out.println("Generated: " + BlockAlignExample.generate()); diff --git a/examples/src/main/java/com/demcha/examples/features/shapes/LineCapExample.java b/examples/src/main/java/com/demcha/examples/features/shapes/LineCapExample.java new file mode 100644 index 000000000..8ba562d5a --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/features/shapes/LineCapExample.java @@ -0,0 +1,88 @@ +package com.demcha.examples.features.shapes; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.style.DocumentColor; +import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentLineCap; +import com.demcha.compose.document.style.DocumentStroke; +import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; + +/** + * Runnable showcase for v1.9 line caps: {@code LineBuilder.lineCap(...)} brings + * the round / square end-caps {@code PathBuilder} already had to plain lines. + * The headline use is a dotted line — a {@code ROUND} cap on a near-zero dash + * draws round dots, the classic table-of-contents leader / separator. + * + *
{@code
+ * flow.addLine(l -> l.horizontal(w).stroke(stroke)
+ *     .dashed(0.1, 4).lineCap(DocumentLineCap.ROUND));   // round dots
+ * }
+ * + * @author Artem Demchyshyn + */ +public final class LineCapExample { + + private static final DocumentColor INK = DocumentColor.rgb(24, 28, 38); + private static final DocumentColor MUTED = DocumentColor.rgb(90, 96, 105); + private static final double RULE = 320; + + private LineCapExample() { + } + + /** + * Renders the line-cap sheet: solid, dashed, and dotted rules, plus a + * thick round-capped vs butt-capped comparison. + * + * @return path to the generated PDF + * @throws Exception if rendering or file IO fails + */ + public static Path generate() throws Exception { + Path pdfFile = ExampleOutputPaths.prepare("features/shapes", "line-cap.pdf"); + + DocumentTextStyle caption = DocumentTextStyle.DEFAULT.withSize(10).withColor(MUTED); + DocumentStroke thin = DocumentStroke.of(INK, 1.4); + DocumentStroke thick = DocumentStroke.of(INK, 8); + + try (DocumentSession document = GraphCompose.document(pdfFile) + .pageSize(400, 320) + .margin(DocumentInsets.of(36)) + .create()) { + document.pageFlow(page -> { + page.addParagraph(p -> p + .text("Line caps & dotted lines") + .textStyle(DocumentTextStyle.DEFAULT.withSize(18))); + + page.addParagraph(p -> p.text("solid (default BUTT)").textStyle(caption) + .padding(DocumentInsets.top(10))); + page.addLine(l -> l.horizontal(RULE).stroke(thin)); + + page.addParagraph(p -> p.text("dashed(4, 3)").textStyle(caption) + .padding(DocumentInsets.top(8))); + page.addLine(l -> l.horizontal(RULE).stroke(thin).dashed(4, 3)); + + page.addParagraph(p -> p.text("dashed(0.1, 4).lineCap(ROUND) — dotted leader") + .textStyle(caption).padding(DocumentInsets.top(8))); + page.addLine(l -> l.horizontal(RULE).stroke(thin) + .dashed(0.1, 4).lineCap(DocumentLineCap.ROUND)); + + page.addParagraph(p -> p.text("thick stroke — BUTT vs lineCap(ROUND)") + .textStyle(caption).padding(DocumentInsets.top(12))); + page.addLine(l -> l.horizontal(120).stroke(thick)); + page.addLine(l -> l.horizontal(120).stroke(thick) + .lineCap(DocumentLineCap.ROUND).padding(DocumentInsets.top(6))); + }); + + document.buildPdf(); + } + + return pdfFile; + } + + public static void main(String[] args) throws Exception { + System.out.println("Generated: " + generate()); + } +} diff --git a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfLineFragmentRenderHandler.java b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfLineFragmentRenderHandler.java index 2e87179ad..39184dfd6 100644 --- a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfLineFragmentRenderHandler.java +++ b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfLineFragmentRenderHandler.java @@ -45,6 +45,7 @@ public void render(PlacedFragment fragment, stream.setStrokingColor(stroke.strokeColor().color()); stream.setLineWidth((float) stroke.width()); PdfShapeGeometry.applyDashPattern(stream, payload.dashPattern()); + PdfShapeGeometry.applyStrokeStyle(stream, payload.lineCap(), null); stream.moveTo((float) (fragment.x() + payload.startX()), (float) (fragment.y() + payload.startY())); stream.lineTo((float) (fragment.x() + payload.endX()), (float) (fragment.y() + payload.endY())); stream.stroke(); diff --git a/src/main/java/com/demcha/compose/document/dsl/LineBuilder.java b/src/main/java/com/demcha/compose/document/dsl/LineBuilder.java index 0ecd83e8c..f5f9aaffa 100644 --- a/src/main/java/com/demcha/compose/document/dsl/LineBuilder.java +++ b/src/main/java/com/demcha/compose/document/dsl/LineBuilder.java @@ -30,6 +30,7 @@ public final class LineBuilder implements Transformable { private DocumentInsets margin = DocumentInsets.zero(); private DocumentTransform transform = DocumentTransform.NONE; private DocumentDashPattern dashPattern = DocumentDashPattern.NONE; + private DocumentLineCap lineCap = DocumentLineCap.BUTT; /** * Creates a line builder. @@ -242,6 +243,22 @@ public LineBuilder dashed() { return this; } + /** + * Sets the line end-cap style. {@code null} keeps the PDF default + * ({@link DocumentLineCap#BUTT}). Pair {@code ROUND} with a short dash to + * draw a dotted line — e.g. + * {@code dashed(0.1, 4).lineCap(DocumentLineCap.ROUND)} renders round dots + * (a zero-length dash is rejected, so use a tiny on-segment). + * + * @param lineCap cap style, or {@code null} for the default + * @return this builder + * @since 1.9.0 + */ + public LineBuilder lineCap(DocumentLineCap lineCap) { + this.lineCap = lineCap == null ? DocumentLineCap.BUTT : lineCap; + return this; + } + /** * Attaches line-level external link metadata. * @@ -364,7 +381,8 @@ public LineNode build() { margin, transform, dashPattern, - anchor); + anchor, + lineCap); } private boolean isHorizontalLine() { diff --git a/src/main/java/com/demcha/compose/document/layout/definitions/LineDefinition.java b/src/main/java/com/demcha/compose/document/layout/definitions/LineDefinition.java index c1a2e53fc..eb457b771 100644 --- a/src/main/java/com/demcha/compose/document/layout/definitions/LineDefinition.java +++ b/src/main/java/com/demcha/compose/document/layout/definitions/LineDefinition.java @@ -64,7 +64,8 @@ public List emitFragments(PreparedNode prepared, node.endY(), node.linkTarget(), node.bookmarkOptions(), - node.dashPattern())); + node.dashPattern(), + node.lineCap())); return withAnchorMarker( wrapAtomicWithTransform(leaf, placement, node.transform()), node.anchor(), diff --git a/src/main/java/com/demcha/compose/document/layout/payloads/LineFragmentPayload.java b/src/main/java/com/demcha/compose/document/layout/payloads/LineFragmentPayload.java index 8b3ee4f73..645e09f5e 100644 --- a/src/main/java/com/demcha/compose/document/layout/payloads/LineFragmentPayload.java +++ b/src/main/java/com/demcha/compose/document/layout/payloads/LineFragmentPayload.java @@ -3,6 +3,7 @@ import com.demcha.compose.document.node.DocumentBookmarkOptions; import com.demcha.compose.document.node.DocumentLinkTarget; import com.demcha.compose.document.style.DocumentDashPattern; +import com.demcha.compose.document.style.DocumentLineCap; import com.demcha.compose.engine.components.content.shape.Stroke; /** @@ -16,6 +17,7 @@ * @param linkTarget optional fragment-level link metadata * @param bookmarkOptions optional fragment-level bookmark metadata * @param dashPattern dash pattern for the stroke; {@link DocumentDashPattern#NONE} is solid + * @param lineCap end-cap style; {@link DocumentLineCap#BUTT} is the default */ public record LineFragmentPayload( Stroke stroke, @@ -25,14 +27,40 @@ public record LineFragmentPayload( double endY, DocumentLinkTarget linkTarget, DocumentBookmarkOptions bookmarkOptions, - DocumentDashPattern dashPattern + DocumentDashPattern dashPattern, + DocumentLineCap lineCap ) implements PdfSemanticFragmentPayload { /** - * Normalizes the dash pattern, defaulting to a solid stroke. + * Normalizes the dash pattern and cap, defaulting to a solid butt-capped stroke. */ public LineFragmentPayload { dashPattern = dashPattern == null ? DocumentDashPattern.NONE : dashPattern; + lineCap = lineCap == null ? DocumentLineCap.BUTT : lineCap; + } + + /** + * Backward-compatible constructor without the cap — defaults to + * {@link DocumentLineCap#BUTT}. + * + * @param stroke line stroke + * @param startX line start x offset inside the fragment + * @param startY line start y offset inside the fragment + * @param endX line end x offset inside the fragment + * @param endY line end y offset inside the fragment + * @param linkTarget optional fragment-level link metadata + * @param bookmarkOptions optional fragment-level bookmark metadata + * @param dashPattern dash pattern for the stroke + */ + public LineFragmentPayload(Stroke stroke, + double startX, + double startY, + double endX, + double endY, + DocumentLinkTarget linkTarget, + DocumentBookmarkOptions bookmarkOptions, + DocumentDashPattern dashPattern) { + this(stroke, startX, startY, endX, endY, linkTarget, bookmarkOptions, dashPattern, DocumentLineCap.BUTT); } /** @@ -53,6 +81,6 @@ public LineFragmentPayload(Stroke stroke, double endY, DocumentLinkTarget linkTarget, DocumentBookmarkOptions bookmarkOptions) { - this(stroke, startX, startY, endX, endY, linkTarget, bookmarkOptions, DocumentDashPattern.NONE); + this(stroke, startX, startY, endX, endY, linkTarget, bookmarkOptions, DocumentDashPattern.NONE, DocumentLineCap.BUTT); } } diff --git a/src/main/java/com/demcha/compose/document/node/LineNode.java b/src/main/java/com/demcha/compose/document/node/LineNode.java index 5a8f52844..bad0fef8e 100644 --- a/src/main/java/com/demcha/compose/document/node/LineNode.java +++ b/src/main/java/com/demcha/compose/document/node/LineNode.java @@ -2,6 +2,7 @@ import com.demcha.compose.document.style.DocumentDashPattern; import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentLineCap; import com.demcha.compose.document.style.DocumentStroke; import com.demcha.compose.document.style.DocumentTransform; @@ -26,6 +27,9 @@ * {@link DocumentDashPattern#NONE} (solid) * @param anchor optional in-document navigation anchor name at the line box's * top-left, or {@code null} for none + * @param lineCap end-cap style for the stroke; defaults to + * {@link DocumentLineCap#BUTT}. {@code ROUND} turns a + * dashed stroke into a dotted line * @author Artem Demchyshyn */ public record LineNode( @@ -43,7 +47,8 @@ public record LineNode( DocumentInsets margin, DocumentTransform transform, DocumentDashPattern dashPattern, - String anchor + String anchor, + DocumentLineCap lineCap ) implements DocumentNode { /** * Normalizes spacing defaults and validates explicit line geometry. @@ -55,6 +60,7 @@ public record LineNode( transform = transform == null ? DocumentTransform.NONE : transform; dashPattern = dashPattern == null ? DocumentDashPattern.NONE : dashPattern; anchor = anchor == null || anchor.isBlank() ? null : anchor.trim(); + lineCap = lineCap == null ? DocumentLineCap.BUTT : lineCap; requireNonNegativeFinite(width, "width"); requireNonNegativeFinite(height, "height"); requireFinite(startX, "startX"); @@ -63,6 +69,45 @@ public record LineNode( requireFinite(endY, "endY"); } + /** + * Backward-compatible canonical constructor without the line cap — defaults + * to {@link DocumentLineCap#BUTT} (a squared, byte-identical end). + * + * @param name node name used in snapshots and layout graph paths + * @param width resolved line box width + * @param height resolved line box height + * @param startX line start x offset inside the box + * @param startY line start y offset inside the box + * @param endX line end x offset inside the box + * @param endY line end y offset inside the box + * @param stroke line stroke descriptor + * @param linkTarget optional node-level link target + * @param bookmarkOptions optional node-level bookmark metadata + * @param padding inner padding + * @param margin outer margin + * @param transform render-time affine transform + * @param dashPattern dash pattern for the stroke + * @param anchor optional navigation anchor name + */ + public LineNode(String name, + double width, + double height, + double startX, + double startY, + double endX, + double endY, + DocumentStroke stroke, + DocumentLinkTarget linkTarget, + DocumentBookmarkOptions bookmarkOptions, + DocumentInsets padding, + DocumentInsets margin, + DocumentTransform transform, + DocumentDashPattern dashPattern, + String anchor) { + this(name, width, height, startX, startY, endX, endY, stroke, linkTarget, bookmarkOptions, + padding, margin, transform, dashPattern, anchor, DocumentLineCap.BUTT); + } + /** * Backwards-compatible canonical constructor taking external * {@link DocumentLinkOptions} (wrapped) and no navigation anchor. diff --git a/src/test/java/com/demcha/compose/document/backend/fixed/pdf/PdfLineStrokeStyleTest.java b/src/test/java/com/demcha/compose/document/backend/fixed/pdf/PdfLineStrokeStyleTest.java new file mode 100644 index 000000000..0ea07fb5b --- /dev/null +++ b/src/test/java/com/demcha/compose/document/backend/fixed/pdf/PdfLineStrokeStyleTest.java @@ -0,0 +1,110 @@ +package com.demcha.compose.document.backend.fixed.pdf; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.dsl.LineBuilder; +import com.demcha.compose.document.style.DocumentColor; +import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentLineCap; +import com.demcha.compose.document.style.DocumentStroke; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.contentstream.operator.Operator; +import org.apache.pdfbox.cos.COSBase; +import org.apache.pdfbox.cos.COSNumber; +import org.apache.pdfbox.pdfparser.PDFStreamParser; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that {@link LineBuilder#lineCap(DocumentLineCap)} reaches the PDF as + * the {@code J} (line-cap) operator, that the {@code BUTT} default emits none + * (byte-identical with the pre-cap backend), and that a round cap composes with + * a dash to draw a dotted line. + */ +class PdfLineStrokeStyleTest { + + private static final DocumentColor INK = DocumentColor.rgb(20, 60, 120); + + @TempDir + Path tempDir; + + private Path render(String name, Consumer spec) throws Exception { + Path out = tempDir.resolve(name + ".pdf"); + try (DocumentSession document = GraphCompose.document(out) + .pageSize(160, 100) + .margin(DocumentInsets.of(16)) + .create()) { + document.pageFlow().name("Flow").addLine(spec).build(); + document.buildPdf(); + } + return out; + } + + /** Collects every {@code (operandInt, operator)} pair in the page stream. */ + private static List operatorInts(Path pdf, String operatorName) throws Exception { + try (PDDocument doc = Loader.loadPDF(pdf.toFile())) { + PDPage page = doc.getPage(0); + PDFStreamParser parser = new PDFStreamParser(page); + List hits = new ArrayList<>(); + List operands = new ArrayList<>(); + for (Object token = parser.parseNextToken(); token != null; token = parser.parseNextToken()) { + if (token instanceof COSBase base) { + operands.add(base); + } else if (token instanceof Operator op) { + if (op.getName().equals(operatorName) && !operands.isEmpty() + && operands.get(operands.size() - 1) instanceof COSNumber n) { + hits.add(n.intValue()); + } + operands.clear(); + } + } + return hits; + } + } + + @Test + void roundCapEmitsJ1() throws Exception { + Path pdf = render("round", l -> l.horizontal(120) + .stroke(DocumentStroke.of(INK, 6)) + .lineCap(DocumentLineCap.ROUND)); + + assertThat(operatorInts(pdf, "J")).contains(1); + } + + @Test + void squareCapEmitsJ2() throws Exception { + Path pdf = render("square", l -> l.horizontal(120) + .stroke(DocumentStroke.of(INK, 6)) + .lineCap(DocumentLineCap.SQUARE)); + + assertThat(operatorInts(pdf, "J")).contains(2); + } + + @Test + void defaultButtEmitsNoCapOperator() throws Exception { + Path pdf = render("default", l -> l.horizontal(120) + .stroke(DocumentStroke.of(INK, 6))); + + assertThat(operatorInts(pdf, "J")).isEmpty(); + } + + @Test + void roundCapWithDashDrawsDottedLine() throws Exception { + Path pdf = render("dotted", l -> l.horizontal(120) + .stroke(DocumentStroke.of(INK, 6)) + .dashed(0.1, 4) + .lineCap(DocumentLineCap.ROUND)); + + // ROUND cap on a near-zero on-dash = round dots; the cap operator must be present. + assertThat(operatorInts(pdf, "J")).contains(1); + } +} diff --git a/src/test/java/com/demcha/compose/document/dsl/LineBuilderTest.java b/src/test/java/com/demcha/compose/document/dsl/LineBuilderTest.java index 184cbeb7e..6196bc5f3 100644 --- a/src/test/java/com/demcha/compose/document/dsl/LineBuilderTest.java +++ b/src/test/java/com/demcha/compose/document/dsl/LineBuilderTest.java @@ -2,6 +2,7 @@ import com.demcha.compose.document.node.LineNode; import com.demcha.compose.document.style.DocumentDashPattern; +import com.demcha.compose.document.style.DocumentLineCap; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -46,4 +47,26 @@ void dashedNullPatternRestoresSolid() { void dashedRejectsInvalidPattern() { assertThatIllegalArgumentException().isThrownBy(() -> new LineBuilder().dashed(0)); } + + @Test + void lineCapDefaultsToButt() { + LineNode node = new LineBuilder().horizontal(120).build(); + assertThat(node.lineCap()).isEqualTo(DocumentLineCap.BUTT); + } + + @Test + void lineCapCarriesIntoNode() { + LineNode node = new LineBuilder().horizontal(120).lineCap(DocumentLineCap.ROUND).build(); + assertThat(node.lineCap()).isEqualTo(DocumentLineCap.ROUND); + } + + @Test + void lineCapNullRestoresButt() { + LineNode node = new LineBuilder() + .horizontal(120) + .lineCap(DocumentLineCap.ROUND) + .lineCap(null) + .build(); + assertThat(node.lineCap()).isEqualTo(DocumentLineCap.BUTT); + } }