< Summary

Information
Class: Gravitas.Constraints.Joint2D
Assembly: Gravitas
File(s): /home/runner/work/Gravitas/Gravitas/src/Gravitas/Constraints/2D/Joint2D.cs
Line coverage
100%
Covered lines: 222
Uncovered lines: 0
Coverable lines: 222
Total lines: 403
Line coverage: 100%
Branch coverage
100%
Covered branches: 64
Total branches: 64
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%
get_CollisionPolicy()100%11100%
get_IsEnabled()100%11100%
set_IsEnabled(...)100%22100%
SetMotor(...)100%11100%
ClearMotor()100%11100%
GetCachedImpulse(...)100%11100%
SetCachedImpulse(...)100%11100%
ClearSolverCache()100%22100%
WakeBodies()100%11100%
SetCollisionPolicyFromRecord(...)100%22100%
HasSolverParticipant()100%22100%
IsSolverBody(...)100%44100%
ContributeReplayHash(...)100%44100%
RecordData(...)100%1818100%
IsSupportedType(...)100%66100%
IsSupportedLimitKind(...)100%66100%
IsSupportedMotorKind(...)100%44100%
ValidatePayload(...)100%1010100%
ResolveRegistrationLimits(...)100%44100%
ResolveCurrentAnchorDistance(...)100%11100%
ResolveCurrentAnchorDistance(...)100%11100%

File(s)

/home/runner/work/Gravitas/Gravitas/src/Gravitas/Constraints/2D/Joint2D.cs

