mirror of
https://gitlab.com/kicad/code/kicad.git
synced 2025-09-14 02:03:12 +02:00
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:
parent
9d074c1679
commit
0459c54a92
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user