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

/// <summary>Measures where an arm leaves the torso. The armpit is the apex of the
/// open space below the arm in the frontal silhouette, so the result does not
/// depend on mesh connectivity, vertex density or closed cross sections.</summary>
internal sealed record ArmSocket(Vector3 Armpit,Vector3 Center,Vector3 Axis,float Radius,float Length)
{
    /// <summary>Wrist joint, when the rig knows it; the forearm need not follow the upper arm's line.</summary>
    internal Vector3? Wrist{get;init;}
    /// <summary>The upper arm's own radius along its shaft, when the rig has
    /// measured it. The armpit is the narrowest place an arm leaves the torso;
    /// the shoulder it hangs from is as broad as the arm itself.</summary>
    internal float Girth{get;init;}
    float Height{get;init;}
    /// <summary>How far from its centerline the arm past the shoulder still
    /// reaches, in units of its own radius. A hand is never thinner than a
    /// twentieth of the height across, whatever rod carries it.</summary>
    float Reach(Vector3 point)=>FromChain(point)/Math.Max(Radius,Height*.025f);
    /// <summary>Distance from the arm's own centerline, shoulder to elbow to a
    /// hand's length past the wrist. Far from that line a point is not the arm,
    /// however far along the axis it lies: for a hanging arm the legs do.</summary>
    float FromChain(Vector3 point)
    {
        var elbow=Center+Axis*Length;float upper=Vector3.Distance(point,Geometry.ClosestOnSegment(point,Center,elbow));
        var forearm=Wrist is {} wrist&&Vector3.DistanceSquared(wrist,elbow)>1e-8f?Vector3.Normalize(wrist-elbow):Axis;
        float reach=Wrist is {} known?Vector3.Distance(known,elbow)+Radius*4:Length;
        return Math.Min(upper,Vector3.Distance(point,Geometry.ClosestOnSegment(point,elbow,elbow+forearm*reach)));
    }
    // Frontal occupancy retained from the measurement.
    FrontSilhouette silhouette;
    /// <summary>The axis across the body: outward, level. A lowered arm's axis
    /// points at the floor, and the shoulder above its joint is not behind the arm.</summary>
    Vector3 Lateral
    {
        get
        {
            var flat=new Vector3(Axis.X,0,Axis.Z);
            if(flat.LengthSquared()>.04f)return Vector3.Normalize(flat);
            float outward=Math.Abs(Center.X-Armpit.X)>1e-6f?Center.X-Armpit.X:Axis.X;
            return new(outward<0?-1:1,0,0);
        }
    }
    /// <summary>How far a point lies out from the joint. The trunk begins medial
    /// of the joint, not behind it along the axis: the deltoid over a lowered
    /// arm's joint follows the arm, up to a radius and a half above it.</summary>
    internal float Forward(Vector3 point)
    {
        var offset=point-Center;
        return Math.Min(Vector3.Dot(offset,Lateral),Vector3.Dot(offset,Axis)+Radius*1.5f);
    }
    /// <summary>How much of a point belongs to the arm itself: distal of the
    /// cross section through the joint, and not separated from the arm's axis
    /// by open air. A flank below the armpit faces the arm across that gap,
    /// whereas a muscle or sleeve of any thickness never does.</summary>
    /// <param name="detached">The point lies on a shell of its own, away from the
    /// trunk. Past the upper arm such a shell is a glove, a gauntlet or a claw of
    /// any size, never the legs the arm's axis may point at, so its distance from
    /// the arm's centerline says nothing.</param>
    internal float Support(Vector3 point,bool detached=false)
    {
        var offset=point-Center;float along=Vector3.Dot(offset,Axis);
        // A bent forearm leaves the line of the upper arm. Everything past
        // the upper arm, and near the arm itself, is arm; only the socket
        // region needs separating.
        float beyond=Smooth((along/Length-.7f)/.3f);
        if(beyond>0&&!detached)beyond*=1-Smooth((Reach(point)-1.6f)/.7f);
        if(beyond>=1)return 1;
        float radial=(offset-Axis*along).Length();
        // The trunk gives way to the arm over the breadth of the shoulder, a
        // radius and a fifth wide, measured from the arm's own girth where a
        // bodybuilder's deltoid dwarfs his armpit.
        float breadth=Math.Max(Radius,Girth);
        float support=Smooth((Forward(point)/breadth+.6f)/1.2f)*(1-Smooth((radial/Radius-1.6f)/.7f));
        support+=(1-support)*beyond;
        if(support<=0)return 0;
        // The open space below the arm is a wedge with its apex at the armpit,
        // between the torso side and the arm's underside. Past that apex, the
        // torso's side of the bisector is flank however close the arm is.
        var lateral=new Vector2(Axis.X,Axis.Y);
        if(lateral.LengthSquared()>1e-6f)
        {
            var bisector=Vector2.Normalize(lateral)-Vector2.UnitY;
            if(bisector.LengthSquared()>1e-6f)
            {
                bisector=Vector2.Normalize(bisector);
                // A hanging arm has almost no sideways axis; its centerline
                // still lies outward of the armpit.
                float outward=Math.Abs(Axis.X)>.2f?Axis.X:Center.X-Armpit.X;
                var torso=new Vector2(bisector.Y,-bisector.X);if(torso.X*outward>0)torso=-torso;
                var fromApex=new Vector2(point.X-Armpit.X,point.Y-Armpit.Y);
                // Surface well inside the torso's side counts as past the apex
                // even when it is level with it, as on the back below a shoulder.
                float inward=Vector2.Dot(fromApex,torso),past=Vector2.Dot(fromApex,bisector)+.5f*Math.Max(0,inward-Radius*.3f);
                float flank=Smooth(past/(Radius*.3f))*Smooth((inward/Radius+.15f)/.3f);
                support*=1-flank*(1-beyond);
                if(support<=0)return 0;
            }
        }
        var target=Center+Axis*Math.Max(along,0);
        float open=silhouette.OpenLength(new(point.X,point.Y),new(target.X,target.Y));
        float separated=Smooth((open/Radius-.1f)/.3f)*(1-beyond);
        return support*(1-separated);
    }
    /// <summary>How far a point lies out along the arm itself. The trunk carries
    /// the socket, not the limb: a spine weight lingering down the arm blends
    /// every vertex there, and blended vertices lose volume when it turns.</summary>
    internal float Beyond(Vector3 point)
    {
        // Only within the arm's own thickness. A hanging arm points at the
        // floor, so the hips lie far along its axis without being any part of it.
        float along=Vector3.Dot(point-Center,Axis);
        return Support(point)*Smooth((along/Radius-.5f)/1.5f)*(1-Smooth((Reach(point)-1.6f)/.7f));
    }
    /// <summary>The shoulder girdle carries the socket: neither the arm beyond
    /// it nor the flank below the armpit.</summary>
    /// <remarks>The girdle ends within half a radius of the socket, and on a
    /// thick arm no farther than an eighth of the upper arm: a thick arm is not
    /// a longer shoulder.</remarks>
    internal float Girdle(Vector3 point)
    {
        float along=Vector3.Dot(point-Center,Axis);
        float reach=Math.Min(1-Smooth((along/Radius-.5f)/1.5f),1-Smooth((along/Length-.12f)/.23f));
        return reach*Smooth((point.Y-Armpit.Y)/Radius+.25f);
    }
    static float Smooth(float t){t=Math.Clamp(t,0,1);return t*t*(3-2*t);}

