Skip to content

Commit b5c1f8a

Browse files
Fix #344
1 parent c7ae446 commit b5c1f8a

6 files changed

Lines changed: 105 additions & 5 deletions

File tree

src/ImageSharp.Drawing/InternalPath.cs

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,16 +225,47 @@ private static PointData[] Simplify(IReadOnlyList<ILineSegment> segments, bool i
225225
{
226226
List<PointF> simplified = new(segments.Count);
227227

228+
// Track indices where collinear direction reversals represent user-intended
229+
// geometry: interior points of multi-point linear segments, and junction
230+
// points between two linear segments (e.g. PathBuilder LineTo → LineTo).
231+
// Reversals at all other indices (flattened curves, curve junctions) are
232+
// artifacts and should be removed normally.
233+
HashSet<int>? linearReversalIndices = null;
234+
ILineSegment? prevSeg = null;
235+
228236
foreach (ILineSegment seg in segments)
229237
{
238+
int start = simplified.Count;
230239
ReadOnlyMemory<PointF> points = seg.Flatten();
231240
simplified.AddRange(points.Span);
241+
242+
if (seg is LinearLineSegment)
243+
{
244+
// Interior points of a multi-point linear segment (e.g. DrawLine with 3+ points).
245+
if (points.Length > 2)
246+
{
247+
linearReversalIndices ??= [];
248+
for (int i = start + 1; i < start + points.Length - 1; i++)
249+
{
250+
_ = linearReversalIndices.Add(i);
251+
}
252+
}
253+
254+
// Junction between two linear segments (e.g. PathBuilder LineTo → LineTo).
255+
if (prevSeg is LinearLineSegment && start > 0)
256+
{
257+
linearReversalIndices ??= [];
258+
_ = linearReversalIndices.Add(start);
259+
}
260+
}
261+
262+
prevSeg = seg;
232263
}
233264

234-
return Simplify(CollectionsMarshal.AsSpan(simplified), isClosed, removeCloseAndCollinear);
265+
return Simplify(CollectionsMarshal.AsSpan(simplified), isClosed, removeCloseAndCollinear, linearReversalIndices);
235266
}
236267

237-
private static PointData[] Simplify(ReadOnlySpan<PointF> points, bool isClosed, bool removeCloseAndCollinear)
268+
private static PointData[] Simplify(ReadOnlySpan<PointF> points, bool isClosed, bool removeCloseAndCollinear, HashSet<int>? linearReversalIndices = null)
238269
{
239270
int polyCorners = points.Length;
240271
if (polyCorners == 0)
@@ -294,9 +325,27 @@ private static PointData[] Simplify(ReadOnlySpan<PointF> points, bool isClosed,
294325
{
295326
int next = WrapArrayIndex(i + 1, polyCorners);
296327
PointOrientation or = CalculateOrientation(lastPoint, points[i], points[next]);
297-
if (or == PointOrientation.Collinear && next != 0)
328+
if (removeCloseAndCollinear && or == PointOrientation.Collinear && next != 0)
298329
{
299-
continue;
330+
// Preserve collinear points that represent a direction reversal (U-turn)
331+
// within a single segment. E.g. (10,10)→(90,10)→(20,10): the middle point
332+
// is collinear but the stroker needs to see the reversal.
333+
// Don't preserve reversals at segment boundaries — these arise from joining
334+
// different path segments (e.g. arc-to-arc) and are not user-intended.
335+
bool preserve = false;
336+
if (linearReversalIndices == null || linearReversalIndices.Contains(i))
337+
{
338+
Vector2 incoming = (Vector2)points[i] - lastPoint;
339+
Vector2 outgoing = (Vector2)points[next] - (Vector2)points[i];
340+
float inLen = incoming.Length();
341+
float outLen = outgoing.Length();
342+
preserve = inLen > Epsilon && outLen > Epsilon && Vector2.Dot(incoming, outgoing) < 0;
343+
}
344+
345+
if (!preserve)
346+
{
347+
continue;
348+
}
300349
}
301350

302351
results.Add(

src/ImageSharp.Drawing/PathBuilder.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,15 @@ public PathBuilder MoveTo(PointF point)
110110
return this;
111111
}
112112

113+
/// <summary>
114+
/// Moves to current point to the supplied vector.
115+
/// </summary>
116+
/// <param name="x">The x-coordinate.</param>
117+
/// <param name="y">The y-coordinate.</param>
118+
/// <returns>The <see cref="PathBuilder"/></returns>
119+
public PathBuilder MoveTo(float x, float y)
120+
=> this.MoveTo(new PointF(x, y));
121+
113122
/// <summary>
114123
/// Draws the line connecting the current the current point to the new point.
115124
/// </summary>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using SixLabors.ImageSharp.Drawing.Processing;
5+
using SixLabors.ImageSharp.PixelFormats;
6+
7+
namespace SixLabors.ImageSharp.Drawing.Tests.Issues;
8+
9+
public class Issue_344
10+
{
11+
[Theory]
12+
[WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32)]
13+
public void CanDrawWhereSegmentsOverlap<TPixel>(TestImageProvider<TPixel> provider)
14+
where TPixel : unmanaged, IPixel<TPixel>
15+
=> provider.RunValidatingProcessorTest(
16+
c => c.ProcessWithCanvas(canvas =>
17+
{
18+
Pen pen = Pens.Solid(Color.Aqua.WithAlpha(.3F), 1);
19+
canvas.DrawLine(pen, new PointF(10, 10), new PointF(90, 10), new PointF(20, 10));
20+
}));
21+
22+
[Theory]
23+
[WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32)]
24+
public void CanDrawWhereSegmentsOverlap_PathBuilder<TPixel>(TestImageProvider<TPixel> provider)
25+
where TPixel : unmanaged, IPixel<TPixel>
26+
=> provider.RunValidatingProcessorTest(
27+
c => c.ProcessWithCanvas(canvas =>
28+
{
29+
PathBuilder pathBuilder = new();
30+
pathBuilder.MoveTo(10, 10);
31+
pathBuilder.LineTo(90, 10);
32+
pathBuilder.LineTo(20, 10);
33+
34+
Pen pen = Pens.Solid(Color.Aqua.WithAlpha(.3F), 1);
35+
canvas.Draw(pen, pathBuilder);
36+
}));
37+
}

tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System.Numerics;
55
using SixLabors.ImageSharp.Drawing.Processing;
6-
using SixLabors.ImageSharp.Drawing.Tests.TestUtilities;
76
using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison;
87
using SixLabors.ImageSharp.PixelFormats;
98
using SixLabors.ImageSharp.Processing;
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)