A 2D line segment utility used for collision tests. It stores endpoints A and B, computes bounds and length, and provides segment-segment and segment-rect intersection tests including returning the nearest intersection point with a rect.
namespace BlockParty;
/// <summary>
/// 2D line segment with segment-segment and segment-rect intersection. Port of the
/// original GameAPI <c>Line</c> (used by the Squid laser).
/// </summary>
public sealed class Line
{
public Vector2 A;
public Vector2 B;
public Line( Vector2 a, Vector2 b ) { A = a; B = b; }
public float XMin => MathF.Min( A.x, B.x );
public float XMax => MathF.Max( A.x, B.x );
public float YMin => MathF.Min( A.y, B.y );
public float YMax => MathF.Max( A.y, B.y );
public float Length => (B - A).Length;
public bool Intersects( RectF rect ) => Intersects( this, rect );
public static bool Intersects( Line line, RectF rect )
{
// Broad-phase AABB reject (cheap fast path).
if ( rect.Left > line.XMax || rect.Right < line.XMin || rect.Top < line.YMin || rect.Bottom > line.YMax )
return false;
// If either endpoint is inside the rect the segment intersects it. Uses a robust
// point-in-rect test rather than a slope-based one, which produces NaN for vertical
// lines (B.x == A.x) and suffers catastrophic cancellation for near-vertical lines —
// both cases previously reported false negatives.
if ( rect.Touches( line.A ) || rect.Touches( line.B ) )
return true;
// Otherwise the segment intersects the rect iff it crosses one of the four edges.
return Intersects( line, new Line( rect.BottomLeft, rect.TopLeft ), out _ )
|| Intersects( line, new Line( rect.BottomRight, rect.TopRight ), out _ )
|| Intersects( line, new Line( rect.TopLeft, rect.TopRight ), out _ )
|| Intersects( line, new Line( rect.BottomLeft, rect.BottomRight ), out _ );
}
public bool Intersects( RectF rect, out Vector2 intersectionPoint ) => Intersects( this, rect, out intersectionPoint );
public static bool Intersects( Line line, RectF rect, out Vector2 intersectionPoint )
{
bool intersects = false;
intersectionPoint = Vector2.Zero;
if ( Intersects( line, rect ) )
{
float distanceSquared = float.MaxValue;
Vector2 hit;
var edges = new[]
{
new Line( rect.BottomLeft, rect.TopLeft ),
new Line( rect.BottomRight, rect.TopRight ),
new Line( rect.TopLeft, rect.TopRight ),
new Line( rect.BottomLeft, rect.BottomRight ),
};
foreach ( var e in edges )
{
if ( Intersects( line, e, out hit ) )
{
float d = (hit - line.A).LengthSquared;
if ( d < distanceSquared )
{
intersectionPoint = hit;
distanceSquared = d;
intersects = true;
}
}
}
}
return intersects;
}
public bool Intersects( Line other, out Vector2 intersectionPoint ) => Intersects( this, other, out intersectionPoint );
static bool Intersects( Line line1, Line line2, out Vector2 intersectionPoint )
{
float s1x = line1.B.x - line1.A.x;
float s1y = line1.B.y - line1.A.y;
float s2x = line2.B.x - line2.A.x;
float s2y = line2.B.y - line2.A.y;
float denom = (-s2x * s1y + s1x * s2y);
if ( denom == 0f ) { intersectionPoint = Vector2.Zero; return false; }
float s = (-s1y * (line1.A.x - line2.A.x) + s1x * (line1.A.y - line2.A.y)) / denom;
float t = (s2x * (line1.A.y - line2.A.y) - s2y * (line1.A.x - line2.A.x)) / denom;
if ( s >= 0 && s <= 1 && t >= 0 && t <= 1 )
{
intersectionPoint = new Vector2( line1.A.x + (t * s1x), line1.A.y + (t * s1y) );
return true;
}
intersectionPoint = Vector2.Zero;
return false;
}
}