Detects an organic body category (Winged, Humanoid, Quadruped, Other) from a skeleton graph and symmetry plane by pairing tip positions across the symmetry plane and checking pair spans and heights relative to the bounds.
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;
}