Core/Calibration/HandFrames.cs
#nullable enable
using System.Numerics;
using HumanoidHandRetargeter.Mapping;
using HumanoidHandRetargeter.Maths;
using SkeletonModel = HumanoidHandRetargeter.Skeleton.Skeleton;
namespace HumanoidHandRetargeter.Calibration;
using Vector3 = System.Numerics.Vector3;
/// <summary>HandGeometry/CanonicalFrames specialization from legacy commit 26084c9.
/// Frames use X along the joint, Z dorsal, Y the curl hinge. Local bone axes are irrelevant.</summary>
internal static class HandFrames
{
internal static bool TryPalm(SkeletonModel skeleton, HandRigDefinition hand, out Quaternion frame)
{
frame = Quaternion.Identity;
var fingers = hand.Digits.Where(d => d.Role is DigitRole.Index or DigitRole.Middle or DigitRole.Ring or DigitRole.Pinky)
.OrderBy(d => d.Role).ToArray();
Vector3 first, last;
if (fingers.Length >= 2)
{
first = skeleton.RestWorld[fingers[0].Segments[0]].Pos;
last = skeleton.RestWorld[fingers[^1].Segments[0]].Pos;
}
else
{
// A confirmed generic/extra chain (e.g. a mitten rig) can still define a
// palm geometrically. Use the thumb to identify the radial end; never
// assign an anatomical digit role just to obtain a frame.
var thumb = hand.Digits.FirstOrDefault(d => d.Role == DigitRole.Thumb);
var nonThumb = hand.Digits.Where(d => d.Role != DigitRole.Thumb).ToArray();
if (thumb is null || nonThumb.Length < 2) return false;
first = last = Vector3.Zero;
var span = 0f;
foreach (var a in nonThumb)
foreach (var b in nonThumb)
{
var pa = skeleton.RestWorld[a.Segments[0]].Pos;
var pb = skeleton.RestWorld[b.Segments[0]].Pos;
if (Vector3.DistanceSquared(pa, pb) <= span) continue;
span = Vector3.DistanceSquared(pa, pb);
first = pa;
last = pb;
}
var thumbPosition = skeleton.RestWorld[thumb.Segments[0]].Pos;
var radialEvidence = Vector3.DistanceSquared(first, thumbPosition) - Vector3.DistanceSquared(last, thumbPosition);
if (MathF.Abs(radialEvidence) < span * 1e-4f) return false;
if (radialEvidence > 0) (first, last) = (last, first);
}
var middle = hand.Digits.Aggregate(Vector3.Zero, (p, d) => p + skeleton.RestWorld[d.Segments[0]].Pos) / hand.Digits.Count;
var forward = middle - skeleton.RestWorld[hand.Wrist].Pos;
var dorsal = Vector3.Cross(first - last, forward) * (hand.Side == HandSide.Left ? 1f : -1f);
return TryBasis(forward, dorsal, out frame);
}
internal static bool TryBasis(Vector3 primary, Vector3 dorsal, out Quaternion frame)
{
frame = Quaternion.Identity;
if (!Finite(primary) || !Finite(dorsal) || !float.IsFinite(primary.LengthSquared()) || !float.IsFinite(dorsal.LengthSquared())
|| primary.LengthSquared() < 1e-12f || dorsal.LengthSquared() < 1e-12f) return false;
var x = Vector3.Normalize(primary);
var z = Vector3.Normalize(dorsal);
z -= x * Vector3.Dot(z, x);
if (z.LengthSquared() < 1e-8f) return false;
z = Vector3.Normalize(z);
var y = Vector3.Cross(z, x);
// Same System.Numerics row-vector basis convention as legacy CanonicalFrames.
frame = MathQ.Normalize(Quaternion.CreateFromRotationMatrix(new Matrix4x4(
x.X, x.Y, x.Z, 0, y.X, y.Y, y.Z, 0, z.X, z.Y, z.Z, 0, 0, 0, 0, 1)));
return true;
}
internal static int[] Joints(DigitChain digit) => (digit.Metacarpal is int meta ? new[] { meta } : Array.Empty<int>())
.Concat(digit.Segments).ToArray();
internal static Vector3 Direction(SkeletonModel skeleton, HandRigDefinition hand, DigitChain digit, int[] joints, int i)
{
var current = skeleton.RestWorld[joints[i]].Pos;
if (i + 1 < joints.Length) return skeleton.RestWorld[joints[i + 1]].Pos - current;
if (digit.Tip is int tip && (skeleton.RestWorld[tip].Pos - current).LengthSquared() > 1e-12f)
return skeleton.RestWorld[tip].Pos - current;
// Legacy distal extrapolation: no fabricated terminal bone or assumption about skin weights.
return current - skeleton.RestWorld[i > 0 ? joints[i - 1] : hand.Wrist].Pos;
}
private static bool Finite(Vector3 p) => float.IsFinite(p.X) && float.IsFinite(p.Y) && float.IsFinite(p.Z);
}