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);
+ }
}