Skip to content

Commit ea688af

Browse files
Replace DashPathSplitter with GenerateDashes extension
1 parent 6f7e0c6 commit ea688af

10 files changed

Lines changed: 88 additions & 214 deletions

File tree

samples/WebGPUWindowDemo/Program.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public static unsafe class Program
2828
{
2929
private const int WindowWidth = 800;
3030
private const int WindowHeight = 600;
31-
private const int BallCount = 10;
31+
private const int BallCount = 50;
3232

3333
// Silk.NET WebGPU API and windowing handles.
3434
private static WebGPU wgpu;
@@ -38,7 +38,6 @@ public static unsafe class Program
3838
private static Instance* instance;
3939
private static Surface* surface;
4040
private static SurfaceConfiguration surfaceConfiguration;
41-
private static SurfaceCapabilities surfaceCapabilities;
4241
private static Adapter* adapter;
4342
private static Device* device;
4443
private static Queue* queue;

src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ DrawingCanvasBatcher.Flush()
7272
For stroke definitions (`CompositionCoverageDefinition.IsStroke`), the backend
7373
performs stroke expansion on the GPU using `StrokeExpandComputeShader`:
7474

75-
1. **Dash splitting** (CPU): If the definition has a dash pattern, `DashPathSplitter.SplitDashes()`
75+
1. **Dash splitting** (CPU): If the definition has a dash pattern, `SplitPathExtensions.GenerateDashes()`
7676
(shared with `DefaultDrawingBackend` in the core project) segments the centerline into
7777
open dash sub-paths before edge building.
7878

@@ -194,7 +194,7 @@ Edge preparation (path flattening, fixed-point conversion, CSR construction) run
194194
Both the CPU and GPU backends use per-band parallel stroke expansion - the CPU
195195
via `DefaultRasterizer.RasterizeStrokeRows` and the GPU via
196196
`StrokeExpandComputeShader`. Both share the same `StrokeEdgeFlags` enum and
197-
`DashPathSplitter` (in the core project). The CPU backend fuses stroke expansion
197+
`SplitPathExtensions.GenerateDashes` (in the core project). The CPU backend fuses stroke expansion
198198
directly into the rasterizer's band loop, while the GPU backend uses a separate
199199
compute dispatch that writes outline edges into pre-allocated per-band output
200200
slots sized by `ComputeOutlineEdgesPerCenterline()`.

src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ private bool TryRenderPreparedFlush<TPixel>(
506506
{
507507
// For dashed strokes, split the path into dash segments on the CPU
508508
// so the GPU evaluates solid strokes on each dash segment.
509-
strokePath = DashPathSplitter.SplitDashes(strokePath, definition.StrokeWidth, definition.StrokePattern.Span);
509+
strokePath = strokePath.GenerateDashes(definition.StrokeWidth, definition.StrokePattern.Span);
510510
}
511511

512512
float halfWidth = definition.StrokeWidth * 0.5f;
Lines changed: 9 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4-
using System.Numerics;
54
using SixLabors.ImageSharp.Drawing.PolygonGeometry;
65
using SixLabors.ImageSharp.Drawing.Processing;
76

@@ -98,144 +97,22 @@ public static IPath GenerateOutline(
9897
return path.GenerateOutline(width, strokeOptions);
9998
}
10099

101-
const float eps = 1e-6f;
102-
const int maxPatternSegments = 10000;
100+
IPath dashed = path.GenerateDashes(width, pattern, startOff);
103101

104-
// Compute the absolute pattern length in path units to detect degenerate patterns.
105-
float patternLength = 0f;
106-
for (int i = 0; i < pattern.Length; i++)
107-
{
108-
patternLength += MathF.Abs(pattern[i]) * width;
109-
}
110-
111-
// Fallback to a solid outline when the dash pattern is too small to be meaningful.
112-
if (patternLength <= eps)
102+
// GenerateDashes returns the original path when the pattern is degenerate
103+
// or when segmentation would exceed safety limits; stroke it as solid.
104+
if (ReferenceEquals(dashed, path))
113105
{
114106
return path.GenerateOutline(width, strokeOptions);
115107
}
116108

117-
IEnumerable<ISimplePath> paths = path.Flatten();
118-
119-
List<PointF[]> outlines = [];
120-
List<PointF> buffer = new(64); // arbitrary initial capacity hint.
121-
122-
foreach (ISimplePath p in paths)
109+
if (dashed == Path.Empty)
123110
{
124-
bool online = !startOff;
125-
int patternPos = 0;
126-
float targetLength = pattern[patternPos] * width;
127-
128-
ReadOnlySpan<PointF> pts = p.Points.Span;
129-
if (pts.Length < 2)
130-
{
131-
continue;
132-
}
133-
134-
// number of edges to traverse (no wrap for open paths)
135-
int edgeCount = p.IsClosed ? pts.Length : pts.Length - 1;
136-
float totalLength = 0f;
137-
138-
// Compute total path length to estimate the number of dash segments to produce.
139-
for (int j = 0; j < edgeCount; j++)
140-
{
141-
int nextIndex = p.IsClosed ? (j + 1) % pts.Length : j + 1;
142-
totalLength += Vector2.Distance(pts[j], pts[nextIndex]);
143-
}
144-
145-
if (totalLength > eps)
146-
{
147-
// Avoid runaway segmentation by falling back when the dash count explodes.
148-
float estimatedSegments = (totalLength / patternLength) * pattern.Length;
149-
if (estimatedSegments > maxPatternSegments)
150-
{
151-
return path.GenerateOutline(width, strokeOptions);
152-
}
153-
}
154-
155-
int i = 0;
156-
Vector2 current = pts[0];
157-
158-
while (i < edgeCount)
159-
{
160-
int nextIndex = p.IsClosed ? (i + 1) % pts.Length : i + 1;
161-
Vector2 next = pts[nextIndex];
162-
float segLen = Vector2.Distance(current, next);
163-
164-
// Skip degenerate segments.
165-
if (segLen <= eps)
166-
{
167-
current = next;
168-
i++;
169-
continue;
170-
}
171-
172-
// Accumulate into the current dash span when the segment is shorter than the target.
173-
if (segLen + eps < targetLength)
174-
{
175-
buffer.Add(current);
176-
current = next;
177-
i++;
178-
targetLength -= segLen;
179-
continue;
180-
}
181-
182-
// Close out a dash span when the segment length matches the target length.
183-
if (MathF.Abs(segLen - targetLength) <= eps)
184-
{
185-
buffer.Add(current);
186-
buffer.Add(next);
187-
188-
if (online && buffer.Count >= 2 && buffer[0] != buffer[^1])
189-
{
190-
outlines.Add([.. buffer]);
191-
}
192-
193-
buffer.Clear();
194-
online = !online;
195-
196-
current = next;
197-
i++;
198-
patternPos = (patternPos + 1) % pattern.Length;
199-
targetLength = pattern[patternPos] * width;
200-
continue;
201-
}
202-
203-
// Split inside this segment to end the current dash span.
204-
float t = targetLength / segLen; // 0 < t < 1 here
205-
Vector2 split = current + (t * (next - current));
206-
207-
buffer.Add(current);
208-
buffer.Add(split);
209-
210-
if (online && buffer.Count >= 2 && buffer[0] != buffer[^1])
211-
{
212-
outlines.Add([.. buffer]);
213-
}
214-
215-
buffer.Clear();
216-
online = !online;
217-
218-
current = split; // continue along the same geometric segment
219-
220-
patternPos = (patternPos + 1) % pattern.Length;
221-
targetLength = pattern[patternPos] * width;
222-
}
223-
224-
// flush tail of the last dash span, if any
225-
if (buffer.Count > 0)
226-
{
227-
buffer.Add(current); // terminate at the true end position
228-
229-
if (online && buffer.Count >= 2 && buffer[0] != buffer[^1])
230-
{
231-
outlines.Add([.. buffer]);
232-
}
233-
234-
buffer.Clear();
235-
}
111+
return Path.Empty;
236112
}
237113

238-
// Each outline span is stroked as an open polyline; the union cleans overlaps.
239-
return StrokedShapeGenerator.GenerateStrokedShapes(outlines, width, strokeOptions);
114+
// Each dash segment is an open sub-path; stroke expansion and boolean merge
115+
// are handled by the generator.
116+
return StrokedShapeGenerator.GenerateStrokedShapes(dashed, width, strokeOptions);
240117
}
241118
}

