< Summary

Information
Class: Gravitas.Queries.ConvexShape
Assembly: Gravitas
File(s): /home/runner/work/Gravitas/Gravitas/src/Gravitas/Queries/3D/Sweeps/ConvexShape.cs
Line coverage
100%
Covered lines: 278
Uncovered lines: 0
Coverable lines: 278
Total lines: 467
Line coverage: 100%
Branch coverage
100%
Covered branches: 100
Total branches: 100
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.ctor(...)100%11100%
.ctor(...)100%11100%
get_ContainsCenter()100%22100%
GetCenterAnchor()100%66100%
CreateCircleSlab(...)100%11100%
CreateSphere(...)100%11100%
GetSourceBounds(...)100%11100%
GetBounds(...)100%88100%
CanTranslateCenter(...)100%66100%
TryGetBoundsRelativeTo(...)100%1212100%
WithSourceOffset(...)100%44100%
GetSupportAnchor(...)100%88100%
GetFallbackSurfaceAnchor(...)100%1616100%
TryGetClosestPointOnSurface(...)100%2222100%
TryGetPlanarSurfaceNormal(...)100%66100%
GetCircleSlabSupportLocalPoint(...)100%44100%
GetSphereSupportLocalPoint(...)100%22100%
GetTriangleSupportLocalPoint(...)100%44100%

File(s)

/home/runner/work/Gravitas/Gravitas/src/Gravitas/Queries/3D/Sweeps/ConvexShape.cs

