< Summary

Line coverage
100%
Covered lines: 1346
Uncovered lines: 0
Coverable lines: 1346
Total lines: 2560
Line coverage: 100%
Branch coverage
100%
Covered branches: 438
Total branches: 438
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: CreateTopologyTriangles(...)100%1010100%
File 1: CompareVertexUses(...)100%66100%
File 1: CreateConvexSatEdgeVertexPairs(...)100%1818100%
File 1: RegisterBoundaryEdge(...)100%1010100%
File 1: CalculateSharedEdgeSide(...)100%44100%
File 1: ValidateOpenConvexBoundary(...)100%22100%
File 1: GetDominantNormalAxis(...)100%66100%
File 1: ProjectToBoundaryPlane(...)100%44100%
File 1: ValidateBoundaryMatchesConvexHull(...)100%1414100%
File 1: ValidateProjectedTriangleFill(...)100%66100%
File 1: AppendConvexHullVertex(...)100%44100%
File 1: MatchesCyclicHull(...)100%66100%
File 1: CompareBoundaryVertices(...)100%22100%
File 1: HasSingleVertexLinks(...)100%1212100%
File 1: FindTriangleCornerIndex(...)100%44100%
File 1: CompareVertexCornerUses(...)100%22100%
File 1: CreateSatEdgeVertexPairs(...)100%44100%
File 1: CountConvexSatEdges(...)100%44100%
File 1: FindEdgeUseGroupEnd(...)100%44100%
File 1: ShouldIncludeConvexSatEdge(...)100%66100%
File 1: FindOppositeVertexIndex(...)100%88100%
File 1: .ctor(...)100%11100%
File 1: .ctor(...)100%11100%
File 1: .ctor(...)100%11100%
File 2: .cctor()100%11100%
File 2: get_LocalVertices()100%11100%
File 2: get_ScaledLocalVertices()100%11100%
File 2: get_VertexCount()100%11100%
File 2: get_Triangles()100%11100%
File 2: get_TriangleCount()100%11100%
File 2: get_ConvexSatEdgeVertexPairs()100%11100%
File 2: get_TotalArea()100%11100%
File 2: get_TriangleBVH()100%11100%
File 2: get_TriangleBvhBuildCount()100%11100%
File 2: .ctor(...)100%1010100%
File 2: get_Origin()100%11100%
File 2: get_Rotation()100%11100%
File 2: get_Bounds()100%11100%
File 2: get_LocalBounds()100%11100%
File 2: .ctor(...)100%11100%
File 2: UpdatePosition(...)100%11100%
File 2: UpdateTransform(...)100%11100%
File 2: ValidateInput(...)100%1010100%
File 2: BuildTriangleBVH(...)100%22100%
File 2: CalculateBounds(...)100%22100%
File 2: CompareEdgeUses(...)100%88100%
File 2: GetFrontalArea(...)100%66100%
File 2: GetTriangleVertices(...)100%22100%
File 2: TryGetTriangleVertices(...)100%22100%
File 2: GetLocalTriangleVertices(...)100%11100%
File 2: GetScaledLocalFaceNormal(...)100%11100%
File 2: TryGetVertexWorld(...)100%11100%
File 2: GetVertexWorld(...)100%22100%
File 2: TryGetSupportVertexWorld(...)100%11100%
File 2: GetSupportVertexWorld(...)100%22100%
File 2: GetSupportVertexLocal(...)100%1010100%
File 2: FindSupportVertexIndex(...)100%1212100%
File 2: SearchSupportLeaf(...)100%88100%
File 2: PushSupportNode(...)100%66100%
File 2: ComesBeforeSupportNode(...)100%22100%
File 2: BuildSupportTreeNode(...)100%22100%
File 2: RefitSupportTree(...)100%44100%
File 2: CalculateSupportRangeBounds(...)100%44100%
File 2: CreateSupportVertexIndices(...)100%22100%
File 2: GetDominantAxis(...)100%66100%
File 2: GetBoundsSupportPoint(...)100%66100%
File 2: GetFaceNormalWorld(...)100%11100%
File 2: GetTrianglesInWorldBounds(...)100%11100%
File 2: GetTrianglesInLocalBounds(...)100%11100%
File 2: TryConvertWorldToScaledLocal(...)100%11100%
File 2: ConvertWorldToScaledLocal(...)100%22100%
File 2: TryConvertScaledLocalToWorld(...)100%11100%
File 2: ConvertScaledLocalToWorld(...)100%22100%
File 2: CreatePointAnchor(...)100%11100%
File 2: TryConvertWorldDirectionToLocal(...)100%11100%
File 2: ConvertWorldDirectionToLocal(...)100%22100%
File 2: .ctor(...)100%11100%
File 2: get_StartVertexIndex()100%11100%
File 2: get_EndVertexIndex()100%11100%
File 2: Create(...)100%66100%
File 2: .ctor(...)100%11100%
File 2: Create(...)100%66100%
File 2: .ctor(...)100%11100%
File 2: get_IsLeaf()100%11100%
File 2: CreateLeaf(...)100%11100%
File 2: CreateBranch(...)100%11100%
File 2: .ctor()100%11100%
File 2: Reset(...)100%11100%
File 2: Compare(...)100%22100%
File 2: GetAxisValue(...)100%44100%
File 3: CalculateInertiaTensor(...)100%11100%
File 3: CalculateInertiaTensor(...)100%66100%
File 3: CalculateInertiaTensor(...)100%66100%
File 3: TryGetClosedVolumeMassProperties(...)100%11100%
File 3: TryGetPreparedClosedVolumeMassProperties(...)100%22100%
File 3: PrepareClosedVolumeMassProperties(...)100%88100%
File 3: TryCalculateCandidateClosedVolumeMassProperties(...)100%1818100%
File 3: TryScalePoint(...)100%66100%
File 3: PublishPreparedClosedVolumeMassProperties()100%22100%
File 3: EnsureScaledClosedVolumeMassProperties()100%22100%
File 3: EnsureClosedVolumeMassProperties()100%22100%
File 3: EvaluateClosedVolumeTopology(...)100%2424100%
File 3: TryCalculateClosedVolumeMassProperties(...)100%66100%
File 3: ContainsDuplicateTriangle(...)100%44100%
File 3: CompareTriangleUses(...)100%1212100%
File 3: Find(...)100%22100%
File 3: Union(...)100%44100%
File 4: .ctor(...)100%11100%
File 4: get_OwnerScale()100%11100%
File 4: get_PartScale()100%11100%
File 4: get_ScaledLocalBounds()100%11100%
File 4: get_ScaledLocalRadius()100%11100%
File 4: get_SurfaceMassWeight()100%11100%
File 4: get_PreparedSurfaceMassWeight()100%11100%
File 4: GetScaledLocalRadius(...)100%1616100%
File 4: get_PreparedBounds()100%11100%
File 4: get_PreparedSurfaceMassProperties()100%11100%
File 4: get_SurfaceMassProperties()100%22100%
File 4: PrepareTransformation(...)100%1010100%
File 4: PublishPreparedTransformation()100%44100%
File 4: ValidateSurfaceMassProperties(...)100%22100%
File 4: ValidateClosedVolumeScaleRepresentability(...)100%66100%
File 4: ValidateRotation(...)100%44100%
File 4: PrepareScaledGeometry(...)100%2222100%
File 4: CopyCommittedGeometryCandidate()100%11100%

File(s)

/home/runner/work/Gravitas/Gravitas/src/Gravitas/Colliders/Mesh/PhysicsMesh.ConvexTopology.cs

