Editor/Core/Profiles/CustomProfile.cs
using System.Numerics;
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;

public sealed record BoneMapping(string Role,int Bone,float Confidence);
public static class CustomProfile
{
    /// <summary>Matches geometry first, then uses name and hierarchical parent agreement to disambiguate.
    /// Returns uncertain assignments for the profile editor rather than accepting a misleading name.</summary>
    public static BoneMapping[] Recognize(ImportedCharacter character,Anatomy anatomy)
    {
        if(character.ExistingBones.Length==0)throw new InvalidOperationException("The selected character has no skeleton.");
        var result=new List<BoneMapping>();var used=new HashSet<int>();var height=anatomy.Height;
        foreach(var definition in Profiles.CanonicalBones())
        {
            if(!anatomy.Points.TryGetValue(definition.Role,out var point))continue;
            var parent=result.FirstOrDefault(m=>m.Role==definition.Parent);
            var candidates=character.ExistingBones.Select((b,i)=>
            {
                float distance=Vector3.Distance(b.Position,point.Position)/height;
                float namePrior=b.Name.Replace("_","").Replace(".","").Contains(definition.Role.Replace(".",""),StringComparison.OrdinalIgnoreCase)?.012f:0;
                float parentPenalty=parent is null||IsAncestor(character.ExistingBones,parent.Bone,i)?.0f:.04f;
                return (Index:i,Score:distance-namePrior+parentPenalty,Distance:distance);
            }).Where(c=>!used.Contains(c.Index)).OrderBy(c=>c.Score).ToArray();
            if(candidates.Length==0)continue;
            var best=candidates[0];if(best.Distance>.16f)continue;
            float separation=candidates.Length>1 ? candidates[1].Score-best.Score : .1f;
            float confidence=Math.Clamp(1-best.Distance*5,0,1)*Math.Clamp(separation/.035f,.35f,1);
            used.Add(best.Index);result.Add(new(definition.Role,best.Index,confidence));
        }
        return result.ToArray();
    }
    static bool IsAncestor(SourceBone[] bones,int ancestor,int child)
    {
        var visited=new HashSet<int>();
        for(int p=bones[child].Parent;p>=0;p=bones[p].Parent)
        {
            if(p>=bones.Length||!visited.Add(p))return false;
            if(p==ancestor)return true;
        }
        return false;
    }
    public static RigProfile Create(string name,ImportedCharacter source,IEnumerable<BoneMapping> mappings)
    {
        var mapped=mappings.ToArray();
        if(mapped.Any(m=>m.Bone<0||m.Bone>=source.ExistingBones.Length)||mapped.Select(m=>m.Bone).Distinct().Count()!=mapped.Length||mapped.Select(m=>m.Role).Distinct().Count()!=mapped.Length)throw new FormatException("A bone or semantic role is mapped more than once.");
        var definitions=Profiles.CanonicalBones().ToDictionary(b=>b.Role);
        foreach(var required in definitions.Values.Where(b=>b.Required))if(!mapped.Any(m=>m.Role==required.Role))throw new FormatException($"Map the required {required.Role} joint.");
        var byIndex=mapped.ToDictionary(m=>m.Bone);var pending=mapped.ToList();var result=new List<BoneDefinition>();
        while(pending.Count>0)
        {
            int before=pending.Count;
            foreach(var m in pending.ToArray())
            {
                int parent=source.ExistingBones[m.Bone].Parent;var visited=new HashSet<int>();
                while(parent>=0&&!byIndex.ContainsKey(parent))
                {
                    if(parent>=source.ExistingBones.Length||!visited.Add(parent))throw new FormatException("Invalid source hierarchy.");
                    parent=source.ExistingBones[parent].Parent;
                }
                var parentRole=parent<0?null:byIndex[parent].Role;
                if(parentRole is not null&&!result.Any(b=>b.Role==parentRole))continue;
                result.Add(new(m.Role,source.ExistingBones[m.Bone].Name,parentRole,definitions.GetValueOrDefault(m.Role)?.Required??false,m.Role!="Root"));pending.Remove(m);
            }
            if(pending.Count==before)throw new FormatException("Cyclic skeleton mappings.");
        }
        var profile=new RigProfile{Id="custom_"+Guid.NewGuid().ToString("N"),Name=name,Bones=result.ToArray(),Metadata=new(){{"source",source.Name},{"kind","custom"}}};profile.Validate();return profile;
    }
}