// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace SixLabors.PolygonClipper {
///
/// Provides functionality to remove self-intersections from polygons using a sweep line algorithm.
///
///
///
/// This class implements a sweep line algorithm that resolves self-intersections
/// and normalizes contours for positive winding output.
///
///
/// The algorithm works in three phases:
///
///
/// -
/// Intersection Detection: Uses a sweep line to find all points where segments
/// intersect each other (both self-intersections and cross-contour intersections).
///
/// -
/// Segment Splitting: Divides segments at intersection points, creating new
/// vertices where crossings occur.
///
/// -
/// Boundary Extraction: Keeps only edges that
/// form the boundary between filled and unfilled regions.
///
///
///
internal static class SelfIntersectionRemover
{
// Mirror PolygonClipper pooling policy: keep a tiny hot set per thread.
private const int MaxOutputBuilderPoolDepth = 4;
// Retain builders only while their pooled internal capacity stays in a
// bounded range. Oversized builders are dropped after heavy/pathological inputs.
private const int MaxRetainedOutputBuilderCapacityScore = 131_072;
[ThreadStatic]
private static Stack? outputBuilderPool;
///
/// Processes a polygon to remove self-intersections.
///
/// The polygon to process.
///
/// A new with self-intersections resolved and contours
/// normalized for positive winding fill semantics.
///
public static Polygon Process(Polygon polygon)
=> Process(polygon, normalizeInputForPositiveFill: true);
///
/// Processes a polygon to remove self-intersections.
///
/// The polygon to process.
///
/// Whether input contours should be normalized before sweep execution.
///
/// The self-intersection-removed polygon.
internal static Polygon Process(Polygon polygon, bool normalizeInputForPositiveFill)
{
if (polygon.Count == 0)
{
return [];
}
List> subject = BuildSubjectPaths(polygon, normalizeInputForPositiveFill);
if (normalizeInputForPositiveFill)
{
GetLowestPathInfo(subject, out int lowestPathIdx, out bool isNegativeArea);
if (lowestPathIdx >= 0 && isNegativeArea)
{
ReverseContours(subject);
}
}
OutputBuilder builder = RentOutputBuilder();
try
{
return UnionWithClipper(subject, polygon.Count, builder);
}
finally
{
ReturnOutputBuilder(builder);
}
}
///
/// Executes a union using the internal clipper.
///
/// The quantized subject contours to union.
/// The initial contour capacity for the output polygon.
/// The reusable output builder instance.
/// A polygon containing the unioned contours.
private static Polygon UnionWithClipper(
List> subject,
int resultCapacity,
OutputBuilder builder)
{
builder.ResetForReuse();
builder.PreserveCollinear = true;
builder.AddSubject(subject);
return builder.Execute(resultCapacity);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ReverseContours(List> subject)
{
for (int i = 0; i < subject.Count; i++)
{
subject[i].Reverse();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static OutputBuilder RentOutputBuilder()
{
Stack? pool = outputBuilderPool;
if (pool != null && pool.Count > 0)
{
return pool.Pop();
}
return new OutputBuilder();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ReturnOutputBuilder(OutputBuilder builder)
{
builder.ResetForReuse();
// Drop oversized builders to prevent long-lived thread-local memory spikes.
if (builder.RetainedCapacityScore > MaxRetainedOutputBuilderCapacityScore)
{
return;
}
Stack pool = outputBuilderPool ??= new Stack(MaxOutputBuilderPoolDepth);
if (pool.Count < MaxOutputBuilderPoolDepth)
{
pool.Push(builder);
}
}
///
/// Determines the lowest point across all paths and whether its contour area is negative.
///
/// The paths to examine.
/// The index of the path containing the lowest point.
/// True when the lowest path has negative area.
private static void GetLowestPathInfo(List> paths, out int lowestPathIdx, out bool isNegativeArea)
{
lowestPathIdx = -1;
isNegativeArea = false;
if (paths.Count == 0)
{
return;
}
Vertex lowestPoint = default;
bool hasPoint = false;
for (int i = 0; i < paths.Count; i++)
{
List path = paths[i];
if (path.Count == 0)
{
continue;
}
Vertex candidate = GetLowestPoint(path);
if (!hasPoint || candidate.Y > lowestPoint.Y || (candidate.Y == lowestPoint.Y && candidate.X < lowestPoint.X))
{
lowestPoint = candidate;
lowestPathIdx = i;
hasPoint = true;
}
}
if (lowestPathIdx >= 0)
{
isNegativeArea = GetSignedArea(paths[lowestPathIdx]) < 0D;
}
}
private static Vertex GetLowestPoint(List path)
{
int count = path.Count;
int lastIndex = count - 1;
if (count > 1 && path[0] == path[^1])
{
lastIndex = count - 2;
}
Vertex lowest = path[0];
for (int i = 1; i <= lastIndex; i++)
{
Vertex candidate = path[i];
if (candidate.Y > lowest.Y || (candidate.Y == lowest.Y && candidate.X < lowest.X))
{
lowest = candidate;
}
}
return lowest;
}
private static int GetDepth(int index, ReadOnlySpan parentIndices)
{
int depth = 0;
int current = parentIndices[index];
while (current >= 0)
{
depth++;
current = parentIndices[current];
}
return depth;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vertex GetContourTestPoint(List contour)
{
if (contour.Count == 0)
{
return default;
}
Vertex first = contour[0];
if (contour.Count > 1 && first == contour[^1])
{
return contour[1];
}
return first;
}
private static double GetSignedArea(List contour)
{
int count = contour.Count;
if (count == 0)
{
return 0D;
}
double area = 0;
Vertex current = contour[0];
for (int i = 1; i < count; i++)
{
Vertex next = contour[i];
area += Vertex.Cross(current, next);
current = next;
}
area += Vertex.Cross(current, contour[0]);
return area * 0.5D;
}
private static bool HasSelfIntersection(List contour)
{
int vertexCount = contour.Count - 1;
if (vertexCount < 4)
{
return false;
}
for (int i = 0; i < vertexCount; i++)
{
Vertex segA1 = contour[i];
Vertex segA2 = contour[i + 1];
if (segA1 == segA2)
{
continue;
}
for (int j = i + 1; j < vertexCount; j++)
{
if (j == i || j == i + 1 || (i == 0 && j == vertexCount - 1))
{
continue;
}
Vertex segB1 = contour[j];
Vertex segB2 = contour[j + 1];
if (segB1 == segB2)
{
continue;
}
if (PolygonUtilities.SegmentsIntersect(segA1, segA2, segB1, segB2, true) ||
(PolygonUtilities.IsCollinear(segA1, segA2, segB1) &&
PolygonUtilities.IsCollinear(segA1, segA2, segB2) &&
SegmentsOverlap(segA1, segA2, segB1, segB2)))
{
return true;
}
}
}
return false;
}
private static bool ContoursIntersect(
List left,
List right,
in Box2 leftBounds,
in Box2 rightBounds)
{
if (!leftBounds.Intersects(rightBounds))
{
return false;
}
int leftCount = left.Count - 1;
int rightCount = right.Count - 1;
for (int i = 0; i < leftCount; i++)
{
Vertex leftSeg1 = left[i];
Vertex leftSeg2 = left[i + 1];
if (leftSeg1 == leftSeg2)
{
continue;
}
for (int j = 0; j < rightCount; j++)
{
Vertex rightSeg1 = right[j];
Vertex rightSeg2 = right[j + 1];
if (rightSeg1 == rightSeg2)
{
continue;
}
if (PolygonUtilities.SegmentsIntersect(leftSeg1, leftSeg2, rightSeg1, rightSeg2, true) ||
(PolygonUtilities.IsCollinear(leftSeg1, leftSeg2, rightSeg1) &&
PolygonUtilities.IsCollinear(leftSeg1, leftSeg2, rightSeg2) &&
SegmentsOverlap(leftSeg1, leftSeg2, rightSeg1, rightSeg2)))
{
return true;
}
}
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool SegmentsOverlap(in Vertex a1, in Vertex a2, in Vertex b1, in Vertex b2)
{
Vertex aMin = Vertex.Min(a1, a2);
Vertex aMax = Vertex.Max(a1, a2);
Vertex bMin = Vertex.Min(b1, b2);
Vertex bMax = Vertex.Max(b1, b2);
return aMax.X >= bMin.X &&
bMax.X >= aMin.X &&
aMax.Y >= bMin.Y &&
bMax.Y >= aMin.Y;
}
///
/// Builds subject paths from a polygon.
///
/// The polygon to convert.
/// Whether input contour orientation should be normalized.
/// A list of fixed-precision vertex paths ready for clipping.
private static List> BuildSubjectPaths(Polygon polygon, bool normalizeForPositiveFill)
{
List> subject = new(polygon.Count);
List? sourceIndices = normalizeForPositiveFill ? new List(polygon.Count) : null;
for (int i = 0; i < polygon.Count; i++)
{
Contour contour = polygon[i];
if (contour.Count == 0)
{
continue;
}
bool isClosed = contour.Count > 1 && contour[0] == contour[^1];
int capacity = contour.Count + (isClosed ? 0 : 1);
List path = new(capacity);
CopyContourVertices(contour, isClosed, path);
subject.Add(path);
sourceIndices?.Add(i);
}
if (normalizeForPositiveFill)
{
ApplyPositiveFillOrientation(polygon, subject, sourceIndices!);
}
return subject;
}
private static void CopyContourVertices(Contour source, bool isClosed, List destination)
{
for (int i = 0; i < source.Count; i++)
{
destination.Add(source[i]);
}
if (!isClosed && destination.Count > 1 && destination[^1] != destination[0])
{
destination.Add(destination[0]);
}
}
private static void ApplyPositiveFillOrientation(
Polygon source,
List> subject,
List sourceIndices)
{
bool[]? reverseFlags = BuildPositiveFillReversalFlags(source, subject, sourceIndices);
if (reverseFlags == null)
{
return;
}
for (int i = 0; i < reverseFlags.Length; i++)
{
if (reverseFlags[i])
{
subject[i].Reverse();
}
}
}
private static bool[]? BuildPositiveFillReversalFlags(
Polygon source,
List> subject,
List sourceIndices)
{
int count = subject.Count;
if (count == 0)
{
return null;
}
if (count == 1)
{
return GetSignedArea(subject[0]) < 0D ? [true] : null;
}
using Buffer parentIndicesBuffer = new(count);
Span parentIndices = parentIndicesBuffer.GetSpan();
parentIndices.Fill(-1);
using Buffer signedAreasBuffer = new(count);
Span signedAreas = signedAreasBuffer.GetSpan();
bool hasHierarchy = false;
bool hasSignedAreas = false;
for (int i = 0; i < count; i++)
{
Contour contour = source[sourceIndices[i]];
if (contour.ParentIndex == null && contour.HoleCount <= 0)
{
continue;
}
hasHierarchy = true;
break;
}
if (hasHierarchy)
{
int sourceCount = source.Count;
using Buffer sourceToSubjectBuffer = new(sourceCount);
Span sourceToSubject = sourceToSubjectBuffer.GetSpan();
sourceToSubject.Fill(-1);
for (int i = 0; i < sourceIndices.Count; i++)
{
sourceToSubject[sourceIndices[i]] = i;
}
for (int i = 0; i < count; i++)
{
int sourceIndex = sourceIndices[i];
int parentIndex = source[sourceIndex].ParentIndex ?? -1;
parentIndices[i] = parentIndex >= 0 && parentIndex < sourceCount
? sourceToSubject[parentIndex]
: -1;
}
}
else
{
using Buffer boundsBuffer = new(count);
Span bounds = boundsBuffer.GetSpan();
using Buffer absAreasBuffer = new(count);
Span absAreas = absAreasBuffer.GetSpan();
for (int i = 0; i < count; i++)
{
List contour = subject[i];
bounds[i] = PolygonUtilities.GetBounds(contour);
double signedArea = GetSignedArea(contour);
signedAreas[i] = signedArea;
absAreas[i] = Math.Abs(signedArea);
if (HasSelfIntersection(contour))
{
// Avoid reorienting inputs that are already self-intersecting.
return null;
}
}
for (int i = 0; i < count; i++)
{
for (int j = i + 1; j < count; j++)
{
if (ContoursIntersect(subject[i], subject[j], bounds[i], bounds[j]))
{
// Overlapping contours can change semantics when reoriented.
return null;
}
}
}
hasSignedAreas = true;
for (int i = 0; i < count; i++)
{
List contour = subject[i];
if (contour.Count == 0)
{
continue;
}
Vertex testPoint = GetContourTestPoint(contour);
double smallestArea = double.PositiveInfinity;
int parentIndex = -1;
for (int j = 0; j < count; j++)
{
if (i == j || !bounds[j].Contains(testPoint))
{
continue;
}
if (PolygonUtilities.PointInPolygon(testPoint, subject[j]) != PointInPolygonResult.Inside)
{
continue;
}
if (absAreas[j] < smallestArea)
{
smallestArea = absAreas[j];
parentIndex = j;
}
}
parentIndices[i] = parentIndex;
}
}
bool[] reverseFlags = new bool[count];
bool needsReversal = false;
for (int i = 0; i < count; i++)
{
List contour = subject[i];
if (contour.Count == 0)
{
continue;
}
int depth = GetDepth(i, parentIndices);
bool shouldBeCounterClockwise = (depth & 1) == 0;
double signedArea = hasSignedAreas ? signedAreas[i] : GetSignedArea(contour);
bool isCounterClockwise = signedArea >= 0D;
if (isCounterClockwise != shouldBeCounterClockwise)
{
reverseFlags[i] = true;
needsReversal = true;
}
}
return needsReversal ? reverseFlags : null;
}
private sealed class OutputBuilder
{
// Clipper integer-space tolerances converted to double-space equivalents
// of ClipperD(6).
private const double NearPointDelta = 2E-6D;
private const double MinimumSplitArea = 2E-12D;
private const double SignificantTriangleArea = 1E-12D;
private readonly SelfIntersectionSweepLine sweepLine;
private bool buildHierarchy;
///
/// Initializes a new instance of the class.
///
public OutputBuilder() => this.sweepLine = new SelfIntersectionSweepLine();
///
/// Gets or sets a value indicating whether collinear output points are preserved.
///
public bool PreserveCollinear
{
get => this.sweepLine.PreserveCollinear;
set => this.sweepLine.PreserveCollinear = value;
}
///
/// Gets or sets a value indicating whether the output orientation is reversed.
///
public bool ReverseSolution { get; set; }
///
/// Gets a retained-capacity score used by caller-side pooling policy.
///
public int RetainedCapacityScore => this.sweepLine.RetainedCapacityScore;
///
/// Clears all cached input and output data.
///
public void Clear() => this.sweepLine.Clear();
///
/// Resets mutable state so this instance can be safely reused.
///
public void ResetForReuse()
{
this.buildHierarchy = false;
this.ReverseSolution = false;
this.PreserveCollinear = true;
this.Clear();
}
///
/// Adds subject contours to the sweep-line clipper.
///
/// The subject contours to add.
public void AddSubject(List> paths) => this.sweepLine.AddSubject(paths);
///
/// Determines whether two points are within a tight tolerance.
///
/// The first point.
/// The second point.
/// if the points are nearly coincident.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool ArePointsVeryClose(in Vertex firstPoint, in Vertex secondPoint)
{
Vertex delta = Vertex.Abs(firstPoint - secondPoint);
return delta.X < NearPointDelta && delta.Y < NearPointDelta;
}
///
/// Tests whether an output ring collapses to a very small triangle.
///
/// A point on the ring.
/// if the triangle is degenerate.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsVerySmallTriangle(OutputPoint outputPoint) => outputPoint.Next!.Next == outputPoint.Prev &&
(ArePointsVeryClose(outputPoint.Prev.Point, outputPoint.Next.Point) ||
ArePointsVeryClose(outputPoint.Point, outputPoint.Next.Point) ||
ArePointsVeryClose(outputPoint.Point, outputPoint.Prev.Point));
///
/// Validates that an output ring is a non-degenerate closed loop.
///
/// A point on the ring.
/// if the ring is valid.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsValidClosedPath(OutputPoint? outputPoint) => outputPoint != null && outputPoint.Next != outputPoint &&
(outputPoint.Next != outputPoint.Prev || !IsVerySmallTriangle(outputPoint));
///
/// Removes an output point from the ring and returns the next point.
///
/// The output point to remove.
/// The next output point in the ring, or .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static OutputPoint? RecycleOutputPoint(OutputPoint outputPoint)
{
OutputPoint? result = outputPoint.Next == outputPoint ? null : outputPoint.Next;
outputPoint.Prev.Next = outputPoint.Next;
outputPoint.Next!.Prev = outputPoint.Prev;
return result;
}
///
/// Creates a new output record with the next stable index.
///
/// The created output record.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private OutputRecord CreateOutputRecord()
{
int idx = this.sweepLine.OutputRecords.Count;
OutputRecord result = this.sweepLine.OutputRecords.Add();
result.Index = idx;
return result;
}
///
/// Duplicates an output point and inserts it before or after the original.
///
/// The point to duplicate.
/// Whether to insert after the original.
/// The newly inserted output point.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private OutputPoint DuplicateOutputPoint(OutputPoint outputPoint, bool insertAfter)
{
OutputPoint result = this.sweepLine.OutputPoints.Add(outputPoint.Point, outputPoint.OutputRecord);
if (insertAfter)
{
result.Next = outputPoint.Next;
result.Next!.Prev = result;
result.Prev = outputPoint;
outputPoint.Next = result;
}
else
{
result.Prev = outputPoint.Prev;
result.Prev.Next = result;
result.Next = outputPoint;
outputPoint.Prev = result;
}
return result;
}
///
/// Removes collinear points and resolves self-intersections in an output record.
///
/// The output record to clean.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CleanCollinearEdges(OutputRecord? outputRecord)
{
outputRecord = SelfIntersectionSweepLine.ResolveOutputRecord(outputRecord);
if (outputRecord == null)
{
return;
}
if (!IsValidClosedPath(outputRecord.Points))
{
outputRecord.Points = null;
return;
}
OutputPoint startOp = outputRecord.Points!;
OutputPoint? outputPoint2 = startOp;
while (true)
{
// Preserve immediate A-B-A return spikes. The flat-ring fix injects
// these intentionally (touch -> tip -> touch), so we keep apex B even
// when collinear to avoid collapsing expected boundary detail.
bool isReturnSpikeApex = outputPoint2!.Prev.Point == outputPoint2.Next!.Point &&
outputPoint2.Point != outputPoint2.Prev.Point;
if (PolygonUtilities.IsCollinear(outputPoint2!.Prev.Point, outputPoint2.Point, outputPoint2.Next!.Point) &&
(outputPoint2.Point == outputPoint2.Prev.Point ||
outputPoint2.Point == outputPoint2.Next.Point ||
(!this.PreserveCollinear && !isReturnSpikeApex) ||
(PolygonUtilities.Dot(outputPoint2.Prev.Point, outputPoint2.Point, outputPoint2.Next.Point) < 0 &&
!isReturnSpikeApex)))
{
if (outputPoint2 == outputRecord.Points)
{
outputRecord.Points = outputPoint2.Prev;
}
outputPoint2 = RecycleOutputPoint(outputPoint2);
if (!IsValidClosedPath(outputPoint2))
{
outputRecord.Points = null;
return;
}
startOp = outputPoint2!;
continue;
}
outputPoint2 = outputPoint2.Next;
if (outputPoint2 == startOp)
{
break;
}
}
this.FixSelfIntersections(outputRecord);
}
///
/// Splits an output record at a self-intersection.
///
/// The record being split.
/// The output point where the split occurs.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SplitOutputRecord(OutputRecord outputRecord, OutputPoint splitOp)
{
// The segments (splitOp.Prev, splitOp) and (splitOp.Next, splitOp.Next.Next) intersect.
OutputPoint prevOp = splitOp.Prev;
OutputPoint nextNextOp = splitOp.Next!.Next!;
outputRecord.Points = prevOp;
PolygonUtilities.TryGetLineIntersection(
prevOp.Point, splitOp.Point, splitOp.Next.Point, nextNextOp.Point, out Vertex intersectionPoint);
double area1 = SelfIntersectionSweepLine.ComputeSignedArea(prevOp);
double absArea1 = Math.Abs(area1);
if (absArea1 < MinimumSplitArea)
{
outputRecord.Points = null;
return;
}
double area2 = AreaTriangle(intersectionPoint, splitOp.Point, splitOp.Next.Point);
double absArea2 = Math.Abs(area2);
// Remove the crossing segment and insert the intersection point.
if (intersectionPoint == prevOp.Point || intersectionPoint == nextNextOp.Point)
{
nextNextOp.Prev = prevOp;
prevOp.Next = nextNextOp;
}
else
{
OutputPoint newOp2 = this.sweepLine.OutputPoints.Add(intersectionPoint, outputRecord);
newOp2.Prev = prevOp;
newOp2.Next = nextNextOp;
nextNextOp.Prev = newOp2;
prevOp.Next = newOp2;
}
// Note: area1 is the path's signed area *before* splitting, whereas area2 is
// the signed area of the triangle containing splitOp & splitOp.Next.
// So the only way for these areas to have the same sign is if
// the split triangle is larger than the path containing prevOp or
// if there's more than one self-intersection.
if (!(absArea2 > SignificantTriangleArea) ||
(!(absArea2 > absArea1) &&
((area2 > 0) != (area1 > 0))))
{
return;
}
OutputRecord newOutputRecord = this.CreateOutputRecord();
newOutputRecord.Owner = outputRecord.Owner;
splitOp.OutputRecord = newOutputRecord;
splitOp.Next.OutputRecord = newOutputRecord;
OutputPoint newOp = this.sweepLine.OutputPoints.Add(intersectionPoint, newOutputRecord);
newOp.Prev = splitOp.Next;
newOp.Next = splitOp;
newOutputRecord.Points = newOp;
splitOp.Prev = newOp;
splitOp.Next.Next = newOp;
if (!this.buildHierarchy)
{
return;
}
if (SelfIntersectionSweepLine.IsPathInsidePath(prevOp, newOp))
{
newOutputRecord.Splits ??= [];
newOutputRecord.Splits.Add(outputRecord.Index);
}
else
{
outputRecord.Splits ??= [];
outputRecord.Splits.Add(newOutputRecord.Index);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double AreaTriangle(in Vertex pt1, in Vertex pt2, in Vertex pt3)
=> ((pt3.Y + pt1.Y) * (pt3.X - pt1.X)) +
((pt1.Y + pt2.Y) * (pt1.X - pt2.X)) +
((pt2.Y + pt3.Y) * (pt2.X - pt3.X));
///
/// Resolves self-intersections within an output record.
///
/// The output record to inspect.
private void FixSelfIntersections(OutputRecord outputRecord)
{
OutputPoint outputPoint2 = outputRecord.Points!;
if (outputPoint2.Prev == outputPoint2.Next!.Next)
{
// Triangles cannot self-intersect.
return;
}
while (true)
{
if (PolygonUtilities.SegmentsIntersect(
outputPoint2!.Prev.Point,
outputPoint2.Point,
outputPoint2.Next!.Point,
outputPoint2.Next.Next!.Point))
{
if (PolygonUtilities.SegmentsIntersect(
outputPoint2.Prev.Point,
outputPoint2.Point,
outputPoint2.Next.Next!.Point,
outputPoint2.Next.Next.Next!.Point))
{
// Adjacent intersections (micro self-intersection).
outputPoint2 = this.DuplicateOutputPoint(outputPoint2, false);
outputPoint2.Point = outputPoint2.Next!.Next!.Next!.Point;
outputPoint2 = outputPoint2.Next;
}
else
{
if (outputPoint2 == outputRecord.Points || outputPoint2.Next == outputRecord.Points)
{
outputRecord.Points = outputRecord.Points.Prev;
}
this.SplitOutputRecord(outputRecord, outputPoint2);
if (outputRecord.Points == null)
{
return;
}
outputPoint2 = outputRecord.Points;
// Triangles cannot self-intersect.
if (outputPoint2.Prev == outputPoint2.Next!.Next)
{
break;
}
continue;
}
}
outputPoint2 = outputPoint2.Next!;
if (outputPoint2 == outputRecord.Points)
{
break;
}
}
}
///
/// Builds a lightweight path from an output ring.
///
/// A point on the output ring.
/// Whether to reverse point order.
/// The destination contour.
/// if a valid path was built.
private static bool BuildPath(OutputPoint? outputPoint, bool reverse, List path)
{
if (outputPoint == null || outputPoint.Next == outputPoint || outputPoint.Next == outputPoint.Prev)
{
return false;
}
path.Clear();
Vertex lastPoint;
OutputPoint currentPoint;
if (reverse)
{
lastPoint = outputPoint.Point;
currentPoint = outputPoint.Prev;
}
else
{
outputPoint = outputPoint.Next!;
lastPoint = outputPoint.Point;
currentPoint = outputPoint.Next!;
}
path.Add(lastPoint);
while (currentPoint != outputPoint)
{
if (currentPoint.Point != lastPoint)
{
lastPoint = currentPoint.Point;
path.Add(lastPoint);
}
currentPoint = reverse ? currentPoint.Prev : currentPoint.Next!;
}
return path.Count != 3 || !IsVerySmallTriangle(currentPoint);
}
///
/// Builds a contour from an output ring.
///
/// A point on the output ring.
/// Whether to reverse point order.
/// The destination contour.
/// if a valid contour was built.
private static bool BuildContour(OutputPoint? outputPoint, bool reverse, Contour contour)
{
if (outputPoint == null || outputPoint.Next == outputPoint || outputPoint.Next == outputPoint.Prev)
{
return false;
}
contour.Clear();
Vertex lastPoint;
OutputPoint currentPoint;
if (reverse)
{
lastPoint = outputPoint.Point;
currentPoint = outputPoint.Prev;
}
else
{
outputPoint = outputPoint.Next!;
lastPoint = outputPoint.Point;
currentPoint = outputPoint.Next!;
}
contour.Add(lastPoint);
while (currentPoint != outputPoint)
{
Vertex current = currentPoint.Point;
if (current != lastPoint)
{
lastPoint = current;
contour.Add(lastPoint);
}
currentPoint = reverse ? currentPoint.Prev : currentPoint.Next!;
}
if (contour.Count == 3 && IsVerySmallTriangle(currentPoint))
{
contour.Clear();
return false;
}
if (contour.Count < 3)
{
contour.Clear();
return false;
}
return true;
}
///
/// Ensures an output record has bounds populated and valid geometry.
///
/// The output record to check.
/// if bounds are available.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool CheckOutputBounds(OutputRecord outputRecord)
{
if (outputRecord.Points == null)
{
return false;
}
if (!outputRecord.Bounds.IsEmpty())
{
return true;
}
this.CleanCollinearEdges(outputRecord);
if (outputRecord.Points == null)
{
return false;
}
if (outputRecord.OutputPointCount > 0)
{
outputRecord.Path.EnsureCapacity(outputRecord.OutputPointCount);
}
if (!BuildPath(outputRecord.Points, this.ReverseSolution, outputRecord.Path))
{
return false;
}
outputRecord.Bounds = PolygonUtilities.GetBounds(outputRecord.Path);
return true;
}
///
/// Determines ownership for split output records.
///
/// The output record whose owner is being resolved.
/// The split indices to evaluate.
/// if ownership could be resolved.
private bool CheckSplitOwner(OutputRecord outputRecord, List? splits)
{
// Use indexing because splits can be modified during iteration (Issue #1029).
for (int i = 0; i < splits!.Count; i++)
{
OutputRecord? splitRecord = this.sweepLine.OutputRecords[splits[i]];
if (splitRecord.Points == null && splitRecord.Splits != null &&
this.CheckSplitOwner(outputRecord, splitRecord.Splits))
{
// Issue #942.
return true;
}
splitRecord = SelfIntersectionSweepLine.ResolveOutputRecord(splitRecord);
if (splitRecord == null || splitRecord == outputRecord || splitRecord.RecursiveSplit == outputRecord)
{
continue;
}
// Issue #599.
splitRecord.RecursiveSplit = outputRecord;
if (splitRecord.Splits != null && this.CheckSplitOwner(outputRecord, splitRecord.Splits))
{
return true;
}
if (!this.CheckOutputBounds(splitRecord) ||
!splitRecord.Bounds.Contains(outputRecord.Bounds) ||
!SelfIntersectionSweepLine.IsPathInsidePath(outputRecord.Points!, splitRecord.Points!))
{
continue;
}
// splitRecord is owned by outputRecord (Issue #957).
if (!SelfIntersectionSweepLine.IsOwnerValid(outputRecord, splitRecord))
{
splitRecord.Owner = outputRecord.Owner;
}
// Found in splitRecord.
outputRecord.Owner = splitRecord;
return true;
}
return false;
}
///
/// Resolves the owning output record for hierarchy construction.
///
/// The output record to resolve.
private void ResolveOutputOwner(OutputRecord outputRecord)
{
if (outputRecord.Bounds.IsEmpty())
{
return;
}
while (outputRecord.Owner != null)
{
if (outputRecord.Owner.Splits != null &&
this.CheckSplitOwner(outputRecord, outputRecord.Owner.Splits))
{
break;
}
if (outputRecord.Owner.Points != null && this.CheckOutputBounds(outputRecord.Owner) &&
SelfIntersectionSweepLine.IsPathInsidePath(outputRecord.Points!, outputRecord.Owner.Points!))
{
break;
}
outputRecord.Owner = outputRecord.Owner.Owner;
}
}
///
/// Builds and returns a hierarchical polygon from output records.
///
/// The initial contour capacity for the output polygon.
/// The built polygon.
private Polygon BuildPolygon(int resultCapacity)
{
int validClosedCount = 0;
int i = 0;
// First pass: validate bounds and resolve owners.
// Complexity is O(N) over current output records, but N can grow during
// the pass because CheckOutputBounds may split/fix paths and append records.
while (i < this.sweepLine.OutputRecords.Count)
{
OutputRecord outputRecord = this.sweepLine.OutputRecords[i++];
if (outputRecord.Points == null)
{
continue;
}
if (this.CheckOutputBounds(outputRecord))
{
this.ResolveOutputOwner(outputRecord);
validClosedCount++;
}
}
if (validClosedCount == 0)
{
return new Polygon(resultCapacity);
}
int outputRecordCount = this.sweepLine.OutputRecords.Count;
Polygon polygon = new(Math.Max(resultCapacity, validClosedCount));
using Buffer contourIndexBuffer = new(outputRecordCount);
Span contourIndexByOutputRecord = contourIndexBuffer.GetSpan();
contourIndexByOutputRecord.Fill(-1);
// Second pass: build contours and map OutputRecord.Index -> contour index.
// This avoids a dictionary allocation and keeps lookups O(1).
for (int index = 0; index < outputRecordCount; index++)
{
OutputRecord outputRecord = this.sweepLine.OutputRecords[index];
if (outputRecord.Points == null || outputRecord.Bounds.IsEmpty())
{
continue;
}
int estimatedCapacity = outputRecord.OutputPointCount > 0
? outputRecord.OutputPointCount
: 0;
Contour contour = estimatedCapacity > 0 ? new Contour(estimatedCapacity) : [];
if (!BuildContour(outputRecord.Points, this.ReverseSolution, contour))
{
continue;
}
int contourIndex = polygon.Count;
polygon.Add(contour);
int outputRecordIndex = outputRecord.Index;
if ((uint)outputRecordIndex < (uint)contourIndexByOutputRecord.Length)
{
contourIndexByOutputRecord[outputRecordIndex] = contourIndex;
}
}
if (polygon.Count == 0)
{
return polygon;
}
for (int index = 0; index < polygon.Count; index++)
{
Contour contour = polygon[index];
contour.ParentIndex = null;
contour.Depth = 0;
contour.ClearHoles();
}
// Third pass: map owner links to parent contour indices.
for (int index = 0; index < outputRecordCount; index++)
{
OutputRecord outputRecord = this.sweepLine.OutputRecords[index];
int outputRecordIndex = outputRecord.Index;
if ((uint)outputRecordIndex >= (uint)contourIndexByOutputRecord.Length)
{
continue;
}
int contourIndex = contourIndexByOutputRecord[outputRecordIndex];
if (contourIndex < 0)
{
continue;
}
OutputRecord? owner = outputRecord.Owner;
while (owner != null)
{
int ownerIndex = owner.Index;
if ((uint)ownerIndex < (uint)contourIndexByOutputRecord.Length)
{
int parentIndex = contourIndexByOutputRecord[ownerIndex];
if (parentIndex >= 0)
{
polygon[contourIndex].ParentIndex = parentIndex;
break;
}
}
owner = owner.Owner;
}
}
// Fourth pass: depth is owner-chain length within the emitted contour set.
for (int index = 0; index < outputRecordCount; index++)
{
OutputRecord outputRecord = this.sweepLine.OutputRecords[index];
int outputRecordIndex = outputRecord.Index;
if ((uint)outputRecordIndex >= (uint)contourIndexByOutputRecord.Length)
{
continue;
}
int contourIndex = contourIndexByOutputRecord[outputRecordIndex];
if (contourIndex < 0)
{
continue;
}
// Depth is the number of owning contours in the chain.
int depth = 0;
OutputRecord? owner = outputRecord.Owner;
while (owner != null)
{
int ownerIndex = owner.Index;
if ((uint)ownerIndex < (uint)contourIndexByOutputRecord.Length &&
contourIndexByOutputRecord[ownerIndex] >= 0)
{
depth++;
}
owner = owner.Owner;
}
polygon[contourIndex].Depth = depth;
}
for (int index = 0; index < polygon.Count; index++)
{
Contour contour = polygon[index];
if (contour.ParentIndex != null)
{
// Map parent links to hole indices for quick traversal.
polygon[contour.ParentIndex.Value].AddHoleIndex(index);
}
}
return polygon;
}
///
/// Executes the union and returns a hierarchical polygon.
///
/// The initial contour capacity for the output polygon.
/// The resulting polygon.
public Polygon Execute(int resultCapacity)
{
this.buildHierarchy = true;
bool succeeded = this.sweepLine.Execute(true);
Polygon result = succeeded ? this.BuildPolygon(resultCapacity) : [];
this.sweepLine.ClearSolutionData();
return result;
}
}
}
}