BoundingPolygon2D.cs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739
  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.Diagnostics.CodeAnalysis;
  7. using System.Runtime.CompilerServices;
  8. using System.Runtime.InteropServices;
  9. using System.Runtime.Serialization;
  10. using Microsoft.Xna.Framework;
  11. namespace MonoGame.Extended
  12. {
  13. /// <summary>
  14. /// Represents a convex polygon bounding volume in 2D space.
  15. /// </summary>
  16. [DataContract]
  17. [DebuggerDisplay("{DebugDisplayString,nq}")]
  18. [StructLayout(LayoutKind.Sequential)]
  19. public struct BoundingPolygon2D : IEquatable<BoundingPolygon2D>
  20. {
  21. #region Public Fields
  22. /// <summary>
  23. /// The vertices of this polygon in counter-clockwise order in 2D space.
  24. /// </summary>
  25. /// <remarks>
  26. /// The counter-clockwise winding order is used to derive outward-facing edge normals for collision detection.
  27. /// </remarks>
  28. [DataMember]
  29. public Vector2[] Vertices;
  30. /// <summary>
  31. /// The outward-facing unit normals for each edge of this polygon.
  32. /// </summary>
  33. /// <remarks>
  34. /// These normals are precomputed to for collision detection.
  35. /// Each normal at index i corresponds to the edge from Vertices[i] to Vertices[(i + 1) % Vertices.Length].
  36. /// </remarks>
  37. [DataMember]
  38. public Vector2[] Normals;
  39. #endregion
  40. #region Public Properties
  41. /// <summary>
  42. /// Gets the number of vertices in this polygon.
  43. /// </summary>
  44. public readonly int VertexCount => Vertices?.Length ?? 0;
  45. /// <summary>
  46. /// Gets the geometric centroid of this polygon in 2D space.
  47. /// </summary>
  48. /// <remarks>
  49. /// Computed as the arithmetic mean of all vertex positions. This represents the geometric center
  50. /// and does not account for mass distribution. For physics simulations requiring a true center of mass
  51. /// based on vertex weights, a separate calculation should be performed.
  52. /// </remarks>
  53. public readonly Vector2 Centroid
  54. {
  55. get
  56. {
  57. if (Vertices == null || Vertices.Length == 0)
  58. {
  59. return Vector2.Zero;
  60. }
  61. Vector2 sum = Vector2.Zero;
  62. for (int i = 0; i < Vertices.Length; i++)
  63. {
  64. sum += Vertices[i];
  65. }
  66. return sum / Vertices.Length;
  67. }
  68. }
  69. /// <summary>
  70. /// Gets the area enclosed by this polygon.
  71. /// </summary>
  72. public readonly float Area
  73. {
  74. get
  75. {
  76. if (Vertices == null || Vertices.Length == 0)
  77. {
  78. return 0.0f;
  79. }
  80. float area = 0.0f;
  81. int n = Vertices.Length;
  82. for (int i = 0; i < n; i++)
  83. {
  84. int j = (i + 1) % n;
  85. area += Vertices[i].X * Vertices[j].Y;
  86. area -= Vertices[j].X * Vertices[i].Y;
  87. }
  88. return MathF.Abs(area) * 0.5f;
  89. }
  90. }
  91. #endregion
  92. #region Internal Properties
  93. internal string DebugDisplayString
  94. {
  95. get
  96. {
  97. return string.Concat(
  98. "Vertices( ", VertexCount.ToString(), " ) \r\n",
  99. "Area( ", Area.ToString("F2"), " )"
  100. );
  101. }
  102. }
  103. #endregion
  104. #region Public Constructors
  105. /// <summary>
  106. /// Creates a new <see cref="BoundingPolygon2D"/> with the specified vertices.
  107. /// </summary>
  108. /// <param name="vertices">
  109. /// The vertices of the polygon in counter-clockwise order in 2D space. Must contain at least three vertices.
  110. /// </param>
  111. /// <exception cref="ArgumentNullException">
  112. /// Thrown when <paramref name="vertices"/> is <see langword="null"/>.
  113. /// </exception>
  114. /// <exception cref="ArgumentException">
  115. /// Thrown when <paramref name="vertices"/> contains fewer than three elements.
  116. /// </exception>
  117. /// <remarks>
  118. /// Edge normals are computed automatically from the vertices and cached for collision detection.
  119. /// </remarks>
  120. public BoundingPolygon2D(Vector2[] vertices)
  121. {
  122. if (vertices == null)
  123. {
  124. throw new ArgumentNullException(nameof(vertices));
  125. }
  126. if (vertices.Length < 3)
  127. {
  128. throw new ArgumentException("Polygon must have at least 3 vertices", nameof(vertices));
  129. }
  130. Debug.Assert(ComputeSignedArea(vertices) > 1e-6f,
  131. "Vertices must be in counter-clockwise order. Ensure vertices follow the documented winding order.");
  132. Vertices = vertices;
  133. Normals = ComputeNormals(vertices);
  134. }
  135. /// <summary>
  136. /// Creates a new <see cref="BoundingPolygon2D"/> with the specified vertices and precomputed edge normals.
  137. /// </summary>
  138. /// <param name="vertices">
  139. /// The vertices of the polygon in counter-clockwise order in 2D space. Must contain at least three vertices.
  140. /// </param>
  141. /// <param name="normals">
  142. /// The outward-facing unit normals for each polygon edge. Must have the same length as <paramref name="vertices"/>.
  143. /// </param>
  144. /// <exception cref="ArgumentNullException">
  145. /// Thrown when <paramref name="vertices"/> or <paramref name="normals"/> is <see langword="null"/>.
  146. /// </exception>
  147. /// <exception cref="ArgumentException">
  148. /// Thrown when <paramref name="vertices"/> contains fewer than three elements,
  149. /// or when the length of <paramref name="normals"/> does not match the length of <paramref name="vertices"/>.
  150. /// </exception>
  151. /// <remarks>
  152. /// This constructor allows precomputed normals to be provided, avoiding recomputation when the data is already available.
  153. /// </remarks>
  154. public BoundingPolygon2D(Vector2[] vertices, Vector2[] normals)
  155. {
  156. if (vertices == null)
  157. {
  158. throw new ArgumentNullException(nameof(vertices));
  159. }
  160. if (normals == null)
  161. {
  162. throw new ArgumentNullException(nameof(normals));
  163. }
  164. if (vertices.Length < 3)
  165. {
  166. throw new ArgumentException("Polygon must have at least 3 vertices", nameof(vertices));
  167. }
  168. if (vertices.Length != normals.Length)
  169. {
  170. throw new ArgumentException("Normals array must have same length as vertices array");
  171. }
  172. Vertices = vertices;
  173. Normals = normals;
  174. }
  175. #endregion
  176. #region Public Methods
  177. /// <summary>
  178. /// Creates a new <see cref="BoundingPolygon2D"/> from the specified vertices.
  179. /// </summary>
  180. /// <param name="vertices">
  181. /// The vertices of the polygon in counter-clockwise order in 2D space. Must contain at least three vertices.
  182. /// </param>
  183. /// <returns>
  184. /// A new <see cref="BoundingPolygon2D"/> with edge normals computed automatically.
  185. /// </returns>
  186. /// <exception cref="ArgumentNullException">
  187. /// Thrown when <paramref name="vertices"/> is <see langword="null"/>.
  188. /// </exception>
  189. /// <exception cref="ArgumentException">
  190. /// Thrown when <paramref name="vertices"/> contains fewer than three elements.
  191. /// </exception>
  192. public static BoundingPolygon2D CreateFromVertices(Vector2[] vertices)
  193. {
  194. return new BoundingPolygon2D(vertices);
  195. }
  196. /// <summary>
  197. /// Creates a new <see cref="BoundingPolygon2D"/> representing a regular polygon with equal side lengths and angles.
  198. /// </summary>
  199. /// <param name="center">The center position of the polygon in 2D space.</param>
  200. /// <param name="radius">The distance from the center to each vertex.</param>
  201. /// <param name="sides">The number of sides of the polygon. Must be at least three.</param>
  202. /// <param name="rotation">
  203. /// The rotation angle of the polygon in radians, measured counter-clockwise from the positive world X-axis.
  204. /// </param>
  205. /// <returns>
  206. /// A new <see cref="BoundingPolygon2D"/> with vertices in counter-clockwise order and edge normals computed automatically.
  207. /// </returns>
  208. /// <exception cref="ArgumentException">
  209. /// Thrown when <paramref name="sides"/> is less than three.
  210. /// </exception>
  211. public static BoundingPolygon2D CreateRegular(Vector2 center, float radius, int sides, float rotation = 0.0f)
  212. {
  213. if (sides < 3)
  214. {
  215. throw new ArgumentException("Regular polygon must have at least 3 sides", nameof(sides));
  216. }
  217. Vector2[] vertices = new Vector2[sides];
  218. float angleStep = MathHelper.TwoPi / sides;
  219. for (int i = 0; i < sides; i++)
  220. {
  221. float angle = i * angleStep + rotation;
  222. vertices[i] = center + new Vector2(
  223. MathF.Cos(angle) * radius,
  224. MathF.Sin(angle) * radius
  225. );
  226. }
  227. return new BoundingPolygon2D(vertices);
  228. }
  229. /// <summary>
  230. /// Creates a new <see cref="BoundingPolygon2D"/> from a bounding box.
  231. /// </summary>
  232. /// <param name="box">The bounding box to convert to a polygon.</param>
  233. /// <returns>
  234. /// A new <see cref="BoundingPolygon2D"/> with four vertices in counter-clockwise order
  235. /// representing the bounding box as a rectangular polygon.
  236. /// </returns>
  237. public static BoundingPolygon2D CreateFromBoundingBox2D(BoundingBox2D box)
  238. {
  239. Vector2[] vertices = new Vector2[]
  240. {
  241. new Vector2(box.Min.X, box.Min.Y), // Top-left
  242. new Vector2(box.Max.X, box.Min.Y), // Top-right
  243. new Vector2(box.Max.X, box.Max.Y), // Bottom-right
  244. new Vector2(box.Min.X, box.Max.Y) // Bottom-left
  245. };
  246. return new BoundingPolygon2D(vertices);
  247. }
  248. /// <summary>
  249. /// Creates a <see cref="BoundingPolygon2D"/> that encloses two polygons.
  250. /// </summary>
  251. /// <param name="original">The first polygon to enclose.</param>
  252. /// <param name="additional">The second polygon to enclose.</param>
  253. /// <returns>
  254. /// A new <see cref="BoundingPolygon2D"/> that completely contains both input polygons.
  255. /// </returns>
  256. /// <remarks>
  257. /// Computes the convex hull of the combined vertex sets from both polygons,
  258. /// producing the minimal enclosing convex polygon.
  259. /// </remarks>
  260. public static BoundingPolygon2D CreateMerged(BoundingPolygon2D original, BoundingPolygon2D additional)
  261. {
  262. // C. Ericson, Real-Time Collision Detection, Morgan Kaufmann, 2005
  263. // Section 4.2.3 "Computing the Convex Hull"
  264. // Merges polygons by computing the convex hull of the combined vertex set.
  265. if (original.Vertices == null || original.Vertices.Length == 0)
  266. {
  267. return additional;
  268. }
  269. if (additional.Vertices == null || additional.Vertices.Length == 0)
  270. {
  271. return original;
  272. }
  273. // Combine all vertices
  274. Vector2[] allVertices = new Vector2[original.Vertices.Length + additional.Vertices.Length];
  275. Array.Copy(original.Vertices, 0, allVertices, 0, original.Vertices.Length);
  276. Array.Copy(additional.Vertices, 0, allVertices, original.Vertices.Length, additional.Vertices.Length);
  277. // Compute convex hull using Graham scan
  278. Vector2[] hullVertices = ComputeConvexHull(allVertices);
  279. // Ensure output is ccw order
  280. EnsureCounterClockwise(hullVertices);
  281. return new BoundingPolygon2D(hullVertices);
  282. }
  283. /// <summary>
  284. /// Tests whether a point lies inside this polygon or on its boundary.
  285. /// </summary>
  286. /// <param name="point">The point to test in 2D space.</param>
  287. /// <returns>
  288. /// <see cref="ContainmentType.Contains"/> if the point is inside or on the boundary;
  289. /// otherwise, <see cref="ContainmentType.Disjoint"/> if the point is outside.
  290. /// </returns>
  291. public readonly ContainmentType Contains(Vector2 point)
  292. {
  293. return Collision2D.ContainsConvexPolygonPoint(point, Vertices, Normals);
  294. }
  295. /// <summary>
  296. /// Tests whether this polygon contains, intersects, or is separate from a bounding box.
  297. /// </summary>
  298. /// <param name="aabb">The bounding box. to test against.</param>
  299. /// <returns>
  300. /// <see cref="ContainmentType.Contains"/> if the bounding box. is completely inside this polygon;
  301. /// <see cref="ContainmentType.Intersects"/> if they partially overlap;
  302. /// or <see cref="ContainmentType.Disjoint"/> if they do not touch.
  303. /// </returns>
  304. public readonly ContainmentType Contains(BoundingBox2D aabb)
  305. {
  306. return Collision2D.ContainsConvexPolygonAabb(Vertices, Normals, aabb.Min, aabb.Max);
  307. }
  308. /// <summary>
  309. /// Tests whether this polygon contains, intersects, or is separate from a circle.
  310. /// </summary>
  311. /// <param name="circle">The circle to test against.</param>
  312. /// <returns>
  313. /// <see cref="ContainmentType.Contains"/> if the circle is completely inside this polygon;
  314. /// <see cref="ContainmentType.Intersects"/> if they partially overlap;
  315. /// or <see cref="ContainmentType.Disjoint"/> if they do not touch.
  316. /// </returns>
  317. public readonly ContainmentType Contains(BoundingCircle2D circle)
  318. {
  319. return Collision2D.ContainsConvexPolygonCircle(Vertices, Normals, circle.Center, circle.Radius);
  320. }
  321. /// <summary>
  322. /// Tests whether this polygon contains, intersects, or is separate from an oriented bounding box.
  323. /// </summary>
  324. /// <param name="obb">The oriented bounding box to test against.</param>
  325. /// <returns>
  326. /// <see cref="ContainmentType.Contains"/> if the oriented bounding box is completely inside this polygon;
  327. /// <see cref="ContainmentType.Intersects"/> if they partially overlap;
  328. /// or <see cref="ContainmentType.Disjoint"/> if they do not touch.
  329. /// </returns>
  330. public readonly ContainmentType Contains(OrientedBoundingBox2D obb)
  331. {
  332. return Collision2D.ContainsConvexPolygonObb(Vertices, Normals, obb.Center, obb.AxisX, obb.AxisY, obb.HalfExtents);
  333. }
  334. /// <summary>
  335. /// Tests whether this polygon contains, intersects, or is separate from a capsule.
  336. /// </summary>
  337. /// <param name="capsule">The vs to test against.</param>
  338. /// <returns>
  339. /// <see cref="ContainmentType.Contains"/> if the capsule is completely inside this polygon;
  340. /// <see cref="ContainmentType.Intersects"/> if they partially overlap;
  341. /// or <see cref="ContainmentType.Disjoint"/> if they do not touch.
  342. /// </returns>
  343. public readonly ContainmentType Contains(BoundingCapsule2D capsule)
  344. {
  345. return Collision2D.ContainsConvexPolygonCapsule(Vertices, Normals, capsule.PointA, capsule.PointB, capsule.Radius);
  346. }
  347. /// <summary>
  348. /// Tests whether this polygon contains, intersects, or is separate from another polygon.
  349. /// </summary>
  350. /// <param name="other">The other polygon to test against.</param>
  351. /// <returns>
  352. /// <see cref="ContainmentType.Contains"/> if the other polygon is completely inside this one;
  353. /// <see cref="ContainmentType.Intersects"/> if they partially overlap;
  354. /// or <see cref="ContainmentType.Disjoint"/> if they do not touch.
  355. /// </returns>
  356. public readonly ContainmentType Contains(BoundingPolygon2D other)
  357. {
  358. return Collision2D.ContainsConvexPolygonConvexPolygon(Vertices, Normals, other.Vertices, other.Normals);
  359. }
  360. /// <summary>
  361. /// Tests whether this polygon intersects with a circle.
  362. /// </summary>
  363. /// <param name="circle">The circle to test against.</param>
  364. /// <returns>
  365. /// <see langword="true"/> if the polygon and circle overlap or touch; otherwise, <see langword="false"/>.
  366. /// </returns>
  367. public readonly bool Intersects(BoundingCircle2D circle)
  368. {
  369. return Collision2D.IntersectsCircleConvexPolygon(circle.Center, circle.Radius, Vertices, Normals);
  370. }
  371. /// <summary>
  372. /// Tests whether this polygon intersects with an axis-aligned bounding box.
  373. /// </summary>
  374. /// <param name="box">The bounding box to test against.</param>
  375. /// <returns>
  376. /// <see langword="true"/> if the polygon and bounding box overlap or touch; otherwise, <see langword="false"/>.
  377. /// </returns>
  378. public readonly bool Intersects(BoundingBox2D box)
  379. {
  380. return Collision2D.IntersectsAabbConvexPolygon(box.Center, box.HalfExtents, Vertices, Normals);
  381. }
  382. /// <summary>
  383. /// Tests whether this polygon intersects with an oriented bounding box.
  384. /// </summary>
  385. /// <param name="obb">The oriented bounding box to test against.</param>
  386. /// <returns>
  387. /// <see langword="true"/> if the polygon and box overlap or touch; otherwise, <see langword="false"/>.
  388. /// </returns>
  389. public readonly bool Intersects(OrientedBoundingBox2D obb)
  390. {
  391. return Collision2D.IntersectsObbConvexPolygon(obb.Center, obb.AxisX, obb.AxisY, obb.HalfExtents, Vertices, Normals);
  392. }
  393. /// <summary>
  394. /// Tests whether this polygon intersects with capsule.
  395. /// </summary>
  396. /// <param name="capsule">The capsule to test against.</param>
  397. /// <returns>
  398. /// <see langword="true"/> if the polygon and capsule overlap or touch; otherwise, <see langword="false"/>.
  399. /// </returns>
  400. public readonly bool Intersects(BoundingCapsule2D capsule)
  401. {
  402. return Collision2D.IntersectsCapsuleConvexPolygon(capsule.PointA, capsule.PointB, capsule.Radius, Vertices, Normals);
  403. }
  404. /// <summary>
  405. /// Tests whether this polygon intersects with another polygon.
  406. /// </summary>
  407. /// <param name="other">The other polygon to test against.</param>
  408. /// <returns>
  409. /// <see langword="true"/> if the polygons overlap or touch; otherwise, <see langword="false"/>.
  410. /// </returns>
  411. public readonly bool Intersects(BoundingPolygon2D other)
  412. {
  413. return Collision2D.IntersectsConvexPolygonConvexPolygon(Vertices, Normals, other.Vertices, other.Normals);
  414. }
  415. /// <summary>
  416. /// Applies a matrix transformation to this polygon and creates a new transformed polygon.
  417. /// </summary>
  418. /// <param name="matrix">The transformation matrix to apply.</param>
  419. /// <returns>
  420. /// A new <see cref="BoundingPolygon2D"/> with vertices transformed by the matrix
  421. /// and edge normals recomputed from the transformed geometry.
  422. /// </returns>
  423. public readonly BoundingPolygon2D Transform(Matrix matrix)
  424. {
  425. if (Vertices == null || Vertices.Length == 0)
  426. {
  427. return this;
  428. }
  429. Vector2[] transformedVertices = new Vector2[Vertices.Length];
  430. for (int i = 0; i < Vertices.Length; i++)
  431. {
  432. transformedVertices[i] = Vector2.Transform(Vertices[i], matrix);
  433. }
  434. return new BoundingPolygon2D(transformedVertices);
  435. }
  436. /// <summary>
  437. /// Creates a new <see cref="BoundingPolygon2D"/> by translating this polygon by the specified offset.
  438. /// </summary>
  439. /// <param name="translation">The offset to translate the polygon by in 2D space.</param>
  440. /// <returns>
  441. /// A new <see cref="BoundingPolygon2D"/> at the translated position with the same shape and orientation.
  442. /// </returns>
  443. public readonly BoundingPolygon2D Translate(Vector2 translation)
  444. {
  445. if (Vertices == null || Vertices.Length == 0)
  446. {
  447. return this;
  448. }
  449. Vector2[] translatedVertices = new Vector2[Vertices.Length];
  450. for (int i = 0; i < Vertices.Length; i++)
  451. {
  452. translatedVertices[i] = Vertices[i] + translation;
  453. }
  454. // Normals don't change with translation
  455. return new BoundingPolygon2D(translatedVertices, Normals);
  456. }
  457. /// <summary>
  458. /// Deconstructs this polygon into its component arrays.
  459. /// </summary>
  460. /// <param name="vertices">
  461. /// When this method returns, contains the vertices of this polygon in counter-clockwise order in 2D space.
  462. /// </param>
  463. /// <param name="normals">
  464. /// When this method returns, contains the outward-facing unit normals for each edge of this polygon.
  465. /// </param>
  466. public readonly void Deconstruct(out Vector2[] vertices, out Vector2[] normals)
  467. {
  468. vertices = Vertices;
  469. normals = Normals;
  470. }
  471. /// <inheritdoc/>
  472. public readonly bool Equals(BoundingPolygon2D other)
  473. {
  474. if (VertexCount != other.VertexCount)
  475. {
  476. return false;
  477. }
  478. if (Vertices == null && other.Vertices == null)
  479. {
  480. return true;
  481. }
  482. if (Vertices == null || other.Vertices == null)
  483. {
  484. return false;
  485. }
  486. for (int i = 0; i < VertexCount; i++)
  487. {
  488. if (!Vertices[i].Equals(other.Vertices[i]))
  489. {
  490. return false;
  491. }
  492. }
  493. return true;
  494. }
  495. /// <inheritdoc/>
  496. public override readonly bool Equals([NotNullWhen(true)] object obj)
  497. {
  498. return obj is BoundingPolygon2D other && Equals(other);
  499. }
  500. /// <inheritdoc/>
  501. public override readonly int GetHashCode()
  502. {
  503. if (Vertices == null || Vertices.Length == 0)
  504. {
  505. return 0;
  506. }
  507. HashCode hash = new HashCode();
  508. for (int i = 0; i < Vertices.Length; i++)
  509. {
  510. hash.Add(Vertices[i]);
  511. }
  512. return hash.ToHashCode();
  513. }
  514. /// <inheritdoc/>
  515. public override readonly string ToString()
  516. {
  517. return $"{{Vertices:{VertexCount} Area:{Area:F2}}}";
  518. }
  519. /// <summary/>
  520. public static bool operator ==(BoundingPolygon2D left, BoundingPolygon2D right)
  521. {
  522. return left.Equals(right);
  523. }
  524. /// <summary/>
  525. public static bool operator !=(BoundingPolygon2D left, BoundingPolygon2D right)
  526. {
  527. return !left.Equals(right);
  528. }
  529. #endregion
  530. #region Private Helper Methods
  531. private static float ComputeSignedArea(Vector2[] vertices)
  532. {
  533. if (vertices == null || vertices.Length < 3)
  534. return 0.0f;
  535. float area = 0.0f;
  536. int n = vertices.Length;
  537. for (int i = 0; i < n; i++)
  538. {
  539. int j = (i + 1) % n;
  540. area += Vector2Extensions.PerpDot(vertices[i], vertices[j]);
  541. }
  542. return area * 0.5f;
  543. }
  544. private static void EnsureCounterClockwise(Vector2[] vertices)
  545. {
  546. if (vertices == null || vertices.Length < 3)
  547. return;
  548. float signedArea = ComputeSignedArea(vertices);
  549. if (signedArea < 0.0f)
  550. Array.Reverse(vertices);
  551. }
  552. private static Vector2[] ComputeConvexHull(Vector2[] points)
  553. {
  554. // C. Ericson, Real-Time Collision Detection, Morgan Kaufmann, 2005
  555. // Section 4.2.3 "Computing the Convex Hull"
  556. // Graham scan algorithm adapted for 2D Vector2 input
  557. if (points.Length < 3)
  558. return points;
  559. // Find the point with the lowest Y coordinate
  560. // and if tied, the lowest x coordinate as well
  561. int minIndex = 0;
  562. for (int i = 1; i < points.Length; i++)
  563. {
  564. if (points[i].Y < points[minIndex].Y ||
  565. (points[i].Y == points[minIndex].Y && points[i].X < points[minIndex].X))
  566. {
  567. minIndex = i;
  568. }
  569. }
  570. Vector2 pivot = points[minIndex];
  571. // Sort points by polar angle with respect to pivot
  572. Vector2[] sorted = new Vector2[points.Length];
  573. Array.Copy(points, sorted, points.Length);
  574. Array.Sort(sorted, (a, b) =>
  575. {
  576. if (a.Equals(pivot)) return -1;
  577. if (b.Equals(pivot)) return 1;
  578. float angleA = MathF.Atan2(a.Y - pivot.Y, a.X - pivot.X);
  579. float angleB = MathF.Atan2(b.Y - pivot.Y, b.X - pivot.X);
  580. if (MathF.Abs(angleA - angleB) < Collision2D.Epsilon)
  581. {
  582. // Same angle, closer point comes first
  583. float distA = Vector2.DistanceSquared(pivot, a);
  584. float distB = Vector2.DistanceSquared(pivot, b);
  585. return distB.CompareTo(distA);
  586. }
  587. return angleA.CompareTo(angleB);
  588. });
  589. // Graham scan
  590. Vector2[] hull = new Vector2[sorted.Length];
  591. int hullSize = 0;
  592. for (int i = 0; i < sorted.Length; i++)
  593. {
  594. // Remove points that make a right turn
  595. while (hullSize >= 2 && Orientation(hull[hullSize - 2], hull[hullSize - 1], sorted[i]) <= 0)
  596. {
  597. hullSize--;
  598. }
  599. hull[hullSize++] = sorted[i];
  600. }
  601. // Resize the actual hull size
  602. Vector2[] result = new Vector2[hullSize];
  603. Array.Copy(hull, result, hullSize);
  604. return result;
  605. }
  606. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  607. private static float Orientation(Vector2 a, Vector2 b, Vector2 c)
  608. {
  609. return (b.X - a.X) * (c.Y - a.Y) - (b.Y - a.Y) * (c.X - a.X);
  610. }
  611. private static Vector2[] ComputeNormals(Vector2[] vertices)
  612. {
  613. int n = vertices.Length;
  614. Vector2[] normals = new Vector2[n];
  615. for (int i = 0; i < n; i++)
  616. {
  617. int j = (i + 1) % n;
  618. Vector2 edge = vertices[j] - vertices[i];
  619. // Perpendicular to edge (rotated 90 degrees clockwise for outward normal)
  620. normals[i] = new Vector2(edge.Y, -edge.X);
  621. // Normalize
  622. float length = normals[i].Length();
  623. if (length > Collision2D.Epsilon)
  624. {
  625. normals[i] /= length;
  626. }
  627. }
  628. return normals;
  629. }
  630. #endregion
  631. }
  632. }