From 998b14ef235dd9192082e5b4d57da7426d7056eb Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Fri, 12 Jun 2026 02:14:20 +0100 Subject: [PATCH] feat(api): dashed strokes for vector paths - PathBuilder.dashed via shared DocumentDashPattern PathNode/PathFragmentPayload gain a dashPattern component (last position, null normalizes to NONE - the LineNode pattern one-to-one), PathBuilder gains dashed(double...) and dashed(DocumentDashPattern), and the dash array is applied inside the shared fill/stroke graphics state. applyDashPattern is deduplicated from PdfLineFragmentRenderHandler into PdfShapeGeometry so lines and paths emit identical dash arrays. Example gains a dashed Bezier wave block (page resized to keep the gallery preview single-page); demo sheet, README section, and CHANGELOG updated. Tests: dash default NONE, builder pass-through, ctor call sites updated. Full gate below. --- CHANGELOG.md | 4 ++- assets/readme/examples/vector-path.pdf | Bin 1288 -> 1382 bytes examples/README.md | 3 +- .../features/shapes/VectorPathExample.java | 12 ++++++- .../PdfLineFragmentRenderHandler.java | 14 +------- .../PdfPathFragmentRenderHandler.java | 1 + .../fixed/pdf/handlers/PdfShapeGeometry.java | 33 ++++++++++++++++++ .../document/chart/LineChartLayout.java | 4 +-- .../compose/document/dsl/PathBuilder.java | 30 +++++++++++++++- .../layout/definitions/PathDefinition.java | 3 +- .../layout/payloads/PathFragmentPayload.java | 9 +++-- .../compose/document/node/PathNode.java | 11 ++++-- .../document/api/PathNodeRenderingTest.java | 4 +-- .../compose/document/dsl/PathBuilderTest.java | 13 +++++++ .../compose/document/node/PathNodeTest.java | 11 ++++-- .../testing/visual/PathPrimitiveDemoTest.java | 17 +++++++-- 16 files changed, 137 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9521442..a2d6260b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,7 +64,9 @@ Entries land here as they merge. and/or stroke. This is the leaf vehicle for smooth chart lines, decorative design shapes, and future SVG path import. DSL: `addPath(p -> p.moveTo(...).curveTo(...).closePath().fillColor(...))` on - every flow builder authors design shapes directly. + every flow builder authors design shapes directly, and + `dashed(on, off, ...)` makes the stroke dashed with the same + `DocumentDashPattern` contract as lines — the pattern follows the curve. - **Inline sparklines** (`@since 1.8.0`). `RichText.sparkline(w, h, color, values...)` draws a filled mini-area silhouette on the text baseline, and `sparklineLine(w, h, thickness, color, values...)` a constant-thickness line diff --git a/assets/readme/examples/vector-path.pdf b/assets/readme/examples/vector-path.pdf index 8324e9e7fe0f7b0e665e2418938956fcdd229939..21492291b149ffc9a5d5d7baed6a032ae01e7b7e 100644 GIT binary patch delta 987 zcmeC+dd4*&k=e}9c=8+;<@&jk&*lXi3bcN&P>nq zuW5K*J7u!aYFp{Di#_#gdScc82)$|xzNY{8+vMo{yV>Pi_gpS{mDCYs{9B@f+q5S# zIrCphk;ta~8?SB3=-9dSnf>l5Qc=2!i`U&-km%qc_X=P|FY<5r%okpGYCE`ZWz3)Hmi*1i?{H9?W>%3@9)^2aeM#a-1F7t zF^$#Vj4XIdEtSHDydJM+~iYg5Lz z7vHx3k6y62cKeN4uN=QDKll0D%sc!ALA)z_8RF~hfB*WIe*0;%;y%|?ow{?)J~(*S za}@fz`ow8J%1(~&t9zSr#OpSP;im8m!_DO@Z>_4^Yv|e|;XFBWUc#ga*)5V5Df5~= zH#}ZPJYNDJb5vbD5Lr0 zolL&kS)YaL|z{JSJ z*v!${%+lG}+`!n>z{1$v!qnN#$j!{e+0qp#X=-L*VPfEDU|?uyV&r0A;OORLr(i>g k5iB4xCg-x`vd6PA=)7i{4Rn|+x1oWlDVM6MtG^o;0C<(NXaE2J delta 885 zcmaFH)xk9(k=fM9c=Asc<@%|U@8$^`3beleE3!|={N0}$#&VpBrng(NJ>nYL7BzJ( z;HvLG-%}o}${M<0?+3ZxJ#Q`VzjKayEA;h@Z1>f9GI(O-=ca<71T#sof&Z+p`$>_hw*|zHDi!-}r%V#fMa<6;l zuRr#!kw<6ul(|olyZA)+vdp}K6@ngfl|#DfGoC-Fxq3J++w59v;GIaiNI?{g&2PYwr#=>o@-37y}T>t^P>-cpS|9l zUw)pk^3MxN%SnBW=?Y)O6=Y|(>@9Z}e5~=k?{UtQMQ1Z5rn|XU&fm~pA9L1plW}x4(0`c9dsM{<$+U@^VZqc1{}- zdL?waCN`+@WO_~i-rVfxd*1Ygf>p1U0k>*az(O_4CdJeL<}Ofv?=pF{xK{qt#B~?q zt88zx<(1V|cX_S5YO>{->kEg4PcoiKO}zT<;5i53_v{9fizn)7HI!bg+|?U)ZbIWH zw_@{~pMOv8UzF<*eRGxH3Aqx{BIT_=n2J9zOWpN3FqwzNy57;k$kEBk(ZJBj)y2rb z#nIBj$l1lh#K_Xvz|z&x(!|o##nRQ>#nH{g(8$Eq(8=7;z|q;o$<^7(+{w(z!rV^5 mh7u!KKxRxn#gfaO#Kxdw&g3}Rj#ZY+(#(iURn^tsjSB#DB!j~M diff --git a/examples/README.md b/examples/README.md index 73d2410b..5fcd6074 100644 --- a/examples/README.md +++ b/examples/README.md @@ -362,7 +362,8 @@ Free-form design shapes with native cubic Bézier curves through ribbons in one closed subpath. Curves render as native PDF `curveTo` operators — perfectly smooth at any zoom, no tessellation. Coordinates are normalized to the shape's box (`(0,0)` bottom-left, `y` up) and -control points may overshoot it. +control points may overshoot it. Strokes can be dashed via +`dashed(on, off, ...)` — the pattern follows the curve. ```java flow.addPath(path -> path diff --git a/examples/src/main/java/com/demcha/examples/features/shapes/VectorPathExample.java b/examples/src/main/java/com/demcha/examples/features/shapes/VectorPathExample.java index eb32c04e..5587cec3 100644 --- a/examples/src/main/java/com/demcha/examples/features/shapes/VectorPathExample.java +++ b/examples/src/main/java/com/demcha/examples/features/shapes/VectorPathExample.java @@ -52,7 +52,7 @@ public static Path generate() throws Exception { Path pdfFile = ExampleOutputPaths.prepare("features/shapes", "vector-path.pdf"); try (DocumentSession document = GraphCompose.document(pdfFile) - .pageSize(420, 420) + .pageSize(420, 540) .margin(DocumentInsets.of(28)) .create()) { document.pageFlow(page -> page @@ -76,6 +76,16 @@ public static Path generate() throws Exception { .fillColor(SAND) .stroke(DocumentStroke.of(SAND_EDGE, 1.4)) .margin(DocumentInsets.bottom(16))) + .addParagraph("Dashed Bézier wave — dashed(6, 3) follows the curve") + .addPath(path -> path + .name("DashedWave") + .size(364, 44) + .moveTo(0.0, 0.5) + .curveTo(0.25, 1.2, 0.25, -0.2, 0.5, 0.5) + .curveTo(0.75, 1.2, 0.75, -0.2, 1.0, 0.5) + .stroke(DocumentStroke.of(INK, 1.8)) + .dashed(6, 3) + .margin(DocumentInsets.bottom(16))) .addParagraph("Mixed ribbon — lines and curves in one closed, filled subpath") .addPath(path -> path .name("Ribbon") 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 a76e6c2a..2e87179a 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 @@ -25,18 +25,6 @@ public final class PdfLineFragmentRenderHandler public PdfLineFragmentRenderHandler() { } - private static void applyDashPattern(PDPageContentStream stream, DocumentDashPattern dash) throws IOException { - if (dash == null || dash.isSolid()) { - return; - } - List segments = dash.segments(); - float[] dashArray = new float[segments.size()]; - for (int i = 0; i < dashArray.length; i++) { - dashArray[i] = segments.get(i).floatValue(); - } - stream.setLineDashPattern(dashArray, 0f); - } - @Override public Class payloadType() { return LineFragmentPayload.class; @@ -56,7 +44,7 @@ public void render(PlacedFragment fragment, try { stream.setStrokingColor(stroke.strokeColor().color()); stream.setLineWidth((float) stroke.width()); - applyDashPattern(stream, payload.dashPattern()); + PdfShapeGeometry.applyDashPattern(stream, payload.dashPattern()); 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/backend/fixed/pdf/handlers/PdfPathFragmentRenderHandler.java b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfPathFragmentRenderHandler.java index 119566b4..b127b0a9 100644 --- a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfPathFragmentRenderHandler.java +++ b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfPathFragmentRenderHandler.java @@ -42,6 +42,7 @@ public void render(PlacedFragment fragment, float width = (float) fragment.width(); float height = (float) fragment.height(); PdfShapeGeometry.fillAndStrokePath(stream, payload.fillColor(), payload.stroke(), + payload.dashPattern(), s -> PdfShapeGeometry.addPathSegments(s, x, y, width, height, payload.segments())); } } diff --git a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfShapeGeometry.java b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfShapeGeometry.java index 9abfa9f9..f11d32b9 100644 --- a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfShapeGeometry.java +++ b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfShapeGeometry.java @@ -1,5 +1,6 @@ package com.demcha.compose.document.backend.fixed.pdf.handlers; +import com.demcha.compose.document.style.DocumentDashPattern; import com.demcha.compose.document.style.DocumentPathSegment; import com.demcha.compose.document.style.ShapePoint; import com.demcha.compose.engine.components.content.shape.Stroke; @@ -28,6 +29,20 @@ static void fillAndStrokePath(PDPageContentStream stream, Color fillColor, Stroke stroke, PathEmitter path) throws IOException { + fillAndStrokePath(stream, fillColor, stroke, null, path); + } + + /** + * Variant of {@link #fillAndStrokePath(PDPageContentStream, Color, Stroke, PathEmitter)} + * with an optional dash pattern applied to the stroke inside the saved + * graphics state ({@code null} or {@link DocumentDashPattern#NONE} keeps + * the stroke solid). + */ + static void fillAndStrokePath(PDPageContentStream stream, + Color fillColor, + Stroke stroke, + DocumentDashPattern dashPattern, + PathEmitter path) throws IOException { boolean hasFill = fillColor != null; boolean hasStroke = stroke != null && stroke.strokeColor() != null @@ -42,6 +57,7 @@ static void fillAndStrokePath(PDPageContentStream stream, PdfAlphaSupport.applyStrokeAlpha(stream, stroke.strokeColor().color()); stream.setStrokingColor(stroke.strokeColor().color()); stream.setLineWidth((float) stroke.width()); + applyDashPattern(stream, dashPattern); } if (hasFill) { PdfAlphaSupport.applyFillAlpha(stream, fillColor); @@ -168,6 +184,23 @@ static void roundedRectPath(PDPageContentStream stream, stream.closePath(); } + /** + * Applies a dash pattern to the stream's stroking state. No-op for + * {@code null} or solid patterns. Shared by the line and path renderers + * so both emit identical dash arrays. + */ + static void applyDashPattern(PDPageContentStream stream, DocumentDashPattern dash) throws IOException { + if (dash == null || dash.isSolid()) { + return; + } + List segments = dash.segments(); + float[] dashArray = new float[segments.size()]; + for (int i = 0; i < dashArray.length; i++) { + dashArray[i] = segments.get(i).floatValue(); + } + stream.setLineDashPattern(dashArray, 0f); + } + /** * A path contribution: the caller adds the geometry (ellipse, rectangle, * polygon, …) so the fill/stroke wrapper can be shared. diff --git a/src/main/java/com/demcha/compose/document/chart/LineChartLayout.java b/src/main/java/com/demcha/compose/document/chart/LineChartLayout.java index 93195d1f..403206cf 100644 --- a/src/main/java/com/demcha/compose/document/chart/LineChartLayout.java +++ b/src/main/java/com/demcha/compose/document/chart/LineChartLayout.java @@ -236,7 +236,7 @@ private static ChartPrimitive bezierRun(String name, List run, Documen (end[0] - minX) / w, (end[1] - minY) / h)); } PathNode node = new PathNode(name, w, h, segments, null, stroke, - DocumentInsets.zero(), DocumentInsets.zero()); + DocumentInsets.zero(), DocumentInsets.zero(), null); return new ChartPrimitive(node, minX, minY, w, h); } @@ -289,7 +289,7 @@ private static void emitCurvedArea(List out, String name, segments.add(DocumentPathSegment.close()); PathNode node = new PathNode(name, w, h, segments, fill, null, - DocumentInsets.zero(), DocumentInsets.zero()); + DocumentInsets.zero(), DocumentInsets.zero(), null); out.add(new ChartPrimitive(node, minX, minY, w, h)); } diff --git a/src/main/java/com/demcha/compose/document/dsl/PathBuilder.java b/src/main/java/com/demcha/compose/document/dsl/PathBuilder.java index e95d63f9..6a59eb32 100644 --- a/src/main/java/com/demcha/compose/document/dsl/PathBuilder.java +++ b/src/main/java/com/demcha/compose/document/dsl/PathBuilder.java @@ -2,6 +2,7 @@ import com.demcha.compose.document.node.PathNode; import com.demcha.compose.document.style.DocumentColor; +import com.demcha.compose.document.style.DocumentDashPattern; import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentPathSegment; import com.demcha.compose.document.style.DocumentStroke; @@ -43,6 +44,7 @@ public final class PathBuilder { private DocumentStroke stroke; private DocumentInsets padding = DocumentInsets.zero(); private DocumentInsets margin = DocumentInsets.zero(); + private DocumentDashPattern dashPattern = DocumentDashPattern.NONE; /** * Creates a path builder. @@ -182,6 +184,32 @@ public PathBuilder stroke(DocumentStroke stroke) { return this; } + /** + * Makes the stroke dashed using alternating on/off lengths in points + * (the same contract as {@code LineBuilder.dashed}). Affects only the + * stroke; fills are unaffected. + * + * @param pattern alternating on/off lengths in points + * @return this builder + */ + public PathBuilder dashed(double... pattern) { + this.dashPattern = DocumentDashPattern.of(pattern); + return this; + } + + /** + * Makes the stroke dashed using a prepared {@link DocumentDashPattern}. + * A {@code null} or {@link DocumentDashPattern#NONE} pattern keeps the + * stroke solid. + * + * @param pattern dash pattern, or {@code null} for solid + * @return this builder + */ + public PathBuilder dashed(DocumentDashPattern pattern) { + this.dashPattern = pattern == null ? DocumentDashPattern.NONE : pattern; + return this; + } + /** * Sets the path padding. * @@ -215,6 +243,6 @@ public PathBuilder margin(DocumentInsets margin) { * added, or the box is not positive */ public PathNode build() { - return new PathNode(name, width, height, segments, fillColor, stroke, padding, margin); + return new PathNode(name, width, height, segments, fillColor, stroke, padding, margin, dashPattern); } } diff --git a/src/main/java/com/demcha/compose/document/layout/definitions/PathDefinition.java b/src/main/java/com/demcha/compose/document/layout/definitions/PathDefinition.java index bad2c5d0..2285539f 100644 --- a/src/main/java/com/demcha/compose/document/layout/definitions/PathDefinition.java +++ b/src/main/java/com/demcha/compose/document/layout/definitions/PathDefinition.java @@ -64,6 +64,7 @@ public List emitFragments(PreparedNode prepared, node.fillColor() == null ? null : node.fillColor().color(), toStroke(node.stroke()), null, - null))); + null, + node.dashPattern()))); } } diff --git a/src/main/java/com/demcha/compose/document/layout/payloads/PathFragmentPayload.java b/src/main/java/com/demcha/compose/document/layout/payloads/PathFragmentPayload.java index 6ad7617e..14b5a660 100644 --- a/src/main/java/com/demcha/compose/document/layout/payloads/PathFragmentPayload.java +++ b/src/main/java/com/demcha/compose/document/layout/payloads/PathFragmentPayload.java @@ -2,6 +2,7 @@ import com.demcha.compose.document.node.DocumentBookmarkOptions; import com.demcha.compose.document.node.DocumentLinkOptions; +import com.demcha.compose.document.style.DocumentDashPattern; import com.demcha.compose.document.style.DocumentPathSegment; import com.demcha.compose.engine.components.content.shape.Stroke; @@ -20,6 +21,8 @@ * @param stroke optional stroke * @param linkOptions optional fragment-level link metadata * @param bookmarkOptions optional fragment-level bookmark metadata + * @param dashPattern dash pattern for the stroke; + * {@link DocumentDashPattern#NONE} is solid * @author Artem Demchyshyn * @since 1.8.0 */ @@ -28,13 +31,15 @@ public record PathFragmentPayload( Color fillColor, Stroke stroke, DocumentLinkOptions linkOptions, - DocumentBookmarkOptions bookmarkOptions + DocumentBookmarkOptions bookmarkOptions, + DocumentDashPattern dashPattern ) implements PdfSemanticFragmentPayload { /** - * Copies the segment list defensively. + * Copies the segment list defensively and normalizes the dash pattern. */ public PathFragmentPayload { Objects.requireNonNull(segments, "segments"); segments = List.copyOf(segments); + dashPattern = dashPattern == null ? DocumentDashPattern.NONE : dashPattern; } } diff --git a/src/main/java/com/demcha/compose/document/node/PathNode.java b/src/main/java/com/demcha/compose/document/node/PathNode.java index 57db84f6..8578bb83 100644 --- a/src/main/java/com/demcha/compose/document/node/PathNode.java +++ b/src/main/java/com/demcha/compose/document/node/PathNode.java @@ -1,6 +1,7 @@ package com.demcha.compose.document.node; import com.demcha.compose.document.style.DocumentColor; +import com.demcha.compose.document.style.DocumentDashPattern; import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentPathSegment; import com.demcha.compose.document.style.DocumentStroke; @@ -28,8 +29,10 @@ * {@link DocumentPathSegment.MoveTo} * @param fillColor optional fill colour (non-zero winding rule) * @param stroke optional outline stroke - * @param padding inner padding - * @param margin outer margin + * @param padding inner padding + * @param margin outer margin + * @param dashPattern dash pattern for the stroke; defaults to + * {@link DocumentDashPattern#NONE} (solid) * @author Artem Demchyshyn * @since 1.8.0 */ @@ -41,7 +44,8 @@ public record PathNode( DocumentColor fillColor, DocumentStroke stroke, DocumentInsets padding, - DocumentInsets margin + DocumentInsets margin, + DocumentDashPattern dashPattern ) implements DocumentNode { /** * Validates dimensions and the segment list; copy-protects the segments. @@ -61,6 +65,7 @@ public record PathNode( } padding = padding == null ? DocumentInsets.zero() : padding; margin = margin == null ? DocumentInsets.zero() : margin; + dashPattern = dashPattern == null ? DocumentDashPattern.NONE : dashPattern; if (width <= 0 || Double.isNaN(width) || Double.isInfinite(width)) { throw new IllegalArgumentException("width must be finite and positive: " + width); } diff --git a/src/test/java/com/demcha/compose/document/api/PathNodeRenderingTest.java b/src/test/java/com/demcha/compose/document/api/PathNodeRenderingTest.java index a1406502..9e36a5de 100644 --- a/src/test/java/com/demcha/compose/document/api/PathNodeRenderingTest.java +++ b/src/test/java/com/demcha/compose/document/api/PathNodeRenderingTest.java @@ -53,7 +53,7 @@ private static PathNode wave() { cubicTo(0.75, 1.0, 0.75, 0.0, 1.0, 0.5)), null, DocumentStroke.of(DocumentColor.rgb(20, 60, 120), 2.0), - DocumentInsets.zero(), DocumentInsets.bottom(8)); + DocumentInsets.zero(), DocumentInsets.bottom(8), null); } private static PathNode blob() { @@ -64,6 +64,6 @@ private static PathNode blob() { close()), DocumentColor.rgb(235, 205, 160), DocumentStroke.of(DocumentColor.rgb(140, 90, 30), 1.2), - DocumentInsets.zero(), DocumentInsets.zero()); + DocumentInsets.zero(), DocumentInsets.zero(), null); } } diff --git a/src/test/java/com/demcha/compose/document/dsl/PathBuilderTest.java b/src/test/java/com/demcha/compose/document/dsl/PathBuilderTest.java index 5584bd66..690de275 100644 --- a/src/test/java/com/demcha/compose/document/dsl/PathBuilderTest.java +++ b/src/test/java/com/demcha/compose/document/dsl/PathBuilderTest.java @@ -51,6 +51,19 @@ void builderAssemblesTheNode() { assertThat(node.margin().bottom()).isEqualTo(6); } + @Test + void dashedFlowsThroughToTheNode() { + PathNode node = new PathBuilder() + .size(100, 40) + .moveTo(0.0, 0.5) + .lineTo(1.0, 0.5) + .stroke(DocumentStroke.of(DocumentColor.rgb(20, 60, 120), 1.0)) + .dashed(4, 2) + .build(); + + assertThat(node.dashPattern().segments()).containsExactly(4.0, 2.0); + } + @Test void nodeValidationFlowsThroughBuild() { PathBuilder missingMoveTo = new PathBuilder() diff --git a/src/test/java/com/demcha/compose/document/node/PathNodeTest.java b/src/test/java/com/demcha/compose/document/node/PathNodeTest.java index e27fa6be..ef0d4808 100644 --- a/src/test/java/com/demcha/compose/document/node/PathNodeTest.java +++ b/src/test/java/com/demcha/compose/document/node/PathNodeTest.java @@ -1,6 +1,7 @@ package com.demcha.compose.document.node; import com.demcha.compose.document.style.DocumentColor; +import com.demcha.compose.document.style.DocumentDashPattern; import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentPathSegment; import com.demcha.compose.document.style.DocumentStroke; @@ -77,11 +78,17 @@ void segmentListIsCopyProtectedAndImmutable() { void boxDimensionsMustBeFiniteAndPositive() { assertThatThrownBy(() -> new PathNode("Bad", 0, 40, List.of(moveTo(0, 0), lineTo(1, 1)), null, STROKE, - DocumentInsets.zero(), DocumentInsets.zero())) + DocumentInsets.zero(), DocumentInsets.zero(), null)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("width must be finite and positive"); } + @Test + void dashPatternDefaultsToSolid() { + assertThat(node(List.of(moveTo(0, 0), lineTo(1, 1))).dashPattern()) + .isEqualTo(DocumentDashPattern.NONE); + } + @Test void nodeKindIsPath() { assertThat(node(List.of(moveTo(0, 0), lineTo(1, 1))).nodeKind()).isEqualTo("Path"); @@ -89,6 +96,6 @@ void nodeKindIsPath() { private static PathNode node(List segments) { return new PathNode("P", 120, 60, segments, null, STROKE, - DocumentInsets.zero(), DocumentInsets.zero()); + DocumentInsets.zero(), DocumentInsets.zero(), null); } } diff --git a/src/test/java/com/demcha/testing/visual/PathPrimitiveDemoTest.java b/src/test/java/com/demcha/testing/visual/PathPrimitiveDemoTest.java index 2f77f356..fc71457f 100644 --- a/src/test/java/com/demcha/testing/visual/PathPrimitiveDemoTest.java +++ b/src/test/java/com/demcha/testing/visual/PathPrimitiveDemoTest.java @@ -4,6 +4,7 @@ import com.demcha.compose.document.api.DocumentSession; import com.demcha.compose.document.node.PathNode; import com.demcha.compose.document.style.DocumentColor; +import com.demcha.compose.document.style.DocumentDashPattern; import com.demcha.compose.document.style.DocumentInsets; import com.demcha.compose.document.style.DocumentStroke; import com.demcha.compose.testing.visual.ImageDiff; @@ -74,7 +75,7 @@ private static byte[] sheet() throws Exception { cubicTo(0.75, 1.1, 0.75, -0.1, 1.0, 0.5)), null, DocumentStroke.of(DocumentColor.rgb(20, 60, 120), 2.4), - DocumentInsets.zero(), DocumentInsets.bottom(14))); + DocumentInsets.zero(), DocumentInsets.bottom(14), null)); // Filled blob — closed curves with overshooting control points. document.add(new PathNode("Blob", 110, 96, @@ -84,7 +85,17 @@ private static byte[] sheet() throws Exception { close()), DocumentColor.rgb(235, 205, 160), DocumentStroke.of(DocumentColor.rgb(140, 90, 30), 1.4), - DocumentInsets.zero(), DocumentInsets.bottom(14))); + DocumentInsets.zero(), DocumentInsets.bottom(14), null)); + + // Dashed Bézier wave — dash pattern follows the curve. + document.add(new PathNode("DashedWave", 316, 40, + List.of(moveTo(0.0, 0.5), + cubicTo(0.25, 1.2, 0.25, -0.2, 0.5, 0.5), + cubicTo(0.75, 1.2, 0.75, -0.2, 1.0, 0.5)), + null, + DocumentStroke.of(DocumentColor.rgb(150, 60, 20), 1.8), + DocumentInsets.zero(), DocumentInsets.bottom(14), + DocumentDashPattern.of(5, 3))); // Mixed ribbon — lines + curves in one closed, filled subpath. document.add(new PathNode("Ribbon", 316, 64, @@ -97,7 +108,7 @@ private static byte[] sheet() throws Exception { close()), DocumentColor.rgb(208, 226, 213), DocumentStroke.of(DocumentColor.rgb(60, 110, 80), 1.2), - DocumentInsets.zero(), DocumentInsets.zero())); + DocumentInsets.zero(), DocumentInsets.zero(), null)); return document.toPdfBytes(); }