Code/AutoRig/Solve/Organic/WingedTemplate.cs

Template builder that detects a winged creature body plan from a skeleton graph and constructs a RigSkeleton with pelvis, spine, neck/head, optional tail, three-joint wings and optional two-joint legs.

Reflection
using AutoRig.Analyze;
using AutoRig.Mesh;
using AutoRig.Rig;

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>
/// Snaps a winged body plan (bird/dragon-like) to a named joint set: pelvis and a
/// spine chain along the horizontal body, neck/head at the front, optional tail at
/// the rear, three-joint wings from the dominant wide tip pair, and optional
/// two-joint legs from a second low pair. Returns null when the plan cannot be
/// identified.
/// </summary>
public static class WingedTemplate
{
    public static RigSkeleton? Build( SkeletonGraph graph, SymmetryPlane symmetry, Aabb3 bounds )
    {
        ArgumentNullException.ThrowIfNull( graph );
        if ( graph.Tips.Count < 4 )
            return null;

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

        var tipPositions = graph.Tips.Select( t => graph.Nodes[t].Position ).ToList();
        var (pairs, unpaired) = CategoryDetector.PairTips(
            tipPositions, symmetry, bounds.Size.Length() * 0.15f );
        if ( pairs.Count < 1 || unpaired.Count == 0 )
            return null;

        // Wings = the widest pair.
        var bySpan = pairs.OrderByDescending( p =>
            Vector3.Distance( tipPositions[p.A], tipPositions[p.B] ) ).ToList();
        var wingsPair = bySpan[0];

        // Head: the highest unpaired tip.
        var headTipIndex = unpaired.OrderByDescending( u => HeightOf( tipPositions[u] ) ).First();
        var headPosition = tipPositions[headTipIndex];

        var (wingLTip, wingRTip) = TemplateUtil.SideOf( tipPositions[wingsPair.A], symmetry ) > 0
            ? (wingsPair.A, wingsPair.B)
            : (wingsPair.B, wingsPair.A);
        var wingL = TemplateUtil.BranchPolyline( graph, graph.Tips[wingLTip] );
        var wingR = TemplateUtil.BranchPolyline( graph, graph.Tips[wingRTip] );
        var headBranch = TemplateUtil.BranchPolyline( graph, graph.Tips[headTipIndex] );
        if ( wingL.Count < 2 || wingR.Count < 2 || headBranch.Count < 2 )
            return null;

        Vector3 Sym( Vector3 left, Vector3 right )
            => (left + CategoryDetector.Mirror( right, symmetry )) * 0.5f;
        Vector3 Flatten( Vector3 p ) => p - up * Vector3.Dot( p, up );

        // Body axis: shoulders (wing attachments) toward the head define front.
        Vector3 OnPlane( Vector3 p ) => (p + CategoryDetector.Mirror( p, symmetry )) * 0.5f;
        var shoulders = OnPlane( Sym( wingL[0], wingR[0] ) );
        var forward = Flatten( headPosition - shoulders );
        if ( forward.LengthSquared() < 1e-8f )
            return null;
        forward = Vector3.Normalize( forward );

        // Pelvis: behind the shoulders by a fraction of the body length; when a low
        // second pair exists (legs), use its attachments instead.
        List<Vector3>? legL = null, legR = null;
        float PairHeight( (int A, int B) p )
            => (HeightOf( tipPositions[p.A] ) + HeightOf( tipPositions[p.B] )) * 0.5f;
        foreach ( var pair in bySpan.Skip( 1 ).OrderBy( PairHeight ) )
        {
            if ( PairHeight( pair ) > 0.45f )
                break;
            var (l, r) = TemplateUtil.SideOf( tipPositions[pair.A], symmetry ) > 0
                ? (pair.A, pair.B)
                : (pair.B, pair.A);
            var candidateL = TemplateUtil.BranchPolyline( graph, graph.Tips[l] );
            var candidateR = TemplateUtil.BranchPolyline( graph, graph.Tips[r] );
            if ( candidateL.Count >= 2 && candidateR.Count >= 2 )
            {
                legL = candidateL;
                legR = candidateR;
            }
            break;
        }

        var bodyLength = Vector3.Dot( bounds.Size, new Vector3(
            MathF.Abs( forward.X ), MathF.Abs( forward.Y ), MathF.Abs( forward.Z ) ) );
        var pelvis = legL is not null && legR is not null
            ? OnPlane( Sym( legL[0], legR[0] ) )
            : shoulders - forward * bodyLength * 0.35f;

        Vector3 SpineAt( float t ) => Vector3.Lerp( pelvis, shoulders, t );

        var skeleton = new RigSkeleton();
        int Add( string name, int parent, Vector3 position )
        {
            skeleton.Joints.Add( new RigJoint { Name = name, Parent = parent, Position = position } );
            return skeleton.Joints.Count - 1;
        }

        var pelvisJ = Add( "pelvis", -1, pelvis );
        var spine0 = Add( "spine_0", pelvisJ, SpineAt( 0.35f ) );
        var spine1 = Add( "spine_1", spine0, SpineAt( 0.7f ) );
        var spine2 = Add( "spine_2", spine1, SpineAt( 1f ) );
        var neck = Add( "neck_0", spine2, TemplateUtil.Along( headBranch, 0.2f ) );
        Add( "head", neck, TemplateUtil.Along( headBranch, 0.65f ) );

        // Optional tail: an unpaired non-head tip behind the pelvis.
        foreach ( var u in unpaired )
        {
            if ( u == headTipIndex )
                continue;
            if ( Vector3.Dot( Flatten( tipPositions[u] - pelvis ), -forward ) <= 0f )
                continue;
            var branch = TemplateUtil.BranchPolyline( graph, graph.Tips[u] );
            if ( branch.Count < 2 )
                continue;
            var tail0 = Add( "tail_0", pelvisJ, TemplateUtil.Along( branch, 0.4f ) );
            Add( "tail_1", tail0, TemplateUtil.Along( branch, 0.9f ) );
            break;
        }

        // Wings: attachment / mid / tip, three joints each side.
        void AddWing( string suffix, bool isLeft )
        {
            Vector3 At( float t ) => isLeft
                ? Sym( TemplateUtil.Along( wingL, t ), TemplateUtil.Along( wingR, t ) )
                : CategoryDetector.Mirror(
                    Sym( TemplateUtil.Along( wingL, t ), TemplateUtil.Along( wingR, t ) ), symmetry );
            var upper = Add( $"wing_upper_{suffix}", spine2, At( 0f ) );
            var lower = Add( $"wing_lower_{suffix}", upper, At( 0.5f ) );
            Add( $"wing_tip_{suffix}", lower, At( 0.95f ) );
        }
        AddWing( "L", isLeft: true );
        AddWing( "R", isLeft: false );

        // Optional legs: attachment + ankle per side.
        if ( legL is not null && legR is not null )
        {
            Vector3 LegAt( float t, bool isLeft ) => isLeft
                ? Sym( TemplateUtil.Along( legL, t ), TemplateUtil.Along( legR, t ) )
                : CategoryDetector.Mirror(
                    Sym( TemplateUtil.Along( legL, t ), TemplateUtil.Along( legR, t ) ), symmetry );
            var legUpperL = Add( "leg_upper_L", pelvisJ, LegAt( 0f, true ) );
            Add( "ankle_L", legUpperL, LegAt( 0.9f, true ) );
            var legUpperR = Add( "leg_upper_R", pelvisJ, LegAt( 0f, false ) );
            Add( "ankle_R", legUpperR, LegAt( 0.9f, false ) );
        }

        skeleton.Validate();
        return skeleton;
    }
}