AutoRig/Solve/Organic/CategoryDetector.cs

Detects an organic body category from a skeleton graph and symmetry plane by examining tip positions, pairing mirrored tips, and applying heuristics to classify Winged, Humanoid, Quadruped or Other with a confidence and explanation.

ReflectionFile Access
using AutoRig.Analyze;
using AutoRig.Mesh;

namespace AutoRig.Solve.Organic;

// s&box compat: the engine defines Vector2/Vector3 in the GLOBAL namespace, which
// shadows using-directive imports - alias explicitly to System.Numerics.
using Vector3 = System.Numerics.Vector3;

/// <summary>
/// Guesses the body category from the curve skeleton's tips: mirrored tip pairs across
/// the symmetry plane, and their heights along the up axis. Deliberately conservative —
/// anything ambiguous is Other (which gets the generic graph rig).
/// </summary>
public static class CategoryDetector
{
    public static (OrganicCategory Category, float Confidence, string Why) Detect(
        SkeletonGraph graph, SymmetryPlane? symmetry, Aabb3 bounds )
    {
        ArgumentNullException.ThrowIfNull( graph );

        if ( graph.Tips.Count < 2 )
            return (OrganicCategory.Other, 0.6f, "fewer than 2 extremities");
        if ( symmetry is null )
            return (OrganicCategory.Other, 0.5f, "no bilateral symmetry");

        var up = TemplateUtil.UpAxis( bounds.Size );
        var upExtent = MathF.Max( 1e-6f, Vector3.Dot( bounds.Size, Abs( up ) ) );
        var bottom = Vector3.Dot( bounds.Min, up );
        float HeightOf( Vector3 p ) => (Vector3.Dot( p, up ) - bottom) / upExtent;

        // Pair tips across the symmetry plane.
        var plane = symmetry.Value;
        var tipPositions = graph.Tips.Select( t => graph.Nodes[t].Position ).ToList();
        var pairThreshold = bounds.Size.Length() * 0.15f;
        var (pairs, unpaired) = PairTips( tipPositions, plane, pairThreshold );

        float PairHeight( (int A, int B) pair )
            => (HeightOf( tipPositions[pair.A] ) + HeightOf( tipPositions[pair.B] )) * 0.5f;

        if ( pairs.Count >= 2 )
        {
            // ---- winged (checked FIRST): one pair vastly wider than every other,
            // on a body that runs horizontally PERPENDICULAR to the wing axis.
            // The perpendicular-body condition keeps T-posed humanoids (whose arm
            // pair also dominates) out: their body is vertical.
            var bySpan = pairs.OrderByDescending( p =>
                Vector3.Distance( tipPositions[p.A], tipPositions[p.B] ) ).ToList();
            var widest = Vector3.Distance(
                tipPositions[bySpan[0].A], tipPositions[bySpan[0].B] );
            var second = Vector3.Distance(
                tipPositions[bySpan[1].A], tipPositions[bySpan[1].B] );
            if ( widest > second * 2f && widest > bounds.Size.Length() * 0.5f )
            {
                var wingDirection = tipPositions[bySpan[0].A] - tipPositions[bySpan[0].B];
                wingDirection -= up * Vector3.Dot( wingDirection, up );
                if ( wingDirection.LengthSquared() > 1e-8f )
                {
                    var bodyAxis = Vector3.Normalize(
                        Vector3.Cross( up, Vector3.Normalize( wingDirection ) ) );
                    var bodyExtent = Vector3.Dot( bounds.Size, Abs( bodyAxis ) );
                    if ( bodyExtent > upExtent )
                        return (OrganicCategory.Winged, 0.65f,
                            "one dominant wide pair (wings) across a horizontal body");
                }
            }

            // ---- humanoid: a high pair (arms), a low pair (legs), a high unpaired (head) ----
            var byHeight = pairs.OrderBy( PairHeight ).ToList();
            var lowest = PairHeight( byHeight[0] );
            var highest = PairHeight( byHeight[^1] );
            var headTip = unpaired.Any( u => HeightOf( tipPositions[u] ) >= 0.75f );
            if ( lowest <= 0.30f && highest >= 0.50f && headTip )
            {
                return (OrganicCategory.Humanoid,
                    Math.Clamp( 0.4f + (highest - lowest), 0.4f, 0.95f ),
                    $"arms pair at {highest:0.00}, legs pair at {lowest:0.00}, head on top");
            }

            // ---- quadruped: two (or more) low pairs, horizontal body ----
            var lowPairs = pairs.Count( p => PairHeight( p ) <= 0.35f );
            var horizontal = MathF.Max( bounds.Size.X, Vector3.Dot( bounds.Size, Abs( Cross( up ) ) ) );
            var bodyHorizontal = horizontal > upExtent;
            if ( lowPairs >= 2 && bodyHorizontal )
            {
                return (OrganicCategory.Quadruped, 0.7f,
                    $"{lowPairs} leg pairs low on a horizontal body");
            }
        }

        return (OrganicCategory.Other, 0.5f,
            $"{pairs.Count} mirrored pair(s), {unpaired.Count} unpaired tip(s) - no clear body plan");
    }

    /// <summary>
    /// Pairs positions across a symmetry plane: each position's mirror is matched to
    /// the nearest other position within the threshold; positions mirroring onto
    /// themselves (on the plane) and unmatched positions come back as unpaired.
    /// </summary>
    internal static (List<(int A, int B)> Pairs, List<int> Unpaired) PairTips(
        List<Vector3> positions, SymmetryPlane plane, float threshold )
    {
        var paired = new bool[positions.Count];
        var pairs = new List<(int A, int B)>();
        var unpaired = new List<int>();
        for ( var i = 0; i < positions.Count; i++ )
        {
            if ( paired[i] )
                continue;
            var mirrored = Mirror( positions[i], plane );
            if ( Vector3.Distance( mirrored, positions[i] ) < threshold * 0.5f )
            {
                unpaired.Add( i );
                continue;
            }
            var best = -1;
            var bestDistance = threshold;
            for ( var j = 0; j < positions.Count; j++ )
            {
                if ( j == i || paired[j] )
                    continue;
                var d = Vector3.Distance( mirrored, positions[j] );
                if ( d < bestDistance )
                {
                    bestDistance = d;
                    best = j;
                }
            }
            if ( best >= 0 )
            {
                paired[i] = paired[best] = true;
                pairs.Add( (i, best) );
            }
            else
            {
                unpaired.Add( i );
            }
        }
        return (pairs, unpaired);
    }

    /// <summary>Reflects a point across a symmetry plane.</summary>
    public static Vector3 Mirror( Vector3 p, SymmetryPlane plane )
    {
        var d = Vector3.Dot( p - plane.Origin, plane.Normal );
        return p - plane.Normal * (2f * d);
    }

    static Vector3 Abs( Vector3 v ) => new( MathF.Abs( v.X ), MathF.Abs( v.Y ), MathF.Abs( v.Z ) );

    /// <summary>A horizontal axis perpendicular to up (whichever of X/Z is not up).</summary>
    static Vector3 Cross( Vector3 up ) => up == Vector3.UnitY ? Vector3.UnitZ : Vector3.UnitY;
}