Skip to content

Commit 41dae5c

Browse files
authored
Merge pull request #171 from DemchaAV/feat/path-dash-pattern
feat(api): dashed strokes for vector paths - PathBuilder.dashed
2 parents 2052cf7 + 998b14e commit 41dae5c

16 files changed

Lines changed: 137 additions & 32 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ Entries land here as they merge.
6464
and/or stroke. This is the leaf vehicle for smooth chart lines, decorative
6565
design shapes, and future SVG path import. DSL:
6666
`addPath(p -> p.moveTo(...).curveTo(...).closePath().fillColor(...))` on
67-
every flow builder authors design shapes directly.
67+
every flow builder authors design shapes directly, and
68+
`dashed(on, off, ...)` makes the stroke dashed with the same
69+
`DocumentDashPattern` contract as lines — the pattern follows the curve.
6870
- **Inline sparklines** (`@since 1.8.0`). `RichText.sparkline(w, h, color,
6971
values...)` draws a filled mini-area silhouette on the text baseline, and
7072
`sparklineLine(w, h, thickness, color, values...)` a constant-thickness line
94 Bytes
Binary file not shown.

examples/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,8 @@ Free-form design shapes with native cubic Bézier curves through
362362
ribbons in one closed subpath. Curves render as native PDF `curveTo`
363363
operators — perfectly smooth at any zoom, no tessellation. Coordinates
364364
are normalized to the shape's box (`(0,0)` bottom-left, `y` up) and
365-
control points may overshoot it.
365+
control points may overshoot it. Strokes can be dashed via
366+
`dashed(on, off, ...)` — the pattern follows the curve.
366367

