Skip to content

Commit 3c8b5d3

Browse files
Clip dashed paths
1 parent 7527241 commit 3c8b5d3

File tree

14 files changed

+245
-266
lines changed

14 files changed

+245
-266
lines changed

src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuratio
5252

5353
if (shape is RectangularPolygon rectPoly)
5454
{
55-
var rectF = new RectangleF(rectPoly.Location, rectPoly.Size);
56-
var rect = (Rectangle)rectF;
55+
RectangleF rectF = new(rectPoly.Location, rectPoly.Size);
56+
Rectangle rect = (Rectangle)rectF;
5757
if (!this.Options.GraphicsOptions.Antialias || rectF == rect)
5858
{
5959
// Cast as in and back are the same or we are using anti-aliasing
@@ -63,7 +63,7 @@ public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuratio
6363
}
6464

6565
// Clone the definition so we can pass the transformed path.
66-
var definition = new FillPathProcessor(this.Options, this.Brush, shape);
66+
FillPathProcessor definition = new(this.Options, this.Brush, shape);
6767
return new FillPathProcessor<TPixel>(configuration, definition, source, sourceRectangle);
6868
}
6969
}

src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ protected override void OnFrameApply(ImageFrame<TPixel> source)
6767

6868
// We need to offset the pixel grid to account for when we outline a path.
6969
// basically if the line is [1,2] => [3,2] then when outlining at 1 we end up with a region of [0.5,1.5],[1.5, 1.5],[3.5,2.5],[2.5,2.5]
70-
// and this can cause missed fills when not using antialiasing.so we offset the pixel grid by 0.5 in the x & y direction thus causing the#
70+
// and this can cause missed fills when not using antialiasing.so we offset the pixel grid by 0.5 in the x & y direction thus causing the
7171
// region to align with the pixel grid.
7272
if (graphicsOptions.Antialias)
7373
{

src/ImageSharp.Drawing/Shapes/InternalPath.cs

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ internal IMemoryOwner<PointF> ExtractVertices(MemoryAllocator allocator)
200200
private static int WrapArrayIndex(int i, int arrayLength) => i < arrayLength ? i : i - arrayLength;
201201

202202
[MethodImpl(MethodImplOptions.AggressiveInlining)]
203-
private static PointOrientation CalulateOrientation(Vector2 p, Vector2 q, Vector2 r)
203+
private static PointOrientation CalculateOrientation(Vector2 p, Vector2 q, Vector2 r)
204204
{
205205
// See http://www.geeksforgeeks.org/orientation-3-ordered-points/
206206
// for details of below formula.
@@ -217,7 +217,7 @@ private static PointOrientation CalulateOrientation(Vector2 p, Vector2 q, Vector
217217
}
218218

219219
[MethodImpl(MethodImplOptions.AggressiveInlining)]
220-
private static PointOrientation CalulateOrientation(Vector2 qp, Vector2 rq)
220+
private static PointOrientation CalculateOrientation(Vector2 qp, Vector2 rq)
221221
{
222222
// See http://www.geeksforgeeks.org/orientation-3-ordered-points/
223223
// for details of below formula.
@@ -242,7 +242,7 @@ private static PointOrientation CalulateOrientation(Vector2 qp, Vector2 rq)
242242
/// </returns>
243243
private static PointData[] Simplify(IReadOnlyList<ILineSegment> segments, bool isClosed, bool removeCloseAndCollinear)
244244
{
245-
var simplified = new List<PointF>();
245+
List<PointF> simplified = new(segments.Count);
246246

247247
foreach (ILineSegment seg in segments)
248248
{
@@ -260,10 +260,10 @@ private static PointData[] Simplify(ReadOnlyMemory<PointF> vectors, bool isClose
260260
int polyCorners = points.Length;
261261
if (polyCorners == 0)
262262
{
263-
return Array.Empty<PointData>();
263+
return [];
264264
}
265265

266-
var results = new List<PointData>();
266+
List<PointData> results = new(polyCorners);
267267
Vector2 lastPoint = points[0];
268268

269269
if (!isClosed)
@@ -292,7 +292,7 @@ private static PointData[] Simplify(ReadOnlyMemory<PointF> vectors, bool isClose
292292
Length = 0,
293293
});
294294

295-
return results.ToArray();
295+
return [.. results];
296296
}
297297
}
298298
while (removeCloseAndCollinear && points[0].Equivalent(points[prev], Epsilon2)); // skip points too close together
@@ -304,31 +304,28 @@ private static PointData[] Simplify(ReadOnlyMemory<PointF> vectors, bool isClose
304304
new PointData
305305
{
306306
Point = points[0],
307-
Orientation = CalulateOrientation(lastPoint, points[0], points[1]),
307+
Orientation = CalculateOrientation(lastPoint, points[0], points[1]),
308308
Length = Vector2.Distance(lastPoint, points[0]),
309309
});
310310

311311
lastPoint = points[0];
312312
}
313313

314-
float totalDist = 0;
315314
for (int i = 1; i < polyCorners; i++)
316315
{
317316
int next = WrapArrayIndex(i + 1, polyCorners);
318-
PointOrientation or = CalulateOrientation(lastPoint, points[i], points[next]);
317+
PointOrientation or = CalculateOrientation(lastPoint, points[i], points[next]);
319318
if (or == PointOrientation.Collinear && next != 0)
320319
{
321320
continue;
322321
}
323322

324-
float dist = Vector2.Distance(lastPoint, points[i]);
325-
totalDist += dist;
326323
results.Add(
327324
new PointData
328325
{
329326
Point = points[i],
330327
Orientation = or,
331-
Length = dist,
328+
Length = Vector2.Distance(lastPoint, points[i]),
332329
});
333330
lastPoint = points[i];
334331
}
@@ -342,7 +339,7 @@ private static PointData[] Simplify(ReadOnlyMemory<PointF> vectors, bool isClose
342339
}
343340
}
344341

345-
return results.ToArray();
342+
return [.. results];
346343
}
347344

