Editor/Core/Validation/Deformation.cs
#nullable enable annotations
using System.Numerics;
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;

public enum PoseAxisSpace { World, BoneBend, BoneTwist, BoneNormal }
public sealed record StressJoint(string Role,Vector3 Axis,float Degrees,PoseAxisSpace AxisSpace=PoseAxisSpace.World);
public sealed record StressPose(string Name,string Role,Vector3 Axis,float Degrees,PoseAxisSpace AxisSpace=PoseAxisSpace.World,IReadOnlyList<StressJoint>? AdditionalJoints=null);
public static class Deformation
{
    // Build definitions on access: editor hotload must not migrate old pose
    // objects into a changed list and silently retain stale angles or names.
    // Canonical anatomy is +X left, +Y up and +Z toward the toes.
    public static IReadOnlyList<StressPose> Poses =>new[]{
        new StressPose("Neutral","Root",Vector3.UnitY,0),new("Arm raise","UpperArm.L",Vector3.UnitZ,75),new("Shoulder rotation","UpperArm.R",Vector3.UnitY,60),
        new("Elbow bend","LowerArm.L",Vector3.UnitY,-100,PoseAxisSpace.BoneBend),new("Elbow extension (extreme)","LowerArm.L",Vector3.UnitY,100,PoseAxisSpace.BoneBend),new("Wrist rotation","Hand.L",Vector3.UnitX,45,PoseAxisSpace.BoneTwist),new("Torso bend","SpineLower",Vector3.UnitX,30),
        new("Head rotation","Head",Vector3.UnitY,55),new("Hip flexion","UpperLeg.L",Vector3.UnitX,-75),new("Hip extension (extreme)","UpperLeg.L",Vector3.UnitX,75),new("Hip abduction","UpperLeg.R",Vector3.UnitZ,-40),
        new("Hip adduction","UpperLeg.R",Vector3.UnitZ,40),
        new("Right hip flexion","UpperLeg.R",Vector3.UnitX,-75),new("Right hip extension (extreme)","UpperLeg.R",Vector3.UnitX,75),
        new("Left hip abduction","UpperLeg.L",Vector3.UnitZ,40),new("Left hip adduction","UpperLeg.L",Vector3.UnitZ,-40),
        new("Knee bend","LowerLeg.L",Vector3.UnitX,100,PoseAxisSpace.BoneBend),new("Knee extension (extreme)","LowerLeg.L",Vector3.UnitX,-100,PoseAxisSpace.BoneBend),new("Ankle movement","Foot.L",Vector3.UnitX,30),new("Finger curl","Index1.L",Vector3.UnitZ,70,PoseAxisSpace.BoneBend),new("Thumb movement","Thumb1.L",Vector3.UnitY,45,PoseAxisSpace.BoneBend),
        new("Right finger curl","Index1.R",Vector3.UnitZ,-70,PoseAxisSpace.BoneBend),new("Right thumb movement","Thumb1.R",Vector3.UnitY,-45,PoseAxisSpace.BoneBend),
        new("Thumb opposition","Thumb1.L",Vector3.UnitZ,45,PoseAxisSpace.BoneNormal),new("Right thumb opposition","Thumb1.R",Vector3.UnitZ,-45,PoseAxisSpace.BoneNormal),
        Curl("L"),Curl("R"),ThumbCurl("L"),ThumbCurl("R"),
        new("Forearm twist","LowerArm.L",Vector3.UnitX,75,PoseAxisSpace.BoneTwist),new("Right forearm twist","LowerArm.R",Vector3.UnitX,-75,PoseAxisSpace.BoneTwist),
        new("Torso twist","SpineLower",Vector3.UnitY,30),
        Finger("Middle","L"),Finger("Ring","L"),Finger("Pinky","L"),
        Finger("Middle","R"),Finger("Ring","R"),Finger("Pinky","R")};
    static StressPose Finger(string finger,string side)=>new(side=="R"?"Right "+finger.ToLowerInvariant()+" finger curl":finger+" finger curl",finger+"1."+side,Vector3.UnitZ,side=="L"?70:-70,PoseAxisSpace.BoneBend);
    static StressPose Curl(string side)
    {
        float sign=side=="L"?1:-1;
        var joints=new[]{"Index","Middle","Ring","Pinky"}.SelectMany(f=>new[]{(1,65f),(2,80f),(3,55f)}
            .Select(j=>new StressJoint(f+j.Item1+"."+side,Vector3.UnitZ,j.Item2*sign,PoseAxisSpace.BoneBend))).ToArray();
        return new(side=="L"?"Full finger curl":"Right full finger curl","Hand."+side,Vector3.UnitZ,0,AdditionalJoints:joints);
    }
    static StressPose ThumbCurl(string side)
    {
        float sign=side=="L"?1:-1;
        return new(side=="L"?"Thumb curl and opposition":"Right thumb curl and opposition","Thumb1."+side,Vector3.UnitZ,30*sign,PoseAxisSpace.BoneNormal,
            [new("Thumb2."+side,Vector3.UnitY,45*sign,PoseAxisSpace.BoneBend),new("Thumb3."+side,Vector3.UnitY,40*sign,PoseAxisSpace.BoneBend)]);
    }
    static IEnumerable<StressJoint> Joints(StressPose pose)
    {
        yield return new(pose.Role,pose.Axis,pose.Degrees,pose.AxisSpace);
        if(pose.AdditionalJoints is not null)foreach(var joint in pose.AdditionalJoints)yield return joint;
    }
    public static bool IsApplicable(StressPose pose,IReadOnlySet<string> roles)
        =>pose.AdditionalJoints is null?roles.Contains(pose.Role):Joints(pose).Any(j=>j.Degrees!=0&&roles.Contains(j.Role));
    public static Vector3[][] Pose(ImportedCharacter character,GeneratedRig rig,StressPose pose)
        =>ApplyRotations(character,rig,JointRotations(rig,pose));
    public static IReadOnlyDictionary<string,Quaternion> JointRotations(GeneratedRig rig,StressPose pose)
    {
        var result=new Dictionary<string,Quaternion>();
        foreach(var specification in Joints(pose))
        {
            int joint=Array.FindIndex(rig.Bones,b=>b.Role==specification.Role);if(joint<0)continue;
            var rotation=JointRotation(rig,joint,specification);
            result[specification.Role]=result.TryGetValue(specification.Role,out var previous)?Quaternion.Normalize(previous*rotation):rotation;
        }
        return result;
    }
    static Quaternion JointRotation(GeneratedRig rig,int joint,StressJoint pose)
    {
        var axis=pose.Axis;
        if(pose.AxisSpace!=PoseAxisSpace.World)
        {
            string aim=rig.Profile.Bones.First(d=>d.Role==pose.Role).AimAxis;
            var local=(pose.AxisSpace,aim) switch
            {
                (PoseAxisSpace.BoneTwist,"Y")=>Vector3.UnitY,
                (PoseAxisSpace.BoneTwist,"Z")=>Vector3.UnitZ,
                (PoseAxisSpace.BoneTwist,_)=>Vector3.UnitX,
                (PoseAxisSpace.BoneBend,"Y")=>Vector3.UnitX,
                (PoseAxisSpace.BoneBend,_)=>Vector3.UnitY,
                (PoseAxisSpace.BoneNormal,"Z")=>Vector3.UnitX,
                _=>Vector3.UnitZ
            };
            axis=Vector3.Transform(local,rig.Bones[joint].Rotation);
        }
        return Quaternion.CreateFromAxisAngle(axis,pose.Degrees*MathF.PI/180);
    }
    /// <summary>Apply simultaneous joint rotations in the bind-space basis.
    /// Descendants inherit their parent's motion; source positions and weights stay immutable.</summary>
    public static Vector3[][] ApplyRotations(ImportedCharacter character,GeneratedRig rig,IReadOnlyDictionary<string,Quaternion> jointRotations)
    {
        var (positions,rotations)=BoneTransforms(rig,jointRotations);
        var result=character.Meshes.Select(m=>new Vector3[m.Vertices.Length]).ToArray();
        ApplyTransforms(character,rig,positions,rotations,result);
        return result;
    }
    /// <summary>Skin into existing buffers so live previews retain their meshes between frames.</summary>
    public static void ApplyTransforms(ImportedCharacter character,GeneratedRig rig,Vector3[] positions,Quaternion[] rotations,Vector3[][] destination)
    {
        for(int part=0;part<character.Meshes.Length;part++)
        {
            var source=character.Meshes[part].Vertices;
            for(int index=0;index<source.Length;index++)
            {
                var p=Vector3.Zero;var v=source[index];
                foreach(var influence in rig.Weights[part][index])p+=(positions[influence.Bone]+Vector3.Transform(v-rig.Bones[influence.Bone].Position,rotations[influence.Bone]))*influence.Weight;
                destination[part][index]=p;
            }
        }
    }
    public static (Vector3[] Positions,Quaternion[] Rotations) BoneTransforms(GeneratedRig rig,IReadOnlyDictionary<string,Quaternion> jointRotations)
    {
        var rotations=new Quaternion[rig.Bones.Length];var positions=new Vector3[rig.Bones.Length];
        for(int i=0;i<rig.Bones.Length;i++)
        {
            var b=rig.Bones[i];var inherited=b.Parent<0 ? Quaternion.Identity : rotations[b.Parent];
            positions[i]=b.Parent<0 ? b.Position : positions[b.Parent]+Vector3.Transform(b.Position-rig.Bones[b.Parent].Position,inherited);
            rotations[i]=jointRotations.TryGetValue(b.Role,out var rotation) ? Quaternion.Normalize(inherited*rotation) : inherited;
        }
        return (positions,rotations);
    }
}