ImageSharp/PolygonClipper/VertexPoolList.cs
2026-08-03 22:31:27 +02:00

343 lines
11 KiB
C#

// 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;
}
}
}
}