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

namespace Effigy;

/// <summary>
/// A bone shaped like the body it will move.
///
/// WHY THIS MEASURES GEOMETRY RATHER THAN READING THE FEATURE THAT BUILT IT. The obvious way to
/// turn a modelled finger into a bone is to ask its Extrude: that feature knows a direction, a
/// distance and a sketch plane, so head, tail and roll all fall straight out of parameters somebody
/// already typed. It is also a trap. It answers for exactly the feature types that happen to carry
/// an axis and has nothing to say about every body that reached its shape another way — a
/// primitive, a revolve, a boolean of two solids, anything filleted or shelled after the fact — and
/// it has to be extended once per feature type by whoever adds the next one, who will have no
/// reason to know they were supposed to. The tool would appear to work and would quietly cover less
/// of the modeller's vocabulary every release.
///
/// A body is a bag of positions no matter what built it. One reader covers the whole tool, cannot
/// fall behind the feature list, and keeps working through a rebuild that changes which features
/// produced the shape. The cost is that the answer is inferred rather than declared, which is why
/// everything below is stated as what it measures rather than what it intends.
///
/// WHAT IT MEASURES. The principal axes of the body's vertices: the direction they spread out along
/// most becomes the bone's aim, the widest direction across that becomes its roll, and the bone
/// spans the body's full extent along the aim. For anything limb-shaped — which is what people rig
/// — that is the answer they would have drawn by hand.
/// </summary>
public static class BoneFromBody
{
	/// <summary>
	/// Head, tail and an up-hint for a bone spanning <paramref name="positions"/>, or false if the
	/// points have no length to span.
	///
	/// <paramref name="anchor"/> is a point the bone should START near — the parent bone's tail,
	/// when there is a parent. WITHOUT IT THERE IS NO WAY TO KNOW WHICH END IS THE ROOT: a
	/// principal axis is a line, not an arrow, and a finger measured on its own is as much
	/// fingertip-to-knuckle as the reverse. The fallback orientation is therefore canonical rather
	/// than correct — the same shape always yields the same answer, so a patterned row of fingers
	/// comes out consistent — and the caller is expected to pass an anchor whenever it knows one.
	///
	/// The returned <c>up</c> is a HINT in the sense <see cref="Skeleton.AddBoneFromPoints"/> means
	/// it: the widest direction across the bone, already perpendicular to the aim, which that method
	/// is free to fall back from if it ever degenerates.
	/// </summary>
	public static bool TryDerive( IReadOnlyList<Vec3> positions,
		out Vec3 head, out Vec3 tail, out Vec3 up, Vec3? anchor = null )
	{
		head = Vec3.Zero;
		tail = Vec3.Zero;
		up = new Vec3( 0, 0, 1 );

		if ( positions is null || positions.Count < 2 )
			return false;

		var centre = Vec3.Zero;

		foreach ( var p in positions )
			centre += p;

		centre /= positions.Count;

		// The covariance of the point cloud about its centre. Symmetric, so six terms rather than
		// nine, and deliberately NOT divided by the count: eigenvectors do not care about scale and
		// the division is one more place for a zero-length body to produce a NaN.
		float xx = 0, xy = 0, xz = 0, yy = 0, yz = 0, zz = 0;

		foreach ( var p in positions )
		{
			var d = p - centre;

			xx += d.x * d.x;
			xy += d.x * d.y;
			xz += d.x * d.z;
			yy += d.y * d.y;
			yz += d.y * d.z;
			zz += d.z * d.z;
		}

		var aim = DominantAxis( xx, xy, xz, yy, yz, zz );

		if ( aim.LengthSquared < 0.5f )
			return false;

		// A principal axis has no sign, so pin one. Largest-magnitude component positive is
		// arbitrary but total: it depends only on the shape, so two bodies of the same shape in the
		// same orientation — a pattern, a mirror — always come out pointing the same way, which is
		// the property that actually matters when the fallback is used.
		if ( LargestComponent( aim ) < 0 )
			aim = -aim;

		float min = float.MaxValue, max = float.MinValue;

		foreach ( var p in positions )
		{
			var t = Vec3.Dot( p - centre, aim );

			if ( t < min ) min = t;
			if ( t > max ) max = t;
		}

		if ( max - min < 1e-4f )
			return false;

		head = centre + aim * min;
		tail = centre + aim * max;
		up = WidestAcross( positions, centre, aim );

		// The anchor knows what the geometry cannot: which end joins the rest of the model. It only
		// ever swaps the two ends — it never moves them — so the bone still spans exactly the body.
		if ( anchor is Vec3 near
			&& (near - tail).LengthSquared < (near - head).LengthSquared )
		{
			(head, tail) = (tail, head);
		}

		return true;
	}

