tests/test_body_clearance.cs
using System.Numerics;
using HumanoidMocapX.Core.Cleanup;
using HumanoidMocapX.Core.Mapping;
using HumanoidMocapX.Core.Maths;
using HumanoidMocapX.Core.Motion;
using HumanoidMocapX.Core.Skeleton;
using HumanoidMocapX.Core.Target;

static class BodyClearanceTests
{
    internal static void Run()
    {
        foreach (var name in new[] { "target_rig_sbox.json", "target_rig_sbox_citizen.json" })
        {
            var rig = TargetRig.Load(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, name)));
            HipContact(rig, true);
            HipContact(rig, false);
            TemporalContact(rig, true);
            TemporalContact(rig, false);
            foreach (var scale in new[] { .01f, 10f })
            {
                var scaled = Scale(rig, scale);
                HipContact(scaled, true);
                HipContact(scaled, false);
            }
        }
        Unreachable();
        Console.WriteLine("PASS: Human/Citizen hip clearance, contact entry/exit, scales, residuals, arm lengths, wrist attitude and finger locals");
    }

    static void HipContact(TargetRig target, bool left)
    {
        var rig = target.Skeleton;
        var frame = rig.Bones.Select(b => b.RestLocal).ToArray();
        var hips = target.BoneForRole(BoneRole.Hips)!.Value;
        var neck = target.BoneForRole(BoneRole.Neck)!.Value;
        var upper = target.BoneForRole(left ? BoneRole.UpperArmL : BoneRole.UpperArmR)!.Value;
        var lower = target.BoneForRole(left ? BoneRole.LowerArmL : BoneRole.LowerArmR)!.Value;
        var hand = target.BoneForRole(left ? BoneRole.HandL : BoneRole.HandR)!.Value;
        var leftShoulder = target.BoneForRole(BoneRole.UpperArmL)!.Value;
        var rightShoulder = target.BoneForRole(BoneRole.UpperArmR)!.Value;
        var world = new Pose(frame).ToWorld(rig);
        var up = Vector3.Normalize(world[neck].Pos - world[hips].Pos);
        var height = Vector3.Distance(world[neck].Pos, world[hips].Pos);
        var shoulderLine = world[leftShoulder].Pos - world[rightShoulder].Pos;
        var halfSpan = shoulderLine.Length() * .5f;
        var across = Vector3.Normalize(shoulderLine - up * Vector3.Dot(shoulderLine, up));
        var forward = Vector3.Cross(across, up);
        var side = left ? 1 : -1;
        var reach = Vector3.Distance(world[upper].Pos, world[lower].Pos) + Vector3.Distance(world[lower].Pos, world[hand].Pos);
        var contactHeight = Math.Max(height * .02f, Vector3.Dot(world[upper].Pos - world[hips].Pos, up) - reach * .9f);
        var goal = world[hips].Pos + up * contactHeight + across * (side * halfSpan * .6f);
        MotionTests.Check(Vector3.Distance(world[upper].Pos, goal) < reach, target.Name + " hip fixture must be reachable");
        var frames = new List<XForm[]> { frame };
        EffectorIk.ApplyGoals(frames, rig, new LimbChain { Upper = upper, Lower = lower, End = hand }, new[] { goal },
            Vector3.Cross(goal - world[upper].Pos, across * side), soften: 0);
        var before = (XForm[])frame.Clone();
        var beforeWorld = new Pose(before).ToWorld(rig);
        var mapping = new MappingResult("body-clearance-fixture", default);
        foreach (var role in Enum.GetValues<BoneRole>())
            if (target.BoneForRole(role) is int bone) mapping.RoleToBone[role] = bone;
        var result = CaptureArmClearance.Apply(frames, new[] { before }, rig, mapping, target, 30);
        var after = new Pose(frame).ToWorld(rig);
        ClearSegments(target, after, left);
        var relative = after[hand].Pos - after[hips].Pos;
        var x = Vector3.Dot(relative, across) / (halfSpan * (CaptureArmClearance.HalfWidth + CaptureArmClearance.ArmRadius));
        var z = Vector3.Dot(relative, forward) / (halfSpan * (CaptureArmClearance.HalfDepth + CaptureArmClearance.ArmRadius));
        var clearance = MathF.Sqrt(x * x + z * z);
        MotionTests.Check(clearance >= .999f, $"{target.Name} {(left ? "left" : "right")} hip hand remains inside torso: {clearance:F6}; moved samples {result.Samples}");
        MotionTests.Near(1, MathF.Abs(Quaternion.Dot(beforeWorld[hand].Rot, after[hand].Rot)), 1e-5);
        Invariants(target, before, frame);
    }

    static TargetRig Scale(TargetRig target, float scale)
    {
        var definitions = target.Skeleton.Bones.Select(b => new BoneDefinition(b.Name,
            b.ParentIndex < 0 ? null : target.Skeleton[b.ParentIndex].Name, new XForm(b.RestLocal.Pos * scale, b.RestLocal.Rot))).ToArray();
        var mapping = new MappingResult(target.Name + "-scaled", default);
        foreach (var role in Enum.GetValues<BoneRole>())
            if (target.BoneForRole(role) is int bone) mapping.RoleToBone[role] = bone;
        return TargetRig.FromSkeleton(Skeleton.Create(definitions), mapping);
    }

    static void Unreachable()
    {
        var definitions = new[]
        {
            new BoneDefinition("Hips", null, XForm.Identity),
            new BoneDefinition("Neck", "Hips", new XForm(Vector3.UnitY, Quaternion.Identity)),
            new BoneDefinition("UpperArmL", "Hips", new XForm(new(.5f,.5f,0), Quaternion.Identity)),
            new BoneDefinition("LowerArmL", "UpperArmL", new XForm(new(0,-.01f,0), Quaternion.Identity)),
            new BoneDefinition("HandL", "LowerArmL", new XForm(new(0,-.01f,0), Quaternion.Identity)),
            new BoneDefinition("UpperArmR", "Hips", new XForm(new(-.5f,.5f,0), Quaternion.Identity)),
            new BoneDefinition("LowerArmR", "UpperArmR", new XForm(new(0,-.01f,0), Quaternion.Identity)),
            new BoneDefinition("HandR", "LowerArmR", new XForm(new(0,-.01f,0), Quaternion.Identity))
        };
        var rig = Skeleton.Create(definitions);
        var mapping = new MappingResult("unreachable-short-arms", default);
        foreach (var bone in rig.Bones) mapping.RoleToBone[Enum.Parse<BoneRole>(bone.Name)] = bone.Index;
        var target = TargetRig.FromSkeleton(rig, mapping);
        var original = rig.Bones.Select(b => b.RestLocal).ToArray();
        var frames = new List<XForm[]> { (XForm[])original.Clone() };
        CaptureArmClearance.Apply(frames, new[] { original }, rig, mapping, target, 30, out var unresolved, out var penetration);
        MotionTests.Check(unresolved == 2 && float.IsFinite(penetration) && penetration > 0,
            "Impossible contacts must report a finite residual, not stretch the arms or claim success");
        Invariants(target, original, frames[0]);
    }

    static void Invariants(TargetRig target, XForm[] before, XForm[] after)
    {
        var mutable = new HashSet<int>();
        foreach (var role in new[] { BoneRole.UpperArmL, BoneRole.LowerArmL, BoneRole.HandL, BoneRole.UpperArmR, BoneRole.LowerArmR, BoneRole.HandR })
            mutable.Add(target.BoneForRole(role)!.Value);
        var beforeWorld = new Pose(before).ToWorld(target.Skeleton);
        var afterWorld = new Pose(after).ToWorld(target.Skeleton);
        foreach (var role in new[] { BoneRole.HandL, BoneRole.HandR })
        {
            var hand = target.BoneForRole(role)!.Value;
            MotionTests.Near(1, MathF.Abs(Quaternion.Dot(beforeWorld[hand].Rot, afterWorld[hand].Rot)), 1e-5);
        }
        for (var bone = 0; bone < after.Length; bone++)
        {
            MotionTests.Near(0, Vector3.Distance(before[bone].Pos, after[bone].Pos), 1e-5);
            if (!mutable.Contains(bone))
                MotionTests.Check(MathF.Abs(Quaternion.Dot(before[bone].Rot, after[bone].Rot)) > .99999f,
                    $"{target.Name} changed non-arm bone {target.Skeleton[bone].Name}");
        }
    }

    static void TemporalContact(TargetRig target, bool left)
    {
        const int count = 91;
        var rig = target.Skeleton;
        var rest = rig.Bones.Select(b => b.RestLocal).ToArray();
        var world = new Pose(rest).ToWorld(rig);
        var hips = target.BoneForRole(BoneRole.Hips)!.Value;
        var neck = target.BoneForRole(BoneRole.Neck)!.Value;
        var upper = target.BoneForRole(left ? BoneRole.UpperArmL : BoneRole.UpperArmR)!.Value;
        var lower = target.BoneForRole(left ? BoneRole.LowerArmL : BoneRole.LowerArmR)!.Value;
        var hand = target.BoneForRole(left ? BoneRole.HandL : BoneRole.HandR)!.Value;
        var up = Vector3.Normalize(world[neck].Pos - world[hips].Pos);
        var shoulders = world[target.BoneForRole(BoneRole.UpperArmL)!.Value].Pos - world[target.BoneForRole(BoneRole.UpperArmR)!.Value].Pos;
        var span = shoulders.Length() * .5f;
        var across = Vector3.Normalize(shoulders - up * Vector3.Dot(shoulders, up));
        var forward = Vector3.Cross(across, up);
        var reach = Vector3.Distance(world[upper].Pos, world[lower].Pos) + Vector3.Distance(world[lower].Pos, world[hand].Pos);
        var height = Math.Max(Vector3.Distance(world[neck].Pos, world[hips].Pos) * .05f,
            Vector3.Dot(world[upper].Pos - world[hips].Pos, up) - reach * .85f);
        var frames = new List<XForm[]>(); var goals = new Vector3[count];
        for (var i = 0; i < count; i++)
        {
            var phase = MathF.PI * i / (count - 1);
            var contact = MathF.Sin(phase) * MathF.Sin(phase);
            var radial = 2.5f - 1.8f * contact;
            goals[i] = world[hips].Pos + up * (height + (1 - contact) * reach * .45f)
                + across * ((left ? 1 : -1) * span * radial);
            MotionTests.Check(Vector3.Distance(world[upper].Pos, goals[i]) < reach, "Contact sweep target must be reachable");
            frames.Add((XForm[])rest.Clone());
        }
        EffectorIk.ApplyGoals(frames, rig, new LimbChain { Upper = upper, Lower = lower, End = hand }, goals,
            Vector3.Cross(goals[count / 2] - world[upper].Pos, across * (left ? 1 : -1)), soften: 0);
        if (target.BoneForRole(left ? BoneRole.IndexProxL : BoneRole.IndexProxR) is int finger)
            for (var i = 0; i < count; i++)
                frames[i][finger].Rot = Quaternion.Normalize(Quaternion.CreateFromAxisAngle(Vector3.UnitZ, .2f * MathF.Sin(MathF.PI * i / (count - 1))) * frames[i][finger].Rot);
        var before = frames.Select(f => (XForm[])f.Clone()).ToArray();
        var mapping = new MappingResult("temporal-contact-fixture", default);
        CaptureArmClearance.Apply(frames, before, rig, mapping, target, 30);
        var maxStep = 0f; var maxTurn = 0f; var last = Vector3.Zero;
        for (var i = 0; i < count; i++)
        {
            Invariants(target, before[i], frames[i]);
            var posed = new Pose(frames[i]).ToWorld(rig);
            ClearSegments(target, posed, left);
            var position = posed[hand].Pos;
            var local = position - posed[hips].Pos;
            var x = Vector3.Dot(local, across) / (span * (CaptureArmClearance.HalfWidth + CaptureArmClearance.ArmRadius));
            var z = Vector3.Dot(local, forward) / (span * (CaptureArmClearance.HalfDepth + CaptureArmClearance.ArmRadius));
            MotionTests.Check(x * x + z * z >= .998f, $"{target.Name} contact frame {i} is inside the hip proxy");
            if (i > 0)
            {
                if (Vector3.Distance(last, position) / span > .15f)
                {
                    var input = new Pose(before[i]).ToWorld(rig);
                    var previousInput = new Pose(before[i - 1]).ToWorld(rig);
                    Console.WriteLine($"JUMP {target.Name} f={i} inputStep={Vector3.Distance(input[hand].Pos, previousInput[hand].Pos) / span} before={previousInput[hand].Pos}->{input[hand].Pos} after={last}->{position}");
                }
                maxStep = Math.Max(maxStep, Vector3.Distance(last, position) / span);
                maxTurn = Math.Max(maxTurn, MathQ.AngleBetween(frames[i - 1][upper].Rot, frames[i][upper].Rot));
                maxTurn = Math.Max(maxTurn, MathQ.AngleBetween(frames[i - 1][lower].Rot, frames[i][lower].Rot));
            }
            last = position;
        }
        Console.WriteLine($"{target.Name} {(left ? "left" : "right")} contact sweep: max wrist step {maxStep:F5} shoulder half-spans; max joint step {maxTurn * 180 / MathF.PI:F3} degrees");
        MotionTests.Check(maxStep < .15f, "Body contact introduced a wrist jump");
        MotionTests.Check(maxTurn < .25f, "Body contact introduced an elbow/shoulder snap");
        foreach (var i in new[] { 0, count - 1 })
            foreach (var bone in new[] { upper, lower, hand })
                MotionTests.Near(0, MathQ.AngleBetween(before[i][bone].Rot, frames[i][bone].Rot), 1e-4);
    }

    static void ClearSegments(TargetRig target, XForm[] world, bool left)
    {
        var rig = target.Skeleton;
        var hips = target.BoneForRole(BoneRole.Hips)!.Value;
        var neck = target.BoneForRole(BoneRole.Neck)!.Value;
        var lower = target.BoneForRole(left ? BoneRole.LowerArmL : BoneRole.LowerArmR)!.Value;
        var hand = target.BoneForRole(left ? BoneRole.HandL : BoneRole.HandR)!.Value;
        var axis = world[neck].Pos - world[hips].Pos;
        var height = axis.Length() * .9f; var up = Vector3.Normalize(axis);
        var shoulders = world[target.BoneForRole(BoneRole.UpperArmL)!.Value].Pos - world[target.BoneForRole(BoneRole.UpperArmR)!.Value].Pos;
        var span = shoulders.Length() * .5f;
        var across = Vector3.Normalize(shoulders - up * Vector3.Dot(shoulders, up));
        var forward = Vector3.Cross(across, up);
        var width = span * (CaptureArmClearance.HalfWidth + CaptureArmClearance.ArmRadius);
        var depth = span * (CaptureArmClearance.HalfDepth + CaptureArmClearance.ArmRadius);
        var cap = span * CaptureArmClearance.ArmRadius;
        for (var bone = hand; bone < rig.Count; bone++)
        {
            if (bone != hand && (target.RoleOf(bone) is not { } role
                || (int)role < (int)BoneRole.ThumbMetaL || (int)role > (int)BoneRole.PinkyDistR)) continue;
            var parent = bone == hand ? lower : rig[bone].ParentIndex;
            var ancestor = parent;
            while (ancestor >= 0 && ancestor != hand) ancestor = rig[ancestor].ParentIndex;
            if (bone != hand && ancestor != hand) continue;
            for (var sample = 0; sample <= 32; sample++)
            {
                var point = Vector3.Lerp(world[parent].Pos, world[bone].Pos, sample / 32f) - world[hips].Pos;
                var x = Vector3.Dot(point, across) / width;
                var z = Vector3.Dot(point, forward) / depth;
                var y = Vector3.Dot(point, up); y = (y - Math.Clamp(y, 0, height)) / cap;
                MotionTests.Check(x * x + y * y + z * z >= .998f, $"{target.Name} {rig[bone].Name} segment penetrates the torso proxy");
            }
        }
    }
}