tests/HumanoidRetargeter.Tests/MirroredVariantTests.cs

Unit tests for the MirroredVariant behavior in the HumanoidRetargeter package. Tests exercise ClipMirror, mirroring invariants (double mirror), footstep swapping, pelvis lateral negation, foot trajectory reflection, helper construction parity, channel-set parity, naming and defaults, and some mirror-safety exclusion logic using Sbox target rigs and synthetic BVH input.

File Access
using System.Numerics;
using System.Text;
using HumanoidRetargeter.Core.Mapping;
using HumanoidRetargeter.Core.Skeleton;
using HumanoidRetargeter.Core.Solve;
using HumanoidRetargeter.Core.Target;
using Xunit;
using HumanoidRetargeter.Core;

namespace HumanoidRetargeter.Tests;

/// <summary>
/// Facade tests for <see cref="RetargetRequest.CreateMirroredVariant"/> plus direct
/// <see cref="ClipMirror"/> math proofs (double-mirror involution, sagittal-plane geometry).
/// </summary>
public class MirroredVariantTests
{
    private static string RepoFile(params string[] parts)
    {
        var dir = new DirectoryInfo(AppContext.BaseDirectory);
        while (dir is not null)
        {
            if (File.Exists(Path.Combine(dir.FullName, "humanoid-retargeter.sbproj")))
                return Path.Combine(new[] { dir.FullName }.Concat(parts).ToArray());
            dir = dir.Parent!;
        }
        throw new InvalidOperationException("Repo root (humanoid-retargeter.sbproj) not found.");
    }

    private static readonly Lazy<RetargetTargetSpec> SboxTarget = new(()
        => RetargetTargetSpec.SboxDefault(
            File.ReadAllText(RepoFile("Assets", "data", "humanoid_retargeter", "target_rig_sbox.json"))));

    private static RetargetRequest WalkRequest(bool mirrored = true, bool footsteps = false) => new()
    {
        SourceData = Encoding.UTF8.GetBytes(WalkFixture.SyntheticWalkBvh()),
        SourceFileName = "synth_walk.bvh",
        CreateMirroredVariant = mirrored,
        GenerateFootstepEvents = footsteps,
    };

    // ---------------------------------------------------------------- math: involution

    [Fact]
    public void ClipMirror_DoubleMirror_IsBitExactIdentity()
    {
        // The s&box rig's computed lateral axis snaps to an exact coordinate axis, making
        // every reflection a pure IEEE sign flip — so mirror ∘ mirror must reproduce the
        // input BIT-EXACTLY (component equality, not tolerance).
        var result = Retargeter.Convert(WalkRequest(mirrored: false), SboxTarget.Value);
        var clip = Assert.Single(result.Clips);
        Assert.True(clip.Success, clip.Error);
        var frames = clip.SolvedFrames!;

        var rig = SboxTarget.Value.Rig;
        var twice = ClipMirror.Mirror(ClipMirror.Mirror(frames, rig), rig);

        Assert.Equal(frames.Count, twice.Count);
        for (var f = 0; f < frames.Count; f++)
        {
            for (var b = 0; b < frames[f].Length; b++)
            {
                Assert.True(frames[f][b] == twice[f][b],
                    $"frame {f} bone {b} ('{rig.Skeleton[b].Name}'): " +
                    $"{frames[f][b]} != {twice[f][b]} (double mirror must be bit-exact)");
            }
        }

        // And the single mirror is NOT the identity (it actually does something).
        var once = ClipMirror.Mirror(frames, rig);
        Assert.Contains(Enumerable.Range(0, frames.Count),
            f => Enumerable.Range(0, frames[f].Length).Any(b => frames[f][b] != once[f][b]));
    }

    // ---------------------------------------------------------------- plant-phase swap

