Game/Line.cs

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.

Native Interop
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;
	}
}