// 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
}
}