// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.Collections.Generic; using System.Runtime.CompilerServices; namespace SixLabors.PolygonClipper { /// /// Sweep-line union clipper specialized for self-intersection removal. /// /// /// This clipper consumes subject-only paths and applies positive winding fill /// semantics to compute the union. It reuses pooled data structures to keep allocations low. /// internal sealed class SelfIntersectionSweepLine { // Clipper's integer constants are calibrated for scaled coordinates. // This port operates directly in double-space, so thresholds must be // converted to the equivalent ClipperD(6) magnitudes. private const double JoinExtremaDelta = 2E-6D; private const double JoinPerpendicularDistanceSquaredTolerance = 2.5E-13D; private const int HorizontalLoopFailSafeLimit = 100_000; private readonly ActiveEdgeList activeEdges; private readonly ScanlineSchedule scanlineSchedule; private readonly List intersectionList; private readonly VertexPoolList vertexList; private readonly List horizontalSegments; private readonly HorizontalJoinPoolList horizontalJoins; private double currentScanlineBottomY; private bool buildHierarchy; private bool succeeded; /// /// Initializes a new instance of the class. /// public SelfIntersectionSweepLine() { this.activeEdges = new ActiveEdgeList(); this.scanlineSchedule = new ScanlineSchedule(); this.intersectionList = []; this.vertexList = []; this.OutputRecords = []; this.horizontalSegments = []; this.horizontalJoins = []; this.OutputPoints = []; this.PreserveCollinear = true; } /// /// Gets or sets a value indicating whether collinear output points are preserved. /// public bool PreserveCollinear { get; set; } /// /// Gets the pooled output records produced by the sweep. /// public OutputRecordPoolList OutputRecords { get; } /// /// Gets the pooled output points produced by the sweep. /// public OutputPointPoolList OutputPoints { get; } /// /// Gets a retained-capacity score used by caller-side pooling policy. /// public int RetainedCapacityScore => this.scanlineSchedule.RetainedCapacityScore + this.intersectionList.Capacity + this.vertexList.Capacity + this.horizontalSegments.Capacity + this.horizontalJoins.Capacity + this.OutputRecords.Capacity + this.OutputPoints.Capacity + this.activeEdges.RetainedPoolCount; /// /// Swaps two active edge references. /// /// The first active edge. /// The second active edge. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SwapActiveEdges(ref ActiveEdge edge1, ref ActiveEdge edge2) => (edge2, edge1) = (edge1, edge2); /// /// Locates the active edge that shares the same maxima vertex. /// /// The active edge being matched. /// The paired maxima edge, or if none exists in the active list. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ActiveEdge? FindMaximaPair(ActiveEdge edge) { ActiveEdge? edge2 = edge.NextInAel; while (edge2 != null) { if (edge2.VertexTop == edge.VertexTop) { // Matched the companion maxima edge. return edge2; } edge2 = edge2.NextInAel; } return null; } /// /// Returns the maxima vertex on the current Y scanline for the edge. /// /// The active edge to inspect. /// The maxima vertex at the current Y, or if not a maxima. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static SweepVertex? GetMaximaVertexAtCurrentY(ActiveEdge edge) { SweepVertex? result = edge.VertexTop; if (result == null) { return null; } SweepVertex start = result; // Horizontal plateaus at the top can have multiple same-Y vertices. // Follow the plateau in winding direction to find the effective scanline // endpoint candidate in O(k), where k is plateau length. if (edge.WindDelta > 0) { while (result.Next!.Point.Y == result.Point.Y) { SweepVertex next = result.Next; if (next == start) { break; } result = next; } } else { while (result.Prev!.Point.Y == result.Point.Y) { SweepVertex prev = result.Prev; if (prev == start) { break; } result = prev; } } // If the traversed endpoint is not flagged maxima but the start vertex is // a maxima on the same scanline, prefer the explicit maxima marker. if (!result.IsMaxima && start.IsMaxima && start.Point.Y == result.Point.Y) { result = start; } if (!result.IsMaxima) { // No maxima at the current scanline. result = null; } return result; } /// /// Assigns the output record edges that define the front and back sides. /// /// The output record to update. /// The edge used for the front side. /// The edge used for the back side. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SetOutputSides(OutputRecord outputRecord, ActiveEdge startEdge, ActiveEdge endEdge) { outputRecord.FrontEdge = startEdge; outputRecord.BackEdge = endEdge; } /// /// Swaps output record ownership between two active edges. /// /// The first active edge. /// The second active edge. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SwapOutputRecords(ActiveEdge edge1, ActiveEdge edge2) { // At least one edge already owns an output record. OutputRecord? outputRecord1 = edge1.OutputRecord; OutputRecord? outputRecord2 = edge2.OutputRecord; if (outputRecord1 == outputRecord2) { ActiveEdge? edge = outputRecord1!.FrontEdge; outputRecord1.FrontEdge = outputRecord1.BackEdge; outputRecord1.BackEdge = edge; return; } if (outputRecord1 != null) { if (edge1 == outputRecord1.FrontEdge) { outputRecord1.FrontEdge = edge2; } else { outputRecord1.BackEdge = edge2; } } if (outputRecord2 != null) { if (edge2 == outputRecord2.FrontEdge) { outputRecord2.FrontEdge = edge1; } else { outputRecord2.BackEdge = edge1; } } edge1.OutputRecord = outputRecord2; edge2.OutputRecord = outputRecord1; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsTwoVertexFlatRingEdge(ActiveEdge edge) => // Degenerate "ring" used by issue-style tests: two opposing horizontal // edges around a single local minimum (A-B-A). This is an O(1) shape check. edge.IsHorizontal && edge.LocalMin.Vertex.Prev == edge.LocalMin.Vertex.Next; [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vertex GetFlatRingTip(ActiveEdge flatEdge, in Vertex touchPoint) { // If touch is at an endpoint, the tip is the opposite endpoint. if (touchPoint == flatEdge.Bottom) { return flatEdge.Top; } if (touchPoint == flatEdge.Top) { return flatEdge.Bottom; } // Otherwise choose the endpoint farther from the touch in X. // This gives a stable spike apex for touch->tip->touch emission. double bottomDx = Math.Abs(touchPoint.X - flatEdge.Bottom.X); double topDx = Math.Abs(touchPoint.X - flatEdge.Top.X); return bottomDx > topDx ? flatEdge.Bottom : flatEdge.Top; } /// /// Assigns an output record's owner while preventing cyclic ownership. /// /// The output record to update. /// The candidate owner. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SetOutputOwner(OutputRecord outputRecord, OutputRecord newOwner) { // Precondition: newOwner is never null. while (newOwner.Owner != null && newOwner.Owner.Points == null) { newOwner.Owner = newOwner.Owner.Owner; } // Avoid cycles: ensure outputRecord is not already an ancestor of newOwner. OutputRecord? tmp = newOwner; while (tmp != null && tmp != outputRecord) { tmp = tmp.Owner; } if (tmp != null) { newOwner.Owner = outputRecord.Owner; } outputRecord.Owner = newOwner; } /// /// Computes the signed area of a closed output ring. /// /// A point on the output ring. /// The signed area of the ring. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double ComputeSignedArea(OutputPoint outputPoint) { // https://en.wikipedia.org/wiki/Shoelace_formula double signedArea = 0.0; OutputPoint outputPoint2 = outputPoint; do { signedArea += Vertex.Cross(outputPoint2.Prev.Point, outputPoint2.Point); outputPoint2 = outputPoint2.Next!; } while (outputPoint2 != outputPoint); return signedArea * 0.5; } /// /// Resolves a non-null output record that still owns geometry. /// /// The candidate output record. /// The resolved output record, or if none remains. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static OutputRecord? ResolveOutputRecord(OutputRecord? outputRecord) { while (outputRecord != null && outputRecord.Points == null) { outputRecord = outputRecord.Owner; } return outputRecord; } /// /// Validates that an output record is not owned by a descendant. /// /// The output record to validate. /// The owner candidate. /// when the ownership chain is valid. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsOwnerValid(OutputRecord? outputRecord, OutputRecord? testOwner) { while (testOwner != null && testOwner != outputRecord) { testOwner = testOwner.Owner; } return testOwner == null; } /// /// Clears output record links from a hot edge. /// /// The active edge to detach. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void DetachOutputRecord(ActiveEdge edge) { OutputRecord? outputRecord = edge.OutputRecord; if (outputRecord == null) { return; } outputRecord.FrontEdge!.OutputRecord = null; outputRecord.BackEdge!.OutputRecord = null; outputRecord.FrontEdge = null; outputRecord.BackEdge = null; } /// /// Determines whether an edge is the front edge of its output record. /// /// The active edge to query. /// when the edge is the front edge. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsOutputRecordAscending(ActiveEdge hotEdge) => hotEdge == hotEdge.OutputRecord!.FrontEdge; /// /// Checks whether the two edges in an intersection node are adjacent in the active list. /// /// The intersection node to inspect. /// if the edges are adjacent in the active list. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool AreEdgesAdjacentInActiveList(in IntersectNode intersectionNode) => (intersectionNode.Edge1.NextInAel == intersectionNode.Edge2) || (intersectionNode.Edge1.PrevInAel == intersectionNode.Edge2); /// /// Clears solution-only data while preserving the input vertices and minima list. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ClearSolutionData() { this.activeEdges.ClearActiveEdges(); this.scanlineSchedule.ClearScanlines(); this.ClearIntersectionNodes(); this.OutputRecords.Clear(); this.horizontalSegments.Clear(); this.horizontalJoins.Clear(); this.OutputPoints.Clear(); } /// /// Clears all clipper state, including cached input data. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Clear() { this.ClearSolutionData(); this.scanlineSchedule.Clear(); this.vertexList.Clear(); } /// /// Resets scanline state and sorts local minima before an execution pass. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ResetState() { this.scanlineSchedule.Reset(); this.currentScanlineBottomY = 0; this.activeEdges.Reset(); this.succeeded = true; } /// /// Adds subject contours for the union operation. /// /// The subject contours to add. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AddSubject(List> paths) { this.scanlineSchedule.MarkDirty(); this.AddPathsToVertexList(paths); } /// /// Registers a local minima vertex once for the sweep. /// /// The vertex that marks a local minima. /// The schedule collecting minima. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void RegisterLocalMinima(SweepVertex vertex, ScanlineSchedule scanlineSchedule) { // Guard against registering the same vertex twice. if ((vertex.Flags & VertexFlags.LocalMin) != VertexFlags.None) { return; } vertex.Flags |= VertexFlags.LocalMin; scanlineSchedule.AddLocalMinima(new LocalMinima(vertex)); } /// /// Builds circular vertex lists and captures local minima/maxima for the sweep. /// /// The subject contours to process. private void AddPathsToVertexList(List> paths) { int totalVertCnt = 0; foreach (List path in paths) { totalVertCnt += path.Count; } // Pre-size the pool to avoid growth during vertex creation. this.vertexList.EnsureCapacity(this.vertexList.Count + totalVertCnt); foreach (List path in paths) { SweepVertex? v0 = null; SweepVertex? prevVertex = null; SweepVertex? currVertex; foreach (Vertex point in path) { if (v0 == null) { v0 = this.vertexList.Add(point, VertexFlags.None, null); prevVertex = v0; continue; } if (prevVertex!.Point != point) { currVertex = this.vertexList.Add(point, VertexFlags.None, prevVertex); prevVertex.Next = currVertex; prevVertex = currVertex; } } if (v0 == null || prevVertex?.Prev == null) { continue; } if (prevVertex.Point == v0.Point) { prevVertex = prevVertex.Prev; } prevVertex.Next = v0; v0.Prev = prevVertex; if (prevVertex.Next == prevVertex) { continue; } // Non-degenerate closed ring. prevVertex = v0.Prev; while (prevVertex != v0 && prevVertex!.Point.Y == v0.Point.Y) { prevVertex = prevVertex.Prev; } if (prevVertex == v0) { // Flat closed rings still contribute when they touch other contours. if (!RegisterFlatRingExtrema(v0, this.scanlineSchedule)) { continue; } continue; } bool goingUp = prevVertex.Point.Y > v0.Point.Y; bool goingUp0 = goingUp; prevVertex = v0; currVertex = v0.Next; while (currVertex != v0) { if (currVertex!.Point.Y > prevVertex.Point.Y && goingUp) { prevVertex.Flags |= VertexFlags.LocalMax; goingUp = false; } else if (currVertex.Point.Y < prevVertex.Point.Y && !goingUp) { goingUp = true; RegisterLocalMinima(prevVertex, this.scanlineSchedule); } prevVertex = currVertex; currVertex = currVertex.Next; } if (goingUp != goingUp0) { if (goingUp0) { RegisterLocalMinima(prevVertex, this.scanlineSchedule); } else { prevVertex.Flags |= VertexFlags.LocalMax; } } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool RegisterFlatRingExtrema(SweepVertex start, ScanlineSchedule scanlineSchedule) { // For a fully flat closed ring, derive synthetic extrema by scanning once // for left/right-most vertices: O(m) in ring vertex count. SweepVertex leftMost = start; SweepVertex rightMost = start; SweepVertex current = start.Next!; while (current != start) { if (current.Point.X < leftMost.Point.X) { leftMost = current; } if (current.Point.X > rightMost.Point.X) { rightMost = current; } current = current.Next!; } if (leftMost == rightMost) { return false; } rightMost.Flags |= VertexFlags.LocalMax; RegisterLocalMinima(leftMost, scanlineSchedule); return true; } /// /// Determines whether a closed edge contributes to the union result. /// /// The edge to test. /// if the edge contributes. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsContributingClosedEdge(ActiveEdge edge) => edge.WindCount == 1; /// /// Updates the winding count for a closed path edge. /// /// The active edge to update. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SetWindingCountForClosedEdge(ActiveEdge edge) { // Winding counts apply to regions, not edges. The edge wind count tracks the // higher of the two adjacent region counts. Adjacent regions differ by one. ActiveEdge? edge2 = edge.PrevInAel; if (edge2 == null) { edge.WindCount = edge.WindDelta; } else { // If edge2's wind count follows its wind delta, // the filled region is to the right of edge2 (so edge is inside). Neither value is 0. if (edge2.WindCount * edge2.WindDelta < 0) { // Opposite signs: edge lies outside edge2's region. if (Math.Abs(edge2.WindCount) > 1) { // Outside this polygon but still inside another. if (edge2.WindDelta * edge.WindDelta < 0) { // Reversing direction; keep the same winding count. edge.WindCount = edge2.WindCount; } else { // Otherwise step the winding count toward zero. edge.WindCount = edge2.WindCount + edge.WindDelta; } } else { // Outside all polygons; reset to the edge's own winding. edge.WindCount = edge.WindDelta; } } else { // Same sign: edge lies inside edge2's region. if (edge2.WindDelta * edge.WindDelta < 0) { // Reversing direction; keep the same winding count. edge.WindCount = edge2.WindCount; } else { // Otherwise step the winding count away from zero. edge.WindCount = edge2.WindCount + edge.WindDelta; } } } } /// /// Inserts any local minima that occur at the current scanline into the active list. /// /// The current scanline Y coordinate. private void InsertLocalMinimaIntoActiveList(double botY) { // Insert all minima on the current scanline. // Horizontal minima use the previous vertex as the descending bound. while (this.scanlineSchedule.HasLocalMinimaAtY(botY)) { LocalMinima localMinima = this.scanlineSchedule.PopLocalMinima(); ActiveEdge leftBound = this.activeEdges.Acquire(); leftBound.Bottom = localMinima.Vertex.Point; leftBound.CurrentX = localMinima.Vertex.Point.X; leftBound.WindDelta = -1; leftBound.VertexTop = localMinima.Vertex.Prev; leftBound.Top = localMinima.Vertex.Prev!.Point; leftBound.OutputRecord = null; leftBound.LocalMin = localMinima; leftBound.UpdateDx(); ActiveEdge rightBound = this.activeEdges.Acquire(); rightBound.Bottom = localMinima.Vertex.Point; rightBound.CurrentX = localMinima.Vertex.Point.X; rightBound.WindDelta = 1; // Ascending bound. rightBound.VertexTop = localMinima.Vertex.Next; rightBound.Top = localMinima.Vertex.Next!.Point; rightBound.OutputRecord = null; rightBound.LocalMin = localMinima; rightBound.UpdateDx(); // leftBound starts descending and rightBound ascending. // Swap them if their geometric ordering is inverted. if (leftBound.IsHorizontal) { if (leftBound.IsHeadingRightHorizontal) { SwapActiveEdges(ref leftBound, ref rightBound); } } else if (rightBound.IsHorizontal) { if (rightBound.IsHeadingLeftHorizontal) { SwapActiveEdges(ref leftBound, ref rightBound); } } else if (leftBound.Dx < rightBound.Dx) { SwapActiveEdges(ref leftBound, ref rightBound); } bool contributing; leftBound.IsLeftBound = true; this.activeEdges.InsertLeft(leftBound); SetWindingCountForClosedEdge(leftBound); contributing = IsContributingClosedEdge(leftBound); if (leftBound.IsHorizontal && rightBound.IsHorizontal && leftBound.LocalMin.Vertex.Prev == leftBound.LocalMin.Vertex.Next) { contributing = false; } rightBound.WindCount = leftBound.WindCount; ActiveEdgeList.InsertRight(leftBound, rightBound); if (contributing) { _ = this.AddLocalMinimumOutput(leftBound, rightBound, leftBound.Bottom, true); if (!leftBound.IsHorizontal) { this.CheckJoinLeft(leftBound, leftBound.Bottom); } } while (rightBound.NextInAel != null && ActiveEdgeList.IsValidActiveEdgeOrder(rightBound.NextInAel, rightBound)) { this.IntersectActiveEdges(rightBound, rightBound.NextInAel, rightBound.Bottom); this.activeEdges.SwapPositions(rightBound, rightBound.NextInAel); } if (rightBound.IsHorizontal) { this.activeEdges.PushHorizontal(rightBound); } else { this.CheckJoinRight(rightBound, rightBound.Bottom); this.scanlineSchedule.InsertScanline(rightBound.Top.Y); } if (leftBound.IsHorizontal) { this.activeEdges.PushHorizontal(leftBound); } else { this.scanlineSchedule.InsertScanline(leftBound.Top.Y); } } } /// /// Creates a new output record at a local minimum. /// /// The first bound edge. /// The second bound edge. /// The local minimum point. /// Whether this output is created for a split. /// The created output point. [MethodImpl(MethodImplOptions.AggressiveInlining)] private OutputPoint AddLocalMinimumOutput(ActiveEdge edge1, ActiveEdge edge2, Vertex point, bool isNew = false) { OutputRecord outputRecord = this.CreateOutputRecord(); edge1.OutputRecord = outputRecord; edge2.OutputRecord = outputRecord; ActiveEdge? prevHotEdge = edge1.GetPrevHotEdge(); // WindDelta reflects input winding, not output orientation. // Output orientation is driven by which edge is assigned as the front (ascending) edge. if (prevHotEdge != null) { if (this.buildHierarchy) { SetOutputOwner(outputRecord, prevHotEdge.OutputRecord!); } outputRecord.Owner = prevHotEdge.OutputRecord; if (IsOutputRecordAscending(prevHotEdge) == isNew) { SetOutputSides(outputRecord, edge2, edge1); } else { SetOutputSides(outputRecord, edge1, edge2); } } else { outputRecord.Owner = null; if (isNew) { SetOutputSides(outputRecord, edge1, edge2); } else { SetOutputSides(outputRecord, edge2, edge1); } } OutputPoint outputPoint = this.OutputPoints.Add(point, outputRecord); outputRecord.Points = outputPoint; return outputPoint; } /// /// Joins two output records when a local maximum is encountered. /// /// The first active edge. /// The second active edge. /// The local maximum point. /// The last output point, or when no output remains. [MethodImpl(MethodImplOptions.AggressiveInlining)] private OutputPoint? AddLocalMaximumOutput(ActiveEdge edge1, ActiveEdge edge2, Vertex point) { if (IsJoined(edge1)) { this.SplitEdge(edge1, point); } if (IsJoined(edge2)) { this.SplitEdge(edge2, point); } if (edge1.IsFront == edge2.IsFront) { bool hasTwoVertexFlatEdge = (edge1.IsHorizontal && edge1.NextVertex == edge1.LocalMin.Vertex) || (edge2.IsHorizontal && edge2.NextVertex == edge2.LocalMin.Vertex); if (hasTwoVertexFlatEdge) { OutputPoint outputPoint = this.AddOutputPoint(edge1, point); _ = this.AddOutputPoint(edge2, point); SwapOutputRecords(edge1, edge2); return outputPoint; } if (edge1.IsHorizontal && edge2.IsHorizontal) { return this.AddOutputPoint(edge1, point); } this.succeeded = false; return null; } OutputPoint result = this.AddOutputPoint(edge1, point); if (edge1.OutputRecord == edge2.OutputRecord) { OutputRecord outputRecord = edge1.OutputRecord!; outputRecord.Points = result; if (this.buildHierarchy) { ActiveEdge? e = edge1.GetPrevHotEdge(); if (e == null) { outputRecord.Owner = null; } else { SetOutputOwner(outputRecord, e.OutputRecord!); } // Owner assignment here is provisional and will be resolved later. } DetachOutputRecord(edge1); } // Join in index order to preserve output orientation. else if (edge1.OutputRecord!.Index < edge2.OutputRecord!.Index) { JoinOutputRecords(edge1, edge2); } else { JoinOutputRecords(edge2, edge1); } return result; } /// /// Merges the output paths from two active edges into a single record. /// /// The primary edge to keep. /// The secondary edge to merge. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void JoinOutputRecords(ActiveEdge edge1, ActiveEdge edge2) { // Append edge2's path onto edge1's path, then discard edge2's path pointers. // The joining ends rarely share coordinates, so pointer swaps are safe. OutputPoint p1Start = edge1.OutputRecord!.Points!; OutputPoint p2Start = edge2.OutputRecord!.Points!; OutputPoint p1End = p1Start.Next!; OutputPoint p2End = p2Start.Next!; if (edge1.IsFront) { p2End.Prev = p1Start; p1Start.Next = p2End; p2Start.Next = p1End; p1End.Prev = p2Start; edge1.OutputRecord!.Points = p2Start; edge1.OutputRecord!.FrontEdge = edge2.OutputRecord!.FrontEdge; if (edge1.OutputRecord!.FrontEdge != null) { edge1.OutputRecord!.FrontEdge!.OutputRecord = edge1.OutputRecord; } } else { p1End.Prev = p2Start; p2Start.Next = p1End; p1Start.Next = p2End; p2End.Prev = p1Start; edge1.OutputRecord!.BackEdge = edge2.OutputRecord!.BackEdge; if (edge1.OutputRecord!.BackEdge != null) { edge1.OutputRecord!.BackEdge!.OutputRecord = edge1.OutputRecord; } } // After joining, edge2's output record contains no vertices. edge2.OutputRecord!.FrontEdge = null; edge2.OutputRecord!.BackEdge = null; edge2.OutputRecord!.Points = null; edge1.OutputRecord!.OutputPointCount += edge2.OutputRecord!.OutputPointCount; SetOutputOwner(edge2.OutputRecord, edge1.OutputRecord); // and edge1 and edge2 are maxima and are about to be dropped from the Actives list. edge1.OutputRecord = null; edge2.OutputRecord = null; } /// /// Adds an output point to the front or back of the current output record. /// /// The active edge that owns the output. /// The point to add. /// The output point that was added or reused. [MethodImpl(MethodImplOptions.AggressiveInlining)] private OutputPoint AddOutputPoint(ActiveEdge edge, Vertex point) { // outputRecord.Points is a circular list; Points is the front point and // Points.Next is the back point for this output record. OutputRecord outputRecord = edge.OutputRecord!; bool toFront = edge.IsFront; OutputPoint opFront = outputRecord.Points!; OutputPoint opBack = opFront.Next!; switch (toFront) { case true when point == opFront.Point: return opFront; case false when point == opBack.Point: return opBack; } OutputPoint newOp = this.OutputPoints.Add(point, outputRecord); opBack.Prev = newOp; newOp.Prev = opFront; newOp.Next = opBack; opFront.Next = newOp; if (toFront) { outputRecord.Points = newOp; } return newOp; } /// /// Creates a new output record and assigns the next index. /// /// The created output record. [MethodImpl(MethodImplOptions.AggressiveInlining)] private OutputRecord CreateOutputRecord() { int idx = this.OutputRecords.Count; OutputRecord result = this.OutputRecords.Add(); result.Index = idx; return result; } /// /// Advances the active edge to the next vertex in the scanbeam. /// /// The active edge to update. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void UpdateEdgeInActiveList(ActiveEdge edge) { edge.Bottom = edge.Top; edge.VertexTop = edge.NextVertex; edge.Top = edge.VertexTop.Point; edge.CurrentX = edge.Bottom.X; edge.UpdateDx(); if (IsJoined(edge)) { // Split joined edges before advancing to avoid missing intersections. this.SplitEdge(edge, edge.Bottom); } if (edge.IsHorizontal) { TrimHorizontal(edge, this.PreserveCollinear); return; } this.scanlineSchedule.InsertScanline(edge.Top.Y); this.CheckJoinLeft(edge, edge.Bottom); // Issue #500: check join on the right bound at the bottom point. this.CheckJoinRight(edge, edge.Bottom, true); } /// /// Handles an intersection between two active edges at a given point. /// /// The first intersecting edge. /// The second intersecting edge. /// The intersection point. private void IntersectActiveEdges(ActiveEdge edge1, ActiveEdge edge2, Vertex point) { if (IsJoined(edge1)) { this.SplitEdge(edge1, point); } if (IsJoined(edge2)) { this.SplitEdge(edge2, point); } // Update winding counts for both edges. if (edge1.WindCount + edge2.WindDelta == 0) { edge1.WindCount = -edge1.WindCount; } else { edge1.WindCount += edge2.WindDelta; } if (edge2.WindCount - edge1.WindDelta == 0) { edge2.WindCount = -edge2.WindCount; } else { edge2.WindCount -= edge1.WindDelta; } int oldE1WindCount = edge1.WindCount; int oldE2WindCount = edge2.WindCount; bool e1WindCountIs0or1 = oldE1WindCount is 0 or 1; bool e2WindCountIs0or1 = oldE2WindCount is 0 or 1; bool edge1IsTwoVertexFlatRing = IsTwoVertexFlatRingEdge(edge1); bool edge2IsTwoVertexFlatRing = IsTwoVertexFlatRingEdge(edge2); if ((!edge1.IsHot && !e1WindCountIs0or1 && !edge1IsTwoVertexFlatRing) || (!edge2.IsHot && !e2WindCountIs0or1 && !edge2IsTwoVertexFlatRing)) { return; } if (edge1IsTwoVertexFlatRing || edge2IsTwoVertexFlatRing) { if (edge1.IsHot ^ edge2.IsHot) { ActiveEdge hotEdge = edge1.IsHot ? edge1 : edge2; ActiveEdge flatEdge = edge1IsTwoVertexFlatRing ? edge1 : edge2; if (!flatEdge.IsHot) { // Keep the fix in-sweep: inject touch->tip->touch directly into the // hot output chain in O(1), avoiding any post-process contour scans. Vertex tip = GetFlatRingTip(flatEdge, point); _ = this.AddOutputPoint(hotEdge, point); _ = this.AddOutputPoint(hotEdge, tip); _ = this.AddOutputPoint(hotEdge, point); return; } } } // Emit output based on hot edges and winding state. // If both edges are hot, treat as maxima or crossing. if (edge1.IsHot && edge2.IsHot) { if ((oldE1WindCount != 0 && oldE1WindCount != 1) || (oldE2WindCount != 0 && oldE2WindCount != 1)) { _ = this.AddLocalMaximumOutput(edge1, edge2, point); } else if (edge1.IsFront || (edge1.OutputRecord == edge2.OutputRecord)) { // this 'else if' condition isn't strictly needed but // it's sensible to split polygons that only touch at // a common vertex (not at common edges). _ = this.AddLocalMaximumOutput(edge1, edge2, point); } else { // Treat as a crossing; emit and swap output records. _ = this.AddOutputPoint(edge1, point); SwapOutputRecords(edge1, edge2); } } // If only one edge is hot, emit and swap. else if (edge1.IsHot) { _ = this.AddOutputPoint(edge1, point); SwapOutputRecords(edge1, edge2); } else if (edge2.IsHot) { _ = this.AddOutputPoint(edge2, point); SwapOutputRecords(edge1, edge2); } // If both edges are cold, only minima with winding=1 start output. else { if (oldE1WindCount == 1 && oldE2WindCount == 1) { _ = this.AddLocalMinimumOutput(edge1, edge2, point); } } } /// /// Executes the sweep-line union. /// private void ExecuteInternal() { this.ResetState(); if (!this.scanlineSchedule.TryPopScanline(out double y)) { return; } // Process each scanbeam: insert local minima, handle horizontals, resolve intersections, // then advance to the next scanline. while (this.succeeded) { this.InsertLocalMinimaIntoActiveList(y); ActiveEdge? edge; while (this.activeEdges.TryPopHorizontal(out edge)) { this.ProcessHorizontal(edge!); } if (this.horizontalSegments.Count > 0) { this.ConvertHorizontalSegmentsToJoins(); this.horizontalSegments.Clear(); } // Advance to the next scanbeam. this.currentScanlineBottomY = y; if (!this.scanlineSchedule.TryPopScanline(out y)) { // y is now the new top of the scanbeam. break; } this.ProcessIntersections(y); this.ProcessScanbeamTop(y); while (this.activeEdges.TryPopHorizontal(out edge)) { this.ProcessHorizontal(edge!); } } if (this.succeeded) { this.ProcessHorizontalJoins(); } } /// /// Executes the sweep-line union, leaving output records populated for conversion. /// /// Whether hierarchy-sensitive output ownership is required. /// if the sweep completed successfully. public bool Execute(bool buildHierarchy) { this.buildHierarchy = buildHierarchy; try { this.ExecuteInternal(); } catch { this.succeeded = false; } return this.succeeded; } /// /// Builds and processes edge intersections for the current scanbeam. /// /// The scanbeam top Y coordinate. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ProcessIntersections(double topY) { if (!this.BuildIntersectionList(topY)) { return; } this.ProcessIntersectionList(); this.ClearIntersectionNodes(); } /// /// Clears the list of pending intersection nodes. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ClearIntersectionNodes() => this.intersectionList.Clear(); /// /// Adds a new intersection node between two edges at the current scanbeam. /// /// The first edge. /// The second edge. /// The scanbeam top Y coordinate. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddIntersectionNode(ActiveEdge edge1, ActiveEdge edge2, double topY) { if (!PolygonUtilities.TryGetLineIntersection( edge1.Bottom, edge1.Top, edge2.Bottom, edge2.Top, out Vertex intersectionPoint)) { intersectionPoint = new Vertex(edge1.CurrentX, topY); } // Prevent vertical segment ordering violations: if the intersection creates a perfectly // vertical split where the new split point has the same X but lower Y than the bottom, // nudge X to the next representable value. Vertical segments must be processed bottom-to-top, // but the current bottom event is already being processed, so we cannot reorder. Moving X by // one ULP ensures the split point sorts after the bottom event. // See: https://github.com/21re/rust-geo-booleanop/pull/11 if (intersectionPoint.X == edge1.Bottom.X && intersectionPoint.Y < edge1.Bottom.Y) { intersectionPoint = new Vertex(intersectionPoint.X.NextAfter(double.PositiveInfinity), intersectionPoint.Y); } else if (intersectionPoint.X == edge2.Bottom.X && intersectionPoint.Y < edge2.Bottom.Y) { intersectionPoint = new Vertex(intersectionPoint.X.NextAfter(double.PositiveInfinity), intersectionPoint.Y); } // Clamp intersections that drift outside the scanbeam due to numeric error. if (intersectionPoint.Y > this.currentScanlineBottomY || intersectionPoint.Y < topY) { double absDx1 = Math.Abs(edge1.Dx); double absDx2 = Math.Abs(edge2.Dx); // dx is dX/dY, so large magnitudes mean the edge is nearly horizontal (dY is tiny). // Using TopX with a clamped Y can amplify floating-point error in that case, so we // fall back to closest-point clamping when |dx| > 100 (about 0.57 degrees from horizontal). // This threshold keeps near-horizontal intersections stable without scaling the input. switch (absDx1 > 100) { case true when absDx2 > 100: { intersectionPoint = absDx1 > absDx2 ? PolygonUtilities.ClosestPointOnSegment(intersectionPoint, edge1.Bottom, edge1.Top) : PolygonUtilities.ClosestPointOnSegment(intersectionPoint, edge2.Bottom, edge2.Top); break; } case true: intersectionPoint = PolygonUtilities.ClosestPointOnSegment(intersectionPoint, edge1.Bottom, edge1.Top); break; default: { if (absDx2 > 100) { intersectionPoint = PolygonUtilities.ClosestPointOnSegment(intersectionPoint, edge2.Bottom, edge2.Top); } else { double targetY = intersectionPoint.Y < topY ? topY : this.currentScanlineBottomY; double targetX = absDx1 < absDx2 ? ActiveEdge.TopX(edge1, targetY) : ActiveEdge.TopX(edge2, targetY); intersectionPoint = new Vertex(targetX, targetY); } break; } } } IntersectNode node = new(intersectionPoint, edge1, edge2); this.intersectionList.Add(node); } /// /// Extracts an edge from the sorted edge list. /// /// The edge to extract. /// The next edge after the extracted one. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ActiveEdge? ExtractFromSortedEdges(ActiveEdge edge) { ActiveEdge? res = edge.NextInSel; if (res != null) { res.PrevInSel = edge.PrevInSel; } edge.PrevInSel!.NextInSel = res; return res; } /// /// Inserts an edge before another edge in the sorted edge list. /// /// The edge to insert. /// The reference edge. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void InsertBeforeInSortedEdges(ActiveEdge edge1, ActiveEdge edge2) { edge1.PrevInSel = edge2.PrevInSel; if (edge1.PrevInSel != null) { edge1.PrevInSel.NextInSel = edge1; } edge1.NextInSel = edge2; edge2.PrevInSel = edge1; } /// /// Builds the list of intersections required to sort edges at the top of the scanbeam. /// /// The scanbeam top Y coordinate. /// if any intersections were found. private bool BuildIntersectionList(double topY) { if (this.activeEdges.Head?.NextInAel == null) { return false; } // Compute edge positions at the top of the scanbeam to derive required intersections. ActiveEdge? sortedHead = this.activeEdges.CopyToSorted(topY); // Find intersections via a stable merge sort so only adjacent edges intersect. // Nodes are stored for ProcessIntersectionList. See https://stackoverflow.com/a/46319131/359538. ActiveEdge? left = sortedHead; while (left!.Jump != null) { ActiveEdge? prevBase = null; while (left?.Jump != null) { ActiveEdge? currBase = left; ActiveEdge? right = left.Jump; ActiveEdge? lEnd = right; ActiveEdge? rEnd = right.Jump; left.Jump = rEnd; while (left != lEnd && right != rEnd) { if (right!.CurrentX < left!.CurrentX) { ActiveEdge? tmp = right.PrevInSel!; while (true) { this.AddIntersectionNode(tmp, right, topY); if (tmp == left) { break; } tmp = tmp.PrevInSel!; } tmp = right; right = ExtractFromSortedEdges(tmp); lEnd = right; InsertBeforeInSortedEdges(tmp, left); if (left != currBase) { continue; } currBase = tmp; currBase.Jump = rEnd; if (prevBase == null) { sortedHead = currBase; } else { prevBase.Jump = currBase; } } else { left = left.NextInSel; } } prevBase = currBase; left = rEnd; } left = sortedHead; } return this.intersectionList.Count > 0; } /// /// Processes the intersection list in bottom-up order, swapping edges and generating output. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ProcessIntersectionList() { // Intersections must be processed bottom-up, and only between adjacent edges. // Sort so intersections proceed from bottom to top. this.intersectionList.Sort(default(IntersectNodeComparer)); // Reorder as needed to ensure intersecting edges are adjacent. for (int i = 0; i < this.intersectionList.Count; ++i) { if (!AreEdgesAdjacentInActiveList(this.intersectionList[i])) { int j = i + 1; while (!AreEdgesAdjacentInActiveList(this.intersectionList[j])) { j++; } // Swap into adjacency. (this.intersectionList[j], this.intersectionList[i]) = (this.intersectionList[i], this.intersectionList[j]); } IntersectNode node = this.intersectionList[i]; this.IntersectActiveEdges(node.Edge1, node.Edge2, node.Point); this.activeEdges.SwapPositions(node.Edge1, node.Edge2); node.Edge1.CurrentX = node.Point.X; node.Edge2.CurrentX = node.Point.X; this.CheckJoinLeft(node.Edge2, node.Point, true); this.CheckJoinRight(node.Edge1, node.Point, true); } } /// /// Resolves left-to-right direction and bounds for a horizontal edge. /// /// The horizontal edge. /// The maxima vertex for the horizontal span. /// The left bound X value. /// The right bound X value. /// when the edge is left-to-right. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool ResetHorizontalDirection( ActiveEdge horizontalEdge, SweepVertex? vertexMax, out double leftX, out double rightX) { if (horizontalEdge.Bottom.X == horizontalEdge.Top.X) { // Degenerate horizontal edge (zero length). leftX = horizontalEdge.CurrentX; rightX = horizontalEdge.CurrentX; ActiveEdge? edge = horizontalEdge.NextInAel; while (edge != null && edge.VertexTop != vertexMax) { edge = edge.NextInAel; } return edge != null; } if (horizontalEdge.CurrentX < horizontalEdge.Top.X) { leftX = horizontalEdge.CurrentX; rightX = horizontalEdge.Top.X; return true; } // Right to left. leftX = horizontalEdge.Top.X; rightX = horizontalEdge.CurrentX; return false; } /// /// Trims collinear points from a horizontal edge. /// /// The horizontal edge. /// Whether collinear points are preserved. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void TrimHorizontal(ActiveEdge horizontalEdge, bool preserveCollinear) { bool wasTrimmed = false; Vertex point = horizontalEdge.NextVertex.Point; while (point.Y == horizontalEdge.Top.Y) { // Always trim 180-degree spikes in closed paths; otherwise stop when preserving collinear. if (preserveCollinear && (point.X < horizontalEdge.Top.X) != (horizontalEdge.Bottom.X < horizontalEdge.Top.X)) { break; } horizontalEdge.VertexTop = horizontalEdge.NextVertex; horizontalEdge.Top = point; wasTrimmed = true; if (horizontalEdge.IsMaxima) { break; } point = horizontalEdge.NextVertex.Point; } if (wasTrimmed) { // Recompute slope after trimming. horizontalEdge.UpdateDx(); } } /// /// Adds a horizontal segment for later join processing. /// /// The output point that anchors the segment. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddHorizontalSegment(OutputPoint outputPoint) => this.horizontalSegments.Add(new HorizontalSegment(outputPoint)); /// /// Returns the last output point for a hot edge. /// /// The hot edge to inspect. /// The last output point. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static OutputPoint GetLastOutputPoint(ActiveEdge hotEdge) { OutputRecord outputRecord = hotEdge.OutputRecord!; return (hotEdge == outputRecord.FrontEdge) ? outputRecord.Points! : outputRecord.Points!.Next!; } /// /// Processes a horizontal edge and resolves any intersections adouble the scanline. /// /// The horizontal edge to process. private void ProcessHorizontal(ActiveEdge horizontalEdge) /******************************************************************************* * Notes: Horizontal edges (HEs) at scanline intersections (i.e. at the top or * * bottom of a scanbeam) are processed as if layered. The order in which HEs * * are processed doesn't matter. HEs intersect with the bottom vertices of * * other HEs[#] and with non-horizontal edges [*]. Once these intersections * * are completed, intermediate HEs are 'promoted' to the next edge in their * * bounds, and they in turn may be intersected[%] by other HEs. * * * * eg: 3 horizontals at a scanline: / | / / * * | / | (HE3)o ========%========== o * * o ======= o(HE2) / | / / * * o ============#=========*======*========#=========o (HE1) * * / | / | / * *******************************************************************************/ { double y = horizontalEdge.Bottom.Y; SweepVertex? vertexMax = GetMaximaVertexAtCurrentY(horizontalEdge); bool isLeftToRight = ResetHorizontalDirection(horizontalEdge, vertexMax, out double leftX, out double rightX); ActiveEdge? immediatePair = horizontalEdge.NextInAel; if (immediatePair == null || immediatePair.LocalMin.Vertex != horizontalEdge.LocalMin.Vertex || !immediatePair.IsHorizontal) { immediatePair = horizontalEdge.PrevInAel; } if (!horizontalEdge.IsHot && IsTwoVertexFlatRingEdge(horizontalEdge) && immediatePair != null && immediatePair.LocalMin.Vertex == horizontalEdge.LocalMin.Vertex && immediatePair.IsHorizontal) { // Fast path for degenerate flat rings. Complexity is O(K) where K is the // number of active edges crossing this horizontal span at the scanline. // No extra contour collections are built. ActiveEdge? scan = isLeftToRight ? horizontalEdge.NextInAel : horizontalEdge.PrevInAel; if (scan == immediatePair) { scan = isLeftToRight ? immediatePair.NextInAel : immediatePair.PrevInAel; } while (scan != null) { if ((isLeftToRight && scan.CurrentX > rightX) || (!isLeftToRight && scan.CurrentX < leftX)) { break; } if (!scan.IsHorizontal) { Vertex point = new(scan.CurrentX, y); if (isLeftToRight) { this.IntersectActiveEdges(horizontalEdge, scan, point); } else { this.IntersectActiveEdges(scan, horizontalEdge, point); } } scan = isLeftToRight ? scan.NextInAel : scan.PrevInAel; } this.activeEdges.Remove(immediatePair); this.activeEdges.Remove(horizontalEdge); return; } if (horizontalEdge.IsHot) { OutputPoint outputPoint = this.AddOutputPoint(horizontalEdge, new Vertex(horizontalEdge.CurrentX, y)); this.AddHorizontalSegment(outputPoint); } int horizontalLoopGuard = 0; while (true) { if (++horizontalLoopGuard > HorizontalLoopFailSafeLimit) { // Fail-safe for corrupted links: bail out instead of throwing/hanging. return; } // Traverse consecutive horizontal edges on this scanline. ActiveEdge? edge = isLeftToRight ? horizontalEdge.NextInAel : horizontalEdge.PrevInAel; int edgeLoopGuard = 0; while (edge != null) { if (++edgeLoopGuard > HorizontalLoopFailSafeLimit) { // Fail-safe for corrupted links: bail out instead of throwing/hanging. return; } if (edge.VertexTop == vertexMax) { // Handle the maxima pair before processing other intersections. if (horizontalEdge.IsHot && IsJoined(edge)) { this.SplitEdge(edge, edge.Top); } if (horizontalEdge.IsHot) { while (horizontalEdge.VertexTop != vertexMax) { _ = this.AddOutputPoint(horizontalEdge, horizontalEdge.Top); this.UpdateEdgeInActiveList(horizontalEdge); } if (isLeftToRight) { _ = this.AddLocalMaximumOutput(horizontalEdge, edge, horizontalEdge.Top); } else { _ = this.AddLocalMaximumOutput(edge, horizontalEdge, horizontalEdge.Top); } } this.activeEdges.Remove(edge); this.activeEdges.Remove(horizontalEdge); return; } // If this horizontal is a maxima, keep going until its pair is reached; // otherwise check for break conditions. Vertex point; if (vertexMax != horizontalEdge.VertexTop) { // Stop once the edge moves beyond the horizontal span. if ((isLeftToRight && edge.CurrentX > rightX) || (!isLeftToRight && edge.CurrentX < leftX)) { break; } if (edge.CurrentX == horizontalEdge.Top.X && !edge.IsHorizontal) { point = horizontalEdge.NextVertex.Point; // At the horizontal end, stop only when the outslope overtakes the edge // (greater when heading right, smaller when heading left). if ((isLeftToRight && (ActiveEdge.TopX(edge, point.Y) >= point.X)) || (!isLeftToRight && (ActiveEdge.TopX(edge, point.Y) <= point.X))) { break; } } } point = new Vertex(edge.CurrentX, y); if (isLeftToRight) { this.IntersectActiveEdges(horizontalEdge, edge, point); this.activeEdges.SwapPositions(horizontalEdge, edge); this.CheckJoinLeft(edge, point); horizontalEdge.CurrentX = edge.CurrentX; edge = horizontalEdge.NextInAel; } else { this.IntersectActiveEdges(edge, horizontalEdge, point); this.activeEdges.SwapPositions(edge, horizontalEdge); this.CheckJoinRight(edge, point); horizontalEdge.CurrentX = edge.CurrentX; edge = horizontalEdge.PrevInAel; } if (horizontalEdge.IsHot) { this.AddHorizontalSegment(GetLastOutputPoint(horizontalEdge)); } } // Stop once no more consecutive horizontals remain. if (horizontalEdge.NextVertex.Point.Y != horizontalEdge.Top.Y) { break; } // Advance to the next horizontal segment in the bound. if (horizontalEdge.IsHot) { _ = this.AddOutputPoint(horizontalEdge, horizontalEdge.Top); } this.UpdateEdgeInActiveList(horizontalEdge); isLeftToRight = ResetHorizontalDirection( horizontalEdge, vertexMax, out leftX, out rightX); } // Finished this horizontal chain. if (horizontalEdge.IsHot) { OutputPoint outputPoint = this.AddOutputPoint(horizontalEdge, horizontalEdge.Top); this.AddHorizontalSegment(outputPoint); } // Advance past the final intermediate horizontal. this.UpdateEdgeInActiveList(horizontalEdge); } /// /// Processes edges that reach the top of the scanbeam, updating or removing them. /// /// The scanbeam top Y coordinate. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ProcessScanbeamTop(double y) { this.activeEdges.ClearHorizontalQueue(); ActiveEdge? edge = this.activeEdges.Head; while (edge != null) { // Edge is never horizontal at this point. if (edge.Top.Y == y) { edge.CurrentX = edge.Top.X; if (edge.IsMaxima) { // Maxima reached; finalize this bound. edge = this.ProcessMaxima(edge); continue; } // Intermediate vertex on the bound. if (edge.IsHot) { _ = this.AddOutputPoint(edge, edge.Top); } this.UpdateEdgeInActiveList(edge); // Queue horizontals for dedicated processing. if (edge.IsHorizontal) { this.activeEdges.PushHorizontal(edge); } } // Edge continues through the scanbeam. else { edge.CurrentX = ActiveEdge.TopX(edge, y); } edge = edge.NextInAel; } } /// /// Handles a maxima event for the active edge. /// /// The active edge at the maxima. /// The next edge to continue scanning from. [MethodImpl(MethodImplOptions.AggressiveInlining)] private ActiveEdge? ProcessMaxima(ActiveEdge edge) { ActiveEdge? prevEdge = edge.PrevInAel; ActiveEdge? nextEdge = edge.NextInAel; ActiveEdge? maxPair = FindMaximaPair(edge); if (maxPair == null) { // Horizontal maxima pair is handled in horizontal processing. return nextEdge; } if (IsJoined(edge)) { this.SplitEdge(edge, edge.Top); } if (IsJoined(maxPair)) { this.SplitEdge(maxPair, maxPair.Top); } // Only non-horizontal maxima reach here. // Process edges between the maxima pair. while (nextEdge != maxPair) { this.IntersectActiveEdges(edge, nextEdge!, edge.Top); this.activeEdges.SwapPositions(edge, nextEdge!); nextEdge = edge.NextInAel; } // At this point edge.NextInAel == maxPair. if (edge.IsHot) { _ = this.AddLocalMaximumOutput(edge, maxPair, edge.Top); } this.activeEdges.Remove(edge); this.activeEdges.Remove(maxPair); return prevEdge != null ? prevEdge.NextInAel : this.activeEdges.Head; } /// /// Tests whether an edge is currently joined to a neighbor. /// /// The edge to inspect. /// if the edge is joined. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsJoined(ActiveEdge edge) => edge.JoinWith != JoinWith.None; /// /// Splits a joined edge at the specified point. /// /// The edge to split. /// The split point. private void SplitEdge(ActiveEdge edge, Vertex point) { if (edge.JoinWith == JoinWith.Right) { edge.JoinWith = JoinWith.None; edge.NextInAel!.JoinWith = JoinWith.None; _ = this.AddLocalMinimumOutput(edge, edge.NextInAel, point, true); } else { edge.JoinWith = JoinWith.None; edge.PrevInAel!.JoinWith = JoinWith.None; _ = this.AddLocalMinimumOutput(edge.PrevInAel, edge, point, true); } } /// /// Attempts to join the current edge with its left neighbor. /// /// The active edge being evaluated. /// The candidate join point. /// Whether to check the current X for proximity. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckJoinLeft( ActiveEdge edge, Vertex point, bool checkCurrX = false) { ActiveEdge? prev = edge.PrevInAel; if (prev == null || !edge.IsHot || !prev.IsHot || edge.IsHorizontal || prev.IsHorizontal) { return; } // Reject joins that are too close to extrema (Issue #490). if ((point.Y < edge.Top.Y + JoinExtremaDelta || point.Y < prev.Top.Y + JoinExtremaDelta) && ((edge.Bottom.Y > point.Y) || (prev.Bottom.Y > point.Y))) { // Issue #490. return; } if (checkCurrX) { if (PolygonUtilities.PerpendicularDistanceSquared(point, prev.Bottom, prev.Top) > JoinPerpendicularDistanceSquaredTolerance) { return; } } else if (edge.CurrentX != prev.CurrentX) { return; } if (!PolygonUtilities.IsCollinear(edge.Top, point, prev.Top)) { return; } if (edge.OutputRecord!.Index == prev.OutputRecord!.Index) { _ = this.AddLocalMaximumOutput(prev, edge, point); } else if (edge.OutputRecord!.Index < prev.OutputRecord!.Index) { JoinOutputRecords(edge, prev); } else { JoinOutputRecords(prev, edge); } prev.JoinWith = JoinWith.Right; edge.JoinWith = JoinWith.Left; } /// /// Attempts to join the current edge with its right neighbor. /// /// The active edge being evaluated. /// The candidate join point. /// Whether to check the current X for proximity. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckJoinRight( ActiveEdge edge, Vertex point, bool checkCurrX = false) { ActiveEdge? next = edge.NextInAel; if (next == null || !edge.IsHot || !next.IsHot || edge.IsHorizontal || next.IsHorizontal) { return; } // Reject joins that are too close to extrema (Issue #490). if ((point.Y < edge.Top.Y + JoinExtremaDelta || point.Y < next.Top.Y + JoinExtremaDelta) && ((edge.Bottom.Y > point.Y) || (next.Bottom.Y > point.Y))) { // Issue #490. return; } if (checkCurrX) { if (PolygonUtilities.PerpendicularDistanceSquared(point, next.Bottom, next.Top) > JoinPerpendicularDistanceSquaredTolerance) { return; } } else if (edge.CurrentX != next.CurrentX) { return; } if (!PolygonUtilities.IsCollinear(edge.Top, point, next.Top)) { return; } if (edge.OutputRecord!.Index == next.OutputRecord!.Index) { _ = this.AddLocalMaximumOutput(edge, next, point); } else if (edge.OutputRecord!.Index < next.OutputRecord!.Index) { JoinOutputRecords(edge, next); } else { JoinOutputRecords(next, edge); } edge.JoinWith = JoinWith.Right; next.JoinWith = JoinWith.Left; } /// /// Ensures all output points in a record reference the correct owner. /// /// The output record to normalize. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void FixOutputRecordPoints(OutputRecord outputRecord) { OutputPoint outputPoint = outputRecord.Points!; do { outputPoint.OutputRecord = outputRecord; outputPoint = outputPoint.Next!; } while (outputPoint != outputRecord.Points); } /// /// Determines the left/right ordering of a horizontal segment. /// /// The segment to update. /// The previous output point. /// The next output point. /// if the segment has non-zero length. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool SetHorizontalSegmentHeadingForward(HorizontalSegment horizontalSegment, OutputPoint prevPoint, OutputPoint nextPoint) { if (prevPoint.Point.X == nextPoint.Point.X) { return false; } if (prevPoint.Point.X < nextPoint.Point.X) { horizontalSegment.LeftPoint = prevPoint; horizontalSegment.RightPoint = nextPoint; horizontalSegment.LeftToRight = true; } else { horizontalSegment.LeftPoint = nextPoint; horizontalSegment.RightPoint = prevPoint; horizontalSegment.LeftToRight = false; } return true; } /// /// Normalizes a horizontal segment and sets its left/right pointers. /// /// The segment to update. /// if the segment remains valid after normalization. private static bool UpdateHorizontalSegment(HorizontalSegment horizontalSegment) { OutputPoint outputPoint = horizontalSegment.LeftPoint!; OutputRecord outputRecord = ResolveOutputRecord(outputPoint.OutputRecord)!; bool outputRecordHasEdges = outputRecord.FrontEdge != null; double currentY = outputPoint.Point.Y; OutputPoint prevPoint = outputPoint, nextPoint = outputPoint; if (outputRecordHasEdges) { OutputPoint opA = outputRecord.Points!, opZ = opA.Next!; while (prevPoint != opZ && prevPoint.Prev.Point.Y == currentY) { prevPoint = prevPoint.Prev; } while (nextPoint != opA && nextPoint.Next!.Point.Y == currentY) { nextPoint = nextPoint.Next; } } else { while (prevPoint.Prev != nextPoint && prevPoint.Prev.Point.Y == currentY) { prevPoint = prevPoint.Prev; } while (nextPoint.Next != prevPoint && nextPoint.Next!.Point.Y == currentY) { nextPoint = nextPoint.Next; } } bool result = SetHorizontalSegmentHeadingForward(horizontalSegment, prevPoint, nextPoint) && horizontalSegment.LeftPoint!.HorizontalSegment == null; if (result) { horizontalSegment.LeftPoint!.HorizontalSegment = horizontalSegment; } else { // Mark invalid so sorting pushes it to the end. horizontalSegment.RightPoint = null; } 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.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; } /// /// Sorts horizontal segments by their X extents. /// /// The first segment. /// The second segment. /// A comparison result for sorting. private static int CompareHorizontalSegments(HorizontalSegment? segment1, HorizontalSegment? segment2) { if (segment1 == null || segment2 == null) { return 0; } if (segment1.RightPoint == null) { return segment2.RightPoint == null ? 0 : 1; } if (segment2.RightPoint == null) { return -1; } return segment1.LeftPoint!.Point.X.CompareTo(segment2.LeftPoint!.Point.X); } /// /// Converts horizontal segments into join candidates for post-processing. /// private void ConvertHorizontalSegmentsToJoins() { int k = 0; foreach (HorizontalSegment horizontalSegment in this.horizontalSegments) { if (UpdateHorizontalSegment(horizontalSegment)) { k++; } } if (k < 2) { return; } this.horizontalSegments.Sort(CompareHorizontalSegments); for (int i = 0; i < k - 1; i++) { HorizontalSegment segment1 = this.horizontalSegments[i]; // Find overlapping segments to generate join candidates. for (int j = i + 1; j < k; j++) { HorizontalSegment segment2 = this.horizontalSegments[j]; if ((segment2.LeftPoint!.Point.X >= segment1.RightPoint!.Point.X) || (segment2.LeftToRight == segment1.LeftToRight) || (segment2.RightPoint!.Point.X <= segment1.LeftPoint!.Point.X)) { continue; } double currentY = segment1.LeftPoint.Point.Y; if (segment1.LeftToRight) { while (segment1.LeftPoint.Next!.Point.Y == currentY && segment1.LeftPoint.Next.Point.X <= segment2.LeftPoint.Point.X) { segment1.LeftPoint = segment1.LeftPoint.Next; } while (segment2.LeftPoint.Prev.Point.Y == currentY && segment2.LeftPoint.Prev.Point.X <= segment1.LeftPoint.Point.X) { segment2.LeftPoint = segment2.LeftPoint.Prev; } _ = this.horizontalJoins.Add( this.DuplicateOutputPoint(segment1.LeftPoint, true), this.DuplicateOutputPoint(segment2.LeftPoint, false)); } else { while (segment1.LeftPoint.Prev.Point.Y == currentY && segment1.LeftPoint.Prev.Point.X <= segment2.LeftPoint.Point.X) { segment1.LeftPoint = segment1.LeftPoint.Prev; } while (segment2.LeftPoint.Next!.Point.Y == currentY && segment2.LeftPoint.Next.Point.X <= segment1.LeftPoint.Point.X) { segment2.LeftPoint = segment2.LeftPoint.Next; } _ = this.horizontalJoins.Add( this.DuplicateOutputPoint(segment2.LeftPoint, true), this.DuplicateOutputPoint(segment1.LeftPoint, false)); } } } } /// /// Builds a cleaned contour by removing redundant collinear points. /// /// A point on the output ring. /// A contour with redundant points removed. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static List BuildCleanContour(OutputPoint outputPoint) { List result = []; OutputPoint outputPoint2 = outputPoint; while (outputPoint2.Next != outputPoint && ((outputPoint2.Point.X == outputPoint2.Next!.Point.X && outputPoint2.Point.X == outputPoint2.Prev.Point.X) || (outputPoint2.Point.Y == outputPoint2.Next.Point.Y && outputPoint2.Point.Y == outputPoint2.Prev.Point.Y))) { outputPoint2 = outputPoint2.Next; } result.Add(outputPoint2.Point); OutputPoint prevOp = outputPoint2; outputPoint2 = outputPoint2.Next; while (outputPoint2 != outputPoint) { if ((outputPoint2.Point.X != outputPoint2.Next!.Point.X || outputPoint2.Point.X != prevOp.Point.X) && (outputPoint2.Point.Y != outputPoint2.Next.Point.Y || outputPoint2.Point.Y != prevOp.Point.Y)) { result.Add(outputPoint2.Point); prevOp = outputPoint2; } outputPoint2 = outputPoint2.Next; } return result; } /// /// Classifies a point against an output polygon. /// /// The point to test. /// A point on the polygon ring. /// The point-in-polygon classification. private static PointInPolygonResult PointInOutputPolygon(Vertex point, OutputPoint outputPoint) { if (outputPoint == outputPoint.Next || outputPoint.Prev == outputPoint.Next) { return PointInPolygonResult.Outside; } OutputPoint outputPoint2 = outputPoint; do { if (outputPoint.Point.Y != point.Y) { break; } outputPoint = outputPoint.Next!; } while (outputPoint != outputPoint2); // Degenerate ring. if (outputPoint.Point.Y == point.Y) { return PointInPolygonResult.Outside; } // Point is strictly above or below the starting Y. bool isAbove = outputPoint.Point.Y < point.Y, startingAbove = isAbove; int val = 0; outputPoint2 = outputPoint.Next!; while (outputPoint2 != outputPoint) { if (isAbove) { while (outputPoint2 != outputPoint && outputPoint2.Point.Y < point.Y) { outputPoint2 = outputPoint2.Next!; } } else { while (outputPoint2 != outputPoint && outputPoint2.Point.Y > point.Y) { outputPoint2 = outputPoint2.Next!; } } if (outputPoint2 == outputPoint) { break; } // The scanline must touch or cross point.Y an even number of times. // Handle horizontal touches explicitly. if (outputPoint2.Point.Y == point.Y) { if (outputPoint2.Point.X == point.X || (outputPoint2.Point.Y == outputPoint2.Prev.Point.Y && (point.X < outputPoint2.Prev.Point.X) != (point.X < outputPoint2.Point.X))) { return PointInPolygonResult.On; } outputPoint2 = outputPoint2.Next!; if (outputPoint2 == outputPoint) { break; } continue; } if (outputPoint2.Point.X <= point.X || outputPoint2.Prev.Point.X <= point.X) { if (outputPoint2.Prev.Point.X < point.X && outputPoint2.Point.X < point.X) { // Toggle parity. val = 1 - val; } else { int d = PolygonUtilities.CrossSign(outputPoint2.Prev.Point, outputPoint2.Point, point); if (d == 0) { return PointInPolygonResult.On; } if ((d < 0) == isAbove) { val = 1 - val; } } } isAbove = !isAbove; outputPoint2 = outputPoint2.Next!; } if (isAbove == startingAbove) { return val == 0 ? PointInPolygonResult.Outside : PointInPolygonResult.Inside; } { int d = PolygonUtilities.CrossSign(outputPoint2.Prev.Point, outputPoint2.Point, point); if (d == 0) { return PointInPolygonResult.On; } if ((d < 0) == isAbove) { val = 1 - val; } } return val == 0 ? PointInPolygonResult.Outside : PointInPolygonResult.Inside; } /// /// Determines whether one output ring lies inside another. /// /// A point on the candidate inner ring. /// A point on the candidate outer ring. /// if the first ring is inside the second. public static bool IsPathInsidePath(OutputPoint outputPoint1, OutputPoint outputPoint2) { // Allow for rounding error; don't decide based solely on the first vertex. PointInPolygonResult pip = PointInPolygonResult.On; OutputPoint outputPoint = outputPoint1; do { switch (PointInOutputPolygon(outputPoint.Point, outputPoint2)) { 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; } outputPoint = outputPoint.Next!; } while (outputPoint != outputPoint1); // Result is unclear, so try again using cleaned paths (Issue #973). return PolygonUtilities.Path2ContainsPath1(BuildCleanContour(outputPoint1), BuildCleanContour(outputPoint2)); } /// /// Moves split ownership from one output record to another. /// /// The output record to move from. /// The output record to move to. private static void MoveOutputSplits(OutputRecord sourceRecord, OutputRecord targetRecord) { if (sourceRecord.Splits == null) { return; } targetRecord.Splits ??= []; foreach (int i in sourceRecord.Splits) { if (i != targetRecord.Index) { targetRecord.Splits.Add(i); } } sourceRecord.Splits = null; } /// /// Processes horizontal joins captured during the sweep. /// private void ProcessHorizontalJoins() { foreach (HorizontalJoin join in this.horizontalJoins) { OutputRecord outputRecord1 = ResolveOutputRecord(join.LeftToRight!.OutputRecord)!; OutputRecord outputRecord2 = ResolveOutputRecord(join.RightToLeft!.OutputRecord)!; OutputPoint op1b = join.LeftToRight.Next!; OutputPoint op2b = join.RightToLeft.Prev; join.LeftToRight.Next = join.RightToLeft; join.RightToLeft.Prev = join.LeftToRight; op1b.Prev = op2b; op2b.Next = op1b; // This join may split a single output record. if (outputRecord1 == outputRecord2) { outputRecord2 = this.CreateOutputRecord(); outputRecord2.Points = op1b; FixOutputRecordPoints(outputRecord2); // If outputRecord1.Points moved to outputRecord2, update outputRecord1.Points. if (outputRecord1.Points!.OutputRecord == outputRecord2) { outputRecord1.Points = join.LeftToRight; outputRecord1.Points.OutputRecord = outputRecord1; } // Issue references: #498, #520, #584, #576, #618 if (this.buildHierarchy) { if (IsPathInsidePath(outputRecord1.Points, outputRecord2.Points)) { // swap outputRecord1's and outputRecord2's points (outputRecord2.Points, outputRecord1.Points) = (outputRecord1.Points, outputRecord2.Points); FixOutputRecordPoints(outputRecord1); FixOutputRecordPoints(outputRecord2); // outputRecord2 is now inside outputRecord1 outputRecord2.Owner = outputRecord1; } else if (IsPathInsidePath(outputRecord2.Points, outputRecord1.Points)) { outputRecord2.Owner = outputRecord1; } else { outputRecord2.Owner = outputRecord1.Owner; } outputRecord1.Splits ??= []; outputRecord1.Splits.Add(outputRecord2.Index); } else { outputRecord2.Owner = outputRecord1; } } else { outputRecord2.Points = null; if (this.buildHierarchy) { SetOutputOwner(outputRecord2, outputRecord1); // Issue #618. MoveOutputSplits(outputRecord2, outputRecord1); } else { outputRecord2.Owner = outputRecord1; } } } } /// /// Sorts intersection nodes from top to bottom, then left to right. /// internal struct IntersectNodeComparer : IComparer { /// /// Compares two intersection nodes for sorting. /// /// The first node. /// The second node. /// A comparison result for sorting. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly int Compare(IntersectNode a, IntersectNode b) { double deltaY = a.Point.Y - b.Point.Y; if (deltaY != 0) { return deltaY > 0 ? -1 : 1; } double deltaX = a.Point.X - b.Point.X; if (deltaX == 0) { return 0; } return deltaX < 0 ? -1 : 1; } } } }