Skip to content

Commit ffdb3a5

Browse files
Use Rust PossibleIntersection and DivideSegment
1 parent da8ee86 commit ffdb3a5

File tree

1 file changed

+89
-20
lines changed

1 file changed

+89
-20
lines changed

src/PolygonClipper/PolygonClipper.cs

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,12 @@ private static int PossibleIntersection(
532532
SweepEvent le2,
533533
StablePriorityQueue<SweepEvent, SweepEventComparer> eventQueue)
534534
{
535+
if (le1.OtherEvent == null || le2.OtherEvent == null)
536+
{
537+
// No intersection possible.
538+
return 0;
539+
}
540+
535541
// Point intersections
536542
int nIntersections = PolygonUtilities.FindIntersection(
537543
le1.Segment(),
@@ -545,22 +551,22 @@ private static int PossibleIntersection(
545551
return 0;
546552
}
547553

554+
// Ignore intersection if it occurs at the exact left or right endpoint of both segments
548555
if (nIntersections == 1 &&
549556
(le1.Point == le2.Point || le1.OtherEvent.Point == le2.OtherEvent.Point))
550557
{
551558
// Line segments intersect at an endpoint of both line segments
552559
return 0;
553560
}
554561

562+
// If segments overlap and belong to the same polygon, ignore them
555563
if (nIntersections == 2 && le1.PolygonType == le2.PolygonType)
556564
{
557-
// Line segments overlap but belong to the same polygon
558565
return 0;
559566
}
560567

568+
// Handle a single intersection point
561569
SweepEventComparer comparer = eventQueue.Comparer;
562-
563-
// The line segments associated with le1 and le2 intersect
564570
if (nIntersections == 1)
565571
{
566572
// If the intersection point is not an endpoint of le1 segment.
@@ -613,8 +619,8 @@ private static int PossibleIntersection(
613619
}
614620
}
615621

616-
// Handle leftCoincide and rightCoincide cases
617-
if ((leftCoincide && rightCoincide) || leftCoincide)
622+
// Handle leftCoincide case
623+
if (leftCoincide)
618624
{
619625
le2.EdgeType = EdgeType.NonContributing;
620626
le1.EdgeType = (le2.InOut == le1.InOut)
@@ -636,15 +642,15 @@ private static int PossibleIntersection(
636642
return 3;
637643
}
638644

639-
// Handle overlapping segments
645+
// Handle general overlapping case
640646
if (events[0] != events[3].OtherEvent)
641647
{
642648
DivideSegment(events[0], events[1].Point, eventQueue, comparer);
643649
DivideSegment(events[1], events[2].Point, eventQueue, comparer);
644650
return 3;
645651
}
646652

647-
// Handle one segment fully containing the other
653+
// One segment fully contains the other
648654
DivideSegment(events[0], events[1].Point, eventQueue, comparer);
649655
DivideSegment(events[3].OtherEvent, events[2].Point, eventQueue, comparer);
650656
return 3;
@@ -663,37 +669,100 @@ private static void DivideSegment(
663669
StablePriorityQueue<SweepEvent, SweepEventComparer> eventQueue,
664670
SweepEventComparer comparer)
665671
{
666-
// Create the right event for the left segment (result of division)
672+
if (le.OtherEvent == null)
673+
{
674+
return;
675+
}
676+
677+
SweepEvent re = le.OtherEvent;
678+
679+
// The idea is to divide the segment based on the given `inter` coordinate as follows:
680+
//
681+
// (se_l)--------(r)(l)--------(re)
682+
//
683+
// Under normal circumstances the resulting events satisfy the conditions:
684+
//
685+
// se_l is before r, and l is before re.
686+
//
687+
// Since the intersection point computation is bounded to the interval [se_l.x, re.x]
688+
// it is impossible for r/l to fall outside the interval. This leaves the corner cases:
689+
//
690+
// 1. r.x == se_l.x and r.y < se_l.y: This corresponds to the case where the first
691+
// sub-segment becomes a perfectly vertical line. The problem is that vertical
692+
// segments always have to be processed from bottom to top consistency. The
693+
// theoretically correct event order would be r first (bottom), se_l later (top).
694+
// However, se_l is the event just being processed, so there is no (easy) way of
695+
// processing r before se_l. The easiest solution to the problem is to avoid it,
696+
// by incrementing inter.x by one ULP.
697+
// 2. l.x == re.x and l.y > re.y: This corresponds to the case where the second
698+
// sub-segment becomes a perfectly vertical line, and because of the bottom-to-top
699+
// convention for vertical segment, the order of l and re must be swapped.
700+
// In this case swapping is not a problem, because both events are in the future.
701+
//
702+
// See also: https://github.com/21re/rust-geo-booleanop/pull/11
703+
704+
// Prevent from corner case 1
705+
if (p.X == le.Point.X && p.Y < le.Point.Y)
706+
{
707+
// TODO: enabling this line makes a single test issue76.geojson fail.
708+
// The files are different in the two reference repositories but both fail.
709+
// p = new Vertex(NextAfter(p.X, true), p.Y);
710+
}
711+
712+
// Create the right event for the left segment (new right endpoint)
667713
SweepEvent r = new(p, false, le, le.PolygonType);
668714

669-
// Create the left event for the right segment (result of division)
670-
SweepEvent l = new(p, true, le.OtherEvent, le.PolygonType);
715+
// Create the left event for the right segment (new left endpoint)
716+
SweepEvent l = new(p, true, re, le.PolygonType);
671717

672-
// Assign the same contour id to the new events for sorting.
718+
// Assign the same contour ID to maintain connectivity
673719
r.ContourId = l.ContourId = le.ContourId;
674720

675-
// Avoid rounding error: ensure the left event is processed before the right event
676-
if (comparer.Compare(l, le.OtherEvent) > 0)
721+
// Corner case 2 can be accounted for by swapping l / se_r
722+
if (comparer.Compare(l, re) > 0)
677723
{
678724
Debug.WriteLine("Rounding error detected: Adjusting left/right flags for event ordering.");
679-
le.OtherEvent.Left = true;
725+
re.Left = true;
680726
l.Left = false;
681727
}
682728

683-
if (comparer.Compare(le, r) > 0)
684-
{
685-
Debug.WriteLine("Rounding error detected: Event ordering issue for right event.");
686-
}
687-
688729
// Update references to maintain correct linkage
689-
le.OtherEvent.OtherEvent = l;
730+
re.OtherEvent = l;
690731
le.OtherEvent = r;
691732

692733
// Add the new events to the event queue
693734
eventQueue.Enqueue(l);
694735
eventQueue.Enqueue(r);
695736
}
696737

738+
/// <summary>
739+
/// Returns the next representable double-precision floating-point value in the given direction.
740+
/// <see href="https://docs.rs/float_next_after/latest/float_next_after/trait.NextAfter.html"/>
741+
/// </summary>
742+
/// <param name="x">The starting double value.</param>
743+
/// <param name="up">If true, moves towards positive infinity; otherwise, towards negative infinity.</param>
744+
/// <returns>The next representable double in the given direction.</returns>
745+
private static double NextAfter(double x, bool up)
746+
{
747+
if (double.IsNaN(x) || x == double.PositiveInfinity || x == double.NegativeInfinity)
748+
{
749+
return x; // NaN and infinity stay the same
750+
}
751+
752+
// Convert double to its IEEE 754 bit representation
753+
long bits = BitConverter.DoubleToInt64Bits(x);
754+
if (up)
755+
{
756+
bits += (bits >= 0) ? 1 : -1; // Increase magnitude
757+
}
758+
else
759+
{
760+
bits += (bits > 0) ? -1 : 1; // Decrease magnitude
761+
}
762+
763+
return BitConverter.Int64BitsToDouble(bits);
764+
}
765+
697766
/// <summary>
698767
/// Connects edges in the result polygon by processing the sweep events
699768
/// and constructing contours for the final result.

0 commit comments

Comments
 (0)