// 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.InteropServices; using System.Runtime.Serialization; using Microsoft.Xna.Framework; namespace MonoGame.Extended { /// /// Represents a capsule bounding volume in 2D space, formed by sweeping a circle along a line segment. /// [DataContract] [DebuggerDisplay("{DebugDisplayString,nq}")] [StructLayout(LayoutKind.Sequential)] public struct BoundingCapsule2D : IEquatable { #region Public Fields /// /// The first endpoint of the capsule's central line segment in 2D space. /// [DataMember] public Vector2 PointA; /// /// The second endpoint of the capsule's central line segment in 2D space. /// [DataMember] public Vector2 PointB; /// /// The radius of this capsule, defining the circular caps and the width of the swept region. /// [DataMember] public float Radius; #endregion #region Public Properties /// /// Gets the center of this capsule in 2D space, located at the midpoint between the two endpoints. /// public readonly Vector2 Center => (PointA + PointB) * 0.5f; /// /// Gets the length of this capsule's central line segment. /// public readonly float Length { get { return MathF.Sqrt(LengthSquared); } } /// /// Gets the squared length of this capsule's central line segment, useful for distance comparisons without square root calculations. /// public readonly float LengthSquared { get { Vector2 diff = PointB - PointA; return diff.X * diff.X + diff.Y * diff.Y; } } /// /// Gets the unit direction vector from PointA toward PointB, defining this capsule's orientation. /// /// /// Returns when the endpoints are identical (degenerate capsule). /// public readonly Vector2 Direction { get { Vector2 dir = PointB - PointA; float lengthSquared = dir.X * dir.X + dir.Y * dir.Y; if (lengthSquared < Collision2D.Epsilon * Collision2D.Epsilon) { return Vector2.Zero; } return dir / MathF.Sqrt(lengthSquared); } } /// /// Gets the total area enclosed by this capsule. /// /// /// Calculated as the area of the central rectangle plus the area of the two semicircular caps, /// which together form a complete circle. /// public readonly float Area { get { float length = Length; return length * (2.0f * Radius) + MathF.PI * Radius * Radius; } } #endregion #region Internal Properties internal string DebugDisplayString { get { return string.Concat( "PointA( ", PointA.ToString(), " ) \r\n", "PointB( ", PointB.ToString(), " ) \r\n", "Radius( ", Radius.ToString(), " )" ); } } #endregion #region Public Constructors /// /// Creates a new with the specified endpoints and radius. /// /// The first endpoint of the capsule's central line segment in 2D space. /// The second endpoint of the capsule's central line segment in 2D space. /// The radius of the capsule. Should be non-negative. public BoundingCapsule2D(Vector2 pointA, Vector2 pointB, float radius) { PointA = pointA; PointB = pointB; Radius = radius; } #endregion #region Public Methods /// /// Creates a new from a center point, direction, length, and radius. /// /// The center point of the capsule in 2D space. /// /// The direction vector defining the capsule's orientation. Will be normalized automatically. /// /// The length of the capsule's central line segment. /// The radius of the capsule. /// /// A new centered at the specified point and oriented along the given direction. /// public static BoundingCapsule2D CreateFromCenterAndDirection(Vector2 center, Vector2 direction, float length, float radius) { // Check if the direction needs to be normalized and normalize it. float lengthSq = direction.LengthSquared(); Vector2 normalizedDir = Vector2.Zero; if (lengthSq >= Collision2D.Epsilon * Collision2D.Epsilon) { normalizedDir = direction / MathF.Sqrt(lengthSq); } Vector2 halfExtent = normalizedDir * (length * 0.5f); return new BoundingCapsule2D( center - halfExtent, center + halfExtent, radius ); } /// /// Creates a new from a line segment with the specified radius. /// /// The line segment defining the capsule's central axis. /// The radius of the capsule. /// /// A new following the line segment with circular caps at each end. /// public static BoundingCapsule2D CreateFromSegment(LineSegment2D segment, float radius) { return new BoundingCapsule2D(segment.Start, segment.End, radius); } /// /// Creates a that encloses two capsules. /// /// The first capsule to enclose. /// The second capsule to enclose. /// /// A that completely contains both input capsules. /// /// /// The merged capsule is constructed by finding the two most distant endpoints among both input capsules /// to form the new central segment, then computing the radius needed to enclose both original capsules. /// public static BoundingCapsule2D CreateMerged(BoundingCapsule2D original, BoundingCapsule2D additional) { // C. Ericson, Real-Time Collision Detection, Morgan Kaufmann, 2005 // Section 4.5 "Sphere-Swept Volumes" and Section 6.5.2 "Merging Two Spheres" // Derived capsule merge by expanding a medial segment to enclose both capsules Vector2[] points = new Vector2[4] { original.PointA, original.PointB, additional.PointA, additional.PointB }; // Find the most distant points to form the new medial segment float maxDistSq = 0.0f; int pointIndex1 = 0; int pointIndex2 = 1; for (int i = 0; i < points.Length; i++) { for (int j = i + 1; j < points.Length; j++) { float distSq = Vector2.DistanceSquared(points[i], points[j]); if (distSq > maxDistSq) { maxDistSq = distSq; pointIndex1 = i; pointIndex2 = j; } } } Vector2 newPointA = points[pointIndex1]; Vector2 newPointB = points[pointIndex2]; // Compute the maximum radius needed to contain both capsules // For each endpoint of the original capsules that isn't part of the new medial segment, // compute the perpendicular distance to the new medial segment and add the capsule radius float newRadius = MathF.Max(original.Radius, additional.Radius); LineSegment2D newSegment = new LineSegment2D(newPointA, newPointB); float[] radii = new float[] { original.Radius, original.Radius, additional.Radius, additional.Radius }; for (int i = 0; i < points.Length; i++) { float distToSegment = newSegment.DistanceToPoint(points[i]); float requiredRadius = distToSegment + radii[i]; newRadius = MathF.Max(newRadius, requiredRadius); } return new BoundingCapsule2D(newPointA, newPointB, newRadius); } /// /// Tests whether a point lies inside this capsule or on its boundary. /// /// The point to test in 2D space. /// /// if the point is inside or on the boundary; /// otherwise, if the point is outside. /// public readonly ContainmentType Contains(Vector2 point) { float rr = Radius * Radius; float d2 = Collision2D.DistanceSquaredPointSegment(point, PointA, PointB, out _, out _); if (d2 <= rr) { return ContainmentType.Contains; } return ContainmentType.Disjoint; } /// /// Tests whether this capsule contains, intersects, or is separate from an axis-aligned bounding box. /// /// The circle to test against. /// /// if the axis-aligned bounding box is completely inside this capsule; /// if they partially overlap; /// or if they do not touch. /// public readonly ContainmentType Contains(BoundingBox2D aabb) { return Collision2D.ContainsCapsuleAabb(PointA, PointB, Radius, aabb.Min, aabb.Max); } /// /// Tests whether this capsule contains, intersects, or is separate from a circle. /// /// The circle to test against. /// /// if the circle is completely inside this capsule; /// if they partially overlap; /// or if they do not touch. /// public readonly ContainmentType Contains(BoundingCircle2D circle) { return Collision2D.ContainsCapsuleCircle(PointA, PointB, Radius, circle.Center, circle.Radius); } /// /// Tests whether this capsule contains, intersects, or is separate from an oriented bounding box. /// /// The oriented bounding box to test against. /// /// if the oriented bounding box is completely inside this capsule; /// if they partially overlap; /// or if they do not touch. /// public readonly ContainmentType Contains(OrientedBoundingBox2D obb) { return Collision2D.ContainsCapsuleObb(PointA, PointB, Radius, obb.Center, obb.AxisX, obb.AxisY, obb.HalfExtents); } /// /// Tests whether this capsule contains, intersects, or is separate from another capsule. /// /// The other capsule to test against. /// /// if the other capsule is completely inside this one; /// if they partially overlap; /// or if they do not touch. /// public readonly ContainmentType Contains(BoundingCapsule2D other) { return Collision2D.ContainsCapsuleCapsule(PointA, PointB, Radius, other.PointA, other.PointB, other.Radius); } /// /// Tests whether this capsule contains, intersects, or is separate from a polygon. /// /// The circle to test against. /// /// if the polygon is completely inside this capsule; /// if they partially overlap; /// or if they do not touch. /// public readonly ContainmentType Contains(BoundingPolygon2D polygon) { return Collision2D.ContainsCapsuleConvexPolygon(PointA, PointB, Radius, polygon.Vertices, polygon.Normals); } /// /// Tests whether this capsule intersects with another capsule. /// /// The other capsule to test against. /// /// if the capsules overlap or touch; otherwise, . /// public readonly bool Intersects(BoundingCapsule2D other) { return Collision2D.IntersectsCapsuleCapsule(PointA, PointB, Radius, other.PointA, other.PointB, other.Radius); } /// /// Tests whether this capsule intersects with a circle. /// /// The circle to test against. /// /// if the capsule and circle overlap or touch; otherwise, . /// public readonly bool Intersects(BoundingCircle2D circle) { return Collision2D.IntersectsCircleCapsule(circle.Center, circle.Radius, PointA, PointB, Radius); } /// /// Tests whether this capsule intersects with an axis-aligned bounding box. /// /// The bounding box to test against. /// /// if the capsule and bounding box overlap or touch; otherwise, . /// public readonly bool Intersects(BoundingBox2D box) { return Collision2D.IntersectsAabbCapsule(box.Min, box.Max, PointA, PointB, Radius); } /// /// Tests whether this capsule intersects with an oriented bounding box. /// /// The oriented bounding box to test against. /// /// if the capsule and box overlap or touch; otherwise, . /// public readonly bool Intersects(OrientedBoundingBox2D obb) { return Collision2D.IntersectsObbCapsule(obb.Center, obb.AxisX, obb.AxisY, obb.HalfExtents, PointA, PointB, Radius); } /// /// Tests whether this capsule intersects with a polygon. /// /// The polygon to test against. /// /// if the capsule and polygon overlap or touch; otherwise, . /// public readonly bool Intersects(BoundingPolygon2D polygon) { return Collision2D.IntersectsCapsuleConvexPolygon(PointA, PointB, Radius, polygon.Vertices, polygon.Normals); } /// /// Applies a matrix transformation to this capsule and creates a new transformed capsule. /// /// The transformation matrix to apply. /// /// A new with endpoints transformed by the matrix and the radius /// scaled by the maximum scale factor to ensure the transformed shape remains enclosed. /// /// /// The radius is scaled by the maximum of the X and Y scale components of the transformation matrix. /// This ensures that the resulting capsule fully encloses the transformed original capsule, even if /// the transformation includes non-uniform scaling or rotation. /// public readonly BoundingCapsule2D Transform(Matrix matrix) { Vector2 transformedA = Vector2.Transform(PointA, matrix); Vector2 transformedB = Vector2.Transform(PointB, matrix); // Scale radius by maximum scale component float scaleX = MathF.Sqrt(matrix.M11 * matrix.M11 + matrix.M12 * matrix.M12); float scaleY = MathF.Sqrt(matrix.M21 * matrix.M21 + matrix.M22 * matrix.M22); float transformedRadius = Radius * MathF.Max(scaleX, scaleY); return new BoundingCapsule2D(transformedA, transformedB, transformedRadius); } /// /// Creates a new by translating this capsule by the specified offset. /// /// The offset to translate the capsule by in 2D space. /// /// A new at the translated position with the same radius and orientation. /// public readonly BoundingCapsule2D Translate(Vector2 translation) { return new BoundingCapsule2D( PointA + translation, PointB + translation, Radius ); } /// /// Deconstructs this capsule into its component values. /// /// /// When this method returns, contains the first endpoint of this capsule's central line segment in 2D space. /// /// /// When this method returns, contains the second endpoint of this capsule's central line segment in 2D space. /// /// /// When this method returns, contains the radius of this capsule. /// public readonly void Deconstruct(out Vector2 pointA, out Vector2 pointB, out float radius) { pointA = PointA; pointB = PointB; radius = Radius; } /// public readonly bool Equals(BoundingCapsule2D other) { return PointA.Equals(other.PointA) && PointB.Equals(other.PointB) && Radius.Equals(other.Radius); } /// public override readonly bool Equals(object obj) { return obj is BoundingCapsule2D other && Equals(other); } /// public override readonly int GetHashCode() { return PointA.GetHashCode() ^ PointB.GetHashCode() ^ Radius.GetHashCode(); } /// public override readonly string ToString() { return $"{{PointA:{PointA} PointB:{PointB} Radius:{Radius:F2}}}"; } /// public static bool operator ==(BoundingCapsule2D left, BoundingCapsule2D right) { return left.Equals(right); } /// public static bool operator !=(BoundingCapsule2D left, BoundingCapsule2D right) { return !left.Equals(right); } #endregion } }