Editor/Core/Validation/PoseWeightFit.cs
#nullable enable annotations
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
using Quaternion=System.Numerics.Quaternion;

/// <summary>Fit local skin weights against signed surface volume in every stress pose.
/// Trials stay private until PoseWeightRepair validates the complete candidate.</summary>
internal static class PoseWeightFit
{
    sealed record Vertex(WeightSeams.Vertex[] Aliases,Vector3 Point,int[] Bones,double[] Initial,double[] Caps,bool Editable);
    sealed record Face(int A,int B,int C,Vector3 Normal,float Area,float AB,float BC,float CA);
    sealed record Sample(Vector3[] Bones,Quaternion[] Rotations,Vector3[][] Delta);
    sealed record Correction(double[][] S,double[][] Y,double Rho,double Scale);
    const double VolumeMargin=.05,StretchMargin=3.8;

    internal static Influence[][][] Solve(ImportedCharacter character,GeneratedRig rig,ValidationGeometry geometry,ValidationReport initial,WeightSeams seams,bool expandSupport,bool repairSupport,int iterations,bool prioritize=false)
    {

        // Keep the ordinary fit's safety margins until sparse-support repair is
        // necessary. In that fallback, actual volume/area violations take priority.
        double violationPenalty=prioritize?1000:0;
        var roles=rig.Bones.Select(b=>b.Role).ToHashSet();
        var specs=geometry.Poses.Where(p=>Deformation.IsApplicable(p,roles)).ToArray();
        var transforms=specs.Select(p=>geometry.Transforms(rig,p)).ToArray();
        var seeds=character.Meshes.Select(_=>new HashSet<int>()).ToArray();
        var support=character.Meshes.Select(_=>new Dictionary<int,Dictionary<int,float>>()).ToArray();
        var buffer=character.Meshes.Select(m=>new Vector3[m.Vertices.Length]).ToArray();
        for(int sample=0;sample<specs.Length;sample++)
        {
            var report=initial.StressTests[sample];
            if(report.ReversedTriangles==0&&report.MaximumStretch<=4&&report.MinimumAreaRatio>=.025)continue;
            var transform=transforms[sample];
            Deformation.ApplyTransforms(character,rig,transform.Positions,transform.Rotations,buffer);
            for(int p=0;p<character.Meshes.Length;p++)foreach(var f in geometry.Faces[p])
            {
                if(f.Area<=geometry.Height*geometry.Height*1e-10)continue;
                var normal=Vector3.Cross(buffer[p][f.B]-buffer[p][f.A],buffer[p][f.C]-buffer[p][f.A]);
                bool bad=SurfaceOrientation.Alignment(f.Normal,normal,rig.Weights[p][f.A],rig.Weights[p][f.B],rig.Weights[p][f.C],transform.Rotations)<SurfaceOrientation.ReversalLimit||normal.Length()/f.Area<.025;
                for(int edge=0;edge<3;edge++){var(a,b,length)=f.Edge(edge);if(length>geometry.Height*1e-6&&Vector3.Distance(buffer[p][a],buffer[p][b])/length>4)bad=true;}
                if(bad)
                {
                    foreach(int v in new[]{f.A,f.B,f.C})
                    {
                        seeds[p].Add(v);var point=character.Meshes[p].Vertices[v];var row=rig.Weights[p][v];
                        Vector3 Posed(int b)=>transform.Positions[b]+Vector3.Transform(point-rig.Bones[b].Position,transform.Rotations[b]);
                        var first=Posed(row[0].Bone);
                        if(row.Any(w=>Vector3.Distance(Posed(w.Bone),first)>geometry.Height*1e-5f))continue;
                        var alternative=geometry.Neighbors[p][v].SelectMany(n=>rig.Weights[p][n])
                            .Where(w=>!row.Any(old=>old.Bone==w.Bone)&&Vector3.Distance(Posed(w.Bone),first)>geometry.Height*1e-5f)
                            .GroupBy(w=>w.Bone).OrderByDescending(g=>g.Sum(w=>w.Weight)).ThenBy(g=>g.Key).FirstOrDefault();
                        if(alternative is null)continue;
                        if(!support[p].TryGetValue(v,out var desired))support[p][v]=desired=[];
                        desired[alternative.Key]=desired.GetValueOrDefault(alternative.Key)+alternative.Sum(w=>w.Weight);
                    }
                }
            }
        }
        if(seeds.All(s=>s.Count==0))return rig.Weights;
        var editable=seeds.Select(s=>s.ToHashSet()).ToArray();
        void CloseSeams()
        {
            var groups=editable.SelectMany((s,p)=>s.Select(v=>seams.Groups[p][v])).Distinct().ToArray();
            foreach(int group in groups)foreach(var v in seams.Vertices[group])editable[v.Part].Add(v.Index);
        }
        CloseSeams();
        for(int ring=0;ring<2;ring++)
        {
            editable=editable.Select((s,p)=>s.Concat(s.SelectMany(v=>geometry.Neighbors[p][v])).ToHashSet()).ToArray();
            CloseSeams();
        }
        if(repairSupport)
        {
            float radius=geometry.Height*.03f;
            var distance=character.Meshes.Select(m=>Enumerable.Repeat(float.PositiveInfinity,m.Vertices.Length).ToArray()).ToArray();
            var queue=new PriorityQueue<WeightSeams.Vertex,float>();
            void Visit(int p,int v,float value)
            {
                if(value>radius||value>=distance[p][v])return;
                distance[p][v]=value;queue.Enqueue(new(p,v),value);
            }
            for(int p=0;p<seeds.Length;p++)foreach(int v in seeds[p])Visit(p,v,0);
            while(queue.TryDequeue(out var v,out float value))
            {
                if(value>distance[v.Part][v.Index])continue;editable[v.Part].Add(v.Index);
                foreach(var alias in seams.Vertices[seams.Groups[v.Part][v.Index]])Visit(alias.Part,alias.Index,value);
                var point=character.Meshes[v.Part].Vertices[v.Index];
                foreach(int n in geometry.Neighbors[v.Part][v.Index])Visit(v.Part,n,value+Vector3.Distance(point,character.Meshes[v.Part].Vertices[n]));
            }
        }
        var selectedFaces=geometry.Faces.Select((part,p)=>part.Where(f=>editable[p].Contains(f.A)||editable[p].Contains(f.B)||editable[p].Contains(f.C)).ToArray()).ToArray();
        var indices=character.Meshes.Select(m=>Enumerable.Repeat(-1,m.Vertices.Length).ToArray()).ToArray();
        var vertices=new List<Vertex>();var faces=new List<Face>();
        var groupIndices=Enumerable.Repeat(-1,seams.Vertices.Length).ToArray();
        var ends=RigGeometry.SegmentEnds(rig);var locality=new SkinningLocality(character,rig,ends);
        for(int p=0;p<selectedFaces.Length;p++)
        {
            foreach(int v in selectedFaces[p].SelectMany(f=>new[]{f.A,f.B,f.C}).Distinct().Order())
            {
                int group=seams.Groups[p][v];
                if(groupIndices[group]>=0){indices[p][v]=groupIndices[group];continue;}
                var aliases=seams.Vertices[group];
                var weights=rig.Weights[p][v];var point=character.Meshes[p].Vertices[v];
                var desired=aliases.Where(a=>support[a.Part].ContainsKey(a.Index)).SelectMany(a=>support[a.Part][a.Index])
                    .GroupBy(x=>x.Key).OrderByDescending(g=>g.Sum(x=>x.Value)).ThenBy(g=>g.Key).FirstOrDefault();
                if(repairSupport&&desired is not null)
                {
                    int extra=desired.Key;
                    var proposed=weights.OrderByDescending(w=>w.Weight).Take(rig.Profile.MaximumInfluences-1).Append(new Influence(extra,.01f));
                    var replacement=Skinning.Cleanup(proposed,rig.Bones.Length,rig.Profile.MaximumInfluences);
                    if(Vector3.Distance(point,Geometry.ClosestOnSegment(point,rig.Bones[extra].Position,ends[extra]))<=locality.Limit(extra,point)&&aliases.All(a=>geometry.Allows(a.Part,a.Index,point,replacement)))weights=replacement;
                }
                if(expandSupport&&editable[p].Contains(v))
                {
                    var retained=weights.Select(w=>w.Bone).ToHashSet();
                    var extra=aliases.SelectMany(a=>geometry.Neighbors[a.Part][a.Index].SelectMany(n=>rig.Weights[a.Part][n])).Where(w=>!retained.Contains(w.Bone))
                        .GroupBy(w=>w.Bone).OrderByDescending(g=>g.Sum(w=>w.Weight)).ThenBy(g=>g.Key)
                        .Where(g=>Vector3.Distance(point,Geometry.ClosestOnSegment(point,rig.Bones[g.Key].Position,ends[g.Key]))<=locality.Limit(g.Key,point)&&aliases.All(a=>geometry.InfluenceAllowed?.Invoke(a.Part,a.Index,g.Key)!=false))
                        .Take(4).Select(g=>new Influence(g.Key,0));
                    weights=weights.Concat(extra).ToArray();
                }
                var caps=weights.Select(w=>Vector3.Distance(point,Geometry.ClosestOnSegment(point,rig.Bones[w.Bone].Position,ends[w.Bone]))>locality.Limit(w.Bone,point)?Math.Max(w.Weight,.05):1d).ToArray();
                if(geometry.InfluenceAllowed is not null)for(int i=0;i<weights.Length;i++)
                    if(aliases.Any(a=>!geometry.InfluenceAllowed(a.Part,a.Index,weights[i].Bone)))caps[i]=0;
                if(geometry.Trunk is {} trunk&&aliases.Any(a=>trunk.Vertices[a.Part][a.Index]))for(int i=0;i<weights.Length;i++)
                    if(trunk.Attachments.Any(a=>a.Support(point)==0&&a.Moving[weights[i].Bone]))caps[i]=weights[i].Weight;
                indices[p][v]=vertices.Count;groupIndices[group]=vertices.Count;
                vertices.Add(new(aliases,point,weights.Select(w=>w.Bone).ToArray(),weights.Select(w=>(double)w.Weight).ToArray(),caps,editable[p].Contains(v)&&weights.Length>1));
            }
            foreach(var f in selectedFaces[p])faces.Add(new(indices[p][f.A],indices[p][f.B],indices[p][f.C],f.Normal,f.Area,f.AB,f.BC,f.CA));
        }
        var activeBones=vertices.SelectMany(v=>v.Bones).Distinct().ToArray();
        var sampleList=new List<Sample>();
        foreach(var t in transforms)
        {
            if(sampleList.Any(s=>activeBones.All(b=>s.Bones[b]==t.Positions[b]&&s.Rotations[b]==t.Rotations[b])))continue;
            sampleList.Add(new(t.Positions,t.Rotations,vertices.Select(v=>v.Bones.Select(b=>t.Positions[b]+Vector3.Transform(v.Point-rig.Bones[b].Position,t.Rotations[b])-v.Point).ToArray()).ToArray()));
        }
        var samples=sampleList.ToArray();
        // A face normal carried by a bone does not depend on the weights being
        // fitted, yet every evaluation and gradient rotated it again for each
        // corner influence. Rotate once per pose; the same values then enter the
        // same arithmetic. Very large regions keep the direct path to bound memory.
        // Most samples turn a single joint. A face none of whose bones turn
        // rests at its bind shape, far inside every margin below, and adds
        // exactly nothing to the loss or its gradient. Visit only the others.
        var turned=samples.Select(sample=>
        {
            var moved=vertices.Select(v=>v.Bones.Any(b=>sample.Rotations[b]!=Quaternion.Identity||Vector3.Distance(sample.Bones[b],rig.Bones[b].Position)>geometry.Height*1e-6f)).ToArray();
            var active=Enumerable.Range(0,faces.Count).Where(f=>moved[faces[f].A]||moved[faces[f].B]||moved[faces[f].C]).ToArray();
            var used=active.SelectMany(f=>new[]{faces[f].A,faces[f].B,faces[f].C}).Distinct().Order().ToArray();
            return(Faces:active,Vertices:used);
        }).ToArray();
        var corners=new int[faces.Count*3+1];
        for(int f=0;f<faces.Count;f++)
        {
            corners[f*3+1]=corners[f*3]+vertices[faces[f].A].Bones.Length;
            corners[f*3+2]=corners[f*3+1]+vertices[faces[f].B].Bones.Length;
            corners[f*3+3]=corners[f*3+2]+vertices[faces[f].C].Bones.Length;
        }
        Vector3[][]? carried=(long)corners[^1]*samples.Length>8_000_000?null:samples.Select(sample=>
        {
            var normals=new Vector3[corners[^1]];
            for(int f=0;f<faces.Count;f++)
            {
                var face=faces[f];int at=corners[f*3];
                foreach(int v in new[]{face.A,face.B,face.C})foreach(int bone in vertices[v].Bones)normals[at++]=Vector3.Transform(face.Normal,sample.Rotations[bone]);
            }
            return normals;
        }).ToArray();
        var weightsNow=vertices.Select(v=>v.Initial.ToArray()).ToArray();var candidate=vertices.Select(v=>new double[v.Bones.Length]).ToArray();
        var gradient=vertices.Select(v=>new double[v.Bones.Length]).ToArray();var previousGradient=vertices.Select(v=>new double[v.Bones.Length]).ToArray();
        var previous=vertices.Select(v=>new double[v.Bones.Length]).ToArray();
        var points=new Vector3[vertices.Count];
        double step=0.001;var history=new List<Correction>();
        for(int iteration=0;iteration<iterations;iteration++)
        {
            double loss=Evaluate(weightsNow,gradient);
            double max=0,ss=0,sy=0;
            for(int v=0;v<vertices.Count;v++)
            {
                double mean=gradient[v].Average();
                for(int i=0;i<gradient[v].Length;i++)
                {
                    gradient[v][i]=vertices[v].Editable?gradient[v][i]-mean:0;
                    double s=weightsNow[v][i]-previous[v][i],y=gradient[v][i]-previousGradient[v][i];ss+=s*s;sy+=s*y;
                    max=Math.Max(max,Math.Abs(gradient[v][i]));
                }
            }
            if(loss<1e-9||max<1e-10)break;
            if(iteration>0&&sy>1e-12&&ss>1e-12)
            {
                var s=weightsNow.Select((w,v)=>w.Select((x,i)=>x-previous[v][i]).ToArray()).ToArray();
                var y=gradient.Select((g,v)=>g.Select((x,i)=>x-previousGradient[v][i]).ToArray()).ToArray();
                history.Add(new(s,y,1/sy,sy/Math.Max(Dot(y,y),1e-12)));if(history.Count>8)history.RemoveAt(0);
            }
            var direction=gradient.Select(g=>g.ToArray()).ToArray();
            var alpha=new double[history.Count];
            for(int h=history.Count-1;h>=0;h--){alpha[h]=history[h].Rho*Dot(history[h].S,direction);Add(direction,history[h].Y,-alpha[h]);}
            double scale=history.Count>0?history[^1].Scale:.01/Math.Max(max,1e-6);
            foreach(var row in direction)for(int i=0;i<row.Length;i++)row[i]*=scale;
            for(int h=0;h<history.Count;h++){double beta=history[h].Rho*Dot(history[h].Y,direction);Add(direction,history[h].S,alpha[h]-beta);}
            step=1;
            bool improved=false;
            for(int search=0;search<14;search++,step*=.5)
            {
                for(int v=0;v<vertices.Count;v++)
                {
                    for(int i=0;i<candidate[v].Length;i++)candidate[v][i]=weightsNow[v][i]-step*direction[v][i];
                    if(vertices[v].Editable)Project(candidate[v],vertices[v].Caps);
                }
                double next=Evaluate(candidate,null);
                if(next<loss-1e-12)
                {
                    for(int v=0;v<vertices.Count;v++)
                    {
                        weightsNow[v].CopyTo(previous[v],0);gradient[v].CopyTo(previousGradient[v],0);candidate[v].CopyTo(weightsNow[v],0);
                    }
                    improved=true;break;
                }
            }
            if(!improved)break;
        }
        var result=rig.Weights.Select(p=>(Influence[][])p.Clone()).ToArray();
        for(int v=0;v<vertices.Count;v++)if(vertices[v].Editable)
        {
            var item=vertices[v];
            var row=item.Bones.Select((b,i)=>new Influence(b,(float)weightsNow[v][i])).Where(w=>w.Weight>1e-8f).ToArray();
            var fitted=expandSupport?Skinning.Cleanup(row,rig.Bones.Length,rig.Profile.MaximumInfluences):row;
            foreach(var alias in item.Aliases)result[alias.Part][alias.Index]=fitted;
        }
        return result;

        double Evaluate(double[][] values,double[][]? g)
        {
            if(g is not null)foreach(var row in g)Array.Clear(row);
            double loss=0;
            for(int sampleIndex=0;sampleIndex<samples.Length;sampleIndex++)
            {
                var sample=samples[sampleIndex];var rotated=carried?[sampleIndex];
                foreach(int v in turned[sampleIndex].Vertices)
                {
                    var point=vertices[v].Point;for(int i=0;i<values[v].Length;i++)point+=sample.Delta[v][i]*(float)values[v][i];points[v]=point;
                }
                foreach(int faceIndex in turned[sampleIndex].Faces)
                {
                    var f=faces[faceIndex];
                    if(f.Area<=geometry.Height*geometry.Height*1e-10)continue;
                    var a=points[f.A];var b=points[f.B];var c=points[f.C];var n=Vector3.Cross(b-a,c-a);
                    var transported=Transport(f.A,0)+Transport(f.B,1)+Transport(f.C,2);
                    double denominator=f.Area*f.Area,volume=Vector3.Dot(n,transported)/denominator;
                    double deficit=VolumeMargin-volume;
                    double violation=Math.Max(0,.001-volume);
                    loss+=violationPenalty*.5*violation*violation;
                    if(deficit>0)
                    {
                        loss+=.5*deficit*deficit;
                        if(g is not null)
                        {
                            AddVolume(f.A,0,Vector3.Cross(b-c,transported));AddVolume(f.B,1,Vector3.Cross(c-a,transported));AddVolume(f.C,2,Vector3.Cross(a-b,transported));
                        }
                    }
                    Edge(f.A,f.B,f.AB);Edge(f.B,f.C,f.BC);Edge(f.C,f.A,f.CA);
                    Vector3 Carried(int v,int corner,int i)=>rotated is null?Vector3.Transform(f.Normal,sample.Rotations[vertices[v].Bones[i]]):rotated[corners[faceIndex*3+corner]+i];
                    Vector3 Transport(int v,int corner)
                    {
                        var normal=Vector3.Zero;
                        for(int i=0;i<values[v].Length;i++)normal+=Carried(v,corner,i)*((float)values[v][i]/3);
                        return normal;
                    }
                    void AddVolume(int v,int corner,Vector3 derivative)
                    {
                        if(!vertices[v].Editable)return;
                        for(int i=0;i<g![v].Length;i++)
                            g[v][i]-=(deficit+violationPenalty*violation)*(Vector3.Dot(derivative,sample.Delta[v][i])+Vector3.Dot(n,Carried(v,corner,i))/3)/denominator;
                    }
                    double areaDeficit=.03-n.Length()/f.Area;
                    if(areaDeficit>0)
                    {
                        loss+=violationPenalty*.5*areaDeficit*areaDeficit;
                        if(g is not null)
                        {
                            var direction=n/Math.Max(n.Length(),1e-12f);
                            AreaGradient(f.A,Vector3.Cross(b-c,direction));AreaGradient(f.B,Vector3.Cross(c-a,direction));AreaGradient(f.C,Vector3.Cross(a-b,direction));
                        }
                        void AreaGradient(int v,Vector3 derivative)
                        {
                            if(!vertices[v].Editable)return;
                            for(int i=0;i<g![v].Length;i++)g[v][i]-=violationPenalty*areaDeficit*Vector3.Dot(derivative,sample.Delta[v][i])/f.Area;
                        }
                    }
                    void Edge(int first,int second,float length)
                    {
                        if(length<=geometry.Height*1e-6)return;
                        var delta=points[first]-points[second];float posedLength=delta.Length();double excess=posedLength/length-StretchMargin;
                        if(excess<=0)return;loss+=.5*excess*excess;
                        if(g is null)return;
                        var direction=delta/(posedLength*length);
                        if(vertices[first].Editable)for(int i=0;i<g[first].Length;i++)g[first][i]+=excess*Vector3.Dot(direction,sample.Delta[first][i]);
                        if(vertices[second].Editable)for(int i=0;i<g[second].Length;i++)g[second][i]-=excess*Vector3.Dot(direction,sample.Delta[second][i]);
                    }
                }
            }
            return loss;
        }
    }
    static double Dot(double[][] a,double[][] b)
    {
        double result=0;for(int v=0;v<a.Length;v++)for(int i=0;i<a[v].Length;i++)result+=a[v][i]*b[v][i];return result;
    }
    static void Add(double[][] a,double[][] b,double scale)
    {
        for(int v=0;v<a.Length;v++)for(int i=0;i<a[v].Length;i++)a[v][i]+=scale*b[v][i];
    }
    static void Project(double[] values,double[] caps)
    {
        double low=values.Select((v,i)=>v-caps[i]).Min(),high=values.Max();
        for(int step=0;step<40;step++)
        {
            double lambda=(low+high)*.5,sum=0;for(int i=0;i<values.Length;i++)sum+=Math.Clamp(values[i]-lambda,0,caps[i]);
            if(sum>1)low=lambda;else high=lambda;
        }
        for(int i=0;i<values.Length;i++)values[i]=Math.Clamp(values[i]-(low+high)*.5,0,caps[i]);
    }
}