src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs

Lines changed: 2 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -13,67 +13,6 @@ namespace SixLabors.ImageSharp.Drawing.PolygonGeometry;
1313
/// </summary>
1414
internal static class StrokedShapeGenerator
1515
{
16-
/// <summary>
17-
/// Strokes a collection of dashed polyline spans and returns a merged outline.
18-
/// </summary>
19-
/// <param name="spans">
20-
/// The input spans. Each <see cref="PointF"/> array is treated as an open polyline
21-
/// and is stroked using the current stroker settings.
22-
/// Spans that are null or contain fewer than 2 points are ignored.
23-
/// </param>
24-
/// <param name="width">The stroke width in the caller's coordinate space.</param>
25-
/// <param name="options">The stroke geometry options.</param>
26-
/// <returns>
27-
/// A <see cref="ComplexPolygon"/> representing the stroked outline after boolean merge.
28-
/// </returns>
29-
public static ComplexPolygon GenerateStrokedShapes(List<PointF[]> spans, float width, StrokeOptions options)
30-
{
31-
// 1) Stroke each dashed span as open.
32-
PCPolygon rings = new(spans.Count);
33-
foreach (PointF[] span in spans)
34-
{
35-
if (span == null || span.Length < 2)
36-
{
37-
continue;
38-
}
39-
40-
Contour ring = new(span.Length);
41-
for (int i = 0; i < span.Length; i++)
42-
{
43-
PointF p = span[i];
44-
ring.Add(new Vertex(p.X, p.Y));
45-
}
46-
47-
rings.Add(ring);
48-
}
49-
50-
int count = rings.Count;
51-
if (count == 0)
52-
{
53-
return new([]);
54-
}
55-
56-
PCPolygon result = PolygonStroker.Stroke(rings, width, CreateStrokeOptions(options));
57-
58-
IPath[] shapes = new IPath[result.Count];
59-
int index = 0;
60-
for (int i = 0; i < result.Count; i++)
61-
{
62-
Contour contour = result[i];
63-
PointF[] points = new PointF[contour.Count];
64-
65-
for (int j = 0; j < contour.Count; j++)
66-
{
67-
Vertex vertex = contour[j];
68-
points[j] = new PointF((float)vertex.X, (float)vertex.Y);
69-
}
70-
71-
shapes[index++] = new Polygon(points);
72-
}
73-
74-
return new(shapes);
75-
}
76-
7716
/// <summary>
7817
/// Strokes a path and returns a merged outline from its flattened segments.
7918
/// </summary>
@@ -168,7 +107,8 @@ private static PolygonClipper.StrokeOptions CreateStrokeOptions(StrokeOptions op
168107
LineCap.Round => PolygonClipper.LineCap.Round,
169108
LineCap.Square => PolygonClipper.LineCap.Square,
170109
_ => PolygonClipper.LineCap.Butt,
171-
}
110+
},
111+
NormalizeOutput = options.NormalizeOutput
172112
};
173113