348345
private struct PointData

src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Six Labors Split License.
33

44
using System.Numerics;
5+
using System.Runtime.InteropServices;
56
using SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
67

78
namespace SixLabors.ImageSharp.Drawing;
@@ -11,7 +12,6 @@ namespace SixLabors.ImageSharp.Drawing;
1112
/// </summary>
1213
public static class OutlinePathExtensions
1314
{
14-
private const float MiterOffsetDelta = 20;
1515
private const JointStyle DefaultJointStyle = JointStyle.Square;
1616
private const EndCapStyle DefaultEndCapStyle = EndCapStyle.Butt;
1717

@@ -45,20 +45,16 @@ public static IPath GenerateOutline(this IPath path, float width, JointStyle joi
4545
return Path.Empty;
4646
}
4747

48-
List<Polygon> polygons = [];
48+
List<Polygon> stroked = [];
49+
50+
// TODO: Wire up options
51+
PolygonStroker stroker = new() { Width = width, LineJoin = LineJoin.BevelJoin, LineCap = LineCap.Butt };
4952
foreach (ISimplePath simplePath in path.Flatten())
5053
{
51-
PolygonStroker stroker = new() { Width = width };
52-
Polygon polygon = stroker.ProcessPath(simplePath.Points.Span, simplePath.IsClosed);
53-
polygons.Add(polygon);
54+
stroked.Add(new Polygon(stroker.ProcessPath(simplePath.Points.Span, simplePath.IsClosed).ToArray()));
5455
}
5556

56-
return new ComplexPolygon(polygons);
57-
58-
// ClipperOffset offset = new(MiterOffsetDelta);
59-
// offset.AddPath(path, jointStyle, endCapStyle);
60-
61-
// return offset.Execute(width);
57+
return new ComplexPolygon(stroked);
6258
}
6359

6460
/// <summary>
@@ -78,11 +74,11 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
7874
/// <param name="path">The path to outline</param>
7975
/// <param name="width">The outline width.</param>
8076
/// <param name="pattern">The pattern made of multiples of the width.</param>
81-
/// <param name="startOff">Whether the first item in the pattern is on or off.</param>
77+
/// <param name="invert">Whether the first item in the pattern is off.</param>
8278
/// <returns>A new <see cref="IPath"/> representing the outline.</returns>
8379
/// <exception cref="ClipperException">Thrown when an offset cannot be calculated.</exception>
84-
public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<float> pattern, bool startOff)
85-
=> GenerateOutline(path, width, pattern, startOff, DefaultJointStyle, DefaultEndCapStyle);
80+
public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<float> pattern, bool invert)
81+
=> GenerateOutline(path, width, pattern, invert, DefaultJointStyle, DefaultEndCapStyle);
8682

