< Summary

Information
Class: Gravitas.Colliders.LSCompoundCollider2D
Assembly: Gravitas
File(s): /home/runner/work/Gravitas/Gravitas/src/Gravitas/Colliders/2D/LSCompoundCollider2D.cs
Line coverage
100%
Covered lines: 163
Uncovered lines: 0
Coverable lines: 163
Total lines: 337
Line coverage: 100%
Branch coverage
100%
Covered branches: 60
Total branches: 60
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/LSCompoundCollider2D.cs

#LineLine coverage
 1//=======================================================================
 2// LSCompoundCollider2D.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.CollisionHandling;
 11using Gravitas.Materials;
 12using System;
 13using System.Runtime.CompilerServices;
 14
 15namespace Gravitas.Colliders;
 16
 17/// <summary>
 18/// Represents one pure 2D collider identity whose collision shape is composed
 19/// from deterministic internal primitive and convex-polygon parts.
 20/// </summary>
 21public sealed class LSCompoundCollider2D : LSCollider2D
 22{
 23    private readonly CompoundColliderPart2D[] _parts;
 24    private readonly LSCollider2D[] _partColliders;
 25    private readonly ExactMassPoint2D[] _massPointScratch;
 26    private readonly ExactMassWeight[] _massWeightScratch;
 13227    private readonly ContactManifold2D _partManifoldScratch = new();
 28
 29    /// <summary>Creates a runtime compound collider from authored pure 2D parts.</summary>
 13230    public LSCompoundCollider2D(params CompoundColliderPart2D[] parts)
 31    {
 13232        SwiftThrowHelper.ThrowIfNull(parts, nameof(parts));
 13233        SwiftThrowHelper.ThrowIfArgument(parts.Length == 0, nameof(parts), "2D compound collider must contain at least o
 34
 75635        for (int i = 0; i < parts.Length; i++)
 24836            ValidatePart(parts[i]);
 37
 13038        _parts = new CompoundColliderPart2D[parts.Length];
 13039        _partColliders = new LSCollider2D[parts.Length];
 13040        _massPointScratch = new ExactMassPoint2D[parts.Length];
 13041        _massWeightScratch = new ExactMassWeight[parts.Length];
 75442        for (int i = 0; i < parts.Length; i++)
 43        {
 24744            _parts[i] = parts[i];
 24745            _partColliders[i] = MaterializePartCollider(parts[i]);
 24746            _partColliders[i].ReserveCompoundPart(this, parts[i].LocalRotation, parts[i].LocalScale);
 47        }
 13048    }
 49
 50    /// <inheritdoc/>
 14951    public override ColliderType2D Shape => ColliderType2D.Compound;
 52
 53    /// <inheritdoc/>
 2654    public override int Priority => ColliderSettings2D.GetPriority(Shape);
 55
 56    /// <summary>
 57    /// Gets the radius of a circle that conservatively contains the current
 58    /// aggregate shape.
 59    /// </summary>
 60    public Fixed64 ScaledRadius
 61    {
 62        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 63        get
 64        {
 965            return HasCommittedShape
 966                ? CanonicalCenteredProxyRadius
 967                : ColliderCanonicalBounds2D.GetCurrentCenteredProxyRadius(this);
 68        }
 69    }
 70
 71    /// <summary>Gets the authored parts in stable source order.</summary>
 1072    public ReadOnlySpan<CompoundColliderPart2D> Parts => _parts;
 73
 74    /// <summary>Gets the number of authored compound parts.</summary>
 75    public int PartCount
 76    {
 77        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 228678        get => _parts.Length;
 79    }
 80
 81    /// <summary>Gets the stable runtime part identifier for a source-order index.</summary>
 82    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 83    public int GetPartId(int index)
 84    {
 285        SwiftThrowHelper.ThrowIfArrayIndexInvalid(index, _parts.Length, nameof(index));
 286        return index;
 87    }
 88
 89    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 90    internal LSCollider2D GetPartCollider(int index)
 91    {
 153592        SwiftThrowHelper.ThrowIfArrayIndexInvalid(index, _parts.Length, nameof(index));
 153593        return _partColliders[index];
 94    }
 95
 1696    internal ContactManifold2D PartManifoldScratch => _partManifoldScratch;
 97
 98    /// <inheritdoc/>
 99    public override bool ContainsPoint(Vector2d point)
 100    {
 1268101        for (int i = 0; i < _partColliders.Length; i++)
 102        {
 424103            if (_partColliders[i].ContainsPoint(point))
 3104                return true;
 105        }
 106
 210107        return false;
 108    }
 109
 110    /// <inheritdoc/>
 111    public override Vector2d GetClosestPoint(Vector2d point)
 112    {
 5113        _ = TryGetClosestBoundaryAnchor(
 5114            point,
 5115            out FixedPointAnchor2d anchor);
 5116        if (anchor.TryGetPoint(out Vector2d closest))
 117        {
 4118            return closest;
 119        }
 120
 1121        throw new InvalidOperationException(
 1122            "The closest compound surface point is outside the Fixed64 coordinate domain.");
 123    }
 124
 125    /// <inheritdoc/>
 126    public override Vector2d GetSupportPoint(Vector2d direction)
 127    {
 4128        Vector2d bestPoint = _partColliders[0].GetSupportPoint(direction);
 4129        Fixed64 bestProjection = Vector2d.Dot(bestPoint, direction);
 12130        for (int i = 1; i < _partColliders.Length; i++)
 131        {
 2132            Vector2d candidate = _partColliders[i].GetSupportPoint(direction);
 2133            Fixed64 projection = Vector2d.Dot(candidate, direction);
 2134            if (projection <= bestProjection)
 135                continue;
 136
 1137            bestProjection = projection;
 1138            bestPoint = candidate;
 139        }
 140
 4141        return bestPoint;
 142    }
 143
 144    internal override ExactMassPoint2D CalculateLocalMassPoint() =>
 513145        CalculateAggregateMassPoint(usePrepared: false);
 146
 147    internal override ExactMassPoint2D CalculatePreparedLocalMassPoint() =>
 200148        CalculateAggregateMassPoint(usePrepared: true);
 149
 150    private ExactMassPoint2D CalculateAggregateMassPoint(bool usePrepared)
 151    {
 713152        bool hasPositiveWeight = false;
 4022153        for (int i = 0; i < _partColliders.Length; i++)
 154        {
 1298155            LSCollider2D partCollider = _partColliders[i];
 1298156            ExactMassWeight weight = usePrepared
 1298157                ? partCollider.CalculatePreparedAreaForMassProperties()
 1298158                : partCollider.CalculateAreaForMassProperties();
 1298159            _massPointScratch[i] = usePrepared
 1298160                ? partCollider.CalculatePreparedLocalMassPoint()
 1298161                : partCollider.CalculateLocalMassPoint();
 1298162            _massWeightScratch[i] = weight;
 1298163            hasPositiveWeight |= !weight.IsZero;
 164        }
 165
 713166        if (!hasPositiveWeight)
 167        {
 32168            for (int i = 0; i < _massWeightScratch.Length; i++)
 11169                _massWeightScratch[i] = ExactMassWeight.One;
 170        }
 171
 713172        if (!ExactMassPoint2D.TryGetWeightedAverage(
 713173            _massPointScratch,
 713174            _massWeightScratch,
 713175            out Vector2d center))
 176        {
 2177            throw new InvalidOperationException(
 2178                usePrepared
 2179                    ? "Prepared 2D compound mass-property point is outside the Fixed64 coordinate domain."
 2180                    : "The 2D compound collider's center of mass is outside the Fixed64 coordinate domain.");
 181        }
 711182        return ExactMassPoint2D.FromPoint(center);
 183    }
 184
 185    internal override ExactMassWeight CalculateAreaForMassProperties() =>
 2186        CalculateAggregateMassWeight(usePrepared: false);
 187
 188    internal override ExactMassWeight CalculatePreparedAreaForMassProperties() =>
 113189        CalculateAggregateMassWeight(usePrepared: true);
 190
 191    private ExactMassWeight CalculateAggregateMassWeight(bool usePrepared)
 192    {
 115193        ExactMassWeight totalWeight = ExactMassWeight.Zero;
 678194        for (int i = 0; i < _partColliders.Length; i++)
 195        {
 224196            ExactMassWeight weight = usePrepared
 224197                ? _partColliders[i].CalculatePreparedAreaForMassProperties()
 224198                : _partColliders[i].CalculateAreaForMassProperties();
 224199            totalWeight = totalWeight.Add(weight);
 200        }
 201
 115202        return totalWeight;
 203    }
 204
 205    internal override Fixed64 CalculateCenterOfMassMoment(Fixed64 mass)
 206    {
 168207        Vector2d center = CalculateLocalCenterOfMassOffset();
 168208        ExactMassWeight totalWeight = ExactMassWeight.Zero;
 168209        int residualPartIndex = _partColliders.Length - 1;
 944210        for (int i = 0; i < _partColliders.Length; i++)
 211        {
 304212            ExactMassWeight weight =
 304213                _partColliders[i].CalculateAreaForMassProperties();
 304214            totalWeight = totalWeight.Add(weight);
 304215            residualPartIndex = i;
 216        }
 217
 168218        bool useEqualWeights = totalWeight.IsZero;
 168219        if (useEqualWeights)
 220        {
 2221            totalWeight = ExactMassWeight.Zero;
 12222            for (int i = 0; i < _partColliders.Length; i++)
 4223                totalWeight = totalWeight.Add(ExactMassWeight.One);
 224        }
 225
 168226        ExactMassWeight cumulativeWeight = ExactMassWeight.Zero;
 168227        Fixed64 assignedMass = Fixed64.Zero;
 168228        Fixed64 moment = Fixed64.Zero;
 940229        for (int i = 0; i < _partColliders.Length; i++)
 230        {
 303231            LSCollider2D part = _partColliders[i];
 303232            ExactMassWeight weight = useEqualWeights
 303233                ? ExactMassWeight.One
 303234                : part.CalculateAreaForMassProperties();
 303235            cumulativeWeight = cumulativeWeight.Add(weight);
 236            Fixed64 partMass;
 303237            if (i == residualPartIndex)
 238            {
 167239                partMass = mass - assignedMass;
 167240                assignedMass = mass;
 241            }
 242            else
 243            {
 136244                _ = cumulativeWeight.TryGetProportionalShare(
 136245                    mass,
 136246                    totalWeight,
 136247                    out Fixed64 cumulativeMass);
 136248                partMass = cumulativeMass - assignedMass;
 136249                assignedMass = cumulativeMass;
 250            }
 251
 303252            if (partMass == Fixed64.Zero)
 253                continue;
 254
 300255            if (!part.CalculateLocalMassPoint()
 300256                    .TryAddParallelAxisMoment(
 300257                        part.CalculateCenterOfMassMoment(partMass),
 300258                        partMass,
 300259                        center,
 300260                        out Fixed64 contribution)
 300261                || !Fixed64.TryAdd(
 300262                    moment,
 300263                    contribution,
 300264                    out moment))
 265            {
 1266                throw new InvalidOperationException(
 1267                    "The 2D compound collider's moment of inertia is outside the Fixed64 scalar domain.");
 268            }
 269        }
 270
 167271        return moment;
 272    }
 273
 274    private protected override void PrepareShape(in ColliderShapeSnapshot2D snapshot)
 275    {
 117276        Vector2d min = Vector2d.Zero;
 117277        Vector2d max = Vector2d.Zero;
 278
 678279        for (int i = 0; i < _parts.Length; i++)
 280        {
 225281            CompoundColliderPart2D part = _parts[i];
 225282            LSCollider2D partCollider = _partColliders[i];
 225283            partCollider.PrepareCompoundPart(
 225284                snapshot,
 225285                part.LocalRotation,
 225286                part.LocalScale,
 225287                PreparedContext);
 288
 222289            Vector2d partMin = partCollider.PreparedShapeBounds.Min;
 222290            Vector2d partMax = partCollider.PreparedShapeBounds.Max;
 222291            if (i == 0)
 292            {
 114293                min = partMin;
 114294                max = partMax;
 114295                continue;
 296            }
 297
 108298            min = new Vector2d(FixedMath.Min(min.X, partMin.X), FixedMath.Min(min.Y, partMin.Y));
 108299            max = new Vector2d(FixedMath.Max(max.X, partMax.X), FixedMath.Max(max.Y, partMax.Y));
 300        }
 301
 114302        _ = CalculatePreparedLocalMassPoint();
 113303        _ = CalculatePreparedAreaForMassProperties();
 113304        SetPreparedBounds(FixedBoundArea.FromMinMax(min, max));
 113305    }
 306
 307    private protected override void PublishShape()
 308    {
 662309        for (int i = 0; i < _parts.Length; i++)
 310        {
 219311            CompoundColliderPart2D part = _parts[i];
 219312            _partColliders[i].PublishCompoundPart(
 219313                part.LocalRotation,
 219314                part.LocalScale,
 219315                PreparedContext);
 316        }
 112317    }
 318
 319    private static LSCollider2D MaterializePartCollider(CompoundColliderPart2D part)
 320    {
 247321        LSCollider2D collider = part.Shape.CreateRuntimeCollider();
 247322        collider.LocalOffset = part.LocalOffset;
 247323        collider.Material = part.ResolveMaterial(PhysicsMaterial.Default);
 247324        return collider;
 325    }
 326
 327    /// <inheritdoc/>
 328    protected override void OnMaterialChanged()
 329    {
 12330        for (int i = 0; i < _parts.Length; i++)
 3331            _partColliders[i].Material = _parts[i].ResolveMaterial(Material);
 3332    }
 333
 334    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 335    private static void ValidatePart(CompoundColliderPart2D part) =>
 248336        SwiftThrowHelper.ThrowIfArgument(part.IsDefault, nameof(part), "2D compound collider part cannot be default.");
 337}