// 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.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; using Microsoft.Xna.Framework; namespace MonoGame.Extended { /// /// Represents a finite line segment connecting two endpoints in 2D space. /// [DataContract] [DebuggerDisplay("{DebugDisplayString,nq}")] public struct LineSegment2D : IEquatable { #region Public Fields /// /// The starting point of this line segment in 2D space. /// [DataMember] public Vector2 Start; /// /// The ending point of this line segment in 2D space. /// [DataMember] public Vector2 End; #endregion #region Public Properties /// /// Gets the unnormalized direction vector from start to end. /// The length of this vector equals the length of the segment. /// public readonly Vector2 Direction { get { return End - Start; } } /// /// Gets the midpoint of this line segment, located halfway between the start and end points. /// public readonly Vector2 Midpoint { get { return (Start + End) * 0.5f; } } /// /// Gets the length of this line segment. /// For length comparisons, use to avoid the square root calculation. /// public readonly float Length { get { return Vector2.Distance(Start, End); } } /// /// Gets the squared length of this line segment. /// /// /// This property avoids the expensive square root operation, making it efficient for length comparisons. /// public readonly float LengthSquared { get { return Vector2.DistanceSquared(Start, End); } } #endregion #region Internal Properties internal readonly string DebugDisplayString { get { return string.Concat( "Start( ", Start.ToString(), " ) \r\n", "End( ", End.ToString(), " )" ); } } #endregion #region Public Constructors /// /// Creates a new connecting two specified points. /// /// The starting point of the line segment in 2D space. /// The ending point of the line segment in 2D space. public LineSegment2D(Vector2 start, Vector2 end) { Start = start; End = end; } #endregion #region Public Methods /// /// Computes the smallest axis-aligned bounding box that contains this line segment. /// /// /// A that tightly encloses both endpoints of this segment. /// /// /// The bounding box is computed using the component-wise minimum and maximum of the start and end points. /// public readonly BoundingBox2D GetBounds() { Vector2 min = Vector2.Min(Start, End); Vector2 max = Vector2.Max(Start, End); return new BoundingBox2D(min, max); } /// /// Computes a point along this line segment at the specified parametric distance. /// /// /// The parametric distance along the segment from the start point, where 0 represents the start, /// 1 represents the end, and values between 0 and 1 lie on the segment. /// /// /// The point at position Start + distanceAlongSegment * (End - Start). /// Values outside [0, 1] return points beyond the segment's endpoints along the line defined by the segment. /// public readonly Vector2 GetPoint(float distanceAlongSegment) { return Start + distanceAlongSegment * (End - Start); } /// /// Computes the closest point on this line segment to a specified point. /// /// The point in 2D space to find the closest point to. /// /// When this method returns, contains the parametric distance along the segment to the closest point, /// clamped to the range [0, 1] where 0 represents the start and 1 represents the end. /// /// /// The closest point on the segment. Returns the start if the point projects before the segment, /// the end if the point projects beyond the segment, or the perpendicular projection if the point /// projects onto the segment. /// public readonly Vector2 ClosestPoint(Vector2 point, out float distanceAlongSegment) { // C. Ericson, Real-Time Collision Detection, Morgan Kaufmann, 2005 // Section 5.1.2 "Closest Point on Line Segment to Point" Vector2 ab = End - Start; // Project point onto ab, but deferring divide by Dot(ab, ab) distanceAlongSegment = Vector2.Dot(point - Start, ab); if (distanceAlongSegment <= 0.0f) { // point projects outside the [a,b] interval, on the a side; clamp to a distanceAlongSegment = 0.0f; return Start; } float denom = Vector2.Dot(ab, ab); if (distanceAlongSegment >= denom) { // point projects outside the [a,b] interval, on the b side; clamp to b distanceAlongSegment = 1.0f; return End; } // Point projects inside the [a,b] interval; must do deferred divide now distanceAlongSegment /= denom; return Start + distanceAlongSegment * ab; } /// /// Computes the squared distance from a point to the closest point on this line segment. /// /// The point in 2D space to measure from. /// /// The squared distance. Returns the distance to if the point /// projects before the segment, distance to if it projects beyond, or the /// perpendicular distance if the point projects onto the segment. /// /// /// This method returns squared distance to avoid the expensive square root operation, /// making it efficient for distance comparisons. /// public readonly float DistanceSquaredToPoint(Vector2 point) { return Collision2D.DistanceSquaredPointSegment(point, Start, End, out _, out _); } /// /// Computes the distance from a point to the closest point on this line segment. /// /// The point in 2D space to measure from. /// /// The distance. Returns the distance to if the point /// projects before the segment, distance to if it projects beyond, or the /// perpendicular distance if the point projects onto the segment. /// /// /// This method performs a square root operation. For distance comparisons, /// use instead to avoid the computational cost. /// public readonly float DistanceToPoint(Vector2 point) { return MathF.Sqrt(DistanceSquaredToPoint(point)); } /// /// Computes the squared distance between this line segment and another line segment. /// /// The other line segment to measure distance to. /// /// When this method returns, contains the parametric distance along this segment to the closest point, /// in the range [0, 1] where 0 represents the start and 1 represents the end. /// /// /// When this method returns, contains the parametric distance along the other segment to the closest point, /// in the range [0, 1] where 0 represents the start and 1 represents the end. /// /// /// When this method returns, contains the closest point on this segment to the other segment. /// /// /// When this method returns, contains the closest point on the other segment to this segment. /// /// /// The squared distance in between the two closest points on the segments. /// /// /// This method returns squared distance to avoid the expensive square root operation, /// making it efficient for distance comparisons. /// public readonly float DistanceSquaredToSegment(LineSegment2D other, out float distanceAlongSegment1, out float distanceAlongSegment2, out Vector2 closestPoint1, out Vector2 closestPoint2) { return Collision2D.DistanceSquaredSegmentSegment(Start, End, other.Start, other.End, out distanceAlongSegment1, out distanceAlongSegment2, out closestPoint1, out closestPoint2); } /// /// Computes the distance between this line segment and another line segment. /// /// The other line segment to measure distance to. /// /// The shortest distance between any two points on the segments. /// /// /// This method performs a square root operation. For distance comparisons, /// use instead to avoid the computational cost. /// public readonly float DistanceToSegment(LineSegment2D other) { return MathF.Sqrt(DistanceSquaredToSegment(other, out _, out _, out _, out _)); } /// /// Tests if this line segment intersects with a line. /// /// The line to test against. /// /// When this method returns , contains the parametric distance along this 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 segment and line intersect. /// When this method returns , contains . /// /// /// if the segment intersects the line within its bounds; /// otherwise, if they are parallel or the intersection point lies outside the segment. /// public readonly bool Intersects(Line2D line, out float? distanceAlongSegment, out Vector2? point) { return line.Intersects(this, out distanceAlongSegment, out point); } /// /// Tests if this line segment intersects with a line. /// /// The line to test against. /// /// if the segment intersects the line within its bounds; /// otherwise, if they are parallel or the intersection point lies outside the segment. /// public readonly bool Intersects(Line2D line) { return line.Intersects(this); } /// /// Tests if this line segment intersects with a ray. /// /// The ray to test against. /// /// When this method returns , contains the parametric distance along this 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 parametric distance along the ray /// to the intersection point. /// When this method returns , contains . /// /// /// When this method returns , contains the point where the segment and ray intersect. /// When this method returns , contains . /// /// /// if the segment intersects the ray within the segment's bounds and the ray's forward /// direction; otherwise, if they are parallel, the intersection is outside the segment, /// or behind the ray's origin. /// public readonly bool Intersects(Ray2D ray, out float? distanceAlongSegment, out float? distanceAlongRay, out Vector2? point) { return ray.Intersects(this, out distanceAlongRay, out distanceAlongSegment, out point); } /// /// Tests if this line segment intersects with a ray. /// /// The ray to test against. /// /// if the segment intersects the ray within the segment's bounds and the ray's forward /// direction; otherwise, if they are parallel, the intersection is outside the segment, /// or behind the ray's origin. /// public readonly bool Intersects(Ray2D ray) { return ray.Intersects(this); } /// /// Tests if this line segment intersects with another line segment. /// /// The other line segment to test against. /// /// When this method returns , contains the parametric distance along this 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 parametric distance along the other 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 segments intersect. /// When this method returns , contains . /// /// /// if both segments intersect within their bounds; /// otherwise, if they are parallel or if the intersection point lies outside either segment. /// public readonly bool Intersects(LineSegment2D other, out float? distanceAlongSegment1, out float? distanceAlongSegment2, out Vector2? point) { if (!Collision2D.SolveParametricIntersection2D(Start, Direction, other.Start, other.Direction, out float t1, out float t2)) { distanceAlongSegment1 = distanceAlongSegment2 = null; point = null; return false; } // Clamp to segment bounds [0,1] if (t1 < 0.0f || t1 > 1.0f || t2 < 0.0f || t2 > 1.0f) { distanceAlongSegment1 = distanceAlongSegment2 = null; point = null; return false; } distanceAlongSegment1 = t1; distanceAlongSegment2 = t2; point = Start + t1 * Direction; return true; } /// /// Tests if this line segment intersects with another line segment. /// /// The other line segment to test against. /// /// if both segments intersect within their bounds; /// otherwise, if they are parallel or if the intersection point lies outside either segment. /// public readonly bool Intersects(LineSegment2D other) { return Intersects(other, out _, out _, out _); } /// /// Tests if this line segment intersects with an axis-aligned bounding box and computes the parametric distances to the intersection points. /// /// The bounding box to test against. /// /// When this method returns , contains the parametric distance along this segment /// to the entry 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 parametric distance along this segment /// to the exit intersection point, in the range [0, 1] where 0 represents the start and 1 represents the end. /// When this method returns , contains . /// /// /// if the segment intersects the bounding box; otherwise, . /// /// /// For degenerate segments (zero length), returns with tMin = tMax = 0 /// if the start point is inside the bounding box. /// public readonly bool Intersects(BoundingBox2D box, out float? tMin, out float? tMax) { Vector2 d = Direction; float dd = Vector2.Dot(d, d); // Handle degenerate segment (zero length) if (dd < Collision2D.Epsilon * Collision2D.Epsilon) { bool inside = Start.X >= box.Min.X && Start.X <= box.Max.X && Start.Y >= box.Min.Y && Start.Y <= box.Max.Y; if (inside) { tMin = tMax = 0.0f; return true; } tMin = tMax = null; return false; } if (!Collision2D.ClipLineToAabb(Start, d, box.Min, box.Max, 0.0f, 1.0f, out float tEnter, out float tExit)) { tMin = tMax = null; return false; } tMin = tEnter; tMax = tExit; return true; } /// /// Tests if this line segment intersects with an axis-aligned bounding box. /// /// The bounding box to test against. /// /// if the segment intersects the bounding box; otherwise, . /// public readonly bool Intersects(BoundingBox2D box) { return Intersects(box, out _, out _); } /// /// Tests if this line segment intersects with a circle and computes the parametric distances to the intersection points. /// /// The circle to test against. /// /// When this method returns , contains the parametric distance along this segment /// to the entry 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 parametric distance along this segment /// to the exit intersection point, in the range [0, 1] where 0 represents the start and 1 represents the end. /// When this method returns , contains . /// /// /// if the segment intersects the circle; otherwise, . /// /// /// For degenerate segments (zero length), returns with tSegmentMin = tSegmentMax = 0 /// if the start point is inside the circle. /// public readonly bool Intersects(BoundingCircle2D circle, out float? tSegmentMin, out float? tSegmentMax) { // C. Ericson, Real-Time Collision Detection, Morgan Kaufmann, 2005 // Parametric intersection of a segment with a circle (2D reduction) // Derived from Section 5.3.2 "Intersecting Ray or Segment Against Sphere" Vector2 d = Direction; float dd = Vector2.Dot(d, d); // Handle degenerate segment (zero length) if (dd <= Collision2D.Epsilon * Collision2D.Epsilon) { float distSq = Vector2.DistanceSquared(Start, circle.Center); if (distSq <= circle.Radius * circle.Radius) { tSegmentMin = tSegmentMax = 0.0f; return true; } tSegmentMin = tSegmentMax = null; return false; } if (!Collision2D.RayCircleIntersectionInterval(Start, d, circle.Center, circle.Radius, out float tMin, out float tMax)) { tSegmentMin = tSegmentMax = null; return false; } // Clip ray interval to segment interval [0,1] if (!Collision2D.ClipInterval(tMin, tMax, 0.0f, 1.0f, out float enter, out float exit)) { tSegmentMin = tSegmentMax = null; return false; } tSegmentMin = enter; tSegmentMax = exit; return true; } /// /// Tests if this line segment intersects with a circle. /// /// The circle to test against. /// /// if the segment intersects the circle; otherwise, . /// public readonly bool Intersects(BoundingCircle2D circle) { return Intersects(circle, out _, out _); } /// /// Deconstructs this line segment into its component values. /// /// /// When this method returns, contains the starting point of this line segment in 2D space. /// /// /// When this method returns, contains the ending point of this line segment in 2D space. /// public readonly void Deconstruct(out Vector2 start, out Vector2 end) { start = Start; end = End; } /// /// Tests if this line segment intersects with a capsule and computes the parametric distances to the intersection points. /// /// The capsule to test against. /// /// When this method returns , contains the parametric distance along this segment /// to the entry 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 parametric distance along this segment /// to the exit intersection point, in the range [0, 1] where 0 represents the start and 1 represents the end. /// When this method returns , contains . /// /// /// if the segment intersects the capsule; otherwise, . /// /// /// For degenerate segments (zero length), returns with tMin = tMax = 0 /// if the start point is inside the capsule. /// public readonly bool Intersects(BoundingCapsule2D capsule, out float? tMin, out float? tMax) { // C. Ericson, Real-Time Collision Detection, Morgan Kaufmann, 2005 // Parametric intersection of a segment with a capsule // Derived from Section 5.1.9 "Closest Points of Two Line Segments" // and Section 5.3.7 "Intersecting Ray or Segment Against Cylinder" Vector2 d = Direction; float dd = Vector2.Dot(d, d); float radiusSq = capsule.Radius * capsule.Radius; // Check for degenerate segment if (dd <= Collision2D.Epsilon * Collision2D.Epsilon) { // Segment is degenerate, treat as point float distSq = Collision2D.DistanceSquaredPointSegment(Start, capsule.PointA, capsule.PointB, out _, out _); if (distSq <= radiusSq) { tMin = tMax = 0.0f; return true; } tMin = tMax = null; return false; } // Get intersection interval against the infinite parametric line P(t)=Start + t * d if (!Collision2D.RayCapsuleIntersectionInterval(Start, d, capsule.PointA, capsule.PointB, capsule.Radius, out float t0, out float t1)) { tMin = tMax = null; return false; } if (!Collision2D.ClipInterval(t0, t1, 0.0f, 1.0f, out float enter, out float exit)) { tMin = tMax = null; return false; } tMin = enter; tMax = exit; return true; } /// /// Tests if this line segment intersects with a capsule. /// /// The capsule to test against. /// /// if the segment intersects the capsule; otherwise, . /// public readonly bool Intersects(BoundingCapsule2D capsule) { return Intersects(capsule, out _, out _); } /// /// Tests if this line segment intersects with an oriented bounding box and computes the parametric distances to the intersection points. /// /// The oriented bounding box to test against. /// /// When this method returns , contains the parametric distance along this segment /// to the entry 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 parametric distance along this segment /// to the exit intersection point, in the range [0, 1] where 0 represents the start and 1 represents the end. /// When this method returns , contains . /// /// /// if the segment intersects the box; otherwise, . /// /// /// For degenerate segments (zero length), returns with tMin = tMax = 0 /// if the start point is inside the box. /// public readonly bool Intersects(OrientedBoundingBox2D obb, out float? tMin, out float? tMax) { // C. Ericson, Real-Time Collision Detection, Morgan Kaufmann, 2005 // Parametric intersection of a segment with an oriented box // Derived from Section 5.3.3 "Intersecting Ray or Segment Against Box" Vector2 d = Direction; float dd = Vector2.Dot(d, d); Vector2 diff = Start - obb.Center; // Check for degenerate segment (zero length) if (dd <= Collision2D.Epsilon * Collision2D.Epsilon) { // Segment is degenerate, treat it as a point float x = Vector2.Dot(diff, obb.AxisX); float y = Vector2.Dot(diff, obb.AxisY); bool inside = MathF.Abs(x) <= obb.HalfExtents.X && MathF.Abs(y) <= obb.HalfExtents.Y; if (inside) { tMin = tMax = 0.0f; return true; } tMin = tMax = null; return false; } // Transform segment start and direction into OBB local space Vector2 localOrigin = new Vector2( Vector2.Dot(diff, obb.AxisX), Vector2.Dot(diff, obb.AxisY) ); Vector2 localDirection = new Vector2( Vector2.Dot(d, obb.AxisX), Vector2.Dot(d, obb.AxisY) ); // Intersect the parametric line: // P(T) = localOrigin + t * localDirection // with local AABB [-halfExtent, +halfExtent] if (!Collision2D.ClipLineToAabb(localOrigin, localDirection, -obb.HalfExtents, obb.HalfExtents, 0.0f, 1.0f, out float enter, out float exit)) { tMin = tMax = null; return false; } tMin = enter; tMax = exit; return true; } /// /// Tests if this line segment intersects with an oriented bounding box. /// /// The oriented bounding box to test against. /// /// if the segment intersects the box; otherwise, . /// public readonly bool Intersects(OrientedBoundingBox2D obb) { return Intersects(obb, out _, out _); } /// /// Tests if this line segment intersects with a polygon and computes the parametric distances to the intersection points. /// /// The polygon to test against. /// /// When this method returns , contains the parametric distance along this segment /// to the entry 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 parametric distance along this segment /// to the exit 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 intersection point corresponding to . /// When this method returns , contains . /// /// /// if the segment intersects the polygon; otherwise, . /// /// /// For degenerate segments (zero length), returns with tMin = tMax = 0 /// if the start point is inside the polygon. /// public readonly bool Intersects(BoundingPolygon2D polygon, out float? tMin, out float? tMax, out Vector2? point) { Vector2 d = Direction; float dd = Vector2.Dot(d, d); // Check for degenerate segment if (dd <= Collision2D.Epsilon * Collision2D.Epsilon) { // Segment is degenerate, treat as point if (polygon.Contains(Start) == ContainmentType.Contains) { tMin = tMax = 0.0f; point = Start; return true; } tMin = tMax = null; point = null; return false; } // Clip the segment's parametric line against the polygon if (!Collision2D.ClipLineToConvexPolygon(Start, d, polygon.Vertices, polygon.Normals, 0.0f, 1.0f, out float t0, out float t1)) { tMin = tMax = null; point = null; return false; } tMin = t0; tMax = t1; point = Start + d * t0; return true; } /// /// Tests if this line segment intersects with a polygon. /// /// The polygon to test against. /// /// if the segment intersects the polygon; otherwise, . /// public readonly bool Intersects(BoundingPolygon2D polygon) { return Intersects(polygon, out _, out _, out _); } /// public override readonly bool Equals([NotNullWhen(true)] object obj) { return obj is LineSegment2D other && Equals(other); } /// public readonly bool Equals(LineSegment2D other) { return Start.Equals(other.Start) && End.Equals(other.End); } /// public override readonly int GetHashCode() { return HashCode.Combine(Start, End); } /// public override readonly string ToString() { return $"LineSegment2D {{ Start: {Start}, End: {End}, Length: {Length:F3} }}"; } /// public static bool operator ==(LineSegment2D left, LineSegment2D right) { return left.Equals(right); } /// public static bool operator !=(LineSegment2D left, LineSegment2D right) { return !left.Equals(right); } #endregion } }