#LineLine coverage
 1//=======================================================================
 2// Joint2D.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.Chronicler;
 11
 12namespace Gravitas.Constraints;
 13
 14/// <summary>
 15/// Context-owned deterministic pure 2D joint runtime state.
 16/// </summary>
 17public sealed class Joint2D : IRecordable
 18{
 19519    private readonly Fixed64[] _accumulatedImpulses = new Fixed64[JointSolver2D.MaxRowsPerJoint];
 20    private JointCollisionPolicy _collisionPolicy;
 19521    private bool _isEnabled = true;
 22
 19523    internal Joint2D(
 19524        GravitasConstraint2DService service,
 19525        int id,
 19526        in JointDefinition2D definition)
 27    {
 19528        Service = service;
 19529        Context = service.Context;
 19530        Id = id;
 19531        BodyA = definition.BodyA;
 19532        BodyB = definition.BodyB;
 19533        LocalFrameA = definition.LocalFrameA;
 19534        LocalFrameB = definition.LocalFrameB;
 19535        Type = definition.Type;
 19536        Limits = ResolveRegistrationLimits(definition);
 19537        Motor = definition.Motor;
 19538        _collisionPolicy = definition.CollisionPolicy;
 19539        IsActive = true;
 19540    }
 41
 42    internal GravitasConstraint2DService Service { get; }
 43
 44    internal RagdollRuntime2D? OwningRagdoll { get; set; }
 45
 46    /// <summary>
 47    /// Gets the owning world context.
 48    /// </summary>
 49    public GravitasWorldContext Context { get; }
 50
 51    /// <summary>
 52    /// Gets this context-local joint ID.
 53    /// </summary>
 54    public int Id { get; }
 55
 56    /// <summary>
 57    /// Gets the first body linked by the joint.
 58    /// </summary>
 59    public SolidBody2D BodyA { get; }
 60
 61    /// <summary>
 62    /// Gets the second body linked by the joint.
 63    /// </summary>
 64    public SolidBody2D BodyB { get; }
 65
 66    /// <summary>
 67    /// Gets the first local anchor frame.
 68    /// </summary>
 69    public JointFrame2D LocalFrameA { get; private set; }
 70
 71    /// <summary>
 72    /// Gets the second local anchor frame.
 73    /// </summary>
 74    public JointFrame2D LocalFrameB { get; private set; }
 75
 76    /// <summary>
 77    /// Gets the joint type.
 78    /// </summary>
 79    public JointType2D Type { get; private set; }
 80
 81    /// <summary>
 82    /// Gets optional scalar limit data.
 83    /// </summary>
 84    public JointLimit2D Limits { get; private set; }
 85
 86    /// <summary>
 87    /// Gets optional scalar motor data.
 88    /// </summary>
 89    public JointMotor2D Motor { get; private set; }
 90
 91    /// <summary>
 92    /// Gets physical collision filtering behavior for the linked colliders.
 93    /// </summary>
 26994    public JointCollisionPolicy CollisionPolicy => _collisionPolicy;
 95
 96    /// <summary>
 97    /// Gets whether this joint is registered with its owning service.
 98    /// </summary>
 99    public bool IsActive { get; internal set; }
 100
 101    /// <summary>
 102    /// Gets or sets whether this active joint emits solver rows.
 103    /// </summary>
 104    public bool IsEnabled
 105    {
 656106        get => _isEnabled;
 107        set
 108        {
 41109            SwiftThrowHelper.ThrowIfTrue(
 41110                !IsActive,
 41111                nameof(Joint2D),
 41112                "Removed joints cannot mutate simulation state.");
 39113            if (_isEnabled == value)
 21114                return;
 115
 18116            bool oldValue = _isEnabled;
 18117            Service.UpdateJointEnabledState(this, oldValue, value);
 18118            _isEnabled = value;
 18119            ClearSolverCache();
 18120            WakeBodies();
 18121        }
 122    }
 123
 124    /// <summary>
 125    /// Gets the number of rows emitted by the most recent solver pass.
 126    /// </summary>
 127    public int LastSolvedRowCount { get; internal set; }
 128
 129    /// <summary>
 130    /// Gets the cumulative absolute incremental impulse emitted since the
 131    /// solver cache was last cleared.
 132    /// </summary>
 133    public Fixed64 AccumulatedImpulseMagnitude { get; internal set; }
 134
 135    /// <summary>
 136    /// Gets deterministic metrics from the most recent solver pass.
 137    /// </summary>
 138    public JointSolveMetrics2D LastSolveMetrics { get; internal set; }
 139
 140    /// <summary>
 141    /// Replaces the motor payload and wakes linked bodies.
 142    /// </summary>
 143    public void SetMotor(JointMotor2D motor)
 144    {
 8145        SwiftThrowHelper.ThrowIfTrue(
 8146            !IsActive,
 8147            nameof(Joint2D),
 8148            "Removed joints cannot mutate simulation state.");
 6149        motor.Validate();
 6150        ValidatePayload(Type, Limits, motor);
 5151        Motor = motor;
 5152        ClearSolverCache();
 5153        WakeBodies();
 5154    }
 155
 156    /// <summary>
 157    /// Disables the joint motor and wakes linked bodies.
 158    /// </summary>
 3159    public void ClearMotor() => SetMotor(JointMotor2D.Disabled);
 160
 411161    internal Fixed64 GetCachedImpulse(int rowIndex) => _accumulatedImpulses[rowIndex];
 162
 12018163    internal void SetCachedImpulse(int rowIndex, Fixed64 impulse) => _accumulatedImpulses[rowIndex] = impulse;
 164
 165    internal void ClearSolverCache()
 166    {
 4302167        for (int i = 0; i < _accumulatedImpulses.Length; i++)
 1912168            _accumulatedImpulses[i] = Fixed64.Zero;
 169
 239170        LastSolvedRowCount = 0;
 239171        AccumulatedImpulseMagnitude = Fixed64.Zero;
 239172        LastSolveMetrics = default;
 239173    }
 174
 175    internal void WakeBodies()
 176    {
 23177        BodyA.Wake();
 23178        BodyB.Wake();
 23179    }
 180
 181    internal void SetCollisionPolicyFromRecord(JointCollisionPolicy collisionPolicy)
 182    {
 11183        if (_collisionPolicy == collisionPolicy)
 3184            return;
 185
 8186        Service.UpdateJointCollisionPolicy(this, _collisionPolicy, collisionPolicy);
 8187        _collisionPolicy = collisionPolicy;
 8188    }
 189
 588190    internal bool HasSolverParticipant() => IsSolverBody(BodyA) || IsSolverBody(BodyB);
 191
 192    internal static bool IsSolverBody(SolidBody2D body) =>
 690193        body.Active && body.DynamicId >= 0 && body.HasSolverMobility;
 194
 195    internal void ContributeReplayHash(
 196        ref ChronicleHashWriter writer,
 197        GravitasReplayHashMode mode)
 198    {
 14199        writer.WriteSection("joint.2d", 1);
 14200        writer.WriteInt32(Id);
 14201        writer.WriteInt32(BodyA.DynamicId);
 14202        writer.WriteInt32(BodyB.DynamicId);
 14203        writer.WriteInt32(BodyA.Collider.Id);
 14204        writer.WriteInt32(BodyB.Collider.Id);
 14205        writer.WriteBool(IsActive);
 14206        writer.WriteBool(IsEnabled);
 14207        writer.WriteEnum(Type);
 14208        writer.WriteEnum(Limits.Kind);
 14209        writer.WriteFixed64(Limits.TargetDistance);
 14210        writer.WriteFixed64(Limits.MinTranslation);
 14211        writer.WriteFixed64(Limits.MaxTranslation);
 14212        writer.WriteFixed64(Limits.MinAngle);
 14213        writer.WriteFixed64(Limits.MaxAngle);
 14214        writer.WriteEnum(Motor.Kind);
 14215        writer.WriteFixed64(Motor.Target);
 14216        writer.WriteFixed64(Motor.DriveStrength);
 14217        writer.WriteFixed64(Motor.Damping);
 14218        writer.WriteFixed64(Motor.MaximumMotorImpulse);
 14219        writer.WriteEnum(CollisionPolicy);
 14220        writer.WriteVector2d(LocalFrameA.Anchor);
 14221        writer.WriteFixed64(LocalFrameA.Angle);
 14222        writer.WriteVector2d(LocalFrameB.Anchor);
 14223        writer.WriteFixed64(LocalFrameB.Angle);
 224
 14225        if (mode != GravitasReplayHashMode.AuthoritativeWithSolverCaches)
 12226            return;
 227
 2228        writer.WriteSection("joint.2d.caches", 1);
 2229        writer.WriteInt32(LastSolvedRowCount);
 2230        writer.WriteFixed64(AccumulatedImpulseMagnitude);
 2231        writer.WriteInt32(LastSolveMetrics.PreparedRowCount);
 2232        writer.WriteFixed64(LastSolveMetrics.LinearAnchorErrorMagnitude);
 2233        writer.WriteFixed64(LastSolveMetrics.AngularErrorMagnitude);
 2234        writer.WriteFixed64(LastSolveMetrics.LimitErrorMagnitude);
 2235        writer.WriteFixed64(LastSolveMetrics.AccumulatedImpulseMagnitude);
 2236        writer.WriteFixed64(LastSolveMetrics.IncrementalImpulseMagnitude);
 2237        writer.WriteFixed64(LastSolveMetrics.MotorImpulseMagnitude);
 2238        writer.WriteFixed64(LastSolveMetrics.MotorErrorMagnitude);
 2239        writer.WriteInt32(LastSolveMetrics.ClampedRowCount);
 36240        for (int i = 0; i < _accumulatedImpulses.Length; i++)
 16241            writer.WriteFixed64(_accumulatedImpulses[i]);
 2242    }
 243
 244    /// <summary>
 245    /// Records mutable joint state into a Chronicler pass.
 246    /// </summary>
 247    public void RecordData(IChronicler chronicler)
 248    {
 16249        SwiftThrowHelper.ThrowIfTrue(
 16250            !IsActive,
 16251            nameof(Joint2D),
 16252            "Removed joints cannot participate in serialization.");
 14253        bool isEnabled = IsEnabled;
 14254        JointType2D type = Type;
 14255        JointLimitKind2D limitKind = Limits.Kind;
 14256        Fixed64 targetDistance = Limits.TargetDistance;
 14257        Fixed64 minTranslation = Limits.MinTranslation;
 14258        Fixed64 maxTranslation = Limits.MaxTranslation;
 14259        Fixed64 minAngle = Limits.MinAngle;
 14260        Fixed64 maxAngle = Limits.MaxAngle;
 14261        JointMotorKind2D motorKind = Motor.Kind;
 14262        Fixed64 motorTarget = Motor.Target;
 14263        Fixed64 motorStrength = Motor.DriveStrength;
 14264        Fixed64 motorDamping = Motor.Damping;
 14265        Fixed64 maxMotorImpulse = Motor.MaximumMotorImpulse;
 14266        JointCollisionPolicy collisionPolicy = CollisionPolicy;
 14267        Vector2d frameAAnchor = LocalFrameA.Anchor;
 14268        Fixed64 frameAAngle = LocalFrameA.Angle;
 14269        Vector2d frameBAnchor = LocalFrameB.Anchor;
 14270        Fixed64 frameBAngle = LocalFrameB.Angle;
 271
 14272        RecordValues.Look(chronicler, ref isEnabled, "IsEnabled", true);
 14273        RecordValues.Look(chronicler, ref type, "Type", JointType2D.Pin);
 14274        RecordValues.Look(chronicler, ref limitKind, "LimitKind", JointLimitKind2D.Unrestricted);
 14275        RecordValues.Look(chronicler, ref targetDistance, "TargetDistance");
 14276        RecordValues.Look(chronicler, ref minTranslation, "MinTranslation");
 14277        RecordValues.Look(chronicler, ref maxTranslation, "MaxTranslation");
 14278        RecordValues.Look(chronicler, ref minAngle, "MinAngle");
 14279        RecordValues.Look(chronicler, ref maxAngle, "MaxAngle");
 14280        RecordValues.Look(chronicler, ref motorKind, "MotorKind", JointMotorKind2D.Disabled);
 14281        RecordValues.Look(chronicler, ref motorTarget, "MotorTarget");
 14282        RecordValues.Look(chronicler, ref motorStrength, "MotorStrength");
 14283        RecordValues.Look(chronicler, ref motorDamping, "MotorDamping");
 14284        RecordValues.Look(chronicler, ref maxMotorImpulse, "MaxMotorImpulse");
 14285        RecordValues.Look(chronicler, ref collisionPolicy, "CollisionPolicy", JointCollisionPolicy.SuppressLinked);
 14286        RecordValues.Look(chronicler, ref frameAAnchor, "LocalFrameAAnchor");
 14287        RecordValues.Look(chronicler, ref frameAAngle, "LocalFrameAAngle");
 14288        RecordValues.Look(chronicler, ref frameBAnchor, "LocalFrameBAnchor");
 14289        RecordValues.Look(chronicler, ref frameBAngle, "LocalFrameBAngle");
 290
 14291        if (chronicler.Mode != SerializationMode.Loading)
 6292            return;
 293
 8294        SwiftThrowHelper.ThrowIfArgument(!IsSupportedType(type), nameof(Type), "Unsupported 2D joint type.");
 8295        SwiftThrowHelper.ThrowIfArgument(!IsSupportedLimitKind(limitKind), nameof(Limits), "Unsupported 2D joint limit k
 8296        SwiftThrowHelper.ThrowIfArgument(!IsSupportedMotorKind(motorKind), nameof(Motor), "Unsupported 2D joint motor ki
 8297        SwiftThrowHelper.ThrowIfArgument(
 8298            collisionPolicy != JointCollisionPolicy.SuppressLinked
 8299                && collisionPolicy != JointCollisionPolicy.Collide,
 8300            nameof(CollisionPolicy),
 8301            "Unsupported joint collision policy.");
 302
 8303        JointLimit2D limits = limitKind switch
 8304        {
 2305            JointLimitKind2D.Distance => JointLimit2D.Distance(targetDistance),
 2306            JointLimitKind2D.Slider => JointLimit2D.Slider(minTranslation, maxTranslation),
 2307            JointLimitKind2D.Angular => JointLimit2D.Angular(minAngle, maxAngle),
 2308            _ => JointLimit2D.Unrestricted
 8309        };
 8310        JointFrame2D localFrameA = new(frameAAnchor, frameAAngle);
 8311        JointFrame2D localFrameB = new(frameBAnchor, frameBAngle);
 8312        if (type == JointType2D.Distance && limits.Kind == JointLimitKind2D.Unrestricted)
 1313            limits = JointLimit2D.Distance(ResolveCurrentAnchorDistance(BodyA, BodyB, localFrameA, localFrameB));
 314
 8315        JointMotor2D motor = motorKind switch
 8316        {
 2317            JointMotorKind2D.Angular => JointMotor2D.Angular(motorTarget, motorStrength, motorDamping, maxMotorImpulse),
 3318            JointMotorKind2D.Linear => JointMotor2D.Linear(motorTarget, motorStrength, motorDamping, maxMotorImpulse),
 3319            _ => JointMotor2D.Disabled
 8320        };
 8321        ValidatePayload(type, limits, motor);
 322
 7323        Type = type;
 7324        Limits = limits;
 7325        Motor = motor;
 7326        SetCollisionPolicyFromRecord(collisionPolicy);
 7327        LocalFrameA = localFrameA;
 7328        LocalFrameB = localFrameB;
 7329        if (_isEnabled == isEnabled)
 5330            ClearSolverCache();
 331        else
 2332            IsEnabled = isEnabled;
 2333    }
 334
 335    internal static bool IsSupportedType(JointType2D type) =>
 231336        type == JointType2D.Distance
 231337        || type == JointType2D.Pin
 231338        || type == JointType2D.Weld
 231339        || type == JointType2D.Prismatic;
 340
 341    internal static bool IsSupportedLimitKind(JointLimitKind2D kind) =>
 8342        kind == JointLimitKind2D.Unrestricted
 8343        || kind == JointLimitKind2D.Distance
 8344        || kind == JointLimitKind2D.Slider
 8345        || kind == JointLimitKind2D.Angular;
 346
 347    internal static bool IsSupportedMotorKind(JointMotorKind2D kind) =>
 8348        kind == JointMotorKind2D.Disabled
 8349        || kind == JointMotorKind2D.Angular
 8350        || kind == JointMotorKind2D.Linear;
 351
 352    internal static void ValidatePayload(
 353        JointType2D type,
 354        in JointLimit2D limits,
 355        in JointMotor2D motor)
 356    {
 235357        SwiftThrowHelper.ThrowIfArgument(
 235358            type == JointType2D.Distance
 235359                && limits.Kind != JointLimitKind2D.Unrestricted
 235360                && limits.Kind != JointLimitKind2D.Distance,
 235361            nameof(limits),
 235362            "Distance joints accept only distance limits.");
 235363        SwiftThrowHelper.ThrowIfArgument(
 235364            type == JointType2D.Prismatic
 235365                && limits.Kind != JointLimitKind2D.Unrestricted
 235366                && limits.Kind != JointLimitKind2D.Slider,
 235367            nameof(limits),
 235368            "Prismatic joints accept only slider limits.");
 235369        SwiftThrowHelper.ThrowIfArgument(
 235370            type != JointType2D.Prismatic
 235371                && motor.Kind == JointMotorKind2D.Linear,
 235372            nameof(motor),
 235373            "Linear motors are supported only by prismatic joints.");
 232374    }
 375
 376    private static JointLimit2D ResolveRegistrationLimits(in JointDefinition2D definition)
 377    {
 195378        if (definition.Type == JointType2D.Distance && definition.Limits.Kind == JointLimitKind2D.Unrestricted)
 1379            return JointLimit2D.Distance(ResolveCurrentAnchorDistance(definition));
 380
 194381        return definition.Limits;
 382    }
 383
 384    private static Fixed64 ResolveCurrentAnchorDistance(in JointDefinition2D definition)
 385    {
 1386        return ResolveCurrentAnchorDistance(
 1387            definition.BodyA,
 1388            definition.BodyB,
 1389            definition.LocalFrameA,
 1390            definition.LocalFrameB);
 391    }
 392
 393    private static Fixed64 ResolveCurrentAnchorDistance(
 394        SolidBody2D bodyA,
 395        SolidBody2D bodyB,
 396        in JointFrame2D localFrameA,
 397        in JointFrame2D localFrameB)
 398    {
 2399        Vector2d anchorA = bodyA.Position + Vector2d.Rotate(localFrameA.Anchor, bodyA.Rotation);
 2400        Vector2d anchorB = bodyB.Position + Vector2d.Rotate(localFrameB.Anchor, bodyB.Rotation);
 2401        return (anchorB - anchorA).Magnitude;
 402    }
 403}