s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (108 total matches found)
notpointless.chomnr_humanoid_hand_retargeter / Core/Validation/HandRigValidator.cs
Game library
#nullable enable
using HumanoidHandRetargeter.Mapping;
using SkeletonModel = HumanoidHandRetargeter.Skeleton.Skeleton;

namespace HumanoidHandRetargeter.Validation;

public sealed record RigIssue(string Code, string Message, HandSide? Side = null, int? Bone = null);

/// <summary>Shared validation for automatic/manual mapping, batch processing and the editor.</summary>
public static class HandRigValidator
{
    public static IReadOnlyList<RigIssue> Validate(SkeletonModel skeleton, IEnumerable<HandRigDefinition> hands)
    {
        ArgumentNullException.ThrowIfNull(skeleton);
        ArgumentNullException.ThrowIfNull(hands);
        var issues = new List<RigIssue>();
        var claimed = new HashSet<int>();
        var sides = new HashSet<HandSide>();
        var count = 0;

        foreach (var bone in skeleton.Bones)
        {
            if (!PoseValidator.ValidTransform(bone.RestLocal))
                issues.Add(new("invalid-rest", $"Bone '{bone.Name}' has an invalid rest transform.", Bone: bone.Index));
            var world = skeleton.RestWorld[bone.Index].Pos;
            if (!float.IsFinite(world.X) || !float.IsFinite(world.Y) || !float.IsFinite(world.Z))
                issues.Add(new("invalid-world-rest", $"Bone '{bone.Name}' has an invalid accumulated rest position.", Bone: bone.Index));
        }

        foreach (var hand in hands)
        {
            count++;
            if (hand is null)
            {
                issues.Add(new("missing-hand", "A hand mapping is missing."));
                continue;
            }
            if (!Enum.IsDefined(typeof(HandSide), hand.Side))
                issues.Add(new("invalid-side", "Choose left or right for the hand."));
            if (!sides.Add(hand.Side))
                issues.Add(new("duplicate-side", $"More than one {hand.Side} hand is mapped.", hand.Side));

            var arm = new[] { hand.Clavicle, hand.UpperArm, hand.Forearm, hand.Wrist }
                .Where(b => b.HasValue).Select(b => b!.Value).ToArray();
            CheckChain(arm, hand.Side, "arm", allowZeroLength: true);
            var roles = new HashSet<string>(StringComparer.Ordinal);
            foreach (var digit in hand.Digits)
            {
                if (digit is null)
                {
                    issues.Add(new("missing-digit", "A digit mapping is missing.", hand.Side));
                    continue;
                }
                var key = digit.Role == DigitRole.Extra ? "Extra:" + digit.ExtraSlot : digit.Role.ToString();
                if (!Enum.IsDefined(typeof(DigitRole), digit.Role)
                    || (digit.Role == DigitRole.Extra && string.IsNullOrWhiteSpace(digit.ExtraSlot)))
                    issues.Add(new("invalid-digit-role", "Choose a digit role or name its extra slot.", hand.Side));
                if (!roles.Add(key))
                    issues.Add(new("duplicate-digit-role", $"{hand.Side} {key} is mapped more than once.", hand.Side));
                if (digit.Segments.Count == 0)
                    issues.Add(new("empty-digit", $"{hand.Side} {key} needs at least one segment.", hand.Side));
                var chain = digit.Bones.ToArray();
                CheckChain(chain, hand.Side, key, allowZeroLength: false, tip: digit.Tip);
                if (chain.Length > 0 && Valid(chain[0]) && Valid(hand.Wrist)
                    && !skeleton.DescendsFrom(chain[0], hand.Wrist))
                    issues.Add(new("wrong-wrist", $"{hand.Side} {key} must descend from its wrist.", hand.Side, chain[0]));
            }
            foreach (var helper in hand.TwistOrHelperBones)
            {
                Claim(helper, hand.Side);
                var armRoot = arm[0];
                if (Valid(helper) && Valid(armRoot) && !skeleton.DescendsFrom(helper, armRoot))
                    issues.Add(new("wrong-helper-root", $"{hand.Side} helper must belong to its mapped arm or hand hierarchy.", hand.Side, helper));
            }
        }
        if (count == 0)
            issues.Add(new("no-hands", "Map at least one wrist before retargeting."));
        return issues.AsReadOnly();

        bool Valid(int bone) => bone >= 0 && bone < skeleton.Count;
        void Claim(int bone, HandSide side)
        {
            if (!Valid(bone))
                issues.Add(new("invalid-bone", $"{side} mapping references a missing bone.", side, bone));
            else if (!claimed.Add(bone))
                issues.Add(new("overlapping-bone", $"Bone '{skeleton[bone].Name}' is assigned to multiple roles.", side, bone));
        }
        void CheckChain(int[] bones, HandSide side, string role, bool allowZeroLength, int? tip = null)
        {
            for (var i = 0; i < bones.Length; i++)
            {
                Claim(bones[i], side);
                if (i == 0 || !Valid(bones[i - 1]) || !Valid(bones[i])) continue;
                if (!skeleton.DescendsFrom(bones[i], bones[i - 1]))
                    issues.Add(new("chain-order", $"{side} {role} joints must follow their hierarchy.", side, bones[i]));
                if (!allowZeroLength && bones[i] != tip &&
                    System.Numerics.Vector3.DistanceSquared(skeleton.RestWorld[bones[i - 1]].Pos,
                        skeleton.RestWorld[bones[i]].Pos) < 1e-12f)
                    issues.Add(new("zero-length-digit", $"{side} {role} has coincident joints; correct the chain or classify a helper.", side, bones[i]));
            }
        }
    }
}
notpointless.chomnr_humanoid_hand_retargeter / Formats/Fbx/FbxScene.cs
Game library
// Reused from humanoid-retargeter 26084c96c3fc870aaf9a5bd798de063ce2fd62df; specialized where needed for hand assets.
#nullable enable annotations

using System;
using System.Collections.Generic;
using System.Numerics;

namespace HumanoidHandRetargeter.Formats.Fbx;

using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidHandRetargeter/Assembly.cs)

/// <summary>One <c>P</c> entry of a <c>Properties70</c> block: name, FBX type string, values.</summary>
public sealed class FbxProperty70
{
    /// <summary>Property name (e.g. <c>"Lcl Translation"</c>, <c>"PreRotation"</c>, <c>"d|X"</c>).</summary>
    public string Name { get; }

    /// <summary>FBX type string (e.g. <c>"Lcl Translation"</c>, <c>"enum"</c>, <c>"Number"</c>).</summary>
    public string Type { get; }

    /// <summary>Raw values (props 4.. of the P node).</summary>
    public IReadOnlyList<object> Values { get; }

    internal FbxProperty70(string name, string type, IReadOnlyList<object> values)
    {
        Name = name;
        Type = type;
        Values = values;
    }

    /// <summary>Value <paramref name="i"/> as a double (tolerant of int/long/float storage).</summary>
    public double GetDouble(int i = 0) => Convert.ToDouble(Values[i], System.Globalization.CultureInfo.InvariantCulture);

    /// <summary>Value <paramref name="i"/> as an int (tolerant of long/double storage).</summary>
    public int GetInt(int i = 0) => Convert.ToInt32(Values[i], System.Globalization.CultureInfo.InvariantCulture);

    /// <summary>First three values as a vector.</summary>
    public Vector3 GetVector3()
        => new((float)GetDouble(0), (float)GetDouble(1), (float)GetDouble(2));
}

/// <summary>
/// One object from the FBX <c>Objects</c> section (Model, NodeAttribute, AnimationStack,
/// AnimationLayer, AnimationCurveNode, AnimationCurve, Pose, ...).
/// </summary>
public sealed class FbxObject
{
    /// <summary>Unique object id (the first property of the object node).</summary>
    public long Id { get; }

    /// <summary>Node type — the FBX node name: "Model", "AnimationCurve", ...</summary>
    public string NodeType { get; }

    /// <summary>Object name (namespace prefixes like <c>mixamorig1:</c> preserved).</summary>
    public string Name { get; }

    /// <summary>Object sub-class (third property): "LimbNode", "Null", "Root", "Mesh", ...</summary>
    public string SubClass { get; }

    /// <summary>The underlying token-tree node.</summary>
    public FbxNode Node { get; }

    /// <summary>Own Properties70 entries by name (template defaults NOT merged — see <see cref="FbxScene.FindProperty"/>).</summary>
    public IReadOnlyDictionary<string, FbxProperty70> Properties { get; }

    /// <summary>For Models: the parent Model via an OO connection, or null at the scene root.</summary>
    public FbxObject? ModelParent { get; internal set; }

    /// <summary>For Models: child Models via OO connections, in connection order.</summary>
    public List<FbxObject> ModelChildren { get; } = new();

    internal FbxObject(
        long id, string nodeType, string name, string subClass, FbxNode node,
        IReadOnlyDictionary<string, FbxProperty70> properties)
    {
        Id = id;
        NodeType = nodeType;
        Name = name;
        SubClass = subClass;
        Node = node;
        Properties = properties;
    }

    /// <inheritdoc />
    public override string ToString() => $"{NodeType} '{Name}' ({SubClass}) #{Id}";
}

/// <summary>A single animation curve: keyframes for one scalar channel.</summary>
public sealed class FbxAnimCurve
{
    /// <summary>KTIME ticks per second (FBX constant).</summary>
    public const long TicksPerSecond = 46186158000L;

    /// <summary>Key times in KTIME ticks, ascending.</summary>
    public long[] KeyTimes { get; }

    /// <summary>Key values, parallel to <see cref="KeyTimes"/>.</summary>
    public float[] KeyValues { get; }

    internal FbxAnimCurve(long[] keyTimes, float[] keyValues)
    {
        KeyTimes = keyTimes;
        KeyValues = keyValues;
    }

    /// <summary>
    /// Samples the curve at a KTIME tick: linear interpolation between keys, constant
    /// extrapolation outside the key range.
    /// </summary>
    public float Evaluate(long ticks)
    {
        var times = KeyTimes;
        int n = times.Length;
        if (n == 0)
            return 0f;
        if (ticks <= times[0])
            return KeyValues[0];
        if (ticks >= times[n - 1])
            return KeyValues[n - 1];

        int hi = Array.BinarySearch(times, ticks);
        if (hi >= 0)
            return KeyValues[hi];
        hi = ~hi; // first index with time > ticks; >=1 and <=n-1 here
        int lo = hi - 1;
        double span = times[hi] - times[lo];
        double t = span <= 0 ? 0.0 : (ticks - times[lo]) / span;
        return (float)(KeyValues[lo] + (KeyValues[hi] - KeyValues[lo]) * t);
    }
}

/// <summary>
/// An AnimationCurveNode: up to three channel curves (X/Y/Z) targeting one transform
/// property (<c>"Lcl Translation"</c> / <c>"Lcl Rotation"</c> / <c>"Lcl Scaling"</c>) of one Model.
/// </summary>
public sealed class FbxAnimCurveNode
{
    /// <summary>The curve node object.</summary>
    public FbxObject Object { get; }

    /// <summary>Channel curves by axis ('X'/'Y'/'Z'), from <c>"d|X"</c>-style OP connections.</summary>
    public Dictionary<char, FbxAnimCurve> Channels { get; } = new();

    internal FbxAnimCurveNode(FbxObject obj) => Object = obj;

    /// <summary>
    /// Samples one component: the channel curve when connected, else the curve node's static
    /// <c>d|X</c> default, else <paramref name="fallback"/> (the model's Lcl value).
    /// </summary>
    public float Component(char axis, long ticks, float fallback)
    {
        if (Channels.TryGetValue(axis, out var curve))
            return curve.Evaluate(ticks);
        if (Object.Properties.TryGetValue("d|" + axis, out var def) && def.Values.Count > 0)
            return (float)def.GetDouble();
        return fallback;
    }
}

/// <summary>One AnimationStack with its curve bindings, flattened across its layers.</summary>
public sealed class FbxAnimStack
{
    /// <summary>The stack object (its Name is the clip name, e.g. "mixamo.com").</summary>
    public FbxObject Object { get; }

    /// <summary>
    /// Curve nodes bound to model transform properties:
    /// (model id, property name) → curve node. When several layers animate the same property
    /// the first connected layer wins (layer blending is not supported).
    /// </summary>
    public Dictionary<(long ModelId, string Property), FbxAnimCurveNode> Bindings { get; } = new();

    /// <summary>LocalStart from the stack's Properties70, in KTIME ticks (0 when absent).</summary>
    public long LocalStart { get; internal set; }

    /// <summary>LocalStop from the stack's Properties70, in KTIME ticks (0 when absent).</summary>
    public long LocalStop { get; internal set; }

    internal FbxAnimStack(FbxObject obj) => Object = obj;
}

/// <summary>
/// Semantic object graph built from an FBX token tree: typed objects, the Model hierarchy
/// (OO connections), animation bindings (OP connections), Properties70 lookup with
/// Definitions-template defaults, bind poses, and GlobalSettings.
/// </summary>
public sealed class FbxScene
{
    /// <summary>All parsed objects by id.</summary>
    public IReadOnlyDictionary<long, FbxObject> ObjectsById => _objectsById;

    /// <summary>All Model objects in document order.</summary>
    public IReadOnlyList<FbxObject> Models => _models;

    /// <summary>All animation stacks in document order.</summary>
    public IReadOnlyList<FbxAnimStack> Stacks => _stacks;

    /// <summary>World-space bind matrices from Pose/BindPose nodes, by model id (row-vector layout).</summary>
    public IReadOnlyDictionary<long, Matrix4x4> BindPose => _bindPose;

    /// <summary>GlobalSettings UnitScaleFactor: source-unit → centimeter factor (FBX default 1 = cm).</summary>
    public double UnitScaleFactor { get; private set; } = 1.0;

    /// <summary>GlobalSettings UpAxis (default 1 = Y).</summary>
    public int UpAxis { get; private set; } = 1;

    /// <summary>GlobalSettings UpAxisSign (default +1).</summary>
    public int UpAxisSign { get; private set; } = 1;

    /// <summary>GlobalSettings FrontAxis (default 2 = Z).</summary>
    public int FrontAxis { get; private set; } = 2;

    /// <summary>GlobalSettings FrontAxisSign (default +1).</summary>
    public int FrontAxisSign { get; private set; } = 1;

    /// <summary>GlobalSettings CoordAxis (default 0 = X).</summary>
    public int CoordAxis { get; private set; } = 0;

    /// <summary>GlobalSettings CoordAxisSign (default +1).</summary>
    public int CoordAxisSign { get; private set; } = 1;

    /// <summary>GlobalSettings OriginalUpAxis (-1 when not recorded).</summary>
    public int OriginalUpAxis { get; private set; } = -1;

    /// <summary>GlobalSettings TimeMode (FbxTime::EMode enum value; 0 = default mode).</summary>
    public int TimeMode { get; private set; }

    /// <summary>GlobalSettings CustomFrameRate (-1 when not recorded).</summary>
    public double CustomFrameRate { get; private set; } = -1.0;

    /// <summary>
    /// Native frame rate (fps) implied by <see cref="TimeMode"/> /
    /// <see cref="CustomFrameRate"/>. This is the rate the source timeline was authored at —
    /// external frame ranges (e.g. Unity <c>.meta</c> clipAnimations) are expressed in it.
    /// The default mode (0) and unknown values map to 30 fps (this importer's resample default).
    /// </summary>
    public double FrameRate => TimeModeFrameRate(TimeMode, CustomFrameRate);

    /// <summary>FbxTime::EMode → frames per second (eCustom uses the custom rate).</summary>
    internal static double TimeModeFrameRate(int timeMode, double customFrameRate) => timeMode switch
    {
        1 => 120.0,            // eFrames120
        2 => 100.0,            // eFrames100
        3 => 60.0,             // eFrames60
        4 => 50.0,             // eFrames50
        5 => 48.0,             // eFrames48
        6 => 30.0,             // eFrames30
        7 => 30.0,             // eFrames30Drop
        8 => 30.0 / 1.001,     // eNTSCDropFrame (29.97)
        9 => 30.0 / 1.001,     // eNTSCFullFrame (29.97)
        10 => 25.0,            // ePAL
        11 => 24.0,            // eFrames24
        12 => 1000.0,          // eFrames1000
        13 => 24.0 / 1.001,    // eFilmFullFrame (23.976)
        14 => customFrameRate > 0 ? customFrameRate : 30.0, // eCustom
        15 => 96.0,            // eFrames96
        16 => 72.0,            // eFrames72
        17 => 60.0 / 1.001,    // eFrames59dot94
        18 => 120.0 / 1.001,   // eFrames119dot88
        _ => 30.0,             // eDefaultMode / unknown
    };

    private readonly Dictionary<long, FbxObject> _objectsById = new();
    private readonly List<FbxObject> _models = new();
    private readonly List<FbxAnimStack> _stacks = new();
    private readonly Dictionary<long, Matrix4x4> _bindPose = new();
    private readonly Dictionary<string, Dictionary<string, FbxProperty70>> _templates = new();

    private FbxScene()
    {
    }

    /// <summary>
    /// Looks up a property on an object: its own Properties70 first, then the Definitions
    /// property template for its node type. Returns null when neither defines it.
    /// </summary>
    public FbxProperty70? FindProperty(FbxObject obj, string name)
    {
        if (obj.Properties.TryGetValue(name, out var own))
            return own;
        if (_templates.TryGetValue(obj.NodeType, out var template) &&
            template.TryGetValue(name, out var def))
            return def;
        return null;
    }

    /// <summary>Vector property with template fallback and a hard default.</summary>
    public Vector3 GetVector3(FbxObject obj, string name, Vector3 fallback)
    {
        var p = FindProperty(obj, name);
        return p is { Values.Count: >= 3 } ? p.GetVector3() : fallback;
    }

    /// <summary>Double property with template fallback and a hard default.</summary>
    public double GetDouble(FbxObject obj, string name, double fallback)
    {
        var p = FindProperty(obj, name);
        return p is { Values.Count: >= 1 } ? p.GetDouble() : fallback;
    }

    /// <summary>Int property with template fallback and a hard default.</summary>
    public int GetInt(FbxObject obj, string name, int fallback)
    {
        var p = FindProperty(obj, name);
        return p is { Values.Count: >= 1 } ? p.GetInt() : fallback;
    }

    /// <summary>Builds the semantic graph from a tokenized FBX document.</summary>
    public static FbxScene Build(FbxNode root)
    {
        ArgumentNullException.ThrowIfNull(root);
        var scene = new FbxScene();
        scene.ReadGlobalSettings(root.Child("GlobalSettings"));
        scene.ReadTemplates(root.Child("Definitions"));
        scene.ReadObjects(root.Child("Objects"));
        scene.ReadConnections(root.Child("Connections"));
        return scene;
    }

    // ------------------------------------------------------------------ objects

    private void ReadObjects(FbxNode? objects)
    {
        if (objects is null)
            return;

        long syntheticId = -1000; // for exotic files whose objects lack numeric ids

        foreach (var node in objects.Children)
        {
            // Standard 7x object layout: (id:int64, "Name\0\x01Class":string, "SubClass":string).
            long id;
            int nameProp;
            if (node.Properties.Count >= 1 && node.Properties[0] is long or int)
            {
                id = node.Prop<long>(0);
                nameProp = 1;
            }
            else
            {
                id = syntheticId--;
                nameProp = 0;
            }

            string name = "", subClass = "";
            if (node.Properties.Count > nameProp && node.Properties[nameProp] is string rawName)
                (name, _) = FbxNode.SplitName(rawName);
            if (node.Properties.Count > nameProp + 1 && node.Properties[nameProp + 1] is string sub)
                subClass = sub;

            var obj = new FbxObject(id, node.Name, name, subClass, node, ReadProperties70(node));
            _objectsById.TryAdd(obj.Id, obj);

            switch (node.Name)
            {
                case "Model":
                    _models.Add(obj);
                    break;
                case "Pose":
                    if (subClass == "BindPose")
                        ReadBindPose(node);
                    break;
            }
        }
    }

    private void ReadBindPose(FbxNode pose)
    {
        foreach (var poseNode in pose.ChildrenNamed("PoseNode"))
        {
            var nodeId = poseNode.Child("Node");
            var matrix = poseNode.Child("Matrix");
            if (nodeId is null || matrix is null)
                continue;

            double[] m = matrix.AsDoubleArray(0);
            if (m.Length < 16)
                continue;

            // FBX matrices are stored as 16 doubles with translation in elements 12..14 —
            // the same memory layout as System.Numerics row-vector matrices.
            _bindPose[nodeId.Prop<long>(0)] = new Matrix4x4(
                (float)m[0], (float)m[1], (float)m[2], (float)m[3],
                (float)m[4], (float)m[5], (float)m[6], (float)m[7],
                (float)m[8], (float)m[9], (float)m[10], (float)m[11],
                (float)m[12], (float)m[13], (float)m[14], (float)m[15]);
        }
    }

    private static IReadOnlyDictionary<string, FbxProperty70> ReadProperties70(FbxNode node)
    {
        var result = new Dictionary<string, FbxProperty70>(StringComparer.Ordinal);
        var block = node.Child("Properties70") ?? node.Child("Properties60");
        if (block is null)
            return result;

        foreach (var p in block.ChildrenNamed("P"))
        {
            if (p.Properties.Count < 2 || p.Properties[0] is not string name)
                continue;
            string type = p.Properties[1] as string ?? "";
            int valueStart = Math.Min(4, p.Properties.Count);
            var values = p.Properties.GetRange(valueStart, p.Properties.Count - valueStart);
            result[name] = new FbxProperty70(name, type, values);
        }
        return result;
    }

    // ------------------------------------------------------------------ templates

    private void ReadTemplates(FbxNode? definitions)
    {
        if (definitions is null)
            return;

        foreach (var objectType in definitions.ChildrenNamed("ObjectType"))
        {
            if (objectType.Properties.Count < 1 || objectType.Properties[0] is not string typeName)
                continue;
            var template = objectType.Child("PropertyTemplate");
            if (template is null)
                continue;
            _templates[typeName] = new Dictionary<string, FbxProperty70>(
                (Dictionary<string, FbxProperty70>)ReadProperties70(template), StringComparer.Ordinal);
        }
    }

    // ------------------------------------------------------------------ global settings

    private void ReadGlobalSettings(FbxNode? globalSettings)
    {
        if (globalSettings is null)
            return;

        var props = ReadProperties70(globalSettings);

        double D(string name, double fallback)
            => props.TryGetValue(name, out var p) && p.Values.Count > 0 ? p.GetDouble() : fallback;
        int I(string name, int fallback)
            => props.TryGetValue(name, out var p) && p.Values.Count > 0 ? p.GetInt() : fallback;

        UnitScaleFactor = D("UnitScaleFactor", 1.0);
        UpAxis = I("UpAxis", 1);
        UpAxisSign = I("UpAxisSign", 1);
        FrontAxis = I("FrontAxis", 2);
        FrontAxisSign = I("FrontAxisSign", 1);
        CoordAxis = I("CoordAxis", 0);
        CoordAxisSign = I("CoordAxisSign", 1);
        OriginalUpAxis = I("OriginalUpAxis", -1);
        TimeMode = I("TimeMode", 0);
        CustomFrameRate = D("CustomFrameRate", -1.0);
    }

    // ------------------------------------------------------------------ connections

    private void ReadConnections(FbxNode? connections)
    {
        if (connections is null)
            return;

        // First pass: typed object wiring that doesn't depend on other connections.
        // OO  child → parent:   Model→Model (hierarchy), AnimationLayer→AnimationStack,
        //                       AnimationCurveNode→AnimationLayer
        // OP  child → parent.property:
        //                       AnimationCurveNode→Model ("Lcl Translation"/"Lcl Rotation"/"Lcl Scaling")
        //                       AnimationCurve→AnimationCurveNode ("d|X"/"d|Y"/"d|Z")
        var layerToStack = new Dictionary<long, FbxObject>();        // layer id → stack object
        var curveNodeToLayers = new Dictionary<long, List<long>>();  // curve node id → layer ids
        var curveNodeTargets = new Dictionary<long, (long ModelId, string Property)>();
        var curveNodes = new Dictionary<long, FbxAnimCurveNode>();
        var stacksById = new Dictionary<long, FbxAnimStack>();

        foreach (var c in connections.ChildrenNamed("C"))
        {
            if (c.Properties.Count < 3 ||
                c.Properties[0] is not string kind ||
                c.Properties[1] is not (long or int) ||
                c.Properties[2] is not (long or int))
                continue;

            long srcId = c.Prop<long>(1); // child
            long dstId = c.Prop<long>(2); // parent
            if (!_objectsById.TryGetValue(srcId, out var src))
                continue;
            _objectsById.TryGetValue(dstId, out var dst); // dst id 0 = scene root (no object)

            if (kind == "OO")
            {
                if (src.NodeType == "Model" && dst?.NodeType == "Model")
                {
                    if (src.ModelParent is null) // first parent wins on instancing
                    {
                        src.ModelParent = dst;
                        dst.ModelChildren.Add(src);
                    }
                }
                else if (src.NodeType == "AnimationLayer" && dst?.NodeType == "AnimationStack")
                {
                    layerToStack[srcId] = dst;
                }
                else if (src.NodeType == "AnimationCurveNode" && dst?.NodeType == "AnimationLayer")
                {
                    if (!curveNodeToLayers.TryGetValue(srcId, out var layers))
                        curveNodeToLayers[srcId] = layers = new List<long>();
                    layers.Add(dstId);
                }
            }
            else if (kind == "OP" && c.Properties.Count >= 4 && c.Properties[3] is string property)
            {
                if (src.NodeType == "AnimationCurveNode" && dst?.NodeType == "Model")
                {
                    curveNodeTargets[srcId] = (dstId, property);
                }
                else if (src.NodeType == "AnimationCurve" && dst?.NodeType == "AnimationCurveNode")
                {
                    // Channel name "d|X" → axis 'X'.
                    char axis = property.Length > 0 ? property[^1] : '?';
                    if (axis is 'X' or 'Y' or 'Z')
                    {
                        if (!curveNodes.TryGetValue(dstId, out var cn))
                            curveNodes[dstId] = cn = new FbxAnimCurveNode(dst);
                        var curve = ReadCurve(src.Node);
                        if (curve is not null && !cn.Channels.ContainsKey(axis))
                            cn.Channels[axis] = curve;
                    }
                }
            }
        }

        // Second pass: assemble stacks (document order) with flattened bindings.
        foreach (var obj in _objectsById.Values)
        {
            if (obj.NodeType != "AnimationStack")
                continue;
            var stack = new FbxAnimStack(obj)
            {
                LocalStart = ReadTimeProperty(obj, "LocalStart"),
                LocalStop = ReadTimeProperty(obj, "LocalStop"),
            };
            stacksById[obj.Id] = stack;
            _stacks.Add(stack);
        }

        foreach (var (curveNodeId, target) in curveNodeTargets)
        {
            if (!curveNodeToLayers.TryGetValue(curveNodeId, out var layerIds))
                continue;
            if (!curveNodes.TryGetValue(curveNodeId, out var cn))
            {
                // Curve node with defaults but no connected curves still drives the property.
                if (!_objectsById.TryGetValue(curveNodeId, out var cnObj))
                    continue;
                cn = new FbxAnimCurveNode(cnObj);
            }

            foreach (var layerId in layerIds)
            {
                if (!layerToStack.TryGetValue(layerId, out var stackObj) ||
                    !stacksById.TryGetValue(stackObj.Id, out var stack))
                    continue;
                stack.Bindings.TryAdd((target.ModelId, target.Property), cn);
            }
        }
    }

    private long ReadTimeProperty(FbxObject obj, string name)
    {
        var p = FindProperty(obj, name);
        if (p is null || p.Values.Count < 1)
            return 0;
        try
        {
            return Convert.ToInt64(p.Values[0], System.Globalization.CultureInfo.InvariantCulture);
        }
        // ArithmeticException covers OverflowException, which is not s&box-whitelisted
        catch (Exception ex) when (ex is InvalidCastException or ArithmeticException or FormatException)
        {
            return 0;
        }
    }

    private static FbxAnimCurve? ReadCurve(FbxNode curveNode)
    {
        var timesNode = curveNode.Child("KeyTime");
        var valuesNode = curveNode.Child("KeyValueFloat") ?? curveNode.Child("KeyValue");
        if (timesNode is null || valuesNode is null)
            return null;

        long[] times = timesNode.AsLongArray(0);
        float[] values = valuesNode.AsFloatArray(0);
        if (times.Length == 0 || times.Length != values.Length)
            return null;
        return new FbxAnimCurve(times, values);
    }
}
notpointless.chomnr_humanoid_hand_retargeter / Target/AnimationDriverMesh.cs
Game library
namespace HumanoidHandRetargeter.Target;

/// <summary>Small hidden mesh required by SceneModel's animation owner.
/// Cube FBX layout from ufbx's public-domain maya_cube_7400_ascii fixture:
/// https://github.com/ufbx/ufbx/blob/master/data/maya_cube_7400_ascii.fbx
/// Used under ufbx LICENSE alternative B (public domain).</summary>
public static class AnimationDriverMesh
{
    public static readonly string Fbx = Text.Replace("\r\n", "\n").Replace("\n", "\r\n");
    private const string Text = """
; FBX 7.4.0 project file
; ----------------------------------------------------

FBXHeaderExtension:  {
	FBXHeaderVersion: 1003
	FBXVersion: 7400
	CreationTimeStamp:  {
		Version: 1000
		Year: 2020
		Month: 3
		Day: 31
		Hour: 21
		Minute: 36
		Second: 10
		Millisecond: 814
	}
	Creator: "FBX SDK/FBX Plugins version 2019.2"
	SceneInfo: "SceneInfo::GlobalInfo", "UserData" {
		Type: "UserData"
		Version: 100
		MetaData:  {
			Version: 100
			Title: ""
			Subject: ""
			Author: ""
			Keywords: ""
			Revision: ""
			Comment: ""
		}
		Properties70:  {
			P: "DocumentUrl", "KString", "Url", "", "animation_driver.fbx"
			P: "SrcDocumentUrl", "KString", "Url", "", "animation_driver.fbx"
			P: "Original", "Compound", "", ""
			P: "Original|ApplicationVendor", "KString", "", "", "Autodesk"
			P: "Original|ApplicationName", "KString", "", "", "Maya"
			P: "Original|ApplicationVersion", "KString", "", "", "201900"
			P: "Original|DateTime_GMT", "DateTime", "", "", "31/03/2020 18:36:10.811"
			P: "Original|FileName", "KString", "", "", "animation_driver.fbx"
			P: "LastSaved", "Compound", "", ""
			P: "LastSaved|ApplicationVendor", "KString", "", "", "Autodesk"
			P: "LastSaved|ApplicationName", "KString", "", "", "Maya"
			P: "LastSaved|ApplicationVersion", "KString", "", "", "201900"
			P: "LastSaved|DateTime_GMT", "DateTime", "", "", "31/03/2020 18:36:10.811"
			P: "Original|ApplicationActiveProject", "KString", "", "", "animation_driver.fbx"
		}
	}
}
GlobalSettings:  {
	Version: 1000
	Properties70:  {
		P: "UpAxis", "int", "Integer", "",1
		P: "UpAxisSign", "int", "Integer", "",1
		P: "FrontAxis", "int", "Integer", "",2
		P: "FrontAxisSign", "int", "Integer", "",1
		P: "CoordAxis", "int", "Integer", "",0
		P: "CoordAxisSign", "int", "Integer", "",1
		P: "OriginalUpAxis", "int", "Integer", "",1
		P: "OriginalUpAxisSign", "int", "Integer", "",1
		P: "UnitScaleFactor", "double", "Number", "",1
		P: "OriginalUnitScaleFactor", "double", "Number", "",1
		P: "AmbientColor", "ColorRGB", "Color", "",0,0,0
		P: "DefaultCamera", "KString", "", "", "Producer Perspective"
		P: "TimeMode", "enum", "", "",11
		P: "TimeProtocol", "enum", "", "",2
		P: "SnapOnFrameMode", "enum", "", "",0
		P: "TimeSpanStart", "KTime", "Time", "",1924423250
		P: "TimeSpanStop", "KTime", "Time", "",384884650000
		P: "CustomFrameRate", "double", "Number", "",-1
		P: "TimeMarker", "Compound", "", ""
		P: "CurrentTimeMarker", "int", "Integer", "",-1
	}
}

; Documents Description
;------------------------------------------------------------------

Documents:  {
	Count: 1
	Document: 1907658701024, "", "Scene" {
		Properties70:  {
			P: "SourceObject", "object", "", ""
			P: "ActiveAnimStackName", "KString", "", "", "Take 001"
		}
		RootNode: 0
	}
}

; Document References
;------------------------------------------------------------------

References:  {
}

; Object definitions
;------------------------------------------------------------------

Definitions:  {
	Version: 100
	Count: 6
	ObjectType: "GlobalSettings" {
		Count: 1
	}
	ObjectType: "AnimationStack" {
		Count: 1
		PropertyTemplate: "FbxAnimStack" {
			Properties70:  {
				P: "Description", "KString", "", "", ""
				P: "LocalStart", "KTime", "Time", "",0
				P: "LocalStop", "KTime", "Time", "",0
				P: "ReferenceStart", "KTime", "Time", "",0
				P: "ReferenceStop", "KTime", "Time", "",0
			}
		}
	}
	ObjectType: "AnimationLayer" {
		Count: 1
		PropertyTemplate: "FbxAnimLayer" {
			Properties70:  {
				P: "Weight", "Number", "", "A",100
				P: "Mute", "bool", "", "",0
				P: "Solo", "bool", "", "",0
				P: "Lock", "bool", "", "",0
				P: "Color", "ColorRGB", "Color", "",0.8,0.8,0.8
				P: "BlendMode", "enum", "", "",0
				P: "RotationAccumulationMode", "enum", "", "",0
				P: "ScaleAccumulationMode", "enum", "", "",0
				P: "BlendModeBypass", "ULongLong", "", "",0
			}
		}
	}
	ObjectType: "Geometry" {
		Count: 1
		PropertyTemplate: "FbxMesh" {
			Properties70:  {
				P: "Color", "ColorRGB", "Color", "",0.8,0.8,0.8
				P: "BBoxMin", "Vector3D", "Vector", "",0,0,0
				P: "BBoxMax", "Vector3D", "Vector", "",0,0,0
				P: "Primary Visibility", "bool", "", "",1
				P: "Casts Shadows", "bool", "", "",1
				P: "Receive Shadows", "bool", "", "",1
			}
		}
	}
	ObjectType: "Material" {
		Count: 1
		PropertyTemplate: "FbxSurfaceLambert" {
			Properties70:  {
				P: "ShadingModel", "KString", "", "", "Lambert"
				P: "MultiLayer", "bool", "", "",0
				P: "EmissiveColor", "Color", "", "A",0,0,0
				P: "EmissiveFactor", "Number", "", "A",1
				P: "AmbientColor", "Color", "", "A",0.2,0.2,0.2
				P: "AmbientFactor", "Number", "", "A",1
				P: "DiffuseColor", "Color", "", "A",0.8,0.8,0.8
				P: "DiffuseFactor", "Number", "", "A",1
				P: "Bump", "Vector3D", "Vector", "",0,0,0
				P: "NormalMap", "Vector3D", "Vector", "",0,0,0
				P: "BumpFactor", "double", "Number", "",1
				P: "TransparentColor", "Color", "", "A",0,0,0
				P: "TransparencyFactor", "Number", "", "A",0
				P: "DisplacementColor", "ColorRGB", "Color", "",0,0,0
				P: "DisplacementFactor", "double", "Number", "",1
				P: "VectorDisplacementColor", "ColorRGB", "Color", "",0,0,0
				P: "VectorDisplacementFactor", "double", "Number", "",1
			}
		}
	}
	ObjectType: "Model" {
		Count: 1
		PropertyTemplate: "FbxNode" {
			Properties70:  {
				P: "QuaternionInterpolate", "enum", "", "",0
				P: "RotationOffset", "Vector3D", "Vector", "",0,0,0
				P: "RotationPivot", "Vector3D", "Vector", "",0,0,0
				P: "ScalingOffset", "Vector3D", "Vector", "",0,0,0
				P: "ScalingPivot", "Vector3D", "Vector", "",0,0,0
				P: "TranslationActive", "bool", "", "",0
				P: "TranslationMin", "Vector3D", "Vector", "",0,0,0
				P: "TranslationMax", "Vector3D", "Vector", "",0,0,0
				P: "TranslationMinX", "bool", "", "",0
				P: "TranslationMinY", "bool", "", "",0
				P: "TranslationMinZ", "bool", "", "",0
				P: "TranslationMaxX", "bool", "", "",0
				P: "TranslationMaxY", "bool", "", "",0
				P: "TranslationMaxZ", "bool", "", "",0
				P: "RotationOrder", "enum", "", "",0
				P: "RotationSpaceForLimitOnly", "bool", "", "",0
				P: "RotationStiffnessX", "double", "Number", "",0
				P: "RotationStiffnessY", "double", "Number", "",0
				P: "RotationStiffnessZ", "double", "Number", "",0
				P: "AxisLen", "double", "Number", "",10
				P: "PreRotation", "Vector3D", "Vector", "",0,0,0
				P: "PostRotation", "Vector3D", "Vector", "",0,0,0
				P: "RotationActive", "bool", "", "",0
				P: "RotationMin", "Vector3D", "Vector", "",0,0,0
				P: "RotationMax", "Vector3D", "Vector", "",0,0,0
				P: "RotationMinX", "bool", "", "",0
				P: "RotationMinY", "bool", "", "",0
				P: "RotationMinZ", "bool", "", "",0
				P: "RotationMaxX", "bool", "", "",0
				P: "RotationMaxY", "bool", "", "",0
				P: "RotationMaxZ", "bool", "", "",0
				P: "InheritType", "enum", "", "",0
				P: "ScalingActive", "bool", "", "",0
				P: "ScalingMin", "Vector3D", "Vector", "",0,0,0
				P: "ScalingMax", "Vector3D", "Vector", "",1,1,1
				P: "ScalingMinX", "bool", "", "",0
				P: "ScalingMinY", "bool", "", "",0
				P: "ScalingMinZ", "bool", "", "",0
				P: "ScalingMaxX", "bool", "", "",0
				P: "ScalingMaxY", "bool", "", "",0
				P: "ScalingMaxZ", "bool", "", "",0
				P: "GeometricTranslation", "Vector3D", "Vector", "",0,0,0
				P: "GeometricRotation", "Vector3D", "Vector", "",0,0,0
				P: "GeometricScaling", "Vector3D", "Vector", "",1,1,1
				P: "MinDampRangeX", "double", "Number", "",0
				P: "MinDampRangeY", "double", "Number", "",0
				P: "MinDampRangeZ", "double", "Number", "",0
				P: "MaxDampRangeX", "double", "Number", "",0
				P: "MaxDampRangeY", "double", "Number", "",0
				P: "MaxDampRangeZ", "double", "Number", "",0
				P: "MinDampStrengthX", "double", "Number", "",0
				P: "MinDampStrengthY", "double", "Number", "",0
				P: "MinDampStrengthZ", "double", "Number", "",0
				P: "MaxDampStrengthX", "double", "Number", "",0
				P: "MaxDampStrengthY", "double", "Number", "",0
				P: "MaxDampStrengthZ", "double", "Number", "",0
				P: "PreferedAngleX", "double", "Number", "",0
				P: "PreferedAngleY", "double", "Number", "",0
				P: "PreferedAngleZ", "double", "Number", "",0
				P: "LookAtProperty", "object", "", ""
				P: "UpVectorProperty", "object", "", ""
				P: "Show", "bool", "", "",1
				P: "NegativePercentShapeSupport", "bool", "", "",1
				P: "DefaultAttributeIndex", "int", "Integer", "",-1
				P: "Freeze", "bool", "", "",0
				P: "LODBox", "bool", "", "",0
				P: "Lcl Translation", "Lcl Translation", "", "A",0,0,0
				P: "Lcl Rotation", "Lcl Rotation", "", "A",0,0,0
				P: "Lcl Scaling", "Lcl Scaling", "", "A",1,1,1
				P: "Visibility", "Visibility", "", "A",1
				P: "Visibility Inheritance", "Visibility Inheritance", "", "",1
			}
		}
	}
}

; Object properties
;------------------------------------------------------------------

Objects:  {
	Geometry: 1907663073408, "Geometry::", "Mesh" {
		Vertices: *24 {
			a: -0.5,-0.5,0.5,0.5,-0.5,0.5,-0.5,0.5,0.5,0.5,0.5,0.5,-0.5,0.5,-0.5,0.5,0.5,-0.5,-0.5,-0.5,-0.5,0.5,-0.5,-0.5
		}
		PolygonVertexIndex: *24 {
			a: 0,1,3,-3,2,3,5,-5,4,5,7,-7,6,7,1,-1,1,7,5,-4,6,0,2,-5
		}
		Edges: *12 {
			a: 0,2,6,10,3,1,7,5,11,9,15,13
		}
		GeometryVersion: 124
		LayerElementNormal: 0 {
			Version: 102
			Name: ""
			MappingInformationType: "ByPolygonVertex"
			ReferenceInformationType: "Direct"
			Normals: *72 {
				a: 0,0,1,0,0,1,0,0,1,0,0,1,0,1,0,0,1,0,0,1,0,0,1,0,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,1,0,0,1,0,0,1,0,0,1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0
			}
			NormalsW: *24 {
				a: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
			}
		}
		LayerElementBinormal: 0 {
			Version: 102
			Name: "map1"
			MappingInformationType: "ByPolygonVertex"
			ReferenceInformationType: "Direct"
			Binormals: *72 {
				a: 0,1,-0,0,1,-0,0,1,-0,0,1,-0,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,0,1,0,0,1,0,0,1,0,0,1,-0,1,0,-0,1,0,0,1,-0,-0,1,0,0,1,0,0,1,0,0,1,0,0,1,0
			}
			BinormalsW: *24 {
				a: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
			}

		}
		LayerElementTangent: 0 {
			Version: 102
			Name: "map1"
			MappingInformationType: "ByPolygonVertex"
			ReferenceInformationType: "Direct"
			Tangents: *72 {
				a: 1,-0,-0,1,-0,0,1,-0,0,1,-0,0,1,-0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,-0,1,0,-0,1,0,-0,1,0,-0,0,0,-1,0,0,-1,0,-0,-1,0,0,-1,0,-0,1,0,-0,1,0,-0,1,0,-0,1
			}
			TangentsW: *24 {
				a: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
			}
		}
		LayerElementUV: 0 {
			Version: 101
			Name: "map1"
			MappingInformationType: "ByPolygonVertex"
			ReferenceInformationType: "IndexToDirect"
			UV: *28 {
				a: 0.375,0,0.625,0,0.375,0.25,0.625,0.25,0.375,0.5,0.625,0.5,0.375,0.75,0.625,0.75,0.375,1,0.625,1,0.875,0,0.875,0.25,0.125,0,0.125,0.25
			}
			UVIndex: *24 {
				a: 0,1,3,2,2,3,5,4,4,5,7,6,6,7,9,8,1,10,11,3,12,0,2,13
			}
		}
		LayerElementSmoothing: 0 {
			Version: 102
			Name: ""
			MappingInformationType: "ByEdge"
			ReferenceInformationType: "Direct"
			Smoothing: *12 {
				a: 0,0,0,0,0,0,0,0,0,0,0,0
			}
		}
		LayerElementMaterial: 0 {
			Version: 101
			Name: ""
			MappingInformationType: "AllSame"
			ReferenceInformationType: "IndexToDirect"
			Materials: *1 {
				a: 0
			}
		}
		Layer: 0 {
			Version: 100
			LayerElement:  {
				Type: "LayerElementNormal"
				TypedIndex: 0
			}
			LayerElement:  {
				Type: "LayerElementBinormal"
				TypedIndex: 0
			}
			LayerElement:  {
				Type: "LayerElementTangent"
				TypedIndex: 0
			}
			LayerElement:  {
				Type: "LayerElementMaterial"
				TypedIndex: 0
			}
			LayerElement:  {
				Type: "LayerElementSmoothing"
				TypedIndex: 0
			}
			LayerElement:  {
				Type: "LayerElementUV"
				TypedIndex: 0
			}
		}
	}
	Model: 1907292236800, "Model::animation_driver", "Mesh" {
		Version: 232
		Properties70:  {
			P: "RotationActive", "bool", "", "",1
			P: "InheritType", "enum", "", "",1
			P: "ScalingMax", "Vector3D", "Vector", "",0,0,0
			P: "DefaultAttributeIndex", "int", "Integer", "",0
			P: "currentUVSet", "KString", "", "U", "map1"
		}
		Shading: T
		Culling: "CullingOff"
	}
	Material: 1908464858960, "Material::lambert1", "" {
		Version: 102
		ShadingModel: "lambert"
		MultiLayer: 0
		Properties70:  {
			P: "AmbientColor", "Color", "", "A",0,0,0
			P: "DiffuseColor", "Color", "", "A",0.5,0.5,0.5
			P: "DiffuseFactor", "Number", "", "A",0.800000011920929
			P: "TransparencyFactor", "Number", "", "A",1
			P: "Emissive", "Vector3D", "Vector", "",0,0,0
			P: "Ambient", "Vector3D", "Vector", "",0,0,0
			P: "Diffuse", "Vector3D", "Vector", "",0.400000005960464,0.400000005960464,0.400000005960464
			P: "Opacity", "double", "Number", "",1
		}
	}
	AnimationStack: 1907674289488, "AnimStack::Take 001", "" {
		Properties70:  {
			P: "LocalStart", "KTime", "Time", "",1924423250
			P: "LocalStop", "KTime", "Time", "",230930790000
			P: "ReferenceStart", "KTime", "Time", "",1924423250
			P: "ReferenceStop", "KTime", "Time", "",230930790000
		}
	}
	AnimationLayer: 1907631731008, "AnimLayer::BaseLayer", "" {
	}
}

; Object connections
;------------------------------------------------------------------

Connections:  {

	;Model::animation_driver, Model::RootNode
	C: "OO",1907292236800,0

	;AnimLayer::BaseLayer, AnimStack::Take 001
	C: "OO",1907631731008,1907674289488

	;Geometry::, Model::animation_driver
	C: "OO",1907663073408,1907292236800

	;Material::lambert1, Model::animation_driver
	C: "OO",1908464858960,1907292236800
}
;Takes section
;----------------------------------------------------

Takes:  {
	Current: "Take 001"
	Take: "Take 001" {
		FileName: "Take_001.tak"
		LocalTime: 1924423250,230930790000
		ReferenceTime: 1924423250,230930790000
	}
}

""";
}
notpointless.chomnr_humanoid_hand_retargeter / Target/HandModelFactory.cs
Game library
#nullable enable
namespace HumanoidHandRetargeter.Target;

/// <summary>Minimal hand ModelDoc using the audited VmdlWriter mesh and scaling node shapes.</summary>
public static class HandModelFactory
{
    public const string Header = "<!-- kv3 encoding:text:version{e21c7f3c-8a33-41c5-9977-a76d3a32aa0d} format:modeldoc30:version{8c2d7a91-9c42-4bf0-883a-5a3b1762d4f1} -->";
    public static string Create(string baseModel = "", string mesh = "", float meshUnitScaleCm = 1, IReadOnlyDictionary<string,string>? materialRemaps = null, IReadOnlyList<string>? meshNames = null)
    {
        if (!float.IsFinite(meshUnitScaleCm) || meshUnitScaleCm <= 0) throw new ArgumentOutOfRangeException(nameof(meshUnitScaleCm));
        if (string.IsNullOrWhiteSpace(baseModel) == string.IsNullOrWhiteSpace(mesh)) throw new ArgumentException("Choose one base model or mesh source.");
        var children = new KvArray();
        if (mesh.Length > 0)
        {
            mesh = VmdlSetupService.NormalizeAssetPath(mesh, ".fbx");
            var meshes = new KvArray();
            meshes.Items.Add(new KvObject { ["_class"] = new KvString("RenderMeshFile"), ["name"] = new KvString("hands_mesh"),
                ["filename"] = new KvString(mesh), ["import_scale"] = new KvDouble(meshUnitScaleCm),
                ["import_translation"] = Vector(0,0,0), ["import_rotation"] = Vector(0,0,0),
                ["align_origin_x_type"] = new KvString("None"), ["align_origin_y_type"] = new KvString("None"), ["align_origin_z_type"] = new KvString("None") });
            if(meshNames is {Count:>0})
            {
                var names=new KvArray();foreach(var name in meshNames)names.Items.Add(new KvString(name));
                ((KvObject)meshes.Items[0])["import_filter"]=new KvObject
                    {["exclude_by_default"]=new KvBool(true),["exception_list"]=names};
            }
            children.Items.Add(new KvObject { ["_class"] = new KvString("RenderMeshList"), ["children"] = meshes });
            children.Items.Add(new KvObject { ["_class"] = new KvString("BoneMarkupList"), ["bone_cull_type"] = new KvString("None"), ["children"] = new KvArray() });
            var remaps=new KvArray();
            if(materialRemaps is not null)foreach(var pair in materialRemaps)remaps.Items.Add(new KvObject{["from"]=new KvString(pair.Key),["to"]=new KvString(pair.Value)});
            var materials = new KvArray();
            materials.Items.Add(new KvObject { ["_class"] = new KvString("DefaultMaterialGroup"), ["use_global_default"] = new KvBool(materialRemaps is null || materialRemaps.Count==0),
                ["global_default_material"] = new KvString("materials/dev/reflectivity_50.vmat"), ["remaps"] = remaps });
            children.Items.Add(new KvObject { ["_class"] = new KvString("MaterialGroupList"), ["children"] = materials });
        }
        else baseModel = VmdlSetupService.NormalizeAssetPath(baseModel, ".vmdl");
        var modifiers = new KvArray();
        modifiers.Items.Add(new KvObject { ["_class"] = new KvString("ModelModifier_ScaleAndMirror"), ["scale"] = new KvDouble(1.0 / 2.54),
            ["mirror_x"] = new KvBool(false), ["mirror_y"] = new KvBool(false), ["mirror_z"] = new KvBool(false),
            ["flip_bone_forward"] = new KvBool(false), ["swap_left_and_right_bones"] = new KvBool(false) });
        children.Items.Add(new KvObject { ["_class"] = new KvString("ModelModifierList"), ["children"] = modifiers });
        var root = new KvObject { ["_class"] = new KvString("RootNode"), ["children"] = children, ["base_model_name"] = new KvString(baseModel),
            ["anim_graph_name"] = new KvString(""), ["model_archetype"] = new KvString(""), ["primary_associated_entity"] = new KvString("") };
        return Kv3.Serialize(new Kv3Document(Header, new KvObject { ["rootNode"] = root }));
    }

    private static KvArray Vector(params double[] values) { var array = new KvArray(); foreach (var value in values) array.Items.Add(new KvDouble(value)); return array; }
}
notpointless.chomnr_humanoid_hand_retargeter / Editor/HumanoidHandRetargeter/HandPreviewDialog.cs
Editor library
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;

namespace HumanoidHandRetargeter.Editor;

/// <summary>Legacy PreviewDialog layout, with Third Person/FPS camera controls.</summary>
public sealed class HandPreviewDialog : Dialog
{
    public HandPreviewWidget Preview {get;}
    public Action? Confirmed {get;set;}
    public HandPreviewDialog(Widget? parent,HandTarget target,IReadOnlyList<HandBakedClip> clips) : base(parent)
    {
        Window.WindowTitle="Preview - "+System.IO.Path.GetFileName(target.ModelPath);Window.SetWindowIcon("preview");Window.SetModal(true,true);
        Window.MinimumWidth=560;Window.MinimumHeight=560;Window.Size=new Vector2(760,780);
        Layout=Layout.Column();Layout.Margin=12;Layout.Spacing=8;
        Preview=Layout.Add(new HandPreviewWidget(this,target),1);
        var error=Layout.Add(new Label(this){WordWrap=true,Visible=false});error.SetStyles($"color:{Theme.Red.Hex};");
        var modeRow=Layout.AddRow();modeRow.Spacing=8;
        var modes=modeRow.Add(new ComboBox(this){MinimumWidth=150});
        modes.AddItem("Third Person","3d_rotation",()=>Preview.Mode=HandPreviewMode.ThirdPerson,selected:true);
        modes.AddItem("FPS","first_page",()=>Preview.Mode=HandPreviewMode.Fps);
        modeRow.Add(new Button("Reset camera","center_focus_strong"){Clicked=Preview.ResetCamera});
        modeRow.Add(new Button("−"){Clicked=()=>Preview.Zoom(1.2f)});modeRow.Add(new Button("+"){Clicked=()=>Preview.Zoom(.8f)});
        modeRow.Add(new Button("Weapon model…","view_in_ar"){Clicked=()=>{var picker=AssetPicker.Create(this,AssetType.Model);picker.OnAssetPicked=assets=>{var a=assets.FirstOrDefault();if(a is not null){try{Preview.SetWeapon(a.Path);error.Visible=false;}catch(Exception ex){error.Text=ex.Message;error.Visible=true;}}};picker.Show();}});
        var cameraRow=Layout.AddRow();cameraRow.Spacing=6;
        var authored=cameraRow.Add(new Checkbox("Authored camera motion"){Value=true});authored.Clicked=()=>Preview.UseAuthoredCamera=authored.Value;
        cameraRow.Add(new Label(this){Text="FOV:"});var fov=cameraRow.Add(new LineEdit(this){Text="75",FixedWidth=42});
        cameraRow.Add(new Label(this){Text="FPS offset X / Y / Z:"});var offsets=Enumerable.Range(0,3).Select(_=>cameraRow.Add(new LineEdit(this){Text="0",FixedWidth=42})).ToArray();
        cameraRow.Add(new Button("Apply"){Clicked=()=>{if(float.TryParse(fov.Text,out var f)&&float.IsFinite(f))Preview.FpsFov=f;var values=offsets.Select(e=>float.TryParse(e.Text,out var v)&&float.IsFinite(v)?v:0).ToArray();Preview.FpsOffset=new Vector3(values[0],values[1],values[2]);Preview.ApplyCurrentFrame();}});
        var clipRow=Layout.AddRow();clipRow.Spacing=8;clipRow.Add(new Label(this){Text="Clip:"});var combo=clipRow.Add(new ComboBox(this){MinimumWidth=220},1);
        for(var i=0;i<clips.Count;i++){var clip=clips[i];combo.AddItem(clip.Baked.Name,"movie",()=>Preview.SetClip(clip),selected:i==0);}
        var transport=Layout.AddRow();transport.Spacing=8;
        var play=transport.Add(new Button("","pause"){FixedWidth=28,FixedHeight=24});play.Clicked=()=>{Preview.Playing=!Preview.Playing;play.Icon=Preview.Playing?"pause":"play_arrow";};
        var slider=transport.Add(new FloatSlider(this),1);slider.Minimum=0;slider.OnValueEdited=()=>Preview.Scrub((int)slider.Value);
        var label=transport.Add(new Label(this){Text="0 / 0",FixedWidth=80});
        var ghost=transport.Add(new Button("Show source","compare"){IsToggle=true,FixedHeight=24});ghost.Clicked=()=>Preview.ShowSource=ghost.IsChecked;
        var skeleton=transport.Add(new Button("Skeleton","polyline"){IsToggle=true,FixedHeight=24});skeleton.Clicked=()=>Preview.SkeletonOnly=skeleton.IsChecked;
        Preview.FrameChanged=frame=>{if(modes.CurrentIndex!=(int)Preview.Mode)modes.CurrentIndex=(int)Preview.Mode;play.Icon=Preview.Playing?"pause":"play_arrow";slider.Maximum=Math.Max(0,Preview.FrameCount-1);slider.Value=frame;label.Text=$"{frame+1} / {Preview.FrameCount}";authored.Enabled=Preview.HasAuthoredCamera;};
        var footer=Layout.AddRow();footer.Spacing=8;footer.AddStretchCell();footer.Add(new Button("Cancel"){Clicked=Close});
        footer.Add(new Button.Primary("Looks good - Convert"){Icon="check",Tint=Theme.Green,Clicked=()=>{Confirmed?.Invoke();Close();}});
        if(clips.Count>0)Preview.SetClip(clips[0]);
    }
}
notpointless.chomnr_humanoid_hand_retargeter / Code/Core/Mapping/BoneNameTokens.cs
Game library
// Tokenization reused from humanoid-retargeter 26084c96c3fc870aaf9a5bd798de063ce2fd62df.
using System.Text;
namespace HumanoidHandRetargeter.Mapping;
internal static class BoneNameTokens
{
    internal static List<string> Tokenize(string name)
    {
        var tokens = new List<string>();
        var current = new StringBuilder();
        char previous = '\0';
        for (var i = 0; i < name.Length; i++)
        {
            var c = name[i];
            if (!char.IsLetterOrDigit(c))
            {
                Flush();
                previous = '\0';
                continue;
            }
            if (current.Length > 0)
            {
                var boundary = char.IsDigit(c) != char.IsDigit(previous)
                    || (char.IsUpper(c) && char.IsLower(previous))
                    // Acronym/single-side prefix before a Pascal word: LThumb → L, Thumb;
                    // FBXNode → FBX, Node. Without this, one-joint BVH thumbs lose both
                    // their side and their otherwise unambiguous finger name.
                    || (char.IsUpper(c) && char.IsUpper(previous)
                        && i + 1 < name.Length && char.IsLower(name[i + 1]));
                if (boundary)
                    Flush();
            }
            current.Append(char.ToLowerInvariant(c));
            previous = c;
        }
        Flush();
        return tokens;

        void Flush()
        {
            if (current.Length > 0)
            {
                tokens.Add(current.ToString());
                current.Clear();
            }
        }
    }

}
notpointless.chomnr_humanoid_hand_retargeter / Code/Core/Mapping/HandDetectionProfiles.SplitArms.cs
Game library
#nullable enable
using Vec = System.Numerics.Vector3;
using Skel = HumanoidHandRetargeter.Skeleton.Skeleton;

namespace HumanoidHandRetargeter.Mapping;

internal static partial class HandDetectionProfiles
{
    // Split biceps/forearms, a wrist-to-palm link, and five palm-rooted rays.
    // Numeric/duplicate suffixes identify objects, not sides or anatomical fingers.
    private static void ResolveUnlabelledSplitArms(Skel rig,Hint?[] hints)
    {
        string Name(int i)
        {
            var name=rig[i].Name[(rig[i].Name.LastIndexOf(':')+1)..];
            return Key(name.Split('#')[0].Replace("_duplicate","",StringComparison.OrdinalIgnoreCase)).TrimEnd("0123456789".ToCharArray());
        }
        int Child(int i,string name)=>rig.ChildrenOf(i).Count==1&&Name(rig.ChildrenOf(i)[0])==name?rig.ChildrenOf(i)[0]:-1;
        var proposals=new List<(HandSide Side,Dictionary<int,string> Roles)>();
        foreach(var upper in rig.Bones.Where(b=>Name(b.Index) is "bisep" or "bicep" or "biceps"))
        {
            var upperSplit=Child(upper.Index,Name(upper.Index));if(upperSplit<0)continue;
            var lower=Child(upperSplit,"forearm");if(lower<0)continue;
            var lowerSplit=Child(lower,"forearm");if(lowerSplit<0)continue;
            var wrist=Child(lowerSplit,"wrist");if(wrist<0)continue;
            var palm=Child(wrist,"hand");if(palm<0||rig.ChildrenOf(palm).Count!=5)continue;
            var rays=new List<int[]>();
            foreach(var root in rig.ChildrenOf(palm))
            {
                var ray=new List<int>{root};var current=root;
                while(rig.ChildrenOf(current).Count==1){current=rig.ChildrenOf(current)[0];ray.Add(current);}
                if(rig.ChildrenOf(current).Count!=0){rays.Clear();break;}
                if(Name(ray[^1]).Contains("end",StringComparison.Ordinal))ray.RemoveAt(ray.Count-1);
                rays.Add(ray.ToArray());
            }
            if(rays.Count!=5)continue;
            var thumbs=rays.Where(r=>r.Length==3&&Name(r[0]) is "thum" or "thumb").ToArray();
            var fingers=rays.Where(r=>r.Length==4&&Name(r[0])=="finger").ToArray();
            if(thumbs.Length!=1||fingers.Length!=4)continue;
            var thumb=thumbs[0];
            Vec Pos(int i)=>rig.RestWorld[i].Pos;
            var ordered=fingers.OrderBy(r=>Vec.DistanceSquared(Pos(r[1]),Pos(thumb[1]))).ToArray();
            var radial=Pos(ordered[0][1])-Pos(ordered[3][1]);
            if(radial.LengthSquared()<1e-8f)continue;
            radial=Vec.Normalize(radial);
            // Require four distinctly ordered knuckles, not just a nearest-thumb guess.
            var spacing=ordered.Select(r=>Vec.Dot(Pos(r[1])-Pos(ordered[3][1]),radial)).ToArray();
            if(Enumerable.Range(0,3).Any(i=>spacing[i]-spacing[i+1]<spacing[0]*.1f))continue;
            var forward=ordered.Select(r=>Pos(r[3])-Pos(r[1])).Aggregate(Vec.Zero,(a,b)=>a+b);
            var normal=Vec.Cross(forward,radial);var thumbBend=Pos(thumb[2])-Pos(thumb[1]);
            if(normal.LengthSquared()<1e-8f||thumbBend.LengthSquared()<1e-8f)continue;
            var chirality=Vec.Dot(Vec.Normalize(normal),Vec.Normalize(thumbBend));
            // An out-of-plane thumb bend supplies the palmar direction. Flat or
            // ambiguous geometry remains for review rather than guessing a side.
            if(MathF.Abs(chirality)<.25f)continue;
            var side=chirality>0?HandSide.Left:HandSide.Right;
            var roles=new Dictionary<int,string>{{upper.Index,"UpperArm"},{lower,"LowerArm"},{wrist,"Hand"},{palm,"PalmMeta"}};
            var digitNames=new[]{"Index","Middle","Ring","Pinky"};
            for(var digit=0;digit<4;digit++)
                for(var joint=0;joint<4;joint++)roles[ordered[digit][joint]]=digitNames[digit]+(joint==0?"Meta":"Prox");
            foreach(var bone in thumb)roles[bone]="ThumbProx";
            if(roles.Keys.Any(i=>hints[i] is not null))continue;
            proposals.Add((side,roles));
        }
        // Require a complete, disjoint bilateral pair before using unnamed sides.
        if(proposals.Count!=2||proposals[0].Side==proposals[1].Side||proposals[0].Roles.Keys.Intersect(proposals[1].Roles.Keys).Any())return;
        foreach(var proposal in proposals)
            foreach(var role in proposal.Roles)hints[role.Key]=new(role.Value,proposal.Side,"split_arm_palm_rays");
    }
}
notpointless.chomnr_humanoid_hand_retargeter / Editor/HumanoidHandRetargeter/HandEditorPipeline.cs
Editor library
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Editor;
using Sandbox;
using HumanoidHandRetargeter.Calibration;
using HumanoidHandRetargeter.Formats.Dmx;
using HumanoidHandRetargeter.Formats.Fbx;
using HumanoidHandRetargeter.Mapping;
using HumanoidHandRetargeter.Maths;
using HumanoidHandRetargeter.Retargeting;
using HumanoidHandRetargeter.Skeleton;
using HumanoidHandRetargeter.Target;
using Skel = HumanoidHandRetargeter.Skeleton.Skeleton;
using Vec = System.Numerics.Vector3;
using Quat = System.Numerics.Quaternion;

namespace HumanoidHandRetargeter.Editor;

public sealed class HandSource
{
    public required string Path { get; init; }
    public required SourceScene Scene { get; init; }
    public required HandMappingResult Mapping { get; set; }
    public string? ModelPath { get; init; }
    public IReadOnlyDictionary<HandSide, Quat>? PalmFrames { get; set; }
}

public sealed class HandTarget
{
    public IReadOnlyList<string> ImportNotes { get; init; } = Array.Empty<string>();
    public required string ModelPath { get; init; }
    public required Skel Skeleton { get; init; }
    public required HandMappingResult Mapping { get; set; }
    public IReadOnlyDictionary<HandSide, Quat>? PalmFrames { get; set; }
}

public sealed record HandExportResult(string ModelPath,IReadOnlyList<string> Changes,string? BackupPath,IReadOnlyList<string>? WeaponPrefabs=null);

public sealed record HandBakedClip(HandSource Source, Clip Original, Clip Baked, IReadOnlyList<string> Notes,Vec? WeaponOffset=null,HandMotionOptions? MotionOptions=null);

/// <summary>Editor adapter over the pure importer, shared bake and transactional setup.
/// Engine calls use the same main-thread dispatch convention as the audited EditorPipeline.</summary>
public static class HandEditorPipeline
{
    public const string HumanArms = "models/first_person/v_first_person_arms_human.vmdl";
    public const string CitizenArms = "models/first_person/v_first_person_arms_citizen.vmdl";
    public static string Assets => Project.Current?.GetAssetsPath() ?? throw new InvalidOperationException("Open an s&box project first.");
    public static MainThreadAwaitable MainThread() => default;
    public readonly struct MainThreadAwaitable : INotifyCompletion
    {
        public MainThreadAwaitable GetAwaiter() => this;
        public bool IsCompleted => ThreadSafe.IsMainThread;
        public void OnCompleted(Action continuation) => Sandbox.MainThread.Queue(continuation);
        public void GetResult() { }
    }

    public static Transform ToEngine(XForm value)
        => new(new Vector3(value.Pos.X, value.Pos.Y, value.Pos.Z) / 2.54f, new Rotation(value.Rot.X,value.Rot.Y,value.Rot.Z,value.Rot.W));
    public static XForm FromEngine(Transform value)
        => new(new Vec(value.Position.x,value.Position.y,value.Position.z)*2.54f, new Quat(value.Rotation.x,value.Rotation.y,value.Rotation.z,value.Rotation.w));

    public static Skel ReadSkeleton(Model model)
    {
        return Skel.Create(model.Bones.AllBones.Select(b => {
            var world = FromEngine(b.LocalTransform);
            return new BoneDefinition(b.Name,b.Parent?.Name,b.Parent is null ? world : XForm.ToLocal(FromEngine(b.Parent.LocalTransform),world));
        }).ToArray());
    }

    public static async Task<HandTarget> LoadTargetAsync(string path, CancellationToken cancel = default)
    {
        await MainThread();
        var importNotes=new List<string>();
        if (path.EndsWith(".fbx",StringComparison.OrdinalIgnoreCase)) path = await PrepareMeshAsync(path,cancel,importNotes);
        path = ModelPath(path);
        var model = Model.Load(path);
        if (model is null || model.IsError || model.BoneCount == 0) throw new InvalidOperationException($"Cannot load a skinned model from '{path}'.");

        var skeleton = ReadSkeleton(model);
        return new HandTarget { ImportNotes=importNotes, ModelPath=path, Skeleton=skeleton, PalmFrames=HandPresetStore.LoadPalms(skeleton), Mapping=HandPresetStore.Load(skeleton) ?? HandRigDetector.Detect(skeleton) };
    }

    public static async Task<HandSource> LoadSourceAsync(string path, float fps = 30, CancellationToken cancel = default)
    {
        if (path.EndsWith(".fbx",StringComparison.OrdinalIgnoreCase))
        {
            var native = await Task.Run(()=>FbxImporter.Import(File.ReadAllBytes(path),new FbxImportOptions {SampleFps=fps}),cancel);
            await MainThread();
            // Share the target's X-forward/Z-up view space so the source grip, eye and weapon
            // motion transfer directly, whatever axes or facing the file was authored in.
            var nativeMapping=HandPresetStore.Load(native.Skeleton);
            var axesMapping=nativeMapping ?? HandRigDetector.Detect(native.Skeleton);
            var turn=HandViewSpace.ViewRotation(native,axesMapping);
            var scene=HandViewSpace.ToViewAxes(native,axesMapping);
            // Mappings are index based; palm frames saved against the native axes turn with the rig.
            var palms=HandPresetStore.LoadPalms(scene.Skeleton) ?? HandPresetStore.LoadPalms(native.Skeleton)?.ToDictionary(p=>p.Key,p=>Quat.Normalize(turn*p.Value));
            return new() { Path=path, Scene=scene, PalmFrames=palms, Mapping=HandPresetStore.Load(scene.Skeleton) ?? nativeMapping ?? HandRigDetector.Detect(scene.Skeleton) };
        }
        await MainThread();
        path = ModelPath(path);
        var model = Model.Load(path);
        if (model is null || model.IsError) throw new InvalidOperationException($"Cannot load source model '{path}'.");
        var skeleton = ReadSkeleton(model);
        var clips = new List<Clip>();
        using var sampling = new ModelSampler(model,skeleton);
        foreach (var name in model.AnimationNames.ToArray())
        {
            cancel.ThrowIfCancellationRequested();
            clips.Add(sampling.Sample(name,fps,cancel));
            await Task.Delay(1,cancel); await MainThread();
        }
        // Facepunch weapons author recoil as additive layers. Capture their complete firing
        // pose through the original weapon graph instead of retargeting a delta as a full pose.
        if(skeleton.IndexOf("weapon_root")>=0&&!clips.Any(c=>WeaponAnimGraph.Classify(c.Name)==WeaponAction.Fire)
            &&clips.FirstOrDefault(c=>c.Name.StartsWith("Fire_",StringComparison.OrdinalIgnoreCase)&&c.Name.EndsWith("_delta",StringComparison.OrdinalIgnoreCase)
                &&!c.Name.Contains("Hold",StringComparison.OrdinalIgnoreCase)&&!c.Name.Contains("Dry",StringComparison.OrdinalIgnoreCase)) is {} recoil)
        {
            var firing=sampling.SampleFire(Math.Max(.25f,(recoil.FrameCount-1)/recoil.Fps),fps,cancel);
            if(firing is not null)clips.Add(firing);
        }
        return new() { Path=path,ModelPath=path, Scene=new SourceScene(skeleton,clips,2.54f,upAxis:2,frontAxis:0,coordAxis:1),
            PalmFrames=HandPresetStore.LoadPalms(skeleton), Mapping=HandPresetStore.Load(skeleton) ?? HandRigDetector.Detect(skeleton) };
    }

    public static HandRetargetProfile Calibrate(HandSource source,HandTarget target)
        => HandRetargetProfile.Calibrate(source.Scene.Skeleton,source.Mapping,target.Skeleton,target.Mapping,source.PalmFrames,target.PalmFrames);

    public static HandBakedClip Bake(HandSource source, Clip clip, HandTarget target, HandMotionOptions options, CancellationToken cancel = default, HandRetargetProfile? profile = null)
    {
        profile ??= Calibrate(source,target);
        // Compiled s&box models already share X-forward/Z-up coordinates. A wrist's
        // bind rotation must not turn vertical reload travel into sideways/downward motion.
        var weaponOffset=options.WeaponSpaceOffset;
        // FBX sources are loaded in view space, so every weapon file (named weapon bones or not)
        // keeps both palms locked to the source grip relative to the eye.
        var viewSpace=source.ModelPath is not null||source.Scene.UpAxis==2&&source.Scene.FrontAxis==0;
        if(options.PreserveWeaponGrip&&options.TransferWristPosition&&(source.ModelPath is null&&viewSpace
            ||source.Scene.Skeleton.Bones.Any(b=>b.Name.Equals("weapon_root",StringComparison.OrdinalIgnoreCase))))
            weaponOffset ??= HandViewSpace.EyePosition(target.Skeleton,target.Mapping)-HandViewSpace.EyePosition(source.Scene.Skeleton,source.Mapping);
        options=new HandMotionOptions{TransferWristPosition=options.TransferWristPosition,ScaleWristTravel=options.ScaleWristTravel,
            SolveArmIk=options.SolveArmIk,PreserveWeaponGrip=options.PreserveWeaponGrip,WeaponSpaceOffset=weaponOffset,
            WristTravelBasis=options.WristTravelBasis??(viewSpace?Quat.Identity:null)};
        var notes=weaponOffset.HasValue?profile.Notes.Concat(new[]{"Preserved both palm grip anchors in one fixed weapon space; weapon motion is not scaled per arm."}).ToArray():profile.Notes;
        return new(source,clip,HandRetargeter.Bake(profile,clip,options,cancel),notes,weaponOffset,options);
    }

    public static async Task<string> ExportAsync(HandTarget target,IReadOnlyList<HandBakedClip> clips,string outputModel,
        bool autoGraph,bool backup,bool weaponCompatible,bool preserveSourceTracks,CancellationToken cancel=default)
        => (await ExportWithReportAsync(target,clips,outputModel,autoGraph,backup,weaponCompatible,preserveSourceTracks,cancel)).ModelPath;

    public static async Task<HandExportResult> ExportWithReportAsync(HandTarget target, IReadOnlyList<HandBakedClip> clips, string outputModel,
        bool autoGraph, bool backup, bool weaponCompatible, bool preserveSourceTracks, CancellationToken cancel = default)
    {
        await MainThread();
        if(clips.Count==0)throw new InvalidOperationException("Select at least one animation to export.");
        if(weaponCompatible)
        {
            foreach(var clip in clips)
            {
                if(clip.Source.ModelPath is null)throw new InvalidOperationException("Weapon-compatible mode requires a weapon VMDL source so its animation owner can be verified.");
                var missing=target.Mapping.Hands.SelectMany(h=>new[]{h.Wrist}.Concat(h.Digits.SelectMany(d=>d.Bones))).Select(i=>target.Skeleton[i].Name).Where(n=>clip.Source.Scene.Skeleton.IndexOf(n)<0).ToArray();
                if(missing.Length>0)throw new InvalidOperationException("These target bones cannot bonemerge onto the selected weapon: "+string.Join(", ",missing)+". Use standalone export for this custom rig, or choose a weapon whose skeleton contains those bones.");
            }
        }
        outputModel = VmdlSetupService.NormalizeAssetPath(outputModel,".vmdl");
        var absolute = System.IO.Path.Combine(Assets,outputModel);
        var existing = File.Exists(absolute) ? File.ReadAllText(absolute) : null;
        if(existing is not null)
        {
            var existingModel=Model.Load(outputModel);
            if(existingModel is null||existingModel.IsError)throw new InvalidOperationException("The existing output model must compile before animations can be added.");
            var existingRig=ReadSkeleton(existingModel);
            if(existingRig.Count!=target.Skeleton.Count||target.Skeleton.Bones.Any(b=>existingRig.IndexOf(b.Name)<0))throw new InvalidOperationException("The existing output model has a different skeleton. Choose the target model or a new output path.");
        }
        var targetSource=System.IO.Path.Combine(Assets,target.ModelPath);
        var original = existing ?? (File.Exists(targetSource)?File.ReadAllText(targetSource):HandModelFactory.Create(baseModel:target.ModelPath));
        var folder = outputModel[..^5] + "_animations";
        var files = new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);
        var entries = new List<HandAnimationEntry>();
        var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "bindPose" };
        // A compiled target is in inches. Existing model scaling controls the animation
        // compiler's input units; new wrappers use the inherited cm-to-inch modifier.
        var factor = ExportPositionFactor(original);
        var turn=ExportRootRotation(original);
        XForm ExportLocal(XForm value,int bone)=>target.Skeleton[bone].ParentIndex<0
            ?new XForm(Vec.Transform(value.Pos*factor,turn),Quat.Normalize(turn*value.Rot))
            :new XForm(value.Pos*factor,value.Rot);
        Clip ExportUnits(Clip clip) => new(clip.Name,clip.Fps,clip.Looping,clip.Frames.Select(frame=>frame.Select(ExportLocal).ToArray()).ToList(),clip.NativeFps);
        var exportSkeleton = Skel.Create(target.Skeleton.Bones.Select(b=>new BoneDefinition(b.Name,b.ParentIndex<0?null:target.Skeleton[b.ParentIndex].Name,ExportLocal(b.RestLocal,b.Index))).ToArray());
        var bind = new Clip("bindPose",30,false,new() { Pose.Rest(exportSkeleton).Locals });
        files[folder+"/bind.dmx"] = DmxWriter.Write(exportSkeleton,bind,new() {Name="bindPose",UpAxisY=false,ForwardParity=1});
        foreach(var clip in clips)
        {
            var stem=SourceKey(clip.Source)+"_"+SafeName(clip.Baked.Name); var name=stem; var suffix=1;
            while(!names.Add(name)) name=stem+"_"+(++suffix);
            var content=DmxWriter.Write(exportSkeleton,ExportUnits(clip.Baked),new() {Name=name,UpAxisY=false,ForwardParity=1,ChannelExcludedBones=target.Mapping.Hands.SelectMany(h=>h.TwistOrHelperBones).ToHashSet()});
            var file=folder+"/"+name+"_"+ContentKey(content)+".dmx";
            files[file]=content;
            entries.Add(new(name,file,clip.Baked.Looping));
            if(preserveSourceTracks)
            {
                // Full source companion intentionally retains every authored camera,
                // weapon and IK track with its original hierarchy, units and timing.
                var tracks=DmxWriter.Write(clip.Source.Scene.Skeleton,clip.Original,new() {Name=name+"_source_tracks",UpAxisY=clip.Source.Scene.UpAxis==1,ForwardParity=clip.Source.ModelPath is null&&clip.Source.Scene.FrontAxis!=0?2:1});
                files[folder+"/source_tracks/"+name+"_"+ContentKey(tracks)+".dmx"]=tracks;
            }
        }
        var prepared=VmdlSetupService.Prepare(original,entries,new() {ModelPath=outputModel,BindPoseSource=folder+"/bind.dmx",AutoConfigureAnimGraph=autoGraph,WeaponCompatibleArms=weaponCompatible});
        var generatedAssets=new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);
        var bundles=autoGraph?HandWeaponExport.Prepare(target,clips,outputModel,files,generatedAssets,cancel):Array.Empty<HandWeaponExport.Bundle>();
        foreach(var bundle in bundles)generatedAssets[bundle.PrefabPath]=HandWeaponExport.Prefab(bundle);
        var committed=await VmdlSetupTransaction.CommitAsync(Assets,outputModel,existing,prepared,files,async(paths,token)=>{
            await MainThread(); foreach(var path in paths) AssetSystem.RegisterFile(path);
            foreach(var bundle in bundles)
            {
                if(bundle.LiveCalibration is null)
                {
                    if(!await CompileAsync(System.IO.Path.Combine(Assets,bundle.ModelPath),token))throw new InvalidOperationException("Weapon animation model did not compile: "+bundle.ModelPath);
                    await MainThread();
                    await Task.Delay(100,token);await MainThread();
                    var owner=Model.Load(bundle.ModelPath);var graph=AnimationGraph.Load(bundle.GraphPath);
                    if(owner is null||owner.IsError||!owner.HasRenderMeshes()||owner.BoneCount<bundle.BoneCount||graph is null||graph.IsError
                        ||!bundle.Actions.All(a=>owner.AnimationNames.Contains(a.Sequence)))throw new InvalidOperationException($"Weapon animation owner validation failed: bones {owner?.BoneCount}/{bundle.BoneCount}, meshes {owner?.MeshCount}, vertices {owner?.MeshInfo.TotalVertices}, triangles {owner?.MeshInfo.TotalTriangles}, graph error {graph?.IsError}, sequences {string.Join(",",owner?.AnimationNames??Array.Empty<string>())}.");
                    if(target.Skeleton.Bones.Any(b=>owner.Bones.GetBone(WeaponClipBuilder.HandPrefix+b.Name) is null))return false;
                }

                if(!await CompileAsync(System.IO.Path.Combine(Assets,bundle.PrefabPath),token))throw new InvalidOperationException("Weapon prefab did not compile: "+bundle.PrefabPath);
                await MainThread();
                // The compiled file reaches disk before the editor's asset record updates.
                // Managed resource loading uses that record, not File.Exists.
                var prefabAsset=AssetSystem.FindByPath(bundle.PrefabPath);
                for(var wait=0;wait<50&&string.IsNullOrEmpty(prefabAsset.GetCompiledFile(true));wait++)
                {await Task.Delay(100,token);await MainThread();}
                var prefab=prefabAsset.LoadResource<PrefabFile>();
                if(prefab is null||prefab.IsError||SceneUtility.GetPrefabScene(prefab) is null)throw new InvalidOperationException("Weapon prefab could not be loaded: "+bundle.PrefabPath);
            }
            if(!await CompileAsync(absolute,token)) return false;
            await MainThread(); var model=Model.Load(outputModel);
            // The compiled file can arrive before an already-loaded model refreshes
            // its sequence list. Re-exporting changed motion adds a preserved-name
            // suffix, so wait for those new sequences before validating the bake.
            var modelDeadline=DateTime.UtcNow.AddSeconds(15);
            while(model is not null&&!model.IsError&&!prepared.Animations.All(a=>model.AnimationNames.Contains(a.SequenceName))
                &&DateTime.UtcNow<modelDeadline)
            {
                await Task.Delay(50,token);await MainThread();model=Model.Load(outputModel);
            }
            if(model is null || model.IsError || !prepared.Animations.All(a=>model.AnimationNames.Contains(a.SequenceName)))return false;
            if(prepared.GeneratedGraphPath is { } graphPath){var graph=AnimationGraph.Load(graphPath);if(graph is null||graph.IsError)return false;}

            var compiledRig=ReadSkeleton(model);
            using var sampler=new ModelSampler(model,compiledRig);
            var checkedBones=target.Mapping.Hands.SelectMany(h=>new int?[]{h.Clavicle,h.UpperArm,h.Forearm,h.Wrist}.Where(i=>i.HasValue).Select(i=>i!.Value).Concat(h.Digits.SelectMany(d=>d.Bones))).Distinct().ToArray();
            for(var i=0;i<clips.Count;i++)
            {
                var expectedClip=clips[i].Baked;var actualClip=sampler.Sample(prepared.Animations[i].SequenceName,expectedClip.Fps,token);
                if(expectedClip.FrameCount>1&&actualClip.FrameCount!=expectedClip.FrameCount)return false;
                foreach(var frame in new[]{0,expectedClip.FrameCount/2,expectedClip.FrameCount-1}.Distinct())
                {
                    var expectedWorld=new Pose(expectedClip.Frames[frame]).ToWorld(target.Skeleton);
                    var actualWorld=new Pose(actualClip.Frames[frame]).ToWorld(compiledRig);
                    foreach(var bone in checkedBones)
                    {
                        var index=compiledRig.IndexOf(target.Skeleton[bone].Name);
                        if(index<0 || Vec.Distance(expectedWorld[bone].Pos,actualWorld[index].Pos)>.1f
                            || MathQ.AngleBetween(expectedWorld[bone].Rot,actualWorld[index].Rot)>.02f)
                            throw new InvalidOperationException($"Compiled sequence '{expectedClip.Name}' differs from the preview at '{target.Skeleton[bone].Name}', frame {frame}. The export was rolled back.");
                    }
                }
            }
            return true;
        },backup,cancel,generatedAssets);
        return new(outputModel,prepared.Changes.Concat(bundles.Select(b=>b.LiveCalibration is not null
            ? $"Preserved complete source graph {b.GraphPath} with live custom-hand retargeting: {b.PrefabPath}"
            : $"Created weapon graph ({string.Join(", ",b.Actions.Select(a=>a.Action))}) and synchronized hands/weapon prefab: {b.PrefabPath}")).ToArray(),committed.BackupPath,bundles.Select(b=>b.PrefabPath).ToArray());
    }

    private static float ExportPositionFactor(string text)
    {
        float scale=1;
        void Walk(KvValue v) { if(v is KvObject o) { if(o.GetString("_class")=="ModelModifier_ScaleAndMirror" && o.GetOrNull("scale") is KvDouble d) scale*=(float)d.Value; foreach(var key in o.Keys) Walk(o[key]); } else if(v is KvArray a) foreach(var item in a.Items) Walk(item); }
        Walk(Kv3.Parse(text).Root);
        if(!float.IsFinite(scale)||scale<=0) throw new InvalidOperationException("Target model has an unsupported scale modifier.");
        return 1f/(2.54f*scale);
    }

    private static Quat ExportRootRotation(string text)
    {
        var turn=false;
        void Walk(KvValue value)
        {
            if(value is KvObject node)
            {
                if(node.GetString("_class")=="ModelModifier_ScaleAndMirror"
                    &&node.GetOrNull("mirror_x") is KvBool {Value:true}
                    &&node.GetOrNull("mirror_y") is KvBool {Value:true}
                    &&node.GetOrNull("mirror_z") is not KvBool {Value:true})turn=!turn;
                foreach(var key in node.Keys)Walk(node[key]);
            }
            else if(value is KvArray array)foreach(var child in array.Items)Walk(child);
        }
        Walk(Kv3.Parse(text).Root);
        return turn?Quat.CreateFromAxisAngle(Vec.UnitZ,MathF.PI):Quat.Identity;
    }

    private static async Task<string> PrepareMeshAsync(string file, CancellationToken cancel,List<string> importNotes)
    {
        var bytes=File.ReadAllBytes(file);
        var materials=await Task.Run(()=>FbxMaterialAssets.Inspect(file,bytes),cancel);
        importNotes.AddRange(materials.Notes);
        var hash=materials.Signature;
        var folder="models/hand_retargeter/targets/"+SafeName(System.IO.Path.GetFileNameWithoutExtension(file))+"_"+hash+"_rig9";
        var mesh=folder+"/hands.fbx"; var model=folder+"/hands.vmdl";
        var scene=await Task.Run(()=>FbxImporter.Import(bytes,new(){SampleFps=(float)FbxScene.Build(FbxTokenizer.Parse(bytes)).FrameRate}),cancel);
        var embeddedClips=scene.Clips;
        var repaired=await Task.Run(()=>
        {
            var result=FbxBindPoseFixer.TryFix(bytes,out var report);
            return (Bytes:result,Report:report);
        },cancel);
        if(repaired.Bytes is {} repairedBytes)
        {
            var repairedScene=await Task.Run(()=>FbxImporter.Import(repairedBytes),cancel);
            if(!scene.Skeleton.Bones.Select(b=>(b.Name,b.ParentIndex)).SequenceEqual(repairedScene.Skeleton.Bones.Select(b=>(b.Name,b.ParentIndex))))
                throw new InvalidOperationException("Bind-pose repair changed the target hierarchy; the original FBX was not modified.");
            scene=repairedScene;bytes=repairedBytes;
            importNotes.Add("Repaired the imported copy from its authored bind pose: "+repaired.Report);
        }
        var meshNames=FbxSkinnedMeshes.ReadNames(bytes);
        await MainThread();
        var meshAbs=System.IO.Path.Combine(Assets,mesh); Directory.CreateDirectory(System.IO.Path.GetDirectoryName(meshAbs)!);
        if(!File.Exists(meshAbs)) File.WriteAllBytes(meshAbs,bytes);
        var modelAbs=System.IO.Path.Combine(Assets,model);
        var remaps=HandMaterialImport.Write(materials,folder);
        var isNew=!File.Exists(modelAbs);
        if(isNew)
        {
            // Keep the full authored hierarchy before ModelDoc can cull unweighted
            // ancestors. The audited importer preview uses this same bind-source technique.
            string EngineName(string name){var suffix=name.LastIndexOf('#');return suffix<0?name:name[..suffix]+"_duplicate";}
            var rig=Skel.Create(scene.Skeleton.Bones.Select(b=>new BoneDefinition(EngineName(b.Name),b.ParentIndex<0?null:EngineName(scene.Skeleton[b.ParentIndex].Name),b.RestLocal)).ToArray());
            var bindPath=folder+"/mesh_bind.dmx";
            File.WriteAllText(System.IO.Path.Combine(Assets,bindPath),DmxWriter.Write(rig,new Clip("bindPose",30,false,new(){Pose.Rest(rig).Locals}),new(){Name="bindPose",UpAxisY=scene.UpAxis==1}));
            var entries=new List<HandAnimationEntry>();
            var names=new HashSet<string>(StringComparer.OrdinalIgnoreCase){"bindPose"};
            for(var i=0;i<embeddedClips.Count;i++)
            {
                cancel.ThrowIfCancellationRequested();
                var clip=embeddedClips[i];
                var stem=SafeName(clip.Name).Replace('-','_');
                var name=stem;var suffix=2;
                while(!names.Add(name))name=stem+"_"+suffix++;
                var path=folder+"/embedded_"+i+".dmx";
                // Imported locals are already centimeters, matching mesh_bind and the
                // model's final unit conversion. Preserve authored takes without IK.
                File.WriteAllText(System.IO.Path.Combine(Assets,path),DmxWriter.Write(rig,clip,new(){Name=name,UpAxisY=scene.UpAxis==1}));
                AssetSystem.RegisterFile(System.IO.Path.Combine(Assets,path));
                entries.Add(new(name,path,clip.Looping));
            }
            var prepared=VmdlSetupService.Prepare(HandModelFactory.Create(mesh:mesh,meshUnitScaleCm:scene.UnitScaleCm,materialRemaps:remaps,meshNames:meshNames),entries,new(){ModelPath=model,BindPoseSource=bindPath,AutoConfigureAnimGraph=false});
            File.WriteAllText(modelAbs,prepared.VmdlText);
            AssetSystem.RegisterFile(System.IO.Path.Combine(Assets,bindPath));
        }
        AssetSystem.RegisterFile(meshAbs);
        if(!await CompileAsync(modelAbs,cancel)) throw new InvalidOperationException("The target FBX could not be compiled into a skinned model.");
        if(isNew)
        {
            await MainThread();
            var compiledRig=ReadSkeleton(Model.Load(model));
            var fit=HandTargetFit.Analyze(compiledRig,HandRigDetector.Detect(compiledRig));
            if(fit.Scale!=1||fit.TurnAround)
            {
                File.WriteAllText(modelAbs,fit.Apply(File.ReadAllText(modelAbs)));
                if(!await CompileAsync(modelAbs,cancel))throw new InvalidOperationException("The fitted FPS target could not be compiled.");
                // Resource compilation finishes before the loaded Model's bone cache
                // refreshes. Never hand an old-size skeleton to a newly scaled mesh.
                var deadline=DateTime.UtcNow.AddSeconds(15);
                while(true)
                {
                    await Task.Delay(50,cancel);await MainThread();
                    var loaded=ReadSkeleton(Model.Load(model));
                    var ready=compiledRig.Bones.All(b=>
                    {
                        var index=loaded.IndexOf(b.Name);if(index<0)return false;
                        var expected=compiledRig.RestWorld[b.Index].Pos*fit.Scale;
                        if(fit.TurnAround)expected=new Vec(-expected.X,-expected.Y,expected.Z);
                        return Vec.Distance(loaded.RestWorld[index].Pos,expected)<.02f;
                    });
                    if(ready)break;
                    if(DateTime.UtcNow>=deadline)throw new InvalidOperationException("The fitted model is still reloading. Select the FBX again after asset compilation finishes.");
                }
                if(fit.Scale!=1)importNotes.Add($"Applied FPS import scale {fit.Scale:G} to the mesh and all embedded animations to correct the oversized or undersized export.");
                if(fit.TurnAround)importNotes.Add("Turned the imported target 180 degrees to align its left and right shoulders with FPS weapon coordinates.");
            }
        }
        await MainThread(); return model;
    }

    public static async Task<bool> CompileAsync(string absolute, CancellationToken cancel)
    {
        await MainThread(); var asset=AssetSystem.RegisterFile(absolute);
        var compiled=absolute+"_c"; var before=File.Exists(compiled)?File.GetLastWriteTimeUtc(compiled):DateTime.MinValue;
        asset.Compile(full:true);
        var deadline=DateTime.UtcNow.AddSeconds(90);
        while(DateTime.UtcNow<deadline)
        {
            cancel.ThrowIfCancellationRequested(); await MainThread();
            if(asset.IsCompileFailed) return false;
            if(File.Exists(compiled) && File.GetLastWriteTimeUtc(compiled)>before) return true;
            await Task.Delay(100,cancel);
        }
        return false;
    }

    public static bool HasMissingMaterials(Model model)
    {
        var materials=model.Materials.Concat(Enumerable.Range(0,model.MaterialGroupCount).SelectMany(group=>model.GetMaterials(group))).Distinct();
        foreach(var material in materials)
        {
            if(material is null||material.IsError||string.Equals(material.ResourcePath,"materials/error.vmat",StringComparison.OrdinalIgnoreCase))return true;
            foreach(var parameter in new[]{"g_tColor","g_tNormal","g_tRoughness","g_tMetalness","g_tAmbientOcclusion","g_tSelfIllumMask","g_tTranslucency","TextureColor","TextureNormal","TextureRoughness","TextureMetalness","TextureAmbientOcclusion","TextureSelfIllumMask","TextureTranslucency"})
                if(material.GetTexture(parameter) is {IsError:true})return true;
            if(material.FirstTexture is {IsError:true})return true;
        }
        return false;
    }

    public static void ApplyPreviewMaterialFallback(SceneModel model)
    {
        // Incomplete visual dependencies must not prevent skeletal retargeting.
        // Override this preview instance only; authored assets remain editable.
        if(HasMissingMaterials(model.Model))model.SetMaterialOverride(Material.Load("materials/dev/reflectivity_50.vmat"));
    }
    public static string ModelPath(string path)
    {
        if(path.EndsWith(".vmdl_c",StringComparison.OrdinalIgnoreCase))path=path[..^2];
        if(System.IO.Path.IsPathRooted(path)) path=System.IO.Path.GetRelativePath(Assets,path);
        return VmdlSetupService.NormalizeAssetPath(path,".vmdl");
    }
    public static string SafeName(string name) => string.Concat(name.Select(c=>char.IsLetterOrDigit(c)||c=='_'||c=='-'?c:'_')).Trim('_') is {Length:>0} result ? result : "animation";
    internal static string ContentKey(string text)=>Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(text))).ToLowerInvariant()[..16];
    internal static string SourceKey(HandSource source)=>SafeName(System.IO.Path.GetFileNameWithoutExtension(source.Path))+"_"+ContentKey(source.Path.Replace('\\','/').ToLowerInvariant())[..8];

    private sealed class ModelSampler : IDisposable
    {
        readonly Scene metadataScene = Scene.CreateEditorScene(); readonly SkinnedModelRenderer metadata;
        readonly SceneWorld world=new(); readonly Model asset; readonly Skel skeleton; readonly int[] bones;
        public ModelSampler(Model asset, Skel rig) { using(metadataScene.Push()) { metadata=new GameObject(true,"sequence metadata").GetOrAddComponent<SkinnedModelRenderer>(); metadata.Model=asset; metadata.UseAnimGraph=false; } this.asset=asset; skeleton=rig; bones=rig.Bones.Select(b=>asset.Bones.GetBone(b.Name).Index).ToArray(); }
        public Clip Sample(string name,float fps,CancellationToken cancel=default)
        {
            var model=new SceneModel(world,asset,Transform.Zero){UseAnimGraph=false};
            try {
            model.SetAnimGraph("");model.UseAnimGraph=false;
            model.CurrentSequence.Name=name; metadata.Sequence.Name=name;
            var duration=model.CurrentSequence.Duration;
            var count=Math.Max(1,(int)MathF.Round(duration*fps)+1);
            if(count>100000) throw new InvalidOperationException($"Sequence '{name}' is too long to sample.");
            var frames=new List<XForm[]>(count);
            for(var f=0;f<count;f++) {
                cancel.ThrowIfCancellationRequested();
                model.CurrentSequence.Time=MathF.Min(f/fps,duration); model.Update(0);
                var worlds=bones.Select(b=>FromEngine(model.GetBoneWorldTransform(b))).ToArray();
                frames.Add(skeleton.Bones.Select(b=>b.ParentIndex<0?worlds[b.Index]:XForm.ToLocal(worlds[b.ParentIndex],worlds[b.Index])).ToArray());
            }
            return new(name,fps,metadata.Sequence.Looping,frames);
            } finally {model.Delete();}
        }
        public Clip? SampleFire(float duration,float fps,CancellationToken cancel)
        {
            var model=new SceneModel(world,asset,Transform.Zero){UseAnimGraph=true};
            try
            {
                if(model.AnimationGraph is null||model.AnimationGraph.IsError)return null;
                model.SetAnimParameter("skeleton",0);model.SetAnimParameter("b_deploy_skip",true);
                for(var i=0;i<120;i++){cancel.ThrowIfCancellationRequested();model.Update(1f/60);}
                model.SetAnimParameter("b_deploy_skip",false);
                var frames=new List<XForm[]>();
                for(var f=0;f<=Math.Max(1,(int)MathF.Ceiling(duration*fps));f++)
                {
                    cancel.ThrowIfCancellationRequested();model.SetAnimParameter("b_attack",f==0);model.Update(f==0?0:1f/fps);
                    var worlds=bones.Select(b=>FromEngine(model.GetBoneWorldTransform(b))).ToArray();
                    frames.Add(skeleton.Bones.Select(b=>b.ParentIndex<0?worlds[b.Index]:XForm.ToLocal(worlds[b.ParentIndex],worlds[b.Index])).ToArray());
                }
                return new Clip("Fire",fps,false,frames);
            }
            finally{model.Delete();}
        }
        public void Dispose() {world.Delete();metadataScene.Destroy();}
    }
}
notpointless.chomnr_humanoid_hand_retargeter / Core/Mapping/HandRigDefinition.cs
Game library
#nullable enable
namespace HumanoidHandRetargeter.Mapping;

public enum HandSide { Left, Right }
public enum DigitRole { Thumb, Index, Middle, Ring, Pinky, Extra }

/// <summary>Ordered anatomical joints; optional palm and terminal bones are explicit.</summary>
public sealed class DigitChain
{
    public DigitRole Role { get; }
    public string ExtraSlot { get; }
    public IReadOnlyList<int> Segments { get; }
    public int? Metacarpal { get; }
    public int? Tip { get; }

    public DigitChain(DigitRole role, IEnumerable<int> segments,
        int? metacarpal = null, int? tip = null, string extraSlot = "")
    {
        ArgumentNullException.ThrowIfNull(segments);
        Role = role;
        Segments = Array.AsReadOnly(segments.ToArray());
        Metacarpal = metacarpal;
        Tip = tip;
        ExtraSlot = extraSlot ?? "";
    }

    public IEnumerable<int> Bones
    {
        get
        {
            if (Metacarpal is int palm) yield return palm;
            foreach (var segment in Segments) yield return segment;
            if (Tip is int tip) yield return tip;
        }
    }
}

/// <summary>A single hand; rigs with only one side do not need a placeholder other hand.</summary>
public sealed class HandRigDefinition
{
    public HandSide Side { get; }
    public int Wrist { get; }
    public int? Clavicle { get; }
    public int? UpperArm { get; }
    public int? Forearm { get; }
    public IReadOnlyList<DigitChain> Digits { get; }
    public IReadOnlyList<int> TwistOrHelperBones { get; }

    public HandRigDefinition(HandSide side, int wrist, IEnumerable<DigitChain> digits,
        int? clavicle = null, int? upperArm = null, int? forearm = null,
        IEnumerable<int>? twistOrHelperBones = null)
    {
        ArgumentNullException.ThrowIfNull(digits);
        Side = side;
        Wrist = wrist;
        Clavicle = clavicle;
        UpperArm = upperArm;
        Forearm = forearm;
        Digits = Array.AsReadOnly(digits.ToArray());
        TwistOrHelperBones = Array.AsReadOnly((twistOrHelperBones ?? Array.Empty<int>()).ToArray());
    }
}
notpointless.chomnr_humanoid_hand_retargeter / Core/Retargeting/HandMotionOptions.cs
Game library
#nullable enable
namespace HumanoidHandRetargeter.Retargeting;

public sealed class HandMotionOptions
{
    public bool TransferWristPosition { get; init; } = true;
    public bool ScaleWristTravel { get; init; } = true;
    /// <summary>Known source-to-target model axes, independent of the wrists' rest rotations.</summary>
    public System.Numerics.Quaternion? WristTravelBasis {get;init;}
    public bool SolveArmIk { get; init; } = true;
    public bool PreserveWeaponGrip { get; init; } = true;
    /// <summary>Shared source weapon-to-target translation in centimeters. Grip motion is never scaled per hand.</summary>
    public System.Numerics.Vector3? WeaponSpaceOffset {get;init;}
}
notpointless.chomnr_humanoid_hand_retargeter / Core/Retargeting/HandRetargeter.cs
Game library
#nullable enable
using System.Numerics;
using HumanoidHandRetargeter.Calibration;
using HumanoidHandRetargeter.Formats;
using HumanoidHandRetargeter.Maths;
using HumanoidHandRetargeter.Skeleton;
using HumanoidHandRetargeter.Validation;

namespace HumanoidHandRetargeter.Retargeting;
using Vector3 = System.Numerics.Vector3;

/// <summary>One offline solve path for static poses and baked clips. Target translations
/// and unmapped local transforms stay authored; helper constraints remain target-owned.</summary>
public static class HandRetargeter
{
    /// <summary>One continuous graph stream, retaining digit angle history between evaluated frames.</summary>
    public sealed class PoseStream
    {
        private readonly HandRetargetProfile profile;
        private readonly HandMotionOptions options;
        private readonly AngleHistory history;
        public PoseStream(HandRetargetProfile profile, HandMotionOptions options)
        { this.profile = profile; this.options = options; history = new(profile); }
        public Pose Step(Pose source)
        {
            var pose = Solve(profile, source, history);
            if (options.TransferWristPosition) ApplyWristMotion(profile, source, pose, options);
            var sourceWorld = source.ToWorld(profile.Source);
            foreach (var pair in profile.PreservedTracks)
            {
                var targetWorld = pose.ToWorld(profile.Target);
                var parent = profile.Target[pair.Target].ParentIndex;
                pose.Locals[pair.Target] = parent < 0 ? sourceWorld[pair.Source] : XForm.ToLocal(targetWorld[parent], sourceWorld[pair.Source]);
            }
            return pose;
        }
    }

    public static Pose RetargetPose(HandRetargetProfile profile, Pose sourcePose)
        => Solve(profile, sourcePose, null);

    /// <summary>Retains timing and fixes quaternion signs. Stateful angle unwrapping is
    /// local to this clip, so a calibrated profile may safely be shared by concurrent jobs.</summary>
    public static Clip RetargetClip(HandRetargetProfile profile, Clip sourceClip, CancellationToken cancellationToken = default)
        => Bake(profile, sourceClip, new HandMotionOptions { TransferWristPosition = false }, cancellationToken);

    /// <summary>The shared editor preview/export bake including optional wrist travel and arm IK.</summary>
    public static Clip Bake(HandRetargetProfile profile, Clip sourceClip, HandMotionOptions options, CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(profile);
        ArgumentNullException.ThrowIfNull(sourceClip);
        ArgumentNullException.ThrowIfNull(options);
        if (sourceClip.FrameCount == 0)
            throw new RigValidationException(new[] { new RigIssue("empty-clip", "Source animation has no frames.") });
        var frames = new List<XForm[]>(sourceClip.FrameCount);
        var continuity = new AngleHistory(profile);
        foreach (var sourceFrame in sourceClip.Frames)
        {
            cancellationToken.ThrowIfCancellationRequested();
            var pose = Solve(profile, new Pose(sourceFrame), continuity);
            if (options.TransferWristPosition) ApplyWristMotion(profile, new Pose(sourceFrame), pose, options);
            if(profile.PreservedTracks.Length>0)
            {
                var sourceWorld=new Pose(sourceFrame).ToWorld(profile.Source);
                foreach(var pair in profile.PreservedTracks)
                {
                    var targetWorld=pose.ToWorld(profile.Target);var parent=profile.Target[pair.Target].ParentIndex;
                    pose.Locals[pair.Target]=parent<0?sourceWorld[pair.Source]:XForm.ToLocal(targetWorld[parent],sourceWorld[pair.Source]);
                }
            }
            frames.Add(pose.Locals);
        }
        QuaternionContinuity.AlignFrames(frames);
        return new Clip(sourceClip.Name, sourceClip.Fps, sourceClip.Looping, frames, sourceClip.NativeFps);
    }

    private static void ApplyWristMotion(HandRetargetProfile profile, Pose source, Pose target, HandMotionOptions options)
    {
        if(options.WeaponSpaceOffset.HasValue&&options.SolveArmIk)
            foreach(var shoulder in profile.FpsShoulders)target.Locals[shoulder.Key].Pos=shoulder.Value;
        var sourceWorld = source.ToWorld(profile.Source);
        foreach (var pair in profile.WristMotion)
        {
            var world = target.ToWorld(profile.Target);
            var travel = Vector3.Transform(sourceWorld[pair.Source].Pos - profile.Source.RestWorld[pair.Source].Pos, options.WristTravelBasis ?? pair.Basis);
            var desired = profile.Target.RestWorld[pair.Target].Pos + travel * (options.ScaleWristTravel ? pair.Scale : 1f);
            var wristRotation = world[pair.Target].Rot;
            if(options.WeaponSpaceOffset is {} weaponOffset)
            {
                wristRotation=MathQ.Normalize(sourceWorld[pair.Source].Rot*pair.GripRotationOffset);
                desired=sourceWorld[pair.Source].Pos+weaponOffset
                    +Vector3.Transform(pair.SourceGripLocal,sourceWorld[pair.Source].Rot)
                    -Vector3.Transform(pair.TargetGripLocal,wristRotation);
            }
            if (options.SolveArmIk && pair.UpperArm is int upper && pair.Forearm is int lower)
            {
                if(options.WeaponSpaceOffset.HasValue)
                {
                    // Detached FPS arms can move their open shoulder ends. Avoid
                    // folding long/thick forearms back through their own upper arm
                    // when the weapon grip lies very close to the shoulder.
                    if(profile.Target[upper].ParentIndex<0)
                    {
                        var shoulder=HandViewSpace.ClearElbowFold(world[upper].Pos,world[lower].Pos,world[pair.Target].Pos,desired);
                        target.Locals[upper].Pos=shoulder;
                        world=target.ToWorld(profile.Target);
                    }
                    // FPS arm meshes have free shoulder ends. Move the shoulder enough
                    // to reach the fixed weapon grip instead of stretching either arm segment.
                    var reach=Vector3.Distance(world[upper].Pos,world[lower].Pos)+Vector3.Distance(world[lower].Pos,world[pair.Target].Pos);
                    var toGrip=desired-world[upper].Pos;var distance=toGrip.Length();
                    if(distance>reach*.98f&&distance>1e-5f)
                    {
                        var shoulder=world[upper].Pos+toGrip/distance*(distance-reach*.98f);
                        var parent=profile.Target[upper].ParentIndex;
                        target.Locals[upper].Pos=parent<0?shoulder:Vector3.Transform(shoulder-world[parent].Pos,Quaternion.Conjugate(world[parent].Rot));
                        world=target.ToWorld(profile.Target);
                    }
                }
                var correction = TwoBoneIk.Solve(world[upper].Pos, world[lower].Pos, world[pair.Target].Pos, desired, 0, pair.BendAxis);
                void SetWorldRotation(int bone, Quaternion rotation)
                {
                    var parent = profile.Target[bone].ParentIndex;
                    target.Locals[bone].Rot = MathQ.Normalize(parent < 0 ? rotation : Quaternion.Conjugate(world[parent].Rot) * rotation);
                    world = target.ToWorld(profile.Target);
                }
                var oldLower = world[lower].Rot;
                SetWorldRotation(upper, correction.UpperWorldDelta * world[upper].Rot);
                SetWorldRotation(lower, correction.LowerWorldDelta * oldLower);
                if(options.WeaponSpaceOffset.HasValue && pair.SourceForearm is int sourceElbow && pair.SourceUpperArm is int sourceShoulder)
                {
                    // Transfer the source bend plane between the two shoulder-to-wrist axes.
                    // An absolute source elbow position can lie on the target arm's axis
                    // when proportions differ, flipping its pole as the weapon recoils.
                    var axis=world[pair.Target].Pos-world[upper].Pos;
                    var sourceAxis=sourceWorld[pair.Source].Pos-sourceWorld[sourceShoulder].Pos;
                    if(axis.LengthSquared()>1e-8f&&sourceAxis.LengthSquared()>1e-8f)
                    {
                        axis=Vector3.Normalize(axis);
                        sourceAxis=Vector3.Normalize(sourceAxis);
                        var actual=world[lower].Pos-world[upper].Pos;
                        var wanted=sourceWorld[sourceElbow].Pos-sourceWorld[sourceShoulder].Pos;
                        wanted-=sourceAxis*Vector3.Dot(wanted,sourceAxis);
                        wanted=Vector3.Transform(wanted,MathQ.FromTo(sourceAxis,axis));
                        actual-=axis*Vector3.Dot(actual,axis);
                        // A straight arm has no defined pole: retain the preceding IK plane.
                        if(actual.LengthSquared()>1e-6f&&wanted.LengthSquared()>1e-6f)
                        {
                            actual=Vector3.Normalize(actual);wanted=Vector3.Normalize(wanted);
                            var angle=MathF.Atan2(Vector3.Dot(axis,Vector3.Cross(actual,wanted)),Vector3.Dot(actual,wanted));
                            var pole=Quaternion.CreateFromAxisAngle(axis,angle);
                            oldLower=world[lower].Rot;
                            SetWorldRotation(upper,pole*world[upper].Rot);
                            SetWorldRotation(lower,pole*oldLower);
                        }
                    }
                }
                if(options.WeaponSpaceOffset.HasValue&&!pair.HasTwistHelpers)
                {
                    // Without authored twist helpers, put axial pronation in the forearm
                    // instead of winding the wrist through the fixed palm orientation.
                    // Rotating about elbow-to-wrist leaves the grip position unchanged.
                    var axis=world[pair.Target].Pos-world[lower].Pos;
                    if(axis.LengthSquared()>1e-8f)
                    {
                        var restRelative=MathQ.Normalize(Quaternion.Conjugate(profile.Target.RestWorld[lower].Rot)*profile.Target.RestWorld[pair.Target].Rot);
                        var relaxedLower=MathQ.Normalize(wristRotation*Quaternion.Conjugate(restRelative));
                        var delta=MathQ.Normalize(relaxedLower*Quaternion.Conjugate(world[lower].Rot));
                        MathQ.SwingTwist(delta,Vector3.Normalize(axis),out _,out var roll);
                        SetWorldRotation(lower,roll*world[lower].Rot);
                    }
                }
                SetWorldRotation(pair.Target, wristRotation);
                if(options.WeaponSpaceOffset.HasValue&&Vector3.Distance(world[pair.Target].Pos,desired)>.1f)
                    throw new InvalidOperationException("The target arm cannot reach the weapon grip without stretching. Adjust the rig's shoulder placement or disable Preserve weapon grip.");
            }
            else
            {
                // Partial rigs cannot reach with a two-link chain. Preserve the authored
                // wrist path by translating the wrist relative to its actual parent.
                var parent = profile.Target[pair.Target].ParentIndex;
                target.Locals[pair.Target].Pos = parent < 0 ? desired
                    : Vector3.Transform(desired - world[parent].Pos, Quaternion.Conjugate(world[parent].Rot));
                target.Locals[pair.Target].Rot=MathQ.Normalize(parent<0?wristRotation:Quaternion.Conjugate(world[parent].Rot)*wristRotation);
            }
        }
        var errors = PoseValidator.Validate(profile.Target, target);
        if (errors.Count > 0) throw new RigValidationException(errors);
    }

    private static Pose Solve(HandRetargetProfile profile, Pose sourcePose, AngleHistory? history)
    {
        ArgumentNullException.ThrowIfNull(profile);
        var errors = PoseValidator.Validate(profile.Source, sourcePose);
        if (errors.Count > 0) throw new RigValidationException(errors);
        var sourceWorld = sourcePose.ToWorld(profile.Source);
        errors = PoseValidator.Validate(profile.Source, new Pose(sourceWorld));
        if (errors.Count > 0) throw new RigValidationException(errors);
        var sourceDeltas = new Quaternion[sourceWorld.Length];
        for (var i = 0; i < sourceWorld.Length; i++)
            sourceDeltas[i] = MathQ.Normalize(sourceWorld[i].Rot * Quaternion.Conjugate(profile.Source.RestWorld[i].Rot));
        var targetDeltas = Enumerable.Repeat(Quaternion.Identity, profile.Target.Count).ToArray();
        var solved = new bool[profile.Target.Count];
        foreach (var pair in profile.Pairs)
        {
            var parentSource = pair.SourceParent >= 0 ? sourceDeltas[pair.SourceParent] : Quaternion.Identity;
            var parentTarget = pair.TargetParent >= 0 ? targetDeltas[pair.TargetParent] : Quaternion.Identity;
            var canonical = MathQ.Normalize(Quaternion.Conjugate(pair.SourceFrame) * Quaternion.Conjugate(parentSource)
                * sourceDeltas[pair.Source] * pair.SourceFrame);
            targetDeltas[pair.Target] = MathQ.Normalize(parentTarget * pair.TargetFrame * canonical * Quaternion.Conjugate(pair.TargetFrame));
            solved[pair.Target] = true;
        }
        for (var d = 0; d < profile.Distributions.Length; d++)
        {
            var digit = profile.Distributions[d];
            var curls = new float[digit.Source.Length];
            var spread = 0f;
            var previous = sourceDeltas[digit.SourceWrist];
            for (var s = 0; s < digit.Source.Length; s++)
            {
                var frame = digit.SourceFrames[s];
                var delta = sourceDeltas[digit.Source[s]];
                var canonical = MathQ.Normalize(Quaternion.Conjugate(frame) * Quaternion.Conjugate(previous) * delta * frame);
                // Legacy FingerSolver decomposition: canonical Y curl, Z splay, discard X twist when redistributing.
                MathQ.SwingTwist(canonical, Vector3.UnitY, out var swing, out var curl);
                curls[s] = SignedAngle(curl, Vector3.UnitY);
                if (history is not null) curls[s] = history.Unwrap(d, s, curls[s], false);
                if (s <= digit.ProximalSourceIndex)
                {
                    MathQ.SwingTwist(swing, Vector3.UnitZ, out _, out var splay);
                    var angle = SignedAngle(splay, Vector3.UnitZ);
                    if (history is not null) angle = history.Unwrap(d, s, angle, true);
                    spread += angle;
                }
                previous = delta;
            }
            var accumulated = targetDeltas[digit.TargetWrist];
            for (var t = 0; t < digit.Target.Length; t++)
            {
                var angle = 0f;
                for (var s = 0; s < curls.Length; s++) angle += digit.Weights[t, s] * curls[s];
                var motion = Quaternion.CreateFromAxisAngle(Vector3.UnitY, angle);
                if (t == 0) motion = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, spread) * motion;
                var frame = digit.TargetFrames[t];
                accumulated = MathQ.Normalize(accumulated * frame * motion * Quaternion.Conjugate(frame));
                targetDeltas[digit.Target[t]] = accumulated;
                solved[digit.Target[t]] = true;
            }
        }

        var targetPose = Pose.Rest(profile.Target);
        var targetWorld = new XForm[profile.Target.Count];
        for (var i = 0; i < targetWorld.Length; i++)
        {
            var parent = profile.Target[i].ParentIndex;
            if (solved[i])
            {
                var desiredWorld = targetDeltas[i] * profile.Target.RestWorld[i].Rot;
                targetPose.Locals[i].Rot = MathQ.Normalize(parent < 0 ? desiredWorld : Quaternion.Conjugate(targetWorld[parent].Rot) * desiredWorld);
            }
            targetWorld[i] = parent < 0 ? targetPose.Locals[i] : XForm.Compose(targetWorld[parent], targetPose.Locals[i]);
        }
        errors = PoseValidator.Validate(profile.Target, targetPose);
        if (errors.Count > 0) throw new RigValidationException(errors);
        errors = PoseValidator.Validate(profile.Target, new Pose(targetWorld));
        if (errors.Count > 0) throw new RigValidationException(errors);
        return targetPose;
    }

    // Same signed twist convention as the legacy FingerSolver.
    private static float SignedAngle(Quaternion twist, Vector3 axis)
    {
        var angle = 2f * MathF.Atan2(twist.X * axis.X + twist.Y * axis.Y + twist.Z * axis.Z, twist.W);
        if (angle > MathF.PI) angle -= 2f * MathF.PI;
        if (angle < -MathF.PI) angle += 2f * MathF.PI;
        return angle;
    }

    private sealed class AngleHistory
    {
        private readonly float[][] _curl;
        private readonly float[][] _spread;
        public AngleHistory(HandRetargetProfile profile)
        {
            _curl = profile.Distributions.Select(d => Enumerable.Repeat(float.NaN, d.Source.Length).ToArray()).ToArray();
            _spread = profile.Distributions.Select(d => Enumerable.Repeat(float.NaN, d.Source.Length).ToArray()).ToArray();
        }
        public float Unwrap(int digit, int joint, float angle, bool spread)
        {
            var values = spread ? _spread : _curl;
            var previous = values[digit][joint];
            if (float.IsFinite(previous))
                angle += 2f * MathF.PI * MathF.Round((previous - angle) / (2f * MathF.PI));
            values[digit][joint] = angle;
            return angle;
        }
    }
}
notpointless.chomnr_humanoid_hand_retargeter / Core/Skeleton/Skeleton.cs
Game library
// Adapted from humanoid-retargeter 26084c96c3fc870aaf9a5bd798de063ce2fd62df; cached hierarchy queries added.
#nullable enable annotations

using System;
using System.Collections.Generic;
using HumanoidHandRetargeter.Maths;

namespace HumanoidHandRetargeter.Skeleton;

/// <summary>
/// Input definition for a single bone, used to build a <see cref="Skeleton"/>.
/// Order does not matter; construction topologically sorts parents before children.
/// </summary>
/// <param name="Name">Unique bone name.</param>
/// <param name="ParentName">Parent bone name, or null for a root bone (multiple roots allowed).</param>
/// <param name="RestLocal">Rest (bind) transform relative to the parent bone, centimeters.</param>
public readonly record struct BoneDefinition(string Name, string? ParentName, XForm RestLocal);

/// <summary>One bone of an immutable <see cref="Skeleton"/>.</summary>
public readonly struct Bone
{
    /// <summary>Index of this bone in the skeleton (parents always have a smaller index).</summary>
    public int Index { get; }

    /// <summary>Unique bone name.</summary>
    public string Name { get; }

    /// <summary>Index of the parent bone, or -1 for a root bone.</summary>
    public int ParentIndex { get; }

    /// <summary>Rest (bind) transform relative to the parent bone (or armature space for roots).</summary>
    public XForm RestLocal { get; }

    internal Bone(int index, string name, int parentIndex, XForm restLocal)
    {
        Index = index;
        Name = name;
        ParentIndex = parentIndex;
        RestLocal = restLocal;
    }

    /// <inheritdoc />
    public override string ToString() => $"[{Index}] {Name} (parent {ParentIndex})";
}

/// <summary>
/// Immutable bone hierarchy with rest pose. Bones are stored topologically sorted
/// (every bone's parent precedes it), which lets forward kinematics run as a single
/// in-order pass. Supports multiple roots (e.g. the s&amp;box rig has both
/// <c>root_IK</c> and <c>pelvis</c> as parentless bones).
/// </summary>
public sealed class Skeleton
{
    private readonly Bone[] _bones;
    private readonly XForm[] _restWorld;
    private readonly Dictionary<string, int> _indexByName;
    private readonly IReadOnlyList<int>[] _children;

    private Skeleton(Bone[] bones, XForm[] restWorld, Dictionary<string, int> indexByName)
    {
        _bones = bones;
        _restWorld = restWorld;
        _indexByName = indexByName;
        var children = Enumerable.Range(0, bones.Length).Select(_ => new List<int>()).ToArray();
        foreach (var bone in bones)
            if (bone.ParentIndex >= 0) children[bone.ParentIndex].Add(bone.Index);
        _children = children.Select(c => (IReadOnlyList<int>)c.AsReadOnly()).ToArray();
    }

    /// <summary>Number of bones.</summary>
    public int Count => _bones.Length;

    /// <summary>All bones, topologically sorted (parents before children).</summary>
    public IReadOnlyList<Bone> Bones => _bones;

    /// <summary>Rest (bind) world transforms, indexed like <see cref="Bones"/>.</summary>
    public IReadOnlyList<XForm> RestWorld => _restWorld;

    /// <summary>Bone name to index lookup.</summary>
    public IReadOnlyDictionary<string, int> IndexByName => _indexByName;

    /// <summary>The bone at <paramref name="index"/>.</summary>
    public Bone this[int index] => _bones[index];

    /// <summary>Returns the index of the named bone, or -1 when absent.</summary>
    public int IndexOf(string name) => _indexByName.TryGetValue(name, out var index) ? index : -1;

    /// <summary>Cached immediate children in stable skeleton order.</summary>
    public IReadOnlyList<int> ChildrenOf(int bone) => _children[bone];

    /// <summary>Whether child is strictly below ancestor; a bone is not its own descendant.</summary>
    public bool DescendsFrom(int child, int ancestor)
    {
        if (child < 0 || child >= Count) throw new ArgumentOutOfRangeException(nameof(child));
        if (ancestor < 0 || ancestor >= Count) throw new ArgumentOutOfRangeException(nameof(ancestor));
        for (var parent = _bones[child].ParentIndex; parent >= 0; parent = _bones[parent].ParentIndex)
            if (parent == ancestor) return true;
        return false;
    }

    /// <summary>
    /// Builds a skeleton from bone definitions in any order: validates names and parent links,
    /// topologically sorts (stable — input order is preserved among bones whose parents are
    /// already placed), and computes rest world transforms.
    /// </summary>
    /// <exception cref="ArgumentException">
    /// Thrown on duplicate bone names, unknown parent names, or parent cycles.
    /// </exception>
    public static Skeleton Create(IReadOnlyList<BoneDefinition> definitions)
    {
        ArgumentNullException.ThrowIfNull(definitions);

        var count = definitions.Count;
        var originalIndexByName = new Dictionary<string, int>(count, StringComparer.Ordinal);
        for (var i = 0; i < count; i++)
        {
            var name = definitions[i].Name;
            if (string.IsNullOrEmpty(name))
                throw new ArgumentException($"Bone at position {i} has an empty name.", nameof(definitions));
            if (!originalIndexByName.TryAdd(name, i))
                throw new ArgumentException($"Duplicate bone name '{name}'.", nameof(definitions));
        }

        foreach (var def in definitions)
        {
            if (def.ParentName is not null && !originalIndexByName.ContainsKey(def.ParentName))
                throw new ArgumentException(
                    $"Bone '{def.Name}' references unknown parent '{def.ParentName}'.", nameof(definitions));
            if (def.ParentName == def.Name)
                throw new ArgumentException($"Bone '{def.Name}' is its own parent.", nameof(definitions));
        }

        // Stable topological sort: repeatedly place, in input order, every bone whose parent
        // is already placed. O(n^2) worst case — fine for skeleton-sized inputs (~100 bones).
        var newIndexByOriginal = new int[count];
        Array.Fill(newIndexByOriginal, -1);
        var order = new List<int>(count);
        while (order.Count < count)
        {
            var progressed = false;
            for (var i = 0; i < count; i++)
            {
                if (newIndexByOriginal[i] >= 0)
                    continue;
                var parentName = definitions[i].ParentName;
                if (parentName is not null && newIndexByOriginal[originalIndexByName[parentName]] < 0)
                    continue;
                newIndexByOriginal[i] = order.Count;
                order.Add(i);
                progressed = true;
            }
            if (!progressed)
                throw new ArgumentException("Bone hierarchy contains a parent cycle.", nameof(definitions));
        }

        var bones = new Bone[count];
        var restWorld = new XForm[count];
        var indexByName = new Dictionary<string, int>(count, StringComparer.Ordinal);
        for (var newIndex = 0; newIndex < count; newIndex++)
        {
            var def = definitions[order[newIndex]];
            var parentIndex = def.ParentName is null ? -1 : newIndexByOriginal[originalIndexByName[def.ParentName]];
            bones[newIndex] = new Bone(newIndex, def.Name, parentIndex, def.RestLocal);
            restWorld[newIndex] = parentIndex < 0
                ? def.RestLocal
                : XForm.Compose(restWorld[parentIndex], def.RestLocal);
            indexByName.Add(def.Name, newIndex);
        }

        return new Skeleton(bones, restWorld, indexByName);
    }
}
notpointless.chomnr_humanoid_hand_retargeter / Formats/Fbx/FbxNode.cs
Game library
// Reused from humanoid-retargeter 26084c96c3fc870aaf9a5bd798de063ce2fd62df; specialized where needed for hand assets.
#nullable enable annotations

namespace HumanoidHandRetargeter.Formats.Fbx;

/// <summary>
/// A single node of an FBX document tree (binary or ASCII): a name, a flat list of
/// typed properties, and nested child nodes.
///
/// Property values are stored as the closest CLR type to what the file contained:
/// <list type="bullet">
///   <item><c>short</c> ('Y'), <c>bool</c> or character <c>byte</c> ('C'), <c>int</c> ('I'), <c>float</c> ('F'),
///         <c>double</c> ('D'), <c>long</c> ('L')</item>
///   <item><c>float[]</c> ('f'), <c>double[]</c> ('d'), <c>long[]</c> ('l'),
///         <c>int[]</c> ('i'), <c>bool[]</c>-as-<c>byte[]</c> ('b')</item>
///   <item><c>string</c> ('S' — kept raw, may contain the <c>\x00\x01</c> name/class
///         separator; see <see cref="SplitName"/>), <c>byte[]</c> ('R')</item>
/// </list>
/// ASCII files store numbers only as <c>long</c> / <c>double</c> (and arrays as
/// <c>long[]</c> / <c>double[]</c>), so the typed accessors below convert tolerantly.
/// </summary>
public sealed class FbxNode
{
    public string Name { get; }
    public List<object> Properties { get; } = new();
    public List<FbxNode> Children { get; } = new();
    public bool HasChildScope { get; set; }
    // Document metadata retained when repairing binary FBX files.
    public byte[]? FooterWatermark { get; set; }

    public FbxNode(string name) => Name = name;

    /// <summary>First child with the given name, or null.</summary>
    public FbxNode? Child(string name)
    {
        foreach (var c in Children)
            if (c.Name == name)
                return c;
        return null;
    }

    /// <summary>All children with the given name, in document order.</summary>
    public IEnumerable<FbxNode> ChildrenNamed(string name)
    {
        foreach (var c in Children)
            if (c.Name == name)
                yield return c;
    }

    /// <summary>
    /// Property <paramref name="i"/> converted to <typeparamref name="T"/>.
    /// Numeric scalars convert tolerantly across widths (e.g. an 'I' i32 read as long);
    /// anything else must match the stored type exactly.
    /// </summary>
    public T Prop<T>(int i)
    {
        object v = RawProp(i);
        if (v is T t)
            return t;

        var target = typeof(T);
        // s&box whitelist: Type.IsPrimitive is banned; enumerate the convertible targets.
        if (v is IConvertible && (ConvertTargets.Contains(target) || target == typeof(string)))
        {
            try
            {
                return (T)Convert.ChangeType(v, target, System.Globalization.CultureInfo.InvariantCulture);
            }
            // ArithmeticException covers OverflowException, which is not s&box-whitelisted
            catch (Exception ex) when (ex is InvalidCastException or ArithmeticException or FormatException)
            {
                throw new FormatException(
                    $"FBX node '{Name}': property {i} is {v.GetType().Name}, not convertible to {target.Name}.", ex);
            }
        }

        throw new FormatException(
            $"FBX node '{Name}': property {i} is {v.GetType().Name}, expected {target.Name}.");
    }

    /// <summary>Property <paramref name="i"/> as a double array (converts f/l/i/b arrays).</summary>
    public double[] AsDoubleArray(int i) => RawProp(i) switch
    {
        double[] d => d,
        float[] f => Array.ConvertAll(f, x => (double)x),
        long[] l => Array.ConvertAll(l, x => (double)x),
        int[] n => Array.ConvertAll(n, x => (double)x),
        byte[] b => Array.ConvertAll(b, x => (double)x),
        bool[] o => Array.ConvertAll(o, x => x ? 1.0 : 0.0),
        var v => throw TypeError(i, v, "double[]"),
    };

    /// <summary>Property <paramref name="i"/> as a float array (converts d/l/i/b arrays).</summary>
    public float[] AsFloatArray(int i) => RawProp(i) switch
    {
        float[] f => f,
        double[] d => Array.ConvertAll(d, x => (float)x),
        long[] l => Array.ConvertAll(l, x => (float)x),
        int[] n => Array.ConvertAll(n, x => (float)x),
        byte[] b => Array.ConvertAll(b, x => (float)x),
        bool[] o => Array.ConvertAll(o, x => x ? 1f : 0f),
        var v => throw TypeError(i, v, "float[]"),
    };

    /// <summary>Property <paramref name="i"/> as a long array (converts i/b; d/f if integral).</summary>
    public long[] AsLongArray(int i) => RawProp(i) switch
    {
        long[] l => l,
        int[] n => Array.ConvertAll(n, x => (long)x),
        byte[] b => Array.ConvertAll(b, x => (long)x),
        bool[] o => Array.ConvertAll(o, x => x ? 1L : 0L),
        double[] d => Array.ConvertAll(d, x => checked((long)x)),
        float[] f => Array.ConvertAll(f, x => checked((long)x)),
        var v => throw TypeError(i, v, "long[]"),
    };

    /// <summary>Property <paramref name="i"/> as an int array (converts b; l/d/f narrowing-checked).</summary>
    public int[] AsIntArray(int i) => RawProp(i) switch
    {
        int[] n => n,
        long[] l => Array.ConvertAll(l, x => checked((int)x)),
        byte[] b => Array.ConvertAll(b, x => (int)x),
        bool[] o => Array.ConvertAll(o, x => x ? 1 : 0),
        double[] d => Array.ConvertAll(d, x => checked((int)x)),
        float[] f => Array.ConvertAll(f, x => checked((int)x)),
        var v => throw TypeError(i, v, "int[]"),
    };

    /// <summary>Property <paramref name="i"/> as raw bytes ('R' blobs or 'b' bool arrays).</summary>
    public byte[] AsByteArray(int i) => RawProp(i) switch
    {
        byte[] b => b,
        var v => throw TypeError(i, v, "byte[]"),
    };

    /// <summary>Property <paramref name="i"/> as a string (raw 'S' content, separators intact).</summary>
    public string AsString(int i) => RawProp(i) switch
    {
        string s => s,
        var v => throw TypeError(i, v, "string"),
    };

    /// <summary>
    /// Splits an FBX object name into (name, class).
    /// Binary files store <c>"Name\x00\x01Class"</c> (e.g. <c>"mixamorig:Hips\x00\x01Model"</c>);
    /// ASCII files store <c>"Class::Name"</c> (e.g. <c>"Model::pelvis"</c>).
    /// A plain string with neither separator yields (name, "").
    /// </summary>
    public static (string Name, string Class) SplitName(string raw)
    {
        int bin = raw.IndexOf("\0\x01", StringComparison.Ordinal);
        if (bin >= 0)
            return (raw[..bin], raw[(bin + 2)..]);

        int ascii = raw.IndexOf("::", StringComparison.Ordinal);
        if (ascii >= 0)
            return (raw[(ascii + 2)..], raw[..ascii]);

        return (raw, "");
    }

    /// <summary>Primitive scalar types <see cref="Prop{T}"/> converts to (whitelist-safe IsPrimitive substitute).</summary>
    private static readonly HashSet<Type> ConvertTargets = new()
    {
        typeof(bool), typeof(byte), typeof(sbyte), typeof(short), typeof(ushort),
        typeof(int), typeof(uint), typeof(long), typeof(ulong),
        typeof(float), typeof(double), typeof(char),
    };

    private object RawProp(int i)
    {
        if (i < 0 || i >= Properties.Count)
            throw new FormatException(
                $"FBX node '{Name}': property index {i} out of range (has {Properties.Count}).");
        return Properties[i];
    }

    private FormatException TypeError(int i, object v, string wanted) =>
        new($"FBX node '{Name}': property {i} is {v.GetType().Name}, expected {wanted}.");
}
notpointless.chomnr_humanoid_hand_retargeter / Runtime/RetargetedWeaponController.cs
Game library
#nullable enable
using Sandbox;
using HumanoidHandRetargeter.Target;

namespace HumanoidHandRetargeter;

/// <summary>Optional local player controls for exported weapons. Games may instead drive RetargetedWeapon directly.</summary>
[Title("Retargeted Weapon Controls"), Category("Animation")]
public sealed class RetargetedWeaponController : Component, ICameraModifier
{
    [Property] public PlayerController? Player { get; set; }
    [Property] public bool ReadInput { get; set; } = true;
    [Property] public int MagazineSize { get; set; } = 30;
    [Property] public int StartingReserve { get; set; } = 120;
    [Property] public float RoundsPerMinute { get; set; } = 800;
    [Property] public float ReloadSeconds { get; set; } = 2.7f;
    [Property] public float EmptyReloadSeconds { get; set; } = 3.2f;
    [Property] public float Damage { get; set; } = 20;
    [Property] public float Range { get; set; } = 10000;
    [Property] public float HipFov { get; set; } = 75;
    [Property] public float AimFov { get; set; } = 55;
    [Property] public string WorldGripBone { get; set; } = "hand_R";
    [Property] public int BodyHoldType { get; set; } = 2;
    [Property] public int BodyHandedness { get; set; }
    public int Ammo => magazine?.Rounds ?? MagazineSize;
    public int Reserve => magazine?.Reserve ?? StartingReserve;
    public bool Reloading => magazine?.Reloading ?? false;
    public bool Aiming { get; private set; }
    public int ShotsFired { get; private set; }
    public int Hits { get; private set; }
    public global::Vector3 LastHit { get; private set; }
    public Action<SceneTraceResult>? Shot { get; set; }
    private WeaponMagazine? magazine;
    private RetargetedWeapon? weapon;
    private float nextShot, reloadEnds, currentFov;
    public int CameraOrder => 200;
    protected override void OnStart()
    {
        weapon=GetComponent<RetargetedWeapon>();
        Player ??= Scene.GetAllComponents<PlayerController>().FirstOrDefault();
        if(Player is not null)Player.HideBodyInFirstPerson=true;
        magazine=new(MagazineSize,StartingReserve);
        currentFov=HipFov;
        weapon?.Deploy(true);
    }
    public bool Fire()
    {
        if(weapon is null||magazine is null||Player is null||Time.Now<nextShot||Reloading)return false;
        nextShot=Time.Now+60/Math.Max(1,RoundsPerMinute);
        if(!magazine.TryFire()){weapon.DryFire();return false;}
        weapon.Attack();weapon.SetEmpty(Ammo==0);ShotsFired++;
        UpdateBodyPose();
        Player.Renderer?.Set("b_attack",true);
        var eye=Player.EyeTransform;
        var hit=Scene.Trace.Ray(eye.Position,eye.Position+eye.Rotation.Forward*Range)
            .IgnoreGameObjectHierarchy(Player.GameObject).IgnoreGameObjectHierarchy(GameObject).UseHitboxes().Run();
        if(hit.Hit)
        {
            Hits++;LastHit=hit.HitPosition;
            var info=new DamageInfo{Damage=Damage,Attacker=Player.GameObject,Weapon=GameObject,Position=hit.HitPosition,Origin=eye.Position};
            for(var obj=hit.GameObject;obj is not null;obj=obj.Parent)
                if(obj.GetComponent<IDamageable>() is {} damageable){damageable.OnDamage(info);break;}
        }
        Shot?.Invoke(hit);
        return true;
    }
    public bool Reload()
    {
        if(magazine is null||weapon is null||!magazine.BeginReload())return false;
        weapon.Reload(Ammo==0);reloadEnds=Time.Now+(Ammo==0?EmptyReloadSeconds:ReloadSeconds);
        UpdateBodyPose();
        Player?.Renderer?.Set("b_reload",true);
        return true;
    }
    public void Aim(bool value){Aiming=value&&!Reloading;if(weapon is not null)weapon.Aiming=Aiming;}
    protected override void OnUpdate()
    {
        if(weapon is null||Player is null||magazine is null)return;
        if(Reloading&&Time.Now>=reloadEnds){magazine.FinishReload();weapon.SetEmpty(Ammo==0);}
        if(ReadInput)Aim(Input.Down("Attack2"));
        if(Reloading)Aim(false);
        weapon.Aiming=Aiming;
        weapon.Sprinting=ReadInput&&Input.Down("Run")&&Player.Velocity.Length>10&&!Aiming;
        weapon.Movement=Math.Clamp(Player.Velocity.Length/150f,0,1);
        weapon.SetParameter("attack_hold",ReadInput&&Input.Down("Attack1")&&!Reloading&&Ammo>0?1f:0f);
        weapon.SetParameter("firing_mode",3);
        weapon.SetParameter("ironsights_fire_scale",.3f);
        if(ReadInput)
        {
            if(Input.Pressed("Reload"))Reload();
            if(Input.Down("Attack1")&&!weapon.Sprinting)Fire();
        }
        weapon.ShowHands=!Player.ThirdPerson;
        if(Player.ThirdPerson)weapon.WorldTransform=Player.EyeTransform;
        // Keep the body graph warm even when hidden, so a camera switch during a shot
        // or reload reveals the same action rather than restarting a holding pose.
        UpdateBodyPose();
        weapon.ThirdPersonBody=Player.ThirdPerson?Player.Renderer:null;
        weapon.WorldGripBone=WorldGripBone;
    }
    private void UpdateBodyPose()
    {
        if(Player?.Renderer is not {} body)return;
        body.Set("holdtype",BodyHoldType);body.Set("holdtype_handedness",BodyHandedness);
        body.Set("aim_body_weight",1f);
    }
    public void ModifyCamera(CameraComponent camera,ref CameraView view)
    {
        if(Player is not null&&!Player.ThirdPerson&&camera==Scene.Camera)
        {
            currentFov=MathX.Lerp(currentFov,Aiming?AimFov:HipFov,Math.Clamp(Time.Delta*12,0,1));
            view.FieldOfView=currentFov;
        }
    }
    public void PostCameraSetup(CameraComponent camera,in CameraView view)
    {
        // EyeTransform during Update can precede the player's final camera pose.
        // Place both viewmodel meshes against the composed view before PreRender.
        if(weapon is not null&&Player is not null&&!Player.ThirdPerson&&camera==Scene.Camera)
            weapon.WorldTransform=new global::Transform(view.Position,view.Rotation);
    }
    protected override void OnDisabled()
    {
        magazine?.CancelReload();Aiming=false;
        if(weapon is not null){weapon.Aiming=false;weapon.ThirdPersonBody=null;}
        Player?.Renderer?.Set("holdtype",0);
    }
}
notpointless.chomnr_humanoid_hand_retargeter / Code/Core/Retargeting/HandMotionOptions.cs
Game library
#nullable enable
namespace HumanoidHandRetargeter.Retargeting;

public sealed class HandMotionOptions
{
    public bool TransferWristPosition { get; init; } = true;
    public bool ScaleWristTravel { get; init; } = true;
    /// <summary>Known source-to-target model axes, independent of the wrists' rest rotations.</summary>
    public System.Numerics.Quaternion? WristTravelBasis {get;init;}
    public bool SolveArmIk { get; init; } = true;
    public bool PreserveWeaponGrip { get; init; } = true;
    /// <summary>Shared source weapon-to-target translation in centimeters. Grip motion is never scaled per hand.</summary>
    public System.Numerics.Vector3? WeaponSpaceOffset {get;init;}
}
notpointless.chomnr_humanoid_hand_retargeter / Code/Core/Validation/HandRigValidator.cs
Game library
#nullable enable
using HumanoidHandRetargeter.Mapping;
using SkeletonModel = HumanoidHandRetargeter.Skeleton.Skeleton;

namespace HumanoidHandRetargeter.Validation;

public sealed record RigIssue(string Code, string Message, HandSide? Side = null, int? Bone = null);

/// <summary>Shared validation for automatic/manual mapping, batch processing and the editor.</summary>
public static class HandRigValidator
{
    public static IReadOnlyList<RigIssue> Validate(SkeletonModel skeleton, IEnumerable<HandRigDefinition> hands)
    {
        ArgumentNullException.ThrowIfNull(skeleton);
        ArgumentNullException.ThrowIfNull(hands);
        var issues = new List<RigIssue>();
        var claimed = new HashSet<int>();
        var sides = new HashSet<HandSide>();
        var count = 0;

        foreach (var bone in skeleton.Bones)
        {
            if (!PoseValidator.ValidTransform(bone.RestLocal))
                issues.Add(new("invalid-rest", $"Bone '{bone.Name}' has an invalid rest transform.", Bone: bone.Index));
            var world = skeleton.RestWorld[bone.Index].Pos;
            if (!float.IsFinite(world.X) || !float.IsFinite(world.Y) || !float.IsFinite(world.Z))
                issues.Add(new("invalid-world-rest", $"Bone '{bone.Name}' has an invalid accumulated rest position.", Bone: bone.Index));
        }

        foreach (var hand in hands)
        {
            count++;
            if (hand is null)
            {
                issues.Add(new("missing-hand", "A hand mapping is missing."));
                continue;
            }
            if (!Enum.IsDefined(typeof(HandSide), hand.Side))
                issues.Add(new("invalid-side", "Choose left or right for the hand."));
            if (!sides.Add(hand.Side))
                issues.Add(new("duplicate-side", $"More than one {hand.Side} hand is mapped.", hand.Side));

            var arm = new[] { hand.Clavicle, hand.UpperArm, hand.Forearm, hand.Wrist }
                .Where(b => b.HasValue).Select(b => b!.Value).ToArray();
            CheckChain(arm, hand.Side, "arm", allowZeroLength: true);
            var roles = new HashSet<string>(StringComparer.Ordinal);
            foreach (var digit in hand.Digits)
            {
                if (digit is null)
                {
                    issues.Add(new("missing-digit", "A digit mapping is missing.", hand.Side));
                    continue;
                }
                var key = digit.Role == DigitRole.Extra ? "Extra:" + digit.ExtraSlot : digit.Role.ToString();
                if (!Enum.IsDefined(typeof(DigitRole), digit.Role)
                    || (digit.Role == DigitRole.Extra && string.IsNullOrWhiteSpace(digit.ExtraSlot)))
                    issues.Add(new("invalid-digit-role", "Choose a digit role or name its extra slot.", hand.Side));
                if (!roles.Add(key))
                    issues.Add(new("duplicate-digit-role", $"{hand.Side} {key} is mapped more than once.", hand.Side));
                if (digit.Segments.Count == 0)
                    issues.Add(new("empty-digit", $"{hand.Side} {key} needs at least one segment.", hand.Side));
                var chain = digit.Bones.ToArray();
                CheckChain(chain, hand.Side, key, allowZeroLength: false, tip: digit.Tip);
                if (chain.Length > 0 && Valid(chain[0]) && Valid(hand.Wrist)
                    && !skeleton.DescendsFrom(chain[0], hand.Wrist))
                    issues.Add(new("wrong-wrist", $"{hand.Side} {key} must descend from its wrist.", hand.Side, chain[0]));
            }
            foreach (var helper in hand.TwistOrHelperBones)
            {
                Claim(helper, hand.Side);
                var armRoot = arm[0];
                if (Valid(helper) && Valid(armRoot) && !skeleton.DescendsFrom(helper, armRoot))
                    issues.Add(new("wrong-helper-root", $"{hand.Side} helper must belong to its mapped arm or hand hierarchy.", hand.Side, helper));
            }
        }
        if (count == 0)
            issues.Add(new("no-hands", "Map at least one wrist before retargeting."));
        return issues.AsReadOnly();

        bool Valid(int bone) => bone >= 0 && bone < skeleton.Count;
        void Claim(int bone, HandSide side)
        {
            if (!Valid(bone))
                issues.Add(new("invalid-bone", $"{side} mapping references a missing bone.", side, bone));
            else if (!claimed.Add(bone))
                issues.Add(new("overlapping-bone", $"Bone '{skeleton[bone].Name}' is assigned to multiple roles.", side, bone));
        }
        void CheckChain(int[] bones, HandSide side, string role, bool allowZeroLength, int? tip = null)
        {
            for (var i = 0; i < bones.Length; i++)
            {
                Claim(bones[i], side);
                if (i == 0 || !Valid(bones[i - 1]) || !Valid(bones[i])) continue;
                if (!skeleton.DescendsFrom(bones[i], bones[i - 1]))
                    issues.Add(new("chain-order", $"{side} {role} joints must follow their hierarchy.", side, bones[i]));
                if (!allowZeroLength && bones[i] != tip &&
                    System.Numerics.Vector3.DistanceSquared(skeleton.RestWorld[bones[i - 1]].Pos,
                        skeleton.RestWorld[bones[i]].Pos) < 1e-12f)
                    issues.Add(new("zero-length-digit", $"{side} {role} has coincident joints; correct the chain or classify a helper.", side, bones[i]));
            }
        }
    }
}
notpointless.chomnr_humanoid_hand_retargeter / Code/Core/Mapping/HandRigDetector.cs
Game library
#nullable enable
using HumanoidHandRetargeter.Validation;
using SkeletonModel = HumanoidHandRetargeter.Skeleton.Skeleton;

namespace HumanoidHandRetargeter.Mapping;

/// <summary>
/// Name candidates constrained by wrist ancestry. Reuses legacy name tokenization;
/// deliberately replaces its full-body confidence and fixed phalanx slots.
/// Unnamed/ambiguous anatomy is proposed for review, never silently accepted.
/// </summary>
public static class HandRigDetector
{
    public static HandMappingResult Detect(SkeletonModel skeleton,
        IEnumerable<HandRigDefinition>? manualOverrides = null)
    {
        ArgumentNullException.ThrowIfNull(skeleton);
        var hints = HandDetectionProfiles.Resolve(skeleton);
        var names = skeleton.Bones.Select(b => new NameInfo(b.Name,hints[b.Index])).ToArray();
        var hands = new List<HandRigDefinition>();
        var candidates = new List<HandMappingCandidate>();
        var issues = new List<RigIssue>();
        var review = false;
        var deformBelow = new bool?[skeleton.Count];
        int? digitWrist = null;
        var overrides = (manualOverrides ?? Array.Empty<HandRigDefinition>()).ToArray();
        if (overrides.Any(h => h is null))
            throw new ArgumentException("Manual overrides cannot contain a null hand.", nameof(manualOverrides));

        foreach (var side in new[] { HandSide.Left, HandSide.Right })
        {
            var manual = overrides.Where(h => h.Side == side).ToArray();
            if (manual.Length > 0)
            {
                hands.AddRange(manual);
                foreach (var hand in manual)
                {
                    Add(side, "Wrist", new[] { hand.Wrist }, 1f, "Manual mapping", true);
                    foreach (var (role, bone) in new[] { ("Clavicle", hand.Clavicle), ("UpperArm", hand.UpperArm), ("Forearm", hand.Forearm) })
                        if (bone is int b) Add(side, role, new[] { b }, 1f, "Manual mapping", true);
                    foreach (var digit in hand.Digits.Where(d => d is not null))
                        Add(side, digit.Role.ToString(), digit.Bones, 1f, "Manual mapping", true);
                    foreach (var helper in hand.TwistOrHelperBones)
                        Add(side, "Helper", new[] { helper }, 1f, "Manual mapping", true);
                }
                continue;
            }
            var wrists = skeleton.Bones.Where(b => names[b.Index].IsWrist && !Blocked(b.Index)
                && SideOf(b.Index) == side).Select(b => b.Index).ToArray();
            var deformWrists=wrists.Where(w=>names[w].IsDeform).ToArray();
            if(deformWrists.Length==1 && wrists.All(w=>names[w].FamilyKey==names[deformWrists[0]].FamilyKey))
                wrists=deformWrists; // Rigify exports can contain DEF, ORG and animator-control copies.
            if (wrists.Length == 0) continue; // A one-sided rig is valid.
            if (wrists.Length > 1)
            {
                foreach (var wrist in wrists)
                    Add(side, "Wrist", new[] { wrist }, .5f, "Multiple wrist candidates on this side; choose one manually", false);
                issues.Add(new("ambiguous-wrist", $"Choose the {side} wrist in Hand Mapping.", side));
                continue;
            }
            var selected = wrists[0];
            digitWrist = selected;
            Add(side, "Wrist", new[] { selected }, .95f, names[selected].Profile is {} wristProfile
                ? $"{wristProfile} profile and wrist hierarchy agree" : "Wrist name and side agree", true);
            var digits = new List<DigitChain>();
            var seenRoles = new HashSet<DigitRole>();
            foreach (var root in DigitRoots(selected))
            {
                var chain = new List<int>();
                int? meta = null, tip = null;
                var current = root;
                var chainReview = false;
                while (true)
                {
                    var info = names[current];
                    if (info.IsTip)
                    {
                        tip = current;
                        if (skeleton.ChildrenOf(current).Count != 0) chainReview = true;
                        break;
                    }
                    if (info.IsMeta && current == root) meta = current;
                    else chain.Add(current);
                    var next = JointChildren(current).ToArray();
                    if (next.Length == 0) break;
                    if (next.Length > 1) { chainReview = true; break; }
                    current = next[0];
                }
                var role = names[root].Digit;
                // A palm/metacarpal may be generically named, but its child's digit is explicit.
                if (role is null && meta.HasValue && chain.Count > 0) role = names[chain[0]].Digit;
                var chainSideMismatch = chain.Any(b => names[b].Side is HandSide explicitSide && explicitSide != side);
                var chainRoleMismatch = role.HasValue && chain.Any(b => names[b].Digit is DigitRole namedRole && namedRole != role);
                if (chainSideMismatch || chainRoleMismatch || chainReview || chain.Count == 0)
                {
                    Add(side, role?.ToString() ?? "Digit", chain, .3f,
                        "Branched, empty or side-inconsistent digit chain; correct the mapping", false);
                    review = true;
                    continue;
                }
                if (role.HasValue && !seenRoles.Add(role.Value))
                {
                    Add(side, role.Value.ToString(), chain, .4f, "Duplicate anatomical digit candidate; choose the intended chain", false);
                    review = true;
                    continue;
                }
                var digit = new DigitChain(role ?? DigitRole.Extra, chain, meta, tip,
                    role is null ? skeleton[root].Name : "");
                digits.Add(digit);
                var confidence = role.HasValue ? .9f : .4f;
                Add(side, role?.ToString() ?? "Extra:" + skeleton[root].Name, digit.Bones, confidence,
                    role.HasValue ? names[root].Profile is {} digitProfile ? $"{digitProfile} digit convention follows this wrist's hierarchy"
                        : "Named digit follows this wrist's hierarchy; segment numbering is not used for side"
                        : "Unlabelled digit chain; assign an anatomical role or confirm an extra digit", true);
                if (!role.HasValue) review = true;
            }

            var ancestors = new List<int>();
            for (var p = skeleton[selected].ParentIndex; p >= 0; p = skeleton[p].ParentIndex)
                ancestors.Add(p);
            int? Find(params string[] aliases) => ancestors
                .Where(b => !names[b].IsHelper && !Blocked(b) && aliases.Any(a => names[b].Core.EndsWith(a, StringComparison.Ordinal)))
                .Select(b => (int?)b).FirstOrDefault();
            int? ProfileJoint(string role)=>ancestors.Where(b=>names[b].ProfileRole==role && !names[b].IsHelper && !Blocked(b))
                .Select(b=>(int?)b).FirstOrDefault();
            var forearm = ProfileJoint("LowerArm") ?? Find("forearm", "lowerarm", "armlower", "elbow");
            var upperArm = ProfileJoint("UpperArm") ?? Find("upperarm", "armupper", "uparm");
            var clavicle = Find("clavicle", "collar") ?? ProfileJoint("Clavicle");
            // Prototype calls upper arm Shoulder, Mixamo calls clavicle Shoulder.
            if (upperArm is null && clavicle.HasValue) upperArm = Find("shoulder");
            upperArm ??= ancestors.Where(b => names[b].Core.EndsWith("arm", StringComparison.Ordinal)
                && b != forearm && !names[b].IsHelper && !Blocked(b)).Select(b => (int?)b).FirstOrDefault();
            clavicle ??= Find("shoulder");
            if (clavicle == upperArm) clavicle = null;
            foreach (var (role, bone) in new[] { ("Clavicle", clavicle), ("UpperArm", upperArm), ("Forearm", forearm) })
                if (bone is int b)
                {
                    var mismatch = names[b].ConflictingSides || (SideOf(b) is HandSide namedSide && namedSide != side);
                    Add(side, role, new[] { b }, mismatch ? .3f : .9f,
                        mismatch ? "Arm name conflicts with the wrist side; correct the mapping" : "Named joint on the wrist ancestor chain", true);
                    review |= mismatch;
                }
            var armRoot = clavicle ?? upperArm ?? forearm ?? selected;
            var helpers = skeleton.Bones.Where(b => names[b.Index].IsHelper && !Blocked(b.Index)
                && skeleton.DescendsFrom(b.Index, armRoot)).Select(b => b.Index).ToArray();
            foreach (var helper in helpers)
                Add(side, "Helper", new[] { helper }, .9f, "Explicit twist/helper name in this arm hierarchy", true);
            hands.Add(new(side, selected, digits, clavicle, upperArm, forearm, helpers));
        }

        // Unsupported enum values must reach validation instead of being silently dropped.
        hands.AddRange(overrides.Where(h => !Enum.IsDefined(typeof(HandSide), h.Side)));
        foreach (var bone in skeleton.Bones.Where(b => names[b.Index].IsWrist && !Blocked(b.Index) && SideOf(b.Index) is null
            && !hands.Any(h => h.Wrist == b.Index)))
        {
            Add(null, "Wrist", new[] { bone.Index }, .4f, "Wrist name found but side is unknown or conflicting", false);
            review = true;
        }
        if (hands.Count == 0 && candidates.Count == 0)
        {
            // Topology hints only: without a known side/frame, geometry must not fabricate left/right.
            foreach (var bone in skeleton.Bones.Where(b => !Blocked(b.Index) &&
                skeleton.ChildrenOf(b.Index).Count(c => !Blocked(c) && skeleton.ChildrenOf(c).Count > 0) >= 2))
                Add(null, "Wrist", new[] { bone.Index }, .2f, "Branching hierarchy may be a palm; confirm side and wrist manually", false);
            review = true;
        }
        issues.AddRange(HandRigValidator.Validate(skeleton, hands));
        return new(hands, candidates, issues, review);

        void Add(HandSide? side, string role, IEnumerable<int> bones, float confidence, string reason, bool selected)
            => candidates.Add(new(side, role, Array.AsReadOnly(bones.ToArray()), confidence, reason, selected));

        // The viewmodel may own the whole hierarchy: a weapon-named ancestor is not
        // evidence that an explicitly named child wrist is a mechanical weapon bone.
        bool Blocked(int bone) => names[bone].IsNonHandTrack;
        HandSide? SideOf(int bone)
        {
            for (var b = bone; b >= 0; b = skeleton[b].ParentIndex)
            {
                if (names[b].ConflictingSides) return null;
                if (names[b].Side.HasValue) return names[b].Side;
            }
            return null;
        }
        IEnumerable<int> DigitRoots(int parent)
        {
            foreach (var child in JointChildren(parent))
            {
                // Some palm/metacarpal containers parent several different digits
                // (notably thumb and index). They are shared anatomy, not a fork
                // within one finger. Keep the container wrist-driven and map each
                // named child chain independently so no bone is assigned twice.
                var branches=JointChildren(child).Where(i=>!names[i].IsTip).ToArray();
                if(names[child].IsMeta&&branches.Length>1
                    &&branches.All(i=>names[i].Digit.HasValue)
                    &&branches.Select(i=>names[i].Digit).Distinct().Count()==branches.Length)
                {
                    foreach(var branch in branches)yield return branch;
                    continue;
                }
                if (!names[child].IsTip) yield return child;
            }
        }
        // Layered rigs (Rigify DEF/ORG/MCH, animator controls) can parent deform digits
        // under a non-deform palm next to their control copies. Below a deform wrist,
        // follow only the deform layer and the containers that lead to it.
        bool LeadsToDeform(int bone)
        {
            if (deformBelow[bone] is bool known) return known;
            var result = names[bone].IsDeform || skeleton.ChildrenOf(bone).Any(LeadsToDeform);
            deformBelow[bone] = result;
            return result;
        }
        IEnumerable<int> JointChildren(int parent)
        {
            var layered = digitWrist is int wrist && names[wrist].IsDeform
                && (parent == wrist || skeleton.DescendsFrom(parent, wrist))
                && skeleton.ChildrenOf(parent).Any(c => !Blocked(c) && LeadsToDeform(c));
            foreach (var child in skeleton.ChildrenOf(parent))
            {
                if (Blocked(child)) continue;
                // The parent's own deform copy is a skinned twin, not a digit.
                if (layered && (!LeadsToDeform(child)
                    || names[child].IsDeform && !names[child].IsTip && names[child].FamilyKey == names[parent].FamilyKey)) continue;
                if (names[child].IsHelper)
                {
                    foreach (var nested in JointChildren(child)) yield return nested;
                }
                else yield return child;
            }
        }
    }

    private sealed class NameInfo
    {
        public string Core { get; }
        public HandSide? Side { get; }
        public bool ConflictingSides { get; }
        public DigitRole? Digit { get; }
        public bool IsWrist { get; }
        public bool IsHelper { get; }
        public bool IsNonHandTrack { get; }
        public bool IsMeta { get; }
        public bool IsTip { get; }
        public string? Profile { get; }
        public string? ProfileRole { get; }
        public bool IsDeform { get; }
        public string FamilyKey { get; }

        public NameInfo(string name,HandDetectionProfiles.Hint? hint)
        {
            var stripped = name[(name.LastIndexOf(':') + 1)..];
            var hash = stripped.LastIndexOf('#');
            if (hash >= 0 && stripped[(hash + 1)..].All(char.IsDigit)) stripped = stripped[..hash];
            IsDeform=stripped.StartsWith("DEF-",StringComparison.OrdinalIgnoreCase);
            FamilyKey=HandDetectionProfiles.Key(IsDeform||stripped.StartsWith("ORG-",StringComparison.OrdinalIgnoreCase)
                ||stripped.StartsWith("MCH-",StringComparison.OrdinalIgnoreCase)?stripped[4..]:stripped);
            Profile=hint?.Profile;ProfileRole=hint?.Role;
            var tokens = BoneNameTokens.Tokenize(stripped);
            bool Has(params string[] words) => tokens.Any(t => words.Contains(t, StringComparer.Ordinal));
            var left = Has("l", "left", "lft");
            var right = Has("r", "right", "rgt");
            // Case-only changes must not erase a wrist's side when camel-case boundaries disappear.
            var compact = string.Concat(tokens.Where(t => !t.All(char.IsDigit)));
            foreach (var joint in new[] { "hand", "wrist" })
            {
                left |= compact.EndsWith("left" + joint, StringComparison.Ordinal) || compact.EndsWith(joint + "left", StringComparison.Ordinal)
                    || compact.EndsWith(joint + "l", StringComparison.Ordinal) || compact == "l" + joint;
                right |= compact.EndsWith("right" + joint, StringComparison.Ordinal) || compact.EndsWith(joint + "right", StringComparison.Ordinal)
                    || compact.EndsWith(joint + "r", StringComparison.Ordinal) || compact == "r" + joint;
            }
            ConflictingSides = left && right;
            Side = left == right ? left ? null : hint?.Side : left ? HandSide.Left : HandSide.Right;
            Core = string.Concat(tokens.Where(t => !new[] { "l", "left", "lft", "r", "right", "rgt", "def", "org", "mch", "stretch" }.Contains(t)
                && !t.All(char.IsDigit)));
            IsHelper = Has("twist", "twistctrl", "roll", "share", "sharebone", "helper", "hlp", "corrective", "control", "ctrl");
            IsNonHandTrack = Has("weapon", "camera", "ik", "ikrule", "hold", "socket", "attachment", "magazine", "bolt", "slide", "trigger")
                || compact.StartsWith("weapon", StringComparison.Ordinal) || compact.StartsWith("camera", StringComparison.Ordinal);
            IsMeta = Has("meta", "metacarpal", "carpal", "palm") || hint?.Role.EndsWith("Meta",StringComparison.Ordinal)==true;
            IsTip = Has("tip", "end", "nub", "endmarker") || hint?.Role.EndsWith("Tip",StringComparison.Ordinal)==true;
            bool DigitName(params string[] aliases) => Has(aliases)
                || aliases.Any(a => Core.EndsWith(a, StringComparison.Ordinal) || Core.EndsWith(a + "finger", StringComparison.Ordinal));
            Digit = DigitName("thumb") ? DigitRole.Thumb : DigitName("index", "forefinger", "point", "pointer", "pointing") ? DigitRole.Index
                : DigitName("middle", "mid") ? DigitRole.Middle : DigitName("ring") ? DigitRole.Ring
                : DigitName("pinky", "pinkie", "pink", "little") ? DigitRole.Pinky : null;
            if(hint is not null && HandDetectionProfiles.IsDigit(hint.Role))
                Digit=Enum.GetValues<DigitRole>().First(d=>hint.Role.StartsWith(d.ToString(),StringComparison.Ordinal));
            IsWrist = !IsHelper && !IsNonHandTrack && !IsTip && Digit is null
                && new[] { "hand", "wrist", "handl", "handr", "wristl", "wristr", "handleft", "handright", "wristleft", "wristright" }
                    .Any(suffix => Core.EndsWith(suffix, StringComparison.Ordinal));
            if(hint?.Role=="Hand"&&!IsHelper&&!IsNonHandTrack&&!IsTip)IsWrist=true;
            else if(hint is not null)IsWrist=false;
        }
    }
}
notpointless.chomnr_humanoid_hand_retargeter / Core/Mapping/HandDetectionProfiles.cs
Game library
#nullable enable
using System.Text.RegularExpressions;
using Skel = HumanoidHandRetargeter.Skeleton.Skeleton;

namespace HumanoidHandRetargeter.Mapping;

/// <summary>Reference conventions supply semantic hints; they never bypass hierarchy validation.</summary>
internal static partial class HandDetectionProfiles
{
    internal sealed record Alias(string Role,string Name);
    internal sealed record Hint(string Role,HandSide Side,string Profile);
    internal sealed class Profile
    {
        public string Name { get; }
        private readonly Regex[] prefixes;
        public Dictionary<string,string[]> Roles { get; }
        public int RoleCount { get; }
        public Profile(string name,string[] patterns,Alias[] aliases)
        {
            Name=name;prefixes=patterns.Select(p=>new Regex(p,RegexOptions.IgnoreCase|RegexOptions.CultureInvariant)).ToArray();
            Roles=aliases.GroupBy(a=>Key(a.Name)).ToDictionary(g=>g.Key,g=>g.Select(a=>a.Role).Distinct().ToArray());
            RoleCount=aliases.Select(a=>a.Role).Distinct().Count();
        }
        public string Strip(string name)
        {
            // Strip FBX namespaces independently of exporter prefixes (e.g. Character:CC_Base_L_Hand).
            name=name[(name.LastIndexOf(':')+1)..];
            var hash=name.LastIndexOf('#');
            if(hash>=0&&name[(hash+1)..].All(char.IsDigit))name=name[..hash];
            foreach(var prefix in prefixes)name=prefix.Replace(name,"");
            return name;
        }
    }
    internal static string Key(string name)=>string.Concat(name.Where(char.IsLetterOrDigit)).ToLowerInvariant();

    internal static Hint?[] Resolve(Skel rig)
    {
        var proposals=new Dictionary<int,List<(int Score,Hint Hint)>>();
        foreach(var profile in Catalog.Concat(new[]{CinemaHumanProfile}))
        {
            var matches=new Dictionary<int,string>();
            foreach(var bone in rig.Bones)
                if(profile.Roles.TryGetValue(Key(profile.Strip(bone.Name)),out var roles)&&roles.Length==1)
                    matches[bone.Index]=roles[0];
            foreach(var wrist in matches.Where(p=>p.Value is "HandL" or "HandR"))
            {
                var side=wrist.Value[^1];
                var relevant=matches.Where(p=>p.Value[^1]==side && (p.Key==wrist.Key ||
                    (IsDigit(p.Value)&&rig.DescendsFrom(p.Key,wrist.Key)) ||
                    (IsArm(p.Value)&&rig.DescendsFrom(wrist.Key,p.Key)))).ToArray();
                // Match coverage distinguishes e.g. SOMA metacarpals from Mixamo phalanges.
                // Namespace evidence breaks equivalent-name ties without overpowering anatomy.
                var score=relevant.Length*1000+relevant.Select(p=>p.Value).Distinct().Count()*100/Math.Max(1,profile.RoleCount/2)
                    +relevant.Count(p=>profile.Strip(rig[p.Key].Name)!=rig[p.Key].Name)*2;
                foreach(var match in relevant)
                {
                    if(!proposals.TryGetValue(match.Key,out var list))proposals[match.Key]=list=new();
                    list.Add((score,new(match.Value[..^1],side=='L'?HandSide.Left:HandSide.Right,profile.Name)));
                }
            }
        }
        var hints=new Hint?[rig.Count];
        foreach(var pair in proposals)
        {
            var best=pair.Value.Where(p=>p.Score==pair.Value.Max(v=>v.Score)).Select(p=>p.Hint).ToArray();
            if(best.Select(p=>(p.Role,p.Side)).Distinct().Count()==1)hints[pair.Key]=best[0];
        }
        ResolveNumberedFpsHands(rig,hints);
        ResolveUnlabelledSplitArms(rig,hints);
        return hints;
    }
    // Numbered FPS rays can run from thumb to pinky or in reverse, with an
    // additional palm joint on the four fingers. Require the whole convention
    // and its radial geometry; bare finger numbers alone are ambiguous.
    private static void ResolveNumberedFpsHands(Skel rig,Hint?[] hints)
    {
        foreach(var side in new[]{HandSide.Left,HandSide.Right})
        {
            var suffix=side==HandSide.Left?"l":"r";
            string Name(int bone)=>Key(rig[bone].Name[(rig[bone].Name.LastIndexOf(':')+1)..]).Replace("left","l").Replace("right","r");
            var wrists=rig.Bones.Where(b=>Name(b.Index)=="hand"+suffix).ToArray();
            if(wrists.Length!=1)continue;
            var wrist=wrists[0].Index;
            foreach(var thumbNumber in new[]{1,5})
            {
            var chains=new List<int[]>();
            for(var finger=1;finger<=5;finger++)
            {
                var chain=new List<int>();var parent=wrist;
                for(var joint=1;joint<=(finger==thumbNumber?3:4);joint++)
                {
                    var pattern=$"^finger0*{finger}0*{joint}{suffix}$";
                    var children=rig.ChildrenOf(parent).Where(i=>Regex.IsMatch(Name(i),pattern)).ToArray();
                    if(children.Length!=1)break;
                    parent=children[0];chain.Add(parent);
                }
                if(chain.Count!=(finger==thumbNumber?3:4))break;
                // A three-joint thumb must not just be a truncated four-joint finger.
                if(finger==thumbNumber&&rig.ChildrenOf(parent).Any(i=>Regex.IsMatch(Name(i),$"^finger0*{finger}0*4{suffix}$")))break;
                chains.Add(chain.ToArray());
            }
            if(chains.Count!=5)continue;
            var thumb=rig.RestWorld[chains[thumbNumber-1][0]].Pos;
            var radial=System.Numerics.Vector3.DistanceSquared(thumb,rig.RestWorld[chains[thumbNumber==5?3:1][0]].Pos);
            var ulnar=System.Numerics.Vector3.DistanceSquared(thumb,rig.RestWorld[chains[thumbNumber==5?0:4][0]].Pos);
            if(!float.IsFinite(radial)||!float.IsFinite(ulnar)||radial>=ulnar*.8f)continue;
            var roles=thumbNumber==5?new[]{"Pinky","Ring","Middle","Index","Thumb"}:new[]{"Thumb","Index","Middle","Ring","Pinky"};
            if(chains.SelectMany(c=>c).Any(i=>hints[i] is not null))continue;
            for(var finger=0;finger<5;finger++)
                for(var joint=0;joint<chains[finger].Length;joint++)
                    hints[chains[finger][joint]]=new(roles[finger]+(finger!=thumbNumber-1&&joint==0?"Meta":"Prox"),side,"numbered_fps_hands");
            }
        }
    }
    internal static bool IsDigit(string role)=>new[]{"Thumb","Index","Middle","Ring","Pinky"}.Any(role.StartsWith);
    private static readonly Profile CinemaHumanProfile=CinemaBaseHuman();
    private static Profile CinemaBaseHuman()
    {
        var aliases=new List<Alias>();
        foreach(var side in new[]{"L","R"})
        {
            aliases.Add(new("UpperArm"+side,side+"Upperarm"));
            aliases.Add(new("LowerArm"+side,side+"Forearm1"));
            aliases.Add(new("Hand"+side,side+"Palm"));
            var digits=new[]{"Thumb","Index","Middle","Ring","Pinky"};
            var joints=new[]{"Prox","Mid","Dist"};
            for(var d=0;d<5;d++)for(var j=0;j<3;j++)
                aliases.Add(new(digits[d]+joints[j]+side,side+"Digit"+(d+1)+(j+1)));
        }
        return new("cinema4d_base_human",new[]{"^Base[-_]Human"},aliases.ToArray());
    }
    private static bool IsArm(string role)=>role.StartsWith("Clavicle")||role.StartsWith("UpperArm")||role.StartsWith("LowerArm");
}
notpointless.chomnr_humanoid_hand_retargeter / Core/Maths/MathQ.cs
Game library
// Adapted from humanoid-retargeter 26084c96c3fc870aaf9a5bd798de063ce2fd62df; namespace only.
#nullable enable annotations

using System;
using System.Numerics;

namespace HumanoidHandRetargeter.Maths;

using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidHandRetargeter/Assembly.cs)

/// <summary>
/// Quaternion algebra helpers used throughout the retargeting pipeline.
/// All angles are radians; quaternions are XYZW (<see cref="System.Numerics.Quaternion"/> native);
/// <c>a * b</c> applies <c>b</c> first (column-vector convention).
/// </summary>
public static class MathQ
{
    private const float NearZeroLengthSq = 1e-12f;

    /// <summary>
    /// Defensively normalizes a quaternion. Returns <see cref="Quaternion.Identity"/> when the
    /// input has near-zero length or non-finite components instead of producing NaNs.
    /// </summary>
    public static Quaternion Normalize(Quaternion q)
    {
        var lengthSq = q.LengthSquared();
        if (!float.IsFinite(lengthSq) || lengthSq < NearZeroLengthSq)
            return Quaternion.Identity;
        return Quaternion.Multiply(q, 1f / MathF.Sqrt(lengthSq));
    }

    /// <summary>
    /// Shortest-arc rotation taking direction <paramref name="a"/> onto direction <paramref name="b"/>
    /// (magnitudes are ignored). For antiparallel inputs a 180° rotation about a stable axis
    /// perpendicular to <paramref name="a"/> is returned. Near-zero inputs yield identity.
    /// </summary>
    public static Quaternion FromTo(Vector3 a, Vector3 b)
    {
        if (a.LengthSquared() < NearZeroLengthSq || b.LengthSquared() < NearZeroLengthSq)
            return Quaternion.Identity;

        var an = Vector3.Normalize(a);
        var bn = Vector3.Normalize(b);
        var dot = Vector3.Dot(an, bn);

        if (dot >= 1f - 1e-8f)
            return Quaternion.Identity;

        if (dot <= -1f + 1e-7f)
        {
            // Antiparallel: any axis perpendicular to a works; pick a stable one.
            return Quaternion.CreateFromAxisAngle(Perpendicular(an), MathF.PI);
        }

        // q = (a x b, 1 + a·b), normalized — the half-angle construction of the shortest arc.
        var cross = Vector3.Cross(an, bn);
        return Normalize(new Quaternion(cross.X, cross.Y, cross.Z, 1f + dot));
    }

    /// <summary>
    /// Decomposes a rotation into <paramref name="swing"/> and <paramref name="twist"/> so that
    /// <c>q == swing * twist</c> (twist applied first). The twist is the projection of the
    /// rotation onto <paramref name="twistAxis"/>: its rotation axis is parallel to
    /// <paramref name="twistAxis"/>, while the swing's rotation axis is perpendicular to it.
    /// When the rotation is a pure 180° swing (no component about the axis), twist is identity.
    /// </summary>
    /// <exception cref="ArgumentException">Thrown when <paramref name="twistAxis"/> has near-zero length.</exception>
    public static void SwingTwist(Quaternion q, Vector3 twistAxis, out Quaternion swing, out Quaternion twist)
    {
        if (twistAxis.LengthSquared() < NearZeroLengthSq)
            throw new ArgumentException("Twist axis must be non-zero.", nameof(twistAxis));

        var axis = Vector3.Normalize(twistAxis);
        q = Normalize(q);

        // Project the rotation's vector part onto the twist axis.
        var projection = q.X * axis.X + q.Y * axis.Y + q.Z * axis.Z;
        var raw = new Quaternion(axis.X * projection, axis.Y * projection, axis.Z * projection, q.W);

        // Degenerate case (q.W ~ 0 and projection ~ 0): pure 180° swing, no twist.
        twist = Normalize(raw);
        swing = Normalize(q * Quaternion.Conjugate(twist));
    }

    /// <summary>
    /// Geodesic angle between two rotations in radians, in [0, π], invariant under the
    /// q / -q double cover (so <c>AngleBetween(q, -q) == 0</c>).
    /// </summary>
    public static float AngleBetween(Quaternion a, Quaternion b)
    {
        // Use atan2 on the delta rotation rather than acos on the dot product: acos is
        // ill-conditioned near 0°, atan2 is accurate over the whole range.
        var delta = Quaternion.Conjugate(Normalize(a)) * Normalize(b);
        var sinHalf = MathF.Sqrt(delta.X * delta.X + delta.Y * delta.Y + delta.Z * delta.Z);
        var cosHalf = MathF.Abs(delta.W); // |W| folds the q / -q double cover.
        return 2f * MathF.Atan2(sinHalf, cosHalf);
    }

    /// <summary>
    /// Angle between two directions in radians, in [0, π] (magnitudes are ignored).
    /// Near-zero inputs yield 0. Uses atan2, which stays accurate near 0 and π where
    /// acos of the dot product loses precision.
    /// </summary>
    public static float AngleBetween(Vector3 a, Vector3 b)
    {
        if (a.LengthSquared() < NearZeroLengthSq || b.LengthSquared() < NearZeroLengthSq)
            return 0f;
        return MathF.Atan2(Vector3.Cross(a, b).Length(), Vector3.Dot(a, b));
    }

    /// <summary>
    /// Builds an orthonormal right-handed basis rotation from a forward and an up hint.
    /// </summary>
    /// <remarks>
    /// Convention: returns the rotation <c>R</c> such that
    /// <list type="bullet">
    /// <item><c>rotate(R, +Y) == normalize(forward)</c> (primary axis),</item>
    /// <item><c>rotate(R, +Z) == </c> the Gram-Schmidt orthonormalization of <paramref name="up"/>
    /// against forward (secondary axis),</item>
    /// <item><c>rotate(R, +X) == forwardN x upOrtho</c> (right-handed completion).</item>
    /// </list>
    /// Degenerate inputs are handled defensively: a near-zero forward yields identity; an up hint
    /// (near-)parallel to forward falls back to world +Z, or world +X when forward itself is
    /// (near-)parallel to +Z.
    /// </remarks>
    public static Quaternion BasisFromForwardUp(Vector3 forward, Vector3 up)
    {
        if (forward.LengthSquared() < NearZeroLengthSq)
            return Quaternion.Identity;

        var y = Vector3.Normalize(forward);

        var z = up - y * Vector3.Dot(up, y);
        if (z.LengthSquared() < 1e-8f)
        {
            // Up hint unusable: pick a stable fallback and orthonormalize it.
            var fallback = MathF.Abs(Vector3.Dot(y, Vector3.UnitZ)) > 0.99f ? Vector3.UnitX : Vector3.UnitZ;
            z = fallback - y * Vector3.Dot(fallback, y);
        }
        z = Vector3.Normalize(z);

        var x = Vector3.Cross(y, z);

        // System.Numerics matrices act on row vectors, so the rows are the images of the
        // unit axes under the rotation: row1 = R*X, row2 = R*Y, row3 = R*Z.
        var m = new Matrix4x4(
            x.X, x.Y, x.Z, 0f,
            y.X, y.Y, y.Z, 0f,
            z.X, z.Y, z.Z, 0f,
            0f, 0f, 0f, 1f);

        return Normalize(Quaternion.CreateFromRotationMatrix(m));
    }

    /// <summary>Stable unit vector perpendicular to <paramref name="unit"/> (assumed normalized).
    /// Internal so geometry passes (e.g. <c>TwoBoneIk</c>) reuse one implementation.</summary>
    internal static Vector3 Perpendicular(Vector3 unit)
    {
        var p = Vector3.Cross(unit, Vector3.UnitX);
        if (p.LengthSquared() < 1e-6f)
            p = Vector3.Cross(unit, Vector3.UnitY);
        return Vector3.Normalize(p);
    }
}
notpointless.chomnr_humanoid_hand_retargeter / Code/Target/AnimationDriverMesh.cs
Game library
namespace HumanoidHandRetargeter.Target;

/// <summary>Small hidden mesh required by SceneModel's animation owner.
/// Cube FBX layout from ufbx's public-domain maya_cube_7400_ascii fixture:
/// https://github.com/ufbx/ufbx/blob/master/data/maya_cube_7400_ascii.fbx
/// Used under ufbx LICENSE alternative B (public domain).</summary>
public static class AnimationDriverMesh
{
    public static readonly string Fbx = Text.Replace("\r\n", "\n").Replace("\n", "\r\n");
    private const string Text = """
; FBX 7.4.0 project file
; ----------------------------------------------------

FBXHeaderExtension:  {
	FBXHeaderVersion: 1003
	FBXVersion: 7400
	CreationTimeStamp:  {
		Version: 1000
		Year: 2020
		Month: 3
		Day: 31
		Hour: 21
		Minute: 36
		Second: 10
		Millisecond: 814
	}
	Creator: "FBX SDK/FBX Plugins version 2019.2"
	SceneInfo: "SceneInfo::GlobalInfo", "UserData" {
		Type: "UserData"
		Version: 100
		MetaData:  {
			Version: 100
			Title: ""
			Subject: ""
			Author: ""
			Keywords: ""
			Revision: ""
			Comment: ""
		}
		Properties70:  {
			P: "DocumentUrl", "KString", "Url", "", "animation_driver.fbx"
			P: "SrcDocumentUrl", "KString", "Url", "", "animation_driver.fbx"
			P: "Original", "Compound", "", ""
			P: "Original|ApplicationVendor", "KString", "", "", "Autodesk"
			P: "Original|ApplicationName", "KString", "", "", "Maya"
			P: "Original|ApplicationVersion", "KString", "", "", "201900"
			P: "Original|DateTime_GMT", "DateTime", "", "", "31/03/2020 18:36:10.811"
			P: "Original|FileName", "KString", "", "", "animation_driver.fbx"
			P: "LastSaved", "Compound", "", ""
			P: "LastSaved|ApplicationVendor", "KString", "", "", "Autodesk"
			P: "LastSaved|ApplicationName", "KString", "", "", "Maya"
			P: "LastSaved|ApplicationVersion", "KString", "", "", "201900"
			P: "LastSaved|DateTime_GMT", "DateTime", "", "", "31/03/2020 18:36:10.811"
			P: "Original|ApplicationActiveProject", "KString", "", "", "animation_driver.fbx"
		}
	}
}
GlobalSettings:  {
	Version: 1000
	Properties70:  {
		P: "UpAxis", "int", "Integer", "",1
		P: "UpAxisSign", "int", "Integer", "",1
		P: "FrontAxis", "int", "Integer", "",2
		P: "FrontAxisSign", "int", "Integer", "",1
		P: "CoordAxis", "int", "Integer", "",0
		P: "CoordAxisSign", "int", "Integer", "",1
		P: "OriginalUpAxis", "int", "Integer", "",1
		P: "OriginalUpAxisSign", "int", "Integer", "",1
		P: "UnitScaleFactor", "double", "Number", "",1
		P: "OriginalUnitScaleFactor", "double", "Number", "",1
		P: "AmbientColor", "ColorRGB", "Color", "",0,0,0
		P: "DefaultCamera", "KString", "", "", "Producer Perspective"
		P: "TimeMode", "enum", "", "",11
		P: "TimeProtocol", "enum", "", "",2
		P: "SnapOnFrameMode", "enum", "", "",0
		P: "TimeSpanStart", "KTime", "Time", "",1924423250
		P: "TimeSpanStop", "KTime", "Time", "",384884650000
		P: "CustomFrameRate", "double", "Number", "",-1
		P: "TimeMarker", "Compound", "", ""
		P: "CurrentTimeMarker", "int", "Integer", "",-1
	}
}

; Documents Description
;------------------------------------------------------------------

Documents:  {
	Count: 1
	Document: 1907658701024, "", "Scene" {
		Properties70:  {
			P: "SourceObject", "object", "", ""
			P: "ActiveAnimStackName", "KString", "", "", "Take 001"
		}
		RootNode: 0
	}
}

; Document References
;------------------------------------------------------------------

References:  {
}

; Object definitions
;------------------------------------------------------------------

Definitions:  {
	Version: 100
	Count: 6
	ObjectType: "GlobalSettings" {
		Count: 1
	}
	ObjectType: "AnimationStack" {
		Count: 1
		PropertyTemplate: "FbxAnimStack" {
			Properties70:  {
				P: "Description", "KString", "", "", ""
				P: "LocalStart", "KTime", "Time", "",0
				P: "LocalStop", "KTime", "Time", "",0
				P: "ReferenceStart", "KTime", "Time", "",0
				P: "ReferenceStop", "KTime", "Time", "",0
			}
		}
	}
	ObjectType: "AnimationLayer" {
		Count: 1
		PropertyTemplate: "FbxAnimLayer" {
			Properties70:  {
				P: "Weight", "Number", "", "A",100
				P: "Mute", "bool", "", "",0
				P: "Solo", "bool", "", "",0
				P: "Lock", "bool", "", "",0
				P: "Color", "ColorRGB", "Color", "",0.8,0.8,0.8
				P: "BlendMode", "enum", "", "",0
				P: "RotationAccumulationMode", "enum", "", "",0
				P: "ScaleAccumulationMode", "enum", "", "",0
				P: "BlendModeBypass", "ULongLong", "", "",0
			}
		}
	}
	ObjectType: "Geometry" {
		Count: 1
		PropertyTemplate: "FbxMesh" {
			Properties70:  {
				P: "Color", "ColorRGB", "Color", "",0.8,0.8,0.8
				P: "BBoxMin", "Vector3D", "Vector", "",0,0,0
				P: "BBoxMax", "Vector3D", "Vector", "",0,0,0
				P: "Primary Visibility", "bool", "", "",1
				P: "Casts Shadows", "bool", "", "",1
				P: "Receive Shadows", "bool", "", "",1
			}
		}
	}
	ObjectType: "Material" {
		Count: 1
		PropertyTemplate: "FbxSurfaceLambert" {
			Properties70:  {
				P: "ShadingModel", "KString", "", "", "Lambert"
				P: "MultiLayer", "bool", "", "",0
				P: "EmissiveColor", "Color", "", "A",0,0,0
				P: "EmissiveFactor", "Number", "", "A",1
				P: "AmbientColor", "Color", "", "A",0.2,0.2,0.2
				P: "AmbientFactor", "Number", "", "A",1
				P: "DiffuseColor", "Color", "", "A",0.8,0.8,0.8
				P: "DiffuseFactor", "Number", "", "A",1
				P: "Bump", "Vector3D", "Vector", "",0,0,0
				P: "NormalMap", "Vector3D", "Vector", "",0,0,0
				P: "BumpFactor", "double", "Number", "",1
				P: "TransparentColor", "Color", "", "A",0,0,0
				P: "TransparencyFactor", "Number", "", "A",0
				P: "DisplacementColor", "ColorRGB", "Color", "",0,0,0
				P: "DisplacementFactor", "double", "Number", "",1
				P: "VectorDisplacementColor", "ColorRGB", "Color", "",0,0,0
				P: "VectorDisplacementFactor", "double", "Number", "",1
			}
		}
	}
	ObjectType: "Model" {
		Count: 1
		PropertyTemplate: "FbxNode" {
			Properties70:  {
				P: "QuaternionInterpolate", "enum", "", "",0
				P: "RotationOffset", "Vector3D", "Vector", "",0,0,0
				P: "RotationPivot", "Vector3D", "Vector", "",0,0,0
				P: "ScalingOffset", "Vector3D", "Vector", "",0,0,0
				P: "ScalingPivot", "Vector3D", "Vector", "",0,0,0
				P: "TranslationActive", "bool", "", "",0
				P: "TranslationMin", "Vector3D", "Vector", "",0,0,0
				P: "TranslationMax", "Vector3D", "Vector", "",0,0,0
				P: "TranslationMinX", "bool", "", "",0
				P: "TranslationMinY", "bool", "", "",0
				P: "TranslationMinZ", "bool", "", "",0
				P: "TranslationMaxX", "bool", "", "",0
				P: "TranslationMaxY", "bool", "", "",0
				P: "TranslationMaxZ", "bool", "", "",0
				P: "RotationOrder", "enum", "", "",0
				P: "RotationSpaceForLimitOnly", "bool", "", "",0
				P: "RotationStiffnessX", "double", "Number", "",0
				P: "RotationStiffnessY", "double", "Number", "",0
				P: "RotationStiffnessZ", "double", "Number", "",0
				P: "AxisLen", "double", "Number", "",10
				P: "PreRotation", "Vector3D", "Vector", "",0,0,0
				P: "PostRotation", "Vector3D", "Vector", "",0,0,0
				P: "RotationActive", "bool", "", "",0
				P: "RotationMin", "Vector3D", "Vector", "",0,0,0
				P: "RotationMax", "Vector3D", "Vector", "",0,0,0
				P: "RotationMinX", "bool", "", "",0
				P: "RotationMinY", "bool", "", "",0
				P: "RotationMinZ", "bool", "", "",0
				P: "RotationMaxX", "bool", "", "",0
				P: "RotationMaxY", "bool", "", "",0
				P: "RotationMaxZ", "bool", "", "",0
				P: "InheritType", "enum", "", "",0
				P: "ScalingActive", "bool", "", "",0
				P: "ScalingMin", "Vector3D", "Vector", "",0,0,0
				P: "ScalingMax", "Vector3D", "Vector", "",1,1,1
				P: "ScalingMinX", "bool", "", "",0
				P: "ScalingMinY", "bool", "", "",0
				P: "ScalingMinZ", "bool", "", "",0
				P: "ScalingMaxX", "bool", "", "",0
				P: "ScalingMaxY", "bool", "", "",0
				P: "ScalingMaxZ", "bool", "", "",0
				P: "GeometricTranslation", "Vector3D", "Vector", "",0,0,0
				P: "GeometricRotation", "Vector3D", "Vector", "",0,0,0
				P: "GeometricScaling", "Vector3D", "Vector", "",1,1,1
				P: "MinDampRangeX", "double", "Number", "",0
				P: "MinDampRangeY", "double", "Number", "",0
				P: "MinDampRangeZ", "double", "Number", "",0
				P: "MaxDampRangeX", "double", "Number", "",0
				P: "MaxDampRangeY", "double", "Number", "",0
				P: "MaxDampRangeZ", "double", "Number", "",0
				P: "MinDampStrengthX", "double", "Number", "",0
				P: "MinDampStrengthY", "double", "Number", "",0
				P: "MinDampStrengthZ", "double", "Number", "",0
				P: "MaxDampStrengthX", "double", "Number", "",0
				P: "MaxDampStrengthY", "double", "Number", "",0
				P: "MaxDampStrengthZ", "double", "Number", "",0
				P: "PreferedAngleX", "double", "Number", "",0
				P: "PreferedAngleY", "double", "Number", "",0
				P: "PreferedAngleZ", "double", "Number", "",0
				P: "LookAtProperty", "object", "", ""
				P: "UpVectorProperty", "object", "", ""
				P: "Show", "bool", "", "",1
				P: "NegativePercentShapeSupport", "bool", "", "",1
				P: "DefaultAttributeIndex", "int", "Integer", "",-1
				P: "Freeze", "bool", "", "",0
				P: "LODBox", "bool", "", "",0
				P: "Lcl Translation", "Lcl Translation", "", "A",0,0,0
				P: "Lcl Rotation", "Lcl Rotation", "", "A",0,0,0
				P: "Lcl Scaling", "Lcl Scaling", "", "A",1,1,1
				P: "Visibility", "Visibility", "", "A",1
				P: "Visibility Inheritance", "Visibility Inheritance", "", "",1
			}
		}
	}
}

; Object properties
;------------------------------------------------------------------

Objects:  {
	Geometry: 1907663073408, "Geometry::", "Mesh" {
		Vertices: *24 {
			a: -0.5,-0.5,0.5,0.5,-0.5,0.5,-0.5,0.5,0.5,0.5,0.5,0.5,-0.5,0.5,-0.5,0.5,0.5,-0.5,-0.5,-0.5,-0.5,0.5,-0.5,-0.5
		}
		PolygonVertexIndex: *24 {
			a: 0,1,3,-3,2,3,5,-5,4,5,7,-7,6,7,1,-1,1,7,5,-4,6,0,2,-5
		}
		Edges: *12 {
			a: 0,2,6,10,3,1,7,5,11,9,15,13
		}
		GeometryVersion: 124
		LayerElementNormal: 0 {
			Version: 102
			Name: ""
			MappingInformationType: "ByPolygonVertex"
			ReferenceInformationType: "Direct"
			Normals: *72 {
				a: 0,0,1,0,0,1,0,0,1,0,0,1,0,1,0,0,1,0,0,1,0,0,1,0,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,1,0,0,1,0,0,1,0,0,1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0
			}
			NormalsW: *24 {
				a: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
			}
		}
		LayerElementBinormal: 0 {
			Version: 102
			Name: "map1"
			MappingInformationType: "ByPolygonVertex"
			ReferenceInformationType: "Direct"
			Binormals: *72 {
				a: 0,1,-0,0,1,-0,0,1,-0,0,1,-0,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,0,1,0,0,1,0,0,1,0,0,1,-0,1,0,-0,1,0,0,1,-0,-0,1,0,0,1,0,0,1,0,0,1,0,0,1,0
			}
			BinormalsW: *24 {
				a: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
			}

		}
		LayerElementTangent: 0 {
			Version: 102
			Name: "map1"
			MappingInformationType: "ByPolygonVertex"
			ReferenceInformationType: "Direct"
			Tangents: *72 {
				a: 1,-0,-0,1,-0,0,1,-0,0,1,-0,0,1,-0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,-0,1,0,-0,1,0,-0,1,0,-0,0,0,-1,0,0,-1,0,-0,-1,0,0,-1,0,-0,1,0,-0,1,0,-0,1,0,-0,1
			}
			TangentsW: *24 {
				a: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
			}
		}
		LayerElementUV: 0 {
			Version: 101
			Name: "map1"
			MappingInformationType: "ByPolygonVertex"
			ReferenceInformationType: "IndexToDirect"
			UV: *28 {
				a: 0.375,0,0.625,0,0.375,0.25,0.625,0.25,0.375,0.5,0.625,0.5,0.375,0.75,0.625,0.75,0.375,1,0.625,1,0.875,0,0.875,0.25,0.125,0,0.125,0.25
			}
			UVIndex: *24 {
				a: 0,1,3,2,2,3,5,4,4,5,7,6,6,7,9,8,1,10,11,3,12,0,2,13
			}
		}
		LayerElementSmoothing: 0 {
			Version: 102
			Name: ""
			MappingInformationType: "ByEdge"
			ReferenceInformationType: "Direct"
			Smoothing: *12 {
				a: 0,0,0,0,0,0,0,0,0,0,0,0
			}
		}
		LayerElementMaterial: 0 {
			Version: 101
			Name: ""
			MappingInformationType: "AllSame"
			ReferenceInformationType: "IndexToDirect"
			Materials: *1 {
				a: 0
			}
		}
		Layer: 0 {
			Version: 100
			LayerElement:  {
				Type: "LayerElementNormal"
				TypedIndex: 0
			}
			LayerElement:  {
				Type: "LayerElementBinormal"
				TypedIndex: 0
			}
			LayerElement:  {
				Type: "LayerElementTangent"
				TypedIndex: 0
			}
			LayerElement:  {
				Type: "LayerElementMaterial"
				TypedIndex: 0
			}
			LayerElement:  {
				Type: "LayerElementSmoothing"
				TypedIndex: 0
			}
			LayerElement:  {
				Type: "LayerElementUV"
				TypedIndex: 0
			}
		}
	}
	Model: 1907292236800, "Model::animation_driver", "Mesh" {
		Version: 232
		Properties70:  {
			P: "RotationActive", "bool", "", "",1
			P: "InheritType", "enum", "", "",1
			P: "ScalingMax", "Vector3D", "Vector", "",0,0,0
			P: "DefaultAttributeIndex", "int", "Integer", "",0
			P: "currentUVSet", "KString", "", "U", "map1"
		}
		Shading: T
		Culling: "CullingOff"
	}
	Material: 1908464858960, "Material::lambert1", "" {
		Version: 102
		ShadingModel: "lambert"
		MultiLayer: 0
		Properties70:  {
			P: "AmbientColor", "Color", "", "A",0,0,0
			P: "DiffuseColor", "Color", "", "A",0.5,0.5,0.5
			P: "DiffuseFactor", "Number", "", "A",0.800000011920929
			P: "TransparencyFactor", "Number", "", "A",1
			P: "Emissive", "Vector3D", "Vector", "",0,0,0
			P: "Ambient", "Vector3D", "Vector", "",0,0,0
			P: "Diffuse", "Vector3D", "Vector", "",0.400000005960464,0.400000005960464,0.400000005960464
			P: "Opacity", "double", "Number", "",1
		}
	}
	AnimationStack: 1907674289488, "AnimStack::Take 001", "" {
		Properties70:  {
			P: "LocalStart", "KTime", "Time", "",1924423250
			P: "LocalStop", "KTime", "Time", "",230930790000
			P: "ReferenceStart", "KTime", "Time", "",1924423250
			P: "ReferenceStop", "KTime", "Time", "",230930790000
		}
	}
	AnimationLayer: 1907631731008, "AnimLayer::BaseLayer", "" {
	}
}

; Object connections
;------------------------------------------------------------------

Connections:  {

	;Model::animation_driver, Model::RootNode
	C: "OO",1907292236800,0

	;AnimLayer::BaseLayer, AnimStack::Take 001
	C: "OO",1907631731008,1907674289488

	;Geometry::, Model::animation_driver
	C: "OO",1907663073408,1907292236800

	;Material::lambert1, Model::animation_driver
	C: "OO",1908464858960,1907292236800
}
;Takes section
;----------------------------------------------------

Takes:  {
	Current: "Take 001"
	Take: "Take 001" {
		FileName: "Take_001.tak"
		LocalTime: 1924423250,230930790000
		ReferenceTime: 1924423250,230930790000
	}
}

""";
}
Debug: View Raw JSON Response
{
    "TotalCount": 108,
    "Files": [
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Core/Validation/HandRigValidator.cs",
            "FileName": "HandRigValidator.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "#nullable enable\r\nusing HumanoidHandRetargeter.Mapping;\r\nusing SkeletonModel = HumanoidHandRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidHandRetargeter.Validation;\r\n\r\npublic sealed record RigIssue(string Code, string Message, HandSide? Side = null, int? Bone = null);\r\n\r\n/// <summary>Shared validation for automatic/manual mapping, batch processing and the editor.</summary>\r\npublic static class HandRigValidator\r\n{\r\n    public static IReadOnlyList<RigIssue> Validate(SkeletonModel skeleton, IEnumerable<HandRigDefinition> hands)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(skeleton);\r\n        ArgumentNullException.ThrowIfNull(hands);\r\n        var issues = new List<RigIssue>();\r\n        var claimed = new HashSet<int>();\r\n        var sides = new HashSet<HandSide>();\r\n        var count = 0;\r\n\r\n        foreach (var bone in skeleton.Bones)\r\n        {\r\n            if (!PoseValidator.ValidTransform(bone.RestLocal))\r\n                issues.Add(new(\"invalid-rest\", $\"Bone '{bone.Name}' has an invalid rest transform.\", Bone: bone.Index));\r\n            var world = skeleton.RestWorld[bone.Index].Pos;\r\n            if (!float.IsFinite(world.X) || !float.IsFinite(world.Y) || !float.IsFinite(world.Z))\r\n                issues.Add(new(\"invalid-world-rest\", $\"Bone '{bone.Name}' has an invalid accumulated rest position.\", Bone: bone.Index));\r\n        }\r\n\r\n        foreach (var hand in hands)\r\n        {\r\n            count++;\r\n            if (hand is null)\r\n            {\r\n                issues.Add(new(\"missing-hand\", \"A hand mapping is missing.\"));\r\n                continue;\r\n            }\r\n            if (!Enum.IsDefined(typeof(HandSide), hand.Side))\r\n                issues.Add(new(\"invalid-side\", \"Choose left or right for the hand.\"));\r\n            if (!sides.Add(hand.Side))\r\n                issues.Add(new(\"duplicate-side\", $\"More than one {hand.Side} hand is mapped.\", hand.Side));\r\n\r\n            var arm = new[] { hand.Clavicle, hand.UpperArm, hand.Forearm, hand.Wrist }\r\n                .Where(b => b.HasValue).Select(b => b!.Value).ToArray();\r\n            CheckChain(arm, hand.Side, \"arm\", allowZeroLength: true);\r\n            var roles = new HashSet<string>(StringComparer.Ordinal);\r\n            foreach (var digit in hand.Digits)\r\n            {\r\n                if (digit is null)\r\n                {\r\n                    issues.Add(new(\"missing-digit\", \"A digit mapping is missing.\", hand.Side));\r\n                    continue;\r\n                }\r\n                var key = digit.Role == DigitRole.Extra ? \"Extra:\" + digit.ExtraSlot : digit.Role.ToString();\r\n                if (!Enum.IsDefined(typeof(DigitRole), digit.Role)\r\n                    || (digit.Role == DigitRole.Extra && string.IsNullOrWhiteSpace(digit.ExtraSlot)))\r\n                    issues.Add(new(\"invalid-digit-role\", \"Choose a digit role or name its extra slot.\", hand.Side));\r\n                if (!roles.Add(key))\r\n                    issues.Add(new(\"duplicate-digit-role\", $\"{hand.Side} {key} is mapped more than once.\", hand.Side));\r\n                if (digit.Segments.Count == 0)\r\n                    issues.Add(new(\"empty-digit\", $\"{hand.Side} {key} needs at least one segment.\", hand.Side));\r\n                var chain = digit.Bones.ToArray();\r\n                CheckChain(chain, hand.Side, key, allowZeroLength: false, tip: digit.Tip);\r\n                if (chain.Length > 0 && Valid(chain[0]) && Valid(hand.Wrist)\r\n                    && !skeleton.DescendsFrom(chain[0], hand.Wrist))\r\n                    issues.Add(new(\"wrong-wrist\", $\"{hand.Side} {key} must descend from its wrist.\", hand.Side, chain[0]));\r\n            }\r\n            foreach (var helper in hand.TwistOrHelperBones)\r\n            {\r\n                Claim(helper, hand.Side);\r\n                var armRoot = arm[0];\r\n                if (Valid(helper) && Valid(armRoot) && !skeleton.DescendsFrom(helper, armRoot))\r\n                    issues.Add(new(\"wrong-helper-root\", $\"{hand.Side} helper must belong to its mapped arm or hand hierarchy.\", hand.Side, helper));\r\n            }\r\n        }\r\n        if (count == 0)\r\n            issues.Add(new(\"no-hands\", \"Map at least one wrist before retargeting.\"));\r\n        return issues.AsReadOnly();\r\n\r\n        bool Valid(int bone) => bone >= 0 && bone < skeleton.Count;\r\n        void Claim(int bone, HandSide side)\r\n        {\r\n            if (!Valid(bone))\r\n                issues.Add(new(\"invalid-bone\", $\"{side} mapping references a missing bone.\", side, bone));\r\n            else if (!claimed.Add(bone))\r\n                issues.Add(new(\"overlapping-bone\", $\"Bone '{skeleton[bone].Name}' is assigned to multiple roles.\", side, bone));\r\n        }\r\n        void CheckChain(int[] bones, HandSide side, string role, bool allowZeroLength, int? tip = null)\r\n        {\r\n            for (var i = 0; i < bones.Length; i++)\r\n            {\r\n                Claim(bones[i], side);\r\n                if (i == 0 || !Valid(bones[i - 1]) || !Valid(bones[i])) continue;\r\n                if (!skeleton.DescendsFrom(bones[i], bones[i - 1]))\r\n                    issues.Add(new(\"chain-order\", $\"{side} {role} joints must follow their hierarchy.\", side, bones[i]));\r\n                if (!allowZeroLength && bones[i] != tip &&\r\n                    System.Numerics.Vector3.DistanceSquared(skeleton.RestWorld[bones[i - 1]].Pos,\r\n                        skeleton.RestWorld[bones[i]].Pos) < 1e-12f)\r\n                    issues.Add(new(\"zero-length-digit\", $\"{side} {role} has coincident joints; correct the chain or classify a helper.\", side, bones[i]));\r\n            }\r\n        }\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Formats/Fbx/FbxScene.cs",
            "FileName": "FbxScene.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "// Reused from humanoid-retargeter 26084c96c3fc870aaf9a5bd798de063ce2fd62df; specialized where needed for hand assets.\r\n#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\n\r\nnamespace HumanoidHandRetargeter.Formats.Fbx;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidHandRetargeter/Assembly.cs)\r\n\r\n/// <summary>One <c>P</c> entry of a <c>Properties70</c> block: name, FBX type string, values.</summary>\r\npublic sealed class FbxProperty70\r\n{\r\n    /// <summary>Property name (e.g. <c>\"Lcl Translation\"</c>, <c>\"PreRotation\"</c>, <c>\"d|X\"</c>).</summary>\r\n    public string Name { get; }\r\n\r\n    /// <summary>FBX type string (e.g. <c>\"Lcl Translation\"</c>, <c>\"enum\"</c>, <c>\"Number\"</c>).</summary>\r\n    public string Type { get; }\r\n\r\n    /// <summary>Raw values (props 4.. of the P node).</summary>\r\n    public IReadOnlyList<object> Values { get; }\r\n\r\n    internal FbxProperty70(string name, string type, IReadOnlyList<object> values)\r\n    {\r\n        Name = name;\r\n        Type = type;\r\n        Values = values;\r\n    }\r\n\r\n    /// <summary>Value <paramref name=\"i\"/> as a double (tolerant of int/long/float storage).</summary>\r\n    public double GetDouble(int i = 0) => Convert.ToDouble(Values[i], System.Globalization.CultureInfo.InvariantCulture);\r\n\r\n    /// <summary>Value <paramref name=\"i\"/> as an int (tolerant of long/double storage).</summary>\r\n    public int GetInt(int i = 0) => Convert.ToInt32(Values[i], System.Globalization.CultureInfo.InvariantCulture);\r\n\r\n    /// <summary>First three values as a vector.</summary>\r\n    public Vector3 GetVector3()\r\n        => new((float)GetDouble(0), (float)GetDouble(1), (float)GetDouble(2));\r\n}\r\n\r\n/// <summary>\r\n/// One object from the FBX <c>Objects</c> section (Model, NodeAttribute, AnimationStack,\r\n/// AnimationLayer, AnimationCurveNode, AnimationCurve, Pose, ...).\r\n/// </summary>\r\npublic sealed class FbxObject\r\n{\r\n    /// <summary>Unique object id (the first property of the object node).</summary>\r\n    public long Id { get; }\r\n\r\n    /// <summary>Node type \u2014 the FBX node name: \"Model\", \"AnimationCurve\", ...</summary>\r\n    public string NodeType { get; }\r\n\r\n    /// <summary>Object name (namespace prefixes like <c>mixamorig1:</c> preserved).</summary>\r\n    public string Name { get; }\r\n\r\n    /// <summary>Object sub-class (third property): \"LimbNode\", \"Null\", \"Root\", \"Mesh\", ...</summary>\r\n    public string SubClass { get; }\r\n\r\n    /// <summary>The underlying token-tree node.</summary>\r\n    public FbxNode Node { get; }\r\n\r\n    /// <summary>Own Properties70 entries by name (template defaults NOT merged \u2014 see <see cref=\"FbxScene.FindProperty\"/>).</summary>\r\n    public IReadOnlyDictionary<string, FbxProperty70> Properties { get; }\r\n\r\n    /// <summary>For Models: the parent Model via an OO connection, or null at the scene root.</summary>\r\n    public FbxObject? ModelParent { get; internal set; }\r\n\r\n    /// <summary>For Models: child Models via OO connections, in connection order.</summary>\r\n    public List<FbxObject> ModelChildren { get; } = new();\r\n\r\n    internal FbxObject(\r\n        long id, string nodeType, string name, string subClass, FbxNode node,\r\n        IReadOnlyDictionary<string, FbxProperty70> properties)\r\n    {\r\n        Id = id;\r\n        NodeType = nodeType;\r\n        Name = name;\r\n        SubClass = subClass;\r\n        Node = node;\r\n        Properties = properties;\r\n    }\r\n\r\n    /// <inheritdoc />\r\n    public override string ToString() => $\"{NodeType} '{Name}' ({SubClass}) #{Id}\";\r\n}\r\n\r\n/// <summary>A single animation curve: keyframes for one scalar channel.</summary>\r\npublic sealed class FbxAnimCurve\r\n{\r\n    /// <summary>KTIME ticks per second (FBX constant).</summary>\r\n    public const long TicksPerSecond = 46186158000L;\r\n\r\n    /// <summary>Key times in KTIME ticks, ascending.</summary>\r\n    public long[] KeyTimes { get; }\r\n\r\n    /// <summary>Key values, parallel to <see cref=\"KeyTimes\"/>.</summary>\r\n    public float[] KeyValues { get; }\r\n\r\n    internal FbxAnimCurve(long[] keyTimes, float[] keyValues)\r\n    {\r\n        KeyTimes = keyTimes;\r\n        KeyValues = keyValues;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Samples the curve at a KTIME tick: linear interpolation between keys, constant\r\n    /// extrapolation outside the key range.\r\n    /// </summary>\r\n    public float Evaluate(long ticks)\r\n    {\r\n        var times = KeyTimes;\r\n        int n = times.Length;\r\n        if (n == 0)\r\n            return 0f;\r\n        if (ticks <= times[0])\r\n            return KeyValues[0];\r\n        if (ticks >= times[n - 1])\r\n            return KeyValues[n - 1];\r\n\r\n        int hi = Array.BinarySearch(times, ticks);\r\n        if (hi >= 0)\r\n            return KeyValues[hi];\r\n        hi = ~hi; // first index with time > ticks; >=1 and <=n-1 here\r\n        int lo = hi - 1;\r\n        double span = times[hi] - times[lo];\r\n        double t = span <= 0 ? 0.0 : (ticks - times[lo]) / span;\r\n        return (float)(KeyValues[lo] + (KeyValues[hi] - KeyValues[lo]) * t);\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// An AnimationCurveNode: up to three channel curves (X/Y/Z) targeting one transform\r\n/// property (<c>\"Lcl Translation\"</c> / <c>\"Lcl Rotation\"</c> / <c>\"Lcl Scaling\"</c>) of one Model.\r\n/// </summary>\r\npublic sealed class FbxAnimCurveNode\r\n{\r\n    /// <summary>The curve node object.</summary>\r\n    public FbxObject Object { get; }\r\n\r\n    /// <summary>Channel curves by axis ('X'/'Y'/'Z'), from <c>\"d|X\"</c>-style OP connections.</summary>\r\n    public Dictionary<char, FbxAnimCurve> Channels { get; } = new();\r\n\r\n    internal FbxAnimCurveNode(FbxObject obj) => Object = obj;\r\n\r\n    /// <summary>\r\n    /// Samples one component: the channel curve when connected, else the curve node's static\r\n    /// <c>d|X</c> default, else <paramref name=\"fallback\"/> (the model's Lcl value).\r\n    /// </summary>\r\n    public float Component(char axis, long ticks, float fallback)\r\n    {\r\n        if (Channels.TryGetValue(axis, out var curve))\r\n            return curve.Evaluate(ticks);\r\n        if (Object.Properties.TryGetValue(\"d|\" + axis, out var def) && def.Values.Count > 0)\r\n            return (float)def.GetDouble();\r\n        return fallback;\r\n    }\r\n}\r\n\r\n/// <summary>One AnimationStack with its curve bindings, flattened across its layers.</summary>\r\npublic sealed class FbxAnimStack\r\n{\r\n    /// <summary>The stack object (its Name is the clip name, e.g. \"mixamo.com\").</summary>\r\n    public FbxObject Object { get; }\r\n\r\n    /// <summary>\r\n    /// Curve nodes bound to model transform properties:\r\n    /// (model id, property name) \u2192 curve node. When several layers animate the same property\r\n    /// the first connected layer wins (layer blending is not supported).\r\n    /// </summary>\r\n    public Dictionary<(long ModelId, string Property), FbxAnimCurveNode> Bindings { get; } = new();\r\n\r\n    /// <summary>LocalStart from the stack's Properties70, in KTIME ticks (0 when absent).</summary>\r\n    public long LocalStart { get; internal set; }\r\n\r\n    /// <summary>LocalStop from the stack's Properties70, in KTIME ticks (0 when absent).</summary>\r\n    public long LocalStop { get; internal set; }\r\n\r\n    internal FbxAnimStack(FbxObject obj) => Object = obj;\r\n}\r\n\r\n/// <summary>\r\n/// Semantic object graph built from an FBX token tree: typed objects, the Model hierarchy\r\n/// (OO connections), animation bindings (OP connections), Properties70 lookup with\r\n/// Definitions-template defaults, bind poses, and GlobalSettings.\r\n/// </summary>\r\npublic sealed class FbxScene\r\n{\r\n    /// <summary>All parsed objects by id.</summary>\r\n    public IReadOnlyDictionary<long, FbxObject> ObjectsById => _objectsById;\r\n\r\n    /// <summary>All Model objects in document order.</summary>\r\n    public IReadOnlyList<FbxObject> Models => _models;\r\n\r\n    /// <summary>All animation stacks in document order.</summary>\r\n    public IReadOnlyList<FbxAnimStack> Stacks => _stacks;\r\n\r\n    /// <summary>World-space bind matrices from Pose/BindPose nodes, by model id (row-vector layout).</summary>\r\n    public IReadOnlyDictionary<long, Matrix4x4> BindPose => _bindPose;\r\n\r\n    /// <summary>GlobalSettings UnitScaleFactor: source-unit \u2192 centimeter factor (FBX default 1 = cm).</summary>\r\n    public double UnitScaleFactor { get; private set; } = 1.0;\r\n\r\n    /// <summary>GlobalSettings UpAxis (default 1 = Y).</summary>\r\n    public int UpAxis { get; private set; } = 1;\r\n\r\n    /// <summary>GlobalSettings UpAxisSign (default +1).</summary>\r\n    public int UpAxisSign { get; private set; } = 1;\r\n\r\n    /// <summary>GlobalSettings FrontAxis (default 2 = Z).</summary>\r\n    public int FrontAxis { get; private set; } = 2;\r\n\r\n    /// <summary>GlobalSettings FrontAxisSign (default +1).</summary>\r\n    public int FrontAxisSign { get; private set; } = 1;\r\n\r\n    /// <summary>GlobalSettings CoordAxis (default 0 = X).</summary>\r\n    public int CoordAxis { get; private set; } = 0;\r\n\r\n    /// <summary>GlobalSettings CoordAxisSign (default +1).</summary>\r\n    public int CoordAxisSign { get; private set; } = 1;\r\n\r\n    /// <summary>GlobalSettings OriginalUpAxis (-1 when not recorded).</summary>\r\n    public int OriginalUpAxis { get; private set; } = -1;\r\n\r\n    /// <summary>GlobalSettings TimeMode (FbxTime::EMode enum value; 0 = default mode).</summary>\r\n    public int TimeMode { get; private set; }\r\n\r\n    /// <summary>GlobalSettings CustomFrameRate (-1 when not recorded).</summary>\r\n    public double CustomFrameRate { get; private set; } = -1.0;\r\n\r\n    /// <summary>\r\n    /// Native frame rate (fps) implied by <see cref=\"TimeMode\"/> /\r\n    /// <see cref=\"CustomFrameRate\"/>. This is the rate the source timeline was authored at \u2014\r\n    /// external frame ranges (e.g. Unity <c>.meta</c> clipAnimations) are expressed in it.\r\n    /// The default mode (0) and unknown values map to 30 fps (this importer's resample default).\r\n    /// </summary>\r\n    public double FrameRate => TimeModeFrameRate(TimeMode, CustomFrameRate);\r\n\r\n    /// <summary>FbxTime::EMode \u2192 frames per second (eCustom uses the custom rate).</summary>\r\n    internal static double TimeModeFrameRate(int timeMode, double customFrameRate) => timeMode switch\r\n    {\r\n        1 => 120.0,            // eFrames120\r\n        2 => 100.0,            // eFrames100\r\n        3 => 60.0,             // eFrames60\r\n        4 => 50.0,             // eFrames50\r\n        5 => 48.0,             // eFrames48\r\n        6 => 30.0,             // eFrames30\r\n        7 => 30.0,             // eFrames30Drop\r\n        8 => 30.0 / 1.001,     // eNTSCDropFrame (29.97)\r\n        9 => 30.0 / 1.001,     // eNTSCFullFrame (29.97)\r\n        10 => 25.0,            // ePAL\r\n        11 => 24.0,            // eFrames24\r\n        12 => 1000.0,          // eFrames1000\r\n        13 => 24.0 / 1.001,    // eFilmFullFrame (23.976)\r\n        14 => customFrameRate > 0 ? customFrameRate : 30.0, // eCustom\r\n        15 => 96.0,            // eFrames96\r\n        16 => 72.0,            // eFrames72\r\n        17 => 60.0 / 1.001,    // eFrames59dot94\r\n        18 => 120.0 / 1.001,   // eFrames119dot88\r\n        _ => 30.0,             // eDefaultMode / unknown\r\n    };\r\n\r\n    private readonly Dictionary<long, FbxObject> _objectsById = new();\r\n    private readonly List<FbxObject> _models = new();\r\n    private readonly List<FbxAnimStack> _stacks = new();\r\n    private readonly Dictionary<long, Matrix4x4> _bindPose = new();\r\n    private readonly Dictionary<string, Dictionary<string, FbxProperty70>> _templates = new();\r\n\r\n    private FbxScene()\r\n    {\r\n    }\r\n\r\n    /// <summary>\r\n    /// Looks up a property on an object: its own Properties70 first, then the Definitions\r\n    /// property template for its node type. Returns null when neither defines it.\r\n    /// </summary>\r\n    public FbxProperty70? FindProperty(FbxObject obj, string name)\r\n    {\r\n        if (obj.Properties.TryGetValue(name, out var own))\r\n            return own;\r\n        if (_templates.TryGetValue(obj.NodeType, out var template) &&\r\n            template.TryGetValue(name, out var def))\r\n            return def;\r\n        return null;\r\n    }\r\n\r\n    /// <summary>Vector property with template fallback and a hard default.</summary>\r\n    public Vector3 GetVector3(FbxObject obj, string name, Vector3 fallback)\r\n    {\r\n        var p = FindProperty(obj, name);\r\n        return p is { Values.Count: >= 3 } ? p.GetVector3() : fallback;\r\n    }\r\n\r\n    /// <summary>Double property with template fallback and a hard default.</summary>\r\n    public double GetDouble(FbxObject obj, string name, double fallback)\r\n    {\r\n        var p = FindProperty(obj, name);\r\n        return p is { Values.Count: >= 1 } ? p.GetDouble() : fallback;\r\n    }\r\n\r\n    /// <summary>Int property with template fallback and a hard default.</summary>\r\n    public int GetInt(FbxObject obj, string name, int fallback)\r\n    {\r\n        var p = FindProperty(obj, name);\r\n        return p is { Values.Count: >= 1 } ? p.GetInt() : fallback;\r\n    }\r\n\r\n    /// <summary>Builds the semantic graph from a tokenized FBX document.</summary>\r\n    public static FbxScene Build(FbxNode root)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(root);\r\n        var scene = new FbxScene();\r\n        scene.ReadGlobalSettings(root.Child(\"GlobalSettings\"));\r\n        scene.ReadTemplates(root.Child(\"Definitions\"));\r\n        scene.ReadObjects(root.Child(\"Objects\"));\r\n        scene.ReadConnections(root.Child(\"Connections\"));\r\n        return scene;\r\n    }\r\n\r\n    // ------------------------------------------------------------------ objects\r\n\r\n    private void ReadObjects(FbxNode? objects)\r\n    {\r\n        if (objects is null)\r\n            return;\r\n\r\n        long syntheticId = -1000; // for exotic files whose objects lack numeric ids\r\n\r\n        foreach (var node in objects.Children)\r\n        {\r\n            // Standard 7x object layout: (id:int64, \"Name\\0\\x01Class\":string, \"SubClass\":string).\r\n            long id;\r\n            int nameProp;\r\n            if (node.Properties.Count >= 1 && node.Properties[0] is long or int)\r\n            {\r\n                id = node.Prop<long>(0);\r\n                nameProp = 1;\r\n            }\r\n            else\r\n            {\r\n                id = syntheticId--;\r\n                nameProp = 0;\r\n            }\r\n\r\n            string name = \"\", subClass = \"\";\r\n            if (node.Properties.Count > nameProp && node.Properties[nameProp] is string rawName)\r\n                (name, _) = FbxNode.SplitName(rawName);\r\n            if (node.Properties.Count > nameProp + 1 && node.Properties[nameProp + 1] is string sub)\r\n                subClass = sub;\r\n\r\n            var obj = new FbxObject(id, node.Name, name, subClass, node, ReadProperties70(node));\r\n            _objectsById.TryAdd(obj.Id, obj);\r\n\r\n            switch (node.Name)\r\n            {\r\n                case \"Model\":\r\n                    _models.Add(obj);\r\n                    break;\r\n                case \"Pose\":\r\n                    if (subClass == \"BindPose\")\r\n                        ReadBindPose(node);\r\n                    break;\r\n            }\r\n        }\r\n    }\r\n\r\n    private void ReadBindPose(FbxNode pose)\r\n    {\r\n        foreach (var poseNode in pose.ChildrenNamed(\"PoseNode\"))\r\n        {\r\n            var nodeId = poseNode.Child(\"Node\");\r\n            var matrix = poseNode.Child(\"Matrix\");\r\n            if (nodeId is null || matrix is null)\r\n                continue;\r\n\r\n            double[] m = matrix.AsDoubleArray(0);\r\n            if (m.Length < 16)\r\n                continue;\r\n\r\n            // FBX matrices are stored as 16 doubles with translation in elements 12..14 \u2014\r\n            // the same memory layout as System.Numerics row-vector matrices.\r\n            _bindPose[nodeId.Prop<long>(0)] = new Matrix4x4(\r\n                (float)m[0], (float)m[1], (float)m[2], (float)m[3],\r\n                (float)m[4], (float)m[5], (float)m[6], (float)m[7],\r\n                (float)m[8], (float)m[9], (float)m[10], (float)m[11],\r\n                (float)m[12], (float)m[13], (float)m[14], (float)m[15]);\r\n        }\r\n    }\r\n\r\n    private static IReadOnlyDictionary<string, FbxProperty70> ReadProperties70(FbxNode node)\r\n    {\r\n        var result = new Dictionary<string, FbxProperty70>(StringComparer.Ordinal);\r\n        var block = node.Child(\"Properties70\") ?? node.Child(\"Properties60\");\r\n        if (block is null)\r\n            return result;\r\n\r\n        foreach (var p in block.ChildrenNamed(\"P\"))\r\n        {\r\n            if (p.Properties.Count < 2 || p.Properties[0] is not string name)\r\n                continue;\r\n            string type = p.Properties[1] as string ?? \"\";\r\n            int valueStart = Math.Min(4, p.Properties.Count);\r\n            var values = p.Properties.GetRange(valueStart, p.Properties.Count - valueStart);\r\n            result[name] = new FbxProperty70(name, type, values);\r\n        }\r\n        return result;\r\n    }\r\n\r\n    // ------------------------------------------------------------------ templates\r\n\r\n    private void ReadTemplates(FbxNode? definitions)\r\n    {\r\n        if (definitions is null)\r\n            return;\r\n\r\n        foreach (var objectType in definitions.ChildrenNamed(\"ObjectType\"))\r\n        {\r\n            if (objectType.Properties.Count < 1 || objectType.Properties[0] is not string typeName)\r\n                continue;\r\n            var template = objectType.Child(\"PropertyTemplate\");\r\n            if (template is null)\r\n                continue;\r\n            _templates[typeName] = new Dictionary<string, FbxProperty70>(\r\n                (Dictionary<string, FbxProperty70>)ReadProperties70(template), StringComparer.Ordinal);\r\n        }\r\n    }\r\n\r\n    // ------------------------------------------------------------------ global settings\r\n\r\n    private void ReadGlobalSettings(FbxNode? globalSettings)\r\n    {\r\n        if (globalSettings is null)\r\n            return;\r\n\r\n        var props = ReadProperties70(globalSettings);\r\n\r\n        double D(string name, double fallback)\r\n            => props.TryGetValue(name, out var p) && p.Values.Count > 0 ? p.GetDouble() : fallback;\r\n        int I(string name, int fallback)\r\n            => props.TryGetValue(name, out var p) && p.Values.Count > 0 ? p.GetInt() : fallback;\r\n\r\n        UnitScaleFactor = D(\"UnitScaleFactor\", 1.0);\r\n        UpAxis = I(\"UpAxis\", 1);\r\n        UpAxisSign = I(\"UpAxisSign\", 1);\r\n        FrontAxis = I(\"FrontAxis\", 2);\r\n        FrontAxisSign = I(\"FrontAxisSign\", 1);\r\n        CoordAxis = I(\"CoordAxis\", 0);\r\n        CoordAxisSign = I(\"CoordAxisSign\", 1);\r\n        OriginalUpAxis = I(\"OriginalUpAxis\", -1);\r\n        TimeMode = I(\"TimeMode\", 0);\r\n        CustomFrameRate = D(\"CustomFrameRate\", -1.0);\r\n    }\r\n\r\n    // ------------------------------------------------------------------ connections\r\n\r\n    private void ReadConnections(FbxNode? connections)\r\n    {\r\n        if (connections is null)\r\n            return;\r\n\r\n        // First pass: typed object wiring that doesn't depend on other connections.\r\n        // OO  child \u2192 parent:   Model\u2192Model (hierarchy), AnimationLayer\u2192AnimationStack,\r\n        //                       AnimationCurveNode\u2192AnimationLayer\r\n        // OP  child \u2192 parent.property:\r\n        //                       AnimationCurveNode\u2192Model (\"Lcl Translation\"/\"Lcl Rotation\"/\"Lcl Scaling\")\r\n        //                       AnimationCurve\u2192AnimationCurveNode (\"d|X\"/\"d|Y\"/\"d|Z\")\r\n        var layerToStack = new Dictionary<long, FbxObject>();        // layer id \u2192 stack object\r\n        var curveNodeToLayers = new Dictionary<long, List<long>>();  // curve node id \u2192 layer ids\r\n        var curveNodeTargets = new Dictionary<long, (long ModelId, string Property)>();\r\n        var curveNodes = new Dictionary<long, FbxAnimCurveNode>();\r\n        var stacksById = new Dictionary<long, FbxAnimStack>();\r\n\r\n        foreach (var c in connections.ChildrenNamed(\"C\"))\r\n        {\r\n            if (c.Properties.Count < 3 ||\r\n                c.Properties[0] is not string kind ||\r\n                c.Properties[1] is not (long or int) ||\r\n                c.Properties[2] is not (long or int))\r\n                continue;\r\n\r\n            long srcId = c.Prop<long>(1); // child\r\n            long dstId = c.Prop<long>(2); // parent\r\n            if (!_objectsById.TryGetValue(srcId, out var src))\r\n                continue;\r\n            _objectsById.TryGetValue(dstId, out var dst); // dst id 0 = scene root (no object)\r\n\r\n            if (kind == \"OO\")\r\n            {\r\n                if (src.NodeType == \"Model\" && dst?.NodeType == \"Model\")\r\n                {\r\n                    if (src.ModelParent is null) // first parent wins on instancing\r\n                    {\r\n                        src.ModelParent = dst;\r\n                        dst.ModelChildren.Add(src);\r\n                    }\r\n                }\r\n                else if (src.NodeType == \"AnimationLayer\" && dst?.NodeType == \"AnimationStack\")\r\n                {\r\n                    layerToStack[srcId] = dst;\r\n                }\r\n                else if (src.NodeType == \"AnimationCurveNode\" && dst?.NodeType == \"AnimationLayer\")\r\n                {\r\n                    if (!curveNodeToLayers.TryGetValue(srcId, out var layers))\r\n                        curveNodeToLayers[srcId] = layers = new List<long>();\r\n                    layers.Add(dstId);\r\n                }\r\n            }\r\n            else if (kind == \"OP\" && c.Properties.Count >= 4 && c.Properties[3] is string property)\r\n            {\r\n                if (src.NodeType == \"AnimationCurveNode\" && dst?.NodeType == \"Model\")\r\n                {\r\n                    curveNodeTargets[srcId] = (dstId, property);\r\n                }\r\n                else if (src.NodeType == \"AnimationCurve\" && dst?.NodeType == \"AnimationCurveNode\")\r\n                {\r\n                    // Channel name \"d|X\" \u2192 axis 'X'.\r\n                    char axis = property.Length > 0 ? property[^1] : '?';\r\n                    if (axis is 'X' or 'Y' or 'Z')\r\n                    {\r\n                        if (!curveNodes.TryGetValue(dstId, out var cn))\r\n                            curveNodes[dstId] = cn = new FbxAnimCurveNode(dst);\r\n                        var curve = ReadCurve(src.Node);\r\n                        if (curve is not null && !cn.Channels.ContainsKey(axis))\r\n                            cn.Channels[axis] = curve;\r\n                    }\r\n                }\r\n            }\r\n        }\r\n\r\n        // Second pass: assemble stacks (document order) with flattened bindings.\r\n        foreach (var obj in _objectsById.Values)\r\n        {\r\n            if (obj.NodeType != \"AnimationStack\")\r\n                continue;\r\n            var stack = new FbxAnimStack(obj)\r\n            {\r\n                LocalStart = ReadTimeProperty(obj, \"LocalStart\"),\r\n                LocalStop = ReadTimeProperty(obj, \"LocalStop\"),\r\n            };\r\n            stacksById[obj.Id] = stack;\r\n            _stacks.Add(stack);\r\n        }\r\n\r\n        foreach (var (curveNodeId, target) in curveNodeTargets)\r\n        {\r\n            if (!curveNodeToLayers.TryGetValue(curveNodeId, out var layerIds))\r\n                continue;\r\n            if (!curveNodes.TryGetValue(curveNodeId, out var cn))\r\n            {\r\n                // Curve node with defaults but no connected curves still drives the property.\r\n                if (!_objectsById.TryGetValue(curveNodeId, out var cnObj))\r\n                    continue;\r\n                cn = new FbxAnimCurveNode(cnObj);\r\n            }\r\n\r\n            foreach (var layerId in layerIds)\r\n            {\r\n                if (!layerToStack.TryGetValue(layerId, out var stackObj) ||\r\n                    !stacksById.TryGetValue(stackObj.Id, out var stack))\r\n                    continue;\r\n                stack.Bindings.TryAdd((target.ModelId, target.Property), cn);\r\n            }\r\n        }\r\n    }\r\n\r\n    private long ReadTimeProperty(FbxObject obj, string name)\r\n    {\r\n        var p = FindProperty(obj, name);\r\n        if (p is null || p.Values.Count < 1)\r\n            return 0;\r\n        try\r\n        {\r\n            return Convert.ToInt64(p.Values[0], System.Globalization.CultureInfo.InvariantCulture);\r\n        }\r\n        // ArithmeticException covers OverflowException, which is not s&box-whitelisted\r\n        catch (Exception ex) when (ex is InvalidCastException or ArithmeticException or FormatException)\r\n        {\r\n            return 0;\r\n        }\r\n    }\r\n\r\n    private static FbxAnimCurve? ReadCurve(FbxNode curveNode)\r\n    {\r\n        var timesNode = curveNode.Child(\"KeyTime\");\r\n        var valuesNode = curveNode.Child(\"KeyValueFloat\") ?? curveNode.Child(\"KeyValue\");\r\n        if (timesNode is null || valuesNode is null)\r\n            return null;\r\n\r\n        long[] times = timesNode.AsLongArray(0);\r\n        float[] values = valuesNode.AsFloatArray(0);\r\n        if (times.Length == 0 || times.Length != values.Length)\r\n            return null;\r\n        return new FbxAnimCurve(times, values);\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Target/AnimationDriverMesh.cs",
            "FileName": "AnimationDriverMesh.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "namespace HumanoidHandRetargeter.Target;\r\n\r\n/// <summary>Small hidden mesh required by SceneModel's animation owner.\r\n/// Cube FBX layout from ufbx's public-domain maya_cube_7400_ascii fixture:\r\n/// https://github.com/ufbx/ufbx/blob/master/data/maya_cube_7400_ascii.fbx\r\n/// Used under ufbx LICENSE alternative B (public domain).</summary>\r\npublic static class AnimationDriverMesh\r\n{\r\n    public static readonly string Fbx = Text.Replace(\"\\r\\n\", \"\\n\").Replace(\"\\n\", \"\\r\\n\");\r\n    private const string Text = \"\"\"\r\n; FBX 7.4.0 project file\r\n; ----------------------------------------------------\r\n\r\nFBXHeaderExtension:  {\r\n\tFBXHeaderVersion: 1003\r\n\tFBXVersion: 7400\r\n\tCreationTimeStamp:  {\r\n\t\tVersion: 1000\r\n\t\tYear: 2020\r\n\t\tMonth: 3\r\n\t\tDay: 31\r\n\t\tHour: 21\r\n\t\tMinute: 36\r\n\t\tSecond: 10\r\n\t\tMillisecond: 814\r\n\t}\r\n\tCreator: \"FBX SDK/FBX Plugins version 2019.2\"\r\n\tSceneInfo: \"SceneInfo::GlobalInfo\", \"UserData\" {\r\n\t\tType: \"UserData\"\r\n\t\tVersion: 100\r\n\t\tMetaData:  {\r\n\t\t\tVersion: 100\r\n\t\t\tTitle: \"\"\r\n\t\t\tSubject: \"\"\r\n\t\t\tAuthor: \"\"\r\n\t\t\tKeywords: \"\"\r\n\t\t\tRevision: \"\"\r\n\t\t\tComment: \"\"\r\n\t\t}\r\n\t\tProperties70:  {\r\n\t\t\tP: \"DocumentUrl\", \"KString\", \"Url\", \"\", \"animation_driver.fbx\"\r\n\t\t\tP: \"SrcDocumentUrl\", \"KString\", \"Url\", \"\", \"animation_driver.fbx\"\r\n\t\t\tP: \"Original\", \"Compound\", \"\", \"\"\r\n\t\t\tP: \"Original|ApplicationVendor\", \"KString\", \"\", \"\", \"Autodesk\"\r\n\t\t\tP: \"Original|ApplicationName\", \"KString\", \"\", \"\", \"Maya\"\r\n\t\t\tP: \"Original|ApplicationVersion\", \"KString\", \"\", \"\", \"201900\"\r\n\t\t\tP: \"Original|DateTime_GMT\", \"DateTime\", \"\", \"\", \"31/03/2020 18:36:10.811\"\r\n\t\t\tP: \"Original|FileName\", \"KString\", \"\", \"\", \"animation_driver.fbx\"\r\n\t\t\tP: \"LastSaved\", \"Compound\", \"\", \"\"\r\n\t\t\tP: \"LastSaved|ApplicationVendor\", \"KString\", \"\", \"\", \"Autodesk\"\r\n\t\t\tP: \"LastSaved|ApplicationName\", \"KString\", \"\", \"\", \"Maya\"\r\n\t\t\tP: \"LastSaved|ApplicationVersion\", \"KString\", \"\", \"\", \"201900\"\r\n\t\t\tP: \"LastSaved|DateTime_GMT\", \"DateTime\", \"\", \"\", \"31/03/2020 18:36:10.811\"\r\n\t\t\tP: \"Original|ApplicationActiveProject\", \"KString\", \"\", \"\", \"animation_driver.fbx\"\r\n\t\t}\r\n\t}\r\n}\r\nGlobalSettings:  {\r\n\tVersion: 1000\r\n\tProperties70:  {\r\n\t\tP: \"UpAxis\", \"int\", \"Integer\", \"\",1\r\n\t\tP: \"UpAxisSign\", \"int\", \"Integer\", \"\",1\r\n\t\tP: \"FrontAxis\", \"int\", \"Integer\", \"\",2\r\n\t\tP: \"FrontAxisSign\", \"int\", \"Integer\", \"\",1\r\n\t\tP: \"CoordAxis\", \"int\", \"Integer\", \"\",0\r\n\t\tP: \"CoordAxisSign\", \"int\", \"Integer\", \"\",1\r\n\t\tP: \"OriginalUpAxis\", \"int\", \"Integer\", \"\",1\r\n\t\tP: \"OriginalUpAxisSign\", \"int\", \"Integer\", \"\",1\r\n\t\tP: \"UnitScaleFactor\", \"double\", \"Number\", \"\",1\r\n\t\tP: \"OriginalUnitScaleFactor\", \"double\", \"Number\", \"\",1\r\n\t\tP: \"AmbientColor\", \"ColorRGB\", \"Color\", \"\",0,0,0\r\n\t\tP: \"DefaultCamera\", \"KString\", \"\", \"\", \"Producer Perspective\"\r\n\t\tP: \"TimeMode\", \"enum\", \"\", \"\",11\r\n\t\tP: \"TimeProtocol\", \"enum\", \"\", \"\",2\r\n\t\tP: \"SnapOnFrameMode\", \"enum\", \"\", \"\",0\r\n\t\tP: \"TimeSpanStart\", \"KTime\", \"Time\", \"\",1924423250\r\n\t\tP: \"TimeSpanStop\", \"KTime\", \"Time\", \"\",384884650000\r\n\t\tP: \"CustomFrameRate\", \"double\", \"Number\", \"\",-1\r\n\t\tP: \"TimeMarker\", \"Compound\", \"\", \"\"\r\n\t\tP: \"CurrentTimeMarker\", \"int\", \"Integer\", \"\",-1\r\n\t}\r\n}\r\n\r\n; Documents Description\r\n;------------------------------------------------------------------\r\n\r\nDocuments:  {\r\n\tCount: 1\r\n\tDocument: 1907658701024, \"\", \"Scene\" {\r\n\t\tProperties70:  {\r\n\t\t\tP: \"SourceObject\", \"object\", \"\", \"\"\r\n\t\t\tP: \"ActiveAnimStackName\", \"KString\", \"\", \"\", \"Take 001\"\r\n\t\t}\r\n\t\tRootNode: 0\r\n\t}\r\n}\r\n\r\n; Document References\r\n;------------------------------------------------------------------\r\n\r\nReferences:  {\r\n}\r\n\r\n; Object definitions\r\n;------------------------------------------------------------------\r\n\r\nDefinitions:  {\r\n\tVersion: 100\r\n\tCount: 6\r\n\tObjectType: \"GlobalSettings\" {\r\n\t\tCount: 1\r\n\t}\r\n\tObjectType: \"AnimationStack\" {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \"FbxAnimStack\" {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \"Description\", \"KString\", \"\", \"\", \"\"\r\n\t\t\t\tP: \"LocalStart\", \"KTime\", \"Time\", \"\",0\r\n\t\t\t\tP: \"LocalStop\", \"KTime\", \"Time\", \"\",0\r\n\t\t\t\tP: \"ReferenceStart\", \"KTime\", \"Time\", \"\",0\r\n\t\t\t\tP: \"ReferenceStop\", \"KTime\", \"Time\", \"\",0\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tObjectType: \"AnimationLayer\" {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \"FbxAnimLayer\" {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \"Weight\", \"Number\", \"\", \"A\",100\r\n\t\t\t\tP: \"Mute\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"Solo\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"Lock\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"Color\", \"ColorRGB\", \"Color\", \"\",0.8,0.8,0.8\r\n\t\t\t\tP: \"BlendMode\", \"enum\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationAccumulationMode\", \"enum\", \"\", \"\",0\r\n\t\t\t\tP: \"ScaleAccumulationMode\", \"enum\", \"\", \"\",0\r\n\t\t\t\tP: \"BlendModeBypass\", \"ULongLong\", \"\", \"\",0\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tObjectType: \"Geometry\" {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \"FbxMesh\" {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \"Color\", \"ColorRGB\", \"Color\", \"\",0.8,0.8,0.8\r\n\t\t\t\tP: \"BBoxMin\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"BBoxMax\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"Primary Visibility\", \"bool\", \"\", \"\",1\r\n\t\t\t\tP: \"Casts Shadows\", \"bool\", \"\", \"\",1\r\n\t\t\t\tP: \"Receive Shadows\", \"bool\", \"\", \"\",1\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tObjectType: \"Material\" {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \"FbxSurfaceLambert\" {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \"ShadingModel\", \"KString\", \"\", \"\", \"Lambert\"\r\n\t\t\t\tP: \"MultiLayer\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"EmissiveColor\", \"Color\", \"\", \"A\",0,0,0\r\n\t\t\t\tP: \"EmissiveFactor\", \"Number\", \"\", \"A\",1\r\n\t\t\t\tP: \"AmbientColor\", \"Color\", \"\", \"A\",0.2,0.2,0.2\r\n\t\t\t\tP: \"AmbientFactor\", \"Number\", \"\", \"A\",1\r\n\t\t\t\tP: \"DiffuseColor\", \"Color\", \"\", \"A\",0.8,0.8,0.8\r\n\t\t\t\tP: \"DiffuseFactor\", \"Number\", \"\", \"A\",1\r\n\t\t\t\tP: \"Bump\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"NormalMap\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"BumpFactor\", \"double\", \"Number\", \"\",1\r\n\t\t\t\tP: \"TransparentColor\", \"Color\", \"\", \"A\",0,0,0\r\n\t\t\t\tP: \"TransparencyFactor\", \"Number\", \"\", \"A\",0\r\n\t\t\t\tP: \"DisplacementColor\", \"ColorRGB\", \"Color\", \"\",0,0,0\r\n\t\t\t\tP: \"DisplacementFactor\", \"double\", \"Number\", \"\",1\r\n\t\t\t\tP: \"VectorDisplacementColor\", \"ColorRGB\", \"Color\", \"\",0,0,0\r\n\t\t\t\tP: \"VectorDisplacementFactor\", \"double\", \"Number\", \"\",1\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tObjectType: \"Model\" {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \"FbxNode\" {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \"QuaternionInterpolate\", \"enum\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationOffset\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"RotationPivot\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"ScalingOffset\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"ScalingPivot\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"TranslationActive\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"TranslationMin\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"TranslationMax\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"TranslationMinX\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"TranslationMinY\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"TranslationMinZ\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"TranslationMaxX\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"TranslationMaxY\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"TranslationMaxZ\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationOrder\", \"enum\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationSpaceForLimitOnly\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationStiffnessX\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"RotationStiffnessY\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"RotationStiffnessZ\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"AxisLen\", \"double\", \"Number\", \"\",10\r\n\t\t\t\tP: \"PreRotation\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"PostRotation\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"RotationActive\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationMin\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"RotationMax\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"RotationMinX\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationMinY\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationMinZ\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationMaxX\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationMaxY\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationMaxZ\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"InheritType\", \"enum\", \"\", \"\",0\r\n\t\t\t\tP: \"ScalingActive\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"ScalingMin\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"ScalingMax\", \"Vector3D\", \"Vector\", \"\",1,1,1\r\n\t\t\t\tP: \"ScalingMinX\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"ScalingMinY\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"ScalingMinZ\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"ScalingMaxX\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"ScalingMaxY\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"ScalingMaxZ\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"GeometricTranslation\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"GeometricRotation\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"GeometricScaling\", \"Vector3D\", \"Vector\", \"\",1,1,1\r\n\t\t\t\tP: \"MinDampRangeX\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MinDampRangeY\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MinDampRangeZ\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MaxDampRangeX\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MaxDampRangeY\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MaxDampRangeZ\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MinDampStrengthX\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MinDampStrengthY\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MinDampStrengthZ\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MaxDampStrengthX\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MaxDampStrengthY\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MaxDampStrengthZ\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"PreferedAngleX\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"PreferedAngleY\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"PreferedAngleZ\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"LookAtProperty\", \"object\", \"\", \"\"\r\n\t\t\t\tP: \"UpVectorProperty\", \"object\", \"\", \"\"\r\n\t\t\t\tP: \"Show\", \"bool\", \"\", \"\",1\r\n\t\t\t\tP: \"NegativePercentShapeSupport\", \"bool\", \"\", \"\",1\r\n\t\t\t\tP: \"DefaultAttributeIndex\", \"int\", \"Integer\", \"\",-1\r\n\t\t\t\tP: \"Freeze\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"LODBox\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"Lcl Translation\", \"Lcl Translation\", \"\", \"A\",0,0,0\r\n\t\t\t\tP: \"Lcl Rotation\", \"Lcl Rotation\", \"\", \"A\",0,0,0\r\n\t\t\t\tP: \"Lcl Scaling\", \"Lcl Scaling\", \"\", \"A\",1,1,1\r\n\t\t\t\tP: \"Visibility\", \"Visibility\", \"\", \"A\",1\r\n\t\t\t\tP: \"Visibility Inheritance\", \"Visibility Inheritance\", \"\", \"\",1\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n\r\n; Object properties\r\n;------------------------------------------------------------------\r\n\r\nObjects:  {\r\n\tGeometry: 1907663073408, \"Geometry::\", \"Mesh\" {\r\n\t\tVertices: *24 {\r\n\t\t\ta: -0.5,-0.5,0.5,0.5,-0.5,0.5,-0.5,0.5,0.5,0.5,0.5,0.5,-0.5,0.5,-0.5,0.5,0.5,-0.5,-0.5,-0.5,-0.5,0.5,-0.5,-0.5\r\n\t\t}\r\n\t\tPolygonVertexIndex: *24 {\r\n\t\t\ta: 0,1,3,-3,2,3,5,-5,4,5,7,-7,6,7,1,-1,1,7,5,-4,6,0,2,-5\r\n\t\t}\r\n\t\tEdges: *12 {\r\n\t\t\ta: 0,2,6,10,3,1,7,5,11,9,15,13\r\n\t\t}\r\n\t\tGeometryVersion: 124\r\n\t\tLayerElementNormal: 0 {\r\n\t\t\tVersion: 102\r\n\t\t\tName: \"\"\r\n\t\t\tMappingInformationType: \"ByPolygonVertex\"\r\n\t\t\tReferenceInformationType: \"Direct\"\r\n\t\t\tNormals: *72 {\r\n\t\t\t\ta: 0,0,1,0,0,1,0,0,1,0,0,1,0,1,0,0,1,0,0,1,0,0,1,0,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,1,0,0,1,0,0,1,0,0,1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0\r\n\t\t\t}\r\n\t\t\tNormalsW: *24 {\r\n\t\t\t\ta: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayerElementBinormal: 0 {\r\n\t\t\tVersion: 102\r\n\t\t\tName: \"map1\"\r\n\t\t\tMappingInformationType: \"ByPolygonVertex\"\r\n\t\t\tReferenceInformationType: \"Direct\"\r\n\t\t\tBinormals: *72 {\r\n\t\t\t\ta: 0,1,-0,0,1,-0,0,1,-0,0,1,-0,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,0,1,0,0,1,0,0,1,0,0,1,-0,1,0,-0,1,0,0,1,-0,-0,1,0,0,1,0,0,1,0,0,1,0,0,1,0\r\n\t\t\t}\r\n\t\t\tBinormalsW: *24 {\r\n\t\t\t\ta: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1\r\n\t\t\t}\r\n\r\n\t\t}\r\n\t\tLayerElementTangent: 0 {\r\n\t\t\tVersion: 102\r\n\t\t\tName: \"map1\"\r\n\t\t\tMappingInformationType: \"ByPolygonVertex\"\r\n\t\t\tReferenceInformationType: \"Direct\"\r\n\t\t\tTangents: *72 {\r\n\t\t\t\ta: 1,-0,-0,1,-0,0,1,-0,0,1,-0,0,1,-0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,-0,1,0,-0,1,0,-0,1,0,-0,0,0,-1,0,0,-1,0,-0,-1,0,0,-1,0,-0,1,0,-0,1,0,-0,1,0,-0,1\r\n\t\t\t}\r\n\t\t\tTangentsW: *24 {\r\n\t\t\t\ta: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayerElementUV: 0 {\r\n\t\t\tVersion: 101\r\n\t\t\tName: \"map1\"\r\n\t\t\tMappingInformationType: \"ByPolygonVertex\"\r\n\t\t\tReferenceInformationType: \"IndexToDirect\"\r\n\t\t\tUV: *28 {\r\n\t\t\t\ta: 0.375,0,0.625,0,0.375,0.25,0.625,0.25,0.375,0.5,0.625,0.5,0.375,0.75,0.625,0.75,0.375,1,0.625,1,0.875,0,0.875,0.25,0.125,0,0.125,0.25\r\n\t\t\t}\r\n\t\t\tUVIndex: *24 {\r\n\t\t\t\ta: 0,1,3,2,2,3,5,4,4,5,7,6,6,7,9,8,1,10,11,3,12,0,2,13\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayerElementSmoothing: 0 {\r\n\t\t\tVersion: 102\r\n\t\t\tName: \"\"\r\n\t\t\tMappingInformationType: \"ByEdge\"\r\n\t\t\tReferenceInformationType: \"Direct\"\r\n\t\t\tSmoothing: *12 {\r\n\t\t\t\ta: 0,0,0,0,0,0,0,0,0,0,0,0\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayerElementMaterial: 0 {\r\n\t\t\tVersion: 101\r\n\t\t\tName: \"\"\r\n\t\t\tMappingInformationType: \"AllSame\"\r\n\t\t\tReferenceInformationType: \"IndexToDirect\"\r\n\t\t\tMaterials: *1 {\r\n\t\t\t\ta: 0\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayer: 0 {\r\n\t\t\tVersion: 100\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \"LayerElementNormal\"\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \"LayerElementBinormal\"\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \"LayerElementTangent\"\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \"LayerElementMaterial\"\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \"LayerElementSmoothing\"\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \"LayerElementUV\"\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tModel: 1907292236800, \"Model::animation_driver\", \"Mesh\" {\r\n\t\tVersion: 232\r\n\t\tProperties70:  {\r\n\t\t\tP: \"RotationActive\", \"bool\", \"\", \"\",1\r\n\t\t\tP: \"InheritType\", \"enum\", \"\", \"\",1\r\n\t\t\tP: \"ScalingMax\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\tP: \"DefaultAttributeIndex\", \"int\", \"Integer\", \"\",0\r\n\t\t\tP: \"currentUVSet\", \"KString\", \"\", \"U\", \"map1\"\r\n\t\t}\r\n\t\tShading: T\r\n\t\tCulling: \"CullingOff\"\r\n\t}\r\n\tMaterial: 1908464858960, \"Material::lambert1\", \"\" {\r\n\t\tVersion: 102\r\n\t\tShadingModel: \"lambert\"\r\n\t\tMultiLayer: 0\r\n\t\tProperties70:  {\r\n\t\t\tP: \"AmbientColor\", \"Color\", \"\", \"A\",0,0,0\r\n\t\t\tP: \"DiffuseColor\", \"Color\", \"\", \"A\",0.5,0.5,0.5\r\n\t\t\tP: \"DiffuseFactor\", \"Number\", \"\", \"A\",0.800000011920929\r\n\t\t\tP: \"TransparencyFactor\", \"Number\", \"\", \"A\",1\r\n\t\t\tP: \"Emissive\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\tP: \"Ambient\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\tP: \"Diffuse\", \"Vector3D\", \"Vector\", \"\",0.400000005960464,0.400000005960464,0.400000005960464\r\n\t\t\tP: \"Opacity\", \"double\", \"Number\", \"\",1\r\n\t\t}\r\n\t}\r\n\tAnimationStack: 1907674289488, \"AnimStack::Take 001\", \"\" {\r\n\t\tProperties70:  {\r\n\t\t\tP: \"LocalStart\", \"KTime\", \"Time\", \"\",1924423250\r\n\t\t\tP: \"LocalStop\", \"KTime\", \"Time\", \"\",230930790000\r\n\t\t\tP: \"ReferenceStart\", \"KTime\", \"Time\", \"\",1924423250\r\n\t\t\tP: \"ReferenceStop\", \"KTime\", \"Time\", \"\",230930790000\r\n\t\t}\r\n\t}\r\n\tAnimationLayer: 1907631731008, \"AnimLayer::BaseLayer\", \"\" {\r\n\t}\r\n}\r\n\r\n; Object connections\r\n;------------------------------------------------------------------\r\n\r\nConnections:  {\r\n\r\n\t;Model::animation_driver, Model::RootNode\r\n\tC: \"OO\",1907292236800,0\r\n\r\n\t;AnimLayer::BaseLayer, AnimStack::Take 001\r\n\tC: \"OO\",1907631731008,1907674289488\r\n\r\n\t;Geometry::, Model::animation_driver\r\n\tC: \"OO\",1907663073408,1907292236800\r\n\r\n\t;Material::lambert1, Model::animation_driver\r\n\tC: \"OO\",1908464858960,1907292236800\r\n}\r\n;Takes section\r\n;----------------------------------------------------\r\n\r\nTakes:  {\r\n\tCurrent: \"Take 001\"\r\n\tTake: \"Take 001\" {\r\n\t\tFileName: \"Take_001.tak\"\r\n\t\tLocalTime: 1924423250,230930790000\r\n\t\tReferenceTime: 1924423250,230930790000\r\n\t}\r\n}\r\n\r\n\"\"\";\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Target/HandModelFactory.cs",
            "FileName": "HandModelFactory.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "#nullable enable\r\nnamespace HumanoidHandRetargeter.Target;\r\n\r\n/// <summary>Minimal hand ModelDoc using the audited VmdlWriter mesh and scaling node shapes.</summary>\r\npublic static class HandModelFactory\r\n{\r\n    public const string Header = \"<!-- kv3 encoding:text:version{e21c7f3c-8a33-41c5-9977-a76d3a32aa0d} format:modeldoc30:version{8c2d7a91-9c42-4bf0-883a-5a3b1762d4f1} -->\";\r\n    public static string Create(string baseModel = \"\", string mesh = \"\", float meshUnitScaleCm = 1, IReadOnlyDictionary<string,string>? materialRemaps = null, IReadOnlyList<string>? meshNames = null)\r\n    {\r\n        if (!float.IsFinite(meshUnitScaleCm) || meshUnitScaleCm <= 0) throw new ArgumentOutOfRangeException(nameof(meshUnitScaleCm));\r\n        if (string.IsNullOrWhiteSpace(baseModel) == string.IsNullOrWhiteSpace(mesh)) throw new ArgumentException(\"Choose one base model or mesh source.\");\r\n        var children = new KvArray();\r\n        if (mesh.Length > 0)\r\n        {\r\n            mesh = VmdlSetupService.NormalizeAssetPath(mesh, \".fbx\");\r\n            var meshes = new KvArray();\r\n            meshes.Items.Add(new KvObject { [\"_class\"] = new KvString(\"RenderMeshFile\"), [\"name\"] = new KvString(\"hands_mesh\"),\r\n                [\"filename\"] = new KvString(mesh), [\"import_scale\"] = new KvDouble(meshUnitScaleCm),\r\n                [\"import_translation\"] = Vector(0,0,0), [\"import_rotation\"] = Vector(0,0,0),\r\n                [\"align_origin_x_type\"] = new KvString(\"None\"), [\"align_origin_y_type\"] = new KvString(\"None\"), [\"align_origin_z_type\"] = new KvString(\"None\") });\r\n            if(meshNames is {Count:>0})\r\n            {\r\n                var names=new KvArray();foreach(var name in meshNames)names.Items.Add(new KvString(name));\r\n                ((KvObject)meshes.Items[0])[\"import_filter\"]=new KvObject\r\n                    {[\"exclude_by_default\"]=new KvBool(true),[\"exception_list\"]=names};\r\n            }\r\n            children.Items.Add(new KvObject { [\"_class\"] = new KvString(\"RenderMeshList\"), [\"children\"] = meshes });\r\n            children.Items.Add(new KvObject { [\"_class\"] = new KvString(\"BoneMarkupList\"), [\"bone_cull_type\"] = new KvString(\"None\"), [\"children\"] = new KvArray() });\r\n            var remaps=new KvArray();\r\n            if(materialRemaps is not null)foreach(var pair in materialRemaps)remaps.Items.Add(new KvObject{[\"from\"]=new KvString(pair.Key),[\"to\"]=new KvString(pair.Value)});\r\n            var materials = new KvArray();\r\n            materials.Items.Add(new KvObject { [\"_class\"] = new KvString(\"DefaultMaterialGroup\"), [\"use_global_default\"] = new KvBool(materialRemaps is null || materialRemaps.Count==0),\r\n                [\"global_default_material\"] = new KvString(\"materials/dev/reflectivity_50.vmat\"), [\"remaps\"] = remaps });\r\n            children.Items.Add(new KvObject { [\"_class\"] = new KvString(\"MaterialGroupList\"), [\"children\"] = materials });\r\n        }\r\n        else baseModel = VmdlSetupService.NormalizeAssetPath(baseModel, \".vmdl\");\r\n        var modifiers = new KvArray();\r\n        modifiers.Items.Add(new KvObject { [\"_class\"] = new KvString(\"ModelModifier_ScaleAndMirror\"), [\"scale\"] = new KvDouble(1.0 / 2.54),\r\n            [\"mirror_x\"] = new KvBool(false), [\"mirror_y\"] = new KvBool(false), [\"mirror_z\"] = new KvBool(false),\r\n            [\"flip_bone_forward\"] = new KvBool(false), [\"swap_left_and_right_bones\"] = new KvBool(false) });\r\n        children.Items.Add(new KvObject { [\"_class\"] = new KvString(\"ModelModifierList\"), [\"children\"] = modifiers });\r\n        var root = new KvObject { [\"_class\"] = new KvString(\"RootNode\"), [\"children\"] = children, [\"base_model_name\"] = new KvString(baseModel),\r\n            [\"anim_graph_name\"] = new KvString(\"\"), [\"model_archetype\"] = new KvString(\"\"), [\"primary_associated_entity\"] = new KvString(\"\") };\r\n        return Kv3.Serialize(new Kv3Document(Header, new KvObject { [\"rootNode\"] = root }));\r\n    }\r\n\r\n    private static KvArray Vector(params double[] values) { var array = new KvArray(); foreach (var value in values) array.Items.Add(new KvDouble(value)); return array; }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Editor/HumanoidHandRetargeter/HandPreviewDialog.cs",
            "FileName": "HandPreviewDialog.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "#nullable enable\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Editor;\r\nusing Sandbox;\r\n\r\nnamespace HumanoidHandRetargeter.Editor;\r\n\r\n/// <summary>Legacy PreviewDialog layout, with Third Person/FPS camera controls.</summary>\r\npublic sealed class HandPreviewDialog : Dialog\r\n{\r\n    public HandPreviewWidget Preview {get;}\r\n    public Action? Confirmed {get;set;}\r\n    public HandPreviewDialog(Widget? parent,HandTarget target,IReadOnlyList<HandBakedClip> clips) : base(parent)\r\n    {\r\n        Window.WindowTitle=\"Preview - \"+System.IO.Path.GetFileName(target.ModelPath);Window.SetWindowIcon(\"preview\");Window.SetModal(true,true);\r\n        Window.MinimumWidth=560;Window.MinimumHeight=560;Window.Size=new Vector2(760,780);\r\n        Layout=Layout.Column();Layout.Margin=12;Layout.Spacing=8;\r\n        Preview=Layout.Add(new HandPreviewWidget(this,target),1);\r\n        var error=Layout.Add(new Label(this){WordWrap=true,Visible=false});error.SetStyles($\"color:{Theme.Red.Hex};\");\r\n        var modeRow=Layout.AddRow();modeRow.Spacing=8;\r\n        var modes=modeRow.Add(new ComboBox(this){MinimumWidth=150});\r\n        modes.AddItem(\"Third Person\",\"3d_rotation\",()=>Preview.Mode=HandPreviewMode.ThirdPerson,selected:true);\r\n        modes.AddItem(\"FPS\",\"first_page\",()=>Preview.Mode=HandPreviewMode.Fps);\r\n        modeRow.Add(new Button(\"Reset camera\",\"center_focus_strong\"){Clicked=Preview.ResetCamera});\r\n        modeRow.Add(new Button(\"\u2212\"){Clicked=()=>Preview.Zoom(1.2f)});modeRow.Add(new Button(\"+\"){Clicked=()=>Preview.Zoom(.8f)});\r\n        modeRow.Add(new Button(\"Weapon model\u2026\",\"view_in_ar\"){Clicked=()=>{var picker=AssetPicker.Create(this,AssetType.Model);picker.OnAssetPicked=assets=>{var a=assets.FirstOrDefault();if(a is not null){try{Preview.SetWeapon(a.Path);error.Visible=false;}catch(Exception ex){error.Text=ex.Message;error.Visible=true;}}};picker.Show();}});\r\n        var cameraRow=Layout.AddRow();cameraRow.Spacing=6;\r\n        var authored=cameraRow.Add(new Checkbox(\"Authored camera motion\"){Value=true});authored.Clicked=()=>Preview.UseAuthoredCamera=authored.Value;\r\n        cameraRow.Add(new Label(this){Text=\"FOV:\"});var fov=cameraRow.Add(new LineEdit(this){Text=\"75\",FixedWidth=42});\r\n        cameraRow.Add(new Label(this){Text=\"FPS offset X / Y / Z:\"});var offsets=Enumerable.Range(0,3).Select(_=>cameraRow.Add(new LineEdit(this){Text=\"0\",FixedWidth=42})).ToArray();\r\n        cameraRow.Add(new Button(\"Apply\"){Clicked=()=>{if(float.TryParse(fov.Text,out var f)&&float.IsFinite(f))Preview.FpsFov=f;var values=offsets.Select(e=>float.TryParse(e.Text,out var v)&&float.IsFinite(v)?v:0).ToArray();Preview.FpsOffset=new Vector3(values[0],values[1],values[2]);Preview.ApplyCurrentFrame();}});\r\n        var clipRow=Layout.AddRow();clipRow.Spacing=8;clipRow.Add(new Label(this){Text=\"Clip:\"});var combo=clipRow.Add(new ComboBox(this){MinimumWidth=220},1);\r\n        for(var i=0;i<clips.Count;i++){var clip=clips[i];combo.AddItem(clip.Baked.Name,\"movie\",()=>Preview.SetClip(clip),selected:i==0);}\r\n        var transport=Layout.AddRow();transport.Spacing=8;\r\n        var play=transport.Add(new Button(\"\",\"pause\"){FixedWidth=28,FixedHeight=24});play.Clicked=()=>{Preview.Playing=!Preview.Playing;play.Icon=Preview.Playing?\"pause\":\"play_arrow\";};\r\n        var slider=transport.Add(new FloatSlider(this),1);slider.Minimum=0;slider.OnValueEdited=()=>Preview.Scrub((int)slider.Value);\r\n        var label=transport.Add(new Label(this){Text=\"0 / 0\",FixedWidth=80});\r\n        var ghost=transport.Add(new Button(\"Show source\",\"compare\"){IsToggle=true,FixedHeight=24});ghost.Clicked=()=>Preview.ShowSource=ghost.IsChecked;\r\n        var skeleton=transport.Add(new Button(\"Skeleton\",\"polyline\"){IsToggle=true,FixedHeight=24});skeleton.Clicked=()=>Preview.SkeletonOnly=skeleton.IsChecked;\r\n        Preview.FrameChanged=frame=>{if(modes.CurrentIndex!=(int)Preview.Mode)modes.CurrentIndex=(int)Preview.Mode;play.Icon=Preview.Playing?\"pause\":\"play_arrow\";slider.Maximum=Math.Max(0,Preview.FrameCount-1);slider.Value=frame;label.Text=$\"{frame+1} / {Preview.FrameCount}\";authored.Enabled=Preview.HasAuthoredCamera;};\r\n        var footer=Layout.AddRow();footer.Spacing=8;footer.AddStretchCell();footer.Add(new Button(\"Cancel\"){Clicked=Close});\r\n        footer.Add(new Button.Primary(\"Looks good - Convert\"){Icon=\"check\",Tint=Theme.Green,Clicked=()=>{Confirmed?.Invoke();Close();}});\r\n        if(clips.Count>0)Preview.SetClip(clips[0]);\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Code/Core/Mapping/BoneNameTokens.cs",
            "FileName": "BoneNameTokens.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "// Tokenization reused from humanoid-retargeter 26084c96c3fc870aaf9a5bd798de063ce2fd62df.\r\nusing System.Text;\r\nnamespace HumanoidHandRetargeter.Mapping;\r\ninternal static class BoneNameTokens\r\n{\r\n    internal static List<string> Tokenize(string name)\r\n    {\r\n        var tokens = new List<string>();\r\n        var current = new StringBuilder();\r\n        char previous = '\\0';\r\n        for (var i = 0; i < name.Length; i++)\r\n        {\r\n            var c = name[i];\r\n            if (!char.IsLetterOrDigit(c))\r\n            {\r\n                Flush();\r\n                previous = '\\0';\r\n                continue;\r\n            }\r\n            if (current.Length > 0)\r\n            {\r\n                var boundary = char.IsDigit(c) != char.IsDigit(previous)\r\n                    || (char.IsUpper(c) && char.IsLower(previous))\r\n                    // Acronym/single-side prefix before a Pascal word: LThumb \u2192 L, Thumb;\r\n                    // FBXNode \u2192 FBX, Node. Without this, one-joint BVH thumbs lose both\r\n                    // their side and their otherwise unambiguous finger name.\r\n                    || (char.IsUpper(c) && char.IsUpper(previous)\r\n                        && i + 1 < name.Length && char.IsLower(name[i + 1]));\r\n                if (boundary)\r\n                    Flush();\r\n            }\r\n            current.Append(char.ToLowerInvariant(c));\r\n            previous = c;\r\n        }\r\n        Flush();\r\n        return tokens;\r\n\r\n        void Flush()\r\n        {\r\n            if (current.Length > 0)\r\n            {\r\n                tokens.Add(current.ToString());\r\n                current.Clear();\r\n            }\r\n        }\r\n    }\r\n\r\n}"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Code/Core/Mapping/HandDetectionProfiles.SplitArms.cs",
            "FileName": "HandDetectionProfiles.SplitArms.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "#nullable enable\r\nusing Vec = System.Numerics.Vector3;\r\nusing Skel = HumanoidHandRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidHandRetargeter.Mapping;\r\n\r\ninternal static partial class HandDetectionProfiles\r\n{\r\n    // Split biceps/forearms, a wrist-to-palm link, and five palm-rooted rays.\r\n    // Numeric/duplicate suffixes identify objects, not sides or anatomical fingers.\r\n    private static void ResolveUnlabelledSplitArms(Skel rig,Hint?[] hints)\r\n    {\r\n        string Name(int i)\r\n        {\r\n            var name=rig[i].Name[(rig[i].Name.LastIndexOf(':')+1)..];\r\n            return Key(name.Split('#')[0].Replace(\"_duplicate\",\"\",StringComparison.OrdinalIgnoreCase)).TrimEnd(\"0123456789\".ToCharArray());\r\n        }\r\n        int Child(int i,string name)=>rig.ChildrenOf(i).Count==1&&Name(rig.ChildrenOf(i)[0])==name?rig.ChildrenOf(i)[0]:-1;\r\n        var proposals=new List<(HandSide Side,Dictionary<int,string> Roles)>();\r\n        foreach(var upper in rig.Bones.Where(b=>Name(b.Index) is \"bisep\" or \"bicep\" or \"biceps\"))\r\n        {\r\n            var upperSplit=Child(upper.Index,Name(upper.Index));if(upperSplit<0)continue;\r\n            var lower=Child(upperSplit,\"forearm\");if(lower<0)continue;\r\n            var lowerSplit=Child(lower,\"forearm\");if(lowerSplit<0)continue;\r\n            var wrist=Child(lowerSplit,\"wrist\");if(wrist<0)continue;\r\n            var palm=Child(wrist,\"hand\");if(palm<0||rig.ChildrenOf(palm).Count!=5)continue;\r\n            var rays=new List<int[]>();\r\n            foreach(var root in rig.ChildrenOf(palm))\r\n            {\r\n                var ray=new List<int>{root};var current=root;\r\n                while(rig.ChildrenOf(current).Count==1){current=rig.ChildrenOf(current)[0];ray.Add(current);}\r\n                if(rig.ChildrenOf(current).Count!=0){rays.Clear();break;}\r\n                if(Name(ray[^1]).Contains(\"end\",StringComparison.Ordinal))ray.RemoveAt(ray.Count-1);\r\n                rays.Add(ray.ToArray());\r\n            }\r\n            if(rays.Count!=5)continue;\r\n            var thumbs=rays.Where(r=>r.Length==3&&Name(r[0]) is \"thum\" or \"thumb\").ToArray();\r\n            var fingers=rays.Where(r=>r.Length==4&&Name(r[0])==\"finger\").ToArray();\r\n            if(thumbs.Length!=1||fingers.Length!=4)continue;\r\n            var thumb=thumbs[0];\r\n            Vec Pos(int i)=>rig.RestWorld[i].Pos;\r\n            var ordered=fingers.OrderBy(r=>Vec.DistanceSquared(Pos(r[1]),Pos(thumb[1]))).ToArray();\r\n            var radial=Pos(ordered[0][1])-Pos(ordered[3][1]);\r\n            if(radial.LengthSquared()<1e-8f)continue;\r\n            radial=Vec.Normalize(radial);\r\n            // Require four distinctly ordered knuckles, not just a nearest-thumb guess.\r\n            var spacing=ordered.Select(r=>Vec.Dot(Pos(r[1])-Pos(ordered[3][1]),radial)).ToArray();\r\n            if(Enumerable.Range(0,3).Any(i=>spacing[i]-spacing[i+1]<spacing[0]*.1f))continue;\r\n            var forward=ordered.Select(r=>Pos(r[3])-Pos(r[1])).Aggregate(Vec.Zero,(a,b)=>a+b);\r\n            var normal=Vec.Cross(forward,radial);var thumbBend=Pos(thumb[2])-Pos(thumb[1]);\r\n            if(normal.LengthSquared()<1e-8f||thumbBend.LengthSquared()<1e-8f)continue;\r\n            var chirality=Vec.Dot(Vec.Normalize(normal),Vec.Normalize(thumbBend));\r\n            // An out-of-plane thumb bend supplies the palmar direction. Flat or\r\n            // ambiguous geometry remains for review rather than guessing a side.\r\n            if(MathF.Abs(chirality)<.25f)continue;\r\n            var side=chirality>0?HandSide.Left:HandSide.Right;\r\n            var roles=new Dictionary<int,string>{{upper.Index,\"UpperArm\"},{lower,\"LowerArm\"},{wrist,\"Hand\"},{palm,\"PalmMeta\"}};\r\n            var digitNames=new[]{\"Index\",\"Middle\",\"Ring\",\"Pinky\"};\r\n            for(var digit=0;digit<4;digit++)\r\n                for(var joint=0;joint<4;joint++)roles[ordered[digit][joint]]=digitNames[digit]+(joint==0?\"Meta\":\"Prox\");\r\n            foreach(var bone in thumb)roles[bone]=\"ThumbProx\";\r\n            if(roles.Keys.Any(i=>hints[i] is not null))continue;\r\n            proposals.Add((side,roles));\r\n        }\r\n        // Require a complete, disjoint bilateral pair before using unnamed sides.\r\n        if(proposals.Count!=2||proposals[0].Side==proposals[1].Side||proposals[0].Roles.Keys.Intersect(proposals[1].Roles.Keys).Any())return;\r\n        foreach(var proposal in proposals)\r\n            foreach(var role in proposal.Roles)hints[role.Key]=new(role.Value,proposal.Side,\"split_arm_palm_rays\");\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Editor/HumanoidHandRetargeter/HandEditorPipeline.cs",
            "FileName": "HandEditorPipeline.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "#nullable enable\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing System.Runtime.CompilerServices;\r\nusing System.Security.Cryptography;\r\nusing System.Threading;\r\nusing System.Threading.Tasks;\r\nusing Editor;\r\nusing Sandbox;\r\nusing HumanoidHandRetargeter.Calibration;\r\nusing HumanoidHandRetargeter.Formats.Dmx;\r\nusing HumanoidHandRetargeter.Formats.Fbx;\r\nusing HumanoidHandRetargeter.Mapping;\r\nusing HumanoidHandRetargeter.Maths;\r\nusing HumanoidHandRetargeter.Retargeting;\r\nusing HumanoidHandRetargeter.Skeleton;\r\nusing HumanoidHandRetargeter.Target;\r\nusing Skel = HumanoidHandRetargeter.Skeleton.Skeleton;\r\nusing Vec = System.Numerics.Vector3;\r\nusing Quat = System.Numerics.Quaternion;\r\n\r\nnamespace HumanoidHandRetargeter.Editor;\r\n\r\npublic sealed class HandSource\r\n{\r\n    public required string Path { get; init; }\r\n    public required SourceScene Scene { get; init; }\r\n    public required HandMappingResult Mapping { get; set; }\r\n    public string? ModelPath { get; init; }\r\n    public IReadOnlyDictionary<HandSide, Quat>? PalmFrames { get; set; }\r\n}\r\n\r\npublic sealed class HandTarget\r\n{\r\n    public IReadOnlyList<string> ImportNotes { get; init; } = Array.Empty<string>();\r\n    public required string ModelPath { get; init; }\r\n    public required Skel Skeleton { get; init; }\r\n    public required HandMappingResult Mapping { get; set; }\r\n    public IReadOnlyDictionary<HandSide, Quat>? PalmFrames { get; set; }\r\n}\r\n\r\npublic sealed record HandExportResult(string ModelPath,IReadOnlyList<string> Changes,string? BackupPath,IReadOnlyList<string>? WeaponPrefabs=null);\r\n\r\npublic sealed record HandBakedClip(HandSource Source, Clip Original, Clip Baked, IReadOnlyList<string> Notes,Vec? WeaponOffset=null,HandMotionOptions? MotionOptions=null);\r\n\r\n/// <summary>Editor adapter over the pure importer, shared bake and transactional setup.\r\n/// Engine calls use the same main-thread dispatch convention as the audited EditorPipeline.</summary>\r\npublic static class HandEditorPipeline\r\n{\r\n    public const string HumanArms = \"models/first_person/v_first_person_arms_human.vmdl\";\r\n    public const string CitizenArms = \"models/first_person/v_first_person_arms_citizen.vmdl\";\r\n    public static string Assets => Project.Current?.GetAssetsPath() ?? throw new InvalidOperationException(\"Open an s&box project first.\");\r\n    public static MainThreadAwaitable MainThread() => default;\r\n    public readonly struct MainThreadAwaitable : INotifyCompletion\r\n    {\r\n        public MainThreadAwaitable GetAwaiter() => this;\r\n        public bool IsCompleted => ThreadSafe.IsMainThread;\r\n        public void OnCompleted(Action continuation) => Sandbox.MainThread.Queue(continuation);\r\n        public void GetResult() { }\r\n    }\r\n\r\n    public static Transform ToEngine(XForm value)\r\n        => new(new Vector3(value.Pos.X, value.Pos.Y, value.Pos.Z) / 2.54f, new Rotation(value.Rot.X,value.Rot.Y,value.Rot.Z,value.Rot.W));\r\n    public static XForm FromEngine(Transform value)\r\n        => new(new Vec(value.Position.x,value.Position.y,value.Position.z)*2.54f, new Quat(value.Rotation.x,value.Rotation.y,value.Rotation.z,value.Rotation.w));\r\n\r\n    public static Skel ReadSkeleton(Model model)\r\n    {\r\n        return Skel.Create(model.Bones.AllBones.Select(b => {\r\n            var world = FromEngine(b.LocalTransform);\r\n            return new BoneDefinition(b.Name,b.Parent?.Name,b.Parent is null ? world : XForm.ToLocal(FromEngine(b.Parent.LocalTransform),world));\r\n        }).ToArray());\r\n    }\r\n\r\n    public static async Task<HandTarget> LoadTargetAsync(string path, CancellationToken cancel = default)\r\n    {\r\n        await MainThread();\r\n        var importNotes=new List<string>();\r\n        if (path.EndsWith(\".fbx\",StringComparison.OrdinalIgnoreCase)) path = await PrepareMeshAsync(path,cancel,importNotes);\r\n        path = ModelPath(path);\r\n        var model = Model.Load(path);\r\n        if (model is null || model.IsError || model.BoneCount == 0) throw new InvalidOperationException($\"Cannot load a skinned model from '{path}'.\");\r\n\r\n        var skeleton = ReadSkeleton(model);\r\n        return new HandTarget { ImportNotes=importNotes, ModelPath=path, Skeleton=skeleton, PalmFrames=HandPresetStore.LoadPalms(skeleton), Mapping=HandPresetStore.Load(skeleton) ?? HandRigDetector.Detect(skeleton) };\r\n    }\r\n\r\n    public static async Task<HandSource> LoadSourceAsync(string path, float fps = 30, CancellationToken cancel = default)\r\n    {\r\n        if (path.EndsWith(\".fbx\",StringComparison.OrdinalIgnoreCase))\r\n        {\r\n            var native = await Task.Run(()=>FbxImporter.Import(File.ReadAllBytes(path),new FbxImportOptions {SampleFps=fps}),cancel);\r\n            await MainThread();\r\n            // Share the target's X-forward/Z-up view space so the source grip, eye and weapon\r\n            // motion transfer directly, whatever axes or facing the file was authored in.\r\n            var nativeMapping=HandPresetStore.Load(native.Skeleton);\r\n            var axesMapping=nativeMapping ?? HandRigDetector.Detect(native.Skeleton);\r\n            var turn=HandViewSpace.ViewRotation(native,axesMapping);\r\n            var scene=HandViewSpace.ToViewAxes(native,axesMapping);\r\n            // Mappings are index based; palm frames saved against the native axes turn with the rig.\r\n            var palms=HandPresetStore.LoadPalms(scene.Skeleton) ?? HandPresetStore.LoadPalms(native.Skeleton)?.ToDictionary(p=>p.Key,p=>Quat.Normalize(turn*p.Value));\r\n            return new() { Path=path, Scene=scene, PalmFrames=palms, Mapping=HandPresetStore.Load(scene.Skeleton) ?? nativeMapping ?? HandRigDetector.Detect(scene.Skeleton) };\r\n        }\r\n        await MainThread();\r\n        path = ModelPath(path);\r\n        var model = Model.Load(path);\r\n        if (model is null || model.IsError) throw new InvalidOperationException($\"Cannot load source model '{path}'.\");\r\n        var skeleton = ReadSkeleton(model);\r\n        var clips = new List<Clip>();\r\n        using var sampling = new ModelSampler(model,skeleton);\r\n        foreach (var name in model.AnimationNames.ToArray())\r\n        {\r\n            cancel.ThrowIfCancellationRequested();\r\n            clips.Add(sampling.Sample(name,fps,cancel));\r\n            await Task.Delay(1,cancel); await MainThread();\r\n        }\r\n        // Facepunch weapons author recoil as additive layers. Capture their complete firing\r\n        // pose through the original weapon graph instead of retargeting a delta as a full pose.\r\n        if(skeleton.IndexOf(\"weapon_root\")>=0&&!clips.Any(c=>WeaponAnimGraph.Classify(c.Name)==WeaponAction.Fire)\r\n            &&clips.FirstOrDefault(c=>c.Name.StartsWith(\"Fire_\",StringComparison.OrdinalIgnoreCase)&&c.Name.EndsWith(\"_delta\",StringComparison.OrdinalIgnoreCase)\r\n                &&!c.Name.Contains(\"Hold\",StringComparison.OrdinalIgnoreCase)&&!c.Name.Contains(\"Dry\",StringComparison.OrdinalIgnoreCase)) is {} recoil)\r\n        {\r\n            var firing=sampling.SampleFire(Math.Max(.25f,(recoil.FrameCount-1)/recoil.Fps),fps,cancel);\r\n            if(firing is not null)clips.Add(firing);\r\n        }\r\n        return new() { Path=path,ModelPath=path, Scene=new SourceScene(skeleton,clips,2.54f,upAxis:2,frontAxis:0,coordAxis:1),\r\n            PalmFrames=HandPresetStore.LoadPalms(skeleton), Mapping=HandPresetStore.Load(skeleton) ?? HandRigDetector.Detect(skeleton) };\r\n    }\r\n\r\n    public static HandRetargetProfile Calibrate(HandSource source,HandTarget target)\r\n        => HandRetargetProfile.Calibrate(source.Scene.Skeleton,source.Mapping,target.Skeleton,target.Mapping,source.PalmFrames,target.PalmFrames);\r\n\r\n    public static HandBakedClip Bake(HandSource source, Clip clip, HandTarget target, HandMotionOptions options, CancellationToken cancel = default, HandRetargetProfile? profile = null)\r\n    {\r\n        profile ??= Calibrate(source,target);\r\n        // Compiled s&box models already share X-forward/Z-up coordinates. A wrist's\r\n        // bind rotation must not turn vertical reload travel into sideways/downward motion.\r\n        var weaponOffset=options.WeaponSpaceOffset;\r\n        // FBX sources are loaded in view space, so every weapon file (named weapon bones or not)\r\n        // keeps both palms locked to the source grip relative to the eye.\r\n        var viewSpace=source.ModelPath is not null||source.Scene.UpAxis==2&&source.Scene.FrontAxis==0;\r\n        if(options.PreserveWeaponGrip&&options.TransferWristPosition&&(source.ModelPath is null&&viewSpace\r\n            ||source.Scene.Skeleton.Bones.Any(b=>b.Name.Equals(\"weapon_root\",StringComparison.OrdinalIgnoreCase))))\r\n            weaponOffset ??= HandViewSpace.EyePosition(target.Skeleton,target.Mapping)-HandViewSpace.EyePosition(source.Scene.Skeleton,source.Mapping);\r\n        options=new HandMotionOptions{TransferWristPosition=options.TransferWristPosition,ScaleWristTravel=options.ScaleWristTravel,\r\n            SolveArmIk=options.SolveArmIk,PreserveWeaponGrip=options.PreserveWeaponGrip,WeaponSpaceOffset=weaponOffset,\r\n            WristTravelBasis=options.WristTravelBasis??(viewSpace?Quat.Identity:null)};\r\n        var notes=weaponOffset.HasValue?profile.Notes.Concat(new[]{\"Preserved both palm grip anchors in one fixed weapon space; weapon motion is not scaled per arm.\"}).ToArray():profile.Notes;\r\n        return new(source,clip,HandRetargeter.Bake(profile,clip,options,cancel),notes,weaponOffset,options);\r\n    }\r\n\r\n    public static async Task<string> ExportAsync(HandTarget target,IReadOnlyList<HandBakedClip> clips,string outputModel,\r\n        bool autoGraph,bool backup,bool weaponCompatible,bool preserveSourceTracks,CancellationToken cancel=default)\r\n        => (await ExportWithReportAsync(target,clips,outputModel,autoGraph,backup,weaponCompatible,preserveSourceTracks,cancel)).ModelPath;\r\n\r\n    public static async Task<HandExportResult> ExportWithReportAsync(HandTarget target, IReadOnlyList<HandBakedClip> clips, string outputModel,\r\n        bool autoGraph, bool backup, bool weaponCompatible, bool preserveSourceTracks, CancellationToken cancel = default)\r\n    {\r\n        await MainThread();\r\n        if(clips.Count==0)throw new InvalidOperationException(\"Select at least one animation to export.\");\r\n        if(weaponCompatible)\r\n        {\r\n            foreach(var clip in clips)\r\n            {\r\n                if(clip.Source.ModelPath is null)throw new InvalidOperationException(\"Weapon-compatible mode requires a weapon VMDL source so its animation owner can be verified.\");\r\n                var missing=target.Mapping.Hands.SelectMany(h=>new[]{h.Wrist}.Concat(h.Digits.SelectMany(d=>d.Bones))).Select(i=>target.Skeleton[i].Name).Where(n=>clip.Source.Scene.Skeleton.IndexOf(n)<0).ToArray();\r\n                if(missing.Length>0)throw new InvalidOperationException(\"These target bones cannot bonemerge onto the selected weapon: \"+string.Join(\", \",missing)+\". Use standalone export for this custom rig, or choose a weapon whose skeleton contains those bones.\");\r\n            }\r\n        }\r\n        outputModel = VmdlSetupService.NormalizeAssetPath(outputModel,\".vmdl\");\r\n        var absolute = System.IO.Path.Combine(Assets,outputModel);\r\n        var existing = File.Exists(absolute) ? File.ReadAllText(absolute) : null;\r\n        if(existing is not null)\r\n        {\r\n            var existingModel=Model.Load(outputModel);\r\n            if(existingModel is null||existingModel.IsError)throw new InvalidOperationException(\"The existing output model must compile before animations can be added.\");\r\n            var existingRig=ReadSkeleton(existingModel);\r\n            if(existingRig.Count!=target.Skeleton.Count||target.Skeleton.Bones.Any(b=>existingRig.IndexOf(b.Name)<0))throw new InvalidOperationException(\"The existing output model has a different skeleton. Choose the target model or a new output path.\");\r\n        }\r\n        var targetSource=System.IO.Path.Combine(Assets,target.ModelPath);\r\n        var original = existing ?? (File.Exists(targetSource)?File.ReadAllText(targetSource):HandModelFactory.Create(baseModel:target.ModelPath));\r\n        var folder = outputModel[..^5] + \"_animations\";\r\n        var files = new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);\r\n        var entries = new List<HandAnimationEntry>();\r\n        var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { \"bindPose\" };\r\n        // A compiled target is in inches. Existing model scaling controls the animation\r\n        // compiler's input units; new wrappers use the inherited cm-to-inch modifier.\r\n        var factor = ExportPositionFactor(original);\r\n        var turn=ExportRootRotation(original);\r\n        XForm ExportLocal(XForm value,int bone)=>target.Skeleton[bone].ParentIndex<0\r\n            ?new XForm(Vec.Transform(value.Pos*factor,turn),Quat.Normalize(turn*value.Rot))\r\n            :new XForm(value.Pos*factor,value.Rot);\r\n        Clip ExportUnits(Clip clip) => new(clip.Name,clip.Fps,clip.Looping,clip.Frames.Select(frame=>frame.Select(ExportLocal).ToArray()).ToList(),clip.NativeFps);\r\n        var exportSkeleton = Skel.Create(target.Skeleton.Bones.Select(b=>new BoneDefinition(b.Name,b.ParentIndex<0?null:target.Skeleton[b.ParentIndex].Name,ExportLocal(b.RestLocal,b.Index))).ToArray());\r\n        var bind = new Clip(\"bindPose\",30,false,new() { Pose.Rest(exportSkeleton).Locals });\r\n        files[folder+\"/bind.dmx\"] = DmxWriter.Write(exportSkeleton,bind,new() {Name=\"bindPose\",UpAxisY=false,ForwardParity=1});\r\n        foreach(var clip in clips)\r\n        {\r\n            var stem=SourceKey(clip.Source)+\"_\"+SafeName(clip.Baked.Name); var name=stem; var suffix=1;\r\n            while(!names.Add(name)) name=stem+\"_\"+(++suffix);\r\n            var content=DmxWriter.Write(exportSkeleton,ExportUnits(clip.Baked),new() {Name=name,UpAxisY=false,ForwardParity=1,ChannelExcludedBones=target.Mapping.Hands.SelectMany(h=>h.TwistOrHelperBones).ToHashSet()});\r\n            var file=folder+\"/\"+name+\"_\"+ContentKey(content)+\".dmx\";\r\n            files[file]=content;\r\n            entries.Add(new(name,file,clip.Baked.Looping));\r\n            if(preserveSourceTracks)\r\n            {\r\n                // Full source companion intentionally retains every authored camera,\r\n                // weapon and IK track with its original hierarchy, units and timing.\r\n                var tracks=DmxWriter.Write(clip.Source.Scene.Skeleton,clip.Original,new() {Name=name+\"_source_tracks\",UpAxisY=clip.Source.Scene.UpAxis==1,ForwardParity=clip.Source.ModelPath is null&&clip.Source.Scene.FrontAxis!=0?2:1});\r\n                files[folder+\"/source_tracks/\"+name+\"_\"+ContentKey(tracks)+\".dmx\"]=tracks;\r\n            }\r\n        }\r\n        var prepared=VmdlSetupService.Prepare(original,entries,new() {ModelPath=outputModel,BindPoseSource=folder+\"/bind.dmx\",AutoConfigureAnimGraph=autoGraph,WeaponCompatibleArms=weaponCompatible});\r\n        var generatedAssets=new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);\r\n        var bundles=autoGraph?HandWeaponExport.Prepare(target,clips,outputModel,files,generatedAssets,cancel):Array.Empty<HandWeaponExport.Bundle>();\r\n        foreach(var bundle in bundles)generatedAssets[bundle.PrefabPath]=HandWeaponExport.Prefab(bundle);\r\n        var committed=await VmdlSetupTransaction.CommitAsync(Assets,outputModel,existing,prepared,files,async(paths,token)=>{\r\n            await MainThread(); foreach(var path in paths) AssetSystem.RegisterFile(path);\r\n            foreach(var bundle in bundles)\r\n            {\r\n                if(bundle.LiveCalibration is null)\r\n                {\r\n                    if(!await CompileAsync(System.IO.Path.Combine(Assets,bundle.ModelPath),token))throw new InvalidOperationException(\"Weapon animation model did not compile: \"+bundle.ModelPath);\r\n                    await MainThread();\r\n                    await Task.Delay(100,token);await MainThread();\r\n                    var owner=Model.Load(bundle.ModelPath);var graph=AnimationGraph.Load(bundle.GraphPath);\r\n                    if(owner is null||owner.IsError||!owner.HasRenderMeshes()||owner.BoneCount<bundle.BoneCount||graph is null||graph.IsError\r\n                        ||!bundle.Actions.All(a=>owner.AnimationNames.Contains(a.Sequence)))throw new InvalidOperationException($\"Weapon animation owner validation failed: bones {owner?.BoneCount}/{bundle.BoneCount}, meshes {owner?.MeshCount}, vertices {owner?.MeshInfo.TotalVertices}, triangles {owner?.MeshInfo.TotalTriangles}, graph error {graph?.IsError}, sequences {string.Join(\",\",owner?.AnimationNames??Array.Empty<string>())}.\");\r\n                    if(target.Skeleton.Bones.Any(b=>owner.Bones.GetBone(WeaponClipBuilder.HandPrefix+b.Name) is null))return false;\r\n                }\r\n\r\n                if(!await CompileAsync(System.IO.Path.Combine(Assets,bundle.PrefabPath),token))throw new InvalidOperationException(\"Weapon prefab did not compile: \"+bundle.PrefabPath);\r\n                await MainThread();\r\n                // The compiled file reaches disk before the editor's asset record updates.\r\n                // Managed resource loading uses that record, not File.Exists.\r\n                var prefabAsset=AssetSystem.FindByPath(bundle.PrefabPath);\r\n                for(var wait=0;wait<50&&string.IsNullOrEmpty(prefabAsset.GetCompiledFile(true));wait++)\r\n                {await Task.Delay(100,token);await MainThread();}\r\n                var prefab=prefabAsset.LoadResource<PrefabFile>();\r\n                if(prefab is null||prefab.IsError||SceneUtility.GetPrefabScene(prefab) is null)throw new InvalidOperationException(\"Weapon prefab could not be loaded: \"+bundle.PrefabPath);\r\n            }\r\n            if(!await CompileAsync(absolute,token)) return false;\r\n            await MainThread(); var model=Model.Load(outputModel);\r\n            // The compiled file can arrive before an already-loaded model refreshes\r\n            // its sequence list. Re-exporting changed motion adds a preserved-name\r\n            // suffix, so wait for those new sequences before validating the bake.\r\n            var modelDeadline=DateTime.UtcNow.AddSeconds(15);\r\n            while(model is not null&&!model.IsError&&!prepared.Animations.All(a=>model.AnimationNames.Contains(a.SequenceName))\r\n                &&DateTime.UtcNow<modelDeadline)\r\n            {\r\n                await Task.Delay(50,token);await MainThread();model=Model.Load(outputModel);\r\n            }\r\n            if(model is null || model.IsError || !prepared.Animations.All(a=>model.AnimationNames.Contains(a.SequenceName)))return false;\r\n            if(prepared.GeneratedGraphPath is { } graphPath){var graph=AnimationGraph.Load(graphPath);if(graph is null||graph.IsError)return false;}\r\n\r\n            var compiledRig=ReadSkeleton(model);\r\n            using var sampler=new ModelSampler(model,compiledRig);\r\n            var checkedBones=target.Mapping.Hands.SelectMany(h=>new int?[]{h.Clavicle,h.UpperArm,h.Forearm,h.Wrist}.Where(i=>i.HasValue).Select(i=>i!.Value).Concat(h.Digits.SelectMany(d=>d.Bones))).Distinct().ToArray();\r\n            for(var i=0;i<clips.Count;i++)\r\n            {\r\n                var expectedClip=clips[i].Baked;var actualClip=sampler.Sample(prepared.Animations[i].SequenceName,expectedClip.Fps,token);\r\n                if(expectedClip.FrameCount>1&&actualClip.FrameCount!=expectedClip.FrameCount)return false;\r\n                foreach(var frame in new[]{0,expectedClip.FrameCount/2,expectedClip.FrameCount-1}.Distinct())\r\n                {\r\n                    var expectedWorld=new Pose(expectedClip.Frames[frame]).ToWorld(target.Skeleton);\r\n                    var actualWorld=new Pose(actualClip.Frames[frame]).ToWorld(compiledRig);\r\n                    foreach(var bone in checkedBones)\r\n                    {\r\n                        var index=compiledRig.IndexOf(target.Skeleton[bone].Name);\r\n                        if(index<0 || Vec.Distance(expectedWorld[bone].Pos,actualWorld[index].Pos)>.1f\r\n                            || MathQ.AngleBetween(expectedWorld[bone].Rot,actualWorld[index].Rot)>.02f)\r\n                            throw new InvalidOperationException($\"Compiled sequence '{expectedClip.Name}' differs from the preview at '{target.Skeleton[bone].Name}', frame {frame}. The export was rolled back.\");\r\n                    }\r\n                }\r\n            }\r\n            return true;\r\n        },backup,cancel,generatedAssets);\r\n        return new(outputModel,prepared.Changes.Concat(bundles.Select(b=>b.LiveCalibration is not null\r\n            ? $\"Preserved complete source graph {b.GraphPath} with live custom-hand retargeting: {b.PrefabPath}\"\r\n            : $\"Created weapon graph ({string.Join(\", \",b.Actions.Select(a=>a.Action))}) and synchronized hands/weapon prefab: {b.PrefabPath}\")).ToArray(),committed.BackupPath,bundles.Select(b=>b.PrefabPath).ToArray());\r\n    }\r\n\r\n    private static float ExportPositionFactor(string text)\r\n    {\r\n        float scale=1;\r\n        void Walk(KvValue v) { if(v is KvObject o) { if(o.GetString(\"_class\")==\"ModelModifier_ScaleAndMirror\" && o.GetOrNull(\"scale\") is KvDouble d) scale*=(float)d.Value; foreach(var key in o.Keys) Walk(o[key]); } else if(v is KvArray a) foreach(var item in a.Items) Walk(item); }\r\n        Walk(Kv3.Parse(text).Root);\r\n        if(!float.IsFinite(scale)||scale<=0) throw new InvalidOperationException(\"Target model has an unsupported scale modifier.\");\r\n        return 1f/(2.54f*scale);\r\n    }\r\n\r\n    private static Quat ExportRootRotation(string text)\r\n    {\r\n        var turn=false;\r\n        void Walk(KvValue value)\r\n        {\r\n            if(value is KvObject node)\r\n            {\r\n                if(node.GetString(\"_class\")==\"ModelModifier_ScaleAndMirror\"\r\n                    &&node.GetOrNull(\"mirror_x\") is KvBool {Value:true}\r\n                    &&node.GetOrNull(\"mirror_y\") is KvBool {Value:true}\r\n                    &&node.GetOrNull(\"mirror_z\") is not KvBool {Value:true})turn=!turn;\r\n                foreach(var key in node.Keys)Walk(node[key]);\r\n            }\r\n            else if(value is KvArray array)foreach(var child in array.Items)Walk(child);\r\n        }\r\n        Walk(Kv3.Parse(text).Root);\r\n        return turn?Quat.CreateFromAxisAngle(Vec.UnitZ,MathF.PI):Quat.Identity;\r\n    }\r\n\r\n    private static async Task<string> PrepareMeshAsync(string file, CancellationToken cancel,List<string> importNotes)\r\n    {\r\n        var bytes=File.ReadAllBytes(file);\r\n        var materials=await Task.Run(()=>FbxMaterialAssets.Inspect(file,bytes),cancel);\r\n        importNotes.AddRange(materials.Notes);\r\n        var hash=materials.Signature;\r\n        var folder=\"models/hand_retargeter/targets/\"+SafeName(System.IO.Path.GetFileNameWithoutExtension(file))+\"_\"+hash+\"_rig9\";\r\n        var mesh=folder+\"/hands.fbx\"; var model=folder+\"/hands.vmdl\";\r\n        var scene=await Task.Run(()=>FbxImporter.Import(bytes,new(){SampleFps=(float)FbxScene.Build(FbxTokenizer.Parse(bytes)).FrameRate}),cancel);\r\n        var embeddedClips=scene.Clips;\r\n        var repaired=await Task.Run(()=>\r\n        {\r\n            var result=FbxBindPoseFixer.TryFix(bytes,out var report);\r\n            return (Bytes:result,Report:report);\r\n        },cancel);\r\n        if(repaired.Bytes is {} repairedBytes)\r\n        {\r\n            var repairedScene=await Task.Run(()=>FbxImporter.Import(repairedBytes),cancel);\r\n            if(!scene.Skeleton.Bones.Select(b=>(b.Name,b.ParentIndex)).SequenceEqual(repairedScene.Skeleton.Bones.Select(b=>(b.Name,b.ParentIndex))))\r\n                throw new InvalidOperationException(\"Bind-pose repair changed the target hierarchy; the original FBX was not modified.\");\r\n            scene=repairedScene;bytes=repairedBytes;\r\n            importNotes.Add(\"Repaired the imported copy from its authored bind pose: \"+repaired.Report);\r\n        }\r\n        var meshNames=FbxSkinnedMeshes.ReadNames(bytes);\r\n        await MainThread();\r\n        var meshAbs=System.IO.Path.Combine(Assets,mesh); Directory.CreateDirectory(System.IO.Path.GetDirectoryName(meshAbs)!);\r\n        if(!File.Exists(meshAbs)) File.WriteAllBytes(meshAbs,bytes);\r\n        var modelAbs=System.IO.Path.Combine(Assets,model);\r\n        var remaps=HandMaterialImport.Write(materials,folder);\r\n        var isNew=!File.Exists(modelAbs);\r\n        if(isNew)\r\n        {\r\n            // Keep the full authored hierarchy before ModelDoc can cull unweighted\r\n            // ancestors. The audited importer preview uses this same bind-source technique.\r\n            string EngineName(string name){var suffix=name.LastIndexOf('#');return suffix<0?name:name[..suffix]+\"_duplicate\";}\r\n            var rig=Skel.Create(scene.Skeleton.Bones.Select(b=>new BoneDefinition(EngineName(b.Name),b.ParentIndex<0?null:EngineName(scene.Skeleton[b.ParentIndex].Name),b.RestLocal)).ToArray());\r\n            var bindPath=folder+\"/mesh_bind.dmx\";\r\n            File.WriteAllText(System.IO.Path.Combine(Assets,bindPath),DmxWriter.Write(rig,new Clip(\"bindPose\",30,false,new(){Pose.Rest(rig).Locals}),new(){Name=\"bindPose\",UpAxisY=scene.UpAxis==1}));\r\n            var entries=new List<HandAnimationEntry>();\r\n            var names=new HashSet<string>(StringComparer.OrdinalIgnoreCase){\"bindPose\"};\r\n            for(var i=0;i<embeddedClips.Count;i++)\r\n            {\r\n                cancel.ThrowIfCancellationRequested();\r\n                var clip=embeddedClips[i];\r\n                var stem=SafeName(clip.Name).Replace('-','_');\r\n                var name=stem;var suffix=2;\r\n                while(!names.Add(name))name=stem+\"_\"+suffix++;\r\n                var path=folder+\"/embedded_\"+i+\".dmx\";\r\n                // Imported locals are already centimeters, matching mesh_bind and the\r\n                // model's final unit conversion. Preserve authored takes without IK.\r\n                File.WriteAllText(System.IO.Path.Combine(Assets,path),DmxWriter.Write(rig,clip,new(){Name=name,UpAxisY=scene.UpAxis==1}));\r\n                AssetSystem.RegisterFile(System.IO.Path.Combine(Assets,path));\r\n                entries.Add(new(name,path,clip.Looping));\r\n            }\r\n            var prepared=VmdlSetupService.Prepare(HandModelFactory.Create(mesh:mesh,meshUnitScaleCm:scene.UnitScaleCm,materialRemaps:remaps,meshNames:meshNames),entries,new(){ModelPath=model,BindPoseSource=bindPath,AutoConfigureAnimGraph=false});\r\n            File.WriteAllText(modelAbs,prepared.VmdlText);\r\n            AssetSystem.RegisterFile(System.IO.Path.Combine(Assets,bindPath));\r\n        }\r\n        AssetSystem.RegisterFile(meshAbs);\r\n        if(!await CompileAsync(modelAbs,cancel)) throw new InvalidOperationException(\"The target FBX could not be compiled into a skinned model.\");\r\n        if(isNew)\r\n        {\r\n            await MainThread();\r\n            var compiledRig=ReadSkeleton(Model.Load(model));\r\n            var fit=HandTargetFit.Analyze(compiledRig,HandRigDetector.Detect(compiledRig));\r\n            if(fit.Scale!=1||fit.TurnAround)\r\n            {\r\n                File.WriteAllText(modelAbs,fit.Apply(File.ReadAllText(modelAbs)));\r\n                if(!await CompileAsync(modelAbs,cancel))throw new InvalidOperationException(\"The fitted FPS target could not be compiled.\");\r\n                // Resource compilation finishes before the loaded Model's bone cache\r\n                // refreshes. Never hand an old-size skeleton to a newly scaled mesh.\r\n                var deadline=DateTime.UtcNow.AddSeconds(15);\r\n                while(true)\r\n                {\r\n                    await Task.Delay(50,cancel);await MainThread();\r\n                    var loaded=ReadSkeleton(Model.Load(model));\r\n                    var ready=compiledRig.Bones.All(b=>\r\n                    {\r\n                        var index=loaded.IndexOf(b.Name);if(index<0)return false;\r\n                        var expected=compiledRig.RestWorld[b.Index].Pos*fit.Scale;\r\n                        if(fit.TurnAround)expected=new Vec(-expected.X,-expected.Y,expected.Z);\r\n                        return Vec.Distance(loaded.RestWorld[index].Pos,expected)<.02f;\r\n                    });\r\n                    if(ready)break;\r\n                    if(DateTime.UtcNow>=deadline)throw new InvalidOperationException(\"The fitted model is still reloading. Select the FBX again after asset compilation finishes.\");\r\n                }\r\n                if(fit.Scale!=1)importNotes.Add($\"Applied FPS import scale {fit.Scale:G} to the mesh and all embedded animations to correct the oversized or undersized export.\");\r\n                if(fit.TurnAround)importNotes.Add(\"Turned the imported target 180 degrees to align its left and right shoulders with FPS weapon coordinates.\");\r\n            }\r\n        }\r\n        await MainThread(); return model;\r\n    }\r\n\r\n    public static async Task<bool> CompileAsync(string absolute, CancellationToken cancel)\r\n    {\r\n        await MainThread(); var asset=AssetSystem.RegisterFile(absolute);\r\n        var compiled=absolute+\"_c\"; var before=File.Exists(compiled)?File.GetLastWriteTimeUtc(compiled):DateTime.MinValue;\r\n        asset.Compile(full:true);\r\n        var deadline=DateTime.UtcNow.AddSeconds(90);\r\n        while(DateTime.UtcNow<deadline)\r\n        {\r\n            cancel.ThrowIfCancellationRequested(); await MainThread();\r\n            if(asset.IsCompileFailed) return false;\r\n            if(File.Exists(compiled) && File.GetLastWriteTimeUtc(compiled)>before) return true;\r\n            await Task.Delay(100,cancel);\r\n        }\r\n        return false;\r\n    }\r\n\r\n    public static bool HasMissingMaterials(Model model)\r\n    {\r\n        var materials=model.Materials.Concat(Enumerable.Range(0,model.MaterialGroupCount).SelectMany(group=>model.GetMaterials(group))).Distinct();\r\n        foreach(var material in materials)\r\n        {\r\n            if(material is null||material.IsError||string.Equals(material.ResourcePath,\"materials/error.vmat\",StringComparison.OrdinalIgnoreCase))return true;\r\n            foreach(var parameter in new[]{\"g_tColor\",\"g_tNormal\",\"g_tRoughness\",\"g_tMetalness\",\"g_tAmbientOcclusion\",\"g_tSelfIllumMask\",\"g_tTranslucency\",\"TextureColor\",\"TextureNormal\",\"TextureRoughness\",\"TextureMetalness\",\"TextureAmbientOcclusion\",\"TextureSelfIllumMask\",\"TextureTranslucency\"})\r\n                if(material.GetTexture(parameter) is {IsError:true})return true;\r\n            if(material.FirstTexture is {IsError:true})return true;\r\n        }\r\n        return false;\r\n    }\r\n\r\n    public static void ApplyPreviewMaterialFallback(SceneModel model)\r\n    {\r\n        // Incomplete visual dependencies must not prevent skeletal retargeting.\r\n        // Override this preview instance only; authored assets remain editable.\r\n        if(HasMissingMaterials(model.Model))model.SetMaterialOverride(Material.Load(\"materials/dev/reflectivity_50.vmat\"));\r\n    }\r\n    public static string ModelPath(string path)\r\n    {\r\n        if(path.EndsWith(\".vmdl_c\",StringComparison.OrdinalIgnoreCase))path=path[..^2];\r\n        if(System.IO.Path.IsPathRooted(path)) path=System.IO.Path.GetRelativePath(Assets,path);\r\n        return VmdlSetupService.NormalizeAssetPath(path,\".vmdl\");\r\n    }\r\n    public static string SafeName(string name) => string.Concat(name.Select(c=>char.IsLetterOrDigit(c)||c=='_'||c=='-'?c:'_')).Trim('_') is {Length:>0} result ? result : \"animation\";\r\n    internal static string ContentKey(string text)=>Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(text))).ToLowerInvariant()[..16];\r\n    internal static string SourceKey(HandSource source)=>SafeName(System.IO.Path.GetFileNameWithoutExtension(source.Path))+\"_\"+ContentKey(source.Path.Replace('\\\\','/').ToLowerInvariant())[..8];\r\n\r\n    private sealed class ModelSampler : IDisposable\r\n    {\r\n        readonly Scene metadataScene = Scene.CreateEditorScene(); readonly SkinnedModelRenderer metadata;\r\n        readonly SceneWorld world=new(); readonly Model asset; readonly Skel skeleton; readonly int[] bones;\r\n        public ModelSampler(Model asset, Skel rig) { using(metadataScene.Push()) { metadata=new GameObject(true,\"sequence metadata\").GetOrAddComponent<SkinnedModelRenderer>(); metadata.Model=asset; metadata.UseAnimGraph=false; } this.asset=asset; skeleton=rig; bones=rig.Bones.Select(b=>asset.Bones.GetBone(b.Name).Index).ToArray(); }\r\n        public Clip Sample(string name,float fps,CancellationToken cancel=default)\r\n        {\r\n            var model=new SceneModel(world,asset,Transform.Zero){UseAnimGraph=false};\r\n            try {\r\n            model.SetAnimGraph(\"\");model.UseAnimGraph=false;\r\n            model.CurrentSequence.Name=name; metadata.Sequence.Name=name;\r\n            var duration=model.CurrentSequence.Duration;\r\n            var count=Math.Max(1,(int)MathF.Round(duration*fps)+1);\r\n            if(count>100000) throw new InvalidOperationException($\"Sequence '{name}' is too long to sample.\");\r\n            var frames=new List<XForm[]>(count);\r\n            for(var f=0;f<count;f++) {\r\n                cancel.ThrowIfCancellationRequested();\r\n                model.CurrentSequence.Time=MathF.Min(f/fps,duration); model.Update(0);\r\n                var worlds=bones.Select(b=>FromEngine(model.GetBoneWorldTransform(b))).ToArray();\r\n                frames.Add(skeleton.Bones.Select(b=>b.ParentIndex<0?worlds[b.Index]:XForm.ToLocal(worlds[b.ParentIndex],worlds[b.Index])).ToArray());\r\n            }\r\n            return new(name,fps,metadata.Sequence.Looping,frames);\r\n            } finally {model.Delete();}\r\n        }\r\n        public Clip? SampleFire(float duration,float fps,CancellationToken cancel)\r\n        {\r\n            var model=new SceneModel(world,asset,Transform.Zero){UseAnimGraph=true};\r\n            try\r\n            {\r\n                if(model.AnimationGraph is null||model.AnimationGraph.IsError)return null;\r\n                model.SetAnimParameter(\"skeleton\",0);model.SetAnimParameter(\"b_deploy_skip\",true);\r\n                for(var i=0;i<120;i++){cancel.ThrowIfCancellationRequested();model.Update(1f/60);}\r\n                model.SetAnimParameter(\"b_deploy_skip\",false);\r\n                var frames=new List<XForm[]>();\r\n                for(var f=0;f<=Math.Max(1,(int)MathF.Ceiling(duration*fps));f++)\r\n                {\r\n                    cancel.ThrowIfCancellationRequested();model.SetAnimParameter(\"b_attack\",f==0);model.Update(f==0?0:1f/fps);\r\n                    var worlds=bones.Select(b=>FromEngine(model.GetBoneWorldTransform(b))).ToArray();\r\n                    frames.Add(skeleton.Bones.Select(b=>b.ParentIndex<0?worlds[b.Index]:XForm.ToLocal(worlds[b.ParentIndex],worlds[b.Index])).ToArray());\r\n                }\r\n                return new Clip(\"Fire\",fps,false,frames);\r\n            }\r\n            finally{model.Delete();}\r\n        }\r\n        public void Dispose() {world.Delete();metadataScene.Destroy();}\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Core/Mapping/HandRigDefinition.cs",
            "FileName": "HandRigDefinition.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "#nullable enable\r\nnamespace HumanoidHandRetargeter.Mapping;\r\n\r\npublic enum HandSide { Left, Right }\r\npublic enum DigitRole { Thumb, Index, Middle, Ring, Pinky, Extra }\r\n\r\n/// <summary>Ordered anatomical joints; optional palm and terminal bones are explicit.</summary>\r\npublic sealed class DigitChain\r\n{\r\n    public DigitRole Role { get; }\r\n    public string ExtraSlot { get; }\r\n    public IReadOnlyList<int> Segments { get; }\r\n    public int? Metacarpal { get; }\r\n    public int? Tip { get; }\r\n\r\n    public DigitChain(DigitRole role, IEnumerable<int> segments,\r\n        int? metacarpal = null, int? tip = null, string extraSlot = \"\")\r\n    {\r\n        ArgumentNullException.ThrowIfNull(segments);\r\n        Role = role;\r\n        Segments = Array.AsReadOnly(segments.ToArray());\r\n        Metacarpal = metacarpal;\r\n        Tip = tip;\r\n        ExtraSlot = extraSlot ?? \"\";\r\n    }\r\n\r\n    public IEnumerable<int> Bones\r\n    {\r\n        get\r\n        {\r\n            if (Metacarpal is int palm) yield return palm;\r\n            foreach (var segment in Segments) yield return segment;\r\n            if (Tip is int tip) yield return tip;\r\n        }\r\n    }\r\n}\r\n\r\n/// <summary>A single hand; rigs with only one side do not need a placeholder other hand.</summary>\r\npublic sealed class HandRigDefinition\r\n{\r\n    public HandSide Side { get; }\r\n    public int Wrist { get; }\r\n    public int? Clavicle { get; }\r\n    public int? UpperArm { get; }\r\n    public int? Forearm { get; }\r\n    public IReadOnlyList<DigitChain> Digits { get; }\r\n    public IReadOnlyList<int> TwistOrHelperBones { get; }\r\n\r\n    public HandRigDefinition(HandSide side, int wrist, IEnumerable<DigitChain> digits,\r\n        int? clavicle = null, int? upperArm = null, int? forearm = null,\r\n        IEnumerable<int>? twistOrHelperBones = null)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(digits);\r\n        Side = side;\r\n        Wrist = wrist;\r\n        Clavicle = clavicle;\r\n        UpperArm = upperArm;\r\n        Forearm = forearm;\r\n        Digits = Array.AsReadOnly(digits.ToArray());\r\n        TwistOrHelperBones = Array.AsReadOnly((twistOrHelperBones ?? Array.Empty<int>()).ToArray());\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Core/Retargeting/HandMotionOptions.cs",
            "FileName": "HandMotionOptions.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "#nullable enable\r\nnamespace HumanoidHandRetargeter.Retargeting;\r\n\r\npublic sealed class HandMotionOptions\r\n{\r\n    public bool TransferWristPosition { get; init; } = true;\r\n    public bool ScaleWristTravel { get; init; } = true;\r\n    /// <summary>Known source-to-target model axes, independent of the wrists' rest rotations.</summary>\r\n    public System.Numerics.Quaternion? WristTravelBasis {get;init;}\r\n    public bool SolveArmIk { get; init; } = true;\r\n    public bool PreserveWeaponGrip { get; init; } = true;\r\n    /// <summary>Shared source weapon-to-target translation in centimeters. Grip motion is never scaled per hand.</summary>\r\n    public System.Numerics.Vector3? WeaponSpaceOffset {get;init;}\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Core/Retargeting/HandRetargeter.cs",
            "FileName": "HandRetargeter.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "#nullable enable\r\nusing System.Numerics;\r\nusing HumanoidHandRetargeter.Calibration;\r\nusing HumanoidHandRetargeter.Formats;\r\nusing HumanoidHandRetargeter.Maths;\r\nusing HumanoidHandRetargeter.Skeleton;\r\nusing HumanoidHandRetargeter.Validation;\r\n\r\nnamespace HumanoidHandRetargeter.Retargeting;\r\nusing Vector3 = System.Numerics.Vector3;\r\n\r\n/// <summary>One offline solve path for static poses and baked clips. Target translations\r\n/// and unmapped local transforms stay authored; helper constraints remain target-owned.</summary>\r\npublic static class HandRetargeter\r\n{\r\n    /// <summary>One continuous graph stream, retaining digit angle history between evaluated frames.</summary>\r\n    public sealed class PoseStream\r\n    {\r\n        private readonly HandRetargetProfile profile;\r\n        private readonly HandMotionOptions options;\r\n        private readonly AngleHistory history;\r\n        public PoseStream(HandRetargetProfile profile, HandMotionOptions options)\r\n        { this.profile = profile; this.options = options; history = new(profile); }\r\n        public Pose Step(Pose source)\r\n        {\r\n            var pose = Solve(profile, source, history);\r\n            if (options.TransferWristPosition) ApplyWristMotion(profile, source, pose, options);\r\n            var sourceWorld = source.ToWorld(profile.Source);\r\n            foreach (var pair in profile.PreservedTracks)\r\n            {\r\n                var targetWorld = pose.ToWorld(profile.Target);\r\n                var parent = profile.Target[pair.Target].ParentIndex;\r\n                pose.Locals[pair.Target] = parent < 0 ? sourceWorld[pair.Source] : XForm.ToLocal(targetWorld[parent], sourceWorld[pair.Source]);\r\n            }\r\n            return pose;\r\n        }\r\n    }\r\n\r\n    public static Pose RetargetPose(HandRetargetProfile profile, Pose sourcePose)\r\n        => Solve(profile, sourcePose, null);\r\n\r\n    /// <summary>Retains timing and fixes quaternion signs. Stateful angle unwrapping is\r\n    /// local to this clip, so a calibrated profile may safely be shared by concurrent jobs.</summary>\r\n    public static Clip RetargetClip(HandRetargetProfile profile, Clip sourceClip, CancellationToken cancellationToken = default)\r\n        => Bake(profile, sourceClip, new HandMotionOptions { TransferWristPosition = false }, cancellationToken);\r\n\r\n    /// <summary>The shared editor preview/export bake including optional wrist travel and arm IK.</summary>\r\n    public static Clip Bake(HandRetargetProfile profile, Clip sourceClip, HandMotionOptions options, CancellationToken cancellationToken = default)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(profile);\r\n        ArgumentNullException.ThrowIfNull(sourceClip);\r\n        ArgumentNullException.ThrowIfNull(options);\r\n        if (sourceClip.FrameCount == 0)\r\n            throw new RigValidationException(new[] { new RigIssue(\"empty-clip\", \"Source animation has no frames.\") });\r\n        var frames = new List<XForm[]>(sourceClip.FrameCount);\r\n        var continuity = new AngleHistory(profile);\r\n        foreach (var sourceFrame in sourceClip.Frames)\r\n        {\r\n            cancellationToken.ThrowIfCancellationRequested();\r\n            var pose = Solve(profile, new Pose(sourceFrame), continuity);\r\n            if (options.TransferWristPosition) ApplyWristMotion(profile, new Pose(sourceFrame), pose, options);\r\n            if(profile.PreservedTracks.Length>0)\r\n            {\r\n                var sourceWorld=new Pose(sourceFrame).ToWorld(profile.Source);\r\n                foreach(var pair in profile.PreservedTracks)\r\n                {\r\n                    var targetWorld=pose.ToWorld(profile.Target);var parent=profile.Target[pair.Target].ParentIndex;\r\n                    pose.Locals[pair.Target]=parent<0?sourceWorld[pair.Source]:XForm.ToLocal(targetWorld[parent],sourceWorld[pair.Source]);\r\n                }\r\n            }\r\n            frames.Add(pose.Locals);\r\n        }\r\n        QuaternionContinuity.AlignFrames(frames);\r\n        return new Clip(sourceClip.Name, sourceClip.Fps, sourceClip.Looping, frames, sourceClip.NativeFps);\r\n    }\r\n\r\n    private static void ApplyWristMotion(HandRetargetProfile profile, Pose source, Pose target, HandMotionOptions options)\r\n    {\r\n        if(options.WeaponSpaceOffset.HasValue&&options.SolveArmIk)\r\n            foreach(var shoulder in profile.FpsShoulders)target.Locals[shoulder.Key].Pos=shoulder.Value;\r\n        var sourceWorld = source.ToWorld(profile.Source);\r\n        foreach (var pair in profile.WristMotion)\r\n        {\r\n            var world = target.ToWorld(profile.Target);\r\n            var travel = Vector3.Transform(sourceWorld[pair.Source].Pos - profile.Source.RestWorld[pair.Source].Pos, options.WristTravelBasis ?? pair.Basis);\r\n            var desired = profile.Target.RestWorld[pair.Target].Pos + travel * (options.ScaleWristTravel ? pair.Scale : 1f);\r\n            var wristRotation = world[pair.Target].Rot;\r\n            if(options.WeaponSpaceOffset is {} weaponOffset)\r\n            {\r\n                wristRotation=MathQ.Normalize(sourceWorld[pair.Source].Rot*pair.GripRotationOffset);\r\n                desired=sourceWorld[pair.Source].Pos+weaponOffset\r\n                    +Vector3.Transform(pair.SourceGripLocal,sourceWorld[pair.Source].Rot)\r\n                    -Vector3.Transform(pair.TargetGripLocal,wristRotation);\r\n            }\r\n            if (options.SolveArmIk && pair.UpperArm is int upper && pair.Forearm is int lower)\r\n            {\r\n                if(options.WeaponSpaceOffset.HasValue)\r\n                {\r\n                    // Detached FPS arms can move their open shoulder ends. Avoid\r\n                    // folding long/thick forearms back through their own upper arm\r\n                    // when the weapon grip lies very close to the shoulder.\r\n                    if(profile.Target[upper].ParentIndex<0)\r\n                    {\r\n                        var shoulder=HandViewSpace.ClearElbowFold(world[upper].Pos,world[lower].Pos,world[pair.Target].Pos,desired);\r\n                        target.Locals[upper].Pos=shoulder;\r\n                        world=target.ToWorld(profile.Target);\r\n                    }\r\n                    // FPS arm meshes have free shoulder ends. Move the shoulder enough\r\n                    // to reach the fixed weapon grip instead of stretching either arm segment.\r\n                    var reach=Vector3.Distance(world[upper].Pos,world[lower].Pos)+Vector3.Distance(world[lower].Pos,world[pair.Target].Pos);\r\n                    var toGrip=desired-world[upper].Pos;var distance=toGrip.Length();\r\n                    if(distance>reach*.98f&&distance>1e-5f)\r\n                    {\r\n                        var shoulder=world[upper].Pos+toGrip/distance*(distance-reach*.98f);\r\n                        var parent=profile.Target[upper].ParentIndex;\r\n                        target.Locals[upper].Pos=parent<0?shoulder:Vector3.Transform(shoulder-world[parent].Pos,Quaternion.Conjugate(world[parent].Rot));\r\n                        world=target.ToWorld(profile.Target);\r\n                    }\r\n                }\r\n                var correction = TwoBoneIk.Solve(world[upper].Pos, world[lower].Pos, world[pair.Target].Pos, desired, 0, pair.BendAxis);\r\n                void SetWorldRotation(int bone, Quaternion rotation)\r\n                {\r\n                    var parent = profile.Target[bone].ParentIndex;\r\n                    target.Locals[bone].Rot = MathQ.Normalize(parent < 0 ? rotation : Quaternion.Conjugate(world[parent].Rot) * rotation);\r\n                    world = target.ToWorld(profile.Target);\r\n                }\r\n                var oldLower = world[lower].Rot;\r\n                SetWorldRotation(upper, correction.UpperWorldDelta * world[upper].Rot);\r\n                SetWorldRotation(lower, correction.LowerWorldDelta * oldLower);\r\n                if(options.WeaponSpaceOffset.HasValue && pair.SourceForearm is int sourceElbow && pair.SourceUpperArm is int sourceShoulder)\r\n                {\r\n                    // Transfer the source bend plane between the two shoulder-to-wrist axes.\r\n                    // An absolute source elbow position can lie on the target arm's axis\r\n                    // when proportions differ, flipping its pole as the weapon recoils.\r\n                    var axis=world[pair.Target].Pos-world[upper].Pos;\r\n                    var sourceAxis=sourceWorld[pair.Source].Pos-sourceWorld[sourceShoulder].Pos;\r\n                    if(axis.LengthSquared()>1e-8f&&sourceAxis.LengthSquared()>1e-8f)\r\n                    {\r\n                        axis=Vector3.Normalize(axis);\r\n                        sourceAxis=Vector3.Normalize(sourceAxis);\r\n                        var actual=world[lower].Pos-world[upper].Pos;\r\n                        var wanted=sourceWorld[sourceElbow].Pos-sourceWorld[sourceShoulder].Pos;\r\n                        wanted-=sourceAxis*Vector3.Dot(wanted,sourceAxis);\r\n                        wanted=Vector3.Transform(wanted,MathQ.FromTo(sourceAxis,axis));\r\n                        actual-=axis*Vector3.Dot(actual,axis);\r\n                        // A straight arm has no defined pole: retain the preceding IK plane.\r\n                        if(actual.LengthSquared()>1e-6f&&wanted.LengthSquared()>1e-6f)\r\n                        {\r\n                            actual=Vector3.Normalize(actual);wanted=Vector3.Normalize(wanted);\r\n                            var angle=MathF.Atan2(Vector3.Dot(axis,Vector3.Cross(actual,wanted)),Vector3.Dot(actual,wanted));\r\n                            var pole=Quaternion.CreateFromAxisAngle(axis,angle);\r\n                            oldLower=world[lower].Rot;\r\n                            SetWorldRotation(upper,pole*world[upper].Rot);\r\n                            SetWorldRotation(lower,pole*oldLower);\r\n                        }\r\n                    }\r\n                }\r\n                if(options.WeaponSpaceOffset.HasValue&&!pair.HasTwistHelpers)\r\n                {\r\n                    // Without authored twist helpers, put axial pronation in the forearm\r\n                    // instead of winding the wrist through the fixed palm orientation.\r\n                    // Rotating about elbow-to-wrist leaves the grip position unchanged.\r\n                    var axis=world[pair.Target].Pos-world[lower].Pos;\r\n                    if(axis.LengthSquared()>1e-8f)\r\n                    {\r\n                        var restRelative=MathQ.Normalize(Quaternion.Conjugate(profile.Target.RestWorld[lower].Rot)*profile.Target.RestWorld[pair.Target].Rot);\r\n                        var relaxedLower=MathQ.Normalize(wristRotation*Quaternion.Conjugate(restRelative));\r\n                        var delta=MathQ.Normalize(relaxedLower*Quaternion.Conjugate(world[lower].Rot));\r\n                        MathQ.SwingTwist(delta,Vector3.Normalize(axis),out _,out var roll);\r\n                        SetWorldRotation(lower,roll*world[lower].Rot);\r\n                    }\r\n                }\r\n                SetWorldRotation(pair.Target, wristRotation);\r\n                if(options.WeaponSpaceOffset.HasValue&&Vector3.Distance(world[pair.Target].Pos,desired)>.1f)\r\n                    throw new InvalidOperationException(\"The target arm cannot reach the weapon grip without stretching. Adjust the rig's shoulder placement or disable Preserve weapon grip.\");\r\n            }\r\n            else\r\n            {\r\n                // Partial rigs cannot reach with a two-link chain. Preserve the authored\r\n                // wrist path by translating the wrist relative to its actual parent.\r\n                var parent = profile.Target[pair.Target].ParentIndex;\r\n                target.Locals[pair.Target].Pos = parent < 0 ? desired\r\n                    : Vector3.Transform(desired - world[parent].Pos, Quaternion.Conjugate(world[parent].Rot));\r\n                target.Locals[pair.Target].Rot=MathQ.Normalize(parent<0?wristRotation:Quaternion.Conjugate(world[parent].Rot)*wristRotation);\r\n            }\r\n        }\r\n        var errors = PoseValidator.Validate(profile.Target, target);\r\n        if (errors.Count > 0) throw new RigValidationException(errors);\r\n    }\r\n\r\n    private static Pose Solve(HandRetargetProfile profile, Pose sourcePose, AngleHistory? history)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(profile);\r\n        var errors = PoseValidator.Validate(profile.Source, sourcePose);\r\n        if (errors.Count > 0) throw new RigValidationException(errors);\r\n        var sourceWorld = sourcePose.ToWorld(profile.Source);\r\n        errors = PoseValidator.Validate(profile.Source, new Pose(sourceWorld));\r\n        if (errors.Count > 0) throw new RigValidationException(errors);\r\n        var sourceDeltas = new Quaternion[sourceWorld.Length];\r\n        for (var i = 0; i < sourceWorld.Length; i++)\r\n            sourceDeltas[i] = MathQ.Normalize(sourceWorld[i].Rot * Quaternion.Conjugate(profile.Source.RestWorld[i].Rot));\r\n        var targetDeltas = Enumerable.Repeat(Quaternion.Identity, profile.Target.Count).ToArray();\r\n        var solved = new bool[profile.Target.Count];\r\n        foreach (var pair in profile.Pairs)\r\n        {\r\n            var parentSource = pair.SourceParent >= 0 ? sourceDeltas[pair.SourceParent] : Quaternion.Identity;\r\n            var parentTarget = pair.TargetParent >= 0 ? targetDeltas[pair.TargetParent] : Quaternion.Identity;\r\n            var canonical = MathQ.Normalize(Quaternion.Conjugate(pair.SourceFrame) * Quaternion.Conjugate(parentSource)\r\n                * sourceDeltas[pair.Source] * pair.SourceFrame);\r\n            targetDeltas[pair.Target] = MathQ.Normalize(parentTarget * pair.TargetFrame * canonical * Quaternion.Conjugate(pair.TargetFrame));\r\n            solved[pair.Target] = true;\r\n        }\r\n        for (var d = 0; d < profile.Distributions.Length; d++)\r\n        {\r\n            var digit = profile.Distributions[d];\r\n            var curls = new float[digit.Source.Length];\r\n            var spread = 0f;\r\n            var previous = sourceDeltas[digit.SourceWrist];\r\n            for (var s = 0; s < digit.Source.Length; s++)\r\n            {\r\n                var frame = digit.SourceFrames[s];\r\n                var delta = sourceDeltas[digit.Source[s]];\r\n                var canonical = MathQ.Normalize(Quaternion.Conjugate(frame) * Quaternion.Conjugate(previous) * delta * frame);\r\n                // Legacy FingerSolver decomposition: canonical Y curl, Z splay, discard X twist when redistributing.\r\n                MathQ.SwingTwist(canonical, Vector3.UnitY, out var swing, out var curl);\r\n                curls[s] = SignedAngle(curl, Vector3.UnitY);\r\n                if (history is not null) curls[s] = history.Unwrap(d, s, curls[s], false);\r\n                if (s <= digit.ProximalSourceIndex)\r\n                {\r\n                    MathQ.SwingTwist(swing, Vector3.UnitZ, out _, out var splay);\r\n                    var angle = SignedAngle(splay, Vector3.UnitZ);\r\n                    if (history is not null) angle = history.Unwrap(d, s, angle, true);\r\n                    spread += angle;\r\n                }\r\n                previous = delta;\r\n            }\r\n            var accumulated = targetDeltas[digit.TargetWrist];\r\n            for (var t = 0; t < digit.Target.Length; t++)\r\n            {\r\n                var angle = 0f;\r\n                for (var s = 0; s < curls.Length; s++) angle += digit.Weights[t, s] * curls[s];\r\n                var motion = Quaternion.CreateFromAxisAngle(Vector3.UnitY, angle);\r\n                if (t == 0) motion = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, spread) * motion;\r\n                var frame = digit.TargetFrames[t];\r\n                accumulated = MathQ.Normalize(accumulated * frame * motion * Quaternion.Conjugate(frame));\r\n                targetDeltas[digit.Target[t]] = accumulated;\r\n                solved[digit.Target[t]] = true;\r\n            }\r\n        }\r\n\r\n        var targetPose = Pose.Rest(profile.Target);\r\n        var targetWorld = new XForm[profile.Target.Count];\r\n        for (var i = 0; i < targetWorld.Length; i++)\r\n        {\r\n            var parent = profile.Target[i].ParentIndex;\r\n            if (solved[i])\r\n            {\r\n                var desiredWorld = targetDeltas[i] * profile.Target.RestWorld[i].Rot;\r\n                targetPose.Locals[i].Rot = MathQ.Normalize(parent < 0 ? desiredWorld : Quaternion.Conjugate(targetWorld[parent].Rot) * desiredWorld);\r\n            }\r\n            targetWorld[i] = parent < 0 ? targetPose.Locals[i] : XForm.Compose(targetWorld[parent], targetPose.Locals[i]);\r\n        }\r\n        errors = PoseValidator.Validate(profile.Target, targetPose);\r\n        if (errors.Count > 0) throw new RigValidationException(errors);\r\n        errors = PoseValidator.Validate(profile.Target, new Pose(targetWorld));\r\n        if (errors.Count > 0) throw new RigValidationException(errors);\r\n        return targetPose;\r\n    }\r\n\r\n    // Same signed twist convention as the legacy FingerSolver.\r\n    private static float SignedAngle(Quaternion twist, Vector3 axis)\r\n    {\r\n        var angle = 2f * MathF.Atan2(twist.X * axis.X + twist.Y * axis.Y + twist.Z * axis.Z, twist.W);\r\n        if (angle > MathF.PI) angle -= 2f * MathF.PI;\r\n        if (angle < -MathF.PI) angle += 2f * MathF.PI;\r\n        return angle;\r\n    }\r\n\r\n    private sealed class AngleHistory\r\n    {\r\n        private readonly float[][] _curl;\r\n        private readonly float[][] _spread;\r\n        public AngleHistory(HandRetargetProfile profile)\r\n        {\r\n            _curl = profile.Distributions.Select(d => Enumerable.Repeat(float.NaN, d.Source.Length).ToArray()).ToArray();\r\n            _spread = profile.Distributions.Select(d => Enumerable.Repeat(float.NaN, d.Source.Length).ToArray()).ToArray();\r\n        }\r\n        public float Unwrap(int digit, int joint, float angle, bool spread)\r\n        {\r\n            var values = spread ? _spread : _curl;\r\n            var previous = values[digit][joint];\r\n            if (float.IsFinite(previous))\r\n                angle += 2f * MathF.PI * MathF.Round((previous - angle) / (2f * MathF.PI));\r\n            values[digit][joint] = angle;\r\n            return angle;\r\n        }\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Core/Skeleton/Skeleton.cs",
            "FileName": "Skeleton.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "// Adapted from humanoid-retargeter 26084c96c3fc870aaf9a5bd798de063ce2fd62df; cached hierarchy queries added.\r\n#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing HumanoidHandRetargeter.Maths;\r\n\r\nnamespace HumanoidHandRetargeter.Skeleton;\r\n\r\n/// <summary>\r\n/// Input definition for a single bone, used to build a <see cref=\"Skeleton\"/>.\r\n/// Order does not matter; construction topologically sorts parents before children.\r\n/// </summary>\r\n/// <param name=\"Name\">Unique bone name.</param>\r\n/// <param name=\"ParentName\">Parent bone name, or null for a root bone (multiple roots allowed).</param>\r\n/// <param name=\"RestLocal\">Rest (bind) transform relative to the parent bone, centimeters.</param>\r\npublic readonly record struct BoneDefinition(string Name, string? ParentName, XForm RestLocal);\r\n\r\n/// <summary>One bone of an immutable <see cref=\"Skeleton\"/>.</summary>\r\npublic readonly struct Bone\r\n{\r\n    /// <summary>Index of this bone in the skeleton (parents always have a smaller index).</summary>\r\n    public int Index { get; }\r\n\r\n    /// <summary>Unique bone name.</summary>\r\n    public string Name { get; }\r\n\r\n    /// <summary>Index of the parent bone, or -1 for a root bone.</summary>\r\n    public int ParentIndex { get; }\r\n\r\n    /// <summary>Rest (bind) transform relative to the parent bone (or armature space for roots).</summary>\r\n    public XForm RestLocal { get; }\r\n\r\n    internal Bone(int index, string name, int parentIndex, XForm restLocal)\r\n    {\r\n        Index = index;\r\n        Name = name;\r\n        ParentIndex = parentIndex;\r\n        RestLocal = restLocal;\r\n    }\r\n\r\n    /// <inheritdoc />\r\n    public override string ToString() => $\"[{Index}] {Name} (parent {ParentIndex})\";\r\n}\r\n\r\n/// <summary>\r\n/// Immutable bone hierarchy with rest pose. Bones are stored topologically sorted\r\n/// (every bone's parent precedes it), which lets forward kinematics run as a single\r\n/// in-order pass. Supports multiple roots (e.g. the s&amp;box rig has both\r\n/// <c>root_IK</c> and <c>pelvis</c> as parentless bones).\r\n/// </summary>\r\npublic sealed class Skeleton\r\n{\r\n    private readonly Bone[] _bones;\r\n    private readonly XForm[] _restWorld;\r\n    private readonly Dictionary<string, int> _indexByName;\r\n    private readonly IReadOnlyList<int>[] _children;\r\n\r\n    private Skeleton(Bone[] bones, XForm[] restWorld, Dictionary<string, int> indexByName)\r\n    {\r\n        _bones = bones;\r\n        _restWorld = restWorld;\r\n        _indexByName = indexByName;\r\n        var children = Enumerable.Range(0, bones.Length).Select(_ => new List<int>()).ToArray();\r\n        foreach (var bone in bones)\r\n            if (bone.ParentIndex >= 0) children[bone.ParentIndex].Add(bone.Index);\r\n        _children = children.Select(c => (IReadOnlyList<int>)c.AsReadOnly()).ToArray();\r\n    }\r\n\r\n    /// <summary>Number of bones.</summary>\r\n    public int Count => _bones.Length;\r\n\r\n    /// <summary>All bones, topologically sorted (parents before children).</summary>\r\n    public IReadOnlyList<Bone> Bones => _bones;\r\n\r\n    /// <summary>Rest (bind) world transforms, indexed like <see cref=\"Bones\"/>.</summary>\r\n    public IReadOnlyList<XForm> RestWorld => _restWorld;\r\n\r\n    /// <summary>Bone name to index lookup.</summary>\r\n    public IReadOnlyDictionary<string, int> IndexByName => _indexByName;\r\n\r\n    /// <summary>The bone at <paramref name=\"index\"/>.</summary>\r\n    public Bone this[int index] => _bones[index];\r\n\r\n    /// <summary>Returns the index of the named bone, or -1 when absent.</summary>\r\n    public int IndexOf(string name) => _indexByName.TryGetValue(name, out var index) ? index : -1;\r\n\r\n    /// <summary>Cached immediate children in stable skeleton order.</summary>\r\n    public IReadOnlyList<int> ChildrenOf(int bone) => _children[bone];\r\n\r\n    /// <summary>Whether child is strictly below ancestor; a bone is not its own descendant.</summary>\r\n    public bool DescendsFrom(int child, int ancestor)\r\n    {\r\n        if (child < 0 || child >= Count) throw new ArgumentOutOfRangeException(nameof(child));\r\n        if (ancestor < 0 || ancestor >= Count) throw new ArgumentOutOfRangeException(nameof(ancestor));\r\n        for (var parent = _bones[child].ParentIndex; parent >= 0; parent = _bones[parent].ParentIndex)\r\n            if (parent == ancestor) return true;\r\n        return false;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Builds a skeleton from bone definitions in any order: validates names and parent links,\r\n    /// topologically sorts (stable \u2014 input order is preserved among bones whose parents are\r\n    /// already placed), and computes rest world transforms.\r\n    /// </summary>\r\n    /// <exception cref=\"ArgumentException\">\r\n    /// Thrown on duplicate bone names, unknown parent names, or parent cycles.\r\n    /// </exception>\r\n    public static Skeleton Create(IReadOnlyList<BoneDefinition> definitions)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(definitions);\r\n\r\n        var count = definitions.Count;\r\n        var originalIndexByName = new Dictionary<string, int>(count, StringComparer.Ordinal);\r\n        for (var i = 0; i < count; i++)\r\n        {\r\n            var name = definitions[i].Name;\r\n            if (string.IsNullOrEmpty(name))\r\n                throw new ArgumentException($\"Bone at position {i} has an empty name.\", nameof(definitions));\r\n            if (!originalIndexByName.TryAdd(name, i))\r\n                throw new ArgumentException($\"Duplicate bone name '{name}'.\", nameof(definitions));\r\n        }\r\n\r\n        foreach (var def in definitions)\r\n        {\r\n            if (def.ParentName is not null && !originalIndexByName.ContainsKey(def.ParentName))\r\n                throw new ArgumentException(\r\n                    $\"Bone '{def.Name}' references unknown parent '{def.ParentName}'.\", nameof(definitions));\r\n            if (def.ParentName == def.Name)\r\n                throw new ArgumentException($\"Bone '{def.Name}' is its own parent.\", nameof(definitions));\r\n        }\r\n\r\n        // Stable topological sort: repeatedly place, in input order, every bone whose parent\r\n        // is already placed. O(n^2) worst case \u2014 fine for skeleton-sized inputs (~100 bones).\r\n        var newIndexByOriginal = new int[count];\r\n        Array.Fill(newIndexByOriginal, -1);\r\n        var order = new List<int>(count);\r\n        while (order.Count < count)\r\n        {\r\n            var progressed = false;\r\n            for (var i = 0; i < count; i++)\r\n            {\r\n                if (newIndexByOriginal[i] >= 0)\r\n                    continue;\r\n                var parentName = definitions[i].ParentName;\r\n                if (parentName is not null && newIndexByOriginal[originalIndexByName[parentName]] < 0)\r\n                    continue;\r\n                newIndexByOriginal[i] = order.Count;\r\n                order.Add(i);\r\n                progressed = true;\r\n            }\r\n            if (!progressed)\r\n                throw new ArgumentException(\"Bone hierarchy contains a parent cycle.\", nameof(definitions));\r\n        }\r\n\r\n        var bones = new Bone[count];\r\n        var restWorld = new XForm[count];\r\n        var indexByName = new Dictionary<string, int>(count, StringComparer.Ordinal);\r\n        for (var newIndex = 0; newIndex < count; newIndex++)\r\n        {\r\n            var def = definitions[order[newIndex]];\r\n            var parentIndex = def.ParentName is null ? -1 : newIndexByOriginal[originalIndexByName[def.ParentName]];\r\n            bones[newIndex] = new Bone(newIndex, def.Name, parentIndex, def.RestLocal);\r\n            restWorld[newIndex] = parentIndex < 0\r\n                ? def.RestLocal\r\n                : XForm.Compose(restWorld[parentIndex], def.RestLocal);\r\n            indexByName.Add(def.Name, newIndex);\r\n        }\r\n\r\n        return new Skeleton(bones, restWorld, indexByName);\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Formats/Fbx/FbxNode.cs",
            "FileName": "FbxNode.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "// Reused from humanoid-retargeter 26084c96c3fc870aaf9a5bd798de063ce2fd62df; specialized where needed for hand assets.\r\n#nullable enable annotations\r\n\r\nnamespace HumanoidHandRetargeter.Formats.Fbx;\r\n\r\n/// <summary>\r\n/// A single node of an FBX document tree (binary or ASCII): a name, a flat list of\r\n/// typed properties, and nested child nodes.\r\n///\r\n/// Property values are stored as the closest CLR type to what the file contained:\r\n/// <list type=\"bullet\">\r\n///   <item><c>short</c> ('Y'), <c>bool</c> or character <c>byte</c> ('C'), <c>int</c> ('I'), <c>float</c> ('F'),\r\n///         <c>double</c> ('D'), <c>long</c> ('L')</item>\r\n///   <item><c>float[]</c> ('f'), <c>double[]</c> ('d'), <c>long[]</c> ('l'),\r\n///         <c>int[]</c> ('i'), <c>bool[]</c>-as-<c>byte[]</c> ('b')</item>\r\n///   <item><c>string</c> ('S' \u2014 kept raw, may contain the <c>\\x00\\x01</c> name/class\r\n///         separator; see <see cref=\"SplitName\"/>), <c>byte[]</c> ('R')</item>\r\n/// </list>\r\n/// ASCII files store numbers only as <c>long</c> / <c>double</c> (and arrays as\r\n/// <c>long[]</c> / <c>double[]</c>), so the typed accessors below convert tolerantly.\r\n/// </summary>\r\npublic sealed class FbxNode\r\n{\r\n    public string Name { get; }\r\n    public List<object> Properties { get; } = new();\r\n    public List<FbxNode> Children { get; } = new();\r\n    public bool HasChildScope { get; set; }\r\n    // Document metadata retained when repairing binary FBX files.\r\n    public byte[]? FooterWatermark { get; set; }\r\n\r\n    public FbxNode(string name) => Name = name;\r\n\r\n    /// <summary>First child with the given name, or null.</summary>\r\n    public FbxNode? Child(string name)\r\n    {\r\n        foreach (var c in Children)\r\n            if (c.Name == name)\r\n                return c;\r\n        return null;\r\n    }\r\n\r\n    /// <summary>All children with the given name, in document order.</summary>\r\n    public IEnumerable<FbxNode> ChildrenNamed(string name)\r\n    {\r\n        foreach (var c in Children)\r\n            if (c.Name == name)\r\n                yield return c;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Property <paramref name=\"i\"/> converted to <typeparamref name=\"T\"/>.\r\n    /// Numeric scalars convert tolerantly across widths (e.g. an 'I' i32 read as long);\r\n    /// anything else must match the stored type exactly.\r\n    /// </summary>\r\n    public T Prop<T>(int i)\r\n    {\r\n        object v = RawProp(i);\r\n        if (v is T t)\r\n            return t;\r\n\r\n        var target = typeof(T);\r\n        // s&box whitelist: Type.IsPrimitive is banned; enumerate the convertible targets.\r\n        if (v is IConvertible && (ConvertTargets.Contains(target) || target == typeof(string)))\r\n        {\r\n            try\r\n            {\r\n                return (T)Convert.ChangeType(v, target, System.Globalization.CultureInfo.InvariantCulture);\r\n            }\r\n            // ArithmeticException covers OverflowException, which is not s&box-whitelisted\r\n            catch (Exception ex) when (ex is InvalidCastException or ArithmeticException or FormatException)\r\n            {\r\n                throw new FormatException(\r\n                    $\"FBX node '{Name}': property {i} is {v.GetType().Name}, not convertible to {target.Name}.\", ex);\r\n            }\r\n        }\r\n\r\n        throw new FormatException(\r\n            $\"FBX node '{Name}': property {i} is {v.GetType().Name}, expected {target.Name}.\");\r\n    }\r\n\r\n    /// <summary>Property <paramref name=\"i\"/> as a double array (converts f/l/i/b arrays).</summary>\r\n    public double[] AsDoubleArray(int i) => RawProp(i) switch\r\n    {\r\n        double[] d => d,\r\n        float[] f => Array.ConvertAll(f, x => (double)x),\r\n        long[] l => Array.ConvertAll(l, x => (double)x),\r\n        int[] n => Array.ConvertAll(n, x => (double)x),\r\n        byte[] b => Array.ConvertAll(b, x => (double)x),\r\n        bool[] o => Array.ConvertAll(o, x => x ? 1.0 : 0.0),\r\n        var v => throw TypeError(i, v, \"double[]\"),\r\n    };\r\n\r\n    /// <summary>Property <paramref name=\"i\"/> as a float array (converts d/l/i/b arrays).</summary>\r\n    public float[] AsFloatArray(int i) => RawProp(i) switch\r\n    {\r\n        float[] f => f,\r\n        double[] d => Array.ConvertAll(d, x => (float)x),\r\n        long[] l => Array.ConvertAll(l, x => (float)x),\r\n        int[] n => Array.ConvertAll(n, x => (float)x),\r\n        byte[] b => Array.ConvertAll(b, x => (float)x),\r\n        bool[] o => Array.ConvertAll(o, x => x ? 1f : 0f),\r\n        var v => throw TypeError(i, v, \"float[]\"),\r\n    };\r\n\r\n    /// <summary>Property <paramref name=\"i\"/> as a long array (converts i/b; d/f if integral).</summary>\r\n    public long[] AsLongArray(int i) => RawProp(i) switch\r\n    {\r\n        long[] l => l,\r\n        int[] n => Array.ConvertAll(n, x => (long)x),\r\n        byte[] b => Array.ConvertAll(b, x => (long)x),\r\n        bool[] o => Array.ConvertAll(o, x => x ? 1L : 0L),\r\n        double[] d => Array.ConvertAll(d, x => checked((long)x)),\r\n        float[] f => Array.ConvertAll(f, x => checked((long)x)),\r\n        var v => throw TypeError(i, v, \"long[]\"),\r\n    };\r\n\r\n    /// <summary>Property <paramref name=\"i\"/> as an int array (converts b; l/d/f narrowing-checked).</summary>\r\n    public int[] AsIntArray(int i) => RawProp(i) switch\r\n    {\r\n        int[] n => n,\r\n        long[] l => Array.ConvertAll(l, x => checked((int)x)),\r\n        byte[] b => Array.ConvertAll(b, x => (int)x),\r\n        bool[] o => Array.ConvertAll(o, x => x ? 1 : 0),\r\n        double[] d => Array.ConvertAll(d, x => checked((int)x)),\r\n        float[] f => Array.ConvertAll(f, x => checked((int)x)),\r\n        var v => throw TypeError(i, v, \"int[]\"),\r\n    };\r\n\r\n    /// <summary>Property <paramref name=\"i\"/> as raw bytes ('R' blobs or 'b' bool arrays).</summary>\r\n    public byte[] AsByteArray(int i) => RawProp(i) switch\r\n    {\r\n        byte[] b => b,\r\n        var v => throw TypeError(i, v, \"byte[]\"),\r\n    };\r\n\r\n    /// <summary>Property <paramref name=\"i\"/> as a string (raw 'S' content, separators intact).</summary>\r\n    public string AsString(int i) => RawProp(i) switch\r\n    {\r\n        string s => s,\r\n        var v => throw TypeError(i, v, \"string\"),\r\n    };\r\n\r\n    /// <summary>\r\n    /// Splits an FBX object name into (name, class).\r\n    /// Binary files store <c>\"Name\\x00\\x01Class\"</c> (e.g. <c>\"mixamorig:Hips\\x00\\x01Model\"</c>);\r\n    /// ASCII files store <c>\"Class::Name\"</c> (e.g. <c>\"Model::pelvis\"</c>).\r\n    /// A plain string with neither separator yields (name, \"\").\r\n    /// </summary>\r\n    public static (string Name, string Class) SplitName(string raw)\r\n    {\r\n        int bin = raw.IndexOf(\"\\0\\x01\", StringComparison.Ordinal);\r\n        if (bin >= 0)\r\n            return (raw[..bin], raw[(bin + 2)..]);\r\n\r\n        int ascii = raw.IndexOf(\"::\", StringComparison.Ordinal);\r\n        if (ascii >= 0)\r\n            return (raw[(ascii + 2)..], raw[..ascii]);\r\n\r\n        return (raw, \"\");\r\n    }\r\n\r\n    /// <summary>Primitive scalar types <see cref=\"Prop{T}\"/> converts to (whitelist-safe IsPrimitive substitute).</summary>\r\n    private static readonly HashSet<Type> ConvertTargets = new()\r\n    {\r\n        typeof(bool), typeof(byte), typeof(sbyte), typeof(short), typeof(ushort),\r\n        typeof(int), typeof(uint), typeof(long), typeof(ulong),\r\n        typeof(float), typeof(double), typeof(char),\r\n    };\r\n\r\n    private object RawProp(int i)\r\n    {\r\n        if (i < 0 || i >= Properties.Count)\r\n            throw new FormatException(\r\n                $\"FBX node '{Name}': property index {i} out of range (has {Properties.Count}).\");\r\n        return Properties[i];\r\n    }\r\n\r\n    private FormatException TypeError(int i, object v, string wanted) =>\r\n        new($\"FBX node '{Name}': property {i} is {v.GetType().Name}, expected {wanted}.\");\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Runtime/RetargetedWeaponController.cs",
            "FileName": "RetargetedWeaponController.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "#nullable enable\r\nusing Sandbox;\r\nusing HumanoidHandRetargeter.Target;\r\n\r\nnamespace HumanoidHandRetargeter;\r\n\r\n/// <summary>Optional local player controls for exported weapons. Games may instead drive RetargetedWeapon directly.</summary>\r\n[Title(\"Retargeted Weapon Controls\"), Category(\"Animation\")]\r\npublic sealed class RetargetedWeaponController : Component, ICameraModifier\r\n{\r\n    [Property] public PlayerController? Player { get; set; }\r\n    [Property] public bool ReadInput { get; set; } = true;\r\n    [Property] public int MagazineSize { get; set; } = 30;\r\n    [Property] public int StartingReserve { get; set; } = 120;\r\n    [Property] public float RoundsPerMinute { get; set; } = 800;\r\n    [Property] public float ReloadSeconds { get; set; } = 2.7f;\r\n    [Property] public float EmptyReloadSeconds { get; set; } = 3.2f;\r\n    [Property] public float Damage { get; set; } = 20;\r\n    [Property] public float Range { get; set; } = 10000;\r\n    [Property] public float HipFov { get; set; } = 75;\r\n    [Property] public float AimFov { get; set; } = 55;\r\n    [Property] public string WorldGripBone { get; set; } = \"hand_R\";\r\n    [Property] public int BodyHoldType { get; set; } = 2;\r\n    [Property] public int BodyHandedness { get; set; }\r\n    public int Ammo => magazine?.Rounds ?? MagazineSize;\r\n    public int Reserve => magazine?.Reserve ?? StartingReserve;\r\n    public bool Reloading => magazine?.Reloading ?? false;\r\n    public bool Aiming { get; private set; }\r\n    public int ShotsFired { get; private set; }\r\n    public int Hits { get; private set; }\r\n    public global::Vector3 LastHit { get; private set; }\r\n    public Action<SceneTraceResult>? Shot { get; set; }\r\n    private WeaponMagazine? magazine;\r\n    private RetargetedWeapon? weapon;\r\n    private float nextShot, reloadEnds, currentFov;\r\n    public int CameraOrder => 200;\r\n    protected override void OnStart()\r\n    {\r\n        weapon=GetComponent<RetargetedWeapon>();\r\n        Player ??= Scene.GetAllComponents<PlayerController>().FirstOrDefault();\r\n        if(Player is not null)Player.HideBodyInFirstPerson=true;\r\n        magazine=new(MagazineSize,StartingReserve);\r\n        currentFov=HipFov;\r\n        weapon?.Deploy(true);\r\n    }\r\n    public bool Fire()\r\n    {\r\n        if(weapon is null||magazine is null||Player is null||Time.Now<nextShot||Reloading)return false;\r\n        nextShot=Time.Now+60/Math.Max(1,RoundsPerMinute);\r\n        if(!magazine.TryFire()){weapon.DryFire();return false;}\r\n        weapon.Attack();weapon.SetEmpty(Ammo==0);ShotsFired++;\r\n        UpdateBodyPose();\r\n        Player.Renderer?.Set(\"b_attack\",true);\r\n        var eye=Player.EyeTransform;\r\n        var hit=Scene.Trace.Ray(eye.Position,eye.Position+eye.Rotation.Forward*Range)\r\n            .IgnoreGameObjectHierarchy(Player.GameObject).IgnoreGameObjectHierarchy(GameObject).UseHitboxes().Run();\r\n        if(hit.Hit)\r\n        {\r\n            Hits++;LastHit=hit.HitPosition;\r\n            var info=new DamageInfo{Damage=Damage,Attacker=Player.GameObject,Weapon=GameObject,Position=hit.HitPosition,Origin=eye.Position};\r\n            for(var obj=hit.GameObject;obj is not null;obj=obj.Parent)\r\n                if(obj.GetComponent<IDamageable>() is {} damageable){damageable.OnDamage(info);break;}\r\n        }\r\n        Shot?.Invoke(hit);\r\n        return true;\r\n    }\r\n    public bool Reload()\r\n    {\r\n        if(magazine is null||weapon is null||!magazine.BeginReload())return false;\r\n        weapon.Reload(Ammo==0);reloadEnds=Time.Now+(Ammo==0?EmptyReloadSeconds:ReloadSeconds);\r\n        UpdateBodyPose();\r\n        Player?.Renderer?.Set(\"b_reload\",true);\r\n        return true;\r\n    }\r\n    public void Aim(bool value){Aiming=value&&!Reloading;if(weapon is not null)weapon.Aiming=Aiming;}\r\n    protected override void OnUpdate()\r\n    {\r\n        if(weapon is null||Player is null||magazine is null)return;\r\n        if(Reloading&&Time.Now>=reloadEnds){magazine.FinishReload();weapon.SetEmpty(Ammo==0);}\r\n        if(ReadInput)Aim(Input.Down(\"Attack2\"));\r\n        if(Reloading)Aim(false);\r\n        weapon.Aiming=Aiming;\r\n        weapon.Sprinting=ReadInput&&Input.Down(\"Run\")&&Player.Velocity.Length>10&&!Aiming;\r\n        weapon.Movement=Math.Clamp(Player.Velocity.Length/150f,0,1);\r\n        weapon.SetParameter(\"attack_hold\",ReadInput&&Input.Down(\"Attack1\")&&!Reloading&&Ammo>0?1f:0f);\r\n        weapon.SetParameter(\"firing_mode\",3);\r\n        weapon.SetParameter(\"ironsights_fire_scale\",.3f);\r\n        if(ReadInput)\r\n        {\r\n            if(Input.Pressed(\"Reload\"))Reload();\r\n            if(Input.Down(\"Attack1\")&&!weapon.Sprinting)Fire();\r\n        }\r\n        weapon.ShowHands=!Player.ThirdPerson;\r\n        if(Player.ThirdPerson)weapon.WorldTransform=Player.EyeTransform;\r\n        // Keep the body graph warm even when hidden, so a camera switch during a shot\r\n        // or reload reveals the same action rather than restarting a holding pose.\r\n        UpdateBodyPose();\r\n        weapon.ThirdPersonBody=Player.ThirdPerson?Player.Renderer:null;\r\n        weapon.WorldGripBone=WorldGripBone;\r\n    }\r\n    private void UpdateBodyPose()\r\n    {\r\n        if(Player?.Renderer is not {} body)return;\r\n        body.Set(\"holdtype\",BodyHoldType);body.Set(\"holdtype_handedness\",BodyHandedness);\r\n        body.Set(\"aim_body_weight\",1f);\r\n    }\r\n    public void ModifyCamera(CameraComponent camera,ref CameraView view)\r\n    {\r\n        if(Player is not null&&!Player.ThirdPerson&&camera==Scene.Camera)\r\n        {\r\n            currentFov=MathX.Lerp(currentFov,Aiming?AimFov:HipFov,Math.Clamp(Time.Delta*12,0,1));\r\n            view.FieldOfView=currentFov;\r\n        }\r\n    }\r\n    public void PostCameraSetup(CameraComponent camera,in CameraView view)\r\n    {\r\n        // EyeTransform during Update can precede the player's final camera pose.\r\n        // Place both viewmodel meshes against the composed view before PreRender.\r\n        if(weapon is not null&&Player is not null&&!Player.ThirdPerson&&camera==Scene.Camera)\r\n            weapon.WorldTransform=new global::Transform(view.Position,view.Rotation);\r\n    }\r\n    protected override void OnDisabled()\r\n    {\r\n        magazine?.CancelReload();Aiming=false;\r\n        if(weapon is not null){weapon.Aiming=false;weapon.ThirdPersonBody=null;}\r\n        Player?.Renderer?.Set(\"holdtype\",0);\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Code/Core/Retargeting/HandMotionOptions.cs",
            "FileName": "HandMotionOptions.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "#nullable enable\r\nnamespace HumanoidHandRetargeter.Retargeting;\r\n\r\npublic sealed class HandMotionOptions\r\n{\r\n    public bool TransferWristPosition { get; init; } = true;\r\n    public bool ScaleWristTravel { get; init; } = true;\r\n    /// <summary>Known source-to-target model axes, independent of the wrists' rest rotations.</summary>\r\n    public System.Numerics.Quaternion? WristTravelBasis {get;init;}\r\n    public bool SolveArmIk { get; init; } = true;\r\n    public bool PreserveWeaponGrip { get; init; } = true;\r\n    /// <summary>Shared source weapon-to-target translation in centimeters. Grip motion is never scaled per hand.</summary>\r\n    public System.Numerics.Vector3? WeaponSpaceOffset {get;init;}\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Code/Core/Validation/HandRigValidator.cs",
            "FileName": "HandRigValidator.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "#nullable enable\r\nusing HumanoidHandRetargeter.Mapping;\r\nusing SkeletonModel = HumanoidHandRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidHandRetargeter.Validation;\r\n\r\npublic sealed record RigIssue(string Code, string Message, HandSide? Side = null, int? Bone = null);\r\n\r\n/// <summary>Shared validation for automatic/manual mapping, batch processing and the editor.</summary>\r\npublic static class HandRigValidator\r\n{\r\n    public static IReadOnlyList<RigIssue> Validate(SkeletonModel skeleton, IEnumerable<HandRigDefinition> hands)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(skeleton);\r\n        ArgumentNullException.ThrowIfNull(hands);\r\n        var issues = new List<RigIssue>();\r\n        var claimed = new HashSet<int>();\r\n        var sides = new HashSet<HandSide>();\r\n        var count = 0;\r\n\r\n        foreach (var bone in skeleton.Bones)\r\n        {\r\n            if (!PoseValidator.ValidTransform(bone.RestLocal))\r\n                issues.Add(new(\"invalid-rest\", $\"Bone '{bone.Name}' has an invalid rest transform.\", Bone: bone.Index));\r\n            var world = skeleton.RestWorld[bone.Index].Pos;\r\n            if (!float.IsFinite(world.X) || !float.IsFinite(world.Y) || !float.IsFinite(world.Z))\r\n                issues.Add(new(\"invalid-world-rest\", $\"Bone '{bone.Name}' has an invalid accumulated rest position.\", Bone: bone.Index));\r\n        }\r\n\r\n        foreach (var hand in hands)\r\n        {\r\n            count++;\r\n            if (hand is null)\r\n            {\r\n                issues.Add(new(\"missing-hand\", \"A hand mapping is missing.\"));\r\n                continue;\r\n            }\r\n            if (!Enum.IsDefined(typeof(HandSide), hand.Side))\r\n                issues.Add(new(\"invalid-side\", \"Choose left or right for the hand.\"));\r\n            if (!sides.Add(hand.Side))\r\n                issues.Add(new(\"duplicate-side\", $\"More than one {hand.Side} hand is mapped.\", hand.Side));\r\n\r\n            var arm = new[] { hand.Clavicle, hand.UpperArm, hand.Forearm, hand.Wrist }\r\n                .Where(b => b.HasValue).Select(b => b!.Value).ToArray();\r\n            CheckChain(arm, hand.Side, \"arm\", allowZeroLength: true);\r\n            var roles = new HashSet<string>(StringComparer.Ordinal);\r\n            foreach (var digit in hand.Digits)\r\n            {\r\n                if (digit is null)\r\n                {\r\n                    issues.Add(new(\"missing-digit\", \"A digit mapping is missing.\", hand.Side));\r\n                    continue;\r\n                }\r\n                var key = digit.Role == DigitRole.Extra ? \"Extra:\" + digit.ExtraSlot : digit.Role.ToString();\r\n                if (!Enum.IsDefined(typeof(DigitRole), digit.Role)\r\n                    || (digit.Role == DigitRole.Extra && string.IsNullOrWhiteSpace(digit.ExtraSlot)))\r\n                    issues.Add(new(\"invalid-digit-role\", \"Choose a digit role or name its extra slot.\", hand.Side));\r\n                if (!roles.Add(key))\r\n                    issues.Add(new(\"duplicate-digit-role\", $\"{hand.Side} {key} is mapped more than once.\", hand.Side));\r\n                if (digit.Segments.Count == 0)\r\n                    issues.Add(new(\"empty-digit\", $\"{hand.Side} {key} needs at least one segment.\", hand.Side));\r\n                var chain = digit.Bones.ToArray();\r\n                CheckChain(chain, hand.Side, key, allowZeroLength: false, tip: digit.Tip);\r\n                if (chain.Length > 0 && Valid(chain[0]) && Valid(hand.Wrist)\r\n                    && !skeleton.DescendsFrom(chain[0], hand.Wrist))\r\n                    issues.Add(new(\"wrong-wrist\", $\"{hand.Side} {key} must descend from its wrist.\", hand.Side, chain[0]));\r\n            }\r\n            foreach (var helper in hand.TwistOrHelperBones)\r\n            {\r\n                Claim(helper, hand.Side);\r\n                var armRoot = arm[0];\r\n                if (Valid(helper) && Valid(armRoot) && !skeleton.DescendsFrom(helper, armRoot))\r\n                    issues.Add(new(\"wrong-helper-root\", $\"{hand.Side} helper must belong to its mapped arm or hand hierarchy.\", hand.Side, helper));\r\n            }\r\n        }\r\n        if (count == 0)\r\n            issues.Add(new(\"no-hands\", \"Map at least one wrist before retargeting.\"));\r\n        return issues.AsReadOnly();\r\n\r\n        bool Valid(int bone) => bone >= 0 && bone < skeleton.Count;\r\n        void Claim(int bone, HandSide side)\r\n        {\r\n            if (!Valid(bone))\r\n                issues.Add(new(\"invalid-bone\", $\"{side} mapping references a missing bone.\", side, bone));\r\n            else if (!claimed.Add(bone))\r\n                issues.Add(new(\"overlapping-bone\", $\"Bone '{skeleton[bone].Name}' is assigned to multiple roles.\", side, bone));\r\n        }\r\n        void CheckChain(int[] bones, HandSide side, string role, bool allowZeroLength, int? tip = null)\r\n        {\r\n            for (var i = 0; i < bones.Length; i++)\r\n            {\r\n                Claim(bones[i], side);\r\n                if (i == 0 || !Valid(bones[i - 1]) || !Valid(bones[i])) continue;\r\n                if (!skeleton.DescendsFrom(bones[i], bones[i - 1]))\r\n                    issues.Add(new(\"chain-order\", $\"{side} {role} joints must follow their hierarchy.\", side, bones[i]));\r\n                if (!allowZeroLength && bones[i] != tip &&\r\n                    System.Numerics.Vector3.DistanceSquared(skeleton.RestWorld[bones[i - 1]].Pos,\r\n                        skeleton.RestWorld[bones[i]].Pos) < 1e-12f)\r\n                    issues.Add(new(\"zero-length-digit\", $\"{side} {role} has coincident joints; correct the chain or classify a helper.\", side, bones[i]));\r\n            }\r\n        }\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Code/Core/Mapping/HandRigDetector.cs",
            "FileName": "HandRigDetector.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "#nullable enable\r\nusing HumanoidHandRetargeter.Validation;\r\nusing SkeletonModel = HumanoidHandRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidHandRetargeter.Mapping;\r\n\r\n/// <summary>\r\n/// Name candidates constrained by wrist ancestry. Reuses legacy name tokenization;\r\n/// deliberately replaces its full-body confidence and fixed phalanx slots.\r\n/// Unnamed/ambiguous anatomy is proposed for review, never silently accepted.\r\n/// </summary>\r\npublic static class HandRigDetector\r\n{\r\n    public static HandMappingResult Detect(SkeletonModel skeleton,\r\n        IEnumerable<HandRigDefinition>? manualOverrides = null)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(skeleton);\r\n        var hints = HandDetectionProfiles.Resolve(skeleton);\r\n        var names = skeleton.Bones.Select(b => new NameInfo(b.Name,hints[b.Index])).ToArray();\r\n        var hands = new List<HandRigDefinition>();\r\n        var candidates = new List<HandMappingCandidate>();\r\n        var issues = new List<RigIssue>();\r\n        var review = false;\r\n        var deformBelow = new bool?[skeleton.Count];\r\n        int? digitWrist = null;\r\n        var overrides = (manualOverrides ?? Array.Empty<HandRigDefinition>()).ToArray();\r\n        if (overrides.Any(h => h is null))\r\n            throw new ArgumentException(\"Manual overrides cannot contain a null hand.\", nameof(manualOverrides));\r\n\r\n        foreach (var side in new[] { HandSide.Left, HandSide.Right })\r\n        {\r\n            var manual = overrides.Where(h => h.Side == side).ToArray();\r\n            if (manual.Length > 0)\r\n            {\r\n                hands.AddRange(manual);\r\n                foreach (var hand in manual)\r\n                {\r\n                    Add(side, \"Wrist\", new[] { hand.Wrist }, 1f, \"Manual mapping\", true);\r\n                    foreach (var (role, bone) in new[] { (\"Clavicle\", hand.Clavicle), (\"UpperArm\", hand.UpperArm), (\"Forearm\", hand.Forearm) })\r\n                        if (bone is int b) Add(side, role, new[] { b }, 1f, \"Manual mapping\", true);\r\n                    foreach (var digit in hand.Digits.Where(d => d is not null))\r\n                        Add(side, digit.Role.ToString(), digit.Bones, 1f, \"Manual mapping\", true);\r\n                    foreach (var helper in hand.TwistOrHelperBones)\r\n                        Add(side, \"Helper\", new[] { helper }, 1f, \"Manual mapping\", true);\r\n                }\r\n                continue;\r\n            }\r\n            var wrists = skeleton.Bones.Where(b => names[b.Index].IsWrist && !Blocked(b.Index)\r\n                && SideOf(b.Index) == side).Select(b => b.Index).ToArray();\r\n            var deformWrists=wrists.Where(w=>names[w].IsDeform).ToArray();\r\n            if(deformWrists.Length==1 && wrists.All(w=>names[w].FamilyKey==names[deformWrists[0]].FamilyKey))\r\n                wrists=deformWrists; // Rigify exports can contain DEF, ORG and animator-control copies.\r\n            if (wrists.Length == 0) continue; // A one-sided rig is valid.\r\n            if (wrists.Length > 1)\r\n            {\r\n                foreach (var wrist in wrists)\r\n                    Add(side, \"Wrist\", new[] { wrist }, .5f, \"Multiple wrist candidates on this side; choose one manually\", false);\r\n                issues.Add(new(\"ambiguous-wrist\", $\"Choose the {side} wrist in Hand Mapping.\", side));\r\n                continue;\r\n            }\r\n            var selected = wrists[0];\r\n            digitWrist = selected;\r\n            Add(side, \"Wrist\", new[] { selected }, .95f, names[selected].Profile is {} wristProfile\r\n                ? $\"{wristProfile} profile and wrist hierarchy agree\" : \"Wrist name and side agree\", true);\r\n            var digits = new List<DigitChain>();\r\n            var seenRoles = new HashSet<DigitRole>();\r\n            foreach (var root in DigitRoots(selected))\r\n            {\r\n                var chain = new List<int>();\r\n                int? meta = null, tip = null;\r\n                var current = root;\r\n                var chainReview = false;\r\n                while (true)\r\n                {\r\n                    var info = names[current];\r\n                    if (info.IsTip)\r\n                    {\r\n                        tip = current;\r\n                        if (skeleton.ChildrenOf(current).Count != 0) chainReview = true;\r\n                        break;\r\n                    }\r\n                    if (info.IsMeta && current == root) meta = current;\r\n                    else chain.Add(current);\r\n                    var next = JointChildren(current).ToArray();\r\n                    if (next.Length == 0) break;\r\n                    if (next.Length > 1) { chainReview = true; break; }\r\n                    current = next[0];\r\n                }\r\n                var role = names[root].Digit;\r\n                // A palm/metacarpal may be generically named, but its child's digit is explicit.\r\n                if (role is null && meta.HasValue && chain.Count > 0) role = names[chain[0]].Digit;\r\n                var chainSideMismatch = chain.Any(b => names[b].Side is HandSide explicitSide && explicitSide != side);\r\n                var chainRoleMismatch = role.HasValue && chain.Any(b => names[b].Digit is DigitRole namedRole && namedRole != role);\r\n                if (chainSideMismatch || chainRoleMismatch || chainReview || chain.Count == 0)\r\n                {\r\n                    Add(side, role?.ToString() ?? \"Digit\", chain, .3f,\r\n                        \"Branched, empty or side-inconsistent digit chain; correct the mapping\", false);\r\n                    review = true;\r\n                    continue;\r\n                }\r\n                if (role.HasValue && !seenRoles.Add(role.Value))\r\n                {\r\n                    Add(side, role.Value.ToString(), chain, .4f, \"Duplicate anatomical digit candidate; choose the intended chain\", false);\r\n                    review = true;\r\n                    continue;\r\n                }\r\n                var digit = new DigitChain(role ?? DigitRole.Extra, chain, meta, tip,\r\n                    role is null ? skeleton[root].Name : \"\");\r\n                digits.Add(digit);\r\n                var confidence = role.HasValue ? .9f : .4f;\r\n                Add(side, role?.ToString() ?? \"Extra:\" + skeleton[root].Name, digit.Bones, confidence,\r\n                    role.HasValue ? names[root].Profile is {} digitProfile ? $\"{digitProfile} digit convention follows this wrist's hierarchy\"\r\n                        : \"Named digit follows this wrist's hierarchy; segment numbering is not used for side\"\r\n                        : \"Unlabelled digit chain; assign an anatomical role or confirm an extra digit\", true);\r\n                if (!role.HasValue) review = true;\r\n            }\r\n\r\n            var ancestors = new List<int>();\r\n            for (var p = skeleton[selected].ParentIndex; p >= 0; p = skeleton[p].ParentIndex)\r\n                ancestors.Add(p);\r\n            int? Find(params string[] aliases) => ancestors\r\n                .Where(b => !names[b].IsHelper && !Blocked(b) && aliases.Any(a => names[b].Core.EndsWith(a, StringComparison.Ordinal)))\r\n                .Select(b => (int?)b).FirstOrDefault();\r\n            int? ProfileJoint(string role)=>ancestors.Where(b=>names[b].ProfileRole==role && !names[b].IsHelper && !Blocked(b))\r\n                .Select(b=>(int?)b).FirstOrDefault();\r\n            var forearm = ProfileJoint(\"LowerArm\") ?? Find(\"forearm\", \"lowerarm\", \"armlower\", \"elbow\");\r\n            var upperArm = ProfileJoint(\"UpperArm\") ?? Find(\"upperarm\", \"armupper\", \"uparm\");\r\n            var clavicle = Find(\"clavicle\", \"collar\") ?? ProfileJoint(\"Clavicle\");\r\n            // Prototype calls upper arm Shoulder, Mixamo calls clavicle Shoulder.\r\n            if (upperArm is null && clavicle.HasValue) upperArm = Find(\"shoulder\");\r\n            upperArm ??= ancestors.Where(b => names[b].Core.EndsWith(\"arm\", StringComparison.Ordinal)\r\n                && b != forearm && !names[b].IsHelper && !Blocked(b)).Select(b => (int?)b).FirstOrDefault();\r\n            clavicle ??= Find(\"shoulder\");\r\n            if (clavicle == upperArm) clavicle = null;\r\n            foreach (var (role, bone) in new[] { (\"Clavicle\", clavicle), (\"UpperArm\", upperArm), (\"Forearm\", forearm) })\r\n                if (bone is int b)\r\n                {\r\n                    var mismatch = names[b].ConflictingSides || (SideOf(b) is HandSide namedSide && namedSide != side);\r\n                    Add(side, role, new[] { b }, mismatch ? .3f : .9f,\r\n                        mismatch ? \"Arm name conflicts with the wrist side; correct the mapping\" : \"Named joint on the wrist ancestor chain\", true);\r\n                    review |= mismatch;\r\n                }\r\n            var armRoot = clavicle ?? upperArm ?? forearm ?? selected;\r\n            var helpers = skeleton.Bones.Where(b => names[b.Index].IsHelper && !Blocked(b.Index)\r\n                && skeleton.DescendsFrom(b.Index, armRoot)).Select(b => b.Index).ToArray();\r\n            foreach (var helper in helpers)\r\n                Add(side, \"Helper\", new[] { helper }, .9f, \"Explicit twist/helper name in this arm hierarchy\", true);\r\n            hands.Add(new(side, selected, digits, clavicle, upperArm, forearm, helpers));\r\n        }\r\n\r\n        // Unsupported enum values must reach validation instead of being silently dropped.\r\n        hands.AddRange(overrides.Where(h => !Enum.IsDefined(typeof(HandSide), h.Side)));\r\n        foreach (var bone in skeleton.Bones.Where(b => names[b.Index].IsWrist && !Blocked(b.Index) && SideOf(b.Index) is null\r\n            && !hands.Any(h => h.Wrist == b.Index)))\r\n        {\r\n            Add(null, \"Wrist\", new[] { bone.Index }, .4f, \"Wrist name found but side is unknown or conflicting\", false);\r\n            review = true;\r\n        }\r\n        if (hands.Count == 0 && candidates.Count == 0)\r\n        {\r\n            // Topology hints only: without a known side/frame, geometry must not fabricate left/right.\r\n            foreach (var bone in skeleton.Bones.Where(b => !Blocked(b.Index) &&\r\n                skeleton.ChildrenOf(b.Index).Count(c => !Blocked(c) && skeleton.ChildrenOf(c).Count > 0) >= 2))\r\n                Add(null, \"Wrist\", new[] { bone.Index }, .2f, \"Branching hierarchy may be a palm; confirm side and wrist manually\", false);\r\n            review = true;\r\n        }\r\n        issues.AddRange(HandRigValidator.Validate(skeleton, hands));\r\n        return new(hands, candidates, issues, review);\r\n\r\n        void Add(HandSide? side, string role, IEnumerable<int> bones, float confidence, string reason, bool selected)\r\n            => candidates.Add(new(side, role, Array.AsReadOnly(bones.ToArray()), confidence, reason, selected));\r\n\r\n        // The viewmodel may own the whole hierarchy: a weapon-named ancestor is not\r\n        // evidence that an explicitly named child wrist is a mechanical weapon bone.\r\n        bool Blocked(int bone) => names[bone].IsNonHandTrack;\r\n        HandSide? SideOf(int bone)\r\n        {\r\n            for (var b = bone; b >= 0; b = skeleton[b].ParentIndex)\r\n            {\r\n                if (names[b].ConflictingSides) return null;\r\n                if (names[b].Side.HasValue) return names[b].Side;\r\n            }\r\n            return null;\r\n        }\r\n        IEnumerable<int> DigitRoots(int parent)\r\n        {\r\n            foreach (var child in JointChildren(parent))\r\n            {\r\n                // Some palm/metacarpal containers parent several different digits\r\n                // (notably thumb and index). They are shared anatomy, not a fork\r\n                // within one finger. Keep the container wrist-driven and map each\r\n                // named child chain independently so no bone is assigned twice.\r\n                var branches=JointChildren(child).Where(i=>!names[i].IsTip).ToArray();\r\n                if(names[child].IsMeta&&branches.Length>1\r\n                    &&branches.All(i=>names[i].Digit.HasValue)\r\n                    &&branches.Select(i=>names[i].Digit).Distinct().Count()==branches.Length)\r\n                {\r\n                    foreach(var branch in branches)yield return branch;\r\n                    continue;\r\n                }\r\n                if (!names[child].IsTip) yield return child;\r\n            }\r\n        }\r\n        // Layered rigs (Rigify DEF/ORG/MCH, animator controls) can parent deform digits\r\n        // under a non-deform palm next to their control copies. Below a deform wrist,\r\n        // follow only the deform layer and the containers that lead to it.\r\n        bool LeadsToDeform(int bone)\r\n        {\r\n            if (deformBelow[bone] is bool known) return known;\r\n            var result = names[bone].IsDeform || skeleton.ChildrenOf(bone).Any(LeadsToDeform);\r\n            deformBelow[bone] = result;\r\n            return result;\r\n        }\r\n        IEnumerable<int> JointChildren(int parent)\r\n        {\r\n            var layered = digitWrist is int wrist && names[wrist].IsDeform\r\n                && (parent == wrist || skeleton.DescendsFrom(parent, wrist))\r\n                && skeleton.ChildrenOf(parent).Any(c => !Blocked(c) && LeadsToDeform(c));\r\n            foreach (var child in skeleton.ChildrenOf(parent))\r\n            {\r\n                if (Blocked(child)) continue;\r\n                // The parent's own deform copy is a skinned twin, not a digit.\r\n                if (layered && (!LeadsToDeform(child)\r\n                    || names[child].IsDeform && !names[child].IsTip && names[child].FamilyKey == names[parent].FamilyKey)) continue;\r\n                if (names[child].IsHelper)\r\n                {\r\n                    foreach (var nested in JointChildren(child)) yield return nested;\r\n                }\r\n                else yield return child;\r\n            }\r\n        }\r\n    }\r\n\r\n    private sealed class NameInfo\r\n    {\r\n        public string Core { get; }\r\n        public HandSide? Side { get; }\r\n        public bool ConflictingSides { get; }\r\n        public DigitRole? Digit { get; }\r\n        public bool IsWrist { get; }\r\n        public bool IsHelper { get; }\r\n        public bool IsNonHandTrack { get; }\r\n        public bool IsMeta { get; }\r\n        public bool IsTip { get; }\r\n        public string? Profile { get; }\r\n        public string? ProfileRole { get; }\r\n        public bool IsDeform { get; }\r\n        public string FamilyKey { get; }\r\n\r\n        public NameInfo(string name,HandDetectionProfiles.Hint? hint)\r\n        {\r\n            var stripped = name[(name.LastIndexOf(':') + 1)..];\r\n            var hash = stripped.LastIndexOf('#');\r\n            if (hash >= 0 && stripped[(hash + 1)..].All(char.IsDigit)) stripped = stripped[..hash];\r\n            IsDeform=stripped.StartsWith(\"DEF-\",StringComparison.OrdinalIgnoreCase);\r\n            FamilyKey=HandDetectionProfiles.Key(IsDeform||stripped.StartsWith(\"ORG-\",StringComparison.OrdinalIgnoreCase)\r\n                ||stripped.StartsWith(\"MCH-\",StringComparison.OrdinalIgnoreCase)?stripped[4..]:stripped);\r\n            Profile=hint?.Profile;ProfileRole=hint?.Role;\r\n            var tokens = BoneNameTokens.Tokenize(stripped);\r\n            bool Has(params string[] words) => tokens.Any(t => words.Contains(t, StringComparer.Ordinal));\r\n            var left = Has(\"l\", \"left\", \"lft\");\r\n            var right = Has(\"r\", \"right\", \"rgt\");\r\n            // Case-only changes must not erase a wrist's side when camel-case boundaries disappear.\r\n            var compact = string.Concat(tokens.Where(t => !t.All(char.IsDigit)));\r\n            foreach (var joint in new[] { \"hand\", \"wrist\" })\r\n            {\r\n                left |= compact.EndsWith(\"left\" + joint, StringComparison.Ordinal) || compact.EndsWith(joint + \"left\", StringComparison.Ordinal)\r\n                    || compact.EndsWith(joint + \"l\", StringComparison.Ordinal) || compact == \"l\" + joint;\r\n                right |= compact.EndsWith(\"right\" + joint, StringComparison.Ordinal) || compact.EndsWith(joint + \"right\", StringComparison.Ordinal)\r\n                    || compact.EndsWith(joint + \"r\", StringComparison.Ordinal) || compact == \"r\" + joint;\r\n            }\r\n            ConflictingSides = left && right;\r\n            Side = left == right ? left ? null : hint?.Side : left ? HandSide.Left : HandSide.Right;\r\n            Core = string.Concat(tokens.Where(t => !new[] { \"l\", \"left\", \"lft\", \"r\", \"right\", \"rgt\", \"def\", \"org\", \"mch\", \"stretch\" }.Contains(t)\r\n                && !t.All(char.IsDigit)));\r\n            IsHelper = Has(\"twist\", \"twistctrl\", \"roll\", \"share\", \"sharebone\", \"helper\", \"hlp\", \"corrective\", \"control\", \"ctrl\");\r\n            IsNonHandTrack = Has(\"weapon\", \"camera\", \"ik\", \"ikrule\", \"hold\", \"socket\", \"attachment\", \"magazine\", \"bolt\", \"slide\", \"trigger\")\r\n                || compact.StartsWith(\"weapon\", StringComparison.Ordinal) || compact.StartsWith(\"camera\", StringComparison.Ordinal);\r\n            IsMeta = Has(\"meta\", \"metacarpal\", \"carpal\", \"palm\") || hint?.Role.EndsWith(\"Meta\",StringComparison.Ordinal)==true;\r\n            IsTip = Has(\"tip\", \"end\", \"nub\", \"endmarker\") || hint?.Role.EndsWith(\"Tip\",StringComparison.Ordinal)==true;\r\n            bool DigitName(params string[] aliases) => Has(aliases)\r\n                || aliases.Any(a => Core.EndsWith(a, StringComparison.Ordinal) || Core.EndsWith(a + \"finger\", StringComparison.Ordinal));\r\n            Digit = DigitName(\"thumb\") ? DigitRole.Thumb : DigitName(\"index\", \"forefinger\", \"point\", \"pointer\", \"pointing\") ? DigitRole.Index\r\n                : DigitName(\"middle\", \"mid\") ? DigitRole.Middle : DigitName(\"ring\") ? DigitRole.Ring\r\n                : DigitName(\"pinky\", \"pinkie\", \"pink\", \"little\") ? DigitRole.Pinky : null;\r\n            if(hint is not null && HandDetectionProfiles.IsDigit(hint.Role))\r\n                Digit=Enum.GetValues<DigitRole>().First(d=>hint.Role.StartsWith(d.ToString(),StringComparison.Ordinal));\r\n            IsWrist = !IsHelper && !IsNonHandTrack && !IsTip && Digit is null\r\n                && new[] { \"hand\", \"wrist\", \"handl\", \"handr\", \"wristl\", \"wristr\", \"handleft\", \"handright\", \"wristleft\", \"wristright\" }\r\n                    .Any(suffix => Core.EndsWith(suffix, StringComparison.Ordinal));\r\n            if(hint?.Role==\"Hand\"&&!IsHelper&&!IsNonHandTrack&&!IsTip)IsWrist=true;\r\n            else if(hint is not null)IsWrist=false;\r\n        }\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Core/Mapping/HandDetectionProfiles.cs",
            "FileName": "HandDetectionProfiles.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "#nullable enable\r\nusing System.Text.RegularExpressions;\r\nusing Skel = HumanoidHandRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidHandRetargeter.Mapping;\r\n\r\n/// <summary>Reference conventions supply semantic hints; they never bypass hierarchy validation.</summary>\r\ninternal static partial class HandDetectionProfiles\r\n{\r\n    internal sealed record Alias(string Role,string Name);\r\n    internal sealed record Hint(string Role,HandSide Side,string Profile);\r\n    internal sealed class Profile\r\n    {\r\n        public string Name { get; }\r\n        private readonly Regex[] prefixes;\r\n        public Dictionary<string,string[]> Roles { get; }\r\n        public int RoleCount { get; }\r\n        public Profile(string name,string[] patterns,Alias[] aliases)\r\n        {\r\n            Name=name;prefixes=patterns.Select(p=>new Regex(p,RegexOptions.IgnoreCase|RegexOptions.CultureInvariant)).ToArray();\r\n            Roles=aliases.GroupBy(a=>Key(a.Name)).ToDictionary(g=>g.Key,g=>g.Select(a=>a.Role).Distinct().ToArray());\r\n            RoleCount=aliases.Select(a=>a.Role).Distinct().Count();\r\n        }\r\n        public string Strip(string name)\r\n        {\r\n            // Strip FBX namespaces independently of exporter prefixes (e.g. Character:CC_Base_L_Hand).\r\n            name=name[(name.LastIndexOf(':')+1)..];\r\n            var hash=name.LastIndexOf('#');\r\n            if(hash>=0&&name[(hash+1)..].All(char.IsDigit))name=name[..hash];\r\n            foreach(var prefix in prefixes)name=prefix.Replace(name,\"\");\r\n            return name;\r\n        }\r\n    }\r\n    internal static string Key(string name)=>string.Concat(name.Where(char.IsLetterOrDigit)).ToLowerInvariant();\r\n\r\n    internal static Hint?[] Resolve(Skel rig)\r\n    {\r\n        var proposals=new Dictionary<int,List<(int Score,Hint Hint)>>();\r\n        foreach(var profile in Catalog.Concat(new[]{CinemaHumanProfile}))\r\n        {\r\n            var matches=new Dictionary<int,string>();\r\n            foreach(var bone in rig.Bones)\r\n                if(profile.Roles.TryGetValue(Key(profile.Strip(bone.Name)),out var roles)&&roles.Length==1)\r\n                    matches[bone.Index]=roles[0];\r\n            foreach(var wrist in matches.Where(p=>p.Value is \"HandL\" or \"HandR\"))\r\n            {\r\n                var side=wrist.Value[^1];\r\n                var relevant=matches.Where(p=>p.Value[^1]==side && (p.Key==wrist.Key ||\r\n                    (IsDigit(p.Value)&&rig.DescendsFrom(p.Key,wrist.Key)) ||\r\n                    (IsArm(p.Value)&&rig.DescendsFrom(wrist.Key,p.Key)))).ToArray();\r\n                // Match coverage distinguishes e.g. SOMA metacarpals from Mixamo phalanges.\r\n                // Namespace evidence breaks equivalent-name ties without overpowering anatomy.\r\n                var score=relevant.Length*1000+relevant.Select(p=>p.Value).Distinct().Count()*100/Math.Max(1,profile.RoleCount/2)\r\n                    +relevant.Count(p=>profile.Strip(rig[p.Key].Name)!=rig[p.Key].Name)*2;\r\n                foreach(var match in relevant)\r\n                {\r\n                    if(!proposals.TryGetValue(match.Key,out var list))proposals[match.Key]=list=new();\r\n                    list.Add((score,new(match.Value[..^1],side=='L'?HandSide.Left:HandSide.Right,profile.Name)));\r\n                }\r\n            }\r\n        }\r\n        var hints=new Hint?[rig.Count];\r\n        foreach(var pair in proposals)\r\n        {\r\n            var best=pair.Value.Where(p=>p.Score==pair.Value.Max(v=>v.Score)).Select(p=>p.Hint).ToArray();\r\n            if(best.Select(p=>(p.Role,p.Side)).Distinct().Count()==1)hints[pair.Key]=best[0];\r\n        }\r\n        ResolveNumberedFpsHands(rig,hints);\r\n        ResolveUnlabelledSplitArms(rig,hints);\r\n        return hints;\r\n    }\r\n    // Numbered FPS rays can run from thumb to pinky or in reverse, with an\r\n    // additional palm joint on the four fingers. Require the whole convention\r\n    // and its radial geometry; bare finger numbers alone are ambiguous.\r\n    private static void ResolveNumberedFpsHands(Skel rig,Hint?[] hints)\r\n    {\r\n        foreach(var side in new[]{HandSide.Left,HandSide.Right})\r\n        {\r\n            var suffix=side==HandSide.Left?\"l\":\"r\";\r\n            string Name(int bone)=>Key(rig[bone].Name[(rig[bone].Name.LastIndexOf(':')+1)..]).Replace(\"left\",\"l\").Replace(\"right\",\"r\");\r\n            var wrists=rig.Bones.Where(b=>Name(b.Index)==\"hand\"+suffix).ToArray();\r\n            if(wrists.Length!=1)continue;\r\n            var wrist=wrists[0].Index;\r\n            foreach(var thumbNumber in new[]{1,5})\r\n            {\r\n            var chains=new List<int[]>();\r\n            for(var finger=1;finger<=5;finger++)\r\n            {\r\n                var chain=new List<int>();var parent=wrist;\r\n                for(var joint=1;joint<=(finger==thumbNumber?3:4);joint++)\r\n                {\r\n                    var pattern=$\"^finger0*{finger}0*{joint}{suffix}$\";\r\n                    var children=rig.ChildrenOf(parent).Where(i=>Regex.IsMatch(Name(i),pattern)).ToArray();\r\n                    if(children.Length!=1)break;\r\n                    parent=children[0];chain.Add(parent);\r\n                }\r\n                if(chain.Count!=(finger==thumbNumber?3:4))break;\r\n                // A three-joint thumb must not just be a truncated four-joint finger.\r\n                if(finger==thumbNumber&&rig.ChildrenOf(parent).Any(i=>Regex.IsMatch(Name(i),$\"^finger0*{finger}0*4{suffix}$\")))break;\r\n                chains.Add(chain.ToArray());\r\n            }\r\n            if(chains.Count!=5)continue;\r\n            var thumb=rig.RestWorld[chains[thumbNumber-1][0]].Pos;\r\n            var radial=System.Numerics.Vector3.DistanceSquared(thumb,rig.RestWorld[chains[thumbNumber==5?3:1][0]].Pos);\r\n            var ulnar=System.Numerics.Vector3.DistanceSquared(thumb,rig.RestWorld[chains[thumbNumber==5?0:4][0]].Pos);\r\n            if(!float.IsFinite(radial)||!float.IsFinite(ulnar)||radial>=ulnar*.8f)continue;\r\n            var roles=thumbNumber==5?new[]{\"Pinky\",\"Ring\",\"Middle\",\"Index\",\"Thumb\"}:new[]{\"Thumb\",\"Index\",\"Middle\",\"Ring\",\"Pinky\"};\r\n            if(chains.SelectMany(c=>c).Any(i=>hints[i] is not null))continue;\r\n            for(var finger=0;finger<5;finger++)\r\n                for(var joint=0;joint<chains[finger].Length;joint++)\r\n                    hints[chains[finger][joint]]=new(roles[finger]+(finger!=thumbNumber-1&&joint==0?\"Meta\":\"Prox\"),side,\"numbered_fps_hands\");\r\n            }\r\n        }\r\n    }\r\n    internal static bool IsDigit(string role)=>new[]{\"Thumb\",\"Index\",\"Middle\",\"Ring\",\"Pinky\"}.Any(role.StartsWith);\r\n    private static readonly Profile CinemaHumanProfile=CinemaBaseHuman();\r\n    private static Profile CinemaBaseHuman()\r\n    {\r\n        var aliases=new List<Alias>();\r\n        foreach(var side in new[]{\"L\",\"R\"})\r\n        {\r\n            aliases.Add(new(\"UpperArm\"+side,side+\"Upperarm\"));\r\n            aliases.Add(new(\"LowerArm\"+side,side+\"Forearm1\"));\r\n            aliases.Add(new(\"Hand\"+side,side+\"Palm\"));\r\n            var digits=new[]{\"Thumb\",\"Index\",\"Middle\",\"Ring\",\"Pinky\"};\r\n            var joints=new[]{\"Prox\",\"Mid\",\"Dist\"};\r\n            for(var d=0;d<5;d++)for(var j=0;j<3;j++)\r\n                aliases.Add(new(digits[d]+joints[j]+side,side+\"Digit\"+(d+1)+(j+1)));\r\n        }\r\n        return new(\"cinema4d_base_human\",new[]{\"^Base[-_]Human\"},aliases.ToArray());\r\n    }\r\n    private static bool IsArm(string role)=>role.StartsWith(\"Clavicle\")||role.StartsWith(\"UpperArm\")||role.StartsWith(\"LowerArm\");\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Core/Maths/MathQ.cs",
            "FileName": "MathQ.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "// Adapted from humanoid-retargeter 26084c96c3fc870aaf9a5bd798de063ce2fd62df; namespace only.\r\n#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Numerics;\r\n\r\nnamespace HumanoidHandRetargeter.Maths;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidHandRetargeter/Assembly.cs)\r\n\r\n/// <summary>\r\n/// Quaternion algebra helpers used throughout the retargeting pipeline.\r\n/// All angles are radians; quaternions are XYZW (<see cref=\"System.Numerics.Quaternion\"/> native);\r\n/// <c>a * b</c> applies <c>b</c> first (column-vector convention).\r\n/// </summary>\r\npublic static class MathQ\r\n{\r\n    private const float NearZeroLengthSq = 1e-12f;\r\n\r\n    /// <summary>\r\n    /// Defensively normalizes a quaternion. Returns <see cref=\"Quaternion.Identity\"/> when the\r\n    /// input has near-zero length or non-finite components instead of producing NaNs.\r\n    /// </summary>\r\n    public static Quaternion Normalize(Quaternion q)\r\n    {\r\n        var lengthSq = q.LengthSquared();\r\n        if (!float.IsFinite(lengthSq) || lengthSq < NearZeroLengthSq)\r\n            return Quaternion.Identity;\r\n        return Quaternion.Multiply(q, 1f / MathF.Sqrt(lengthSq));\r\n    }\r\n\r\n    /// <summary>\r\n    /// Shortest-arc rotation taking direction <paramref name=\"a\"/> onto direction <paramref name=\"b\"/>\r\n    /// (magnitudes are ignored). For antiparallel inputs a 180\u00b0 rotation about a stable axis\r\n    /// perpendicular to <paramref name=\"a\"/> is returned. Near-zero inputs yield identity.\r\n    /// </summary>\r\n    public static Quaternion FromTo(Vector3 a, Vector3 b)\r\n    {\r\n        if (a.LengthSquared() < NearZeroLengthSq || b.LengthSquared() < NearZeroLengthSq)\r\n            return Quaternion.Identity;\r\n\r\n        var an = Vector3.Normalize(a);\r\n        var bn = Vector3.Normalize(b);\r\n        var dot = Vector3.Dot(an, bn);\r\n\r\n        if (dot >= 1f - 1e-8f)\r\n            return Quaternion.Identity;\r\n\r\n        if (dot <= -1f + 1e-7f)\r\n        {\r\n            // Antiparallel: any axis perpendicular to a works; pick a stable one.\r\n            return Quaternion.CreateFromAxisAngle(Perpendicular(an), MathF.PI);\r\n        }\r\n\r\n        // q = (a x b, 1 + a\u00b7b), normalized \u2014 the half-angle construction of the shortest arc.\r\n        var cross = Vector3.Cross(an, bn);\r\n        return Normalize(new Quaternion(cross.X, cross.Y, cross.Z, 1f + dot));\r\n    }\r\n\r\n    /// <summary>\r\n    /// Decomposes a rotation into <paramref name=\"swing\"/> and <paramref name=\"twist\"/> so that\r\n    /// <c>q == swing * twist</c> (twist applied first). The twist is the projection of the\r\n    /// rotation onto <paramref name=\"twistAxis\"/>: its rotation axis is parallel to\r\n    /// <paramref name=\"twistAxis\"/>, while the swing's rotation axis is perpendicular to it.\r\n    /// When the rotation is a pure 180\u00b0 swing (no component about the axis), twist is identity.\r\n    /// </summary>\r\n    /// <exception cref=\"ArgumentException\">Thrown when <paramref name=\"twistAxis\"/> has near-zero length.</exception>\r\n    public static void SwingTwist(Quaternion q, Vector3 twistAxis, out Quaternion swing, out Quaternion twist)\r\n    {\r\n        if (twistAxis.LengthSquared() < NearZeroLengthSq)\r\n            throw new ArgumentException(\"Twist axis must be non-zero.\", nameof(twistAxis));\r\n\r\n        var axis = Vector3.Normalize(twistAxis);\r\n        q = Normalize(q);\r\n\r\n        // Project the rotation's vector part onto the twist axis.\r\n        var projection = q.X * axis.X + q.Y * axis.Y + q.Z * axis.Z;\r\n        var raw = new Quaternion(axis.X * projection, axis.Y * projection, axis.Z * projection, q.W);\r\n\r\n        // Degenerate case (q.W ~ 0 and projection ~ 0): pure 180\u00b0 swing, no twist.\r\n        twist = Normalize(raw);\r\n        swing = Normalize(q * Quaternion.Conjugate(twist));\r\n    }\r\n\r\n    /// <summary>\r\n    /// Geodesic angle between two rotations in radians, in [0, \u03c0], invariant under the\r\n    /// q / -q double cover (so <c>AngleBetween(q, -q) == 0</c>).\r\n    /// </summary>\r\n    public static float AngleBetween(Quaternion a, Quaternion b)\r\n    {\r\n        // Use atan2 on the delta rotation rather than acos on the dot product: acos is\r\n        // ill-conditioned near 0\u00b0, atan2 is accurate over the whole range.\r\n        var delta = Quaternion.Conjugate(Normalize(a)) * Normalize(b);\r\n        var sinHalf = MathF.Sqrt(delta.X * delta.X + delta.Y * delta.Y + delta.Z * delta.Z);\r\n        var cosHalf = MathF.Abs(delta.W); // |W| folds the q / -q double cover.\r\n        return 2f * MathF.Atan2(sinHalf, cosHalf);\r\n    }\r\n\r\n    /// <summary>\r\n    /// Angle between two directions in radians, in [0, \u03c0] (magnitudes are ignored).\r\n    /// Near-zero inputs yield 0. Uses atan2, which stays accurate near 0 and \u03c0 where\r\n    /// acos of the dot product loses precision.\r\n    /// </summary>\r\n    public static float AngleBetween(Vector3 a, Vector3 b)\r\n    {\r\n        if (a.LengthSquared() < NearZeroLengthSq || b.LengthSquared() < NearZeroLengthSq)\r\n            return 0f;\r\n        return MathF.Atan2(Vector3.Cross(a, b).Length(), Vector3.Dot(a, b));\r\n    }\r\n\r\n    /// <summary>\r\n    /// Builds an orthonormal right-handed basis rotation from a forward and an up hint.\r\n    /// </summary>\r\n    /// <remarks>\r\n    /// Convention: returns the rotation <c>R</c> such that\r\n    /// <list type=\"bullet\">\r\n    /// <item><c>rotate(R, +Y) == normalize(forward)</c> (primary axis),</item>\r\n    /// <item><c>rotate(R, +Z) == </c> the Gram-Schmidt orthonormalization of <paramref name=\"up\"/>\r\n    /// against forward (secondary axis),</item>\r\n    /// <item><c>rotate(R, +X) == forwardN x upOrtho</c> (right-handed completion).</item>\r\n    /// </list>\r\n    /// Degenerate inputs are handled defensively: a near-zero forward yields identity; an up hint\r\n    /// (near-)parallel to forward falls back to world +Z, or world +X when forward itself is\r\n    /// (near-)parallel to +Z.\r\n    /// </remarks>\r\n    public static Quaternion BasisFromForwardUp(Vector3 forward, Vector3 up)\r\n    {\r\n        if (forward.LengthSquared() < NearZeroLengthSq)\r\n            return Quaternion.Identity;\r\n\r\n        var y = Vector3.Normalize(forward);\r\n\r\n        var z = up - y * Vector3.Dot(up, y);\r\n        if (z.LengthSquared() < 1e-8f)\r\n        {\r\n            // Up hint unusable: pick a stable fallback and orthonormalize it.\r\n            var fallback = MathF.Abs(Vector3.Dot(y, Vector3.UnitZ)) > 0.99f ? Vector3.UnitX : Vector3.UnitZ;\r\n            z = fallback - y * Vector3.Dot(fallback, y);\r\n        }\r\n        z = Vector3.Normalize(z);\r\n\r\n        var x = Vector3.Cross(y, z);\r\n\r\n        // System.Numerics matrices act on row vectors, so the rows are the images of the\r\n        // unit axes under the rotation: row1 = R*X, row2 = R*Y, row3 = R*Z.\r\n        var m = new Matrix4x4(\r\n            x.X, x.Y, x.Z, 0f,\r\n            y.X, y.Y, y.Z, 0f,\r\n            z.X, z.Y, z.Z, 0f,\r\n            0f, 0f, 0f, 1f);\r\n\r\n        return Normalize(Quaternion.CreateFromRotationMatrix(m));\r\n    }\r\n\r\n    /// <summary>Stable unit vector perpendicular to <paramref name=\"unit\"/> (assumed normalized).\r\n    /// Internal so geometry passes (e.g. <c>TwoBoneIk</c>) reuse one implementation.</summary>\r\n    internal static Vector3 Perpendicular(Vector3 unit)\r\n    {\r\n        var p = Vector3.Cross(unit, Vector3.UnitX);\r\n        if (p.LengthSquared() < 1e-6f)\r\n            p = Vector3.Cross(unit, Vector3.UnitY);\r\n        return Vector3.Normalize(p);\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_hand_retargeter",
            "Path": "Code/Target/AnimationDriverMesh.cs",
            "FileName": "AnimationDriverMesh.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 394704,
            "IsPrivate": false,
            "Code": "namespace HumanoidHandRetargeter.Target;\r\n\r\n/// <summary>Small hidden mesh required by SceneModel's animation owner.\r\n/// Cube FBX layout from ufbx's public-domain maya_cube_7400_ascii fixture:\r\n/// https://github.com/ufbx/ufbx/blob/master/data/maya_cube_7400_ascii.fbx\r\n/// Used under ufbx LICENSE alternative B (public domain).</summary>\r\npublic static class AnimationDriverMesh\r\n{\r\n    public static readonly string Fbx = Text.Replace(\"\\r\\n\", \"\\n\").Replace(\"\\n\", \"\\r\\n\");\r\n    private const string Text = \"\"\"\r\n; FBX 7.4.0 project file\r\n; ----------------------------------------------------\r\n\r\nFBXHeaderExtension:  {\r\n\tFBXHeaderVersion: 1003\r\n\tFBXVersion: 7400\r\n\tCreationTimeStamp:  {\r\n\t\tVersion: 1000\r\n\t\tYear: 2020\r\n\t\tMonth: 3\r\n\t\tDay: 31\r\n\t\tHour: 21\r\n\t\tMinute: 36\r\n\t\tSecond: 10\r\n\t\tMillisecond: 814\r\n\t}\r\n\tCreator: \"FBX SDK/FBX Plugins version 2019.2\"\r\n\tSceneInfo: \"SceneInfo::GlobalInfo\", \"UserData\" {\r\n\t\tType: \"UserData\"\r\n\t\tVersion: 100\r\n\t\tMetaData:  {\r\n\t\t\tVersion: 100\r\n\t\t\tTitle: \"\"\r\n\t\t\tSubject: \"\"\r\n\t\t\tAuthor: \"\"\r\n\t\t\tKeywords: \"\"\r\n\t\t\tRevision: \"\"\r\n\t\t\tComment: \"\"\r\n\t\t}\r\n\t\tProperties70:  {\r\n\t\t\tP: \"DocumentUrl\", \"KString\", \"Url\", \"\", \"animation_driver.fbx\"\r\n\t\t\tP: \"SrcDocumentUrl\", \"KString\", \"Url\", \"\", \"animation_driver.fbx\"\r\n\t\t\tP: \"Original\", \"Compound\", \"\", \"\"\r\n\t\t\tP: \"Original|ApplicationVendor\", \"KString\", \"\", \"\", \"Autodesk\"\r\n\t\t\tP: \"Original|ApplicationName\", \"KString\", \"\", \"\", \"Maya\"\r\n\t\t\tP: \"Original|ApplicationVersion\", \"KString\", \"\", \"\", \"201900\"\r\n\t\t\tP: \"Original|DateTime_GMT\", \"DateTime\", \"\", \"\", \"31/03/2020 18:36:10.811\"\r\n\t\t\tP: \"Original|FileName\", \"KString\", \"\", \"\", \"animation_driver.fbx\"\r\n\t\t\tP: \"LastSaved\", \"Compound\", \"\", \"\"\r\n\t\t\tP: \"LastSaved|ApplicationVendor\", \"KString\", \"\", \"\", \"Autodesk\"\r\n\t\t\tP: \"LastSaved|ApplicationName\", \"KString\", \"\", \"\", \"Maya\"\r\n\t\t\tP: \"LastSaved|ApplicationVersion\", \"KString\", \"\", \"\", \"201900\"\r\n\t\t\tP: \"LastSaved|DateTime_GMT\", \"DateTime\", \"\", \"\", \"31/03/2020 18:36:10.811\"\r\n\t\t\tP: \"Original|ApplicationActiveProject\", \"KString\", \"\", \"\", \"animation_driver.fbx\"\r\n\t\t}\r\n\t}\r\n}\r\nGlobalSettings:  {\r\n\tVersion: 1000\r\n\tProperties70:  {\r\n\t\tP: \"UpAxis\", \"int\", \"Integer\", \"\",1\r\n\t\tP: \"UpAxisSign\", \"int\", \"Integer\", \"\",1\r\n\t\tP: \"FrontAxis\", \"int\", \"Integer\", \"\",2\r\n\t\tP: \"FrontAxisSign\", \"int\", \"Integer\", \"\",1\r\n\t\tP: \"CoordAxis\", \"int\", \"Integer\", \"\",0\r\n\t\tP: \"CoordAxisSign\", \"int\", \"Integer\", \"\",1\r\n\t\tP: \"OriginalUpAxis\", \"int\", \"Integer\", \"\",1\r\n\t\tP: \"OriginalUpAxisSign\", \"int\", \"Integer\", \"\",1\r\n\t\tP: \"UnitScaleFactor\", \"double\", \"Number\", \"\",1\r\n\t\tP: \"OriginalUnitScaleFactor\", \"double\", \"Number\", \"\",1\r\n\t\tP: \"AmbientColor\", \"ColorRGB\", \"Color\", \"\",0,0,0\r\n\t\tP: \"DefaultCamera\", \"KString\", \"\", \"\", \"Producer Perspective\"\r\n\t\tP: \"TimeMode\", \"enum\", \"\", \"\",11\r\n\t\tP: \"TimeProtocol\", \"enum\", \"\", \"\",2\r\n\t\tP: \"SnapOnFrameMode\", \"enum\", \"\", \"\",0\r\n\t\tP: \"TimeSpanStart\", \"KTime\", \"Time\", \"\",1924423250\r\n\t\tP: \"TimeSpanStop\", \"KTime\", \"Time\", \"\",384884650000\r\n\t\tP: \"CustomFrameRate\", \"double\", \"Number\", \"\",-1\r\n\t\tP: \"TimeMarker\", \"Compound\", \"\", \"\"\r\n\t\tP: \"CurrentTimeMarker\", \"int\", \"Integer\", \"\",-1\r\n\t}\r\n}\r\n\r\n; Documents Description\r\n;------------------------------------------------------------------\r\n\r\nDocuments:  {\r\n\tCount: 1\r\n\tDocument: 1907658701024, \"\", \"Scene\" {\r\n\t\tProperties70:  {\r\n\t\t\tP: \"SourceObject\", \"object\", \"\", \"\"\r\n\t\t\tP: \"ActiveAnimStackName\", \"KString\", \"\", \"\", \"Take 001\"\r\n\t\t}\r\n\t\tRootNode: 0\r\n\t}\r\n}\r\n\r\n; Document References\r\n;------------------------------------------------------------------\r\n\r\nReferences:  {\r\n}\r\n\r\n; Object definitions\r\n;------------------------------------------------------------------\r\n\r\nDefinitions:  {\r\n\tVersion: 100\r\n\tCount: 6\r\n\tObjectType: \"GlobalSettings\" {\r\n\t\tCount: 1\r\n\t}\r\n\tObjectType: \"AnimationStack\" {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \"FbxAnimStack\" {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \"Description\", \"KString\", \"\", \"\", \"\"\r\n\t\t\t\tP: \"LocalStart\", \"KTime\", \"Time\", \"\",0\r\n\t\t\t\tP: \"LocalStop\", \"KTime\", \"Time\", \"\",0\r\n\t\t\t\tP: \"ReferenceStart\", \"KTime\", \"Time\", \"\",0\r\n\t\t\t\tP: \"ReferenceStop\", \"KTime\", \"Time\", \"\",0\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tObjectType: \"AnimationLayer\" {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \"FbxAnimLayer\" {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \"Weight\", \"Number\", \"\", \"A\",100\r\n\t\t\t\tP: \"Mute\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"Solo\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"Lock\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"Color\", \"ColorRGB\", \"Color\", \"\",0.8,0.8,0.8\r\n\t\t\t\tP: \"BlendMode\", \"enum\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationAccumulationMode\", \"enum\", \"\", \"\",0\r\n\t\t\t\tP: \"ScaleAccumulationMode\", \"enum\", \"\", \"\",0\r\n\t\t\t\tP: \"BlendModeBypass\", \"ULongLong\", \"\", \"\",0\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tObjectType: \"Geometry\" {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \"FbxMesh\" {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \"Color\", \"ColorRGB\", \"Color\", \"\",0.8,0.8,0.8\r\n\t\t\t\tP: \"BBoxMin\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"BBoxMax\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"Primary Visibility\", \"bool\", \"\", \"\",1\r\n\t\t\t\tP: \"Casts Shadows\", \"bool\", \"\", \"\",1\r\n\t\t\t\tP: \"Receive Shadows\", \"bool\", \"\", \"\",1\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tObjectType: \"Material\" {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \"FbxSurfaceLambert\" {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \"ShadingModel\", \"KString\", \"\", \"\", \"Lambert\"\r\n\t\t\t\tP: \"MultiLayer\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"EmissiveColor\", \"Color\", \"\", \"A\",0,0,0\r\n\t\t\t\tP: \"EmissiveFactor\", \"Number\", \"\", \"A\",1\r\n\t\t\t\tP: \"AmbientColor\", \"Color\", \"\", \"A\",0.2,0.2,0.2\r\n\t\t\t\tP: \"AmbientFactor\", \"Number\", \"\", \"A\",1\r\n\t\t\t\tP: \"DiffuseColor\", \"Color\", \"\", \"A\",0.8,0.8,0.8\r\n\t\t\t\tP: \"DiffuseFactor\", \"Number\", \"\", \"A\",1\r\n\t\t\t\tP: \"Bump\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"NormalMap\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"BumpFactor\", \"double\", \"Number\", \"\",1\r\n\t\t\t\tP: \"TransparentColor\", \"Color\", \"\", \"A\",0,0,0\r\n\t\t\t\tP: \"TransparencyFactor\", \"Number\", \"\", \"A\",0\r\n\t\t\t\tP: \"DisplacementColor\", \"ColorRGB\", \"Color\", \"\",0,0,0\r\n\t\t\t\tP: \"DisplacementFactor\", \"double\", \"Number\", \"\",1\r\n\t\t\t\tP: \"VectorDisplacementColor\", \"ColorRGB\", \"Color\", \"\",0,0,0\r\n\t\t\t\tP: \"VectorDisplacementFactor\", \"double\", \"Number\", \"\",1\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tObjectType: \"Model\" {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \"FbxNode\" {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \"QuaternionInterpolate\", \"enum\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationOffset\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"RotationPivot\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"ScalingOffset\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"ScalingPivot\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"TranslationActive\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"TranslationMin\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"TranslationMax\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"TranslationMinX\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"TranslationMinY\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"TranslationMinZ\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"TranslationMaxX\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"TranslationMaxY\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"TranslationMaxZ\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationOrder\", \"enum\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationSpaceForLimitOnly\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationStiffnessX\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"RotationStiffnessY\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"RotationStiffnessZ\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"AxisLen\", \"double\", \"Number\", \"\",10\r\n\t\t\t\tP: \"PreRotation\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"PostRotation\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"RotationActive\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationMin\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"RotationMax\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"RotationMinX\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationMinY\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationMinZ\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationMaxX\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationMaxY\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"RotationMaxZ\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"InheritType\", \"enum\", \"\", \"\",0\r\n\t\t\t\tP: \"ScalingActive\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"ScalingMin\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"ScalingMax\", \"Vector3D\", \"Vector\", \"\",1,1,1\r\n\t\t\t\tP: \"ScalingMinX\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"ScalingMinY\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"ScalingMinZ\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"ScalingMaxX\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"ScalingMaxY\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"ScalingMaxZ\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"GeometricTranslation\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"GeometricRotation\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\t\tP: \"GeometricScaling\", \"Vector3D\", \"Vector\", \"\",1,1,1\r\n\t\t\t\tP: \"MinDampRangeX\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MinDampRangeY\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MinDampRangeZ\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MaxDampRangeX\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MaxDampRangeY\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MaxDampRangeZ\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MinDampStrengthX\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MinDampStrengthY\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MinDampStrengthZ\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MaxDampStrengthX\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MaxDampStrengthY\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"MaxDampStrengthZ\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"PreferedAngleX\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"PreferedAngleY\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"PreferedAngleZ\", \"double\", \"Number\", \"\",0\r\n\t\t\t\tP: \"LookAtProperty\", \"object\", \"\", \"\"\r\n\t\t\t\tP: \"UpVectorProperty\", \"object\", \"\", \"\"\r\n\t\t\t\tP: \"Show\", \"bool\", \"\", \"\",1\r\n\t\t\t\tP: \"NegativePercentShapeSupport\", \"bool\", \"\", \"\",1\r\n\t\t\t\tP: \"DefaultAttributeIndex\", \"int\", \"Integer\", \"\",-1\r\n\t\t\t\tP: \"Freeze\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"LODBox\", \"bool\", \"\", \"\",0\r\n\t\t\t\tP: \"Lcl Translation\", \"Lcl Translation\", \"\", \"A\",0,0,0\r\n\t\t\t\tP: \"Lcl Rotation\", \"Lcl Rotation\", \"\", \"A\",0,0,0\r\n\t\t\t\tP: \"Lcl Scaling\", \"Lcl Scaling\", \"\", \"A\",1,1,1\r\n\t\t\t\tP: \"Visibility\", \"Visibility\", \"\", \"A\",1\r\n\t\t\t\tP: \"Visibility Inheritance\", \"Visibility Inheritance\", \"\", \"\",1\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n\r\n; Object properties\r\n;------------------------------------------------------------------\r\n\r\nObjects:  {\r\n\tGeometry: 1907663073408, \"Geometry::\", \"Mesh\" {\r\n\t\tVertices: *24 {\r\n\t\t\ta: -0.5,-0.5,0.5,0.5,-0.5,0.5,-0.5,0.5,0.5,0.5,0.5,0.5,-0.5,0.5,-0.5,0.5,0.5,-0.5,-0.5,-0.5,-0.5,0.5,-0.5,-0.5\r\n\t\t}\r\n\t\tPolygonVertexIndex: *24 {\r\n\t\t\ta: 0,1,3,-3,2,3,5,-5,4,5,7,-7,6,7,1,-1,1,7,5,-4,6,0,2,-5\r\n\t\t}\r\n\t\tEdges: *12 {\r\n\t\t\ta: 0,2,6,10,3,1,7,5,11,9,15,13\r\n\t\t}\r\n\t\tGeometryVersion: 124\r\n\t\tLayerElementNormal: 0 {\r\n\t\t\tVersion: 102\r\n\t\t\tName: \"\"\r\n\t\t\tMappingInformationType: \"ByPolygonVertex\"\r\n\t\t\tReferenceInformationType: \"Direct\"\r\n\t\t\tNormals: *72 {\r\n\t\t\t\ta: 0,0,1,0,0,1,0,0,1,0,0,1,0,1,0,0,1,0,0,1,0,0,1,0,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,1,0,0,1,0,0,1,0,0,1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0\r\n\t\t\t}\r\n\t\t\tNormalsW: *24 {\r\n\t\t\t\ta: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayerElementBinormal: 0 {\r\n\t\t\tVersion: 102\r\n\t\t\tName: \"map1\"\r\n\t\t\tMappingInformationType: \"ByPolygonVertex\"\r\n\t\t\tReferenceInformationType: \"Direct\"\r\n\t\t\tBinormals: *72 {\r\n\t\t\t\ta: 0,1,-0,0,1,-0,0,1,-0,0,1,-0,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,0,1,0,0,1,0,0,1,0,0,1,-0,1,0,-0,1,0,0,1,-0,-0,1,0,0,1,0,0,1,0,0,1,0,0,1,0\r\n\t\t\t}\r\n\t\t\tBinormalsW: *24 {\r\n\t\t\t\ta: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1\r\n\t\t\t}\r\n\r\n\t\t}\r\n\t\tLayerElementTangent: 0 {\r\n\t\t\tVersion: 102\r\n\t\t\tName: \"map1\"\r\n\t\t\tMappingInformationType: \"ByPolygonVertex\"\r\n\t\t\tReferenceInformationType: \"Direct\"\r\n\t\t\tTangents: *72 {\r\n\t\t\t\ta: 1,-0,-0,1,-0,0,1,-0,0,1,-0,0,1,-0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,-0,1,0,-0,1,0,-0,1,0,-0,0,0,-1,0,0,-1,0,-0,-1,0,0,-1,0,-0,1,0,-0,1,0,-0,1,0,-0,1\r\n\t\t\t}\r\n\t\t\tTangentsW: *24 {\r\n\t\t\t\ta: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayerElementUV: 0 {\r\n\t\t\tVersion: 101\r\n\t\t\tName: \"map1\"\r\n\t\t\tMappingInformationType: \"ByPolygonVertex\"\r\n\t\t\tReferenceInformationType: \"IndexToDirect\"\r\n\t\t\tUV: *28 {\r\n\t\t\t\ta: 0.375,0,0.625,0,0.375,0.25,0.625,0.25,0.375,0.5,0.625,0.5,0.375,0.75,0.625,0.75,0.375,1,0.625,1,0.875,0,0.875,0.25,0.125,0,0.125,0.25\r\n\t\t\t}\r\n\t\t\tUVIndex: *24 {\r\n\t\t\t\ta: 0,1,3,2,2,3,5,4,4,5,7,6,6,7,9,8,1,10,11,3,12,0,2,13\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayerElementSmoothing: 0 {\r\n\t\t\tVersion: 102\r\n\t\t\tName: \"\"\r\n\t\t\tMappingInformationType: \"ByEdge\"\r\n\t\t\tReferenceInformationType: \"Direct\"\r\n\t\t\tSmoothing: *12 {\r\n\t\t\t\ta: 0,0,0,0,0,0,0,0,0,0,0,0\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayerElementMaterial: 0 {\r\n\t\t\tVersion: 101\r\n\t\t\tName: \"\"\r\n\t\t\tMappingInformationType: \"AllSame\"\r\n\t\t\tReferenceInformationType: \"IndexToDirect\"\r\n\t\t\tMaterials: *1 {\r\n\t\t\t\ta: 0\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayer: 0 {\r\n\t\t\tVersion: 100\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \"LayerElementNormal\"\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \"LayerElementBinormal\"\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \"LayerElementTangent\"\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \"LayerElementMaterial\"\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \"LayerElementSmoothing\"\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \"LayerElementUV\"\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tModel: 1907292236800, \"Model::animation_driver\", \"Mesh\" {\r\n\t\tVersion: 232\r\n\t\tProperties70:  {\r\n\t\t\tP: \"RotationActive\", \"bool\", \"\", \"\",1\r\n\t\t\tP: \"InheritType\", \"enum\", \"\", \"\",1\r\n\t\t\tP: \"ScalingMax\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\tP: \"DefaultAttributeIndex\", \"int\", \"Integer\", \"\",0\r\n\t\t\tP: \"currentUVSet\", \"KString\", \"\", \"U\", \"map1\"\r\n\t\t}\r\n\t\tShading: T\r\n\t\tCulling: \"CullingOff\"\r\n\t}\r\n\tMaterial: 1908464858960, \"Material::lambert1\", \"\" {\r\n\t\tVersion: 102\r\n\t\tShadingModel: \"lambert\"\r\n\t\tMultiLayer: 0\r\n\t\tProperties70:  {\r\n\t\t\tP: \"AmbientColor\", \"Color\", \"\", \"A\",0,0,0\r\n\t\t\tP: \"DiffuseColor\", \"Color\", \"\", \"A\",0.5,0.5,0.5\r\n\t\t\tP: \"DiffuseFactor\", \"Number\", \"\", \"A\",0.800000011920929\r\n\t\t\tP: \"TransparencyFactor\", \"Number\", \"\", \"A\",1\r\n\t\t\tP: \"Emissive\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\tP: \"Ambient\", \"Vector3D\", \"Vector\", \"\",0,0,0\r\n\t\t\tP: \"Diffuse\", \"Vector3D\", \"Vector\", \"\",0.400000005960464,0.400000005960464,0.400000005960464\r\n\t\t\tP: \"Opacity\", \"double\", \"Number\", \"\",1\r\n\t\t}\r\n\t}\r\n\tAnimationStack: 1907674289488, \"AnimStack::Take 001\", \"\" {\r\n\t\tProperties70:  {\r\n\t\t\tP: \"LocalStart\", \"KTime\", \"Time\", \"\",1924423250\r\n\t\t\tP: \"LocalStop\", \"KTime\", \"Time\", \"\",230930790000\r\n\t\t\tP: \"ReferenceStart\", \"KTime\", \"Time\", \"\",1924423250\r\n\t\t\tP: \"ReferenceStop\", \"KTime\", \"Time\", \"\",230930790000\r\n\t\t}\r\n\t}\r\n\tAnimationLayer: 1907631731008, \"AnimLayer::BaseLayer\", \"\" {\r\n\t}\r\n}\r\n\r\n; Object connections\r\n;------------------------------------------------------------------\r\n\r\nConnections:  {\r\n\r\n\t;Model::animation_driver, Model::RootNode\r\n\tC: \"OO\",1907292236800,0\r\n\r\n\t;AnimLayer::BaseLayer, AnimStack::Take 001\r\n\tC: \"OO\",1907631731008,1907674289488\r\n\r\n\t;Geometry::, Model::animation_driver\r\n\tC: \"OO\",1907663073408,1907292236800\r\n\r\n\t;Material::lambert1, Model::animation_driver\r\n\tC: \"OO\",1908464858960,1907292236800\r\n}\r\n;Takes section\r\n;----------------------------------------------------\r\n\r\nTakes:  {\r\n\tCurrent: \"Take 001\"\r\n\tTake: \"Take 001\" {\r\n\t\tFileName: \"Take_001.tak\"\r\n\t\tLocalTime: 1924423250,230930790000\r\n\t\tReferenceTime: 1924423250,230930790000\r\n\t}\r\n}\r\n\r\n\"\"\";\r\n}\r\n"
        }
    ]
}