first commit

This commit is contained in:
2026-08-03 22:31:27 +02:00
commit 7e8cddf208
1986 changed files with 370602 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Represents an edge that is currently active in the sweep-line.
/// </summary>
/// <remarks>
/// The sweep assumes a Y-axis-positive-down coordinate system. "Bottom" and "Top"
/// refer to the lower and upper scanline endpoints (larger and smaller Y respectively).
/// </remarks>
internal sealed class ActiveEdge
{
#pragma warning disable SA1401 // Hot sweep state uses fields to avoid accessor overhead.
/// <summary>
/// The lower endpoint of the edge in scanline order.
/// </summary>
public Vertex Bottom;
/// <summary>
/// The upper endpoint of the edge in scanline order.
/// </summary>
public Vertex Top;
/// <summary>
/// The X coordinate where the edge intersects the current scanline.
/// </summary>
public double CurrentX;
/// <summary>
/// The delta-X per delta-Y for the edge (its scanline slope).
/// </summary>
public double Dx;
/// <summary>
/// The winding delta contributed by this edge (+1 or -1).
/// </summary>
public int WindDelta;
/// <summary>
/// The accumulated winding count for this edge.
/// </summary>
public int WindCount;
/// <summary>
/// The output record this edge is contributing to, if any.
/// </summary>
public OutputRecord? OutputRecord;
/// <summary>
/// The previous edge in the Active Edge List (AEL).
/// </summary>
public ActiveEdge? PrevInAel;
/// <summary>
/// The next edge in the Active Edge List (AEL).
/// </summary>
public ActiveEdge? NextInAel;
/// <summary>
/// The previous edge in the Sorted Edge List (SEL).
/// </summary>
public ActiveEdge? PrevInSel;
/// <summary>
/// The next edge in the Sorted Edge List (SEL).
/// </summary>
public ActiveEdge? NextInSel;
/// <summary>
/// The temporary link used when sorting intersections.
/// </summary>
public ActiveEdge? Jump;
/// <summary>
/// The current top vertex for this edge's bound.
/// </summary>
public SweepVertex? VertexTop;
/// <summary>
/// The local minima that spawned this edge.
/// </summary>
public LocalMinima LocalMin;
/// <summary>
/// Indicates whether this edge is the left bound of its pair.
/// </summary>
public bool IsLeftBound;
/// <summary>
/// The pending join state for this edge.
/// </summary>
public JoinWith JoinWith;
#pragma warning restore SA1401
/// <summary>
/// Gets a value indicating whether this edge currently contributes to output.
/// </summary>
public bool IsHot => this.OutputRecord != null;
/// <summary>
/// Gets a value indicating whether the edge is horizontal within tolerance.
/// </summary>
public bool IsHorizontal => this.Top.Y == this.Bottom.Y;
/// <summary>
/// Gets a value indicating whether a horizontal edge is heading right.
/// </summary>
public bool IsHeadingRightHorizontal => double.IsNegativeInfinity(this.Dx);
/// <summary>
/// Gets a value indicating whether a horizontal edge is heading left.
/// </summary>
public bool IsHeadingLeftHorizontal => double.IsPositiveInfinity(this.Dx);
/// <summary>
/// Gets a value indicating whether the current top vertex is a local maxima.
/// </summary>
public bool IsMaxima => this.VertexTop != null && this.VertexTop.IsMaxima;
/// <summary>
/// Gets a value indicating whether this edge is the front edge of its output record.
/// </summary>
public bool IsFront => this.OutputRecord != null && this == this.OutputRecord.FrontEdge;
/// <summary>
/// Gets the next input vertex along the bound in the winding direction.
/// </summary>
public SweepVertex NextVertex => this.WindDelta > 0 ? this.VertexTop!.Next! : this.VertexTop!.Prev!;
/// <summary>
/// Gets the vertex two steps behind the current top, used for turn tests.
/// </summary>
public SweepVertex PrevPrevVertex => this.WindDelta > 0 ? this.VertexTop!.Prev!.Prev! : this.VertexTop!.Next!.Next!;
/// <summary>
/// Finds the previous hot edge in the AEL, if any.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ActiveEdge? GetPrevHotEdge()
{
ActiveEdge? prev = this.PrevInAel;
while (prev != null && !prev.IsHot)
{
prev = prev.PrevInAel;
}
return prev;
}
/// <summary>
/// Calculates the X coordinate where this edge intersects the scanline at <paramref name="currentY" />.
/// </summary>
// This method sits on the hottest path in large self-intersection workloads.
// AggressiveOptimization consistently improves codegen here versus tiered defaults.
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static double TopX(ActiveEdge edge, double currentY)
{
if (currentY == edge.Top.Y || edge.Top.X == edge.Bottom.X)
{
return edge.Top.X;
}
if (currentY == edge.Bottom.Y)
{
return edge.Bottom.X;
}
return edge.Bottom.X + (edge.Dx * (currentY - edge.Bottom.Y));
}
/// <summary>
/// Recomputes <see cref="Dx" /> from the current endpoints.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void UpdateDx() => this.Dx = GetDx(this.Bottom, this.Top);
/// <summary>
/// Computes delta-X per delta-Y, returning infinities for horizontal edges.
/// </summary>
private static double GetDx(Vertex pt1, Vertex pt2)
{
double dy = pt2.Y - pt1.Y;
if (dy != 0)
{
return (pt2.X - pt1.X) / dy;
}
return pt2.X > pt1.X ? double.NegativeInfinity : double.PositiveInfinity;
}
}
}
+349
View File
@@ -0,0 +1,349 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Maintains the active edge list (AEL) plus a horizontal edge stack for the sweep.
/// </summary>
/// <remarks>
/// The AEL is ordered left-to-right at the current scanline. As edges are inserted
/// and removed, this list preserves adjacency for intersection processing.
/// The horizontal stack is a lightweight LIFO queue used to process horizontal
/// bounds separately from the main sweep order.
/// </remarks>
internal sealed class ActiveEdgeList
{
private readonly Stack<ActiveEdge> pool;
private ActiveEdge? horizontalHead;
/// <summary>
/// Initializes a new instance of the <see cref="ActiveEdgeList"/> class.
/// </summary>
public ActiveEdgeList() => this.pool = new Stack<ActiveEdge>();
/// <summary>
/// Gets the head of the active edge list.
/// </summary>
public ActiveEdge? Head { get; private set; }
/// <summary>
/// Gets the number of retained pooled edge objects.
/// </summary>
public int RetainedPoolCount => this.pool.Count;
/// <summary>
/// Clears all active edges and returns them to the pool.
/// </summary>
public void ClearActiveEdges()
{
while (this.Head != null)
{
this.Remove(this.Head);
}
this.horizontalHead = null;
}
/// <summary>
/// Resets sweep pointers without clearing the pool.
/// </summary>
public void Reset()
{
this.Head = null;
this.horizontalHead = null;
}
/// <summary>
/// Acquires a reusable active edge, allocating if the pool is empty.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ActiveEdge Acquire()
=> this.pool.Count == 0 ? new ActiveEdge() : this.pool.Pop();
/// <summary>
/// Inserts an edge into the active list, maintaining left-to-right order.
/// </summary>
/// <param name="edge">The edge to insert.</param>
public void InsertLeft(ActiveEdge edge)
{
if (this.Head == null)
{
edge.PrevInAel = null;
edge.NextInAel = null;
this.Head = edge;
return;
}
if (!IsValidActiveEdgeOrder(this.Head, edge))
{
edge.PrevInAel = null;
edge.NextInAel = this.Head;
this.Head.PrevInAel = edge;
this.Head = edge;
return;
}
ActiveEdge edge2 = this.Head;
while (edge2.NextInAel != null && IsValidActiveEdgeOrder(edge2.NextInAel, edge))
{
edge2 = edge2.NextInAel;
}
// Keep joined edges adjacent in the active list.
if (edge2.JoinWith == JoinWith.Right)
{
edge2 = edge2.NextInAel!;
}
edge.NextInAel = edge2.NextInAel;
if (edge2.NextInAel != null)
{
edge2.NextInAel.PrevInAel = edge;
}
edge.PrevInAel = edge2;
edge2.NextInAel = edge;
}
/// <summary>
/// Inserts a right bound edge immediately after another edge in the active list.
/// </summary>
/// <param name="edge">The anchor edge.</param>
/// <param name="edge2">The edge to insert.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void InsertRight(ActiveEdge edge, ActiveEdge edge2)
{
edge2.NextInAel = edge.NextInAel;
if (edge.NextInAel != null)
{
edge.NextInAel.PrevInAel = edge2;
}
edge2.PrevInAel = edge;
edge.NextInAel = edge2;
}
/// <summary>
/// Removes an edge from the active list and returns it to the pool.
/// </summary>
/// <param name="edge">The edge to remove.</param>
public void Remove(ActiveEdge edge)
{
ActiveEdge? prev = edge.PrevInAel;
ActiveEdge? next = edge.NextInAel;
if (prev == null && next == null && edge != this.Head)
{
return;
}
if (prev != null)
{
prev.NextInAel = next;
}
else
{
this.Head = next;
}
if (next != null)
{
next.PrevInAel = prev;
}
this.Recycle(edge);
}
/// <summary>
/// Swaps the positions of two adjacent edges in the active list.
/// </summary>
/// <param name="left">The left edge.</param>
/// <param name="right">The right edge.</param>
public void SwapPositions(ActiveEdge left, ActiveEdge right)
{
// Precondition: left must be immediately to the left of right.
ActiveEdge? next = right.NextInAel;
if (next != null)
{
next.PrevInAel = left;
}
ActiveEdge? prev = left.PrevInAel;
if (prev != null)
{
prev.NextInAel = right;
}
right.PrevInAel = prev;
right.NextInAel = left;
left.PrevInAel = right;
left.NextInAel = next;
if (right.PrevInAel == null)
{
this.Head = right;
}
}
/// <summary>
/// Clears the horizontal edge stack.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ClearHorizontalQueue() => this.horizontalHead = null;
/// <summary>
/// Pushes a horizontal edge onto the processing stack.
/// </summary>
/// <param name="edge">The horizontal edge to push.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void PushHorizontal(ActiveEdge edge)
{
edge.NextInSel = this.horizontalHead;
this.horizontalHead = edge;
}
/// <summary>
/// Pops the next horizontal edge to process.
/// </summary>
/// <param name="edge">The next horizontal edge, or <see langword="null"/>.</param>
/// <returns><see langword="true"/> when a horizontal edge was available.</returns>
public bool TryPopHorizontal(out ActiveEdge? edge)
{
while (true)
{
edge = this.horizontalHead;
if (edge == null)
{
return false;
}
ActiveEdge? next = edge.NextInSel;
this.horizontalHead = ReferenceEquals(next, edge) ? null : next;
if (edge.VertexTop != null)
{
return true;
}
}
}
/// <summary>
/// Copies the active list into a sorted list and updates current X values.
/// </summary>
/// <param name="topY">The scanline top Y coordinate.</param>
/// <returns>The head of the sorted list.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public ActiveEdge? CopyToSorted(double topY)
{
ActiveEdge? edge = this.Head;
ActiveEdge? sortedHead = edge;
while (edge != null)
{
edge.PrevInSel = edge.PrevInAel;
edge.NextInSel = edge.NextInAel;
edge.Jump = edge.NextInSel;
// Joined edges can be split later during intersection processing.
edge.CurrentX = ActiveEdge.TopX(edge, topY);
// Defer any Y updates; intersection tests use original bounds.
edge = edge.NextInAel;
}
return sortedHead;
}
/// <summary>
/// Determines whether the newcomer should be inserted after the resident in the active list.
/// </summary>
/// <param name="resident">The current resident edge.</param>
/// <param name="newcomer">The incoming edge to compare.</param>
/// <returns><see langword="true"/> if the newcomer bedoubles after the resident.</returns>
public static bool IsValidActiveEdgeOrder(ActiveEdge resident, ActiveEdge newcomer)
{
if (newcomer.CurrentX != resident.CurrentX)
{
return newcomer.CurrentX > resident.CurrentX;
}
// Compare turning direction: resident.Top -> newcomer.Bottom -> newcomer.Top.
int d = PolygonUtilities.CrossSign(resident.Top, newcomer.Bottom, newcomer.Top);
if (d != 0)
{
return d < 0;
}
// For collinear bounds, use the next turn to order them.
if (!resident.IsMaxima && (resident.Top.Y > newcomer.Top.Y))
{
return PolygonUtilities.CrossSign(
newcomer.Bottom,
resident.Top,
resident.NextVertex.Point) <= 0;
}
if (!newcomer.IsMaxima && (newcomer.Top.Y > resident.Top.Y))
{
return PolygonUtilities.CrossSign(
newcomer.Bottom,
newcomer.Top,
newcomer.NextVertex.Point) >= 0;
}
double y = newcomer.Bottom.Y;
bool newcomerIsLeft = newcomer.IsLeftBound;
if (resident.Bottom.Y != y || resident.LocalMin.Vertex.Point.Y != y)
{
return newcomer.IsLeftBound;
}
// Only newly inserted edges reach this branch.
if (resident.IsLeftBound != newcomerIsLeft)
{
return newcomerIsLeft;
}
if (PolygonUtilities.IsCollinear(
resident.PrevPrevVertex.Point,
resident.Bottom,
resident.Top))
{
return true;
}
// Use the alternate bound turn to break the tie.
return (PolygonUtilities.CrossSign(
resident.PrevPrevVertex.Point,
newcomer.Bottom,
newcomer.PrevPrevVertex.Point) > 0) == newcomerIsLeft;
}
/// <summary>
/// Resets and returns an active edge to the reuse pool.
/// </summary>
/// <param name="edge">The edge to recycle.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Recycle(ActiveEdge edge)
{
// Clear references so pooled edges do not retain objects.
edge.Bottom = default;
edge.Top = default;
edge.Dx = 0.0;
edge.CurrentX = 0;
edge.WindCount = 0;
edge.OutputRecord = null;
edge.PrevInAel = null;
edge.NextInAel = null;
edge.PrevInSel = null;
edge.NextInSel = null;
edge.Jump = null;
edge.VertexTop = null;
edge.LocalMin = default;
edge.IsLeftBound = false;
edge.JoinWith = JoinWith.None;
this.pool.Push(edge);
}
}
}
+180
View File
@@ -0,0 +1,180 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace SixLabors.PolygonClipper {
/// <summary>
/// A helper type for avoiding allocations while building arrays.
/// </summary>
/// <typeparam name="T">The type of item contained in the array.</typeparam>
internal struct ArrayBuilder<T>
where T : struct
{
private const int DefaultCapacity = 4;
// Starts out null, initialized on first Add.
private T[]? data;
private int size;
/// <summary>
/// Initializes a new instance of the <see cref="ArrayBuilder{T}"/> struct.
/// </summary>
/// <param name="capacity">The initial capacity of the array.</param>
public ArrayBuilder(int capacity)
: this()
{
if (capacity > 0)
{
this.data = new T[capacity];
}
}
/// <summary>
/// Gets or sets the number of items in the array.
/// </summary>
public int Length
{
readonly get => this.size;
set
{
if (value > 0)
{
this.EnsureCapacity(value);
this.size = value;
}
else
{
this.size = 0;
}
}
}
/// <summary>
/// Gets the backing buffer capacity.
/// </summary>
public readonly int Capacity
=> this.data?.Length ?? 0;
/// <summary>
/// Returns a reference to specified element of the array.
/// </summary>
/// <param name="index">The index of the element to return.</param>
/// <returns>The <typeparamref name="T"/>.</returns>
/// <exception cref="IndexOutOfRangeException">
/// Thrown when index less than 0 or index greater than or equal to <see cref="Length"/>.
/// </exception>
public readonly ref T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
DebugGuard.MustBeBetweenOrEqualTo(index, 0, this.size, nameof(index));
return ref this.data![index];
}
}
/// <summary>
/// Adds the given item to the array.
/// </summary>
/// <param name="item">The item to add.</param>
public void Add(T item)
{
int position = this.size;
T[]? array = this.data;
if (array != null && (uint)position < (uint)array.Length)
{
this.size = position + 1;
array[position] = item;
}
else
{
this.AddWithResize(item);
}
}
// Non-inline from Add to improve its code quality as uncommon path
[MethodImpl(MethodImplOptions.NoInlining)]
private void AddWithResize(T item)
{
int size = this.size;
this.Grow(size + 1);
this.size = size + 1;
this.data[size] = item;
}
/// <summary>
/// Remove the last item from the array.
/// </summary>
public void RemoveLast()
{
DebugGuard.MustBeGreaterThan(this.size, 0, nameof(this.size));
this.size--;
}
/// <summary>
/// Clears the array.
/// Allocated memory is left intact for future usage.
/// </summary>
public void Clear() =>
// No need to actually clear since we're not allowing reference types.
this.size = 0;
/// <summary>
/// Sorts the active range of items using the specified comparer.
/// </summary>
/// <param name="comparer">The comparer to use, or null for the default comparer.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void Sort(IComparer<T>? comparer = null)
{
if (this.size <= 1 || this.data == null)
{
return;
}
Array.Sort(this.data, 0, this.size, comparer);
}
private void EnsureCapacity(int min)
{
int length = this.data?.Length ?? 0;
if (length < min)
{
this.Grow(min);
}
}
[MemberNotNull(nameof(data))]
private void Grow(int capacity)
{
// Same expansion algorithm as List<T>.
int length = this.data?.Length ?? 0;
int newCapacity = length == 0 ? DefaultCapacity : length * 2;
if ((uint)newCapacity > Array.MaxLength)
{
newCapacity = Array.MaxLength;
}
if (newCapacity < capacity)
{
newCapacity = capacity;
}
T[] array = new T[newCapacity];
if (this.size > 0)
{
Array.Copy(this.data!, array, this.size);
}
this.data = array;
}
}
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.PolygonClipper {
/// <summary>
/// Specifies the type of boolean operation to perform on polygons.
/// </summary>
public enum BooleanOperation
{
/// <summary>
/// The intersection operation, which results in the area common to both polygons.
/// </summary>
Intersection = 0,
/// <summary>
/// The union operation, which results in the combined area of both polygons.
/// </summary>
Union = 1,
/// <summary>
/// The difference operation, which subtracts the clipping polygon from the subject polygon.
/// </summary>
Difference = 2,
/// <summary>
/// The exclusive OR (XOR) operation, which results in the area covered by exactly one polygon,
/// excluding the overlapping areas.
/// </summary>
Xor = 3
}
}
+134
View File
@@ -0,0 +1,134 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Runtime.CompilerServices;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Represents a bounding box.
/// </summary>
public readonly struct Box2 : IEquatable<Box2>
{
/// <summary>
/// Gets the minimum xy-coordinate.
/// </summary>
#pragma warning disable CA1051 // Do not declare visible instance fields
public readonly Vertex Min;
/// <summary>
/// Gets the maximum xy-coordinate.
/// </summary>
public readonly Vertex Max;
#pragma warning restore CA1051 // Do not declare visible instance fields
/// <summary>
/// Initializes a new instance of the <see cref="Box2"/> struct.
/// </summary>
/// <param name="vector">The xy-coordinate.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Box2(in Vertex vector)
: this(vector, vector)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Box2"/> struct.
/// </summary>
/// <param name="min">The minimum xy-coordinate.</param>
/// <param name="max">The maximum xy-coordinate.</param>
public Box2(in Vertex min, in Vertex max)
{
this.Min = min;
this.Max = max;
}
/// <summary>
/// Gets an invalid bounds instance.
/// </summary>
public static Box2 Invalid { get; } = new(
new Vertex(double.MaxValue, double.MaxValue),
new Vertex(-double.MaxValue, -double.MaxValue));
/// <summary>
/// Compares two <see cref="Box2"/> instances for equality.
/// </summary>
/// <param name="left">The left <see cref="Box2"/> object.</param>
/// <param name="right">The right <see cref="Box2"/> object.</param>
/// <returns><c>true</c> if both boxes are equal; otherwise, <c>false</c>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(in Box2 left, in Box2 right)
=> left.Equals(right);
/// <summary>
/// Determines whether two <see cref="Box2"/> instances are not equal.
/// </summary>
/// <param name="left">The left <see cref="Box2"/> object.</param>
/// <param name="right">The right <see cref="Box2"/> object.</param>
/// <returns><c>true</c> if the boxes are not equal; otherwise, <c>false</c>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(in Box2 left, in Box2 right)
=> !(left == right);
/// <summary>
/// Returns true if the box is empty.
/// </summary>
/// <returns><see langword="true"/> if the box is empty; otherwise, <see langword="false"/>.</returns>
public bool IsEmpty() => this.Max.X <= this.Min.X || this.Max.Y <= this.Min.Y;
/// <summary>
/// Returns true if the point lies within the box.
/// </summary>
/// <param name="point">The point to test.</param>
/// <returns><see langword="true"/> if the point lies within the box; otherwise, <see langword="false"/>.</returns>
public bool Contains(in Vertex point)
=> point.X > this.Min.X && point.X < this.Max.X && point.Y > this.Min.Y && point.Y < this.Max.Y;
/// <summary>
/// Returns true if the box contains another box.
/// </summary>
/// <param name="bounds">The other box.</param>
/// <returns><see langword="true"/> if the box contains the other box; otherwise, <see langword="false"/>.</returns>
public bool Contains(in Box2 bounds)
=> bounds.Min.X >= this.Min.X && bounds.Max.X <= this.Max.X &&
bounds.Min.Y >= this.Min.Y && bounds.Max.Y <= this.Max.Y;
/// <summary>
/// Returns true if the boxes intersect.
/// </summary>
/// <param name="bounds">The other box.</param>
/// <returns><see langword="true"/> if the boxes intersect; otherwise, <see langword="false"/>.</returns>
public bool Intersects(in Box2 bounds)
=> Math.Max(this.Min.X, bounds.Min.X) <= Math.Min(this.Max.X, bounds.Max.X) &&
Math.Max(this.Min.Y, bounds.Min.Y) <= Math.Min(this.Max.Y, bounds.Max.Y);
/// <summary>
/// Returns the midpoint of the box.
/// </summary>
/// <returns>The midpoint.</returns>
public Vertex MidPoint() => new((this.Min.X + this.Max.X) / 2D, (this.Min.Y + this.Max.Y) / 2D);
/// <summary>
/// Adds another bounding box to this instance.
/// </summary>
/// <param name="other">The other box.</param>
/// <returns>The summed <see cref="Box2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Box2 Add(in Box2 other)
=> new(Vertex.Min(this.Min, other.Min), Vertex.Max(this.Max, other.Max));
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool Equals(object? obj)
=> obj is Box2 box
&& this.Equals(box);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Box2 other)
=> this.Min == other.Min && this.Max == other.Max;
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.Min, this.Max);
}
}
+63
View File
@@ -0,0 +1,63 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
namespace SixLabors.PolygonClipper {
/// <summary>
/// An disposable buffer that is backed by an array pool.
/// </summary>
/// <typeparam name="T">The type of buffer element.</typeparam>
internal ref struct Buffer<T>
where T : unmanaged
{
private int length;
private readonly byte[] buffer;
private readonly Span<T> span;
private bool isDisposed;
public Buffer(int length)
{
Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
int itemSizeBytes = Unsafe.SizeOf<T>();
int bufferSizeInBytes = length * itemSizeBytes;
this.buffer = ArrayPool<byte>.Shared.Rent(bufferSizeInBytes);
this.length = length;
using ByteMemoryManager<T> manager = new(this.buffer);
this.Memory = manager.Memory[..this.length];
this.span = this.Memory.Span;
this.isDisposed = false;
}
public Memory<T> Memory { get; }
public readonly Span<T> GetSpan()
{
if (this.buffer is null)
{
ThrowObjectDisposedException();
}
return this.span;
}
public void Dispose()
{
if (this.isDisposed)
{
return;
}
ArrayPool<byte>.Shared.Return(this.buffer);
this.length = 0;
this.isDisposed = true;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowObjectDisposedException() => throw new ObjectDisposedException("Buffer<T>");
}
}
+50
View File
@@ -0,0 +1,50 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.PolygonClipper {
/// <summary>
/// A custom <see cref="MemoryManager{T}"/> that can wrap <see cref="Memory{T}"/> of <see cref="byte"/> instances
/// and cast them to be <see cref="Memory{T}"/> for any arbitrary unmanaged <typeparamref name="T"/> value type.
/// </summary>
/// <typeparam name="T">The value type to use when casting the wrapped <see cref="Memory{T}"/> instance.</typeparam>
internal sealed class ByteMemoryManager<T> : MemoryManager<T>
where T : unmanaged
{
/// <summary>
/// The wrapped <see cref="Memory{T}"/> of <see cref="byte"/> instance.
/// </summary>
private readonly Memory<byte> memory;
/// <summary>
/// Initializes a new instance of the <see cref="ByteMemoryManager{T}"/> class.
/// </summary>
/// <param name="memory">The <see cref="Memory{T}"/> of <see cref="byte"/> instance to wrap.</param>
public ByteMemoryManager(Memory<byte> memory) => this.memory = memory;
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
/// <inheritdoc/>
public override Span<T> GetSpan() => MemoryMarshal.Cast<byte, T>(this.memory.Span);
/// <inheritdoc/>
public override MemoryHandle Pin(int elementIndex = 0)
// We need to adjust the offset into the wrapped byte segment,
// as the input index refers to the target-cast memory of T.
// We just have to shift this index by the byte size of T.
=> this.memory[(elementIndex * Unsafe.SizeOf<T>())..].Pin();
/// <inheritdoc/>
public override void Unpin()
{
}
}
}
+282
View File
@@ -0,0 +1,282 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Represents a single polygon ring (outer contour or hole).
/// </summary>
/// <remarks>
/// A contour is treated as implicitly closed: an edge is always considered between the last
/// vertex and the first vertex. A duplicated terminal closing vertex is optional on input
/// but not required.
/// </remarks>
[DebuggerDisplay("Count = {Count}")]
#pragma warning disable CA1710 // Identifiers should have correct suffix
public sealed class Contour : IReadOnlyCollection<Vertex>
#pragma warning restore CA1710 // Identifiers should have correct suffix
{
private bool hasCachedOrientation;
private bool cachedCounterClockwise;
/// <summary>
/// Set of vertices conforming the external contour
/// </summary>
private readonly List<Vertex> vertices = [];
/// <summary>
/// Holes of the contour. They are stored as the indexes of
/// the holes in a polygon class
/// </summary>
private readonly List<int> holeIndices = [];
/// <summary>
/// Initializes a new instance of the <see cref="Contour"/> class.
/// </summary>
public Contour()
=> this.vertices = [];
/// <summary>
/// Initializes a new instance of the <see cref="Contour"/> class with a vertex capacity.
/// </summary>
/// <param name="capacity">The initial vertex capacity.</param>
public Contour(int capacity)
=> this.vertices = new List<Vertex>(capacity);
/// <summary>
/// Gets the number of stored vertices.
/// </summary>
public int Count
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.vertices.Count;
}
/// <summary>
/// Gets the number of holes.
/// </summary>
public int HoleCount => this.holeIndices.Count;
/// <summary>
/// Gets a value indicating whether the contour is external (not a hole).
/// </summary>
public bool IsExternal => this.ParentIndex == null;
/// <summary>
/// Gets or sets the index of the parent contour in the polygon if this contour is a hole.
/// </summary>
public int? ParentIndex { get; set; }
/// <summary>
/// Gets or sets the depth of the contour.
/// </summary>
public int Depth { get; set; }
/// <summary>
/// Gets the vertex at the specified index.
/// </summary>
/// <param name="index">The index of the vertex.</param>
/// <returns>The <see cref="Vertex"/> at the specified index.</returns>
public Vertex this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.vertices[index];
}
/// <summary>
/// Gets the hole index at the specified position in the contour.
/// </summary>
/// <param name="index">The index of the hole.</param>
/// <returns>The hole index.</returns>
public int GetHoleIndex(int index) => this.holeIndices[index];
/// <summary>
/// Gets the segment at the specified index of the contour.
/// </summary>
/// <param name="index">The index of the segment.</param>
/// <returns>The <see cref="Segment"/>. The final segment wraps from last vertex to first vertex.</returns>
internal Segment GetSegment(int index)
=> (index == this.Count - 1)
? new Segment(this.vertices[^1], this.vertices[0])
: new Segment(this.vertices[index], this.vertices[index + 1]);
/// <summary>
/// Gets the bounding box of the contour.
/// </summary>
/// <returns>The <see cref="Box2"/>.</returns>
public Box2 GetBoundingBox()
{
if (this.Count == 0)
{
return default;
}
List<Vertex> points = this.vertices;
Box2 b = new(points[0]);
for (int i = 1; i < points.Count; ++i)
{
b = b.Add(new Box2(points[i]));
}
return b;
}
/// <summary>
/// Gets a value indicating whether the contour is counterclockwise oriented
/// </summary>
/// <returns>
/// <see langword="true"/> if the contour is counterclockwise oriented; otherwise <see langword="false"/>.
/// </returns>
public bool IsCounterClockwise()
{
if (this.hasCachedOrientation)
{
return this.cachedCounterClockwise;
}
this.hasCachedOrientation = true;
double area = 0;
Vertex c;
Vertex c1;
List<Vertex> points = this.vertices;
for (int i = 0; i < points.Count - 1; i++)
{
c = points[i];
c1 = points[i + 1];
area += Vertex.Cross(c, c1);
}
c = points[^1];
c1 = points[0];
area += Vertex.Cross(c, c1);
return this.cachedCounterClockwise = area >= 0;
}
/// <summary>
/// Gets a value indicating whether the contour is clockwise oriented
/// </summary>
/// <returns>
/// <see langword="true"/> if the contour is clockwise oriented; otherwise <see langword="false"/>.
/// </returns>
public bool IsClockwise() => !this.IsCounterClockwise();
/// <summary>
/// Reverses the orientation of the contour.
/// </summary>
public void Reverse()
{
this.vertices.Reverse();
this.cachedCounterClockwise = !this.cachedCounterClockwise;
}
/// <summary>
/// Sets the contour to clockwise orientation.
/// </summary>
public void SetClockwise()
{
if (this.IsCounterClockwise())
{
this.Reverse();
}
}
/// <summary>
/// Sets the contour to counterclockwise orientation.
/// </summary>
public void SetCounterClockwise()
{
if (this.IsClockwise())
{
this.Reverse();
}
}
/// <summary>
/// Translates the contour by the specified x and y values.
/// </summary>
/// <param name="x">The x-coordinate offset.</param>
/// <param name="y">The y-coordinate offset.</param>
public void Translate(double x, double y)
{
List<Vertex> points = this.vertices;
for (int i = 0; i < points.Count; i++)
{
points[i] += new Vertex(x, y);
}
}
/// <summary>
/// Adds a vertex to the end of the vertices collection.
/// </summary>
/// <param name="vertex">The vertex to add.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(in Vertex vertex) => this.vertices.Add(vertex);
/// <summary>
/// Removes the vertex at the specified index from the contour.
/// </summary>
/// <param name="index">The index of the vertex to remove.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveVertexAt(int index) => this.vertices.RemoveAt(index);
/// <summary>
/// Clears all vertices and holes from the contour.
/// </summary>
public void Clear()
{
this.vertices.Clear();
this.holeIndices.Clear();
}
/// <summary>
/// Clears all holes from the contour.
/// </summary>
public void ClearHoles() => this.holeIndices.Clear();
/// <summary>
/// Gets the last vertex in the contour.
/// </summary>
/// <returns>The last <see cref="Vertex"/> in the contour.</returns>
public Vertex GetLastVertex() => this.vertices[^1];
/// <summary>
/// Adds a hole index to the contour.
/// </summary>
/// <param name="index">The index of the hole to add.</param>
public void AddHoleIndex(int index) => this.holeIndices.Add(index);
/// <summary>
/// Creates a deep copy of this contour.
/// </summary>
/// <returns>A detached contour copy.</returns>
public Contour DeepClone()
{
Contour clone = new(this.vertices.Count)
{
ParentIndex = this.ParentIndex,
Depth = this.Depth,
hasCachedOrientation = this.hasCachedOrientation,
cachedCounterClockwise = this.cachedCounterClockwise
};
clone.vertices.AddRange(this.vertices);
clone.holeIndices.AddRange(this.holeIndices);
return clone;
}
/// <inheritdoc/>
public IEnumerator<Vertex> GetEnumerator()
=> ((IEnumerable<Vertex>)this.vertices).GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
=> ((IEnumerable)this.vertices).GetEnumerator();
}
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.PolygonClipper {
/// <summary>
/// Specifies the type of an edge in a boolean operation on polygons.
/// </summary>
internal enum EdgeType
{
/// <summary>
/// A normal edge that contributes to the resulting polygon.
/// </summary>
Normal = 0,
/// <summary>
/// An edge that does not contribute to the resulting polygon.
/// This typically occurs when the edge lies entirely inside another polygon.
/// </summary>
NonContributing = 1,
/// <summary>
/// An edge that represents a transition within the same polygon,
/// meaning it does not cross into another polygon.
/// </summary>
SameTransition = 2,
/// <summary>
/// An edge that represents a transition between different polygons,
/// meaning it crosses from one polygon to another.
/// </summary>
DifferentTransition = 3
}
}
+76
View File
@@ -0,0 +1,76 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Runtime.CompilerServices;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Provides extension methods for floating-point numbers.
/// </summary>
internal static class FloatExtensions
{
/// <summary>
/// Returns the next representable double value in the direction of y.
/// </summary>
/// <remarks><see href="https://docs.rs/float_next_after/latest/src/float_next_after/lib.rs.html"/></remarks>
/// <param name="x">The starting floating-point number.</param>
/// <param name="y">The target floating-point number.</param>
/// <returns>The next representable value of x towards y.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double NextAfter(this double x, double y)
{
// Special cases
if (double.IsNaN(x) || double.IsNaN(y))
{
return double.NaN;
}
if (x == y)
{
return y;
}
if (double.IsPositiveInfinity(x))
{
return double.PositiveInfinity;
}
if (double.IsNegativeInfinity(x))
{
return double.NegativeInfinity;
}
// Handle stepping from zero
if (x == 0D)
{
return Math.CopySign(double.Epsilon, y); // Smallest positive subnormal double
}
// Convert double to raw bits
long bits = BitConverter.DoubleToInt64Bits(x);
// Adjust bits to get the next representable value
// Moving in the same sign direction
if ((y > x) == (x > 0D))
{
bits++;
}
else
{
bits--;
}
// Convert bits back to double
double next = BitConverter.Int64BitsToDouble(bits);
// Ensure correct handling of signed zeros
if (next == 0D)
{
return Math.CopySign(next, x);
}
return next;
}
}
}
+41
View File
@@ -0,0 +1,41 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.PolygonClipper {
/// <summary>
/// Represents a pending intersection between two active edges.
/// </summary>
/// <remarks>
/// Intersections are sorted and processed from higher scanlines to lower ones so
/// that edge order in the AEL remains consistent as the sweep descends.
/// </remarks>
internal readonly struct IntersectNode
{
#pragma warning disable SA1401 // Hot path intersection sorting benefits from field access.
/// <summary>
/// Gets the intersection point between <see cref="Edge1"/> and <see cref="Edge2"/>.
/// </summary>
public readonly Vertex Point;
/// <summary>
/// Gets the first active edge participating in the intersection.
/// </summary>
public readonly ActiveEdge Edge1;
/// <summary>
/// Gets the second active edge participating in the intersection.
/// </summary>
public readonly ActiveEdge Edge2;
#pragma warning restore SA1401
/// <summary>
/// Initializes a new instance of the <see cref="IntersectNode"/> struct.
/// </summary>
internal IntersectNode(Vertex point, ActiveEdge edge1, ActiveEdge edge2)
{
this.Point = point;
this.Edge1 = edge1;
this.Edge2 = edge2;
}
}
}
+25
View File
@@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.PolygonClipper {
/// <summary>
/// Indicates whether an active edge should be joined with a neighbor after a split.
/// </summary>
internal enum JoinWith
{
/// <summary>
/// No pending join.
/// </summary>
None,
/// <summary>
/// Join with the left neighbor in the AEL.
/// </summary>
Left,
/// <summary>
/// Join with the right neighbor in the AEL.
/// </summary>
Right
}
}
+28
View File
@@ -0,0 +1,28 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.PolygonClipper {
/// <summary>
/// Specifies the shape to be used at the ends of open lines or paths when stroking.
/// </summary>
public enum LineCap
{
/// <summary>
/// The stroke ends exactly at the endpoint.
/// No extension is added beyond the path's end coordinates.
/// </summary>
Butt,
/// <summary>
/// The stroke extends beyond the endpoint by half the line width,
/// producing a square edge.
/// </summary>
Square,
/// <summary>
/// The stroke ends with a semicircular cap,
/// extending beyond the endpoint by half the line width.
/// </summary>
Round
}
}
+41
View File
@@ -0,0 +1,41 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.PolygonClipper {
/// <summary>
/// Specifies how the connection between two consecutive line segments (a join)
/// is rendered when stroking paths or polygons.
/// </summary>
public enum LineJoin
{
/// <summary>
/// Joins lines by extending their outer edges until they meet at a sharp corner.
/// If the miter limit is exceeded, the join is truncated at the limit distance.
/// </summary>
Miter = 0,
/// <summary>
/// Joins lines by extending their outer edges to form a miter.
/// If the miter limit is exceeded, the join falls back to a bevel.
/// </summary>
MiterRevert = 1,
/// <summary>
/// Joins lines by connecting them with a circular arc centered at the join point,
/// producing a smooth, rounded corner.
/// </summary>
Round = 2,
/// <summary>
/// Joins lines by connecting the outer corners directly with a straight line,
/// forming a flat edge at the join point.
/// </summary>
Bevel = 3,
/// <summary>
/// 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.
/// </summary>
MiterRound = 4
}
}
+42
View File
@@ -0,0 +1,42 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Collections.Generic;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Describes the lowest vertex of an edge bound for the sweep line.
/// </summary>
internal readonly struct LocalMinima : IEquatable<LocalMinima>
{
/// <summary>
/// Initializes a new instance of the <see cref="LocalMinima"/> struct.
/// </summary>
internal LocalMinima(SweepVertex vertex) => this.Vertex = vertex;
/// <summary>
/// Gets the vertex associated with this local minima.
/// </summary>
internal SweepVertex Vertex { get; }
public static bool operator ==(LocalMinima lm1, LocalMinima lm2) => lm1.Equals(lm2);
public static bool operator !=(LocalMinima lm1, LocalMinima lm2) => !(lm1 == lm2);
public override bool Equals(object? obj) => obj is LocalMinima minima && this.Equals(minima);
public override int GetHashCode() => this.Vertex.GetHashCode();
public bool Equals(LocalMinima other) => ReferenceEquals(this.Vertex, other.Vertex);
}
/// <summary>
/// Orders local minima so higher Y-values are processed first during the sweep.
/// </summary>
internal sealed class LocalMinimaComparer : IComparer<LocalMinima>
{
public int Compare(LocalMinima locMin1, LocalMinima locMin2)
=> locMin2.Vertex.Point.Y.CompareTo(locMin1.Vertex.Point.Y);
}
}
+163
View File
@@ -0,0 +1,163 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Collections.Generic;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Captures a clipped contour, its topology, and its ownership hierarchy.
/// </summary>
internal sealed class OutputRecord
{
/// <summary>
/// Gets or sets the stable index assigned when the record is pooled.
/// </summary>
public int Index { get; set; }
/// <summary>
/// Gets or sets the number of output points in the contour.
/// </summary>
public int OutputPointCount { get; set; }
/// <summary>
/// Gets or sets the containing output record, if any.
/// </summary>
public OutputRecord? Owner { get; set; }
/// <summary>
/// Gets or sets the front edge that defines the output orientation.
/// </summary>
public ActiveEdge? FrontEdge { get; set; }
/// <summary>
/// Gets or sets the back edge that defines the output orientation.
/// </summary>
public ActiveEdge? BackEdge { get; set; }
/// <summary>
/// Gets or sets the circular linked list of output points.
/// </summary>
public OutputPoint? Points { get; set; }
/// <summary>
/// Gets or sets the cached bounds for ownership tests.
/// </summary>
public Box2 Bounds { get; set; }
/// <summary>
/// Gets or sets the temporary contour used during bounds checks.
/// </summary>
public List<Vertex> Path { get; set; } = [];
/// <summary>
/// Gets or sets split indices used to resolve complex self-intersections.
/// </summary>
public List<int>? Splits { get; set; }
/// <summary>
/// Gets or sets the cached split ownership used to avoid recursion.
/// </summary>
public OutputRecord? RecursiveSplit { get; set; }
}
/// <summary>
/// Represents a vertex in the output contour linked list.
/// </summary>
internal sealed class OutputPoint
{
#pragma warning disable SA1401 // Hot output ring traversal uses fields to avoid accessor overhead.
/// <summary>
/// The vertex coordinate.
/// </summary>
public Vertex Point;
/// <summary>
/// The next point in the linked list.
/// </summary>
public OutputPoint? Next;
/// <summary>
/// The previous point in the linked list.
/// </summary>
public OutputPoint Prev;
/// <summary>
/// The owning output record.
/// </summary>
public OutputRecord OutputRecord;
/// <summary>
/// The horizontal segment reference used for joins.
/// </summary>
public HorizontalSegment? HorizontalSegment;
#pragma warning restore SA1401
/// <summary>
/// Initializes a new instance of the <see cref="OutputPoint"/> class.
/// </summary>
public OutputPoint(Vertex point, OutputRecord outputRecord)
{
this.Point = point;
this.OutputRecord = outputRecord;
this.Next = this;
this.Prev = this;
this.HorizontalSegment = null;
}
}
/// <summary>
/// Captures a pending horizontal segment to be joined.
/// </summary>
internal sealed class HorizontalSegment
{
/// <summary>
/// Initializes a new instance of the <see cref="HorizontalSegment"/> class.
/// </summary>
public HorizontalSegment(OutputPoint op)
{
this.LeftPoint = op;
this.RightPoint = null;
this.LeftToRight = true;
}
/// <summary>
/// Gets or sets the left-most point of the segment.
/// </summary>
public OutputPoint? LeftPoint { get; set; }
/// <summary>
/// Gets or sets the right-most point of the segment.
/// </summary>
public OutputPoint? RightPoint { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the segment runs left-to-right.
/// </summary>
public bool LeftToRight { get; set; }
}
/// <summary>
/// Stores a pair of horizontal edges to be joined.
/// </summary>
internal sealed class HorizontalJoin
{
/// <summary>
/// Initializes a new instance of the <see cref="HorizontalJoin"/> class.
/// </summary>
public HorizontalJoin(OutputPoint leftToRight, OutputPoint rightToLeft)
{
this.LeftToRight = leftToRight;
this.RightToLeft = rightToLeft;
}
/// <summary>
/// Gets or sets the left-to-right point of the join.
/// </summary>
public OutputPoint? LeftToRight { get; set; }
/// <summary>
/// Gets or sets the right-to-left point of the join.
/// </summary>
public OutputPoint? RightToLeft { get; set; }
}
}
+38
View File
@@ -0,0 +1,38 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Encodes path commands used by the stroker path-emission pipeline.
/// </summary>
[Flags]
internal enum PathCommand : byte
{
/// <summary>
/// Marks the end of a command stream.
/// </summary>
Stop = 0,
/// <summary>
/// Starts a new contour at the supplied vertex.
/// </summary>
MoveTo = 1,
/// <summary>
/// Emits a line segment to the supplied vertex.
/// </summary>
LineTo = 2,
/// <summary>
/// Terminates the current contour and applies path flags.
/// </summary>
EndPoly = 0x0F,
/// <summary>
/// Bit mask for extracting the command portion of a value.
/// </summary>
Mask = 0x0F
}
}
+54
View File
@@ -0,0 +1,54 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Provides helper methods for querying <see cref="PathCommand"/> values.
/// </summary>
internal static class PathCommandExtensions
{
/// <summary>
/// Returns whether the command emits a vertex coordinate.
/// </summary>
/// <param name="command">The command to evaluate.</param>
/// <returns>
/// <see langword="true"/> when the command is a vertex-emitting command; otherwise, <see langword="false"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Vertex(this PathCommand command) => command is >= PathCommand.MoveTo and < PathCommand.EndPoly;
/// <summary>
/// Returns whether the command is <see cref="PathCommand.Stop"/>.
/// </summary>
/// <param name="command">The command to evaluate.</param>
/// <returns><see langword="true"/> if the command is stop; otherwise, <see langword="false"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Stop(this PathCommand command) => command == PathCommand.Stop;
/// <summary>
/// Returns whether the command is <see cref="PathCommand.MoveTo"/>.
/// </summary>
/// <param name="command">The command to evaluate.</param>
/// <returns><see langword="true"/> if the command is move-to; otherwise, <see langword="false"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool MoveTo(this PathCommand command) => command == PathCommand.MoveTo;
/// <summary>
/// Returns whether the masked command type is <see cref="PathCommand.EndPoly"/>.
/// </summary>
/// <param name="command">The command to evaluate.</param>
/// <returns><see langword="true"/> if the command type is end-poly; otherwise, <see langword="false"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool EndPoly(this PathCommand command) => (command & PathCommand.Mask) == PathCommand.EndPoly;
/// <summary>
/// Extracts the close-path flag from the command.
/// </summary>
/// <param name="command">The command to evaluate.</param>
/// <returns>The command value masked with <see cref="PathFlags.Close"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetCloseFlag(this PathCommand command) => (int)command & (int)PathFlags.Close;
}
}
+38
View File
@@ -0,0 +1,38 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Flags that annotate <see cref="PathCommand.EndPoly"/> path-termination commands.
/// </summary>
[Flags]
internal enum PathFlags : byte
{
/// <summary>
/// No path flags are set.
/// </summary>
None = 0,
/// <summary>
/// Marks a counter-clockwise contour orientation.
/// </summary>
Ccw = 0x10,
/// <summary>
/// Marks a clockwise contour orientation.
/// </summary>
Cw = 0x20,
/// <summary>
/// Marks the contour as closed.
/// </summary>
Close = 0x40,
/// <summary>
/// Bit mask for extracting the flag portion of a command value.
/// </summary>
Mask = 0xF0
}
}
+25
View File
@@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.PolygonClipper {
/// <summary>
/// Describes the relationship between a point and a polygon.
/// </summary>
internal enum PointInPolygonResult
{
/// <summary>
/// The point lies on the polygon boundary.
/// </summary>
On = 0,
/// <summary>
/// The point lies strictly inside the polygon.
/// </summary>
Inside = 1,
/// <summary>
/// The point lies outside the polygon.
/// </summary>
Outside = 2
}
}
+192
View File
@@ -0,0 +1,192 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Represents a complex polygon.
/// </summary>
#pragma warning disable CA1710 // Identifiers should have correct suffix
public sealed class Polygon : IReadOnlyCollection<Contour>
#pragma warning restore CA1710 // Identifiers should have correct suffix
{
/// <summary>
/// The collection of contours that make up the polygon.
/// </summary>
private readonly List<Contour> contours;
/// <summary>
/// Initializes a new instance of the <see cref="Polygon"/> class.
/// </summary>
public Polygon()
=> this.contours = [];
/// <summary>
/// Initializes a new instance of the <see cref="Polygon"/> class with a contour capacity.
/// </summary>
/// <param name="capacity">The initial contour capacity.</param>
public Polygon(int capacity)
=> this.contours = new List<Contour>(capacity);
/// <summary>
/// Gets the number of contours in the polygon.
/// </summary>
public int Count
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.contours.Count;
}
/// <summary>
/// Gets the total number of vertices across all contours in the polygon.
/// </summary>
/// <returns>The total vertex count.</returns>
public int VertexCount
{
get
{
int count = 0;
for (int i = 0; i < this.contours.Count; i++)
{
count += this.contours[i].Count;
}
return count;
}
}
/// <summary>
/// Gets the contour at the specified index.
/// </summary>
/// <param name="index">The index of the contour.</param>
/// <returns>The <see cref="Contour"/> at the given index.</returns>
public Contour this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.contours[index];
}
/// <summary>
/// Joins another polygon to this instance.
/// </summary>
/// <param name="polygon">The polygon to join.</param>
public void Join(Polygon polygon)
{
int size = this.Count;
for (int i = 0; i < polygon.contours.Count; ++i)
{
Contour contour = polygon.contours[i];
this.Add(contour);
this.GetLastContour().ClearHoles();
for (int j = 0; j < contour.HoleCount; ++j)
{
this.GetLastContour().AddHoleIndex(contour.GetHoleIndex(j) + size);
}
}
}
/// <summary>
/// Gets the bounding box.
/// </summary>
/// <returns>The <see cref="Box2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Box2 GetBoundingBox()
{
if (this.Count == 0)
{
return default;
}
Box2 b = this.contours[0].GetBoundingBox();
for (int i = 1; i < this.Count; i++)
{
b = b.Add(this.contours[i].GetBoundingBox());
}
return b;
}
/// <summary>
/// Translates the polygon by the specified x and y values.
/// </summary>
/// <param name="x">The x-coordinate offset.</param>
/// <param name="y">The y-coordinate offset.</param>
public void Translate(double x, double y)
{
for (int i = 0; i < this.contours.Count; i++)
{
this.contours[i].Translate(x, y);
}
}
/// <summary>
/// Adds a contour to the end of the contour collection.
/// </summary>
/// <param name="contour">The contour to add.</param>
public void Add(Contour contour) => this.contours.Add(contour);
/// <summary>
/// Gets the last contour in the polygon.
/// </summary>
/// <returns>The last <see cref="Contour"/> in the collection.</returns>
public Contour GetLastContour() => this.contours[^1];
/// <summary>
/// Clears all contours from the polygon.
/// </summary>
public void Clear() => this.contours.Clear();
/// <summary>
/// Creates a deep copy of this polygon and all of its contours.
/// </summary>
/// <returns>A detached polygon copy.</returns>
public Polygon DeepClone()
{
Polygon clone = new(this.contours.Count);
for (int i = 0; i < this.contours.Count; i++)
{
clone.contours.Add(this.contours[i].DeepClone());
}
return clone;
}
/// <inheritdoc/>
public IEnumerator<Contour> GetEnumerator()
=> ((IEnumerable<Contour>)this.contours).GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
=> ((IEnumerable)this.contours).GetEnumerator();
/// <summary>
/// Creates a string useful for debugging.
/// </summary>
/// <returns>The <see cref="string"/>.</returns>
public string ToDebugString()
{
StringBuilder stringBuilder = new();
stringBuilder.AppendLine("[");
foreach (Contour contour in this.contours)
{
stringBuilder.AppendLine(" [");
foreach (Vertex vertex in contour)
{
stringBuilder.AppendLine(" new Vertex(" + vertex.X + ", " + vertex.Y + "),");
}
stringBuilder.AppendLine(" ],");
}
stringBuilder.AppendLine("];");
return stringBuilder.ToString();
}
}
}
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>SixLabors.PolygonClipper</AssemblyName>
<AssemblyTitle>SixLabors.PolygonClipper</AssemblyTitle>
<RootNamespace>SixLabors.PolygonClipper</RootNamespace>
<PackageId>SixLabors.PolygonClipper</PackageId>
<PackageIcon>sixlabors.polygonclipper.128.png</PackageIcon>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<RepositoryUrl Condition="'$(RepositoryUrl)' == ''">https://github.com/SixLabors/PolygonClipper/</RepositoryUrl>
<PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl>
<PackageTags></PackageTags>
<Description></Description>
<Configurations>Debug;Release</Configurations>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
<PropertyGroup>
<!-- <CodeAnalysisRuleSet>..\sixlabors.ruleset</CodeAnalysisRuleSet> -->
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<!--Bump to v1.0 prior to tagged release.-->
<MinVerMinimumMajorMinor>1.0</MinVerMinimumMajorMinor>
</PropertyGroup>
<ItemGroup>
<None Include="..\LICENSE" Pack="true" PackagePath="" />
<!-- <None Include="..\..\shared-infrastructure\branding\icons\polygonclipper\sixlabors.polygonclipper.128.png" Pack="true" PackagePath="" /> -->
<!-- <None Include="..\..\SixLabors.PolygonClipper.props" Pack="true" PackagePath="build" /> -->
</ItemGroup>
<Import Project="..\SharedInfrastructure.projitems" Label="Shared" />
</Project>
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.PolygonClipper {
/// <summary>
/// Specifies the type of a polygon in a boolean operation.
/// </summary>
internal enum PolygonType
{
/// <summary>
/// Represents the subject polygon in a boolean operation.
/// </summary>
Subject = 0,
/// <summary>
/// Represents the clipping polygon in a boolean operation.
/// </summary>
Clipping = 1
}
}
+860
View File
@@ -0,0 +1,860 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Provides utility methods for performing geometric calculations related to polygons, such as calculating signed areas
/// and finding intersections of line segments.
/// </summary>
internal static class PolygonUtilities
{
/// <summary>
/// Returns the signed area of a triangle.
/// </summary>
/// <param name="p0">The first point.</param>
/// <param name="p1">The second point.</param>
/// <param name="p2">The third point.</param>
/// <returns>The <see cref="double"/> area.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double SignedArea(in Vertex p0, in Vertex p1, in Vertex p2)
=> Vertex.Cross(p0 - p2, p1 - p2);
/// <summary>
/// Finds the intersection of two line segments, constraining results to their intersection bounding box.
/// </summary>
/// <param name="seg0">The first segment.</param>
/// <param name="seg1">The second segment.</param>
/// <param name="pi0">The first intersection point.</param>
/// <param name="pi1">The second intersection point (if overlap occurs).</param>
/// <returns>
/// An <see cref="int"/> indicating the number of intersection points:
/// - Returns 0 if there is no intersection.
/// - Returns 1 if the segments intersect at a single point.
/// - Returns 2 if the segments overlap.
/// </returns>
public static int FindIntersection(in Segment seg0, in Segment seg1, out Vertex pi0, out Vertex pi1)
{
pi0 = default;
pi1 = default;
if (!TryGetIntersectionBoundingBox(seg0.Source, seg0.Target, seg1.Source, seg1.Target, out Box2? bbox))
{
return 0;
}
int interResult = FindIntersectionImpl(seg0, seg1, out pi0, out pi1);
if (interResult == 1)
{
pi0 = ConstrainToBoundingBox(pi0, bbox.Value);
}
else if (interResult == 2)
{
pi0 = ConstrainToBoundingBox(pi0, bbox.Value);
pi1 = ConstrainToBoundingBox(pi1, bbox.Value);
}
return interResult;
}
/// <summary>
/// Finds the intersection of two line segments.
/// </summary>
/// <param name="seg0">The first line segment.</param>
/// <param name="seg1">The second line segment.</param>
/// <param name="pi0">
/// The first intersection point (if any). If the segments intersect at a single point, this will contain the intersection point.
/// If the segments overlap, this will contain the start of the overlapping segment.
/// </param>
/// <param name="pi1">
/// The second intersection point (if any). If the segments overlap, this will contain the end of the overlapping segment.
/// </param>
/// <returns>
/// An <see cref="int"/> indicating the number of intersection points:
/// - Returns 0 if there is no intersection.
/// - Returns 1 if the segments intersect at a single point.
/// - Returns 2 if the segments overlap.
/// </returns>
private static int FindIntersectionImpl(in Segment seg0, in Segment seg1, out Vertex pi0, out Vertex pi1)
{
pi0 = default;
pi1 = default;
Vertex a1 = seg0.Source;
Vertex a2 = seg1.Source;
Vertex va = seg0.Target - a1;
Vertex vb = seg1.Target - a2;
Vertex e = a2 - a1;
double kross = Vertex.Cross(va, vb);
double sqrKross = kross * kross;
double sqrLenA = Vertex.Dot(va, va);
if (sqrKross > 0)
{
// Lines of the segments are not parallel
double s = Vertex.Cross(e, vb) / kross;
if (s is < 0 or > 1)
{
return 0;
}
double t = Vertex.Cross(e, va) / kross;
if (t is < 0 or > 1)
{
return 0;
}
// If s or t is exactly 0 or 1, the intersection is on an endpoint
if (s is 0 or 1)
{
// On an endpoint of line segment a
pi0 = MidPoint(a1, s, va);
return 1;
}
if (t is 0 or 1)
{
// On an endpoint of line segment b
pi0 = MidPoint(a2, t, vb);
return 1;
}
// Intersection of lines is a point on each segment
pi0 = a1 + (s * va);
return 1;
}
// Lines are parallel; check if they are collinear
kross = Vertex.Cross(e, va);
sqrKross = kross * kross;
if (sqrKross > 0)
{
// Lines of the segments are different
return 0;
}
// Segments are collinear, check for overlap
double sa = Vertex.Dot(va, e) / sqrLenA;
double sb = sa + (Vertex.Dot(va, vb) / sqrLenA);
double smin = Math.Min(sa, sb);
double smax = Math.Max(sa, sb);
if (smin <= 1 && smax >= 0)
{
if (smin == 1)
{
pi0 = MidPoint(a1, smin, va);
return 1;
}
if (smax == 0)
{
pi0 = MidPoint(a1, smax, va);
return 1;
}
pi0 = MidPoint(a1, Math.Max(smin, 0), va);
pi1 = MidPoint(a1, Math.Min(smax, 1), va);
return 2;
}
return 0;
}
/// <summary>
/// Computes the bounding box of the intersection area of two line segments.
/// </summary>
/// <param name="a1">The first point of the first segment.</param>
/// <param name="a2">The second point of the first segment.</param>
/// <param name="b1">The first point of the second segment.</param>
/// <param name="b2">The second point of the second segment.</param>
/// <param name="result">The intersection bounding box if one exists, otherwise null.</param>
/// <returns>
/// <see langword="true"/> if the segments intersect; otherwise, <see langword="false"/>.
/// </returns>
private static bool TryGetIntersectionBoundingBox(
in Vertex a1,
in Vertex a2,
in Vertex b1,
in Vertex b2,
[NotNullWhen(true)] out Box2? result)
{
Vertex minA = Vertex.Min(a1, a2);
Vertex maxA = Vertex.Max(a1, a2);
Vertex minB = Vertex.Min(b1, b2);
Vertex maxB = Vertex.Max(b1, b2);
Vertex interMin = Vertex.Max(minA, minB);
Vertex interMax = Vertex.Min(maxA, maxB);
if (interMin.X <= interMax.X && interMin.Y <= interMax.Y)
{
result = new Box2(interMin, interMax);
return true;
}
result = null;
return false;
}
/// <summary>
/// Constrains a point to the given bounding box.
/// </summary>
/// <param name="p">The point to constrain.</param>
/// <param name="bbox">The bounding box.</param>
/// <returns>The constrained point.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vertex ConstrainToBoundingBox(in Vertex p, in Box2 bbox)
=> Vertex.Min(Vertex.Max(p, bbox.Min), bbox.Max);
/// <summary>
/// Computes the point at a given fractional distance adouble a directed line segment.
/// </summary>
/// <param name="p">The starting vertex of the segment.</param>
/// <param name="s">The scalar factor representing the fractional distance adouble the segment.</param>
/// <param name="d">The direction vector of the segment.</param>
/// <returns>The interpolated vertex at the given fractional distance.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vertex MidPoint(in Vertex p, double s, in Vertex d) => p + (s * d);
/// <summary>
/// Returns the dot product of the vectors AB and BC.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double Dot(in Vertex a, in Vertex b, in Vertex c)
=> Vertex.Dot(b - a, c - b);
/// <summary>
/// Returns the cross product of the vectors AB and BC.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double Cross(in Vertex a, in Vertex b, in Vertex c)
=> Vertex.Cross(b - a, c - b);
/// <summary>
/// Returns the sign of the cross product of the vectors AB and BC.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int CrossSign(in Vertex a, in Vertex b, in Vertex c)
{
double crossValueInt = Cross(a, b, c);
if (crossValueInt == 0)
{
return 0;
}
return crossValueInt > 0 ? 1 : -1;
}
/// <summary>
/// Returns true when three vertices are collinear.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsCollinear(in Vertex a, in Vertex shared, in Vertex b)
=> CrossSign(a, shared, b) == 0;
/// <summary>
/// Computes the signed area of a contour.
/// </summary>
public static double Area(List<Vertex> path)
{
int count = path.Count;
if (count < 3)
{
return 0D;
}
double area = 0;
Vertex prev = path[count - 1];
for (int i = 0; i < count; i++)
{
Vertex current = path[i];
area += (prev.Y + current.Y) * (prev.X - current.X);
prev = current;
}
return area * 0.5D;
}
/// <summary>
/// Computes the squared perpendicular distance from a point to a line segment.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double PerpendicularDistanceSquared(in Vertex point, in Vertex line1, in Vertex line2)
{
Vertex toPoint = point - line1;
Vertex direction = line2 - line1;
double lengthSquared = Vertex.Dot(direction, direction);
if (lengthSquared == 0D)
{
return 0D;
}
double cross = Vertex.Cross(toPoint, direction);
return (cross * cross) / lengthSquared;
}
/// <summary>
/// Finds the intersection of two line segments, including endpoints.
/// </summary>
public static bool TryGetLineIntersection(
in Vertex a1,
in Vertex a2,
in Vertex b1,
in Vertex b2,
out Vertex intersection)
{
double dy1 = a2.Y - a1.Y;
double dx1 = a2.X - a1.X;
double dy2 = b2.Y - b1.Y;
double dx2 = b2.X - b1.X;
double det = (dy1 * dx2) - (dy2 * dx1);
if (det == 0D)
{
intersection = default;
return false;
}
double t = (((a1.X - b1.X) * dy2) - ((a1.Y - b1.Y) * dx2)) / det;
if (t <= 0D)
{
intersection = a1;
return true;
}
if (t >= 1D)
{
intersection = a2;
return true;
}
intersection = new Vertex(a1.X + (t * dx1), a1.Y + (t * dy1));
return true;
}
/// <summary>
/// Projects a point onto a segment and returns the closest point.
/// </summary>
public static Vertex ClosestPointOnSegment(in Vertex point, in Vertex seg1, in Vertex seg2)
{
if (seg1 == seg2)
{
return seg1;
}
double dx = seg2.X - seg1.X;
double dy = seg2.Y - seg1.Y;
double q = (((point.X - seg1.X) * dx) + ((point.Y - seg1.Y) * dy)) / ((dx * dx) + (dy * dy));
// Clamp to segment bounds so we always return the closest point on the finite segment.
q = Math.Clamp(q, 0D, 1D);
return new Vertex(seg1.X + (q * dx), seg1.Y + (q * dy));
}
/// <summary>
/// Returns true when two segments intersect.
/// </summary>
public static bool SegmentsIntersect(in Vertex a1, in Vertex a2, in Vertex b1, in Vertex b2, bool inclusive = false)
{
// Uses cross-product tests to solve a1 + d1 * t == b1 + d2 * u.
// cp is the denominator (cross of directions); cp == 0 means parallel/collinear.
Vertex d1 = a2 - a1;
Vertex d2 = b2 - b1;
double cp = Vertex.Cross(d2, d1);
if (cp == 0)
{
return false;
}
if (inclusive)
{
// Inclusive mode allows intersections at endpoints.
double t = Vertex.Cross(a1 - b1, d2);
if (t == 0)
{
return true;
}
if (t > 0)
{
if (cp < 0 || t > cp)
{
// t outside [0, cp] once sign is normalized.
return false;
}
}
else if (cp > 0 || t < cp)
{
return false;
}
t = Vertex.Cross(a1 - b1, d1);
if (t == 0)
{
return true;
}
if (t > 0)
{
// t within bounds for the second segment.
return cp > 0 && t <= cp;
}
return cp < 0 && t >= cp;
}
// Exclusive mode requires the intersection to be strictly inside both segments.
double t2 = Vertex.Cross(a1 - b1, d2);
if (t2 == 0)
{
return false;
}
if (t2 > 0)
{
if (cp < 0 || t2 >= cp)
{
// Reject if t2 is outside the open interval.
return false;
}
}
else if (cp > 0 || t2 <= cp)
{
return false;
}
t2 = Vertex.Cross(a1 - b1, d1);
if (t2 == 0)
{
return false;
}
if (t2 > 0)
{
// Both parameters are inside open intervals.
return cp > 0 && t2 < cp;
}
return cp < 0 && t2 > cp;
}
/// <summary>
/// Computes the bounding box of a contour.
/// </summary>
public static Box2 GetBounds(List<Vertex> path)
{
if (path.Count == 0)
{
return default;
}
double minX = double.MaxValue;
double minY = double.MaxValue;
double maxX = double.MinValue;
double maxY = double.MinValue;
for (int i = 0; i < path.Count; i++)
{
Vertex pt = path[i];
if (pt.X < minX)
{
minX = pt.X;
}
if (pt.X > maxX)
{
maxX = pt.X;
}
if (pt.Y < minY)
{
minY = pt.Y;
}
if (pt.Y > maxY)
{
maxY = pt.Y;
}
}
if (minX == double.MaxValue)
{
return default;
}
return new Box2(new Vertex(minX, minY), new Vertex(maxX, maxY));
}
/// <summary>
/// Returns the midpoint of a contour's bounding box.
/// </summary>
private static Vertex GetBoundsMidPoint(List<Vertex> path) => GetBounds(path).MidPoint();
/// <summary>
/// Determines whether a point is inside a contour.
/// </summary>
public static PointInPolygonResult PointInPolygon(in Vertex point, List<Vertex> polygon)
{
int len = polygon.Count;
int start = 0;
if (len < 3)
{
return PointInPolygonResult.Outside;
}
while (start < len && polygon[start].Y == point.Y)
{
start++;
}
if (start == len)
{
return PointInPolygonResult.Outside;
}
bool isAbove = polygon[start].Y < point.Y;
bool startingAbove = isAbove;
int val = 0;
int i = start + 1;
int end = len;
while (true)
{
if (i == end)
{
if (end == 0 || start == 0)
{
break;
}
end = start;
i = 0;
}
if (isAbove)
{
while (i < end && polygon[i].Y < point.Y)
{
i++;
}
}
else
{
while (i < end && polygon[i].Y > point.Y)
{
i++;
}
}
if (i == end)
{
continue;
}
Vertex curr = polygon[i];
Vertex prev = i > 0 ? polygon[i - 1] : polygon[len - 1];
if (curr.Y == point.Y)
{
if (curr.X == point.X ||
(curr.Y == prev.Y && ((point.X < prev.X) != (point.X < curr.X))))
{
return PointInPolygonResult.On;
}
i++;
if (i == start)
{
break;
}
continue;
}
if (point.X < curr.X && point.X < prev.X)
{
// no-op
}
else if (point.X > prev.X && point.X > curr.X)
{
val = 1 - val;
}
else
{
int cps2 = CrossSign(prev, curr, point);
if (cps2 == 0)
{
return PointInPolygonResult.On;
}
if ((cps2 < 0) == isAbove)
{
val = 1 - val;
}
}
isAbove = !isAbove;
i++;
}
if (isAbove == startingAbove)
{
return val == 0 ? PointInPolygonResult.Outside : PointInPolygonResult.Inside;
}
if (i == len)
{
i = 0;
}
int cps = i == 0
? CrossSign(polygon[len - 1], polygon[0], point)
: CrossSign(polygon[i - 1], polygon[i], point);
if (cps == 0)
{
return PointInPolygonResult.On;
}
if ((cps < 0) == isAbove)
{
val = 1 - val;
}
return val == 0 ? PointInPolygonResult.Outside : PointInPolygonResult.Inside;
}
/// <summary>
/// Returns true if the outer contour contains the inner contour.
/// </summary>
private static bool PathContainsPath(List<Vertex> inner, List<Vertex> outer)
{
PointInPolygonResult pip = PointInPolygonResult.On;
for (int i = 0; i < inner.Count; i++)
{
switch (PointInPolygon(inner[i], outer))
{
case PointInPolygonResult.Outside:
if (pip == PointInPolygonResult.Outside)
{
return false;
}
pip = PointInPolygonResult.Outside;
break;
case PointInPolygonResult.Inside:
if (pip == PointInPolygonResult.Inside)
{
return true;
}
pip = PointInPolygonResult.Inside;
break;
default:
break;
}
}
Vertex midpoint = GetBoundsMidPoint(inner);
return PointInPolygon(midpoint, outer) != PointInPolygonResult.Outside;
}
/// <summary>
/// Returns true if the outer contour contains the inner contour.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Path2ContainsPath1(List<Vertex> inner, List<Vertex> outer) => PathContainsPath(inner, outer);
/// <summary>
/// Finds the intersection of two line segments, constraining results to their intersection bounding box.
/// </summary>
/// <param name="a1">The first point of the first segment.</param>
/// <param name="a2">The second point of the first segment.</param>
/// <param name="b1">The first point of the second segment.</param>
/// <param name="b2">The second point of the second segment.</param>
/// <param name="pi0">The first intersection point.</param>
/// <param name="pi1">The second intersection point (if overlap occurs).</param>
/// <returns>
/// An <see cref="int"/> indicating the number of intersection points:
/// - Returns 0 if there is no intersection.
/// - Returns 1 if the segments intersect at a single point.
/// - Returns 2 if the segments overlap.
/// </returns>
public static int FindIntersection(in Vertex a1, in Vertex a2, in Vertex b1, in Vertex b2, out Vertex pi0, out Vertex pi1)
{
pi0 = default;
pi1 = default;
if (!TryGetIntersectionBoundingBox(a1, a2, b1, b2, out Box2 bbox))
{
return 0;
}
int interResult = FindIntersectionImpl(a1, a2, b1, b2, out pi0, out pi1);
if (interResult == 1)
{
pi0 = ConstrainToBoundingBox(pi0, bbox);
}
else if (interResult == 2)
{
pi0 = ConstrainToBoundingBox(pi0, bbox);
pi1 = ConstrainToBoundingBox(pi1, bbox);
}
return interResult;
}
/// <summary>
/// Finds the intersection of two line segments.
/// </summary>
/// <param name="a1">The first point of the first segment.</param>
/// <param name="a2">The second point of the first segment.</param>
/// <param name="b1">The first point of the second segment.</param>
/// <param name="b2">The second point of the second segment.</param>
/// <param name="pi0">
/// The first intersection point (if any). If the segments intersect at a single point, this will contain the intersection point.
/// If the segments overlap, this will contain the start of the overlapping segment.
/// </param>
/// <param name="pi1">
/// The second intersection point (if any). If the segments overlap, this will contain the end of the overlapping segment.
/// </param>
/// <returns>
/// An <see cref="int"/> indicating the number of intersection points:
/// - Returns 0 if there is no intersection.
/// - Returns 1 if the segments intersect at a single point.
/// - Returns 2 if the segments overlap.
/// </returns>
private static int FindIntersectionImpl(in Vertex a1, in Vertex a2, in Vertex b1, in Vertex b2, out Vertex pi0, out Vertex pi1)
{
pi0 = default;
pi1 = default;
Vertex va = a2 - a1;
Vertex vb = b2 - b1;
Vertex e = b1 - a1;
double kross = Vertex.Cross(va, vb);
double sqrKross = kross * kross;
double sqrLenA = Vertex.Dot(va, va);
if (sqrKross > 0D)
{
// Lines of the segments are not parallel.
double s = Vertex.Cross(e, vb) / kross;
if (s is < 0D or > 1D)
{
return 0;
}
double t = Vertex.Cross(e, va) / kross;
if (t is < 0D or > 1D)
{
return 0;
}
// If s or t is exactly 0 or 1, the intersection is on an endpoint.
if (s is 0D or 1D)
{
// On an endpoint of segment a.
pi0 = MidPoint(a1, s, va);
return 1;
}
if (t is 0D or 1D)
{
// On an endpoint of segment b.
pi0 = MidPoint(a2, t, vb);
return 1;
}
// Intersection of lines is a point on each segment.
pi0 = MidPoint(a1, s, va);
return 1;
}
// Lines are parallel; check if they are collinear.
kross = Vertex.Cross(e, va);
sqrKross = kross * kross;
if (sqrKross > 0D)
{
// Parallel but not collinear.
return 0;
}
if (sqrLenA == 0D)
{
return 0;
}
// Segments are collinear, check 1D overlap in segment-a parameter space.
double sa = Vertex.Dot(va, e) / sqrLenA;
double sb = sa + (Vertex.Dot(va, vb) / sqrLenA);
double smin = Math.Min(sa, sb);
double smax = Math.Max(sa, sb);
if (smin <= 1D && smax >= 0D)
{
if (smin == 1D)
{
pi0 = MidPoint(a1, smin, va);
return 1;
}
if (smax == 0D)
{
pi0 = MidPoint(a1, smax, va);
return 1;
}
pi0 = MidPoint(a1, Math.Max(smin, 0D), va);
pi1 = MidPoint(a1, Math.Min(smax, 1D), va);
return pi0 == pi1 ? 1 : 2;
}
return 0;
}
/// <summary>
/// Computes the bounding box of the intersection area of two line segments.
/// </summary>
/// <param name="a1">The first point of the first segment.</param>
/// <param name="a2">The second point of the first segment.</param>
/// <param name="b1">The first point of the second segment.</param>
/// <param name="b2">The second point of the second segment.</param>
/// <param name="result">The intersection bounding box if one exists, otherwise null.</param>
/// <returns>
/// <see langword="true"/> if the segments intersect; otherwise, <see langword="false"/>.
/// </returns>
private static bool TryGetIntersectionBoundingBox(
in Vertex a1,
in Vertex a2,
in Vertex b1,
in Vertex b2,
out Box2 result)
{
Vertex minA = Vertex.Min(a1, a2);
Vertex maxA = Vertex.Max(a1, a2);
Vertex minB = Vertex.Min(b1, b2);
Vertex maxB = Vertex.Max(b1, b2);
Vertex interMin = Vertex.Max(minA, minB);
Vertex interMax = Vertex.Min(maxA, maxB);
if (interMin.X <= interMax.X && interMin.Y <= interMax.Y)
{
result = new Box2(interMin, interMax);
return true;
}
result = default;
return false;
}
}
}
+25
View File
@@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.PolygonClipper {
/// <summary>
/// Represents the result transition for a sweep event.
/// </summary>
public enum ResultTransition
{
/// <summary>
/// The event does not contribute to the result.
/// </summary>
NonContributing = -1,
/// <summary>
/// The event transitions within the result.
/// </summary>
Neutral = 0,
/// <summary>
/// The event contributes to the result.
/// </summary>
Contributing = 1
}
}
+158
View File
@@ -0,0 +1,158 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Manages scanline ordering and local minima scheduling for the sweep line.
/// </summary>
/// <remarks>
/// This type keeps local minima in sorted order and seeds scanlines from their Y coordinates.
/// It also provides ordered scanline pop/insert operations used during the sweep.
/// </remarks>
internal sealed class ScanlineSchedule
{
private static readonly LocalMinimaComparer LocalMinimaComparerInstance = new();
private ArrayBuilder<LocalMinima> localMinima;
private readonly List<double> scanlines;
private int localMinimaIndex;
private bool isLocalMinimaSorted;
/// <summary>
/// Initializes a new instance of the <see cref="ScanlineSchedule"/> class.
/// </summary>
public ScanlineSchedule()
{
this.localMinima = new ArrayBuilder<LocalMinima>(16);
this.scanlines = [];
}
/// <summary>
/// Gets the number of registered local minima.
/// </summary>
public int LocalMinimaCount => this.localMinima.Length;
/// <summary>
/// Gets a retained-capacity score used to decide pooling reuse.
/// </summary>
public int RetainedCapacityScore => this.localMinima.Capacity + this.scanlines.Capacity;
/// <summary>
/// Adds a local minima to the schedule.
/// </summary>
/// <param name="localMinima">The minima to append.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddLocalMinima(in LocalMinima localMinima)
{
this.localMinima.Add(localMinima);
this.isLocalMinimaSorted = false;
}
/// <summary>
/// Marks the local minima list as unsorted.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MarkDirty() => this.isLocalMinimaSorted = false;
/// <summary>
/// Clears all minima and scanline state.
/// </summary>
public void Clear()
{
this.localMinima.Clear();
this.scanlines.Clear();
this.localMinimaIndex = 0;
this.isLocalMinimaSorted = false;
}
/// <summary>
/// Clears scanlines while keeping local minima intact.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ClearScanlines() => this.scanlines.Clear();
/// <summary>
/// Sorts minima (if needed) and seeds the scanline list.
/// </summary>
public void Reset()
{
if (!this.isLocalMinimaSorted)
{
this.localMinima.Sort(LocalMinimaComparerInstance);
this.isLocalMinimaSorted = true;
}
this.scanlines.Clear();
int localMinimaCount = this.localMinima.Length;
this.scanlines.EnsureCapacity(localMinimaCount);
for (int i = localMinimaCount - 1; i >= 0; i--)
{
this.scanlines.Add(this.localMinima[i].Vertex.Point.Y);
}
this.localMinimaIndex = 0;
}
/// <summary>
/// Determines whether the next local minima is on the given scanline.
/// </summary>
/// <param name="y">The scanline Y coordinate.</param>
/// <returns><see langword="true"/> if a minima exists at this Y.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool HasLocalMinimaAtY(double y)
=> this.localMinimaIndex < this.localMinima.Length &&
this.localMinima[this.localMinimaIndex].Vertex.Point.Y == y;
/// <summary>
/// Pops the next local minima from the schedule.
/// </summary>
/// <returns>The next local minima.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public LocalMinima PopLocalMinima() => this.localMinima[this.localMinimaIndex++];
/// <summary>
/// Inserts a scanline value into the ordered list.
/// </summary>
/// <param name="y">The scanline Y coordinate.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void InsertScanline(double y)
{
int index = this.scanlines.BinarySearch(y);
if (index >= 0)
{
return;
}
index = ~index;
this.scanlines.Insert(index, y);
}
/// <summary>
/// Pops the next scanline from the schedule.
/// </summary>
/// <param name="y">The popped scanline value.</param>
/// <returns><see langword="true"/> when a scanline was available.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryPopScanline(out double y)
{
int count = this.scanlines.Count - 1;
if (count < 0)
{
y = 0;
return false;
}
y = this.scanlines[count];
this.scanlines.RemoveAt(count--);
while (count >= 0 && y == this.scanlines[count])
{
this.scanlines.RemoveAt(count--);
}
return true;
}
}
}
+95
View File
@@ -0,0 +1,95 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Runtime.CompilerServices;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Represents a line segment on a plane.
/// </summary>
internal readonly struct Segment : IEquatable<Segment>
{
/// <summary>
/// Initializes a new instance of the <see cref="Segment"/> struct.
/// </summary>
/// <param name="source">The segment source.</param>
/// <param name="target">The segment target.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Segment(in Vertex source, in Vertex target)
{
this.Source = source;
this.Target = target;
this.Min = Vertex.Min(source, target);
this.Max = Vertex.Max(source, target);
}
/// <summary>
/// Gets the segment source vector.
/// </summary>
public Vertex Source { get; }
/// <summary>
/// Gets the segment target vector.
/// </summary>
public Vertex Target { get; }
/// <summary>
/// Gets the point of the segment with lexicographically smallest coordinate.
/// </summary>
public Vertex Min { get; }
/// <summary>
/// Gets the point of the segment with lexicographically largest coordinate.
/// </summary>
public Vertex Max { get; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(in Segment left, in Segment right)
=> left.Equals(right);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(in Segment left, in Segment right)
=> !(left == right);
/// <summary>
/// Gets a value indicating whether the segment is degenerate.
/// </summary>
/// <returns>
/// <see langword="true"/> if the segment is degenerate; otherwise <see langword="false"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsDegenerate() => this.Source.Equals(this.Target);
/// <summary>
/// Gets a value indicating whether the segment is vertical.
/// </summary>
/// <returns>
/// <see langword="true"/> if the segment is vertical; otherwise <see langword="false"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsVertical() => this.Source.X == this.Target.X;
/// <summary>
/// Changes the segment orientation.
/// </summary>
/// <returns>The <see cref="Segment"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Segment Reverse()
=> new(this.Target, this.Source);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool Equals(object? obj)
=> obj is Segment segment && this.Equals(segment);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Segment other)
=> this.Source.Equals(other.Source) && this.Target.Equals(other.Target);
/// <inheritdoc/>
public override int GetHashCode()
=> HashCode.Combine(this.Source, this.Target);
}
}
+158
View File
@@ -0,0 +1,158 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Allows the comparison of segments for sorting.
/// </summary>
internal sealed class SegmentComparer : IComparer<SweepEvent>, IComparer
{
/// <inheritdoc/>
public int Compare(SweepEvent? x, SweepEvent? y)
{
// If the events are the same, return 0 (no order difference)
if (ReferenceEquals(x, y))
{
return 0;
}
if (x == null)
{
return -1;
}
if (y == null)
{
return 1;
}
SweepEvent perhapsInversedX, perhapsInversedY;
bool inversed;
if (x.IsBefore(y))
{
perhapsInversedX = x;
perhapsInversedY = y;
inversed = false;
}
else
{
perhapsInversedX = y;
perhapsInversedY = x;
inversed = true;
}
// Check if the segments are collinear by comparing their signed areas
double area1 = PolygonUtilities.SignedArea(perhapsInversedX.Point, perhapsInversedX.OtherEvent.Point, perhapsInversedY.Point);
double area2 = PolygonUtilities.SignedArea(perhapsInversedX.Point, perhapsInversedX.OtherEvent.Point, perhapsInversedY.OtherEvent.Point);
if (area1 != 0 || area2 != 0)
{
// Segments are not collinear
// If they share their left endpoint, use the right endpoint to sort
if (perhapsInversedX.Point == perhapsInversedY.Point)
{
return LessIf(perhapsInversedX.IsBelow(perhapsInversedY.OtherEvent.Point), inversed);
}
// Different left endpoints: use the y-coordinate to sort if x-coordinates are the same
if (perhapsInversedX.Point.X == perhapsInversedY.Point.X)
{
return LessIf(perhapsInversedX.Point.Y < perhapsInversedY.Point.Y, inversed);
}
// If `x` and `y` lie on the same side of the reference segment,
// no intersection check is necessary.
if ((area1 > 0) == (area2 > 0))
{
return LessIf(area1 > 0, inversed);
}
// If `x` lies on the reference segment, compare based on `y`.
if (area1 == 0)
{
return LessIf(area2 > 0, inversed);
}
// Form segments from the events.
Segment seg0 = new(perhapsInversedX.Point, perhapsInversedX.OtherEvent.Point);
Segment seg1 = new(perhapsInversedY.Point, perhapsInversedY.OtherEvent.Point);
// Call the provided intersection method.
int interResult = PolygonUtilities.FindIntersection(seg0, seg1, out Vertex pi0, out Vertex _);
if (interResult == 0)
{
// No unique intersection found: decide based on area1.
return LessIf(area1 > 0, inversed);
}
else if (interResult == 1)
{
// Unique intersection found.
if (pi0 == y.Point)
{
return LessIf(area2 > 0, inversed);
}
return LessIf(area1 > 0, inversed);
}
// If interResult is neither 0 nor 1, fall through to collinear logic.
}
// Collinear branch – mimicking the Rust logic:
if (perhapsInversedX.PolygonType == perhapsInversedY.PolygonType)
{
// Both segments belong to the same polygon.
if (perhapsInversedX.Point == perhapsInversedY.Point)
{
// When left endpoints are identical, order by contour id.
return LessIf(perhapsInversedX.ContourId < perhapsInversedY.ContourId, inversed);
}
// If left endpoints differ, the Rust version simply returns "less" (i.e. the one inserted earlier).
// Here we mimic that by always returning -1.
return LessIf(true, inversed);
}
// Segments are collinear but belong to different polygons.
return LessIf(perhapsInversedX.PolygonType == PolygonType.Subject, inversed);
}
/// <inheritdoc/>
public int Compare(object? x, object? y)
{
if (x == null)
{
return -1;
}
if (y == null)
{
return 1;
}
if (x is SweepEvent a && y is SweepEvent b)
{
return this.Compare(a, b);
}
throw new ArgumentException("Both arguments must be of type SweepEvent.", nameof(x));
}
/// <summary>
/// Converts a boolean comparison result to an ordering value.
/// Returns -1 if the condition is true, 1 if false.
/// </summary>
/// <param name="condition">The boolean condition to evaluate.</param>
/// <param name="inversed">Should the result be inversed.</param>
/// <returns>-1 if condition is true, 1 if false.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int LessIf(bool condition, bool inversed = false) => condition ^ inversed ? -1 : 1;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,220 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Represents a stable priority queue that maintains the order of items with the same priority.
/// </summary>
/// <typeparam name="T">The type of elements in the priority queue.</typeparam>
/// <typeparam name="TComparer">The type of comparer used to determine the priority of the elements.</typeparam>
[DebuggerDisplay("Count = {Count}")]
internal sealed class StablePriorityQueue<T, TComparer>
where TComparer : IComparer<T>
{
private const int Log2Arity = 2;
private const int DefaultCapacity = 16;
private readonly List<T> heap;
/// <summary>
/// Initializes a new instance of the <see cref="StablePriorityQueue{T, TComparer}"/> class with a specified comparer.
/// </summary>
/// <param name="comparer">The comparer to determine the priority of the elements.</param>
public StablePriorityQueue(TComparer comparer)
: this(comparer, DefaultCapacity)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="StablePriorityQueue{T, TComparer}"/> class with a specified comparer.
/// </summary>
/// <param name="comparer">The comparer to determine the priority of the elements.</param>
/// <param name="capacity">The initial capacity of the priority queue.</param>
public StablePriorityQueue(TComparer comparer, int capacity)
{
this.Comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
this.heap = new List<T>(capacity > 0 ? capacity : DefaultCapacity);
}
/// <summary>
/// Initializes a new instance of the <see cref="StablePriorityQueue{T, TComparer}"/> class
/// with a specified comparer and an initial collection of unordered elements.
/// The heap property is established in linear time.
/// </summary>
/// <param name="comparer">The comparer to determine the priority of the elements.</param>
/// <param name="items">
/// The initial collection of elements to heapify.
/// Note: The collection is modified to establish the heap property.
/// </param>
public StablePriorityQueue(TComparer comparer, List<T> items)
{
this.Comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
this.heap = items ?? throw new ArgumentNullException(nameof(items));
this.Heapify(this.heap);
}
/// <summary>
/// Gets the number of elements in the priority queue.
/// </summary>
public int Count
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.heap.Count;
}
/// <summary>
/// Gets the comparer used to determine the priority of the elements.
/// </summary>
public TComparer Comparer { get; }
/// <summary>
/// Adds an item to the priority queue, maintaining the heap property.
/// </summary>
/// <param name="item">The item to add.</param>
public void Enqueue(T item)
{
List<T> data = this.heap;
data.Add(item);
this.Up((uint)data.Count - 1, data);
}
/// <summary>
/// Removes and returns the item with the highest priority (lowest value) from the priority queue.
/// </summary>
/// <returns>The item with the highest priority.</returns>
/// <exception cref="InvalidOperationException">Thrown if the priority queue is empty.</exception>
public T Dequeue()
{
List<T> data = this.heap;
int count = data.Count;
ThrowIfEmpty(count);
ref T dRef = ref MemoryMarshal.GetReference(CollectionsMarshal.AsSpan(data));
int maxIndex = count - 1;
T top = Unsafe.Add(ref dRef, 0u);
T bottom = Unsafe.Add(ref dRef, (uint)maxIndex);
data.RemoveAt(maxIndex);
if (--count > 0)
{
Unsafe.Add(ref dRef, 0u) = bottom;
this.Down(0u, data);
}
return top;
}
/// <summary>
/// Returns the item with the highest priority (lowest value) without removing it.
/// </summary>
/// <returns>The item with the highest priority.</returns>
/// <exception cref="InvalidOperationException">Thrown if the priority queue is empty.</exception>
public T Peek()
{
ThrowIfEmpty(this.Count);
return this.heap[0];
}
/// <summary>
/// Restores the min-heap property by moving the item at the specified index upward
/// through the heap until it is in the correct position. This is called after insertion.
/// </summary>
/// <param name="index">The index of the newly added item to sift upward.</param>
/// <param name="heap">The heap to operate on.</param>
private void Up(uint index, List<T> heap)
{
ref T dRef = ref MemoryMarshal.GetReference(CollectionsMarshal.AsSpan(heap));
T item = Unsafe.Add(ref dRef, index);
TComparer comparer = this.Comparer;
while (index > 0)
{
uint parent = (index - 1u) >> Log2Arity;
T current = Unsafe.Add(ref dRef, parent);
if (comparer.Compare(item, current) >= 0)
{
break;
}
Unsafe.Add(ref dRef, index) = current;
index = parent;
}
Unsafe.Add(ref dRef, index) = item;
}
/// <summary>
/// Restores the min-heap property by moving the item at the specified index downward
/// through the heap until it is in the correct position. This is called after removal of the root.
/// </summary>
/// <param name="index">The index of the item to sift downward (typically the root).</param>
/// <param name="heap">The heap to operate on.</param>
private void Down(uint index, List<T> heap)
{
Span<T> data = CollectionsMarshal.AsSpan(heap);
ref T dRef = ref MemoryMarshal.GetReference(data);
uint length = (uint)data.Length;
T item = Unsafe.Add(ref dRef, index);
TComparer comparer = this.Comparer;
while ((index << Log2Arity) + 1u < length)
{
uint firstChild = (index << Log2Arity) + 1u;
uint bestChild = firstChild;
uint maxChild = Math.Min(firstChild + (1u << Log2Arity), length);
for (uint i = firstChild + 1u; i < maxChild; i++)
{
if (comparer.Compare(Unsafe.Add(ref dRef, i), Unsafe.Add(ref dRef, bestChild)) < 0)
{
bestChild = i;
}
}
if (comparer.Compare(Unsafe.Add(ref dRef, bestChild), item) >= 0)
{
break;
}
Unsafe.Add(ref dRef, index) = Unsafe.Add(ref dRef, bestChild);
index = bestChild;
}
Unsafe.Add(ref dRef, index) = item;
}
/// <summary>
/// Heapifies the given list to establish the min-heap property.
/// </summary>
/// <param name="heap">The list to heapify.</param>
private void Heapify(List<T> heap)
{
int count = heap.Count;
if (count <= 1)
{
return;
}
int lastParent = (count - 2) >> Log2Arity;
for (int i = lastParent; i >= 0; i--)
{
this.Down((uint)i, heap);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowIfEmpty(int count)
{
if (count == 0)
{
throw new InvalidOperationException("Queue is empty.");
}
}
}
}
+216
View File
@@ -0,0 +1,216 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Represents a status line for the sweep line algorithm, maintaining a sorted collection of sweep events.
/// <para>
/// Performance Characteristics:
/// - **Insertion**: O(n) in the worst case. The operation consists of:
/// 1. A binary search (O(log n)) to determine the correct insertion point.
/// 2. A shift operation to move subsequent elements in the list (O(k)), where k is the number of elements
/// after the insertion index. In the worst case, this can approach O(n).
/// - **Removal**: O(n) in the worst case. After finding the index of the element to remove, subsequent
/// elements in the list need to be shifted (O(k)), where k is the number of elements after the removed index.
/// - **Next/Previous Access**: O(1) after the index is known, as the list provides constant-time indexing.
/// </para>
/// The implementation ensures efficient neighbor traversal (next/previous) at O(1), making it suitable for
/// algorithms where neighboring elements are accessed frequently. The use of `BinarySearch` minimizes the cost
/// of insertion/removal compared to naive search-based approaches.
/// </summary>
[DebuggerDisplay("Count = {Count}")]
internal sealed class StatusLine
{
private const int DefaultCapacity = 16;
private readonly List<SweepEvent> sortedEvents;
private readonly SegmentComparer comparer = new();
public StatusLine()
: this(DefaultCapacity)
{
}
public StatusLine(int capacity)
=> this.sortedEvents = new List<SweepEvent>(capacity > 0 ? capacity : DefaultCapacity);
/// <summary>
/// Gets the number of events in the status line.
/// </summary>
public int Count
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.sortedEvents.Count;
}
/// <summary>
/// Gets the minimum sweep event in the status line (first in sort order).
/// </summary>
public SweepEvent Min
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.sortedEvents[0];
}
/// <summary>
/// Gets the maximum sweep event in the status line (last in sort order).
/// </summary>
public SweepEvent Max
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.sortedEvents[^1];
}
/// <summary>
/// Gets the retained list capacity.
/// </summary>
public int RetainedCapacity => this.sortedEvents.Capacity;
/// <summary>
/// Gets the event at the specified index.
/// </summary>
/// <param name="index">The index of the event.</param>
/// <returns>The sweep event at the given index.</returns>
public SweepEvent this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.sortedEvents[index];
}
/// <summary>
/// Clears active events and ensures the desired capacity.
/// </summary>
/// <param name="capacity">Desired minimum capacity.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset(int capacity)
{
this.sortedEvents.Clear();
if (capacity > this.sortedEvents.Capacity)
{
this.sortedEvents.EnsureCapacity(capacity);
}
}
/// <summary>
/// Adds a sweep event into the status line, maintaining sorted order.
/// </summary>
/// <param name="e">The sweep event to insert.</param>
/// <returns>The index where the event was inserted.</returns>
public int Add(SweepEvent e)
{
int index = this.sortedEvents.BinarySearch(e, this.comparer);
if (index < 0)
{
index = ~index; // Get the correct insertion point
}
this.sortedEvents.Insert(index, e);
e.PosSL = index;
return index;
}
/// <summary>
/// Removes a sweep event from the status line.
/// </summary>
/// <param name="index">The index of the event to remove.</param>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if <paramref name="index"/> is less than 0 or greater than or equal to the number of events.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveAt(int index)
=> this.sortedEvents.RemoveAt(index);
/// <summary>
/// Finds the current index of a sweep event in the status line.
/// </summary>
/// <param name="e">The event to locate.</param>
/// <returns>The index of the event, or -1 if it is not present.</returns>
public int IndexOf(SweepEvent e)
{
List<SweepEvent> events = this.sortedEvents;
int count = events.Count;
int hint = e.PosSL;
if ((uint)hint < (uint)count && ReferenceEquals(events[hint], e))
{
return hint;
}
int index = events.BinarySearch(e, this.comparer);
if (index >= 0)
{
if (ReferenceEquals(events[index], e))
{
e.PosSL = index;
return index;
}
// BinarySearch can return any comparer-equal slot. Scan local ties by reference.
for (int i = index - 1; i >= 0 && this.comparer.Compare(events[i], e) == 0; i--)
{
if (ReferenceEquals(events[i], e))
{
e.PosSL = i;
return i;
}
}
for (int i = index + 1; i < count && this.comparer.Compare(events[i], e) == 0; i++)
{
if (ReferenceEquals(events[i], e))
{
e.PosSL = i;
return i;
}
}
}
// Fail-safe reference lookup for correctness if comparer order is temporarily unstable.
for (int i = 0; i < count; i++)
{
if (ReferenceEquals(events[i], e))
{
e.PosSL = i;
return i;
}
}
return -1;
}
/// <summary>
/// Gets the next sweep event relative to the given index.
/// </summary>
/// <param name="index">The reference index.</param>
/// <returns>The next sweep event, or <c>null</c> if none exists.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SweepEvent? Next(int index)
{
if (index >= 0 && index < this.sortedEvents.Count - 1)
{
return this.sortedEvents[index + 1];
}
return null;
}
/// <summary>
/// Gets the previous sweep event relative to the given index.
/// </summary>
/// <param name="index">The reference index.</param>
/// <returns>The previous sweep event, or <c>null</c> if none exists.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SweepEvent? Prev(int index)
{
if (index > 0 && index < this.sortedEvents.Count)
{
return this.sortedEvents[index - 1];
}
return null;
}
}
}
+65
View File
@@ -0,0 +1,65 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Provides configuration options for geometric stroke generation.
/// </summary>
public sealed class StrokeOptions : IEquatable<StrokeOptions?>
{
/// <summary>
/// Gets or sets a value indicating whether stroked contours should be normalized by
/// resolving self-intersections and overlaps before returning.
/// </summary>
/// <remarks>
/// Defaults to <see langword="false"/> for maximum throughput.
/// When disabled, callers should rasterize with a non-zero winding fill rule.
/// </remarks>
public bool NormalizeOutput { get; set; }
/// <summary>
/// Gets or sets the miter limit used to clamp outer miter joins.
/// </summary>
public double MiterLimit { get; set; } = 4D;
/// <summary>
/// Gets or sets the tessellation detail scale for round joins and round caps.
/// Higher values produce more vertices (smoother curves, more work).
/// Lower values produce fewer vertices.
/// </summary>
public double ArcDetailScale { get; set; } = 1D;
/// <summary>
/// Gets or sets the outer line join style used for stroking corners.
/// </summary>
public LineJoin LineJoin { get; set; } = LineJoin.Bevel;
/// <summary>
/// Gets or sets the line cap style used for open path ends.
/// </summary>
public LineCap LineCap { get; set; } = LineCap.Butt;
/// <inheritdoc/>
public override bool Equals(object? obj) => this.Equals(obj as StrokeOptions);
/// <inheritdoc/>
public bool Equals(StrokeOptions? other)
=> other is not null &&
this.NormalizeOutput == other.NormalizeOutput &&
this.MiterLimit == other.MiterLimit &&
this.ArcDetailScale == other.ArcDetailScale &&
this.LineJoin == other.LineJoin &&
this.LineCap == other.LineCap;
/// <inheritdoc/>
public override int GetHashCode()
=> HashCode.Combine(
this.NormalizeOutput,
this.MiterLimit,
this.ArcDetailScale,
this.LineJoin,
this.LineCap);
}
}
+71
View File
@@ -0,0 +1,71 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Represents a stroke-processing vertex with a cached outgoing segment length.
/// </summary>
/// <remarks>
/// This is an internal mutable value used by <see cref="PolygonStroker"/> while
/// normalizing source contours and computing joins/caps.
/// </remarks>
internal struct StrokeVertexDistance
{
private const double VertexDistanceEpsilon = 1E-14D;
private const double Dd = 1D / VertexDistanceEpsilon;
/// <summary>
/// The X-coordinate.
/// </summary>
public double X;
/// <summary>
/// The Y-coordinate.
/// </summary>
public double Y;
/// <summary>
/// Cached distance to another vertex measured by <see cref="Measure(in StrokeVertexDistance)"/>.
/// </summary>
public double Distance;
/// <summary>
/// Initializes a new instance of the <see cref="StrokeVertexDistance"/> struct.
/// </summary>
/// <param name="x">The X-coordinate.</param>
/// <param name="y">The Y-coordinate.</param>
/// <param name="distance">Initial cached distance value.</param>
public StrokeVertexDistance(double x, double y, double distance)
{
this.X = x;
this.Y = y;
this.Distance = distance;
}
/// <summary>
/// Measures the Euclidean distance from this vertex to <paramref name="vd"/> and stores it in <see cref="Distance"/>.
/// </summary>
/// <param name="vd">The vertex to measure to.</param>
/// <returns>
/// <see langword="true"/> when the measured distance is greater than the internal epsilon;
/// otherwise <see langword="false"/>.
/// </returns>
/// <remarks>
/// When points are closer than epsilon, <see cref="Distance"/> is set to a large sentinel value
/// to avoid divide-by-near-zero behavior in downstream stroker math.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Measure(in StrokeVertexDistance vd)
{
bool ret = (this.Distance = Vertex.Distance(new Vertex(this.X, this.Y), new Vertex(vd.X, vd.Y))) > VertexDistanceEpsilon;
if (!ret)
{
this.Distance = Dd;
}
return ret;
}
}
}
+205
View File
@@ -0,0 +1,205 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#nullable disable
using System.Runtime.CompilerServices;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Represents a sweep.
/// </summary>
internal sealed class SweepEvent
{
/// <summary>
/// Initializes a new instance of the <see cref="SweepEvent"/> class.
/// </summary>
/// <param name="point">The point associated with the event.</param>
/// <param name="left">Whether the point is the left endpoint of the segment.</param>
/// <param name="otherEvent">The event associated with the other endpoint of the segment.</param>
/// <param name="polygonType">The polygon type to which the segment belongs.</param>
/// <param name="edgeType">The type of the edge. Default is <see cref="EdgeType.Normal"/>.</param>
public SweepEvent(
Vertex point,
bool left,
SweepEvent otherEvent,
PolygonType polygonType = PolygonType.Subject,
EdgeType edgeType = EdgeType.Normal)
{
this.Point = point;
this.Left = left;
this.OtherEvent = otherEvent;
this.PolygonType = polygonType;
this.EdgeType = edgeType;
}
/// <summary>
/// Initializes a new instance of the <see cref="SweepEvent"/> class.
/// </summary>
/// <param name="point">The point associated with the event.</param>
/// <param name="left">Whether the point is the left endpoint of the segment.</param>
/// <param name="polygonType">The polygon type to which the segment belongs.</param>
public SweepEvent(Vertex point, bool left, PolygonType polygonType = PolygonType.Subject)
{
this.Point = point;
this.Left = left;
this.PolygonType = polygonType;
this.EdgeType = EdgeType.Normal;
}
/// <summary>
/// Initializes a new instance of the <see cref="SweepEvent"/> class.
/// </summary>
/// <param name="point">The point associated with the event.</param>
/// <param name="left">Whether the point is the left endpoint of the segment.</param>
/// <param name="contourId">The ID of the contour to which the event belongs.</param>
public SweepEvent(Vertex point, bool left, int contourId)
{
this.Point = point;
this.Left = left;
this.ContourId = contourId;
this.PolygonType = PolygonType.Subject;
this.EdgeType = EdgeType.Normal;
}
/// <summary>
/// Gets the point associated with the event.
/// </summary>
public Vertex Point { get; }
/// <summary>
/// Gets or sets a value indicating whether the point is the
/// left (source) endpoint of the segment (p, other->p).
/// </summary>
public bool Left { get; set; }
/// <summary>
/// Gets or sets the ID of the contour to which the event belongs.
/// </summary>
public int ContourId { get; set; }
/// <summary>
/// Gets index of the polygon to which the associated segment belongs to;
/// </summary>
public PolygonType PolygonType { get; }
/// <summary>
/// Gets or sets the type of the edge.
/// </summary>
public EdgeType EdgeType { get; set; }
/// <summary>
/// Gets or sets the event associated to the other endpoint of the segment.
/// </summary>
public SweepEvent OtherEvent { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the segment (p, other->p) represent an
/// inside-outside transition in the polygon for a vertical ray from (p.x, -infinite)
/// that crosses the segment.
/// </summary>
public bool InOut { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the inOut transition for the segment from
/// the other polygon preceding this segment in the sweep line.
/// </summary>
public bool OtherInOut { get; set; }
/// <summary>
/// Gets or sets the sorted sweep events. Only used in "left" events.
/// Position of the event (segment) in SL (status line).
/// </summary>
public int PosSL { get; set; }
/// <summary>
/// Gets or sets the previous segment in the sweep line belonging to the result of the
/// boolean operation.
/// </summary>
public SweepEvent PrevInResult { get; set; }
/// <summary>
/// Gets or sets the transition state of the event in the result.
/// </summary>
public ResultTransition ResultTransition { get; set; }
/// <summary>
/// Gets a value indicating whether the event contributes to the result.
/// </summary>
public bool InResult => this.ResultTransition != ResultTransition.Neutral;
/// <summary>
/// Gets or sets the position of the event in the sorted events.
/// </summary>
public int Pos { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the event is a result in-out transition.
/// </summary>
public bool ResultInOut { get; set; }
/// <summary>
/// Gets or sets the output contour ID associated with this contour.
/// </summary>
public int OutputContourId { get; set; }
/// <summary>
/// Is the line segment (point, otherEvent->point) below point p.
/// </summary>
/// <param name="p">The point to check against.</param>
/// <returns>
/// <see langword="true"/> if the line segment is below the point; otherwise <see langword="false"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsBelow(in Vertex p)
=> this.Left
? PolygonUtilities.SignedArea(this.Point, this.OtherEvent.Point, p) > 0D
: PolygonUtilities.SignedArea(this.OtherEvent.Point, this.Point, p) > 0D;
/// <summary>
/// Is the line segment (point, otherEvent->point) above point p.
/// </summary>
/// <param name="p">The point to check against.</param>
/// <returns>
/// <see langword="true"/> if the line segment is above the point; otherwise <see langword="false"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsAbove(in Vertex p) => !this.IsBelow(p);
/// <summary>
/// Is the line segment (point, otherEvent->point) a vertical line segment.
/// </summary>
/// <returns>
/// <see langword="true"/> if the line segment is vertical; otherwise <see langword="false"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsVertical() => this.Point.X == this.OtherEvent.Point.X;
/// <summary>
/// Determines if this sweep event comes before another sweep event.
/// </summary>
/// <param name="other">The other sweep event to compare with.</param>
/// <returns>
/// <see langword="true"/> if this event comes before the other; otherwise <see langword="false"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsBefore(SweepEvent other)
{
// Compare by x-coordinate first
if (this.Point.X != other.Point.X)
{
return this.Point.X < other.Point.X;
}
// If x-coordinates are equal, compare by y-coordinate
return this.Point.Y < other.Point.Y;
}
/// <summary>
/// Returns the segment associated with the sweep event.
/// </summary>
/// <returns>The <see cref="Segment"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Segment GetSegment() => new(this.Point, this.OtherEvent.Point);
}
}
+72
View File
@@ -0,0 +1,72 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Collections;
using System.Collections.Generic;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Compares two <see cref="SweepEvent"/> instances for sorting in the event queue.
/// </summary>
internal sealed class SweepEventComparer : IComparer<SweepEvent>, IComparer
{
/// <inheritdoc/>
public int Compare(SweepEvent? x, SweepEvent? y)
{
if (x == null)
{
return -1;
}
if (y == null)
{
return 1;
}
// Compare by x-coordinate
if (x.Point.X > y.Point.X)
{
return 1;
}
if (x.Point.X < y.Point.X)
{
return -1;
}
// Compare by y-coordinate when x-coordinates are the same
if (x.Point.Y != y.Point.Y)
{
return x.Point.Y > y.Point.Y ? 1 : -1;
}
// Compare left vs. right endpoint
if (x.Left != y.Left)
{
return x.Left ? 1 : -1;
}
// Compare collinearity using signed area
double area = PolygonUtilities.SignedArea(x.Point, x.OtherEvent.Point, y.OtherEvent.Point);
if (area != 0)
{
return x.IsBelow(y.OtherEvent.Point) ? -1 : 1;
}
// Compare by polygon type: subject polygons have higher priority
return x.PolygonType != PolygonType.Subject && y.PolygonType == PolygonType.Subject ? 1 : -1;
}
/// <inheritdoc/>
public int Compare(object? x, object? y)
{
if (x is SweepEvent a && y is SweepEvent b)
{
return this.Compare(a, b);
}
throw new ArgumentException("Both arguments must be of type SweepEvent.", nameof(x));
}
}
}
+53
View File
@@ -0,0 +1,53 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.PolygonClipper {
/// <summary>
/// Represents a vertex in the input contour linked list used by the sweep.
/// </summary>
/// <remarks>
/// Vertices are linked in a circular doubly linked list (see <see cref="Prev" /> and
/// <see cref="Next" />) so the sweep can traverse ascending/descending bounds and
/// detect local minima/maxima efficiently.
/// </remarks>
internal sealed class SweepVertex
{
#pragma warning disable SA1401 // Hot sweep vertex state uses fields to avoid accessor overhead.
/// <summary>
/// The vertex position.
/// </summary>
public Vertex Point;
/// <summary>
/// The next vertex in the contour.
/// </summary>
public SweepVertex? Next;
/// <summary>
/// The previous vertex in the contour.
/// </summary>
public SweepVertex? Prev;
/// <summary>
/// Flags describing sweep-related classification.
/// </summary>
public VertexFlags Flags;
#pragma warning restore SA1401
/// <summary>
/// Initializes a new instance of the <see cref="SweepVertex"/> class.
/// </summary>
public SweepVertex(Vertex point, VertexFlags flags, SweepVertex? prev)
{
this.Point = point;
this.Flags = flags;
this.Next = null;
this.Prev = prev;
}
/// <summary>
/// Gets a value indicating whether this vertex is marked as a local maxima.
/// </summary>
public bool IsMaxima => (this.Flags & VertexFlags.LocalMax) != 0;
}
}
+248
View File
@@ -0,0 +1,248 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Represents a two-dimensional vertex with X and Y coordinates.
/// </summary>
public readonly struct Vertex : IEquatable<Vertex>
{
/// <summary>
/// Gets the X-coordinate of the vertex.
/// </summary>
#pragma warning disable CA1051 // Do not declare visible instance fields
public readonly double X;
/// <summary>
/// Gets the Y-coordinate of the vertex.
/// </summary>
public readonly double Y;
#pragma warning restore CA1051 // Do not declare visible instance fields
/// <summary>
/// Initializes a new instance of the <see cref="Vertex"/> struct.
/// </summary>
/// <param name="xy">The X and Y coordinates of the vertex.</param>
public Vertex(double xy)
{
this.X = xy;
this.Y = xy;
}
/// <summary>
/// Initializes a new instance of the <see cref="Vertex"/> struct.
/// </summary>
/// <param name="x">The X-coordinate of the vertex.</param>
/// <param name="y">The Y-coordinate of the vertex.</param>
public Vertex(double x, double y)
{
this.X = x;
this.Y = y;
}
/// <summary>
/// Adds two vectors together.
/// </summary>
/// <param name="left">The first vector to add.</param>
/// <param name="right">The second vector to add.</param>
/// <returns>The summed vector.</returns>
/// <remarks>The <see cref="op_Addition" /> method defines the addition operation for <see cref="Vector2" /> objects.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vertex operator +(in Vertex left, in Vertex right)
=> AsVertexUnsafe(AsVector128Unsafe(left) + AsVector128Unsafe(right));
/// <summary>
/// Subtracts the second vector from the first.
/// </summary>
/// <param name="left">The first vector.</param>
/// <param name="right">The second vector.</param>
/// <returns>The vector that results from subtracting <paramref name="right" /> from <paramref name="left" />.</returns>
/// <remarks>The <see cref="op_Subtraction" /> method defines the subtraction operation for <see cref="Vector2" /> objects.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vertex operator -(in Vertex left, in Vertex right)
=> AsVertexUnsafe(AsVector128Unsafe(left) - AsVector128Unsafe(right));
/// <summary>
/// Returns a new vector whose values are the product of each pair of elements in two specified vertices.
/// </summary>
/// <param name="left">The first vertex.</param>
/// <param name="right">The second vertex.</param>
/// <returns>The element-wise product vertex.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vertex operator *(in Vertex left, in Vertex right) => AsVertexUnsafe(AsVector128Unsafe(left) * AsVector128Unsafe(right));
/// <summary>
/// Multiplies the specified vertex by the specified scalar value.
/// </summary>
/// <param name="left">The first vertex.</param>
/// <param name="right">The scalar value.</param>
/// <returns>The scaled vertex.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vertex operator *(in Vertex left, double right) => AsVertexUnsafe(AsVector128Unsafe(left) * right);
/// <summary>
/// Multiplies the specified vertex by the specified scalar value.
/// </summary>
/// <param name="left">The first vertex.</param>
/// <param name="right">The scalar value.</param>
/// <returns>The scaled vertex.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vertex operator *(double left, in Vertex right) => right * left;
/// <summary>
/// Divides the first vertex by the second.
/// </summary>
/// <param name="left">The first vertex.</param>
/// <param name="right">The second vertex.</param>
/// <returns>The vertex that results from dividing <paramref name="left" /> by <paramref name="right" />.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vertex operator /(in Vertex left, in Vertex right)
=> AsVertexUnsafe(AsVector128Unsafe(left) / AsVector128Unsafe(right));
/// <summary>
/// Divides the specified vertex by a specified scalar value.
/// </summary>
/// <param name="left">The first vertex.</param>
/// <param name="right">The scalar value.</param>
/// <returns>The result of the division.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vertex operator /(in Vertex left, double right)
=> AsVertexUnsafe(AsVector128Unsafe(left) / right);
/// <summary>
/// Divides the specified vertex by the specified scalar value.
/// </summary>
/// <param name="left">The first vertex.</param>
/// <param name="right">The scalar value.</param>
/// <returns>The result of the division.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vertex operator /(double left, in Vertex right) => right / left;
/// <summary>
/// Determines whether two vertices are equal.
/// </summary>
/// <param name="left">The first vertex.</param>
/// <param name="right">The second vertex.</param>
/// <returns><see langword="true"/> if the vertices are equal; otherwise, <see langword="false"/>.</returns>
public static bool operator ==(in Vertex left, in Vertex right) => left.Equals(right);
/// <summary>
/// Determines whether two vertices are not equal.
/// </summary>
/// <param name="left">The first vertex.</param>
/// <param name="right">The second vertex.</param>
/// <returns><see langword="true"/> if the vertices are not equal; otherwise, <see langword="false"/>.</returns>
public static bool operator !=(in Vertex left, in Vertex right) => !left.Equals(right);
/// <summary>
/// Returns the dot product of two vertices.
/// </summary>
/// <param name="a">The first vertex.</param>
/// <param name="b">The second vertex.</param>
/// <returns>The <see cref="double"/> dot product.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double Dot(in Vertex a, in Vertex b)
{
Vector128<double> a128 = AsVector128Unsafe(a);
Vector128<double> b128 = AsVector128Unsafe(b);
return Vector128.Dot(a128, b128);
}
/// <summary>
/// Returns the cross product of two vertices.
/// </summary>
/// <param name="a">The first vertex.</param>
/// <param name="b">The second vertex.</param>
/// <returns>The <see cref="double"/> cross product.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double Cross(in Vertex a, in Vertex b)
=> (a.X * b.Y) - (a.Y * b.X);
/// <summary>Computes the Euclidean distance between the two given vertices.</summary>
/// <param name="a">The first vertex.</param>
/// <param name="b">The second vertex.</param>
/// <returns>The distance.</returns>
public static double Distance(in Vertex a, in Vertex b)
=> double.Sqrt(DistanceSquared(a, b));
/// <summary>Returns the Euclidean distance squared between two specified vertices.</summary>
/// <param name="a">The first vertex.</param>
/// <param name="b">The second vertex.</param>
/// <returns>The distance squared.</returns>
public static double DistanceSquared(in Vertex a, in Vertex b)
=> (a - b).LengthSquared();
/// <summary>
/// Returns the length of the vertex.
/// </summary>
/// <returns>The vertex's length.</returns>
/// <altmember cref="LengthSquared" />
public double Length()
=> double.Sqrt(this.LengthSquared());
/// <summary>Returns the length of the vertex squared.</summary>
/// <returns>The vertex's length squared.</returns>
/// <remarks>This operation offers better performance than a call to the <see cref="Length" /> method.</remarks>
/// <altmember cref="Length" />
public double LengthSquared()
=> Dot(this, this);
/// <summary>
/// Returns a vertex whose elements are the minimum of each of the pairs of elements in two specified vertices.
/// </summary>
/// <param name="a">The first vertex.</param>
/// <param name="b">The second vertex.</param>
/// <returns>The minimized <see cref="Vertex"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vertex Min(in Vertex a, in Vertex b)
=> AsVertexUnsafe(Vector128.Min(AsVector128Unsafe(a), AsVector128Unsafe(b)));
/// <summary>
/// Returns a vertex whose elements are the maximum of each of the pairs of elements in two specified vertices.
/// </summary>
/// <param name="a">The first vertex.</param>
/// <param name="b">The second vertex.</param>
/// <returns>The maximized <see cref="Vertex"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vertex Max(in Vertex a, in Vertex b)
=> AsVertexUnsafe(Vector128.Max(AsVector128Unsafe(a), AsVector128Unsafe(b)));
/// <summary>
/// Computes the absolute value of each element in a specified vertex.
/// </summary>
/// <param name="value">The vertex that will have its absolute value computed.</param>
/// <returns>
/// A vertex with the absolute value of each of the elements in <paramref name="value"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vertex Abs(in Vertex value)
=> AsVertexUnsafe(Vector128.Abs(AsVector128Unsafe(value)));
/// <inheritdoc/>
public bool Equals(Vertex other)
=> this.X == other.X && this.Y == other.Y;
/// <inheritdoc/>
public override bool Equals(object? obj) =>
obj is Vertex vertex && this.Equals(vertex);
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.X, this.Y);
/// <inheritdoc/>
public override string ToString() => $"Vertex [ X={this.X}, Y={this.Y} ]";
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<double> AsVector128Unsafe(in Vertex value)
=> Unsafe.BitCast<Vertex, Vector128<double>>(value);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vertex AsVertexUnsafe(Vector128<double> value)
=> Unsafe.BitCast<Vector128<double>, Vertex>(value);
}
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Classifies sweep vertices by local-extrema role during bound construction.
/// </summary>
/// <remarks>
/// The self-intersection sweep decomposes each contour into monotonic bounds that
/// start at local minima and terminate at local maxima. These flags annotate
/// each <see cref="SweepVertex"/> with that role.
/// </remarks>
[Flags]
internal enum VertexFlags
{
/// <summary>
/// No extrema role is assigned.
/// </summary>
None = 0,
/// <summary>
/// Marks a local maximum vertex (bound endpoint).
/// </summary>
LocalMax = 1 << 0,
/// <summary>
/// Marks a local minimum vertex (bound start).
/// </summary>
LocalMin = 1 << 1
}
}
+342
View File
@@ -0,0 +1,342 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace SixLabors.PolygonClipper {
/// <summary>
/// Pool-backed list of clip vertices reused across clipping operations.
/// </summary>
internal sealed class VertexPoolList : PooledList<SweepVertex>
{
/// <summary>
/// Adds or reuses a clip vertex initialized with the given data.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SweepVertex Add(Vertex point, VertexFlags flags, SweepVertex? prev)
{
this.TryGrow();
SweepVertex poolVertex = this.Items[this.Size];
if (poolVertex == null)
{
poolVertex = new SweepVertex(point, flags, prev);
this.Items[this.Size] = poolVertex;
}
else
{
// Reset pooled state so linked lists are rebuilt safely.
poolVertex.Point = point;
poolVertex.Flags = flags;
poolVertex.Prev = prev;
poolVertex.Next = null;
}
this.Size++;
return poolVertex;
}
}
/// <summary>
/// Pool-backed list of output points allocated during clipping.
/// </summary>
internal sealed class OutputPointPoolList : PooledList<OutputPoint>
{
/// <summary>
/// Adds or reuses an output point and increments the owning record count.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public OutputPoint Add(Vertex pt, OutputRecord outputRecord)
{
this.TryGrow();
OutputPoint pooledPoint = this.Items[this.Size];
if (pooledPoint == null)
{
pooledPoint = new OutputPoint(pt, outputRecord);
this.Items[this.Size] = pooledPoint;
}
else
{
pooledPoint.Point = pt;
pooledPoint.OutputRecord = outputRecord;
pooledPoint.Next = pooledPoint;
pooledPoint.Prev = pooledPoint;
pooledPoint.HorizontalSegment = null;
}
this.Size++;
outputRecord.OutputPointCount++;
return pooledPoint;
}
}
/// <summary>
/// Pool-backed list of output records that preserves per-record state between runs.
/// </summary>
internal sealed class OutputRecordPoolList : PooledList<OutputRecord>
{
private static readonly List<Vertex> Tombstone = [];
/// <summary>
/// Adds or reuses an output record with cleared state.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public OutputRecord Add()
{
this.TryGrow();
OutputRecord outputRecord = this.Items[this.Size];
if (outputRecord == null)
{
outputRecord = new OutputRecord();
this.Items[this.Size] = outputRecord;
}
else
{
outputRecord.Index = 0;
outputRecord.OutputPointCount = 0;
outputRecord.Owner = null;
outputRecord.FrontEdge = null;
outputRecord.BackEdge = null;
outputRecord.Points = null;
outputRecord.Bounds = default;
outputRecord.Path.Clear();
outputRecord.Splits?.Clear();
outputRecord.RecursiveSplit = null;
}
this.Size++;
return outputRecord;
}
public override void Clear()
{
base.Clear();
for (int i = 0; i < this.Items.Length; i++)
{
OutputRecord outputRecord = this.Items[i];
if (outputRecord == null || outputRecord.Path == Tombstone)
{
break;
}
// Mark paths so pooled records are not accidentally reused without reset.
outputRecord.Path = Tombstone;
outputRecord.Owner = null;
outputRecord.FrontEdge = null;
outputRecord.BackEdge = null;
outputRecord.Points = null;
outputRecord.RecursiveSplit = null;
}
}
}
/// <summary>
/// Pool-backed list of horizontal joins used during sweep processing.
/// </summary>
internal sealed class HorizontalJoinPoolList : PooledList<HorizontalJoin>
{
/// <summary>
/// Adds or reuses a horizontal join entry.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public HorizontalJoin Add(OutputPoint ltor, OutputPoint rtol)
{
this.TryGrow();
HorizontalJoin hJoin = this.Items[this.Size];
if (hJoin == null)
{
hJoin = new HorizontalJoin(ltor, rtol);
this.Items[this.Size] = hJoin;
}
else
{
hJoin.LeftToRight = ltor;
hJoin.RightToLeft = rtol;
}
this.Size++;
return hJoin;
}
}
/// <summary>
/// Pool-backed list of sweep events reused between clipping runs.
/// </summary>
internal sealed class SweepEventPoolList : PooledList<SweepEvent>
{
/// <summary>
/// Adds a sweep event to the active range.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(SweepEvent sweepEvent)
{
this.TryGrow();
this.Items[this.Size] = sweepEvent;
this.Size++;
}
}
/// <summary>
/// Base class for pool-backed lists with stable indexing and reuse.
/// </summary>
/// <remarks>
/// These lists are append-only during a run and reset via <see cref="Clear" /> to
/// reuse previously allocated storage and object instances. The internal array
/// can grow but never shrinks, so callers should treat <see cref="Capacity" />
/// as a long-lived pool size. Elements are only valid in the range
/// <c>[0, Count)</c>; indices remain stable for the lifetime of a run, which allows
/// pooled nodes to store indices instead of references when needed.
/// </remarks>
internal abstract class PooledList<T> : IReadOnlyList<T>
where T : class
{
private const int DefaultCapacity = 4;
/// <summary>
/// Initializes a new instance of the <see cref="PooledList{T}" /> class.
/// </summary>
protected PooledList() => this.Items = [];
/// <summary>
/// Gets the number of items that have been added during the current run.
/// </summary>
public int Count => this.Size;
/// <summary>
/// Gets the backing array used for pooled storage.
/// </summary>
protected T[] Items { get; private set; }
/// <summary>
/// Gets or sets the number of active items in the pool.
/// </summary>
protected int Size { get; set; }
/// <summary>
/// Gets the current capacity of the pooled storage.
/// </summary>
public int Capacity
{
get => this.Items.Length;
private set
{
if (value <= this.Items.Length)
{
return;
}
int target = (int)BitOperations.RoundUpToPowerOf2((uint)value);
T[] newItems = new T[target];
if (this.Size > 0)
{
Array.Copy(this.Items, newItems, this.Size);
}
this.Items = newItems;
}
}
/// <summary>
/// Gets the item at the specified index within the active range.
/// </summary>
public T this[int index]
{
get
{
DebugGuard.MustBeLessThan((uint)index, (uint)this.Size, nameof(index));
return this.Items[index];
}
}
/// <summary>
/// Ensures the pool can hold at least <paramref name="capacity" /> items.
/// </summary>
public void EnsureCapacity(int capacity) => this.Capacity = capacity;
/// <summary>
/// Resets the active count to zero without clearing the backing array.
/// </summary>
public virtual void Clear() => this.Size = 0;
/// <summary>
/// Gets a struct enumerator over the active items.
/// </summary>
public PooledListEnumerator<T> GetEnumerator() => new(this);
/// <inheritdoc/>
IEnumerator<T> IEnumerable<T>.GetEnumerator() => new PooledListEnumerator<T>(this);
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => new PooledListEnumerator<T>(this);
/// <summary>
/// Grows the pool by at least one slot, doubling capacity when needed.
/// </summary>
protected void TryGrow()
{
int newSize = this.Size + 1;
if (newSize <= this.Items.Length)
{
return;
}
int newCapacity = this.Items.Length == 0 ? DefaultCapacity : this.Items.Length * 2;
this.Capacity = newCapacity;
}
/// <summary>
/// Struct enumerator for iterating active items without allocations.
/// </summary>
internal struct PooledListEnumerator<TItem> : IEnumerator<TItem>
where TItem : class
{
private readonly PooledList<TItem> list;
private int index;
private TItem? current;
public PooledListEnumerator(PooledList<TItem> list)
{
this.list = list;
this.index = 0;
this.current = null;
}
public readonly TItem Current => this.current!;
readonly object IEnumerator.Current => this.current!;
public readonly void Dispose()
{
}
public bool MoveNext()
{
int count = this.list.Size;
if ((uint)this.index < (uint)count)
{
this.current = this.list[this.index];
this.index++;
return true;
}
return this.MoveNextRare(count);
}
private bool MoveNextRare(int count)
{
this.index = count + 1;
this.current = null;
return false;
}
public void Reset()
{
this.index = 0;
this.current = null;
}
}
}
}