< Summary

Information
Class: Gravitas.Colliders.LSCapsuleCollider
Assembly: Gravitas
File(s): /home/runner/work/Gravitas/Gravitas/src/Gravitas/Colliders/3D/LSCapsuleCollider.cs
Line coverage
100%
Covered lines: 167
Uncovered lines: 0
Coverable lines: 167
Total lines: 290
Line coverage: 100%
Branch coverage
100%
Covered branches: 8
Total branches: 8
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%
get_Shape()100%11100%
get_Priority()100%11100%
get_ScaledRadius()100%11100%
OnRadiusChanged()100%11100%
NormalizeSize(...)100%11100%
PrepareShape(...)100%11100%
HasValidScaledDimensions(...)100%11100%
PublishShape()100%11100%
CalculateMassPropertyWeight()100%11100%
CalculatePreparedMassPropertyWeight()100%11100%
CalculateCenterOfMassInertiaTensor(...)100%22100%
GetCylinderWeight(...)100%11100%
GetCapWeight(...)100%11100%
GetFrontalArea(...)100%44100%
ClosestPointOnSurface(...)100%22100%
GetNormalAtPoint(...)100%11100%
GetClosestSurfaceAnchor(...)100%11100%
ColliderOverlapsRay(...)100%11100%

File(s)

/home/runner/work/Gravitas/Gravitas/src/Gravitas/Colliders/3D/LSCapsuleCollider.cs

