Cleanup and clarify SEG::intersect and SEG::Collide

Fix handling of end point intersection case
Fix degenerate handling
Fix overflow cases
Simplify logic in SEG::Collide
Remove overly simplistic check for intersection

Add multiple QA regression tests
This commit is contained in:
Seth Hillbrand 2025-07-20 15:48:13 -07:00
parent 9d074c1679
commit 0459c54a92
3 changed files with 780 additions and 74 deletions

View File

@ -380,8 +380,8 @@ public:
}
private:
bool ccw( const VECTOR2I& aA, const VECTOR2I& aB, const VECTOR2I &aC ) const;
bool checkCollinearOverlap( const SEG& aSeg, bool useXAxis, bool aIgnoreEndpoints, VECTOR2I* aPt ) const;
bool intersects( const SEG& aSeg, bool aIgnoreEndpoints = false, bool aLines = false,
VECTOR2I* aPt = nullptr ) const;

View File

@ -210,40 +210,220 @@ bool SEG::NearestPoints( const SEG& aSeg, VECTOR2I& aPtA, VECTOR2I& aPtB, int64_
}
bool SEG::intersects( const SEG& aSeg, bool aIgnoreEndpoints, bool aLines, VECTOR2I* aPt ) const
bool SEG::checkCollinearOverlap( const SEG& aSeg, bool useXAxis, bool aIgnoreEndpoints, VECTOR2I* aPt ) const
{
const VECTOR2<ecoord> e = VECTOR2<ecoord>( B.x - A.x, B.y - A.y );
const VECTOR2<ecoord> f = VECTOR2<ecoord>( aSeg.B.x - aSeg.A.x, aSeg.B.y - aSeg.A.y );
const VECTOR2<ecoord> ac = VECTOR2<ecoord>( aSeg.A.x - A.x, aSeg.A.y - A.y );
// Extract coordinates based on the chosen axis
int seg1_start, seg1_end, seg2_start, seg2_end;
int coord1_start, coord1_end; // For calculating other axis coordinate
ecoord d = f.Cross( e );
ecoord p = f.Cross( ac );
ecoord q = e.Cross( ac );
if( useXAxis )
{
seg1_start = A.x; seg1_end = B.x;
seg2_start = aSeg.A.x; seg2_end = aSeg.B.x;
coord1_start = A.y; coord1_end = B.y;
}
else
{
seg1_start = A.y; seg1_end = B.y;
seg2_start = aSeg.A.y; seg2_end = aSeg.B.y;
coord1_start = A.x; coord1_end = B.x;
}
if( d == 0 )
// Find segment ranges on the projection axis
const int seg1_min = std::min( seg1_start, seg1_end );
const int seg1_max = std::max( seg1_start, seg1_end );
const int seg2_min = std::min( seg2_start, seg2_end );
const int seg2_max = std::max( seg2_start, seg2_end );
// Check for overlap
const bool overlaps = seg1_max >= seg2_min && seg2_max >= seg1_min;
if( !overlaps )
return false;
if( !aLines && d > 0 && ( q < 0 || q > d || p < 0 || p > d ) )
return false;
// Check if intersection is only at endpoints when aIgnoreEndpoints is true
if( aIgnoreEndpoints )
{
// Calculate overlap region
const int overlap_start = std::max( seg1_min, seg2_min );
const int overlap_end = std::min( seg1_max, seg2_max );
if( !aLines && d < 0 && ( q < d || p < d || p > 0 || q > 0 ) )
return false;
// If overlap region has zero length, segments only touch at endpoint
if( overlap_start == overlap_end )
{
// Check if this endpoint touching involves actual segment endpoints
// (not just projected endpoints due to min/max calculation)
bool isEndpointTouch = false;
if( !aLines && aIgnoreEndpoints && ( q == 0 || q == d ) && ( p == 0 || p == d ) )
return false;
// Check if the touch point corresponds to actual segment endpoints
if( overlap_start == seg1_min || overlap_start == seg1_max )
{
// Touch point is at seg1's endpoint, check if it's also at seg2's endpoint
if( overlap_start == seg2_min || overlap_start == seg2_max )
{
isEndpointTouch = true;
}
}
if( isEndpointTouch )
return false; // Ignore endpoint-only intersection
}
}
// Calculate intersection point if requested
if( aPt )
{
VECTOR2<ecoord> result( aSeg.A.x + rescale( q, (ecoord) f.x, d ),
aSeg.A.y + rescale( q, (ecoord) f.y, d ) );
// Find midpoint of overlap region
const int overlap_start = std::max( seg1_min, seg2_min );
const int overlap_end = std::min( seg1_max, seg2_max );
const int intersection_proj = ( overlap_start + overlap_end ) / 2;
if( abs( result.x ) > std::numeric_limits<VECTOR2I::coord_type>::max()
|| abs( result.y ) > std::numeric_limits<VECTOR2I::coord_type>::max() )
// Calculate corresponding coordinate on the other axis
int intersection_other;
if( seg1_end != seg1_start )
{
// Use this segment's line equation to find other coordinate
intersection_other = coord1_start + static_cast<int>(
rescale( intersection_proj - seg1_start, coord1_end - coord1_start, seg1_end - seg1_start ) );
}
else
{
// Degenerate segment (point) or perpendicular to projection axis
intersection_other = coord1_start;
}
// Set result based on projection axis
if( useXAxis )
*aPt = VECTOR2I( intersection_proj, intersection_other );
else
*aPt = VECTOR2I( intersection_other, intersection_proj );
}
return true;
}
bool SEG::intersects( const SEG& aSeg, bool aIgnoreEndpoints, bool aLines, VECTOR2I* aPt ) const
{
// Quick rejection: check if segment bounding boxes overlap
// (Skip for line mode since infinite lines can intersect anywhere)
if( !aLines )
{
const int this_min_x = std::min( A.x, B.x );
const int this_max_x = std::max( A.x, B.x );
const int this_min_y = std::min( A.y, B.y );
const int this_max_y = std::max( A.y, B.y );
const int other_min_x = std::min( aSeg.A.x, aSeg.B.x );
const int other_max_x = std::max( aSeg.A.x, aSeg.B.x );
const int other_min_y = std::min( aSeg.A.y, aSeg.B.y );
const int other_max_y = std::max( aSeg.A.y, aSeg.B.y );
if( this_max_x < other_min_x || other_max_x < this_min_x ||
this_max_y < other_min_y || other_max_y < this_min_y )
{
return false;
}
}
*aPt = VECTOR2I( (int) result.x, (int) result.y );
// Calculate direction vectors and offset vector using VECTOR2 operations
// Using parametric form: P₁ = A + t*dir1, P₂ = aSeg.A + s*dir2
const VECTOR2L dir1 = VECTOR2L( B ) - A; // direction vector e
const VECTOR2L dir2 = VECTOR2L( aSeg.B ) - aSeg.A; // direction vector f
const VECTOR2L offset = VECTOR2L( aSeg.A ) - A; // offset vector ac
const ecoord determinant = dir2.Cross( dir1 );
// Handle parallel/collinear case
if( determinant == 0 )
{
// Check if lines are collinear (not just parallel) using cross product
// Lines are collinear if offset vector is also parallel to direction vector
const ecoord collinear_test = dir1.Cross( offset );
if( collinear_test != 0 )
return false; // Parallel but not collinear
// Lines are collinear - for infinite lines, they always intersect
if( aLines )
{
// For infinite collinear lines, intersection point is ambiguous
// Use the midpoint between the two segment start points as a reasonable choice
if( aPt )
{
// If aSeg is degenerate (point), use its start point
if( aSeg.A == aSeg.B )
{
*aPt = aSeg.A;
}
else if( A == B )
{ // If this segment is degenerate (point), use its start point
*aPt = A;
}
else
{
const VECTOR2I midpoint = ( A + aSeg.A ) / 2;
*aPt = midpoint;
}
}
return true;
}
// For segments, check overlap using the axis with larger coordinate range
const bool use_x_axis = std::abs( dir1.x ) >= std::abs( dir1.y );
return checkCollinearOverlap( aSeg, use_x_axis, aIgnoreEndpoints, aPt );
}
// param2_num = f × ac (parameter for second segment: s = p/d)
// param1_num = e × ac (parameter for first segment: t = q/d)
const ecoord param2_num = dir2.Cross( offset );
const ecoord param1_num = dir1.Cross( offset );
// For segments (not infinite lines), check if intersection is within both segments
if( !aLines )
{
// Parameters must be in [0,1] for intersection within segments
// Since we're comparing t = q/d and s = p/d to [0,1], we need to handle sign of d
if( determinant > 0 )
{
// d > 0: check 0 ≤ q ≤ d and 0 ≤ p ≤ d
if( param1_num < 0 || param1_num > determinant ||
param2_num < 0 || param2_num > determinant )
return false;
}
else
{
// d < 0: check d ≤ q ≤ 0 and d ≤ p ≤ 0
if( param1_num > 0 || param1_num < determinant ||
param2_num > 0 || param2_num < determinant )
return false;
}
// Optionally exclude endpoint intersections (when segments share vertices)
if( aIgnoreEndpoints &&
( param1_num == 0 || param1_num == determinant ) &&
( param2_num == 0 || param2_num == determinant ) )
{
return false;
}
}
if( aPt )
{
// Use parametric equation: intersection = aSeg.A + (q/d) * f
const VECTOR2L scaled_dir2( rescale( param1_num, dir2.x, determinant ),
rescale( param1_num, dir2.y, determinant ) );
const VECTOR2L result = VECTOR2L( aSeg.A ) + scaled_dir2;
// Verify result fits in coordinate type range
constexpr ecoord max_coord = std::numeric_limits<VECTOR2I::coord_type>::max();
constexpr ecoord min_coord = std::numeric_limits<VECTOR2I::coord_type>::min();
if( result.x > max_coord || result.x < min_coord ||
result.y > max_coord || result.y < min_coord )
{
return false; // Intersection exists but coordinates overflow
}
*aPt = VECTOR2I( static_cast<int>( result.x ), static_cast<int>( result.y ) );
}
return true;
@ -285,18 +465,19 @@ SEG SEG::ParallelSeg( const VECTOR2I& aP ) const
}
bool SEG::ccw( const VECTOR2I& aA, const VECTOR2I& aB, const VECTOR2I& aC ) const
{
return (ecoord) ( aC.y - aA.y ) * ( aB.x - aA.x ) > (ecoord) ( aB.y - aA.y ) * ( aC.x - aA.x );
}
bool SEG::Collide( const SEG& aSeg, int aClearance, int* aActual ) const
{
// check for intersection
// fixme: move to a method
if( ccw( A, aSeg.A, aSeg.B ) != ccw( B, aSeg.A, aSeg.B ) &&
ccw( A, B, aSeg.A ) != ccw( A, B, aSeg.B ) )
// Handle negative clearance
if( aClearance < 0 )
{
if( aActual )
*aActual = 0;
return false;
}
// Check for exact intersection first
if( intersects( aSeg, false, false ) )
{
if( aActual )
*aActual = 0;
@ -304,60 +485,45 @@ bool SEG::Collide( const SEG& aSeg, int aClearance, int* aActual ) const
return true;
}
ecoord dist_sq = VECTOR2I::ECOORD_MAX;
ecoord clearance_sq = (ecoord) aClearance * aClearance;
const ecoord clearance_sq = static_cast<ecoord>( aClearance ) * aClearance;
ecoord min_dist_sq = VECTOR2I::ECOORD_MAX;
auto checkCollision =
[&]( bool aFinal )
auto checkDistance = [&]( ecoord dist, ecoord& min_dist ) -> bool
{
if( dist_sq == 0 )
if( dist == 0 )
{
if( aActual )
*aActual = 0;
return true;
}
else if( dist_sq < clearance_sq )
{
if( aActual )
{
if( aFinal )
{
*aActual = int( isqrt( dist_sq ) );
return true;
}
else
{
// We have to keep going to ensure we have the lowest value
// for aActual
return false;
}
}
return true;
}
return false;
min_dist = std::min( min_dist, dist );
return false; // Continue checking
};
dist_sq = std::min( dist_sq, SquaredDistance( aSeg.A ) );
if( checkCollision( false ) )
// There are 4 points to check: start and end of this segment, and
// start and end of the other segment.
if( checkDistance( SquaredDistance( aSeg.A ), min_dist_sq ) ||
checkDistance( SquaredDistance( aSeg.B ), min_dist_sq ) ||
checkDistance( aSeg.SquaredDistance( A ), min_dist_sq ) ||
checkDistance( aSeg.SquaredDistance( B ), min_dist_sq ) )
{
return true;
}
dist_sq = std::min( dist_sq, SquaredDistance( aSeg.B ) );
if( min_dist_sq < clearance_sq )
{
if( aActual )
*aActual = static_cast<int>( isqrt( min_dist_sq ) );
if( checkCollision( false ) )
return true;
}
dist_sq = std::min( dist_sq, aSeg.SquaredDistance( A ) );
if( aActual )
*aActual = static_cast<int>( isqrt( min_dist_sq ) );
if( checkCollision( false ) )
return true;
dist_sq = std::min( dist_sq, aSeg.SquaredDistance( B ) );
return checkCollision( true );
return false;
}

