diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln
index 68bd3309..74e8e154 100644
--- a/ImageSharp.Drawing.sln
+++ b/ImageSharp.Drawing.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.14.36623.8 d17.14
+# Visual Studio Version 18
+VisualStudioVersion = 18.0.11123.170
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_root", "_root", "{C317F1B1-D75E-4C6D-83EB-80367343E0D7}"
ProjectSection(SolutionItems) = preProject
diff --git a/samples/DrawShapesWithImageSharp/Program.cs b/samples/DrawShapesWithImageSharp/Program.cs
index e2862d48..04497dc9 100644
--- a/samples/DrawShapesWithImageSharp/Program.cs
+++ b/samples/DrawShapesWithImageSharp/Program.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Globalization;
using System.Numerics;
using SixLabors.Fonts;
using SixLabors.ImageSharp;
@@ -26,13 +27,13 @@ public static void Main(string[] args)
private static void OutputStars()
{
- OutputStarOutline(5, 150, 250, width: 20, jointStyle: JointStyle.Miter);
- OutputStarOutline(5, 150, 250, width: 20, jointStyle: JointStyle.Round);
- OutputStarOutline(5, 150, 250, width: 20, jointStyle: JointStyle.Square);
+ OutputStarOutline(5, 150, 250, width: 20, jointStyle: LineJoin.Miter);
+ OutputStarOutline(5, 150, 250, width: 20, jointStyle: LineJoin.Round);
+ OutputStarOutline(5, 150, 250, width: 20, jointStyle: LineJoin.Bevel);
- OutputStarOutlineDashed(5, 150, 250, width: 20, jointStyle: JointStyle.Square, cap: EndCapStyle.Butt);
- OutputStarOutlineDashed(5, 150, 250, width: 20, jointStyle: JointStyle.Round, cap: EndCapStyle.Round);
- OutputStarOutlineDashed(5, 150, 250, width: 20, jointStyle: JointStyle.Square, cap: EndCapStyle.Square);
+ OutputStarOutlineDashed(5, 150, 250, width: 20, jointStyle: LineJoin.Bevel, cap: LineCap.Butt);
+ OutputStarOutlineDashed(5, 150, 250, width: 20, jointStyle: LineJoin.Round, cap: LineCap.Round);
+ OutputStarOutlineDashed(5, 150, 250, width: 20, jointStyle: LineJoin.Bevel, cap: LineCap.Square);
OutputStar(3, 5);
OutputStar(4);
@@ -103,15 +104,13 @@ private static void DrawSerializedOPenSansLetterShape_a()
const string path = @"36.57813x49.16406 35.41797x43.67969 35.41797x43.67969 35.13672x43.67969 35.13672x43.67969 34.41629x44.54843 33.69641x45.34412 32.97708x46.06674 32.2583x46.71631 31.54007x47.29282 30.82239x47.79626 30.10526x48.22665 29.38867x48.58398 29.38867x48.58398 28.65012x48.88474 27.86707x49.14539 27.03952x49.36594 26.16748x49.54639 25.25095x49.68674 24.28992x49.78699 23.28439x49.84714 22.23438x49.86719 22.23438x49.86719 21.52775x49.85564 20.84048x49.82104 20.17258x49.76337 19.52405x49.68262 18.28506x49.4519 17.12354x49.12891 16.03946x48.71362 15.03284x48.20605 14.10367x47.6062 13.25195x46.91406 13.25195x46.91406 12.48978x46.13678 11.82922x45.28149 11.27029x44.34821 10.81299x43.33691 10.45731x42.24762 10.20325x41.08032 10.05081x39.83502 10.0127x39.18312 10x38.51172 10x38.51172 10.01823x37.79307 10.07292x37.09613 10.16407x36.42088 10.29169x35.76733 10.6563x34.52533 11.16675x33.37012 11.82304x32.3017 12.62518x31.32007 13.57317x30.42523 14.10185x30.01036 14.66699x29.61719 15.2686x29.24571 15.90666x28.89594 16.58119x28.56786 17.29218x28.26147 18.03962x27.97679 18.82353x27.71381 19.6439x27.47252 20.50073x27.25293 22.32378x26.87885 24.29266x26.59155 26.40739x26.39105 28.66797x26.27734 28.66797x26.27734 35.20703x26.06641 35.20703x26.06641 35.20703x23.67578 35.20703x23.67578 35.17654x22.57907 35.08508x21.55652 34.93265x20.60812 34.71924x19.73389 34.44485x18.93381 34.1095x18.20789 33.71317x17.55612 33.25586x16.97852 33.25586x16.97852 32.73154x16.47177 32.13416x16.03259 31.46371x15.66098 30.72021x15.35693 29.90366x15.12045 29.01404x14.95154 28.05136x14.85019 27.01563x14.81641 27.01563x14.81641 25.79175x14.86255 24.52832x15.00098 23.88177x15.1048 23.22534x15.23169 21.88281x15.55469 20.50073x15.96997 19.0791x16.47754 17.61792x17.07739 16.11719x17.76953 16.11719x17.76953 14.32422x13.30469 14.32422x13.30469 15.04465x12.92841 15.7821x12.573 17.30811x11.9248 18.90222x11.36011 20.56445x10.87891 20.56445x10.87891 22.26184x10.49438 23.96143x10.21973 24.81204x10.1236 25.66321x10.05493 26.51492x10.01373 27.36719x10 27.36719x10 29.03409x10.04779 29.82572x10.10753 30.58948x10.19116 31.32536x10.29869 32.03336x10.43011 32.71348x10.58543 33.36572x10.76465 34.58658x11.19476 35.69592x11.72046 36.69376x12.34174 37.58008x13.05859 37.58008x13.05859 38.35873x13.88092 39.03357x14.8186 39.60458x15.87164 40.07178x17.04004 40.26644x17.6675 40.43515x18.32379 40.5779x19.00893 40.6947x19.7229 40.78555x20.46571 40.85043x21.23737 40.88937x22.03786 40.90234x22.86719 40.90234x22.86719 40.90234x49.16406
23.39453x45.05078 24.06655x45.03911 24.72031x45.00409 25.97302x44.86401 27.15268x44.63055 28.25928x44.30371 29.29282x43.88348 30.2533x43.36987 31.14072x42.76288 31.95508x42.0625 31.95508x42.0625 32.6843x41.27808 33.31628x40.41895 33.85104x39.48511 34.28857x38.47656 34.62888x37.39331 34.87195x36.23535 35.01779x35.00269 35.06641x33.69531 35.06641x33.69531 35.06641x30.21484 35.06641x30.21484 29.23047x30.46094 29.23047x30.46094 27.55093x30.54855 25.9928x30.68835 24.55606x30.88034 23.24072x31.12451 22.04678x31.42087 20.97424x31.76941 20.0231x32.17014 19.19336x32.62305 19.19336x32.62305 18.47238x33.13528 17.84753x33.71399 17.31882x34.35916 16.88623x35.0708 16.54977x35.84891 16.30945x36.69348 16.16525x37.60452 16.11719x38.58203 16.11719x38.58203 16.14713x39.34943 16.23694x40.06958 16.38663x40.74249 16.59619x41.36816 17.19495x42.47778 18.0332x43.39844 18.0332x43.39844 19.08679x44.12134 19.68527x44.40533 20.33154x44.6377 21.0256x44.81842 21.76746x44.94751 22.5571x45.02496 23.39453x45.05078";
string[] paths = path.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries);
- Polygon[] polys = paths.Select(line =>
+ Polygon[] polys = [.. paths.Select(line =>
{
string[] pl = line.Split([' '], StringSplitOptions.RemoveEmptyEntries);
- PointF[] points = pl.Select(p => p.Split('x'))
- .Select(p => new PointF(float.Parse(p[0]), float.Parse(p[1])))
- .ToArray();
+ PointF[] points = [.. pl.Select(p => p.Split('x')).Select(p => new PointF(float.Parse(p[0], CultureInfo.InvariantCulture), float.Parse(p[1], CultureInfo.InvariantCulture)))];
return new Polygon(points);
- }).ToArray();
+ })];
ComplexPolygon complex = new(polys);
complex.SaveImage("letter", "a.png");
@@ -122,16 +121,14 @@ private static void DrawSerializedOPenSansLetterShape_o()
const string path = @"45.40234x29.93359 45.3838x31.09519 45.32819x32.22452 45.23549x33.32157 45.10571x34.38635 44.93886x35.41886 44.73492x36.4191 44.49391x37.38706 44.21582x38.32275 43.90065x39.22617 43.5484x40.09732 43.15907x40.9362 42.73267x41.7428 42.26918x42.51713 41.76862x43.25919 41.23097x43.96897 40.65625x44.64648 40.65625x44.64648 40.04884x45.28719 39.41315x45.88657 38.74916x46.4446 38.05688x46.9613 37.33632x47.43667 36.58746x47.8707 35.81032x48.26339 35.00488x48.61475 34.17116x48.92477 33.30914x49.19345 32.41884x49.4208 31.50024x49.60681 30.55336x49.75149 29.57819x49.85483 28.57472x49.91683 27.54297x49.9375 27.54297x49.9375 26.2691x49.8996 25.03149x49.78589 23.83014x49.59637 22.66504x49.33105 21.53619x48.98993 20.4436x48.573 19.38727x48.08026 18.36719x47.51172 18.36719x47.51172 17.3938x46.87231 16.47754x46.16699 15.61841x45.39575 14.81641x44.55859 14.07153x43.65552 13.38379x42.68652 12.75317x41.65161 12.17969x40.55078 12.17969x40.55078 11.66882x39.39282 11.22607x38.18652 10.85144x36.93188 10.54492x35.62891 10.30652x34.27759 10.13623x32.87793 10.03406x31.42993 10x29.93359 10x29.93359 10.0184x28.77213 10.07361x27.64322 10.16562x26.54685 10.29443x25.48303 10.46005x24.45176 10.66248x23.45303 10.9017x22.48685 11.17773x21.55322 11.49057x20.65214 11.84021x19.7836 12.22665x18.94761 12.6499x18.14417 13.10995x17.37327 13.60681x16.63492 14.14047x15.92912 14.71094x15.25586 14.71094x15.25586 15.31409x14.61941 15.9458x14.02402 16.60608x13.46969 17.29492x12.95642 18.01233x12.48421 18.7583x12.05307 19.53284x11.66299 20.33594x11.31396 21.1676x11.006 22.02783x10.73911 22.91663x10.51327 23.83398x10.32849 24.77991x10.18478 25.75439x10.08212 26.75745x10.02053 27.78906x10 27.78906x10 28.78683x10.02101 29.75864x10.08405 30.70449x10.1891 31.62439x10.33618 32.51833x10.52528 33.38632x10.75641 34.22836x11.02956 35.04443x11.34473 35.83456x11.70192 36.59872x12.10114 37.33694x12.54237 38.04919x13.02563 38.7355x13.55092 39.39584x14.11823 40.03024x14.72755 40.63867x15.37891 40.63867x15.37891 41.21552x16.0661 41.75516x16.78296 42.25757x17.52948 42.72278x18.30566 43.15077x19.11151 43.54153x19.94702 43.89509x20.81219 44.21143x21.70703 44.49055x22.63153 44.73245x23.58569 44.93714x24.56952 45.10461x25.58301 45.23487x26.62616 45.32791x27.69897 45.38374x28.80145 45.40234x29.93359
16.04688x29.93359 16.09302x31.72437 16.23145x33.40527 16.33527x34.20453 16.46216x34.97632 16.61212x35.72064 16.78516x36.4375 16.98126x37.12689 17.20044x37.78882 17.44269x38.42328 17.70801x39.03027 18.30786x40.16187 19x41.18359 19x41.18359 19.78168x42.08997 20.65015x42.87549 21.60541x43.54016 22.64746x44.08398 23.77631x44.50696 24.99194x44.80908 26.29437x44.99036 26.97813x45.03568 27.68359x45.05078 27.68359x45.05078 28.38912x45.03575 29.07309x44.99063 30.37634x44.81018 31.59335x44.50943 32.72412x44.08838 33.76865x43.54703 34.72693x42.88538 35.59897x42.10342 36.38477x41.20117 36.38477x41.20117 37.08102x40.18301 37.68445x39.05334 37.95135x38.44669 38.19504x37.81216 38.41552x37.14976 38.61279x36.45947 38.78686x35.74131 38.93771x34.99527 39.06536x34.22135 39.1698x33.41956 39.30905x31.73233 39.35547x29.93359 39.35547x29.93359 39.30905x28.15189 39.1698x26.48059 39.06536x25.68635 38.93771x24.91971 38.78686x24.18067 38.61279x23.46924 38.41552x22.78541 38.19504x22.12918 37.95135x21.50056 37.68445x20.89954 37.08102x19.7803 36.38477x18.77148 36.38477x18.77148 35.59787x17.87747 34.72253x17.10266 33.75876x16.44705 32.70654x15.91064 31.56589x15.49344 30.33679x15.19543 29.68908x15.09113 29.01926x15.01663 28.32732x14.97193 27.61328x14.95703 27.61328x14.95703 26.90796x14.97173 26.22461x15.01581 24.92383x15.19214 23.71094x15.48602 22.58594x15.89746 21.54883x16.42645 20.59961x17.073 19.73828x17.8371 18.96484x18.71875 18.96484x18.71875 18.28094x19.71686 17.68823x20.83032 17.42607x21.43031 17.18671x22.05914 16.97014x22.71681 16.77637x23.40332 16.60539x24.11867 16.45721x24.86285 16.33183x25.63588 16.22925x26.43774 16.09247x28.12799 16.04688x29.93359 ";
string[] paths = path.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries);
- Polygon[] polys = paths.Select(line =>
+ Polygon[] polys = [.. paths.Select(line =>
{
string[] pl = line.Split([' '], StringSplitOptions.RemoveEmptyEntries);
- PointF[] points = pl.Select(p => p.Split('x'))
- .Select(p => new PointF(float.Parse(p[0]), float.Parse(p[1])))
- .ToArray();
+ PointF[] points = [.. pl.Select(p => p.Split('x')).Select(p => new PointF(float.Parse(p[0], CultureInfo.InvariantCulture), float.Parse(p[1], CultureInfo.InvariantCulture)))];
return new Polygon(points);
- }).ToArray();
+ })];
ComplexPolygon complex = new(polys);
complex.SaveImage("letter", "o.png");
@@ -182,23 +179,33 @@ private static void OutputDrawnShapeHourGlass()
sb.Build().Translate(0, 10).Scale(10).SaveImage("drawing", $"HourGlass.png");
}
- private static void OutputStarOutline(int points, float inner = 10, float outer = 20, float width = 5, JointStyle jointStyle = JointStyle.Miter)
+ private static void OutputStarOutline(int points, float inner = 10, float outer = 20, float width = 5, LineJoin jointStyle = LineJoin.Miter)
{
// center the shape outerRadii + 10 px away from edges
float offset = outer + 10;
Star star = new(offset, offset, points, inner, outer);
- IPath outline = star.GenerateOutline(width, jointStyle, EndCapStyle.Butt);
+ StrokeOptions options = new()
+ {
+ LineJoin = jointStyle,
+ LineCap = LineCap.Butt
+ };
+ IPath outline = star.GenerateOutline(width, options);
outline.SaveImage("Stars", $"StarOutline_{points}_{jointStyle}.png");
}
- private static void OutputStarOutlineDashed(int points, float inner = 10, float outer = 20, float width = 5, JointStyle jointStyle = JointStyle.Miter, EndCapStyle cap = EndCapStyle.Butt)
+ private static void OutputStarOutlineDashed(int points, float inner = 10, float outer = 20, float width = 5, LineJoin jointStyle = LineJoin.Miter, LineCap cap = LineCap.Butt)
{
// center the shape outerRadii + 10 px away from edges
float offset = outer + 10;
Star star = new(offset, offset, points, inner, outer);
- IPath outline = star.GenerateOutline(width, [3, 3], jointStyle, cap);
+ StrokeOptions options = new()
+ {
+ LineCap = cap,
+ LineJoin = jointStyle
+ };
+ IPath outline = star.GenerateOutline(width, [3, 3], options);
outline.SaveImage("Stars", $"StarOutlineDashed_{points}_{jointStyle}_{cap}.png");
}
diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
index 91da382b..488180d6 100644
--- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
+++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
@@ -48,4 +48,4 @@
-
\ No newline at end of file
+
diff --git a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs
index 78799c43..ef315427 100644
--- a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs
+++ b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs
@@ -224,7 +224,7 @@ public PathGradientBrushApplicator(
: base(configuration, options, source)
{
this.edges = edges;
- Vector2[] points = edges.Select(s => s.Start).ToArray();
+ Vector2[] points = [.. edges.Select(s => s.Start)];
this.center = points.Aggregate((p1, p2) => p1 + p2) / edges.Count;
this.centerColor = centerColor.ToScaledVector4();
diff --git a/src/ImageSharp.Drawing/Processing/PatternPen.cs b/src/ImageSharp.Drawing/Processing/PatternPen.cs
index c2872be5..f6da8ee0 100644
--- a/src/ImageSharp.Drawing/Processing/PatternPen.cs
+++ b/src/ImageSharp.Drawing/Processing/PatternPen.cs
@@ -75,5 +75,5 @@ public override bool Equals(Pen? other)
///
public override IPath GeneratePath(IPath path, float strokeWidth)
- => path.GenerateOutline(strokeWidth, this.StrokePattern, this.JointStyle, this.EndCapStyle);
+ => path.GenerateOutline(strokeWidth, this.StrokePattern, this.StrokeOptions);
}
diff --git a/src/ImageSharp.Drawing/Processing/Pen.cs b/src/ImageSharp.Drawing/Processing/Pen.cs
index 9602c5c9..e3fbd309 100644
--- a/src/ImageSharp.Drawing/Processing/Pen.cs
+++ b/src/ImageSharp.Drawing/Processing/Pen.cs
@@ -58,6 +58,7 @@ protected Pen(Brush strokeFill, float strokeWidth, float[] strokePattern)
this.StrokeFill = strokeFill;
this.StrokeWidth = strokeWidth;
this.pattern = strokePattern;
+ this.StrokeOptions = new StrokeOptions();
}
///
@@ -69,8 +70,7 @@ protected Pen(PenOptions options)
this.StrokeFill = options.StrokeFill;
this.StrokeWidth = options.StrokeWidth;
this.pattern = options.StrokePattern;
- this.JointStyle = options.JointStyle;
- this.EndCapStyle = options.EndCapStyle;
+ this.StrokeOptions = options.StrokeOptions ?? new StrokeOptions();
}
///
@@ -82,11 +82,8 @@ protected Pen(PenOptions options)
///
public ReadOnlySpan StrokePattern => this.pattern;
- ///
- public JointStyle JointStyle { get; }
-
- ///
- public EndCapStyle EndCapStyle { get; }
+ ///
+ public StrokeOptions StrokeOptions { get; }
///
/// Applies the styling from the pen to a path and generate a new path with the final vector.
@@ -108,9 +105,8 @@ public IPath GeneratePath(IPath path)
public virtual bool Equals(Pen? other)
=> other != null
&& this.StrokeWidth == other.StrokeWidth
- && this.JointStyle == other.JointStyle
- && this.EndCapStyle == other.EndCapStyle
&& this.StrokeFill.Equals(other.StrokeFill)
+ && this.StrokeOptions.Equals(other.StrokeOptions)
&& this.StrokePattern.SequenceEqual(other.StrokePattern);
///
@@ -118,5 +114,5 @@ public virtual bool Equals(Pen? other)
///
public override int GetHashCode()
- => HashCode.Combine(this.StrokeWidth, this.JointStyle, this.EndCapStyle, this.StrokeFill, this.pattern);
+ => HashCode.Combine(this.StrokeWidth, this.StrokeFill, this.StrokeOptions, this.pattern);
}
diff --git a/src/ImageSharp.Drawing/Processing/PenOptions.cs b/src/ImageSharp.Drawing/Processing/PenOptions.cs
index d000b9f9..9cd1ab22 100644
--- a/src/ImageSharp.Drawing/Processing/PenOptions.cs
+++ b/src/ImageSharp.Drawing/Processing/PenOptions.cs
@@ -51,8 +51,7 @@ public PenOptions(Brush strokeFill, float strokeWidth, float[]? strokePattern)
this.StrokeFill = strokeFill;
this.StrokeWidth = strokeWidth;
this.StrokePattern = strokePattern ?? Pens.EmptyPattern;
- this.JointStyle = JointStyle.Square;
- this.EndCapStyle = EndCapStyle.Butt;
+ this.StrokeOptions = new StrokeOptions();
}
///
@@ -71,12 +70,7 @@ public PenOptions(Brush strokeFill, float strokeWidth, float[]? strokePattern)
public float[] StrokePattern { get; }
///
- /// Gets or sets the joint style.
+ /// Gets or sets the stroke geometry options used to stroke paths drawn with this pen.
///
- public JointStyle JointStyle { get; set; }
-
- ///
- /// Gets or sets the end cap style.
- ///
- public EndCapStyle EndCapStyle { get; set; }
+ public StrokeOptions? StrokeOptions { get; set; }
}
diff --git a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs
index 11c188d8..4df70625 100644
--- a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs
+++ b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs
@@ -18,15 +18,15 @@ public ShapeOptions()
private ShapeOptions(ShapeOptions source)
{
this.IntersectionRule = source.IntersectionRule;
- this.ClippingOperation = source.ClippingOperation;
+ this.BooleanOperation = source.BooleanOperation;
}
///
/// Gets or sets the clipping operation.
///
- /// Defaults to .
+ /// Defaults to .
///
- public ClippingOperation ClippingOperation { get; set; } = ClippingOperation.Difference;
+ public BooleanOperation BooleanOperation { get; set; } = BooleanOperation.Difference;
///
/// Gets or sets the rule for calculating intersection points.
diff --git a/src/ImageSharp.Drawing/Processing/SolidPen.cs b/src/ImageSharp.Drawing/Processing/SolidPen.cs
index e2c827e1..b56e465a 100644
--- a/src/ImageSharp.Drawing/Processing/SolidPen.cs
+++ b/src/ImageSharp.Drawing/Processing/SolidPen.cs
@@ -68,5 +68,5 @@ public override bool Equals(Pen? other)
///
public override IPath GeneratePath(IPath path, float strokeWidth)
- => path.GenerateOutline(strokeWidth, this.JointStyle, this.EndCapStyle);
+ => path.GenerateOutline(strokeWidth, this.StrokeOptions);
}
diff --git a/src/ImageSharp.Drawing/Processing/StrokeOptions.cs b/src/ImageSharp.Drawing/Processing/StrokeOptions.cs
new file mode 100644
index 00000000..4e4b34e8
--- /dev/null
+++ b/src/ImageSharp.Drawing/Processing/StrokeOptions.cs
@@ -0,0 +1,63 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Drawing.Processing;
+
+///
+/// Provides configuration options for geometric stroke generation.
+///
+public sealed class StrokeOptions : IEquatable
+{
+ ///
+ /// Gets or sets the miter limit used to clamp outer miter joins.
+ ///
+ public double MiterLimit { get; set; } = 4;
+
+ ///
+ /// Gets or sets the inner miter limit used to clamp joins on acute interior angles.
+ ///
+ public double InnerMiterLimit { get; set; } = 1.01;
+
+ ///
+ /// Gets or sets the arc approximation scale used for round joins and caps.
+ ///
+ public double ApproximationScale { get; set; } = 1.0;
+
+ ///
+ /// Gets or sets the outer line join style used for stroking corners.
+ ///
+ public LineJoin LineJoin { get; set; } = LineJoin.Bevel;
+
+ ///
+ /// Gets or sets the line cap style used for open path ends.
+ ///
+ public LineCap LineCap { get; set; } = LineCap.Butt;
+
+ ///
+ /// Gets or sets the join style used for sharp interior angles.
+ ///
+ public InnerJoin InnerJoin { get; set; } = InnerJoin.Miter;
+
+ ///
+ public override bool Equals(object? obj) => this.Equals(obj as StrokeOptions);
+
+ ///
+ public bool Equals(StrokeOptions? other)
+ => other is not null &&
+ this.MiterLimit == other.MiterLimit &&
+ this.InnerMiterLimit == other.InnerMiterLimit &&
+ this.ApproximationScale == other.ApproximationScale &&
+ this.LineJoin == other.LineJoin &&
+ this.LineCap == other.LineCap &&
+ this.InnerJoin == other.InnerJoin;
+
+ ///
+ public override int GetHashCode()
+ => HashCode.Combine(
+ this.MiterLimit,
+ this.InnerMiterLimit,
+ this.ApproximationScale,
+ this.LineJoin,
+ this.LineCap,
+ this.InnerJoin);
+}
diff --git a/src/ImageSharp.Drawing/Shapes/BooleanOperation.cs b/src/ImageSharp.Drawing/Shapes/BooleanOperation.cs
new file mode 100644
index 00000000..7ee16019
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/BooleanOperation.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Drawing;
+
+///
+/// Specifies the type of boolean operation to perform on polygons.
+///
+public enum BooleanOperation
+{
+ ///
+ /// The intersection operation, which results in the area common to both polygons.
+ ///
+ Intersection = 0,
+
+ ///
+ /// The union operation, which results in the combined area of both polygons.
+ ///
+ Union = 1,
+
+ ///
+ /// The difference operation, which subtracts the clipping polygon from the subject polygon.
+ ///
+ Difference = 2,
+
+ ///
+ /// The exclusive OR (XOR) operation, which results in the area covered by exactly one polygon,
+ /// excluding the overlapping areas.
+ ///
+ Xor = 3
+}
diff --git a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs b/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs
index 690d2291..a1101853 100644
--- a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs
+++ b/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs
@@ -2,7 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Drawing.Processing;
-using SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
+using SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
namespace SixLabors.ImageSharp.Drawing;
@@ -17,7 +17,6 @@ public static class ClipPathExtensions
/// The subject path.
/// The clipping paths.
/// The clipped .
- /// Thrown when an error occurred while attempting to clip the polygon.
public static IPath Clip(this IPath subjectPath, params IPath[] clipPaths)
=> subjectPath.Clip((IEnumerable)clipPaths);
@@ -28,7 +27,6 @@ public static IPath Clip(this IPath subjectPath, params IPath[] clipPaths)
/// The shape options.
/// The clipping paths.
/// The clipped .
- /// Thrown when an error occurred while attempting to clip the polygon.
public static IPath Clip(
this IPath subjectPath,
ShapeOptions options,
@@ -41,7 +39,6 @@ public static IPath Clip(
/// The subject path.
/// The clipping paths.
/// The clipped .
- /// Thrown when an error occurred while attempting to clip the polygon.
public static IPath Clip(this IPath subjectPath, IEnumerable clipPaths)
=> subjectPath.Clip(new ShapeOptions(), clipPaths);
@@ -52,18 +49,17 @@ public static IPath Clip(this IPath subjectPath, IEnumerable clipPaths)
/// The shape options.
/// The clipping paths.
/// The clipped .
- /// Thrown when an error occurred while attempting to clip the polygon.
public static IPath Clip(
this IPath subjectPath,
ShapeOptions options,
IEnumerable clipPaths)
{
- Clipper clipper = new();
+ ClippedShapeGenerator clipper = new(options.IntersectionRule);
clipper.AddPath(subjectPath, ClippingType.Subject);
clipper.AddPaths(clipPaths, ClippingType.Clip);
- IPath[] result = clipper.GenerateClippedShapes(options.ClippingOperation, options.IntersectionRule);
+ IPath[] result = clipper.GenerateClippedShapes(options.BooleanOperation);
return new ComplexPolygon(result);
}
diff --git a/src/ImageSharp.Drawing/Shapes/ClippingOperation.cs b/src/ImageSharp.Drawing/Shapes/ClippingOperation.cs
deleted file mode 100644
index 4adbfc06..00000000
--- a/src/ImageSharp.Drawing/Shapes/ClippingOperation.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Drawing;
-
-///
-/// Provides options for boolean clipping operations.
-///
-///
-/// All clipping operations except for Difference are commutative.
-///
-public enum ClippingOperation
-{
- ///
- /// No clipping is performed.
- ///
- None,
-
- ///
- /// Clips regions covered by both subject and clip polygons.
- ///
- Intersection,
-
- ///
- /// Clips regions covered by subject or clip polygons, or both polygons.
- ///
- Union,
-
- ///
- /// Clips regions covered by subject, but not clip polygons.
- ///
- Difference,
-
- ///
- /// Clips regions covered by subject or clip polygons, but not both.
- ///
- Xor
-}
diff --git a/src/ImageSharp.Drawing/Shapes/EndCapStyle.cs b/src/ImageSharp.Drawing/Shapes/EndCapStyle.cs
deleted file mode 100644
index 50607e20..00000000
--- a/src/ImageSharp.Drawing/Shapes/EndCapStyle.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Drawing;
-
-///
-/// The style to apply to the end cap when generating an outline.
-///
-public enum EndCapStyle
-{
- ///
- /// The outline stops exactly at the end of the path.
- ///
- Butt = 0,
-
- ///
- /// The outline extends with a rounded style passed the end of the path.
- ///
- Round = 1,
-
- ///
- /// The outlines ends squared off passed the end of the path.
- ///
- Square = 2,
-
- ///
- /// The outline is treated as a polygon.
- ///
- Polygon = 3,
-
- ///
- /// The outlines ends are joined and the path treated as a polyline
- ///
- Joined = 4
-}
-
-internal enum LineCap
-{
- Butt,
- Square,
- Round
-}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs b/src/ImageSharp.Drawing/Shapes/Helpers/ArrayBuilder{T}.cs
similarity index 98%
rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs
rename to src/ImageSharp.Drawing/Shapes/Helpers/ArrayBuilder{T}.cs
index 916592fd..c8e7cc26 100644
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs
+++ b/src/ImageSharp.Drawing/Shapes/Helpers/ArrayBuilder{T}.cs
@@ -4,7 +4,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
+namespace SixLabors.ImageSharp.Drawing.Shapes.Helpers;
///
/// A helper type for avoiding allocations while building arrays.
diff --git a/src/ImageSharp.Drawing/Shapes/IPath.cs b/src/ImageSharp.Drawing/Shapes/IPath.cs
index 755f53d7..4e8be584 100644
--- a/src/ImageSharp.Drawing/Shapes/IPath.cs
+++ b/src/ImageSharp.Drawing/Shapes/IPath.cs
@@ -13,29 +13,29 @@ public interface IPath
///
/// Gets a value indicating whether this instance is closed, open or a composite path with a mixture of open and closed figures.
///
- PathTypes PathType { get; }
+ public PathTypes PathType { get; }
///
/// Gets the bounds enclosing the path.
///
- RectangleF Bounds { get; }
+ public RectangleF Bounds { get; }
///
/// Converts the into a simple linear path.
///
/// Returns the current as simple linear path.
- IEnumerable Flatten();
+ public IEnumerable Flatten();
///
/// Transforms the path using the specified matrix.
///
/// The matrix.
/// A new path with the matrix applied to it.
- IPath Transform(Matrix3x2 matrix);
+ public IPath Transform(Matrix3x2 matrix);
///
/// Returns this path with all figures closed.
///
/// A new close .
- IPath AsClosedPath();
+ public IPath AsClosedPath();
}
diff --git a/src/ImageSharp.Drawing/Shapes/ISimplePath.cs b/src/ImageSharp.Drawing/Shapes/ISimplePath.cs
index 70727c95..cabea969 100644
--- a/src/ImageSharp.Drawing/Shapes/ISimplePath.cs
+++ b/src/ImageSharp.Drawing/Shapes/ISimplePath.cs
@@ -11,10 +11,10 @@ public interface ISimplePath
///
/// Gets a value indicating whether this instance is a closed path.
///
- bool IsClosed { get; }
+ public bool IsClosed { get; }
///
/// Gets the points that make this up as a simple linear path.
///
- ReadOnlyMemory Points { get; }
+ public ReadOnlyMemory Points { get; }
}
diff --git a/src/ImageSharp.Drawing/Shapes/InnerJoin.cs b/src/ImageSharp.Drawing/Shapes/InnerJoin.cs
new file mode 100644
index 00000000..c8c1c7b3
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/InnerJoin.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Drawing;
+
+///
+/// Specifies how inner corners of a stroked path or polygon are rendered
+/// when the path turns sharply inward. These settings control how the interior
+/// edge of the stroke is joined at such corners.
+///
+public enum InnerJoin
+{
+ ///
+ /// Joins inner corners by connecting the edges with a straight line,
+ /// producing a flat, beveled appearance.
+ ///
+ Bevel,
+
+ ///
+ /// Joins inner corners by extending the inner edges until they meet at a sharp point.
+ /// This can create long, narrow joins for acute angles.
+ ///
+ Miter,
+
+ ///
+ /// Joins inner corners with a notched appearance,
+ /// forming a small cut or indentation at the join.
+ ///
+ Jag,
+
+ ///
+ /// Joins inner corners using a circular arc between the edges,
+ /// creating a smooth, rounded interior transition.
+ ///
+ Round
+}
diff --git a/src/ImageSharp.Drawing/Shapes/JointStyle.cs b/src/ImageSharp.Drawing/Shapes/JointStyle.cs
deleted file mode 100644
index c1464824..00000000
--- a/src/ImageSharp.Drawing/Shapes/JointStyle.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Drawing;
-
-///
-/// The style to apply to the joints when generating an outline.
-///
-public enum JointStyle
-{
- ///
- /// Joints are squared off 1 width distance from the corner.
- ///
- Square = 0,
-
- ///
- /// Rounded joints. Joints generate with a rounded profile.
- ///
- Round = 1,
-
- ///
- /// Joints will generate to a long point unless the end of the point will exceed 4 times the width then we generate the joint using .
- ///
- Miter = 2
-}
-
-internal enum LineJoin
-{
- MiterJoin = 0,
- MiterJoinRevert = 1,
- RoundJoin = 2,
- BevelJoin = 3,
- MiterJoinRound = 4
-}
-
-internal enum InnerJoin
-{
- InnerBevel,
- InnerMiter,
- InnerJag,
- InnerRound
-}
diff --git a/src/ImageSharp.Drawing/Shapes/LineCap.cs b/src/ImageSharp.Drawing/Shapes/LineCap.cs
new file mode 100644
index 00000000..1df99225
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/LineCap.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Drawing;
+
+///
+/// Specifies the shape to be used at the ends of open lines or paths when stroking.
+///
+public enum LineCap
+{
+ ///
+ /// The stroke ends exactly at the endpoint.
+ /// No extension is added beyond the path's end coordinates.
+ ///
+ Butt,
+
+ ///
+ /// The stroke extends beyond the endpoint by half the line width,
+ /// producing a square edge.
+ ///
+ Square,
+
+ ///
+ /// The stroke ends with a semicircular cap,
+ /// extending beyond the endpoint by half the line width.
+ ///
+ Round
+}
diff --git a/src/ImageSharp.Drawing/Shapes/LineJoin.cs b/src/ImageSharp.Drawing/Shapes/LineJoin.cs
new file mode 100644
index 00000000..4ea8ea81
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/LineJoin.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Drawing;
+
+///
+/// Specifies how the connection between two consecutive line segments (a join)
+/// is rendered when stroking paths or polygons.
+///
+public enum LineJoin
+{
+ ///
+ /// Joins lines by extending their outer edges until they meet at a sharp corner.
+ /// The miter length is limited by the miter limit; if exceeded, the join may fall back to a bevel.
+ ///
+ Miter = 0,
+
+ ///
+ /// Joins lines by extending their outer edges to form a miter,
+ /// but if the miter length exceeds the miter limit, the join is truncated
+ /// at the limit distance rather than falling back to a bevel.
+ ///
+ MiterRevert = 1,
+
+ ///
+ /// Joins lines by connecting them with a circular arc centered at the join point,
+ /// producing a smooth, rounded corner.
+ ///
+ Round = 2,
+
+ ///
+ /// Joins lines by connecting the outer corners directly with a straight line,
+ /// forming a flat edge at the join point.
+ ///
+ Bevel = 3,
+
+ ///
+ /// Joins lines by forming a miter, but if the miter limit is exceeded,
+ /// the join falls back to a round join instead of a bevel.
+ ///
+ MiterRound = 4
+}
diff --git a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs
index 29213304..466a597f 100644
--- a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs
+++ b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs
@@ -2,8 +2,8 @@
// Licensed under the Six Labors Split License.
using System.Numerics;
-using System.Runtime.InteropServices;
-using SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
+using SixLabors.ImageSharp.Drawing.Processing;
+using SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
namespace SixLabors.ImageSharp.Drawing;
@@ -12,69 +12,31 @@ namespace SixLabors.ImageSharp.Drawing;
///
public static class OutlinePathExtensions
{
- private const float MiterOffsetDelta = 20;
- private const JointStyle DefaultJointStyle = JointStyle.Square;
- private const EndCapStyle DefaultEndCapStyle = EndCapStyle.Butt;
-
- ///
- /// Calculates the scaling matrixes tha tmust be applied to the inout and output paths of for successful clipping.
- ///
- /// the requested width
- /// The matrix to apply to the input path
- /// The matrix to apply to the output path
- /// The final width to use internally to outlining
- private static float CalculateScalingMatrix(float width, out Matrix3x2 scaleUpMartrix, out Matrix3x2 scaleDownMartrix)
- {
- // when the thickness is below a 0.5 threshold we need to scale
- // the source path (up) and result path (down) by a factor to ensure
- // the offest is greater than 0.5 to ensure offsetting isn't skipped.
- scaleUpMartrix = Matrix3x2.Identity;
- scaleDownMartrix = Matrix3x2.Identity;
- if (width < 0.5)
- {
- float scale = 1 / width;
- scaleUpMartrix = Matrix3x2.CreateScale(scale);
- scaleDownMartrix = Matrix3x2.CreateScale(width);
- width = 1;
- }
-
- return width;
- }
-
///
/// Generates an outline of the path.
///
/// The path to outline
/// The outline width.
/// A new representing the outline.
- /// Thrown when an offset cannot be calculated.
public static IPath GenerateOutline(this IPath path, float width)
- => GenerateOutline(path, width, DefaultJointStyle, DefaultEndCapStyle);
+ => GenerateOutline(path, width, new StrokeOptions());
///
/// Generates an outline of the path.
///
/// The path to outline
/// The outline width.
- /// The style to apply to the joints.
- /// The style to apply to the end caps.
+ /// The stroke geometry options.
/// A new representing the outline.
- /// Thrown when an offset cannot be calculated.
- public static IPath GenerateOutline(this IPath path, float width, JointStyle jointStyle, EndCapStyle endCapStyle)
+ public static IPath GenerateOutline(this IPath path, float width, StrokeOptions strokeOptions)
{
if (width <= 0)
{
return Path.Empty;
}
- width = CalculateScalingMatrix(width, out Matrix3x2 scaleUpMartrix, out Matrix3x2 scaleDownMartrix);
-
- ClipperOffset offset = new(MiterOffsetDelta);
-
- // transform is noop for Matrix3x2.Identity
- offset.AddPath(path.Transform(scaleUpMartrix), jointStyle, endCapStyle);
-
- return offset.Execute(width).Transform(scaleDownMartrix);
+ StrokedShapeGenerator generator = new(strokeOptions);
+ return new ComplexPolygon(generator.GenerateStrokedShapes(path, width));
}
///
@@ -84,7 +46,6 @@ public static IPath GenerateOutline(this IPath path, float width, JointStyle joi
/// The outline width.
/// The pattern made of multiples of the width.
/// A new representing the outline.
- /// Thrown when an offset cannot be calculated.
public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern)
=> path.GenerateOutline(width, pattern, false);
@@ -94,11 +55,10 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpanThe path to outline
/// The outline width.
/// The pattern made of multiples of the width.
- /// Whether the first item in the pattern is on or off.
+ /// The stroke geometry options.
/// A new representing the outline.
- /// Thrown when an offset cannot be calculated.
- public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, bool startOff)
- => GenerateOutline(path, width, pattern, startOff, DefaultJointStyle, DefaultEndCapStyle);
+ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, StrokeOptions strokeOptions)
+ => GenerateOutline(path, width, pattern, false, strokeOptions);
///
/// Generates an outline of the path with alternating on and off segments based on the pattern.
@@ -106,12 +66,10 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpanThe path to outline
/// The outline width.
/// The pattern made of multiples of the width.
- /// The style to apply to the joints.
- /// The style to apply to the end caps.
+ /// Whether the first item in the pattern is on or off.
/// A new representing the outline.
- /// Thrown when an offset cannot be calculated.
- public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, JointStyle jointStyle, EndCapStyle endCapStyle)
- => GenerateOutline(path, width, pattern, false, jointStyle, endCapStyle);
+ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, bool startOff)
+ => GenerateOutline(path, width, pattern, startOff, new StrokeOptions());
///
/// Generates an outline of the path with alternating on and off segments based on the pattern.
@@ -120,11 +78,14 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpanThe outline width.
/// The pattern made of multiples of the width.
/// Whether the first item in the pattern is on or off.
- /// The style to apply to the joints.
- /// The style to apply to the end caps.
+ /// The stroke geometry options.
/// A new representing the outline.
- /// Thrown when an offset cannot be calculated.
- public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, bool startOff, JointStyle jointStyle, EndCapStyle endCapStyle)
+ public static IPath GenerateOutline(
+ this IPath path,
+ float width,
+ ReadOnlySpan pattern,
+ bool startOff,
+ StrokeOptions strokeOptions)
{
if (width <= 0)
{
@@ -133,91 +94,148 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan paths = path.Flatten();
- IEnumerable paths = path.Transform(scaleUpMartrix).Flatten();
+ List outlines = [];
+ List buffer = new(64); // arbitrary initial capacity hint.
- ClipperOffset offset = new(MiterOffsetDelta);
- List buffer = [];
foreach (ISimplePath p in paths)
{
bool online = !startOff;
- float targetLength = pattern[0] * width;
int patternPos = 0;
- ReadOnlySpan points = p.Points.Span;
+ float targetLength = pattern[patternPos] * width;
- // Create a new list of points representing the new outline
- int pCount = points.Length;
- if (!p.IsClosed)
+ ReadOnlySpan pts = p.Points.Span;
+ if (pts.Length < 2)
{
- pCount--;
+ continue;
+ }
+
+ // number of edges to traverse (no wrap for open paths)
+ int edgeCount = p.IsClosed ? pts.Length : pts.Length - 1;
+ float totalLength = 0f;
+
+ // Compute total path length to estimate the number of dash segments to produce.
+ for (int j = 0; j < edgeCount; j++)
+ {
+ int nextIndex = p.IsClosed ? (j + 1) % pts.Length : j + 1;
+ totalLength += Vector2.Distance(pts[j], pts[nextIndex]);
+ }
+
+ if (totalLength > eps)
+ {
+ // Avoid runaway segmentation by falling back when the dash count explodes.
+ float estimatedSegments = (totalLength / patternLength) * pattern.Length;
+ if (estimatedSegments > maxPatternSegments)
+ {
+ return path.GenerateOutline(width, strokeOptions);
+ }
}
int i = 0;
- Vector2 currentPoint = points[0];
+ Vector2 current = pts[0];
- while (i < pCount)
+ while (i < edgeCount)
{
- int next = (i + 1) % points.Length;
- Vector2 targetPoint = points[next];
- float distToNext = Vector2.Distance(currentPoint, targetPoint);
- if (distToNext > targetLength)
+ int nextIndex = p.IsClosed ? (i + 1) % pts.Length : i + 1;
+ Vector2 next = pts[nextIndex];
+ float segLen = Vector2.Distance(current, next);
+
+ // Skip degenerate segments.
+ if (segLen <= eps)
+ {
+ current = next;
+ i++;
+ continue;
+ }
+
+ // Accumulate into the current dash span when the segment is shorter than the target.
+ if (segLen + eps < targetLength)
{
- // find a point between the 2
- float t = targetLength / distToNext;
+ buffer.Add(current);
+ current = next;
+ i++;
+ targetLength -= segLen;
+ continue;
+ }
- Vector2 point = (currentPoint * (1 - t)) + (targetPoint * t);
- buffer.Add(currentPoint);
- buffer.Add(point);
+ // Close out a dash span when the segment length matches the target length.
+ if (MathF.Abs(segLen - targetLength) <= eps)
+ {
+ buffer.Add(current);
+ buffer.Add(next);
- // we now inset a line joining
- if (online)
+ if (online && buffer.Count >= 2 && buffer[0] != buffer[^1])
{
- offset.AddPath(CollectionsMarshal.AsSpan(buffer), jointStyle, endCapStyle);
+ outlines.Add([.. buffer]);
}
- online = !online;
-
buffer.Clear();
+ online = !online;
- currentPoint = point;
-
- // next length
+ current = next;
+ i++;
patternPos = (patternPos + 1) % pattern.Length;
targetLength = pattern[patternPos] * width;
+ continue;
}
- else if (distToNext <= targetLength)
+
+ // Split inside this segment to end the current dash span.
+ float t = targetLength / segLen; // 0 < t < 1 here
+ Vector2 split = current + (t * (next - current));
+
+ buffer.Add(current);
+ buffer.Add(split);
+
+ if (online && buffer.Count >= 2 && buffer[0] != buffer[^1])
{
- buffer.Add(currentPoint);
- currentPoint = targetPoint;
- i++;
- targetLength -= distToNext;
+ outlines.Add([.. buffer]);
}
+
+ buffer.Clear();
+ online = !online;
+
+ current = split; // continue along the same geometric segment
+
+ patternPos = (patternPos + 1) % pattern.Length;
+ targetLength = pattern[patternPos] * width;
}
+ // flush tail of the last dash span, if any
if (buffer.Count > 0)
{
- if (p.IsClosed)
- {
- buffer.Add(points[0]);
- }
- else
- {
- buffer.Add(points[^1]);
- }
+ buffer.Add(current); // terminate at the true end position
- if (online)
+ if (online && buffer.Count >= 2 && buffer[0] != buffer[^1])
{
- offset.AddPath(CollectionsMarshal.AsSpan(buffer), jointStyle, endCapStyle);
+ outlines.Add([.. buffer]);
}
buffer.Clear();
}
}
- return offset.Execute(width).Transform(scaleDownMartrix);
+ // Each outline span is stroked as an open polyline; the union cleans overlaps.
+ StrokedShapeGenerator generator = new(strokeOptions);
+ return new ComplexPolygon(generator.GenerateStrokedShapes(outlines, width));
}
}
diff --git a/src/ImageSharp.Drawing/Shapes/Polygon.cs b/src/ImageSharp.Drawing/Shapes/Polygon.cs
index a4f60e24..e928d32e 100644
--- a/src/ImageSharp.Drawing/Shapes/Polygon.cs
+++ b/src/ImageSharp.Drawing/Shapes/Polygon.cs
@@ -55,6 +55,20 @@ internal Polygon(Path path)
{
}
+ ///
+ /// Initializes a new instance of the class using the specified line segments.
+ ///
+ ///
+ /// If owned is set to , modifications to the segments array after construction may affect
+ /// the Polygon instance. If owned is , the segments are copied to ensure the Polygon is not affected by
+ /// external changes.
+ ///
+ /// An array of line segments that define the edges of the polygon. The order of segments determines the shape of
+ /// the polygon.
+ ///
+ /// to indicate that the Polygon instance takes ownership of the segments array;
+ /// to create a copy of the array.
+ ///
internal Polygon(ILineSegment[] segments, bool owned)
: base(owned ? segments : [.. segments])
{
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs
deleted file mode 100644
index f035a06c..00000000
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs
+++ /dev/null
@@ -1,111 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
-
-///
-/// Library to clip polygons.
-///
-internal class Clipper
-{
- private readonly PolygonClipper polygonClipper;
-
- ///
- /// Initializes a new instance of the class.
- ///
- public Clipper()
- => this.polygonClipper = new PolygonClipper() { PreserveCollinear = true };
-
- ///
- /// Generates the clipped shapes from the previously provided paths.
- ///
- /// The clipping operation.
- /// The intersection rule.
- /// The .
- public IPath[] GenerateClippedShapes(ClippingOperation operation, IntersectionRule rule)
- {
- PathsF closedPaths = [];
- PathsF openPaths = [];
-
- FillRule fillRule = rule == IntersectionRule.EvenOdd ? FillRule.EvenOdd : FillRule.NonZero;
- this.polygonClipper.Execute(operation, fillRule, closedPaths, openPaths);
-
- IPath[] shapes = new IPath[closedPaths.Count + openPaths.Count];
-
- int index = 0;
- for (int i = 0; i < closedPaths.Count; i++)
- {
- PathF path = closedPaths[i];
- PointF[] points = new PointF[path.Count];
-
- for (int j = 0; j < path.Count; j++)
- {
- points[j] = path[j];
- }
-
- shapes[index++] = new Polygon(points);
- }
-
- for (int i = 0; i < openPaths.Count; i++)
- {
- PathF path = openPaths[i];
- PointF[] points = new PointF[path.Count];
-
- for (int j = 0; j < path.Count; j++)
- {
- points[j] = path[j];
- }
-
- shapes[index++] = new Polygon(points);
- }
-
- return shapes;
- }
-
- ///
- /// Adds the shapes.
- ///
- /// The paths.
- /// The clipping type.
- public void AddPaths(IEnumerable paths, ClippingType clippingType)
- {
- Guard.NotNull(paths, nameof(paths));
-
- foreach (IPath p in paths)
- {
- this.AddPath(p, clippingType);
- }
- }
-
- ///
- /// Adds the path.
- ///
- /// The path.
- /// The clipping type.
- public void AddPath(IPath path, ClippingType clippingType)
- {
- Guard.NotNull(path, nameof(path));
-
- foreach (ISimplePath p in path.Flatten())
- {
- this.AddPath(p, clippingType);
- }
- }
-
- ///
- /// Adds the path.
- ///
- /// The path.
- /// Type of the poly.
- internal void AddPath(ISimplePath path, ClippingType clippingType)
- {
- ReadOnlySpan vectors = path.Points.Span;
- PathF points = new(vectors.Length);
- for (int i = 0; i < vectors.Length; i++)
- {
- points.Add(vectors[i]);
- }
-
- this.polygonClipper.AddPath(points, clippingType, !path.IsClosed);
- }
-}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperOffset.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperOffset.cs
deleted file mode 100644
index 4c94f641..00000000
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperOffset.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
-
-///
-/// Wrapper for clipper offset
-///
-internal class ClipperOffset
-{
- private readonly PolygonOffsetter polygonClipperOffset;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// meter limit
- /// arc tolerance
- public ClipperOffset(float meterLimit = 2F, float arcTolerance = .25F)
- => this.polygonClipperOffset = new PolygonOffsetter(meterLimit, arcTolerance);
-
- ///
- /// Calculates an offset polygon based on the given path and width.
- ///
- /// Width
- /// path offset
- public ComplexPolygon Execute(float width)
- {
- PathsF solution = [];
- this.polygonClipperOffset.Execute(width, solution);
-
- Polygon[] polygons = new Polygon[solution.Count];
- for (int i = 0; i < solution.Count; i++)
- {
- PathF pt = solution[i];
- PointF[] points = pt.ToArray();
-
- polygons[i] = new Polygon(points);
- }
-
- return new ComplexPolygon(polygons);
- }
-
- ///
- /// Adds the path points
- ///
- /// The path points
- /// Joint Style
- /// Endcap Style
- public void AddPath(ReadOnlySpan pathPoints, JointStyle jointStyle, EndCapStyle endCapStyle)
- {
- PathF points = new(pathPoints.Length);
- points.AddRange(pathPoints);
-
- this.polygonClipperOffset.AddPath(points, jointStyle, endCapStyle);
- }
-
- ///
- /// Adds the path.
- ///
- /// The path.
- /// Joint Style
- /// Endcap Style
- public void AddPath(IPath path, JointStyle jointStyle, EndCapStyle endCapStyle)
- {
- Guard.NotNull(path, nameof(path));
-
- foreach (ISimplePath p in path.Flatten())
- {
- this.AddPath(p, jointStyle, endCapStyle);
- }
- }
-
- ///
- /// Adds the path.
- ///
- /// The path.
- /// Joint Style
- /// Endcap Style
- private void AddPath(ISimplePath path, JointStyle jointStyle, EndCapStyle endCapStyle)
- {
- ReadOnlySpan vectors = path.Points.Span;
- this.AddPath(vectors, jointStyle, path.IsClosed ? EndCapStyle.Joined : endCapStyle);
- }
-}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/JoinWith.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/JoinWith.cs
deleted file mode 100644
index acfbef55..00000000
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/JoinWith.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
-
-internal enum JoinWith
-{
- None,
- Left,
- Right
-}
-
-internal enum HorzPosition
-{
- Bottom,
- Middle,
- Top
-}
-
-// Vertex: a pre-clipping data structure. It is used to separate polygons
-// into ascending and descending 'bounds' (or sides) that start at local
-// minima and ascend to a local maxima, before descending again.
-[Flags]
-internal enum PointInPolygonResult
-{
- IsOn = 0,
- IsInside = 1,
- IsOutside = 2
-}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs
deleted file mode 100644
index 10c63a6e..00000000
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs
+++ /dev/null
@@ -1,700 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-using System.Numerics;
-using System.Runtime.CompilerServices;
-
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
-
-///
-/// Contains functions to offset paths (inflate/shrink).
-/// Ported from and originally licensed
-/// under
-///
-internal sealed class PolygonOffsetter
-{
- private const float Tolerance = 1.0E-6F;
- private readonly List groupList = [];
- private readonly PathF normals = [];
- private readonly PathsF solution = [];
- private float groupDelta; // *0.5 for open paths; *-1.0 for negative areas
- private float delta;
- private float absGroupDelta;
- private float mitLimSqr;
- private float stepsPerRad;
- private float stepSin;
- private float stepCos;
- private JointStyle joinType;
- private EndCapStyle endType;
-
- public PolygonOffsetter(
- float miterLimit = 2F,
- float arcTolerance = 0F,
- bool preserveCollinear = false,
- bool reverseSolution = false)
- {
- this.MiterLimit = miterLimit;
- this.ArcTolerance = arcTolerance;
- this.MergeGroups = true;
- this.PreserveCollinear = preserveCollinear;
- this.ReverseSolution = reverseSolution;
- }
-
- public float ArcTolerance { get; }
-
- public bool MergeGroups { get; }
-
- public float MiterLimit { get; }
-
- public bool PreserveCollinear { get; }
-
- public bool ReverseSolution { get; }
-
- public void AddPath(PathF path, JointStyle joinType, EndCapStyle endType)
- {
- if (path.Count == 0)
- {
- return;
- }
-
- PathsF pp = new(1) { path };
- this.AddPaths(pp, joinType, endType);
- }
-
- public void AddPaths(PathsF paths, JointStyle joinType, EndCapStyle endType)
- {
- if (paths.Count == 0)
- {
- return;
- }
-
- this.groupList.Add(new Group(paths, joinType, endType));
- }
-
- public void Execute(float delta, PathsF solution)
- {
- solution.Clear();
- this.ExecuteInternal(delta);
- if (this.groupList.Count == 0)
- {
- return;
- }
-
- // Clean up self-intersections.
- PolygonClipper clipper = new()
- {
- PreserveCollinear = this.PreserveCollinear,
-
- // The solution should retain the orientation of the input
- ReverseSolution = this.ReverseSolution != this.groupList[0].PathsReversed
- };
-
- clipper.AddSubject(this.solution);
- if (this.groupList[0].PathsReversed)
- {
- clipper.Execute(ClippingOperation.Union, FillRule.Negative, solution);
- }
- else
- {
- clipper.Execute(ClippingOperation.Union, FillRule.Positive, solution);
- }
-
- // PolygonClipper will throw for unhandled exceptions but if a result is empty
- // we should just return the original path.
- if (solution.Count == 0)
- {
- foreach (PathF path in this.solution)
- {
- solution.Add(path);
- }
- }
- }
-
- private void ExecuteInternal(float delta)
- {
- this.solution.Clear();
- if (this.groupList.Count == 0)
- {
- return;
- }
-
- if (MathF.Abs(delta) < .5F)
- {
- foreach (Group group in this.groupList)
- {
- foreach (PathF path in group.InPaths)
- {
- this.solution.Add(path);
- }
- }
- }
- else
- {
- this.delta = delta;
- this.mitLimSqr = this.MiterLimit <= 1 ? 2F : 2F / ClipperUtils.Sqr(this.MiterLimit);
- foreach (Group group in this.groupList)
- {
- this.DoGroupOffset(group);
- }
- }
- }
-
- private void DoGroupOffset(Group group)
- {
- if (group.EndType == EndCapStyle.Polygon)
- {
- // The lowermost polygon must be an outer polygon. So we can use that as the
- // designated orientation for outer polygons (needed for tidy-up clipping).
- GetBoundsAndLowestPolyIdx(group.InPaths, out int lowestIdx, out _);
- if (lowestIdx < 0)
- {
- return;
- }
-
- float area = ClipperUtils.Area(group.InPaths[lowestIdx]);
- group.PathsReversed = area < 0;
- if (group.PathsReversed)
- {
- this.groupDelta = -this.delta;
- }
- else
- {
- this.groupDelta = this.delta;
- }
- }
- else
- {
- group.PathsReversed = false;
- this.groupDelta = MathF.Abs(this.delta) * .5F;
- }
-
- this.absGroupDelta = MathF.Abs(this.groupDelta);
- this.joinType = group.JoinType;
- this.endType = group.EndType;
-
- // Calculate a sensible number of steps (for 360 deg for the given offset).
- if (group.JoinType == JointStyle.Round || group.EndType == EndCapStyle.Round)
- {
- // arcTol - when fArcTolerance is undefined (0), the amount of
- // curve imprecision that's allowed is based on the size of the
- // offset (delta). Obviously very large offsets will almost always
- // require much less precision. See also offset_triginometry2.svg
- float arcTol = this.ArcTolerance > 0.01F
- ? this.ArcTolerance
- : (float)Math.Log10(2 + this.absGroupDelta) * ClipperUtils.DefaultArcTolerance;
- float stepsPer360 = MathF.PI / (float)Math.Acos(1 - (arcTol / this.absGroupDelta));
- this.stepSin = MathF.Sin(2 * MathF.PI / stepsPer360);
- this.stepCos = MathF.Cos(2 * MathF.PI / stepsPer360);
-
- if (this.groupDelta < 0)
- {
- this.stepSin = -this.stepSin;
- }
-
- this.stepsPerRad = stepsPer360 / (2 * MathF.PI);
- }
-
- bool isJoined = group.EndType is EndCapStyle.Joined or EndCapStyle.Polygon;
-
- foreach (PathF p in group.InPaths)
- {
- PathF path = ClipperUtils.StripDuplicates(p, isJoined);
- int cnt = path.Count;
- if ((cnt == 0) || ((cnt < 3) && (this.endType == EndCapStyle.Polygon)))
- {
- continue;
- }
-
- if (cnt == 1)
- {
- group.OutPath = [];
-
- // Single vertex so build a circle or square.
- if (group.EndType == EndCapStyle.Round)
- {
- float r = this.absGroupDelta;
- group.OutPath = ClipperUtils.Ellipse(path[0], r, r);
- }
- else
- {
- float d = this.groupDelta;
- Vector2 xy = path[0];
- BoundsF r = new(xy.X - d, xy.Y - d, xy.X + d, xy.Y + d);
- group.OutPath = r.AsPath();
- }
-
- group.OutPaths.Add(group.OutPath);
- }
- else
- {
- if (cnt == 2 && group.EndType == EndCapStyle.Joined)
- {
- if (group.JoinType == JointStyle.Round)
- {
- this.endType = EndCapStyle.Round;
- }
- else
- {
- this.endType = EndCapStyle.Square;
- }
- }
-
- this.BuildNormals(path);
-
- if (this.endType == EndCapStyle.Polygon)
- {
- this.OffsetPolygon(group, path);
- }
- else if (this.endType == EndCapStyle.Joined)
- {
- this.OffsetOpenJoined(group, path);
- }
- else
- {
- this.OffsetOpenPath(group, path);
- }
- }
- }
-
- this.solution.AddRange(group.OutPaths);
- group.OutPaths.Clear();
- }
-
- private static void GetBoundsAndLowestPolyIdx(PathsF paths, out int index, out BoundsF bounds)
- {
- // TODO: default?
- bounds = new BoundsF(false); // ie invalid rect
- float pX = float.MinValue;
- index = -1;
- for (int i = 0; i < paths.Count; i++)
- {
- foreach (Vector2 pt in paths[i])
- {
- if (pt.Y >= bounds.Bottom)
- {
- if (pt.Y > bounds.Bottom || pt.X < pX)
- {
- index = i;
- pX = pt.X;
- bounds.Bottom = pt.Y;
- }
- }
- else if (pt.Y < bounds.Top)
- {
- bounds.Top = pt.Y;
- }
-
- if (pt.X > bounds.Right)
- {
- bounds.Right = pt.X;
- }
- else if (pt.X < bounds.Left)
- {
- bounds.Left = pt.X;
- }
- }
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void BuildNormals(PathF path)
- {
- int cnt = path.Count;
- this.normals.Clear();
- this.normals.EnsureCapacity(cnt);
-
- for (int i = 0; i < cnt - 1; i++)
- {
- this.normals.Add(GetUnitNormal(path[i], path[i + 1]));
- }
-
- this.normals.Add(GetUnitNormal(path[cnt - 1], path[0]));
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void OffsetOpenJoined(Group group, PathF path)
- {
- this.OffsetPolygon(group, path);
-
- // TODO: Just reverse inline?
- path = ClipperUtils.ReversePath(path);
- this.BuildNormals(path);
- this.OffsetPolygon(group, path);
- }
-
- private void OffsetOpenPath(Group group, PathF path)
- {
- group.OutPath = new PathF(path.Count);
- int highI = path.Count - 1;
-
- // Further reduced extraneous vertices in solutions (#499)
- if (MathF.Abs(this.groupDelta) < Tolerance)
- {
- group.OutPath.Add(path[0]);
- }
- else
- {
- // do the line start cap
- switch (this.endType)
- {
- case EndCapStyle.Butt:
- group.OutPath.Add(path[0] - (this.normals[0] * this.groupDelta));
- group.OutPath.Add(this.GetPerpendic(path[0], this.normals[0]));
- break;
- case EndCapStyle.Round:
- this.DoRound(group, path, 0, 0, MathF.PI);
- break;
- default:
- this.DoSquare(group, path, 0, 0);
- break;
- }
- }
-
- // offset the left side going forward
- for (int i = 1, k = 0; i < highI; i++)
- {
- this.OffsetPoint(group, path, i, ref k);
- }
-
- // reverse normals ...
- for (int i = highI; i > 0; i--)
- {
- this.normals[i] = Vector2.Negate(this.normals[i - 1]);
- }
-
- this.normals[0] = this.normals[highI];
-
- // do the line end cap
- switch (this.endType)
- {
- case EndCapStyle.Butt:
- group.OutPath.Add(new Vector2(
- path[highI].X - (this.normals[highI].X * this.groupDelta),
- path[highI].Y - (this.normals[highI].Y * this.groupDelta)));
- group.OutPath.Add(this.GetPerpendic(path[highI], this.normals[highI]));
- break;
- case EndCapStyle.Round:
- this.DoRound(group, path, highI, highI, MathF.PI);
- break;
- default:
- this.DoSquare(group, path, highI, highI);
- break;
- }
-
- // offset the left side going back
- for (int i = highI, k = 0; i > 0; i--)
- {
- this.OffsetPoint(group, path, i, ref k);
- }
-
- group.OutPaths.Add(group.OutPath);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Vector2 GetUnitNormal(Vector2 pt1, Vector2 pt2)
- {
- Vector2 dxy = pt2 - pt1;
- if (dxy == Vector2.Zero)
- {
- return default;
- }
-
- dxy *= 1F / MathF.Sqrt(ClipperUtils.DotProduct(dxy, dxy));
- return new Vector2(dxy.Y, -dxy.X);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void OffsetPolygon(Group group, PathF path)
- {
- // Dereference the current outpath.
- group.OutPath = new PathF(path.Count);
- int cnt = path.Count, prev = cnt - 1;
- for (int i = 0; i < cnt; i++)
- {
- this.OffsetPoint(group, path, i, ref prev);
- }
-
- group.OutPaths.Add(group.OutPath);
- }
-
- private void OffsetPoint(Group group, PathF path, int j, ref int k)
- {
- // Further reduced extraneous vertices in solutions (#499)
- if (MathF.Abs(this.groupDelta) < Tolerance)
- {
- group.OutPath.Add(path[j]);
- return;
- }
-
- // Let A = change in angle where edges join
- // A == 0: ie no change in angle (flat join)
- // A == PI: edges 'spike'
- // sin(A) < 0: right turning
- // cos(A) < 0: change in angle is more than 90 degree
- float sinA = ClipperUtils.CrossProduct(this.normals[j], this.normals[k]);
- float cosA = ClipperUtils.DotProduct(this.normals[j], this.normals[k]);
- if (sinA > 1F)
- {
- sinA = 1F;
- }
- else if (sinA < -1F)
- {
- sinA = -1F;
- }
-
- // almost straight - less than 1 degree (#424)
- if (cosA > 0.99F)
- {
- this.DoMiter(group, path, j, k, cosA);
- }
- else if (cosA > -0.99F && (sinA * this.groupDelta < 0F))
- {
- // is concave
- group.OutPath.Add(this.GetPerpendic(path[j], this.normals[k]));
-
- // this extra point is the only (simple) way to ensure that
- // path reversals are fully cleaned with the trailing clipper
- group.OutPath.Add(path[j]); // (#405)
- group.OutPath.Add(this.GetPerpendic(path[j], this.normals[j]));
- }
- else if (this.joinType == JointStyle.Miter)
- {
- // miter unless the angle is so acute the miter would exceeds ML
- if (cosA > this.mitLimSqr - 1)
- {
- this.DoMiter(group, path, j, k, cosA);
- }
- else
- {
- this.DoSquare(group, path, j, k);
- }
- }
- else if (this.joinType == JointStyle.Square)
- {
- // angle less than 8 degrees or a squared join
- this.DoSquare(group, path, j, k);
- }
- else
- {
- this.DoRound(group, path, j, k, MathF.Atan2(sinA, cosA));
- }
-
- k = j;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private Vector2 GetPerpendic(Vector2 pt, Vector2 norm)
- => pt + (norm * this.groupDelta);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void DoSquare(Group group, PathF path, int j, int k)
- {
- Vector2 vec;
- if (j == k)
- {
- vec = new Vector2(this.normals[0].Y, -this.normals[0].X);
- }
- else
- {
- vec = GetAvgUnitVector(
- new Vector2(-this.normals[k].Y, this.normals[k].X),
- new Vector2(this.normals[j].Y, -this.normals[j].X));
- }
-
- // now offset the original vertex delta units along unit vector
- Vector2 ptQ = path[j];
- ptQ = TranslatePoint(ptQ, this.absGroupDelta * vec.X, this.absGroupDelta * vec.Y);
-
- // get perpendicular vertices
- Vector2 pt1 = TranslatePoint(ptQ, this.groupDelta * vec.Y, this.groupDelta * -vec.X);
- Vector2 pt2 = TranslatePoint(ptQ, this.groupDelta * -vec.Y, this.groupDelta * vec.X);
-
- // get 2 vertices along one edge offset
- Vector2 pt3 = this.GetPerpendic(path[k], this.normals[k]);
-
- if (j == k)
- {
- Vector2 pt4 = pt3 + (vec * this.groupDelta);
- Vector2 pt = IntersectPoint(pt1, pt2, pt3, pt4);
-
- // get the second intersect point through reflecion
- group.OutPath.Add(ReflectPoint(pt, ptQ));
- group.OutPath.Add(pt);
- }
- else
- {
- Vector2 pt4 = this.GetPerpendic(path[j], this.normals[k]);
- Vector2 pt = IntersectPoint(pt1, pt2, pt3, pt4);
-
- group.OutPath.Add(pt);
-
- // Get the second intersect point through reflecion
- group.OutPath.Add(ReflectPoint(pt, ptQ));
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void DoMiter(Group group, PathF path, int j, int k, float cosA)
- {
- float q = this.groupDelta / (cosA + 1);
- Vector2 pv = path[j];
- Vector2 nk = this.normals[k];
- Vector2 nj = this.normals[j];
- group.OutPath.Add(pv + ((nk + nj) * q));
- }
-
- private void DoRound(Group group, PathF path, int j, int k, float angle)
- {
- Vector2 pt = path[j];
- Vector2 offsetVec = this.normals[k] * new Vector2(this.groupDelta);
- if (j == k)
- {
- offsetVec = Vector2.Negate(offsetVec);
- }
-
- group.OutPath.Add(pt + offsetVec);
-
- // avoid 180deg concave
- if (angle > -MathF.PI + .01F)
- {
- int steps = Math.Max(2, (int)Math.Ceiling(this.stepsPerRad * MathF.Abs(angle)));
-
- // ie 1 less than steps
- for (int i = 1; i < steps; i++)
- {
- offsetVec = new Vector2((offsetVec.X * this.stepCos) - (this.stepSin * offsetVec.Y), (offsetVec.X * this.stepSin) + (offsetVec.Y * this.stepCos));
-
- group.OutPath.Add(pt + offsetVec);
- }
- }
-
- group.OutPath.Add(this.GetPerpendic(pt, this.normals[j]));
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Vector2 TranslatePoint(Vector2 pt, float dx, float dy)
- => pt + new Vector2(dx, dy);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Vector2 ReflectPoint(Vector2 pt, Vector2 pivot)
- => pivot + (pivot - pt);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Vector2 IntersectPoint(Vector2 pt1a, Vector2 pt1b, Vector2 pt2a, Vector2 pt2b)
- {
- // vertical
- if (ClipperUtils.IsAlmostZero(pt1a.X - pt1b.X))
- {
- if (ClipperUtils.IsAlmostZero(pt2a.X - pt2b.X))
- {
- return default;
- }
-
- float m2 = (pt2b.Y - pt2a.Y) / (pt2b.X - pt2a.X);
- float b2 = pt2a.Y - (m2 * pt2a.X);
- return new Vector2(pt1a.X, (m2 * pt1a.X) + b2);
- }
-
- // vertical
- if (ClipperUtils.IsAlmostZero(pt2a.X - pt2b.X))
- {
- float m1 = (pt1b.Y - pt1a.Y) / (pt1b.X - pt1a.X);
- float b1 = pt1a.Y - (m1 * pt1a.X);
- return new Vector2(pt2a.X, (m1 * pt2a.X) + b1);
- }
- else
- {
- float m1 = (pt1b.Y - pt1a.Y) / (pt1b.X - pt1a.X);
- float b1 = pt1a.Y - (m1 * pt1a.X);
- float m2 = (pt2b.Y - pt2a.Y) / (pt2b.X - pt2a.X);
- float b2 = pt2a.Y - (m2 * pt2a.X);
- if (ClipperUtils.IsAlmostZero(m1 - m2))
- {
- return default;
- }
-
- float x = (b2 - b1) / (m1 - m2);
- return new Vector2(x, (m1 * x) + b1);
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Vector2 GetAvgUnitVector(Vector2 vec1, Vector2 vec2)
- => NormalizeVector(vec1 + vec2);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static float Hypotenuse(Vector2 vector)
- => MathF.Sqrt(Vector2.Dot(vector, vector));
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Vector2 NormalizeVector(Vector2 vector)
- {
- float h = Hypotenuse(vector);
- if (ClipperUtils.IsAlmostZero(h))
- {
- return default;
- }
-
- float inverseHypot = 1 / h;
- return vector * inverseHypot;
- }
-
- private class Group
- {
- public Group(PathsF paths, JointStyle joinType, EndCapStyle endType = EndCapStyle.Polygon)
- {
- this.InPaths = paths;
- this.JoinType = joinType;
- this.EndType = endType;
- this.OutPath = [];
- this.OutPaths = [];
- this.PathsReversed = false;
- }
-
- public PathF OutPath { get; set; }
-
- public PathsF OutPaths { get; }
-
- public JointStyle JoinType { get; }
-
- public EndCapStyle EndType { get; set; }
-
- public bool PathsReversed { get; set; }
-
- public PathsF InPaths { get; }
- }
-}
-
-internal class PathsF : List
-{
- public PathsF()
- {
- }
-
- public PathsF(IEnumerable items)
- : base(items)
- {
- }
-
- public PathsF(int capacity)
- : base(capacity)
- {
- }
-}
-
-internal class PathF : List
-{
- public PathF()
- {
- }
-
- public PathF(IEnumerable items)
- : base(items)
- {
- }
-
- public PathF(int capacity)
- : base(capacity)
- {
- }
-}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ArrayBuilder{T}.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ArrayBuilder{T}.cs
new file mode 100644
index 00000000..6654102c
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ArrayBuilder{T}.cs
@@ -0,0 +1,156 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
+
+///
+/// A helper type for avoiding allocations while building arrays.
+///
+/// The type of item contained in the array.
+internal struct ArrayBuilder
+ where T : struct
+{
+ private const int DefaultCapacity = 4;
+
+ // Starts out null, initialized on first Add.
+ private T[]? data;
+ private int size;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The initial capacity of the array.
+ public ArrayBuilder(int capacity)
+ : this()
+ {
+ if (capacity > 0)
+ {
+ this.data = new T[capacity];
+ }
+ }
+
+ ///
+ /// Gets or sets the number of items in the array.
+ ///
+ public int Length
+ {
+ readonly get => this.size;
+
+ set
+ {
+ if (value > 0)
+ {
+ this.EnsureCapacity(value);
+ this.size = value;
+ }
+ else
+ {
+ this.size = 0;
+ }
+ }
+ }
+
+ ///
+ /// Returns a reference to specified element of the array.
+ ///
+ /// The index of the element to return.
+ /// The .
+ ///
+ /// Thrown when index less than 0 or index greater than or equal to .
+ ///
+ public readonly ref T this[int index]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ DebugGuard.MustBeBetweenOrEqualTo(index, 0, this.size, nameof(index));
+ return ref this.data![index];
+ }
+ }
+
+ ///
+ /// Adds the given item to the array.
+ ///
+ /// The item to add.
+ public void Add(T item)
+ {
+ int position = this.size;
+ T[]? array = this.data;
+
+ if (array != null && (uint)position < (uint)array.Length)
+ {
+ this.size = position + 1;
+ array[position] = item;
+ }
+ else
+ {
+ this.AddWithResize(item);
+ }
+ }
+
+ // Non-inline from Add to improve its code quality as uncommon path
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void AddWithResize(T item)
+ {
+ int size = this.size;
+ this.Grow(size + 1);
+ this.size = size + 1;
+ this.data[size] = item;
+ }
+
+ ///
+ /// Remove the last item from the array.
+ ///
+ public void RemoveLast()
+ {
+ DebugGuard.MustBeGreaterThan(this.size, 0, nameof(this.size));
+ this.size--;
+ }
+
+ ///
+ /// Clears the array.
+ /// Allocated memory is left intact for future usage.
+ ///
+ public void Clear() =>
+
+ // No need to actually clear since we're not allowing reference types.
+ this.size = 0;
+
+ private void EnsureCapacity(int min)
+ {
+ int length = this.data?.Length ?? 0;
+ if (length < min)
+ {
+ this.Grow(min);
+ }
+ }
+
+ [MemberNotNull(nameof(this.data))]
+ private void Grow(int capacity)
+ {
+ // Same expansion algorithm as List.
+ int length = this.data?.Length ?? 0;
+ int newCapacity = length == 0 ? DefaultCapacity : length * 2;
+ if ((uint)newCapacity > Array.MaxLength)
+ {
+ newCapacity = Array.MaxLength;
+ }
+
+ if (newCapacity < capacity)
+ {
+ newCapacity = capacity;
+ }
+
+ T[] array = new T[newCapacity];
+
+ if (this.size > 0)
+ {
+ Array.Copy(this.data!, array, this.size);
+ }
+
+ this.data = array;
+ }
+}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/BoundsF.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/BoundsF.cs
similarity index 95%
rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/BoundsF.cs
rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/BoundsF.cs
index 9d48889a..14ac870b 100644
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/BoundsF.cs
+++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/BoundsF.cs
@@ -3,7 +3,7 @@
using System.Numerics;
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
+namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
internal struct BoundsF
{
@@ -80,11 +80,11 @@ public readonly bool Intersects(BoundsF bounds)
&& (Math.Max(this.Top, bounds.Top) < Math.Min(this.Bottom, bounds.Bottom));
public readonly PathF AsPath()
- => new(4)
- {
+ =>
+ [
new Vector2(this.Left, this.Top),
new Vector2(this.Right, this.Top),
new Vector2(this.Right, this.Bottom),
new Vector2(this.Left, this.Bottom)
- };
+ ];
}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs
new file mode 100644
index 00000000..7fff6b6e
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs
@@ -0,0 +1,134 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
+
+///
+/// Generates clipped shapes from one or more input paths using polygon boolean operations.
+///
+///
+/// This class provides a high-level wrapper around the low-level .
+/// It accumulates subject and clip polygons, applies the specified ,
+/// and converts the resulting polygon contours back into instances suitable
+/// for rendering or further processing.
+///
+internal sealed class ClippedShapeGenerator
+{
+ private readonly PolygonClipper polygonClipper;
+ private readonly IntersectionRule rule;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The intersection rule.
+ public ClippedShapeGenerator(IntersectionRule rule)
+ {
+ this.rule = rule;
+ this.polygonClipper = new PolygonClipper() { PreserveCollinear = true };
+ }
+
+ ///
+ /// Generates the final clipped shapes from the previously provided subject and clip paths.
+ ///
+ ///
+ /// The boolean operation to perform, such as ,
+ /// , or .
+ ///
+ /// TEMP. Remove when we update IntersectionRule to add missing entries.
+ ///
+ /// An array of instances representing the result of the boolean operation.
+ ///
+ public IPath[] GenerateClippedShapes(BooleanOperation operation, bool? positive = null)
+ {
+ PathsF closedPaths = [];
+ PathsF openPaths = [];
+
+ ClipperFillRule fillRule = this.rule == IntersectionRule.EvenOdd ? ClipperFillRule.EvenOdd : ClipperFillRule.NonZero;
+
+ if (positive.HasValue)
+ {
+ fillRule = positive.Value ? ClipperFillRule.Positive : ClipperFillRule.Negative;
+ }
+
+ this.polygonClipper.Execute(operation, fillRule, closedPaths, openPaths);
+
+ IPath[] shapes = new IPath[closedPaths.Count + openPaths.Count];
+
+ int index = 0;
+ for (int i = 0; i < closedPaths.Count; i++)
+ {
+ PathF path = closedPaths[i];
+ PointF[] points = new PointF[path.Count];
+
+ for (int j = 0; j < path.Count; j++)
+ {
+ points[j] = path[j];
+ }
+
+ shapes[index++] = new Polygon(points);
+ }
+
+ for (int i = 0; i < openPaths.Count; i++)
+ {
+ PathF path = openPaths[i];
+ PointF[] points = new PointF[path.Count];
+
+ for (int j = 0; j < path.Count; j++)
+ {
+ points[j] = path[j];
+ }
+
+ shapes[index++] = new Polygon(points);
+ }
+
+ return shapes;
+ }
+
+ ///
+ /// Adds a collection of paths to the current clipping operation.
+ ///
+ ///
+ /// The paths to add. Each path may represent a simple or complex polygon.
+ ///
+ ///
+ /// Determines whether the paths are assigned to the subject or clip polygon.
+ ///
+ public void AddPaths(IEnumerable paths, ClippingType clippingType)
+ {
+ Guard.NotNull(paths, nameof(paths));
+
+ foreach (IPath p in paths)
+ {
+ this.AddPath(p, clippingType);
+ }
+ }
+
+ ///
+ /// Adds a single path to the current clipping operation.
+ ///
+ /// The path to add.
+ ///
+ /// Determines whether the path is assigned to the subject or clip polygon.
+ ///
+ public void AddPath(IPath path, ClippingType clippingType)
+ {
+ Guard.NotNull(path, nameof(path));
+
+ foreach (ISimplePath p in path.Flatten())
+ {
+ this.AddPath(p, clippingType);
+ }
+ }
+
+ private void AddPath(ISimplePath path, ClippingType clippingType)
+ {
+ ReadOnlySpan vectors = path.Points.Span;
+ PathF points = new(vectors.Length);
+ for (int i = 0; i < vectors.Length; i++)
+ {
+ points.Add(vectors[i]);
+ }
+
+ this.polygonClipper.AddPath(points, clippingType, !path.IsClosed);
+ }
+}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperException.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperException.cs
similarity index 95%
rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperException.cs
rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperException.cs
index 39ddcfa0..d22aff79 100644
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperException.cs
+++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperException.cs
@@ -1,7 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
+namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
///
/// The exception that is thrown when an error occurs clipping a polygon.
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/FillRule.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperFillRule.cs
similarity index 86%
rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/FillRule.cs
rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperFillRule.cs
index a4f42b29..90d1c614 100644
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/FillRule.cs
+++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperFillRule.cs
@@ -3,7 +3,7 @@
using SixLabors.ImageSharp.Drawing.Shapes.Rasterization;
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
+namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
///
/// By far the most widely used filling rules for polygons are EvenOdd
@@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
/// TODO: This overlaps with the enum.
/// We should see if we can enhance the to support all these rules.
///
-internal enum FillRule
+internal enum ClipperFillRule
{
EvenOdd,
NonZero,
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClippingType.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippingType.cs
similarity index 80%
rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/ClippingType.cs
rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippingType.cs
index 00aa96a4..2ac4ef90 100644
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClippingType.cs
+++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippingType.cs
@@ -1,12 +1,12 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
+namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
///
/// Defines the polygon clipping type.
///
-public enum ClippingType
+internal enum ClippingType
{
///
/// Represents a shape to act as a subject which will be clipped or merged.
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/JoinWith.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/JoinWith.cs
new file mode 100644
index 00000000..ee3272a8
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/JoinWith.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
+
+///
+/// Specifies how a vertex should be joined with adjacent paths during polygon operations.
+///
+internal enum JoinWith
+{
+ ///
+ /// No joining operation.
+ ///
+ None,
+
+ ///
+ /// Join with the left adjacent path.
+ ///
+ Left,
+
+ ///
+ /// Join with the right adjacent path.
+ ///
+ Right
+}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs
similarity index 78%
rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipper.cs
rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs
index 042382cd..58a6fd2c 100644
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipper.cs
+++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs
@@ -7,31 +7,44 @@
using System.Numerics;
using System.Runtime.CompilerServices;
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
+namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
///
/// Contains functions that cover most polygon boolean and offsetting needs.
/// Ported from and originally licensed
/// under
///
+///
+/// This class implements the Vatti clipping algorithm using a scanline sweep approach.
+/// It processes polygon edges by sweeping a horizontal line from bottom to top,
+/// maintaining an active edge list (AEL) of edges that intersect the current scanline.
+///
internal sealed class PolygonClipper
{
- private ClippingOperation clipType;
- private FillRule fillRule;
- private Active actives;
- private Active flaggedHorizontal;
- private readonly List minimaList;
- private readonly List intersectList;
- private readonly List vertexList;
- private readonly List outrecList;
- private readonly List scanlineList;
- private readonly List horzSegList;
- private readonly List horzJoinList;
- private int currentLocMin;
- private float currentBotY;
- private bool isSortedMinimaList;
- private bool hasOpenPaths;
-
+ private const float MinimumDistanceThreshold = 1e-6f;
+ private const float MinimumAreaThreshold = 1e-6f;
+ private const float JoinYTolerance = 1e-6f;
+ private const float JoinDistanceSqrdThreshold = 1e-12f;
+
+ private BooleanOperation clipType;
+ private ClipperFillRule fillRule;
+ private Active actives; // Head of the active edge list
+ private Active flaggedHorizontal; // Linked list of horizontal edges awaiting processing
+ private readonly List minimaList; // Local minima sorted by Y coordinate
+ private readonly List intersectList; // Intersections at current scanbeam
+ private readonly List vertexList; // All vertices from input paths
+ private readonly List outrecList; // Output polygon records
+ private readonly List scanlineList; // Y coordinates requiring processing
+ private readonly List horzSegList; // Horizontal segments for joining
+ private readonly List horzJoinList; // Horizontal joins to process
+ private int currentLocMin; // Index of current local minimum being processed
+ private float currentBotY; // Y coordinate of current scanbeam bottom
+ private bool isSortedMinimaList; // Whether minimaList has been sorted
+ private bool hasOpenPaths; // Whether any input paths are open (not closed)
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
public PolygonClipper()
{
this.minimaList = [];
@@ -44,20 +57,36 @@ public PolygonClipper()
this.PreserveCollinear = true;
}
+ ///
+ /// Gets or sets a value indicating whether collinear vertices should be preserved in the output.
+ /// When true, only 180-degree spikes are removed. When false, all collinear vertices are removed.
+ ///
public bool PreserveCollinear { get; set; }
+ ///
+ /// Gets or sets a value indicating whether the output polygon orientation should be reversed.
+ ///
public bool ReverseSolution { get; set; }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void AddSubject(PathsF paths) => this.AddPaths(paths, ClippingType.Subject);
-
+ ///
+ /// Adds a single path to the clipping operation.
+ ///
+ /// The path to add.
+ /// Whether this is a subject or clip path.
+ /// Whether the path is open (polyline) or closed (polygon).
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddPath(PathF path, ClippingType polytype, bool isOpen = false)
{
- PathsF tmp = new(1) { path };
+ PathsF tmp = [path];
this.AddPaths(tmp, polytype, isOpen);
}
+ ///
+ /// Adds multiple paths to the clipping operation.
+ ///
+ /// The paths to add.
+ /// Whether these are subject or clip paths.
+ /// Whether the paths are open (polylines) or closed (polygons).
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddPaths(PathsF paths, ClippingType polytype, bool isOpen = false)
{
@@ -70,11 +99,25 @@ public void AddPaths(PathsF paths, ClippingType polytype, bool isOpen = false)
this.AddPathsToVertexList(paths, polytype, isOpen);
}
+ ///
+ /// Executes the clipping operation and returns only closed paths.
+ ///
+ /// The boolean operation to perform.
+ /// The fill rule to use for polygon interiors.
+ /// Output collection for closed solution paths.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void Execute(ClippingOperation clipType, FillRule fillRule, PathsF solutionClosed)
+ public void Execute(BooleanOperation clipType, ClipperFillRule fillRule, PathsF solutionClosed)
=> this.Execute(clipType, fillRule, solutionClosed, []);
- public void Execute(ClippingOperation clipType, FillRule fillRule, PathsF solutionClosed, PathsF solutionOpen)
+ ///
+ /// Executes the clipping operation and returns both closed and open paths.
+ ///
+ /// The boolean operation to perform (union, intersection, difference, xor).
+ /// The fill rule to determine polygon interiors (even-odd, non-zero, positive, negative).
+ /// Output collection for closed solution paths (polygons).
+ /// Output collection for open solution paths (polylines).
+ /// Thrown when an error occurs during clipping.
+ public void Execute(BooleanOperation clipType, ClipperFillRule fillRule, PathsF solutionClosed, PathsF solutionOpen)
{
solutionClosed.Clear();
solutionOpen.Clear();
@@ -94,16 +137,19 @@ public void Execute(ClippingOperation clipType, FillRule fillRule, PathsF soluti
}
}
- private void ExecuteInternal(ClippingOperation ct, FillRule fillRule)
+ ///
+ /// Executes the core clipping algorithm using the Vatti scanbeam sweep.
+ /// Processes all edges from bottom to top, handling intersections and building output polygons.
+ ///
+ /// The boolean operation type.
+ /// The fill rule for determining polygon interiors.
+ private void ExecuteInternal(BooleanOperation ct, ClipperFillRule fillRule)
{
- if (ct == ClippingOperation.None)
- {
- return;
- }
-
this.fillRule = fillRule;
this.clipType = ct;
this.Reset();
+
+ // Get the first scanline Y coordinate
if (!this.PopScanline(out float y))
{
return;
@@ -111,36 +157,53 @@ private void ExecuteInternal(ClippingOperation ct, FillRule fillRule)
while (true)
{
+ // Add local minima edges that start at current Y
this.InsertLocalMinimaIntoAEL(y);
+
+ // Process all horizontal edges at this Y
Active ae;
while (this.PopHorz(out ae))
{
this.DoHorizontal(ae);
}
+ // Convert horizontal segments to joins for later processing
if (this.horzSegList.Count > 0)
{
this.ConvertHorzSegsToJoins();
this.horzSegList.Clear();
}
- this.currentBotY = y; // bottom of scanbeam
+ this.currentBotY = y; // bottom of current scanbeam
+
+ // Get next scanline; break if no more
if (!this.PopScanline(out y))
{
- break; // y new top of scanbeam
+ break;
}
+ // Process intersections between current and next scanline
this.DoIntersections(y);
+
+ // Update edges at top of scanbeam
this.DoTopOfScanbeam(y);
+
+ // Process any horizontal edges that emerged
while (this.PopHorz(out ae))
{
this.DoHorizontal(ae!);
}
}
+ // Complete horizontal joins
this.ProcessHorzJoins();
}
+ ///
+ /// Processes edge intersections at the top of the current scanbeam.
+ /// Builds intersection list, processes intersections in order, then cleans up.
+ ///
+ /// The Y coordinate of the top of the scanbeam.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DoIntersections(float topY)
{
@@ -151,69 +214,53 @@ private void DoIntersections(float topY)
}
}
+ ///
+ /// Clears the intersection node list.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DisposeIntersectNodes()
=> this.intersectList.Clear();
+ ///
+ /// Adds a new intersection node for two edges at the specified Y coordinate.
+ /// Calculates the exact intersection point, adjusting for numerical precision when needed.
+ ///
+ /// First edge.
+ /// Second edge.
+ /// Top Y coordinate of the scanbeam.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void AddNewIntersectNode(Active ae1, Active ae2, float topY)
{
- if (!ClipperUtils.GetIntersectPt(ae1.Bot, ae1.Top, ae2.Bot, ae2.Top, out Vector2 ip))
+ // Calculate line intersection point
+ if (!PolygonClipperUtilities.GetLineIntersectPoint(ae1.Bot, ae1.Top, ae2.Bot, ae2.Top, out Vector2 ip))
{
+ // Lines are parallel; use current X position
ip = new Vector2(ae1.CurX, topY);
}
+ // Adjust intersection point if it's outside the scanbeam bounds
if (ip.Y > this.currentBotY || ip.Y < topY)
{
+ // Clamp Y to scanbeam bounds
+ ip.Y = ip.Y < topY ? topY : this.currentBotY;
+
+ // Use the more vertical edge (smaller |Dx|) to compute X for numerical stability
float absDx1 = MathF.Abs(ae1.Dx);
float absDx2 = MathF.Abs(ae2.Dx);
-
- // TODO: Check threshold here once we remove upscaling.
- if (absDx1 > 100 && absDx2 > 100)
- {
- if (absDx1 > absDx2)
- {
- ip = ClipperUtils.GetClosestPtOnSegment(ip, ae1.Bot, ae1.Top);
- }
- else
- {
- ip = ClipperUtils.GetClosestPtOnSegment(ip, ae2.Bot, ae2.Top);
- }
- }
- else if (absDx1 > 100)
- {
- ip = ClipperUtils.GetClosestPtOnSegment(ip, ae1.Bot, ae1.Top);
- }
- else if (absDx2 > 100)
- {
- ip = ClipperUtils.GetClosestPtOnSegment(ip, ae2.Bot, ae2.Top);
- }
- else
- {
- if (ip.Y < topY)
- {
- ip.Y = topY;
- }
- else
- {
- ip.Y = this.currentBotY;
- }
-
- if (absDx1 < absDx2)
- {
- ip.X = TopX(ae1, ip.Y);
- }
- else
- {
- ip.X = TopX(ae2, ip.Y);
- }
- }
+ ip.X = absDx1 < absDx2 ? TopX(ae1, ip.Y) : TopX(ae2, ip.Y);
}
IntersectNode node = new(ip, ae1, ae2);
this.intersectList.Add(node);
}
+ ///
+ /// Sets the heading direction for a horizontal segment based on two output points.
+ ///
+ /// The horizontal segment to configure.
+ /// Previous output point.
+ /// Next output point.
+ /// True if the segment has a valid direction; false if the points have the same X coordinate.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool SetHorzSegHeadingForward(HorzSegment hs, OutPt opP, OutPt opN)
{
@@ -238,6 +285,11 @@ private static bool SetHorzSegHeadingForward(HorzSegment hs, OutPt opP, OutPt op
return true;
}
+ ///
+ /// Updates a horizontal segment by extending it to include all consecutive horizontal output points.
+ ///
+ /// The horizontal segment to update.
+ /// True if the segment was successfully updated; false otherwise.
private static bool UpdateHorzSegment(HorzSegment hs)
{
OutPt op = hs.LeftOp;
@@ -245,6 +297,8 @@ private static bool UpdateHorzSegment(HorzSegment hs)
bool outrecHasEdges = outrec.FrontEdge != null;
float curr_y = op.Point.Y;
OutPt opP = op, opN = op;
+
+ // Extend the segment backwards and forwards along the horizontal line
if (outrecHasEdges)
{
OutPt opA = outrec.Pts!, opZ = opA.Next;
@@ -279,12 +333,18 @@ private static bool UpdateHorzSegment(HorzSegment hs)
}
else
{
- hs.RightOp = null; // (for sorting)
+ hs.RightOp = null; // Mark as invalid for sorting
}
return result;
}
+ ///
+ /// Duplicates an output point, inserting it either after or before the original.
+ ///
+ /// The output point to duplicate.
+ /// If true, insert after op; otherwise insert before.
+ /// The newly created output point.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static OutPt DuplicateOp(OutPt op, bool insert_after)
{
@@ -307,9 +367,15 @@ private static OutPt DuplicateOp(OutPt op, bool insert_after)
return result;
}
+ ///
+ /// Converts horizontal segments into join operations.
+ /// Finds overlapping horizontal segments and creates joins between them.
+ ///
private void ConvertHorzSegsToJoins()
{
int k = 0;
+
+ // Update all segments and count valid ones
foreach (HorzSegment hs in this.horzSegList)
{
if (UpdateHorzSegment(hs))
@@ -320,19 +386,23 @@ private void ConvertHorzSegsToJoins()
if (k < 2)
{
- return;
+ return; // Need at least 2 segments to join
}
+ // Sort segments by left X coordinate
this.horzSegList.Sort(default(HorzSegSorter));
+ // Find overlapping segments and create joins
for (int i = 0; i < k - 1; i++)
{
HorzSegment hs1 = this.horzSegList[i];
- // for each HorzSegment, find others that overlap
+ // Check each subsequent segment for overlap
for (int j = i + 1; j < k; j++)
{
HorzSegment hs2 = this.horzSegList[j];
+
+ // Skip if no overlap or same direction
if ((hs2.LeftOp.Point.X >= hs1.RightOp.Point.X) ||
(hs2.LeftToRight == hs1.LeftToRight) ||
(hs2.RightOp.Point.X <= hs1.LeftOp.Point.X))
@@ -341,6 +411,8 @@ private void ConvertHorzSegsToJoins()
}
float curr_y = hs1.LeftOp.Point.Y;
+
+ // Adjust segment endpoints to find join points
if (hs1.LeftToRight)
{
while (hs1.LeftOp.Next.Point.Y == curr_y &&
@@ -379,6 +451,9 @@ private void ConvertHorzSegsToJoins()
}
}
+ ///
+ /// Clears the solution data while preserving input paths.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ClearSolutionOnly()
{
@@ -394,6 +469,13 @@ private void ClearSolutionOnly()
this.horzJoinList.Clear();
}
+ ///
+ /// Builds output paths from the output record list.
+ /// Processes each output record, cleaning collinear points and building the final paths.
+ ///
+ /// Collection to receive closed paths (polygons).
+ /// Collection to receive open paths (polylines).
+ /// True if paths were successfully built.
private bool BuildPaths(PathsF solutionClosed, PathsF solutionOpen)
{
solutionClosed.Clear();
@@ -403,7 +485,7 @@ private bool BuildPaths(PathsF solutionClosed, PathsF solutionOpen)
int i = 0;
- // _outrecList.Count is not static here because
+ // Note: outrecList.Count is not static here because
// CleanCollinear can indirectly add additional OutRec
while (i < this.outrecList.Count)
{
@@ -423,9 +505,10 @@ private bool BuildPaths(PathsF solutionClosed, PathsF solutionOpen)
}
else
{
+ // Clean collinear points from closed paths
this.CleanCollinear(outrec);
- // closed paths should always return a Positive orientation
+ // Closed paths should always return a positive orientation
// except when ReverseSolution == true
if (BuildPath(outrec.Pts, this.ReverseSolution, false, path))
{
@@ -437,8 +520,17 @@ private bool BuildPaths(PathsF solutionClosed, PathsF solutionOpen)
return true;
}
+ ///
+ /// Builds a path from an output point list.
+ ///
+ /// Starting output point.
+ /// Whether to traverse the list in reverse.
+ /// Whether this is an open path.
+ /// The path to populate.
+ /// True if a valid path was created.
private static bool BuildPath(OutPt op, bool reverse, bool isOpen, PathF path)
{
+ // Validate minimum path requirements
if (op == null || op.Next == op || (!isOpen && op.Next == op.Prev))
{
return false;
@@ -448,6 +540,8 @@ private static bool BuildPath(OutPt op, bool reverse, bool isOpen, PathF path)
Vector2 lastPt;
OutPt op2;
+
+ // Set starting point and direction
if (reverse)
{
lastPt = op.Point;
@@ -462,6 +556,7 @@ private static bool BuildPath(OutPt op, bool reverse, bool isOpen, PathF path)
path.Add(lastPt);
+ // Traverse the output point list, adding unique points
while (op2 != op)
{
if (op2.Point != lastPt)
@@ -480,6 +575,7 @@ private static bool BuildPath(OutPt op, bool reverse, bool isOpen, PathF path)
}
}
+ // Filter out very small triangles
return path.Count != 3 || !IsVerySmallTriangle(op2);
}
@@ -969,9 +1065,11 @@ private void ProcessHorzJoins()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool PtsReallyClose(Vector2 pt1, Vector2 pt2)
-
- // TODO: Check scale once we can remove upscaling.
- => (Math.Abs(pt1.X - pt2.X) < 2F) && (Math.Abs(pt1.Y - pt2.Y) < 2F);
+ {
+ Vector2 delta = Vector2.Abs(pt1 - pt2);
+ return delta.X < MinimumDistanceThreshold &&
+ delta.Y < MinimumDistanceThreshold;
+ }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CleanCollinear(OutRec outrec)
@@ -994,8 +1092,8 @@ private void CleanCollinear(OutRec outrec)
do
{
// NB if preserveCollinear == true, then only remove 180 deg. spikes
- if ((ClipperUtils.CrossProduct(op2.Prev.Point, op2.Point, op2.Next.Point) == 0)
- && ((op2.Point == op2.Prev.Point) || (op2.Point == op2.Next.Point) || !this.PreserveCollinear || (ClipperUtils.DotProduct(op2.Prev.Point, op2.Point, op2.Next.Point) < 0)))
+ if ((PolygonClipperUtilities.CrossProduct(op2.Prev.Point, op2.Point, op2.Next.Point) == 0)
+ && ((op2.Point == op2.Prev.Point) || (op2.Point == op2.Next.Point) || !this.PreserveCollinear || (PolygonClipperUtilities.DotProduct(op2.Prev.Point, op2.Point, op2.Next.Point) < 0)))
{
if (op2 == outrec.Pts)
{
@@ -1029,20 +1127,20 @@ private void DoSplitOp(OutRec outrec, OutPt splitOp)
OutPt nextNextOp = splitOp.Next.Next;
outrec.Pts = prevOp;
- ClipperUtils.GetIntersectPoint(
+ _ = PolygonClipperUtilities.GetLineIntersectPoint(
prevOp.Point, splitOp.Point, splitOp.Next.Point, nextNextOp.Point, out Vector2 ip);
float area1 = Area(prevOp);
- float absArea1 = Math.Abs(area1);
+ float absArea1 = MathF.Abs(area1);
- if (absArea1 < 2)
+ if (absArea1 < MinimumAreaThreshold)
{
outrec.Pts = null;
return;
}
float area2 = AreaTriangle(ip, splitOp.Point, splitOp.Next.Point);
- float absArea2 = Math.Abs(area2);
+ float absArea2 = MathF.Abs(area2);
// de-link splitOp and splitOp.next from the path
// while inserting the intersection point
@@ -1068,7 +1166,7 @@ private void DoSplitOp(OutRec outrec, OutPt splitOp)
// So the only way for these areas to have the same sign is if
// the split triangle is larger than the path containing prevOp or
// if there's more than one self=intersection.
- if (absArea2 > 1 && (absArea2 > absArea1 || ((area2 > 0) == (area1 > 0))))
+ if (absArea2 > MinimumAreaThreshold && (absArea2 > absArea1 || ((area2 > 0) == (area1 > 0))))
{
OutRec newOutRec = this.NewOutRec();
newOutRec.Owner = outrec.Owner;
@@ -1090,7 +1188,7 @@ private void FixSelfIntersects(OutRec outrec)
// triangles can't self-intersect
while (op2.Prev != op2.Next.Next)
{
- if (ClipperUtils.SegsIntersect(op2.Prev.Point, op2.Point, op2.Next.Point, op2.Next.Next.Point))
+ if (PolygonClipperUtilities.SegsIntersect(op2.Prev.Point, op2.Point, op2.Next.Point, op2.Next.Next.Point))
{
this.DoSplitOp(outrec, op2);
if (outrec.Pts == null)
@@ -1582,7 +1680,7 @@ private OutPt IntersectEdges(Active ae1, Active ae2, Vector2 pt)
this.Split(ae2, pt); // needed for safety
}
- if (this.clipType == ClippingOperation.Union)
+ if (this.clipType == BooleanOperation.Union)
{
if (!IsHotEdge(ae2))
{
@@ -1596,14 +1694,14 @@ private OutPt IntersectEdges(Active ae1, Active ae2, Vector2 pt)
switch (this.fillRule)
{
- case FillRule.Positive:
+ case ClipperFillRule.Positive:
if (ae2.WindCount != 1)
{
return null;
}
break;
- case FillRule.Negative:
+ case ClipperFillRule.Negative:
if (ae2.WindCount != -1)
{
return null;
@@ -1681,7 +1779,7 @@ private OutPt IntersectEdges(Active ae1, Active ae2, Vector2 pt)
int oldE1WindCount, oldE2WindCount;
if (ae1.LocalMin.Polytype == ae2.LocalMin.Polytype)
{
- if (this.fillRule == FillRule.EvenOdd)
+ if (this.fillRule == ClipperFillRule.EvenOdd)
{
oldE1WindCount = ae1.WindCount;
ae1.WindCount = ae2.WindCount;
@@ -1710,7 +1808,7 @@ private OutPt IntersectEdges(Active ae1, Active ae2, Vector2 pt)
}
else
{
- if (this.fillRule != FillRule.EvenOdd)
+ if (this.fillRule != ClipperFillRule.EvenOdd)
{
ae1.WindCount2 += ae2.WindDx;
}
@@ -1719,7 +1817,7 @@ private OutPt IntersectEdges(Active ae1, Active ae2, Vector2 pt)
ae1.WindCount2 = ae1.WindCount2 == 0 ? 1 : 0;
}
- if (this.fillRule != FillRule.EvenOdd)
+ if (this.fillRule != ClipperFillRule.EvenOdd)
{
ae2.WindCount2 -= ae1.WindDx;
}
@@ -1731,11 +1829,11 @@ private OutPt IntersectEdges(Active ae1, Active ae2, Vector2 pt)
switch (this.fillRule)
{
- case FillRule.Positive:
+ case ClipperFillRule.Positive:
oldE1WindCount = ae1.WindCount;
oldE2WindCount = ae2.WindCount;
break;
- case FillRule.Negative:
+ case ClipperFillRule.Negative:
oldE1WindCount = -ae1.WindCount;
oldE2WindCount = -ae2.WindCount;
break;
@@ -1759,7 +1857,7 @@ private OutPt IntersectEdges(Active ae1, Active ae2, Vector2 pt)
if (IsHotEdge(ae1) && IsHotEdge(ae2))
{
if ((oldE1WindCount != 0 && oldE1WindCount != 1) || (oldE2WindCount != 0 && oldE2WindCount != 1) ||
- (ae1.LocalMin.Polytype != ae2.LocalMin.Polytype && this.clipType != ClippingOperation.Xor))
+ (ae1.LocalMin.Polytype != ae2.LocalMin.Polytype && this.clipType != BooleanOperation.Xor))
{
resultOp = this.AddLocalMaxPoly(ae1, ae2, pt);
}
@@ -1798,11 +1896,11 @@ private OutPt IntersectEdges(Active ae1, Active ae2, Vector2 pt)
float e1Wc2, e2Wc2;
switch (this.fillRule)
{
- case FillRule.Positive:
+ case ClipperFillRule.Positive:
e1Wc2 = ae1.WindCount2;
e2Wc2 = ae2.WindCount2;
break;
- case FillRule.Negative:
+ case ClipperFillRule.Negative:
e1Wc2 = -ae1.WindCount2;
e2Wc2 = -ae2.WindCount2;
break;
@@ -1821,7 +1919,7 @@ private OutPt IntersectEdges(Active ae1, Active ae2, Vector2 pt)
resultOp = null;
switch (this.clipType)
{
- case ClippingOperation.Union:
+ case BooleanOperation.Union:
if (e1Wc2 > 0 && e2Wc2 > 0)
{
return null;
@@ -1830,7 +1928,7 @@ private OutPt IntersectEdges(Active ae1, Active ae2, Vector2 pt)
resultOp = this.AddLocalMinPoly(ae1, ae2, pt);
break;
- case ClippingOperation.Difference:
+ case BooleanOperation.Difference:
if (((GetPolyType(ae1) == ClippingType.Clip) && (e1Wc2 > 0) && (e2Wc2 > 0))
|| ((GetPolyType(ae1) == ClippingType.Subject) && (e1Wc2 <= 0) && (e2Wc2 <= 0)))
{
@@ -1839,7 +1937,7 @@ private OutPt IntersectEdges(Active ae1, Active ae2, Vector2 pt)
break;
- case ClippingOperation.Xor:
+ case BooleanOperation.Xor:
resultOp = this.AddLocalMinPoly(ae1, ae2, pt);
break;
@@ -2218,21 +2316,21 @@ private bool IsContributingClosed(Active ae)
{
switch (this.fillRule)
{
- case FillRule.Positive:
+ case ClipperFillRule.Positive:
if (ae.WindCount != 1)
{
return false;
}
break;
- case FillRule.Negative:
+ case ClipperFillRule.Negative:
if (ae.WindCount != -1)
{
return false;
}
break;
- case FillRule.NonZero:
+ case ClipperFillRule.NonZero:
if (Math.Abs(ae.WindCount) != 1)
{
return false;
@@ -2243,32 +2341,32 @@ private bool IsContributingClosed(Active ae)
switch (this.clipType)
{
- case ClippingOperation.Intersection:
+ case BooleanOperation.Intersection:
return this.fillRule switch
{
- FillRule.Positive => ae.WindCount2 > 0,
- FillRule.Negative => ae.WindCount2 < 0,
+ ClipperFillRule.Positive => ae.WindCount2 > 0,
+ ClipperFillRule.Negative => ae.WindCount2 < 0,
_ => ae.WindCount2 != 0,
};
- case ClippingOperation.Union:
+ case BooleanOperation.Union:
return this.fillRule switch
{
- FillRule.Positive => ae.WindCount2 <= 0,
- FillRule.Negative => ae.WindCount2 >= 0,
+ ClipperFillRule.Positive => ae.WindCount2 <= 0,
+ ClipperFillRule.Negative => ae.WindCount2 >= 0,
_ => ae.WindCount2 == 0,
};
- case ClippingOperation.Difference:
+ case BooleanOperation.Difference:
bool result = this.fillRule switch
{
- FillRule.Positive => ae.WindCount2 <= 0,
- FillRule.Negative => ae.WindCount2 >= 0,
+ ClipperFillRule.Positive => ae.WindCount2 <= 0,
+ ClipperFillRule.Negative => ae.WindCount2 >= 0,
_ => ae.WindCount2 == 0,
};
return (GetPolyType(ae) == ClippingType.Subject) ? result : !result;
- case ClippingOperation.Xor:
+ case BooleanOperation.Xor:
return true; // XOr is always contributing unless open
default:
@@ -2282,11 +2380,11 @@ private bool IsContributingOpen(Active ae)
bool isInClip, isInSubj;
switch (this.fillRule)
{
- case FillRule.Positive:
+ case ClipperFillRule.Positive:
isInSubj = ae.WindCount > 0;
isInClip = ae.WindCount2 > 0;
break;
- case FillRule.Negative:
+ case ClipperFillRule.Negative:
isInSubj = ae.WindCount < 0;
isInClip = ae.WindCount2 < 0;
break;
@@ -2298,8 +2396,8 @@ private bool IsContributingOpen(Active ae)
bool result = this.clipType switch
{
- ClippingOperation.Intersection => isInClip,
- ClippingOperation.Union => !isInSubj && !isInClip,
+ BooleanOperation.Intersection => isInClip,
+ BooleanOperation.Union => !isInSubj && !isInClip,
_ => !isInClip
};
return result;
@@ -2326,7 +2424,7 @@ private void SetWindCountForClosedPathEdge(Active ae)
ae.WindCount = ae.WindDx;
ae2 = this.actives;
}
- else if (this.fillRule == FillRule.EvenOdd)
+ else if (this.fillRule == ClipperFillRule.EvenOdd)
{
ae.WindCount = ae.WindDx;
ae.WindCount2 = ae2.WindCount2;
@@ -2381,7 +2479,7 @@ private void SetWindCountForClosedPathEdge(Active ae)
}
// update windCount2 ...
- if (this.fillRule == FillRule.EvenOdd)
+ if (this.fillRule == ClipperFillRule.EvenOdd)
{
while (ae2 != ae)
{
@@ -2411,7 +2509,7 @@ private void SetWindCountForClosedPathEdge(Active ae)
private void SetWindCountForOpenPathEdge(Active ae)
{
Active ae2 = this.actives;
- if (this.fillRule == FillRule.EvenOdd)
+ if (this.fillRule == ClipperFillRule.EvenOdd)
{
int cnt1 = 0, cnt2 = 0;
while (ae2 != ae)
@@ -2458,7 +2556,7 @@ private static bool IsValidAelOrder(Active resident, Active newcomer)
}
// get the turning direction a1.top, a2.bot, a2.top
- float d = ClipperUtils.CrossProduct(resident.Top, newcomer.Bot, newcomer.Top);
+ float d = PolygonClipperUtilities.CrossProduct(resident.Top, newcomer.Bot, newcomer.Top);
if (d != 0)
{
return d < 0;
@@ -2470,12 +2568,12 @@ private static bool IsValidAelOrder(Active resident, Active newcomer)
// the direction they're about to turn
if (!IsMaxima(resident) && (resident.Top.Y > newcomer.Top.Y))
{
- return ClipperUtils.CrossProduct(newcomer.Bot, resident.Top, NextVertex(resident).Point) <= 0;
+ return PolygonClipperUtilities.CrossProduct(newcomer.Bot, resident.Top, NextVertex(resident).Point) <= 0;
}
if (!IsMaxima(newcomer) && (newcomer.Top.Y > resident.Top.Y))
{
- return ClipperUtils.CrossProduct(newcomer.Bot, newcomer.Top, NextVertex(newcomer).Point) >= 0;
+ return PolygonClipperUtilities.CrossProduct(newcomer.Bot, newcomer.Top, NextVertex(newcomer).Point) >= 0;
}
float y = newcomer.Bot.Y;
@@ -2492,13 +2590,13 @@ private static bool IsValidAelOrder(Active resident, Active newcomer)
return newcomerIsLeft;
}
- if (ClipperUtils.CrossProduct(PrevPrevVertex(resident).Point, resident.Bot, resident.Top) == 0)
+ if (PolygonClipperUtilities.CrossProduct(PrevPrevVertex(resident).Point, resident.Bot, resident.Top) == 0)
{
return true;
}
// compare turning direction of the alternate bound
- return (ClipperUtils.CrossProduct(PrevPrevVertex(resident).Point, newcomer.Bot, PrevPrevVertex(newcomer).Point) > 0) == newcomerIsLeft;
+ return (PolygonClipperUtilities.CrossProduct(PrevPrevVertex(resident).Point, newcomer.Bot, PrevPrevVertex(newcomer).Point) > 0) == newcomerIsLeft;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -2930,7 +3028,7 @@ private void CheckJoinLeft(Active e, Vector2 pt, bool checkCurrX = false)
}
// Avoid trivial joins
- if ((pt.Y < e.Top.Y + 2 || pt.Y < prev.Top.Y + 2)
+ if ((pt.Y < e.Top.Y + JoinYTolerance || pt.Y < prev.Top.Y + JoinYTolerance)
&& ((e.Bot.Y > pt.Y) || (prev.Bot.Y > pt.Y)))
{
return;
@@ -2938,7 +3036,7 @@ private void CheckJoinLeft(Active e, Vector2 pt, bool checkCurrX = false)
if (checkCurrX)
{
- if (ClipperUtils.PerpendicDistFromLineSqrd(pt, prev.Bot, prev.Top) > 0.25)
+ if (PolygonClipperUtilities.PerpendicDistFromLineSqrd(pt, prev.Bot, prev.Top) > JoinDistanceSqrdThreshold)
{
return;
}
@@ -2948,7 +3046,7 @@ private void CheckJoinLeft(Active e, Vector2 pt, bool checkCurrX = false)
return;
}
- if (ClipperUtils.CrossProduct(e.Top, pt, prev.Top) != 0)
+ if (PolygonClipperUtilities.CrossProduct(e.Top, pt, prev.Top) != 0)
{
return;
}
@@ -2985,7 +3083,7 @@ private void CheckJoinRight(Active e, Vector2 pt, bool checkCurrX = false)
}
// Avoid trivial joins
- if ((pt.Y < e.Top.Y + 2 || pt.Y < next.Top.Y + 2)
+ if ((pt.Y < e.Top.Y + JoinYTolerance || pt.Y < next.Top.Y + JoinYTolerance)
&& ((e.Bot.Y > pt.Y) || (next.Bot.Y > pt.Y)))
{
return;
@@ -2993,7 +3091,7 @@ private void CheckJoinRight(Active e, Vector2 pt, bool checkCurrX = false)
if (checkCurrX)
{
- if (ClipperUtils.PerpendicDistFromLineSqrd(pt, next.Bot, next.Top) > 0.25)
+ if (PolygonClipperUtilities.PerpendicDistFromLineSqrd(pt, next.Bot, next.Top) > JoinDistanceSqrdThreshold)
{
return;
}
@@ -3003,7 +3101,7 @@ private void CheckJoinRight(Active e, Vector2 pt, bool checkCurrX = false)
return;
}
- if (ClipperUtils.CrossProduct(e.Top, pt, next.Top) != 0)
+ if (PolygonClipperUtilities.CrossProduct(e.Top, pt, next.Top) != 0)
{
return;
}
@@ -3122,18 +3220,42 @@ private void Split(Active e, Vector2 currPt)
private static bool IsFront(Active ae)
=> ae == ae.Outrec.FrontEdge;
+ ///
+ /// Comparer for sorting local minima by Y coordinate (descending).
+ ///
private struct LocMinSorter : IComparer
{
public readonly int Compare(LocalMinima locMin1, LocalMinima locMin2)
=> locMin2.Vertex.Point.Y.CompareTo(locMin1.Vertex.Point.Y);
}
+ ///
+ /// Represents a local minimum in a polygon path.
+ /// A local minimum is a vertex where the path changes from descending to ascending.
+ ///
private readonly struct LocalMinima
{
+ ///
+ /// Gets the vertex at the local minimum.
+ ///
public readonly Vertex Vertex;
+
+ ///
+ /// Gets the polygon type (subject or clip).
+ ///
public readonly ClippingType Polytype;
+
+ ///
+ /// Gets a value indicating whether this is an open path (polyline).
+ ///
public readonly bool IsOpen;
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The vertex at the local minimum.
+ /// The polygon type.
+ /// Whether this is an open path.
public LocalMinima(Vertex vertex, ClippingType polytype, bool isOpen = false)
{
this.Vertex = vertex;
@@ -3142,7 +3264,7 @@ public LocalMinima(Vertex vertex, ClippingType polytype, bool isOpen = false)
}
public static bool operator ==(LocalMinima lm1, LocalMinima lm2)
-
+ // Use reference equality for vertex comparison
// TODO: Check this. Why ref equals.
=> ReferenceEquals(lm1.Vertex, lm2.Vertex);
@@ -3156,15 +3278,34 @@ public override int GetHashCode()
=> this.Vertex.GetHashCode();
}
- // IntersectNode: a structure representing 2 intersecting edges.
- // Intersections must be sorted so they are processed from the largest
- // Y coordinates to the smallest while keeping edges adjacent.
+ ///
+ /// Represents an intersection between two active edges.
+ /// Intersections must be sorted so they are processed from the largest
+ /// Y coordinates to the smallest while keeping edges adjacent.
+ ///
private readonly struct IntersectNode
{
+ ///
+ /// Gets the intersection point.
+ ///
public readonly Vector2 Point;
+
+ ///
+ /// Gets the first intersecting edge.
+ ///
public readonly Active Edge1;
+
+ ///
+ /// Gets the second intersecting edge.
+ ///
public readonly Active Edge2;
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The intersection point.
+ /// The first intersecting edge.
+ /// The second intersecting edge.
public IntersectNode(Vector2 pt, Active edge1, Active edge2)
{
this.Point = pt;
@@ -3173,6 +3314,9 @@ public IntersectNode(Vector2 pt, Active edge1, Active edge2)
}
}
+ ///
+ /// Comparer for sorting horizontal segments by left X coordinate.
+ ///
private struct HorzSegSorter : IComparer
{
public readonly int Compare(HorzSegment hs1, HorzSegment hs2)
@@ -3197,6 +3341,9 @@ public readonly int Compare(HorzSegment hs1, HorzSegment hs2)
}
}
+ ///
+ /// Comparer for sorting intersection nodes by Y coordinate (descending), then X coordinate.
+ ///
private struct IntersectListSort : IComparer
{
public readonly int Compare(IntersectNode a, IntersectNode b)
@@ -3215,8 +3362,16 @@ public readonly int Compare(IntersectNode a, IntersectNode b)
}
}
+ ///
+ /// Represents a horizontal segment in the output polygon.
+ /// Used to identify and join horizontal edges.
+ ///
private class HorzSegment
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The starting output point.
public HorzSegment(OutPt op)
{
this.LeftOp = op;
@@ -3224,29 +3379,60 @@ public HorzSegment(OutPt op)
this.LeftToRight = true;
}
+ ///
+ /// Gets or sets the left output point.
+ ///
public OutPt LeftOp { get; set; }
+ ///
+ /// Gets or sets the right output point.
+ ///
public OutPt RightOp { get; set; }
+ ///
+ /// Gets or sets a value indicating whether the segment is oriented left-to-right.
+ ///
public bool LeftToRight { get; set; }
}
+ ///
+ /// Represents a horizontal join operation between two output points.
+ ///
private class HorzJoin
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Left-to-right output point.
+ /// Right-to-left output point.
public HorzJoin(OutPt ltor, OutPt rtol)
{
this.Op1 = ltor;
this.Op2 = rtol;
}
+ ///
+ /// Gets the first output point in the join.
+ ///
public OutPt Op1 { get; }
+ ///
+ /// Gets the second output point in the join.
+ ///
public OutPt Op2 { get; }
}
- // OutPt: vertex data structure for clipping solutions
+ ///
+ /// Output point: represents a vertex in a clipping solution polygon.
+ /// Forms a circular doubly-linked list of vertices.
+ ///
private class OutPt
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The point coordinates.
+ /// The output record this point belongs to.
public OutPt(Vector2 pt, OutRec outrec)
{
this.Point = pt;
@@ -3256,43 +3442,101 @@ public OutPt(Vector2 pt, OutRec outrec)
this.HorizSegment = null;
}
+ ///
+ /// Gets the point coordinates.
+ ///
public Vector2 Point { get; }
+ ///
+ /// Gets or sets the next output point in the circular list.
+ ///
public OutPt Next { get; set; }
+ ///
+ /// Gets or sets the previous output point in the circular list.
+ ///
public OutPt Prev { get; set; }
+ ///
+ /// Gets or sets the output record this point belongs to.
+ ///
public OutRec OutRec { get; set; }
+ ///
+ /// Gets or sets the horizontal segment this point is part of (if any).
+ ///
public HorzSegment HorizSegment { get; set; }
}
- // OutRec: path data structure for clipping solutions
+ ///
+ /// Output record: represents a complete output polygon path.
+ /// Contains a circular doubly-linked list of output points.
+ ///
private class OutRec
{
+ ///
+ /// Gets or sets the index of this output record in the output list.
+ ///
public int Idx { get; set; }
+ ///
+ /// Gets or sets the parent output record (for holes).
+ ///
public OutRec Owner { get; set; }
+ ///
+ /// Gets or sets the front (ascending) edge of the output polygon.
+ ///
public Active FrontEdge { get; set; }
+ ///
+ /// Gets or sets the back (descending) edge of the output polygon.
+ ///
public Active BackEdge { get; set; }
+ ///
+ /// Gets or sets the starting point in the circular output point list.
+ ///
public OutPt Pts { get; set; }
+ ///
+ /// Gets or sets the polytree path (for hierarchical output).
+ ///
public PolyPathF PolyPath { get; set; }
+ ///
+ /// Gets or sets the bounding rectangle.
+ ///
public BoundsF Bounds { get; set; }
+ ///
+ /// Gets or sets the final output path.
+ ///
public PathF Path { get; set; } = [];
+ ///
+ /// Gets or sets a value indicating whether this is an open path.
+ ///
public bool IsOpen { get; set; }
+ ///
+ /// Gets or sets the list of split indices (for self-intersecting polygons).
+ ///
public List Splits { get; set; }
}
+ ///
+ /// Represents a vertex in an input polygon path.
+ /// Forms a circular doubly-linked list of vertices.
+ ///
private class Vertex
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The point coordinates.
+ /// Vertex flags (local min/max, open start/end).
+ /// The previous vertex in the list.
public Vertex(Vector2 pt, VertexFlags flags, Vertex prev)
{
this.Point = pt;
@@ -3301,78 +3545,172 @@ public Vertex(Vector2 pt, VertexFlags flags, Vertex prev)
this.Prev = prev;
}
+ ///
+ /// Gets the point coordinates.
+ ///
public Vector2 Point { get; }
+ ///
+ /// Gets or sets the next vertex in the circular list.
+ ///
public Vertex Next { get; set; }
+ ///
+ /// Gets or sets the previous vertex in the circular list.
+ ///
public Vertex Prev { get; set; }
+ ///
+ /// Gets or sets the vertex flags indicating properties like local minima/maxima.
+ ///
public VertexFlags Flags { get; set; }
}
+ ///
+ /// Active edge: represents an edge currently intersecting the scanline.
+ /// Stored in the Active Edge List (AEL) during scanline processing.
+ ///
private class Active
{
+ ///
+ /// Gets or sets the bottom point of the edge.
+ ///
public Vector2 Bot { get; set; }
+ ///
+ /// Gets or sets the top point of the edge.
+ ///
public Vector2 Top { get; set; }
- public float CurX { get; set; } // current (updated at every new scanline)
+ ///
+ /// Gets or sets the current X coordinate at the scanline (updated at every scanline).
+ ///
+ public float CurX { get; set; }
+ ///
+ /// Gets or sets the edge's reciprocal slope (dx/dy).
+ ///
public float Dx { get; set; }
- public int WindDx { get; set; } // 1 or -1 depending on winding direction
+ ///
+ /// Gets or sets the winding direction (1 for ascending, -1 for descending).
+ ///
+ public int WindDx { get; set; }
+ ///
+ /// Gets or sets the winding count for this edge's polygon type.
+ ///
public int WindCount { get; set; }
- public int WindCount2 { get; set; } // winding count of the opposite polytype
+ ///
+ /// Gets or sets the winding count for the opposite polygon type.
+ ///
+ public int WindCount2 { get; set; }
+ ///
+ /// Gets or sets the output record this edge contributes to.
+ ///
public OutRec Outrec { get; set; }
- // AEL: 'active edge list' (Vatti's AET - active edge table)
- // a linked list of all edges (from left to right) that are present
- // (or 'active') within the current scanbeam (a horizontal 'beam' that
- // sweeps from bottom to top over the paths in the clipping operation).
+ ///
+ /// Gets or sets the previous edge in the Active Edge List.
+ /// The AEL is a doubly-linked list of all edges intersecting the current scanbeam,
+ /// ordered from left to right.
+ ///
public Active PrevInAEL { get; set; }
+ ///
+ /// Gets or sets the next edge in the Active Edge List.
+ ///
public Active NextInAEL { get; set; }
- // SEL: 'sorted edge list' (Vatti's ST - sorted table)
- // linked list used when sorting edges into their new positions at the
- // top of scanbeams, but also (re)used to process horizontals.
+ ///
+ /// Gets or sets the previous edge in the Sorted Edge List.
+ /// The SEL is used when sorting edges into their new positions at scanbeam tops,
+ /// and is also reused to process horizontal edges.
+ ///
public Active PrevInSEL { get; set; }
+ ///
+ /// Gets or sets the next edge in the Sorted Edge List.
+ ///
public Active NextInSEL { get; set; }
+ ///
+ /// Gets or sets the jump pointer used during merge sort operations.
+ ///
public Active Jump { get; set; }
+ ///
+ /// Gets or sets the vertex at the top of this edge segment.
+ ///
public Vertex VertexTop { get; set; }
- public LocalMinima LocalMin { get; set; } // the bottom of an edge 'bound' (also Vatti)
+ ///
+ /// Gets or sets the local minimum this edge belongs to.
+ ///
+ public LocalMinima LocalMin { get; set; }
+ ///
+ /// Gets or sets a value indicating whether this is a left bound edge.
+ ///
public bool IsLeftBound { get; set; }
+ ///
+ /// Gets or sets the join status indicating if this edge is joined with an adjacent edge.
+ ///
public JoinWith JoinWith { get; set; }
}
}
+///
+/// Represents a node in a hierarchical polygon tree structure.
+/// Can contain child paths representing holes or nested polygons.
+///
internal class PolyPathF : IEnumerable
{
private readonly PolyPathF parent;
private readonly List items = [];
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The parent path, or null for the root.
public PolyPathF(PolyPathF parent = null)
=> this.parent = parent;
- public PathF Polygon { get; private set; } // polytree root's polygon == null
+ ///
+ /// Gets the polygon path. The polytree root's polygon is null.
+ ///
+ public PathF Polygon { get; private set; }
+ ///
+ /// Gets the nesting level in the tree (0 for root).
+ ///
public int Level => this.GetLevel();
+ ///
+ /// Gets a value indicating whether this path represents a hole.
+ ///
public bool IsHole => this.GetIsHole();
+ ///
+ /// Gets the number of child paths.
+ ///
public int Count => this.items.Count;
+ ///
+ /// Gets the child path at the specified index.
+ ///
+ /// The child index.
+ /// The child path.
public PolyPathF this[int index] => this.items[index];
+ ///
+ /// Adds a child path to this polytree node.
+ ///
+ /// The polygon path to add.
+ /// The created child node.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PolyPathF AddChild(PathF p)
{
@@ -3385,10 +3723,14 @@ public PolyPathF AddChild(PathF p)
return child;
}
+ ///
+ /// Calculates the total area of this polygon and all its children.
+ ///
+ /// The signed area.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public float Area()
{
- float result = this.Polygon == null ? 0 : ClipperUtils.Area(this.Polygon);
+ float result = this.Polygon == null ? 0 : PolygonClipperUtilities.SignedArea(this.Polygon);
for (int i = 0; i < this.items.Count; i++)
{
PolyPathF child = this.items[i];
@@ -3398,6 +3740,9 @@ public float Area()
return result;
}
+ ///
+ /// Removes all child paths.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Clear() => this.items.Clear();
@@ -3422,11 +3767,80 @@ private int GetLevel()
return result;
}
+ ///
+ /// Returns an enumerator that iterates through the child paths.
+ ///
+ /// An enumerator for the children.
public IEnumerator GetEnumerator() => this.items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => this.items.GetEnumerator();
}
+///
+/// Root of a polytree structure containing hierarchical polygon data.
+///
internal class PolyTreeF : PolyPathF
{
}
+
+///
+/// Collection of polygon paths.
+///
+internal class PathsF : List
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PathsF()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with items.
+ ///
+ /// Initial paths.
+ public PathsF(IEnumerable items)
+ : base(items)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with capacity.
+ ///
+ /// Initial capacity.
+ public PathsF(int capacity)
+ : base(capacity)
+ {
+ }
+}
+
+///
+/// Represents a polygon path as a list of points.
+///
+internal class PathF : List
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PathF()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with items.
+ ///
+ /// Initial points.
+ public PathF(IEnumerable items)
+ : base(items)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with capacity.
+ ///
+ /// Initial capacity.
+ public PathF(int capacity)
+ : base(capacity)
+ {
+ }
+}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperUtils.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperUtilities.cs
similarity index 53%
rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperUtils.cs
rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperUtilities.cs
index 39114d8b..dae10d2d 100644
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperUtils.cs
+++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperUtilities.cs
@@ -4,20 +4,18 @@
using System.Numerics;
using System.Runtime.CompilerServices;
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
+namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
-internal static class ClipperUtils
+internal static class PolygonClipperUtilities
{
- public const float DefaultArcTolerance = .25F;
- public const float FloatingPointTolerance = 1e-05F;
- public const float DefaultMinimumEdgeLength = .1F;
-
- // TODO: rename to Pow2?
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static float Sqr(float value) => value * value;
-
+ ///
+ /// Computes the signed area of a path using the shoelace formula.
+ ///
+ ///
+ /// Positive values indicate clockwise orientation in screen space.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static float Area(PathF path)
+ public static float SignedArea(PathF path)
{
// https://en.wikipedia.org/wiki/Shoelace_formula
float a = 0F;
@@ -26,7 +24,8 @@ public static float Area(PathF path)
return a;
}
- Vector2 prevPt = path[path.Count - 1];
+ // Sum over edges (prev -> current).
+ Vector2 prevPt = path[^1];
for (int i = 0; i < path.Count; i++)
{
Vector2 pt = path[i];
@@ -37,87 +36,34 @@ public static float Area(PathF path)
return a * .5F;
}
- public static PathF StripDuplicates(PathF path, bool isClosedPath)
- {
- int cnt = path.Count;
- PathF result = new(cnt);
- if (cnt == 0)
- {
- return result;
- }
-
- PointF lastPt = path[0];
- result.Add(lastPt);
- for (int i = 1; i < cnt; i++)
- {
- if (lastPt != path[i])
- {
- lastPt = path[i];
- result.Add(lastPt);
- }
- }
-
- if (isClosedPath && lastPt == result[0])
- {
- result.RemoveAt(result.Count - 1);
- }
-
- return result;
- }
-
- public static PathF Ellipse(Vector2 center, float radiusX, float radiusY = 0, int steps = 0)
- {
- if (radiusX <= 0)
- {
- return [];
- }
-
- if (radiusY <= 0)
- {
- radiusY = radiusX;
- }
-
- if (steps <= 2)
- {
- steps = (int)MathF.Ceiling(MathF.PI * MathF.Sqrt((radiusX + radiusY) * .5F));
- }
-
- float si = MathF.Sin(2 * MathF.PI / steps);
- float co = MathF.Cos(2 * MathF.PI / steps);
- float dx = co, dy = si;
- PathF result = new(steps) { new Vector2(center.X + radiusX, center.Y) };
- Vector2 radiusXY = new(radiusX, radiusY);
- for (int i = 1; i < steps; ++i)
- {
- result.Add(center + (radiusXY * new Vector2(dx, dy)));
- float x = (dx * co) - (dy * si);
- dy = (dy * co) + (dx * si);
- dx = x;
- }
-
- return result;
- }
-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float DotProduct(Vector2 vec1, Vector2 vec2)
=> Vector2.Dot(vec1, vec2);
+ ///
+ /// Returns the dot product of the segments (pt1->pt2) and (pt2->pt3).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float DotProduct(Vector2 pt1, Vector2 pt2, Vector2 pt3)
+ => Vector2.Dot(pt2 - pt1, pt3 - pt2);
+
+ ///
+ /// Returns the 2D cross product magnitude of and .
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float CrossProduct(Vector2 vec1, Vector2 vec2)
=> (vec1.Y * vec2.X) - (vec2.Y * vec1.X);
+ ///
+ /// Returns the cross product of the segments (pt1->pt2) and (pt2->pt3).
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float CrossProduct(Vector2 pt1, Vector2 pt2, Vector2 pt3)
=> ((pt2.X - pt1.X) * (pt3.Y - pt2.Y)) - ((pt2.Y - pt1.Y) * (pt3.X - pt2.X));
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static float DotProduct(Vector2 pt1, Vector2 pt2, Vector2 pt3)
- => Vector2.Dot(pt2 - pt1, pt3 - pt2);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static bool IsAlmostZero(float value)
- => MathF.Abs(value) <= FloatingPointTolerance;
-
+ ///
+ /// Returns the squared perpendicular distance from a point to a line segment.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float PerpendicDistFromLineSqrd(Vector2 pt, Vector2 line1, Vector2 line2)
{
@@ -128,9 +74,18 @@ public static float PerpendicDistFromLineSqrd(Vector2 pt, Vector2 line1, Vector2
return 0;
}
- return Sqr(CrossProduct(cd, ab)) / DotProduct(cd, cd);
+ float cross = CrossProduct(cd, ab);
+ return (cross * cross) / DotProduct(cd, cd);
}
+ ///
+ /// Returns true when two segments intersect.
+ ///
+ /// First endpoint of segment 1.
+ /// Second endpoint of segment 1.
+ /// First endpoint of segment 2.
+ /// Second endpoint of segment 2.
+ /// If true, allows shared endpoints; if false, requires a proper intersection.
public static bool SegsIntersect(Vector2 seg1a, Vector2 seg1b, Vector2 seg2a, Vector2 seg2b, bool inclusive = false)
{
if (inclusive)
@@ -158,26 +113,7 @@ public static bool SegsIntersect(Vector2 seg1a, Vector2 seg1b, Vector2 seg2a, Ve
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static bool GetIntersectPt(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, Vector2 ln2b, out Vector2 ip)
- {
- Vector2 dxy1 = ln1b - ln1a;
- Vector2 dxy2 = ln2b - ln2a;
- float cp = CrossProduct(dxy1, dxy2);
- if (cp == 0F)
- {
- ip = default;
- return false;
- }
-
- float qx = CrossProduct(ln1a, dxy1);
- float qy = CrossProduct(ln2a, dxy2);
-
- ip = ((dxy1 * qy) - (dxy2 * qx)) / cp;
- return ip != new Vector2(float.MaxValue);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static bool GetIntersectPoint(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, Vector2 ln2b, out Vector2 ip)
+ public static bool GetLineIntersectPoint(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, Vector2 ln2b, out Vector2 ip)
{
Vector2 dxy1 = ln1b - ln1a;
Vector2 dxy2 = ln2b - ln2a;
@@ -189,6 +125,8 @@ public static bool GetIntersectPoint(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, V
}
float t = (((ln1a.X - ln2a.X) * dxy2.Y) - ((ln1a.Y - ln2a.Y) * dxy2.X)) / det;
+
+ // Clamp intersection to the segment endpoints.
if (t <= 0F)
{
ip = ln1a;
@@ -205,6 +143,12 @@ public static bool GetIntersectPoint(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, V
return true;
}
+ ///
+ /// Returns the closest point on a segment to an external point.
+ ///
+ /// The point to project onto the segment.
+ /// First endpoint of the segment.
+ /// Second endpoint of the segment.
public static Vector2 GetClosestPtOnSegment(Vector2 offPt, Vector2 seg1, Vector2 seg2)
{
if (seg1 == seg2)
@@ -227,10 +171,4 @@ public static Vector2 GetClosestPtOnSegment(Vector2 offPt, Vector2 seg1, Vector2
return seg1 + (dxy * q);
}
-
- public static PathF ReversePath(PathF path)
- {
- path.Reverse();
- return path;
- }
}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs
similarity index 71%
rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs
rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs
index 4061d300..772c37ca 100644
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs
+++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs
@@ -2,10 +2,31 @@
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
-
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
+using SixLabors.ImageSharp.Drawing.Processing;
#pragma warning disable SA1201 // Elements should appear in the correct order
+namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
+
+///
+/// Generates polygonal stroke outlines for vector paths using analytic joins and caps.
+///
+///
+///
+/// This class performs geometric stroking of input paths, producing an explicit polygonal
+/// outline suitable for filling or clipping. It replicates the behavior of analytic stroking
+/// as implemented in vector renderers (e.g., AGG, Skia), without relying on rasterization.
+///
+///
+/// The stroker supports multiple join and cap styles, adjustable miter limits, and an
+/// approximation scale for arc and round joins. It operates entirely in double precision
+/// for numerical stability, emitting coordinates for downstream use
+/// in polygon merging or clipping operations.
+///
+///
+/// Used by higher-level utility to produce consistent,
+/// merged outlines for stroked paths and dashed spans.
+///
+///
internal sealed class PolygonStroker
{
private ArrayBuilder outVertices = new(1);
@@ -20,18 +41,56 @@ internal sealed class PolygonStroker
private double widthEps = 0.5 / 1024.0;
private int widthSign = 1;
- public double MiterLimit { get; set; } = 4;
-
- public double InnerMiterLimit { get; set; } = 1.01;
-
- public double ApproximationScale { get; set; } = 1.0;
-
- public LineJoin LineJoin { get; set; } = LineJoin.MiterJoin;
-
- public LineCap LineCap { get; set; } = LineCap.Butt;
-
- public InnerJoin InnerJoin { get; set; } = InnerJoin.InnerMiter;
+ ///
+ /// Initializes a new instance of the class with the specified stroke options.
+ ///
+ ///
+ /// The stroke options to use for configuring line joins, caps, miter limits, and approximation scale.
+ /// Cannot be .
+ ///
+ public PolygonStroker(StrokeOptions options)
+ {
+ this.LineJoin = options.LineJoin;
+ this.InnerJoin = options.InnerJoin;
+ this.LineCap = options.LineCap;
+ this.MiterLimit = options.MiterLimit;
+ this.InnerMiterLimit = options.InnerMiterLimit;
+ this.ApproximationScale = options.ApproximationScale;
+ }
+ ///
+ /// Gets the miter limit used to clamp outer miter joins.
+ ///
+ public double MiterLimit { get; }
+
+ ///
+ /// Gets the inner miter limit used to clamp joins on acute interior angles.
+ ///
+ public double InnerMiterLimit { get; }
+
+ ///
+ /// Gets the arc approximation scale used for round joins and caps.
+ ///
+ public double ApproximationScale { get; }
+
+ ///
+ /// Gets the outer line join style used for stroking corners.
+ ///
+ public LineJoin LineJoin { get; }
+
+ ///
+ /// Gets the line cap style used for open path ends.
+ ///
+ public LineCap LineCap { get; }
+
+ ///
+ /// Gets the join style used for sharp interior angles.
+ ///
+ public InnerJoin InnerJoin { get; }
+
+ ///
+ /// Gets or sets the stroke width in the caller's coordinate space.
+ ///
public double Width
{
get => this.strokeWidth * 2.0;
@@ -53,8 +112,33 @@ public double Width
}
}
- public PathF ProcessPath(ReadOnlySpan linePoints, bool isClosed)
+ ///
+ /// Strokes the provided polyline or polygon and returns the outline vertices.
+ ///
+ /// The input points to stroke.
+ /// Whether the input is a closed ring.
+ /// The stroked outline as a closed point array.
+ public PointF[] ProcessPath(ReadOnlySpan linePoints, bool isClosed)
{
+ if (linePoints.Length < 2)
+ {
+ return [];
+ }
+
+ // Special case: for 2-point inputs, check if both points are identical (degenerate case)
+ if (linePoints.Length == 2)
+ {
+ PointF p0 = linePoints[0];
+ PointF p1 = linePoints[1];
+
+ if (Math.Abs(p1.X - p0.X) <= Constants.Misc.VertexDistanceEpsilon &&
+ Math.Abs(p1.Y - p0.Y) <= Constants.Misc.VertexDistanceEpsilon)
+ {
+ // Both points are identical - generate a point cap shape
+ return this.GeneratePointCap(p0.X, p0.Y);
+ }
+ }
+
this.Reset();
this.AddLinePath(linePoints);
@@ -63,11 +147,15 @@ public PathF ProcessPath(ReadOnlySpan linePoints, bool isClosed)
this.ClosePath();
}
- PathF results = new(linePoints.Length * 3);
+ List results = new(linePoints.Length * 3);
this.FinishPath(results);
- return results;
+ return [.. results];
}
+ ///
+ /// Adds a sequence of line segments to the current stroker state.
+ ///
+ /// The input points to add as line segments.
public void AddLinePath(ReadOnlySpan linePoints)
{
for (int i = 0; i < linePoints.Length; i++)
@@ -77,11 +165,20 @@ public void AddLinePath(ReadOnlySpan linePoints)
}
}
+ ///
+ /// Marks the current path as closed before finishing the outline.
+ ///
public void ClosePath()
{
- this.AddVertex(0, 0, PathCommand.EndPoly | (PathCommand)PathFlags.Close);
+ // Mark the current src path as closed; no geometry is pushed here.
+ this.closed = (int)PathFlags.Close;
+ this.status = Status.Initial;
}
+ ///
+ /// Finalizes stroking and appends output points to the provided list.
+ ///
+ /// The list that receives the stroked outline vertices.
public void FinishPath(List results)
{
PointF currentPoint = new(0, 0);
@@ -108,6 +205,9 @@ public void FinishPath(List results)
}
}
+ ///
+ /// Resets the stroker state so it can be reused for a new path.
+ ///
public void Reset()
{
this.srcVertices.Clear();
@@ -327,6 +427,8 @@ private void CloseVertexPath(bool closed)
this.srcVertices.RemoveLast();
}
+ // Remove the tail pair (vd2 and its predecessor vd1) and re-add the tail 't'.
+ // Re-adding forces a fresh Measure() against the new predecessor, collapsing zero-length edges.
if (this.srcVertices.Length != 0)
{
this.srcVertices.RemoveLast();
@@ -340,6 +442,7 @@ private void CloseVertexPath(bool closed)
return;
}
+ // TODO: Why check again? Doesn't the while loop above already ensure this?
while (this.srcVertices.Length > 1)
{
ref VertexDistance vd1 = ref this.srcVertices[^1];
@@ -451,14 +554,14 @@ private void CalcMiter(
switch (lj)
{
- case LineJoin.MiterJoinRevert:
+ case LineJoin.MiterRevert:
this.AddPoint(v1.X + dx1, v1.Y - dy1);
this.AddPoint(v1.X + dx2, v1.Y - dy2);
break;
- case LineJoin.MiterJoinRound:
+ case LineJoin.MiterRound:
this.CalcArc(v1.X, v1.Y, dx1, -dy1, dx2, -dy2);
break;
@@ -489,6 +592,15 @@ private void CalcCap(ref VertexDistance v0, ref VertexDistance v1, double len)
{
this.outVertices.Clear();
+ if (len < Constants.Misc.VertexDistanceEpsilon)
+ {
+ // Degenerate cap: emit a symmetric butt cap of zero span.
+ // This avoids div-by-zero in direction computation.
+ this.AddPoint(v0.X, v0.Y);
+ this.AddPoint(v1.X, v1.Y);
+ return;
+ }
+
double dx1 = (v1.Y - v0.Y) / len;
double dy1 = (v1.X - v0.X) / len;
double dx2 = 0;
@@ -544,6 +656,26 @@ private void CalcCap(ref VertexDistance v0, ref VertexDistance v1, double len)
private void CalcJoin(ref VertexDistance v0, ref VertexDistance v1, ref VertexDistance v2, double len1, double len2)
{
+ const double eps = Constants.Misc.VertexDistanceEpsilon;
+ if (len1 < eps || len2 < eps)
+ {
+ // Degenerate join: reuse the non-degenerate edge length for both offsets
+ // to emit a simple bevel and avoid unstable direction math.
+ this.outVertices.Clear();
+
+ double l1 = len1 >= eps ? len1 : len2;
+ double l2 = len2 >= eps ? len2 : len1;
+
+ double offX1 = this.strokeWidth * (v1.Y - v0.Y) / l1;
+ double offY1 = this.strokeWidth * (v1.X - v0.X) / l1;
+ double offX2 = this.strokeWidth * (v2.Y - v1.Y) / l2;
+ double offY2 = this.strokeWidth * (v2.X - v1.X) / l2;
+
+ this.AddPoint(v1.X + offX1, v1.Y - offY1);
+ this.AddPoint(v1.X + offX2, v1.Y - offY2);
+ return;
+ }
+
double dx1 = this.strokeWidth * (v1.Y - v0.Y) / len1;
double dy1 = this.strokeWidth * (v1.X - v0.X) / len1;
double dx2 = this.strokeWidth * (v2.Y - v1.Y) / len2;
@@ -568,19 +700,19 @@ private void CalcJoin(ref VertexDistance v0, ref VertexDistance v1, ref VertexDi
break;
- case InnerJoin.InnerMiter:
- this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, LineJoin.MiterJoinRevert, limit, 0);
+ case InnerJoin.Miter:
+ this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, LineJoin.MiterRevert, limit, 0);
break;
- case InnerJoin.InnerJag:
- case InnerJoin.InnerRound:
+ case InnerJoin.Jag:
+ case InnerJoin.Round:
cp = ((dx1 - dx2) * (dx1 - dx2)) + ((dy1 - dy2) * (dy1 - dy2));
if (cp < len1 * len1 && cp < len2 * len2)
{
- this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, LineJoin.MiterJoinRevert, limit, 0);
+ this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, LineJoin.MiterRevert, limit, 0);
}
- else if (this.InnerJoin == InnerJoin.InnerJag)
+ else if (this.InnerJoin == InnerJoin.Jag)
{
this.AddPoint(v1.X + dx1, v1.Y - dy1);
this.AddPoint(v1.X, v1.Y);
@@ -604,7 +736,7 @@ private void CalcJoin(ref VertexDistance v0, ref VertexDistance v1, ref VertexDi
double dy = (dy1 + dy2) / 2;
double dbevel = Math.Sqrt((dx * dx) + (dy * dy));
- if (this.LineJoin is LineJoin.RoundJoin or LineJoin.BevelJoin && this.ApproximationScale * (this.widthAbs - dbevel) < this.widthEps)
+ if (this.LineJoin is LineJoin.Round or LineJoin.Bevel && this.ApproximationScale * (this.widthAbs - dbevel) < this.widthEps)
{
if (UtilityMethods.CalcIntersection(v0.X + dx1, v0.Y - dy1, v1.X + dx1, v1.Y - dy1, v1.X + dx2, v1.Y - dy2, v2.X + dx2, v2.Y - dy2, ref dx, ref dy))
{
@@ -620,14 +752,14 @@ private void CalcJoin(ref VertexDistance v0, ref VertexDistance v1, ref VertexDi
switch (this.LineJoin)
{
- case LineJoin.MiterJoin:
- case LineJoin.MiterJoinRevert:
- case LineJoin.MiterJoinRound:
+ case LineJoin.Miter:
+ case LineJoin.MiterRevert:
+ case LineJoin.MiterRound:
this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, this.LineJoin, this.MiterLimit, dbevel);
break;
- case LineJoin.RoundJoin:
+ case LineJoin.Round:
this.CalcArc(v1.X, v1.Y, dx1, -dy1, dx2, -dy2);
break;
@@ -644,6 +776,52 @@ private void CalcJoin(ref VertexDistance v0, ref VertexDistance v1, ref VertexDi
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void AddPoint(double x, double y) => this.outVertices.Add(new PointF((float)x, (float)y));
+ ///
+ /// Generates a cap shape for a degenerate point (when all input points are identical).
+ /// Creates a circle for round caps or a square for square/butt caps.
+ ///
+ /// The X coordinate of the point.
+ /// The Y coordinate of the point.
+ /// The vertices forming the cap shape.
+ private PointF[] GeneratePointCap(double x, double y)
+ {
+ if (this.LineCap == LineCap.Round)
+ {
+ // Generate a circle with radius = strokeWidth
+ double da = Math.Acos(this.widthAbs / (this.widthAbs + (0.125 / this.ApproximationScale))) * 2;
+ int n = Math.Max(4, (int)(Constants.Misc.PiMul2 / da));
+ double angleStep = Constants.Misc.PiMul2 / n;
+
+ PointF[] points = new PointF[n + 1];
+
+ for (int i = 0; i < n; i++)
+ {
+ double angle = i * angleStep;
+ points[i] = new PointF(
+ (float)(x + (Math.Cos(angle) * this.strokeWidth)),
+ (float)(y + (Math.Sin(angle) * this.strokeWidth)));
+ }
+
+ // Close the circle
+ points[n] = points[0];
+
+ return points;
+ }
+ else
+ {
+ // Generate a square cap (used for both Square and Butt caps)
+ double w = this.strokeWidth;
+ return
+ [
+ new PointF((float)(x - w), (float)(y - w)),
+ new PointF((float)(x + w), (float)(y - w)),
+ new PointF((float)(x + w), (float)(y + w)),
+ new PointF((float)(x - w), (float)(y + w)),
+ new PointF((float)(x - w), (float)(y - w)) // Close the square
+ ];
+ }
+ }
+
private enum Status
{
Initial,
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs
new file mode 100644
index 00000000..f04f9a04
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs
@@ -0,0 +1,226 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using SixLabors.ImageSharp.Drawing.Processing;
+using SixLabors.ImageSharp.Drawing.Utilities;
+
+namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
+
+///
+/// Generates stroked and merged shapes using polygon stroking and boolean clipping.
+///
+internal sealed class StrokedShapeGenerator
+{
+ private readonly PolygonStroker polygonStroker;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public StrokedShapeGenerator(StrokeOptions options)
+ => this.polygonStroker = new PolygonStroker(options);
+
+ ///
+ /// Strokes a collection of dashed polyline spans and returns a merged outline.
+ ///
+ ///
+ /// The input spans. Each array is treated as an open polyline
+ /// and is stroked using the current stroker settings.
+ /// Spans that are null or contain fewer than 2 points are ignored.
+ ///
+ /// The stroke width in the caller’s coordinate space.
+ ///
+ /// An array of closed paths representing the stroked outline after boolean merge.
+ /// Returns an empty array when no valid spans are provided. Returns a single path
+ /// when only one valid stroked ring is produced.
+ ///
+ ///
+ /// This method streams each dashed span through the internal stroker as an open polyline,
+ /// producing closed stroke rings. To clean self overlaps, all rings are added as subject
+ /// paths and a is performed.
+ /// The union uses to preserve winding density.
+ ///
+ public IPath[] GenerateStrokedShapes(List spans, float width)
+ {
+ // 1) Stroke each dashed span as open.
+ this.polygonStroker.Width = width;
+
+ List ringPoints = new(spans.Count);
+ List rings = new(spans.Count);
+ foreach (PointF[] span in spans)
+ {
+ if (span == null || span.Length < 2)
+ {
+ continue;
+ }
+
+ PointF[] stroked = this.polygonStroker.ProcessPath(span, isClosed: false);
+ if (stroked.Length < 3)
+ {
+ continue;
+ }
+
+ ringPoints.Add(stroked);
+ rings.Add(new Polygon(new LinearLineSegment(stroked)));
+ }
+
+ int count = rings.Count;
+ if (count == 0)
+ {
+ return [];
+ }
+
+ if (!HasIntersections(ringPoints))
+ {
+ return count == 1 ? [rings[0]] : [.. rings];
+ }
+
+ // 2) Union all rings as subject paths
+ ClippedShapeGenerator clipper = new(IntersectionRule.NonZero);
+ clipper.AddPaths(rings, ClippingType.Subject);
+ return clipper.GenerateClippedShapes(BooleanOperation.Union, true);
+ }
+
+ ///
+ /// Strokes a path and returns a merged outline from its flattened segments.
+ ///
+ /// The source path. It is flattened using the current flattening settings.
+ /// The stroke width in the caller’s coordinate space.
+ ///
+ /// An array of closed paths representing the stroked outline after boolean merge.
+ /// Returns an empty array when no valid rings are produced. Returns a single path
+ /// when only one valid stroked ring exists.
+ ///
+ ///
+ /// Each flattened simple path is streamed through the internal stroker as open or closed
+ /// according to . The resulting stroke rings are split
+ /// paths and combined using . Using
+ /// preserves fill across overlaps and prevents
+ /// unintended holes in the merged outline.
+ ///
+ public IPath[] GenerateStrokedShapes(IPath path, float width)
+ {
+ // 1) Stroke the input path into closed rings
+ List ringPoints = [];
+ List rings = [];
+ this.polygonStroker.Width = width;
+
+ foreach (ISimplePath p in path.Flatten())
+ {
+ PointF[] stroked = this.polygonStroker.ProcessPath(p.Points.Span, p.IsClosed);
+ if (stroked.Length < 3)
+ {
+ continue; // skip degenerate outputs
+ }
+
+ ringPoints.Add(stroked);
+ rings.Add(new Polygon(new LinearLineSegment(stroked)));
+ }
+
+ int count = rings.Count;
+ if (count == 0)
+ {
+ return [];
+ }
+
+ if (!HasIntersections(ringPoints))
+ {
+ return count == 1 ? [rings[0]] : [.. rings];
+ }
+
+ // 2) Union all rings as subject paths
+ ClippedShapeGenerator clipper = new(IntersectionRule.NonZero);
+ clipper.AddPaths(rings, ClippingType.Subject);
+
+ // 3) Return the cleaned, merged outline
+ return clipper.GenerateClippedShapes(BooleanOperation.Union, true);
+ }
+
+ ///
+ /// Determines whether any of the provided rings contain self-intersections or intersect with other rings.
+ ///
+ ///
+ /// This method performs a conservative scan to detect intersections among the provided rings. It
+ /// checks for both self-intersections within each ring and intersections between different rings. Rings are treated
+ /// as polylines; if a ring is closed (its first and last points are equal), the closing segment is included in the
+ /// intersection checks. This method is intended for fast intersection detection and may be used to determine
+ /// whether further geometric processing, such as clipping, is necessary.
+ ///
+ ///
+ /// A list of rings, where each ring is represented as an array of points defining its vertices. Each ring is
+ /// expected to be a sequence of points forming a polyline or polygon.
+ ///
+ /// if any ring self-intersects or any two rings intersect; otherwise, .
+ private static bool HasIntersections(List rings)
+ {
+ // Detect whether any stroked ring self-intersects or intersects another ring.
+ // This is a fast, conservative scan used to decide whether we can skip clipping.
+ Vector2 intersection = default;
+
+ for (int r = 0; r < rings.Count; r++)
+ {
+ PointF[] ring = rings[r];
+ int segmentCount = ring.Length - 1;
+ if (segmentCount < 2)
+ {
+ continue;
+ }
+
+ // 1) Self-intersection scan for the current ring.
+ // Adjacent segments share a vertex and are skipped to avoid trivial hits.
+ bool isClosed = ring[0] == ring[^1];
+ for (int i = 0; i < segmentCount; i++)
+ {
+ Vector2 a0 = new(ring[i].X, ring[i].Y);
+ Vector2 a1 = new(ring[i + 1].X, ring[i + 1].Y);
+
+ for (int j = i + 1; j < segmentCount; j++)
+ {
+ // Skip neighbors and the closing edge pair in a closed ring.
+ if (j == i + 1 || (isClosed && i == 0 && j == segmentCount - 1))
+ {
+ continue;
+ }
+
+ Vector2 b0 = new(ring[j].X, ring[j].Y);
+ Vector2 b1 = new(ring[j + 1].X, ring[j + 1].Y);
+ if (Intersect.LineSegmentToLineSegmentIgnoreCollinear(a0, a1, b0, b1, ref intersection))
+ {
+ return true;
+ }
+ }
+ }
+
+ // 2) Cross-ring intersection scan against later rings only.
+ // This avoids double work while checking all ring pairs.
+ for (int s = r + 1; s < rings.Count; s++)
+ {
+ PointF[] other = rings[s];
+ int otherSegmentCount = other.Length - 1;
+ if (otherSegmentCount < 1)
+ {
+ continue;
+ }
+
+ for (int i = 0; i < segmentCount; i++)
+ {
+ Vector2 a0 = new(ring[i].X, ring[i].Y);
+ Vector2 a1 = new(ring[i + 1].X, ring[i + 1].Y);
+
+ for (int j = 0; j < otherSegmentCount; j++)
+ {
+ Vector2 b0 = new(other[j].X, other[j].Y);
+ Vector2 b1 = new(other[j + 1].X, other[j + 1].Y);
+ if (Intersect.LineSegmentToLineSegmentIgnoreCollinear(a0, a1, b0, b1, ref intersection))
+ {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ // No intersections detected.
+ return false;
+ }
+}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexDistance.cs
similarity index 90%
rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs
rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexDistance.cs
index 89383756..8dd0e724 100644
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs
+++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexDistance.cs
@@ -3,8 +3,10 @@
using System.Runtime.CompilerServices;
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
+namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
+// TODO: We can improve the performance of some of the operations here by using unsafe casting to Vector128
+// Like we do in PolygonClipper.
internal struct VertexDistance
{
private const double Dd = 1.0 / Constants.Misc.VertexDistanceEpsilon;
@@ -91,5 +93,6 @@ public static bool CalcIntersection(double ax, double ay, double bx, double by,
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static double CrossProduct(double x1, double y1, double x2, double y2, double x, double y) => ((x - x2) * (y2 - y1)) - ((y - y2) * (x2 - x1));
+ public static double CrossProduct(double x1, double y1, double x2, double y2, double x, double y)
+ => ((x - x2) * (y2 - y1)) - ((y - y2) * (x2 - x1));
}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexFlags.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexFlags.cs
similarity index 76%
rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexFlags.cs
rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexFlags.cs
index 2a990ecf..fd038b4a 100644
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexFlags.cs
+++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexFlags.cs
@@ -1,7 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
+namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
[Flags]
internal enum VertexFlags
diff --git a/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs b/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs
index 8cc45fbe..2966e3f7 100644
--- a/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs
+++ b/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs
@@ -220,7 +220,7 @@ void IGlyphRenderer.EndLayer()
ShapeOptions options = new()
{
- ClippingOperation = ClippingOperation.Intersection,
+ BooleanOperation = BooleanOperation.Intersection,
IntersectionRule = TextUtilities.MapFillRule(this.currentLayerFillRule)
};
diff --git a/src/ImageSharp.Drawing/Utilities/Intersect.cs b/src/ImageSharp.Drawing/Utilities/Intersect.cs
index 624f0953..e20ed9eb 100644
--- a/src/ImageSharp.Drawing/Utilities/Intersect.cs
+++ b/src/ImageSharp.Drawing/Utilities/Intersect.cs
@@ -1,33 +1,71 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.Drawing.Utilities;
+///
+/// Lightweight 2D segment intersection helpers for polygon and path processing.
+///
+///
+/// This is intentionally small and allocation-free. It favors speed and numerical tolerance
+/// over exhaustive classification (e.g., collinear overlap detection), which keeps it fast
+/// enough for per-segment scanning in stroking or clipping preparation passes.
+///
internal static class Intersect
{
+ // Epsilon used for floating-point tolerance. We treat values within ±Eps as zero.
+ // This helps avoid instability when segments are nearly parallel or endpoints are
+ // very close to the intersection boundary.
private const float Eps = 1e-3f;
private const float MinusEps = -Eps;
private const float OnePlusEps = 1 + Eps;
+ ///
+ /// Tests two line segments for intersection, ignoring collinear overlap.
+ ///
+ /// Start of segment A.
+ /// End of segment A.
+ /// Start of segment B.
+ /// End of segment B.
+ ///
+ /// Receives the intersection point when the segments intersect within tolerance.
+ /// When no intersection is detected, the value is left unchanged.
+ ///
+ ///
+ /// if the segments intersect within their extents (including endpoints),
+ /// if they are disjoint or collinear.
+ ///
+ ///
+ /// The method is based on solving two parametric line equations and uses a small epsilon
+ /// window around [0, 1] to account for floating-point error. Collinear cases are rejected
+ /// early (crossD ≈ 0) to keep the method fast; callers that need collinear overlap detection
+ /// must implement that separately.
+ ///
public static bool LineSegmentToLineSegmentIgnoreCollinear(Vector2 a0, Vector2 a1, Vector2 b0, Vector2 b1, ref Vector2 intersectionPoint)
{
+ // Direction vectors of the segments.
float dax = a1.X - a0.X;
float day = a1.Y - a0.Y;
float dbx = b1.X - b0.X;
float dby = b1.Y - b0.Y;
+ // Cross product of directions. When near zero, the lines are parallel or collinear.
float crossD = (-dbx * day) + (dax * dby);
- if (crossD > MinusEps && crossD < Eps)
+ // Reject parallel/collinear lines. Collinear overlap is intentionally ignored.
+ if (crossD is > MinusEps and < Eps)
{
return false;
}
+ // Solve for parameters s and t where:
+ // a0 + t*(a1-a0) = b0 + s*(b1-b0)
float s = ((-day * (a0.X - b0.X)) + (dax * (a0.Y - b0.Y))) / crossD;
float t = ((dbx * (a0.Y - b0.Y)) - (dby * (a0.X - b0.X))) / crossD;
+ // If both parameters are within [0,1] (with tolerance), the segments intersect.
if (s > MinusEps && s < OnePlusEps && t > MinusEps && t < OnePlusEps)
{
intersectionPoint.X = a0.X + (t * dax);
diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs
index 375fb2d5..b2ba8752 100644
--- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs
+++ b/tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs
@@ -99,7 +99,10 @@ public void DrawLines_EndCapRound(TestImageProvider provider, st
where TPixel : unmanaged, IPixel
{
Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha);
- PatternPen pen = new(new PenOptions(color, thickness, [3f, 3f]) { EndCapStyle = EndCapStyle.Round });
+ PatternPen pen = new(new PenOptions(color, thickness, [3f, 3f])
+ {
+ StrokeOptions = new StrokeOptions { LineCap = LineCap.Round },
+ });
DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen);
}
@@ -110,7 +113,10 @@ public void DrawLines_EndCapButt(TestImageProvider provider, str
where TPixel : unmanaged, IPixel
{
Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha);
- PatternPen pen = new(new PenOptions(color, thickness, [3f, 3f]) { EndCapStyle = EndCapStyle.Butt });
+ PatternPen pen = new(new PenOptions(color, thickness, [3f, 3f])
+ {
+ StrokeOptions = new StrokeOptions { LineCap = LineCap.Butt },
+ });
DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen);
}
@@ -121,7 +127,10 @@ public void DrawLines_EndCapSquare(TestImageProvider provider, s
where TPixel : unmanaged, IPixel
{
Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha);
- PatternPen pen = new(new PenOptions(color, thickness, [3f, 3f]) { EndCapStyle = EndCapStyle.Square });
+ PatternPen pen = new(new PenOptions(color, thickness, [3f, 3f])
+ {
+ StrokeOptions = new StrokeOptions { LineCap = LineCap.Square },
+ });
DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen);
}
@@ -132,7 +141,10 @@ public void DrawLines_JointStyleRound(TestImageProvider provider
where TPixel : unmanaged, IPixel
{
Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha);
- SolidPen pen = new(new PenOptions(color, thickness) { JointStyle = JointStyle.Round });
+ SolidPen pen = new(new PenOptions(color, thickness)
+ {
+ StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Round },
+ });
DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen);
}
@@ -143,7 +155,10 @@ public void DrawLines_JointStyleSquare(TestImageProvider provide
where TPixel : unmanaged, IPixel
{
Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha);
- SolidPen pen = new(new PenOptions(color, thickness) { JointStyle = JointStyle.Square });
+ SolidPen pen = new(new PenOptions(color, thickness)
+ {
+ StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Bevel },
+ });
DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen);
}
@@ -154,7 +169,10 @@ public void DrawLines_JointStyleMiter(TestImageProvider provider
where TPixel : unmanaged, IPixel
{
Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha);
- SolidPen pen = new(new PenOptions(color, thickness) { JointStyle = JointStyle.Miter });
+ SolidPen pen = new(new PenOptions(color, thickness)
+ {
+ StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Miter },
+ });
DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen);
}
diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs
index bc4963cd..b86078fc 100644
--- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs
+++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs
@@ -27,9 +27,9 @@ public void FillPolygon_Solid_Basic(TestImageProvider provider,
c => c.SetGraphicsOptions(options)
.FillPolygon(Color.White, polygon1)
.FillPolygon(Color.White, polygon2),
+ testOutputDetails: $"aa{antialias}",
appendPixelTypeToFileName: false,
- appendSourceFileOrDescription: false,
- testOutputDetails: $"aa{antialias}");
+ appendSourceFileOrDescription: false);
}
[Theory]
@@ -177,8 +177,8 @@ public void FillPolygon_StarCircle(TestImageProvider provider)
provider.RunValidatingProcessorTest(
c => c.Fill(Color.White, shape),
comparer: ImageComparer.TolerantPercentage(0.01f),
- appendSourceFileOrDescription: false,
- appendPixelTypeToFileName: false);
+ appendPixelTypeToFileName: false,
+ appendSourceFileOrDescription: false);
}
[Theory]
@@ -189,17 +189,17 @@ public void FillPolygon_StarCircle_AllOperations(TestImageProvider provi
Star star = new(64, 64, 5, 24, 64);
// See http://www.angusj.com/clipper2/Docs/Units/Clipper/Types/ClipType.htm for reference.
- foreach (ClippingOperation operation in (ClippingOperation[])Enum.GetValues(typeof(ClippingOperation)))
+ foreach (BooleanOperation operation in (BooleanOperation[])Enum.GetValues(typeof(BooleanOperation)))
{
- ShapeOptions options = new() { ClippingOperation = operation };
+ ShapeOptions options = new() { BooleanOperation = operation };
IPath shape = star.Clip(options, circle);
provider.RunValidatingProcessorTest(
c => c.Fill(Color.DeepPink, circle).Fill(Color.LightGray, star).Fill(Color.ForestGreen, shape),
- comparer: ImageComparer.TolerantPercentage(0.01F),
testOutputDetails: operation.ToString(),
- appendSourceFileOrDescription: false,
- appendPixelTypeToFileName: false);
+ comparer: ImageComparer.TolerantPercentage(0.01F),
+ appendPixelTypeToFileName: false,
+ appendSourceFileOrDescription: false);
}
}
@@ -300,8 +300,8 @@ public void Fill_RegularPolygon(TestImageProvider provider, int
provider.RunValidatingProcessorTest(
c => c.Fill(color, polygon),
testOutput,
- appendSourceFileOrDescription: false,
- appendPixelTypeToFileName: false);
+ appendPixelTypeToFileName: false,
+ appendSourceFileOrDescription: false);
}
public static readonly TheoryData Fill_EllipsePolygon_Data =
@@ -336,8 +336,8 @@ public void Fill_EllipsePolygon(TestImageProvider provider, bool
c.Fill(color, polygon);
},
testOutputDetails: $"Reverse({reverse})_IntersectionRule({intersectionRule})",
- appendSourceFileOrDescription: false,
- appendPixelTypeToFileName: false);
+ appendPixelTypeToFileName: false,
+ appendSourceFileOrDescription: false);
}
[Theory]
diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawBezier.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawBezier.cs
index ef268520..91d56671 100644
--- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawBezier.cs
+++ b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawBezier.cs
@@ -121,8 +121,8 @@ public void JointAndEndCapStyle()
Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions);
this.VerifyPoints(this.points, processor.Path);
SolidPen processorPen = Assert.IsType(processor.Pen);
- Assert.Equal(this.pen.JointStyle, processorPen.JointStyle);
- Assert.Equal(this.pen.EndCapStyle, processorPen.EndCapStyle);
+ Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin);
+ Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap);
}
[Fact]
@@ -135,7 +135,7 @@ public void JointAndEndCapStyleDefaultOptions()
Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions);
this.VerifyPoints(this.points, processor.Path);
SolidPen processorPen = Assert.IsType(processor.Pen);
- Assert.Equal(this.pen.JointStyle, processorPen.JointStyle);
- Assert.Equal(this.pen.EndCapStyle, processorPen.EndCapStyle);
+ Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin);
+ Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap);
}
}
diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawLine.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawLine.cs
index 6cdb5c25..5ab5ae86 100644
--- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawLine.cs
+++ b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawLine.cs
@@ -117,8 +117,8 @@ public void JointAndEndCapStyle()
Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions);
this.VerifyPoints(this.points, processor.Path);
SolidPen processorPen = Assert.IsType(processor.Pen);
- Assert.Equal(this.pen.JointStyle, processorPen.JointStyle);
- Assert.Equal(this.pen.EndCapStyle, processorPen.EndCapStyle);
+ Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin);
+ Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap);
}
[Fact]
@@ -131,7 +131,7 @@ public void JointAndEndCapStyleDefaultOptions()
Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions);
this.VerifyPoints(this.points, processor.Path);
SolidPen processorPen = Assert.IsType(processor.Pen);
- Assert.Equal(this.pen.JointStyle, processorPen.JointStyle);
- Assert.Equal(this.pen.EndCapStyle, processorPen.EndCapStyle);
+ Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin);
+ Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap);
}
}
diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPath.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPath.cs
index d57bf36d..8c283ed2 100644
--- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPath.cs
+++ b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPath.cs
@@ -104,8 +104,8 @@ public void JointAndEndCapStyle()
Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions);
Assert.Equal(this.path, processor.Path);
SolidPen processorPen = Assert.IsType(processor.Pen);
- Assert.Equal(this.pen.JointStyle, processorPen.JointStyle);
- Assert.Equal(this.pen.EndCapStyle, processorPen.EndCapStyle);
+ Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin);
+ Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap);
}
[Fact]
@@ -118,7 +118,7 @@ public void JointAndEndCapStyleDefaultOptions()
Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions);
Assert.Equal(this.path, processor.Path);
SolidPen processorPen = Assert.IsType(processor.Pen);
- Assert.Equal(this.pen.JointStyle, processorPen.JointStyle);
- Assert.Equal(this.pen.EndCapStyle, processorPen.EndCapStyle);
+ Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin);
+ Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap);
}
}
diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPathCollection.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPathCollection.cs
index df0bbf1f..cb104bbb 100644
--- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPathCollection.cs
+++ b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPathCollection.cs
@@ -10,8 +10,6 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths;
public class DrawPathCollection : BaseImageOperationsExtensionTest
{
- private readonly GraphicsOptions nonDefault = new() { Antialias = false };
- private readonly Color color = Color.HotPink;
private readonly SolidPen pen = Pens.Solid(Color.HotPink, 1);
private readonly IPath path1 = new Path(new LinearLineSegment(
[
@@ -162,8 +160,8 @@ public void JointAndEndCapStyle()
{
Assert.NotEqual(this.shapeOptions, p.Options.ShapeOptions);
SolidPen pPen = Assert.IsType(p.Pen);
- Assert.Equal(this.pen.JointStyle, pPen.JointStyle);
- Assert.Equal(this.pen.EndCapStyle, pPen.EndCapStyle);
+ Assert.Equal(this.pen.StrokeOptions.LineJoin, pPen.StrokeOptions.LineJoin);
+ Assert.Equal(this.pen.StrokeOptions.LineCap, pPen.StrokeOptions.LineCap);
});
Assert.Collection(
@@ -182,8 +180,8 @@ public void JointAndEndCapStyleDefaultOptions()
{
Assert.Equal(this.shapeOptions, p.Options.ShapeOptions);
SolidPen pPen = Assert.IsType(p.Pen);
- Assert.Equal(this.pen.JointStyle, pPen.JointStyle);
- Assert.Equal(this.pen.EndCapStyle, pPen.EndCapStyle);
+ Assert.Equal(this.pen.StrokeOptions.LineJoin, pPen.StrokeOptions.LineJoin);
+ Assert.Equal(this.pen.StrokeOptions.LineCap, pPen.StrokeOptions.LineCap);
});
Assert.Collection(
diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPolygon.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPolygon.cs
index 0b6900cc..fbc3cbee 100644
--- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPolygon.cs
+++ b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPolygon.cs
@@ -19,7 +19,7 @@ public class DrawPolygon : BaseImageOperationsExtensionTest
new PointF(25, 10)
];
- private void VerifyPoints(PointF[] expectedPoints, IPath path)
+ private static void VerifyPoints(PointF[] expectedPoints, IPath path)
{
ISimplePath simplePath = Assert.Single(path.Flatten());
Assert.True(simplePath.IsClosed);
@@ -34,7 +34,7 @@ public void Pen()
DrawPathProcessor processor = this.Verify();
Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions);
- this.VerifyPoints(this.points, processor.Path);
+ VerifyPoints(this.points, processor.Path);
Assert.Equal(this.pen, processor.Pen);
}
@@ -46,7 +46,7 @@ public void PenDefaultOptions()
DrawPathProcessor processor = this.Verify();
Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions);
- this.VerifyPoints(this.points, processor.Path);
+ VerifyPoints(this.points, processor.Path);
Assert.Equal(this.pen, processor.Pen);
}
@@ -58,7 +58,7 @@ public void BrushAndThickness()
DrawPathProcessor processor = this.Verify();
Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions);
- this.VerifyPoints(this.points, processor.Path);
+ VerifyPoints(this.points, processor.Path);
SolidPen processorPen = Assert.IsType(processor.Pen);
Assert.Equal(this.pen.StrokeFill, processorPen.StrokeFill);
Assert.Equal(10, processorPen.StrokeWidth);
@@ -72,7 +72,7 @@ public void BrushAndThicknessDefaultOptions()
DrawPathProcessor processor = this.Verify();
Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions);
- this.VerifyPoints(this.points, processor.Path);
+ VerifyPoints(this.points, processor.Path);
SolidPen processorPen = Assert.IsType(processor.Pen);
Assert.Equal(this.pen.StrokeFill, processorPen.StrokeFill);
Assert.Equal(10, processorPen.StrokeWidth);
@@ -86,7 +86,7 @@ public void ColorAndThickness()
DrawPathProcessor processor = this.Verify();
Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions);
- this.VerifyPoints(this.points, processor.Path);
+ VerifyPoints(this.points, processor.Path);
SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill);
SolidPen processorPen = Assert.IsType(processor.Pen);
Assert.Equal(Color.Red, brush.Color);
@@ -101,7 +101,7 @@ public void ColorAndThicknessDefaultOptions()
DrawPathProcessor processor = this.Verify();
Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions);
- this.VerifyPoints(this.points, processor.Path);
+ VerifyPoints(this.points, processor.Path);
SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill);
Assert.Equal(Color.Red, brush.Color);
SolidPen processorPen = Assert.IsType(processor.Pen);
@@ -116,10 +116,10 @@ public void JointAndEndCapStyle()
DrawPathProcessor processor = this.Verify();
Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions);
- this.VerifyPoints(this.points, processor.Path);
+ VerifyPoints(this.points, processor.Path);
SolidPen processorPen = Assert.IsType(processor.Pen);
- Assert.Equal(this.pen.JointStyle, processorPen.JointStyle);
- Assert.Equal(this.pen.EndCapStyle, processorPen.EndCapStyle);
+ Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin);
+ Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap);
}
[Fact]
@@ -130,9 +130,9 @@ public void JointAndEndCapStyleDefaultOptions()
DrawPathProcessor processor = this.Verify();
Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions);
- this.VerifyPoints(this.points, processor.Path);
+ VerifyPoints(this.points, processor.Path);
SolidPen processorPen = Assert.IsType(processor.Pen);
- Assert.Equal(this.pen.JointStyle, processorPen.JointStyle);
- Assert.Equal(this.pen.EndCapStyle, processorPen.EndCapStyle);
+ Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin);
+ Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap);
}
}
diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawRectangle.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawRectangle.cs
index b40b41c1..5e5ed330 100644
--- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawRectangle.cs
+++ b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawRectangle.cs
@@ -112,7 +112,7 @@ public void JointAndEndCapStyle()
Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Path));
Assert.NotEqual(this.pen, processor.Pen);
SolidPen processorPen = Assert.IsType(processor.Pen);
- Assert.Equal(this.pen.JointStyle, processorPen.JointStyle);
- Assert.Equal(this.pen.EndCapStyle, processorPen.EndCapStyle);
+ Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin);
+ Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap);
}
}
diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_323.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_323.cs
index 631058a5..c77648fe 100644
--- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_323.cs
+++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_323.cs
@@ -22,19 +22,19 @@ public void DrawPolygonMustDrawoutlineOnly(TestImageProvider pro
x => x.DrawPolygon(
color,
scale,
- new PointF[] {
+ [
new(5, 5),
new(5, 150),
new(190, 150),
- }),
+ ]),
new { scale });
}
[Theory]
- [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 3f)]
- [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 1f)]
- [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 0.3f)]
- [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 0.7f)]
+ //[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 3f)]
+ //[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 1f)]
+ //[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 0.3f)]
+ //[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 0.7f)]
[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 0.003f)]
public void DrawPolygonMustDrawoutlineOnly_Pattern(TestImageProvider provider, float scale)
where TPixel : unmanaged, IPixel
@@ -44,11 +44,11 @@ public void DrawPolygonMustDrawoutlineOnly_Pattern(TestImageProvider x.DrawPolygon(
pen,
- new PointF[] {
- new(5, 5),
- new(5, 150),
- new(190, 150),
- }),
+ [
+ new(5, 5),
+ new(5, 150),
+ new(190, 150),
+ ]),
new { scale });
}
}
diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs
index 28a20662..766efb0e 100644
--- a/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs
+++ b/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs
@@ -27,7 +27,7 @@ public void UpdateDefaultOptionsOnProcessingContext_AlwaysNewInstance()
{
ShapeOptions option = new()
{
- ClippingOperation = ClippingOperation.Intersection,
+ BooleanOperation = BooleanOperation.Intersection,
IntersectionRule = IntersectionRule.NonZero
};
Configuration config = new();
@@ -36,18 +36,18 @@ public void UpdateDefaultOptionsOnProcessingContext_AlwaysNewInstance()
context.SetShapeOptions(o =>
{
- Assert.Equal(ClippingOperation.Intersection, o.ClippingOperation); // has original values
+ Assert.Equal(BooleanOperation.Intersection, o.BooleanOperation); // has original values
Assert.Equal(IntersectionRule.NonZero, o.IntersectionRule);
- o.ClippingOperation = ClippingOperation.Xor;
+ o.BooleanOperation = BooleanOperation.Xor;
o.IntersectionRule = IntersectionRule.EvenOdd;
});
ShapeOptions returnedOption = context.GetShapeOptions();
- Assert.Equal(ClippingOperation.Xor, returnedOption.ClippingOperation);
+ Assert.Equal(BooleanOperation.Xor, returnedOption.BooleanOperation);
Assert.Equal(IntersectionRule.EvenOdd, returnedOption.IntersectionRule);
- Assert.Equal(ClippingOperation.Intersection, option.ClippingOperation); // hasn't been mutated
+ Assert.Equal(BooleanOperation.Intersection, option.BooleanOperation); // hasn't been mutated
Assert.Equal(IntersectionRule.NonZero, option.IntersectionRule);
}
@@ -67,7 +67,7 @@ public void UpdateDefaultOptionsOnConfiguration_AlwaysNewInstance()
{
ShapeOptions option = new()
{
- ClippingOperation = ClippingOperation.Intersection,
+ BooleanOperation = BooleanOperation.Intersection,
IntersectionRule = IntersectionRule.NonZero
};
Configuration config = new();
@@ -75,16 +75,16 @@ public void UpdateDefaultOptionsOnConfiguration_AlwaysNewInstance()
config.SetShapeOptions(o =>
{
- Assert.Equal(ClippingOperation.Intersection, o.ClippingOperation); // has original values
+ Assert.Equal(BooleanOperation.Intersection, o.BooleanOperation); // has original values
Assert.Equal(IntersectionRule.NonZero, o.IntersectionRule);
- o.ClippingOperation = ClippingOperation.Xor;
+ o.BooleanOperation = BooleanOperation.Xor;
o.IntersectionRule = IntersectionRule.EvenOdd;
});
ShapeOptions returnedOption = config.GetShapeOptions();
- Assert.Equal(ClippingOperation.Xor, returnedOption.ClippingOperation);
+ Assert.Equal(BooleanOperation.Xor, returnedOption.BooleanOperation);
Assert.Equal(IntersectionRule.EvenOdd, returnedOption.IntersectionRule);
- Assert.Equal(ClippingOperation.Intersection, option.ClippingOperation); // hasn't been mutated
+ Assert.Equal(BooleanOperation.Intersection, option.BooleanOperation); // hasn't been mutated
Assert.Equal(IntersectionRule.NonZero, option.IntersectionRule);
}
diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs
index 5d85c26a..c8a84d34 100644
--- a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs
+++ b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs
@@ -2,7 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Numerics;
-using SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
+using SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
using SixLabors.ImageSharp.Drawing.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Drawing.Tests.PolygonClipper;
@@ -27,18 +27,15 @@ public class ClipperTests
private IEnumerable Clip(IPath shape, params IPath[] hole)
{
- Clipper clipper = new();
+ ClippedShapeGenerator clipper = new(IntersectionRule.EvenOdd);
clipper.AddPath(shape, ClippingType.Subject);
if (hole != null)
{
- foreach (IPath s in hole)
- {
- clipper.AddPath(s, ClippingType.Clip);
- }
+ clipper.AddPaths(hole, ClippingType.Clip);
}
- return clipper.GenerateClippedShapes(ClippingOperation.Difference, IntersectionRule.EvenOdd);
+ return clipper.GenerateClippedShapes(BooleanOperation.Difference);
}
[Fact]
diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/RectangularPolygonValueComparer.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/RectangularPolygonValueComparer.cs
index 65583e1d..91f64607 100644
--- a/tests/ImageSharp.Drawing.Tests/TestUtilities/RectangularPolygonValueComparer.cs
+++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/RectangularPolygonValueComparer.cs
@@ -1,8 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
-
namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities;
///
@@ -10,7 +8,7 @@ namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities;
///
internal static class RectangularPolygonValueComparer
{
- public const float DefaultTolerance = ClipperUtils.FloatingPointTolerance;
+ public const float DefaultTolerance = 1e-05F;
public static bool Equals(RectangularPolygon x, RectangularPolygon y, float epsilon = DefaultTolerance)
=> Math.Abs(x.Left - y.Left) < epsilon