first commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>");
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user