    /// <summary>Shoulders are level. A pouch, a holster or a hand on the hip fills
    /// the space under one lowered arm, and the silhouette then offers a corner
    /// well below the armpit: the lower of two sockets rises to the other.</summary>
    internal static (ArmSocket? Left,ArmSocket? Right) Level(ArmSocket? left,ArmSocket? right,Vector3 leftElbow,Vector3 rightElbow,float height)
    {
        if(left is null||right is null)return(left,right);
        float gap=left.Center.Y-right.Center.Y;
        if(Math.Abs(gap)<=height*.03f)return(left,right);
        return gap>0?(left,right.Raise(gap,rightElbow)):(left.Raise(-gap,leftElbow),right);
    }
    ArmSocket Raise(float by,Vector3 elbow)
    {
        var center=Center+Vector3.UnitY*by;var direction=elbow-center;
        if(direction.LengthSquared()<1e-8f)return this;
        return this with{Armpit=Armpit+Vector3.UnitY*by,Center=center,Axis=Vector3.Normalize(direction),Length=direction.Length()};
    }

    internal static ArmSocket? Measure(IEnumerable<MeshPart> meshes,Vector3 shoulder,Vector3 elbow,float centerX,float height)
    {
        float sign=Math.Sign(shoulder.X-centerX);if(sign==0||height<=0)return null;
        float cell=height/360,reach=Math.Abs(elbow.X-centerX);
        float left=Math.Min(centerX,centerX+sign*(reach+height*.08f)),bottom=shoulder.Y-height*.3f;
        var window=new FrontSilhouette(meshes,left,bottom,reach+height*.08f,height*.45f,cell);int nx=window.Columns;
        bool Filled(int x,int y)=>window.Filled(x,y);
        int medial=sign>0?-1:1,limit=(int)(height*.3f/cell);
        // Open cells enclosed by the torso on the medial side and by the arm
        // above. Their corner is the armpit for raised and lowered arms alike.
        var regions=window.Regions((x,y)=>
        {
            if(Filled(x,y))return false;
            float lateral=sign*(left+(x+.5f)*cell-centerX);if(lateral<height*.03f||lateral>reach)return false;
            int inward=1;while(inward<=limit&&!Filled(x+medial*inward,y)&&sign*(left+(x+medial*inward+.5f)*cell-centerX)>0)inward++;
            if(!Filled(x+medial*inward,y))return false;
            int upward=1;while(upward<=limit&&y+upward<window.Rows&&!Filled(x,y+upward))upward++;
            return Filled(x,y+upward);
        }).Where(region=>region.Count>=40).ToList();
        // The space opens between the torso side, which runs downward, and the
        // underside of the arm. Its apex lies farthest against their bisector.
        var along=new Vector2(elbow.X-shoulder.X,elbow.Y-shoulder.Y);if(along.LengthSquared()<1e-8f)return null;
        var opening=Vector2.Normalize(along)-Vector2.UnitY;if(opening.LengthSquared()<1e-4f)return null;opening=Vector2.Normalize(opening);
        int Apex(List<int> region)=>region.MinBy(i=>(i%nx)*opening.X+(i/nx)*opening.Y);
        // An armpit lies below the shoulder. The corner beside the neck under a
        // hat brim or a hood does not.
        regions.RemoveAll(region=>bottom+(Apex(region)/nx+1)*cell>shoulder.Y+height*.02f);
        if(regions.Count==0)return null;
        var owner=new int[nx*window.Rows];Array.Fill(owner,-1);
        for(int r=0;r<regions.Count;r++)foreach(int i in regions[r])owner[i]=r;
        var gap=regions.MaxBy(region=>region.Count)!;
        // A pouch, a holster or a hand on the hip splits the space below a
        // lowered arm, and the larger part may lie beneath it. The armpit
        // tops the whole stack: climb through whatever sits over the space.
        for(int hops=0;hops<4;hops++)
        {
            var top=new Dictionary<int,int>();
            foreach(int i in gap){int x=i%nx,y=i/nx;top[x]=Math.Max(y,top.GetValueOrDefault(x,-1));}
            var votes=new Dictionary<int,int>();
            foreach(var (x,y) in top)
            {
                int up=y+1;while(up<window.Rows&&Filled(x,up))up++;
                if(up>=window.Rows||up-y>limit)continue;
                int above=owner[x+nx*up];
                if(above>=0&&regions[above]!=gap)votes[above]=votes.GetValueOrDefault(above)+1;
            }
            if(votes.Count==0)break;
            gap=regions[votes.MaxBy(vote=>vote.Value).Key];
        }
        int apex=Apex(gap);
        var armpit=new Vector2(left+(apex%nx+.5f+medial*.5f)*cell,bottom+(apex/nx+1)*cell);
        if(sign*(armpit.X-centerX)<height*.04f||sign*(armpit.X-centerX)>height*.22f)return null;
        var center=armpit;var axis=Vector2.Zero;float radius=0;
        var target=new Vector2(elbow.X,elbow.Y);
        bool Inside(Vector2 p)=>window.Inside(p);
        for(int pass=0;pass<3;pass++)
        {
            axis=target-(pass==0?new Vector2(shoulder.X,shoulder.Y):center);
            if(axis.LengthSquared()<1e-8f)return null;axis=Vector2.Normalize(axis);
            // Across the arm, away from the torso: upward for a raised arm and
            // outward for one hanging beside the body.
            var normal=new Vector2(-axis.Y,axis.X);if(Vector2.Dot(normal,new(sign,1))<0)normal=-normal;
            int steps=0,open=0,last=0;
            while(open<3&&steps<limit)
            {
                steps++;
                if(Inside(armpit+normal*(steps*cell))){open=0;last=steps;}else open++;
            }
            radius=last*cell*.5f;
            if(radius<height*.0125f||radius>height*.1f)return null;
            // The joint sits on the arm's centerline above the armpit. A lowered
            // arm meets that line higher up, but never closer than its own
            // radius to the top of the shoulder.
            center=armpit+normal*radius;
            while(sign*(center.X-armpit.X)>0&&axis.Y<-.05f)
            {
                var next=center-axis*cell;
                if(!Inside(next)||!Inside(next+Vector2.UnitY*radius*.9f))break;
                // The seed axis only points the walk. The joint keeps a radius
                // inside the arm's own outer edge, up the slant of an upper arm
                // the estimate never saw, a cell at a time.
                int outer=0;while(outer<limit&&Inside(next+normal*((outer+1)*cell)))outer++;
                if(outer<limit)next+=normal*Math.Clamp(outer*cell-radius,-cell,cell);
                if(!Inside(next)||!Inside(next+Vector2.UnitY*radius*.9f))break;
                center=next;
            }
        }
        float depth=window.Depth(center)??shoulder.Z;
        var direction=elbow-new Vector3(center,depth);if(direction.LengthSquared()<1e-8f)return null;
        return new(new(armpit,depth),new(center,depth),Vector3.Normalize(direction),radius,direction.Length()){silhouette=window,Height=height};
    }
}