using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Serialization;
using Microsoft.Xna.Framework;
namespace MonoGame.Extended;
///
/// Represents a line segment defined by two endpoints.
///
[DataContract]
[DebuggerDisplay("{DebugDisplayString,nq}")]
public struct LineSegment : IEquatable
{
#region Fields
private static readonly LineSegment s_empty = new LineSegment(Vector2.Zero, Vector2.Zero);
///
/// The starting point of the line segment.
///
[DataMember]
public Vector2 Start;
///
/// The ending point of the line segment.
///
[DataMember]
public Vector2 End;
#endregion
#region Properties
///
/// Gets an empty line segment representing a degenerate case with both endpoints at the same position.
///
public static LineSegment Empty => s_empty;
///
/// Gets a value indicating whether this line segment represents a degenerate case with no length,
/// having both endpoints at the same position.
///
public readonly bool IsEmpty => Start == End;
///
/// Gets the direction vector from start to end point with magnitude equal to segment length.
///
public readonly Vector2 Direction => End - Start;
///
/// Gets the direction vector from start to end point normalized to unit length.
///
public readonly Vector2 DirectionNormalized => Vector2.Normalize(Direction);
///
/// Gets the length of the line segment.
///
public readonly float Length => Vector2.Distance(Start, End);
///
/// Gets the squared length of the line segment, avoiding square root calculation.
///
///
/// This property avoids the expensive square root operation, making it more efficient
/// for length comparisons when the actual distance value is not needed.
///
public readonly float LengthSquared => Vector2.DistanceSquared(Start, End);
///
/// Gets the midpoint of the line segment.
///
public readonly Vector2 Center => (Start + End) * 0.5f;
#endregion
#region Constructors
///
/// Initializes a new line segment with the specified endpoints.
///
/// The starting point of the segment.
/// The ending point of the segment.
public LineSegment(Vector2 start, Vector2 end)
{
Start = start;
End = end;
}
#endregion
#region Create From Methods
///
/// Creates a line segment from a starting point, direction, and length.
///
/// The starting point of the segment.
/// The direction vector (will be normalized if non-zero).
/// The length of the segment.
/// A new line segment with the specified properties.
public static LineSegment FromDirection(Vector2 start, Vector2 direction, float length)
{
Vector2 normalizedDir = direction;
if (direction != Vector2.Zero)
{
normalizedDir.Normalize();
}
return new LineSegment(start, start + normalizedDir * length);
}
#endregion
#region Intersection Methods
///
/// Determines whether this line segment intersects with another line segment.
///
/// The other line segment to test for intersection.
///
/// if this line segment intersects the other; otherwise, .
///
public readonly bool Intersects(LineSegment other) => IntersectionTests.LineSegmentLineSegment(in this, in other);
///
/// Determines whether this line segment intersects with a circle
///
/// The circle to test for intersection.
///
/// if this line segment intersects the circle; otherwise, .
///
public readonly bool Intersects(Circle circle) => IntersectionTests.CirceLineSegment(in circle, in this);
///
/// Determines whether this line segment intersects with an ellipse
///
/// The ellipse to test for intersection.
///
/// if this line segment intersects the ellipse; otherwise, .
///
public readonly bool Intersects(Ellipse ellipse) => IntersectionTests.EllipseLineSegment(in ellipse, in this);
///
/// Determines whether this line segment intersects with a rectangle
///
/// The rectangle to test for intersection.
///
/// if this line segment intersects the rectangle; otherwise, .
///
public readonly bool Intersects(RectangleF rectangle) => IntersectionTests.RectangleFLineSegment(in rectangle, in this);
///
/// Determines whether this line segment intersects with a ray
///
/// The ray to test for intersection.
///
/// if this line segment intersects the ray; otherwise, .
///
public readonly bool Intersects(Ray ray) => IntersectionTests.RayLineSegment(in ray, in this);
#endregion
#region Closest Point To Methods
///
/// Computes the closest point on the line segment to the specified point.
///
/// The query point for closest-point computation.
/// The point on the segment closest to the query point.
///
/// This method projects the point onto the line defined by the segment, then clamps
/// the result to the segment endpoints if necessary.
///
public readonly Vector2 ClosestPointTo(Vector2 point)
{
Vector2 ab = End - Start;
float t = Vector2.Dot(point - Start, ab);
if (t <= 0.0f)
{
// Point projects outside segment on the start side
return Start;
}
float denom = Vector2.Dot(ab, ab);
if (t >= denom)
{
// Point project outside segment on the end side
return End;
}
// Point project inside segment
return Start + ab * (t / denom);
}
///
/// Computes the closest point on the line segment to the specified point with parametric position.
///
/// The query point for closest-point computation.
///
/// When this method returns, contains the parametric position (0 to 1) of the closest point along the segment,
/// where 0 corresponds to the start point and 1 corresponds to the end point.
///
/// The point on the segment closest to the query point.
public readonly Vector2 ClosestPointTo(Vector2 point, out float t)
{
Vector2 ab = End - Start;
t = Vector2.Dot(point - Start, ab);
if (t <= 0.0f)
{
// Point projects outside segment on the start side
t = 0.0f;
return Start;
}
float denom = Vector2.Dot(ab, ab);
if (t >= denom)
{
// Point projects outside segment on the End side
t = 1.0f;
return End;
}
// Point projects inside segment
t = t / denom;
return Start + t * ab;
}
///
/// Computes the closest points between this segment and another segment.
///
/// The other line segment to test against.
/// When this method returns, contains the closest point on this segment.
/// When this method returns, contains the closest point on the other segment.
/// The distance between the two closest points.
public readonly float ClosestPoints(LineSegment other, out Vector2 pointOnThis, out Vector2 pointOnOther)
{
return ClosestPointsSegmentSegment(Start, End, other.Start, other.End, out _, out _, out pointOnThis, out pointOnOther);
}
///
/// Computes the closest points between two line segments.
///
/// The start point of the first segment.
/// The end point of the first segment.
/// The start point of the second segment.
/// The end point of the second segment.
/// When this method returns, contains the parametric position on the first segment.
/// When this method returns, contains the parametric position on the second segment.
/// When this method returns, contains the closest point on the first segment.
/// When this method returns, contains the closest point on the second segment.
/// The distance between the closest points.
private static float ClosestPointsSegmentSegment(Vector2 p1, Vector2 q1, Vector2 p2, Vector2 q2, out float s, out float t, out Vector2 c1, out Vector2 c2)
{
// Direction vector of segment S1
Vector2 d1 = q1 - p1;
// Direction vector of segment S2
Vector2 d2 = q2 - p2;
Vector2 r = p1 - p2;
// Squared length of segment S1
float a = Vector2.Dot(d1, d1);
// Squared length of segment S2
float e = Vector2.Dot(d2, d2);
float f = Vector2.Dot(d2, r);
// Check if either or both segments degenerate into points
if (a <= 0.001f && e <= 0.001f)
{
// Both segments degenerate into points
s = t = 0.0f;
c1 = p1;
c2 = p2;
return Vector2.Distance(c1, c2);
}
if (a <= 0.001f)
{
// First segment degenerates into a point
s = 0.0f;
t = f / e;
t = Math.Clamp(t, 0.0f, 1.0f);
}
else
{
float c = Vector2.Dot(d1, r);
if (e <= 0.001f)
{
// second segment degenerates into a point
t = 0.0f;
s = Math.Clamp(-c / a, 0.0f, 1.0f);
}
else
{
// The general non-degenerate case starts here
float b = Vector2.Dot(d1, d2);
float denom = a * e - b * b;
// If segments not parallel, compute closest point on L1 to L2
if (denom != 0.0f)
{
s = Math.Clamp((b * f - c * e) / denom, 0.0f, 1.0f);
}
else
{
// Parallel segments - pick arbitrary s
s = 0.0f;
}
// Compute point on L2 closest to S1(s)
t = (b * s + f) / e;
// If t in [0,1] done. Else clamp t, recompute s
if (t < 0.0f)
{
t = 0.0f;
s = Math.Clamp(-c / a, 0.0f, 1.0f);
}
else if (t > 1.0f)
{
t = 1.0f;
s = Math.Clamp((b - c) / a, 0.0f, 1.0f);
}
}
}
c1 = p1 + d1 * s;
c2 = p2 + d2 * t;
return Vector2.Distance(c1, c2);
}
///
/// Computes the point on the segment at the specified parametric position.
///
///
/// The parametric position along the segment, where 0 returns
/// and 1 returns . Values outside [0,1] extrapolate beyond the segment.
///
/// The point at position t along the segment.
public readonly Vector2 GetPointAt(float t)
{
return Start + t * (End - Start);
}
#endregion
#region Distance Methods
///
/// Computes the shortest distance from the line segment to the specified point.
///
/// The point to measure distance to.
/// The shortest distance from the segment to the point.
public readonly float DistanceTo(Vector2 point)
{
return MathF.Sqrt(DistanceSquaredTo(point));
}
///
/// Computes the squared shortest distance from the line segment to the specified point.
///
/// The point to measure distance to.
/// The squared shortest distance from the segment to the point.
///
/// This method is more efficient than as it avoids the expensive square root
/// operation.
///
public readonly float DistanceSquaredTo(Vector2 point)
{
Vector2 ab = End - Start;
Vector2 ac = point - Start;
Vector2 bc = point - End;
float e = Vector2.Dot(ac, ab);
// Point projects outside the segment on the start side
if (e <= 0.0f)
{
return Vector2.Dot(ac, ac);
}
float f = Vector2.Dot(ab, ab);
// Point projects outside the segment on the end side
if (e >= f)
{
return Vector2.Dot(bc, bc);
}
// Point projects inside the segment
return Vector2.Dot(ac, ac) - e * e / f;
}
#endregion
#region Offset Methods
///
/// Translates the line segment by moving both endpoints, preserving direction and length.
///
/// The translation vector to apply to both endpoints.
public void Offset(Vector2 offset)
{
Start += offset;
End += offset;
}
///
/// Creates a new line segment translated by the specified offset vector.
///
/// The line segment to translate.
/// The translation vector to apply.
/// A new line segment with both endpoints translated.
public static LineSegment Offset(LineSegment lineSegment, Vector2 offset)
{
return lineSegment with { Start = lineSegment.Start + offset, End = lineSegment.End + offset };
}
#endregion
#region Transform Methods
///
/// Applies a 2D transformation matrix to the line segment, transforming both endpoints.
///
/// The transformation matrix to apply.
public void Transform(Matrix3x2 matrix)
{
Transform(ref matrix);
}
///
/// Applies a 2D transformation matrix to the line segment, transforming both endpoints.
///
/// The transformation matrix to apply.
public void Transform(ref Matrix3x2 matrix)
{
Start = Vector2.Transform(Start, matrix);
End = Vector2.Transform(End, matrix);
}
///
/// Creates a new line segment by applying a 2D transformation matrix.
///
/// The line segment to transform.
/// The transformation matrix to apply.
/// A new transformed line segment.
public static LineSegment Transform(LineSegment lineSegment, Matrix3x2 matrix)
{
return Transform(lineSegment, ref matrix);
}
///
/// Computes a transformed line segment using reference parameters for performance.
///
/// The line segment to transform.
/// The transformation matrix to apply.
public static LineSegment Transform(LineSegment lineSegment, ref Matrix3x2 matrix)
{
return lineSegment with { Start = Vector2.Transform(lineSegment.Start, matrix), End = Vector2.Transform(lineSegment.End, matrix) };
}
#endregion
#region Equality Methods
///
/// Determines whether this line segment is equal to the specified object.
///
/// The object to compare.
///
/// true if the object is a with the same start and end; otherwise, false.
///
public override readonly bool Equals([NotNullWhen(true)] object obj)
{
return obj is LineSegment other && Equals(other);
}
///
/// Determines whether this line segment is equal to another line segment.
///
/// The line segment to compare with this line segment.
///
/// true if the line segments have the same start and end; otherwise, false.
///
public readonly bool Equals(LineSegment other)
{
return Equals(other);
}
#endregion
///
/// Computes the hash code for this line segment.
///
/// A hash code derived from the start and end points.
public override readonly int GetHashCode()
{
return HashCode.Combine(Start, End);
}
///
/// Creates a string representation of this line segment.
///
/// A formatted string containing the start and end points.
public override readonly string ToString()
{
return $"LineSegment({Start} - {End})";
}
internal readonly string DebugDisplayString => ToString();
#region Operators
///
/// Tests whether two line segments are equal.
///
/// The first line segment.
/// The second line segment.
/// true if both line segments have the same start and end; otherwise, false.
public static bool operator ==(LineSegment left, LineSegment right)
{
return left.Equals(right);
}
///
/// Tests whether two line segments are not equal.
///
/// The first line segment.
/// The second line segment.
/// true if the line segments have different start or end points; otherwise, false.
public static bool operator !=(LineSegment left, LineSegment right)
{
return !left.Equals(right);
}
#endregion
}