#LineLine coverage
 1//=======================================================================
 2// PhysicsMesh.ConvexTopology.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 FixedMathSharp.Geometry;
 10using System;
 11
 12namespace Gravitas.Colliders;
 13
 14public partial class PhysicsMesh
 15{
 16    private static int[] CreateTopologyTriangles(Vector3d[] vertices, int[] triangles)
 17    {
 40618        var vertexUses = new VertexUse[vertices.Length];
 3486419        for (int i = 0; i < vertices.Length; i++)
 1702620            vertexUses[i] = new VertexUse(vertices[i], i);
 21
 40622        Array.Sort(vertexUses, CompareVertexUses);
 40623        var representativeIndices = new int[vertices.Length];
 40624        int representativeIndex = vertexUses[0].VertexIndex;
 40625        representativeIndices[representativeIndex] = representativeIndex;
 40626        bool hasExactPositionSeams = false;
 3405227        for (int i = 1; i < vertexUses.Length; i++)
 28        {
 1662029            VertexUse current = vertexUses[i];
 1662030            if (current.Position != vertexUses[i - 1].Position)
 1611531                representativeIndex = current.VertexIndex;
 32            else
 50533                hasExactPositionSeams = true;
 34
 1662035            representativeIndices[current.VertexIndex] = representativeIndex;
 36        }
 37
 40638        if (!hasExactPositionSeams)
 37139            return triangles;
 40
 3541        var topologyTriangles = new int[triangles.Length];
 276442        for (int i = 0; i < triangles.Length; i++)
 134743            topologyTriangles[i] = representativeIndices[triangles[i]];
 44
 3545        return topologyTriangles;
 46    }
 47
 48    private static int CompareVertexUses(VertexUse first, VertexUse second)
 49    {
 19433650        int comparison = first.Position.X.CompareTo(second.Position.X);
 19433651        if (comparison != 0)
 7975552            return comparison;
 53
 11458154        comparison = first.Position.Y.CompareTo(second.Position.Y);
 11458155        if (comparison != 0)
 6659356            return comparison;
 57
 4798858        comparison = first.Position.Z.CompareTo(second.Position.Z);
 4798859        if (comparison != 0)
 4730060            return comparison;
 61
 68862        return first.VertexIndex.CompareTo(second.VertexIndex);
 63    }
 64
 65    private int[] CreateConvexSatEdgeVertexPairs(
 66        Vector3d[] vertices,
 67        int[] triangles,
 68        int triangleCount)
 69    {
 24170        var triangleUses = new TriangleUse[triangleCount];
 24171        var edgeUses = new EdgeUse[triangleCount * 3];
 24172        int edgeIndex = 0;
 5947273        for (int i = 0; i < triangleCount; i++)
 74        {
 2949575            int triangleIndex = i * 3;
 2949576            int index0 = triangles[triangleIndex];
 2949577            int index1 = triangles[triangleIndex + 1];
 2949578            int index2 = triangles[triangleIndex + 2];
 79
 2949580            triangleUses[i] = TriangleUse.Create(index0, index1, index2);
 2949581            edgeUses[edgeIndex++] = EdgeUse.Create(index0, index1, i);
 2949582            edgeUses[edgeIndex++] = EdgeUse.Create(index1, index2, i);
 2949583            edgeUses[edgeIndex++] = EdgeUse.Create(index2, index0, i);
 84        }
 85
 24186        SwiftThrowHelper.ThrowIfArgument(
 24187            ContainsDuplicateTriangle(triangleUses),
 24188            nameof(triangles),
 24189            "Convex mesh triangles must not contain duplicate faces.");
 90
 24191        Array.Sort(edgeUses, CompareEdgeUses);
 24192        var parents = new int[triangleCount];
 5947293        for (int i = 0; i < parents.Length; i++)
 2949594            parents[i] = i;
 95
 24196        var boundaryNext = new int[vertices.Length];
 24197        var boundaryPrevious = new int[vertices.Length];
 24198        Array.Fill(boundaryNext, -1);
 24199        Array.Fill(boundaryPrevious, -1);
 100
 241101        int boundaryEdgeCount = 0;
 241102        int boundaryStart = -1;
 241103        int dihedralSign = 0;
 45112104        for (int start = 0; start < edgeUses.Length;)
 105        {
 44633106            int end = FindEdgeUseGroupEnd(edgeUses, start);
 44633107            int useCount = end - start;
 44633108            SwiftThrowHelper.ThrowIfArgument(
 44633109                useCount > 2,
 44633110                nameof(triangles),
 44633111                "Convex mesh edges must be manifold and used by at most two triangles.");
 112
 44633113            EdgeUse first = edgeUses[start];
 44633114            if (useCount == 1)
 115            {
 796116                RegisterBoundaryEdge(
 796117                    first,
 796118                    boundaryNext,
 796119                    boundaryPrevious,
 796120                    ref boundaryEdgeCount,
 796121                    ref boundaryStart,
 796122                    triangles);
 795123                start = end;
 795124                continue;
 125            }
 126
 43837127            EdgeUse second = edgeUses[start + 1];
 43837128            SwiftThrowHelper.ThrowIfArgument(
 43837129                first.Direction + second.Direction != 0,
 43837130                nameof(triangles),
 43837131                "Adjacent convex mesh triangles must use opposite shared-edge winding.");
 132
 43836133            Union(parents, first.TriangleIndex, second.TriangleIndex);
 43836134            int currentSign = CalculateSharedEdgeSide(vertices, triangles, first, second);
 43836135            if (currentSign != 0)
 136            {
 2460137                SwiftThrowHelper.ThrowIfArgument(
 2460138                    dihedralSign != 0 && currentSign != dihedralSign,
 2460139                    nameof(triangles),
 2460140                    "Closed convex mesh triangles must not contain reflex edges.");
 2459141                dihedralSign = currentSign;
 142            }
 143
 43835144            start = end;
 145        }
 146
 238147        int root = Find(parents, 0);
 58956148        for (int i = 1; i < parents.Length; i++)
 149        {
 29241150            SwiftThrowHelper.ThrowIfArgument(
 29241151                Find(parents, i) != root,
 29241152                nameof(triangles),
 29241153                "Convex mesh triangles must form one edge-connected surface.");
 154        }
 155
 237156        IsClosedSurface = boundaryEdgeCount == 0;
 237157        _surfaceClosureValidationResult = IsClosedSurface
 237158            ? MeshVolumeValidationResult.Valid
 237159            : MeshVolumeValidationResult.BoundaryEdge;
 237160        if (IsClosedSurface)
 161        {
 126162            SwiftThrowHelper.ThrowIfArgument(
 126163                dihedralSign == 0,
 126164                nameof(triangles),
 126165                "Closed convex mesh triangles must enclose nonzero volume.");
 126166            SwiftThrowHelper.ThrowIfArgument(
 126167                !HasSingleVertexLinks(triangles, edgeUses),
 126168                nameof(triangles),
 126169                "Closed convex mesh triangles must be manifold around every vertex.");
 170        }
 171        else
 172        {
 111173            SwiftThrowHelper.ThrowIfArgument(
 111174                dihedralSign != 0,
 111175                nameof(triangles),
 111176                "Open convex mesh triangles must form one coplanar surface.");
 109177            ValidateOpenConvexBoundary(
 109178                vertices,
 109179                triangles,
 109180                triangleCount,
 109181                boundaryNext,
 109182                boundaryEdgeCount,
 109183                boundaryStart);
 184        }
 185
 232186        return CreateSatEdgeVertexPairs(edgeUses, vertices, triangles);
 187    }
 188
 189    private static void RegisterBoundaryEdge(
 190        EdgeUse edgeUse,
 191        int[] boundaryNext,
 192        int[] boundaryPrevious,
 193        ref int boundaryEdgeCount,
 194        ref int boundaryStart,
 195        int[] triangles)
 196    {
 796197        int start = edgeUse.Direction > 0
 796198            ? edgeUse.StartVertexIndex
 796199            : edgeUse.EndVertexIndex;
 796200        int end = edgeUse.Direction > 0
 796201            ? edgeUse.EndVertexIndex
 796202            : edgeUse.StartVertexIndex;
 203
 796204        SwiftThrowHelper.ThrowIfArgument(
 796205            boundaryNext[start] >= 0 || boundaryPrevious[end] >= 0,
 796206            nameof(triangles),
 796207            "Open convex mesh triangles must form one consistently wound boundary loop.");
 208
 795209        boundaryNext[start] = end;
 795210        boundaryPrevious[end] = start;
 795211        boundaryEdgeCount++;
 795212        if (boundaryStart < 0 || start < boundaryStart)
 135213            boundaryStart = start;
 795214    }
 215
 216    private static int CalculateSharedEdgeSide(
 217        Vector3d[] vertices,
 218        int[] triangles,
 219        EdgeUse firstUse,
 220        EdgeUse secondUse)
 221    {
 43836222        int startIndex = firstUse.Direction > 0
 43836223            ? firstUse.StartVertexIndex
 43836224            : firstUse.EndVertexIndex;
 43836225        int endIndex = firstUse.Direction > 0
 43836226            ? firstUse.EndVertexIndex
 43836227            : firstUse.StartVertexIndex;
 43836228        Vector3d start = vertices[startIndex];
 43836229        Vector3d end = vertices[endIndex];
 43836230        Vector3d firstOpposite = vertices[FindOppositeVertexIndex(
 43836231            triangles,
 43836232            firstUse.TriangleIndex,
 43836233            startIndex,
 43836234            endIndex)];
 43836235        Vector3d secondOpposite = vertices[FindOppositeVertexIndex(
 43836236            triangles,
 43836237            secondUse.TriangleIndex,
 43836238            startIndex,
 43836239            endIndex)];
 240
 43836241        return Vector3d.ScalarTripleProductSign(
 43836242            end - start,
 43836243            firstOpposite - start,
 43836244            secondOpposite - start);
 245    }
 246
 247    private static void ValidateOpenConvexBoundary(
 248        Vector3d[] vertices,
 249        int[] triangles,
 250        int triangleCount,
 251        int[] boundaryNext,
 252        int boundaryEdgeCount,
 253        int boundaryStart)
 254    {
 109255        SwiftThrowHelper.ThrowIfArgument(
 109256            boundaryStart < 0,
 109257            nameof(triangles),
 109258            "Open convex mesh triangles must form one boundary loop.");
 259
 109260        int droppedAxis = GetDominantNormalAxis(vertices, triangles);
 109261        var boundaryVertices = new BoundaryVertex[boundaryEdgeCount];
 109262        int current = boundaryStart;
 109263        int visitedEdges = 0;
 264        do
 265        {
 777266            int next = boundaryNext[current];
 267
 777268            boundaryVertices[visitedEdges] = new BoundaryVertex(
 777269                ProjectToBoundaryPlane(vertices[current], droppedAxis),
 777270                current);
 777271            current = next;
 777272            visitedEdges++;
 777273            SwiftThrowHelper.ThrowIfArgument(
 777274                visitedEdges > boundaryEdgeCount,
 777275                nameof(triangles),
 777276                "Open convex mesh triangles must form one boundary loop.");
 277        }
 777278        while (current != boundaryStart);
 279
 109280        SwiftThrowHelper.ThrowIfArgument(
 109281            visitedEdges != boundaryEdgeCount,
 109282            nameof(triangles),
 109283            "Open convex mesh triangles must form one boundary loop.");
 284
 109285        ValidateBoundaryMatchesConvexHull(boundaryVertices, triangles);
 107286        ValidateProjectedTriangleFill(
 107287            vertices,
 107288            triangles,
 107289            triangleCount,
 107290            boundaryVertices,
 107291            droppedAxis);
 106292    }
 293
 294    private static int GetDominantNormalAxis(Vector3d[] vertices, int[] triangles)
 295    {
 109296        Vector3d first = vertices[triangles[0]];
 109297        Vector3d second = vertices[triangles[1]];
 109298        Vector3d third = vertices[triangles[2]];
 109299        Vector3d absoluteNormal = Vector3d.Abs(Vector3d.Cross(second - first, third - first));
 109300        if (absoluteNormal.X >= absoluteNormal.Y && absoluteNormal.X >= absoluteNormal.Z)
 22301            return 0;
 302
 87303        return absoluteNormal.Y >= absoluteNormal.Z ? 1 : 2;
 304    }
 305
 306    private static Vector2d ProjectToBoundaryPlane(Vector3d vertex, int droppedAxis) =>
 2475307        droppedAxis switch
 2475308        {
 188309            0 => new Vector2d(vertex.Y, vertex.Z),
 1268310            1 => new Vector2d(vertex.X, vertex.Z),
 1019311            _ => new Vector2d(vertex.X, vertex.Y),
 2475312        };
 313
 314    private static void ValidateBoundaryMatchesConvexHull(
 315        BoundaryVertex[] authoredBoundary,
 316        int[] triangles)
 317    {
 109318        var sorted = new BoundaryVertex[authoredBoundary.Length];
 109319        Array.Copy(authoredBoundary, sorted, authoredBoundary.Length);
 109320        Array.Sort(sorted, CompareBoundaryVertices);
 321
 1554322        for (int i = 1; i < sorted.Length; i++)
 323        {
 668324            SwiftThrowHelper.ThrowIfArgument(
 668325                sorted[i - 1].Position == sorted[i].Position,
 668326                nameof(triangles),
 668327                "Open convex mesh triangles must form one nondegenerate convex boundary loop.");
 328        }
 329
 109330        var hull = new BoundaryVertex[sorted.Length * 2];
 109331        int hullCount = 0;
 1772332        for (int i = 0; i < sorted.Length; i++)
 777333            AppendConvexHullVertex(hull, ref hullCount, 2, sorted[i]);
 334
 109335        int upperStart = hullCount + 1;
 1554336        for (int i = sorted.Length - 2; i >= 0; i--)
 668337            AppendConvexHullVertex(hull, ref hullCount, upperStart, sorted[i]);
 338
 339        // The upper pass repeats the lexicographically first lower-hull point.
 109340        hullCount--;
 341
 109342        var strictAuthoredBoundary = new BoundaryVertex[authoredBoundary.Length];
 109343        int strictAuthoredCount = 0;
 1772344        for (int i = 0; i < authoredBoundary.Length; i++)
 345        {
 777346            BoundaryVertex previous = authoredBoundary[(i + authoredBoundary.Length - 1) % authoredBoundary.Length];
 777347            BoundaryVertex current = authoredBoundary[i];
 777348            BoundaryVertex next = authoredBoundary[(i + 1) % authoredBoundary.Length];
 777349            if (Vector2d.OrientationSign(previous.Position, current.Position, next.Position) == 0)
 350                continue;
 351
 486352            strictAuthoredBoundary[strictAuthoredCount++] = current;
 353        }
 354
 109355        SwiftThrowHelper.ThrowIfArgument(
 109356            hullCount < 3
 109357            || strictAuthoredCount != hullCount
 109358            || !MatchesCyclicHull(strictAuthoredBoundary, strictAuthoredCount, hull),
 109359            nameof(triangles),
 109360            "Open convex mesh triangles must have a convex boundary.");
 107361    }
 362
 363    private static void ValidateProjectedTriangleFill(
 364        Vector3d[] vertices,
 365        int[] triangles,
 366        int triangleCount,
 367        BoundaryVertex[] boundary,
 368        int droppedAxis)
 369    {
 107370        Fixed64 triangleArea = Fixed64.Zero;
 107371        int surfaceOrientation = 0;
 1344372        for (int i = 0; i < triangleCount; i++)
 373        {
 566374            int index = i * 3;
 566375            Vector2d first = ProjectToBoundaryPlane(vertices[triangles[index]], droppedAxis);
 566376            Vector2d second = ProjectToBoundaryPlane(vertices[triangles[index + 1]], droppedAxis);
 566377            Vector2d third = ProjectToBoundaryPlane(vertices[triangles[index + 2]], droppedAxis);
 566378            int orientation = Vector2d.OrientationSign(first, second, third);
 566379            if (surfaceOrientation == 0)
 107380                surfaceOrientation = orientation;
 381
 566382            SwiftThrowHelper.ThrowIfArgument(
 566383                orientation != surfaceOrientation,
 566384                nameof(triangles),
 566385                "Open convex mesh triangles must fill their convex boundary without holes, overlap, or folds.");
 565386            SwiftThrowHelper.ThrowIfArgument(
 565387                !Fixed64.TryAdd(triangleArea, new FixedTriangle2d(first, second, third).Area, out triangleArea),
 565388                nameof(triangles),
 565389                "Open convex mesh projected area must be representable.");
 390        }
 391
 106392        Fixed64 boundaryArea = Fixed64.Zero;
 106393        Vector2d anchor = boundary[0].Position;
 1312394        for (int i = 1; i < boundary.Length - 1; i++)
 395        {
 550396            Fixed64 area = new FixedTriangle2d(
 550397                anchor,
 550398                boundary[i].Position,
 550399                boundary[i + 1].Position).Area;
 550400            SwiftThrowHelper.ThrowIfArgument(
 550401                !Fixed64.TryAdd(boundaryArea, area, out boundaryArea),
 550402                nameof(triangles),
 550403                "Open convex mesh projected boundary area must be representable.");
 404        }
 405
 106406        Fixed64 areaTolerance = Fixed64.Epsilon * (Fixed64)(triangleCount + boundary.Length);
 106407        SwiftThrowHelper.ThrowIfArgument(
 106408            FixedMath.Abs(triangleArea - boundaryArea) > areaTolerance,
 106409            nameof(triangles),
 106410            "Open convex mesh triangles must fill their convex boundary without holes, overlap, or folds.");
 106411    }
 412
 413    private static void AppendConvexHullVertex(
 414        BoundaryVertex[] hull,
 415        ref int hullCount,
 416        int minimumCount,
 417        BoundaryVertex candidate)
 418    {
 2296419        while (hullCount >= minimumCount
 2296420            && Vector2d.OrientationSign(
 2296421                hull[hullCount - 2].Position,
 2296422                hull[hullCount - 1].Position,
 2296423                candidate.Position) <= 0)
 424        {
 851425            hullCount--;
 426        }
 427
 1445428        hull[hullCount++] = candidate;
 1445429    }
 430
 431    private static bool MatchesCyclicHull(
 432        BoundaryVertex[] authoredBoundary,
 433        int authoredCount,
 434        BoundaryVertex[] hull)
 435    {
 108436        int authoredStart = 0;
 960437        for (int i = 1; i < authoredCount; i++)
 438        {
 372439            if (CompareBoundaryVertices(authoredBoundary[i], authoredBoundary[authoredStart]) < 0)
 69440                authoredStart = i;
 441        }
 442
 108443        bool forward = true;
 108444        bool reverse = true;
 1176445        for (int i = 0; i < authoredCount; i++)
 446        {
 480447            forward &= authoredBoundary[(authoredStart + i) % authoredCount].VertexIndex
 480448                == hull[i].VertexIndex;
 480449            reverse &= authoredBoundary[(authoredStart - i + authoredCount) % authoredCount].VertexIndex
 480450                == hull[i].VertexIndex;
 451        }
 452
 108453        return forward || reverse;
 454    }
 455
 456    private static int CompareBoundaryVertices(BoundaryVertex first, BoundaryVertex second)
 457    {
 5522458        int comparison = first.Position.X.CompareTo(second.Position.X);
 5522459        if (comparison != 0)
 5016460            return comparison;
 461
 506462        return first.Position.Y.CompareTo(second.Position.Y);
 463    }
 464
 465    private static bool HasSingleVertexLinks(
 466        int[] triangles,
 467        EdgeUse[] sortedEdgeUses)
 468    {
 469        // Edge validation has already proven that every edge has exactly two
 470        // oppositely wound uses. Link connectivity is the remaining local
 471        // manifold condition at each welded vertex.
 139472        var linkParents = new int[triangles.Length];
 139473        var cornerUses = new VertexCornerUse[triangles.Length];
 174614474        for (int i = 0; i < triangles.Length; i++)
 475        {
 87168476            linkParents[i] = i;
 87168477            cornerUses[i] = new VertexCornerUse(triangles[i], i);
 478        }
 479
 43862480        for (int start = 0; start < sortedEdgeUses.Length;)
 481        {
 43584482            int end = FindEdgeUseGroupEnd(sortedEdgeUses, start);
 43584483            EdgeUse first = sortedEdgeUses[start];
 43584484            EdgeUse second = sortedEdgeUses[start + 1];
 43584485            Union(
 43584486                linkParents,
 43584487                FindTriangleCornerIndex(triangles, first.TriangleIndex, first.StartVertexIndex),
 43584488                FindTriangleCornerIndex(triangles, second.TriangleIndex, first.StartVertexIndex));
 43584489            Union(
 43584490                linkParents,
 43584491                FindTriangleCornerIndex(triangles, first.TriangleIndex, first.EndVertexIndex),
 43584492                FindTriangleCornerIndex(triangles, second.TriangleIndex, first.EndVertexIndex));
 43584493            start = end;
 494        }
 495
 139496        Array.Sort(cornerUses, CompareVertexCornerUses);
 15078497        for (int start = 0; start < cornerUses.Length;)
 498        {
 14801499            int root = Find(linkParents, cornerUses[start].CornerIndex);
 14801500            int vertexIndex = cornerUses[start].VertexIndex;
 14801501            int end = start + 1;
 87133502            while (end < cornerUses.Length && cornerUses[end].VertexIndex == vertexIndex)
 503            {
 72333504                if (Find(linkParents, cornerUses[end].CornerIndex) != root)
 1505                    return false;
 72332506                end++;
 507            }
 508
 14800509            start = end;
 510        }
 511
 138512        return true;
 513    }
 514
 515    private static int FindTriangleCornerIndex(
 516        int[] triangles,
 517        int triangleIndex,
 518        int vertexIndex)
 519    {
 174336520        int cornerIndex = triangleIndex * 3;
 174336521        if (triangles[cornerIndex] == vertexIndex)
 58112522            return cornerIndex;
 116224523        if (triangles[cornerIndex + 1] == vertexIndex)
 58112524            return cornerIndex + 1;
 58112525        return cornerIndex + 2;
 526    }
 527
 528    private static int CompareVertexCornerUses(VertexCornerUse first, VertexCornerUse second)
 529    {
 1474777530        int comparison = first.VertexIndex.CompareTo(second.VertexIndex);
 1474777531        return comparison != 0
 1474777532            ? comparison
 1474777533            : first.CornerIndex.CompareTo(second.CornerIndex);
 534    }
 535
 536    private static int[] CreateSatEdgeVertexPairs(
 537        EdgeUse[] edgeUses,
 538        Vector3d[] vertices,
 539        int[] triangles)
 540    {
 232541        int satEdgeCount = CountConvexSatEdges(edgeUses, vertices, triangles);
 232542        var edgeVertexPairs = new int[satEdgeCount * 2];
 232543        int pairIndex = 0;
 45035544        for (int start = 0; start < edgeUses.Length;)
 545        {
 44571546            int end = FindEdgeUseGroupEnd(edgeUses, start);
 44571547            if (ShouldIncludeConvexSatEdge(edgeUses, start, end, vertices, triangles))
 548            {
 3210549                edgeVertexPairs[pairIndex++] = edgeUses[start].StartVertexIndex;
 3210550                edgeVertexPairs[pairIndex++] = edgeUses[start].EndVertexIndex;
 551            }
 552
 44571553            start = end;
 554        }
 555
 232556        return edgeVertexPairs;
 557    }
 558
 559    private static int CountConvexSatEdges(
 560        EdgeUse[] edgeUses,
 561        Vector3d[] vertices,
 562        int[] triangles)
 563    {
 232564        int count = 0;
 45035565        for (int start = 0; start < edgeUses.Length;)
 566        {
 44571567            int end = FindEdgeUseGroupEnd(edgeUses, start);
 44571568            if (ShouldIncludeConvexSatEdge(edgeUses, start, end, vertices, triangles))
 3210569                count++;
 570
 44571571            start = end;
 572        }
 573
 232574        return count;
 575    }
 576
 577    private static int FindEdgeUseGroupEnd(EdgeUse[] edgeUses, int start)
 578    {
 177359579        long key = edgeUses[start].Key;
 177359580        int end = start + 1;
 352398581        while (end < edgeUses.Length && edgeUses[end].Key == key)
 175039582            end++;
 583
 177359584        return end;
 585    }
 586
 587    private static bool ShouldIncludeConvexSatEdge(
 588        EdgeUse[] edgeUses,
 589        int start,
 590        int end,
 591        Vector3d[] vertices,
 592        int[] triangles)
 593    {
 89142594        if (end - start <= 1)
 1524595            return true;
 596
 87618597        int edgeStart = edgeUses[start].StartVertexIndex;
 87618598        int edgeEnd = edgeUses[start].EndVertexIndex;
 87618599        Vector3d first = vertices[edgeStart];
 87618600        Vector3d edge = vertices[edgeEnd] - first;
 87618601        Vector3d firstOpposite = vertices[FindOppositeVertexIndex(
 87618602            triangles,
 87618603            edgeUses[start].TriangleIndex,
 87618604            edgeStart,
 87618605            edgeEnd)];
 340680606        for (int i = start + 1; i < end; i++)
 607        {
 87618608            Vector3d nextOpposite = vertices[FindOppositeVertexIndex(
 87618609                triangles,
 87618610                edgeUses[i].TriangleIndex,
 87618611                edgeStart,
 87618612                edgeEnd)];
 87618613            if (Vector3d.ScalarTripleProductSign(
 87618614                edge,
 87618615                firstOpposite - first,
 87618616                nextOpposite - first) != 0)
 617            {
 4896618                return true;
 619            }
 620        }
 621
 82722622        return false;
 623    }
 624
 625    private static int FindOppositeVertexIndex(
 626        int[] triangles,
 627        int triangleIndex,
 628        int edgeStart,
 629        int edgeEnd)
 630    {
 262908631        int index = triangleIndex * 3;
 262908632        int first = triangles[index];
 262908633        if (first != edgeStart && first != edgeEnd)
 87194634            return first;
 635
 175714636        int second = triangles[index + 1];
 175714637        return second != edgeStart && second != edgeEnd
 175714638            ? second
 175714639            : triangles[index + 2];
 640    }
 641
 642    private readonly struct VertexUse
 643    {
 644        public VertexUse(Vector3d position, int vertexIndex)
 645        {
 17026646            Position = position;
 17026647            VertexIndex = vertexIndex;
 17026648        }
 649
 650        public Vector3d Position { get; }
 651
 652        public int VertexIndex { get; }
 653    }
 654
 655    private readonly struct BoundaryVertex
 656    {
 657        public BoundaryVertex(Vector2d position, int vertexIndex)
 658        {
 777659            Position = position;
 777660            VertexIndex = vertexIndex;
 777661        }
 662
 663        public Vector2d Position { get; }
 664
 665        public int VertexIndex { get; }
 666    }
 667
 668    private readonly struct VertexCornerUse
 669    {
 670        public VertexCornerUse(int vertexIndex, int cornerIndex)
 671        {
 87168672            VertexIndex = vertexIndex;
 87168673            CornerIndex = cornerIndex;
 87168674        }
 675
 676        public int VertexIndex { get; }
 677
 678        public int CornerIndex { get; }
 679    }
 680}