#LineLine coverage
 1//=======================================================================
 2// ConvexSweepQueryWorker.ConvexShape.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 Gravitas.Colliders;
 11using Gravitas.CollisionHandling;
 12using SwiftCollections;
 13using System;
 14
 15namespace Gravitas.Queries;
 16
 17internal readonly struct ConvexShape
 18{
 19    internal enum ConvexShapeKind
 20    {
 21        Collider,
 22        Triangle,
 23        CircleSlab,
 24        Sphere
 25    }
 26
 27    private readonly ConvexShapeKind _kind;
 28    private readonly LSCollider? _collider;
 29    private readonly LSMeshCollider? _triangleOwner;
 30    private readonly int _triangleIndex;
 31    private readonly Vector3d _offset;
 32    private readonly Vector3d _triangleA;
 33    private readonly Vector3d _triangleB;
 34    private readonly Vector3d _triangleC;
 35    private readonly Vector3d _center;
 36    private readonly Fixed64 _radius;
 37    private readonly Fixed64 _halfHeight;
 38
 39    public ConvexShape(LSCollider collider, Vector3d offset)
 40    {
 53941        _kind = ConvexShapeKind.Collider;
 53942        _collider = collider;
 53943        _triangleOwner = null;
 53944        _triangleIndex = -1;
 53945        _offset = offset;
 53946        _triangleA = Vector3d.Zero;
 53947        _triangleB = Vector3d.Zero;
 53948        _triangleC = Vector3d.Zero;
 53949        _center = Vector3d.Zero;
 53950        _radius = Fixed64.Zero;
 53951        _halfHeight = Fixed64.Zero;
 53952    }
 53
 54    public ConvexShape(
 55        LSMeshCollider triangleOwner,
 56        int triangleIndex,
 57        Vector3d triangleA,
 58        Vector3d triangleB,
 59        Vector3d triangleC)
 60    {
 5761        _kind = ConvexShapeKind.Triangle;
 5762        _collider = null;
 5763        _triangleOwner = triangleOwner;
 5764        _triangleIndex = triangleIndex;
 5765        _offset = Vector3d.Zero;
 5766        _triangleA = triangleA;
 5767        _triangleB = triangleB;
 5768        _triangleC = triangleC;
 5769        _center = Vector3d.Zero;
 5770        _radius = Fixed64.Zero;
 5771        _halfHeight = Fixed64.Zero;
 5772    }
 73
 74    private ConvexShape(
 75        ConvexShapeKind kind,
 76        Vector3d center,
 77        Fixed64 radius,
 78        Fixed64 halfHeight,
 79        Vector3d offset)
 80    {
 1463481        _kind = kind;
 1463482        _collider = null;
 1463483        _triangleOwner = null;
 1463484        _triangleIndex = -1;
 1463485        _offset = offset;
 1463486        _triangleA = Vector3d.Zero;
 1463487        _triangleB = Vector3d.Zero;
 1463488        _triangleC = Vector3d.Zero;
 1463489        _center = center;
 1463490        _radius = radius;
 1463491        _halfHeight = halfHeight;
 1463492    }
 93
 94    public bool ContainsCenter =>
 2195        _kind != ConvexShapeKind.Collider || _collider is not LSMeshCollider;
 96
 97    public FixedPointAnchor GetCenterAnchor()
 98    {
 85299        if (_kind == ConvexShapeKind.Triangle)
 100        {
 85101            return new FixedPointAnchor(
 85102                _triangleOwner!.Mesh.Origin,
 85103                _triangleOwner.Mesh.Rotation,
 85104                new Vector3d(
 85105                    FixedMath.Average(
 85106                        _triangleA.X,
 85107                        _triangleB.X,
 85108                        _triangleC.X),
 85109                    FixedMath.Average(
 85110                        _triangleA.Y,
 85111                        _triangleB.Y,
 85112                        _triangleC.Y),
 85113                    FixedMath.Average(
 85114                        _triangleA.Z,
 85115                        _triangleB.Z,
 85116                        _triangleC.Z)));
 117        }
 118
 767119        return new FixedPointAnchor(
 767120            _kind is ConvexShapeKind.CircleSlab or ConvexShapeKind.Sphere
 767121                ? _center
 767122                : _collider!.Center,
 767123            FixedQuaternion.Identity,
 767124            Vector3d.Zero,
 767125            _offset);
 126    }
 127
 128    public static ConvexShape CreateCircleSlab(Vector3d center, Fixed64 radius, Fixed64 halfHeight) =>
 18129        new(ConvexShapeKind.CircleSlab, center, radius, halfHeight, Vector3d.Zero);
 130
 131    public static ConvexShape CreateSphere(Vector3d center, Fixed64 radius) =>
 14473132        new(ConvexShapeKind.Sphere, center, radius, Fixed64.Zero, Vector3d.Zero);
 133
 134    public void GetSourceBounds(out Vector3d min, out Vector3d max) =>
 14859135        GetBounds(out min, out max);
 136
 137    public void GetBounds(out Vector3d min, out Vector3d max)
 138    {
 14888139        if (_kind is ConvexShapeKind.CircleSlab or ConvexShapeKind.Sphere)
 140        {
 14565141            Vector3d center = _center + _offset;
 14565142            Vector3d extents = _kind == ConvexShapeKind.Sphere
 14565143                ? Vector3d.One * _radius
 14565144                : new Vector3d(_radius, _halfHeight, _radius);
 14565145            min = center - extents;
 14565146            max = center + extents;
 14565147            return;
 148        }
 149
 323150        if (_kind == ConvexShapeKind.Triangle)
 151        {
 29152            Vector3d localMin = Vector3d.Min(
 29153                _triangleA,
 29154                Vector3d.Min(_triangleB, _triangleC));
 29155            Vector3d localMax = Vector3d.Max(
 29156                _triangleA,
 29157                Vector3d.Max(_triangleB, _triangleC));
 29158            FixedBoundBox bounds =
 29159                FixedBoundBox.FromRelativeRotatedBoundsClippedToDomain(
 29160                    _triangleOwner!.Mesh.Origin,
 29161                    _triangleOwner.Mesh.Rotation,
 29162                    localMin,
 29163                    localMax,
 29164                    Vector3d.Zero,
 29165                    FixedQuaternion.Identity);
 29166            min = bounds.Min;
 29167            max = bounds.Max;
 29168            return;
 169        }
 170
 294171        min = _collider!.Bounds.Min + _offset;
 294172        max = _collider.Bounds.Max + _offset;
 294173    }
 174
 175    public bool CanTranslateCenter(Vector3d displacement)
 176    {
 14834177        Vector3d canonicalCenter =
 14834178            _kind is ConvexShapeKind.CircleSlab or ConvexShapeKind.Sphere
 14834179                ? _center
 14834180                : _collider!.Center;
 14834181        return Vector3d.TryAdd(
 14834182                canonicalCenter,
 14834183                _offset,
 14834184                out Vector3d currentCenter)
 14834185            && Vector3d.TryAdd(
 14834186                currentCenter,
 14834187                displacement,
 14834188                out _);
 189    }
 190
 191    public bool TryGetBoundsRelativeTo(
 192        Vector3d referenceOrigin,
 193        FixedQuaternion referenceRotation,
 194        out Vector3d min,
 195        out Vector3d max)
 196    {
 197        FixedBoundBox relativeBounds;
 23198        if (_kind is ConvexShapeKind.CircleSlab or ConvexShapeKind.Sphere)
 199        {
 11200            Vector3d extents = _kind == ConvexShapeKind.Sphere
 11201                ? Vector3d.One * _radius
 11202                : new Vector3d(_radius, _halfHeight, _radius);
 11203            relativeBounds =
 11204                FixedBoundBox.FromRelativeRotatedBoundsClippedToDomain(
 11205                    _center,
 11206                    FixedQuaternion.Identity,
 11207                    -extents,
 11208                    extents,
 11209                    referenceOrigin,
 11210                    referenceRotation);
 211        }
 212        else
 213        {
 12214            relativeBounds = ColliderCanonicalBounds.GetRelativeBounds(
 12215                _collider!,
 12216                referenceOrigin,
 12217                referenceRotation);
 218        }
 219
 23220        if (_offset == Vector3d.Zero)
 221        {
 20222            min = relativeBounds.Min;
 20223            max = relativeBounds.Max;
 20224            return true;
 225        }
 226
 227        // Source offsets come from a chord whose magnitude was admitted by
 228        // Prepare, so a unit rotation cannot move them outside Fixed64.
 3229        _ = referenceRotation.Inverse().TryRotate(
 3230            _offset,
 3231            out Vector3d localOffset);
 3232        if (!Vector3d.TryAdd(relativeBounds.Min, localOffset, out min)
 3233            || !Vector3d.TryAdd(relativeBounds.Max, localOffset, out max))
 234        {
 2235            min = default;
 2236            max = default;
 2237            return false;
 238        }
 239
 1240        return true;
 241    }
 242
 243    public ConvexShape WithSourceOffset(Vector3d additionalOffset) =>
 333244        _kind is ConvexShapeKind.CircleSlab or ConvexShapeKind.Sphere
 333245            ? new ConvexShape(
 333246                _kind,
 333247                _center,
 333248                _radius,
 333249                _halfHeight,
 333250                _offset + additionalOffset)
 333251            : new ConvexShape(_collider!, _offset + additionalOffset);
 252
 253    public FixedPointAnchor GetSupportAnchor(Vector3d direction)
 254    {
 2898255        if (_kind == ConvexShapeKind.Triangle)
 256        {
 215257            return new FixedPointAnchor(
 215258                _triangleOwner!.Mesh.Origin,
 215259                _triangleOwner.Mesh.Rotation,
 215260                GetTriangleSupportLocalPoint(direction));
 261        }
 262
 2683263        if (_kind is ConvexShapeKind.CircleSlab or ConvexShapeKind.Sphere)
 264        {
 657265            if (_kind == ConvexShapeKind.Sphere)
 266            {
 558267                return FixedSegment.GetCenteredCapsuleSupportAnchor(
 558268                        _center,
 558269                        FixedQuaternion.Identity,
 558270                        Fixed64.Zero,
 558271                        _radius,
 558272                        direction)
 558273                    .WithLocalTranslation(_offset);
 274            }
 275
 99276            return new FixedPointAnchor(
 99277                _center,
 99278                FixedQuaternion.Identity,
 99279                GetCircleSlabSupportLocalPoint(direction),
 99280                _offset);
 281        }
 282
 2026283        return ConvexColliderSupport.GetSupportAnchor(
 2026284            _collider!,
 2026285            direction,
 2026286            _offset);
 287    }
 288
 289    public FixedPointAnchor GetFallbackSurfaceAnchor(Vector3d direction)
 290    {
 15291        if (_kind == ConvexShapeKind.Collider
 15292            && _collider is LSCuboidCollider cuboid)
 293        {
 11294            Vector3d localDirection =
 11295                cuboid.Rotation.Inverse().Rotate(direction);
 11296            Vector3d absoluteDirection = Vector3d.Abs(localDirection);
 11297            Vector3d halfExtents = cuboid.OrientedBox.HalfExtents;
 298            Vector3d localPoint;
 11299            if (absoluteDirection.X >= absoluteDirection.Y
 11300                && absoluteDirection.X >= absoluteDirection.Z)
 301            {
 4302                localPoint = new Vector3d(
 4303                    localDirection.X >= Fixed64.Zero
 4304                        ? halfExtents.X
 4305                        : -halfExtents.X,
 4306                    Fixed64.Zero,
 4307                    Fixed64.Zero);
 308            }
 7309            else if (absoluteDirection.Y >= absoluteDirection.Z)
 310            {
 3311                localPoint = new Vector3d(
 3312                    Fixed64.Zero,
 3313                    localDirection.Y >= Fixed64.Zero
 3314                        ? halfExtents.Y
 3315                        : -halfExtents.Y,
 3316                    Fixed64.Zero);
 317            }
 318            else
 319            {
 4320                localPoint = new Vector3d(
 4321                    Fixed64.Zero,
 4322                    Fixed64.Zero,
 4323                    localDirection.Z >= Fixed64.Zero
 4324                        ? halfExtents.Z
 4325                        : -halfExtents.Z);
 326            }
 327
 11328            return new FixedPointAnchor(
 11329                cuboid.Center,
 11330                cuboid.Rotation,
 11331                localPoint,
 11332                ConvexColliderSupport.GetLocalDisplacement(
 11333                    cuboid.Rotation,
 11334                    _offset));
 335        }
 336
 4337        return GetSupportAnchor(direction);
 338    }
 339
 340    public bool TryGetClosestPointOnSurface(Vector3d point, out Vector3d closest)
 341    {
 72342        SwiftThrowHelper.ThrowIfTrue(
 72343            _kind == ConvexShapeKind.Triangle,
 72344            nameof(ConvexShape),
 72345            "Triangle shapes are stationary mesh targets and cannot be sweep sources.");
 346
 71347        if (_kind == ConvexShapeKind.Collider)
 348        {
 55349            if (_collider is LSMeshCollider)
 350            {
 10351                closest = Vector3d.Zero;
 10352                return false;
 353            }
 45354            if (_collider is LSCuboidCollider cuboid)
 355            {
 21356                if (!Vector3d.TrySubtract(
 21357                        point,
 21358                        _offset,
 21359                        out Vector3d untranslatedPoint))
 360                {
 1361                    closest = Vector3d.Zero;
 1362                    return false;
 363                }
 364
 365                // Sweep offsets come from a chord whose magnitude was admitted
 366                // by Prepare, so a unit rotation preserves representability.
 20367                _ = cuboid.Rotation.Inverse().TryRotate(
 20368                    _offset,
 20369                    out Vector3d localTranslation);
 20370                return cuboid.OrientedBox
 20371                    .GetClosestPointAnchor(untranslatedPoint)
 20372                    .WithLocalTranslation(localTranslation)
 20373                    .TryGetPoint(out closest);
 374            }
 24375            closest = _collider!.ClosestPointOnSurface(point - _offset) + _offset;
 24376            return true;
 377        }
 378
 16379        Vector3d center = _center + _offset;
 16380        if (_kind == ConvexShapeKind.Sphere)
 381        {
 2382            Vector3d direction = point - center;
 2383            closest = center + GetSphereSupportLocalPoint(direction);
 2384            return true;
 385        }
 386
 14387        Vector3d local = point - center;
 14388        Vector3d radial = new(local.X, Fixed64.Zero, local.Z);
 14389        Fixed64 radialDistance = radial.Magnitude;
 14390        Vector3d radialDirection = radialDistance > Fixed64.Epsilon
 14391            ? radial / radialDistance
 14392            : Vector3d.Right;
 14393        Fixed64 clampedY = FixedMath.Clamp(local.Y, -_halfHeight, _halfHeight);
 394
 14395        if (radialDistance <= _radius && local.Y >= -_halfHeight && local.Y <= _halfHeight)
 396        {
 6397            Fixed64 sideDistance = _radius - radialDistance;
 6398            Fixed64 capDistance = _halfHeight - local.Y.Abs();
 6399            closest = sideDistance <= capDistance
 6400                ? center + new Vector3d(radialDirection.X * _radius, local.Y, radialDirection.Z * _radius)
 6401                : center + new Vector3d(local.X, local.Y.Sign() * _halfHeight, local.Z);
 6402            return true;
 403        }
 404
 8405        Fixed64 surfaceRadius = radialDistance > _radius ? _radius : radialDistance;
 8406        closest = center + new Vector3d(
 8407            radialDirection.X * surfaceRadius,
 8408            clampedY,
 8409            radialDirection.Z * surfaceRadius);
 8410        return true;
 411    }
 412
 413    public bool TryGetPlanarSurfaceNormal(Vector3d point, out Vector3d normal)
 414    {
 153415        if (_kind == ConvexShapeKind.CircleSlab)
 416        {
 1417            throw new InvalidOperationException(
 1418                "Circle slabs are sweep sources and cannot be target shapes.");
 419        }
 152420        if (_kind == ConvexShapeKind.Sphere)
 421        {
 1422            throw new InvalidOperationException(
 1423                "Sphere query sources cannot be target shapes.");
 424        }
 425
 151426        if (_kind == ConvexShapeKind.Triangle)
 427        {
 24428            normal = _triangleOwner!.Mesh.GetFaceNormalWorld(_triangleIndex);
 24429            return true;
 430        }
 431
 127432        return _collider!.TryGetPlanarSurfaceNormal(point, out normal);
 433    }
 434
 435    private Vector3d GetCircleSlabSupportLocalPoint(Vector3d direction)
 436    {
 99437        Vector3d radial = new(direction.X, Fixed64.Zero, direction.Z);
 99438        Fixed64 radialMagnitude = radial.Magnitude;
 99439        Vector3d radialSupport = radialMagnitude > Fixed64.Epsilon
 99440            ? radial / radialMagnitude * _radius
 99441            : Vector3d.Right * _radius;
 99442        Fixed64 y = direction.Y >= Fixed64.Zero ? _halfHeight : -_halfHeight;
 99443        return new Vector3d(radialSupport.X, y, radialSupport.Z);
 444    }
 445
 446    private Vector3d GetSphereSupportLocalPoint(Vector3d direction)
 447    {
 2448        Vector3d normal = direction.IsZero
 2449            ? Vector3d.Right
 2450            : direction.Normalized;
 2451        return normal * _radius;
 452    }
 453
 454    private Vector3d GetTriangleSupportLocalPoint(Vector3d direction)
 455    {
 215456        Vector3d localDirection =
 215457            _triangleOwner!.Mesh.Rotation.Inverse().Rotate(direction);
 215458        Vector3d best = _triangleA;
 215459        if (Vector3d.CompareProjection(_triangleB, best, localDirection) > 0)
 51460            best = _triangleB;
 461
 215462        if (Vector3d.CompareProjection(_triangleC, best, localDirection) > 0)
 56463            best = _triangleC;
 464
 215465        return best;
 466    }
 467}