8783
/// <summary>
8884
/// Generates an outline of the path with alternating on and off segments based on the pattern.
@@ -103,12 +99,12 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
10399
/// <param name="path">The path to outline</param>
104100
/// <param name="width">The outline width.</param>
105101
/// <param name="pattern">The pattern made of multiples of the width.</param>
106-
/// <param name="startOff">Whether the first item in the pattern is on or off.</param>
102+
/// <param name="invert">Whether the first item in the pattern is off.</param>
107103
/// <param name="jointStyle">The style to apply to the joints.</param>
108104
/// <param name="endCapStyle">The style to apply to the end caps.</param>
109105
/// <returns>A new <see cref="IPath"/> representing the outline.</returns>
110106
/// <exception cref="ClipperException">Thrown when an offset cannot be calculated.</exception>
111-
public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<float> pattern, bool startOff, JointStyle jointStyle, EndCapStyle endCapStyle)
107+
public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<float> pattern, bool invert, JointStyle jointStyle, EndCapStyle endCapStyle)
112108
{
113109
if (width <= 0)
114110
{
@@ -122,18 +118,21 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
122118

123119
IEnumerable<ISimplePath> paths = path.Flatten();
124120

125-
ClipperOffset offset = new(MiterOffsetDelta);
126-
List<PointF> buffer = new();
127-
foreach (ISimplePath p in paths)
121+
// TODO: Wire up options
122+
PolygonStroker stroker = new() { Width = width, LineJoin = LineJoin.BevelJoin, LineCap = LineCap.Butt };
123+
124+
PathsF stroked = [];
125+
List<PointF> buffer = [];
126+
foreach (ISimplePath simplePath in paths)
128127
{
129-
bool online = !startOff;
128+
bool online = !invert;
130129
float targetLength = pattern[0] * width;
131130
int patternPos = 0;
132-
ReadOnlySpan<PointF> points = p.Points.Span;
131+
ReadOnlySpan<PointF> points = simplePath.Points.Span;
133132

134133
// Create a new list of points representing the new outline
135134
int pCount = points.Length;
136-
if (!p.IsClosed)
135+
if (!simplePath.IsClosed)
137136
{
138137
pCount--;
139138
}
@@ -145,20 +144,20 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
145144
{
146145
int next = (i + 1) % points.Length;
147146
Vector2 targetPoint = points[next];
148-
float distToNext = Vector2.Distance(currentPoint, targetPoint);
149-
if (distToNext > targetLength)
147+
float distanceToNext = Vector2.Distance(currentPoint, targetPoint);
148+
if (distanceToNext > targetLength)
150149
{
151-
// find a point between the 2
152-
float t = targetLength / distToNext;
150+
// Find a point between the 2
151+
float t = targetLength / distanceToNext;
153152

154153
Vector2 point = (currentPoint * (1 - t)) + (targetPoint * t);
155154
buffer.Add(currentPoint);
156155
buffer.Add(point);
157156

158-
// we now inset a line joining
157+
// We now insert a line
159158
if (online)
160159
{
161-
offset.AddPath(new ReadOnlySpan<PointF>(buffer.ToArray()), jointStyle, endCapStyle);
160+
stroked.Add(stroker.ProcessPath(CollectionsMarshal.AsSpan(buffer), false));
162161
}
163162

164163
online = !online;
@@ -167,39 +166,64 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
167166

168167
currentPoint = point;
169168

170-
// next length
169+
// Next length
171170
patternPos = (patternPos + 1) % pattern.Length;
172171
targetLength = pattern[patternPos] * width;
173172
}
174-
else if (distToNext <= targetLength)
173+
else if (distanceToNext <= targetLength)
175174
{
176175
buffer.Add(currentPoint);
177176
currentPoint = targetPoint;
178177
i++;
179-
targetLength -= distToNext;
178+
targetLength -= distanceToNext;
180179
}
181180
}
182181

183182
if (buffer.Count > 0)
184183
{
185-
if (p.IsClosed)
184+
if (simplePath.IsClosed)
186185
{
187186
buffer.Add(points[0]);
188187
}
189188
else
190189
{
191-
buffer.Add(points[points.Length - 1]);
190+
buffer.Add(points[^1]);
192191
}
193192

194193
if (online)
195194
{
196-
offset.AddPath(new ReadOnlySpan<PointF>(buffer.ToArray()), jointStyle, endCapStyle);
195+
stroked.Add(stroker.ProcessPath(CollectionsMarshal.AsSpan(buffer), false));
197196
}
198197

199198
buffer.Clear();
200199
}
201200
}
202201

203-
return offset.Execute(width);
202+
// Clean up self intersections.
203+
PolygonClipper clipper = new() { PreserveCollinear = true };
204+
clipper.AddSubject(stroked);
205+
PathsF clipped = [];
206+
clipper.Execute(ClippingOperation.Union, FillRule.Positive, clipped);
207+
208+
if (clipped.Count == 0)
209+
{
210+
// Cannot clip. Return the stroked path.
211+
Polygon[] polygons = new Polygon[stroked.Count];
212+
for (int i = 0; i < stroked.Count; i++)
213+
{
214+
polygons[i] = new Polygon(stroked[i].ToArray());
215+
}
216+
217+
return new ComplexPolygon(polygons);
218+
}
219+
220+
// Convert the clipped paths back to polygons.
221+
Polygon[] result = new Polygon[clipped.Count];
222+
for (int i = 0; i < clipped.Count; i++)
223+
{
224+
result[i] = new Polygon(clipped[i].ToArray());
225+
}
226+
227+
return new ComplexPolygon(result);
204228
}
205229
}

