Editor/Core/Rigging/SkinningLocality.cs
namespace HumanoidRigger.EditorTools.Core.Rigging;
using Vector3=System.Numerics.Vector3;

/// <summary>An enlarged skull can be wider than the ordinary whole-body
/// distance limit. Its measured envelope applies only above the neck.
/// A very broad torso likewise outgrows that limit: its flanks still belong
/// to the spine, however far they are from it.</summary>
internal sealed class SkinningLocality
{
    readonly float ordinary,headRadius,headMinimumY;
    readonly int head=-1;
    readonly bool[] axial=[];
    readonly float trunkRadius,trunkBottom,trunkTop,trunkHalfWidth,trunkCenter;
    public SkinningLocality(ImportedCharacter character,GeneratedRig rig,Vector3[] ends)
    {
        ordinary=character.AnatomicalHeight*.25f;headRadius=ordinary;trunkRadius=ordinary;
        MeasureTrunk(character,rig,ends,out axial,out trunkRadius,out trunkBottom,out trunkTop,out trunkHalfWidth,out trunkCenter);
        if(rig.Anatomy?.HeadEnd is null||!rig.Anatomy.Points.TryGetValue("Neck",out var neck))return;
        head=Array.FindIndex(rig.Bones,b=>b.Role=="Head");if(head<0)return;
        headMinimumY=neck.Position.Y-character.AnatomicalHeight*.02f;
        foreach(var p in character.Meshes.Where(m=>m.Kind==MeshKind.Body).SelectMany(m=>m.Vertices).Where(p=>p.Y>=headMinimumY))
            headRadius=Math.Max(headRadius,Vector3.Distance(p,Geometry.ClosestOnSegment(p,rig.Bones[head].Position,ends[head]))*1.1f);
        headRadius=Math.Min(headRadius,character.AnatomicalHeight*.5f);
    }
    void MeasureTrunk(ImportedCharacter character,GeneratedRig rig,Vector3[] ends,out bool[] axial,out float radius,out float bottom,out float top,out float halfWidth,out float center)
    {
        axial=rig.Bones.Select(b=>b.Deform&&(b.Role is "Pelvis" or "SpineLower" or "SpineMid" or "SpineUpper" or "Chest")).ToArray();
        radius=ordinary;bottom=top=halfWidth=center=0;
        RigBone? Bone(string role)=>rig.Bones.FirstOrDefault(b=>b.Role==role);
        if(Bone("Pelvis") is not {} pelvis||Bone("Neck") is not {} neck||Bone("UpperArm.L") is not {} left||Bone("UpperArm.R") is not {} right){axial=[];return;}
        center=pelvis.Position.X;top=neck.Position.Y;
        bottom=Math.Min(pelvis.Position.Y,Math.Min(Bone("UpperLeg.L")?.Position.Y??pelvis.Position.Y,Bone("UpperLeg.R")?.Position.Y??pelvis.Position.Y));
        // Between the shoulder joints, with room for the muscle around them.
        halfWidth=Math.Max(Math.Abs(left.Position.X-center),Math.Abs(right.Position.X-center))*1.25f;
        var members=axial;var chain=Enumerable.Range(0,rig.Bones.Length).Where(b=>members[b]).ToArray();
        if(chain.Length==0||top<=bottom||halfWidth<=0){axial=[];return;}
        foreach(var part in character.Meshes)if(part.Kind==MeshKind.Body)foreach(var p in part.Vertices)
        {
            if(p.Y<bottom||p.Y>top||Math.Abs(p.X-center)>halfWidth)continue;
            float nearest=float.PositiveInfinity;
            foreach(int b in chain)nearest=Math.Min(nearest,Vector3.DistanceSquared(p,Geometry.ClosestOnSegment(p,rig.Bones[b].Position,ends[b])));
            radius=Math.Max(radius,MathF.Sqrt(nearest)*1.1f);
        }
        radius=Math.Min(radius,character.AnatomicalHeight*.5f);
    }
    public float Limit(int bone,Vector3 point)=>bone==head&&point.Y>=headMinimumY?headRadius:
        bone<axial.Length&&axial[bone]&&point.Y>=trunkBottom&&point.Y<=trunkTop&&Math.Abs(point.X-trunkCenter)<=trunkHalfWidth?trunkRadius:ordinary;
}