View File

@ -685,4 +685,544 @@ BOOST_AUTO_TEST_CASE( LineDistanceSided )
BOOST_TEST( seg.LineDistance( { 5, -8 }, true ) == -8 );
}
/**
* Test cases for segment intersection
*/
struct SEG_SEG_INTERSECT_CASE : public KI_TEST::NAMED_CASE
{
SEG m_seg_a;
SEG m_seg_b;
bool m_ignore_endpoints;
bool m_lines;
bool m_exp_intersect;
VECTOR2I m_exp_point;
};
// clang-format off
static const std::vector<SEG_SEG_INTERSECT_CASE> seg_intersect_cases = {
// Basic crossing cases
{
"Crossing at origin",
{ { -10, 0 }, { 10, 0 } },
{ { 0, -10 }, { 0, 10 } },
false, false, true,
{ 0, 0 }
},
{
"Crossing at (5,5)",
{ { 0, 5 }, { 10, 5 } },
{ { 5, 0 }, { 5, 10 } },
false, false, true,
{ 5, 5 }
},
{
"T-junction intersection",
{ { 0, 0 }, { 10, 0 } },
{ { 5, -5 }, { 5, 0 } },
false, false, true,
{ 5, 0 }
},
// Non-intersecting cases
{
"Parallel segments",
{ { 0, 0 }, { 10, 0 } },
{ { 0, 5 }, { 10, 5 } },
false, false, false,
{ 0, 0 }
},
{
"Separated segments",
{ { 0, 0 }, { 5, 0 } },
{ { 10, 0 }, { 15, 0 } },
false, false, false,
{ 0, 0 }
},
{
"Lines would intersect, but segments don't",
{ { 0, 0 }, { 2, 0 } },
{ { 5, -5 }, { 5, 5 } },
false, false, false,
{ 0, 0 }
},
// Endpoint intersection cases
{
"Endpoint touching - should intersect",
{ { 0, 0 }, { 10, 0 } },
{ { 10, 0 }, { 20, 0 } },
false, false, true,
{ 10, 0 }
},
{
"Endpoint touching - ignore endpoints",
{ { 0, 0 }, { 10, 0 } },
{ { 10, 0 }, { 20, 0 } },
true, false, false,
{ 0, 0 }
},
{
"Endpoint touching at angle",
{ { 0, 0 }, { 10, 0 } },
{ { 10, 0 }, { 15, 5 } },
false, false, true,
{ 10, 0 }
},
// Collinear cases
{
"Collinear overlapping segments",
{ { 0, 0 }, { 10, 0 } },
{ { 5, 0 }, { 15, 0 } },
false, false, true,
{ 7, 0 } // Midpoint of overlap [5,10]
},
{
"Collinear non-overlapping segments",
{ { 0, 0 }, { 5, 0 } },
{ { 10, 0 }, { 15, 0 } },
false, false, false,
{ 0, 0 }
},
{
"Collinear touching at endpoint",
{ { 0, 0 }, { 10, 0 } },
{ { 10, 0 }, { 20, 0 } },
false, false, true,
{ 10, 0 }
},
{
"Collinear contained segment",
{ { 0, 0 }, { 20, 0 } },
{ { 5, 0 }, { 15, 0 } },
false, false, true,
{ 10, 0 } // Midpoint of contained segment
},
{
"Collinear vertical overlapping",
{ { 5, 0 }, { 5, 10 } },
{ { 5, 5 }, { 5, 15 } },
false, false, true,
{ 5, 7 } // Midpoint of overlap [5,10]
},
// Line mode cases (infinite lines)
{
"Lines intersect, segments don't",
{ { 0, 0 }, { 2, 0 } },
{ { 5, -5 }, { 5, 5 } },
false, true, true,
{ 5, 0 }
},
{
"Parallel lines (infinite)",
{ { 0, 0 }, { 10, 0 } },
{ { 0, 5 }, { 10, 5 } },
false, true, false,
{ 0, 0 }
},
{
"Collinear lines (infinite)",
{ { 0, 0 }, { 10, 0 } },
{ { 20, 0 }, { 30, 0 } },
false, true, true,
{ 10, 0 } // Midpoint between segment starts
},
// Edge cases
{
"Zero-length segment intersection",
{ { 5, 5 }, { 5, 5 } },
{ { 0, 5 }, { 10, 5 } },
false, false, true,
{ 5, 5 }
},
{
"Both zero-length, same point",
{ { 5, 5 }, { 5, 5 } },
{ { 5, 5 }, { 5, 5 } },
false, false, true,
{ 5, 5 }
},
{
"Both zero-length, different points",
{ { 5, 5 }, { 5, 5 } },
{ { 10, 10 }, { 10, 10 } },
false, false, false,
{ 0, 0 }
},
// Diagonal intersection cases
{
"45-degree crossing",
{ { 0, 0 }, { 10, 10 } },
{ { 0, 10 }, { 10, 0 } },
false, false, true,
{ 5, 5 }
},
{
"Arbitrary angle crossing",
{ { 0, 0 }, { 6, 8 } },
{ { 0, 8 }, { 6, 0 } },
false, false, true,
{ 3, 4 }
},
// Bounding box optimization test cases
{
"Far apart horizontal segments",
{ { 0, 0 }, { 10, 0 } },
{ { 100, 0 }, { 110, 0 } },
false, false, false,
{ 0, 0 }
},
{
"Far apart vertical segments",
{ { 0, 0 }, { 0, 10 } },
{ { 0, 100 }, { 0, 110 } },
false, false, false,
{ 0, 0 }
},
{
"Far apart diagonal segments",
{ { 0, 0 }, { 10, 10 } },
{ { 100, 100 }, { 110, 110 } },
false, false, false,
{ 0, 0 }
},
};
// clang-format on
/**
* Predicate to check expected intersection between two segments
* @param aCase the test case containing all parameters
* @return does the intersection calculated agree?
*/
bool SegIntersectCorrect( const SEG_SEG_INTERSECT_CASE& aCase )
{
const auto resultA = aCase.m_seg_a.Intersect( aCase.m_seg_b, aCase.m_ignore_endpoints, aCase.m_lines );
const auto resultB = aCase.m_seg_b.Intersect( aCase.m_seg_a, aCase.m_ignore_endpoints, aCase.m_lines );
const bool intersectsA = resultA.has_value();
const bool intersectsB = resultB.has_value();
bool ok = ( intersectsA == aCase.m_exp_intersect ) && ( intersectsB == aCase.m_exp_intersect );
if( intersectsA != intersectsB )
{
std::stringstream ss;
ss << "Segment intersection is not the same in both directions: expected " << aCase.m_exp_intersect
<< ", got " << intersectsA << " & " << intersectsB;
BOOST_TEST_INFO( ss.str() );
ok = false;
}
else if( !ok )
{
std::stringstream ss;
ss << "Intersection incorrect: expected " << aCase.m_exp_intersect << ", got " << intersectsA;
BOOST_TEST_INFO( ss.str() );
}
// Check intersection point if intersection was expected
if( ok && aCase.m_exp_intersect && aCase.m_exp_point != VECTOR2I() )
{
// Allow some tolerance for intersection point calculation
const int tolerance = 1;
if( !resultA || !resultB )
{
std::stringstream ss;
ss << "Expected intersection but got nullopt";
BOOST_TEST_INFO( ss.str() );
ok = false;
}
else
{
const VECTOR2I pointA = *resultA;
const VECTOR2I pointB = *resultB;
bool pointOk = ( std::abs( pointA.x - aCase.m_exp_point.x ) <= tolerance &&
std::abs( pointA.y - aCase.m_exp_point.y ) <= tolerance &&
std::abs( pointB.x - aCase.m_exp_point.x ) <= tolerance &&
std::abs( pointB.y - aCase.m_exp_point.y ) <= tolerance );
if( !pointOk )
{
std::stringstream ss;
ss << "Intersection point incorrect: expected " << aCase.m_exp_point.Format()
<< ", got " << pointA.Format() << " & " << pointB.Format();
BOOST_TEST_INFO( ss.str() );
ok = false;
}
}
}
return ok;
}
BOOST_DATA_TEST_CASE( SegSegIntersection, boost::unit_test::data::make( seg_intersect_cases ), c )
{
BOOST_CHECK_PREDICATE( SegIntersectCorrect, ( c ) );
}
// Additional focused test cases for specific scenarios
BOOST_AUTO_TEST_CASE( IntersectLargeCoordinates )
{
// Test with large coordinates to verify overflow protection
SEG segA( { 1000000000, 0 }, { -1000000000, 0 } );
SEG segB( { 0, 1000000000 }, { 0, -1000000000 } );
auto intersection = segA.Intersect( segB, false, false );
BOOST_CHECK( intersection.has_value() );
BOOST_CHECK_EQUAL( intersection->x, 0 );
BOOST_CHECK_EQUAL( intersection->y, 0 );
}
BOOST_AUTO_TEST_CASE( IntersectOverflowDetection )
{
// Test intersection that would overflow coordinate range
constexpr int max_coord = std::numeric_limits<int>::max();
SEG segA( { 0, 0 }, { max_coord, max_coord } );
SEG segB( { max_coord, 0 }, { 0, max_coord } );
// This should either work or return nullopt due to overflow protection
auto intersection = segA.Intersect( segB, false, false );
// The test passes if it doesn't crash - the exact result depends on overflow handling
BOOST_TEST_MESSAGE( "Overflow test completed without crash. Has intersection: " << intersection.has_value() );
if( intersection.has_value() )
{
BOOST_TEST_MESSAGE( "Intersection point: " << intersection->Format() );
}
}
BOOST_AUTO_TEST_CASE( IntersectPrecisionEdgeCases )
{
// Test cases that might have precision issues
SEG segA( { 0, 0 }, { 1000000, 1 } );
SEG segB( { 500000, -1 }, { 500000, 2 } );
auto intersection = segA.Intersect( segB, false, false );
BOOST_CHECK( intersection.has_value() );
// The intersection should be very close to (500000, 0.5), rounded to (500000, 1) or (500000, 0)
BOOST_CHECK_EQUAL( intersection->x, 500000 );
BOOST_CHECK( intersection->y >= 0 && intersection->y <= 1 );
}
BOOST_AUTO_TEST_CASE( IntersectIgnoreEndpointsEdgeCases )
{
// Test edge cases with ignore endpoints
SEG segA( { 0, 0 }, { 10, 0 } );
SEG segB( { 5, -5 }, { 5, 5 } );
// Normal intersection should work
auto intersection1 = segA.Intersect( segB, false, false );
BOOST_CHECK( intersection1.has_value() );
BOOST_CHECK_EQUAL( *intersection1, VECTOR2I( 5, 0 ) );
// Should still work when ignoring endpoints (this is a middle intersection)
auto intersection2 = segA.Intersect( segB, true, false );
BOOST_CHECK( intersection2.has_value() );
BOOST_CHECK_EQUAL( *intersection2, VECTOR2I( 5, 0 ) );
// Test actual endpoint intersection
SEG segC( { 10, 0 }, { 20, 0 } );
auto intersection3 = segA.Intersect( segC, false, false );
BOOST_CHECK( intersection3.has_value() );
BOOST_CHECK_EQUAL( *intersection3, VECTOR2I( 10, 0 ) );
// Should not intersect when ignoring endpoints
auto intersection4 = segA.Intersect( segC, true, false );
BOOST_CHECK( !intersection4.has_value() );
}
BOOST_AUTO_TEST_CASE( IntersectCollinearRegressionTests )
{
// Regression tests for collinear segment handling
// Test case: horizontal segments with partial overlap
SEG seg1( { 0, 5 }, { 10, 5 } );
SEG seg2( { 5, 5 }, { 15, 5 } );
auto intersection = seg1.Intersect( seg2, false, false );
BOOST_CHECK( intersection.has_value() );
BOOST_CHECK_EQUAL( intersection->y, 5 );
BOOST_CHECK( intersection->x >= 5 && intersection->x <= 10 ); // Should be in overlap region
// Test case: vertical segments with complete overlap (one contained in other)
SEG seg3( { 3, 0 }, { 3, 20 } );
SEG seg4( { 3, 5 }, { 3, 15 } );
auto intersection2 = seg3.Intersect( seg4, false, false );
BOOST_CHECK( intersection2.has_value() );
BOOST_CHECK_EQUAL( intersection2->x, 3 );
BOOST_CHECK( intersection2->y >= 5 && intersection2->y <= 15 ); // Should be in contained segment
// Test case: diagonal collinear segments
SEG seg5( { 0, 0 }, { 10, 10 } );
SEG seg6( { 5, 5 }, { 15, 15 } );
auto intersection3 = seg5.Intersect( seg6, false, false );
BOOST_CHECK( intersection3.has_value() );
BOOST_CHECK( intersection3->x >= 5 && intersection3->x <= 10 );
BOOST_CHECK( intersection3->y >= 5 && intersection3->y <= 10 );
BOOST_CHECK_EQUAL( intersection3->x, intersection3->y ); // Should maintain diagonal relationship
// Test case: collinear segments that touch only at endpoints
SEG seg7( { 0, 0 }, { 5, 0 } );
SEG seg8( { 5, 0 }, { 10, 0 } );
auto intersection4 = seg7.Intersect( seg8, false, false );
BOOST_CHECK( intersection4.has_value() );
BOOST_CHECK_EQUAL( *intersection4, VECTOR2I( 5, 0 ) );
// Same test but ignoring endpoints
auto intersection5 = seg7.Intersect( seg8, true, false );
BOOST_CHECK( !intersection5.has_value() );
// Test case: collinear segments that don't overlap
SEG seg9( { 0, 0 }, { 5, 0 } );
SEG seg10( { 10, 0 }, { 15, 0 } );
auto intersection6 = seg9.Intersect( seg10, false, false );
BOOST_CHECK( !intersection6.has_value() );
}
BOOST_AUTO_TEST_CASE( IntersectBoundingBoxOptimization )
{
// Test that bounding box optimization works correctly
// Segments that are clearly separated - should be rejected quickly
SEG seg1( { 0, 0 }, { 10, 10 } );
SEG seg2( { 100, 100 }, { 110, 110 } );
auto intersection = seg1.Intersect( seg2, false, false );
BOOST_CHECK( !intersection.has_value() );
// Segments with overlapping bounding boxes but no intersection
SEG seg3( { 0, 0 }, { 10, 0 } );
SEG seg4( { 5, 5 }, { 15, 5 } );
auto intersection2 = seg3.Intersect( seg4, false, false );
BOOST_CHECK( !intersection2.has_value() );
// Segments with touching bounding boxes and actual intersection
SEG seg5( { 0, 0 }, { 10, 10 } );
SEG seg6( { 10, 0 }, { 0, 10 } );
auto intersection3 = seg5.Intersect( seg6, false, false );
BOOST_CHECK( intersection3.has_value() );
BOOST_CHECK_EQUAL( *intersection3, VECTOR2I( 5, 5 ) );
}
BOOST_AUTO_TEST_CASE( IntersectLineVsSegmentMode )
{
// Test the difference between line mode and segment mode
SEG seg1( { 0, 0 }, { 5, 0 } );
SEG seg2( { 10, -5 }, { 10, 5 } );
// In segment mode, these don't intersect
auto segmentIntersect = seg1.Intersect( seg2, false, false );
BOOST_CHECK( !segmentIntersect.has_value() );
// In line mode, they should intersect
auto lineIntersect = seg1.Intersect( seg2, false, true );
BOOST_CHECK( lineIntersect.has_value() );
BOOST_CHECK_EQUAL( *lineIntersect, VECTOR2I( 10, 0 ) );
// Test collinear case in line mode
SEG seg3( { 0, 0 }, { 10, 0 } );
SEG seg4( { 20, 0 }, { 30, 0 } );
// Segments don't intersect
auto segmentIntersect2 = seg3.Intersect( seg4, false, false );
BOOST_CHECK( !segmentIntersect2.has_value() );
// Lines (infinite) do intersect (collinear)
auto lineIntersect2 = seg3.Intersect( seg4, false, true );
BOOST_CHECK( lineIntersect2.has_value() );
}
BOOST_AUTO_TEST_CASE( IntersectNumericalStability )
{
// Test cases designed to stress numerical precision
// Very small segments
SEG seg1( { 0, 0 }, { 1, 1 } );
SEG seg2( { 0, 1 }, { 1, 0 } );
auto intersection = seg1.Intersect( seg2, false, false );
BOOST_CHECK( intersection.has_value() );
// Intersection should be very close to (0.5, 0.5), rounded to (0,0), (0,1), (1,0), or (1,1)
BOOST_CHECK( intersection->x >= 0 && intersection->x <= 1 );
BOOST_CHECK( intersection->y >= 0 && intersection->y <= 1 );
// Nearly parallel segments
SEG seg3( { 0, 0 }, { 1000, 1 } );
SEG seg4( { 0, 1 }, { 1000, 2 } );
auto intersection2 = seg3.Intersect( seg4, false, false );
BOOST_CHECK( !intersection2.has_value() ); // Should be detected as parallel/non-intersecting
// Segments that intersect at a very acute angle
SEG seg5( { 0, 0 }, { 1000000, 1 } );
SEG seg6( { 500000, -1 }, { 500000, 2 } );
auto intersection3 = seg5.Intersect( seg6, false, false );
BOOST_CHECK( intersection3.has_value() );
BOOST_CHECK_EQUAL( intersection3->x, 500000 );
}
BOOST_AUTO_TEST_CASE( IntersectZeroLengthSegments )
{
// Comprehensive tests for zero-length segments (points)
VECTOR2I point1( 5, 5 );
VECTOR2I point2( 10, 10 );
SEG pointSeg1( point1, point1 ); // Zero-length segment (point)
SEG pointSeg2( point2, point2 ); // Another zero-length segment
SEG normalSeg( { 0, 5 }, { 10, 5 } ); // Normal segment
// Point intersecting with normal segment
auto intersection1 = pointSeg1.Intersect( normalSeg, false, false );
BOOST_CHECK( intersection1.has_value() );
BOOST_CHECK_EQUAL( *intersection1, point1 );
// Point not intersecting with normal segment
auto intersection2 = pointSeg2.Intersect( normalSeg, false, false );
BOOST_CHECK( !intersection2.has_value() );
// Two points at same location
SEG pointSeg3( point1, point1 );
auto intersection3 = pointSeg1.Intersect( pointSeg3, false, false );
BOOST_CHECK( intersection3.has_value() );
BOOST_CHECK_EQUAL( *intersection3, point1 );
// Two points at different locations
auto intersection4 = pointSeg1.Intersect( pointSeg2, false, false );
BOOST_CHECK( !intersection4.has_value() );
// Point on line (infinite mode)
SEG lineSeg( { 0, 0 }, { 1, 1 } ); // Diagonal line segment
SEG pointOnLine( { 100, 100 }, { 100, 100 } ); // Point on extended line
auto intersection5 = pointOnLine.Intersect( lineSeg, false, false );
BOOST_CHECK( !intersection5.has_value() ); // Point not on segment
auto intersection6 = pointOnLine.Intersect( lineSeg, false, true );
BOOST_CHECK( intersection6.has_value() ); // Point on infinite line
BOOST_CHECK_EQUAL( *intersection6, VECTOR2I( 100, 100 ) );
}
BOOST_AUTO_TEST_SUITE_END()