AutoRig/Solve/Organic/TemplateUtil.cs

Shared geometry helpers for anatomical templates. Provides utilities to choose model up axis, compute signed side relative to a symmetry plane, extract an inward branch polyline trimmed at the limb attachment, and interpolate along a polyline by arc length.

Reflection
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];
    }
}