Code/Core/Mapping/HandDetectionProfiles.SplitArms.cs
#nullable enable
using Vec = System.Numerics.Vector3;
using Skel = HumanoidHandRetargeter.Skeleton.Skeleton;

namespace HumanoidHandRetargeter.Mapping;

internal static partial class HandDetectionProfiles
{
    // Split biceps/forearms, a wrist-to-palm link, and five palm-rooted rays.
    // Numeric/duplicate suffixes identify objects, not sides or anatomical fingers.
    private static void ResolveUnlabelledSplitArms(Skel rig,Hint?[] hints)
    {
        string Name(int i)
        {
            var name=rig[i].Name[(rig[i].Name.LastIndexOf(':')+1)..];
            return Key(name.Split('#')[0].Replace("_duplicate","",StringComparison.OrdinalIgnoreCase)).TrimEnd("0123456789".ToCharArray());
        }
        int Child(int i,string name)=>rig.ChildrenOf(i).Count==1&&Name(rig.ChildrenOf(i)[0])==name?rig.ChildrenOf(i)[0]:-1;
        var proposals=new List<(HandSide Side,Dictionary<int,string> Roles)>();
        foreach(var upper in rig.Bones.Where(b=>Name(b.Index) is "bisep" or "bicep" or "biceps"))
        {
            var upperSplit=Child(upper.Index,Name(upper.Index));if(upperSplit<0)continue;
            var lower=Child(upperSplit,"forearm");if(lower<0)continue;
            var lowerSplit=Child(lower,"forearm");if(lowerSplit<0)continue;
            var wrist=Child(lowerSplit,"wrist");if(wrist<0)continue;
            var palm=Child(wrist,"hand");if(palm<0||rig.ChildrenOf(palm).Count!=5)continue;
            var rays=new List<int[]>();
            foreach(var root in rig.ChildrenOf(palm))
            {
                var ray=new List<int>{root};var current=root;
                while(rig.ChildrenOf(current).Count==1){current=rig.ChildrenOf(current)[0];ray.Add(current);}
                if(rig.ChildrenOf(current).Count!=0){rays.Clear();break;}
                if(Name(ray[^1]).Contains("end",StringComparison.Ordinal))ray.RemoveAt(ray.Count-1);
                rays.Add(ray.ToArray());
            }
            if(rays.Count!=5)continue;
            var thumbs=rays.Where(r=>r.Length==3&&Name(r[0]) is "thum" or "thumb").ToArray();
            var fingers=rays.Where(r=>r.Length==4&&Name(r[0])=="finger").ToArray();
            if(thumbs.Length!=1||fingers.Length!=4)continue;
            var thumb=thumbs[0];
            Vec Pos(int i)=>rig.RestWorld[i].Pos;
            var ordered=fingers.OrderBy(r=>Vec.DistanceSquared(Pos(r[1]),Pos(thumb[1]))).ToArray();
            var radial=Pos(ordered[0][1])-Pos(ordered[3][1]);
            if(radial.LengthSquared()<1e-8f)continue;
            radial=Vec.Normalize(radial);
            // Require four distinctly ordered knuckles, not just a nearest-thumb guess.
            var spacing=ordered.Select(r=>Vec.Dot(Pos(r[1])-Pos(ordered[3][1]),radial)).ToArray();
            if(Enumerable.Range(0,3).Any(i=>spacing[i]-spacing[i+1]<spacing[0]*.1f))continue;
            var forward=ordered.Select(r=>Pos(r[3])-Pos(r[1])).Aggregate(Vec.Zero,(a,b)=>a+b);
            var normal=Vec.Cross(forward,radial);var thumbBend=Pos(thumb[2])-Pos(thumb[1]);
            if(normal.LengthSquared()<1e-8f||thumbBend.LengthSquared()<1e-8f)continue;
            var chirality=Vec.Dot(Vec.Normalize(normal),Vec.Normalize(thumbBend));
            // An out-of-plane thumb bend supplies the palmar direction. Flat or
            // ambiguous geometry remains for review rather than guessing a side.
            if(MathF.Abs(chirality)<.25f)continue;
            var side=chirality>0?HandSide.Left:HandSide.Right;
            var roles=new Dictionary<int,string>{{upper.Index,"UpperArm"},{lower,"LowerArm"},{wrist,"Hand"},{palm,"PalmMeta"}};
            var digitNames=new[]{"Index","Middle","Ring","Pinky"};
            for(var digit=0;digit<4;digit++)
                for(var joint=0;joint<4;joint++)roles[ordered[digit][joint]]=digitNames[digit]+(joint==0?"Meta":"Prox");
            foreach(var bone in thumb)roles[bone]="ThumbProx";
            if(roles.Keys.Any(i=>hints[i] is not null))continue;
            proposals.Add((side,roles));
        }
        // Require a complete, disjoint bilateral pair before using unnamed sides.
        if(proposals.Count!=2||proposals[0].Side==proposals[1].Side||proposals[0].Roles.Keys.Intersect(proposals[1].Roles.Keys).Any())return;
        foreach(var proposal in proposals)
            foreach(var role in proposal.Roles)hints[role.Key]=new(role.Value,proposal.Side,"split_arm_palm_rays");
    }
}