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

/// <summary>Fit each leg to its own triangle sections. Stance width is measured,
/// rather than restricting ankles to a fixed strip below the hips.</summary>
internal static class LegFitting
{
    public static void Refine(ImportedCharacter character,Anatomy anatomy,float bodyHeight)
    {
        float bottom=anatomy["Root"].Y,h=bodyHeight,center=anatomy.SymmetryPlaneX;
        var volume=new SurfaceVisibility(character.Meshes.Where(m=>m.Kind==MeshKind.Body));
        PreserveSymmetricHipHeight(character,anatomy,h,volume);
        var sections=new List<MeshSections.Section>();
        for(int i=0;i<=126;i++)
        {
            float y=bottom+h*(.02537f+i*.005f);
            sections.AddRange(MeshSections.Cut(character,new(center,y,0),Vector3.UnitY,h*.45f,h*.00001f));
        }
        foreach(var (side,sign) in new[]{("L",1f),("R",-1f)})
        {
            var leg=sections.Where(s=>(s.Center.X-center)*sign>h*.015f&&(s.Center.X-center)*sign<h*.3f&&s.Radius<h*.095f).ToArray();
            if(leg.Length<8)continue;
            MeshSections.Section? At(float y,Vector3 prior,float tolerance=.008f)=>leg.Where(s=>Math.Abs(s.Center.Y-y)<h*tolerance)
                .MinBy(s=>Math.Abs(s.Center.Y-y)*4+Vector3.Distance(s.Center,prior));
            void Set(string role,Vector3 position)
            {
                var old=anatomy.Points[role+"."+side];
                if(!old.Corrected&&volume.Contains(position,h*.00001f))anatomy.Set(old.Role,position,old.Confidence);
            }
            var knee=anatomy["LowerLeg."+side];
            var kneeSection=At(knee.Y,knee);
            if(kneeSection is null)continue;
            // A constriction between thigh and calf is evidence for the joint,
            // but surface shape alone cannot identify its exact articulation.
            var candidates=leg.Where(s=>Math.Abs(s.Center.Y-knee.Y)<h*.065f&&Vector3.Distance(s.Center,kneeSection.Center)<h*.09f)
                .Where(s=>At(s.Center.Y-h*.025f,s.Center) is {} below&&At(s.Center.Y+h*.025f,s.Center) is {} above&&s.Area<below.Area*.92f&&s.Area<above.Area*.92f).ToArray();
            var narrow=candidates.MinBy(s=>s.Area/kneeSection.Area+MathF.Pow((s.Center.Y-knee.Y)/(h*.065f),2));
            float kneeY=narrow is null?knee.Y:(knee.Y+narrow.Center.Y)*.5f;
            var fit=At(kneeY,kneeSection.Center);
            if(fit is not null)
            {
                // A proportion-based seed can still be on the widening thigh.
                // Look across the local calf length for a substantial constriction;
                // tiny inner/accessory contours are not an articulation center.
                float reach=Vector3.Distance(fit.Center,anatomy["Foot."+side])*.18f;
                var missed=leg.Where(s=>Math.Abs(s.Center.Y-fit.Center.Y)<h*.065f&&s.Area>=fit.Area*.4f)
                    .Where(s=>At(s.Center.Y-reach,s.Center) is {} below&&At(s.Center.Y+reach,s.Center) is {} above&&s.Area<below.Area*.92f&&s.Area<above.Area*.92f&&
                        !volume.Blocked(below.Center,s.Center,h*.00001f)&&!volume.Blocked(s.Center,above.Center,h*.00001f))
                    .MinBy(s=>s.Area/fit.Area+MathF.Pow((s.Center.Y-fit.Center.Y)/(h*.13f),2));
                // Keep an already plausible knee. A shallow silhouette minimum
                // alone cannot justify moving an articulation.
                if(missed is not null&&fit.Area>missed.Area*1.2f)fit=missed;
                Set("LowerLeg",fit.Center);
            }
            var ankle=anatomy["Foot."+side];
            if(At(ankle.Y,kneeSection.Center) is {} ankleSection)Set("Foot",ankleSection.Center);
            if(FootFitting.Toe(character,anatomy,side,h) is {} toe)
            {
                Set("Toe",toe.Joint);
                if(Enumerable.Range(0,13).All(i=>volume.Contains(Vector3.Lerp(anatomy["Toe."+side],toe.End,i/12f),h*.00001f)))
                    anatomy.FootEnds[side]=toe.End;
            }
        }
        HipFitting.Refine(anatomy,sections,h,volume);
        HipFitting.RaiseLowHips(anatomy,sections,h,volume);
    }
    static void PreserveSymmetricHipHeight(ImportedCharacter character,Anatomy anatomy,float h,SurfaceVisibility volume)
    {
        var left=anatomy.Points["UpperLeg.L"];var right=anatomy.Points["UpperLeg.R"];
        if(left.Corrected||right.Corrected||Math.Abs(left.Position.Y-right.Position.Y)<h*.00001f)return;
        float height=anatomy["Root"].Y+h*BodyDetector.HipHeightFraction,center=anatomy.SymmetryPlaneX;
        // Restore the anatomical section only for small volume-grid displacements.
        // Clearance in different voxels is not evidence that one hip is higher.
        if(Math.Abs(left.Position.Y-height)>h*.025f||Math.Abs(right.Position.Y-height)>h*.025f)return;
        var l=left.Position;var r=right.Position;l.Y=r.Y=height;
        if(!volume.Contains(l,h*.00001f)||!volume.Contains(r,h*.00001f))return;
        var samples=character.Meshes.Where(m=>m.Kind==MeshKind.Body).SelectMany(m=>m.Vertices)
            .Where(p=>Math.Abs(p.Y-height)<h*.09f&&Math.Abs(p.X-center)<h*.16f).Distinct().ToArray();
        foreach(float sign in new[]{1f,-1f})
        {
            var side=samples.Where(p=>(p.X-center)*sign>h*.001f).ToArray();
            if(side.Length<12)return;
            // Compare reflected points with actual triangles, not vertex partners:
            // seams and unequal tessellation must not manufacture asymmetry.
            int supported=side.Count(p=>volume.NearSurface(new(2*center-p.X,p.Y,p.Z),h*.001f));
            if(supported<side.Length*.95f)return;
        }
        anatomy.Points[left.Role]=left with{Position=l};
        anatomy.Points[right.Role]=right with{Position=r};
    }
}