    [Fact]
    public void Convert_MirroredVariant_SwapsFootPlantPhases()
    {
        // The synthetic walk has ONE left touchdown (~frame 15) and ONE right touchdown
        // (~frame 45). Plant detection on the mirrored clip must see them swapped: the
        // mirrored LEFT foot touches down where the primary RIGHT did, and vice versa.
        var result = Retargeter.Convert(WalkRequest(footsteps: true), SboxTarget.Value);
        Assert.Equal(2, result.Clips.Count);
        Assert.All(result.Clips, c => Assert.True(c.Success, c.Error));

        var primary = result.Clips.Single(c => !c.IsMirroredVariant);
        var mirrored = result.Clips.Single(c => c.IsMirroredVariant);
        Assert.Equal(primary.ClipName + "_M", mirrored.ClipName);

        int[] Feet(ClipResult clip, string foot)
            => clip.FootstepEvents.Where(e => e.Foot == foot).Select(e => e.Frame).ToArray();

        // Mirroring is an exact reflection of the solved frames, so the swapped plant
        // phases land on exactly the same frames.
        Assert.Equal(Feet(primary, "1"), Feet(mirrored, "0")); // primary right → mirrored left
        Assert.Equal(Feet(primary, "0"), Feet(mirrored, "1")); // primary left → mirrored right

        // Sanity: the phases really are distinct (left ~15, right ~45).
        Assert.NotEqual(Feet(primary, "0"), Feet(primary, "1"));
    }

    // ---------------------------------------------------------------- sagittal geometry

    [Fact]
    public void Convert_MirroredVariant_PelvisLateralOffsetNegates()
    {
        var result = Retargeter.Convert(WalkRequest(), SboxTarget.Value);
        var primary = result.Clips.Single(c => !c.IsMirroredVariant);
        var mirrored = result.Clips.Single(c => c.IsMirroredVariant);
        Assert.True(primary.Success, primary.Error);
        Assert.True(mirrored.Success, mirrored.Error);

        var rig = SboxTarget.Value.Rig;
        var pelvis = rig.BoneForRole(BoneRole.Hips)!.Value;
        var lateral = RestLateral(rig);

        var maxAbs = 0f;
        for (var f = 0; f < primary.SolvedFrames!.Count; f++)
        {
            var p = Vector3.Dot(new Pose(primary.SolvedFrames[f]).ToWorld(rig.Skeleton)[pelvis].Pos, lateral);
            var m = Vector3.Dot(new Pose(mirrored.SolvedFrames![f]).ToWorld(rig.Skeleton)[pelvis].Pos, lateral);
            Assert.True(MathF.Abs(p + m) < 1e-3f,
                $"frame {f}: pelvis lateral {p:F4} should negate to {-p:F4}, got {m:F4}");
            maxAbs = MathF.Max(maxAbs, MathF.Abs(p));
        }

        // The fixture's hip sway makes this non-trivial: the lateral offset is really moving.
        Assert.True(maxAbs > 0.5f, $"expected ≥0.5 cm of lateral sway, got {maxAbs:F3} cm");
    }

    [Fact]
    public void Convert_MirroredVariant_FootTrajectoriesSwapSides()
    {
        var result = Retargeter.Convert(WalkRequest(), SboxTarget.Value);
        var primary = result.Clips.Single(c => !c.IsMirroredVariant);
        var mirrored = result.Clips.Single(c => c.IsMirroredVariant);

        var rig = SboxTarget.Value.Rig;
        var footL = rig.BoneForRole(BoneRole.FootL)!.Value;
        var footR = rig.BoneForRole(BoneRole.FootR)!.Value;
        var lateral = RestLateral(rig);

        // The mirrored clip's LEFT ankle must follow the primary RIGHT ankle's path
        // reflected across the sagittal plane: same height/forward, negated lateral.
        // Tolerance: the twin is a full re-solve of the mirrored SOURCE (G8 source-side
        // mirror), reflection-faithful to ~2e-3 cm on the symmetric fixture rather than
        // bit-exact; 0.01 cm still asserts a physically perfect mirror.
        for (var f = 0; f < primary.SolvedFrames!.Count; f += 7)
        {
            var pr = new Pose(primary.SolvedFrames[f]).ToWorld(rig.Skeleton)[footR].Pos;
            var ml = new Pose(mirrored.SolvedFrames![f]).ToWorld(rig.Skeleton)[footL].Pos;
            var reflected = pr - 2f * Vector3.Dot(pr, lateral) * lateral;
            TestUtil.AssertVectorEqual(reflected, ml, 1e-2f);
        }
    }

