Code/AutoRig/Rig/RigSkeleton.cs

Defines RigJoint and RigSkeleton types for a generated skeleton. RigJoint stores name, parent index, bind position and optional hinge axis. RigSkeleton holds a list of joints, provides Root and IndexOf helpers, and a Validate method that enforces single root, parent-before-child ordering, unique names, and finite positions.

using System.Numerics;

namespace AutoRig.Rig;

// s&box compat: the engine defines Vector2/Vector3 in the GLOBAL namespace, which
// shadows using-directive imports - alias explicitly to System.Numerics.
using Vector3 = System.Numerics.Vector3;

/// <summary>One joint of a generated skeleton (bind pose, world space).</summary>
public sealed class RigJoint
{
    public required string Name { get; set; }

    /// <summary>Parent joint index; -1 for the root. Parents always precede children.</summary>
    public required int Parent { get; set; }

    /// <summary>Bind position in mesh (world) space.</summary>
    public required Vector3 Position { get; set; }

    /// <summary>Preferred rotation axis (unit) for mechanical joints; Zero = free/ball joint.</summary>
    public Vector3 HingeAxis { get; set; }
}

/// <summary>A generated skeleton: joints in parent-before-child order.</summary>
public sealed class RigSkeleton
{
    public List<RigJoint> Joints { get; } = new();

    /// <summary>Index of the root joint (the single joint with Parent == -1).</summary>
    public int Root
    {
        get
        {
            for ( var i = 0; i < Joints.Count; i++ )
                if ( Joints[i].Parent < 0 )
                    return i;
            return -1;
        }
    }

    /// <summary>Index of the named joint, -1 when absent.</summary>
    public int IndexOf( string name )
    {
        for ( var i = 0; i < Joints.Count; i++ )
            if ( string.Equals( Joints[i].Name, name, StringComparison.Ordinal ) )
                return i;
        return -1;
    }

    /// <summary>Checks structural invariants.</summary>
    /// <exception cref="FormatException">On any malformed skeleton.</exception>
    public void Validate()
    {
        if ( Joints.Count == 0 )
            throw new FormatException( "Skeleton has no joints." );

        var roots = 0;
        var names = new HashSet<string>( StringComparer.Ordinal );
        for ( var i = 0; i < Joints.Count; i++ )
        {
            var joint = Joints[i];
            if ( joint.Parent < 0 )
                roots++;
            else if ( joint.Parent >= i )
                throw new FormatException(
                    $"Joint '{joint.Name}' (index {i}) has parent {joint.Parent}; parents must precede children." );

            if ( !names.Add( joint.Name ) )
                throw new FormatException( $"Duplicate joint name '{joint.Name}'." );

            var p = joint.Position;
            if ( !float.IsFinite( p.X ) || !float.IsFinite( p.Y ) || !float.IsFinite( p.Z ) )
                throw new FormatException( $"Joint '{joint.Name}' has a non-finite position." );
        }

        if ( roots != 1 )
            throw new FormatException( $"Skeleton must have exactly one root joint (found {roots})." );
    }
}