Effigy/Rig/BoneSelection.cs
using System;
using System.Collections.Generic;

namespace Effigy;

/// <summary>
/// What a multi-bone selection means when you drag it.
///
/// WHY THIS IS NOT IN THE VIEWPORT, where it started. It is a question about a tree and a set of
/// names — no engine types, no widgets, nothing to draw — and getting it wrong is invisible:
/// a bone transformed twice looks like a bone that moved further than the others, which reads as
/// a gizmo problem rather than a selection one. Out here a test can state the shape and check it.
/// </summary>
public static class BoneSelection
{
	/// <summary>
	/// The selected bones with no selected ANCESTOR — the ones a group drag is applied to. Anything
	/// under one of them is carried by it through the hierarchy and must not be transformed again.
	///
	/// EVERY ANCESTOR, NOT THE IMMEDIATE PARENT. Checking only the parent is right whenever the
	/// selection is contiguous and wrong the moment it skips a generation: select a bone and its
	/// GRANDCHILD without the bone between them, and the grandchild's parent is not in the
	/// selection, so it passes the test — it is then transformed directly AND carried by its
	/// grandparent, moving twice as far as everything else. That reads as a broken gizmo rather
	/// than a broken selection, which is why it is worth spelling out here.
	///
	/// <paramref name="parentOf"/> returns the parent's name, or null at a root. A cycle would
	/// hang the walk, so the depth is capped at the number of bones in the selection plus the
	/// chain it climbs; a malformed skeleton stops being this function's problem at that point.
	/// </summary>
	public static List<string> TopMost( IEnumerable<string> selected, Func<string, string> parentOf )
	{
		var names = new HashSet<string>( selected ?? Array.Empty<string>() );
		var tops = new List<string>();

		if ( parentOf is null )
			return new List<string>( names );

		foreach ( var name in names )
		{
			if ( !HasSelectedAncestor( name, names, parentOf ) )
				tops.Add( name );
		}

		return tops;
	}

	/// <summary>Whether anything above <paramref name="name"/> is also selected.</summary>
	public static bool HasSelectedAncestor( string name, ISet<string> selected, Func<string, string> parentOf )
	{
		if ( name is null || selected is null || parentOf is null )
			return false;

		// Guard against a skeleton that points at itself. The walk is bounded by the selection
		// size plus a sane chain length rather than by trust in the data.
		var guard = 0;
		var limit = selected.Count + 256;

		for ( var current = parentOf( name ); current is not null; current = parentOf( current ) )
		{
			if ( selected.Contains( current ) )
				return true;

			if ( ++guard > limit )
				return false;
		}

		return false;
	}
}