    // ---------------------------------------------------------------- source-mirrored twin
    //
    // G8 regeneration mirror fix (southpaw project, gate3_review.md 3.4 plus the G8
    // bisect evidence). The OLD test here ("IkBonesFollowMirroredBody") asserted a bare
    // target-space re-bake. Target-space channel mirroring in ANY convention produced
    // data the engine's render-side sequence evaluation mangles (a mirrored pelvis
    // channel alone renders the model upside down while every CPU bone API reports a
    // perfect mirror), so the twin is now produced by mirroring the SOURCE clip and
    // running the COMPLETE primary pipeline on it. The contract: the twin is a
    // first-class primary clip (helper construction, aim constants, channel set all
    // primary-identical) whose solved body is the reflection of the primary's within
    // solver tolerance.

    [Fact]
    public void Convert_MirroredVariant_HelperConstructionMatchesPrimary()
    {
        // The twin's helpers must be a FIXED POINT of the primary helper chain: applying
        // IkBoneBaker.Bake + the orphan-hips re-anchor to the twin's own solved frames
        // must change nothing. That proves the twin ends in exactly the state the
        // primary pipeline produces for its clips (bake, then re-anchor), i.e. its data
        // shape is primary-identical and the engine cannot tell it apart.
        var result = Retargeter.Convert(WalkRequest(), SboxTarget.Value);
        var mirrored = result.Clips.Single(c => c.IsMirroredVariant);
        Assert.True(mirrored.Success, mirrored.Error);

        var rig = SboxTarget.Value.Rig;
        var skeleton = rig.Skeleton;

        var reapplied = mirrored.SolvedFrames!
            .Select(f => (HumanoidRetargeter.Core.Maths.XForm[])f.Clone())
            .ToList();
        IkBoneBaker.Bake(reapplied, rig);
        Retargeter.TestHook_FollowOrphans(rig, reapplied);

        for (var f = 0; f < reapplied.Count; f += 5)
        {
            for (var b = 0; b < skeleton.Count; b++)
            {
                TestUtil.AssertVectorEqual(
                    mirrored.SolvedFrames[f][b].Pos, reapplied[f][b].Pos, 1e-3f);
            }
        }
    }

    [Fact]
    public void Convert_MirroredVariant_AimReferencesStayAtRestLikeEveryPrimaryClip()
    {
        // The aim-space reference bones (aim_matrix_*) and the hold/attach helpers are
        // engine-expected CONSTANTS: every shipped citizen clip holds them at REST
        // locals and the model's aim/eye constraint setup is authored against exactly
        // those. The twin must carry the identical convention.
        var result = Retargeter.Convert(WalkRequest(), SboxTarget.Value);
        var mirrored = result.Clips.Single(c => c.IsMirroredVariant);
        Assert.True(mirrored.Success, mirrored.Error);

        var rig = SboxTarget.Value.Rig;
        var skeleton = rig.Skeleton;
        var pinnedChecked = 0;
        for (var f = 0; f < mirrored.SolvedFrames!.Count; f += 7)
        {
            for (var i = 0; i < skeleton.Count; i++)
            {
                var name = skeleton[i].Name;
                if (!name.StartsWith("aim_matrix", StringComparison.Ordinal)
                    && !name.EndsWith("_IK_attach", StringComparison.Ordinal)
                    && name is not ("hold_L" or "hold_R"))
                    continue;
                TestUtil.AssertVectorEqual(
                    skeleton[i].RestLocal.Pos, mirrored.SolvedFrames[f][i].Pos, 1e-3f);
                pinnedChecked++;
            }
        }
        Assert.True(pinnedChecked > 0, "no aim-space reference bones found on the sbox rig");
    }

    // ---------------------------------------------------------------- asymmetric-rest exclusions

