< Summary

Information
Class: Gravitas.Colliders.LSCapsuleCollider2D
Assembly: Gravitas
File(s): /home/runner/work/Gravitas/Gravitas/src/Gravitas/Colliders/2D/LSCapsuleCollider2D.cs
Line coverage
100%
Covered lines: 279
Uncovered lines: 0
Coverable lines: 279
Total lines: 457
Line coverage: 100%
Branch coverage
100%
Covered branches: 24
Total branches: 24
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Gravitas/Gravitas/src/Gravitas/Colliders/2D/LSCapsuleCollider2D.cs

#LineLine coverage
 1//=======================================================================
 2// LSCapsuleCollider2D.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 Chronicler;
 9using FixedMathSharp;
 10using FixedMathSharp.Geometry;
 11using System;
 12using System.Runtime.CompilerServices;
 13
 14namespace Gravitas.Colliders;
 15
 16/// <summary>
 17/// Pure 2D capsule collider whose local center segment follows local 2D Y.
 18/// </summary>
 19public sealed class LSCapsuleCollider2D : LSCollider2D
 20{
 21    private Fixed64 _radius;
 22    private Fixed64 _height;
 23    private Fixed64 _scaledRadius;
 24    private Fixed64 _axisLength;
 25    private Fixed64 _preparedRadius;
 26    private Fixed64 _preparedAxisLength;
 27    private Vector2d _preparedAxis;
 28
 29    /// <summary>Creates a pure 2D capsule with authored dimensions.</summary>
 70730    public LSCapsuleCollider2D(Fixed64 radius, Fixed64 height)
 31    {
 70732        ValidateDimensions(radius, height);
 70633        _radius = radius;
 70634        _height = height;
 70635        MarkShapeDirty();
 70636    }
 37
 38    /// <summary>Creates a runtime capsule from an authored shape definition.</summary>
 2639    public LSCapsuleCollider2D(ColliderShapeDefinition2D definition)
 40    {
 2641        definition.EnsureKind(ColliderShapeDefinition2DKind.Capsule);
 2642        Material = definition.Material;
 2643        Fixed64 radius = definition.Radius;
 2644        Fixed64 height = definition.Size.Y;
 2645        ValidateDimensions(radius, height);
 2646        _radius = radius;
 2647        _height = height;
 2648        MarkShapeDirty();
 2649    }
 50
 51    /// <inheritdoc/>
 124452    public override ColliderType2D Shape => ColliderType2D.Capsule;
 53
 54    /// <summary>
 55    /// Gets or sets the unscaled semicircle radius.
 56    /// </summary>
 57    public Fixed64 Radius
 58    {
 59        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 960        get => _radius;
 61        set
 62        {
 663            ValidateDimensions(value, _height);
 464            if (_radius == value)
 365                return;
 66
 167            _radius = value;
 168            MarkShapeDirty();
 169        }
 70    }
 71
 72    /// <summary>
 73    /// Gets or sets the unscaled full end-to-end capsule height.
 74    /// </summary>
 75    public Fixed64 Height
 76    {
 77        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 1078        get => _height;
 79        set
 80        {
 581            ValidateDimensions(_radius, value);
 482            if (_height == value)
 383                return;
 84
 185            _height = value;
 186            MarkShapeDirty();
 187        }
 88    }
 89
 90    /// <summary>
 91    /// Gets the radius after compound/local scaling.
 92    /// </summary>
 93    public Fixed64 ScaledRadius
 94    {
 95        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 96        get
 97        {
 623398            GetMassPropertyGeometry(
 623399                out Fixed64 radius,
 6233100                out _);
 6233101            return radius;
 102        }
 103    }
 104
 105    /// <summary>
 106    /// Gets the derived normalized world-space direction of the capsule's
 107    /// conceptual center axis. Exact geometry remains authoritative in the
 108    /// collider's planar rigid frame.
 109    /// </summary>
 110    public Vector2d WorldAxis { get; private set; } = Vector2d.Forward;
 111
 112    /// <summary>
 113    /// Gets the full length of the conceptual center axis.
 114    /// </summary>
 115    public Fixed64 AxisLength
 116    {
 117        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 118        get
 119        {
 7001120            if (HasCommittedShape)
 6281121                return _axisLength;
 122
 720123            GetMassPropertyGeometry(
 720124                out _,
 720125                out Fixed64 axisLength);
 720126            return axisLength;
 127        }
 128    }
 129
 130    /// <inheritdoc/>
 131    public override bool ContainsPoint(Vector2d point)
 398132        => FixedSegment2d.ContainsPointInCenteredCapsule(
 398133            point,
 398134            Center,
 398135            Rotation,
 398136            AxisLength,
 398137            ScaledRadius,
 398138            Fixed64.Zero,
 398139            strict: false);
 140
 141    /// <inheritdoc/>
 142    public override Vector2d GetClosestPoint(Vector2d point)
 143    {
 4144        Vector2d direction = FixedSegment2d.GetDirectionFromCenteredAxis(
 4145            point,
 4146            Center,
 4147            Rotation,
 4148            AxisLength);
 4149        if (direction == Vector2d.Zero)
 3150            direction = Rotate(Vector2d.Right, Rotation).Normalized;
 4151        if (FixedSegment2d.TryGetSurfacePointOnCenteredCapsule(
 4152                point,
 4153                Center,
 4154                Rotation,
 4155                AxisLength,
 4156                ScaledRadius,
 4157                direction,
 4158                out Vector2d surfacePoint))
 2159            return surfacePoint;
 160
 2161        throw new InvalidOperationException("The closest capsule surface point is outside the Fixed64 coordinate domain.
 162    }
 163
 164    internal Vector2d GetNormalFromCenteredAxis(Vector2d point)
 165    {
 660166        Vector2d direction = FixedSegment2d.GetDirectionFromCenteredAxis(
 660167            point,
 660168            Center,
 660169            Rotation,
 660170            AxisLength);
 660171        return direction != Vector2d.Zero
 660172            ? direction
 660173            : Rotate(Vector2d.Right, Rotation).Normalized;
 174    }
 175
 176    internal bool TryGetSurfacePointFromCenteredAxis(
 177        Vector2d point,
 178        Vector2d normal,
 179        out Vector2d surfacePoint) =>
 2180        TryGetSurfacePoint(point, normal, out surfacePoint);
 181
 182    /// <inheritdoc/>
 183    public override Vector2d GetSupportPoint(Vector2d direction)
 184    {
 13185        if (TryGetSupportPoint(direction, out Vector2d support))
 12186            return support;
 187
 1188        throw new InvalidOperationException("The capsule support point is outside the Fixed64 coordinate domain.");
 189    }
 190
 191    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 192    internal bool TryGetSupportPoint(Vector2d direction, out Vector2d support)
 193    {
 13194        Vector2d supportDirection = direction == Vector2d.Zero
 13195            ? Rotate(Vector2d.Right, Rotation)
 13196            : direction;
 13197        return FixedSegment2d.TryGetCenteredCapsuleSupport(
 13198            Center,
 13199            Rotation,
 13200            AxisLength,
 13201            ScaledRadius,
 13202            supportDirection,
 13203            out support);
 204    }
 205
 206    private bool TryGetSurfacePoint(
 207        Vector2d point,
 208        Vector2d worldNormal,
 209        out Vector2d surfacePoint) =>
 2210        FixedSegment2d.TryGetSurfacePointOnCenteredCapsule(
 2211            point,
 2212            Center,
 2213            Rotation,
 2214            AxisLength,
 2215            ScaledRadius,
 2216            worldNormal,
 2217            out surfacePoint);
 218
 219    internal override ExactMassWeight CalculateAreaForMassProperties()
 220    {
 71221        GetMassPropertyGeometry(
 71222            out Fixed64 radius,
 71223            out Fixed64 cylinderLength);
 71224        return GetRectangleWeight(radius, cylinderLength)
 71225            .Add(GetCircleWeight(radius));
 226    }
 227
 228    internal override ExactMassWeight CalculatePreparedAreaForMassProperties() =>
 43229        GetRectangleWeight(_preparedRadius, _preparedAxisLength)
 43230            .Add(GetCircleWeight(_preparedRadius));
 231
 232    internal override Fixed64 CalculateCenterOfMassMoment(Fixed64 mass)
 233    {
 1375234        GetMassPropertyGeometry(
 1375235            out Fixed64 radius,
 1375236            out Fixed64 cylinderLength);
 1375237        return CalculateCenteredMoment(mass, radius, cylinderLength);
 238    }
 239
 240    private protected override void PrepareShape(in ColliderShapeSnapshot2D snapshot)
 241    {
 754242        Fixed64 radiusX = ColliderScalePolicy.ScalePositive(
 754243            _radius,
 754244            snapshot.OwnerScale.X,
 754245            snapshot.PartScale.X);
 754246        Fixed64 radiusY = ColliderScalePolicy.ScalePositive(
 754247            _radius,
 754248            snapshot.OwnerScale.Y,
 754249            snapshot.PartScale.Y);
 754250        _preparedRadius = FixedMath.Max(radiusX, radiusY);
 754251        SwiftThrowHelper.ThrowIfArgument(
 754252            !HasValidScaledDimensions(
 754253                _radius,
 754254                _height,
 754255                snapshot.OwnerScale,
 754256                snapshot.PartScale),
 754257            nameof(snapshot),
 754258            "Scaled 2D capsule height must be at least the capsule diameter.");
 752259        SwiftThrowHelper.ThrowIfArgument(
 752260            !Fixed64.TryMultiplySubtractClamped(
 752261                _height,
 752262                snapshot.OwnerScale.Y,
 752263                snapshot.PartScale.Y,
 752264                Fixed64.One,
 752265                _radius,
 752266                Fixed64.Two,
 752267                snapshot.OwnerScale.X,
 752268                snapshot.PartScale.X,
 752269                out Fixed64 axisLengthX)
 752270            | !Fixed64.TryMultiplySubtractClamped(
 752271                _height,
 752272                snapshot.OwnerScale.Y,
 752273                snapshot.PartScale.Y,
 752274                Fixed64.One,
 752275                _radius,
 752276                Fixed64.Two,
 752277                snapshot.OwnerScale.Y,
 752278                snapshot.PartScale.Y,
 752279                out Fixed64 axisLengthY),
 752280            nameof(snapshot),
 752281            "Scaled 2D capsule center-axis length must be representable.");
 752282        _preparedAxisLength = FixedMath.Min(axisLengthX, axisLengthY);
 752283        _preparedAxis = Rotate(Vector2d.Forward, snapshot.Rotation).Normalized;
 752284        SetPreparedBounds(FixedBoundArea.FromCenteredRotatedCapsuleClippedToDomain(
 752285            snapshot.Center,
 752286            snapshot.Rotation,
 752287            _preparedAxisLength,
 752288            _preparedRadius));
 752289    }
 290
 291    private protected override void PublishShape()
 292    {
 749293        _scaledRadius = _preparedRadius;
 749294        _axisLength = _preparedAxisLength;
 749295        WorldAxis = _preparedAxis;
 749296    }
 297
 298    /// <inheritdoc/>
 299    protected override void RecordShapeData(IChronicler chronicler)
 300    {
 4301        Fixed64 radius = _radius;
 4302        Fixed64 height = _height;
 4303        RecordValues.Look(chronicler, ref radius, "Radius", Fixed64.Half);
 4304        RecordValues.Look(chronicler, ref height, "Height", Fixed64.One);
 4305        if (chronicler.Mode == SerializationMode.Loading)
 306        {
 2307            ValidateDimensions(radius, height);
 2308            _radius = radius;
 2309            _height = height;
 310        }
 4311    }
 312
 313    private void GetMassPropertyGeometry(
 314        out Fixed64 radius,
 315        out Fixed64 axisLength)
 316    {
 8399317        if (HasCommittedShape)
 318        {
 5514319            radius = _scaledRadius;
 5514320            axisLength = _axisLength;
 5514321            return;
 322        }
 323
 2885324        GetCurrentScaleFactors(
 2885325            out Vector2d ownerScale,
 2885326            out Vector2d partScale);
 2885327        Fixed64 radiusX = ColliderScalePolicy.Scale(
 2885328            _radius,
 2885329            ownerScale.X,
 2885330            partScale.X);
 2885331        Fixed64 radiusY = ColliderScalePolicy.Scale(
 2885332            _radius,
 2885333            ownerScale.Y,
 2885334            partScale.Y);
 2885335        radius = FixedMath.Max(radiusX, radiusY);
 2885336        SwiftThrowHelper.ThrowIfTrue(
 2885337            !HasValidScaledDimensions(
 2885338                _radius,
 2885339                _height,
 2885340                ownerScale,
 2885341                partScale),
 2885342            nameof(CalculateAreaForMassProperties),
 2885343            "Scaled 2D capsule height must be at least the capsule diameter.");
 2885344        bool representable = Fixed64.TryMultiplySubtractClamped(
 2885345                _height,
 2885346                ownerScale.Y,
 2885347                partScale.Y,
 2885348                Fixed64.One,
 2885349                _radius,
 2885350                Fixed64.Two,
 2885351                ownerScale.X,
 2885352                partScale.X,
 2885353                out Fixed64 axisLengthX)
 2885354            & Fixed64.TryMultiplySubtractClamped(
 2885355                _height,
 2885356                ownerScale.Y,
 2885357                partScale.Y,
 2885358                Fixed64.One,
 2885359                _radius,
 2885360                Fixed64.Two,
 2885361                ownerScale.Y,
 2885362                partScale.Y,
 2885363                out Fixed64 axisLengthY);
 2885364        SwiftThrowHelper.ThrowIfTrue(
 2885365            !representable,
 2885366            nameof(CalculateAreaForMassProperties),
 2885367            "Scaled 2D capsule center-axis length must be representable.");
 2885368        axisLength = FixedMath.Min(axisLengthX, axisLengthY);
 2885369    }
 370
 371    private static Fixed64 CalculateCenteredMoment(Fixed64 mass, Fixed64 radius, Fixed64 cylinderLength)
 372    {
 1375373        if (cylinderLength <= Fixed64.Epsilon)
 50374            return mass * radius * radius * Fixed64.Half;
 375
 1325376        ExactMassWeight rectangleWeight =
 1325377            GetRectangleWeight(radius, cylinderLength);
 1325378        ExactMassWeight circleWeight = GetCircleWeight(radius);
 1325379        ExactMassWeight totalWeight =
 1325380            rectangleWeight.Add(circleWeight);
 1325381        if (totalWeight.IsZero)
 7382            return mass * cylinderLength * cylinderLength / (Fixed64)12;
 383
 1318384        _ = rectangleWeight.TryGetProportionalShare(
 1318385            mass,
 1318386            totalWeight,
 1318387            out Fixed64 rectangleMass);
 1318388        Fixed64 capMass = (mass - rectangleMass) * Fixed64.Half;
 1318389        Fixed64 rectangleMoment =
 1318390            rectangleMass * ((radius * (Fixed64)2 * radius * (Fixed64)2) + (cylinderLength * cylinderLength)) / (Fixed64
 391
 392        // Semicircle centroid distance from its flat edge is 4r / (3pi).
 1318393        Fixed64 capCentroidOffset = (Fixed64)4 * radius / ((Fixed64)3 * Fixed64.Pi);
 1318394        Fixed64 capCenterDistance = cylinderLength * Fixed64.Half + capCentroidOffset;
 1318395        Fixed64 capCenteredFactor =
 1318396            Fixed64.One / (Fixed64)4
 1318397            - (Fixed64)16 / ((Fixed64)9 * Fixed64.Pi * Fixed64.Pi);
 1318398        Fixed64 capMomentAboutOwnCentroid = capMass * radius * radius * capCenteredFactor;
 1318399        Fixed64 capMomentAboutCapsuleCenter =
 1318400            capMomentAboutOwnCentroid + capMass * capCenterDistance * capCenterDistance;
 401
 1318402        return rectangleMoment + capMomentAboutCapsuleCenter * (Fixed64)2;
 403    }
 404
 405    private static ExactMassWeight GetRectangleWeight(
 406        Fixed64 radius,
 407        Fixed64 cylinderLength) =>
 1439408        ExactMassWeight.FromProduct(
 1439409            Fixed64.Two,
 1439410            radius,
 1439411            cylinderLength);
 412
 413    private static ExactMassWeight GetCircleWeight(Fixed64 radius) =>
 1439414        ExactMassWeight.FromProduct(
 1439415            Fixed64.Pi,
 1439416            radius,
 1439417            radius);
 418
 419    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 420    private static void ValidateDimensions(Fixed64 radius, Fixed64 height)
 421    {
 746422        SwiftThrowHelper.ThrowIfArgument(radius <= Fixed64.Zero, nameof(radius), "2D capsule radius must be greater than
 745423        SwiftThrowHelper.ThrowIfArgument(
 745424            Fixed64.CompareProducts(
 745425                height,
 745426                Fixed64.One,
 745427                radius,
 745428                Fixed64.Two) < 0,
 745429            nameof(height),
 745430            "2D capsule height must be at least the capsule diameter.");
 742431    }
 432
 433    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 434    private static bool HasValidScaledDimensions(
 435        Fixed64 radius,
 436        Fixed64 height,
 437        Vector2d ownerScale,
 438        Vector2d partScale) =>
 3639439        Fixed64.CompareProducts(
 3639440            height,
 3639441            ownerScale.Y,
 3639442            partScale.Y,
 3639443            Fixed64.One,
 3639444            radius,
 3639445            Fixed64.Two,
 3639446            ownerScale.X,
 3639447            partScale.X) >= 0
 3639448        & Fixed64.CompareProducts(
 3639449            height,
 3639450            ownerScale.Y,
 3639451            partScale.Y,
 3639452            Fixed64.One,
 3639453            radius,
 3639454            Fixed64.Two,
 3639455            ownerScale.Y,
 3639456            partScale.Y) >= 0;
 457}