Classifier for RigMesh objects that decides whether a mesh is Organic, Mechanical or Hybrid. It combines signals from part count, surface smoothness (dihedral angles), name hints, and dominant-part area ratio, and returns a Classification with kind, confidence and explanation.
using System.Numerics;
using AutoRig.Mesh;
namespace AutoRig.Analyze;
// s&box compat: the engine defines Vector2/Vector3 in the GLOBAL namespace, which
// shadows using-directive imports - alias explicitly to System.Numerics.
using Vector2 = System.Numerics.Vector2;
using Vector3 = System.Numerics.Vector3;
/// <summary>
/// Organic / mechanical / hybrid classification from four signals: part count,
/// surface smoothness (dihedral angles), name hints, and dominant-part area ratio.
/// Hybrid is a per-part override: a smooth dominant body carrying several rigid
/// attachments (weighted-score gaps alone never flag that case — the big organic
/// part outweighs its small rigid attachments).
/// </summary>
public static class MeshClassifier
{
const float SmoothDihedralDegrees = 30f;
public static Classification Classify(
RigMesh mesh, IReadOnlyList<MeshPart> parts, SymmetryPlane? symmetry )
{
ArgumentNullException.ThrowIfNull( mesh );
ArgumentNullException.ThrowIfNull( parts );
if ( parts.Count == 0 || mesh.TriangleCount == 0 )
{
return new Classification
{
Kind = MeshKind.Organic,
Confidence = 0.05f,
Explanation = "Empty or degenerate mesh - defaulting to a single-bone organic rig.",
};
}
// ---- signal: part count ----
var partCountOrganic = parts.Count switch
{
1 => 0.9f,
>= 5 => 0.1f,
_ => 0.9f - (parts.Count - 1) / 4f * 0.8f,
};
// ---- signal: smoothness (global + per part) ----
var weld = MeshSegmenter.WeldMap( mesh, 0f );
var partSmoothness = new float[parts.Count];
float smoothEdges = 0, totalEdges = 0;
for ( var i = 0; i < parts.Count; i++ )
{
var (smooth, total) = SmoothEdgeCounts( mesh, weld, parts[i] );
partSmoothness[i] = total > 0 ? (float)smooth / total : 1f;
smoothEdges += smooth;
totalEdges += total;
}
var smoothness = totalEdges > 0 ? smoothEdges / totalEdges : 1f;
// ---- signal: names ----
var names = parts.Select( p => p.Name ).Concat( mesh.Tags ).ToArray();
var mechanicalNames = NameHints.MechanicalScore( names );
var organicNames = NameHints.OrganicScore( names );
// ---- signal: dominance ----
var totalArea = parts.Sum( p => p.SurfaceArea );
var dominance = totalArea > 0 ? parts[0].SurfaceArea / totalArea : 1f;
// Smoothness is a weak discriminator on finely-tessellated CAD (machined curves
// have small dihedrals too), so part structure and names carry more weight.
var organic = 0.40f * partCountOrganic
+ 0.20f * smoothness
+ 0.30f * organicNames
+ 0.10f * dominance;
var mechanical = 0.40f * (1f - partCountOrganic)
+ 0.20f * (1f - smoothness)
+ 0.30f * mechanicalNames
+ 0.10f * (1f - dominance);
// ---- hard rule: a swarm of parts with no dominant body is a machine ----
// No organic deforming creature is 8+ loose pieces none of which dominates;
// organic naming evidence (hair/eye/body parts on characters) vetoes the rule.
if ( parts.Count >= 8 && dominance < 0.7f && organicNames <= mechanicalNames )
{
return new Classification
{
Kind = MeshKind.Mechanical,
Confidence = 0.9f,
Explanation = $"{parts.Count} rigid part(s) with no dominant body - treating as a machine.",
};
}
// ---- hybrid override: smooth dominant body + several rigid attachments ----
var rigidAttachments = 0;
for ( var i = 1; i < parts.Count; i++ )
if ( partSmoothness[i] < 0.7f )
rigidAttachments++;
var isHybrid = parts.Count >= 3
&& partSmoothness[0] >= 0.7f
&& rigidAttachments >= 2
&& dominance is >= 0.5f and <= 0.97f;
var gap = MathF.Abs( organic - mechanical );
if ( isHybrid )
{
return new Classification
{
Kind = MeshKind.Hybrid,
Confidence = Math.Clamp( 0.4f + 0.2f * rigidAttachments / (parts.Count - 1f), 0.05f, 0.99f ),
Explanation = $"Smooth main body with {rigidAttachments} rigid attachment(s) - mixed rig.",
};
}
if ( organic >= mechanical )
{
return new Classification
{
Kind = MeshKind.Organic,
Confidence = Math.Clamp( gap, 0.05f, 0.99f ),
Explanation = parts.Count == 1
? "One smooth connected surface - treating as a creature/character."
: $"{parts.Count} part(s), mostly smooth surfaces - treating as a creature/character.",
};
}
var namedHint = mechanicalNames > 0
? $" with mechanical names ({FirstMechanicalNames( names )})"
: "";
return new Classification
{
Kind = MeshKind.Mechanical,
Confidence = Math.Clamp( gap, 0.05f, 0.99f ),
Explanation = $"{parts.Count} rigid part(s){namedHint} - treating as a machine.",
};
}
static string FirstMechanicalNames( string[] names )
{
var hits = names
.Where( n => NameHints.MechanicalScore( [ n ] ) > 0 )
.Take( 2 )
.Select( n => n.ToLowerInvariant() );
return string.Join( ", ", hits );
}
/// <summary>
/// Counts (smooth, total) interior edges of a part. An interior edge is shared by
/// exactly two of the part's triangles under welded vertex identity; smooth means
/// the dihedral angle between face normals is under 30°. Capped at 5000 edges by
/// deterministic stride.
/// </summary>
static (int Smooth, int Total) SmoothEdgeCounts( RigMesh mesh, int[] weld, MeshPart part )
{
var edges = new Dictionary<(int A, int B), (int Tri, bool Used)>();
var pairs = new List<(int TriA, int TriB)>();
foreach ( var tri in part.TriangleIndices )
{
for ( var k = 0; k < 3; k++ )
{
var a = weld[mesh.Triangles[tri * 3 + k]];
var b = weld[mesh.Triangles[tri * 3 + (k + 1) % 3]];
if ( a == b )
continue; // degenerate edge
var key = a < b ? (a, b) : (b, a);
if ( edges.TryGetValue( key, out var existing ) )
{
if ( !existing.Used )
{
pairs.Add( (existing.Tri, tri) );
edges[key] = (existing.Tri, true);
}
}
else
{
edges[key] = (tri, false);
}
}
}
var stride = Math.Max( 1, pairs.Count / 5000 );
var smooth = 0;
var total = 0;
var cosThreshold = MathF.Cos( SmoothDihedralDegrees * MathF.PI / 180f );
for ( var i = 0; i < pairs.Count; i += stride )
{
var (ta, tb) = pairs[i];
var na = FaceNormal( mesh, ta );
var nb = FaceNormal( mesh, tb );
if ( na is null || nb is null )
continue;
total++;
if ( Vector3.Dot( na.Value, nb.Value ) >= cosThreshold )
smooth++;
}
return (smooth, total);
}
static Vector3? FaceNormal( RigMesh mesh, int tri )
{
var a = mesh.Positions[mesh.Triangles[tri * 3]];
var b = mesh.Positions[mesh.Triangles[tri * 3 + 1]];
var c = mesh.Positions[mesh.Triangles[tri * 3 + 2]];
var n = Vector3.Cross( b - a, c - a );
var length = n.Length();
return length < 1e-12f ? null : n / length;
}
}