Editor/HumanoidHandRetargeter/HandPresetStore.cs
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using HumanoidHandRetargeter.Mapping;
using Skel=HumanoidHandRetargeter.Skeleton.Skeleton;
namespace HumanoidHandRetargeter.Editor;
/// <summary>Local, skeleton-signature-keyed mapping corrections, following legacy UserPresets.</summary>
public static class HandPresetStore
{
public sealed record Digit(DigitRole Role,int[] Segments,int? Metacarpal,int? Tip,string ExtraSlot);
public sealed record Hand(HandSide Side,int Wrist,int? Clavicle,int? UpperArm,int? Forearm,Digit[] Digits,int[] Helpers);
static string PathFor(Skel skeleton)
{
using var bytes=new MemoryStream(); using(var writer=new BinaryWriter(bytes,Encoding.UTF8,true))
foreach(var b in skeleton.Bones) { writer.Write(b.Name);writer.Write(b.ParentIndex);writer.Write(b.RestLocal.Pos.X);writer.Write(b.RestLocal.Pos.Y);writer.Write(b.RestLocal.Pos.Z);writer.Write(b.RestLocal.Rot.X);writer.Write(b.RestLocal.Rot.Y);writer.Write(b.RestLocal.Rot.Z);writer.Write(b.RestLocal.Rot.W); }
var signature=Convert.ToHexString(SHA256.HashData(bytes.ToArray())).ToLowerInvariant();
return System.IO.Path.Combine(System.IO.Path.GetDirectoryName(HandEditorPipeline.Assets)!,".sbox","hand-retargeter",signature+".json");
}
public static HandMappingResult? Load(Skel skeleton)
{
var path=PathFor(skeleton); if(!File.Exists(path)) return null;
try {
var hands=JsonSerializer.Deserialize<Hand[]>(File.ReadAllText(path));if(hands is null)return null;
var map=HandRigDetector.Detect(skeleton,hands.Select(h=>new HandRigDefinition(h.Side,h.Wrist,h.Digits.Select(d=>new DigitChain(d.Role,d.Segments,d.Metacarpal,d.Tip,d.ExtraSlot)),h.Clavicle,h.UpperArm,h.Forearm,h.Helpers)));
// Older versions let users confirm every numbered ray as an extra.
// Upgrade that placeholder only when automatic detection now identifies
// the exact same five chains. Keep explicit anatomical corrections.
var automatic=HandRigDetector.Detect(skeleton);
if(!automatic.NeedsReview&&LoadPalms(skeleton) is not {Count:>0})
{
var upgraded=map.Hands.Select(hand=>
{
var detected=automatic.Hands.FirstOrDefault(h=>h.Side==hand.Side&&h.Wrist==hand.Wrist);
return hand.Digits.Count==5&&hand.Digits.All(d=>d.Role==DigitRole.Extra)
&&detected is not null&&detected.Digits.Select(d=>d.Role).Distinct().Count()==5
&&!detected.Digits.Any(d=>d.Role==DigitRole.Extra)
&&hand.Digits.All(d=>detected.Digits.Any(a=>a.Bones.SequenceEqual(d.Bones)))?detected:hand;
}).ToArray();
map=HandRigDetector.Detect(skeleton,upgraded);
}
return map.NeedsReview?null:map;
}
catch(JsonException) { return null; }
catch(ArgumentException) { return null; }
}
public sealed record Palm(HandSide Side,float X,float Y,float Z,float W);
public static IReadOnlyDictionary<HandSide,System.Numerics.Quaternion>? LoadPalms(Skel skeleton)
{
var path=PathFor(skeleton)+".palms.json";if(!File.Exists(path))return null;
try {
var values=JsonSerializer.Deserialize<Palm[]>(File.ReadAllText(path));
if(values is null)return null;
var result=new Dictionary<HandSide,System.Numerics.Quaternion>();
foreach(var value in values){var q=new System.Numerics.Quaternion(value.X,value.Y,value.Z,value.W);if(!float.IsFinite(q.LengthSquared())||q.LengthSquared()<1e-8f)return null;result.Add(value.Side,System.Numerics.Quaternion.Normalize(q));}
return result;
}catch(JsonException){return null;}catch(ArgumentException){return null;}
}
public static void SavePalms(Skel skeleton,IReadOnlyDictionary<HandSide,System.Numerics.Quaternion> frames)
{
var path=PathFor(skeleton)+".palms.json";Directory.CreateDirectory(System.IO.Path.GetDirectoryName(path)!);
File.WriteAllText(path,JsonSerializer.Serialize(frames.Select(p=>new Palm(p.Key,p.Value.X,p.Value.Y,p.Value.Z,p.Value.W))));
}
public static void Save(Skel skeleton,HandMappingResult mapping)
{
if(mapping.NeedsReview) throw new InvalidOperationException("Confirm a valid mapping before saving it.");
var path=PathFor(skeleton);Directory.CreateDirectory(System.IO.Path.GetDirectoryName(path)!);
var hands=mapping.Hands.Select(h=>new Hand(h.Side,h.Wrist,h.Clavicle,h.UpperArm,h.Forearm,h.Digits.Select(d=>new Digit(d.Role,d.Segments.ToArray(),d.Metacarpal,d.Tip,d.ExtraSlot)).ToArray(),h.TwistOrHelperBones.ToArray())).ToArray();
File.WriteAllText(path,JsonSerializer.Serialize(hands,new JsonSerializerOptions{WriteIndented=true}));
}
}