< Summary

Information
Class: Gravitas.Constraints.Joint3D
Assembly: Gravitas
File(s): /home/runner/work/Gravitas/Gravitas/src/Gravitas/Constraints/3D/Joint3D.cs
Line coverage
100%
Covered lines: 187
Uncovered lines: 0
Coverable lines: 187
Total lines: 347
Line coverage: 100%
Branch coverage
100%
Covered branches: 48
Total branches: 48
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%1010100%
IsSupportedType(...)100%66100%
IsSupportedLimitKind(...)100%44100%
ValidatePayload(...)100%1212100%

File(s)

/home/runner/work/Gravitas/Gravitas/src/Gravitas/Constraints/3D/Joint3D.cs

#LineLine coverage
 1//=======================================================================
 2// Joint3D.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 3D joint runtime state.
 16/// </summary>
 17public sealed class Joint3D : IRecordable
 18{
 31219    private readonly Fixed64[] _accumulatedImpulses = new Fixed64[JointSolver3D.MaxRowsPerJoint];
 20    private JointCollisionPolicy _collisionPolicy;
 31221    private bool _isEnabled = true;
 22
 31223    internal Joint3D(
 31224        GravitasConstraint3DService service,
 31225        int id,
 31226        JointDefinition3D definition,
 31227        JointFrame3D localFrameA,
 31228        JointFrame3D localFrameB)
 29    {
 31230        Service = service;
 31231        Context = service.Context;
 31232        Id = id;
 31233        BodyA = definition.BodyA;
 31234        BodyB = definition.BodyB;
 31235        LocalFrameA = localFrameA;
 31236        LocalFrameB = localFrameB;
 31237        Type = definition.Type;
 31238        Limits = definition.Limits;
 31239        Motor = definition.Motor;
 31240        _collisionPolicy = definition.CollisionPolicy;
 31241        IsActive = true;
 31242    }
 43
 44    internal GravitasConstraint3DService Service { get; }
 45
 46    internal RagdollRuntime3D? OwningRagdoll { get; set; }
 47
 48    /// <summary>
 49    /// Gets the owning world context.
 50    /// </summary>
 51    public GravitasWorldContext Context { get; }
 52
 53    /// <summary>
 54    /// Gets this context-local joint ID.
 55    /// </summary>
 56    public int Id { get; }
 57
 58    /// <summary>
 59    /// Gets the first body linked by the joint.
 60    /// </summary>
 61    public SolidBody BodyA { get; }
 62
 63    /// <summary>
 64    /// Gets the second body linked by the joint.
 65    /// </summary>
 66    public SolidBody BodyB { get; }
 67
 68    /// <summary>
 69    /// Gets the first local anchor frame.
 70    /// </summary>
 71    public JointFrame3D LocalFrameA { get; private set; }
 72
 73    /// <summary>
 74    /// Gets the second local anchor frame.
 75    /// </summary>
 76    public JointFrame3D LocalFrameB { get; private set; }
 77
 78    /// <summary>
 79    /// Gets the joint type.
 80    /// </summary>
 81    public JointType3D Type { get; private set; }
 82
 83    /// <summary>
 84    /// Gets optional angular limit data.
 85    /// </summary>
 86    public JointLimit3D Limits { get; private set; }
 87
 88    /// <summary>
 89    /// Gets optional motor data.
 90    /// </summary>
 91    public JointMotor3D Motor { get; private set; }
 92
 93    /// <summary>
 94    /// Gets physical collision filtering behavior for the linked colliders.
 95    /// </summary>
 165896    public JointCollisionPolicy CollisionPolicy => _collisionPolicy;
 97
 98    /// <summary>
 99    /// Gets whether this joint is registered with its owning service.
 100    /// </summary>
 101    public bool IsActive { get; internal set; }
 102
 103    /// <summary>
 104    /// Gets or sets whether this active joint emits solver rows.
 105    /// </summary>
 106    public bool IsEnabled
 107    {
 16369108        get => _isEnabled;
 109        set
 110        {
 77111            SwiftThrowHelper.ThrowIfTrue(
 77112                !IsActive,
 77113                nameof(Joint3D),
 77114                "Removed joints cannot mutate simulation state.");
 75115            if (_isEnabled == value)
 55116                return;
 117
 20118            bool oldValue = _isEnabled;
 20119            Service.UpdateJointEnabledState(this, oldValue, value);
 20120            _isEnabled = value;
 20121            ClearSolverCache();
 20122            WakeBodies();
 20123        }
 124    }
 125
 126    /// <summary>
 127    /// Gets the number of rows emitted by the most recent solver pass.
 128    /// </summary>
 129    public int LastSolvedRowCount { get; internal set; }
 130
 131    /// <summary>
 132    /// Gets the cumulative absolute incremental impulse emitted since the
 133    /// solver cache was last cleared.
 134    /// </summary>
 135    public Fixed64 AccumulatedImpulseMagnitude { get; internal set; }
 136
 137    /// <summary>
 138    /// Gets deterministic metrics from the most recent solver pass.
 139    /// </summary>
 140    public JointSolveMetrics3D LastSolveMetrics { get; internal set; }
 141
 142    /// <summary>
 143    /// Replaces the motor payload and wakes linked bodies.
 144    /// </summary>
 145    public void SetMotor(JointMotor3D motor)
 146    {
 5147        SwiftThrowHelper.ThrowIfTrue(
 5148            !IsActive,
 5149            nameof(Joint3D),
 5150            "Removed joints cannot mutate simulation state.");
 3151        motor.Validate();
 3152        Motor = motor;
 3153        ClearSolverCache();
 3154        WakeBodies();
 3155    }
 156
 157    /// <summary>
 158    /// Disables the joint motor and wakes linked bodies.
 159    /// </summary>
 3160    public void ClearMotor() => SetMotor(JointMotor3D.Disabled);
 161
 16295162    internal Fixed64 GetCachedImpulse(int rowIndex) => _accumulatedImpulses[rowIndex];
 163
 596762164    internal void SetCachedImpulse(int rowIndex, Fixed64 impulse) => _accumulatedImpulses[rowIndex] = impulse;
 165
 166    internal void ClearSolverCache()
 167    {
 9204168        for (int i = 0; i < _accumulatedImpulses.Length; i++)
 4248169            _accumulatedImpulses[i] = Fixed64.Zero;
 170
 354171        LastSolvedRowCount = 0;
 354172        AccumulatedImpulseMagnitude = Fixed64.Zero;
 354173        LastSolveMetrics = default;
 354174    }
 175
 176    internal void WakeBodies()
 177    {
 23178        BodyA.Wake();
 23179        BodyB.Wake();
 23180    }
 181
 182    internal void SetCollisionPolicyFromRecord(JointCollisionPolicy collisionPolicy)
 183    {
 9184        if (_collisionPolicy == collisionPolicy)
 3185            return;
 186
 6187        Service.UpdateJointCollisionPolicy(this, _collisionPolicy, collisionPolicy);
 6188        _collisionPolicy = collisionPolicy;
 6189    }
 190
 15027191    internal bool HasSolverParticipant() => IsSolverBody(BodyA) || IsSolverBody(BodyB);
 192
 193    internal static bool IsSolverBody(SolidBody body) =>
 16026194        body.Active && body.DynamicId >= 0 && body.HasSolverMobility;
 195
 196    internal void ContributeReplayHash(
 197        ref ChronicleHashWriter writer,
 198        GravitasReplayHashMode mode)
 199    {
 1289200        writer.WriteSection("joint.3d", 1);
 1289201        writer.WriteInt32(Id);
 1289202        writer.WriteInt32(BodyA.DynamicId);
 1289203        writer.WriteInt32(BodyB.DynamicId);
 1289204        writer.WriteInt32(BodyA.Collider.Id);
 1289205        writer.WriteInt32(BodyB.Collider.Id);
 1289206        writer.WriteBool(IsActive);
 1289207        writer.WriteBool(IsEnabled);
 1289208        writer.WriteEnum(Type);
 1289209        writer.WriteEnum(Limits.Kind);
 1289210        writer.WriteFixed64(Limits.MaxHingeAngle);
 1289211        writer.WriteFixed64(Limits.MaxConeAngle);
 1289212        writer.WriteFixed64(Limits.MaxTwistAngle);
 1289213        writer.WriteQuaternion(Motor.TargetLocalRotation);
 1289214        writer.WriteFixed64(Motor.AngularDriveStrength);
 1289215        writer.WriteFixed64(Motor.AngularDriveDamping);
 1289216        writer.WriteFixed64(Motor.MaximumMotorImpulse);
 1289217        writer.WriteEnum(CollisionPolicy);
 1289218        writer.WriteVector3d(LocalFrameA.Position);
 1289219        writer.WriteQuaternion(LocalFrameA.Rotation);
 1289220        writer.WriteVector3d(LocalFrameB.Position);
 1289221        writer.WriteQuaternion(LocalFrameB.Rotation);
 222
 1289223        if (mode != GravitasReplayHashMode.AuthoritativeWithSolverCaches)
 7224            return;
 225
 1282226        writer.WriteSection("joint.3d.caches", 1);
 1282227        writer.WriteInt32(LastSolvedRowCount);
 1282228        writer.WriteFixed64(AccumulatedImpulseMagnitude);
 1282229        writer.WriteInt32(LastSolveMetrics.PreparedRowCount);
 1282230        writer.WriteFixed64(LastSolveMetrics.LinearAnchorErrorMagnitude);
 1282231        writer.WriteFixed64(LastSolveMetrics.AngularLimitErrorMagnitude);
 1282232        writer.WriteFixed64(LastSolveMetrics.AccumulatedImpulseMagnitude);
 1282233        writer.WriteFixed64(LastSolveMetrics.IncrementalImpulseMagnitude);
 1282234        writer.WriteFixed64(LastSolveMetrics.MotorImpulseMagnitude);
 1282235        writer.WriteFixed64(LastSolveMetrics.MotorErrorMagnitude);
 1282236        writer.WriteInt32(LastSolveMetrics.ClampedRowCount);
 33332237        for (int i = 0; i < _accumulatedImpulses.Length; i++)
 15384238            writer.WriteFixed64(_accumulatedImpulses[i]);
 1282239    }
 240
 241    /// <summary>
 242    /// Records mutable joint state into a Chronicler pass.
 243    /// </summary>
 244    public void RecordData(IChronicler chronicler)
 245    {
 13246        SwiftThrowHelper.ThrowIfTrue(
 13247            !IsActive,
 13248            nameof(Joint3D),
 13249            "Removed joints cannot participate in serialization.");
 11250        bool isEnabled = IsEnabled;
 11251        JointType3D type = Type;
 11252        JointLimitKind3D limitKind = Limits.Kind;
 11253        Fixed64 maxHingeAngle = Limits.MaxHingeAngle;
 11254        Fixed64 maxConeAngle = Limits.MaxConeAngle;
 11255        Fixed64 maxTwistAngle = Limits.MaxTwistAngle;
 11256        FixedQuaternion motorTarget = Motor.TargetLocalRotation;
 11257        Fixed64 motorStrength = Motor.AngularDriveStrength;
 11258        Fixed64 motorDamping = Motor.AngularDriveDamping;
 11259        Fixed64 maxMotorImpulse = Motor.MaximumMotorImpulse;
 11260        JointCollisionPolicy collisionPolicy = CollisionPolicy;
 11261        Vector3d frameAPosition = LocalFrameA.Position;
 11262        FixedQuaternion frameARotation = LocalFrameA.Rotation;
 11263        Vector3d frameBPosition = LocalFrameB.Position;
 11264        FixedQuaternion frameBRotation = LocalFrameB.Rotation;
 265
 11266        RecordValues.Look(chronicler, ref isEnabled, "IsEnabled", true);
 11267        RecordValues.Look(chronicler, ref type, "Type", JointType3D.BallSocket);
 11268        RecordValues.Look(chronicler, ref limitKind, "LimitKind", JointLimitKind3D.Unrestricted);
 11269        RecordValues.Look(chronicler, ref maxHingeAngle, "MaxHingeAngle");
 11270        RecordValues.Look(chronicler, ref maxConeAngle, "MaxConeAngle");
 11271        RecordValues.Look(chronicler, ref maxTwistAngle, "MaxTwistAngle");
 11272        RecordValues.Look(chronicler, ref motorTarget, "MotorTarget");
 11273        RecordValues.Look(chronicler, ref motorStrength, "MotorStrength");
 11274        RecordValues.Look(chronicler, ref motorDamping, "MotorDamping");
 11275        RecordValues.Look(chronicler, ref maxMotorImpulse, "MaxMotorImpulse");
 11276        RecordValues.Look(chronicler, ref collisionPolicy, "CollisionPolicy", JointCollisionPolicy.SuppressLinked);
 11277        RecordValues.Look(chronicler, ref frameAPosition, "LocalFrameAPosition");
 11278        RecordValues.Look(chronicler, ref frameARotation, "LocalFrameARotation");
 11279        RecordValues.Look(chronicler, ref frameBPosition, "LocalFrameBPosition");
 11280        RecordValues.Look(chronicler, ref frameBRotation, "LocalFrameBRotation");
 281
 11282        if (chronicler.Mode != SerializationMode.Loading)
 5283            return;
 284
 6285        SwiftThrowHelper.ThrowIfArgument(!IsSupportedType(type), nameof(Type), "Unsupported 3D joint type.");
 6286        SwiftThrowHelper.ThrowIfArgument(!IsSupportedLimitKind(limitKind), nameof(Limits), "Unsupported joint limit kind
 6287        SwiftThrowHelper.ThrowIfArgument(
 6288            collisionPolicy != JointCollisionPolicy.SuppressLinked
 6289                && collisionPolicy != JointCollisionPolicy.Collide,
 6290            nameof(CollisionPolicy),
 6291            "Unsupported joint collision policy.");
 292
 6293        JointLimit3D limits = limitKind switch
 6294        {
 3295            JointLimitKind3D.Hinge => JointLimit3D.Hinge(maxHingeAngle),
 2296            JointLimitKind3D.ConeTwist => JointLimit3D.ConeTwist(maxConeAngle, maxTwistAngle),
 1297            _ => JointLimit3D.Unrestricted
 6298        };
 6299        JointMotor3D motor = new(motorTarget, motorStrength, motorDamping, maxMotorImpulse);
 6300        ValidatePayload(type, limits);
 301
 5302        Type = type;
 5303        Limits = limits;
 5304        Motor = motor;
 5305        SetCollisionPolicyFromRecord(collisionPolicy);
 5306        LocalFrameA = new JointFrame3D(frameAPosition, frameARotation);
 5307        LocalFrameB = new JointFrame3D(frameBPosition, frameBRotation);
 5308        if (_isEnabled == isEnabled)
 2309            ClearSolverCache();
 310        else
 3311            IsEnabled = isEnabled;
 3312    }
 313
 314    internal static bool IsSupportedType(JointType3D type) =>
 380315        type == JointType3D.BallSocket
 380316        || type == JointType3D.Hinge
 380317        || type == JointType3D.ConeTwist
 380318        || type == JointType3D.Fixed;
 319
 320    internal static bool IsSupportedLimitKind(JointLimitKind3D kind) =>
 6321        kind == JointLimitKind3D.Unrestricted
 6322        || kind == JointLimitKind3D.Hinge
 6323        || kind == JointLimitKind3D.ConeTwist;
 324
 325    internal static void ValidatePayload(
 326        JointType3D type,
 327        in JointLimit3D limits)
 328    {
 379329        SwiftThrowHelper.ThrowIfArgument(
 379330            type == JointType3D.Hinge
 379331                && limits.Kind != JointLimitKind3D.Unrestricted
 379332                && limits.Kind != JointLimitKind3D.Hinge,
 379333            nameof(limits),
 379334            "Hinge joints accept only hinge limits.");
 379335        SwiftThrowHelper.ThrowIfArgument(
 379336            type == JointType3D.ConeTwist
 379337                && limits.Kind != JointLimitKind3D.Unrestricted
 379338                && limits.Kind != JointLimitKind3D.ConeTwist,
 379339            nameof(limits),
 379340            "Cone-twist joints accept only cone-twist limits.");
 379341        SwiftThrowHelper.ThrowIfArgument(
 379342            (type == JointType3D.BallSocket || type == JointType3D.Fixed)
 379343                && limits.Kind != JointLimitKind3D.Unrestricted,
 379344            nameof(limits),
 379345            "Ball-socket and fixed joints do not accept angular limit payloads.");
 376346    }
 347}