@@ -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