	/// <summary>Convenience over a whole mesh, which is what a caller holding a Body has.</summary>
	public static bool TryDerive( PolyMesh mesh,
		out Vec3 head, out Vec3 tail, out Vec3 up, Vec3? anchor = null )
	{
		if ( mesh is null )
		{
			head = tail = Vec3.Zero;
			up = new Vec3( 0, 0, 1 );
			return false;
		}

		return TryDerive( mesh.Positions, out head, out tail, out up, anchor );
	}

	/// <summary>
	/// The eigenvector of the covariance with the largest eigenvalue, by power iteration.
	///
	/// Iteration rather than a closed form because the closed form for a symmetric 3x3 goes through
	/// trigonometric roots that lose precision exactly where this is used most — a long thin limb,
	/// where one eigenvalue dwarfs the other two. That same lopsidedness is what makes power
	/// iteration converge almost immediately here.
	///
	/// A BODY WITH NO LONGEST AXIS gets an arbitrary but repeatable answer rather than a refusal. A
	/// cube's three eigenvalues are equal and every direction is a legitimate principal axis, so
	/// there is nothing to be right about; the seed decides, the seed is derived from the shape, and
	/// the same cube therefore always produces the same bone. A caller who wants a specific
	/// direction out of a symmetric body has to say so by drawing the bone.
	/// </summary>
	static Vec3 DominantAxis( float xx, float xy, float xz, float yy, float yz, float zz )
	{
		Vec3 Multiply( Vec3 v ) => new(
			xx * v.x + xy * v.y + xz * v.z,
			xy * v.x + yy * v.y + yz * v.z,
			xz * v.x + yz * v.y + zz * v.z );

		// Seeding with a world axis risks starting exactly perpendicular to the answer, which
		// stalls the iteration. Multiplying a spread of directions through the matrix once and
		// keeping the longest result cannot: the product is already biased toward the dominant
		// eigenvector, and only an all-zero matrix leaves every candidate at zero.
		var seed = Vec3.Zero;

		foreach ( var candidate in new[]
		{
			new Vec3( 1, 1, 1 ), new Vec3( 1, 0, 0 ), new Vec3( 0, 1, 0 ), new Vec3( 0, 0, 1 )
		} )
		{
			var mapped = Multiply( candidate );

			if ( mapped.LengthSquared > seed.LengthSquared )
				seed = mapped;
		}

		var v = seed.Normal;

		if ( v.LengthSquared < 0.5f )
			return Vec3.Zero;

		for ( var i = 0; i < 64; i++ )
		{
			var next = Multiply( v ).Normal;

			if ( next.LengthSquared < 0.5f )
				return v;

			// Converged: another sixty iterations would move it by less than the tolerance every
			// consumer of this already rounds away.
			if ( (next - v).LengthSquared < 1e-14f )
				return next;

			v = next;
		}

		return v;
	}

	/// <summary>
	/// The direction across the bone that the body is widest in — its roll.
	///
	/// Solved in the two dimensions that are left once the aim is fixed, where the dominant
	/// eigenvector of a symmetric 2x2 IS a closed form and needs no iteration at all. A section that
	/// is genuinely round makes the off-diagonal term vanish and the answer falls out as "either of
	/// the two axes we happened to build", which is the honest result for a cylinder: nothing about
	/// it prefers a roll.
	/// </summary>
	static Vec3 WidestAcross( IReadOnlyList<Vec3> positions, Vec3 centre, Vec3 aim )
	{
		var seed = MathF.Abs( aim.x ) < 0.9f ? new Vec3( 1, 0, 0 ) : new Vec3( 0, 0, 1 );
		var e0 = Vec3.Cross( seed, aim ).Normal;
		var e1 = Vec3.Cross( aim, e0 );

		float a = 0, b = 0, d = 0;

		foreach ( var p in positions )
		{
			var r = p - centre;
			var u = Vec3.Dot( r, e0 );
			var v = Vec3.Dot( r, e1 );

			a += u * u;
			b += u * v;
			d += v * v;
		}

		if ( MathF.Abs( b ) < 1e-12f )
			return a >= d ? e0 : e1;

		var theta = 0.5f * MathF.Atan2( 2f * b, a - d );

		return e0 * MathF.Cos( theta ) + e1 * MathF.Sin( theta );
	}

	/// <summary>The component with the largest magnitude, ties broken x then y then z so the
	/// choice is a function of the vector alone.</summary>
	static float LargestComponent( Vec3 v )
	{
		var ax = MathF.Abs( v.x );
		var ay = MathF.Abs( v.y );
		var az = MathF.Abs( v.z );

		if ( ax >= ay && ax >= az )
			return v.x;

		return ay >= az ? v.y : v.z;
	}
}