Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file modified assets/readme/examples/vector-path.pdf
Binary file not shown.
3 changes: 2 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Double> 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<LineFragmentPayload> payloadType() {
return LineFragmentPayload.class;
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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<Double> 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ private static ChartPrimitive bezierRun(String name, List<double[]> 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);
}

Expand Down Expand Up @@ -289,7 +289,7 @@ private static void emitCurvedArea(List<ChartPrimitive> 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));
}

Expand Down
30 changes: 29 additions & 1 deletion src/main/java/com/demcha/compose/document/dsl/PathBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public List<LayoutFragment> emitFragments(PreparedNode<PathNode> prepared,
node.fillColor() == null ? null : node.fillColor().color(),
toStroke(node.stroke()),
null,
null)));
null,
node.dashPattern())));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
*/
Expand All @@ -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;
}
}
11 changes: 8 additions & 3 deletions src/main/java/com/demcha/compose/document/node/PathNode.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
*/
Expand All @@ -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.
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -77,18 +78,24 @@ 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");
}

private static PathNode node(List<DocumentPathSegment> segments) {
return new PathNode("P", 120, 60, segments, null, STROKE,
DocumentInsets.zero(), DocumentInsets.zero());
DocumentInsets.zero(), DocumentInsets.zero(), null);
}
}
Loading