// Copyright (c) Craftwork Games. All rights reserved.
// Licensed under the MIT license.
// See LICENSE file in the project root for full license information.
using System;
using System.Diagnostics;
using System.Runtime.Serialization;
using Microsoft.Xna.Framework;
namespace MonoGame.Extended
{
///
/// Represents an infinite line in 2D space that extends in both directions without bounds.
///
[DataContract]
[DebuggerDisplay("{DebugDisplayString,nq}")]
public struct Line2D : IEquatable
{
#region Public Fields
///
/// The signed perpendicular distance from the origin to this line along its normal direction.
/// Positive values indicate the origin is on the opposite side of the line from the normal.
///
[DataMember]
public float Distance;
///
/// The unit normal vector perpendicular to this line, used with
/// to define the line's position and orientation.
///
[DataMember]
public Vector2 Normal;
#endregion
#region Internal Properties
internal readonly string DebugDisplayString
{
get
{
return string.Concat(
"Normal( ", Normal.ToString(), " ) \r\n",
"Distance( ", Distance.ToString(), " )"
);
}
}
#endregion
#region Public Constructors
///
/// Creates a new with the specified normal and distance from the origin.
///
/// The unit normal vector perpendicular to the line.
/// The perpendicular distance from the origin to the line along its normal direction.
public Line2D(Vector2 normal, float distance)
{
Normal = normal;
Distance = distance;
}
#endregion
#region Public Methods
///
/// Creates a that passes through a specified point with a given normal direction.
///
/// A point in 2D space that the line passes through.
///
/// The normal vector perpendicular to the line. This vector will be normalized automatically.
///
///
/// A new with a unit normal passing through the specified point.
///
public static Line2D CreateFromPointAndNormal(Vector2 point, Vector2 normal)
{
Vector2 n = Vector2.Normalize(normal);
float d = Vector2.Dot(n, point);
return new Line2D(n, d);
}
///
/// Creates a that passes through two specified points.
///
/// The first point in 2D space.
/// The second point in 2D space, must be distinct from the first.
///
/// A new passing through both points. The line's normal is oriented
/// 90 degrees counter-clockwise from the direction vector p1 to p2.
///
///
/// Thrown when the two points are too close to define a unique line.
///
public static Line2D CreateFromTwoPoints(Vector2 p1, Vector2 p2)
{
Vector2 direction = p2 - p1;
float lengthSquared = direction.LengthSquared();
if (lengthSquared < Collision2D.Epsilon * Collision2D.Epsilon)
{
throw new ArgumentException("Points must be distinct to define a line.");
}
// Normal is perpendicular to direction (rotate 90deg CCW)
Vector2 normal = new Vector2(-direction.Y, direction.X);
return CreateFromPointAndNormal(p1, normal);
}
///
/// Creates a that passes through a specified point and extends in a given direction.
///
/// A point in 2D space that the line passes through.
///
/// The direction vector the line extends along. This vector will be normalized automatically.
///
///
/// A new with a unit normal perpendicular to the direction, passing through the specified point.
///
public static Line2D CreateFromPointAndDirection(Vector2 point, Vector2 direction)
{
// Normal is perpendicular to direction (rotate 90deg CCW)
Vector2 normal = Vector2.Normalize(new Vector2(-direction.Y, direction.X));
float distance = Vector2.Dot(normal, point);
return new Line2D(normal, distance);
}
///
/// Computes the signed perpendicular distance from a point to this line.
///
/// The point in 2D space to measure from.
///
/// The signed distance from the point to the line. Positive values indicate the point is on
/// the same side as the normal vector, negative values indicate the opposite side, and zero
/// indicates the point lies on the line.
///
public readonly float DistanceToPoint(Vector2 point)
{
// C. Ericson, Real-Time Collision Detection, Morgan Kaufmann, 2005
// Section 5.1.1 "Closest Point on Plane To Point"
// Adapted from 3D plane to 2D line using implicit line equation
return Vector2.Dot(Normal, point) - Distance;
}
///
/// Computes the closest point on this line to a specified point.
///
/// The point in 2D space to project onto the line.
///
/// When this method returns, contains the parametric distance along the line's direction vector
/// to the closest point, where distanceAlongLine = 0 corresponds to the point on the line
/// closest to the origin.
///
///
/// The point on the line closest to the specified point, computed as the perpendicular projection
/// of the point onto the line.
///
public readonly Vector2 ClosestPoint(Vector2 point, out float distanceAlongLine)
{
// C. Ericson, Real-Time Collision Detection, Morgan Kaufmann, 2005
// Section 5.1.2 "Closest Point on Line Segment to Point"
// Applied to an infinite line (no clamping of t), as described by Ericson.
// This line is represented in implicit form:
// Dot(Normal, X) = Distance
// We need to calculate X, as any point X that satisfies this
// equation lies on the line.
Vector2 n = Normal;
float nn = Vector2.Dot(n, n);
if (nn <= Collision2D.Epsilon)
{
// Degenerate line, normal has no meaningful direction
// Treat line as a single point
distanceAlongLine = 0.0f;
return Vector2.Zero;
}
// Compute a specific point 'a' on the line by scaling the normal.
// Since Dot(n, n * (Distance / Dot(N, n))) = Distance
// this point satisfies the line equation
Vector2 a = n * (Distance / nn);
// Compute a direction vector 'ab' that lies along the line.
// In 2D a direction perpendicular to the normal is given by (-n.Y, n.X)
Vector2 ab = new Vector2(-n.Y, n.X);
float denom = Vector2.Dot(ab, ab);
distanceAlongLine = Vector2.Dot(point - a, ab);
distanceAlongLine /= denom;
return a + distanceAlongLine * ab;
}
///
/// Returns a normalized representation of the specified line with a unit normal vector.
///
/// The line to normalize.
///
/// A new representing the same geometric line with a unit normal vector.
///
public static Line2D Normalize(Line2D line)
{
Line2D result;
Normalize(ref line, out result);
return result;
}
///
/// Returns a normalized representation of the specified line with a unit normal vector.
///
/// The line to normalize.
///
/// When this method returns, contains a representing the same geometric line
/// with a unit normal vector and proportionally adjusted distance.
///
public static void Normalize(ref Line2D value, out Line2D result)
{
float length = value.Normal.Length();
if (length < Collision2D.Epsilon)
{
result = value;
return;
}
result = new Line2D(value.Normal / length, value.Distance / length);
}
///
/// Normalizes this line's representation by ensuring the normal vector has unit length.
///
///
/// After normalization, the will be a unit vector and
/// will be adjusted proportionally to maintain the same geometric line.
///
public void Normalize()
{
float length = Normal.Length();
if (length > Collision2D.Epsilon)
{
Normal /= length;
Distance /= length;
}
}
///
/// Tests if this line intersects with another line.
///
/// The other line to test against.
///
/// When this method returns , contains the point where the lines intersect.
/// When this method returns , contains indicating
/// the lines are parallel or coincident.
///
///
/// if the lines intersect at a single point; otherwise,
/// if the lines are parallel or coincident.
///
public readonly bool Intersects(Line2D other, out Vector2? point)
{
// Use implicit line representation and Cramer's rule
// to solve a 2D line-line intersection
// Check if lines are parallel
float cross = Normal.X * other.Normal.Y - Normal.Y * other.Normal.X;
if (MathF.Abs(cross) < Collision2D.Epsilon)
{
// Lines are parallel or coincident
point = null;
return false;
}
float x = (Distance * other.Normal.Y - other.Distance * Normal.Y) / cross;
float y = (other.Distance * Normal.X - Distance * other.Normal.X) / cross;
point = new Vector2(x, y);
return true;
}
///
/// Tests if this line intersects with another line.
///
/// The other line to test against.
///
/// if the lines intersect at a single point; otherwise,
/// if the lines are parallel or coincident.
///
public readonly bool Intersects(Line2D other)
{
return Intersects(other, out _);
}
///
/// Tests if this line intersects with a ray.
///
/// The ray to test against.
///
/// When this method returns , contains the parametric distance along the ray
/// to the intersection point.
/// When this method returns , contains .
///
///
/// When this method returns , contains the point where the line and ray intersect.
/// When this method returns , contains .
///
///
/// if the line and ray intersect in the ray's forward direction;
/// otherwise, if they are parallel or the intersection point is behind the ray's origin.
///
public readonly bool Intersects(Ray2D ray, out float? distanceAlongRay, out Vector2? point)
{
if (!Collision2D.SolveParametricIntersectionWithImplicitLine(Normal, Distance, ray.Origin, ray.Direction, out float t))
{
// Parallel or coincident
distanceAlongRay = null;
point = null;
return false;
}
// Ray only intersects in forward direction
if (t < 0.0f)
{
distanceAlongRay = null;
point = null;
return false;
}
distanceAlongRay = t;
point = ray.Origin + t * ray.Direction;
return true;
}
///
/// Tests if this line intersects with a ray.
///
/// The ray to test against.
///
/// if the line and ray intersect in the ray's forward direction;
/// otherwise, if they are parallel or the intersection point is behind the ray's origin.
///
public readonly bool Intersects(Ray2D ray)
{
return Intersects(ray, out _, out _);
}
///
/// Tests if this line intersects with a line segment.
///
/// The line segment to test against.
///
/// When this method returns , contains the parametric distance along the segment
/// to the intersection point, in the range [0, 1] where 0 represents the start and 1 represents the end.
/// When this method returns , contains .
///
///
/// When this method returns , contains the point where the line and segment intersect.
/// When this method returns , contains .
///
///
/// if the line intersects the segment within its bounds;
/// otherwise, if they are parallel or the intersection point lies outside the segment.
///
public readonly bool Intersects(LineSegment2D segment, out float? distanceAlongSegment, out Vector2? point)
{
Vector2 ab = segment.End - segment.Start;
if (!Collision2D.SolveParametricIntersectionWithImplicitLine(Normal, Distance, segment.Start, ab, out float t))
{
distanceAlongSegment = null;
point = null;
return false;
}
// Check if the intersection is within the segment bounds
if (t < 0.0f || t > 1.0f)
{
distanceAlongSegment = null;
point = null;
return false;
}
distanceAlongSegment = t;
point = segment.Start + t * ab;
return true;
}
///
/// Tests if this line intersects with a line segment.
///
/// The line segment to test against.
///
/// if the line intersects the segment within its bounds;
/// otherwise, if they are parallel or the intersection point lies outside the segment.
///
public readonly bool Intersects(LineSegment2D segment)
{
return Intersects(segment, out _, out _);
}
///
/// Tests if this line intersects with an axis-aligned bounding box.
///
/// The bounding box to test against.
///
/// if the line passes through or touches the bounding box;
/// otherwise, .
///
public readonly bool Intersects(BoundingBox2D box)
{
Vector2 n = Normal;
float nn = Vector2.Dot(n, n);
// Check for degenerate line
if(nn <= Collision2D.Epsilon * Collision2D.Epsilon)
return false;
// Point on the line: a = n * (d / Dot(n,n))
Vector2 origin = n * (Distance / nn);
// Direction along the line (perpendicular to normal)
Vector2 dir = new Vector2(-n.Y, n.X);
return Collision2D.ClipLineToAabb(origin, dir, box.Min, box.Max, float.MinValue, float.MaxValue, out _, out _);
}
///
/// Tests if this line intersects with a circle.
///
/// The circle to test against.
///
/// if the line passes through or is tangent to the circle;
/// otherwise, .
///
public readonly bool Intersects(BoundingCircle2D circle)
{
// C. Ericson, Real-Time Collision Detection, Morgan Kaufmann, 2005
// Distance‑based intersection of implicit line with circle
// Derived from Section 5.2.2 "Testing Sphere Against Plane" (2D reduction)
// Compute signed distance from circle center to line
float signedDist = DistanceToPoint(circle.Center);
// Line intersects circle if perpendicular distance is within radius
return MathF.Abs(signedDist) <= circle.Radius;
}
///
/// Tests if this line intersects with a capsule.
///
/// The capsule to test against.
///
/// if the line passes through or is tangent to the capsule;
/// otherwise, .
///
public readonly bool Intersects(BoundingCapsule2D capsule)
{
// C. Ericson, Real-Time Collision Detection, Morgan Kaufmann, 2005
// Intersection of an implicit line with a 2D capsule (line segment swept by a circle)
// Derived from Section 4.5.1 "Sphere-swept Volume Intersection" and Section 5.2.2 "Testing Sphere Against Plane"
// Compute signed distance to capsule endpoints
float signedDistA = DistanceToPoint(capsule.PointA);
float signedDistB = DistanceToPoint(capsule.PointB);
// Get absolute distances for comparison
float distToA = MathF.Abs(signedDistA);
float distToB = MathF.Abs(signedDistB);
// minimum distance starts as the closer of the two end points
float minDistSq = MathF.Min(distToA * distToA, distToB * distToB);
// Check if capsule's medial segment is not degenerate
Vector2 segmentDir = capsule.PointB - capsule.PointA;
float segmentLenSq = segmentDir.LengthSquared();
if (segmentLenSq > Collision2D.Epsilon * Collision2D.Epsilon)
{
// If endpoints are on opposite sides of the line, the segment crosses it
if (signedDistA * signedDistB <= 0.0f)
{
minDistSq = 0.0f;
}
}
// Line intersects capsule if minimum distance is within radius
return minDistSq <= capsule.Radius * capsule.Radius;
}
///
/// Tests if this line intersects with an oriented bounding box.
///
/// The oriented bounding box to test against.
///
/// if the line passes through or touches the box;
/// otherwise, .
///
public readonly bool Intersects(OrientedBoundingBox2D obb)
{
Vector2 n = Normal;
float nn = Vector2.Dot(n, n);
// Handle degenerate line
if(nn <= Collision2D.Epsilon * Collision2D.Epsilon)
return false;
// Get a world space point and direction for the line
Vector2 a = n * (Distance / nn);
Vector2 dir = new Vector2(-n.Y, n.X);
// Transform line into OBB local space
Vector2 diff = a - obb.Center;
Vector2 localOrigin = new Vector2(Vector2.Dot(diff, obb.AxisX), Vector2.Dot(diff, obb.AxisY));
Vector2 localDirection = new Vector2(Vector2.Dot(dir, obb.AxisX), Vector2.Dot(dir, obb.AxisY));
// Local OBB is just ABB [-halfExtents, +halfExtents]
return Collision2D.ClipLineToAabb(localOrigin, localDirection, -obb.HalfExtents, obb.HalfExtents, float.MinValue, float.MaxValue, out _, out _);
}
///
/// Tests if this line intersects with a polygon.
///
/// The polygon to test against.
///
/// if the line passes through or is tangent to the polygon;
/// otherwise, .
///
public readonly bool Intersects(BoundingPolygon2D polygon)
{
Vector2 n = Normal;
float nn = Vector2.Dot(n, n);
// Check for degenerate line
if(nn <= Collision2D.Epsilon * Collision2D.Epsilon)
return false;
// A point on the line: a = n * (d / Dot(n,n))
Vector2 a = n * (Distance / nn);
// A direction along the line (perpendicular to n)
Vector2 dir = new Vector2(-n.Y, n.X);
// Clip infinite line against polygon half-spaces
return Collision2D.ClipLineToConvexPolygon(a, dir, polygon.Vertices, polygon.Normals, float.MinValue, float.MaxValue, out _, out _);
}
///
/// Deconstructs this line into its component values.
///
///
/// When this method returns, contains the unit normal vector perpendicular to this line.
///
///
/// When this method returns, contains the signed perpendicular distance from the origin
/// to this line along its normal direction.
///
public readonly void Deconstruct(out Vector2 normal, out float distance)
{
normal = Normal;
distance = Distance;
}
///
public readonly bool Equals(Line2D other)
{
return Normal.Equals(other.Normal)
&& Math.Abs(Distance - other.Distance) < Collision2D.Epsilon;
}
///
public override readonly bool Equals(object obj)
{
return obj is Line2D other && Equals(other);
}
///
public override readonly int GetHashCode()
{
return Normal.GetHashCode() ^ Distance.GetHashCode();
}
///
public override readonly string ToString()
{
return "{Normal:" + Normal.ToString() + " Distance:" + Distance.ToString() + "}";
}
///
public static bool operator ==(Line2D left, Line2D right)
{
return left.Equals(right);
}
///
public static bool operator !=(Line2D left, Line2D right)
{
return !left.Equals(right);
}
#endregion
}
}