| | | 1 | | //======================================================================= |
| | | 2 | | // ContactManifold2D.cs |
| | | 3 | | //======================================================================= |
| | | 4 | | // MIT License, Copyright (c) 2026–present David Oravsky (mrdav30) |
| | | 5 | | // See LICENSE file in the project root for full license information. |
| | | 6 | | //======================================================================= |
| | | 7 | | |
| | | 8 | | using FixedMathSharp; |
| | | 9 | | using Gravitas.Materials; |
| | | 10 | | using System.Collections; |
| | | 11 | | using System.Collections.Generic; |
| | | 12 | | using System.Runtime.CompilerServices; |
| | | 13 | | |
| | | 14 | | namespace Gravitas.CollisionHandling; |
| | | 15 | | |
| | | 16 | | /// <summary> |
| | | 17 | | /// Fixed-capacity deterministic pure 2D contact manifold owned by one collision pair. |
| | | 18 | | /// </summary> |
| | | 19 | | public sealed class ContactManifold2D : IEnumerable<ManifoldContact2D> |
| | | 20 | | { |
| | | 21 | | /// <summary>Maximum number of contacts retained by a pure 2D manifold.</summary> |
| | | 22 | | public const int MaxContactCount = 2; |
| | | 23 | | |
| | | 24 | | private ManifoldContact2D _contact0; |
| | | 25 | | private ManifoldContact2D _contact1; |
| | | 26 | | private int _count; |
| | 598 | 27 | | private int _lastUpdatedFrame = -1; |
| | | 28 | | |
| | | 29 | | /// <summary> |
| | | 30 | | /// Number of active contacts in this manifold. |
| | | 31 | | /// </summary> |
| | | 32 | | public int Count |
| | | 33 | | { |
| | | 34 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | 3142 | 35 | | get => _count; |
| | | 36 | | } |
| | | 37 | | |
| | | 38 | | /// <summary> |
| | | 39 | | /// Gets whether this manifold currently contains narrow-phase contact data. |
| | | 40 | | /// </summary> |
| | | 41 | | public bool HasContact |
| | | 42 | | { |
| | | 43 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | 2092 | 44 | | get => _count > 0; |
| | | 45 | | } |
| | | 46 | | |
| | | 47 | | /// <summary> |
| | | 48 | | /// Simulation frame in which the active contacts were last rebuilt. |
| | | 49 | | /// </summary> |
| | | 50 | | public int LastUpdatedFrame |
| | | 51 | | { |
| | | 52 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | 544 | 53 | | get => _lastUpdatedFrame; |
| | | 54 | | } |
| | | 55 | | |
| | | 56 | | /// <summary> |
| | | 57 | | /// Deepest contact in the manifold. Ties use the lowest contact identity. |
| | | 58 | | /// </summary> |
| | | 59 | | public ManifoldContact2D PrimaryContact |
| | | 60 | | { |
| | | 61 | | get |
| | | 62 | | { |
| | 164 | 63 | | SwiftThrowHelper.ThrowIfListIndexInvalid(0, _count); |
| | | 64 | | |
| | 164 | 65 | | if (_count == 1) |
| | 147 | 66 | | return _contact0; |
| | | 67 | | |
| | 17 | 68 | | return IsDeeper(_contact1, _contact0) |
| | 17 | 69 | | || HasEqualDepth(_contact1, _contact0) && _contact1.ContactId < _contact0.ContactId |
| | 17 | 70 | | ? _contact1 |
| | 17 | 71 | | : _contact0; |
| | | 72 | | } |
| | | 73 | | } |
| | | 74 | | |
| | | 75 | | /// <summary>Gets the contact at the specified deterministic order index.</summary> |
| | | 76 | | public ManifoldContact2D this[int index] |
| | | 77 | | { |
| | | 78 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 79 | | get |
| | | 80 | | { |
| | 1855 | 81 | | SwiftThrowHelper.ThrowIfListIndexInvalid(index, _count); |
| | 1855 | 82 | | return GetContactUnchecked(index); |
| | | 83 | | } |
| | | 84 | | } |
| | | 85 | | |
| | | 86 | | /// <summary> |
| | | 87 | | /// Clears contacts and records the frame for a new narrow-phase pass. |
| | | 88 | | /// </summary> |
| | | 89 | | public void BeginUpdate(int frame) |
| | | 90 | | { |
| | 655 | 91 | | _count = 0; |
| | 655 | 92 | | _lastUpdatedFrame = frame; |
| | 655 | 93 | | } |
| | | 94 | | |
| | | 95 | | /// <summary> |
| | | 96 | | /// Clears all contact data. |
| | | 97 | | /// </summary> |
| | | 98 | | public void Reset() |
| | | 99 | | { |
| | 432 | 100 | | _count = 0; |
| | 432 | 101 | | _lastUpdatedFrame = -1; |
| | 432 | 102 | | _contact0 = default; |
| | 432 | 103 | | _contact1 = default; |
| | 432 | 104 | | } |
| | | 105 | | |
| | | 106 | | /// <summary> |
| | | 107 | | /// Replaces the manifold with one contact. |
| | | 108 | | /// </summary> |
| | | 109 | | public void SetContact(Vector2d pointA, Vector2d pointB, Fixed64 depth, Vector2d normal) |
| | | 110 | | { |
| | 65 | 111 | | _count = 0; |
| | 65 | 112 | | AddContact(pointA, pointB, depth, normal); |
| | 65 | 113 | | } |
| | | 114 | | |
| | | 115 | | /// <summary> |
| | | 116 | | /// Replaces the manifold with one canonical rigid-frame planar contact. |
| | | 117 | | /// </summary> |
| | | 118 | | public void SetContact( |
| | | 119 | | ContactAnchor2D anchorA, |
| | | 120 | | ContactAnchor2D anchorB, |
| | | 121 | | Fixed64 depth, |
| | | 122 | | Vector2d normal, |
| | | 123 | | bool depthIsClamped = false) |
| | | 124 | | { |
| | 11 | 125 | | _count = 0; |
| | 11 | 126 | | AddContact(anchorA, anchorB, depth, normal, depthIsClamped); |
| | 11 | 127 | | } |
| | | 128 | | |
| | | 129 | | /// <summary> |
| | | 130 | | /// Adds a contact, keeping the deepest two contacts and exposing them by stable contact identity. |
| | | 131 | | /// </summary> |
| | | 132 | | public void AddContact(Vector2d pointA, Vector2d pointB, Fixed64 depth, Vector2d normal) |
| | | 133 | | { |
| | 126 | 134 | | AddContact( |
| | 126 | 135 | | ContactAnchor2D.FromWorldPoint(pointA), |
| | 126 | 136 | | ContactAnchor2D.FromWorldPoint(pointB), |
| | 126 | 137 | | depth, |
| | 126 | 138 | | normal); |
| | 126 | 139 | | } |
| | | 140 | | |
| | | 141 | | /// <summary> |
| | | 142 | | /// Adds a canonical rigid-frame contact, keeping the deepest two contacts and |
| | | 143 | | /// exposing them by stable anchor identity. |
| | | 144 | | /// </summary> |
| | | 145 | | public void AddContact( |
| | | 146 | | ContactAnchor2D anchorA, |
| | | 147 | | ContactAnchor2D anchorB, |
| | | 148 | | Fixed64 depth, |
| | | 149 | | Vector2d normal, |
| | | 150 | | bool depthIsClamped = false) |
| | | 151 | | { |
| | 144 | 152 | | AddContactCore( |
| | 144 | 153 | | anchorA, |
| | 144 | 154 | | anchorB, |
| | 144 | 155 | | depth, |
| | 144 | 156 | | normal, |
| | 144 | 157 | | hasMaterialOverride: false, |
| | 144 | 158 | | default, |
| | 144 | 159 | | default, |
| | 144 | 160 | | depthIsClamped, |
| | 144 | 161 | | featureNamespaceA: 0, |
| | 144 | 162 | | featureNamespaceB: 0); |
| | 144 | 163 | | } |
| | | 164 | | |
| | | 165 | | internal void AddContact( |
| | | 166 | | Vector2d pointA, |
| | | 167 | | Vector2d pointB, |
| | | 168 | | Fixed64 depth, |
| | | 169 | | Vector2d normal, |
| | | 170 | | PhysicsMaterial materialA, |
| | | 171 | | PhysicsMaterial materialB, |
| | | 172 | | bool depthIsClamped = false) |
| | | 173 | | { |
| | 2 | 174 | | AddContactCore( |
| | 2 | 175 | | ContactAnchor2D.FromWorldPoint(pointA), |
| | 2 | 176 | | ContactAnchor2D.FromWorldPoint(pointB), |
| | 2 | 177 | | depth, |
| | 2 | 178 | | normal, |
| | 2 | 179 | | hasMaterialOverride: true, |
| | 2 | 180 | | materialA, |
| | 2 | 181 | | materialB, |
| | 2 | 182 | | depthIsClamped, |
| | 2 | 183 | | featureNamespaceA: 0, |
| | 2 | 184 | | featureNamespaceB: 0); |
| | 2 | 185 | | } |
| | | 186 | | |
| | | 187 | | internal void AddContact( |
| | | 188 | | ContactAnchor2D anchorA, |
| | | 189 | | ContactAnchor2D anchorB, |
| | | 190 | | Fixed64 depth, |
| | | 191 | | Vector2d normal, |
| | | 192 | | PhysicsMaterial materialA, |
| | | 193 | | PhysicsMaterial materialB, |
| | | 194 | | bool depthIsClamped = false, |
| | | 195 | | int featureNamespaceA = 0, |
| | | 196 | | int featureNamespaceB = 0) |
| | | 197 | | { |
| | 652 | 198 | | AddContactCore( |
| | 652 | 199 | | anchorA, |
| | 652 | 200 | | anchorB, |
| | 652 | 201 | | depth, |
| | 652 | 202 | | normal, |
| | 652 | 203 | | hasMaterialOverride: true, |
| | 652 | 204 | | materialA, |
| | 652 | 205 | | materialB, |
| | 652 | 206 | | depthIsClamped, |
| | 652 | 207 | | featureNamespaceA, |
| | 652 | 208 | | featureNamespaceB); |
| | 652 | 209 | | } |
| | | 210 | | |
| | | 211 | | private void AddContactCore( |
| | | 212 | | ContactAnchor2D anchorA, |
| | | 213 | | ContactAnchor2D anchorB, |
| | | 214 | | Fixed64 depth, |
| | | 215 | | Vector2d normal, |
| | | 216 | | bool hasMaterialOverride, |
| | | 217 | | PhysicsMaterial materialA, |
| | | 218 | | PhysicsMaterial materialB, |
| | | 219 | | bool depthIsClamped, |
| | | 220 | | int featureNamespaceA, |
| | | 221 | | int featureNamespaceB) |
| | | 222 | | { |
| | 798 | 223 | | ulong contactId = CreateContactId( |
| | 798 | 224 | | anchorA, |
| | 798 | 225 | | featureNamespaceA, |
| | 798 | 226 | | anchorB, |
| | 798 | 227 | | featureNamespaceB); |
| | 798 | 228 | | var contact = new ManifoldContact2D( |
| | 798 | 229 | | contactId, |
| | 798 | 230 | | anchorA, |
| | 798 | 231 | | anchorB, |
| | 798 | 232 | | depth, |
| | 798 | 233 | | normal, |
| | 798 | 234 | | hasMaterialOverride, |
| | 798 | 235 | | materialA, |
| | 798 | 236 | | materialB, |
| | 798 | 237 | | depthIsClamped, |
| | 798 | 238 | | featureNamespaceA, |
| | 798 | 239 | | featureNamespaceB); |
| | | 240 | | |
| | 1714 | 241 | | for (int i = 0; i < _count; i++) |
| | | 242 | | { |
| | 64 | 243 | | ManifoldContact2D existing = GetContactUnchecked(i); |
| | 64 | 244 | | if (existing.ContactId != contactId) |
| | | 245 | | continue; |
| | | 246 | | |
| | 5 | 247 | | if (IsDeeper(contact, existing)) |
| | 3 | 248 | | SetContactUnchecked(i, contact); |
| | 5 | 249 | | SortContactsById(); |
| | 5 | 250 | | return; |
| | | 251 | | } |
| | | 252 | | |
| | 793 | 253 | | if (_count < MaxContactCount) |
| | | 254 | | { |
| | 789 | 255 | | SetContactUnchecked(_count, contact); |
| | 789 | 256 | | _count++; |
| | 789 | 257 | | SortContactsById(); |
| | 789 | 258 | | return; |
| | | 259 | | } |
| | | 260 | | |
| | 4 | 261 | | int replaceIndex = FindShallowestReplacementIndex(contact); |
| | 4 | 262 | | if (replaceIndex < 0) |
| | 1 | 263 | | return; |
| | | 264 | | |
| | 3 | 265 | | SetContactUnchecked(replaceIndex, contact); |
| | 3 | 266 | | SortContactsById(); |
| | 3 | 267 | | } |
| | | 268 | | |
| | | 269 | | /// <summary>Returns an allocation-free enumerator over the active contacts.</summary> |
| | 41 | 270 | | public Enumerator GetEnumerator() => new(this); |
| | | 271 | | |
| | 40 | 272 | | IEnumerator<ManifoldContact2D> IEnumerable<ManifoldContact2D>.GetEnumerator() => GetEnumerator(); |
| | | 273 | | |
| | 1 | 274 | | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
| | | 275 | | |
| | | 276 | | private int FindShallowestReplacementIndex(ManifoldContact2D candidate) |
| | | 277 | | { |
| | 4 | 278 | | int replaceIndex = !IsDeeper(_contact1, _contact0) |
| | 4 | 279 | | ? 1 |
| | 4 | 280 | | : 0; |
| | | 281 | | |
| | 4 | 282 | | ManifoldContact2D shallowest = GetContactUnchecked(replaceIndex); |
| | 4 | 283 | | if (IsDeeper(candidate, shallowest)) |
| | 2 | 284 | | return replaceIndex; |
| | | 285 | | |
| | 2 | 286 | | if (HasEqualDepth(candidate, shallowest) && candidate.ContactId < shallowest.ContactId) |
| | 1 | 287 | | return replaceIndex; |
| | | 288 | | |
| | 1 | 289 | | return -1; |
| | | 290 | | } |
| | | 291 | | |
| | | 292 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 293 | | private static bool IsDeeper(ManifoldContact2D candidate, ManifoldContact2D existing) => |
| | 30 | 294 | | candidate.Depth > existing.Depth |
| | 30 | 295 | | || candidate.Depth == existing.Depth |
| | 30 | 296 | | && candidate.DepthIsClamped |
| | 30 | 297 | | && !existing.DepthIsClamped; |
| | | 298 | | |
| | | 299 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 300 | | private static bool HasEqualDepth(ManifoldContact2D left, ManifoldContact2D right) => |
| | 17 | 301 | | left.Depth == right.Depth |
| | 17 | 302 | | && left.DepthIsClamped == right.DepthIsClamped; |
| | | 303 | | |
| | | 304 | | private void SortContactsById() |
| | | 305 | | { |
| | 797 | 306 | | if (_count < 2 || _contact0.ContactId <= _contact1.ContactId) |
| | 761 | 307 | | return; |
| | | 308 | | |
| | 36 | 309 | | (_contact0, _contact1) = (_contact1, _contact0); |
| | 36 | 310 | | } |
| | | 311 | | |
| | | 312 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 313 | | private ManifoldContact2D GetContactUnchecked(int index) => |
| | 1923 | 314 | | index == 0 ? _contact0 : _contact1; |
| | | 315 | | |
| | | 316 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 317 | | private void SetContactUnchecked(int index, ManifoldContact2D contact) |
| | | 318 | | { |
| | 795 | 319 | | if (index == 0) |
| | | 320 | | { |
| | 742 | 321 | | _contact0 = contact; |
| | 742 | 322 | | return; |
| | | 323 | | } |
| | | 324 | | |
| | 53 | 325 | | _contact1 = contact; |
| | 53 | 326 | | } |
| | | 327 | | |
| | | 328 | | private static ulong CreateContactId( |
| | | 329 | | ContactAnchor2D anchorA, |
| | | 330 | | int featureNamespaceA, |
| | | 331 | | ContactAnchor2D anchorB, |
| | | 332 | | int featureNamespaceB) |
| | | 333 | | { |
| | 798 | 334 | | if (CompareLocalFeature( |
| | 798 | 335 | | featureNamespaceB, |
| | 798 | 336 | | anchorB, |
| | 798 | 337 | | featureNamespaceA, |
| | 798 | 338 | | anchorA) < 0) |
| | | 339 | | { |
| | 581 | 340 | | (anchorA, anchorB) = (anchorB, anchorA); |
| | 581 | 341 | | (featureNamespaceA, featureNamespaceB) = |
| | 581 | 342 | | (featureNamespaceB, featureNamespaceA); |
| | | 343 | | } |
| | | 344 | | |
| | 798 | 345 | | ulong hash = 14695981039346656037UL; |
| | 798 | 346 | | Mix(ref hash, featureNamespaceA); |
| | 798 | 347 | | Mix( |
| | 798 | 348 | | ref hash, |
| | 798 | 349 | | unchecked((long)anchorA.GetLocalFeatureHash64())); |
| | 798 | 350 | | Mix(ref hash, featureNamespaceB); |
| | 798 | 351 | | Mix( |
| | 798 | 352 | | ref hash, |
| | 798 | 353 | | unchecked((long)anchorB.GetLocalFeatureHash64())); |
| | 798 | 354 | | return hash; |
| | | 355 | | } |
| | | 356 | | |
| | | 357 | | private static int CompareLocalFeature( |
| | | 358 | | int leftNamespace, |
| | | 359 | | ContactAnchor2D left, |
| | | 360 | | int rightNamespace, |
| | | 361 | | ContactAnchor2D right) |
| | | 362 | | { |
| | 798 | 363 | | int comparison = leftNamespace.CompareTo(rightNamespace); |
| | 798 | 364 | | return comparison != 0 |
| | 798 | 365 | | ? comparison |
| | 798 | 366 | | : left.CompareLocalFeature(right); |
| | | 367 | | } |
| | | 368 | | |
| | | 369 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 370 | | private static void Mix(ref ulong hash, long value) |
| | | 371 | | { |
| | | 372 | | unchecked |
| | | 373 | | { |
| | 3192 | 374 | | hash ^= (ulong)value; |
| | 3192 | 375 | | hash *= 1099511628211UL; |
| | | 376 | | } |
| | 3192 | 377 | | } |
| | | 378 | | |
| | | 379 | | /// <summary>Enumerates the active contacts in deterministic order.</summary> |
| | | 380 | | public struct Enumerator : IEnumerator<ManifoldContact2D> |
| | | 381 | | { |
| | | 382 | | private readonly ContactManifold2D _manifold; |
| | | 383 | | private int _index; |
| | | 384 | | |
| | | 385 | | internal Enumerator(ContactManifold2D manifold) |
| | | 386 | | { |
| | 41 | 387 | | _manifold = manifold; |
| | 41 | 388 | | _index = -1; |
| | 41 | 389 | | } |
| | | 390 | | |
| | | 391 | | /// <summary>Gets the current contact.</summary> |
| | 83 | 392 | | public ManifoldContact2D Current => _manifold[_index]; |
| | | 393 | | |
| | 3 | 394 | | object IEnumerator.Current => Current; |
| | | 395 | | |
| | | 396 | | /// <summary>Advances to the next active contact.</summary> |
| | | 397 | | public bool MoveNext() |
| | | 398 | | { |
| | 123 | 399 | | int next = _index + 1; |
| | 123 | 400 | | if (next >= _manifold._count) |
| | 40 | 401 | | return false; |
| | | 402 | | |
| | 83 | 403 | | _index = next; |
| | 83 | 404 | | return true; |
| | | 405 | | } |
| | | 406 | | |
| | | 407 | | /// <summary>Resets the enumerator to its initial position.</summary> |
| | 1 | 408 | | public void Reset() => _index = -1; |
| | | 409 | | |
| | | 410 | | /// <summary>Releases enumerator resources.</summary> |
| | 40 | 411 | | public void Dispose() { } |
| | | 412 | | } |
| | | 413 | | } |