BoundingCapsule2D.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. // Copyright (c) Craftwork Games. All rights reserved.
  2. // Licensed under the MIT license.
  3. // See LICENSE file in the project root for full license information.
  4. using System;
  5. using System.Diagnostics;
  6. using System.Runtime.InteropServices;
  7. using System.Runtime.Serialization;
  8. using Microsoft.Xna.Framework;
  9. namespace MonoGame.Extended
  10. {
  11. /// <summary>
  12. /// Represents a capsule bounding volume in 2D space, formed by sweeping a circle along a line segment.
  13. /// </summary>
  14. [DataContract]
  15. [DebuggerDisplay("{DebugDisplayString,nq}")]
  16. [StructLayout(LayoutKind.Sequential)]
  17. public struct BoundingCapsule2D : IEquatable<BoundingCapsule2D>
  18. {
  19. #region Public Fields
  20. /// <summary>
  21. /// The first endpoint of the capsule's central line segment in 2D space.
  22. /// </summary>
  23. [DataMember]
  24. public Vector2 PointA;
  25. /// <summary>
  26. /// The second endpoint of the capsule's central line segment in 2D space.
  27. /// </summary>
  28. [DataMember]
  29. public Vector2 PointB;
  30. /// <summary>
  31. /// The radius of this capsule, defining the circular caps and the width of the swept region.
  32. /// </summary>
  33. [DataMember]
  34. public float Radius;
  35. #endregion
  36. #region Public Properties
  37. /// <summary>
  38. /// Gets the center of this capsule in 2D space, located at the midpoint between the two endpoints.
  39. /// </summary>
  40. public readonly Vector2 Center => (PointA + PointB) * 0.5f;
  41. /// <summary>
  42. /// Gets the length of this capsule's central line segment.
  43. /// </summary>
  44. public readonly float Length
  45. {
  46. get
  47. {
  48. return MathF.Sqrt(LengthSquared);
  49. }
  50. }
  51. /// <summary>
  52. /// Gets the squared length of this capsule's central line segment, useful for distance comparisons without square root calculations.
  53. /// </summary>
  54. public readonly float LengthSquared
  55. {
  56. get
  57. {
  58. Vector2 diff = PointB - PointA;
  59. return diff.X * diff.X + diff.Y * diff.Y;
  60. }
  61. }
  62. /// <summary>
  63. /// Gets the unit direction vector from PointA toward PointB, defining this capsule's orientation.
  64. /// </summary>
  65. /// <remarks>
  66. /// Returns <see cref="Vector2.Zero"/> when the endpoints are identical (degenerate capsule).
  67. /// </remarks>
  68. public readonly Vector2 Direction
  69. {
  70. get
  71. {
  72. Vector2 dir = PointB - PointA;
  73. float lengthSquared = dir.X * dir.X + dir.Y * dir.Y;
  74. if (lengthSquared < Collision2D.Epsilon * Collision2D.Epsilon)
  75. {
  76. return Vector2.Zero;
  77. }
  78. return dir / MathF.Sqrt(lengthSquared);
  79. }
  80. }
  81. /// <summary>
  82. /// Gets the total area enclosed by this capsule.
  83. /// </summary>
  84. /// <remarks>
  85. /// Calculated as the area of the central rectangle plus the area of the two semicircular caps,
  86. /// which together form a complete circle.
  87. /// </remarks>
  88. public readonly float Area
  89. {
  90. get
  91. {
  92. float length = Length;
  93. return length * (2.0f * Radius) + MathF.PI * Radius * Radius;
  94. }
  95. }
  96. #endregion
  97. #region Internal Properties
  98. internal string DebugDisplayString
  99. {
  100. get
  101. {
  102. return string.Concat(
  103. "PointA( ", PointA.ToString(), " ) \r\n",
  104. "PointB( ", PointB.ToString(), " ) \r\n",
  105. "Radius( ", Radius.ToString(), " )"
  106. );
  107. }
  108. }
  109. #endregion
  110. #region Public Constructors
  111. /// <summary>
  112. /// Creates a new <see cref="BoundingCapsule2D"/> with the specified endpoints and radius.
  113. /// </summary>
  114. /// <param name="pointA">The first endpoint of the capsule's central line segment in 2D space.</param>
  115. /// <param name="pointB">The second endpoint of the capsule's central line segment in 2D space.</param>
  116. /// <param name="radius">The radius of the capsule. Should be non-negative.</param>
  117. public BoundingCapsule2D(Vector2 pointA, Vector2 pointB, float radius)
  118. {
  119. PointA = pointA;
  120. PointB = pointB;
  121. Radius = radius;
  122. }
  123. #endregion
  124. #region Public Methods
  125. /// <summary>
  126. /// Creates a new <see cref="BoundingCapsule2D"/> from a center point, direction, length, and radius.
  127. /// </summary>
  128. /// <param name="center">The center point of the capsule in 2D space.</param>
  129. /// <param name="direction">
  130. /// The direction vector defining the capsule's orientation. Will be normalized automatically.
  131. /// </param>
  132. /// <param name="length">The length of the capsule's central line segment.</param>
  133. /// <param name="radius">The radius of the capsule.</param>
  134. /// <returns>
  135. /// A new <see cref="BoundingCapsule2D"/> centered at the specified point and oriented along the given direction.
  136. /// </returns>
  137. public static BoundingCapsule2D CreateFromCenterAndDirection(Vector2 center, Vector2 direction, float length, float radius)
  138. {
  139. // Check if the direction needs to be normalized and normalize it.
  140. float lengthSq = direction.LengthSquared();
  141. Vector2 normalizedDir = Vector2.Zero;
  142. if (lengthSq >= Collision2D.Epsilon * Collision2D.Epsilon)
  143. {
  144. normalizedDir = direction / MathF.Sqrt(lengthSq);
  145. }
  146. Vector2 halfExtent = normalizedDir * (length * 0.5f);
  147. return new BoundingCapsule2D(
  148. center - halfExtent,
  149. center + halfExtent,
  150. radius
  151. );
  152. }
  153. /// <summary>
  154. /// Creates a new <see cref="BoundingCapsule2D"/> from a line segment with the specified radius.
  155. /// </summary>
  156. /// <param name="segment">The line segment defining the capsule's central axis.</param>
  157. /// <param name="radius">The radius of the capsule.</param>
  158. /// <returns>
  159. /// A new <see cref="BoundingCapsule2D"/> following the line segment with circular caps at each end.
  160. /// </returns>
  161. public static BoundingCapsule2D CreateFromSegment(LineSegment2D segment, float radius)
  162. {
  163. return new BoundingCapsule2D(segment.Start, segment.End, radius);
  164. }
  165. /// <summary>
  166. /// Creates a <see cref="BoundingCapsule2D"/> that encloses two capsules.
  167. /// </summary>
  168. /// <param name="original">The first capsule to enclose.</param>
  169. /// <param name="additional">The second capsule to enclose.</param>
  170. /// <returns>
  171. /// A <see cref="BoundingCapsule2D"/> that completely contains both input capsules.
  172. /// </returns>
  173. /// <remarks>
  174. /// The merged capsule is constructed by finding the two most distant endpoints among both input capsules
  175. /// to form the new central segment, then computing the radius needed to enclose both original capsules.
  176. /// </remarks>
  177. public static BoundingCapsule2D CreateMerged(BoundingCapsule2D original, BoundingCapsule2D additional)
  178. {
  179. // C. Ericson, Real-Time Collision Detection, Morgan Kaufmann, 2005
  180. // Section 4.5 "Sphere-Swept Volumes" and Section 6.5.2 "Merging Two Spheres"
  181. // Derived capsule merge by expanding a medial segment to enclose both capsules
  182. Vector2[] points = new Vector2[4]
  183. {
  184. original.PointA,
  185. original.PointB,
  186. additional.PointA,
  187. additional.PointB
  188. };
  189. // Find the most distant points to form the new medial segment
  190. float maxDistSq = 0.0f;
  191. int pointIndex1 = 0;
  192. int pointIndex2 = 1;
  193. for (int i = 0; i < points.Length; i++)
  194. {
  195. for (int j = i + 1; j < points.Length; j++)
  196. {
  197. float distSq = Vector2.DistanceSquared(points[i], points[j]);
  198. if (distSq > maxDistSq)
  199. {
  200. maxDistSq = distSq;
  201. pointIndex1 = i;
  202. pointIndex2 = j;
  203. }
  204. }
  205. }
  206. Vector2 newPointA = points[pointIndex1];
  207. Vector2 newPointB = points[pointIndex2];
  208. // Compute the maximum radius needed to contain both capsules
  209. // For each endpoint of the original capsules that isn't part of the new medial segment,
  210. // compute the perpendicular distance to the new medial segment and add the capsule radius
  211. float newRadius = MathF.Max(original.Radius, additional.Radius);
  212. LineSegment2D newSegment = new LineSegment2D(newPointA, newPointB);
  213. float[] radii = new float[]
  214. {
  215. original.Radius,
  216. original.Radius,
  217. additional.Radius,
  218. additional.Radius
  219. };
  220. for (int i = 0; i < points.Length; i++)
  221. {
  222. float distToSegment = newSegment.DistanceToPoint(points[i]);
  223. float requiredRadius = distToSegment + radii[i];
  224. newRadius = MathF.Max(newRadius, requiredRadius);
  225. }
  226. return new BoundingCapsule2D(newPointA, newPointB, newRadius);
  227. }
  228. /// <summary>
  229. /// Tests whether a point lies inside this capsule or on its boundary.
  230. /// </summary>
  231. /// <param name="point">The point to test in 2D space.</param>
  232. /// <returns>
  233. /// <see cref="ContainmentType.Contains"/> if the point is inside or on the boundary;
  234. /// otherwise, <see cref="ContainmentType.Disjoint"/> if the point is outside.
  235. /// </returns>
  236. public readonly ContainmentType Contains(Vector2 point)
  237. {
  238. float rr = Radius * Radius;
  239. float d2 = Collision2D.DistanceSquaredPointSegment(point, PointA, PointB, out _, out _);
  240. if (d2 <= rr)
  241. {
  242. return ContainmentType.Contains;
  243. }
  244. return ContainmentType.Disjoint;
  245. }
  246. /// <summary>
  247. /// Tests whether this capsule contains, intersects, or is separate from an axis-aligned bounding box.
  248. /// </summary>
  249. /// <param name="aabb">The circle to test against.</param>
  250. /// <returns>
  251. /// <see cref="ContainmentType.Contains"/> if the axis-aligned bounding box is completely inside this capsule;
  252. /// <see cref="ContainmentType.Intersects"/> if they partially overlap;
  253. /// or <see cref="ContainmentType.Disjoint"/> if they do not touch.
  254. /// </returns>
  255. public readonly ContainmentType Contains(BoundingBox2D aabb)
  256. {
  257. return Collision2D.ContainsCapsuleAabb(PointA, PointB, Radius, aabb.Min, aabb.Max);
  258. }
  259. /// <summary>
  260. /// Tests whether this capsule contains, intersects, or is separate from a circle.
  261. /// </summary>
  262. /// <param name="circle">The circle to test against.</param>
  263. /// <returns>
  264. /// <see cref="ContainmentType.Contains"/> if the circle is completely inside this capsule;
  265. /// <see cref="ContainmentType.Intersects"/> if they partially overlap;
  266. /// or <see cref="ContainmentType.Disjoint"/> if they do not touch.
  267. /// </returns>
  268. public readonly ContainmentType Contains(BoundingCircle2D circle)
  269. {
  270. return Collision2D.ContainsCapsuleCircle(PointA, PointB, Radius, circle.Center, circle.Radius);
  271. }
  272. /// <summary>
  273. /// Tests whether this capsule contains, intersects, or is separate from an oriented bounding box.
  274. /// </summary>
  275. /// <param name="obb">The oriented bounding box to test against.</param>
  276. /// <returns>
  277. /// <see cref="ContainmentType.Contains"/> if the oriented bounding box is completely inside this capsule;
  278. /// <see cref="ContainmentType.Intersects"/> if they partially overlap;
  279. /// or <see cref="ContainmentType.Disjoint"/> if they do not touch.
  280. /// </returns>
  281. public readonly ContainmentType Contains(OrientedBoundingBox2D obb)
  282. {
  283. return Collision2D.ContainsCapsuleObb(PointA, PointB, Radius, obb.Center, obb.AxisX, obb.AxisY, obb.HalfExtents);
  284. }
  285. /// <summary>
  286. /// Tests whether this capsule contains, intersects, or is separate from another capsule.
  287. /// </summary>
  288. /// <param name="other">The other capsule to test against.</param>
  289. /// <returns>
  290. /// <see cref="ContainmentType.Contains"/> if the other capsule is completely inside this one;
  291. /// <see cref="ContainmentType.Intersects"/> if they partially overlap;
  292. /// or <see cref="ContainmentType.Disjoint"/> if they do not touch.
  293. /// </returns>
  294. public readonly ContainmentType Contains(BoundingCapsule2D other)
  295. {
  296. return Collision2D.ContainsCapsuleCapsule(PointA, PointB, Radius, other.PointA, other.PointB, other.Radius);
  297. }
  298. /// <summary>
  299. /// Tests whether this capsule contains, intersects, or is separate from a polygon.
  300. /// </summary>
  301. /// <param name="polygon">The circle to test against.</param>
  302. /// <returns>
  303. /// <see cref="ContainmentType.Contains"/> if the polygon is completely inside this capsule;
  304. /// <see cref="ContainmentType.Intersects"/> if they partially overlap;
  305. /// or <see cref="ContainmentType.Disjoint"/> if they do not touch.
  306. /// </returns>
  307. public readonly ContainmentType Contains(BoundingPolygon2D polygon)
  308. {
  309. return Collision2D.ContainsCapsuleConvexPolygon(PointA, PointB, Radius, polygon.Vertices, polygon.Normals);
  310. }
  311. /// <summary>
  312. /// Tests whether this capsule intersects with another capsule.
  313. /// </summary>
  314. /// <param name="other">The other capsule to test against.</param>
  315. /// <returns>
  316. /// <see langword="true"/> if the capsules overlap or touch; otherwise, <see langword="false"/>.
  317. /// </returns>
  318. public readonly bool Intersects(BoundingCapsule2D other)
  319. {
  320. return Collision2D.IntersectsCapsuleCapsule(PointA, PointB, Radius, other.PointA, other.PointB, other.Radius);
  321. }
  322. /// <summary>
  323. /// Tests whether this capsule intersects with a circle.
  324. /// </summary>
  325. /// <param name="circle">The circle to test against.</param>
  326. /// <returns>
  327. /// <see langword="true"/> if the capsule and circle overlap or touch; otherwise, <see langword="false"/>.
  328. /// </returns>
  329. public readonly bool Intersects(BoundingCircle2D circle)
  330. {
  331. return Collision2D.IntersectsCircleCapsule(circle.Center, circle.Radius, PointA, PointB, Radius);
  332. }
  333. /// <summary>
  334. /// Tests whether this capsule intersects with an axis-aligned bounding box.
  335. /// </summary>
  336. /// <param name="box">The bounding box to test against.</param>
  337. /// <returns>
  338. /// <see langword="true"/> if the capsule and bounding box overlap or touch; otherwise, <see langword="false"/>.
  339. /// </returns>
  340. public readonly bool Intersects(BoundingBox2D box)
  341. {
  342. return Collision2D.IntersectsAabbCapsule(box.Min, box.Max, PointA, PointB, Radius);
  343. }
  344. /// <summary>
  345. /// Tests whether this capsule intersects with an oriented bounding box.
  346. /// </summary>
  347. /// <param name="obb">The oriented bounding box to test against.</param>
  348. /// <returns>
  349. /// <see langword="true"/> if the capsule and box overlap or touch; otherwise, <see langword="false"/>.
  350. /// </returns>
  351. public readonly bool Intersects(OrientedBoundingBox2D obb)
  352. {
  353. return Collision2D.IntersectsObbCapsule(obb.Center, obb.AxisX, obb.AxisY, obb.HalfExtents, PointA, PointB, Radius);
  354. }
  355. /// <summary>
  356. /// Tests whether this capsule intersects with a polygon.
  357. /// </summary>
  358. /// <param name="polygon">The polygon to test against.</param>
  359. /// <returns>
  360. /// <see langword="true"/> if the capsule and polygon overlap or touch; otherwise, <see langword="false"/>.
  361. /// </returns>
  362. public readonly bool Intersects(BoundingPolygon2D polygon)
  363. {
  364. return Collision2D.IntersectsCapsuleConvexPolygon(PointA, PointB, Radius, polygon.Vertices, polygon.Normals);
  365. }
  366. /// <summary>
  367. /// Applies a matrix transformation to this capsule and creates a new transformed capsule.
  368. /// </summary>
  369. /// <param name="matrix">The transformation matrix to apply.</param>
  370. /// <returns>
  371. /// A new <see cref="BoundingCapsule2D"/> with endpoints transformed by the matrix and the radius
  372. /// scaled by the maximum scale factor to ensure the transformed shape remains enclosed.
  373. /// </returns>
  374. /// <remarks>
  375. /// The radius is scaled by the maximum of the X and Y scale components of the transformation matrix.
  376. /// This ensures that the resulting capsule fully encloses the transformed original capsule, even if
  377. /// the transformation includes non-uniform scaling or rotation.
  378. /// </remarks>
  379. public readonly BoundingCapsule2D Transform(Matrix matrix)
  380. {
  381. Vector2 transformedA = Vector2.Transform(PointA, matrix);
  382. Vector2 transformedB = Vector2.Transform(PointB, matrix);
  383. // Scale radius by maximum scale component
  384. float scaleX = MathF.Sqrt(matrix.M11 * matrix.M11 + matrix.M12 * matrix.M12);
  385. float scaleY = MathF.Sqrt(matrix.M21 * matrix.M21 + matrix.M22 * matrix.M22);
  386. float transformedRadius = Radius * MathF.Max(scaleX, scaleY);
  387. return new BoundingCapsule2D(transformedA, transformedB, transformedRadius);
  388. }
  389. /// <summary>
  390. /// Creates a new <see cref="BoundingCapsule2D"/> by translating this capsule by the specified offset.
  391. /// </summary>
  392. /// <param name="translation">The offset to translate the capsule by in 2D space.</param>
  393. /// <returns>
  394. /// A new <see cref="BoundingCapsule2D"/> at the translated position with the same radius and orientation.
  395. /// </returns>
  396. public readonly BoundingCapsule2D Translate(Vector2 translation)
  397. {
  398. return new BoundingCapsule2D(
  399. PointA + translation,
  400. PointB + translation,
  401. Radius
  402. );
  403. }
  404. /// <summary>
  405. /// Deconstructs this capsule into its component values.
  406. /// </summary>
  407. /// <param name="pointA">
  408. /// When this method returns, contains the first endpoint of this capsule's central line segment in 2D space.
  409. /// </param>
  410. /// <param name="pointB">
  411. /// When this method returns, contains the second endpoint of this capsule's central line segment in 2D space.
  412. /// </param>
  413. /// <param name="radius">
  414. /// When this method returns, contains the radius of this capsule.
  415. /// </param>
  416. public readonly void Deconstruct(out Vector2 pointA, out Vector2 pointB, out float radius)
  417. {
  418. pointA = PointA;
  419. pointB = PointB;
  420. radius = Radius;
  421. }
  422. /// <inheritdoc/>
  423. public readonly bool Equals(BoundingCapsule2D other)
  424. {
  425. return PointA.Equals(other.PointA)
  426. && PointB.Equals(other.PointB)
  427. && Radius.Equals(other.Radius);
  428. }
  429. /// <inheritdoc/>
  430. public override readonly bool Equals(object obj)
  431. {
  432. return obj is BoundingCapsule2D other && Equals(other);
  433. }
  434. /// <inheritdoc/>
  435. public override readonly int GetHashCode()
  436. {
  437. return PointA.GetHashCode() ^
  438. PointB.GetHashCode() ^
  439. Radius.GetHashCode();
  440. }
  441. /// <inheritdoc/>
  442. public override readonly string ToString()
  443. {
  444. return $"{{PointA:{PointA} PointB:{PointB} Radius:{Radius:F2}}}";
  445. }
  446. /// <summary/>
  447. public static bool operator ==(BoundingCapsule2D left, BoundingCapsule2D right)
  448. {
  449. return left.Equals(right);
  450. }
  451. /// <summary/>
  452. public static bool operator !=(BoundingCapsule2D left, BoundingCapsule2D right)
  453. {
  454. return !left.Equals(right);
  455. }
  456. #endregion
  457. }
  458. }