    [Fact]
    public void MirrorSafeExclusions_KeepsSymmetric_LiftsAsymmetricRestBones()
    {
        // G8 mirror fix MECHANISM 1: channel-excluded constraint-driven bones whose L/R
        // rest locals are NOT mirror conjugates (the citizen *_twist1 chains and
        // neck_clothing, measured 19 to 37 cm off in the Gate 3 review) must get explicit
        // channels on mirrored clips; truly symmetric helpers stay excluded as on primary
        // clips.
        var citizen = RetargetTargetSpec.SboxCitizen(File.ReadAllText(
            TestUtil.RepoFile("Assets", "data", "humanoid_retargeter", "target_rig_sbox_citizen.json")));
        var rig = citizen.Rig;
        var excluded = new HashSet<int>(rig.BonesOfClass(BoneClass.ConstraintDriven));
        var safe = ClipMirror.MirrorSafeExclusions(rig, excluded);
        Assert.NotNull(safe);

        int IndexOf(string n) => rig.Skeleton.IndexOf(n);

        foreach (var name in new[] { "arm_upper_L_twist0", "arm_upper_R_twist0",
            "leg_upper_L_twist0", "leg_upper_R_twist0" })
        {
            var i = IndexOf(name);
            if (i >= 0 && excluded.Contains(i))
                Assert.Contains(i, safe!);
        }

        foreach (var name in new[] { "leg_upper_L_twist1", "leg_upper_R_twist1",
            "leg_lower_L_twist1", "leg_lower_R_twist1",
            "arm_upper_L_twist1", "arm_upper_R_twist1",
            "arm_lower_L_twist1", "arm_lower_R_twist1", "neck_clothing" })
        {
            var i = IndexOf(name);
            if (i >= 0 && excluded.Contains(i))
                Assert.DoesNotContain(i, safe!);
        }
    }

    [Fact]
    public void ClipMirror_HierarchyInconsistentHelpers_FkSolvedToExactReflection()
    {
        // ClipMirror utility contract (kept for measurement tooling): the citizen rig
        // parents arm_elbow_helper_R under arm_lower_R_twist0 while arm_elbow_helper_L
        // hangs under arm_lower_L (the W3a-documented quirk), so the partner's
        // conjugated LOCAL cannot place them; ClipMirror FK-solves their locals so the
        // mirrored WORLD is the exact sagittal reflection of the partner's world.
        var citizen = RetargetTargetSpec.SboxCitizen(File.ReadAllText(
            TestUtil.RepoFile("Assets", "data", "humanoid_retargeter", "target_rig_sbox_citizen.json")));
        var result = Retargeter.Convert(WalkRequest(mirrored: false), citizen);
        var primary = result.Clips.Single();
        Assert.True(primary.Success, primary.Error);

        var rig = citizen.Rig;
        var skeleton = rig.Skeleton;
        var lateral = RestLateral(rig);
        var mirroredFrames = ClipMirror.Mirror(primary.SolvedFrames!, rig);

        var checkedHelpers = 0;
        foreach (var (name, partner) in new[]
        {
            ("arm_elbow_helper_L", "arm_elbow_helper_R"),
            ("arm_elbow_helper_R", "arm_elbow_helper_L"),
            ("leg_knee_helper_L", "leg_knee_helper_R"),
            ("leg_knee_helper_R", "leg_knee_helper_L"),
        })
        {
            var i = skeleton.IndexOf(name);
            var p = skeleton.IndexOf(partner);
            if (i < 0 || p < 0)
                continue;
            checkedHelpers++;
            for (var f = 0; f < primary.SolvedFrames!.Count; f += 9)
            {
                var wp = new Pose(primary.SolvedFrames[f]).ToWorld(skeleton);
                var wm = new Pose(mirroredFrames[f]).ToWorld(skeleton);
                var src = wp[p].Pos;
                var reflected = src - 2f * Vector3.Dot(src, lateral) * lateral;
                TestUtil.AssertVectorEqual(reflected, wm[i].Pos, 0.05f);
            }
        }
        Assert.True(checkedHelpers > 0, "citizen rig should carry the elbow/knee helpers");
    }

    [Fact]
    public void Convert_MirroredVariant_ChannelSetMatchesPrimaryExactly()
    {
        // DMX data-shape parity (southpaw G8 mirror fix): the mirrored clip carries the
        // IDENTICAL channel set as the primary: constraint-driven bones stay excluded on
        // both (the engine's AnimConstraintList drives them for any pose, mirrored or
        // not); a twin with a different channel set is a data shape no primary clip has.
        var citizen = RetargetTargetSpec.SboxCitizen(File.ReadAllText(
            TestUtil.RepoFile("Assets", "data", "humanoid_retargeter", "target_rig_sbox_citizen.json")));
        var result = Retargeter.Convert(WalkRequest(), citizen);
        var primary = result.Clips.Single(c => !c.IsMirroredVariant);
        var mirrored = result.Clips.Single(c => c.IsMirroredVariant);
        Assert.True(primary.Success, primary.Error);
        Assert.True(mirrored.Success, mirrored.Error);

        foreach (var bone in citizen.Rig.Skeleton.Bones)
        {
            var needle = $"\"{bone.Name}_o\"";
            Assert.Equal(
                primary.DmxContent!.Contains(needle, StringComparison.Ordinal),
                mirrored.DmxContent!.Contains(needle, StringComparison.Ordinal));
        }
    }

