BoundingCircle2D.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  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.Serialization;
  7. using Microsoft.Xna.Framework;
  8. namespace MonoGame.Extended
  9. {
  10. /// <summary>
  11. /// Represents a circular bounding volume in 2D space, defined by a center point and radius.
  12. /// </summary>
  13. [DataContract]
  14. [DebuggerDisplay("{DebugDisplayString,nq}")]
  15. public struct BoundingCircle2D : IEquatable<BoundingCircle2D>
  16. {
  17. #region Public Fields
  18. /// <summary>
  19. /// The center position of this circle in 2D space.
  20. /// </summary>
  21. [DataMember]
  22. public Vector2 Center;
  23. /// <summary>
  24. /// The radius of this circle, representing the distance from the center to any point on the edge.
  25. /// </summary>
  26. [DataMember]
  27. public float Radius;
  28. #endregion
  29. #region Public Properties
  30. /// <summary>
  31. /// Gets the squared radius of this circle, useful for distance comparisons without square root calculations.
  32. /// </summary>
  33. public readonly float RadiusSquared => Radius * Radius;
  34. /// <summary>
  35. /// Gets the diameter of this circle, the distance across through the center.
  36. /// </summary>
  37. public readonly float Diameter => Radius * 2.0f;
  38. /// <summary>
  39. /// Gets the area enclosed by this circle.
  40. /// </summary>
  41. public readonly float Area => MathF.PI * Radius * Radius;
  42. #endregion
  43. #region Internal Properties
  44. internal string DebugDisplayString
  45. {
  46. get
  47. {
  48. return string.Concat(
  49. "Center( ", Center.ToString(), " ) \r\n",
  50. "Radius( ", Radius.ToString(), " )"
  51. );
  52. }
  53. }
  54. #endregion
  55. #region Public Constructors
  56. /// <summary>
  57. /// Creates a new <see cref="BoundingCircle2D"/> with the specified center and radius.
  58. /// </summary>
  59. /// <param name="center">The center position of the circle in 2D space.</param>
  60. /// <param name="radius">The radius of the circle in. Should be non-negative.</param>
  61. public BoundingCircle2D(Vector2 center, float radius)
  62. {
  63. Center = center;
  64. Radius = radius;
  65. }
  66. #endregion
  67. #region Public Methods
  68. /// <summary>
  69. /// Creates a <see cref="BoundingCircle2D"/> that encloses all specified points.
  70. /// </summary>
  71. /// <param name="points">The array of points to enclose within the circle.</param>
  72. /// <returns>
  73. /// A <see cref="BoundingCircle2D"/> that contains all the specified points.
  74. /// The circle is computed using an approximation algorithm and may not be the absolute minimal bounding circle.
  75. /// </returns>
  76. /// <exception cref="ArgumentNullException">
  77. /// Thrown when <paramref name="points"/> is <see langword="null"/>.
  78. /// </exception>
  79. /// <exception cref="ArgumentException">
  80. /// Thrown when <paramref name="points"/> is empty.
  81. /// </exception>
  82. /// <remarks>
  83. /// Uses Ritter's algorithm for efficient approximate bounding circle computation.
  84. /// For a single point, returns a circle with radius <c>0</c> centered at that point.
  85. /// </remarks>
  86. public static BoundingCircle2D CreateFromPoints(Vector2[] points)
  87. {
  88. // C. Ericson, Real-Time Collision Detection, Morgan Kaufmann, 2005
  89. // Section 4.3.2 "Computing a Bounding Sphere"
  90. // Implements Ritter's algorithm for approximate bounding sphere computation
  91. if (points == null)
  92. {
  93. throw new ArgumentNullException(nameof(points));
  94. }
  95. if (points.Length == 0)
  96. {
  97. throw new ArgumentException("Cannot create bounding circle from empty point array", nameof(points));
  98. }
  99. if (points.Length == 1)
  100. {
  101. return new BoundingCircle2D(points[0], 0.0f);
  102. }
  103. // Find the most separate point pair along the principle axis
  104. int minX = 0;
  105. int maxX = 0;
  106. int minY = 0;
  107. int maxY = 0;
  108. for (int i = 1; i < points.Length; i++)
  109. {
  110. if (points[i].X < points[minX].X) minX = i;
  111. if (points[i].X > points[maxX].X) maxX = i;
  112. if (points[i].Y < points[minY].Y) minY = i;
  113. if (points[i].Y > points[maxY].Y) maxY = i;
  114. }
  115. // Compute squared distances for the two pairs of points
  116. Vector2 dx = points[maxX] - points[minX];
  117. Vector2 dy = points[maxY] - points[minY];
  118. float distSqX = dx.X * dx.X + dx.Y * dx.Y;
  119. float distSqY = dy.X * dy.X + dy.Y * dy.Y;
  120. // Pick the pair of points most distant
  121. int min = minX;
  122. int max = maxX;
  123. if (distSqY > distSqX)
  124. {
  125. max = maxY;
  126. min = minY;
  127. }
  128. Vector2 center = (points[min] + points[max]) * 0.5f;
  129. Vector2 diff = points[max] - center;
  130. float radius = MathF.Sqrt(diff.X * diff.X + diff.Y * diff.Y);
  131. // Grow sphere to include all points
  132. for (int i = 0; i < points.Length; i++)
  133. {
  134. Vector2 point = points[i];
  135. Vector2 d = point - center;
  136. float distSq = d.X * d.X + d.Y * d.Y;
  137. // Only update sphere if point is outside it
  138. if (distSq > radius * radius)
  139. {
  140. float dist = MathF.Sqrt(distSq);
  141. float newRadius = (radius + dist) * 0.5f;
  142. float k = (newRadius - radius) / dist;
  143. radius = newRadius;
  144. center += d * k;
  145. }
  146. }
  147. return new BoundingCircle2D(center, radius);
  148. }
  149. /// <summary>
  150. /// Creates a <see cref="BoundingCircle2D"/> that completely encloses the specified bounding box.
  151. /// </summary>
  152. /// <param name="box">The bounding box to enclose within a circle.</param>
  153. /// <returns>
  154. /// A <see cref="BoundingCircle2D"/> centered at the bounding box's center with radius equal to half the diagonal of the box.
  155. /// </returns>
  156. public static BoundingCircle2D CreateFromBoundingBox2D(BoundingBox2D box)
  157. {
  158. Vector2 center = box.Center;
  159. Vector2 halfExtents = box.HalfExtents;
  160. float radius = halfExtents.Length();
  161. return new BoundingCircle2D(center, radius);
  162. }
  163. /// <summary>
  164. /// Creates a <see cref="BoundingCircle2D"/> that completely encloses the specified capsule.
  165. /// </summary>
  166. /// <param name="capsule">The capsule to enclose within a circle.</param>
  167. /// <returns>
  168. /// A <see cref="BoundingCircle2D"/> centered at the capsule's center with radius sufficient to contain both endpoint caps.
  169. /// </returns>
  170. /// <remarks>
  171. /// The radius is computed as half the capsule's length plus the capsule's radius, ensuring that
  172. /// the circle reaches the farthest points on both circular caps.
  173. /// </remarks>
  174. public static BoundingCircle2D CreateFromBoundingCapsule2D(BoundingCapsule2D capsule)
  175. {
  176. // The bounding circle center is at the capsule's midpoint
  177. Vector2 center = capsule.Center;
  178. // The radius needs to reach from the center to the farthest point on either cap
  179. // This is half the capsule length plus the cap radius
  180. float radius = (capsule.Length * 0.5f) + capsule.Radius;
  181. return new BoundingCircle2D(center, radius);
  182. }
  183. /// <summary>
  184. /// Creates a <see cref="BoundingCircle2D"/> that encloses two bounding circles.
  185. /// </summary>
  186. /// <param name="original">The first bounding circle to enclose.</param>
  187. /// <param name="additional">The second bounding circle to enclose.</param>
  188. /// <returns>
  189. /// A <see cref="BoundingCircle2D"/> with the smallest radius that completely contains both input circles.
  190. /// </returns>
  191. /// <remarks>
  192. /// If one circle completely contains the other, the larger circle is returned.
  193. /// Otherwise, computes the optimal merged circle that tightly encloses both.
  194. /// </remarks>
  195. public static BoundingCircle2D CreateMerged(BoundingCircle2D original, BoundingCircle2D additional)
  196. {
  197. // C. Ericson, Real-Time Collision Detection, Morgan Kaufmann, 2005
  198. // Section 6.5.2 "Merging Two Spheres" (2D circle adaptation)
  199. // Calculate distance between centers
  200. Vector2 centerDiff = additional.Center - original.Center;
  201. float distSq = centerDiff.X * centerDiff.X + centerDiff.Y * centerDiff.Y;
  202. float dist = MathF.Sqrt(distSq);
  203. // Calculate radius difference
  204. float radiusDiff = MathF.Abs(additional.Radius - original.Radius);
  205. // Check if one circle contain the other
  206. if (radiusDiff >= dist)
  207. {
  208. // One circle fully enclosed by the other
  209. // return the larger circle
  210. if (additional.Radius >= original.Radius)
  211. return additional;
  212. else
  213. return original;
  214. }
  215. // Circles are partially overlapping or disjoint
  216. // Calculate the radius of the new circle as half the maximum distance between surfaces
  217. float radius = (dist + original.Radius + additional.Radius) * 0.5f;
  218. // Calculate the center by adjusting original center toward additional center
  219. Vector2 center = original.Center;
  220. if (dist > Collision2D.Epsilon)
  221. {
  222. center += ((radius - original.Radius) / dist) * centerDiff;
  223. }
  224. return new BoundingCircle2D(center, radius);
  225. }
  226. /// <summary>
  227. /// Tests whether a point lies inside this circle or on its boundary.
  228. /// </summary>
  229. /// <param name="point">The point to test in 2D space.</param>
  230. /// <returns>
  231. /// <see cref="ContainmentType.Contains"/> if the point is inside or on the boundary;
  232. /// otherwise, <see cref="ContainmentType.Disjoint"/> if the point is outside.
  233. /// </returns>
  234. public readonly ContainmentType Contains(Vector2 point)
  235. {
  236. return Collision2D.ContainsCirclePoint(point, Center, Radius);
  237. }
  238. /// <summary>
  239. /// Tests whether this circle contains, intersects, or is separate from a bounding box.
  240. /// </summary>
  241. /// <param name="box">The bounding box to test against.</param>
  242. /// <returns>
  243. /// <see cref="ContainmentType.Contains"/> if the bounding box is completely inside this circle;
  244. /// <see cref="ContainmentType.Intersects"/> if they partially overlap;
  245. /// or <see cref="ContainmentType.Disjoint"/> if they do not touch.
  246. /// </returns>
  247. public readonly ContainmentType Contains(BoundingBox2D box)
  248. {
  249. return Collision2D.ContainsCircleAabb(Center, Radius, box.Min, box.Max);
  250. }
  251. /// <summary>
  252. /// Tests whether this circle contains, intersects, or is separate from another circle.
  253. /// </summary>
  254. /// <param name="other">The other circle to test against.</param>
  255. /// <returns>
  256. /// <see cref="ContainmentType.Contains"/> if the other circle is completely inside this one;
  257. /// <see cref="ContainmentType.Intersects"/> if they partially overlap;
  258. /// or <see cref="ContainmentType.Disjoint"/> if they do not touch.
  259. /// </returns>
  260. public readonly ContainmentType Contains(BoundingCircle2D other)
  261. {
  262. return Collision2D.ContainsCircleCircle(Center, Radius, other.Center, other.Radius);
  263. }
  264. /// <summary>
  265. /// Tests whether this circle contains, intersects, or is separate from an oriented bounding box
  266. /// </summary>
  267. /// <param name="obb">The oriented bounding box to test against.</param>
  268. /// <returns>
  269. /// <see cref="ContainmentType.Contains"/> if the oriented bounding box is completely inside this circle;
  270. /// <see cref="ContainmentType.Intersects"/> if they partially overlap;
  271. /// or <see cref="ContainmentType.Disjoint"/> if they do not touch.
  272. /// </returns>
  273. public readonly ContainmentType Contains(OrientedBoundingBox2D obb)
  274. {
  275. return Collision2D.ContainsCircleObb(Center, Radius, obb.Center, obb.AxisX, obb.AxisY, obb.HalfExtents);
  276. }
  277. /// <summary>
  278. /// Tests whether this circle contains, intersects, or is separate from a capsule.
  279. /// </summary>
  280. /// <param name="capsule">The capsule to test against.</param>
  281. /// <returns>
  282. /// <see cref="ContainmentType.Contains"/> if the capsule is completely inside this circle;
  283. /// <see cref="ContainmentType.Intersects"/> if they partially overlap;
  284. /// or <see cref="ContainmentType.Disjoint"/> if they do not touch.
  285. /// </returns>
  286. public readonly ContainmentType Contains(BoundingCapsule2D capsule)
  287. {
  288. return Collision2D.ContainsCircleCapsule(Center, Radius, capsule.PointA, capsule.PointB, capsule.Radius);
  289. }
  290. /// <summary>
  291. /// Tests whether this circle contains, intersects, or is separate from a polygon.
  292. /// </summary>
  293. /// <param name="polygon">The capsule to test against.</param>
  294. /// <returns>
  295. /// <see cref="ContainmentType.Contains"/> if the polygon is completely inside this circle;
  296. /// <see cref="ContainmentType.Intersects"/> if they partially overlap;
  297. /// or <see cref="ContainmentType.Disjoint"/> if they do not touch.
  298. /// </returns>
  299. public readonly ContainmentType Contains(BoundingPolygon2D polygon)
  300. {
  301. return Collision2D.ContainsCircleConvexPolygon(Center, Radius, polygon.Vertices, polygon.Normals);
  302. }
  303. /// <summary>
  304. /// Tests whether this circle intersects with another circle.
  305. /// </summary>
  306. /// <param name="other">The other circle to test against.</param>
  307. /// <returns>
  308. /// <see langword="true"/> if the circles overlap or touch; otherwise, <see langword="false"/>.
  309. /// </returns>
  310. public readonly bool Intersects(BoundingCircle2D other)
  311. {
  312. return Collision2D.IntersectsCircleCircle(Center, Radius, other.Center, other.Radius);
  313. }
  314. /// <summary>
  315. /// Tests whether this circle intersects with an axis-aligned bounding box.
  316. /// </summary>
  317. /// <param name="box">The bounding box to test against.</param>
  318. /// <returns>
  319. /// <see langword="true"/> if the circle and bounding box overlap or touch; otherwise, <see langword="false"/>.
  320. /// </returns>
  321. public readonly bool Intersects(BoundingBox2D box)
  322. {
  323. return Collision2D.IntersectsCircleAabb(Center, Radius, box.Min, box.Max);
  324. }
  325. /// <summary>
  326. /// Tests whether this circle intersects with a capsule.
  327. /// </summary>
  328. /// <param name="capsule">The capsule to test against.</param>
  329. /// <returns>
  330. /// <see langword="true"/> if the circle and capsule overlap or touch; otherwise, <see langword="false"/>.
  331. /// </returns>
  332. public readonly bool Intersects(BoundingCapsule2D capsule)
  333. {
  334. return Collision2D.IntersectsCircleCapsule(Center, Radius, capsule.PointA, capsule.PointB, capsule.Radius);
  335. }
  336. /// <summary>
  337. /// Tests whether this circle intersects with an oriented bounding box.
  338. /// </summary>
  339. /// <param name="obb">The oriented bounding box to test against.</param>
  340. /// <returns>
  341. /// <see langword="true"/> if the circle and box overlap or touch; otherwise, <see langword="false"/>.
  342. /// </returns>
  343. public readonly bool Intersects(OrientedBoundingBox2D obb)
  344. {
  345. return Collision2D.IntersectsCircleObb(Center, Radius, obb.Center, obb.AxisX, obb.AxisY, obb.HalfExtents);
  346. }
  347. /// <summary>
  348. /// Tests whether this circle intersects with a polygon.
  349. /// </summary>
  350. /// <param name="polygon">The polygon to test against.</param>
  351. /// <returns>
  352. /// <see langword="true"/> if the circle and polygon overlap or touch; otherwise, <see langword="false"/>.
  353. /// </returns>
  354. public readonly bool Intersects(BoundingPolygon2D polygon)
  355. {
  356. return Collision2D.IntersectsCircleConvexPolygon(Center, Radius, polygon.Vertices, polygon.Normals);
  357. }
  358. /// <summary>
  359. /// Applies a matrix transformation to this circle and creates a new transformed circle.
  360. /// </summary>
  361. /// <param name="matrix">The transformation matrix to apply.</param>
  362. /// <returns>
  363. /// A new <see cref="BoundingCircle2D"/> with the center transformed by the matrix and the radius
  364. /// scaled by the maximum scale factor to ensure the transformed shape remains enclosed.
  365. /// </returns>
  366. /// <remarks>
  367. /// The radius is scaled by the maximum of the X and Y scale components of the transformation matrix.
  368. /// This ensures that the resulting circle fully encloses the transformed original circle, even if
  369. /// the transformation includes non-uniform scaling or rotation.
  370. /// </remarks>
  371. public readonly BoundingCircle2D Transform(Matrix matrix)
  372. {
  373. BoundingCircle2D circle = new BoundingCircle2D();
  374. circle.Center = Vector2.Transform(Center, matrix);
  375. // Scale radius by maximum scale component
  376. float scaleX = MathF.Sqrt(matrix.M11 * matrix.M11 + matrix.M12 * matrix.M12);
  377. float scaleY = MathF.Sqrt(matrix.M21 * matrix.M21 + matrix.M22 * matrix.M22);
  378. circle.Radius = Radius * MathF.Max(scaleX, scaleY);
  379. return circle;
  380. }
  381. /// <summary>
  382. /// Creates a new <see cref="BoundingCircle2D"/> by translating this circle by the specified offset.
  383. /// </summary>
  384. /// <param name="translation">The offset to translate the circle by in 2D space.</param>
  385. /// <returns>
  386. /// A new <see cref="BoundingCircle2D"/> at the translated position with the same radius.
  387. /// </returns>
  388. public readonly BoundingCircle2D Translate(Vector2 translation)
  389. {
  390. return new BoundingCircle2D(Center + translation, Radius);
  391. }
  392. /// <summary>
  393. /// Deconstructs this circle into its component values.
  394. /// </summary>
  395. /// <param name="center">
  396. /// When this method returns, contains the center position of this circle in 2D space.
  397. /// </param>
  398. /// <param name="radius">
  399. /// When this method returns, contains the radius of this circle in.
  400. /// </param>
  401. public readonly void Deconstruct(out Vector2 center, out float radius)
  402. {
  403. center = Center;
  404. radius = Radius;
  405. }
  406. /// <inheritdoc/>
  407. public readonly bool Equals(BoundingCircle2D other)
  408. {
  409. return Center.Equals(other.Center) && Radius.Equals(other.Radius);
  410. }
  411. /// <inheritdoc/>
  412. public override readonly bool Equals(object obj)
  413. {
  414. return obj is BoundingCircle2D other && Equals(other);
  415. }
  416. /// <inheritdoc/>
  417. public override readonly int GetHashCode()
  418. {
  419. return Center.GetHashCode() ^ Radius.GetHashCode();
  420. }
  421. /// <inheritdoc/>
  422. public override readonly string ToString()
  423. {
  424. return $"{{Center:{Center} Radius:{Radius}}}";
  425. }
  426. /// <summary/>
  427. public static bool operator ==(BoundingCircle2D left, BoundingCircle2D right)
  428. {
  429. return left.Equals(right);
  430. }
  431. /// <summary/>
  432. public static bool operator !=(BoundingCircle2D left, BoundingCircle2D right)
  433. {
  434. return !left.Equals(right);
  435. }
  436. #endregion
  437. }
  438. }