Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 55 additions & 31 deletions src/PolygonClipper/PolygonClipper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace PolygonClipper;

Expand Down Expand Up @@ -120,7 +122,9 @@ public Polygon Run()
// Process all segments in the subject polygon
Vertex min = new(double.PositiveInfinity);
Vertex max = new(double.NegativeInfinity);
StablePriorityQueue<SweepEvent, SweepEventComparer> eventQueue = new(new SweepEventComparer());

int eventCount = (subject.GetVertexCount() + clipping.GetVertexCount()) * 2;
StablePriorityQueue<SweepEvent, SweepEventComparer> eventQueue = new(new SweepEventComparer(), eventCount);
int contourId = 0;
for (int i = 0; i < subject.ContourCount; i++)
{
Expand Down Expand Up @@ -162,6 +166,7 @@ public Polygon Run()

SweepEvent? prevEvent;
SweepEvent? nextEvent;
Span<SweepEvent> workspace = new SweepEvent[4];
Comment thread
JimBobSquarePants marked this conversation as resolved.
while (eventQueue.Count > 0)
{
SweepEvent sweepEvent = eventQueue.Dequeue();
Expand All @@ -188,7 +193,7 @@ public Polygon Run()
if (nextEvent != null)
{
// Check intersection with the next neighbor
if (PossibleIntersection(sweepEvent, nextEvent, eventQueue) == 2)
if (PossibleIntersection(sweepEvent, nextEvent, eventQueue, workspace) == 2)
{
ComputeFields(sweepEvent, prevEvent, operation);
ComputeFields(nextEvent, sweepEvent, operation);
Expand All @@ -199,7 +204,7 @@ public Polygon Run()
if (prevEvent != null)
{
// Check intersection with the previous neighbor
if (PossibleIntersection(prevEvent, sweepEvent, eventQueue) == 2)
if (PossibleIntersection(prevEvent, sweepEvent, eventQueue, workspace) == 2)
{
SweepEvent? prevPrevEvent = statusLine.Prev(prevEvent.PosSL);
ComputeFields(prevEvent, prevPrevEvent, operation);
Expand All @@ -218,7 +223,7 @@ public Polygon Run()
// Check intersection between neighbors
if (prevEvent != null && nextEvent != null)
{
_ = PossibleIntersection(prevEvent, nextEvent, eventQueue);
_ = PossibleIntersection(prevEvent, nextEvent, eventQueue, workspace);
}

statusLine.RemoveAt(it);
Expand Down Expand Up @@ -499,6 +504,10 @@ private static bool InResult(SweepEvent sweepEvent, BooleanOperation operation)
/// <param name="le1">The first sweep event representing a line segment.</param>
/// <param name="le2">The second sweep event representing a line segment.</param>
/// <param name="eventQueue">The event queue to add new events to.</param>
/// <param name="workspace">
/// A scratch space for temporary storage of sweep events.
/// Must be at least 4 elements long to hold the events for the two segments and their associated other events.
/// </param>
/// <returns>
/// An integer indicating the result of the intersection:
/// <list type="bullet">
Expand All @@ -514,7 +523,8 @@ private static bool InResult(SweepEvent sweepEvent, BooleanOperation operation)
private static int PossibleIntersection(
SweepEvent le1,
SweepEvent le2,
StablePriorityQueue<SweepEvent, SweepEventComparer> eventQueue)
StablePriorityQueue<SweepEvent, SweepEventComparer> eventQueue,
Span<SweepEvent> workspace)
{
if (le1.OtherEvent == null || le2.OtherEvent == null)
{
Expand Down Expand Up @@ -569,41 +579,55 @@ private static int PossibleIntersection(
}

// The line segments associated with le1 and le2 overlap.
// TODO: Rewrite this to avoid allocation.
List<SweepEvent> events = new(4);
bool leftCoincide = le1.Point == le2.Point;
bool rightCoincide = le1.OtherEvent.Point == le2.OtherEvent.Point;

// Populate the events
// Populate the events.
// The working buffer has a length of 4, which is sufficient to hold the events
// for the two segments and their associated other events.
// Events are assigned in a specific order to avoid overwriting shared references.
ref SweepEvent wRef = ref MemoryMarshal.GetReference(workspace);
if (!leftCoincide)
{
if (comparer.Compare(le1, le2) > 0)
{
events.Add(le2);
events.Add(le1);
Unsafe.Add(ref wRef, 0u) = le2;
Unsafe.Add(ref wRef, 1u) = le1;
}
else
{
events.Add(le1);
events.Add(le2);
Unsafe.Add(ref wRef, 0u) = le1;
Unsafe.Add(ref wRef, 1u) = le2;
}
}

if (!rightCoincide)
// Positions 0 and 1 contain the left events of the segments.
// Positions 2 and 3 will contain the right events of the segments.
if (!rightCoincide)
{
Unsafe.Add(ref wRef, 2u) = le1.OtherEvent;
Unsafe.Add(ref wRef, 3u) = le2.OtherEvent;
}
else
{
Unsafe.Add(ref wRef, 2u) = le2.OtherEvent;
Unsafe.Add(ref wRef, 3u) = le1.OtherEvent;
}
}
else if (leftCoincide && !rightCoincide)
{
// Only the right endpoints differ, so we use positions 0 and 1 for their sorted order.
if (comparer.Compare(le1.OtherEvent, le2.OtherEvent) > 0)
{
events.Add(le2.OtherEvent);
events.Add(le1.OtherEvent);
Unsafe.Add(ref wRef, 0u) = le2.OtherEvent;
Unsafe.Add(ref wRef, 1u) = le1.OtherEvent;
}
else
{
events.Add(le1.OtherEvent);
events.Add(le2.OtherEvent);
Unsafe.Add(ref wRef, 0u) = le1.OtherEvent;
Unsafe.Add(ref wRef, 1u) = le2.OtherEvent;
}
}

// Handle leftCoincide case
if (leftCoincide)
{
le2.EdgeType = EdgeType.NonContributing;
Expand All @@ -613,30 +637,31 @@ private static int PossibleIntersection(

if (leftCoincide && !rightCoincide)
{
DivideSegment(events[1].OtherEvent, events[0].Point, eventQueue, comparer);
DivideSegment(Unsafe.Add(ref wRef, 1u).OtherEvent, Unsafe.Add(ref wRef, 0u).Point, eventQueue, comparer);
}

return 2;
}

// Handle the rightCoincide case
if (rightCoincide)
{
DivideSegment(events[0], events[1].Point, eventQueue, comparer);
// Since leftCoincide is false, the first two workspace slots contain distinct left events.
DivideSegment(Unsafe.Add(ref wRef, 0u), Unsafe.Add(ref wRef, 1u).Point, eventQueue, comparer);
return 3;
}

// Handle general overlapping case
if (events[0] != events[3].OtherEvent)
// At this point: workspace[0,1] = sorted left events, workspace[2,3] = sorted right events.
if (Unsafe.Add(ref wRef, 0u) != Unsafe.Add(ref wRef, 3u).OtherEvent)
{
DivideSegment(events[0], events[1].Point, eventQueue, comparer);
DivideSegment(events[1], events[2].Point, eventQueue, comparer);
DivideSegment(Unsafe.Add(ref wRef, 0u), Unsafe.Add(ref wRef, 1u).Point, eventQueue, comparer);
DivideSegment(Unsafe.Add(ref wRef, 1u), Unsafe.Add(ref wRef, 2u).Point, eventQueue, comparer);
return 3;
}

// One segment fully contains the other
DivideSegment(events[0], events[1].Point, eventQueue, comparer);
DivideSegment(events[3].OtherEvent, events[2].Point, eventQueue, comparer);
DivideSegment(Unsafe.Add(ref wRef, 0u), Unsafe.Add(ref wRef, 1u).Point, eventQueue, comparer);
DivideSegment(Unsafe.Add(ref wRef, 3u).OtherEvent, Unsafe.Add(ref wRef, 2u).Point, eventQueue, comparer);
return 3;
}

Expand Down Expand Up @@ -924,6 +949,7 @@ private static ReadOnlySpan<int> PrecomputeIterationOrder(List<SweepEvent> data)
return map;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void MarkProcessed(SweepEvent sweepEvent, Span<bool> processed, int pos, int contourId)
{
processed[pos] = true;
Expand Down Expand Up @@ -994,11 +1020,9 @@ private static Contour InitializeContourFromContext(SweepEvent sweepEvent, Polyg
/// starting from the given position.
/// </summary>
/// <param name="pos">The current position in the result events.</param>
/// <param name="resultEvents">The list of sweep events representing result segments.</param>
/// <param name="processed">A list indicating whether each event at the corresponding index has been processed.</param>
/// <param name="originalPos">The original position to return if no unprocessed event is found.</param>
/// <param name="iterationMap"></param>
/// <param name="found"></param>
/// <param name="iterationMap">A precomputed map that indicates the next position to check for unprocessed events.</param>
/// <param name="found">A boolean indicating whether an unprocessed event was found.</param>
/// <returns>The index of the next unprocessed position.</returns>
/// <remarks>
/// This method searches forward from the current position until it finds an unprocessed event with
Expand Down
63 changes: 37 additions & 26 deletions src/PolygonClipper/StablePriorityQueue{T,TComparer}.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// Copyright (c) Six Labors.
// 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 PolygonClipper;

Expand All @@ -16,14 +18,18 @@ namespace PolygonClipper;
internal sealed class StablePriorityQueue<T, TComparer>
where TComparer : IComparer<T>
{
private readonly List<T> heap = [];
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 = comparer ?? throw new ArgumentNullException(nameof(comparer));
/// <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 : 16);
}

/// <summary>
/// Gets the number of elements in the priority queue.
Expand All @@ -42,7 +48,7 @@ public StablePriorityQueue(TComparer comparer)
public void Enqueue(T item)
{
this.heap.Add(item);
this.Up(this.heap.Count - 1);
this.Up((uint)this.heap.Count - 1);
}

/// <summary>
Expand Down Expand Up @@ -86,61 +92,66 @@ public T Peek()
}

/// <summary>
/// Restores the heap property by moving the item at the specified index upward.
/// 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 item to move upward.</param>
private void Up(int index)
/// <param name="index">The index of the newly added item to sift upward.</param>
Copy link
Copy Markdown
Contributor

@stefannikolei stefannikolei Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo sift --> shift

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, that's intentional, sift is standard terminology in heap operations

private void Up(uint index)
{
List<T> data = this.heap;
T item = data[index];
ref T dRef = ref MemoryMarshal.GetReference(CollectionsMarshal.AsSpan(this.heap));
T item = Unsafe.Add(ref dRef, index);
TComparer comparer = this.Comparer;

while (index > 0)
{
int parent = (index - 1) >> 1;
T current = data[parent];
uint parent = (index - 1u) >> 1;
T current = Unsafe.Add(ref dRef, parent);
if (comparer.Compare(item, current) >= 0)
{
break;
}

data[index] = current;
Unsafe.Add(ref dRef, index) = current;
index = parent;
}

data[index] = item;
Unsafe.Add(ref dRef, index) = item;
}

/// <summary>
/// Restores the heap property by moving the item at the specified index downward.
/// 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 move downward.</param>
private void Down(int index)
/// <param name="index">The index of the item to sift downward (typically the root).</param>
private void Down(uint index)
{
List<T> data = this.heap;
int halfLength = data.Count >> 1;
T item = data[index];
Span<T> data = CollectionsMarshal.AsSpan(this.heap);
ref T dRef = ref MemoryMarshal.GetReference(data);

uint length = (uint)data.Length;
uint halfLength = length >> 1;
T item = Unsafe.Add(ref dRef, index);
TComparer comparer = this.Comparer;

while (index < halfLength)
{
int bestChild = (index << 1) + 1; // Initially left child
int right = bestChild + 1;
uint bestChild = (index << 1) + 1; // Initially left child
uint right = bestChild + 1u;

if (right < data.Count && comparer.Compare(data[right], data[bestChild]) < 0)
if (right < length && comparer.Compare(Unsafe.Add(ref dRef, right), Unsafe.Add(ref dRef, bestChild)) < 0)
{
bestChild = right;
}

if (comparer.Compare(data[bestChild], item) >= 0)
if (comparer.Compare(Unsafe.Add(ref dRef, bestChild), item) >= 0)
{
break;
}

data[index] = data[bestChild];
Unsafe.Add(ref dRef, index) = Unsafe.Add(ref dRef, bestChild);
index = bestChild;
}

data[index] = item;
Unsafe.Add(ref dRef, index) = item;
}
}
Loading
Loading