174114
return o;

src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ private void FlushPreparedBatch<TPixel>(
162162
{
163163
// Dashed strokes: split into dash segments on the CPU, then stroke-expand
164164
// each segment via the per-band parallel path (same as solid strokes).
165-
rasterPath = DashPathSplitter.SplitDashes(rasterPath, definition.StrokeWidth, definition.StrokePattern.Span);
165+
rasterPath = rasterPath.GenerateDashes(definition.StrokeWidth, definition.StrokePattern.Span);
166166

167167
// Recompute interest from the split path bounds with stroke expansion.
168168
float halfWidth = definition.StrokeWidth * 0.5f;

src/ImageSharp.Drawing/Processing/Backends/PolygonScanning.MD

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@ each parallel band only expands the centerline edges that overlap it. This avoid
5252
the cost of a serial full-path `GenerateOutline()` call and eliminates the
5353
intermediate `IPath` allocation for the expanded outline.
5454

55-
For dashed strokes, `DashPathSplitter` splits the centerline into dash segments
55+
For dashed strokes, `SplitPathExtensions` splits the centerline into dash segments
5656
on the CPU before passing the result through the same per-band stroke expansion
5757
pipeline.
5858

5959
```
6060
IPath (centerline)
6161
|
62-
+--> [if dashed] DashPathSplitter.SplitDashes(path, strokeWidth, pattern)
62+
+--> [if dashed] SplitPathExtensions.GenerateDashes(path, strokeWidth, pattern)
6363
|
6464
v
6565
path.Flatten() -> List<ISimplePath> (preserving open/closed state)

src/ImageSharp.Drawing/Processing/StrokeOptions.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ public sealed class StrokeOptions : IEquatable<StrokeOptions?>
4040
/// </summary>
4141
public InnerJoin InnerJoin { get; set; } = InnerJoin.Miter;
4242

43+
/// <summary>
44+
/// Gets or sets a value indicating whether stroked contours should be normalized
45+
/// by resolving self-intersections and overlaps before returning.
46+
/// </summary>
47+
/// <remarks>
48+
/// Defaults to false for maximum throughput. When disabled, callers should rasterize
49+
/// with a non-zero winding fill rule.
50+
/// </remarks>
51+
public bool NormalizeOutput { get; set; }
52+
4353
/// <inheritdoc/>
4454
public override bool Equals(object? obj) => this.Equals(obj as StrokeOptions);
4555

@@ -51,7 +61,8 @@ public bool Equals(StrokeOptions? other)
5161
this.ArcDetailScale == other.ArcDetailScale &&
5262
this.LineJoin == other.LineJoin &&
5363
this.LineCap == other.LineCap &&
54-
this.InnerJoin == other.InnerJoin;
64+
this.InnerJoin == other.InnerJoin &&
65+
this.NormalizeOutput == other.NormalizeOutput;
5566

5667
/// <inheritdoc/>
5768
public override int GetHashCode()
@@ -61,5 +72,6 @@ public override int GetHashCode()
6172
this.ArcDetailScale,
6273
this.LineJoin,
6374
this.LineCap,
64-
this.InnerJoin);
75+
this.InnerJoin,
76+
this.NormalizeOutput);
6577
}

0 commit comments

Comments
 (0)