    // ---------------------------------------------------------------- naming / defaults

    [Fact]
    public void ConvertBatch_MirroredNames_CollisionSuffixAsUsual()
    {
        var result = Retargeter.ConvertBatch(
            new[] { WalkRequest(), WalkRequest() }, SboxTarget.Value);

        Assert.Equal(4, result.Clips.Count);
        Assert.All(result.Clips, c => Assert.True(c.Success, c.Error));
        Assert.Equal(
            new[] { "synth_walk", "synth_walk_M", "synth_walk_2", "synth_walk_2_M" },
            result.Clips.Select(c => c.ClipName));
        Assert.Equal(new[] { false, true, false, true },
            result.Clips.Select(c => c.IsMirroredVariant));

        // All four registered in the standalone vmdl with distinct DMX files.
        Assert.Equal(4, result.Clips.Select(c => c.DmxFileName).Distinct().Count());
        foreach (var clip in result.Clips)
            Assert.Contains($"name = \"{clip.ClipName}\"", result.StandaloneVmdl);
    }

    [Fact]
    public void Convert_DefaultOff_ProducesNoMirroredVariant()
    {
        var result = Retargeter.Convert(WalkRequest(mirrored: false), SboxTarget.Value);
        var clip = Assert.Single(result.Clips);
        Assert.True(clip.Success, clip.Error);
        Assert.False(clip.IsMirroredVariant);
        Assert.DoesNotContain("_M", clip.ClipName);
    }

    [Fact]
    public void Convert_MirroredVariant_PreservesLoopingAndFps()
    {
        var result = Retargeter.Convert(new RetargetRequest
        {
            SourceData = Encoding.UTF8.GetBytes(WalkFixture.SyntheticWalkBvh()),
            SourceFileName = "synth_walk.bvh",
            CreateMirroredVariant = true,
            LoopingOverride = true,
        }, SboxTarget.Value);

        var primary = result.Clips.Single(c => !c.IsMirroredVariant);
        var mirrored = result.Clips.Single(c => c.IsMirroredVariant);
        Assert.True(mirrored.Looping);
        Assert.Equal(primary.Fps, mirrored.Fps);
        Assert.Equal(primary.SolvedFrames!.Count, mirrored.SolvedFrames!.Count);
        Assert.False(string.IsNullOrEmpty(mirrored.DmxContent));
    }

    // ---------------------------------------------------------------- helpers

    /// <summary>Target lateral (left-positive) from public rest geometry, orthogonalized
    /// against the character up and snapped to the dominant coordinate axis — mirrors what
    /// ClipMirror computes internally (the snap keeps assertion tolerances meaningful).</summary>
    private static Vector3 RestLateral(TargetRig rig)
    {
        Vector3 Pos(BoneRole role) => rig.Skeleton.RestWorld[rig.BoneForRole(role)!.Value].Pos;
        var midHips = (Pos(BoneRole.UpperLegL) + Pos(BoneRole.UpperLegR)) * 0.5f;
        var midShoulders = (Pos(BoneRole.UpperArmL) + Pos(BoneRole.UpperArmR)) * 0.5f;
        var up = Vector3.Normalize(midShoulders - midHips);
        var across = Pos(BoneRole.UpperLegL) - Pos(BoneRole.UpperLegR);
        var lateral = Vector3.Normalize(across - up * Vector3.Dot(across, up));

        var a = Vector3.Abs(lateral);
        if (a.Y <= 1e-3f && a.Z <= 1e-3f)
            return Vector3.UnitX;
        if (a.X <= 1e-3f && a.Z <= 1e-3f)
            return Vector3.UnitY;
        if (a.X <= 1e-3f && a.Y <= 1e-3f)
            return Vector3.UnitZ;
        return lateral;
    }
}