367368
```java
368369
flow.addPath(path -> path

examples/src/main/java/com/demcha/examples/features/shapes/VectorPathExample.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public static Path generate() throws Exception {
5252
Path pdfFile = ExampleOutputPaths.prepare("features/shapes", "vector-path.pdf");
5353

5454
try (DocumentSession document = GraphCompose.document(pdfFile)
55-
.pageSize(420, 420)
55+
.pageSize(420, 540)
5656
.margin(DocumentInsets.of(28))
5757
.create()) {
5858
document.pageFlow(page -> page
@@ -76,6 +76,16 @@ public static Path generate() throws Exception {
7676
.fillColor(SAND)
7777
.stroke(DocumentStroke.of(SAND_EDGE, 1.4))
7878
.margin(DocumentInsets.bottom(16)))
79+
.addParagraph("Dashed Bézier wave — dashed(6, 3) follows the curve")
80+
.addPath(path -> path
81+
.name("DashedWave")
82+
.size(364, 44)
83+
.moveTo(0.0, 0.5)
84+
.curveTo(0.25, 1.2, 0.25, -0.2, 0.5, 0.5)
85+
.curveTo(0.75, 1.2, 0.75, -0.2, 1.0, 0.5)
86+
.stroke(DocumentStroke.of(INK, 1.8))
87+
.dashed(6, 3)
88+
.margin(DocumentInsets.bottom(16)))
7989
.addParagraph("Mixed ribbon — lines and curves in one closed, filled subpath")
8090
.addPath(path -> path
8191
.name("Ribbon")

src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfLineFragmentRenderHandler.java

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,6 @@ public final class PdfLineFragmentRenderHandler
2525
public PdfLineFragmentRenderHandler() {
2626
}
2727

28-
private static void applyDashPattern(PDPageContentStream stream, DocumentDashPattern dash) throws IOException {
29-
if (dash == null || dash.isSolid()) {
30-
return;
31-
}
32-
List<Double> segments = dash.segments();
33-
float[] dashArray = new float[segments.size()];
34-
for (int i = 0; i < dashArray.length; i++) {
35-
dashArray[i] = segments.get(i).floatValue();
36-
}
37-
stream.setLineDashPattern(dashArray, 0f);
38-
}
39-
4028
@Override
4129
public Class<LineFragmentPayload> payloadType() {
4230
return LineFragmentPayload.class;
@@ -56,7 +44,7 @@ public void render(PlacedFragment fragment,
5644
try {
5745
stream.setStrokingColor(stroke.strokeColor().color());
5846
stream.setLineWidth((float) stroke.width());
59-
applyDashPattern(stream, payload.dashPattern());
47+
PdfShapeGeometry.applyDashPattern(stream, payload.dashPattern());
6048
stream.moveTo((float) (fragment.x() + payload.startX()), (float) (fragment.y() + payload.startY()));
6149
stream.lineTo((float) (fragment.x() + payload.endX()), (float) (fragment.y() + payload.endY()));
6250
stream.stroke();

src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfPathFragmentRenderHandler.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public void render(PlacedFragment fragment,
4242
float width = (float) fragment.width();
4343
float height = (float) fragment.height();
4444
PdfShapeGeometry.fillAndStrokePath(stream, payload.fillColor(), payload.stroke(),
45+
payload.dashPattern(),
4546
s -> PdfShapeGeometry.addPathSegments(s, x, y, width, height, payload.segments()));
4647
}
4748
}

src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfShapeGeometry.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.demcha.compose.document.backend.fixed.pdf.handlers;
22

3+
import com.demcha.compose.document.style.DocumentDashPattern;
34
import com.demcha.compose.document.style.DocumentPathSegment;
45
import com.demcha.compose.document.style.ShapePoint;
56
import com.demcha.compose.engine.components.content.shape.Stroke;
@@ -28,6 +29,20 @@ static void fillAndStrokePath(PDPageContentStream stream,
2829
Color fillColor,
2930
Stroke stroke,
3031
PathEmitter path) throws IOException {
32+
fillAndStrokePath(stream, fillColor, stroke, null, path);
33+
}
34+
35+
/**
36+
* Variant of {@link #fillAndStrokePath(PDPageContentStream, Color, Stroke, PathEmitter)}
37+
* with an optional dash pattern applied to the stroke inside the saved
38+
* graphics state ({@code null} or {@link DocumentDashPattern#NONE} keeps
39+
* the stroke solid).
40+
*/
41+
static void fillAndStrokePath(PDPageContentStream stream,
42+
Color fillColor,
43+
Stroke stroke,
44+
DocumentDashPattern dashPattern,
45+
PathEmitter path) throws IOException {
3146
boolean hasFill = fillColor != null;
3247
boolean hasStroke = stroke != null
3348
&& stroke.strokeColor() != null
@@ -42,6 +57,7 @@ static void fillAndStrokePath(PDPageContentStream stream,
4257
PdfAlphaSupport.applyStrokeAlpha(stream, stroke.strokeColor().color());
4358
stream.setStrokingColor(stroke.strokeColor().color());
4459
stream.setLineWidth((float) stroke.width());
60+
applyDashPattern(stream, dashPattern);
4561
}
4662
if (hasFill) {
4763
PdfAlphaSupport.applyFillAlpha(stream, fillColor);
@@ -168,6 +184,23 @@ static void roundedRectPath(PDPageContentStream stream,
168184
stream.closePath();
169185
}
170186

187+
/**
188+
* Applies a dash pattern to the stream's stroking state. No-op for
189+
* {@code null} or solid patterns. Shared by the line and path renderers
190+
* so both emit identical dash arrays.
191+
*/
192+
static void applyDashPattern(PDPageContentStream stream, DocumentDashPattern dash) throws IOException {
193+
if (dash == null || dash.isSolid()) {
194+
return;
195+
}
196+
List<Double> segments = dash.segments();
197+
float[] dashArray = new float[segments.size()];
198+
for (int i = 0; i < dashArray.length; i++) {
199+
dashArray[i] = segments.get(i).floatValue();
200+
}
201+
stream.setLineDashPattern(dashArray, 0f);
202+
}
203+
171204
/**
172205
* A path contribution: the caller adds the geometry (ellipse, rectangle,
173206
* polygon, …) so the fill/stroke wrapper can be shared.

src/main/java/com/demcha/compose/document/chart/LineChartLayout.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ private static ChartPrimitive bezierRun(String name, List<double[]> run, Documen
236236
(end[0] - minX) / w, (end[1] - minY) / h));
237237
}
238238
PathNode node = new PathNode(name, w, h, segments, null, stroke,
239-
DocumentInsets.zero(), DocumentInsets.zero());
239+
DocumentInsets.zero(), DocumentInsets.zero(), null);
240240
return new ChartPrimitive(node, minX, minY, w, h);
241241
}
242242

@@ -289,7 +289,7 @@ private static void emitCurvedArea(List<ChartPrimitive> out, String name,
289289
segments.add(DocumentPathSegment.close());
290290

291291
PathNode node = new PathNode(name, w, h, segments, fill, null,
292-
DocumentInsets.zero(), DocumentInsets.zero());
292+
DocumentInsets.zero(), DocumentInsets.zero(), null);
293293
out.add(new ChartPrimitive(node, minX, minY, w, h));
294294
}
295295

src/main/java/com/demcha/compose/document/dsl/PathBuilder.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.demcha.compose.document.node.PathNode;
44
import com.demcha.compose.document.style.DocumentColor;
5+
import com.demcha.compose.document.style.DocumentDashPattern;
56
import com.demcha.compose.document.style.DocumentInsets;
67
import com.demcha.compose.document.style.DocumentPathSegment;
78
import com.demcha.compose.document.style.DocumentStroke;
@@ -43,6 +44,7 @@ public final class PathBuilder {
4344
private DocumentStroke stroke;
4445
private DocumentInsets padding = DocumentInsets.zero();
4546
private DocumentInsets margin = DocumentInsets.zero();
47+
private DocumentDashPattern dashPattern = DocumentDashPattern.NONE;
4648

4749
/**
4850
* Creates a path builder.
@@ -182,6 +184,32 @@ public PathBuilder stroke(DocumentStroke stroke) {
182184
return this;
183185
}
184186

187+
/**
188+
* Makes the stroke dashed using alternating on/off lengths in points
189+
* (the same contract as {@code LineBuilder.dashed}). Affects only the
190+
* stroke; fills are unaffected.
191+
*
192+
* @param pattern alternating on/off lengths in points
193+
* @return this builder
194+
*/
195+
public PathBuilder dashed(double... pattern) {
196+
this.dashPattern = DocumentDashPattern.of(pattern);
197+
return this;
198+
}
199+
200+
/**
201+
* Makes the stroke dashed using a prepared {@link DocumentDashPattern}.
202+
* A {@code null} or {@link DocumentDashPattern#NONE} pattern keeps the
203+
* stroke solid.
204+
*
205+
* @param pattern dash pattern, or {@code null} for solid
206+
* @return this builder
207+
*/
208+
public PathBuilder dashed(DocumentDashPattern pattern) {
209+
this.dashPattern = pattern == null ? DocumentDashPattern.NONE : pattern;
210+
return this;
211+
}
212+
185213
/**
186214
* Sets the path padding.
187215
*
@@ -215,6 +243,6 @@ public PathBuilder margin(DocumentInsets margin) {
215243
* added, or the box is not positive
216244
*/
217245
public PathNode build() {
218-
return new PathNode(name, width, height, segments, fillColor, stroke, padding, margin);
246+
return new PathNode(name, width, height, segments, fillColor, stroke, padding, margin, dashPattern);
219247
}
220248
}

src/main/java/com/demcha/compose/document/layout/definitions/PathDefinition.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public List<LayoutFragment> emitFragments(PreparedNode<PathNode> prepared,
6464
node.fillColor() == null ? null : node.fillColor().color(),
6565
toStroke(node.stroke()),
6666
null,
67-
null)));
67+
null,
68+
node.dashPattern())));
6869
}
6970
}

0 commit comments

Comments
 (0)