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