Editor/Inference/RotationFit.cs
using System;
using System.Numerics;

namespace HumanoidMocapX.Inference;

using Vector3 = System.Numerics.Vector3;

/// <summary>Best-fit rotation between two sets of paired directions, such as SAM's camera-space
/// body directions and the same directions in a capture's world.</summary>
internal static class RotationFit
{
    /// <summary>The rotation taking directions <paramref name="seen"/> to directions
    /// <paramref name="posed"/>, refitted twice without pairs more than <paramref name="residual"/>
    /// radians off. Zero-length pairs are skipped; with no usable pair it returns identity.</summary>
    internal static Quaternion Fit(ReadOnlySpan<Vector3> seen, ReadOnlySpan<Vector3> posed, float residual)
    {
        var rotation = Quaternion.Identity; var everyPair = true;
        Span<double> s = stackalloc double[9];
        for (var pass = 0; pass < 3; pass++)
        {
            s.Clear();
            for (var i = 0; i < seen.Length; i++)
            {
                if (!(seen[i].LengthSquared() > 1e-12f) || !(posed[i].LengthSquared() > 1e-12f)) continue;
                var a = Vector3.Normalize(seen[i]); var b = Vector3.Normalize(posed[i]);
                if (!everyPair && Vector3.Dot(Vector3.Transform(a, rotation), b) < MathF.Cos(residual)) continue;
                s[0] += a.X * b.X; s[1] += a.X * b.Y; s[2] += a.X * b.Z;
                s[3] += a.Y * b.X; s[4] += a.Y * b.Y; s[5] += a.Y * b.Z;
                s[6] += a.Z * b.X; s[7] += a.Z * b.Y; s[8] += a.Z * b.Z;
            }
            rotation = Horn(s); everyPair = false;
        }
        return rotation;
    }

    /// <summary>Horn's closed-form absolute orientation: the unit quaternion maximising the
    /// alignment is the dominant eigenvector of a symmetric 4×4 built from the cross-covariance
    /// <paramref name="s"/> (row-major Σ a bᵀ). Found by power iteration, shifted so every
    /// eigenvalue is positive.</summary>
    static Quaternion Horn(ReadOnlySpan<double> s)
    {
        double xx = s[0], xy = s[1], xz = s[2], yx = s[3], yy = s[4], yz = s[5], zx = s[6], zy = s[7], zz = s[8];
        Span<double> n = stackalloc double[]
        {
            xx + yy + zz, yz - zy, zx - xz, xy - yx,
            yz - zy, xx - yy - zz, xy + yx, zx + xz,
            zx - xz, xy + yx, -xx + yy - zz, yz + zy,
            xy - yx, zx + xz, yz + zy, -xx - yy + zz
        };
        double shift = 0;
        for (var i = 0; i < 16; i++) shift += Math.Abs(n[i]);
        for (var i = 0; i < 4; i++) n[i * 5] += shift;
        Span<double> q = stackalloc double[] { 1, .1, .1, .1 }; Span<double> next = stackalloc double[4];
        for (var iteration = 0; iteration < 500; iteration++)
        {
            double length = 0;
            for (var r = 0; r < 4; r++) { next[r] = n[r * 4] * q[0] + n[r * 4 + 1] * q[1] + n[r * 4 + 2] * q[2] + n[r * 4 + 3] * q[3]; length += next[r] * next[r]; }
            length = Math.Sqrt(length);
            for (var r = 0; r < 4; r++) q[r] = next[r] / length;
        }
        // Horn orders the quaternion (w, x, y, z).
        return Quaternion.Normalize(new Quaternion((float)q[1], (float)q[2], (float)q[3], (float)q[0]));
    }
}