Editor/AutoRig/JointEdits.cs

Editor utility for editing a rig skeleton used by the AutoRig preview. Provides pure-skeleton operations that return a new RigResult: DeleteJoint removes a joint and reparents children while remapping bone indices, RenameJoint returns a copy with one joint renamed.

File Access
using System.Collections.Generic;
using AutoRig.Rig;

namespace AutoRig.Editor;

/// <summary>
/// Pure skeleton edits used by the preview's realtime editing (delete, rename,
/// move). Each returns a NEW <see cref="RigResult"/> so the caller can swap it
/// onto the entry and re-render; the mesh is never touched.
/// </summary>
public static class JointEdits
{
    /// <summary>Removes a joint: its children reparent to its parent, and its
    /// skin influence is folded onto that parent (weights renormalize for free
    /// because the bone index simply remaps). The root cannot be removed.</summary>
    public static RigResult DeleteJoint( RigResult rig, int removed )
    {
        var joints = rig.Skeleton.Joints;
        if ( removed < 0 || removed >= joints.Count
            || joints.Count <= 1 || joints[removed].Parent < 0 )
            return rig;

        var parentOfRemoved = joints[removed].Parent;
        var skeleton = new RigSkeleton();
        var remap = new int[joints.Count];
        for ( var i = 0; i < joints.Count; i++ )
        {
            if ( i == removed )
            {
                remap[i] = remap[parentOfRemoved];
                continue;
            }
            var parent = joints[i].Parent;
            if ( parent == removed )
                parent = parentOfRemoved;
            remap[i] = skeleton.Joints.Count;
            skeleton.Joints.Add( new RigJoint
            {
                Name = joints[i].Name,
                Parent = parent < 0 ? -1 : remap[parent],
                Position = joints[i].Position,
                HingeAxis = joints[i].HingeAxis,
            } );
        }

        var boneIndices = (int[])rig.Weights.BoneIndices.Clone();
        for ( var k = 0; k < boneIndices.Length; k++ )
            boneIndices[k] = remap[boneIndices[k]];
        var weights = new SkinWeights
        {
            BoneIndices = boneIndices,
            Weights = (float[])rig.Weights.Weights.Clone(),
        };

        return new RigResult
        {
            Skeleton = skeleton,
            Weights = weights,
            SolverName = rig.SolverName,
            Degraded = rig.Degraded,
            Explanation = rig.Explanation,
        };
    }

    /// <summary>Renames a joint into a NEW rig (so the old one stays intact for
    /// undo). No-op - returns the same rig - on a blank name or a collision.</summary>
    public static RigResult RenameJoint( RigResult rig, int index, string name )
    {
        name = name?.Trim();
        if ( index < 0 || index >= rig.Skeleton.Joints.Count || string.IsNullOrEmpty( name ) )
            return rig;
        if ( rig.Skeleton.IndexOf( name ) is var existing and >= 0 && existing != index )
            return rig;

        var skeleton = new RigSkeleton();
        var source = rig.Skeleton.Joints;
        for ( var i = 0; i < source.Count; i++ )
            skeleton.Joints.Add( new RigJoint
            {
                Name = i == index ? name : source[i].Name,
                Parent = source[i].Parent,
                Position = source[i].Position,
                HingeAxis = source[i].HingeAxis,
            } );

        return new RigResult
        {
            Skeleton = skeleton,
            Weights = rig.Weights,   // rename doesn't touch weights
            SolverName = rig.SolverName,
            Degraded = rig.Degraded,
            Explanation = rig.Explanation,
        };
    }
}