Editor/Core/Validation/TorsoWeightRepair.cs
namespace HumanoidRigger;
using Vector3 = System.Numerics.Vector3;

/// <summary>Keeps limb motion from pulling the central spine surface sideways.</summary>
public static class TorsoWeightRepair
{
    /// <summary>Keep the lumbar correction only when all existing stress poses remain safe.</summary>
    public static ValidationReport Improve(ImportedCharacter character, GeneratedRig rig, ValidationReport initial)
    {
        var roles = rig.Bones.Select(b => b.Role).ToHashSet();
        var expected = Deformation.Poses.Where(p => Deformation.IsApplicable(p, roles))
            .Select(p => p.Name).Order().ToArray();
        if (!WeightRepair.HasCompleteEvidence(initial, expected)) return initial;
        var original = rig.Weights;
        rig.Weights = original.Select(p => (Influence[][])p.Clone()).ToArray();
        bool accepted = false;
        try
        {
            int changed = Apply(character, rig);
            if (changed == 0) return initial;
            // Redistributing torso weights can expose a fold at the hip boundary.
            // Run the same bounded surface repair used by initial rig generation.
            var candidate = RigValidator.ValidateAndRepair(character, rig);
            if (!candidate.Passed || !WeightRepair.HasCompleteEvidence(candidate, expected) ||
                !candidate.StressTests.Select(t => t.Pose).SequenceEqual(initial.StressTests.Select(t => t.Pose)) ||
                candidate.StressTests.Zip(initial.StressTests).Any(p =>
                    p.First.ReversedTriangles > p.Second.ReversedTriangles ||
                    p.First.ReversedAreaFraction > p.Second.ReversedAreaFraction + 1e-7f)) return initial;
            candidate.Repairs += initial.Repairs + changed;
            candidate.RepairPasses = initial.RepairPasses + 1;
            accepted = true;
            return candidate;
        }
        finally { if (!accepted) rig.Weights = original; }
    }

    public static int Apply(ImportedCharacter character, GeneratedRig rig)
    {
        int Find(string role) => Array.FindIndex(rig.Bones, b => b.Role == role);
        int pelvis = Find("Pelvis"), spine = Find("SpineLower"), left = Find("UpperLeg.L"), right = Find("UpperLeg.R");
        if (pelvis < 0 || spine < 0 || left < 0 || right < 0) return 0;
        var hips = (rig.Bones[left].Position + rig.Bones[right].Position) * .5f;
        float halfWidth = Math.Abs(rig.Bones[left].Position.X - rig.Bones[right].Position.X) * .5f;
        float rise = rig.Bones[spine].Position.Y - hips.Y;
        if (halfWidth < .001f || rise < .001f) return 0;
        var legs = new bool[rig.Bones.Length];
        var arms = new bool[rig.Bones.Length];
        var torso = new bool[rig.Bones.Length];
        int shoulderLeft = Find("UpperArm.L"), shoulderRight = Find("UpperArm.R");
        int chest = Find("Chest"), neck = Find("Neck");
        float shoulderWidth = shoulderLeft < 0 || shoulderRight < 0 ? 0
            : Math.Abs(rig.Bones[shoulderLeft].Position.X - rig.Bones[shoulderRight].Position.X) * .5f;
        bool constrainShoulders = shoulderWidth > .001f && chest >= 0 && neck >= 0;
        for (int b = 0; b < rig.Bones.Length; b++)
        {
            legs[b] = b == left || b == right || rig.Bones[b].Parent >= 0 && legs[rig.Bones[b].Parent];
            arms[b] = rig.Bones[b].Role is "Clavicle.L" or "Clavicle.R" || rig.Bones[b].Parent >= 0 && arms[rig.Bones[b].Parent];
            torso[b] = rig.Bones[b].Role is "Pelvis" or "SpineLower" or "SpineMid" or "Chest" or "Neck";
        }
        static float Smooth(float x) { x = Math.Clamp(x, 0, 1); return x * x * (3 - 2 * x); }
        int changed = 0;
        for (int part = 0; part < character.Meshes.Length; part++)
        {
            var mesh = character.Meshes[part];
            if (mesh.Kind != MeshKind.Body) continue;
            for (int v = 0; v < mesh.Vertices.Length; v++)
            {
                var point = mesh.Vertices[v];
                float central = 1 - Smooth((Math.Abs(point.X - hips.X) - halfWidth) / halfWidth);
                float maximum = 1 - central * Smooth((point.Y - hips.Y) / rise);
                var source = rig.Weights[part][v];
                var adjusted = Limit(source, legs, maximum, pelvis);
                if (constrainShoulders && point.Y >= rig.Bones[spine].Position.Y && point.Y <= rig.Bones[neck].Position.Y)
                {
                    // The shoulder blades may follow the clavicles; the middle of the
                    // spine must follow the axial skeleton. Fade toward the shoulders
                    // so their attachment remains free to articulate.
                    float centerX = rig.Bones[chest].Position.X;
                    float shoulderLimit = Smooth((Math.Abs(point.X - centerX) / shoulderWidth - .1f) / .6f);
                    adjusted = Limit(adjusted, arms, shoulderLimit, chest);
                }
                if (ReferenceEquals(source, adjusted)) continue;
                rig.Weights[part][v] = adjusted;
                changed++;
            }
        }
        return changed;

        Influence[] Limit(Influence[] source, bool[] limb, float maximum, int fallback)
        {
            float limbTotal = 0, torsoTotal = 0;
            foreach (var w in source)
            {
                if (limb[w.Bone]) limbTotal += w.Weight;
                if (torso[w.Bone]) torsoTotal += w.Weight;
            }
            if (limbTotal <= maximum + .000001f) return source;
            float removed = limbTotal - maximum;
            var adjusted = new Influence[source.Length + (torsoTotal > .000001f ? 0 : 1)];
            for (int i = 0; i < source.Length; i++)
            {
                var w = source[i];
                float factor = limb[w.Bone] ? maximum / limbTotal
                    : torso[w.Bone] && torsoTotal > .000001f ? 1 + removed / torsoTotal : 1;
                adjusted[i] = w with { Weight = w.Weight * factor };
            }
            if (torsoTotal <= .000001f) adjusted[^1] = new(fallback, removed);
            return Skinning.Cleanup(adjusted, rig.Bones.Length, rig.Profile.MaximumInfluences);
        }
    }
}