< Summary

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

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%66100%
get_Count()100%11100%
Clear()100%22100%
Add(...)100%22100%
AddOrUpdate(...)100%88100%
Remove(...)100%88100%
Sort()100%44100%
Query(...)100%1212100%
CreateSweptSphereBounds(...)100%11100%
CreateSweptBounds(...)100%11100%
CreateBoundsBetween(...)100%11100%
IncludeExtent(...)100%66100%
RemoveExtent(...)100%44100%
RebuildExtents()100%22100%
FindFirstCandidateIndex(...)100%44100%
IsOrderedAt(...)100%66100%
HeapSort()100%44100%
SiftDown(...)100%1010100%
Swap(...)100%22100%
Compare(...)100%1212100%
.ctor(...)100%11100%
Intersects(...)100%1010100%

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;
 2034022    private bool _isSorted = true;
 23
 2034024    public DynamicCcdCandidateIndex(int capacity = 0, bool supportsUpdates = false)
 25    {
 2034026        _entries = capacity > 0 ? new SwiftList<Entry>(capacity) : new SwiftList<Entry>();
 2034027        _entryIndices = !supportsUpdates
 2034028            ? null
 2034029            : capacity > 0
 2034030                ? new SwiftDictionary<int, int>(capacity)
 2034031                : new SwiftDictionary<int, int>();
 2034032    }
 33
 89734    public int Count => _entries.Count;
 35
 36    public void Clear()
 37    {
 697638        _entries.FastClear();
 697639        _entryIndices?.Clear();
 697640        _maxExtentX = Fixed64.Zero;
 697641        _maxExtentXCount = 0;
 697642        _unrepresentableExtentXCount = 0;
 697643        _isSorted = true;
 697644    }
 45
 46    public void Add(int dynamicId, FixedBoundVolume bounds)
 47    {
 1450248        _entryIndices?.Add(dynamicId, _entries.Count);
 1450249        _entries.Add(new Entry(dynamicId, bounds));
 1450250        IncludeExtent(bounds.Min.X, bounds.Max.X);
 51
 1450252        _isSorted = false;
 1450253    }
 54
 55    public void AddOrUpdate(int dynamicId, FixedBoundVolume bounds)
 56    {
 70457        SwiftDictionary<int, int>? entryIndices = _entryIndices;
 70458        SwiftThrowHelper.ThrowIfTrue(
 70459            entryIndices == null,
 70460            nameof(DynamicCcdCandidateIndex),
 70461            "Candidate index was not configured for updates.");
 70462        if (entryIndices.TryGetValue(dynamicId, out int index))
 63        {
 28164            Entry previous = _entries[index];
 28165            _entries[index] = new Entry(dynamicId, bounds);
 28166            bool remainsSorted = _isSorted && IsOrderedAt(index);
 28167            if (!DynamicCcdExtentMetadata.IsEquivalent(
 28168                    previous.MinX,
 28169                    previous.MaxX,
 28170                    bounds.Min.X,
 28171                    bounds.Max.X))
 72            {
 6573                if (RemoveExtent(previous.MinX, previous.MaxX))
 4774                    RebuildExtents();
 75                else
 1876                    IncludeExtent(bounds.Min.X, bounds.Max.X);
 77            }
 78
 28179            _isSorted = remainsSorted;
 28180            return;
 81        }
 82
 42383        Add(dynamicId, bounds);
 42384    }
 85
 86    public bool Remove(int dynamicId)
 87    {
 1188        SwiftDictionary<int, int>? entryIndices = _entryIndices;
 1189        if (entryIndices == null || !entryIndices.TryGetValue(dynamicId, out int index))
 590            return false;
 91
 692        Entry removed = _entries[index];
 693        int lastIndex = _entries.Count - 1;
 694        if (index != lastIndex)
 95        {
 396            Entry moved = _entries[lastIndex];
 397            _entries[index] = moved;
 398            entryIndices[moved.DynamicId] = index;
 99        }
 100
 6101        _entries.RemoveAt(lastIndex);
 6102        entryIndices.Remove(dynamicId);
 6103        if (RemoveExtent(removed.MinX, removed.MaxX))
 3104            RebuildExtents();
 6105        _isSorted = false;
 6106        return true;
 107    }
 108
 109    public void Sort()
 110    {
 17819111        if (!_isSorted && _entries.Count > 1)
 1488112            HeapSort();
 113
 17819114        _isSorted = true;
 17819115    }
 116
 117    public void Query(FixedBoundVolume queryBounds, SwiftList<int> results)
 118    {
 28749119        results.FastClear();
 28749120        if (_entries.Count == 0)
 13643121            return;
 122
 15106123        Sort();
 15106124        Fixed64 scanMinX = _unrepresentableExtentXCount > 0
 15106125            || !Fixed64.TrySubtract(queryBounds.Min.X, _maxExtentX, out Fixed64 representableScanMinX)
 15106126                ? Fixed64.MinValue
 15106127                : representableScanMinX;
 15106128        int index = FindFirstCandidateIndex(scanMinX);
 415544129        for (; index < _entries.Count; index++)
 130        {
 211780131            Entry entry = _entries[index];
 211780132            if (entry.MinX > queryBounds.Max.X)
 133                break;
 134
 200219135            if (entry.Intersects(queryBounds))
 15789136                results.Add(entry.DynamicId);
 137        }
 15106138    }
 139
 140    public static FixedBoundVolume CreateSweptSphereBounds(Vector3d start, Vector3d displacement, Fixed64 radius)
 13998141        => CreateBoundsBetween(start, start + displacement, Vector3d.One * radius);
 142
 143    public static FixedBoundVolume CreateSweptBounds(
 144        Vector3d start,
 145        Vector3d displacement,
 146        Vector3d extents) =>
 1147        CreateBoundsBetween(start, start + displacement, extents);
 148
 149    public static FixedBoundVolume CreateBoundsBetween(
 150        Vector3d start,
 151        Vector3d end,
 152        Vector3d extents)
 153    {
 27832154        return new FixedBoundVolume(Vector3d.Min(start, end) - extents, Vector3d.Max(start, end) + extents);
 155    }
 156
 157    private void IncludeExtent(Fixed64 minX, Fixed64 maxX)
 158    {
 14601159        if (!Fixed64.TrySubtract(maxX, minX, out Fixed64 extentX))
 160        {
 22161            _unrepresentableExtentXCount++;
 22162            return;
 163        }
 164
 14579165        if (extentX > _maxExtentX)
 166        {
 5332167            _maxExtentX = extentX;
 5332168            _maxExtentXCount = 1;
 169        }
 9247170        else if (extentX == _maxExtentX)
 171        {
 7249172            _maxExtentXCount++;
 173        }
 9247174    }
 175
 176    private bool RemoveExtent(Fixed64 minX, Fixed64 maxX)
 177    {
 71178        if (!Fixed64.TrySubtract(maxX, minX, out Fixed64 extentX))
 179        {
 2180            _unrepresentableExtentXCount--;
 2181            return false;
 182        }
 183
 69184        return extentX == _maxExtentX && --_maxExtentXCount == 0;
 185    }
 186
 187    private void RebuildExtents()
 188    {
 50189        _maxExtentX = Fixed64.Zero;
 50190        _maxExtentXCount = 0;
 50191        _unrepresentableExtentXCount = 0;
 262192        for (int i = 0; i < _entries.Count; i++)
 81193            IncludeExtent(_entries[i].MinX, _entries[i].MaxX);
 50194    }
 195
 196    private int FindFirstCandidateIndex(Fixed64 minX)
 197    {
 15106198        int low = 0;
 15106199        int high = _entries.Count;
 117733200        while (low < high)
 201        {
 102627202            int middle = low + ((high - low) >> 1);
 102627203            if (_entries[middle].MinX < minX)
 67607204                low = middle + 1;
 205            else
 35020206                high = middle;
 207        }
 208
 15106209        return low;
 210    }
 211
 212    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 213    private bool IsOrderedAt(int index)
 214    {
 251215        Entry entry = _entries[index];
 251216        return (index == 0 || Compare(_entries[index - 1], entry) <= 0)
 251217            && (index == _entries.Count - 1 || Compare(entry, _entries[index + 1]) <= 0);
 218    }
 219
 220    private void HeapSort()
 221    {
 1488222        int count = _entries.Count;
 15620223        for (int start = (count >> 1) - 1; start >= 0; start--)
 6322224            SiftDown(start, count);
 225
 26078226        for (int end = count - 1; end > 0; end--)
 227        {
 11551228            Swap(0, end);
 11551229            SiftDown(0, end);
 230        }
 1488231    }
 232
 233    private void SiftDown(int root, int count)
 234    {
 48295235        while (true)
 236        {
 66168237            int child = (root << 1) + 1;
 66168238            if (child >= count)
 15426239                return;
 240
 50742241            int swapIndex = root;
 50742242            if (Compare(_entries[swapIndex], _entries[child]) < 0)
 46572243                swapIndex = child;
 244
 50742245            int right = child + 1;
 50742246            if (right < count && Compare(_entries[swapIndex], _entries[right]) < 0)
 22306247                swapIndex = right;
 248
 50742249            if (swapIndex == root)
 2447250                return;
 251
 48295252            Swap(root, swapIndex);
 48295253            root = swapIndex;
 254        }
 255    }
 256
 257    private void Swap(int first, int second)
 258    {
 59846259        Entry firstEntry = _entries[first];
 59846260        Entry secondEntry = _entries[second];
 59846261        _entries[first] = secondEntry;
 59846262        _entries[second] = firstEntry;
 59846263        if (_entryIndices != null)
 264        {
 122265            _entryIndices[secondEntry.DynamicId] = first;
 122266            _entryIndices[firstEntry.DynamicId] = second;
 267        }
 59846268    }
 269
 270    private static int Compare(Entry x, Entry y)
 271    {
 98570272        int result = x.MinX.CompareTo(y.MinX);
 98570273        if (result != 0)
 76847274            return result;
 275
 21723276        result = x.MinY.CompareTo(y.MinY);
 21723277        if (result != 0)
 82278            return result;
 279
 21641280        result = x.MinZ.CompareTo(y.MinZ);
 21641281        if (result != 0)
 21613282            return result;
 283
 28284        result = x.MaxX.CompareTo(y.MaxX);
 28285        if (result != 0)
 5286            return result;
 287
 23288        result = x.MaxY.CompareTo(y.MaxY);
 23289        if (result != 0)
 2290            return result;
 291
 21292        result = x.MaxZ.CompareTo(y.MaxZ);
 21293        if (result != 0)
 3294            return result;
 295
 18296        return x.DynamicId.CompareTo(y.DynamicId);
 297    }
 298
 299    private readonly struct Entry
 300    {
 301        public Entry(int dynamicId, FixedBoundVolume bounds)
 302        {
 14783303            DynamicId = dynamicId;
 14783304            MinX = bounds.Min.X;
 14783305            MinY = bounds.Min.Y;
 14783306            MinZ = bounds.Min.Z;
 14783307            MaxX = bounds.Max.X;
 14783308            MaxY = bounds.Max.Y;
 14783309            MaxZ = bounds.Max.Z;
 14783310        }
 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        {
 200219323            return !(MinX > queryBounds.Max.X || MaxX < queryBounds.Min.X ||
 200219324                     MinY > queryBounds.Max.Y || MaxY < queryBounds.Min.Y ||
 200219325                     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    {
 334        MinX = minX;
 335        MinZ = minZ;
 336        MaxX = maxX;
 337        MaxZ = maxZ;
 338    }
 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}