< Summary

Information
Class: Gravitas.CollisionHandling.DynamicCcdPlanarBounds
Assembly: Gravitas
File(s): /home/runner/work/Gravitas/Gravitas/src/Gravitas/CollisionHandling/Continuous/DynamicCcdCandidateIndex.cs
Line coverage
100%
Covered lines: 5
Uncovered lines: 0
Coverable lines: 5
Total lines: 660
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%

File(s)

/home/runner/work/Gravitas/Gravitas/src/Gravitas/CollisionHandling/Continuous/DynamicCcdCandidateIndex.cs

#LineLine coverage
 1//=======================================================================
 2// DynamicCcdCandidateIndex.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
 8using FixedMathSharp;
 9using SwiftCollections;
 10using SwiftCollections.Query;
 11using System.Runtime.CompilerServices;
 12
 13namespace Gravitas.CollisionHandling;
 14
 15internal sealed class DynamicCcdCandidateIndex
 16{
 17    private readonly SwiftList<Entry> _entries;
 18    private readonly SwiftDictionary<int, int>? _entryIndices;
 19    private Fixed64 _maxExtentX;
 20    private int _maxExtentXCount;
 21    private int _unrepresentableExtentXCount;
 22    private bool _isSorted = true;
 23
 24    public DynamicCcdCandidateIndex(int capacity = 0, bool supportsUpdates = false)
 25    {
 26        _entries = capacity > 0 ? new SwiftList<Entry>(capacity) : new SwiftList<Entry>();
 27        _entryIndices = !supportsUpdates
 28            ? null
 29            : capacity > 0
 30                ? new SwiftDictionary<int, int>(capacity)
 31                : new SwiftDictionary<int, int>();
 32    }
 33
 34    public int Count => _entries.Count;
 35
 36    public void Clear()
 37    {
 38        _entries.FastClear();
 39        _entryIndices?.Clear();
 40        _maxExtentX = Fixed64.Zero;
 41        _maxExtentXCount = 0;
 42        _unrepresentableExtentXCount = 0;
 43        _isSorted = true;
 44    }
 45
 46    public void Add(int dynamicId, FixedBoundVolume bounds)
 47    {
 48        _entryIndices?.Add(dynamicId, _entries.Count);
 49        _entries.Add(new Entry(dynamicId, bounds));
 50        IncludeExtent(bounds.Min.X, bounds.Max.X);
 51
 52        _isSorted = false;
 53    }
 54
 55    public void AddOrUpdate(int dynamicId, FixedBoundVolume bounds)
 56    {
 57        SwiftDictionary<int, int>? entryIndices = _entryIndices;
 58        SwiftThrowHelper.ThrowIfTrue(
 59            entryIndices == null,
 60            nameof(DynamicCcdCandidateIndex),
 61            "Candidate index was not configured for updates.");
 62        if (entryIndices.TryGetValue(dynamicId, out int index))
 63        {
 64            Entry previous = _entries[index];
 65            _entries[index] = new Entry(dynamicId, bounds);
 66            bool remainsSorted = _isSorted && IsOrderedAt(index);
 67            if (!DynamicCcdExtentMetadata.IsEquivalent(
 68                    previous.MinX,
 69                    previous.MaxX,
 70                    bounds.Min.X,
 71                    bounds.Max.X))
 72            {
 73                if (RemoveExtent(previous.MinX, previous.MaxX))
 74                    RebuildExtents();
 75                else
 76                    IncludeExtent(bounds.Min.X, bounds.Max.X);
 77            }
 78
 79            _isSorted = remainsSorted;
 80            return;
 81        }
 82
 83        Add(dynamicId, bounds);
 84    }
 85
 86    public bool Remove(int dynamicId)
 87    {
 88        SwiftDictionary<int, int>? entryIndices = _entryIndices;
 89        if (entryIndices == null || !entryIndices.TryGetValue(dynamicId, out int index))
 90            return false;
 91
 92        Entry removed = _entries[index];
 93        int lastIndex = _entries.Count - 1;
 94        if (index != lastIndex)
 95        {
 96            Entry moved = _entries[lastIndex];
 97            _entries[index] = moved;
 98            entryIndices[moved.DynamicId] = index;
 99        }
 100
 101        _entries.RemoveAt(lastIndex);
 102        entryIndices.Remove(dynamicId);
 103        if (RemoveExtent(removed.MinX, removed.MaxX))
 104            RebuildExtents();
 105        _isSorted = false;
 106        return true;
 107    }
 108
 109    public void Sort()
 110    {
 111        if (!_isSorted && _entries.Count > 1)
 112            HeapSort();
 113
 114        _isSorted = true;
 115    }
 116
 117    public void Query(FixedBoundVolume queryBounds, SwiftList<int> results)
 118    {
 119        results.FastClear();
 120        if (_entries.Count == 0)
 121            return;
 122
 123        Sort();
 124        Fixed64 scanMinX = _unrepresentableExtentXCount > 0
 125            || !Fixed64.TrySubtract(queryBounds.Min.X, _maxExtentX, out Fixed64 representableScanMinX)
 126                ? Fixed64.MinValue
 127                : representableScanMinX;
 128        int index = FindFirstCandidateIndex(scanMinX);
 129        for (; index < _entries.Count; index++)
 130        {
 131            Entry entry = _entries[index];
 132            if (entry.MinX > queryBounds.Max.X)
 133                break;
 134
 135            if (entry.Intersects(queryBounds))
 136                results.Add(entry.DynamicId);
 137        }
 138    }
 139
 140    public static FixedBoundVolume CreateSweptSphereBounds(Vector3d start, Vector3d displacement, Fixed64 radius)
 141        => CreateBoundsBetween(start, start + displacement, Vector3d.One * radius);
 142
 143    public static FixedBoundVolume CreateSweptBounds(
 144        Vector3d start,
 145        Vector3d displacement,
 146        Vector3d extents) =>
 147        CreateBoundsBetween(start, start + displacement, extents);
 148
 149    public static FixedBoundVolume CreateBoundsBetween(
 150        Vector3d start,
 151        Vector3d end,
 152        Vector3d extents)
 153    {
 154        return new FixedBoundVolume(Vector3d.Min(start, end) - extents, Vector3d.Max(start, end) + extents);
 155    }
 156
 157    private void IncludeExtent(Fixed64 minX, Fixed64 maxX)
 158    {
 159        if (!Fixed64.TrySubtract(maxX, minX, out Fixed64 extentX))
 160        {
 161            _unrepresentableExtentXCount++;
 162            return;
 163        }
 164
 165        if (extentX > _maxExtentX)
 166        {
 167            _maxExtentX = extentX;
 168            _maxExtentXCount = 1;
 169        }
 170        else if (extentX == _maxExtentX)
 171        {
 172            _maxExtentXCount++;
 173        }
 174    }
 175
 176    private bool RemoveExtent(Fixed64 minX, Fixed64 maxX)
 177    {
 178        if (!Fixed64.TrySubtract(maxX, minX, out Fixed64 extentX))
 179        {
 180            _unrepresentableExtentXCount--;
 181            return false;
 182        }
 183
 184        return extentX == _maxExtentX && --_maxExtentXCount == 0;
 185    }
 186
 187    private void RebuildExtents()
 188    {
 189        _maxExtentX = Fixed64.Zero;
 190        _maxExtentXCount = 0;
 191        _unrepresentableExtentXCount = 0;
 192        for (int i = 0; i < _entries.Count; i++)
 193            IncludeExtent(_entries[i].MinX, _entries[i].MaxX);
 194    }
 195
 196    private int FindFirstCandidateIndex(Fixed64 minX)
 197    {
 198        int low = 0;
 199        int high = _entries.Count;
 200        while (low < high)
 201        {
 202            int middle = low + ((high - low) >> 1);
 203            if (_entries[middle].MinX < minX)
 204                low = middle + 1;
 205            else
 206                high = middle;
 207        }
 208
 209        return low;
 210    }
 211
 212    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 213    private bool IsOrderedAt(int index)
 214    {
 215        Entry entry = _entries[index];
 216        return (index == 0 || Compare(_entries[index - 1], entry) <= 0)
 217            && (index == _entries.Count - 1 || Compare(entry, _entries[index + 1]) <= 0);
 218    }
 219
 220    private void HeapSort()
 221    {
 222        int count = _entries.Count;
 223        for (int start = (count >> 1) - 1; start >= 0; start--)
 224            SiftDown(start, count);
 225
 226        for (int end = count - 1; end > 0; end--)
 227        {
 228            Swap(0, end);
 229            SiftDown(0, end);
 230        }
 231    }
 232
 233    private void SiftDown(int root, int count)
 234    {
 235        while (true)
 236        {
 237            int child = (root << 1) + 1;
 238            if (child >= count)
 239                return;
 240
 241            int swapIndex = root;
 242            if (Compare(_entries[swapIndex], _entries[child]) < 0)
 243                swapIndex = child;
 244
 245            int right = child + 1;
 246            if (right < count && Compare(_entries[swapIndex], _entries[right]) < 0)
 247                swapIndex = right;
 248
 249            if (swapIndex == root)
 250                return;
 251
 252            Swap(root, swapIndex);
 253            root = swapIndex;
 254        }
 255    }
 256
 257    private void Swap(int first, int second)
 258    {
 259        Entry firstEntry = _entries[first];
 260        Entry secondEntry = _entries[second];
 261        _entries[first] = secondEntry;
 262        _entries[second] = firstEntry;
 263        if (_entryIndices != null)
 264        {
 265            _entryIndices[secondEntry.DynamicId] = first;
 266            _entryIndices[firstEntry.DynamicId] = second;
 267        }
 268    }
 269
 270    private static int Compare(Entry x, Entry y)
 271    {
 272        int result = x.MinX.CompareTo(y.MinX);
 273        if (result != 0)
 274            return result;
 275
 276        result = x.MinY.CompareTo(y.MinY);
 277        if (result != 0)
 278            return result;
 279
 280        result = x.MinZ.CompareTo(y.MinZ);
 281        if (result != 0)
 282            return result;
 283
 284        result = x.MaxX.CompareTo(y.MaxX);
 285        if (result != 0)
 286            return result;
 287
 288        result = x.MaxY.CompareTo(y.MaxY);
 289        if (result != 0)
 290            return result;
 291
 292        result = x.MaxZ.CompareTo(y.MaxZ);
 293        if (result != 0)
 294            return result;
 295
 296        return x.DynamicId.CompareTo(y.DynamicId);
 297    }
 298
 299    private readonly struct Entry
 300    {
 301        public Entry(int dynamicId, FixedBoundVolume bounds)
 302        {
 303            DynamicId = dynamicId;
 304            MinX = bounds.Min.X;
 305            MinY = bounds.Min.Y;
 306            MinZ = bounds.Min.Z;
 307            MaxX = bounds.Max.X;
 308            MaxY = bounds.Max.Y;
 309            MaxZ = bounds.Max.Z;
 310        }
 311
 312        public int DynamicId { get; }
 313        public Fixed64 MinX { get; }
 314        public Fixed64 MinY { get; }
 315        public Fixed64 MinZ { get; }
 316        public Fixed64 MaxX { get; }
 317        public Fixed64 MaxY { get; }
 318        public Fixed64 MaxZ { get; }
 319
 320        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 321        public bool Intersects(FixedBoundVolume queryBounds)
 322        {
 323            return !(MinX > queryBounds.Max.X || MaxX < queryBounds.Min.X ||
 324                     MinY > queryBounds.Max.Y || MaxY < queryBounds.Min.Y ||
 325                     MinZ > queryBounds.Max.Z || MaxZ < queryBounds.Min.Z);
 326        }
 327    }
 328}
 329
 330internal readonly struct DynamicCcdPlanarBounds
 331{
 332    public DynamicCcdPlanarBounds(Fixed64 minX, Fixed64 minZ, Fixed64 maxX, Fixed64 maxZ)
 333    {
 27153334        MinX = minX;
 27153335        MinZ = minZ;
 27153336        MaxX = maxX;
 27153337        MaxZ = maxZ;
 27153338    }
 339
 340    public Fixed64 MinX { get; }
 341    public Fixed64 MinZ { get; }
 342    public Fixed64 MaxX { get; }
 343    public Fixed64 MaxZ { get; }
 344}
 345
 346internal sealed class DynamicCcdCandidateIndex2D
 347{
 348    private readonly SwiftList<Entry> _entries;
 349    private readonly SwiftDictionary<int, int>? _entryIndices;
 350    private Fixed64 _maxExtentX;
 351    private int _maxExtentXCount;
 352    private int _unrepresentableExtentXCount;
 353    private bool _isSorted = true;
 354
 355    public DynamicCcdCandidateIndex2D(int capacity = 0, bool supportsUpdates = false)
 356    {
 357        _entries = capacity > 0 ? new SwiftList<Entry>(capacity) : new SwiftList<Entry>();
 358        _entryIndices = !supportsUpdates
 359            ? null
 360            : capacity > 0
 361                ? new SwiftDictionary<int, int>(capacity)
 362                : new SwiftDictionary<int, int>();
 363    }
 364
 365    public int Count => _entries.Count;
 366
 367    public void Clear()
 368    {
 369        _entries.FastClear();
 370        _entryIndices?.Clear();
 371        _maxExtentX = Fixed64.Zero;
 372        _maxExtentXCount = 0;
 373        _unrepresentableExtentXCount = 0;
 374        _isSorted = true;
 375    }
 376
 377    public void Add(int dynamicId, DynamicCcdPlanarBounds bounds)
 378    {
 379        _entryIndices?.Add(dynamicId, _entries.Count);
 380        _entries.Add(new Entry(dynamicId, bounds));
 381        IncludeExtent(bounds.MinX, bounds.MaxX);
 382
 383        _isSorted = false;
 384    }
 385
 386    public void AddOrUpdate(int dynamicId, DynamicCcdPlanarBounds bounds)
 387    {
 388        SwiftDictionary<int, int>? entryIndices = _entryIndices;
 389        SwiftThrowHelper.ThrowIfTrue(
 390            entryIndices == null,
 391            nameof(DynamicCcdCandidateIndex2D),
 392            "Candidate index was not configured for updates.");
 393        if (entryIndices.TryGetValue(dynamicId, out int index))
 394        {
 395            Entry previous = _entries[index];
 396            _entries[index] = new Entry(dynamicId, bounds);
 397            bool remainsSorted = _isSorted && IsOrderedAt(index);
 398            if (!DynamicCcdExtentMetadata.IsEquivalent(
 399                    previous.MinX,
 400                    previous.MaxX,
 401                    bounds.MinX,
 402                    bounds.MaxX))
 403            {
 404                if (RemoveExtent(previous.MinX, previous.MaxX))
 405                    RebuildExtents();
 406                else
 407                    IncludeExtent(bounds.MinX, bounds.MaxX);
 408            }
 409
 410            _isSorted = remainsSorted;
 411            return;
 412        }
 413
 414        Add(dynamicId, bounds);
 415    }
 416
 417    public bool Remove(int dynamicId)
 418    {
 419        SwiftDictionary<int, int>? entryIndices = _entryIndices;
 420        if (entryIndices == null || !entryIndices.TryGetValue(dynamicId, out int index))
 421            return false;
 422
 423        Entry removed = _entries[index];
 424        int lastIndex = _entries.Count - 1;
 425        if (index != lastIndex)
 426        {
 427            Entry moved = _entries[lastIndex];
 428            _entries[index] = moved;
 429            entryIndices[moved.DynamicId] = index;
 430        }
 431
 432        _entries.RemoveAt(lastIndex);
 433        entryIndices.Remove(dynamicId);
 434        if (RemoveExtent(removed.MinX, removed.MaxX))
 435            RebuildExtents();
 436        _isSorted = false;
 437        return true;
 438    }
 439
 440    public void Sort()
 441    {
 442        if (!_isSorted && _entries.Count > 1)
 443            HeapSort();
 444
 445        _isSorted = true;
 446    }
 447
 448    public void Query(DynamicCcdPlanarBounds queryBounds, SwiftList<int> results)
 449    {
 450        results.FastClear();
 451        if (_entries.Count == 0)
 452            return;
 453
 454        Sort();
 455        Fixed64 scanMinX = _unrepresentableExtentXCount > 0
 456            || !Fixed64.TrySubtract(queryBounds.MinX, _maxExtentX, out Fixed64 representableScanMinX)
 457                ? Fixed64.MinValue
 458                : representableScanMinX;
 459        int index = FindFirstCandidateIndex(scanMinX);
 460        for (; index < _entries.Count; index++)
 461        {
 462            Entry entry = _entries[index];
 463            if (entry.MinX > queryBounds.MaxX)
 464                break;
 465
 466            if (entry.Intersects(queryBounds))
 467                results.Add(entry.DynamicId);
 468        }
 469    }
 470
 471    public static DynamicCcdPlanarBounds CreateSweptCircleBounds(Vector2d start, Vector2d displacement, Fixed64 radius)
 472        => CreateBoundsBetween(start, start + displacement, radius);
 473
 474    public static DynamicCcdPlanarBounds CreateBoundsBetween(
 475        Vector2d start,
 476        Vector2d end,
 477        Fixed64 radius)
 478    {
 479        Fixed64 minX = FixedMath.Min(start.X, end.X) - radius;
 480        Fixed64 maxX = FixedMath.Max(start.X, end.X) + radius;
 481        Fixed64 minZ = FixedMath.Min(start.Y, end.Y) - radius;
 482        Fixed64 maxZ = FixedMath.Max(start.Y, end.Y) + radius;
 483        return new DynamicCcdPlanarBounds(minX, minZ, maxX, maxZ);
 484    }
 485
 486    private void IncludeExtent(Fixed64 minX, Fixed64 maxX)
 487    {
 488        if (!Fixed64.TrySubtract(maxX, minX, out Fixed64 extentX))
 489        {
 490            _unrepresentableExtentXCount++;
 491            return;
 492        }
 493
 494        if (extentX > _maxExtentX)
 495        {
 496            _maxExtentX = extentX;
 497            _maxExtentXCount = 1;
 498        }
 499        else if (extentX == _maxExtentX)
 500        {
 501            _maxExtentXCount++;
 502        }
 503    }
 504
 505    private bool RemoveExtent(Fixed64 minX, Fixed64 maxX)
 506    {
 507        if (!Fixed64.TrySubtract(maxX, minX, out Fixed64 extentX))
 508        {
 509            _unrepresentableExtentXCount--;
 510            return false;
 511        }
 512
 513        return extentX == _maxExtentX && --_maxExtentXCount == 0;
 514    }
 515
 516    private void RebuildExtents()
 517    {
 518        _maxExtentX = Fixed64.Zero;
 519        _maxExtentXCount = 0;
 520        _unrepresentableExtentXCount = 0;
 521        for (int i = 0; i < _entries.Count; i++)
 522            IncludeExtent(_entries[i].MinX, _entries[i].MaxX);
 523    }
 524
 525    private int FindFirstCandidateIndex(Fixed64 minX)
 526    {
 527        int low = 0;
 528        int high = _entries.Count;
 529        while (low < high)
 530        {
 531            int middle = low + ((high - low) >> 1);
 532            if (_entries[middle].MinX < minX)
 533                low = middle + 1;
 534            else
 535                high = middle;
 536        }
 537
 538        return low;
 539    }
 540
 541    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 542    private bool IsOrderedAt(int index)
 543    {
 544        Entry entry = _entries[index];
 545        return (index == 0 || Compare(_entries[index - 1], entry) <= 0)
 546            && (index == _entries.Count - 1 || Compare(entry, _entries[index + 1]) <= 0);
 547    }
 548
 549    private void HeapSort()
 550    {
 551        int count = _entries.Count;
 552        for (int start = (count >> 1) - 1; start >= 0; start--)
 553            SiftDown(start, count);
 554
 555        for (int end = count - 1; end > 0; end--)
 556        {
 557            Swap(0, end);
 558            SiftDown(0, end);
 559        }
 560    }
 561
 562    private void SiftDown(int root, int count)
 563    {
 564        while (true)
 565        {
 566            int child = (root << 1) + 1;
 567            if (child >= count)
 568                return;
 569
 570            int swapIndex = root;
 571            if (Compare(_entries[swapIndex], _entries[child]) < 0)
 572                swapIndex = child;
 573
 574            int right = child + 1;
 575            if (right < count && Compare(_entries[swapIndex], _entries[right]) < 0)
 576                swapIndex = right;
 577
 578            if (swapIndex == root)
 579                return;
 580
 581            Swap(root, swapIndex);
 582            root = swapIndex;
 583        }
 584    }
 585
 586    private void Swap(int first, int second)
 587    {
 588        Entry firstEntry = _entries[first];
 589        Entry secondEntry = _entries[second];
 590        _entries[first] = secondEntry;
 591        _entries[second] = firstEntry;
 592        if (_entryIndices != null)
 593        {
 594            _entryIndices[secondEntry.DynamicId] = first;
 595            _entryIndices[firstEntry.DynamicId] = second;
 596        }
 597    }
 598
 599    private static int Compare(Entry x, Entry y)
 600    {
 601        int result = x.MinX.CompareTo(y.MinX);
 602        if (result != 0)
 603            return result;
 604
 605        result = x.MinZ.CompareTo(y.MinZ);
 606        if (result != 0)
 607            return result;
 608
 609        result = x.MaxX.CompareTo(y.MaxX);
 610        if (result != 0)
 611            return result;
 612
 613        result = x.MaxZ.CompareTo(y.MaxZ);
 614        if (result != 0)
 615            return result;
 616
 617        return x.DynamicId.CompareTo(y.DynamicId);
 618    }
 619
 620    private readonly struct Entry
 621    {
 622        public Entry(int dynamicId, DynamicCcdPlanarBounds bounds)
 623        {
 624            DynamicId = dynamicId;
 625            MinX = bounds.MinX;
 626            MinZ = bounds.MinZ;
 627            MaxX = bounds.MaxX;
 628            MaxZ = bounds.MaxZ;
 629        }
 630
 631        public int DynamicId { get; }
 632        public Fixed64 MinX { get; }
 633        public Fixed64 MinZ { get; }
 634        public Fixed64 MaxX { get; }
 635        public Fixed64 MaxZ { get; }
 636
 637        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 638        public bool Intersects(DynamicCcdPlanarBounds queryBounds)
 639        {
 640            return !(MinX > queryBounds.MaxX || MaxX < queryBounds.MinX ||
 641                     MinZ > queryBounds.MaxZ || MaxZ < queryBounds.MinZ);
 642        }
 643    }
 644}
 645
 646file static class DynamicCcdExtentMetadata
 647{
 648    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 649    public static bool IsEquivalent(
 650        Fixed64 previousMinX,
 651        Fixed64 previousMaxX,
 652        Fixed64 currentMinX,
 653        Fixed64 currentMaxX)
 654    {
 655        bool previousRepresentable = Fixed64.TrySubtract(previousMaxX, previousMinX, out Fixed64 previousExtentX);
 656        bool currentRepresentable = Fixed64.TrySubtract(currentMaxX, currentMinX, out Fixed64 currentExtentX);
 657        return previousRepresentable == currentRepresentable
 658            && (!previousRepresentable || previousExtentX == currentExtentX);
 659    }
 660}