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 }