Utility class with geometry helpers for anatomical templates. It picks an up axis from model extents, computes sidedness relative to a symmetry plane, extracts a trimmed branch polyline from a skeleton graph (attachment-first), and performs arc-length interpolation along a polyline.
using AutoRig.Analyze;
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>Shared geometry helpers for the anatomical templates.</summary>
internal static class TemplateUtil
{
/// <summary>
/// The model's up axis: Z only when it is the strict global maximum extent
/// (a genuinely z-up model), otherwise Y. Comparing just Y vs Z misfires on
/// horizontal creatures whose wings/body inflate the Z extent.
/// </summary>
public static Vector3 UpAxis( Vector3 size )
=> size.Z > size.Y && size.Z >= size.X ? Vector3.UnitZ : Vector3.UnitY;
/// <summary>Signed side of the symmetry plane (used for consistent L/R naming).</summary>
public static float SideOf( Vector3 p, SymmetryPlane plane )
=> Vector3.Dot( p - plane.Origin, plane.Normal );
/// <summary>
/// Branch polyline from a tip node, returned attachment-first (limb base → tip),
/// trimmed to the anatomical limb: centerline paths keep walking deep into the
/// torso before merging, so the branch is cut where node thickness jumps well
/// above the limb's own thickness (that's where the limb enters the body volume).
/// </summary>
public static List<Vector3> BranchPolyline( SkeletonGraph graph, int tip )
{
var chain = new List<int> { tip };
var previous = -1;
var current = tip;
while ( true )
{
var next = graph.Nodes[current].Neighbors.FirstOrDefault( n => n != previous, -1 );
if ( next < 0 )
break;
if ( graph.Nodes[next].Neighbors.Count >= 3 || next == graph.Core )
break;
chain.Add( next );
previous = current;
current = next;
}
// chain runs tip → inward. Median thickness of the outer half = the limb's
// own caliber; the attachment is the last node before thickness exceeds it.
var outer = chain.Take( Math.Max( 2, chain.Count / 2 ) )
.Select( i => graph.Nodes[i].Thickness )
.OrderBy( t => t )
.ToList();
var limbThickness = outer[outer.Count / 2];
var end = chain.Count;
for ( var i = 0; i < chain.Count; i++ )
{
if ( graph.Nodes[chain[i]].Thickness > limbThickness * 1.5f )
{
end = i;
break;
}
}
if ( end < 2 )
end = Math.Min( 2, chain.Count );
var trimmed = chain.Take( end ).ToList();
trimmed.Reverse();
return trimmed.Select( i => graph.Nodes[i].Position ).ToList();
}
/// <summary>Arc-length interpolation along a polyline, t in 0..1.</summary>
public static Vector3 Along( List<Vector3> polyline, float t )
{
if ( polyline.Count == 1 )
return polyline[0];
float total = 0;
for ( var i = 1; i < polyline.Count; i++ )
total += Vector3.Distance( polyline[i - 1], polyline[i] );
if ( total <= 0f )
return polyline[0];
var target = Math.Clamp( t, 0f, 1f ) * total;
float walked = 0;
for ( var i = 1; i < polyline.Count; i++ )
{
var segment = Vector3.Distance( polyline[i - 1], polyline[i] );
if ( walked + segment >= target )
return Vector3.Lerp( polyline[i - 1], polyline[i], segment <= 0 ? 0 : (target - walked) / segment );
walked += segment;
}
return polyline[^1];
}
}