src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ internal struct ArrayBuilder<T>
2626
public ArrayBuilder(int capacity)
2727
: this()
2828
{
29-
Guard.MustBeGreaterThanOrEqualTo(capacity, 0, nameof(capacity));
30-
31-
this.data = new T[capacity];
32-
this.size = capacity;
29+
if (capacity > 0)
30+
{
31+
this.data = new T[capacity];
32+
}
3333
}
3434

3535
/// <summary>

src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ internal class Clipper
1414
/// Initializes a new instance of the <see cref="Clipper"/> class.
1515
/// </summary>
1616
public Clipper()
17-
=> this.polygonClipper = new PolygonClipper();
17+
=> this.polygonClipper = new PolygonClipper() { PreserveCollinear = true };
1818

1919
/// <summary>
2020
/// Generates the clipped shapes from the previously provided paths.

src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperUtils.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public static PathF StripDuplicates(PathF path, bool isClosedPath)
4646
return result;
4747
}
4848

49-
Vector2 lastPt = path[0];
49+
PointF lastPt = path[0];
5050
result.Add(lastPt);
5151
for (int i = 1; i < cnt; i++)
5252
{
@@ -152,11 +152,9 @@ public static bool SegsIntersect(Vector2 seg1a, Vector2 seg1b, Vector2 seg2a, Ve
152152
// ensure NOT collinear
153153
return res1 != 0 || res2 != 0 || res3 != 0 || res4 != 0;
154154
}
155-
else
156-
{
157-
return (CrossProduct(seg1a, seg2a, seg2b) * CrossProduct(seg1b, seg2a, seg2b) < 0)
158-
&& (CrossProduct(seg2a, seg1a, seg1b) * CrossProduct(seg2b, seg1a, seg1b) < 0);
159-
}
155+
156+
return (CrossProduct(seg1a, seg2a, seg2b) * CrossProduct(seg1b, seg2a, seg2b) < 0)
157+
&& (CrossProduct(seg2a, seg1a, seg1b) * CrossProduct(seg2b, seg1a, seg1b) < 0);
160158
}
161159

162160
[MethodImpl(MethodImplOptions.AggressiveInlining)]

src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipper.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -398,8 +398,8 @@ private bool BuildPaths(PathsF solutionClosed, PathsF solutionOpen)
398398
{
399399
solutionClosed.Clear();
400400
solutionOpen.Clear();
401-
solutionClosed.Capacity = this.outrecList.Count;
402-
solutionOpen.Capacity = this.outrecList.Count;
401+
solutionClosed.EnsureCapacity(this.outrecList.Count);
402+
solutionOpen.EnsureCapacity(this.outrecList.Count);
403403

404404
int i = 0;
405405

@@ -1122,7 +1122,7 @@ private void Reset()
11221122
this.isSortedMinimaList = true;
11231123
}
11241124

1125-
this.scanlineList.Capacity = this.minimaList.Count;
1125+
this.scanlineList.EnsureCapacity(this.minimaList.Count);
11261126
for (int i = this.minimaList.Count - 1; i >= 0; i--)
11271127
{
11281128
this.scanlineList.Add(this.minimaList[i].Vertex.Point.Y);
@@ -1924,7 +1924,7 @@ private void AddPathsToVertexList(PathsF paths, ClippingType polytype, bool isOp
19241924
totalVertCnt += path.Count;
19251925
}
19261926

1927-
this.vertexList.Capacity = this.vertexList.Count + totalVertCnt;
1927+
this.vertexList.EnsureCapacity(this.vertexList.Count + totalVertCnt);
19281928

19291929
foreach (PathF path in paths)
19301930
{

0 commit comments

Comments
 (0)