/home/runner/work/Gravitas/Gravitas/src/Gravitas/Colliders/Mesh/PhysicsMesh.cs

#LineLine coverage
 1//=======================================================================
 2// PhysicsMesh.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 FixedMathSharp.Geometry;
 10using SwiftCollections;
 11using SwiftCollections.Query;
 12using System;
 13using System.Collections.Generic;
 14
 15namespace Gravitas.Colliders
 16{
 17    /// <summary>
 18    /// Owns immutable authored triangle topology and committed deterministic runtime mesh geometry.
 19    /// </summary>
 20    public partial class PhysicsMesh
 21    {
 22        private const int SupportTreeVertexThreshold = 32;
 23        private const int SupportTreeLeafVertexCount = 8;
 24        private const int SupportTreeStackCapacity = 64;
 25
 26        /// <summary>
 27        /// Maximum accepted vertex count for deterministic runtime mesh construction.
 28        /// </summary>
 29        public const int MaxVertexCount = 65535;
 30
 31        /// <summary>
 32        /// Maximum accepted triangle count for deterministic runtime mesh construction.
 33        /// </summary>
 34        public const int MaxTriangleCount = 131072;
 35
 136        private static readonly Fixed64 TetrahedronVolumeDivisor = (Fixed64)6;
 137        private static readonly Fixed64 TetrahedronCentroidDivisor = (Fixed64)4;
 138        private static readonly Fixed64 SecondMomentIntegralDivisor = (Fixed64)10;
 139        private static readonly Fixed64 ProductMomentIntegralDivisor = (Fixed64)20;
 40
 41        private readonly Vector3d[] _localVertices;
 42
 43        private Vector3d[] _scaledLocalVertices;
 44        private Vector3d[] _preparedScaledLocalVertices;
 45        private int[]? _supportVertexIndices;
 46        private SupportTreeNode[]? _supportTreeNodes;
 47        private SupportTreeNode[]? _preparedSupportTreeNodes;
 48        private int _supportTreeNodeCount;
 49
 50        /// <summary>
 51        /// Holds the source vertices in local mesh space.
 52        /// </summary>
 16153        internal ReadOnlySpan<Vector3d> LocalVertices => _localVertices;
 54
 55        /// <summary>
 56        /// Holds the committed scaled vertices as center-relative local
 57        /// offsets. Collision relations apply <see cref="Rotation"/> without
 58        /// materializing absolute world points.
 59        /// </summary>
 60        internal ReadOnlySpan<Vector3d> ScaledLocalVertices =>
 48561            _scaledLocalVertices;
 62
 63        /// <summary>
 64        /// Number of vertices in the immutable local mesh topology.
 65        /// </summary>
 182266        public int VertexCount => _localVertices.Length;
 67
 68        private readonly int[] _triangles;
 69        /// <summary>
 70        /// Holds all the triangles that make up the mesh in the form of indices to the vertices array.
 71        /// example: Triangles[0-3] = 4,2,3 means that the first triangle of the mesh is made up of the vertices _vertic
 72        /// </summary>
 72773        public ReadOnlySpan<int> Triangles => _triangles;
 74
 75        private readonly int _triangleCount;
 76        /// <summary>Gets the number of authored triangles.</summary>
 289477        public int TriangleCount => _triangleCount;
 78
 79        private readonly int[] _convexSatEdgeVertexPairs;
 48580        internal ReadOnlySpan<int> ConvexSatEdgeVertexPairs => _convexSatEdgeVertexPairs;
 81
 82        /// <summary>Gets the mesh collision mode.</summary>
 83        public MeshColliderMode Mode { get; }
 84
 85        /// <summary>
 86        /// Gets whether the complete exact-position-welded triangle topology is one
 87        /// connected, consistently wound, closed two-manifold surface.
 88        /// </summary>
 89        public bool IsClosedSurface { get; private set; }
 90
 91        /// <summary>
 92        /// Gets the current triangle surface area after local scale.
 93        /// </summary>
 5494        public Fixed64 TotalArea => _scaledTotalArea;
 95
 96        private int _triangleBvhBuildCount;
 97        private SwiftFixedBVH<int> _triangleBVH;
 98        private SwiftFixedBVH<int> _preparedTriangleBVH;
 99
 100        /// <summary>
 101        /// Triangle acceleration structure in local mesh space.
 102        /// </summary>
 10570103        internal SwiftFixedBVH<int> TriangleBVH => _triangleBVH;
 104
 105        /// <summary>Gets the number of committed triangle BVH builds.</summary>
 11106        public int TriangleBvhBuildCount => _triangleBvhBuildCount;
 107
 417108        private FixedQuaternion _rotation = FixedQuaternion.Identity;
 109
 110        /// <summary>
 111        /// Gets the committed world-space origin of the mesh's rigid frame.
 112        /// </summary>
 82143113        internal Vector3d Origin => _position;
 114
 115        /// <summary>
 116        /// Gets the committed local-to-world mesh orientation.
 117        /// </summary>
 83348118        internal FixedQuaternion Rotation => _rotation;
 119
 120        private FixedBoundBox _bounds;
 121        /// <summary>Gets the committed world-space axis-aligned bounds.</summary>
 344122        public FixedBoundBox Bounds => _bounds;
 123
 124        private readonly FixedBoundBox _localBounds;
 125
 126        /// <summary>
 127        /// Axis-aligned bounds of the source vertices in local mesh space.
 128        /// </summary>
 651129        public FixedBoundBox LocalBounds => _localBounds;
 130
 131        /// <summary>Creates a convex runtime mesh at a world-space pose.</summary>
 132        public PhysicsMesh(Vector3d[] vertices, int[] triangles, Vector3d position, FixedQuaternion rotation)
 83133            : this(vertices, triangles, position, rotation, MeshColliderMode.Convex) { }
 134
 135        /// <summary>Creates a runtime mesh with an explicit collision mode and world-space pose.</summary>
 417136        public PhysicsMesh(
 417137            Vector3d[] vertices,
 417138            int[] triangles,
 417139            Vector3d position,
 417140            FixedQuaternion rotation,
 417141            MeshColliderMode mode)
 142        {
 417143            SwiftThrowHelper.ThrowIfArgument(
 417144                mode != MeshColliderMode.Convex && mode != MeshColliderMode.Concave,
 417145                nameof(mode),
 417146                "Unsupported mesh collider mode.");
 147
 417148            ValidateInput(vertices, triangles);
 149
 406150            Mode = mode;
 406151            _localVertices = new Vector3d[vertices.Length];
 406152            Array.Copy(vertices, _localVertices, vertices.Length);
 406153            _scaledLocalVertices = new Vector3d[vertices.Length];
 406154            _preparedScaledLocalVertices = new Vector3d[vertices.Length];
 406155            _triangles = new int[triangles.Length];
 406156            Array.Copy(triangles, _triangles, triangles.Length);
 406157            _triangleCount = triangles.Length / 3; // 3 vertices per triangle
 406158            _triangleBVH = new SwiftFixedBVH<int>(2 * TriangleCount - 1);
 406159            _preparedTriangleBVH = new SwiftFixedBVH<int>(2 * TriangleCount - 1);
 406160            _scaledFaceAreas = new Fixed64[TriangleCount];
 406161            _preparedScaledFaceAreas = new Fixed64[TriangleCount];
 406162            _scaledFaceNormals = new Vector3d[TriangleCount];
 406163            _preparedScaledFaceNormals = new Vector3d[TriangleCount];
 164
 406165            _localBounds = CalculateBounds(_localVertices);
 166
 167            // Scale validation proves every vertex span and triangle cross product used
 168            // by the exact topology predicates below is representable.
 406169            int[] topologyTriangles = CreateTopologyTriangles(_localVertices, _triangles);
 406170            _convexSatEdgeVertexPairs = Mode == MeshColliderMode.Convex
 406171                ? CreateConvexSatEdgeVertexPairs(_localVertices, topologyTriangles, _triangleCount)
 406172                : Array.Empty<int>();
 397173            if (Mode == MeshColliderMode.Concave)
 174            {
 165175                IsClosedSurface = EvaluateClosedVolumeTopology(topologyTriangles, out _surfaceClosureValidationResult);
 176            }
 177
 397178            if (Mode == MeshColliderMode.Convex && _localVertices.Length > SupportTreeVertexThreshold)
 179            {
 12180                _supportVertexIndices = CreateSupportVertexIndices(_localVertices.Length);
 12181                _supportTreeNodes = new SupportTreeNode[(2 * _localVertices.Length) - 1];
 12182                _preparedSupportTreeNodes = new SupportTreeNode[(2 * _localVertices.Length) - 1];
 183            }
 184
 397185            PrepareTransformation(position, rotation, Vector3d.One, Vector3d.One, null);
 396186            PublishPreparedTransformation();
 396187        }
 188
 189        /// <summary>Updates the mesh world-space position and rigid rotation.</summary>
 190        public void UpdatePosition(Vector3d position, FixedQuaternion rotation)
 191        {
 330192            PrepareTransformation(position, rotation, _ownerScale, _partScale, null);
 330193            PublishPreparedTransformation();
 330194        }
 195
 196        /// <summary>
 197        /// Updates the mesh center, normalized rigid rotation, and strictly positive authored scale.
 198        /// </summary>
 199        public void UpdateTransform(Vector3d position, FixedQuaternion rotation, Vector3d scale)
 200        {
 23201            PrepareTransformation(position, rotation, scale, Vector3d.One, null);
 18202            PublishPreparedTransformation();
 18203        }
 204
 205        private static void ValidateInput(Vector3d[] vertices, int[] triangles)
 206        {
 417207            SwiftThrowHelper.ThrowIfNull(vertices, nameof(vertices));
 416208            SwiftThrowHelper.ThrowIfNull(triangles, nameof(triangles));
 415209            SwiftThrowHelper.ThrowIfArgument(vertices.Length < 3, nameof(vertices), "Mesh must contain at least three ve
 415210            SwiftThrowHelper.ThrowIfArgument(triangles.Length == 0 || triangles.Length % 3 != 0, nameof(triangles), "Tri
 413211            SwiftThrowHelper.ThrowIfArgumentOutOfRange(vertices.Length > MaxVertexCount, vertices.Length, nameof(vertice
 212
 412213            int triangleCount = triangles.Length / 3;
 412214            SwiftThrowHelper.ThrowIfArgumentOutOfRange(triangleCount > MaxTriangleCount, triangleCount, nameof(triangles
 215
 412216            var referencedVertices = new bool[vertices.Length];
 261370217            for (int i = 0; i < triangleCount; i++)
 218            {
 130277219                int index0 = triangles[i * 3];
 130277220                int index1 = triangles[i * 3 + 1];
 130277221                int index2 = triangles[i * 3 + 2];
 222
 130277223                SwiftThrowHelper.ThrowIfArgumentOutOfRange((uint)index0 >= (uint)vertices.Length, vertices.Length, nameo
 130277224                SwiftThrowHelper.ThrowIfArgumentOutOfRange((uint)index1 >= (uint)vertices.Length, vertices.Length, nameo
 130277225                SwiftThrowHelper.ThrowIfArgumentOutOfRange((uint)index2 >= (uint)vertices.Length, vertices.Length, nameo
 226
 130276227                SwiftThrowHelper.ThrowIfArgument(
 130276228                    index0 == index1 || index1 == index2 || index2 == index0,
 130276229                    nameof(triangles),
 130276230                    "Triangle indices must be unique within each triangle.");
 231
 130275232                Fixed64 area = new FixedTriangle(
 130275233                    vertices[index0],
 130275234                    vertices[index1],
 130275235                    vertices[index2]).Area;
 130275236                SwiftThrowHelper.ThrowIfArgument(area <= Fixed64.Epsilon, nameof(triangles), "Degenerate triangles are n
 237
 130273238                referencedVertices[index0] = true;
 130273239                referencedVertices[index1] = true;
 130273240                referencedVertices[index2] = true;
 241            }
 242
 34880243            for (int i = 0; i < referencedVertices.Length; i++)
 17034244                SwiftThrowHelper.ThrowIfArgument(!referencedVertices[i], nameof(vertices), "Every mesh vertex must be re
 406245        }
 246
 247        private void BuildTriangleBVH(
 248            SwiftFixedBVH<int> bvh,
 249            ReadOnlySpan<Vector3d> vertices)
 250        {
 453251            bvh.Clear();
 324794252            for (int i = 0; i < _triangleCount; i++)
 253            {
 161944254                int index0 = _triangles[i * 3];
 161944255                int index1 = _triangles[i * 3 + 1];
 161944256                int index2 = _triangles[i * 3 + 2];
 161944257                Vector3d min = Vector3d.Min(Vector3d.Min(vertices[index0], vertices[index1]), vertices[index2]);
 161944258                Vector3d max = Vector3d.Max(Vector3d.Max(vertices[index0], vertices[index1]), vertices[index2]);
 161944259                bvh.Insert(i, new FixedBoundVolume(min, max));
 260            }
 453261        }
 262
 263        private static FixedBoundBox CalculateBounds(Vector3d[] vertices)
 264        {
 861265            Vector3d min = vertices[0];
 861266            Vector3d max = vertices[0];
 100074267            for (int i = 1; i < vertices.Length; i++)
 268            {
 49176269                min = Vector3d.Min(min, vertices[i]);
 49176270                max = Vector3d.Max(max, vertices[i]);
 271            }
 272
 861273            return FixedBoundBox.FromMinMax(min, max);
 274        }
 275
 276        /// <summary>Compares deterministic edge uses for topology validation.</summary>
 277        private static int CompareEdgeUses(EdgeUse first, EdgeUse second)
 278        {
 1439978279            if (first.Key != second.Key)
 1394489280                return first.Key < second.Key ? -1 : 1;
 281
 45489282            if (first.TriangleIndex == second.TriangleIndex)
 48283                return 0;
 284
 45441285            return first.TriangleIndex < second.TriangleIndex ? -1 : 1;
 286        }
 287
 288        /// <summary>Gets the projected mesh area facing a world-space direction.</summary>
 289        public Fixed64 GetFrontalArea(Vector3d direction)
 290        {
 12291            Fixed64 directionMagnitude = direction.Magnitude;
 12292            if (directionMagnitude <= Fixed64.Epsilon)
 1293                return Fixed64.Zero;
 294
 11295            Vector3d normalizedDirection = direction / directionMagnitude;
 11296            Fixed64 totalArea = Fixed64.Zero;
 82297            for (int i = 0; i < _triangleCount; i++)
 298            {
 30299                Fixed64 projection = Vector3d.Dot(GetFaceNormalWorld(i), normalizedDirection);
 30300                if (projection > Fixed64.Zero)
 13301                    totalArea += _scaledFaceAreas[i] * FixedMath.Min(projection, Fixed64.One);
 302            }
 303
 11304            return totalArea;
 305        }
 306
 307        /// <summary>Gets one triangle's vertices in world space.</summary>
 308        public void GetTriangleVertices(int index, out Vector3d first, out Vector3d second, out Vector3d third)
 309        {
 4310            if (TryGetTriangleVertices(index, out first, out second, out third))
 3311                return;
 312
 1313            throw new InvalidOperationException(
 1314                "At least one triangle vertex lies outside the Fixed64 world-coordinate domain.");
 315        }
 316
 317        /// <summary>
 318        /// Attempts to materialize one triangle's absolute world vertices.
 319        /// Canonical collision and query paths should consume scaled-local
 320        /// geometry instead.
 321        /// </summary>
 322        public bool TryGetTriangleVertices(
 323            int index,
 324            out Vector3d first,
 325            out Vector3d second,
 326            out Vector3d third)
 327        {
 279328            GetLocalTriangleVertices(
 279329                index,
 279330                out Vector3d localFirst,
 279331                out Vector3d localSecond,
 279332                out Vector3d localThird);
 279333            var firstAnchor = new FixedPointAnchor(
 279334                _position,
 279335                _rotation,
 279336                localFirst);
 279337            var secondAnchor = new FixedPointAnchor(
 279338                _position,
 279339                _rotation,
 279340                localSecond);
 279341            var thirdAnchor = new FixedPointAnchor(
 279342                _position,
 279343                _rotation,
 279344                localThird);
 279345            bool representable =
 279346                firstAnchor.TryGetPoint(out first)
 279347                & secondAnchor.TryGetPoint(out second)
 279348                & thirdAnchor.TryGetPoint(out third);
 279349            if (representable)
 273350                return true;
 351
 6352            first = default;
 6353            second = default;
 6354            third = default;
 6355            return false;
 356        }
 357
 358        /// <summary>
 359        /// Gets one triangle's committed scaled vertices in local mesh space.
 360        /// </summary>
 361        public void GetLocalTriangleVertices(int index, out Vector3d first, out Vector3d second, out Vector3d third)
 362        {
 43624363            SwiftThrowHelper.ThrowIfArrayIndexInvalid(index, _triangleCount, nameof(index));
 364
 43624365            int triangleIndex = index * 3;
 43624366            first = _scaledLocalVertices[_triangles[triangleIndex]];
 43624367            second = _scaledLocalVertices[_triangles[triangleIndex + 1]];
 43624368            third = _scaledLocalVertices[_triangles[triangleIndex + 2]];
 43624369        }
 370
 371        /// <summary>
 372        /// Gets one triangle's committed scaled normal in local mesh space.
 373        /// </summary>
 374        internal Vector3d GetScaledLocalFaceNormal(int index)
 375        {
 970376            SwiftThrowHelper.ThrowIfArrayIndexInvalid(index, _triangleCount, nameof(index));
 970377            return _scaledFaceNormals[index];
 378        }
 379
 380        /// <summary>
 381        /// Attempts to materialize one scaled vertex in world space.
 382        /// </summary>
 383        public bool TryGetVertexWorld(int index, out Vector3d vertex)
 384        {
 1830385            SwiftThrowHelper.ThrowIfArrayIndexInvalid(index, _localVertices.Length, nameof(index));
 1830386            return CreatePointAnchor(_scaledLocalVertices[index])
 1830387                .TryGetPoint(out vertex);
 388        }
 389
 390        /// <summary>
 391        /// Materializes one scaled vertex in world space.
 392        /// </summary>
 393        /// <exception cref="InvalidOperationException">
 394        /// The conceptual vertex lies outside the representable world-coordinate
 395        /// domain.
 396        /// </exception>
 397        public Vector3d GetVertexWorld(int index)
 398        {
 1823399            if (TryGetVertexWorld(index, out Vector3d vertex))
 1822400                return vertex;
 401
 1402            throw new InvalidOperationException(
 1403                "The selected vertex lies outside the Fixed64 world-coordinate domain.");
 404        }
 405
 406        /// <summary>
 407        /// Attempts to materialize the world-space vertex with the greatest
 408        /// projection onto <paramref name="direction"/>, preserving source
 409        /// vertex order for ties.
 410        /// </summary>
 411        public bool TryGetSupportVertexWorld(
 412            Vector3d direction,
 413            out Vector3d vertex)
 414        {
 36415            Vector3d localPoint = GetSupportVertexLocal(direction);
 36416            return CreatePointAnchor(localPoint).TryGetPoint(out vertex);
 417        }
 418
 419        /// <summary>
 420        /// Materializes the world-space vertex with the greatest projection
 421        /// onto <paramref name="direction"/>, preserving source vertex order
 422        /// for ties.
 423        /// </summary>
 424        /// <exception cref="InvalidOperationException">
 425        /// The conceptual support vertex lies outside the representable
 426        /// world-coordinate domain.
 427        /// </exception>
 428        public Vector3d GetSupportVertexWorld(Vector3d direction)
 429        {
 35430            if (TryGetSupportVertexWorld(direction, out Vector3d vertex))
 34431                return vertex;
 432
 1433            throw new InvalidOperationException(
 1434                "The selected support vertex lies outside the Fixed64 world-coordinate domain.");
 435        }
 436
 437        internal Vector3d GetSupportVertexLocal(Vector3d direction)
 438        {
 331439            Vector3d localDirection = ConvertWorldDirectionToLocal(direction);
 331440            localDirection = localDirection != Vector3d.Zero
 331441                ? localDirection.Normalized
 331442                : Vector3d.Right;
 443
 331444            if (_supportTreeNodes != null && _supportVertexIndices != null)
 21445                return _scaledLocalVertices[FindSupportVertexIndex(localDirection)];
 446
 310447            int bestIndex = 0;
 4924448            for (int i = 1; i < _scaledLocalVertices.Length; i++)
 449            {
 2152450                if (Vector3d.CompareProjection(
 2152451                        _scaledLocalVertices[i],
 2152452                        _scaledLocalVertices[bestIndex],
 2152453                        localDirection) <= 0)
 454                    continue;
 455
 624456                bestIndex = i;
 457            }
 458
 310459            return _scaledLocalVertices[bestIndex];
 460        }
 461
 462        private int FindSupportVertexIndex(Vector3d localDirection)
 463        {
 21464            int bestIndex = 0;
 21465            Span<int> stack = stackalloc int[SupportTreeStackCapacity];
 21466            int stackCount = 0;
 21467            stack[stackCount++] = 0;
 468
 242469            while (stackCount > 0)
 470            {
 221471                int nodeIndex = stack[--stackCount];
 221472                SupportTreeNode node = _supportTreeNodes![nodeIndex];
 221473                Vector3d upperPoint = GetBoundsSupportPoint(node.Min, node.Max, localDirection);
 221474                int upperComparison = Vector3d.CompareProjection(
 221475                    upperPoint,
 221476                    _scaledLocalVertices[bestIndex],
 221477                    localDirection);
 221478                if (upperComparison < 0 || (upperComparison == 0 && node.MinVertexIndex >= bestIndex))
 479                    continue;
 480
 165481                if (node.IsLeaf)
 482                {
 40483                    SearchSupportLeaf(node, localDirection, ref bestIndex);
 40484                    continue;
 485                }
 486
 125487                SupportTreeNode left = _supportTreeNodes[node.Left];
 125488                SupportTreeNode right = _supportTreeNodes[node.Right];
 489
 125490                if (ComesBeforeSupportNode(left, right, localDirection))
 491                {
 46492                    PushSupportNode(right, bestIndex, localDirection, stack, ref stackCount);
 46493                    PushSupportNode(left, bestIndex, localDirection, stack, ref stackCount);
 46494                    continue;
 495                }
 496
 79497                PushSupportNode(left, bestIndex, localDirection, stack, ref stackCount);
 79498                PushSupportNode(right, bestIndex, localDirection, stack, ref stackCount);
 499            }
 500
 21501            return bestIndex;
 502        }
 503
 504        private void SearchSupportLeaf(
 505            SupportTreeNode node,
 506            Vector3d localDirection,
 507            ref int bestIndex)
 508        {
 542509            for (int i = 0; i < node.Count; i++)
 510            {
 231511                int vertexIndex = _supportVertexIndices![node.Start + i];
 231512                Vector3d vertex = _scaledLocalVertices[vertexIndex];
 231513                int projectionComparison = Vector3d.CompareProjection(
 231514                    vertex,
 231515                    _scaledLocalVertices[bestIndex],
 231516                    localDirection);
 231517                if (projectionComparison < 0 || (projectionComparison == 0 && vertexIndex >= bestIndex))
 518                    continue;
 519
 42520                bestIndex = vertexIndex;
 521            }
 40522        }
 523
 524        private void PushSupportNode(
 525            SupportTreeNode node,
 526            int bestIndex,
 527            Vector3d localDirection,
 528            Span<int> stack,
 529            ref int stackCount)
 530        {
 250531            Vector3d upperPoint = GetBoundsSupportPoint(node.Min, node.Max, localDirection);
 250532            int upperComparison = Vector3d.CompareProjection(
 250533                upperPoint,
 250534                _scaledLocalVertices[bestIndex],
 250535                localDirection);
 250536            if (upperComparison < 0 || (upperComparison == 0 && node.MinVertexIndex >= bestIndex))
 50537                return;
 538
 200539            stack[stackCount++] = node.Index;
 200540        }
 541
 542        private static bool ComesBeforeSupportNode(
 543            SupportTreeNode left,
 544            SupportTreeNode right,
 545            Vector3d localDirection)
 546        {
 125547            Vector3d leftPoint = GetBoundsSupportPoint(left.Min, left.Max, localDirection);
 125548            Vector3d rightPoint = GetBoundsSupportPoint(right.Min, right.Max, localDirection);
 125549            int projectionComparison = Vector3d.CompareProjection(
 125550                leftPoint,
 125551                rightPoint,
 125552                localDirection);
 125553            if (projectionComparison != 0)
 69554                return projectionComparison > 0;
 555
 56556            return left.MinVertexIndex < right.MinVertexIndex;
 557        }
 558
 559        private int BuildSupportTreeNode(
 560            Vector3d[] vertices,
 561            int[] vertexIndices,
 562            SupportTreeNode[] nodes,
 563            SupportVertexIndexComparer comparer,
 564            ref int nodeCount,
 565            int start,
 566            int count)
 567        {
 4744568            int nodeIndex = nodeCount++;
 569
 4744570            CalculateSupportRangeBounds(
 4744571                vertices,
 4744572                vertexIndices,
 4744573                start,
 4744574                count,
 4744575                out Vector3d min,
 4744576                out Vector3d max,
 4744577                out int minVertexIndex);
 4744578            if (count <= SupportTreeLeafVertexCount)
 579            {
 2378580                nodes[nodeIndex] = SupportTreeNode.CreateLeaf(
 2378581                    nodeIndex,
 2378582                    min,
 2378583                    max,
 2378584                    start,
 2378585                    count,
 2378586                    minVertexIndex);
 2378587                return nodeIndex;
 588            }
 589
 2366590            int axis = GetDominantAxis(max - min);
 2366591            comparer.Reset(vertices, axis);
 2366592            Array.Sort(
 2366593                vertexIndices,
 2366594                start,
 2366595                count,
 2366596                comparer);
 597
 2366598            int leftCount = count / 2;
 2366599            int rightCount = count - leftCount;
 2366600            int leftIndex = BuildSupportTreeNode(
 2366601                vertices,
 2366602                vertexIndices,
 2366603                nodes,
 2366604                comparer,
 2366605                ref nodeCount,
 2366606                start,
 2366607                leftCount);
 2366608            int rightIndex = BuildSupportTreeNode(
 2366609                vertices,
 2366610                vertexIndices,
 2366611                nodes,
 2366612                comparer,
 2366613                ref nodeCount,
 2366614                start + leftCount,
 2366615                rightCount);
 2366616            nodes[nodeIndex] = SupportTreeNode.CreateBranch(
 2366617                nodeIndex,
 2366618                min,
 2366619                max,
 2366620                leftIndex,
 2366621                rightIndex,
 2366622                minVertexIndex);
 2366623            return nodeIndex;
 624        }
 625
 626        private void RefitSupportTree(
 627            Vector3d[] vertices,
 628            SupportTreeNode[] sourceNodes,
 629            SupportTreeNode[] targetNodes)
 630        {
 631            // Children follow their parent in the immutable preorder topology,
 632            // so reverse traversal makes both child bounds available first.
 10528633            for (int i = _supportTreeNodeCount - 1; i >= 0; i--)
 634            {
 5252635                SupportTreeNode source = sourceNodes[i];
 5252636                if (source.IsLeaf)
 637                {
 2632638                    CalculateSupportRangeBounds(
 2632639                        vertices,
 2632640                        _supportVertexIndices!,
 2632641                        source.Start,
 2632642                        source.Count,
 2632643                        out Vector3d min,
 2632644                        out Vector3d max,
 2632645                        out int minVertexIndex);
 2632646                    targetNodes[i] = SupportTreeNode.CreateLeaf(
 2632647                        source.Index,
 2632648                        min,
 2632649                        max,
 2632650                        source.Start,
 2632651                        source.Count,
 2632652                        minVertexIndex);
 2632653                    continue;
 654                }
 655
 2620656                SupportTreeNode left = targetNodes[source.Left];
 2620657                SupportTreeNode right = targetNodes[source.Right];
 2620658                targetNodes[i] = SupportTreeNode.CreateBranch(
 2620659                    source.Index,
 2620660                    Vector3d.Min(left.Min, right.Min),
 2620661                    Vector3d.Max(left.Max, right.Max),
 2620662                    source.Left,
 2620663                    source.Right,
 2620664                    source.MinVertexIndex);
 665            }
 12666        }
 667
 668        private static void CalculateSupportRangeBounds(
 669            Vector3d[] vertices,
 670            int[] vertexIndices,
 671            int start,
 672            int count,
 673            out Vector3d min,
 674            out Vector3d max,
 675            out int minVertexIndex)
 676        {
 7376677            int firstIndex = vertexIndices[start];
 7376678            minVertexIndex = firstIndex;
 7376679            min = vertices[firstIndex];
 7376680            max = min;
 328386681            for (int i = 1; i < count; i++)
 682            {
 156817683                int vertexIndex = vertexIndices[start + i];
 156817684                Vector3d vertex = vertices[vertexIndex];
 156817685                min = Vector3d.Min(min, vertex);
 156817686                max = Vector3d.Max(max, vertex);
 156817687                if (vertexIndex < minVertexIndex)
 2442688                    minVertexIndex = vertexIndex;
 689            }
 7376690        }
 691
 692        private static int[] CreateSupportVertexIndices(int vertexCount)
 693        {
 12694            var indices = new int[vertexCount];
 28612695            for (int i = 0; i < indices.Length; i++)
 14294696                indices[i] = i;
 697
 12698            return indices;
 699        }
 700
 701        private static int GetDominantAxis(Vector3d extents)
 702        {
 2366703            if (extents.X >= extents.Y && extents.X >= extents.Z)
 720704                return 0;
 705
 1646706            return extents.Y >= extents.Z ? 1 : 2;
 707        }
 708
 709        private static Vector3d GetBoundsSupportPoint(Vector3d min, Vector3d max, Vector3d direction) =>
 721710            new(
 721711                direction.X >= Fixed64.Zero ? max.X : min.X,
 721712                direction.Y >= Fixed64.Zero ? max.Y : min.Y,
 721713                direction.Z >= Fixed64.Zero ? max.Z : min.Z);
 714
 715        /// <summary>Gets a triangle face normal in world space.</summary>
 716        public Vector3d GetFaceNormalWorld(int index)
 717        {
 1380718            SwiftThrowHelper.ThrowIfArrayIndexInvalid(index, _triangleCount, nameof(index));
 1380719            _ = _rotation.TryRotate(
 1380720                _scaledFaceNormals[index],
 1380721                out Vector3d worldNormal);
 1380722            return worldNormal.Normalized;
 723        }
 724
 725        /// <summary>Writes triangle indices overlapping world-space bounds into a caller-owned buffer.</summary>
 726        public void GetTrianglesInWorldBounds(FixedBoundVolume worldBounds, SwiftList<int> result)
 727        {
 2346728            result.FastClear();
 2346729            FixedBoundBox localBounds =
 2346730                FixedBoundBox.FromRelativeRotatedBoundsClippedToDomain(
 2346731                    Vector3d.Zero,
 2346732                    FixedQuaternion.Identity,
 2346733                    worldBounds.Min,
 2346734                    worldBounds.Max,
 2346735                    _position,
 2346736                    _rotation);
 2346737            TriangleBVH.Query(
 2346738                new FixedBoundVolume(localBounds.Min, localBounds.Max),
 2346739                result);
 2346740        }
 741
 742        /// <summary>Writes triangle indices overlapping scaled-local bounds into a caller-owned buffer.</summary>
 743        public void GetTrianglesInLocalBounds(FixedBoundVolume localBounds, SwiftList<int> result)
 744        {
 8221745            result.FastClear();
 8221746            TriangleBVH.Query(localBounds, result);
 8221747        }
 748
 749        /// <summary>
 750        /// Attempts to express a world point in the committed scaled-local
 751        /// mesh frame without saturating the relative displacement.
 752        /// </summary>
 753        public bool TryConvertWorldToScaledLocal(
 754            Vector3d worldPoint,
 755            out Vector3d localPoint) =>
 1017756            new FixedPointAnchor(
 1017757                worldPoint,
 1017758                FixedQuaternion.Identity,
 1017759                Vector3d.Zero)
 1017760            .TryGetLocalPointIn(
 1017761                _position,
 1017762                _rotation,
 1017763                out localPoint);
 764
 765        /// <summary>
 766        /// Expresses a world point in the committed scaled-local mesh frame.
 767        /// </summary>
 768        /// <exception cref="InvalidOperationException">
 769        /// The exact relative displacement lies outside the representable
 770        /// coordinate domain.
 771        /// </exception>
 772        public Vector3d ConvertWorldToScaledLocal(Vector3d worldPoint)
 773        {
 2774            if (TryConvertWorldToScaledLocal(worldPoint, out Vector3d localPoint))
 1775                return localPoint;
 776
 1777            throw new InvalidOperationException(
 1778                "The world point cannot be represented in the mesh's scaled-local frame.");
 779        }
 780
 781        /// <summary>
 782        /// Attempts to materialize a scaled-local mesh point in world space.
 783        /// </summary>
 784        public bool TryConvertScaledLocalToWorld(
 785            Vector3d scaledLocalPoint,
 786            out Vector3d worldPoint) =>
 543787            CreatePointAnchor(scaledLocalPoint).TryGetPoint(out worldPoint);
 788
 789        /// <summary>
 790        /// Materializes a scaled-local mesh point in world space.
 791        /// </summary>
 792        /// <exception cref="InvalidOperationException">
 793        /// The conceptual world point lies outside the representable
 794        /// coordinate domain.
 795        /// </exception>
 796        public Vector3d ConvertScaledLocalToWorld(Vector3d scaledLocalPoint)
 797        {
 3798            if (TryConvertScaledLocalToWorld(
 3799                    scaledLocalPoint,
 3800                    out Vector3d worldPoint))
 801            {
 2802                return worldPoint;
 803            }
 804
 1805            throw new InvalidOperationException(
 1806                "The scaled-local point lies outside the Fixed64 world-coordinate domain.");
 807        }
 808
 809        internal FixedPointAnchor CreatePointAnchor(
 810            Vector3d scaledLocalPoint) =>
 4147811            new(
 4147812                _position,
 4147813                _rotation,
 4147814                scaledLocalPoint);
 815
 816        /// <summary>
 817        /// Attempts to express a world-space direction in the committed
 818        /// scaled-local mesh frame.
 819        /// </summary>
 820        public bool TryConvertWorldDirectionToLocal(
 821            Vector3d worldDirection,
 822            out Vector3d localDirection) =>
 333823            _rotation.Inverse().TryRotate(
 333824                worldDirection,
 333825                out localDirection);
 826
 827        /// <summary>
 828        /// Expresses a world-space direction in the committed scaled-local
 829        /// mesh frame.
 830        /// </summary>
 831        public Vector3d ConvertWorldDirectionToLocal(
 832            Vector3d worldDirection)
 833        {
 332834            if (TryConvertWorldDirectionToLocal(
 332835                    worldDirection,
 332836                    out Vector3d localDirection))
 837            {
 331838                return localDirection;
 839            }
 840
 1841            throw new InvalidOperationException(
 1842                "The world direction cannot be represented in the mesh's scaled-local frame.");
 843        }
 844
 845        private readonly struct EdgeUse
 846        {
 847            private EdgeUse(long key, int direction, int triangleIndex)
 848            {
 390813849                Key = key;
 390813850                Direction = direction;
 390813851                TriangleIndex = triangleIndex;
 390813852            }
 853
 854            public long Key { get; }
 855
 222628856            public int StartVertexIndex => (int)(Key >> 32);
 857
 222628858            public int EndVertexIndex => (int)(uint)Key;
 859
 860            public int Direction { get; }
 861
 862            public int TriangleIndex { get; }
 863
 864            public static EdgeUse Create(int start, int end, int triangleIndex)
 865            {
 390813866                int min = start < end ? start : end;
 390813867                int max = start < end ? end : start;
 390813868                long key = ((long)min << 32) | (uint)max;
 390813869                int direction = start == min ? 1 : -1;
 390813870                return new EdgeUse(key, direction, triangleIndex);
 871            }
 872        }
 873
 874        private readonly struct TriangleUse
 875        {
 876            private TriangleUse(int a, int b, int c)
 877            {
 130271878                A = a;
 130271879                B = b;
 130271880                C = c;
 130271881            }
 882
 883            public int A { get; }
 884
 885            public int B { get; }
 886
 887            public int C { get; }
 888
 889            public static TriangleUse Create(int first, int second, int third)
 890            {
 130271891                int a = first;
 130271892                int b = second;
 130271893                int c = third;
 894
 130271895                if (a > b)
 7268896                    (a, b) = (b, a);
 130271897                if (b > c)
 14818898                    (b, c) = (c, b);
 130271899                if (a > b)
 6737900                    (a, b) = (b, a);
 901
 130271902                return new TriangleUse(a, b, c);
 903            }
 904        }
 905
 906        private readonly struct SupportTreeNode
 907        {
 908            private SupportTreeNode(
 909                int index,
 910                Vector3d min,
 911                Vector3d max,
 912                int left,
 913                int right,
 914                int start,
 915                int count,
 916                int minVertexIndex)
 917            {
 9996918                Index = index;
 9996919                Min = min;
 9996920                Max = max;
 9996921                Left = left;
 9996922                Right = right;
 9996923                Start = start;
 9996924                Count = count;
 9996925                MinVertexIndex = minVertexIndex;
 9996926            }
 927
 928            public int Index { get; }
 929
 930            public Vector3d Min { get; }
 931
 932            public Vector3d Max { get; }
 933
 934            public int Left { get; }
 935
 936            public int Right { get; }
 937
 938            public int Start { get; }
 939
 940            public int Count { get; }
 941
 942            public int MinVertexIndex { get; }
 943
 5417944            public bool IsLeaf => Count > 0;
 945
 946            public static SupportTreeNode CreateLeaf(
 947                int index,
 948                Vector3d min,
 949                Vector3d max,
 950                int start,
 951                int count,
 952                int minVertexIndex) =>
 5010953                new(index, min, max, -1, -1, start, count, minVertexIndex);
 954
 955            public static SupportTreeNode CreateBranch(
 956                int index,
 957                Vector3d min,
 958                Vector3d max,
 959                int left,
 960                int right,
 961                int minVertexIndex) =>
 4986962                new(index, min, max, left, right, 0, 0, minVertexIndex);
 963        }
 964
 965        private sealed class SupportVertexIndexComparer : IComparer<int>
 966        {
 12967            private Vector3d[] _vertices = Array.Empty<Vector3d>();
 968            private int _axis;
 969
 970            public void Reset(Vector3d[] vertices, int axis)
 971            {
 2366972                _vertices = vertices;
 2366973                _axis = axis;
 2366974            }
 975
 976            public int Compare(int first, int second)
 977            {
 1072003978                Fixed64 firstValue = GetAxisValue(_vertices[first], _axis);
 1072003979                Fixed64 secondValue = GetAxisValue(_vertices[second], _axis);
 1072003980                int valueComparison = firstValue.CompareTo(secondValue);
 1072003981                if (valueComparison != 0)
 535743982                    return valueComparison;
 983
 536260984                return first.CompareTo(second);
 985            }
 986
 987            private static Fixed64 GetAxisValue(Vector3d vertex, int axis)
 988            {
 2144006989                return axis switch
 2144006990                {
 799798991                    0 => vertex.X,
 735626992                    1 => vertex.Y,
 608582993                    _ => vertex.Z
 2144006994                };
 995            }
 996        }
 997    }
 998}

/home/runner/work/Gravitas/Gravitas/src/Gravitas/Colliders/Mesh/PhysicsMesh.MassProperties.cs

#LineLine coverage
 1//=======================================================================
 2// PhysicsMesh.MassProperties.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 FixedMathSharp.Geometry;
 10using System;
 11
 12namespace Gravitas.Colliders;
 13
 14public partial class PhysicsMesh
 15{
 16    private bool _closedVolumeMassPropertiesEvaluated;
 17    private MeshMassProperties _closedVolumeMassProperties;
 18    private MeshVolumeValidationResult _closedVolumeValidationResult;
 19    private bool _scaledClosedVolumeMassPropertiesEvaluated;
 20    private MeshMassProperties _scaledClosedVolumeMassProperties;
 21    private MeshVolumeValidationResult _scaledClosedVolumeValidationResult;
 22    private bool _preparedClosedVolumeMassPropertiesEvaluated;
 23    private MeshMassProperties _preparedClosedVolumeMassProperties;
 24    private MeshVolumeValidationResult _preparedClosedVolumeValidationResult;
 25    private bool _preparedClosedVolumeEvaluationIncrement;
 26    private MeshVolumeValidationResult _surfaceClosureValidationResult;
 27
 28    internal int ClosedVolumeScaleEvaluationCount { get; private set; }
 29
 30    /// <summary>Calculates closed-volume mesh inertia for the supplied mass.</summary>
 31    public Fixed3x3 CalculateInertiaTensor(Fixed64 mass) =>
 832        CalculateInertiaTensor(mass, MeshInertiaPolicy.RequireClosedVolume);
 33
 34    /// <summary>
 35    /// Calculates mesh inertia for the supplied mass using the requested policy.
 36    /// This is a geometry/topology API; callers apply body mobility gates before requesting inertia.
 37    /// </summary>
 38    public Fixed3x3 CalculateInertiaTensor(Fixed64 mass, MeshInertiaPolicy policy)
 39    {
 1040        if (policy == MeshInertiaPolicy.RequireClosedVolume
 1041            && TryGetClosedVolumeMassProperties(out MeshMassProperties properties, out _))
 42        {
 843            return CalculateInertiaTensor(mass, policy, properties.CenterOfMass);
 44        }
 45
 246        if (policy == MeshInertiaPolicy.SurfaceApproximation)
 147            return CalculateInertiaTensor(mass, policy, SurfaceMassProperties.CenterOfMass);
 48
 149        return CalculateInertiaTensor(mass, policy, _scaledLocalBounds.Center);
 50    }
 51
 52    /// <summary>
 53    /// Calculates mesh inertia for the supplied mass about a specific local reference point.
 54    /// This is a geometry/topology API; callers apply body mobility gates before requesting inertia.
 55    /// </summary>
 56    public Fixed3x3 CalculateInertiaTensor(Fixed64 mass, MeshInertiaPolicy policy, Vector3d localReferencePoint)
 57    {
 58        switch (policy)
 59        {
 60            case MeshInertiaPolicy.RequireClosedVolume:
 4561                if (!TryGetClosedVolumeMassProperties(out MeshMassProperties properties, out MeshVolumeValidationResult 
 262                    throw new InvalidOperationException($"Mesh inertia requires a validated closed volume. Validation re
 63
 4364                return properties.CalculateInertiaTensor(mass, localReferencePoint);
 65
 66            case MeshInertiaPolicy.SurfaceApproximation:
 12267                return SurfaceMassProperties.CalculateInertiaTensor(mass, localReferencePoint);
 68
 69            default:
 170                throw new ArgumentOutOfRangeException(nameof(policy), policy, "Unsupported mesh inertia policy.");
 71        }
 72    }
 73
 74    /// <summary>
 75    /// Gets cached closed-volume mass properties when this mesh is a valid closed triangle shell.
 76    /// </summary>
 77    public bool TryGetClosedVolumeMassProperties(
 78        out MeshMassProperties properties,
 79        out MeshVolumeValidationResult result)
 80    {
 27781        EnsureScaledClosedVolumeMassProperties();
 27782        properties = _scaledClosedVolumeMassProperties;
 27783        result = _scaledClosedVolumeValidationResult;
 27784        return result == MeshVolumeValidationResult.Valid;
 85    }
 86
 87    internal bool TryGetPreparedClosedVolumeMassProperties(
 88        out MeshMassProperties properties,
 89        out MeshVolumeValidationResult result)
 90    {
 20891        properties = _preparedClosedVolumeMassProperties;
 20892        result = _preparedClosedVolumeValidationResult;
 20893        return _preparedClosedVolumeMassPropertiesEvaluated
 20894            && result == MeshVolumeValidationResult.Valid;
 95    }
 96
 97    private void PrepareClosedVolumeMassProperties(
 98        MeshInertiaPolicy? inertiaPolicy,
 99        bool geometryChanged,
 100        Vector3d ownerScale,
 101        Vector3d partScale)
 102    {
 1049103        _preparedClosedVolumeEvaluationIncrement = false;
 1049104        if (!geometryChanged)
 105        {
 597106            _preparedClosedVolumeMassPropertiesEvaluated =
 597107                _scaledClosedVolumeMassPropertiesEvaluated;
 597108            _preparedClosedVolumeMassProperties =
 597109                _scaledClosedVolumeMassProperties;
 597110            _preparedClosedVolumeValidationResult =
 597111                _scaledClosedVolumeValidationResult;
 597112            return;
 113        }
 114
 452115        _preparedClosedVolumeMassPropertiesEvaluated = false;
 452116        if (!IsClosedSurface)
 117        {
 286118            _preparedClosedVolumeMassPropertiesEvaluated = true;
 286119            _preparedClosedVolumeValidationResult = _surfaceClosureValidationResult;
 286120            _preparedClosedVolumeMassProperties = default;
 286121            return;
 122        }
 123
 166124        if (inertiaPolicy != MeshInertiaPolicy.RequireClosedVolume)
 150125            return;
 126
 16127        _preparedClosedVolumeMassPropertiesEvaluated = true;
 16128        _preparedClosedVolumeEvaluationIncrement = true;
 16129        if (!TryCalculateCandidateClosedVolumeMassProperties(
 16130                ownerScale,
 16131                partScale,
 16132                usePreparedVertices: true,
 16133                out _preparedClosedVolumeMassProperties,
 16134                out _preparedClosedVolumeValidationResult))
 135        {
 7136            throw new ArgumentException(
 7137                $"Mesh scale must preserve a representable closed volume. Validation result: {_preparedClosedVolumeValid
 7138                nameof(inertiaPolicy));
 139        }
 9140    }
 141
 142    private bool TryCalculateCandidateClosedVolumeMassProperties(
 143        Vector3d ownerScale,
 144        Vector3d partScale,
 145        bool usePreparedVertices,
 146        out MeshMassProperties properties,
 147        out MeshVolumeValidationResult result)
 148    {
 69149        Fixed64 x = default;
 69150        Fixed64 y = default;
 69151        Fixed64 z = default;
 69152        if (Fixed64.TryMultiplyDivide(
 69153                ownerScale.X,
 69154                partScale.X,
 69155                Fixed64.One,
 69156                out x)
 69157            && Fixed64.TryMultiplyDivide(
 69158                ownerScale.Y,
 69159                partScale.Y,
 69160                Fixed64.One,
 69161                out y)
 69162            && Fixed64.TryMultiplyDivide(
 69163                ownerScale.Z,
 69164                partScale.Z,
 69165                Fixed64.One,
 69166                out z)
 69167            && TryScalePoint(
 69168                _localBounds.Center,
 69169                ownerScale,
 69170                partScale,
 69171                out Vector3d scaledSourceCenter))
 172        {
 66173            EnsureClosedVolumeMassProperties();
 66174            if (_closedVolumeValidationResult != MeshVolumeValidationResult.Valid)
 175            {
 1176                properties = default;
 1177                result = _closedVolumeValidationResult;
 1178                return false;
 179            }
 180
 65181            MeshMassScaleResult scaleResult = _closedVolumeMassProperties.TryScale(
 65182                new Vector3d(x, y, z),
 65183                out properties);
 65184            if (scaleResult == MeshMassScaleResult.Valid)
 185            {
 186                // Both differences lie inside the admitted centered vertex
 187                // bounds, so candidate scaling guarantees representability.
 58188                _ = Vector3d.TrySubtract(
 58189                    properties.CenterOfMass,
 58190                    scaledSourceCenter,
 58191                    out Vector3d centeredMass);
 58192                _ = Vector3d.TrySubtract(
 58193                    properties.InertiaReferencePoint,
 58194                    scaledSourceCenter,
 58195                    out Vector3d centeredReference);
 196
 58197                properties = new MeshMassProperties(
 58198                    properties.Volume,
 58199                    centeredMass,
 58200                    centeredReference,
 58201                    properties.UnitMassInertiaTensor);
 58202                result = properties.Volume > Fixed64.Epsilon
 58203                    ? MeshVolumeValidationResult.Valid
 58204                    : MeshVolumeValidationResult.ZeroVolume;
 58205                return result == MeshVolumeValidationResult.Valid;
 206            }
 207
 7208            properties = default;
 7209            result = scaleResult == MeshMassScaleResult.NonRepresentableVolume
 7210                ? MeshVolumeValidationResult.NonRepresentableVolume
 7211                : MeshVolumeValidationResult.NonRepresentableMassProperties;
 7212            return false;
 213        }
 214
 3215        if (usePreparedVertices)
 216        {
 1217            return TryCalculateClosedVolumeMassProperties(
 1218                _preparedScaledLocalVertices,
 1219                _preparedScaledLocalBounds,
 1220                out properties,
 1221                out result);
 222        }
 223
 2224        return TryCalculateClosedVolumeMassProperties(
 2225            _scaledLocalVertices,
 2226            _scaledLocalBounds,
 2227            out properties,
 2228            out result);
 229    }
 230
 231    private static bool TryScalePoint(
 232        Vector3d point,
 233        Vector3d ownerScale,
 234        Vector3d partScale,
 235        out Vector3d result)
 236    {
 68237        result = default;
 68238        if (!Fixed64.TryMultiplyDivide(
 68239                point.X,
 68240                ownerScale.X,
 68241                partScale.X,
 68242                Fixed64.One,
 68243                out Fixed64 x)
 68244            || !Fixed64.TryMultiplyDivide(
 68245                point.Y,
 68246                ownerScale.Y,
 68247                partScale.Y,
 68248                Fixed64.One,
 68249                out Fixed64 y)
 68250            || !Fixed64.TryMultiplyDivide(
 68251                point.Z,
 68252                ownerScale.Z,
 68253                partScale.Z,
 68254                Fixed64.One,
 68255                out Fixed64 z))
 256        {
 2257            return false;
 258        }
 259
 66260        result = new Vector3d(x, y, z);
 66261        return true;
 262    }
 263
 264    private void PublishPreparedClosedVolumeMassProperties()
 265    {
 445266        _scaledClosedVolumeMassPropertiesEvaluated =
 445267            _preparedClosedVolumeMassPropertiesEvaluated;
 445268        _scaledClosedVolumeMassProperties =
 445269            _preparedClosedVolumeMassProperties;
 445270        _scaledClosedVolumeValidationResult =
 445271            _preparedClosedVolumeValidationResult;
 445272        if (_preparedClosedVolumeEvaluationIncrement)
 9273            ClosedVolumeScaleEvaluationCount++;
 445274    }
 275
 276    private void EnsureScaledClosedVolumeMassProperties()
 277    {
 277278        if (_scaledClosedVolumeMassPropertiesEvaluated)
 224279            return;
 280
 53281        _scaledClosedVolumeMassPropertiesEvaluated = true;
 53282        ClosedVolumeScaleEvaluationCount++;
 53283        TryCalculateCandidateClosedVolumeMassProperties(
 53284            _ownerScale,
 53285            _partScale,
 53286            usePreparedVertices: false,
 53287            out _scaledClosedVolumeMassProperties,
 53288            out _scaledClosedVolumeValidationResult);
 53289    }
 290
 291    private void EnsureClosedVolumeMassProperties()
 292    {
 66293        if (_closedVolumeMassPropertiesEvaluated)
 7294            return;
 295
 59296        _closedVolumeMassPropertiesEvaluated = true;
 59297        TryCalculateClosedVolumeMassProperties(
 59298            _localVertices,
 59299            _localBounds,
 59300            out _closedVolumeMassProperties,
 59301            out _closedVolumeValidationResult);
 59302    }
 303
 304    private bool EvaluateClosedVolumeTopology(int[] triangles, out MeshVolumeValidationResult result)
 305    {
 165306        var triangleUses = new TriangleUse[_triangleCount];
 165307        var edgeUses = new EdgeUse[_triangleCount * 3];
 165308        int edgeIndex = 0;
 201882309        for (int i = 0; i < _triangleCount; i++)
 310        {
 100776311            int triangleIndex = i * 3;
 100776312            int index0 = triangles[triangleIndex];
 100776313            int index1 = triangles[triangleIndex + 1];
 100776314            int index2 = triangles[triangleIndex + 2];
 315
 100776316            triangleUses[i] = TriangleUse.Create(index0, index1, index2);
 100776317            edgeUses[edgeIndex++] = EdgeUse.Create(index0, index1, i);
 100776318            edgeUses[edgeIndex++] = EdgeUse.Create(index1, index2, i);
 100776319            edgeUses[edgeIndex++] = EdgeUse.Create(index2, index0, i);
 320        }
 321
 165322        if (ContainsDuplicateTriangle(triangleUses))
 323        {
 4324            result = MeshVolumeValidationResult.DuplicateTriangle;
 4325            return false;
 326        }
 327
 161328        Array.Sort(edgeUses, CompareEdgeUses);
 329
 161330        int[] parents = new int[_triangleCount];
 1836331        for (int i = 0; i < parents.Length; i++)
 757332            parents[i] = i;
 333
 698334        for (int i = 0; i < edgeUses.Length;)
 335        {
 524336            int groupStart = i;
 524337            EdgeUse first = edgeUses[i++];
 904338            while (i < edgeUses.Length && edgeUses[i].Key == first.Key)
 380339                i++;
 340
 524341            int count = i - groupStart;
 524342            if (count == 1)
 343            {
 145344                result = MeshVolumeValidationResult.BoundaryEdge;
 145345                return false;
 346            }
 347
 379348            if (count > 2)
 349            {
 1350                result = MeshVolumeValidationResult.NonManifoldEdge;
 1351                return false;
 352            }
 353
 378354            EdgeUse second = edgeUses[groupStart + 1];
 378355            if (first.Direction + second.Direction != 0)
 356            {
 2357                result = MeshVolumeValidationResult.InconsistentWinding;
 2358                return false;
 359            }
 360
 376361            Union(parents, first.TriangleIndex, second.TriangleIndex);
 362        }
 363
 13364        if (!HasSingleVertexLinks(triangles, edgeUses))
 365        {
 1366            result = MeshVolumeValidationResult.NonManifoldVertex;
 1367            return false;
 368        }
 369
 12370        int root = Find(parents, 0);
 272371        for (int i = 1; i < parents.Length; i++)
 372        {
 125373            if (Find(parents, i) == root)
 374                continue;
 375
 1376            result = MeshVolumeValidationResult.DisconnectedShell;
 1377            return false;
 378        }
 379
 11380        result = MeshVolumeValidationResult.Valid;
 11381        return true;
 382    }
 383
 384    private bool TryCalculateClosedVolumeMassProperties(
 385        ReadOnlySpan<Vector3d> vertices,
 386        FixedBoundBox bounds,
 387        out MeshMassProperties properties,
 388        out MeshVolumeValidationResult result)
 389    {
 62390        Vector3d reference = bounds.Center;
 62391        Fixed64 signedVolume = Fixed64.Zero;
 62392        Vector3d firstMoment = Vector3d.Zero;
 62393        Fixed64 integralX2 = Fixed64.Zero;
 62394        Fixed64 integralY2 = Fixed64.Zero;
 62395        Fixed64 integralZ2 = Fixed64.Zero;
 62396        Fixed64 integralXY = Fixed64.Zero;
 62397        Fixed64 integralXZ = Fixed64.Zero;
 62398        Fixed64 integralYZ = Fixed64.Zero;
 399
 55028400        for (int i = 0; i < _triangleCount; i++)
 401        {
 27452402            int triangleIndex = i * 3;
 27452403            Vector3d a = vertices[_triangles[triangleIndex]] - reference;
 27452404            Vector3d b = vertices[_triangles[triangleIndex + 1]] - reference;
 27452405            Vector3d c = vertices[_triangles[triangleIndex + 2]] - reference;
 406
 27452407            Fixed64 volume = Vector3d.Dot(a, Vector3d.Cross(b, c)) / TetrahedronVolumeDivisor;
 27452408            signedVolume += volume;
 27452409            firstMoment += (a + b + c) * (volume / TetrahedronCentroidDivisor);
 410
 27452411            Fixed3x3 productSums = Fixed3x3.CreateBarycentricProductSums(a, b, c);
 27452412            integralX2 += volume * productSums.M11 / SecondMomentIntegralDivisor;
 27452413            integralY2 += volume * productSums.M22 / SecondMomentIntegralDivisor;
 27452414            integralZ2 += volume * productSums.M33 / SecondMomentIntegralDivisor;
 27452415            integralXY += volume * productSums.M12 / ProductMomentIntegralDivisor;
 27452416            integralXZ += volume * productSums.M13 / ProductMomentIntegralDivisor;
 27452417            integralYZ += volume * productSums.M23 / ProductMomentIntegralDivisor;
 418        }
 419
 62420        Fixed64 absoluteVolume = signedVolume.Abs();
 62421        if (absoluteVolume <= Fixed64.Epsilon)
 422        {
 1423            properties = default;
 1424            result = MeshVolumeValidationResult.ZeroVolume;
 1425            return false;
 426        }
 427
 61428        Fixed64 orientationSign = signedVolume < Fixed64.Zero ? -Fixed64.One : Fixed64.One;
 61429        Vector3d centerOfMass = reference + firstMoment / signedVolume;
 61430        Fixed64 ixx = orientationSign * (integralY2 + integralZ2) / absoluteVolume;
 61431        Fixed64 iyy = orientationSign * (integralX2 + integralZ2) / absoluteVolume;
 61432        Fixed64 izz = orientationSign * (integralX2 + integralY2) / absoluteVolume;
 61433        Fixed64 ixy = -orientationSign * integralXY / absoluteVolume;
 61434        Fixed64 ixz = -orientationSign * integralXZ / absoluteVolume;
 61435        Fixed64 iyz = -orientationSign * integralYZ / absoluteVolume;
 436
 61437        properties = new MeshMassProperties(
 61438            absoluteVolume,
 61439            centerOfMass,
 61440            reference,
 61441            new Fixed3x3(
 61442                ixx, ixy, ixz,
 61443                ixy, iyy, iyz,
 61444                ixz, iyz, izz));
 61445        result = MeshVolumeValidationResult.Valid;
 61446        return true;
 447    }
 448
 449    private static bool ContainsDuplicateTriangle(TriangleUse[] triangleUses)
 450    {
 406451        Array.Sort(triangleUses, CompareTriangleUses);
 60514452        for (int i = 1; i < triangleUses.Length; i++)
 453        {
 29855454            if (CompareTriangleUses(triangleUses[i], triangleUses[i - 1]) == 0)
 4455                return true;
 456        }
 457
 402458        return false;
 459    }
 460
 461    private static int CompareTriangleUses(TriangleUse first, TriangleUse second)
 462    {
 1829077463        if (first.A != second.A)
 409239464            return first.A < second.A ? -1 : 1;
 465
 1419838466        if (first.B != second.B)
 32010467            return first.B < second.B ? -1 : 1;
 468
 1387828469        if (first.C != second.C)
 407470            return first.C < second.C ? -1 : 1;
 471
 1387421472        return 0;
 473    }
 474
 475    private static int Find(int[] parents, int index)
 476    {
 610825477        while (parents[index] != index)
 478        {
 231315479            parents[index] = parents[parents[index]];
 231315480            index = parents[index];
 481        }
 482
 379510483        return index;
 484    }
 485
 486    private static void Union(int[] parents, int first, int second)
 487    {
 131380488        int firstRoot = Find(parents, first);
 131380489        int secondRoot = Find(parents, second);
 131380490        if (firstRoot == secondRoot)
 29520491            return;
 492
 101860493        if (firstRoot < secondRoot)
 101689494            parents[secondRoot] = firstRoot;
 495        else
 171496            parents[firstRoot] = secondRoot;
 171497    }
 498}

/home/runner/work/Gravitas/Gravitas/src/Gravitas/Colliders/Mesh/PhysicsMesh.Scale.cs

#LineLine coverage
 1//=======================================================================
 2// PhysicsMesh.Scale.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 FixedMathSharp.Geometry;
 10using SwiftCollections.Query;
 11using System;
 12using static Gravitas.Colliders.MeshCheckedMath;
 13
 14namespace Gravitas.Colliders;
 15
 16public partial class PhysicsMesh
 17{
 18    private Vector3d _position;
 41719    private Vector3d _ownerScale = Vector3d.One;
 41720    private Vector3d _partScale = Vector3d.One;
 21    private bool _scaleInitialized;
 22    private bool _validatedRotationValid;
 23    private FixedQuaternion _validatedRotation;
 24    private Fixed64[] _scaledFaceAreas;
 25    private Fixed64[] _preparedScaledFaceAreas;
 26    private Vector3d[] _scaledFaceNormals;
 27    private Vector3d[] _preparedScaledFaceNormals;
 28    private Fixed64 _scaledTotalArea;
 29    private ExactMassWeight _scaledTotalAreaWeight;
 30    private Fixed64 _scaledLocalRadius;
 31    private FixedBoundBox _scaledLocalBounds;
 32    private MeshSurfaceMassProperties _surfaceMassProperties;
 33    private bool _surfaceMassPropertiesValid;
 34
 35    private Vector3d _preparedPosition;
 36    private Vector3d _preparedOwnerScale;
 37    private Vector3d _preparedPartScale;
 38    private FixedQuaternion _preparedRotation;
 39    private FixedBoundBox _preparedBounds;
 40    private FixedBoundBox _preparedScaledLocalBounds;
 41    private Fixed64 _preparedScaledTotalArea;
 42    private ExactMassWeight _preparedScaledTotalAreaWeight;
 43    private Fixed64 _preparedScaledLocalRadius;
 44    private MeshSurfaceMassProperties _preparedSurfaceMassProperties;
 45    private bool _preparedSurfaceMassPropertiesValid;
 46    private bool _preparedGeometryChanged;
 47
 48    /// <summary>
 49    /// Gets the committed host or standalone scale factor.
 50    /// </summary>
 1551    public Vector3d OwnerScale => _ownerScale;
 52
 53    /// <summary>
 54    /// Gets the committed authored compound-part scale factor.
 55    /// Standalone meshes use <see cref="Vector3d.One"/>.
 56    /// </summary>
 757    public Vector3d PartScale => _partScale;
 58
 59    /// <summary>
 60    /// Gets the current centered, scaled bounds in mesh-local coordinates.
 61    /// </summary>
 8162    public FixedBoundBox ScaledLocalBounds => _scaledLocalBounds;
 63
 30164    internal Fixed64 ScaledLocalRadius => _scaledLocalRadius;
 65
 66    internal ExactMassWeight SurfaceMassWeight =>
 2267        _scaledTotalAreaWeight;
 68
 69    internal ExactMassWeight PreparedSurfaceMassWeight =>
 2270        _preparedScaledTotalAreaWeight;
 71
 72    internal Fixed64 GetScaledLocalRadius(
 73        Vector3d ownerScale,
 74        Vector3d partScale)
 75    {
 676        if (_scaleInitialized
 677            && ownerScale == _ownerScale
 678            && partScale == _partScale)
 79        {
 180            return _scaledLocalRadius;
 81        }
 82
 583        Vector3d sourceCenter = _localBounds.Center;
 584        Fixed64 radius = Fixed64.Zero;
 1885        for (int i = 0; i < _localVertices.Length; i++)
 86        {
 887            Vector3d vertex = _localVertices[i];
 888            if (!Fixed64.TryMultiplyDifference(
 889                    vertex.X,
 890                    sourceCenter.X,
 891                    ownerScale.X,
 892                    partScale.X,
 893                    out Fixed64 x)
 894                || !Fixed64.TryMultiplyDifference(
 895                    vertex.Y,
 896                    sourceCenter.Y,
 897                    ownerScale.Y,
 898                    partScale.Y,
 899                    out Fixed64 y)
 8100                || !Fixed64.TryMultiplyDifference(
 8101                    vertex.Z,
 8102                    sourceCenter.Z,
 8103                    ownerScale.Z,
 8104                    partScale.Z,
 8105                    out Fixed64 z)
 8106                || !new Vector3d(x, y, z).TryGetMagnitudeCeiling(
 8107                    out Fixed64 vertexRadius))
 108            {
 4109                return Fixed64.MaxValue;
 110            }
 111
 4112            radius = FixedMath.Max(radius, vertexRadius);
 113        }
 114
 1115        return radius;
 116    }
 117
 285118    internal FixedBoundBox PreparedBounds => _preparedBounds;
 119
 120    internal MeshSurfaceMassProperties PreparedSurfaceMassProperties =>
 337121        _preparedSurfaceMassProperties;
 122
 123    /// <summary>
 124    /// Gets cached uniform thin-shell mass properties for the committed geometry.
 125    /// </summary>
 126    public MeshSurfaceMassProperties SurfaceMassProperties
 127    {
 128        get
 129        {
 514130            if (!_surfaceMassPropertiesValid)
 2131                throw new InvalidOperationException("Scaled mesh surface mass properties are not representable.");
 132
 512133            return _surfaceMassProperties;
 134        }
 135    }
 136
 137    internal void PrepareTransformation(
 138        Vector3d position,
 139        FixedQuaternion rotation,
 140        Vector3d ownerScale,
 141        Vector3d partScale,
 142        MeshInertiaPolicy? inertiaPolicy)
 143    {
 1059144        ColliderScalePolicy.Validate(ownerScale);
 1057145        ColliderScalePolicy.Validate(partScale);
 1057146        ValidateRotation(rotation);
 147
 1055148        bool geometryChanged = !_scaleInitialized
 1055149            || ownerScale != _ownerScale
 1055150            || partScale != _partScale;
 1055151        _preparedGeometryChanged = geometryChanged;
 1055152        if (geometryChanged)
 458153            PrepareScaledGeometry(ownerScale, partScale);
 154        else
 597155            CopyCommittedGeometryCandidate();
 156
 1050157        if (inertiaPolicy == MeshInertiaPolicy.SurfaceApproximation
 1050158            && !_preparedSurfaceMassPropertiesValid)
 159        {
 1160            throw new ArgumentException(
 1161                "Mesh scale must preserve representable surface mass properties.",
 1162                nameof(ownerScale));
 163        }
 164
 1049165        PrepareClosedVolumeMassProperties(
 1049166            inertiaPolicy,
 1049167            geometryChanged,
 1049168            ownerScale,
 1049169            partScale);
 170
 1042171        _preparedPosition = position;
 1042172        _preparedRotation = rotation;
 1042173        _preparedOwnerScale = ownerScale;
 1042174        _preparedPartScale = partScale;
 1042175        _preparedBounds =
 1042176            FixedBoundBox.FromRelativeRotatedBoundsClippedToDomain(
 1042177                position,
 1042178                rotation,
 1042179                _preparedScaledLocalBounds.Min,
 1042180                _preparedScaledLocalBounds.Max,
 1042181                Vector3d.Zero,
 1042182                FixedQuaternion.Identity);
 1042183    }
 184
 185    internal void PublishPreparedTransformation()
 186    {
 1040187        if (_preparedGeometryChanged)
 188        {
 445189            (_scaledLocalVertices, _preparedScaledLocalVertices) =
 445190                (_preparedScaledLocalVertices, _scaledLocalVertices);
 445191            (_scaledFaceAreas, _preparedScaledFaceAreas) =
 445192                (_preparedScaledFaceAreas, _scaledFaceAreas);
 445193            (_scaledFaceNormals, _preparedScaledFaceNormals) =
 445194                (_preparedScaledFaceNormals, _scaledFaceNormals);
 445195            (_triangleBVH, _preparedTriangleBVH) =
 445196                (_preparedTriangleBVH, _triangleBVH);
 445197            if (_supportVertexIndices != null)
 198            {
 21199                (_supportTreeNodes, _preparedSupportTreeNodes) =
 21200                    (_preparedSupportTreeNodes, _supportTreeNodes);
 201            }
 202
 445203            _scaledLocalBounds = _preparedScaledLocalBounds;
 445204            _scaledTotalArea = _preparedScaledTotalArea;
 445205            _scaledTotalAreaWeight =
 445206                _preparedScaledTotalAreaWeight;
 445207            _scaledLocalRadius = _preparedScaledLocalRadius;
 445208            _surfaceMassProperties = _preparedSurfaceMassProperties;
 445209            _surfaceMassPropertiesValid = _preparedSurfaceMassPropertiesValid;
 445210            _triangleBvhBuildCount++;
 445211            PublishPreparedClosedVolumeMassProperties();
 212        }
 213
 1040214        _position = _preparedPosition;
 1040215        _rotation = _preparedRotation;
 1040216        _ownerScale = _preparedOwnerScale;
 1040217        _partScale = _preparedPartScale;
 1040218        _scaleInitialized = true;
 1040219        _bounds = _preparedBounds;
 1040220    }
 221
 222    internal void ValidateSurfaceMassProperties(Vector3d scale)
 223    {
 3224        SwiftThrowHelper.ThrowIfArgument(
 3225            scale != _ownerScale || _partScale != Vector3d.One,
 3226            nameof(scale),
 3227            "Scale must match the committed standalone mesh scale.");
 1228        _ = SurfaceMassProperties;
 1229    }
 230
 231    internal void ValidateClosedVolumeScaleRepresentability(Vector3d scale)
 232    {
 4233        SwiftThrowHelper.ThrowIfArgument(
 4234            scale != _ownerScale || _partScale != Vector3d.One,
 4235            nameof(scale),
 4236            "Scale must match the committed standalone mesh scale.");
 2237        if (!TryGetClosedVolumeMassProperties(out _, out MeshVolumeValidationResult result)
 2238            && IsClosedSurface)
 239        {
 1240            throw new ArgumentException(
 1241                $"Mesh scale must preserve a representable closed volume. Validation result: {result}.",
 1242                nameof(scale));
 243        }
 1244    }
 245
 246    internal void ValidateRotation(FixedQuaternion rotation)
 247    {
 1057248        if (_validatedRotationValid & rotation == _validatedRotation)
 639249            return;
 250
 418251        Fixed64 magnitudeSquared = rotation.MagnitudeSquared;
 418252        bool rotationValid = IsRepresentable(rotation.X)
 418253            & IsRepresentable(rotation.Y)
 418254            & IsRepresentable(rotation.Z)
 418255            & IsRepresentable(rotation.W)
 418256            & magnitudeSquared > Fixed64.Epsilon
 418257            & magnitudeSquared != Fixed64.MaxValue
 418258            & rotation.IsNormalized();
 418259        if (!rotationValid)
 2260            throw new ArgumentException("Mesh rotation must be a normalized representable quaternion.", nameof(rotation)
 261
 416262        _validatedRotation = rotation;
 416263        _validatedRotationValid = true;
 416264    }
 265
 266    private void PrepareScaledGeometry(Vector3d ownerScale, Vector3d partScale)
 267    {
 458268        Vector3d sourceCenter = _localBounds.Center;
 458269        _preparedScaledLocalRadius = Fixed64.Zero;
 66938270        for (int i = 0; i < _localVertices.Length; i++)
 271        {
 33014272            Vector3d vertex = _localVertices[i];
 33014273            if (!Fixed64.TryMultiplyDifference(
 33014274                    vertex.X,
 33014275                    sourceCenter.X,
 33014276                    ownerScale.X,
 33014277                    partScale.X,
 33014278                    out Fixed64 x)
 33014279                || !Fixed64.TryMultiplyDifference(
 33014280                    vertex.Y,
 33014281                    sourceCenter.Y,
 33014282                    ownerScale.Y,
 33014283                    partScale.Y,
 33014284                    out Fixed64 y)
 33014285                || !Fixed64.TryMultiplyDifference(
 33014286                    vertex.Z,
 33014287                    sourceCenter.Z,
 33014288                    ownerScale.Z,
 33014289                    partScale.Z,
 33014290                    out Fixed64 z))
 291            {
 3292                throw new ArgumentException(
 3293                    "Mesh scale must keep every centered authored vertex representable.",
 3294                    nameof(ownerScale));
 295            }
 296
 33011297            Vector3d scaledVertex = new(x, y, z);
 33011298            _preparedScaledLocalVertices[i] = scaledVertex;
 33011299            if (!scaledVertex.TryGetMagnitudeCeiling(
 33011300                    out Fixed64 vertexRadius))
 301            {
 24302                _preparedScaledLocalRadius = Fixed64.MaxValue;
 303            }
 32987304            else if (vertexRadius > _preparedScaledLocalRadius)
 305            {
 510306                _preparedScaledLocalRadius = vertexRadius;
 307            }
 308        }
 309
 455310        _preparedScaledLocalBounds = CalculateBounds(_preparedScaledLocalVertices);
 455311        _preparedScaledTotalArea = Fixed64.Zero;
 455312        _preparedScaledTotalAreaWeight = ExactMassWeight.Zero;
 324798313        for (int i = 0; i < _triangleCount; i++)
 314        {
 161946315            int triangleIndex = i * 3;
 161946316            Vector3d first = _preparedScaledLocalVertices[_triangles[triangleIndex]];
 161946317            Vector3d second = _preparedScaledLocalVertices[_triangles[triangleIndex + 1]];
 161946318            Vector3d third = _preparedScaledLocalVertices[_triangles[triangleIndex + 2]];
 161946319            var triangle = new FixedTriangle(first, second, third);
 161946320            Vector3d normal = triangle.Normal;
 161946321            Fixed64 area = triangle.Area;
 161946322            if (area <= Fixed64.Epsilon)
 323            {
 2324                throw new ArgumentException(
 2325                    "Mesh scale must keep every source triangle representably nondegenerate.",
 2326                    nameof(ownerScale));
 327            }
 161944328            if (normal == Vector3d.Zero)
 329            {
 330                // FixedTriangle's public degeneracy threshold intentionally uses
 331                // squared area. Mesh admission has always used surface area, so
 332                // preserve an admitted micro-triangle's representable direction.
 32333                normal = triangle.UnnormalizedNormal.Normalized;
 334            }
 335
 161944336            _preparedScaledTotalArea += area;
 161944337            _preparedScaledFaceAreas[i] = area;
 161944338            _preparedScaledFaceNormals[i] = normal;
 339        }
 340
 453341        _preparedSurfaceMassPropertiesValid = MeshSurfaceMassProperties.TryCreate(
 453342            _preparedScaledLocalVertices,
 453343            _triangles,
 453344            _preparedScaledTotalArea,
 453345            out _preparedScaledTotalAreaWeight,
 453346            out _preparedSurfaceMassProperties);
 347
 453348        BuildTriangleBVH(_preparedTriangleBVH, _preparedScaledLocalVertices);
 453349        if (_supportVertexIndices != null)
 350        {
 24351            if (!_scaleInitialized)
 352            {
 12353                _supportTreeNodeCount = 0;
 12354                BuildSupportTreeNode(
 12355                    _preparedScaledLocalVertices,
 12356                    _supportVertexIndices,
 12357                    _preparedSupportTreeNodes!,
 12358                    new SupportVertexIndexComparer(),
 12359                    ref _supportTreeNodeCount,
 12360                    0,
 12361                    _supportVertexIndices.Length);
 362            }
 363            else
 364            {
 12365                RefitSupportTree(
 12366                    _preparedScaledLocalVertices,
 12367                    _supportTreeNodes!,
 12368                    _preparedSupportTreeNodes!);
 369            }
 370        }
 441371    }
 372
 373    private void CopyCommittedGeometryCandidate()
 374    {
 597375        _preparedScaledLocalBounds = _scaledLocalBounds;
 597376        _preparedScaledTotalArea = _scaledTotalArea;
 597377        _preparedScaledTotalAreaWeight =
 597378            _scaledTotalAreaWeight;
 597379        _preparedScaledLocalRadius = _scaledLocalRadius;
 597380        _preparedSurfaceMassProperties = _surfaceMassProperties;
 597381        _preparedSurfaceMassPropertiesValid = _surfaceMassPropertiesValid;
 597382    }
 383
 384}

Methods/Properties

CreateTopologyTriangles(FixedMathSharp.Vector3d[],System.Int32[])
CompareVertexUses(Gravitas.Colliders.PhysicsMesh/VertexUse,Gravitas.Colliders.PhysicsMesh/VertexUse)
CreateConvexSatEdgeVertexPairs(FixedMathSharp.Vector3d[],System.Int32[],System.Int32)
RegisterBoundaryEdge(Gravitas.Colliders.PhysicsMesh/EdgeUse,System.Int32[],System.Int32[],System.Int32&,System.Int32&,System.Int32[])
CalculateSharedEdgeSide(FixedMathSharp.Vector3d[],System.Int32[],Gravitas.Colliders.PhysicsMesh/EdgeUse,Gravitas.Colliders.PhysicsMesh/EdgeUse)
ValidateOpenConvexBoundary(FixedMathSharp.Vector3d[],System.Int32[],System.Int32,System.Int32[],System.Int32,System.Int32)
GetDominantNormalAxis(FixedMathSharp.Vector3d[],System.Int32[])
ProjectToBoundaryPlane(FixedMathSharp.Vector3d,System.Int32)
ValidateBoundaryMatchesConvexHull(Gravitas.Colliders.PhysicsMesh/BoundaryVertex[],System.Int32[])
ValidateProjectedTriangleFill(FixedMathSharp.Vector3d[],System.Int32[],System.Int32,Gravitas.Colliders.PhysicsMesh/BoundaryVertex[],System.Int32)
AppendConvexHullVertex(Gravitas.Colliders.PhysicsMesh/BoundaryVertex[],System.Int32&,System.Int32,Gravitas.Colliders.PhysicsMesh/BoundaryVertex)
MatchesCyclicHull(Gravitas.Colliders.PhysicsMesh/BoundaryVertex[],System.Int32,Gravitas.Colliders.PhysicsMesh/BoundaryVertex[])
CompareBoundaryVertices(Gravitas.Colliders.PhysicsMesh/BoundaryVertex,Gravitas.Colliders.PhysicsMesh/BoundaryVertex)
HasSingleVertexLinks(System.Int32[],Gravitas.Colliders.PhysicsMesh/EdgeUse[])
FindTriangleCornerIndex(System.Int32[],System.Int32,System.Int32)
CompareVertexCornerUses(Gravitas.Colliders.PhysicsMesh/VertexCornerUse,Gravitas.Colliders.PhysicsMesh/VertexCornerUse)
CreateSatEdgeVertexPairs(Gravitas.Colliders.PhysicsMesh/EdgeUse[],FixedMathSharp.Vector3d[],System.Int32[])
CountConvexSatEdges(Gravitas.Colliders.PhysicsMesh/EdgeUse[],FixedMathSharp.Vector3d[],System.Int32[])
FindEdgeUseGroupEnd(Gravitas.Colliders.PhysicsMesh/EdgeUse[],System.Int32)
ShouldIncludeConvexSatEdge(Gravitas.Colliders.PhysicsMesh/EdgeUse[],System.Int32,System.Int32,FixedMathSharp.Vector3d[],System.Int32[])
FindOppositeVertexIndex(System.Int32[],System.Int32,System.Int32,System.Int32)
.ctor(FixedMathSharp.Vector3d,System.Int32)
.ctor(FixedMathSharp.Vector2d,System.Int32)
.ctor(System.Int32,System.Int32)
.cctor()
get_LocalVertices()
get_ScaledLocalVertices()
get_VertexCount()
get_Triangles()
get_TriangleCount()
get_ConvexSatEdgeVertexPairs()
get_TotalArea()
get_TriangleBVH()
get_TriangleBvhBuildCount()
.ctor(FixedMathSharp.Vector3d[],System.Int32[],FixedMathSharp.Vector3d,FixedMathSharp.FixedQuaternion,Gravitas.Colliders.MeshColliderMode)
get_Origin()
get_Rotation()
get_Bounds()
get_LocalBounds()
.ctor(FixedMathSharp.Vector3d[],System.Int32[],FixedMathSharp.Vector3d,FixedMathSharp.FixedQuaternion)
UpdatePosition(FixedMathSharp.Vector3d,FixedMathSharp.FixedQuaternion)
UpdateTransform(FixedMathSharp.Vector3d,FixedMathSharp.FixedQuaternion,FixedMathSharp.Vector3d)
ValidateInput(FixedMathSharp.Vector3d[],System.Int32[])
BuildTriangleBVH(SwiftCollections.Query.SwiftFixedBVH`1<System.Int32>,System.ReadOnlySpan`1<FixedMathSharp.Vector3d>)
CalculateBounds(FixedMathSharp.Vector3d[])
CompareEdgeUses(Gravitas.Colliders.PhysicsMesh/EdgeUse,Gravitas.Colliders.PhysicsMesh/EdgeUse)
GetFrontalArea(FixedMathSharp.Vector3d)
GetTriangleVertices(System.Int32,FixedMathSharp.Vector3d&,FixedMathSharp.Vector3d&,FixedMathSharp.Vector3d&)
TryGetTriangleVertices(System.Int32,FixedMathSharp.Vector3d&,FixedMathSharp.Vector3d&,FixedMathSharp.Vector3d&)
GetLocalTriangleVertices(System.Int32,FixedMathSharp.Vector3d&,FixedMathSharp.Vector3d&,FixedMathSharp.Vector3d&)
GetScaledLocalFaceNormal(System.Int32)
TryGetVertexWorld(System.Int32,FixedMathSharp.Vector3d&)
GetVertexWorld(System.Int32)
TryGetSupportVertexWorld(FixedMathSharp.Vector3d,FixedMathSharp.Vector3d&)
GetSupportVertexWorld(FixedMathSharp.Vector3d)
GetSupportVertexLocal(FixedMathSharp.Vector3d)
FindSupportVertexIndex(FixedMathSharp.Vector3d)
SearchSupportLeaf(Gravitas.Colliders.PhysicsMesh/SupportTreeNode,FixedMathSharp.Vector3d,System.Int32&)
PushSupportNode(Gravitas.Colliders.PhysicsMesh/SupportTreeNode,System.Int32,FixedMathSharp.Vector3d,System.Span`1<System.Int32>,System.Int32&)
ComesBeforeSupportNode(Gravitas.Colliders.PhysicsMesh/SupportTreeNode,Gravitas.Colliders.PhysicsMesh/SupportTreeNode,FixedMathSharp.Vector3d)
BuildSupportTreeNode(FixedMathSharp.Vector3d[],System.Int32[],Gravitas.Colliders.PhysicsMesh/SupportTreeNode[],Gravitas.Colliders.PhysicsMesh/SupportVertexIndexComparer,System.Int32&,System.Int32,System.Int32)
RefitSupportTree(FixedMathSharp.Vector3d[],Gravitas.Colliders.PhysicsMesh/SupportTreeNode[],Gravitas.Colliders.PhysicsMesh/SupportTreeNode[])
CalculateSupportRangeBounds(FixedMathSharp.Vector3d[],System.Int32[],System.Int32,System.Int32,FixedMathSharp.Vector3d&,FixedMathSharp.Vector3d&,System.Int32&)
CreateSupportVertexIndices(System.Int32)
GetDominantAxis(FixedMathSharp.Vector3d)
GetBoundsSupportPoint(FixedMathSharp.Vector3d,FixedMathSharp.Vector3d,FixedMathSharp.Vector3d)
GetFaceNormalWorld(System.Int32)
GetTrianglesInWorldBounds(SwiftCollections.Query.FixedBoundVolume,SwiftCollections.SwiftList`1<System.Int32>)
GetTrianglesInLocalBounds(SwiftCollections.Query.FixedBoundVolume,SwiftCollections.SwiftList`1<System.Int32>)
TryConvertWorldToScaledLocal(FixedMathSharp.Vector3d,FixedMathSharp.Vector3d&)
ConvertWorldToScaledLocal(FixedMathSharp.Vector3d)
TryConvertScaledLocalToWorld(FixedMathSharp.Vector3d,FixedMathSharp.Vector3d&)
ConvertScaledLocalToWorld(FixedMathSharp.Vector3d)
CreatePointAnchor(FixedMathSharp.Vector3d)
TryConvertWorldDirectionToLocal(FixedMathSharp.Vector3d,FixedMathSharp.Vector3d&)
ConvertWorldDirectionToLocal(FixedMathSharp.Vector3d)
.ctor(System.Int64,System.Int32,System.Int32)
get_StartVertexIndex()
get_EndVertexIndex()
Create(System.Int32,System.Int32,System.Int32)
.ctor(System.Int32,System.Int32,System.Int32)
Create(System.Int32,System.Int32,System.Int32)
.ctor(System.Int32,FixedMathSharp.Vector3d,FixedMathSharp.Vector3d,System.Int32,System.Int32,System.Int32,System.Int32,System.Int32)
get_IsLeaf()
CreateLeaf(System.Int32,FixedMathSharp.Vector3d,FixedMathSharp.Vector3d,System.Int32,System.Int32,System.Int32)
CreateBranch(System.Int32,FixedMathSharp.Vector3d,FixedMathSharp.Vector3d,System.Int32,System.Int32,System.Int32)
.ctor()
Reset(FixedMathSharp.Vector3d[],System.Int32)
Compare(System.Int32,System.Int32)
GetAxisValue(FixedMathSharp.Vector3d,System.Int32)
CalculateInertiaTensor(FixedMathSharp.Fixed64)
CalculateInertiaTensor(FixedMathSharp.Fixed64,Gravitas.Colliders.MeshInertiaPolicy)
CalculateInertiaTensor(FixedMathSharp.Fixed64,Gravitas.Colliders.MeshInertiaPolicy,FixedMathSharp.Vector3d)
TryGetClosedVolumeMassProperties(Gravitas.Colliders.MeshMassProperties&,Gravitas.Colliders.MeshVolumeValidationResult&)
TryGetPreparedClosedVolumeMassProperties(Gravitas.Colliders.MeshMassProperties&,Gravitas.Colliders.MeshVolumeValidationResult&)
PrepareClosedVolumeMassProperties(System.Nullable`1<Gravitas.Colliders.MeshInertiaPolicy>,System.Boolean,FixedMathSharp.Vector3d,FixedMathSharp.Vector3d)
TryCalculateCandidateClosedVolumeMassProperties(FixedMathSharp.Vector3d,FixedMathSharp.Vector3d,System.Boolean,Gravitas.Colliders.MeshMassProperties&,Gravitas.Colliders.MeshVolumeValidationResult&)
TryScalePoint(FixedMathSharp.Vector3d,FixedMathSharp.Vector3d,FixedMathSharp.Vector3d,FixedMathSharp.Vector3d&)
PublishPreparedClosedVolumeMassProperties()
EnsureScaledClosedVolumeMassProperties()
EnsureClosedVolumeMassProperties()
EvaluateClosedVolumeTopology(System.Int32[],Gravitas.Colliders.MeshVolumeValidationResult&)
TryCalculateClosedVolumeMassProperties(System.ReadOnlySpan`1<FixedMathSharp.Vector3d>,FixedMathSharp.Geometry.FixedBoundBox,Gravitas.Colliders.MeshMassProperties&,Gravitas.Colliders.MeshVolumeValidationResult&)
ContainsDuplicateTriangle(Gravitas.Colliders.PhysicsMesh/TriangleUse[])
CompareTriangleUses(Gravitas.Colliders.PhysicsMesh/TriangleUse,Gravitas.Colliders.PhysicsMesh/TriangleUse)
Find(System.Int32[],System.Int32)
Union(System.Int32[],System.Int32,System.Int32)
.ctor(FixedMathSharp.Vector3d[],System.Int32[],FixedMathSharp.Vector3d,FixedMathSharp.FixedQuaternion,Gravitas.Colliders.MeshColliderMode)
get_OwnerScale()
get_PartScale()
get_ScaledLocalBounds()
get_ScaledLocalRadius()
get_SurfaceMassWeight()
get_PreparedSurfaceMassWeight()
GetScaledLocalRadius(FixedMathSharp.Vector3d,FixedMathSharp.Vector3d)
get_PreparedBounds()
get_PreparedSurfaceMassProperties()
get_SurfaceMassProperties()
PrepareTransformation(FixedMathSharp.Vector3d,FixedMathSharp.FixedQuaternion,FixedMathSharp.Vector3d,FixedMathSharp.Vector3d,System.Nullable`1<Gravitas.Colliders.MeshInertiaPolicy>)
PublishPreparedTransformation()
ValidateSurfaceMassProperties(FixedMathSharp.Vector3d)
ValidateClosedVolumeScaleRepresentability(FixedMathSharp.Vector3d)
ValidateRotation(FixedMathSharp.FixedQuaternion)
PrepareScaledGeometry(FixedMathSharp.Vector3d,FixedMathSharp.Vector3d)
CopyCommittedGeometryCandidate()