#LineLine coverage
 1//=======================================================================
 2// LSCapsuleCollider.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.Queries;
 11using SwiftCollections;
 12using System;
 13
 14namespace Gravitas.Colliders;
 15
 16/// <summary>Represents a runtime 3D capsule collider whose local axis follows Y.</summary>
 17public sealed class LSCapsuleCollider : LSCollider
 18{
 56419    private Fixed64 _scaledRadius = Fixed64.Half;
 20    private Fixed64 _preparedRadius;
 21    private Fixed64 _preparedAxisLength;
 22    private Vector3d _preparedAxis;
 23    private Fixed64 _preparedArea;
 24
 25    /// <summary>Creates a capsule collider with default dimensions.</summary>
 107226    public LSCapsuleCollider() { }
 27
 28    /// <summary>Creates a runtime capsule from an authored shape definition.</summary>
 2829    public LSCapsuleCollider(ColliderShapeDefinition definition)
 30    {
 2831        definition.EnsureKind(ColliderShapeDefinitionKind.Capsule);
 2832        Material = definition.Material;
 2833        Radius = definition.Radius;
 2834        Size = definition.Size;
 2835    }
 36
 37    /// <inheritdoc/>
 88138    public override ColliderType Shape => ColliderType.Capsule;
 39    /// <inheritdoc/>
 14540    public override int Priority => ColliderSettings.GetPriority(Shape);
 41
 42    /// <inheritdoc/>
 1096843    public override Fixed64 ScaledRadius => _scaledRadius;
 44
 45    /// <summary>
 46    /// Gets the full physical distance between the capsule's hemisphere centers.
 47    /// </summary>
 48    public Fixed64 AxisLength { get; private set; }
 49
 50    /// <summary>
 51    /// Gets the derived normalized world-space direction of the capsule's
 52    /// conceptual center axis. Exact geometry remains authoritative in the
 53    /// collider's rigid frame.
 54    /// </summary>
 55    public Vector3d WorldAxis { get; private set; } = Vector3d.Up;
 56
 57    /// <inheritdoc/>
 58    protected override void OnRadiusChanged()
 59    {
 3760        Fixed64 diameter = _radius * 2;
 3761        _size = new Vector3d(diameter, _size.Y, diameter);
 3762    }
 63
 64    /// <inheritdoc/>
 65    protected override Vector3d NormalizeSize(Vector3d value) =>
 51966        new(_radius * 2, value.Y, _radius * 2);
 67
 68    private protected override void PrepareShape(in ColliderShapeSnapshot snapshot)
 69    {
 57770        Fixed64 radiusX = ColliderScalePolicy.ScalePositive(
 57771            snapshot.Radius,
 57772            snapshot.OwnerScale.X,
 57773            snapshot.PartScale.X);
 57674        Fixed64 radiusZ = ColliderScalePolicy.ScalePositive(
 57675            snapshot.Radius,
 57676            snapshot.OwnerScale.Z,
 57677            snapshot.PartScale.Z);
 57678        _preparedRadius = FixedMath.Max(radiusX, radiusZ);
 57679        SwiftThrowHelper.ThrowIfArgument(
 57680            !HasValidScaledDimensions(snapshot),
 57681            nameof(snapshot),
 57682            "Scaled capsule height must be at least the capsule diameter.");
 57583        SwiftThrowHelper.ThrowIfArgument(
 57584            !Fixed64.TryMultiplySubtractClamped(
 57585                snapshot.Size.Y,
 57586                snapshot.OwnerScale.Y,
 57587                snapshot.PartScale.Y,
 57588                Fixed64.One,
 57589                snapshot.Radius,
 57590                Fixed64.Two,
 57591                snapshot.OwnerScale.X,
 57592                snapshot.PartScale.X,
 57593                out Fixed64 axisLengthX)
 57594            | !Fixed64.TryMultiplySubtractClamped(
 57595                snapshot.Size.Y,
 57596                snapshot.OwnerScale.Y,
 57597                snapshot.PartScale.Y,
 57598                Fixed64.One,
 57599                snapshot.Radius,
 575100                Fixed64.Two,
 575101                snapshot.OwnerScale.Z,
 575102                snapshot.PartScale.Z,
 575103                out Fixed64 axisLengthZ),
 575104            nameof(snapshot),
 575105            "Scaled capsule center-axis length must be representable.");
 575106        _preparedAxisLength = FixedMath.Min(axisLengthX, axisLengthZ);
 575107        _preparedAxis = (snapshot.Rotation * Vector3d.Up).Normalized;
 575108        _preparedArea = Fixed64.Two * Fixed64.Pi * _preparedRadius * _preparedAxisLength
 575109            + Fixed64.Two * Fixed64.Pi * _preparedRadius * _preparedRadius;
 575110        SetPreparedBounds(FixedBoundBox.FromCenteredCapsuleClippedToDomain(
 575111            snapshot.Center,
 575112            snapshot.Rotation,
 575113            Vector3d.Up,
 575114            _preparedAxisLength,
 575115            _preparedRadius));
 575116    }
 117
 118    private static bool HasValidScaledDimensions(
 119        in ColliderShapeSnapshot snapshot) =>
 576120        Fixed64.CompareProducts(
 576121            snapshot.Size.Y,
 576122            snapshot.OwnerScale.Y,
 576123            snapshot.PartScale.Y,
 576124            Fixed64.One,
 576125            snapshot.Radius,
 576126            Fixed64.Two,
 576127            snapshot.OwnerScale.X,
 576128            snapshot.PartScale.X) >= 0
 576129        & Fixed64.CompareProducts(
 576130            snapshot.Size.Y,
 576131            snapshot.OwnerScale.Y,
 576132            snapshot.PartScale.Y,
 576133            Fixed64.One,
 576134            snapshot.Radius,
 576135            Fixed64.Two,
 576136            snapshot.OwnerScale.Z,
 576137            snapshot.PartScale.Z) >= 0;
 138
 139    private protected override void PublishShape()
 140    {
 575141        _scaledRadius = _preparedRadius;
 575142        AxisLength = _preparedAxisLength;
 575143        WorldAxis = _preparedAxis;
 575144        Area = _preparedArea;
 575145    }
 146
 147    internal override ExactMassWeight CalculateMassPropertyWeight() =>
 48148        GetCylinderWeight(ScaledRadius, AxisLength)
 48149            .Add(GetCapWeight(ScaledRadius));
 150
 151    internal override ExactMassWeight CalculatePreparedMassPropertyWeight() =>
 59152        GetCylinderWeight(_preparedRadius, _preparedAxisLength)
 59153            .Add(GetCapWeight(_preparedRadius));
 154
 155    // The capsule is split into a cylinder and a pair of solid hemispheres with masses
 156    // proportional to their volumes. Each hemisphere's centroid lies 3r/8 outward from
 157    // its sphere center, which contributes the 3dr/4 cross term to transverse cap inertia.
 158    internal override Fixed3x3 CalculateCenterOfMassInertiaTensor(Fixed64 mass)
 159    {
 523160        if (AxisLength <= Fixed64.Epsilon)
 161        {
 63162            Fixed64 sphereDiagonal = Fixed64.FromFraction(2, 5) * mass * ScaledRadiusSqr;
 63163            Fixed3x3 sphereTensor = new(
 63164                sphereDiagonal, Fixed64.Zero, Fixed64.Zero,
 63165                Fixed64.Zero, sphereDiagonal, Fixed64.Zero,
 63166                Fixed64.Zero, Fixed64.Zero, sphereDiagonal
 63167            );
 63168            return sphereTensor;
 169        }
 170
 171        // Masses of the cylinder and spheres (proportional to their volumes)
 460172        ExactMassWeight cylinderWeight =
 460173            GetCylinderWeight(ScaledRadius, AxisLength);
 460174        ExactMassWeight capWeight = GetCapWeight(ScaledRadius);
 460175        ExactMassWeight totalWeight = cylinderWeight.Add(capWeight);
 176
 460177        _ = cylinderWeight.TryGetProportionalShare(
 460178            mass,
 460179            totalWeight,
 460180            out Fixed64 cylinderMass);
 460181        Fixed64 sphereMass = mass - cylinderMass;
 182
 183        // Distance from the center of the hemisphere to the center of the capsule
 460184        Fixed64 d = AxisLength / 2;
 185
 186        // Calculating the inertia tensors for the cylinder and the spheres
 460187        Fixed64 cylinderInertiaY = Fixed64.FromFraction(1, 2) * cylinderMass * ScaledRadiusSqr;
 460188        Fixed64 cylinderInertiaXZ = Fixed64.FromFraction(1, 12) * cylinderMass * ((3 * ScaledRadiusSqr) + (AxisLength * 
 460189        Fixed64 sphereInertiaXZ = sphereMass
 460190            * (Fixed64.FromFraction(2, 5) * ScaledRadiusSqr
 460191                + d * d
 460192                + Fixed64.FromFraction(3, 4) * d * ScaledRadius);
 460193        Fixed64 sphereInertiaY = Fixed64.FromFraction(2, 5) * sphereMass * ScaledRadiusSqr;
 194
 195        // The total inertia tensor for the capsule
 460196        Fixed64 totalInertia_xz = cylinderInertiaXZ + sphereInertiaXZ;
 460197        Fixed64 totalInertia_y = cylinderInertiaY + sphereInertiaY;
 198
 460199        Fixed3x3 tensor = new(
 460200            totalInertia_xz, Fixed64.Zero, Fixed64.Zero,
 460201            Fixed64.Zero, totalInertia_y, Fixed64.Zero,
 460202            Fixed64.Zero, Fixed64.Zero, totalInertia_xz
 460203        );
 460204        return tensor;
 205    }
 206
 207    private static ExactMassWeight GetCylinderWeight(
 208        Fixed64 radius,
 209        Fixed64 axisLength) =>
 567210        ExactMassWeight.FromProduct(
 567211            Fixed64.Pi,
 567212            radius,
 567213            radius,
 567214            axisLength);
 215
 216    private static ExactMassWeight GetCapWeight(Fixed64 radius) =>
 567217        ExactMassWeight.FromProduct(
 567218            Fixed64.FromFraction(4, 3) * Fixed64.Pi,
 567219            radius,
 567220            radius,
 567221            radius);
 222
 223    // If the capsule is moving in the direction of its main axis,
 224    // the frontal area would be a circle (the end cap of the capsule).
 225    // Therefore, the frontal area would be πr^2, where r is the radius of the capsule.
 226    // If it's moving perpendicular to its main axis,
 227    // then the frontal area would be a rectangle with a semicircle on either end,
 228    // which would be (2r)*h + πr^2, where h is the height of the cylindrical part of the capsule.
 229    /// <inheritdoc/>
 230    public override Fixed64 GetFrontalArea(Vector3d direction)
 231    {
 11232        Fixed64 directionMagnitude = direction.Magnitude;
 11233        if (directionMagnitude <= Fixed64.Epsilon)
 2234            return Area;
 235
 9236        Vector3d normalizedDirection = direction / directionMagnitude;
 9237        Fixed64 axial = Rotation.Inverse()
 9238            .Rotate(normalizedDirection).Y.Abs();
 9239        Fixed64 radialFactorSqr = Fixed64.One - axial * axial;
 9240        Fixed64 radialFactor = radialFactorSqr <= Fixed64.Zero
 9241            ? Fixed64.Zero
 9242            : FixedMath.Sqrt(radialFactorSqr);
 243
 9244        Fixed64 capArea = Fixed64.Pi * ScaledRadiusSqr;
 9245        Fixed64 sideProfile = 2 * ScaledRadius * AxisLength;
 9246        return capArea + radialFactor * sideProfile;
 247    }
 248
 249    /// <inheritdoc/>
 250    public override Vector3d ClosestPointOnSurface(Vector3d other)
 251    {
 87252        FixedPointAnchor surfaceAnchor =
 87253            GetClosestSurfaceAnchor(other, out _);
 87254        if (!surfaceAnchor.TryGetPoint(out Vector3d surfacePoint))
 255        {
 1256            throw new InvalidOperationException(
 1257                "The closest capsule surface point is outside the representable coordinate domain.");
 258        }
 259
 86260        return surfacePoint;
 261    }
 262
 263    /// <inheritdoc/>
 264    public override Vector3d GetNormalAtPoint(Vector3d point)
 265    {
 44266        _ = GetClosestSurfaceAnchor(
 44267            point,
 44268            out Vector3d outwardNormal);
 44269        return outwardNormal;
 270    }
 271
 272    internal override FixedPointAnchor GetClosestSurfaceAnchor(
 273        Vector3d point,
 274        out Vector3d normal) =>
 341275        WideFiniteAxisIntersection
 341276            .GetClosestCenteredCapsuleSurfaceAnchor(
 341277                point,
 341278                Center,
 341279                Rotation,
 341280                Vector3d.Up,
 341281                AxisLength,
 341282                ScaledRadius,
 341283                Vector3d.Right,
 341284                out normal,
 341285                out _);
 286
 287    /// <inheritdoc/>
 288    public override bool ColliderOverlapsRay(RaycastSegmentWorker worker, ref SwiftList<Vector3d> outputIntersectionPoin
 8289        worker.CheckCapsuleOverlaps(this, ref outputIntersectionPoints);
 290}