diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln
index 3f753f6c..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 18
-VisualStudioVersion = 18.0.11123.170 d18.0
+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
@@ -359,14 +359,6 @@ Global
{5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.Build.0 = Release|Any CPU
- {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Release|Any CPU.Build.0 = Release|Any CPU
- {5490DFAF-0891-535F-08B4-2BF03C2BB778}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5490DFAF-0891-535F-08B4-2BF03C2BB778}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5490DFAF-0891-535F-08B4-2BF03C2BB778}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5490DFAF-0891-535F-08B4-2BF03C2BB778}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
index da689136..c5ec1798 100644
--- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
+++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
@@ -46,6 +46,7 @@
+
\ No newline at end of file
diff --git a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs
index 11c188d8..6f079e43 100644
--- a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs
+++ b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.PolygonClipper;
+
namespace SixLabors.ImageSharp.Drawing.Processing;
///
@@ -24,9 +26,9 @@ private ShapeOptions(ShapeOptions source)
///
/// Gets or sets the clipping operation.
///
- /// Defaults to .
+ /// Defaults to .
///
- public ClippingOperation ClippingOperation { get; set; } = ClippingOperation.Difference;
+ public BooleanOperation ClippingOperation { get; set; } = BooleanOperation.Difference;
///
/// Gets or sets the rule for calculating intersection points.
diff --git a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs b/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs
index 690d2291..498126da 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.ClippingOperation);
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
index 50607e20..f5d8d0f5 100644
--- a/src/ImageSharp.Drawing/Shapes/EndCapStyle.cs
+++ b/src/ImageSharp.Drawing/Shapes/EndCapStyle.cs
@@ -34,9 +34,26 @@ public enum EndCapStyle
Joined = 4
}
+///
+/// Specifies the shape to be used at the ends of open lines or paths when stroking.
+///
internal 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/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/JointStyle.cs b/src/ImageSharp.Drawing/Shapes/JointStyle.cs
index c1464824..d3d4d58e 100644
--- a/src/ImageSharp.Drawing/Shapes/JointStyle.cs
+++ b/src/ImageSharp.Drawing/Shapes/JointStyle.cs
@@ -24,19 +24,72 @@ public enum JointStyle
Miter = 2
}
+///
+/// Specifies how the connection between two consecutive line segments (a join)
+/// is rendered when stroking paths or polygons.
+///
internal 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.
+ ///
MiterJoin = 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.
+ ///
MiterJoinRevert = 1,
+
+ ///
+ /// Joins lines by connecting them with a circular arc centered at the join point,
+ /// producing a smooth, rounded corner.
+ ///
RoundJoin = 2,
+
+ ///
+ /// Joins lines by connecting the outer corners directly with a straight line,
+ /// forming a flat edge at the join point.
+ ///
BevelJoin = 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.
+ ///
MiterJoinRound = 4
}
+///
+/// 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.
+///
internal enum InnerJoin
{
+ ///
+ /// Joins inner corners by connecting the edges with a straight line,
+ /// producing a flat, beveled appearance.
+ ///
InnerBevel,
+
+ ///
+ /// Joins inner corners by extending the inner edges until they meet at a sharp point.
+ /// This can create long, narrow joins for acute angles.
+ ///
InnerMiter,
+
+ ///
+ /// Joins inner corners with a notched appearance,
+ /// forming a small cut or indentation at the join.
+ ///
InnerJag,
+
+ ///
+ /// Joins inner corners using a circular arc between the edges,
+ /// creating a smooth, rounded interior transition.
+ ///
InnerRound
}
diff --git a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs
index 29213304..7533df08 100644
--- a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs
+++ b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs
@@ -2,8 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Numerics;
-using System.Runtime.InteropServices;
-using SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
+using SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
namespace SixLabors.ImageSharp.Drawing;
@@ -16,38 +15,12 @@ public static class OutlinePathExtensions
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);
@@ -59,7 +32,6 @@ public static IPath GenerateOutline(this IPath path, float width)
/// The style to apply to the joints.
/// The style to apply to the end caps.
/// 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)
{
if (width <= 0)
@@ -67,14 +39,8 @@ public static IPath GenerateOutline(this IPath path, float width, JointStyle joi
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(MiterOffsetDelta);
+ return new ComplexPolygon(generator.GenerateStrokedShapes(path, width));
}
///
@@ -84,7 +50,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);
@@ -96,7 +61,6 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpanThe pattern made of multiples of the width.
/// 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, bool startOff)
=> GenerateOutline(path, width, pattern, startOff, DefaultJointStyle, DefaultEndCapStyle);
@@ -109,7 +73,6 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpanThe style to apply to the joints.
/// The style to apply to the end caps.
/// 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);
@@ -123,7 +86,6 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpanThe style to apply to the joints.
/// The style to apply to the end caps.
/// 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)
{
if (width <= 0)
@@ -136,88 +98,110 @@ 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;
+
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);
+
+ if (segLen <= eps)
{
- // find a point between the 2
- float t = targetLength / distToNext;
+ current = next;
+ i++;
+ continue;
+ }
+
+ if (segLen + eps < targetLength)
+ {
+ buffer.Add(current);
+ current = next;
+ i++;
+ targetLength -= segLen;
+ continue;
+ }
- Vector2 point = (currentPoint * (1 - t)) + (targetPoint * t);
- buffer.Add(currentPoint);
- buffer.Add(point);
+ 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
+ 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(MiterOffsetDelta);
+ 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/BoundsF.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/BoundsF.cs
deleted file mode 100644
index 9d48889a..00000000
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/BoundsF.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-using System.Numerics;
-
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
-
-internal struct BoundsF
-{
- public float Left;
- public float Top;
- public float Right;
- public float Bottom;
-
- public BoundsF(float l, float t, float r, float b)
- {
- this.Left = l;
- this.Top = t;
- this.Right = r;
- this.Bottom = b;
- }
-
- public BoundsF(BoundsF bounds)
- {
- this.Left = bounds.Left;
- this.Top = bounds.Top;
- this.Right = bounds.Right;
- this.Bottom = bounds.Bottom;
- }
-
- public BoundsF(bool isValid)
- {
- if (isValid)
- {
- this.Left = 0;
- this.Top = 0;
- this.Right = 0;
- this.Bottom = 0;
- }
- else
- {
- this.Left = float.MaxValue;
- this.Top = float.MaxValue;
- this.Right = -float.MaxValue;
- this.Bottom = -float.MaxValue;
- }
- }
-
- public float Width
- {
- readonly get => this.Right - this.Left;
- set => this.Right = this.Left + value;
- }
-
- public float Height
- {
- readonly get => this.Bottom - this.Top;
- set => this.Bottom = this.Top + value;
- }
-
- public readonly bool IsEmpty()
- => this.Bottom <= this.Top || this.Right <= this.Left;
-
- public readonly Vector2 MidPoint()
- => new Vector2(this.Left + this.Right, this.Top + this.Bottom) * .5F;
-
- public readonly bool Contains(Vector2 pt)
- => pt.X > this.Left
- && pt.X < this.Right
- && pt.Y > this.Top && pt.Y < this.Bottom;
-
- public readonly bool Contains(BoundsF bounds)
- => bounds.Left >= this.Left
- && bounds.Right <= this.Right
- && bounds.Top >= this.Top
- && bounds.Bottom <= this.Bottom;
-
- public readonly bool Intersects(BoundsF bounds)
- => (Math.Max(this.Left, bounds.Left) < Math.Min(this.Right, bounds.Right))
- && (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/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/ClipperException.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperException.cs
deleted file mode 100644
index 39ddcfa0..00000000
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperException.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
-
-///
-/// The exception that is thrown when an error occurs clipping a polygon.
-///
-public class ClipperException : Exception
-{
- ///
- /// Initializes a new instance of the class.
- ///
- public ClipperException()
- {
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The message that describes the error.
- public ClipperException(string message)
- : base(message)
- {
- }
-
- ///
- /// Initializes a new instance of the class with a specified error message and a
- /// reference to the inner exception that is the cause of this exception.
- /// The error message that explains the reason for the exception.
- /// The exception that is the cause of the current exception, or a
- /// reference if no inner exception is specified.
- public ClipperException(string message, Exception innerException)
- : base(message, innerException)
- {
- }
-}
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/ClipperUtils.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperUtils.cs
deleted file mode 100644
index 39114d8b..00000000
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperUtils.cs
+++ /dev/null
@@ -1,236 +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;
-
-internal static class ClipperUtils
-{
- 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;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static float Area(PathF path)
- {
- // https://en.wikipedia.org/wiki/Shoelace_formula
- float a = 0F;
- if (path.Count < 3)
- {
- return a;
- }
-
- Vector2 prevPt = path[path.Count - 1];
- for (int i = 0; i < path.Count; i++)
- {
- Vector2 pt = path[i];
- a += (prevPt.Y + pt.Y) * (prevPt.X - pt.X);
- prevPt = pt;
- }
-
- 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);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static float CrossProduct(Vector2 vec1, Vector2 vec2)
- => (vec1.Y * vec2.X) - (vec2.Y * vec1.X);
-
- [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;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static float PerpendicDistFromLineSqrd(Vector2 pt, Vector2 line1, Vector2 line2)
- {
- Vector2 ab = pt - line1;
- Vector2 cd = line2 - line1;
- if (cd == Vector2.Zero)
- {
- return 0;
- }
-
- return Sqr(CrossProduct(cd, ab)) / DotProduct(cd, cd);
- }
-
- public static bool SegsIntersect(Vector2 seg1a, Vector2 seg1b, Vector2 seg2a, Vector2 seg2b, bool inclusive = false)
- {
- if (inclusive)
- {
- float res1 = CrossProduct(seg1a, seg2a, seg2b);
- float res2 = CrossProduct(seg1b, seg2a, seg2b);
- if (res1 * res2 > 0)
- {
- return false;
- }
-
- float res3 = CrossProduct(seg2a, seg1a, seg1b);
- float res4 = CrossProduct(seg2b, seg1a, seg1b);
- if (res3 * res4 > 0)
- {
- return false;
- }
-
- // ensure NOT collinear
- return res1 != 0 || res2 != 0 || res3 != 0 || res4 != 0;
- }
-
- return (CrossProduct(seg1a, seg2a, seg2b) * CrossProduct(seg1b, seg2a, seg2b) < 0)
- && (CrossProduct(seg2a, seg1a, seg1b) * CrossProduct(seg2b, seg1a, seg1b) < 0);
- }
-
- [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)
- {
- Vector2 dxy1 = ln1b - ln1a;
- Vector2 dxy2 = ln2b - ln2a;
- float det = CrossProduct(dxy1, dxy2);
- if (det == 0F)
- {
- ip = default;
- return false;
- }
-
- float t = (((ln1a.X - ln2a.X) * dxy2.Y) - ((ln1a.Y - ln2a.Y) * dxy2.X)) / det;
- if (t <= 0F)
- {
- ip = ln1a;
- }
- else if (t >= 1F)
- {
- ip = ln1b;
- }
- else
- {
- ip = ln1a + (t * dxy1);
- }
-
- return true;
- }
-
- public static Vector2 GetClosestPtOnSegment(Vector2 offPt, Vector2 seg1, Vector2 seg2)
- {
- if (seg1 == seg2)
- {
- return seg1;
- }
-
- Vector2 dxy = seg2 - seg1;
- Vector2 oxy = (offPt - seg1) * dxy;
- float q = (oxy.X + oxy.Y) / DotProduct(dxy, dxy);
-
- if (q < 0)
- {
- q = 0;
- }
- else if (q > 1)
- {
- q = 1;
- }
-
- return seg1 + (dxy * q);
- }
-
- public static PathF ReversePath(PathF path)
- {
- path.Reverse();
- return path;
- }
-}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/FillRule.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/FillRule.cs
deleted file mode 100644
index a4f42b29..00000000
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/FillRule.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-using SixLabors.ImageSharp.Drawing.Shapes.Rasterization;
-
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
-
-///
-/// By far the most widely used filling rules for polygons are EvenOdd
-/// and NonZero, sometimes called Alternate and Winding respectively.
-///
-///
-///
-/// TODO: This overlaps with the enum.
-/// We should see if we can enhance the to support all these rules.
-///
-internal enum FillRule
-{
- EvenOdd,
- NonZero,
- Positive,
- Negative
-}
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/PolygonClipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipper.cs
deleted file mode 100644
index 042382cd..00000000
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipper.cs
+++ /dev/null
@@ -1,3432 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-#nullable disable
-
-using System.Collections;
-using System.Numerics;
-using System.Runtime.CompilerServices;
-
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
-
-///
-/// Contains functions that cover most polygon boolean and offsetting needs.
-/// Ported from and originally licensed
-/// under
-///
-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;
-
- public PolygonClipper()
- {
- this.minimaList = [];
- this.intersectList = [];
- this.vertexList = [];
- this.outrecList = [];
- this.scanlineList = [];
- this.horzSegList = [];
- this.horzJoinList = [];
- this.PreserveCollinear = true;
- }
-
- public bool PreserveCollinear { get; set; }
-
- public bool ReverseSolution { get; set; }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void AddSubject(PathsF paths) => this.AddPaths(paths, ClippingType.Subject);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void AddPath(PathF path, ClippingType polytype, bool isOpen = false)
- {
- PathsF tmp = new(1) { path };
- this.AddPaths(tmp, polytype, isOpen);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void AddPaths(PathsF paths, ClippingType polytype, bool isOpen = false)
- {
- if (isOpen)
- {
- this.hasOpenPaths = true;
- }
-
- this.isSortedMinimaList = false;
- this.AddPathsToVertexList(paths, polytype, isOpen);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void Execute(ClippingOperation clipType, FillRule fillRule, PathsF solutionClosed)
- => this.Execute(clipType, fillRule, solutionClosed, []);
-
- public void Execute(ClippingOperation clipType, FillRule fillRule, PathsF solutionClosed, PathsF solutionOpen)
- {
- solutionClosed.Clear();
- solutionOpen.Clear();
-
- try
- {
- this.ExecuteInternal(clipType, fillRule);
- this.BuildPaths(solutionClosed, solutionOpen);
- }
- catch (Exception ex)
- {
- throw new ClipperException("An error occurred while attempting to clip the polygon. See the inner exception for details.", ex);
- }
- finally
- {
- this.ClearSolutionOnly();
- }
- }
-
- private void ExecuteInternal(ClippingOperation ct, FillRule fillRule)
- {
- if (ct == ClippingOperation.None)
- {
- return;
- }
-
- this.fillRule = fillRule;
- this.clipType = ct;
- this.Reset();
- if (!this.PopScanline(out float y))
- {
- return;
- }
-
- while (true)
- {
- this.InsertLocalMinimaIntoAEL(y);
- Active ae;
- while (this.PopHorz(out ae))
- {
- this.DoHorizontal(ae);
- }
-
- if (this.horzSegList.Count > 0)
- {
- this.ConvertHorzSegsToJoins();
- this.horzSegList.Clear();
- }
-
- this.currentBotY = y; // bottom of scanbeam
- if (!this.PopScanline(out y))
- {
- break; // y new top of scanbeam
- }
-
- this.DoIntersections(y);
- this.DoTopOfScanbeam(y);
- while (this.PopHorz(out ae))
- {
- this.DoHorizontal(ae!);
- }
- }
-
- this.ProcessHorzJoins();
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void DoIntersections(float topY)
- {
- if (this.BuildIntersectList(topY))
- {
- this.ProcessIntersectList();
- this.DisposeIntersectNodes();
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void DisposeIntersectNodes()
- => this.intersectList.Clear();
-
- [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))
- {
- ip = new Vector2(ae1.CurX, topY);
- }
-
- if (ip.Y > this.currentBotY || ip.Y < topY)
- {
- 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);
- }
- }
- }
-
- IntersectNode node = new(ip, ae1, ae2);
- this.intersectList.Add(node);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool SetHorzSegHeadingForward(HorzSegment hs, OutPt opP, OutPt opN)
- {
- if (opP.Point.X == opN.Point.X)
- {
- return false;
- }
-
- if (opP.Point.X < opN.Point.X)
- {
- hs.LeftOp = opP;
- hs.RightOp = opN;
- hs.LeftToRight = true;
- }
- else
- {
- hs.LeftOp = opN;
- hs.RightOp = opP;
- hs.LeftToRight = false;
- }
-
- return true;
- }
-
- private static bool UpdateHorzSegment(HorzSegment hs)
- {
- OutPt op = hs.LeftOp;
- OutRec outrec = GetRealOutRec(op.OutRec);
- bool outrecHasEdges = outrec.FrontEdge != null;
- float curr_y = op.Point.Y;
- OutPt opP = op, opN = op;
- if (outrecHasEdges)
- {
- OutPt opA = outrec.Pts!, opZ = opA.Next;
- while (opP != opZ && opP.Prev.Point.Y == curr_y)
- {
- opP = opP.Prev;
- }
-
- while (opN != opA && opN.Next.Point.Y == curr_y)
- {
- opN = opN.Next;
- }
- }
- else
- {
- while (opP.Prev != opN && opP.Prev.Point.Y == curr_y)
- {
- opP = opP.Prev;
- }
-
- while (opN.Next != opP && opN.Next.Point.Y == curr_y)
- {
- opN = opN.Next;
- }
- }
-
- bool result = SetHorzSegHeadingForward(hs, opP, opN) && hs.LeftOp.HorizSegment == null;
-
- if (result)
- {
- hs.LeftOp.HorizSegment = hs;
- }
- else
- {
- hs.RightOp = null; // (for sorting)
- }
-
- return result;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static OutPt DuplicateOp(OutPt op, bool insert_after)
- {
- OutPt result = new(op.Point, op.OutRec);
- if (insert_after)
- {
- result.Next = op.Next;
- result.Next.Prev = result;
- result.Prev = op;
- op.Next = result;
- }
- else
- {
- result.Prev = op.Prev;
- result.Prev.Next = result;
- result.Next = op;
- op.Prev = result;
- }
-
- return result;
- }
-
- private void ConvertHorzSegsToJoins()
- {
- int k = 0;
- foreach (HorzSegment hs in this.horzSegList)
- {
- if (UpdateHorzSegment(hs))
- {
- k++;
- }
- }
-
- if (k < 2)
- {
- return;
- }
-
- this.horzSegList.Sort(default(HorzSegSorter));
-
- for (int i = 0; i < k - 1; i++)
- {
- HorzSegment hs1 = this.horzSegList[i];
-
- // for each HorzSegment, find others that overlap
- for (int j = i + 1; j < k; j++)
- {
- HorzSegment hs2 = this.horzSegList[j];
- if ((hs2.LeftOp.Point.X >= hs1.RightOp.Point.X) ||
- (hs2.LeftToRight == hs1.LeftToRight) ||
- (hs2.RightOp.Point.X <= hs1.LeftOp.Point.X))
- {
- continue;
- }
-
- float curr_y = hs1.LeftOp.Point.Y;
- if (hs1.LeftToRight)
- {
- while (hs1.LeftOp.Next.Point.Y == curr_y &&
- hs1.LeftOp.Next.Point.X <= hs2.LeftOp.Point.X)
- {
- hs1.LeftOp = hs1.LeftOp.Next;
- }
-
- while (hs2.LeftOp.Prev.Point.Y == curr_y &&
- hs2.LeftOp.Prev.Point.X <= hs1.LeftOp.Point.X)
- {
- hs2.LeftOp = hs2.LeftOp.Prev;
- }
-
- HorzJoin join = new(DuplicateOp(hs1.LeftOp, true), DuplicateOp(hs2.LeftOp, false));
- this.horzJoinList.Add(join);
- }
- else
- {
- while (hs1.LeftOp.Prev.Point.Y == curr_y &&
- hs1.LeftOp.Prev.Point.X <= hs2.LeftOp.Point.X)
- {
- hs1.LeftOp = hs1.LeftOp.Prev;
- }
-
- while (hs2.LeftOp.Next.Point.Y == curr_y &&
- hs2.LeftOp.Next.Point.X <= hs1.LeftOp.Point.X)
- {
- hs2.LeftOp = hs2.LeftOp.Next;
- }
-
- HorzJoin join = new(DuplicateOp(hs2.LeftOp, true), DuplicateOp(hs1.LeftOp, false));
- this.horzJoinList.Add(join);
- }
- }
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void ClearSolutionOnly()
- {
- while (this.actives != null)
- {
- this.DeleteFromAEL(this.actives);
- }
-
- this.scanlineList.Clear();
- this.DisposeIntersectNodes();
- this.outrecList.Clear();
- this.horzSegList.Clear();
- this.horzJoinList.Clear();
- }
-
- private bool BuildPaths(PathsF solutionClosed, PathsF solutionOpen)
- {
- solutionClosed.Clear();
- solutionOpen.Clear();
- solutionClosed.EnsureCapacity(this.outrecList.Count);
- solutionOpen.EnsureCapacity(this.outrecList.Count);
-
- int i = 0;
-
- // _outrecList.Count is not static here because
- // CleanCollinear can indirectly add additional OutRec
- while (i < this.outrecList.Count)
- {
- OutRec outrec = this.outrecList[i++];
- if (outrec.Pts == null)
- {
- continue;
- }
-
- PathF path = [];
- if (outrec.IsOpen)
- {
- if (BuildPath(outrec.Pts, this.ReverseSolution, true, path))
- {
- solutionOpen.Add(path);
- }
- }
- else
- {
- this.CleanCollinear(outrec);
-
- // closed paths should always return a Positive orientation
- // except when ReverseSolution == true
- if (BuildPath(outrec.Pts, this.ReverseSolution, false, path))
- {
- solutionClosed.Add(path);
- }
- }
- }
-
- return true;
- }
-
- private static bool BuildPath(OutPt op, bool reverse, bool isOpen, PathF path)
- {
- if (op == null || op.Next == op || (!isOpen && op.Next == op.Prev))
- {
- return false;
- }
-
- path.Clear();
-
- Vector2 lastPt;
- OutPt op2;
- if (reverse)
- {
- lastPt = op.Point;
- op2 = op.Prev;
- }
- else
- {
- op = op.Next;
- lastPt = op.Point;
- op2 = op.Next;
- }
-
- path.Add(lastPt);
-
- while (op2 != op)
- {
- if (op2.Point != lastPt)
- {
- lastPt = op2.Point;
- path.Add(lastPt);
- }
-
- if (reverse)
- {
- op2 = op2.Prev;
- }
- else
- {
- op2 = op2.Next;
- }
- }
-
- return path.Count != 3 || !IsVerySmallTriangle(op2);
- }
-
- private void DoHorizontal(Active horz)
- /*******************************************************************************
- * Notes: Horizontal edges (HEs) at scanline intersections (i.e. at the top or *
- * bottom of a scanbeam) are processed as if layered.The order in which HEs *
- * are processed doesn't matter. HEs intersect with the bottom vertices of *
- * other HEs[#] and with non-horizontal edges [*]. Once these intersections *
- * are completed, intermediate HEs are 'promoted' to the next edge in their *
- * bounds, and they in turn may be intersected[%] by other HEs. *
- * *
- * eg: 3 horizontals at a scanline: / | / / *
- * | / | (HE3)o ========%========== o *
- * o ======= o(HE2) / | / / *
- * o ============#=========*======*========#=========o (HE1) *
- * / | / | / *
- *******************************************************************************/
- {
- Vector2 pt;
- bool horzIsOpen = IsOpen(horz);
- float y = horz.Bot.Y;
-
- Vertex vertex_max = horzIsOpen ? GetCurrYMaximaVertex_Open(horz) : GetCurrYMaximaVertex(horz);
-
- // remove 180 deg.spikes and also simplify
- // consecutive horizontals when PreserveCollinear = true
- if (vertex_max != null &&
- !horzIsOpen && vertex_max != horz.VertexTop)
- {
- TrimHorz(horz, this.PreserveCollinear);
- }
-
- bool isLeftToRight = ResetHorzDirection(horz, vertex_max, out float leftX, out float rightX);
-
- if (IsHotEdge(horz))
- {
- OutPt op = AddOutPt(horz, new Vector2(horz.CurX, y));
- this.AddToHorzSegList(op);
- }
-
- OutRec currOutrec = horz.Outrec;
-
- while (true)
- {
- // loops through consec. horizontal edges (if open)
- Active ae = isLeftToRight ? horz.NextInAEL : horz.PrevInAEL;
-
- while (ae != null)
- {
- if (ae.VertexTop == vertex_max)
- {
- // do this first!!
- if (IsHotEdge(horz) && IsJoined(ae!))
- {
- this.Split(ae, ae.Top);
- }
-
- if (IsHotEdge(horz))
- {
- while (horz.VertexTop != vertex_max)
- {
- AddOutPt(horz, horz.Top);
- this.UpdateEdgeIntoAEL(horz);
- }
-
- if (isLeftToRight)
- {
- this.AddLocalMaxPoly(horz, ae, horz.Top);
- }
- else
- {
- this.AddLocalMaxPoly(ae, horz, horz.Top);
- }
- }
-
- this.DeleteFromAEL(ae);
- this.DeleteFromAEL(horz);
- return;
- }
-
- // if horzEdge is a maxima, keep going until we reach
- // its maxima pair, otherwise check for break conditions
- if (vertex_max != horz.VertexTop || IsOpenEnd(horz))
- {
- // otherwise stop when 'ae' is beyond the end of the horizontal line
- if ((isLeftToRight && ae.CurX > rightX) || (!isLeftToRight && ae.CurX < leftX))
- {
- break;
- }
-
- if (ae.CurX == horz.Top.X && !IsHorizontal(ae))
- {
- pt = NextVertex(horz).Point;
-
- // to maximize the possibility of putting open edges into
- // solutions, we'll only break if it's past HorzEdge's end
- if (IsOpen(ae) && !IsSamePolyType(ae, horz) && !IsHotEdge(ae))
- {
- if ((isLeftToRight && (TopX(ae, pt.Y) > pt.X)) ||
- (!isLeftToRight && (TopX(ae, pt.Y) < pt.X)))
- {
- break;
- }
- }
-
- // otherwise for edges at horzEdge's end, only stop when horzEdge's
- // outslope is greater than e's slope when heading right or when
- // horzEdge's outslope is less than e's slope when heading left.
- else if ((isLeftToRight && (TopX(ae, pt.Y) >= pt.X)) || (!isLeftToRight && (TopX(ae, pt.Y) <= pt.X)))
- {
- break;
- }
- }
- }
-
- pt = new Vector2(ae.CurX, y);
-
- if (isLeftToRight)
- {
- this.IntersectEdges(horz, ae, pt);
- this.SwapPositionsInAEL(horz, ae);
- horz.CurX = ae.CurX;
- ae = horz.NextInAEL;
- }
- else
- {
- this.IntersectEdges(ae, horz, pt);
- this.SwapPositionsInAEL(ae, horz);
- horz.CurX = ae.CurX;
- ae = horz.PrevInAEL;
- }
-
- if (IsHotEdge(horz) && (horz.Outrec != currOutrec))
- {
- currOutrec = horz.Outrec;
- this.AddToHorzSegList(GetLastOp(horz));
- }
-
- // we've reached the end of this horizontal
- }
-
- // check if we've finished looping
- // through consecutive horizontals
- // ie open at top
- if (horzIsOpen && IsOpenEnd(horz))
- {
- if (IsHotEdge(horz))
- {
- AddOutPt(horz, horz.Top);
- if (IsFront(horz))
- {
- horz.Outrec.FrontEdge = null;
- }
- else
- {
- horz.Outrec.BackEdge = null;
- }
-
- horz.Outrec = null;
- }
-
- this.DeleteFromAEL(horz);
- return;
- }
- else if (NextVertex(horz).Point.Y != horz.Top.Y)
- {
- break;
- }
-
- // still more horizontals in bound to process ...
- if (IsHotEdge(horz))
- {
- AddOutPt(horz, horz.Top);
- }
-
- this.UpdateEdgeIntoAEL(horz);
-
- if (this.PreserveCollinear && !horzIsOpen && HorzIsSpike(horz))
- {
- TrimHorz(horz, true);
- }
-
- isLeftToRight = ResetHorzDirection(horz, vertex_max, out leftX, out rightX);
-
- // end for loop and end of (possible consecutive) horizontals
- }
-
- if (IsHotEdge(horz))
- {
- this.AddToHorzSegList(AddOutPt(horz, horz.Top));
- }
-
- this.UpdateEdgeIntoAEL(horz); // this is the end of an intermediate horiz.
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void DoTopOfScanbeam(float y)
- {
- this.flaggedHorizontal = null; // sel_ is reused to flag horizontals (see PushHorz below)
- Active ae = this.actives;
- while (ae != null)
- {
- // NB 'ae' will never be horizontal here
- if (ae.Top.Y == y)
- {
- ae.CurX = ae.Top.X;
- if (IsMaxima(ae))
- {
- ae = this.DoMaxima(ae); // TOP OF BOUND (MAXIMA)
- continue;
- }
-
- // INTERMEDIATE VERTEX ...
- if (IsHotEdge(ae))
- {
- AddOutPt(ae, ae.Top);
- }
-
- this.UpdateEdgeIntoAEL(ae);
- if (IsHorizontal(ae))
- {
- this.PushHorz(ae); // horizontals are processed later
- }
- }
- else
- {
- // i.e. not the top of the edge
- ae.CurX = TopX(ae, y);
- }
-
- ae = ae.NextInAEL;
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private Active DoMaxima(Active ae)
- {
- Active prevE;
- Active nextE, maxPair;
- prevE = ae.PrevInAEL;
- nextE = ae.NextInAEL;
-
- if (IsOpenEnd(ae))
- {
- if (IsHotEdge(ae))
- {
- AddOutPt(ae, ae.Top);
- }
-
- if (!IsHorizontal(ae))
- {
- if (IsHotEdge(ae))
- {
- if (IsFront(ae))
- {
- ae.Outrec.FrontEdge = null;
- }
- else
- {
- ae.Outrec.BackEdge = null;
- }
-
- ae.Outrec = null;
- }
-
- this.DeleteFromAEL(ae);
- }
-
- return nextE;
- }
-
- maxPair = GetMaximaPair(ae);
- if (maxPair == null)
- {
- return nextE; // eMaxPair is horizontal
- }
-
- if (IsJoined(ae))
- {
- this.Split(ae, ae.Top);
- }
-
- if (IsJoined(maxPair))
- {
- this.Split(maxPair, maxPair.Top);
- }
-
- // only non-horizontal maxima here.
- // process any edges between maxima pair ...
- while (nextE != maxPair)
- {
- this.IntersectEdges(ae, nextE!, ae.Top);
- this.SwapPositionsInAEL(ae, nextE!);
- nextE = ae.NextInAEL;
- }
-
- if (IsOpen(ae))
- {
- if (IsHotEdge(ae))
- {
- this.AddLocalMaxPoly(ae, maxPair, ae.Top);
- }
-
- this.DeleteFromAEL(maxPair);
- this.DeleteFromAEL(ae);
- return prevE != null ? prevE.NextInAEL : this.actives;
- }
-
- // here ae.nextInAel == ENext == EMaxPair ...
- if (IsHotEdge(ae))
- {
- this.AddLocalMaxPoly(ae, maxPair, ae.Top);
- }
-
- this.DeleteFromAEL(ae);
- this.DeleteFromAEL(maxPair);
- return prevE != null ? prevE.NextInAEL : this.actives;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void TrimHorz(Active horzEdge, bool preserveCollinear)
- {
- bool wasTrimmed = false;
- Vector2 pt = NextVertex(horzEdge).Point;
-
- while (pt.Y == horzEdge.Top.Y)
- {
- // always trim 180 deg. spikes (in closed paths)
- // but otherwise break if preserveCollinear = true
- if (preserveCollinear && (pt.X < horzEdge.Top.X) != (horzEdge.Bot.X < horzEdge.Top.X))
- {
- break;
- }
-
- horzEdge.VertexTop = NextVertex(horzEdge);
- horzEdge.Top = pt;
- wasTrimmed = true;
- if (IsMaxima(horzEdge))
- {
- break;
- }
-
- pt = NextVertex(horzEdge).Point;
- }
-
- if (wasTrimmed)
- {
- SetDx(horzEdge); // +/-infinity
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void AddToHorzSegList(OutPt op)
- {
- if (op.OutRec.IsOpen)
- {
- return;
- }
-
- this.horzSegList.Add(new HorzSegment(op));
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static OutPt GetLastOp(Active hotEdge)
- {
- OutRec outrec = hotEdge.Outrec;
- return (hotEdge == outrec.FrontEdge) ? outrec.Pts : outrec.Pts.Next;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Vertex GetCurrYMaximaVertex_Open(Active ae)
- {
- Vertex result = ae.VertexTop;
- if (ae.WindDx > 0)
- {
- while (result.Next.Point.Y == result.Point.Y && ((result.Flags & (VertexFlags.OpenEnd | VertexFlags.LocalMax)) == VertexFlags.None))
- {
- result = result.Next;
- }
- }
- else
- {
- while (result.Prev.Point.Y == result.Point.Y && ((result.Flags & (VertexFlags.OpenEnd | VertexFlags.LocalMax)) == VertexFlags.None))
- {
- result = result.Prev;
- }
- }
-
- if (!IsMaxima(result))
- {
- result = null; // not a maxima
- }
-
- return result;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Vertex GetCurrYMaximaVertex(Active ae)
- {
- Vertex result = ae.VertexTop;
- if (ae.WindDx > 0)
- {
- while (result.Next.Point.Y == result.Point.Y)
- {
- result = result.Next;
- }
- }
- else
- {
- while (result.Prev.Point.Y == result.Point.Y)
- {
- result = result.Prev;
- }
- }
-
- if (!IsMaxima(result))
- {
- result = null; // not a maxima
- }
-
- return result;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsVerySmallTriangle(OutPt op)
- => op.Next.Next == op.Prev
- && (PtsReallyClose(op.Prev.Point, op.Next.Point)
- || PtsReallyClose(op.Point, op.Next.Point)
- || PtsReallyClose(op.Point, op.Prev.Point));
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsValidClosedPath(OutPt op)
- => op != null && op.Next != op && (op.Next != op.Prev || !IsVerySmallTriangle(op));
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static OutPt DisposeOutPt(OutPt op)
- {
- OutPt result = op.Next == op ? null : op.Next;
- op.Prev.Next = op.Next;
- op.Next.Prev = op.Prev;
-
- return result;
- }
-
- private void ProcessHorzJoins()
- {
- foreach (HorzJoin j in this.horzJoinList)
- {
- OutRec or1 = GetRealOutRec(j.Op1.OutRec);
- OutRec or2 = GetRealOutRec(j.Op2.OutRec);
-
- OutPt op1b = j.Op1.Next;
- OutPt op2b = j.Op2.Prev;
- j.Op1.Next = j.Op2;
- j.Op2.Prev = j.Op1;
- op1b.Prev = op2b;
- op2b.Next = op1b;
-
- // 'join' is really a split
- if (or1 == or2)
- {
- or2 = new OutRec
- {
- Pts = op1b
- };
-
- FixOutRecPts(or2);
-
- if (or1.Pts.OutRec == or2)
- {
- or1.Pts = j.Op1;
- or1.Pts.OutRec = or1;
- }
-
- or2.Owner = or1;
-
- this.outrecList.Add(or2);
- }
- else
- {
- or2.Pts = null;
- or2.Owner = or1;
- }
- }
- }
-
- [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);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void CleanCollinear(OutRec outrec)
- {
- outrec = GetRealOutRec(outrec);
-
- if (outrec?.IsOpen != false)
- {
- return;
- }
-
- if (!IsValidClosedPath(outrec.Pts))
- {
- outrec.Pts = null;
- return;
- }
-
- OutPt startOp = outrec.Pts;
- OutPt op2 = startOp;
- 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 (op2 == outrec.Pts)
- {
- outrec.Pts = op2.Prev;
- }
-
- op2 = DisposeOutPt(op2);
- if (!IsValidClosedPath(op2))
- {
- outrec.Pts = null;
- return;
- }
-
- startOp = op2;
- continue;
- }
-
- op2 = op2.Next;
- }
- while (op2 != startOp);
-
- this.FixSelfIntersects(outrec);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void DoSplitOp(OutRec outrec, OutPt splitOp)
- {
- // splitOp.prev <=> splitOp &&
- // splitOp.next <=> splitOp.next.next are intersecting
- OutPt prevOp = splitOp.Prev;
- OutPt nextNextOp = splitOp.Next.Next;
- outrec.Pts = prevOp;
-
- ClipperUtils.GetIntersectPoint(
- prevOp.Point, splitOp.Point, splitOp.Next.Point, nextNextOp.Point, out Vector2 ip);
-
- float area1 = Area(prevOp);
- float absArea1 = Math.Abs(area1);
-
- if (absArea1 < 2)
- {
- outrec.Pts = null;
- return;
- }
-
- float area2 = AreaTriangle(ip, splitOp.Point, splitOp.Next.Point);
- float absArea2 = Math.Abs(area2);
-
- // de-link splitOp and splitOp.next from the path
- // while inserting the intersection point
- if (ip == prevOp.Point || ip == nextNextOp.Point)
- {
- nextNextOp.Prev = prevOp;
- prevOp.Next = nextNextOp;
- }
- else
- {
- OutPt newOp2 = new(ip, outrec)
- {
- Prev = prevOp,
- Next = nextNextOp
- };
-
- nextNextOp.Prev = newOp2;
- prevOp.Next = newOp2;
- }
-
- // nb: area1 is the path's area *before* splitting, whereas area2 is
- // the area of the triangle containing splitOp & splitOp.next.
- // 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))))
- {
- OutRec newOutRec = this.NewOutRec();
- newOutRec.Owner = outrec.Owner;
- splitOp.OutRec = newOutRec;
- splitOp.Next.OutRec = newOutRec;
-
- OutPt newOp = new(ip, newOutRec) { Prev = splitOp.Next, Next = splitOp };
- newOutRec.Pts = newOp;
- splitOp.Prev = newOp;
- splitOp.Next.Next = newOp;
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void FixSelfIntersects(OutRec outrec)
- {
- OutPt op2 = outrec.Pts;
-
- // 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))
- {
- this.DoSplitOp(outrec, op2);
- if (outrec.Pts == null)
- {
- return;
- }
-
- op2 = outrec.Pts;
- continue;
- }
- else
- {
- op2 = op2.Next;
- }
-
- if (op2 == outrec.Pts)
- {
- break;
- }
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void Reset()
- {
- if (!this.isSortedMinimaList)
- {
- this.minimaList.Sort(default(LocMinSorter));
- this.isSortedMinimaList = true;
- }
-
- this.scanlineList.EnsureCapacity(this.minimaList.Count);
- for (int i = this.minimaList.Count - 1; i >= 0; i--)
- {
- this.scanlineList.Add(this.minimaList[i].Vertex.Point.Y);
- }
-
- this.currentBotY = 0;
- this.currentLocMin = 0;
- this.actives = null;
- this.flaggedHorizontal = null;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void InsertScanline(float y)
- {
- int index = this.scanlineList.BinarySearch(y);
- if (index >= 0)
- {
- return;
- }
-
- index = ~index;
- this.scanlineList.Insert(index, y);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private bool PopScanline(out float y)
- {
- int cnt = this.scanlineList.Count - 1;
- if (cnt < 0)
- {
- y = 0;
- return false;
- }
-
- y = this.scanlineList[cnt];
- this.scanlineList.RemoveAt(cnt--);
- while (cnt >= 0 && y == this.scanlineList[cnt])
- {
- this.scanlineList.RemoveAt(cnt--);
- }
-
- return true;
- }
-
- private void InsertLocalMinimaIntoAEL(float botY)
- {
- LocalMinima localMinima;
- Active leftBound, rightBound;
-
- // Add any local minima (if any) at BotY
- // NB horizontal local minima edges should contain locMin.vertex.prev
- while (this.HasLocMinAtY(botY))
- {
- localMinima = this.PopLocalMinima();
- if ((localMinima.Vertex.Flags & VertexFlags.OpenStart) != VertexFlags.None)
- {
- leftBound = null;
- }
- else
- {
- leftBound = new Active
- {
- Bot = localMinima.Vertex.Point,
- CurX = localMinima.Vertex.Point.X,
- WindDx = -1,
- VertexTop = localMinima.Vertex.Prev,
- Top = localMinima.Vertex.Prev.Point,
- Outrec = null,
- LocalMin = localMinima
- };
- SetDx(leftBound);
- }
-
- if ((localMinima.Vertex.Flags & VertexFlags.OpenEnd) != VertexFlags.None)
- {
- rightBound = null;
- }
- else
- {
- rightBound = new Active
- {
- Bot = localMinima.Vertex.Point,
- CurX = localMinima.Vertex.Point.X,
- WindDx = 1,
- VertexTop = localMinima.Vertex.Next, // i.e. ascending
- Top = localMinima.Vertex.Next.Point,
- Outrec = null,
- LocalMin = localMinima
- };
- SetDx(rightBound);
- }
-
- // Currently LeftB is just the descending bound and RightB is the ascending.
- // Now if the LeftB isn't on the left of RightB then we need swap them.
- if (leftBound != null && rightBound != null)
- {
- if (IsHorizontal(leftBound))
- {
- if (IsHeadingRightHorz(leftBound))
- {
- SwapActives(ref leftBound, ref rightBound);
- }
- }
- else if (IsHorizontal(rightBound))
- {
- if (IsHeadingLeftHorz(rightBound))
- {
- SwapActives(ref leftBound, ref rightBound);
- }
- }
- else if (leftBound.Dx < rightBound.Dx)
- {
- SwapActives(ref leftBound, ref rightBound);
- }
-
- // so when leftBound has windDx == 1, the polygon will be oriented
- // counter-clockwise in Cartesian coords (clockwise with inverted Y).
- }
- else if (leftBound == null)
- {
- leftBound = rightBound;
- rightBound = null;
- }
-
- bool contributing;
- leftBound.IsLeftBound = true;
- this.InsertLeftEdge(leftBound);
-
- if (IsOpen(leftBound))
- {
- this.SetWindCountForOpenPathEdge(leftBound);
- contributing = this.IsContributingOpen(leftBound);
- }
- else
- {
- this.SetWindCountForClosedPathEdge(leftBound);
- contributing = this.IsContributingClosed(leftBound);
- }
-
- if (rightBound != null)
- {
- rightBound.WindCount = leftBound.WindCount;
- rightBound.WindCount2 = leftBound.WindCount2;
- InsertRightEdge(leftBound, rightBound); ///////
-
- if (contributing)
- {
- this.AddLocalMinPoly(leftBound, rightBound, leftBound.Bot, true);
- if (!IsHorizontal(leftBound))
- {
- this.CheckJoinLeft(leftBound, leftBound.Bot);
- }
- }
-
- while (rightBound.NextInAEL != null && IsValidAelOrder(rightBound.NextInAEL, rightBound))
- {
- this.IntersectEdges(rightBound, rightBound.NextInAEL, rightBound.Bot);
- this.SwapPositionsInAEL(rightBound, rightBound.NextInAEL);
- }
-
- if (IsHorizontal(rightBound))
- {
- this.PushHorz(rightBound);
- }
- else
- {
- this.CheckJoinRight(rightBound, rightBound.Bot);
- this.InsertScanline(rightBound.Top.Y);
- }
- }
- else if (contributing)
- {
- this.StartOpenPath(leftBound, leftBound.Bot);
- }
-
- if (IsHorizontal(leftBound))
- {
- this.PushHorz(leftBound);
- }
- else
- {
- this.InsertScanline(leftBound.Top.Y);
- }
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Active ExtractFromSEL(Active ae)
- {
- Active res = ae.NextInSEL;
- if (res != null)
- {
- res.PrevInSEL = ae.PrevInSEL;
- }
-
- ae.PrevInSEL.NextInSEL = res;
- return res;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void Insert1Before2InSEL(Active ae1, Active ae2)
- {
- ae1.PrevInSEL = ae2.PrevInSEL;
- if (ae1.PrevInSEL != null)
- {
- ae1.PrevInSEL.NextInSEL = ae1;
- }
-
- ae1.NextInSEL = ae2;
- ae2.PrevInSEL = ae1;
- }
-
- private bool BuildIntersectList(float topY)
- {
- if (this.actives == null || this.actives.NextInAEL == null)
- {
- return false;
- }
-
- // Calculate edge positions at the top of the current scanbeam, and from this
- // we will determine the intersections required to reach these new positions.
- this.AdjustCurrXAndCopyToSEL(topY);
-
- // Find all edge intersections in the current scanbeam using a stable merge
- // sort that ensures only adjacent edges are intersecting. Intersect info is
- // stored in FIntersectList ready to be processed in ProcessIntersectList.
- // Re merge sorts see https://stackoverflow.com/a/46319131/359538
- Active left = this.flaggedHorizontal;
- Active right;
- Active lEnd;
- Active rEnd;
- Active currBase;
- Active prevBase;
- Active tmp;
-
- while (left.Jump != null)
- {
- prevBase = null;
- while (left?.Jump != null)
- {
- currBase = left;
- right = left.Jump;
- lEnd = right;
- rEnd = right.Jump;
- left.Jump = rEnd;
- while (left != lEnd && right != rEnd)
- {
- if (right.CurX < left.CurX)
- {
- tmp = right.PrevInSEL;
- while (true)
- {
- this.AddNewIntersectNode(tmp, right, topY);
- if (tmp == left)
- {
- break;
- }
-
- tmp = tmp.PrevInSEL;
- }
-
- tmp = right;
- right = ExtractFromSEL(tmp);
- lEnd = right;
- Insert1Before2InSEL(tmp, left);
- if (left == currBase)
- {
- currBase = tmp;
- currBase.Jump = rEnd;
- if (prevBase == null)
- {
- this.flaggedHorizontal = currBase;
- }
- else
- {
- prevBase.Jump = currBase;
- }
- }
- }
- else
- {
- left = left.NextInSEL;
- }
- }
-
- prevBase = currBase;
- left = rEnd;
- }
-
- left = this.flaggedHorizontal;
- }
-
- return this.intersectList.Count > 0;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void ProcessIntersectList()
- {
- // We now have a list of intersections required so that edges will be
- // correctly positioned at the top of the scanbeam. However, it's important
- // that edge intersections are processed from the bottom up, but it's also
- // crucial that intersections only occur between adjacent edges.
-
- // First we do a quicksort so intersections proceed in a bottom up order ...
- this.intersectList.Sort(default(IntersectListSort));
-
- // Now as we process these intersections, we must sometimes adjust the order
- // to ensure that intersecting edges are always adjacent ...
- for (int i = 0; i < this.intersectList.Count; ++i)
- {
- if (!EdgesAdjacentInAEL(this.intersectList[i]))
- {
- int j = i + 1;
- while (!EdgesAdjacentInAEL(this.intersectList[j]))
- {
- j++;
- }
-
- // swap
- (this.intersectList[j], this.intersectList[i]) =
- (this.intersectList[i], this.intersectList[j]);
- }
-
- IntersectNode node = this.intersectList[i];
- this.IntersectEdges(node.Edge1, node.Edge2, node.Point);
- this.SwapPositionsInAEL(node.Edge1, node.Edge2);
-
- node.Edge1.CurX = node.Point.X;
- node.Edge2.CurX = node.Point.X;
- this.CheckJoinLeft(node.Edge2, node.Point, true);
- this.CheckJoinRight(node.Edge1, node.Point, true);
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void SwapPositionsInAEL(Active ae1, Active ae2)
- {
- // preconditon: ae1 must be immediately to the left of ae2
- Active next = ae2.NextInAEL;
- if (next != null)
- {
- next.PrevInAEL = ae1;
- }
-
- Active prev = ae1.PrevInAEL;
- if (prev != null)
- {
- prev.NextInAEL = ae2;
- }
-
- ae2.PrevInAEL = prev;
- ae2.NextInAEL = ae1;
- ae1.PrevInAEL = ae2;
- ae1.NextInAEL = next;
- if (ae2.PrevInAEL == null)
- {
- this.actives = ae2;
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool ResetHorzDirection(Active horz, Vertex vertexMax, out float leftX, out float rightX)
- {
- if (horz.Bot.X == horz.Top.X)
- {
- // the horizontal edge is going nowhere ...
- leftX = horz.CurX;
- rightX = horz.CurX;
- Active ae = horz.NextInAEL;
- while (ae != null && ae.VertexTop != vertexMax)
- {
- ae = ae.NextInAEL;
- }
-
- return ae != null;
- }
-
- if (horz.CurX < horz.Top.X)
- {
- leftX = horz.CurX;
- rightX = horz.Top.X;
- return true;
- }
-
- leftX = horz.Top.X;
- rightX = horz.CurX;
- return false; // right to left
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool HorzIsSpike(Active horz)
- {
- Vector2 nextPt = NextVertex(horz).Point;
- return (horz.Bot.X < horz.Top.X) != (horz.Top.X < nextPt.X);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Active FindEdgeWithMatchingLocMin(Active e)
- {
- Active result = e.NextInAEL;
- while (result != null)
- {
- if (result.LocalMin == e.LocalMin)
- {
- return result;
- }
-
- if (!IsHorizontal(result) && e.Bot != result.Bot)
- {
- result = null;
- }
- else
- {
- result = result.NextInAEL;
- }
- }
-
- result = e.PrevInAEL;
- while (result != null)
- {
- if (result.LocalMin == e.LocalMin)
- {
- return result;
- }
-
- if (!IsHorizontal(result) && e.Bot != result.Bot)
- {
- return null;
- }
-
- result = result.PrevInAEL;
- }
-
- return result;
- }
-
- private OutPt IntersectEdges(Active ae1, Active ae2, Vector2 pt)
- {
- OutPt resultOp = null;
-
- // MANAGE OPEN PATH INTERSECTIONS SEPARATELY ...
- if (this.hasOpenPaths && (IsOpen(ae1) || IsOpen(ae2)))
- {
- if (IsOpen(ae1) && IsOpen(ae2))
- {
- return null;
- }
-
- // the following line avoids duplicating quite a bit of code
- if (IsOpen(ae2))
- {
- SwapActives(ref ae1, ref ae2);
- }
-
- if (IsJoined(ae2))
- {
- this.Split(ae2, pt); // needed for safety
- }
-
- if (this.clipType == ClippingOperation.Union)
- {
- if (!IsHotEdge(ae2))
- {
- return null;
- }
- }
- else if (ae2.LocalMin.Polytype == ClippingType.Subject)
- {
- return null;
- }
-
- switch (this.fillRule)
- {
- case FillRule.Positive:
- if (ae2.WindCount != 1)
- {
- return null;
- }
-
- break;
- case FillRule.Negative:
- if (ae2.WindCount != -1)
- {
- return null;
- }
-
- break;
- default:
- if (Math.Abs(ae2.WindCount) != 1)
- {
- return null;
- }
-
- break;
- }
-
- // toggle contribution ...
- if (IsHotEdge(ae1))
- {
- resultOp = AddOutPt(ae1, pt);
- if (IsFront(ae1))
- {
- ae1.Outrec.FrontEdge = null;
- }
- else
- {
- ae1.Outrec.BackEdge = null;
- }
-
- ae1.Outrec = null;
- }
-
- // horizontal edges can pass under open paths at a LocMins
- else if (pt == ae1.LocalMin.Vertex.Point && !IsOpenEnd(ae1.LocalMin.Vertex))
- {
- // find the other side of the LocMin and
- // if it's 'hot' join up with it ...
- Active ae3 = FindEdgeWithMatchingLocMin(ae1);
- if (ae3 != null && IsHotEdge(ae3))
- {
- ae1.Outrec = ae3.Outrec;
- if (ae1.WindDx > 0)
- {
- SetSides(ae3.Outrec!, ae1, ae3);
- }
- else
- {
- SetSides(ae3.Outrec!, ae3, ae1);
- }
-
- return ae3.Outrec.Pts;
- }
-
- resultOp = this.StartOpenPath(ae1, pt);
- }
- else
- {
- resultOp = this.StartOpenPath(ae1, pt);
- }
-
- return resultOp;
- }
-
- // MANAGING CLOSED PATHS FROM HERE ON
- if (IsJoined(ae1))
- {
- this.Split(ae1, pt);
- }
-
- if (IsJoined(ae2))
- {
- this.Split(ae2, pt);
- }
-
- // UPDATE WINDING COUNTS...
- int oldE1WindCount, oldE2WindCount;
- if (ae1.LocalMin.Polytype == ae2.LocalMin.Polytype)
- {
- if (this.fillRule == FillRule.EvenOdd)
- {
- oldE1WindCount = ae1.WindCount;
- ae1.WindCount = ae2.WindCount;
- ae2.WindCount = oldE1WindCount;
- }
- else
- {
- if (ae1.WindCount + ae2.WindDx == 0)
- {
- ae1.WindCount = -ae1.WindCount;
- }
- else
- {
- ae1.WindCount += ae2.WindDx;
- }
-
- if (ae2.WindCount - ae1.WindDx == 0)
- {
- ae2.WindCount = -ae2.WindCount;
- }
- else
- {
- ae2.WindCount -= ae1.WindDx;
- }
- }
- }
- else
- {
- if (this.fillRule != FillRule.EvenOdd)
- {
- ae1.WindCount2 += ae2.WindDx;
- }
- else
- {
- ae1.WindCount2 = ae1.WindCount2 == 0 ? 1 : 0;
- }
-
- if (this.fillRule != FillRule.EvenOdd)
- {
- ae2.WindCount2 -= ae1.WindDx;
- }
- else
- {
- ae2.WindCount2 = ae2.WindCount2 == 0 ? 1 : 0;
- }
- }
-
- switch (this.fillRule)
- {
- case FillRule.Positive:
- oldE1WindCount = ae1.WindCount;
- oldE2WindCount = ae2.WindCount;
- break;
- case FillRule.Negative:
- oldE1WindCount = -ae1.WindCount;
- oldE2WindCount = -ae2.WindCount;
- break;
- default:
- oldE1WindCount = Math.Abs(ae1.WindCount);
- oldE2WindCount = Math.Abs(ae2.WindCount);
- break;
- }
-
- bool e1WindCountIs0or1 = oldE1WindCount is 0 or 1;
- bool e2WindCountIs0or1 = oldE2WindCount is 0 or 1;
-
- if ((!IsHotEdge(ae1) && !e1WindCountIs0or1) || (!IsHotEdge(ae2) && !e2WindCountIs0or1))
- {
- return null;
- }
-
- // NOW PROCESS THE INTERSECTION ...
-
- // if both edges are 'hot' ...
- if (IsHotEdge(ae1) && IsHotEdge(ae2))
- {
- if ((oldE1WindCount != 0 && oldE1WindCount != 1) || (oldE2WindCount != 0 && oldE2WindCount != 1) ||
- (ae1.LocalMin.Polytype != ae2.LocalMin.Polytype && this.clipType != ClippingOperation.Xor))
- {
- resultOp = this.AddLocalMaxPoly(ae1, ae2, pt);
- }
- else if (IsFront(ae1) || (ae1.Outrec == ae2.Outrec))
- {
- // this 'else if' condition isn't strictly needed but
- // it's sensible to split polygons that ony touch at
- // a common vertex (not at common edges).
- resultOp = this.AddLocalMaxPoly(ae1, ae2, pt);
- this.AddLocalMinPoly(ae1, ae2, pt);
- }
- else
- {
- // can't treat as maxima & minima
- resultOp = AddOutPt(ae1, pt);
- AddOutPt(ae2, pt);
- SwapOutrecs(ae1, ae2);
- }
- }
-
- // if one or other edge is 'hot' ...
- else if (IsHotEdge(ae1))
- {
- resultOp = AddOutPt(ae1, pt);
- SwapOutrecs(ae1, ae2);
- }
- else if (IsHotEdge(ae2))
- {
- resultOp = AddOutPt(ae2, pt);
- SwapOutrecs(ae1, ae2);
- }
-
- // neither edge is 'hot'
- else
- {
- float e1Wc2, e2Wc2;
- switch (this.fillRule)
- {
- case FillRule.Positive:
- e1Wc2 = ae1.WindCount2;
- e2Wc2 = ae2.WindCount2;
- break;
- case FillRule.Negative:
- e1Wc2 = -ae1.WindCount2;
- e2Wc2 = -ae2.WindCount2;
- break;
- default:
- e1Wc2 = Math.Abs(ae1.WindCount2);
- e2Wc2 = Math.Abs(ae2.WindCount2);
- break;
- }
-
- if (!IsSamePolyType(ae1, ae2))
- {
- resultOp = this.AddLocalMinPoly(ae1, ae2, pt);
- }
- else if (oldE1WindCount == 1 && oldE2WindCount == 1)
- {
- resultOp = null;
- switch (this.clipType)
- {
- case ClippingOperation.Union:
- if (e1Wc2 > 0 && e2Wc2 > 0)
- {
- return null;
- }
-
- resultOp = this.AddLocalMinPoly(ae1, ae2, pt);
- break;
-
- case ClippingOperation.Difference:
- if (((GetPolyType(ae1) == ClippingType.Clip) && (e1Wc2 > 0) && (e2Wc2 > 0))
- || ((GetPolyType(ae1) == ClippingType.Subject) && (e1Wc2 <= 0) && (e2Wc2 <= 0)))
- {
- resultOp = this.AddLocalMinPoly(ae1, ae2, pt);
- }
-
- break;
-
- case ClippingOperation.Xor:
- resultOp = this.AddLocalMinPoly(ae1, ae2, pt);
- break;
-
- default: // ClipType.Intersection:
- if (e1Wc2 <= 0 || e2Wc2 <= 0)
- {
- return null;
- }
-
- resultOp = this.AddLocalMinPoly(ae1, ae2, pt);
- break;
- }
- }
- }
-
- return resultOp;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void DeleteFromAEL(Active ae)
- {
- Active prev = ae.PrevInAEL;
- Active next = ae.NextInAEL;
- if (prev == null && next == null && (ae != this.actives))
- {
- return; // already deleted
- }
-
- if (prev != null)
- {
- prev.NextInAEL = next;
- }
- else
- {
- this.actives = next;
- }
-
- if (next != null)
- {
- next.PrevInAEL = prev;
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void AdjustCurrXAndCopyToSEL(float topY)
- {
- Active ae = this.actives;
- this.flaggedHorizontal = ae;
- while (ae != null)
- {
- ae.PrevInSEL = ae.PrevInAEL;
- ae.NextInSEL = ae.NextInAEL;
- ae.Jump = ae.NextInSEL;
- if (ae.JoinWith == JoinWith.Left)
- {
- ae.CurX = ae.PrevInAEL.CurX; // this also avoids complications
- }
- else
- {
- ae.CurX = TopX(ae, topY);
- }
-
- // NB don't update ae.curr.Y yet (see AddNewIntersectNode)
- ae = ae.NextInAEL;
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private bool HasLocMinAtY(float y)
- => this.currentLocMin < this.minimaList.Count && this.minimaList[this.currentLocMin].Vertex.Point.Y == y;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private LocalMinima PopLocalMinima()
- => this.minimaList[this.currentLocMin++];
-
- private void AddPathsToVertexList(PathsF paths, ClippingType polytype, bool isOpen)
- {
- int totalVertCnt = 0;
- for (int i = 0; i < paths.Count; i++)
- {
- PathF path = paths[i];
- totalVertCnt += path.Count;
- }
-
- this.vertexList.EnsureCapacity(this.vertexList.Count + totalVertCnt);
-
- foreach (PathF path in paths)
- {
- Vertex v0 = null, prev_v = null, curr_v;
- foreach (Vector2 pt in path)
- {
- if (v0 == null)
- {
- v0 = new Vertex(pt, VertexFlags.None, null);
- this.vertexList.Add(v0);
- prev_v = v0;
- }
- else if (prev_v.Point != pt)
- {
- // ie skips duplicates
- curr_v = new Vertex(pt, VertexFlags.None, prev_v);
- this.vertexList.Add(curr_v);
- prev_v.Next = curr_v;
- prev_v = curr_v;
- }
- }
-
- if (prev_v == null || prev_v.Prev == null)
- {
- continue;
- }
-
- if (!isOpen && prev_v.Point == v0.Point)
- {
- prev_v = prev_v.Prev;
- }
-
- prev_v.Next = v0;
- v0.Prev = prev_v;
- if (!isOpen && prev_v.Next == prev_v)
- {
- continue;
- }
-
- // OK, we have a valid path
- bool going_up, going_up0;
- if (isOpen)
- {
- curr_v = v0.Next;
- while (curr_v != v0 && curr_v.Point.Y == v0.Point.Y)
- {
- curr_v = curr_v.Next;
- }
-
- going_up = curr_v.Point.Y <= v0.Point.Y;
- if (going_up)
- {
- v0.Flags = VertexFlags.OpenStart;
- this.AddLocMin(v0, polytype, true);
- }
- else
- {
- v0.Flags = VertexFlags.OpenStart | VertexFlags.LocalMax;
- }
- }
- else
- {
- // closed path
- prev_v = v0.Prev;
- while (prev_v != v0 && prev_v.Point.Y == v0.Point.Y)
- {
- prev_v = prev_v.Prev;
- }
-
- if (prev_v == v0)
- {
- continue; // only open paths can be completely flat
- }
-
- going_up = prev_v.Point.Y > v0.Point.Y;
- }
-
- going_up0 = going_up;
- prev_v = v0;
- curr_v = v0.Next;
- while (curr_v != v0)
- {
- if (curr_v.Point.Y > prev_v.Point.Y && going_up)
- {
- prev_v.Flags |= VertexFlags.LocalMax;
- going_up = false;
- }
- else if (curr_v.Point.Y < prev_v.Point.Y && !going_up)
- {
- going_up = true;
- this.AddLocMin(prev_v, polytype, isOpen);
- }
-
- prev_v = curr_v;
- curr_v = curr_v.Next;
- }
-
- if (isOpen)
- {
- prev_v.Flags |= VertexFlags.OpenEnd;
- if (going_up)
- {
- prev_v.Flags |= VertexFlags.LocalMax;
- }
- else
- {
- this.AddLocMin(prev_v, polytype, isOpen);
- }
- }
- else if (going_up != going_up0)
- {
- if (going_up0)
- {
- this.AddLocMin(prev_v, polytype, false);
- }
- else
- {
- prev_v.Flags |= VertexFlags.LocalMax;
- }
- }
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void AddLocMin(Vertex vert, ClippingType polytype, bool isOpen)
- {
- // make sure the vertex is added only once.
- if ((vert.Flags & VertexFlags.LocalMin) != VertexFlags.None)
- {
- return;
- }
-
- vert.Flags |= VertexFlags.LocalMin;
-
- LocalMinima lm = new(vert, polytype, isOpen);
- this.minimaList.Add(lm);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void PushHorz(Active ae)
- {
- ae.NextInSEL = this.flaggedHorizontal;
- this.flaggedHorizontal = ae;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private bool PopHorz(out Active ae)
- {
- ae = this.flaggedHorizontal;
- if (this.flaggedHorizontal == null)
- {
- return false;
- }
-
- this.flaggedHorizontal = this.flaggedHorizontal.NextInSEL;
- return true;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private OutPt AddLocalMinPoly(Active ae1, Active ae2, Vector2 pt, bool isNew = false)
- {
- OutRec outrec = this.NewOutRec();
- ae1.Outrec = outrec;
- ae2.Outrec = outrec;
-
- if (IsOpen(ae1))
- {
- outrec.Owner = null;
- outrec.IsOpen = true;
- if (ae1.WindDx > 0)
- {
- SetSides(outrec, ae1, ae2);
- }
- else
- {
- SetSides(outrec, ae2, ae1);
- }
- }
- else
- {
- outrec.IsOpen = false;
- Active prevHotEdge = GetPrevHotEdge(ae1);
-
- // e.windDx is the winding direction of the **input** paths
- // and unrelated to the winding direction of output polygons.
- // Output orientation is determined by e.outrec.frontE which is
- // the ascending edge (see AddLocalMinPoly).
- if (prevHotEdge != null)
- {
- outrec.Owner = prevHotEdge.Outrec;
- if (OutrecIsAscending(prevHotEdge) == isNew)
- {
- SetSides(outrec, ae2, ae1);
- }
- else
- {
- SetSides(outrec, ae1, ae2);
- }
- }
- else
- {
- outrec.Owner = null;
- if (isNew)
- {
- SetSides(outrec, ae1, ae2);
- }
- else
- {
- SetSides(outrec, ae2, ae1);
- }
- }
- }
-
- OutPt op = new(pt, outrec);
- outrec.Pts = op;
- return op;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void SetDx(Active ae)
- => ae.Dx = GetDx(ae.Bot, ae.Top);
-
- /*******************************************************************************
- * Dx: 0(90deg) *
- * | *
- * +inf (180deg) <--- o --. -inf (0deg) *
- *******************************************************************************/
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static float GetDx(Vector2 pt1, Vector2 pt2)
- {
- float dy = pt2.Y - pt1.Y;
- if (dy != 0)
- {
- return (pt2.X - pt1.X) / dy;
- }
-
- if (pt2.X > pt1.X)
- {
- return float.NegativeInfinity;
- }
-
- return float.PositiveInfinity;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static float TopX(Active ae, float currentY)
- {
- Vector2 top = ae.Top;
- Vector2 bottom = ae.Bot;
-
- if ((currentY == top.Y) || (top.X == bottom.X))
- {
- return top.X;
- }
-
- if (currentY == bottom.Y)
- {
- return bottom.X;
- }
-
- return bottom.X + (ae.Dx * (currentY - bottom.Y));
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsHorizontal(Active ae)
- => ae.Top.Y == ae.Bot.Y;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsHeadingRightHorz(Active ae)
- => float.IsNegativeInfinity(ae.Dx);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsHeadingLeftHorz(Active ae)
- => float.IsPositiveInfinity(ae.Dx);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void SwapActives(ref Active ae1, ref Active ae2)
- => (ae2, ae1) = (ae1, ae2);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static ClippingType GetPolyType(Active ae)
- => ae.LocalMin.Polytype;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsSamePolyType(Active ae1, Active ae2)
- => ae1.LocalMin.Polytype == ae2.LocalMin.Polytype;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private bool IsContributingClosed(Active ae)
- {
- switch (this.fillRule)
- {
- case FillRule.Positive:
- if (ae.WindCount != 1)
- {
- return false;
- }
-
- break;
- case FillRule.Negative:
- if (ae.WindCount != -1)
- {
- return false;
- }
-
- break;
- case FillRule.NonZero:
- if (Math.Abs(ae.WindCount) != 1)
- {
- return false;
- }
-
- break;
- }
-
- switch (this.clipType)
- {
- case ClippingOperation.Intersection:
- return this.fillRule switch
- {
- FillRule.Positive => ae.WindCount2 > 0,
- FillRule.Negative => ae.WindCount2 < 0,
- _ => ae.WindCount2 != 0,
- };
-
- case ClippingOperation.Union:
- return this.fillRule switch
- {
- FillRule.Positive => ae.WindCount2 <= 0,
- FillRule.Negative => ae.WindCount2 >= 0,
- _ => ae.WindCount2 == 0,
- };
-
- case ClippingOperation.Difference:
- bool result = this.fillRule switch
- {
- FillRule.Positive => ae.WindCount2 <= 0,
- FillRule.Negative => ae.WindCount2 >= 0,
- _ => ae.WindCount2 == 0,
- };
- return (GetPolyType(ae) == ClippingType.Subject) ? result : !result;
-
- case ClippingOperation.Xor:
- return true; // XOr is always contributing unless open
-
- default:
- return false;
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private bool IsContributingOpen(Active ae)
- {
- bool isInClip, isInSubj;
- switch (this.fillRule)
- {
- case FillRule.Positive:
- isInSubj = ae.WindCount > 0;
- isInClip = ae.WindCount2 > 0;
- break;
- case FillRule.Negative:
- isInSubj = ae.WindCount < 0;
- isInClip = ae.WindCount2 < 0;
- break;
- default:
- isInSubj = ae.WindCount != 0;
- isInClip = ae.WindCount2 != 0;
- break;
- }
-
- bool result = this.clipType switch
- {
- ClippingOperation.Intersection => isInClip,
- ClippingOperation.Union => !isInSubj && !isInClip,
- _ => !isInClip
- };
- return result;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void SetWindCountForClosedPathEdge(Active ae)
- {
- // Wind counts refer to polygon regions not edges, so here an edge's WindCnt
- // indicates the higher of the wind counts for the two regions touching the
- // edge. (nb: Adjacent regions can only ever have their wind counts differ by
- // one. Also, open paths have no meaningful wind directions or counts.)
- Active ae2 = ae.PrevInAEL;
-
- // find the nearest closed path edge of the same PolyType in AEL (heading left)
- ClippingType pt = GetPolyType(ae);
- while (ae2 != null && (GetPolyType(ae2) != pt || IsOpen(ae2)))
- {
- ae2 = ae2.PrevInAEL;
- }
-
- if (ae2 == null)
- {
- ae.WindCount = ae.WindDx;
- ae2 = this.actives;
- }
- else if (this.fillRule == FillRule.EvenOdd)
- {
- ae.WindCount = ae.WindDx;
- ae.WindCount2 = ae2.WindCount2;
- ae2 = ae2.NextInAEL;
- }
- else
- {
- // NonZero, positive, or negative filling here ...
- // when e2's WindCnt is in the SAME direction as its WindDx,
- // then polygon will fill on the right of 'e2' (and 'e' will be inside)
- // nb: neither e2.WindCnt nor e2.WindDx should ever be 0.
- if (ae2.WindCount * ae2.WindDx < 0)
- {
- // opposite directions so 'ae' is outside 'ae2' ...
- if (Math.Abs(ae2.WindCount) > 1)
- {
- // outside prev poly but still inside another.
- if (ae2.WindDx * ae.WindDx < 0)
- {
- // reversing direction so use the same WC
- ae.WindCount = ae2.WindCount;
- }
- else
- {
- // otherwise keep 'reducing' the WC by 1 (i.e. towards 0) ...
- ae.WindCount = ae2.WindCount + ae.WindDx;
- }
- }
- else
- {
- // now outside all polys of same polytype so set own WC ...
- ae.WindCount = IsOpen(ae) ? 1 : ae.WindDx;
- }
- }
- else
- {
- // 'ae' must be inside 'ae2'
- if (ae2.WindDx * ae.WindDx < 0)
- {
- // reversing direction so use the same WC
- ae.WindCount = ae2.WindCount;
- }
- else
- {
- // otherwise keep 'increasing' the WC by 1 (i.e. away from 0) ...
- ae.WindCount = ae2.WindCount + ae.WindDx;
- }
- }
-
- ae.WindCount2 = ae2.WindCount2;
- ae2 = ae2.NextInAEL; // i.e. get ready to calc WindCnt2
- }
-
- // update windCount2 ...
- if (this.fillRule == FillRule.EvenOdd)
- {
- while (ae2 != ae)
- {
- if (GetPolyType(ae2!) != pt && !IsOpen(ae2!))
- {
- ae.WindCount2 = ae.WindCount2 == 0 ? 1 : 0;
- }
-
- ae2 = ae2.NextInAEL;
- }
- }
- else
- {
- while (ae2 != ae)
- {
- if (GetPolyType(ae2!) != pt && !IsOpen(ae2!))
- {
- ae.WindCount2 += ae2.WindDx;
- }
-
- ae2 = ae2.NextInAEL;
- }
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void SetWindCountForOpenPathEdge(Active ae)
- {
- Active ae2 = this.actives;
- if (this.fillRule == FillRule.EvenOdd)
- {
- int cnt1 = 0, cnt2 = 0;
- while (ae2 != ae)
- {
- if (GetPolyType(ae2!) == ClippingType.Clip)
- {
- cnt2++;
- }
- else if (!IsOpen(ae2!))
- {
- cnt1++;
- }
-
- ae2 = ae2.NextInAEL;
- }
-
- ae.WindCount = IsOdd(cnt1) ? 1 : 0;
- ae.WindCount2 = IsOdd(cnt2) ? 1 : 0;
- }
- else
- {
- while (ae2 != ae)
- {
- if (GetPolyType(ae2!) == ClippingType.Clip)
- {
- ae.WindCount2 += ae2.WindDx;
- }
- else if (!IsOpen(ae2!))
- {
- ae.WindCount += ae2.WindDx;
- }
-
- ae2 = ae2.NextInAEL;
- }
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsValidAelOrder(Active resident, Active newcomer)
- {
- if (newcomer.CurX != resident.CurX)
- {
- return newcomer.CurX > resident.CurX;
- }
-
- // get the turning direction a1.top, a2.bot, a2.top
- float d = ClipperUtils.CrossProduct(resident.Top, newcomer.Bot, newcomer.Top);
- if (d != 0)
- {
- return d < 0;
- }
-
- // edges must be collinear to get here
-
- // for starting open paths, place them according to
- // 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;
- }
-
- if (!IsMaxima(newcomer) && (newcomer.Top.Y > resident.Top.Y))
- {
- return ClipperUtils.CrossProduct(newcomer.Bot, newcomer.Top, NextVertex(newcomer).Point) >= 0;
- }
-
- float y = newcomer.Bot.Y;
- bool newcomerIsLeft = newcomer.IsLeftBound;
-
- if (resident.Bot.Y != y || resident.LocalMin.Vertex.Point.Y != y)
- {
- return newcomer.IsLeftBound;
- }
-
- // resident must also have just been inserted
- if (resident.IsLeftBound != newcomerIsLeft)
- {
- return newcomerIsLeft;
- }
-
- if (ClipperUtils.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;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void InsertLeftEdge(Active ae)
- {
- Active ae2;
-
- if (this.actives == null)
- {
- ae.PrevInAEL = null;
- ae.NextInAEL = null;
- this.actives = ae;
- }
- else if (!IsValidAelOrder(this.actives, ae))
- {
- ae.PrevInAEL = null;
- ae.NextInAEL = this.actives;
- this.actives.PrevInAEL = ae;
- this.actives = ae;
- }
- else
- {
- ae2 = this.actives;
- while (ae2.NextInAEL != null && IsValidAelOrder(ae2.NextInAEL, ae))
- {
- ae2 = ae2.NextInAEL;
- }
-
- // don't separate joined edges
- if (ae2.JoinWith == JoinWith.Right)
- {
- ae2 = ae2.NextInAEL;
- }
-
- ae.NextInAEL = ae2.NextInAEL;
- if (ae2.NextInAEL != null)
- {
- ae2.NextInAEL.PrevInAEL = ae;
- }
-
- ae.PrevInAEL = ae2;
- ae2.NextInAEL = ae;
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void InsertRightEdge(Active ae, Active ae2)
- {
- ae2.NextInAEL = ae.NextInAEL;
- if (ae.NextInAEL != null)
- {
- ae.NextInAEL.PrevInAEL = ae2;
- }
-
- ae2.PrevInAEL = ae;
- ae.NextInAEL = ae2;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Vertex NextVertex(Active ae)
- {
- if (ae.WindDx > 0)
- {
- return ae.VertexTop.Next;
- }
-
- return ae.VertexTop.Prev;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Vertex PrevPrevVertex(Active ae)
- {
- if (ae.WindDx > 0)
- {
- return ae.VertexTop.Prev.Prev;
- }
-
- return ae.VertexTop.Next.Next;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsMaxima(Vertex vertex)
- => (vertex.Flags & VertexFlags.LocalMax) != VertexFlags.None;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsMaxima(Active ae)
- => IsMaxima(ae.VertexTop);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Active GetMaximaPair(Active ae)
- {
- Active ae2;
- ae2 = ae.NextInAEL;
- while (ae2 != null)
- {
- if (ae2.VertexTop == ae.VertexTop)
- {
- return ae2; // Found!
- }
-
- ae2 = ae2.NextInAEL;
- }
-
- return null;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsOdd(int val)
- => (val & 1) != 0;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsHotEdge(Active ae)
- => ae.Outrec != null;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsOpen(Active ae)
- => ae.LocalMin.IsOpen;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsOpenEnd(Active ae)
- => ae.LocalMin.IsOpen && IsOpenEnd(ae.VertexTop);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsOpenEnd(Vertex v)
- => (v.Flags & (VertexFlags.OpenStart | VertexFlags.OpenEnd)) != VertexFlags.None;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Active GetPrevHotEdge(Active ae)
- {
- Active prev = ae.PrevInAEL;
- while (prev != null && (IsOpen(prev) || !IsHotEdge(prev)))
- {
- prev = prev.PrevInAEL;
- }
-
- return prev;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void JoinOutrecPaths(Active ae1, Active ae2)
- {
- // join ae2 outrec path onto ae1 outrec path and then delete ae2 outrec path
- // pointers. (NB Only very rarely do the joining ends share the same coords.)
- OutPt p1Start = ae1.Outrec.Pts;
- OutPt p2Start = ae2.Outrec.Pts;
- OutPt p1End = p1Start.Next;
- OutPt p2End = p2Start.Next;
- if (IsFront(ae1))
- {
- p2End.Prev = p1Start;
- p1Start.Next = p2End;
- p2Start.Next = p1End;
- p1End.Prev = p2Start;
- ae1.Outrec.Pts = p2Start;
-
- // nb: if IsOpen(e1) then e1 & e2 must be a 'maximaPair'
- ae1.Outrec.FrontEdge = ae2.Outrec.FrontEdge;
- if (ae1.Outrec.FrontEdge != null)
- {
- ae1.Outrec.FrontEdge.Outrec = ae1.Outrec;
- }
- }
- else
- {
- p1End.Prev = p2Start;
- p2Start.Next = p1End;
- p1Start.Next = p2End;
- p2End.Prev = p1Start;
-
- ae1.Outrec.BackEdge = ae2.Outrec.BackEdge;
- if (ae1.Outrec.BackEdge != null)
- {
- ae1.Outrec.BackEdge.Outrec = ae1.Outrec;
- }
- }
-
- // after joining, the ae2.OutRec must contains no vertices ...
- ae2.Outrec.FrontEdge = null;
- ae2.Outrec.BackEdge = null;
- ae2.Outrec.Pts = null;
- SetOwner(ae2.Outrec, ae1.Outrec);
-
- if (IsOpenEnd(ae1))
- {
- ae2.Outrec.Pts = ae1.Outrec.Pts;
- ae1.Outrec.Pts = null;
- }
-
- // and ae1 and ae2 are maxima and are about to be dropped from the Actives list.
- ae1.Outrec = null;
- ae2.Outrec = null;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static OutPt AddOutPt(Active ae, Vector2 pt)
- {
- // Outrec.OutPts: a circular doubly-linked-list of POutPt where ...
- // opFront[.Prev]* ~~~> opBack & opBack == opFront.Next
- OutRec outrec = ae.Outrec;
- bool toFront = IsFront(ae);
- OutPt opFront = outrec.Pts;
- OutPt opBack = opFront.Next;
-
- if (toFront && (pt == opFront.Point))
- {
- return opFront;
- }
- else if (!toFront && (pt == opBack.Point))
- {
- return opBack;
- }
-
- OutPt newOp = new(pt, outrec);
- opBack.Prev = newOp;
- newOp.Prev = opFront;
- newOp.Next = opBack;
- opFront.Next = newOp;
- if (toFront)
- {
- outrec.Pts = newOp;
- }
-
- return newOp;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private OutRec NewOutRec()
- {
- OutRec result = new()
- {
- Idx = this.outrecList.Count
- };
- this.outrecList.Add(result);
- return result;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private OutPt StartOpenPath(Active ae, Vector2 pt)
- {
- OutRec outrec = this.NewOutRec();
- outrec.IsOpen = true;
- if (ae.WindDx > 0)
- {
- outrec.FrontEdge = ae;
- outrec.BackEdge = null;
- }
- else
- {
- outrec.FrontEdge = null;
- outrec.BackEdge = ae;
- }
-
- ae.Outrec = outrec;
- OutPt op = new(pt, outrec);
- outrec.Pts = op;
- return op;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void UpdateEdgeIntoAEL(Active ae)
- {
- ae.Bot = ae.Top;
- ae.VertexTop = NextVertex(ae);
- ae.Top = ae.VertexTop.Point;
- ae.CurX = ae.Bot.X;
- SetDx(ae);
-
- if (IsJoined(ae))
- {
- this.Split(ae, ae.Bot);
- }
-
- if (IsHorizontal(ae))
- {
- return;
- }
-
- this.InsertScanline(ae.Top.Y);
-
- this.CheckJoinLeft(ae, ae.Bot);
- this.CheckJoinRight(ae, ae.Bot, true);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void SetSides(OutRec outrec, Active startEdge, Active endEdge)
- {
- outrec.FrontEdge = startEdge;
- outrec.BackEdge = endEdge;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void SwapOutrecs(Active ae1, Active ae2)
- {
- OutRec or1 = ae1.Outrec; // at least one edge has
- OutRec or2 = ae2.Outrec; // an assigned outrec
- if (or1 == or2)
- {
- (or1.BackEdge, or1.FrontEdge) = (or1.FrontEdge, or1.BackEdge);
- return;
- }
-
- if (or1 != null)
- {
- if (ae1 == or1.FrontEdge)
- {
- or1.FrontEdge = ae2;
- }
- else
- {
- or1.BackEdge = ae2;
- }
- }
-
- if (or2 != null)
- {
- if (ae2 == or2.FrontEdge)
- {
- or2.FrontEdge = ae1;
- }
- else
- {
- or2.BackEdge = ae1;
- }
- }
-
- ae1.Outrec = or2;
- ae2.Outrec = or1;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void SetOwner(OutRec outrec, OutRec newOwner)
- {
- // precondition1: new_owner is never null
- while (newOwner.Owner != null && newOwner.Owner.Pts == null)
- {
- newOwner.Owner = newOwner.Owner.Owner;
- }
-
- // make sure that outrec isn't an owner of newOwner
- OutRec tmp = newOwner;
- while (tmp != null && tmp != outrec)
- {
- tmp = tmp.Owner;
- }
-
- if (tmp != null)
- {
- newOwner.Owner = outrec.Owner;
- }
-
- outrec.Owner = newOwner;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static float Area(OutPt op)
- {
- // https://en.wikipedia.org/wiki/Shoelace_formula
- float area = 0;
- OutPt op2 = op;
- do
- {
- area += (op2.Prev.Point.Y + op2.Point.Y) * (op2.Prev.Point.X - op2.Point.X);
- op2 = op2.Next;
- }
- while (op2 != op);
- return area * .5F;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static float AreaTriangle(Vector2 pt1, Vector2 pt2, Vector2 pt3)
- => ((pt3.Y + pt1.Y) * (pt3.X - pt1.X))
- + ((pt1.Y + pt2.Y) * (pt1.X - pt2.X))
- + ((pt2.Y + pt3.Y) * (pt2.X - pt3.X));
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static OutRec GetRealOutRec(OutRec outRec)
- {
- while ((outRec != null) && (outRec.Pts == null))
- {
- outRec = outRec.Owner;
- }
-
- return outRec;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void UncoupleOutRec(Active ae)
- {
- OutRec outrec = ae.Outrec;
- if (outrec == null)
- {
- return;
- }
-
- outrec.FrontEdge.Outrec = null;
- outrec.BackEdge.Outrec = null;
- outrec.FrontEdge = null;
- outrec.BackEdge = null;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool OutrecIsAscending(Active hotEdge)
- => hotEdge == hotEdge.Outrec.FrontEdge;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void SwapFrontBackSides(OutRec outrec)
- {
- // while this proc. is needed for open paths
- // it's almost never needed for closed paths
- (outrec.BackEdge, outrec.FrontEdge) = (outrec.FrontEdge, outrec.BackEdge);
- outrec.Pts = outrec.Pts.Next;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool EdgesAdjacentInAEL(IntersectNode inode)
- => (inode.Edge1.NextInAEL == inode.Edge2) || (inode.Edge1.PrevInAEL == inode.Edge2);
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void CheckJoinLeft(Active e, Vector2 pt, bool checkCurrX = false)
- {
- Active prev = e.PrevInAEL;
- if (prev == null
- || IsOpen(e)
- || IsOpen(prev)
- || !IsHotEdge(e)
- || !IsHotEdge(prev))
- {
- return;
- }
-
- // Avoid trivial joins
- if ((pt.Y < e.Top.Y + 2 || pt.Y < prev.Top.Y + 2)
- && ((e.Bot.Y > pt.Y) || (prev.Bot.Y > pt.Y)))
- {
- return;
- }
-
- if (checkCurrX)
- {
- if (ClipperUtils.PerpendicDistFromLineSqrd(pt, prev.Bot, prev.Top) > 0.25)
- {
- return;
- }
- }
- else if (e.CurX != prev.CurX)
- {
- return;
- }
-
- if (ClipperUtils.CrossProduct(e.Top, pt, prev.Top) != 0)
- {
- return;
- }
-
- if (e.Outrec.Idx == prev.Outrec.Idx)
- {
- this.AddLocalMaxPoly(prev, e, pt);
- }
- else if (e.Outrec.Idx < prev.Outrec.Idx)
- {
- JoinOutrecPaths(e, prev);
- }
- else
- {
- JoinOutrecPaths(prev, e);
- }
-
- prev.JoinWith = JoinWith.Right;
- e.JoinWith = JoinWith.Left;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void CheckJoinRight(Active e, Vector2 pt, bool checkCurrX = false)
- {
- Active next = e.NextInAEL;
- if (IsOpen(e)
- || !IsHotEdge(e)
- || IsJoined(e)
- || next == null
- || IsOpen(next)
- || !IsHotEdge(next))
- {
- return;
- }
-
- // Avoid trivial joins
- if ((pt.Y < e.Top.Y + 2 || pt.Y < next.Top.Y + 2)
- && ((e.Bot.Y > pt.Y) || (next.Bot.Y > pt.Y)))
- {
- return;
- }
-
- if (checkCurrX)
- {
- if (ClipperUtils.PerpendicDistFromLineSqrd(pt, next.Bot, next.Top) > 0.25)
- {
- return;
- }
- }
- else if (e.CurX != next.CurX)
- {
- return;
- }
-
- if (ClipperUtils.CrossProduct(e.Top, pt, next.Top) != 0)
- {
- return;
- }
-
- if (e.Outrec.Idx == next.Outrec.Idx)
- {
- this.AddLocalMaxPoly(e, next, pt);
- }
- else if (e.Outrec.Idx < next.Outrec.Idx)
- {
- JoinOutrecPaths(e, next);
- }
- else
- {
- JoinOutrecPaths(next, e);
- }
-
- e.JoinWith = JoinWith.Right;
- next.JoinWith = JoinWith.Left;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void FixOutRecPts(OutRec outrec)
- {
- OutPt op = outrec.Pts;
- do
- {
- op.OutRec = outrec;
- op = op.Next;
- }
- while (op != outrec.Pts);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private OutPt AddLocalMaxPoly(Active ae1, Active ae2, Vector2 pt)
- {
- if (IsJoined(ae1))
- {
- this.Split(ae1, pt);
- }
-
- if (IsJoined(ae2))
- {
- this.Split(ae2, pt);
- }
-
- if (IsFront(ae1) == IsFront(ae2))
- {
- if (IsOpenEnd(ae1))
- {
- SwapFrontBackSides(ae1.Outrec!);
- }
- else if (IsOpenEnd(ae2))
- {
- SwapFrontBackSides(ae2.Outrec!);
- }
- else
- {
- return null;
- }
- }
-
- OutPt result = AddOutPt(ae1, pt);
- if (ae1.Outrec == ae2.Outrec)
- {
- OutRec outrec = ae1.Outrec;
- outrec.Pts = result;
- UncoupleOutRec(ae1);
- }
-
- // and to preserve the winding orientation of outrec ...
- else if (IsOpen(ae1))
- {
- if (ae1.WindDx < 0)
- {
- JoinOutrecPaths(ae1, ae2);
- }
- else
- {
- JoinOutrecPaths(ae2, ae1);
- }
- }
- else if (ae1.Outrec.Idx < ae2.Outrec.Idx)
- {
- JoinOutrecPaths(ae1, ae2);
- }
- else
- {
- JoinOutrecPaths(ae2, ae1);
- }
-
- return result;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsJoined(Active e)
- => e.JoinWith != JoinWith.None;
-
- private void Split(Active e, Vector2 currPt)
- {
- if (e.JoinWith == JoinWith.Right)
- {
- e.JoinWith = JoinWith.None;
- e.NextInAEL.JoinWith = JoinWith.None;
- this.AddLocalMinPoly(e, e.NextInAEL, currPt, true);
- }
- else
- {
- e.JoinWith = JoinWith.None;
- e.PrevInAEL.JoinWith = JoinWith.None;
- this.AddLocalMinPoly(e.PrevInAEL, e, currPt, true);
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsFront(Active ae)
- => ae == ae.Outrec.FrontEdge;
-
- private struct LocMinSorter : IComparer
- {
- public readonly int Compare(LocalMinima locMin1, LocalMinima locMin2)
- => locMin2.Vertex.Point.Y.CompareTo(locMin1.Vertex.Point.Y);
- }
-
- private readonly struct LocalMinima
- {
- public readonly Vertex Vertex;
- public readonly ClippingType Polytype;
- public readonly bool IsOpen;
-
- public LocalMinima(Vertex vertex, ClippingType polytype, bool isOpen = false)
- {
- this.Vertex = vertex;
- this.Polytype = polytype;
- this.IsOpen = isOpen;
- }
-
- public static bool operator ==(LocalMinima lm1, LocalMinima lm2)
-
- // TODO: Check this. Why ref equals.
- => ReferenceEquals(lm1.Vertex, lm2.Vertex);
-
- public static bool operator !=(LocalMinima lm1, LocalMinima lm2)
- => !(lm1 == lm2);
-
- public override bool Equals(object obj)
- => obj is LocalMinima minima && this == minima;
-
- 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.
- private readonly struct IntersectNode
- {
- public readonly Vector2 Point;
- public readonly Active Edge1;
- public readonly Active Edge2;
-
- public IntersectNode(Vector2 pt, Active edge1, Active edge2)
- {
- this.Point = pt;
- this.Edge1 = edge1;
- this.Edge2 = edge2;
- }
- }
-
- private struct HorzSegSorter : IComparer
- {
- public readonly int Compare(HorzSegment hs1, HorzSegment hs2)
- {
- if (hs1 == null || hs2 == null)
- {
- return 0;
- }
-
- if (hs1.RightOp == null)
- {
- return hs2.RightOp == null ? 0 : 1;
- }
- else if (hs2.RightOp == null)
- {
- return -1;
- }
- else
- {
- return hs1.LeftOp.Point.X.CompareTo(hs2.LeftOp.Point.X);
- }
- }
- }
-
- private struct IntersectListSort : IComparer
- {
- public readonly int Compare(IntersectNode a, IntersectNode b)
- {
- if (a.Point.Y == b.Point.Y)
- {
- if (a.Point.X == b.Point.X)
- {
- return 0;
- }
-
- return (a.Point.X < b.Point.X) ? -1 : 1;
- }
-
- return (a.Point.Y > b.Point.Y) ? -1 : 1;
- }
- }
-
- private class HorzSegment
- {
- public HorzSegment(OutPt op)
- {
- this.LeftOp = op;
- this.RightOp = null;
- this.LeftToRight = true;
- }
-
- public OutPt LeftOp { get; set; }
-
- public OutPt RightOp { get; set; }
-
- public bool LeftToRight { get; set; }
- }
-
- private class HorzJoin
- {
- public HorzJoin(OutPt ltor, OutPt rtol)
- {
- this.Op1 = ltor;
- this.Op2 = rtol;
- }
-
- public OutPt Op1 { get; }
-
- public OutPt Op2 { get; }
- }
-
- // OutPt: vertex data structure for clipping solutions
- private class OutPt
- {
- public OutPt(Vector2 pt, OutRec outrec)
- {
- this.Point = pt;
- this.OutRec = outrec;
- this.Next = this;
- this.Prev = this;
- this.HorizSegment = null;
- }
-
- public Vector2 Point { get; }
-
- public OutPt Next { get; set; }
-
- public OutPt Prev { get; set; }
-
- public OutRec OutRec { get; set; }
-
- public HorzSegment HorizSegment { get; set; }
- }
-
- // OutRec: path data structure for clipping solutions
- private class OutRec
- {
- public int Idx { get; set; }
-
- public OutRec Owner { get; set; }
-
- public Active FrontEdge { get; set; }
-
- public Active BackEdge { get; set; }
-
- public OutPt Pts { get; set; }
-
- public PolyPathF PolyPath { get; set; }
-
- public BoundsF Bounds { get; set; }
-
- public PathF Path { get; set; } = [];
-
- public bool IsOpen { get; set; }
-
- public List Splits { get; set; }
- }
-
- private class Vertex
- {
- public Vertex(Vector2 pt, VertexFlags flags, Vertex prev)
- {
- this.Point = pt;
- this.Flags = flags;
- this.Next = null;
- this.Prev = prev;
- }
-
- public Vector2 Point { get; }
-
- public Vertex Next { get; set; }
-
- public Vertex Prev { get; set; }
-
- public VertexFlags Flags { get; set; }
- }
-
- private class Active
- {
- public Vector2 Bot { get; set; }
-
- public Vector2 Top { get; set; }
-
- public float CurX { get; set; } // current (updated at every new scanline)
-
- public float Dx { get; set; }
-
- public int WindDx { get; set; } // 1 or -1 depending on winding direction
-
- public int WindCount { get; set; }
-
- public int WindCount2 { get; set; } // winding count of the opposite polytype
-
- 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).
- public Active PrevInAEL { get; set; }
-
- 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.
- public Active PrevInSEL { get; set; }
-
- public Active NextInSEL { get; set; }
-
- public Active Jump { get; set; }
-
- public Vertex VertexTop { get; set; }
-
- public LocalMinima LocalMin { get; set; } // the bottom of an edge 'bound' (also Vatti)
-
- public bool IsLeftBound { get; set; }
-
- public JoinWith JoinWith { get; set; }
- }
-}
-
-internal class PolyPathF : IEnumerable
-{
- private readonly PolyPathF parent;
- private readonly List items = [];
-
- public PolyPathF(PolyPathF parent = null)
- => this.parent = parent;
-
- public PathF Polygon { get; private set; } // polytree root's polygon == null
-
- public int Level => this.GetLevel();
-
- public bool IsHole => this.GetIsHole();
-
- public int Count => this.items.Count;
-
- public PolyPathF this[int index] => this.items[index];
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PolyPathF AddChild(PathF p)
- {
- PolyPathF child = new(this)
- {
- Polygon = p
- };
-
- this.items.Add(child);
- return child;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public float Area()
- {
- float result = this.Polygon == null ? 0 : ClipperUtils.Area(this.Polygon);
- for (int i = 0; i < this.items.Count; i++)
- {
- PolyPathF child = this.items[i];
- result += child.Area();
- }
-
- return result;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void Clear() => this.items.Clear();
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private bool GetIsHole()
- {
- int lvl = this.Level;
- return lvl != 0 && (lvl & 1) == 0;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private int GetLevel()
- {
- int result = 0;
- PolyPathF pp = this.parent;
- while (pp != null)
- {
- ++result;
- pp = pp.parent;
- }
-
- return result;
- }
-
- public IEnumerator GetEnumerator() => this.items.GetEnumerator();
-
- IEnumerator IEnumerable.GetEnumerator() => this.items.GetEnumerator();
-}
-
-internal class PolyTreeF : PolyPathF
-{
-}
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/PolygonClipper/VertexFlags.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexFlags.cs
deleted file mode 100644
index 2a990ecf..00000000
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexFlags.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
-
-[Flags]
-internal enum VertexFlags
-{
- None = 0,
- OpenStart = 1,
- OpenEnd = 2,
- LocalMax = 4,
- LocalMin = 8
-}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs
new file mode 100644
index 00000000..9a6ac206
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs
@@ -0,0 +1,117 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.PolygonClipper;
+using ClipperPolygon = SixLabors.PolygonClipper.Polygon;
+using PolygonClipperAction = SixLabors.PolygonClipper.PolygonClipper;
+
+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 ClipperPolygon? subject;
+ private ClipperPolygon? clip;
+ private readonly IntersectionRule rule;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The intersection rule.
+ public ClippedShapeGenerator(IntersectionRule rule) => this.rule = rule;
+
+ ///
+ /// Generates the final clipped shapes from the previously provided subject and clip paths.
+ ///
+ ///
+ /// The boolean operation to perform, such as ,
+ /// , or .
+ ///
+ ///
+ /// An array of instances representing the result of the boolean operation.
+ ///
+ public IPath[] GenerateClippedShapes(BooleanOperation operation)
+ {
+ ArgumentNullException.ThrowIfNull(this.subject);
+ ArgumentNullException.ThrowIfNull(this.clip);
+
+ PolygonClipperAction polygonClipper = new(this.subject, this.clip, operation);
+
+ ClipperPolygon result = polygonClipper.Run();
+
+ IPath[] shapes = new IPath[result.Count];
+
+ int index = 0;
+ for (int i = 0; i < result.Count; i++)
+ {
+ Contour contour = result[i];
+ PointF[] points = new PointF[contour.Count];
+
+ for (int j = 0; j < contour.Count; j++)
+ {
+ Vertex vertex = contour[j];
+ points[j] = new PointF((float)vertex.X, (float)vertex.Y);
+ }
+
+ 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));
+
+ // Accumulate all paths of the complex shape into a single polygon.
+ ClipperPolygon polygon = PolygonClipperFactory.FromPaths(paths, this.rule);
+
+ if (clippingType == ClippingType.Clip)
+ {
+ this.clip = polygon;
+ }
+ else
+ {
+ this.subject = polygon;
+ }
+ }
+
+ ///
+ /// 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));
+
+ ClipperPolygon polygon = PolygonClipperFactory.FromSimplePaths(path.Flatten(), this.rule);
+ if (clippingType == ClippingType.Clip)
+ {
+ this.clip = polygon;
+ }
+ else
+ {
+ this.subject = polygon;
+ }
+ }
+}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClippingType.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippingType.cs
similarity index 86%
rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/ClippingType.cs
rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippingType.cs
index 00aa96a4..f2e252f2 100644
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClippingType.cs
+++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippingType.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;
///
/// Defines the polygon clipping type.
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs
new file mode 100644
index 00000000..f904629e
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs
@@ -0,0 +1,384 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using SixLabors.PolygonClipper;
+using ClipperPolygon = SixLabors.PolygonClipper.Polygon;
+
+namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry;
+
+///
+/// Builders for from ImageSharp paths.
+/// PolygonClipper requires explicit orientation and nesting of contours ImageSharp polygons do not contain that information
+/// so we must derive that from the input.
+///
+internal static class PolygonClipperFactory
+{
+ ///
+ /// Creates a new polygon by combining multiple paths using the specified intersection rule.
+ ///
+ /// Use this method to construct complex polygons from multiple input paths, such as when
+ /// importing shapes from vector graphics or combining user-drawn segments. The resulting polygon's structure
+ /// depends on the order and geometry of the input paths as well as the chosen intersection rule.
+ ///
+ ///
+ /// A collection of paths that define the shapes to be combined into a single polygon. Each path is expected to
+ /// represent a simple or complex shape.
+ ///
+ /// Containment rule for nesting, or .
+ /// A representing the union of all input paths, combined according to the specified intersection rule.
+ public static ClipperPolygon FromPaths(IEnumerable paths, IntersectionRule rule)
+ {
+ // Accumulate all paths of the complex shape into a single polygon.
+ ClipperPolygon polygon = [];
+
+ foreach (IPath path in paths)
+ {
+ polygon = FromSimplePaths(path.Flatten(), rule, polygon);
+ }
+
+ return polygon;
+ }
+
+ ///
+ /// Builds a from closed rings.
+ ///
+ ///
+ /// Pipeline:
+ /// 1) Filter to closed paths with ≥3 unique points, copy to rings.
+ /// 2) Compute signed area via the shoelace formula to get orientation and magnitude.
+ /// 3) For each ring, pick its lexicographic bottom-left vertex.
+ /// 4) Parent assignment: for ring i, shoot a conceptual vertical ray downward from its bottom-left point
+ /// and test containment against all other rings using the selected .
+ /// The parent is the smallest-area ring that contains the point.
+ /// 5) Depth is the number of ancestors by repeated parent lookup.
+ /// 6) Materialize s, enforce even depth CCW and odd depth CW,
+ /// set and , add to and wire holes.
+ /// Notes:
+ /// - Step 4 mirrors the parent-detection approach formalized in Martínez–Rueda 2013.
+ /// - Containment uses Even-Odd or Non-Zero consistently, so glyph-like inputs can use Non-Zero.
+ /// - Boundary handling: points exactly on edges are not special-cased here, which is typical for nesting.
+ ///
+ /// Closed simple paths.
+ /// Containment rule for nesting, or .
+ /// Optional existing polygon to populate.
+ /// The constructed .
+ public static ClipperPolygon FromSimplePaths(IEnumerable paths, IntersectionRule rule, ClipperPolygon? polygon = null)
+ {
+ // Gather rings as Vertex lists (explicitly closed), plus per-ring metadata.
+ List> rings = [];
+ List areas = [];
+ List bottomLeft = [];
+
+ foreach (ISimplePath p in paths)
+ {
+ if (!p.IsClosed)
+ {
+ // TODO: could append first point to close, but that fabricates geometry.
+ continue;
+ }
+
+ ReadOnlySpan s = p.Points.Span;
+ int n = s.Length;
+
+ // Need at least 3 points to form area.
+ if (n < 3)
+ {
+ continue;
+ }
+
+ // Copy all points as-is.
+ List ring = new(n);
+ for (int i = 0; i < n; i++)
+ {
+ ring.Add(new Vertex(s[i].X, s[i].Y));
+ }
+
+ // Ensure explicit closure: start == end.
+ if (ring.Count > 0)
+ {
+ Vertex first = ring[0];
+ Vertex last = ring[^1];
+ if (first.X != last.X || first.Y != last.Y)
+ {
+ ring.Add(first);
+ }
+ }
+
+ // After closure, still require at least 3 unique vertices.
+ if (ring.Count < 4) // 3 unique + repeated first == last
+ {
+ continue;
+ }
+
+ rings.Add(ring);
+
+ // SignedArea must handle a closed ring (last == first).
+ areas.Add(SignedArea(ring));
+
+ // Choose lexicographic bottom-left vertex index for nesting test.
+ bottomLeft.Add(IndexOfBottomLeft(ring));
+ }
+
+ int m = rings.Count;
+ if (m == 0)
+ {
+ return [];
+ }
+
+ // Parent assignment: pick the smallest-area ring that contains the bottom-left vertex.
+ // TODO: We can use pooling here if we care about large numbers of rings.
+ int[] parent = new int[m];
+ Array.Fill(parent, -1);
+
+ for (int i = 0; i < m; i++)
+ {
+ Vertex q = rings[i][bottomLeft[i]];
+ int best = -1;
+ double bestArea = double.MaxValue;
+
+ for (int j = 0; j < m; j++)
+ {
+ if (i == j)
+ {
+ continue;
+ }
+
+ if (IsPointInPolygon(q, rings[j], rule))
+ {
+ double a = Math.Abs(areas[j]);
+ if (a < bestArea)
+ {
+ bestArea = a;
+ best = j;
+ }
+ }
+ }
+
+ parent[i] = best;
+ }
+
+ // Depth = number of ancestors by following Parent links.
+ // TODO: We can pool this if we care about large numbers of rings.
+ int[] depth = new int[m];
+ for (int i = 0; i < m; i++)
+ {
+ int d = 0;
+ for (int pIdx = parent[i]; pIdx >= 0; pIdx = parent[pIdx])
+ {
+ d++;
+ }
+
+ depth[i] = d;
+ }
+
+ // Emit contours, enforce orientation by depth, and wire into polygon.
+ polygon ??= [];
+ for (int i = 0; i < m; i++)
+ {
+ Contour c = new();
+
+ // Stream vertices into the contour. Ring is already explicitly closed.
+ foreach (Vertex v in rings[i])
+ {
+ c.AddVertex(v);
+ }
+
+ // Orientation convention: even depth = outer => CCW, odd depth = hole => CW.
+ if ((depth[i] & 1) == 0)
+ {
+ c.SetCounterClockwise();
+ }
+ else
+ {
+ c.SetClockwise();
+ }
+
+ // Topology annotations.
+ c.ParentIndex = parent[i] >= 0 ? parent[i] : null;
+ c.Depth = depth[i];
+
+ polygon.Add(c);
+ }
+
+ // Record hole indices for parents now that indices are stable.
+ for (int i = 0; i < m; i++)
+ {
+ int pIdx = parent[i];
+ if (pIdx >= 0)
+ {
+ polygon[pIdx].AddHoleIndex(i);
+ }
+ }
+
+ return polygon;
+ }
+
+ ///
+ /// Computes the signed area of a closed ring using the shoelace formula.
+ ///
+ /// Ring of vertices.
+ ///
+ /// Formula:
+ ///
+ /// A = 0.5 * Σ cross(v[j], v[i]) with j = (i - 1) mod n
+ ///
+ /// where cross(a,b) = a.X * b.Y - a.Y * b.X.
+ /// Interpretation:
+ /// - A > 0 means counter-clockwise orientation.
+ /// - A < 0 means clockwise orientation.
+ ///
+ private static double SignedArea(List r)
+ {
+ double area = 0d;
+
+ for (int i = 0, j = r.Count - 1; i < r.Count; j = i, i++)
+ {
+ area += Vertex.Cross(r[j], r[i]);
+ }
+
+ return 0.5d * area;
+ }
+
+ ///
+ /// Returns the index of the lexicographically bottom-left vertex.
+ ///
+ /// Ring of vertices.
+ ///
+ /// Lexicographic order (X then Y) yields a unique seed for nesting tests and matches
+ /// common parent-detection proofs that cast a ray from the lowest-leftmost point.
+ ///
+ private static int IndexOfBottomLeft(List r)
+ {
+ int k = 0;
+
+ for (int i = 1; i < r.Count; i++)
+ {
+ Vertex a = r[i];
+ Vertex b = r[k];
+
+ if (a.X < b.X || (a.X == b.X && a.Y < b.Y))
+ {
+ k = i;
+ }
+ }
+
+ return k;
+ }
+
+ ///
+ /// Dispatches to the selected point-in-polygon implementation.
+ ///
+ /// Query point.
+ /// Closed ring.
+ /// Fill rule.
+ private static bool IsPointInPolygon(in Vertex p, List ring, IntersectionRule rule)
+ {
+ if (rule == IntersectionRule.EvenOdd)
+ {
+ return PointInPolygonEvenOdd(p, ring);
+ }
+
+ return PointInPolygonNonZero(p, ring);
+ }
+
+ ///
+ /// Even-odd point-in-polygon via ray casting.
+ ///
+ /// Query point.
+ /// Closed ring.
+ ///
+ /// Let a horizontal ray start at and extend to +∞ in X.
+ /// For each edge (a→b), count an intersection if the edge straddles the ray’s Y
+ /// and the ray’s X is strictly less than the edge’s X at that Y:
+ ///
+ /// intersects = ((b.Y > p.Y) != (a.Y > p.Y)) amp;& p.X < x_at_pY(a,b)
+ ///
+ /// Parity of the count determines interior.
+ /// Horizontal edges contribute zero because the straddle test excludes equal Y.
+ /// Using a half-open interval on Y prevents double-counting shared vertices.
+ ///
+ private static bool PointInPolygonEvenOdd(in Vertex p, List ring)
+ {
+ bool inside = false;
+ int n = ring.Count;
+ int j = n - 1;
+
+ for (int i = 0; i < n; j = i, i++)
+ {
+ Vertex a = ring[j];
+ Vertex b = ring[i];
+
+ bool straddles = (b.Y > p.Y) != (a.Y > p.Y);
+
+ if (straddles)
+ {
+ double ySpan = a.Y - b.Y;
+ double xAtPY = (((a.X - b.X) * (p.Y - b.Y)) / (ySpan == 0d ? double.Epsilon : ySpan)) + b.X;
+
+ if (p.X < xAtPY)
+ {
+ inside = !inside;
+ }
+ }
+ }
+
+ return inside;
+ }
+
+ ///
+ /// Non-zero winding point-in-polygon.
+ ///
+ /// Query point.
+ /// Closed ring.
+ ///
+ /// Scan all edges (a→b).
+ /// - If the edge crosses the scanline upward (a.Y ≤ p.Y && b.Y > p.Y) and
+ /// lies strictly to the left of the edge, increment the winding.
+ /// - If it crosses downward (a.Y > p.Y && b.Y ≤ p.Y) and
+ /// lies strictly to the right, decrement the winding.
+ /// The point is inside iff the winding number is non-zero.
+ /// Left/right is decided by the sign of the cross product of vectors a→b and a→p.
+ ///
+ private static bool PointInPolygonNonZero(in Vertex p, List ring)
+ {
+ int winding = 0;
+ int n = ring.Count;
+
+ for (int i = 0, j = n - 1; i < n; j = i, i++)
+ {
+ Vertex a = ring[j];
+ Vertex b = ring[i];
+
+ if (a.Y <= p.Y)
+ {
+ if (b.Y > p.Y && IsLeft(a, b, p))
+ {
+ winding++;
+ }
+ }
+ else if (b.Y <= p.Y && !IsLeft(a, b, p))
+ {
+ winding--;
+ }
+ }
+
+ return winding != 0;
+ }
+
+ ///
+ /// Returns true if is strictly left of the directed edge a→b.
+ ///
+ /// Edge start.
+ /// Edge end.
+ /// Query point.
+ ///
+ /// Tests the sign of the 2D cross product:
+ ///
+ /// cross = (b - a) × (p - a) = (b.X - a.X)*(p.Y - a.Y) - (b.Y - a.Y)*(p.X - a.X)
+ ///
+ /// Left if cross > 0, right if cross < 0, collinear if cross == 0.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsLeft(Vertex a, Vertex b, Vertex p) => Vertex.Cross(b - a, p - a) > 0d;
+}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs
similarity index 88%
rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs
rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs
index 4061d300..9d5ac054 100644
--- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs
+++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs
@@ -2,11 +2,33 @@
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
-
-namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper;
+using SixLabors.ImageSharp.Drawing.Shapes.Helpers;
#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);
private ArrayBuilder srcVertices = new(16);
@@ -26,7 +48,7 @@ internal sealed class PolygonStroker
public double ApproximationScale { get; set; } = 1.0;
- public LineJoin LineJoin { get; set; } = LineJoin.MiterJoin;
+ public LineJoin LineJoin { get; set; } = LineJoin.BevelJoin;
public LineCap LineCap { get; set; } = LineCap.Butt;
@@ -53,8 +75,13 @@ public double Width
}
}
- public PathF ProcessPath(ReadOnlySpan linePoints, bool isClosed)
+ public PointF[] ProcessPath(ReadOnlySpan linePoints, bool isClosed)
{
+ if (linePoints.Length < 2)
+ {
+ return [];
+ }
+
this.Reset();
this.AddLinePath(linePoints);
@@ -63,9 +90,9 @@ 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];
}
public void AddLinePath(ReadOnlySpan linePoints)
@@ -79,7 +106,9 @@ public void AddLinePath(ReadOnlySpan linePoints)
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;
}
public void FinishPath(List results)
@@ -327,6 +356,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 +371,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];
@@ -489,6 +521,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 +585,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;
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs
new file mode 100644
index 00000000..a2277acb
--- /dev/null
+++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs
@@ -0,0 +1,207 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.PolygonClipper;
+
+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.
+ ///
+ /// meter limit
+ /// arc tolerance
+ public StrokedShapeGenerator(float meterLimit = 2F, float arcTolerance = .25F)
+ {
+ // TODO: We need to consume the joint type properties here.
+ // to do so we need to replace the existing ones with our new enums and update
+ // the overloads and pens.
+ this.polygonStroker = new PolygonStroker();
+ }
+
+ ///
+ /// 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, the rings are split between
+ /// subject and clip sets and a is performed.
+ /// The split ensures at least two operands so the union resolves overlaps.
+ /// The union uses to preserve winding density.
+ ///
+ public IPath[] GenerateStrokedShapes(List spans, float width)
+ {
+ // PolygonClipper is not designed to clean up self-intersecting geometry within a single polygon.
+ // It operates strictly on two polygon operands (subject and clip) and only resolves overlaps
+ // between them. To force cleanup of dashed stroke overlaps, we alternate assigning each
+ // stroked segment to subject or clip, ensuring at least two operands exist so the union
+ // operation performs a true merge rather than a no-op on a single polygon.
+
+ // 1) Stroke each dashed span as open.
+ this.polygonStroker.Width = width;
+
+ 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;
+ }
+
+ rings.Add(new Polygon(new LinearLineSegment(stroked)));
+ }
+
+ int count = rings.Count;
+ if (count == 0)
+ {
+ return [];
+ }
+
+ if (count == 1)
+ {
+ // Only one stroked ring. Return as-is; two-operand union requires both sides non-empty.
+ return [rings[0]];
+ }
+
+ // 2) Partition so the first and last are on different polygons
+ List subjectRings = new(count);
+ List clipRings = new(count);
+
+ // First => subject
+ subjectRings.Add(rings[0]);
+
+ // Middle by alternation using a single bool flag
+ bool assignToSubject = false; // start with clip for i=1
+ for (int i = 1; i < count - 1; i++)
+ {
+ if (assignToSubject)
+ {
+ subjectRings.Add(rings[i]);
+ }
+ else
+ {
+ clipRings.Add(rings[i]);
+ }
+
+ assignToSubject = !assignToSubject;
+ }
+
+ // Last => opposite of first (i.e., clip)
+ clipRings.Add(rings[count - 1]);
+
+ // 3) Union subject vs clip
+ ClippedShapeGenerator clipper = new(IntersectionRule.NonZero);
+ clipper.AddPaths(subjectRings, ClippingType.Subject);
+ clipper.AddPaths(clipRings, ClippingType.Clip);
+ return clipper.GenerateClippedShapes(BooleanOperation.Union);
+ }
+
+ ///
+ /// 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
+ /// between subject and clip sets and combined using .
+ /// This split is required because the Martinez based clipper resolves overlaps only between
+ /// two operands. 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 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
+ }
+
+ rings.Add(new Polygon(new LinearLineSegment(stroked)));
+ }
+
+ int count = rings.Count;
+ if (count == 0)
+ {
+ return [];
+ }
+
+ if (count == 1)
+ {
+ // Only one stroked ring. Return as-is; two-operand union requires both sides non-empty.
+ return [rings[0]];
+ }
+
+ // 2) Partition so the first and last are on different polygons
+ // PolygonClipper is not designed to clean up self-intersecting geometry within a single polygon.
+ // It operates strictly on two polygon operands (subject and clip) and only resolves overlaps
+ // between them. To force cleanup of overlaps, we alternate assigning each stroked ring to
+ // subject or clip, ensuring at least two operands exist so the union performs a true merge.
+ List subjectRings = new(count);
+ List clipRings = new(count);
+
+ // First => subject
+ subjectRings.Add(rings[0]);
+
+ // Middle by alternation using a single bool flag
+ bool assignToSubject = false; // start with clip for i=1
+ for (int i = 1; i < count - 1; i++)
+ {
+ if (assignToSubject)
+ {
+ subjectRings.Add(rings[i]);
+ }
+ else
+ {
+ clipRings.Add(rings[i]);
+ }
+
+ assignToSubject = !assignToSubject;
+ }
+
+ // Last => opposite of first (i.e., clip)
+ clipRings.Add(rings[count - 1]);
+
+ // 3) Union subject vs clip
+ ClippedShapeGenerator clipper = new(IntersectionRule.NonZero);
+ clipper.AddPaths(subjectRings, ClippingType.Subject);
+ clipper.AddPaths(clipRings, ClippingType.Clip);
+
+ // 4) Return the cleaned, merged outline
+ return clipper.GenerateClippedShapes(BooleanOperation.Union);
+ }
+}
diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexDistance.cs
similarity index 93%
rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs
rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexDistance.cs
index 89383756..c27a5658 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;
diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs
index bc4963cd..f4f45bc9 100644
--- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs
+++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs
@@ -6,6 +6,7 @@
using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
+using SixLabors.PolygonClipper;
namespace SixLabors.ImageSharp.Drawing.Tests.Drawing;
@@ -27,9 +28,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 +178,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 +190,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 };
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 +301,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 +337,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/Processing/ShapeOptionsDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs
index 28a20662..1853828e 100644
--- a/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs
+++ b/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs
@@ -3,6 +3,7 @@
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.PolygonClipper;
namespace SixLabors.ImageSharp.Drawing.Tests.Processing;
@@ -27,7 +28,7 @@ public void UpdateDefaultOptionsOnProcessingContext_AlwaysNewInstance()
{
ShapeOptions option = new()
{
- ClippingOperation = ClippingOperation.Intersection,
+ ClippingOperation = BooleanOperation.Intersection,
IntersectionRule = IntersectionRule.NonZero
};
Configuration config = new();
@@ -36,18 +37,18 @@ public void UpdateDefaultOptionsOnProcessingContext_AlwaysNewInstance()
context.SetShapeOptions(o =>
{
- Assert.Equal(ClippingOperation.Intersection, o.ClippingOperation); // has original values
+ Assert.Equal(BooleanOperation.Intersection, o.ClippingOperation); // has original values
Assert.Equal(IntersectionRule.NonZero, o.IntersectionRule);
- o.ClippingOperation = ClippingOperation.Xor;
+ o.ClippingOperation = BooleanOperation.Xor;
o.IntersectionRule = IntersectionRule.EvenOdd;
});
ShapeOptions returnedOption = context.GetShapeOptions();
- Assert.Equal(ClippingOperation.Xor, returnedOption.ClippingOperation);
+ Assert.Equal(BooleanOperation.Xor, returnedOption.ClippingOperation);
Assert.Equal(IntersectionRule.EvenOdd, returnedOption.IntersectionRule);
- Assert.Equal(ClippingOperation.Intersection, option.ClippingOperation); // hasn't been mutated
+ Assert.Equal(BooleanOperation.Intersection, option.ClippingOperation); // hasn't been mutated
Assert.Equal(IntersectionRule.NonZero, option.IntersectionRule);
}
@@ -67,7 +68,7 @@ public void UpdateDefaultOptionsOnConfiguration_AlwaysNewInstance()
{
ShapeOptions option = new()
{
- ClippingOperation = ClippingOperation.Intersection,
+ ClippingOperation = BooleanOperation.Intersection,
IntersectionRule = IntersectionRule.NonZero
};
Configuration config = new();
@@ -75,16 +76,16 @@ public void UpdateDefaultOptionsOnConfiguration_AlwaysNewInstance()
config.SetShapeOptions(o =>
{
- Assert.Equal(ClippingOperation.Intersection, o.ClippingOperation); // has original values
+ Assert.Equal(BooleanOperation.Intersection, o.ClippingOperation); // has original values
Assert.Equal(IntersectionRule.NonZero, o.IntersectionRule);
- o.ClippingOperation = ClippingOperation.Xor;
+ o.ClippingOperation = BooleanOperation.Xor;
o.IntersectionRule = IntersectionRule.EvenOdd;
});
ShapeOptions returnedOption = config.GetShapeOptions();
- Assert.Equal(ClippingOperation.Xor, returnedOption.ClippingOperation);
+ Assert.Equal(BooleanOperation.Xor, returnedOption.ClippingOperation);
Assert.Equal(IntersectionRule.EvenOdd, returnedOption.IntersectionRule);
- Assert.Equal(ClippingOperation.Intersection, option.ClippingOperation); // hasn't been mutated
+ Assert.Equal(BooleanOperation.Intersection, option.ClippingOperation); // 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..6a48e823 100644
--- a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs
+++ b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs
@@ -2,8 +2,9 @@
// 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;
+using SixLabors.PolygonClipper;
namespace SixLabors.ImageSharp.Drawing.Tests.PolygonClipper;
@@ -27,18 +28,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