{"TotalCount":154,"Files":[{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Core/Validation/HandRigValidator.cs","FileName":"HandRigValidator.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":379744,"Code":"#nullable enable\nusing HumanoidHandRetargeter.Mapping;\nusing SkeletonModel = HumanoidHandRetargeter.Skeleton.Skeleton;\n\nnamespace HumanoidHandRetargeter.Validation;\n\npublic sealed record RigIssue(string Code, string Message, HandSide? Side = null, int? Bone = null);\n\n/// \u003Csummary\u003EShared validation for automatic/manual mapping, batch processing and the editor.\u003C/summary\u003E\npublic static class HandRigValidator\n{\n    public static IReadOnlyList\u003CRigIssue\u003E Validate(SkeletonModel skeleton, IEnumerable\u003CHandRigDefinition\u003E hands)\n    {\n        ArgumentNullException.ThrowIfNull(skeleton);\n        ArgumentNullException.ThrowIfNull(hands);\n        var issues = new List\u003CRigIssue\u003E();\n        var claimed = new HashSet\u003Cint\u003E();\n        var sides = new HashSet\u003CHandSide\u003E();\n        var count = 0;\n\n        foreach (var bone in skeleton.Bones)\n        {\n            if (!PoseValidator.ValidTransform(bone.RestLocal))\n                issues.Add(new(\u0022invalid-rest\u0022, $\u0022Bone \u0027{bone.Name}\u0027 has an invalid rest transform.\u0022, Bone: bone.Index));\n            var world = skeleton.RestWorld[bone.Index].Pos;\n            if (!float.IsFinite(world.X) || !float.IsFinite(world.Y) || !float.IsFinite(world.Z))\n                issues.Add(new(\u0022invalid-world-rest\u0022, $\u0022Bone \u0027{bone.Name}\u0027 has an invalid accumulated rest position.\u0022, Bone: bone.Index));\n        }\n\n        foreach (var hand in hands)\n        {\n            count\u002B\u002B;\n            if (hand is null)\n            {\n                issues.Add(new(\u0022missing-hand\u0022, \u0022A hand mapping is missing.\u0022));\n                continue;\n            }\n            if (!Enum.IsDefined(typeof(HandSide), hand.Side))\n                issues.Add(new(\u0022invalid-side\u0022, \u0022Choose left or right for the hand.\u0022));\n            if (!sides.Add(hand.Side))\n                issues.Add(new(\u0022duplicate-side\u0022, $\u0022More than one {hand.Side} hand is mapped.\u0022, hand.Side));\n\n            var arm = new[] { hand.Clavicle, hand.UpperArm, hand.Forearm, hand.Wrist }\n                .Where(b =\u003E b.HasValue).Select(b =\u003E b!.Value).ToArray();\n            CheckChain(arm, hand.Side, \u0022arm\u0022, allowZeroLength: true);\n            var roles = new HashSet\u003Cstring\u003E(StringComparer.Ordinal);\n            foreach (var digit in hand.Digits)\n            {\n                if (digit is null)\n                {\n                    issues.Add(new(\u0022missing-digit\u0022, \u0022A digit mapping is missing.\u0022, hand.Side));\n                    continue;\n                }\n                var key = digit.Role == DigitRole.Extra ? \u0022Extra:\u0022 \u002B digit.ExtraSlot : digit.Role.ToString();\n                if (!Enum.IsDefined(typeof(DigitRole), digit.Role)\n                    || (digit.Role == DigitRole.Extra \u0026\u0026 string.IsNullOrWhiteSpace(digit.ExtraSlot)))\n                    issues.Add(new(\u0022invalid-digit-role\u0022, \u0022Choose a digit role or name its extra slot.\u0022, hand.Side));\n                if (!roles.Add(key))\n                    issues.Add(new(\u0022duplicate-digit-role\u0022, $\u0022{hand.Side} {key} is mapped more than once.\u0022, hand.Side));\n                if (digit.Segments.Count == 0)\n                    issues.Add(new(\u0022empty-digit\u0022, $\u0022{hand.Side} {key} needs at least one segment.\u0022, hand.Side));\n                var chain = digit.Bones.ToArray();\n                CheckChain(chain, hand.Side, key, allowZeroLength: false, tip: digit.Tip);\n                if (chain.Length \u003E 0 \u0026\u0026 Valid(chain[0]) \u0026\u0026 Valid(hand.Wrist)\n                    \u0026\u0026 !skeleton.DescendsFrom(chain[0], hand.Wrist))\n                    issues.Add(new(\u0022wrong-wrist\u0022, $\u0022{hand.Side} {key} must descend from its wrist.\u0022, hand.Side, chain[0]));\n            }\n            foreach (var helper in hand.TwistOrHelperBones)\n            {\n                Claim(helper, hand.Side);\n                var armRoot = arm[0];\n                if (Valid(helper) \u0026\u0026 Valid(armRoot) \u0026\u0026 !skeleton.DescendsFrom(helper, armRoot))\n                    issues.Add(new(\u0022wrong-helper-root\u0022, $\u0022{hand.Side} helper must belong to its mapped arm or hand hierarchy.\u0022, hand.Side, helper));\n            }\n        }\n        if (count == 0)\n            issues.Add(new(\u0022no-hands\u0022, \u0022Map at least one wrist before retargeting.\u0022));\n        return issues.AsReadOnly();\n\n        bool Valid(int bone) =\u003E bone \u003E= 0 \u0026\u0026 bone \u003C skeleton.Count;\n        void Claim(int bone, HandSide side)\n        {\n            if (!Valid(bone))\n                issues.Add(new(\u0022invalid-bone\u0022, $\u0022{side} mapping references a missing bone.\u0022, side, bone));\n            else if (!claimed.Add(bone))\n                issues.Add(new(\u0022overlapping-bone\u0022, $\u0022Bone \u0027{skeleton[bone].Name}\u0027 is assigned to multiple roles.\u0022, side, bone));\n        }\n        void CheckChain(int[] bones, HandSide side, string role, bool allowZeroLength, int? tip = null)\n        {\n            for (var i = 0; i \u003C bones.Length; i\u002B\u002B)\n            {\n                Claim(bones[i], side);\n                if (i == 0 || !Valid(bones[i - 1]) || !Valid(bones[i])) continue;\n                if (!skeleton.DescendsFrom(bones[i], bones[i - 1]))\n                    issues.Add(new(\u0022chain-order\u0022, $\u0022{side} {role} joints must follow their hierarchy.\u0022, side, bones[i]));\n                if (!allowZeroLength \u0026\u0026 bones[i] != tip \u0026\u0026\n                    System.Numerics.Vector3.DistanceSquared(skeleton.RestWorld[bones[i - 1]].Pos,\n                        skeleton.RestWorld[bones[i]].Pos) \u003C 1e-12f)\n                    issues.Add(new(\u0022zero-length-digit\u0022, $\u0022{side} {role} has coincident joints; correct the chain or classify a helper.\u0022, side, bones[i]));\n            }\n        }\n    }\n}\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Formats/Fbx/FbxScene.cs","FileName":"FbxScene.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":379744,"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\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/HumanoidHandRetargeter/Assembly.cs)\r\n\r\n/// \u003Csummary\u003EOne \u003Cc\u003EP\u003C/c\u003E entry of a \u003Cc\u003EProperties70\u003C/c\u003E block: name, FBX type string, values.\u003C/summary\u003E\r\npublic sealed class FbxProperty70\r\n{\r\n    /// \u003Csummary\u003EProperty name (e.g. \u003Cc\u003E\u0022Lcl Translation\u0022\u003C/c\u003E, \u003Cc\u003E\u0022PreRotation\u0022\u003C/c\u003E, \u003Cc\u003E\u0022d|X\u0022\u003C/c\u003E).\u003C/summary\u003E\r\n    public string Name { get; }\r\n\r\n    /// \u003Csummary\u003EFBX type string (e.g. \u003Cc\u003E\u0022Lcl Translation\u0022\u003C/c\u003E, \u003Cc\u003E\u0022enum\u0022\u003C/c\u003E, \u003Cc\u003E\u0022Number\u0022\u003C/c\u003E).\u003C/summary\u003E\r\n    public string Type { get; }\r\n\r\n    /// \u003Csummary\u003ERaw values (props 4.. of the P node).\u003C/summary\u003E\r\n    public IReadOnlyList\u003Cobject\u003E Values { get; }\r\n\r\n    internal FbxProperty70(string name, string type, IReadOnlyList\u003Cobject\u003E values)\r\n    {\r\n        Name = name;\r\n        Type = type;\r\n        Values = values;\r\n    }\r\n\r\n    /// \u003Csummary\u003EValue \u003Cparamref name=\u0022i\u0022/\u003E as a double (tolerant of int/long/float storage).\u003C/summary\u003E\r\n    public double GetDouble(int i = 0) =\u003E Convert.ToDouble(Values[i], System.Globalization.CultureInfo.InvariantCulture);\r\n\r\n    /// \u003Csummary\u003EValue \u003Cparamref name=\u0022i\u0022/\u003E as an int (tolerant of long/double storage).\u003C/summary\u003E\r\n    public int GetInt(int i = 0) =\u003E Convert.ToInt32(Values[i], System.Globalization.CultureInfo.InvariantCulture);\r\n\r\n    /// \u003Csummary\u003EFirst three values as a vector.\u003C/summary\u003E\r\n    public Vector3 GetVector3()\r\n        =\u003E new((float)GetDouble(0), (float)GetDouble(1), (float)GetDouble(2));\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// One object from the FBX \u003Cc\u003EObjects\u003C/c\u003E section (Model, NodeAttribute, AnimationStack,\r\n/// AnimationLayer, AnimationCurveNode, AnimationCurve, Pose, ...).\r\n/// \u003C/summary\u003E\r\npublic sealed class FbxObject\r\n{\r\n    /// \u003Csummary\u003EUnique object id (the first property of the object node).\u003C/summary\u003E\r\n    public long Id { get; }\r\n\r\n    /// \u003Csummary\u003ENode type \u2014 the FBX node name: \u0022Model\u0022, \u0022AnimationCurve\u0022, ...\u003C/summary\u003E\r\n    public string NodeType { get; }\r\n\r\n    /// \u003Csummary\u003EObject name (namespace prefixes like \u003Cc\u003Emixamorig1:\u003C/c\u003E preserved).\u003C/summary\u003E\r\n    public string Name { get; }\r\n\r\n    /// \u003Csummary\u003EObject sub-class (third property): \u0022LimbNode\u0022, \u0022Null\u0022, \u0022Root\u0022, \u0022Mesh\u0022, ...\u003C/summary\u003E\r\n    public string SubClass { get; }\r\n\r\n    /// \u003Csummary\u003EThe underlying token-tree node.\u003C/summary\u003E\r\n    public FbxNode Node { get; }\r\n\r\n    /// \u003Csummary\u003EOwn Properties70 entries by name (template defaults NOT merged \u2014 see \u003Csee cref=\u0022FbxScene.FindProperty\u0022/\u003E).\u003C/summary\u003E\r\n    public IReadOnlyDictionary\u003Cstring, FbxProperty70\u003E Properties { get; }\r\n\r\n    /// \u003Csummary\u003EFor Models: the parent Model via an OO connection, or null at the scene root.\u003C/summary\u003E\r\n    public FbxObject? ModelParent { get; internal set; }\r\n\r\n    /// \u003Csummary\u003EFor Models: child Models via OO connections, in connection order.\u003C/summary\u003E\r\n    public List\u003CFbxObject\u003E ModelChildren { get; } = new();\r\n\r\n    internal FbxObject(\r\n        long id, string nodeType, string name, string subClass, FbxNode node,\r\n        IReadOnlyDictionary\u003Cstring, FbxProperty70\u003E 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    /// \u003Cinheritdoc /\u003E\r\n    public override string ToString() =\u003E $\u0022{NodeType} \u0027{Name}\u0027 ({SubClass}) #{Id}\u0022;\r\n}\r\n\r\n/// \u003Csummary\u003EA single animation curve: keyframes for one scalar channel.\u003C/summary\u003E\r\npublic sealed class FbxAnimCurve\r\n{\r\n    /// \u003Csummary\u003EKTIME ticks per second (FBX constant).\u003C/summary\u003E\r\n    public const long TicksPerSecond = 46186158000L;\r\n\r\n    /// \u003Csummary\u003EKey times in KTIME ticks, ascending.\u003C/summary\u003E\r\n    public long[] KeyTimes { get; }\r\n\r\n    /// \u003Csummary\u003EKey values, parallel to \u003Csee cref=\u0022KeyTimes\u0022/\u003E.\u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Samples the curve at a KTIME tick: linear interpolation between keys, constant\r\n    /// extrapolation outside the key range.\r\n    /// \u003C/summary\u003E\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 \u003C= times[0])\r\n            return KeyValues[0];\r\n        if (ticks \u003E= times[n - 1])\r\n            return KeyValues[n - 1];\r\n\r\n        int hi = Array.BinarySearch(times, ticks);\r\n        if (hi \u003E= 0)\r\n            return KeyValues[hi];\r\n        hi = ~hi; // first index with time \u003E ticks; \u003E=1 and \u003C=n-1 here\r\n        int lo = hi - 1;\r\n        double span = times[hi] - times[lo];\r\n        double t = span \u003C= 0 ? 0.0 : (ticks - times[lo]) / span;\r\n        return (float)(KeyValues[lo] \u002B (KeyValues[hi] - KeyValues[lo]) * t);\r\n    }\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// An AnimationCurveNode: up to three channel curves (X/Y/Z) targeting one transform\r\n/// property (\u003Cc\u003E\u0022Lcl Translation\u0022\u003C/c\u003E / \u003Cc\u003E\u0022Lcl Rotation\u0022\u003C/c\u003E / \u003Cc\u003E\u0022Lcl Scaling\u0022\u003C/c\u003E) of one Model.\r\n/// \u003C/summary\u003E\r\npublic sealed class FbxAnimCurveNode\r\n{\r\n    /// \u003Csummary\u003EThe curve node object.\u003C/summary\u003E\r\n    public FbxObject Object { get; }\r\n\r\n    /// \u003Csummary\u003EChannel curves by axis (\u0027X\u0027/\u0027Y\u0027/\u0027Z\u0027), from \u003Cc\u003E\u0022d|X\u0022\u003C/c\u003E-style OP connections.\u003C/summary\u003E\r\n    public Dictionary\u003Cchar, FbxAnimCurve\u003E Channels { get; } = new();\r\n\r\n    internal FbxAnimCurveNode(FbxObject obj) =\u003E Object = obj;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Samples one component: the channel curve when connected, else the curve node\u0027s static\r\n    /// \u003Cc\u003Ed|X\u003C/c\u003E default, else \u003Cparamref name=\u0022fallback\u0022/\u003E (the model\u0027s Lcl value).\r\n    /// \u003C/summary\u003E\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(\u0022d|\u0022 \u002B axis, out var def) \u0026\u0026 def.Values.Count \u003E 0)\r\n            return (float)def.GetDouble();\r\n        return fallback;\r\n    }\r\n}\r\n\r\n/// \u003Csummary\u003EOne AnimationStack with its curve bindings, flattened across its layers.\u003C/summary\u003E\r\npublic sealed class FbxAnimStack\r\n{\r\n    /// \u003Csummary\u003EThe stack object (its Name is the clip name, e.g. \u0022mixamo.com\u0022).\u003C/summary\u003E\r\n    public FbxObject Object { get; }\r\n\r\n    /// \u003Csummary\u003E\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    /// \u003C/summary\u003E\r\n    public Dictionary\u003C(long ModelId, string Property), FbxAnimCurveNode\u003E Bindings { get; } = new();\r\n\r\n    /// \u003Csummary\u003ELocalStart from the stack\u0027s Properties70, in KTIME ticks (0 when absent).\u003C/summary\u003E\r\n    public long LocalStart { get; internal set; }\r\n\r\n    /// \u003Csummary\u003ELocalStop from the stack\u0027s Properties70, in KTIME ticks (0 when absent).\u003C/summary\u003E\r\n    public long LocalStop { get; internal set; }\r\n\r\n    internal FbxAnimStack(FbxObject obj) =\u003E Object = obj;\r\n}\r\n\r\n/// \u003Csummary\u003E\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/// \u003C/summary\u003E\r\npublic sealed class FbxScene\r\n{\r\n    /// \u003Csummary\u003EAll parsed objects by id.\u003C/summary\u003E\r\n    public IReadOnlyDictionary\u003Clong, FbxObject\u003E ObjectsById =\u003E _objectsById;\r\n\r\n    /// \u003Csummary\u003EAll Model objects in document order.\u003C/summary\u003E\r\n    public IReadOnlyList\u003CFbxObject\u003E Models =\u003E _models;\r\n\r\n    /// \u003Csummary\u003EAll animation stacks in document order.\u003C/summary\u003E\r\n    public IReadOnlyList\u003CFbxAnimStack\u003E Stacks =\u003E _stacks;\r\n\r\n    /// \u003Csummary\u003EWorld-space bind matrices from Pose/BindPose nodes, by model id (row-vector layout).\u003C/summary\u003E\r\n    public IReadOnlyDictionary\u003Clong, Matrix4x4\u003E BindPose =\u003E _bindPose;\r\n\r\n    /// \u003Csummary\u003EGlobalSettings UnitScaleFactor: source-unit \u2192 centimeter factor (FBX default 1 = cm).\u003C/summary\u003E\r\n    public double UnitScaleFactor { get; private set; } = 1.0;\r\n\r\n    /// \u003Csummary\u003EGlobalSettings UpAxis (default 1 = Y).\u003C/summary\u003E\r\n    public int UpAxis { get; private set; } = 1;\r\n\r\n    /// \u003Csummary\u003EGlobalSettings UpAxisSign (default \u002B1).\u003C/summary\u003E\r\n    public int UpAxisSign { get; private set; } = 1;\r\n\r\n    /// \u003Csummary\u003EGlobalSettings FrontAxis (default 2 = Z).\u003C/summary\u003E\r\n    public int FrontAxis { get; private set; } = 2;\r\n\r\n    /// \u003Csummary\u003EGlobalSettings FrontAxisSign (default \u002B1).\u003C/summary\u003E\r\n    public int FrontAxisSign { get; private set; } = 1;\r\n\r\n    /// \u003Csummary\u003EGlobalSettings CoordAxis (default 0 = X).\u003C/summary\u003E\r\n    public int CoordAxis { get; private set; } = 0;\r\n\r\n    /// \u003Csummary\u003EGlobalSettings CoordAxisSign (default \u002B1).\u003C/summary\u003E\r\n    public int CoordAxisSign { get; private set; } = 1;\r\n\r\n    /// \u003Csummary\u003EGlobalSettings OriginalUpAxis (-1 when not recorded).\u003C/summary\u003E\r\n    public int OriginalUpAxis { get; private set; } = -1;\r\n\r\n    /// \u003Csummary\u003EGlobalSettings TimeMode (FbxTime::EMode enum value; 0 = default mode).\u003C/summary\u003E\r\n    public int TimeMode { get; private set; }\r\n\r\n    /// \u003Csummary\u003EGlobalSettings CustomFrameRate (-1 when not recorded).\u003C/summary\u003E\r\n    public double CustomFrameRate { get; private set; } = -1.0;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Native frame rate (fps) implied by \u003Csee cref=\u0022TimeMode\u0022/\u003E /\r\n    /// \u003Csee cref=\u0022CustomFrameRate\u0022/\u003E. This is the rate the source timeline was authored at \u2014\r\n    /// external frame ranges (e.g. Unity \u003Cc\u003E.meta\u003C/c\u003E clipAnimations) are expressed in it.\r\n    /// The default mode (0) and unknown values map to 30 fps (this importer\u0027s resample default).\r\n    /// \u003C/summary\u003E\r\n    public double FrameRate =\u003E TimeModeFrameRate(TimeMode, CustomFrameRate);\r\n\r\n    /// \u003Csummary\u003EFbxTime::EMode \u2192 frames per second (eCustom uses the custom rate).\u003C/summary\u003E\r\n    internal static double TimeModeFrameRate(int timeMode, double customFrameRate) =\u003E timeMode switch\r\n    {\r\n        1 =\u003E 120.0,            // eFrames120\r\n        2 =\u003E 100.0,            // eFrames100\r\n        3 =\u003E 60.0,             // eFrames60\r\n        4 =\u003E 50.0,             // eFrames50\r\n        5 =\u003E 48.0,             // eFrames48\r\n        6 =\u003E 30.0,             // eFrames30\r\n        7 =\u003E 30.0,             // eFrames30Drop\r\n        8 =\u003E 30.0 / 1.001,     // eNTSCDropFrame (29.97)\r\n        9 =\u003E 30.0 / 1.001,     // eNTSCFullFrame (29.97)\r\n        10 =\u003E 25.0,            // ePAL\r\n        11 =\u003E 24.0,            // eFrames24\r\n        12 =\u003E 1000.0,          // eFrames1000\r\n        13 =\u003E 24.0 / 1.001,    // eFilmFullFrame (23.976)\r\n        14 =\u003E customFrameRate \u003E 0 ? customFrameRate : 30.0, // eCustom\r\n        15 =\u003E 96.0,            // eFrames96\r\n        16 =\u003E 72.0,            // eFrames72\r\n        17 =\u003E 60.0 / 1.001,    // eFrames59dot94\r\n        18 =\u003E 120.0 / 1.001,   // eFrames119dot88\r\n        _ =\u003E 30.0,             // eDefaultMode / unknown\r\n    };\r\n\r\n    private readonly Dictionary\u003Clong, FbxObject\u003E _objectsById = new();\r\n    private readonly List\u003CFbxObject\u003E _models = new();\r\n    private readonly List\u003CFbxAnimStack\u003E _stacks = new();\r\n    private readonly Dictionary\u003Clong, Matrix4x4\u003E _bindPose = new();\r\n    private readonly Dictionary\u003Cstring, Dictionary\u003Cstring, FbxProperty70\u003E\u003E _templates = new();\r\n\r\n    private FbxScene()\r\n    {\r\n    }\r\n\r\n    /// \u003Csummary\u003E\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    /// \u003C/summary\u003E\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) \u0026\u0026\r\n            template.TryGetValue(name, out var def))\r\n            return def;\r\n        return null;\r\n    }\r\n\r\n    /// \u003Csummary\u003EVector property with template fallback and a hard default.\u003C/summary\u003E\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: \u003E= 3 } ? p.GetVector3() : fallback;\r\n    }\r\n\r\n    /// \u003Csummary\u003EDouble property with template fallback and a hard default.\u003C/summary\u003E\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: \u003E= 1 } ? p.GetDouble() : fallback;\r\n    }\r\n\r\n    /// \u003Csummary\u003EInt property with template fallback and a hard default.\u003C/summary\u003E\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: \u003E= 1 } ? p.GetInt() : fallback;\r\n    }\r\n\r\n    /// \u003Csummary\u003EBuilds the semantic graph from a tokenized FBX document.\u003C/summary\u003E\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(\u0022GlobalSettings\u0022));\r\n        scene.ReadTemplates(root.Child(\u0022Definitions\u0022));\r\n        scene.ReadObjects(root.Child(\u0022Objects\u0022));\r\n        scene.ReadConnections(root.Child(\u0022Connections\u0022));\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, \u0022Name\\0\\x01Class\u0022:string, \u0022SubClass\u0022:string).\r\n            long id;\r\n            int nameProp;\r\n            if (node.Properties.Count \u003E= 1 \u0026\u0026 node.Properties[0] is long or int)\r\n            {\r\n                id = node.Prop\u003Clong\u003E(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 = \u0022\u0022, subClass = \u0022\u0022;\r\n            if (node.Properties.Count \u003E nameProp \u0026\u0026 node.Properties[nameProp] is string rawName)\r\n                (name, _) = FbxNode.SplitName(rawName);\r\n            if (node.Properties.Count \u003E nameProp \u002B 1 \u0026\u0026 node.Properties[nameProp \u002B 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 \u0022Model\u0022:\r\n                    _models.Add(obj);\r\n                    break;\r\n                case \u0022Pose\u0022:\r\n                    if (subClass == \u0022BindPose\u0022)\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(\u0022PoseNode\u0022))\r\n        {\r\n            var nodeId = poseNode.Child(\u0022Node\u0022);\r\n            var matrix = poseNode.Child(\u0022Matrix\u0022);\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 \u003C 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\u003Clong\u003E(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\u003Cstring, FbxProperty70\u003E ReadProperties70(FbxNode node)\r\n    {\r\n        var result = new Dictionary\u003Cstring, FbxProperty70\u003E(StringComparer.Ordinal);\r\n        var block = node.Child(\u0022Properties70\u0022) ?? node.Child(\u0022Properties60\u0022);\r\n        if (block is null)\r\n            return result;\r\n\r\n        foreach (var p in block.ChildrenNamed(\u0022P\u0022))\r\n        {\r\n            if (p.Properties.Count \u003C 2 || p.Properties[0] is not string name)\r\n                continue;\r\n            string type = p.Properties[1] as string ?? \u0022\u0022;\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(\u0022ObjectType\u0022))\r\n        {\r\n            if (objectType.Properties.Count \u003C 1 || objectType.Properties[0] is not string typeName)\r\n                continue;\r\n            var template = objectType.Child(\u0022PropertyTemplate\u0022);\r\n            if (template is null)\r\n                continue;\r\n            _templates[typeName] = new Dictionary\u003Cstring, FbxProperty70\u003E(\r\n                (Dictionary\u003Cstring, FbxProperty70\u003E)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            =\u003E props.TryGetValue(name, out var p) \u0026\u0026 p.Values.Count \u003E 0 ? p.GetDouble() : fallback;\r\n        int I(string name, int fallback)\r\n            =\u003E props.TryGetValue(name, out var p) \u0026\u0026 p.Values.Count \u003E 0 ? p.GetInt() : fallback;\r\n\r\n        UnitScaleFactor = D(\u0022UnitScaleFactor\u0022, 1.0);\r\n        UpAxis = I(\u0022UpAxis\u0022, 1);\r\n        UpAxisSign = I(\u0022UpAxisSign\u0022, 1);\r\n        FrontAxis = I(\u0022FrontAxis\u0022, 2);\r\n        FrontAxisSign = I(\u0022FrontAxisSign\u0022, 1);\r\n        CoordAxis = I(\u0022CoordAxis\u0022, 0);\r\n        CoordAxisSign = I(\u0022CoordAxisSign\u0022, 1);\r\n        OriginalUpAxis = I(\u0022OriginalUpAxis\u0022, -1);\r\n        TimeMode = I(\u0022TimeMode\u0022, 0);\r\n        CustomFrameRate = D(\u0022CustomFrameRate\u0022, -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\u0027t 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 (\u0022Lcl Translation\u0022/\u0022Lcl Rotation\u0022/\u0022Lcl Scaling\u0022)\r\n        //                       AnimationCurve\u2192AnimationCurveNode (\u0022d|X\u0022/\u0022d|Y\u0022/\u0022d|Z\u0022)\r\n        var layerToStack = new Dictionary\u003Clong, FbxObject\u003E();        // layer id \u2192 stack object\r\n        var curveNodeToLayers = new Dictionary\u003Clong, List\u003Clong\u003E\u003E();  // curve node id \u2192 layer ids\r\n        var curveNodeTargets = new Dictionary\u003Clong, (long ModelId, string Property)\u003E();\r\n        var curveNodes = new Dictionary\u003Clong, FbxAnimCurveNode\u003E();\r\n        var stacksById = new Dictionary\u003Clong, FbxAnimStack\u003E();\r\n\r\n        foreach (var c in connections.ChildrenNamed(\u0022C\u0022))\r\n        {\r\n            if (c.Properties.Count \u003C 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\u003Clong\u003E(1); // child\r\n            long dstId = c.Prop\u003Clong\u003E(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 == \u0022OO\u0022)\r\n            {\r\n                if (src.NodeType == \u0022Model\u0022 \u0026\u0026 dst?.NodeType == \u0022Model\u0022)\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 == \u0022AnimationLayer\u0022 \u0026\u0026 dst?.NodeType == \u0022AnimationStack\u0022)\r\n                {\r\n                    layerToStack[srcId] = dst;\r\n                }\r\n                else if (src.NodeType == \u0022AnimationCurveNode\u0022 \u0026\u0026 dst?.NodeType == \u0022AnimationLayer\u0022)\r\n                {\r\n                    if (!curveNodeToLayers.TryGetValue(srcId, out var layers))\r\n                        curveNodeToLayers[srcId] = layers = new List\u003Clong\u003E();\r\n                    layers.Add(dstId);\r\n                }\r\n            }\r\n            else if (kind == \u0022OP\u0022 \u0026\u0026 c.Properties.Count \u003E= 4 \u0026\u0026 c.Properties[3] is string property)\r\n            {\r\n                if (src.NodeType == \u0022AnimationCurveNode\u0022 \u0026\u0026 dst?.NodeType == \u0022Model\u0022)\r\n                {\r\n                    curveNodeTargets[srcId] = (dstId, property);\r\n                }\r\n                else if (src.NodeType == \u0022AnimationCurve\u0022 \u0026\u0026 dst?.NodeType == \u0022AnimationCurveNode\u0022)\r\n                {\r\n                    // Channel name \u0022d|X\u0022 \u2192 axis \u0027X\u0027.\r\n                    char axis = property.Length \u003E 0 ? property[^1] : \u0027?\u0027;\r\n                    if (axis is \u0027X\u0027 or \u0027Y\u0027 or \u0027Z\u0027)\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 \u0026\u0026 !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 != \u0022AnimationStack\u0022)\r\n                continue;\r\n            var stack = new FbxAnimStack(obj)\r\n            {\r\n                LocalStart = ReadTimeProperty(obj, \u0022LocalStart\u0022),\r\n                LocalStop = ReadTimeProperty(obj, \u0022LocalStop\u0022),\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 \u003C 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\u0026box-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(\u0022KeyTime\u0022);\r\n        var valuesNode = curveNode.Child(\u0022KeyValueFloat\u0022) ?? curveNode.Child(\u0022KeyValue\u0022);\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":379744,"Code":"namespace HumanoidHandRetargeter.Target;\n\n/// \u003Csummary\u003ESmall hidden mesh required by SceneModel\u0027s animation owner.\n/// Cube FBX layout from ufbx\u0027s public-domain maya_cube_7400_ascii fixture:\n/// https://github.com/ufbx/ufbx/blob/master/data/maya_cube_7400_ascii.fbx\n/// Used under ufbx LICENSE alternative B (public domain).\u003C/summary\u003E\npublic static class AnimationDriverMesh\n{\n    public static readonly string Fbx = Text.Replace(\u0022\\r\\n\u0022, \u0022\\n\u0022).Replace(\u0022\\n\u0022, \u0022\\r\\n\u0022);\n    private const string Text = \u0022\u0022\u0022\r\n; FBX 7.4.0 project file\n; ----------------------------------------------------\n\nFBXHeaderExtension:  {\n\tFBXHeaderVersion: 1003\n\tFBXVersion: 7400\n\tCreationTimeStamp:  {\n\t\tVersion: 1000\n\t\tYear: 2020\n\t\tMonth: 3\n\t\tDay: 31\n\t\tHour: 21\n\t\tMinute: 36\n\t\tSecond: 10\n\t\tMillisecond: 814\n\t}\n\tCreator: \u0022FBX SDK/FBX Plugins version 2019.2\u0022\n\tSceneInfo: \u0022SceneInfo::GlobalInfo\u0022, \u0022UserData\u0022 {\n\t\tType: \u0022UserData\u0022\n\t\tVersion: 100\n\t\tMetaData:  {\n\t\t\tVersion: 100\n\t\t\tTitle: \u0022\u0022\n\t\t\tSubject: \u0022\u0022\n\t\t\tAuthor: \u0022\u0022\n\t\t\tKeywords: \u0022\u0022\n\t\t\tRevision: \u0022\u0022\n\t\t\tComment: \u0022\u0022\n\t\t}\n\t\tProperties70:  {\n\t\t\tP: \u0022DocumentUrl\u0022, \u0022KString\u0022, \u0022Url\u0022, \u0022\u0022, \u0022animation_driver.fbx\u0022\n\t\t\tP: \u0022SrcDocumentUrl\u0022, \u0022KString\u0022, \u0022Url\u0022, \u0022\u0022, \u0022animation_driver.fbx\u0022\n\t\t\tP: \u0022Original\u0022, \u0022Compound\u0022, \u0022\u0022, \u0022\u0022\n\t\t\tP: \u0022Original|ApplicationVendor\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Autodesk\u0022\n\t\t\tP: \u0022Original|ApplicationName\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Maya\u0022\n\t\t\tP: \u0022Original|ApplicationVersion\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022201900\u0022\n\t\t\tP: \u0022Original|DateTime_GMT\u0022, \u0022DateTime\u0022, \u0022\u0022, \u0022\u0022, \u002231/03/2020 18:36:10.811\u0022\n\t\t\tP: \u0022Original|FileName\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022animation_driver.fbx\u0022\n\t\t\tP: \u0022LastSaved\u0022, \u0022Compound\u0022, \u0022\u0022, \u0022\u0022\n\t\t\tP: \u0022LastSaved|ApplicationVendor\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Autodesk\u0022\n\t\t\tP: \u0022LastSaved|ApplicationName\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Maya\u0022\n\t\t\tP: \u0022LastSaved|ApplicationVersion\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022201900\u0022\n\t\t\tP: \u0022LastSaved|DateTime_GMT\u0022, \u0022DateTime\u0022, \u0022\u0022, \u0022\u0022, \u002231/03/2020 18:36:10.811\u0022\n\t\t\tP: \u0022Original|ApplicationActiveProject\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022animation_driver.fbx\u0022\n\t\t}\n\t}\n}\nGlobalSettings:  {\n\tVersion: 1000\n\tProperties70:  {\n\t\tP: \u0022UpAxis\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,1\n\t\tP: \u0022UpAxisSign\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,1\n\t\tP: \u0022FrontAxis\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,2\n\t\tP: \u0022FrontAxisSign\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,1\n\t\tP: \u0022CoordAxis\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,0\n\t\tP: \u0022CoordAxisSign\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,1\n\t\tP: \u0022OriginalUpAxis\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,1\n\t\tP: \u0022OriginalUpAxisSign\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,1\n\t\tP: \u0022UnitScaleFactor\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,1\n\t\tP: \u0022OriginalUnitScaleFactor\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,1\n\t\tP: \u0022AmbientColor\u0022, \u0022ColorRGB\u0022, \u0022Color\u0022, \u0022\u0022,0,0,0\n\t\tP: \u0022DefaultCamera\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Producer Perspective\u0022\n\t\tP: \u0022TimeMode\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,11\n\t\tP: \u0022TimeProtocol\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,2\n\t\tP: \u0022SnapOnFrameMode\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\n\t\tP: \u0022TimeSpanStart\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,1924423250\n\t\tP: \u0022TimeSpanStop\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,384884650000\n\t\tP: \u0022CustomFrameRate\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,-1\n\t\tP: \u0022TimeMarker\u0022, \u0022Compound\u0022, \u0022\u0022, \u0022\u0022\n\t\tP: \u0022CurrentTimeMarker\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,-1\n\t}\n}\n\n; Documents Description\n;------------------------------------------------------------------\n\nDocuments:  {\n\tCount: 1\n\tDocument: 1907658701024, \u0022\u0022, \u0022Scene\u0022 {\n\t\tProperties70:  {\n\t\t\tP: \u0022SourceObject\u0022, \u0022object\u0022, \u0022\u0022, \u0022\u0022\n\t\t\tP: \u0022ActiveAnimStackName\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Take 001\u0022\n\t\t}\n\t\tRootNode: 0\n\t}\n}\n\n; Document References\n;------------------------------------------------------------------\n\nReferences:  {\n}\n\n; Object definitions\n;------------------------------------------------------------------\n\nDefinitions:  {\n\tVersion: 100\n\tCount: 6\n\tObjectType: \u0022GlobalSettings\u0022 {\n\t\tCount: 1\n\t}\n\tObjectType: \u0022AnimationStack\u0022 {\n\t\tCount: 1\n\t\tPropertyTemplate: \u0022FbxAnimStack\u0022 {\n\t\t\tProperties70:  {\n\t\t\t\tP: \u0022Description\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022\u0022\n\t\t\t\tP: \u0022LocalStart\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022LocalStop\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022ReferenceStart\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022ReferenceStop\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,0\n\t\t\t}\n\t\t}\n\t}\n\tObjectType: \u0022AnimationLayer\u0022 {\n\t\tCount: 1\n\t\tPropertyTemplate: \u0022FbxAnimLayer\u0022 {\n\t\t\tProperties70:  {\n\t\t\t\tP: \u0022Weight\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,100\n\t\t\t\tP: \u0022Mute\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022Solo\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022Lock\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022Color\u0022, \u0022ColorRGB\u0022, \u0022Color\u0022, \u0022\u0022,0.8,0.8,0.8\n\t\t\t\tP: \u0022BlendMode\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022RotationAccumulationMode\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022ScaleAccumulationMode\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022BlendModeBypass\u0022, \u0022ULongLong\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t}\n\t\t}\n\t}\n\tObjectType: \u0022Geometry\u0022 {\n\t\tCount: 1\n\t\tPropertyTemplate: \u0022FbxMesh\u0022 {\n\t\t\tProperties70:  {\n\t\t\t\tP: \u0022Color\u0022, \u0022ColorRGB\u0022, \u0022Color\u0022, \u0022\u0022,0.8,0.8,0.8\n\t\t\t\tP: \u0022BBoxMin\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022BBoxMax\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022Primary Visibility\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,1\n\t\t\t\tP: \u0022Casts Shadows\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,1\n\t\t\t\tP: \u0022Receive Shadows\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,1\n\t\t\t}\n\t\t}\n\t}\n\tObjectType: \u0022Material\u0022 {\n\t\tCount: 1\n\t\tPropertyTemplate: \u0022FbxSurfaceLambert\u0022 {\n\t\t\tProperties70:  {\n\t\t\t\tP: \u0022ShadingModel\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Lambert\u0022\n\t\t\t\tP: \u0022MultiLayer\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022EmissiveColor\u0022, \u0022Color\u0022, \u0022\u0022, \u0022A\u0022,0,0,0\n\t\t\t\tP: \u0022EmissiveFactor\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,1\n\t\t\t\tP: \u0022AmbientColor\u0022, \u0022Color\u0022, \u0022\u0022, \u0022A\u0022,0.2,0.2,0.2\n\t\t\t\tP: \u0022AmbientFactor\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,1\n\t\t\t\tP: \u0022DiffuseColor\u0022, \u0022Color\u0022, \u0022\u0022, \u0022A\u0022,0.8,0.8,0.8\n\t\t\t\tP: \u0022DiffuseFactor\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,1\n\t\t\t\tP: \u0022Bump\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022NormalMap\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022BumpFactor\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,1\n\t\t\t\tP: \u0022TransparentColor\u0022, \u0022Color\u0022, \u0022\u0022, \u0022A\u0022,0,0,0\n\t\t\t\tP: \u0022TransparencyFactor\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,0\n\t\t\t\tP: \u0022DisplacementColor\u0022, \u0022ColorRGB\u0022, \u0022Color\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022DisplacementFactor\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,1\n\t\t\t\tP: \u0022VectorDisplacementColor\u0022, \u0022ColorRGB\u0022, \u0022Color\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022VectorDisplacementFactor\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,1\n\t\t\t}\n\t\t}\n\t}\n\tObjectType: \u0022Model\u0022 {\n\t\tCount: 1\n\t\tPropertyTemplate: \u0022FbxNode\u0022 {\n\t\t\tProperties70:  {\n\t\t\t\tP: \u0022QuaternionInterpolate\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022RotationOffset\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022RotationPivot\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022ScalingOffset\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022ScalingPivot\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022TranslationActive\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022TranslationMin\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022TranslationMax\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022TranslationMinX\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022TranslationMinY\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022TranslationMinZ\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022TranslationMaxX\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022TranslationMaxY\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022TranslationMaxZ\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022RotationOrder\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022RotationSpaceForLimitOnly\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022RotationStiffnessX\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022RotationStiffnessY\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022RotationStiffnessZ\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022AxisLen\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,10\n\t\t\t\tP: \u0022PreRotation\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022PostRotation\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022RotationActive\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022RotationMin\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022RotationMax\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022RotationMinX\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022RotationMinY\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022RotationMinZ\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022RotationMaxX\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022RotationMaxY\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022RotationMaxZ\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022InheritType\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022ScalingActive\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022ScalingMin\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022ScalingMax\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,1,1,1\n\t\t\t\tP: \u0022ScalingMinX\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022ScalingMinY\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022ScalingMinZ\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022ScalingMaxX\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022ScalingMaxY\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022ScalingMaxZ\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022GeometricTranslation\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022GeometricRotation\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\t\tP: \u0022GeometricScaling\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,1,1,1\n\t\t\t\tP: \u0022MinDampRangeX\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022MinDampRangeY\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022MinDampRangeZ\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022MaxDampRangeX\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022MaxDampRangeY\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022MaxDampRangeZ\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022MinDampStrengthX\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022MinDampStrengthY\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022MinDampStrengthZ\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022MaxDampStrengthX\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022MaxDampStrengthY\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022MaxDampStrengthZ\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022PreferedAngleX\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022PreferedAngleY\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022PreferedAngleZ\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022LookAtProperty\u0022, \u0022object\u0022, \u0022\u0022, \u0022\u0022\n\t\t\t\tP: \u0022UpVectorProperty\u0022, \u0022object\u0022, \u0022\u0022, \u0022\u0022\n\t\t\t\tP: \u0022Show\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,1\n\t\t\t\tP: \u0022NegativePercentShapeSupport\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,1\n\t\t\t\tP: \u0022DefaultAttributeIndex\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,-1\n\t\t\t\tP: \u0022Freeze\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022LODBox\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\n\t\t\t\tP: \u0022Lcl Translation\u0022, \u0022Lcl Translation\u0022, \u0022\u0022, \u0022A\u0022,0,0,0\n\t\t\t\tP: \u0022Lcl Rotation\u0022, \u0022Lcl Rotation\u0022, \u0022\u0022, \u0022A\u0022,0,0,0\n\t\t\t\tP: \u0022Lcl Scaling\u0022, \u0022Lcl Scaling\u0022, \u0022\u0022, \u0022A\u0022,1,1,1\n\t\t\t\tP: \u0022Visibility\u0022, \u0022Visibility\u0022, \u0022\u0022, \u0022A\u0022,1\n\t\t\t\tP: \u0022Visibility Inheritance\u0022, \u0022Visibility Inheritance\u0022, \u0022\u0022, \u0022\u0022,1\n\t\t\t}\n\t\t}\n\t}\n}\n\n; Object properties\n;------------------------------------------------------------------\n\nObjects:  {\n\tGeometry: 1907663073408, \u0022Geometry::\u0022, \u0022Mesh\u0022 {\n\t\tVertices: *24 {\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\n\t\t}\n\t\tPolygonVertexIndex: *24 {\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\n\t\t}\n\t\tEdges: *12 {\n\t\t\ta: 0,2,6,10,3,1,7,5,11,9,15,13\n\t\t}\n\t\tGeometryVersion: 124\n\t\tLayerElementNormal: 0 {\n\t\t\tVersion: 102\n\t\t\tName: \u0022\u0022\n\t\t\tMappingInformationType: \u0022ByPolygonVertex\u0022\n\t\t\tReferenceInformationType: \u0022Direct\u0022\n\t\t\tNormals: *72 {\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\n\t\t\t}\n\t\t\tNormalsW: *24 {\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\n\t\t\t}\n\t\t}\n\t\tLayerElementBinormal: 0 {\n\t\t\tVersion: 102\n\t\t\tName: \u0022map1\u0022\n\t\t\tMappingInformationType: \u0022ByPolygonVertex\u0022\n\t\t\tReferenceInformationType: \u0022Direct\u0022\n\t\t\tBinormals: *72 {\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\n\t\t\t}\n\t\t\tBinormalsW: *24 {\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\n\t\t\t}\n\n\t\t}\n\t\tLayerElementTangent: 0 {\n\t\t\tVersion: 102\n\t\t\tName: \u0022map1\u0022\n\t\t\tMappingInformationType: \u0022ByPolygonVertex\u0022\n\t\t\tReferenceInformationType: \u0022Direct\u0022\n\t\t\tTangents: *72 {\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\n\t\t\t}\n\t\t\tTangentsW: *24 {\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\n\t\t\t}\n\t\t}\n\t\tLayerElementUV: 0 {\n\t\t\tVersion: 101\n\t\t\tName: \u0022map1\u0022\n\t\t\tMappingInformationType: \u0022ByPolygonVertex\u0022\n\t\t\tReferenceInformationType: \u0022IndexToDirect\u0022\n\t\t\tUV: *28 {\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\n\t\t\t}\n\t\t\tUVIndex: *24 {\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\n\t\t\t}\n\t\t}\n\t\tLayerElementSmoothing: 0 {\n\t\t\tVersion: 102\n\t\t\tName: \u0022\u0022\n\t\t\tMappingInformationType: \u0022ByEdge\u0022\n\t\t\tReferenceInformationType: \u0022Direct\u0022\n\t\t\tSmoothing: *12 {\n\t\t\t\ta: 0,0,0,0,0,0,0,0,0,0,0,0\n\t\t\t}\n\t\t}\n\t\tLayerElementMaterial: 0 {\n\t\t\tVersion: 101\n\t\t\tName: \u0022\u0022\n\t\t\tMappingInformationType: \u0022AllSame\u0022\n\t\t\tReferenceInformationType: \u0022IndexToDirect\u0022\n\t\t\tMaterials: *1 {\n\t\t\t\ta: 0\n\t\t\t}\n\t\t}\n\t\tLayer: 0 {\n\t\t\tVersion: 100\n\t\t\tLayerElement:  {\n\t\t\t\tType: \u0022LayerElementNormal\u0022\n\t\t\t\tTypedIndex: 0\n\t\t\t}\n\t\t\tLayerElement:  {\n\t\t\t\tType: \u0022LayerElementBinormal\u0022\n\t\t\t\tTypedIndex: 0\n\t\t\t}\n\t\t\tLayerElement:  {\n\t\t\t\tType: \u0022LayerElementTangent\u0022\n\t\t\t\tTypedIndex: 0\n\t\t\t}\n\t\t\tLayerElement:  {\n\t\t\t\tType: \u0022LayerElementMaterial\u0022\n\t\t\t\tTypedIndex: 0\n\t\t\t}\n\t\t\tLayerElement:  {\n\t\t\t\tType: \u0022LayerElementSmoothing\u0022\n\t\t\t\tTypedIndex: 0\n\t\t\t}\n\t\t\tLayerElement:  {\n\t\t\t\tType: \u0022LayerElementUV\u0022\n\t\t\t\tTypedIndex: 0\n\t\t\t}\n\t\t}\n\t}\n\tModel: 1907292236800, \u0022Model::animation_driver\u0022, \u0022Mesh\u0022 {\n\t\tVersion: 232\n\t\tProperties70:  {\n\t\t\tP: \u0022RotationActive\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,1\n\t\t\tP: \u0022InheritType\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,1\n\t\t\tP: \u0022ScalingMax\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\tP: \u0022DefaultAttributeIndex\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,0\n\t\t\tP: \u0022currentUVSet\u0022, \u0022KString\u0022, \u0022\u0022, \u0022U\u0022, \u0022map1\u0022\n\t\t}\n\t\tShading: T\n\t\tCulling: \u0022CullingOff\u0022\n\t}\n\tMaterial: 1908464858960, \u0022Material::lambert1\u0022, \u0022\u0022 {\n\t\tVersion: 102\n\t\tShadingModel: \u0022lambert\u0022\n\t\tMultiLayer: 0\n\t\tProperties70:  {\n\t\t\tP: \u0022AmbientColor\u0022, \u0022Color\u0022, \u0022\u0022, \u0022A\u0022,0,0,0\n\t\t\tP: \u0022DiffuseColor\u0022, \u0022Color\u0022, \u0022\u0022, \u0022A\u0022,0.5,0.5,0.5\n\t\t\tP: \u0022DiffuseFactor\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,0.800000011920929\n\t\t\tP: \u0022TransparencyFactor\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,1\n\t\t\tP: \u0022Emissive\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\tP: \u0022Ambient\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\n\t\t\tP: \u0022Diffuse\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0.400000005960464,0.400000005960464,0.400000005960464\n\t\t\tP: \u0022Opacity\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,1\n\t\t}\n\t}\n\tAnimationStack: 1907674289488, \u0022AnimStack::Take 001\u0022, \u0022\u0022 {\n\t\tProperties70:  {\n\t\t\tP: \u0022LocalStart\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,1924423250\n\t\t\tP: \u0022LocalStop\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,230930790000\n\t\t\tP: \u0022ReferenceStart\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,1924423250\n\t\t\tP: \u0022ReferenceStop\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,230930790000\n\t\t}\n\t}\n\tAnimationLayer: 1907631731008, \u0022AnimLayer::BaseLayer\u0022, \u0022\u0022 {\n\t}\n}\n\n; Object connections\n;------------------------------------------------------------------\n\nConnections:  {\n\n\t;Model::animation_driver, Model::RootNode\n\tC: \u0022OO\u0022,1907292236800,0\n\n\t;AnimLayer::BaseLayer, AnimStack::Take 001\n\tC: \u0022OO\u0022,1907631731008,1907674289488\n\n\t;Geometry::, Model::animation_driver\n\tC: \u0022OO\u0022,1907663073408,1907292236800\n\n\t;Material::lambert1, Model::animation_driver\n\tC: \u0022OO\u0022,1908464858960,1907292236800\n}\n;Takes section\n;----------------------------------------------------\n\nTakes:  {\n\tCurrent: \u0022Take 001\u0022\n\tTake: \u0022Take 001\u0022 {\n\t\tFileName: \u0022Take_001.tak\u0022\n\t\tLocalTime: 1924423250,230930790000\n\t\tReferenceTime: 1924423250,230930790000\n\t}\n}\n\r\n\u0022\u0022\u0022;\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Target/HandModelFactory.cs","FileName":"HandModelFactory.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":379744,"Code":"#nullable enable\r\nnamespace HumanoidHandRetargeter.Target;\r\n\r\n/// \u003Csummary\u003EMinimal hand ModelDoc using the audited VmdlWriter mesh and scaling node shapes.\u003C/summary\u003E\r\npublic static class HandModelFactory\r\n{\r\n    public const string Header = \u0022\u003C!-- kv3 encoding:text:version{e21c7f3c-8a33-41c5-9977-a76d3a32aa0d} format:modeldoc30:version{8c2d7a91-9c42-4bf0-883a-5a3b1762d4f1} --\u003E\u0022;\r\n    public static string Create(string baseModel = \u0022\u0022, string mesh = \u0022\u0022, float meshUnitScaleCm = 1, IReadOnlyDictionary\u003Cstring,string\u003E? materialRemaps = null, IReadOnlyList\u003Cstring\u003E? meshNames = null)\n    {\r\n        if (!float.IsFinite(meshUnitScaleCm) || meshUnitScaleCm \u003C= 0) throw new ArgumentOutOfRangeException(nameof(meshUnitScaleCm));\r\n        if (string.IsNullOrWhiteSpace(baseModel) == string.IsNullOrWhiteSpace(mesh)) throw new ArgumentException(\u0022Choose one base model or mesh source.\u0022);\r\n        var children = new KvArray();\r\n        if (mesh.Length \u003E 0)\r\n        {\r\n            mesh = VmdlSetupService.NormalizeAssetPath(mesh, \u0022.fbx\u0022);\r\n            var meshes = new KvArray();\r\n            meshes.Items.Add(new KvObject { [\u0022_class\u0022] = new KvString(\u0022RenderMeshFile\u0022), [\u0022name\u0022] = new KvString(\u0022hands_mesh\u0022),\n                [\u0022filename\u0022] = new KvString(mesh), [\u0022import_scale\u0022] = new KvDouble(meshUnitScaleCm),\r\n                [\u0022import_translation\u0022] = Vector(0,0,0), [\u0022import_rotation\u0022] = Vector(0,0,0),\r\n                [\u0022align_origin_x_type\u0022] = new KvString(\u0022None\u0022), [\u0022align_origin_y_type\u0022] = new KvString(\u0022None\u0022), [\u0022align_origin_z_type\u0022] = new KvString(\u0022None\u0022) });\n            if(meshNames is {Count:\u003E0})\n            {\n                var names=new KvArray();foreach(var name in meshNames)names.Items.Add(new KvString(name));\n                ((KvObject)meshes.Items[0])[\u0022import_filter\u0022]=new KvObject\n                    {[\u0022exclude_by_default\u0022]=new KvBool(true),[\u0022exception_list\u0022]=names};\n            }\n            children.Items.Add(new KvObject { [\u0022_class\u0022] = new KvString(\u0022RenderMeshList\u0022), [\u0022children\u0022] = meshes });\r\n            children.Items.Add(new KvObject { [\u0022_class\u0022] = new KvString(\u0022BoneMarkupList\u0022), [\u0022bone_cull_type\u0022] = new KvString(\u0022None\u0022), [\u0022children\u0022] = 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{[\u0022from\u0022]=new KvString(pair.Key),[\u0022to\u0022]=new KvString(pair.Value)});\r\n            var materials = new KvArray();\r\n            materials.Items.Add(new KvObject { [\u0022_class\u0022] = new KvString(\u0022DefaultMaterialGroup\u0022), [\u0022use_global_default\u0022] = new KvBool(materialRemaps is null || materialRemaps.Count==0),\r\n                [\u0022global_default_material\u0022] = new KvString(\u0022materials/dev/reflectivity_50.vmat\u0022), [\u0022remaps\u0022] = remaps });\r\n            children.Items.Add(new KvObject { [\u0022_class\u0022] = new KvString(\u0022MaterialGroupList\u0022), [\u0022children\u0022] = materials });\r\n        }\r\n        else baseModel = VmdlSetupService.NormalizeAssetPath(baseModel, \u0022.vmdl\u0022);\r\n        var modifiers = new KvArray();\r\n        modifiers.Items.Add(new KvObject { [\u0022_class\u0022] = new KvString(\u0022ModelModifier_ScaleAndMirror\u0022), [\u0022scale\u0022] = new KvDouble(1.0 / 2.54),\r\n            [\u0022mirror_x\u0022] = new KvBool(false), [\u0022mirror_y\u0022] = new KvBool(false), [\u0022mirror_z\u0022] = new KvBool(false),\r\n            [\u0022flip_bone_forward\u0022] = new KvBool(false), [\u0022swap_left_and_right_bones\u0022] = new KvBool(false) });\r\n        children.Items.Add(new KvObject { [\u0022_class\u0022] = new KvString(\u0022ModelModifierList\u0022), [\u0022children\u0022] = modifiers });\r\n        var root = new KvObject { [\u0022_class\u0022] = new KvString(\u0022RootNode\u0022), [\u0022children\u0022] = children, [\u0022base_model_name\u0022] = new KvString(baseModel),\r\n            [\u0022anim_graph_name\u0022] = new KvString(\u0022\u0022), [\u0022model_archetype\u0022] = new KvString(\u0022\u0022), [\u0022primary_associated_entity\u0022] = new KvString(\u0022\u0022) };\r\n        return Kv3.Serialize(new Kv3Document(Header, new KvObject { [\u0022rootNode\u0022] = 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":379744,"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/// \u003Csummary\u003ELegacy PreviewDialog layout, with Third Person/FPS camera controls.\u003C/summary\u003E\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\u003CHandBakedClip\u003E clips) : base(parent)\r\n    {\r\n        Window.WindowTitle=\u0022Preview - \u0022\u002BSystem.IO.Path.GetFileName(target.ModelPath);Window.SetWindowIcon(\u0022preview\u0022);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($\u0022color:{Theme.Red.Hex};\u0022);\r\n        var modeRow=Layout.AddRow();modeRow.Spacing=8;\r\n        var modes=modeRow.Add(new ComboBox(this){MinimumWidth=150});\r\n        modes.AddItem(\u0022Third Person\u0022,\u00223d_rotation\u0022,()=\u003EPreview.Mode=HandPreviewMode.ThirdPerson,selected:true);\r\n        modes.AddItem(\u0022FPS\u0022,\u0022first_page\u0022,()=\u003EPreview.Mode=HandPreviewMode.Fps);\r\n        modeRow.Add(new Button(\u0022Reset camera\u0022,\u0022center_focus_strong\u0022){Clicked=Preview.ResetCamera});\r\n        modeRow.Add(new Button(\u0022\u2212\u0022){Clicked=()=\u003EPreview.Zoom(1.2f)});modeRow.Add(new Button(\u0022\u002B\u0022){Clicked=()=\u003EPreview.Zoom(.8f)});\r\n        modeRow.Add(new Button(\u0022Weapon model\u2026\u0022,\u0022view_in_ar\u0022){Clicked=()=\u003E{var picker=AssetPicker.Create(this,AssetType.Model);picker.OnAssetPicked=assets=\u003E{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(\u0022Authored camera motion\u0022){Value=true});authored.Clicked=()=\u003EPreview.UseAuthoredCamera=authored.Value;\r\n        cameraRow.Add(new Label(this){Text=\u0022FOV:\u0022});var fov=cameraRow.Add(new LineEdit(this){Text=\u002275\u0022,FixedWidth=42});\r\n        cameraRow.Add(new Label(this){Text=\u0022FPS offset X / Y / Z:\u0022});var offsets=Enumerable.Range(0,3).Select(_=\u003EcameraRow.Add(new LineEdit(this){Text=\u00220\u0022,FixedWidth=42})).ToArray();\r\n        cameraRow.Add(new Button(\u0022Apply\u0022){Clicked=()=\u003E{if(float.TryParse(fov.Text,out var f)\u0026\u0026float.IsFinite(f))Preview.FpsFov=f;var values=offsets.Select(e=\u003Efloat.TryParse(e.Text,out var v)\u0026\u0026float.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=\u0022Clip:\u0022});var combo=clipRow.Add(new ComboBox(this){MinimumWidth=220},1);\r\n        for(var i=0;i\u003Cclips.Count;i\u002B\u002B){var clip=clips[i];combo.AddItem(clip.Baked.Name,\u0022movie\u0022,()=\u003EPreview.SetClip(clip),selected:i==0);}\r\n        var transport=Layout.AddRow();transport.Spacing=8;\r\n        var play=transport.Add(new Button(\u0022\u0022,\u0022pause\u0022){FixedWidth=28,FixedHeight=24});play.Clicked=()=\u003E{Preview.Playing=!Preview.Playing;play.Icon=Preview.Playing?\u0022pause\u0022:\u0022play_arrow\u0022;};\r\n        var slider=transport.Add(new FloatSlider(this),1);slider.Minimum=0;slider.OnValueEdited=()=\u003EPreview.Scrub((int)slider.Value);\r\n        var label=transport.Add(new Label(this){Text=\u00220 / 0\u0022,FixedWidth=80});\r\n        var ghost=transport.Add(new Button(\u0022Show source\u0022,\u0022compare\u0022){IsToggle=true,FixedHeight=24});ghost.Clicked=()=\u003EPreview.ShowSource=ghost.IsChecked;\r\n        var skeleton=transport.Add(new Button(\u0022Skeleton\u0022,\u0022polyline\u0022){IsToggle=true,FixedHeight=24});skeleton.Clicked=()=\u003EPreview.SkeletonOnly=skeleton.IsChecked;\r\n        Preview.FrameChanged=frame=\u003E{if(modes.CurrentIndex!=(int)Preview.Mode)modes.CurrentIndex=(int)Preview.Mode;play.Icon=Preview.Playing?\u0022pause\u0022:\u0022play_arrow\u0022;slider.Maximum=Math.Max(0,Preview.FrameCount-1);slider.Value=frame;label.Text=$\u0022{frame\u002B1} / {Preview.FrameCount}\u0022;authored.Enabled=Preview.HasAuthoredCamera;};\r\n        var footer=Layout.AddRow();footer.Spacing=8;footer.AddStretchCell();footer.Add(new Button(\u0022Cancel\u0022){Clicked=Close});\r\n        footer.Add(new Button.Primary(\u0022Looks good - Convert\u0022){Icon=\u0022check\u0022,Tint=Theme.Green,Clicked=()=\u003E{Confirmed?.Invoke();Close();}});\r\n        if(clips.Count\u003E0)Preview.SetClip(clips[0]);\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Editor/FemaleImportVerification.cs","FileName":"FemaleImportVerification.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":379744,"Code":"using System;\nusing System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing HumanoidHandRetargeter.Editor;\npublic static class FemaleImportVerification\n{\n public static async Task Verify()\n {\n  var report=Path.Combine(HandEditorPipeline.Assets,\u0022../female-import-report.json\u0022);\n  try {\n   var target=await HandEditorPipeline.LoadTargetAsync(@\u0022D:\\02_Assets\\Hands\\Female Arms And Hands\\source\\[FPS Female Arms].fbx\u0022);\n   var model=Sandbox.Model.Load(target.ModelPath);\n   \n   var animations=model.AnimationNames.ToArray();\n   if(animations.Length\u003C13)throw new Exception(\u0022Lost embedded animations: \u0022\u002Bstring.Join(\u0022,\u0022,animations));\n   File.WriteAllText(report,System.Text.Json.JsonSerializer.Serialize(new{target.ModelPath,animations,materials=model.Materials.Select(m=\u003Em.ResourcePath)}));\n  }catch(Exception e){File.WriteAllText(report,e.ToString());throw;}\n }\n}\r\n\r\n\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Editor/HandVariantAudit.cs","FileName":"HandVariantAudit.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":379744,"Code":"using System;\nusing System.Linq;\nusing System.IO;\nusing System.Threading.Tasks;\nusing Sandbox;\nusing Editor;\nusing HumanoidHandRetargeter;\nusing HumanoidHandRetargeter.Editor;\npublic static class HandVariantAudit\n{\n public static async Task Build()\n {\n  var original=File.ReadAllText(Path.Combine(HandEditorPipeline.Assets,\u0022models/hand_retargeter/targets/character_fpshands_02_a5bf27a9a7dda367_rig3/hands.vmdl\u0022));\n  for(int i=1;i\u003C=3;i\u002B\u002B)\n  {\n   var text=original.Replace(\u0022name = \\\u0022hands_mesh\\\u0022\u0022, \u0022name = \\\u0022hands_mesh\\\u0022\\n import_filter = { exclude_by_default = true exception_list = [\\\u0022Character_VR_Arms_0\u0022\u002Bi\u002B\u0022\\\u0022] }\u0022);\n   var path=Path.Combine(HandEditorPipeline.Assets,\u0022models/hand_verification/arm_variant_\u0022\u002Bi\u002B\u0022.vmdl\u0022);\n   File.WriteAllText(path,text);if(!await HandEditorPipeline.CompileAsync(path,default))throw new Exception(\u0022Variant failed\u0022);\n  }\n }\n public static string Show(int number)\n {\n  var w=Game.ActiveScene.GetAllComponents\u003CRetargetedWeapon\u003E().First(x=\u003Ex.Enabled);\n  w.HandsModel=Model.Load(\u0022models/hand_verification/arm_variant_\u0022\u002Bnumber\u002B\u0022.vmdl\u0022);\n  w.Deploy(true);\n  return \u0022bones \u0022\u002Bw.HandsModel.BoneCount\u002B\u0022 vertices \u0022\u002Bw.HandsModel.MeshInfo.TotalVertices;\n }\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Editor/ReloadElbowAudit.cs","FileName":"ReloadElbowAudit.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":379744,"Code":"using System;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing System.IO;\nusing Sandbox;\nusing Editor;\nusing HumanoidHandRetargeter.Editor;\nusing HumanoidHandRetargeter.Mapping;\npublic static class ReloadElbowAudit\n{\n public static async Task\u003Cstring\u003E Build()\n {\n  var source=await HandEditorPipeline.LoadSourceAsync(Path.Combine(HandEditorPipeline.Assets,\u0022models/weapons/sbox_smg_mp5/v_mp5.vmdl_c\u0022),15);\n  var target=await HandEditorPipeline.LoadTargetAsync(\u0022models/hand_retargeter/targets/character_fpshands_02_a5bf27a9a7dda367_rig3/hands.vmdl\u0022);\n  if(target.Mapping.NeedsReview)target.Mapping=HandRigDetector.Detect(target.Skeleton,target.Mapping.Hands);\n  var clip=source.Scene.Clips.First(c=\u003Ec.Name==\u0022Reload\u0022);\n  var baked=HandEditorPipeline.Bake(source,clip,target,new());\n  var result=await HandEditorPipeline.ExportWithReportAsync(target,new[]{baked},\u0022models/hand_verification/live_hands.vmdl\u0022,true,true,false,true);\n  File.WriteAllText(Path.Combine(HandEditorPipeline.Assets,\u0022../live-weapon-path.txt\u0022),result.WeaponPrefabs.Single());\n  return result.WeaponPrefabs.Single();\n }\n public static string Validate()\n {\n  var path=File.ReadAllText(Path.Combine(HandEditorPipeline.Assets,\u0022../live-weapon-path.txt\u0022)).Trim();\n  var prefab=AssetSystem.FindByPath(path).LoadResource\u003CPrefabFile\u003E();\n  var scene=Scene.CreateEditorScene();\n  try\n  {\n   using var scope=scene.Push();var go=SceneUtility.GetPrefabScene(prefab).Clone();\n   var component=go.GetComponent\u003CHumanoidHandRetargeter.RetargetedWeapon\u003E();\n   go.GetComponent\u003CHumanoidHandRetargeter.RetargetedWeaponController\u003E().Enabled=false;\n   go.WorldTransform=new Transform(new Vector3(100,30,20),Rotation.FromYaw(70));\n   var flags=System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic;\n   var tick=component.GetType().GetMethod(\u0022Evaluate\u0022,flags);\n   SceneModel Field(string name)=\u003E(SceneModel)component.GetType().GetField(name,flags).GetValue(component);\n   void Step()=\u003Etick.Invoke(component,new object[]{1f/60});\n   component.Deploy(true);for(var f=0;f\u003C240;f\u002B\u002B)Step();\n   var driver=Field(\u0022driver\u0022);var hands=Field(\u0022hands\u0022);var weapon=Field(\u0022weapon\u0022);\n   var setup=HumanoidHandRetargeter.Target.LiveHandSetup.Deserialize(component.LiveCalibration);\n   var profile=setup.Calibrate();var stream=new HumanoidHandRetargeter.Retargeting.HandRetargeter.PoseStream(profile,setup.Motion);\n   var samples=new System.Collections.Generic.List\u003Cobject\u003E(); var actions=new System.Collections.Generic.List\u003Cobject\u003E();var maxError=0f;\n   Vector3[] Snapshot()=\u003Ecomponent.WeaponModel.Bones.AllBones.Select(b=\u003Edriver.GetBoneWorldTransform(b.Index).Position).ToArray();\n   foreach(var action in new[]{\u0022reload\u0022})\n   {\n    component.Aiming=false;component.Sprinting=false;for(var f=0;f\u003C240;f\u002B\u002B)Step();\n    var initial=Snapshot();float motion=0;\n    if(action==\u0022aim\u0022)component.Aiming=true;else if(action==\u0022fire\u0022)component.Attack();else if(action==\u0022reload\u0022)component.Reload();else component.Sprinting=true;\n    for(var f=0;f\u003C360;f\u002B\u002B)\n    {\n     Step(); if(action==\u0022reload\u0022) samples.Add(new{frame=f, source=component.WeaponModel.Bones.AllBones.Where(b=\u003Eb.Name.Contains(\u0022arm\u0022,StringComparison.OrdinalIgnoreCase)||b.Name.Contains(\u0022hand\u0022,StringComparison.OrdinalIgnoreCase)||b.Name.Contains(\u0022elbow\u0022,StringComparison.OrdinalIgnoreCase)||b.Name.Contains(\u0022shoulder\u0022,StringComparison.OrdinalIgnoreCase)).Select(b=\u003Enew{name=b.Name,position=driver.GetBoneWorldTransform(b.Index).Position,rotation=driver.GetBoneWorldTransform(b.Index).Rotation}).ToArray(), target=component.HandsModel.Bones.AllBones.Where(b=\u003Eb.Name.Contains(\u0022arm\u0022,StringComparison.OrdinalIgnoreCase)||b.Name.Contains(\u0022hand\u0022,StringComparison.OrdinalIgnoreCase)||b.Name.Contains(\u0022elbow\u0022,StringComparison.OrdinalIgnoreCase)||b.Name.Contains(\u0022shoulder\u0022,StringComparison.OrdinalIgnoreCase)).Select(b=\u003Enew{name=b.Name,position=go.WorldTransform.PointToLocal(hands.GetBoneWorldTransform(b.Index).Position),rotation=hands.GetBoneWorldTransform(b.Index).Rotation}).ToArray()});var current=Snapshot();motion=Math.Max(motion,current.Select((v,i)=\u003EVector3.DistanceBetween(v,initial[i])).Max());\n     var worlds=profile.Source.Bones.Select(b=\u003EHandEditorPipeline.FromEngine(driver.GetBoneWorldTransform(component.WeaponModel.Bones.GetBone(b.Name).Index))).ToArray();\n     var locals=profile.Source.Bones.Select(b=\u003Eb.ParentIndex\u003C0?worlds[b.Index]:HumanoidHandRetargeter.Maths.XForm.ToLocal(worlds[b.ParentIndex],worlds[b.Index])).ToArray();\n     var expected=stream.Step(new HumanoidHandRetargeter.Skeleton.Pose(locals)).ToWorld(profile.Target);\n     foreach(var bone in profile.Target.Bones)\n     {\n      var pose=expected[bone.Index];pose.Pos-=setup.Motion.WeaponSpaceOffset??System.Numerics.Vector3.Zero;\n      var local=HandEditorPipeline.ToEngine(pose);local.Position-=component.ViewOrigin;\n      var position=go.WorldTransform.PointToWorld(local.Position);\n      maxError=Math.Max(maxError,Vector3.DistanceBetween(position,hands.GetBoneWorldTransform(component.HandsModel.Bones.GetBone(bone.Name).Index).Position));\n     }\n     foreach(var bone in component.WeaponModel.Bones.AllBones)\n     {\n      var position=go.WorldTransform.PointToWorld(driver.GetBoneWorldTransform(bone.Index).Position-component.ViewOrigin);\n      maxError=Math.Max(maxError,Vector3.DistanceBetween(position,weapon.GetBoneWorldTransform(bone.Index).Position));\n     }\n    }\n    if(motion\u003C.01f)throw new Exception(\u0022Original graph action did not animate: \u0022\u002Baction);\n    actions.Add(new{action,motion});\n   }\n   if(driver.AnimationGraph.ParamCount\u003C10||!Enumerable.Range(0,driver.AnimationGraph.ParamCount).Select(driver.AnimationGraph.GetParameterName).Contains(\u0022ironsights\u0022))throw new Exception(\u0022Original graph parameters were lost\u0022);\n   if(maxError\u003E.003f)throw new Exception(\u0022Live followers diverged: \u0022\u002BmaxError);\n   var result=System.Text.Json.JsonSerializer.Serialize(new{samples,passed=true,graph=driver.AnimationGraph.Name,parameters=driver.AnimationGraph.ParamCount,actions,maxError});\n   File.WriteAllText(Path.Combine(HandEditorPipeline.Assets,\u0022../reload-elbow-audit.json\u0022),result);return result;\n  }\n  catch(Exception e){File.WriteAllText(Path.Combine(HandEditorPipeline.Assets,\u0022../reload-elbow-audit.json\u0022),e.ToString());throw;}\n  finally{scene.Destroy();}\n }}\r\n\r\n\r\n\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Editor/SharedFitLiveAudit2.cs","FileName":"SharedFitLiveAudit2.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":379744,"Code":"using System;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing System.IO;\nusing Sandbox;\nusing Editor;\nusing HumanoidHandRetargeter.Editor;\nusing HumanoidHandRetargeter.Mapping;\npublic static class SharedFitLiveAudit2\n{\n public static async Task\u003Cstring\u003E Build()\n {\n  var source=await HandEditorPipeline.LoadSourceAsync(Path.Combine(HandEditorPipeline.Assets,\u0022models/weapons/sbox_smg_mp5/v_mp5.vmdl_c\u0022),15);\n  var target=await HandEditorPipeline.LoadTargetAsync(\u0022models/hand_retargeter/targets/character_fpshands_02_a5bf27a9a7dda367_rig3/hands.vmdl\u0022);\n  if(target.Mapping.NeedsReview)target.Mapping=HandRigDetector.Detect(target.Skeleton,target.Mapping.Hands);\n  var clip=source.Scene.Clips.First(c=\u003Ec.Name==\u0022Reload\u0022);\n  var baked=HandEditorPipeline.Bake(source,clip,target,new());\n  var result=await HandEditorPipeline.ExportWithReportAsync(target,new[]{baked},\u0022models/hand_verification/live_hands.vmdl\u0022,true,true,false,true);\n  File.WriteAllText(Path.Combine(HandEditorPipeline.Assets,\u0022../live-weapon-path.txt\u0022),result.WeaponPrefabs.Single());\n  return result.WeaponPrefabs.Single();\n }\n public static string Validate()\n {\n  var path=\u0022models/hand_verification/Arms_shared_fit_reload_weapons/v_mp5_d68a0afb/007d06a1ca6569c0/weapon.prefab\u0022;\n  var prefab=AssetSystem.FindByPath(path).LoadResource\u003CPrefabFile\u003E();\n  var scene=Scene.CreateEditorScene();\n  try\n  {\n   using var scope=scene.Push();var go=SceneUtility.GetPrefabScene(prefab).Clone();\n   var component=go.GetComponent\u003CHumanoidHandRetargeter.RetargetedWeapon\u003E();\n   go.GetComponent\u003CHumanoidHandRetargeter.RetargetedWeaponController\u003E().Enabled=false;\n   go.WorldTransform=new Transform(new Vector3(100,30,20),Rotation.FromYaw(70));\n   var flags=System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic;\n   var tick=component.GetType().GetMethod(\u0022Evaluate\u0022,flags);\n   SceneModel Field(string name)=\u003E(SceneModel)component.GetType().GetField(name,flags).GetValue(component);\n   void Step()=\u003Etick.Invoke(component,new object[]{1f/60});\n   component.Deploy(true);for(var f=0;f\u003C240;f\u002B\u002B)Step();\n   var driver=Field(\u0022driver\u0022);var hands=Field(\u0022hands\u0022);var weapon=Field(\u0022weapon\u0022);\n   var setup=HumanoidHandRetargeter.Target.LiveHandSetup.Deserialize(component.LiveCalibration);\n   var profile=setup.Calibrate();var stream=new HumanoidHandRetargeter.Retargeting.HandRetargeter.PoseStream(profile,setup.Motion);\n   var actions=new System.Collections.Generic.List\u003Cobject\u003E();var maxError=0f;\n   Vector3[] Snapshot()=\u003Ecomponent.WeaponModel.Bones.AllBones.Select(b=\u003Edriver.GetBoneWorldTransform(b.Index).Position).ToArray();\n   foreach(var action in new[]{\u0022aim\u0022,\u0022fire\u0022,\u0022reload\u0022,\u0022sprint\u0022})\n   {\n    component.Aiming=false;component.Sprinting=false;for(var f=0;f\u003C240;f\u002B\u002B)Step();\n    var initial=Snapshot();float motion=0;\n    if(action==\u0022aim\u0022)component.Aiming=true;else if(action==\u0022fire\u0022)component.Attack();else if(action==\u0022reload\u0022)component.Reload();else component.Sprinting=true;\n    for(var f=0;f\u003C180;f\u002B\u002B)\n    {\n     Step();var current=Snapshot();motion=Math.Max(motion,current.Select((v,i)=\u003EVector3.DistanceBetween(v,initial[i])).Max());\n     var worlds=profile.Source.Bones.Select(b=\u003EHandEditorPipeline.FromEngine(driver.GetBoneWorldTransform(component.WeaponModel.Bones.GetBone(b.Name).Index))).ToArray();\n     var locals=profile.Source.Bones.Select(b=\u003Eb.ParentIndex\u003C0?worlds[b.Index]:HumanoidHandRetargeter.Maths.XForm.ToLocal(worlds[b.ParentIndex],worlds[b.Index])).ToArray();\n     var expected=stream.Step(new HumanoidHandRetargeter.Skeleton.Pose(locals)).ToWorld(profile.Target);\n     foreach(var bone in profile.Target.Bones)\n     {\n      var pose=expected[bone.Index];pose.Pos-=setup.Motion.WeaponSpaceOffset??System.Numerics.Vector3.Zero;\n      var local=HandEditorPipeline.ToEngine(pose);local.Position-=component.ViewOrigin;\n      var position=go.WorldTransform.PointToWorld(local.Position);\n      maxError=Math.Max(maxError,Vector3.DistanceBetween(position,hands.GetBoneWorldTransform(component.HandsModel.Bones.GetBone(bone.Name).Index).Position));\n     }\n     foreach(var bone in component.WeaponModel.Bones.AllBones)\n     {\n      var position=go.WorldTransform.PointToWorld(driver.GetBoneWorldTransform(bone.Index).Position-component.ViewOrigin);\n      maxError=Math.Max(maxError,Vector3.DistanceBetween(position,weapon.GetBoneWorldTransform(bone.Index).Position));\n     }\n    }\n    if(motion\u003C.01f)throw new Exception(\u0022Original graph action did not animate: \u0022\u002Baction);\n    actions.Add(new{action,motion});\n   }\n   if(driver.AnimationGraph.ParamCount\u003C10||!Enumerable.Range(0,driver.AnimationGraph.ParamCount).Select(driver.AnimationGraph.GetParameterName).Contains(\u0022ironsights\u0022))throw new Exception(\u0022Original graph parameters were lost\u0022);\n   if(maxError\u003E.003f)throw new Exception(\u0022Live followers diverged: \u0022\u002BmaxError);\n   var result=System.Text.Json.JsonSerializer.Serialize(new{passed=true,graph=driver.AnimationGraph.Name,parameters=driver.AnimationGraph.ParamCount,actions,maxError});\n   File.WriteAllText(Path.Combine(HandEditorPipeline.Assets,\u0022../shared-fit-live-validation-2.json\u0022),result);return result;\n  }\n  catch(Exception e){File.WriteAllText(Path.Combine(HandEditorPipeline.Assets,\u0022../shared-fit-live-validation-2.json\u0022),e.ToString());throw;}\n  finally{scene.Destroy();}\n }}\r\n\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Editor/SimpleArmsVerification.cs","FileName":"SimpleArmsVerification.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":379744,"Code":"using System;\nusing System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing HumanoidHandRetargeter.Editor;\npublic static class SimpleArmsVerification\n{\n public static async Task Verify()\n {\n  var s=await HandEditorPipeline.LoadSourceAsync(\u0022models/weapons/sbox_smg_mp5/v_mp5.vmdl\u0022,30);\n  var reports=new System.Collections.Generic.List\u003Cobject\u003E();\n  foreach(var path in new[]{@\u0022D:\\02_Assets\\Hands\\simple-arms\\source\\Arms.fbx\u0022})\n  {\n   var t=await HandEditorPipeline.LoadTargetAsync(path);var b=HandEditorPipeline.Bake(s,s.Scene.Clips.First(c=\u003Ec.Name==\u0022Reload\u0022),t,new());\n   var label=HandEditorPipeline.SafeName(Path.GetFileNameWithoutExtension(path));\n   var output=await HandEditorPipeline.ExportWithReportAsync(t,new[]{b},\u0022models/hand_verification/\u0022\u002Blabel\u002B\u0022_simple_elbow_reload.vmdl\u0022,true,true,false,true);\n   var widget=new HandPreviewWidget(null,t);widget.SetClip(b);widget.SetWeapon(s.ModelPath);widget.Mode=HandPreviewMode.Fps;\n   foreach(var f in new[]{0,40,65}){widget.Scrub(f);File.WriteAllBytes(Path.Combine(HandEditorPipeline.Assets,\u0022../fixed_\u0022\u002Blabel\u002B\u0022_\u0022\u002Bf\u002B\u0022.png\u0022),widget.RenderToPng(768));}widget.Destroy();\n   reports.Add(new{t.ModelPath,t.ImportNotes,output,fit=HumanoidHandRetargeter.Target.HandTargetFit.Analyze(t.Skeleton,t.Mapping),hands=t.Mapping.Hands.Select(h=\u003Enew{h.Side,shoulder=t.Skeleton.RestWorld[h.UpperArm.Value].Pos.ToString()})});\n  }\n  File.WriteAllText(Path.Combine(HandEditorPipeline.Assets,\u0022../simple-arms-report.json\u0022),System.Text.Json.JsonSerializer.Serialize(reports));\n }\n}\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Editor/UntexturedArmsVerification.cs","FileName":"UntexturedArmsVerification.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":379744,"Code":"using System;\nusing System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing HumanoidHandRetargeter.Editor;\npublic static class UntexturedArmsVerification\n{\n public static async Task Verify()\n {\n  var t=await HandEditorPipeline.LoadTargetAsync(@\u0022D:\\02_Assets\\Hands\\UnrealEngineFPSOnlyUEFN.fbx\u0022);\n  var model=Sandbox.Model.Load(t.ModelPath);\n  File.WriteAllText(Path.Combine(HandEditorPipeline.Assets,\u0022../untextured-arms-report.json\u0022),System.Text.Json.JsonSerializer.Serialize(new{t.ModelPath,t.ImportNotes,bones=model.BoneCount,materials=model.Materials.Select(m=\u003Enew{m.ResourcePath,m.IsError})}));\n }\n}\r\n\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Editor/HumanoidHandRetargeter/HandEditorPipeline.cs","FileName":"HandEditorPipeline.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":379744,"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\u003CHandSide, Quat\u003E? PalmFrames { get; set; }\r\n}\r\n\r\npublic sealed class HandTarget\n{\n    public IReadOnlyList\u003Cstring\u003E ImportNotes { get; init; } = Array.Empty\u003Cstring\u003E();\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\u003CHandSide, Quat\u003E? PalmFrames { get; set; }\r\n}\r\n\r\npublic sealed record HandExportResult(string ModelPath,IReadOnlyList\u003Cstring\u003E Changes,string? BackupPath,IReadOnlyList\u003Cstring\u003E? WeaponPrefabs=null);\n\r\npublic sealed record HandBakedClip(HandSource Source, Clip Original, Clip Baked, IReadOnlyList\u003Cstring\u003E Notes,Vec? WeaponOffset=null,HandMotionOptions? MotionOptions=null);\n\r\n/// \u003Csummary\u003EEditor 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.\u003C/summary\u003E\r\npublic static class HandEditorPipeline\r\n{\r\n    public const string HumanArms = \u0022models/first_person/v_first_person_arms_human.vmdl\u0022;\r\n    public const string CitizenArms = \u0022models/first_person/v_first_person_arms_citizen.vmdl\u0022;\r\n    public static string Assets =\u003E Project.Current?.GetAssetsPath() ?? throw new InvalidOperationException(\u0022Open an s\u0026box project first.\u0022);\r\n    public static MainThreadAwaitable MainThread() =\u003E default;\r\n    public readonly struct MainThreadAwaitable : INotifyCompletion\r\n    {\r\n        public MainThreadAwaitable GetAwaiter() =\u003E this;\r\n        public bool IsCompleted =\u003E ThreadSafe.IsMainThread;\r\n        public void OnCompleted(Action continuation) =\u003E Sandbox.MainThread.Queue(continuation);\r\n        public void GetResult() { }\r\n    }\r\n\r\n    public static Transform ToEngine(XForm value)\r\n        =\u003E 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        =\u003E 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 =\u003E {\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\u003CHandTarget\u003E LoadTargetAsync(string path, CancellationToken cancel = default)\r\n    {\r\n        await MainThread();\r\n        var importNotes=new List\u003Cstring\u003E();\n        if (path.EndsWith(\u0022.fbx\u0022,StringComparison.OrdinalIgnoreCase)) path = await PrepareMeshAsync(path,cancel,importNotes);\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($\u0022Cannot load a skinned model from \u0027{path}\u0027.\u0022);\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) };\n    }\r\n\r\n    public static async Task\u003CHandSource\u003E LoadSourceAsync(string path, float fps = 30, CancellationToken cancel = default)\r\n    {\r\n        if (path.EndsWith(\u0022.fbx\u0022,StringComparison.OrdinalIgnoreCase))\r\n        {\r\n            var scene = await Task.Run(()=\u003EFbxImporter.Import(File.ReadAllBytes(path),new FbxImportOptions {SampleFps=fps}),cancel);\r\n            await MainThread();\r\n            return new() { Path=path, Scene=scene, PalmFrames=HandPresetStore.LoadPalms(scene.Skeleton), Mapping=HandPresetStore.Load(scene.Skeleton) ?? 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($\u0022Cannot load source model \u0027{path}\u0027.\u0022);\r\n        var skeleton = ReadSkeleton(model);\r\n        var clips = new List\u003CClip\u003E();\r\n        using var sampling = new ModelSampler(model,skeleton);\r\n        foreach (var name in model.AnimationNames.ToArray())\n        {\r\n            cancel.ThrowIfCancellationRequested();\r\n            clips.Add(sampling.Sample(name,fps,cancel));\r\n            await Task.Delay(1,cancel); await MainThread();\n        }\n        // Facepunch weapons author recoil as additive layers. Capture their complete firing\n        // pose through the original weapon graph instead of retargeting a delta as a full pose.\n        if(skeleton.IndexOf(\u0022weapon_root\u0022)\u003E=0\u0026\u0026!clips.Any(c=\u003EWeaponAnimGraph.Classify(c.Name)==WeaponAction.Fire)\n            \u0026\u0026clips.FirstOrDefault(c=\u003Ec.Name.StartsWith(\u0022Fire_\u0022,StringComparison.OrdinalIgnoreCase)\u0026\u0026c.Name.EndsWith(\u0022_delta\u0022,StringComparison.OrdinalIgnoreCase)\n                \u0026\u0026!c.Name.Contains(\u0022Hold\u0022,StringComparison.OrdinalIgnoreCase)\u0026\u0026!c.Name.Contains(\u0022Dry\u0022,StringComparison.OrdinalIgnoreCase)) is {} recoil)\n        {\n            var firing=sampling.SampleFire(Math.Max(.25f,(recoil.FrameCount-1)/recoil.Fps),fps,cancel);\n            if(firing is not null)clips.Add(firing);\n        }\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        =\u003E 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\u0026box models already share X-forward/Z-up coordinates. A wrist\u0027s\r\n        // bind rotation must not turn vertical reload travel into sideways/downward motion.\r\n        var weaponOffset=options.WeaponSpaceOffset;\n        if(source.ModelPath is not null\u0026\u0026options.PreserveWeaponGrip\u0026\u0026options.TransferWristPosition\n            \u0026\u0026source.Scene.Skeleton.Bones.Any(b=\u003Eb.Name.Equals(\u0022weapon_root\u0022,StringComparison.OrdinalIgnoreCase)))\n            weaponOffset ??= HandViewSpace.EyePosition(target.Skeleton,target.Mapping)-HandViewSpace.EyePosition(source.Scene.Skeleton,source.Mapping);\n        options=new HandMotionOptions{TransferWristPosition=options.TransferWristPosition,ScaleWristTravel=options.ScaleWristTravel,\n            SolveArmIk=options.SolveArmIk,PreserveWeaponGrip=options.PreserveWeaponGrip,WeaponSpaceOffset=weaponOffset,\n            WristTravelBasis=options.WristTravelBasis??(source.ModelPath is not null?Quat.Identity:null)};\n        var notes=weaponOffset.HasValue?profile.Notes.Concat(new[]{\u0022Preserved both palm grip anchors in one fixed weapon space; weapon motion is not scaled per arm.\u0022}).ToArray():profile.Notes;\n        return new(source,clip,HandRetargeter.Bake(profile,clip,options,cancel),notes,weaponOffset,options);\n    }\r\n\r\n    public static async Task\u003Cstring\u003E ExportAsync(HandTarget target,IReadOnlyList\u003CHandBakedClip\u003E clips,string outputModel,\r\n        bool autoGraph,bool backup,bool weaponCompatible,bool preserveSourceTracks,CancellationToken cancel=default)\r\n        =\u003E (await ExportWithReportAsync(target,clips,outputModel,autoGraph,backup,weaponCompatible,preserveSourceTracks,cancel)).ModelPath;\r\n\r\n    public static async Task\u003CHandExportResult\u003E ExportWithReportAsync(HandTarget target, IReadOnlyList\u003CHandBakedClip\u003E 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(\u0022Select at least one animation to export.\u0022);\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(\u0022Weapon-compatible mode requires a weapon VMDL source so its animation owner can be verified.\u0022);\r\n                var missing=target.Mapping.Hands.SelectMany(h=\u003Enew[]{h.Wrist}.Concat(h.Digits.SelectMany(d=\u003Ed.Bones))).Select(i=\u003Etarget.Skeleton[i].Name).Where(n=\u003Eclip.Source.Scene.Skeleton.IndexOf(n)\u003C0).ToArray();\r\n                if(missing.Length\u003E0)throw new InvalidOperationException(\u0022These target bones cannot bonemerge onto the selected weapon: \u0022\u002Bstring.Join(\u0022, \u0022,missing)\u002B\u0022. Use standalone export for this custom rig, or choose a weapon whose skeleton contains those bones.\u0022);\r\n            }\r\n        }\r\n        outputModel = VmdlSetupService.NormalizeAssetPath(outputModel,\u0022.vmdl\u0022);\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(\u0022The existing output model must compile before animations can be added.\u0022);\r\n            var existingRig=ReadSkeleton(existingModel);\r\n            if(existingRig.Count!=target.Skeleton.Count||target.Skeleton.Bones.Any(b=\u003EexistingRig.IndexOf(b.Name)\u003C0))throw new InvalidOperationException(\u0022The existing output model has a different skeleton. Choose the target model or a new output path.\u0022);\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] \u002B \u0022_animations\u0022;\r\n        var files = new Dictionary\u003Cstring,string\u003E(StringComparer.OrdinalIgnoreCase);\r\n        var entries = new List\u003CHandAnimationEntry\u003E();\r\n        var names = new HashSet\u003Cstring\u003E(StringComparer.OrdinalIgnoreCase) { \u0022bindPose\u0022 };\r\n        // A compiled target is in inches. Existing model scaling controls the animation\r\n        // compiler\u0027s input units; new wrappers use the inherited cm-to-inch modifier.\r\n        var factor = ExportPositionFactor(original);\n        var turn=ExportRootRotation(original);\n        XForm ExportLocal(XForm value,int bone)=\u003Etarget.Skeleton[bone].ParentIndex\u003C0\n            ?new XForm(Vec.Transform(value.Pos*factor,turn),Quat.Normalize(turn*value.Rot))\n            :new XForm(value.Pos*factor,value.Rot);\n        Clip ExportUnits(Clip clip) =\u003E new(clip.Name,clip.Fps,clip.Looping,clip.Frames.Select(frame=\u003Eframe.Select(ExportLocal).ToArray()).ToList(),clip.NativeFps);\n        var exportSkeleton = Skel.Create(target.Skeleton.Bones.Select(b=\u003Enew BoneDefinition(b.Name,b.ParentIndex\u003C0?null:target.Skeleton[b.ParentIndex].Name,ExportLocal(b.RestLocal,b.Index))).ToArray());\n        var bind = new Clip(\u0022bindPose\u0022,30,false,new() { Pose.Rest(exportSkeleton).Locals });\r\n        files[folder\u002B\u0022/bind.dmx\u0022] = DmxWriter.Write(exportSkeleton,bind,new() {Name=\u0022bindPose\u0022,UpAxisY=false,ForwardParity=1});\r\n        foreach(var clip in clips)\r\n        {\r\n            var stem=SourceKey(clip.Source)\u002B\u0022_\u0022\u002BSafeName(clip.Baked.Name); var name=stem; var suffix=1;\n            while(!names.Add(name)) name=stem\u002B\u0022_\u0022\u002B(\u002B\u002Bsuffix);\r\n            var content=DmxWriter.Write(exportSkeleton,ExportUnits(clip.Baked),new() {Name=name,UpAxisY=false,ForwardParity=1,ChannelExcludedBones=target.Mapping.Hands.SelectMany(h=\u003Eh.TwistOrHelperBones).ToHashSet()});\n            var file=folder\u002B\u0022/\u0022\u002Bname\u002B\u0022_\u0022\u002BContentKey(content)\u002B\u0022.dmx\u0022;\n            files[file]=content;\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\u002B\u0022_source_tracks\u0022,UpAxisY=clip.Source.Scene.UpAxis==1,ForwardParity=clip.Source.ModelPath is null?2:1});\n                files[folder\u002B\u0022/source_tracks/\u0022\u002Bname\u002B\u0022_\u0022\u002BContentKey(tracks)\u002B\u0022.dmx\u0022]=tracks;\n            }\r\n        }\r\n        var prepared=VmdlSetupService.Prepare(original,entries,new() {ModelPath=outputModel,BindPoseSource=folder\u002B\u0022/bind.dmx\u0022,AutoConfigureAnimGraph=autoGraph,WeaponCompatibleArms=weaponCompatible});\n        var generatedAssets=new Dictionary\u003Cstring,string\u003E(StringComparer.OrdinalIgnoreCase);\n        var bundles=autoGraph?HandWeaponExport.Prepare(target,clips,outputModel,files,generatedAssets,cancel):Array.Empty\u003CHandWeaponExport.Bundle\u003E();\n        foreach(var bundle in bundles)generatedAssets[bundle.PrefabPath]=HandWeaponExport.Prefab(bundle);\n        var committed=await VmdlSetupTransaction.CommitAsync(Assets,outputModel,existing,prepared,files,async(paths,token)=\u003E{\n            await MainThread(); foreach(var path in paths) AssetSystem.RegisterFile(path);\n            foreach(var bundle in bundles)\n            {\n                if(bundle.LiveCalibration is null)\n                {\n                    if(!await CompileAsync(System.IO.Path.Combine(Assets,bundle.ModelPath),token))throw new InvalidOperationException(\u0022Weapon animation model did not compile: \u0022\u002Bbundle.ModelPath);\n                    await MainThread();\n                    await Task.Delay(100,token);await MainThread();\n                    var owner=Model.Load(bundle.ModelPath);var graph=AnimationGraph.Load(bundle.GraphPath);\n                    if(owner is null||owner.IsError||!owner.HasRenderMeshes()||owner.BoneCount\u003Cbundle.BoneCount||graph is null||graph.IsError\n                        ||!bundle.Actions.All(a=\u003Eowner.AnimationNames.Contains(a.Sequence)))throw new InvalidOperationException($\u0022Weapon 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(\u0022,\u0022,owner?.AnimationNames??Array.Empty\u003Cstring\u003E())}.\u0022);\n                    if(target.Skeleton.Bones.Any(b=\u003Eowner.Bones.GetBone(WeaponClipBuilder.HandPrefix\u002Bb.Name) is null))return false;\n                }\n\n                if(!await CompileAsync(System.IO.Path.Combine(Assets,bundle.PrefabPath),token))throw new InvalidOperationException(\u0022Weapon prefab did not compile: \u0022\u002Bbundle.PrefabPath);\n                await MainThread();\n                // The compiled file reaches disk before the editor\u0027s asset record updates.\n                // Managed resource loading uses that record, not File.Exists.\n                var prefabAsset=AssetSystem.FindByPath(bundle.PrefabPath);\n                for(var wait=0;wait\u003C50\u0026\u0026string.IsNullOrEmpty(prefabAsset.GetCompiledFile(true));wait\u002B\u002B)\n                {await Task.Delay(100,token);await MainThread();}\n                var prefab=prefabAsset.LoadResource\u003CPrefabFile\u003E();\n                if(prefab is null||prefab.IsError||SceneUtility.GetPrefabScene(prefab) is null)throw new InvalidOperationException(\u0022Weapon prefab could not be loaded: \u0022\u002Bbundle.PrefabPath);\n            }\n            if(!await CompileAsync(absolute,token)) return false;\n            await MainThread(); var model=Model.Load(outputModel);\n            // The compiled file can arrive before an already-loaded model refreshes\n            // its sequence list. Re-exporting changed motion adds a preserved-name\n            // suffix, so wait for those new sequences before validating the bake.\n            var modelDeadline=DateTime.UtcNow.AddSeconds(15);\n            while(model is not null\u0026\u0026!model.IsError\u0026\u0026!prepared.Animations.All(a=\u003Emodel.AnimationNames.Contains(a.SequenceName))\n                \u0026\u0026DateTime.UtcNow\u003CmodelDeadline)\n            {\n                await Task.Delay(50,token);await MainThread();model=Model.Load(outputModel);\n            }\n            if(model is null || model.IsError || !prepared.Animations.All(a=\u003Emodel.AnimationNames.Contains(a.SequenceName)))return false;\n            if(prepared.GeneratedGraphPath is { } graphPath){var graph=AnimationGraph.Load(graphPath);if(graph is null||graph.IsError)return false;}\r\n\n            var compiledRig=ReadSkeleton(model);\r\n            using var sampler=new ModelSampler(model,compiledRig);\r\n            var checkedBones=target.Mapping.Hands.SelectMany(h=\u003Enew int?[]{h.Clavicle,h.UpperArm,h.Forearm,h.Wrist}.Where(i=\u003Ei.HasValue).Select(i=\u003Ei!.Value).Concat(h.Digits.SelectMany(d=\u003Ed.Bones))).Distinct().ToArray();\r\n            for(var i=0;i\u003Cclips.Count;i\u002B\u002B)\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\u003E1\u0026\u0026actualClip.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\u003C0 || Vec.Distance(expectedWorld[bone].Pos,actualWorld[index].Pos)\u003E.1f\r\n                            || MathQ.AngleBetween(expectedWorld[bone].Rot,actualWorld[index].Rot)\u003E.02f)\r\n                            throw new InvalidOperationException($\u0022Compiled sequence \u0027{expectedClip.Name}\u0027 differs from the preview at \u0027{target.Skeleton[bone].Name}\u0027, frame {frame}. The export was rolled back.\u0022);\r\n                    }\r\n                }\r\n            }\r\n            return true;\r\n        },backup,cancel,generatedAssets);\n        return new(outputModel,prepared.Changes.Concat(bundles.Select(b=\u003Eb.LiveCalibration is not null\n            ? $\u0022Preserved complete source graph {b.GraphPath} with live custom-hand retargeting: {b.PrefabPath}\u0022\n            : $\u0022Created weapon graph ({string.Join(\u0022, \u0022,b.Actions.Select(a=\u003Ea.Action))}) and synchronized hands/weapon prefab: {b.PrefabPath}\u0022)).ToArray(),committed.BackupPath,bundles.Select(b=\u003Eb.PrefabPath).ToArray());\n    }\r\n\r\n    private static float ExportPositionFactor(string text)\n    {\r\n        float scale=1;\r\n        void Walk(KvValue v) { if(v is KvObject o) { if(o.GetString(\u0022_class\u0022)==\u0022ModelModifier_ScaleAndMirror\u0022 \u0026\u0026 o.GetOrNull(\u0022scale\u0022) 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\u003C=0) throw new InvalidOperationException(\u0022Target model has an unsupported scale modifier.\u0022);\r\n        return 1f/(2.54f*scale);\n    }\n\n    private static Quat ExportRootRotation(string text)\n    {\n        var turn=false;\n        void Walk(KvValue value)\n        {\n            if(value is KvObject node)\n            {\n                if(node.GetString(\u0022_class\u0022)==\u0022ModelModifier_ScaleAndMirror\u0022\n                    \u0026\u0026node.GetOrNull(\u0022mirror_x\u0022) is KvBool {Value:true}\n                    \u0026\u0026node.GetOrNull(\u0022mirror_y\u0022) is KvBool {Value:true}\n                    \u0026\u0026node.GetOrNull(\u0022mirror_z\u0022) is not KvBool {Value:true})turn=!turn;\n                foreach(var key in node.Keys)Walk(node[key]);\n            }\n            else if(value is KvArray array)foreach(var child in array.Items)Walk(child);\n        }\n        Walk(Kv3.Parse(text).Root);\n        return turn?Quat.CreateFromAxisAngle(Vec.UnitZ,MathF.PI):Quat.Identity;\n    }\n\r\n    private static async Task\u003Cstring\u003E PrepareMeshAsync(string file, CancellationToken cancel,List\u003Cstring\u003E importNotes)\n    {\r\n        var bytes=File.ReadAllBytes(file);\r\n        var materials=await Task.Run(()=\u003EFbxMaterialAssets.Inspect(file,bytes),cancel);\n        importNotes.AddRange(materials.Notes);\n        var hash=materials.Signature;\r\n        var folder=\u0022models/hand_retargeter/targets/\u0022\u002BSafeName(System.IO.Path.GetFileNameWithoutExtension(file))\u002B\u0022_\u0022\u002Bhash\u002B\u0022_rig9\u0022;\n        var mesh=folder\u002B\u0022/hands.fbx\u0022; var model=folder\u002B\u0022/hands.vmdl\u0022;\r\n        var scene=await Task.Run(()=\u003EFbxImporter.Import(bytes,new(){SampleFps=(float)FbxScene.Build(FbxTokenizer.Parse(bytes)).FrameRate}),cancel);\n        var embeddedClips=scene.Clips;\n        var repaired=await Task.Run(()=\u003E\n        {\n            var result=FbxBindPoseFixer.TryFix(bytes,out var report);\n            return (Bytes:result,Report:report);\n        },cancel);\n        if(repaired.Bytes is {} repairedBytes)\n        {\n            var repairedScene=await Task.Run(()=\u003EFbxImporter.Import(repairedBytes),cancel);\n            if(!scene.Skeleton.Bones.Select(b=\u003E(b.Name,b.ParentIndex)).SequenceEqual(repairedScene.Skeleton.Bones.Select(b=\u003E(b.Name,b.ParentIndex))))\n                throw new InvalidOperationException(\u0022Bind-pose repair changed the target hierarchy; the original FBX was not modified.\u0022);\n            scene=repairedScene;bytes=repairedBytes;\n            importNotes.Add(\u0022Repaired the imported copy from its authored bind pose: \u0022\u002Brepaired.Report);\n        }\n        var meshNames=FbxSkinnedMeshes.ReadNames(bytes);\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);\n        var isNew=!File.Exists(modelAbs);\n        if(isNew)\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(\u0027#\u0027);return suffix\u003C0?name:name[..suffix]\u002B\u0022_duplicate\u0022;}\r\n            var rig=Skel.Create(scene.Skeleton.Bones.Select(b=\u003Enew BoneDefinition(EngineName(b.Name),b.ParentIndex\u003C0?null:EngineName(scene.Skeleton[b.ParentIndex].Name),b.RestLocal)).ToArray());\r\n            var bindPath=folder\u002B\u0022/mesh_bind.dmx\u0022;\r\n            File.WriteAllText(System.IO.Path.Combine(Assets,bindPath),DmxWriter.Write(rig,new Clip(\u0022bindPose\u0022,30,false,new(){Pose.Rest(rig).Locals}),new(){Name=\u0022bindPose\u0022,UpAxisY=scene.UpAxis==1}));\r\n            var entries=new List\u003CHandAnimationEntry\u003E();\n            var names=new HashSet\u003Cstring\u003E(StringComparer.OrdinalIgnoreCase){\u0022bindPose\u0022};\n            for(var i=0;i\u003CembeddedClips.Count;i\u002B\u002B)\n            {\n                cancel.ThrowIfCancellationRequested();\n                var clip=embeddedClips[i];\n                var stem=SafeName(clip.Name).Replace(\u0027-\u0027,\u0027_\u0027);\n                var name=stem;var suffix=2;\n                while(!names.Add(name))name=stem\u002B\u0022_\u0022\u002Bsuffix\u002B\u002B;\n                var path=folder\u002B\u0022/embedded_\u0022\u002Bi\u002B\u0022.dmx\u0022;\n                // Imported locals are already centimeters, matching mesh_bind and the\n                // model\u0027s final unit conversion. Preserve authored takes without IK.\n                File.WriteAllText(System.IO.Path.Combine(Assets,path),DmxWriter.Write(rig,clip,new(){Name=name,UpAxisY=scene.UpAxis==1}));\n                AssetSystem.RegisterFile(System.IO.Path.Combine(Assets,path));\n                entries.Add(new(name,path,clip.Looping));\n            }\n            var prepared=VmdlSetupService.Prepare(HandModelFactory.Create(mesh:mesh,meshUnitScaleCm:scene.UnitScaleCm,materialRemaps:remaps,meshNames:meshNames),entries,new(){ModelPath=model,BindPoseSource=bindPath,AutoConfigureAnimGraph=false});\n            File.WriteAllText(modelAbs,prepared.VmdlText);\r\n            AssetSystem.RegisterFile(System.IO.Path.Combine(Assets,bindPath));\r\n        }\r\n        AssetSystem.RegisterFile(meshAbs);\n        if(!await CompileAsync(modelAbs,cancel)) throw new InvalidOperationException(\u0022The target FBX could not be compiled into a skinned model.\u0022);\n        if(isNew)\n        {\n            await MainThread();\n            var compiledRig=ReadSkeleton(Model.Load(model));\n            var fit=HandTargetFit.Analyze(compiledRig,HandRigDetector.Detect(compiledRig));\n            if(fit.Scale!=1||fit.TurnAround)\n            {\n                File.WriteAllText(modelAbs,fit.Apply(File.ReadAllText(modelAbs)));\n                if(!await CompileAsync(modelAbs,cancel))throw new InvalidOperationException(\u0022The fitted FPS target could not be compiled.\u0022);\n                // Resource compilation finishes before the loaded Model\u0027s bone cache\n                // refreshes. Never hand an old-size skeleton to a newly scaled mesh.\n                var deadline=DateTime.UtcNow.AddSeconds(15);\n                while(true)\n                {\n                    await Task.Delay(50,cancel);await MainThread();\n                    var loaded=ReadSkeleton(Model.Load(model));\n                    var ready=compiledRig.Bones.All(b=\u003E\n                    {\n                        var index=loaded.IndexOf(b.Name);if(index\u003C0)return false;\n                        var expected=compiledRig.RestWorld[b.Index].Pos*fit.Scale;\n                        if(fit.TurnAround)expected=new Vec(-expected.X,-expected.Y,expected.Z);\n                        return Vec.Distance(loaded.RestWorld[index].Pos,expected)\u003C.02f;\n                    });\n                    if(ready)break;\n                    if(DateTime.UtcNow\u003E=deadline)throw new InvalidOperationException(\u0022The fitted model is still reloading. Select the FBX again after asset compilation finishes.\u0022);\n                }\n                if(fit.Scale!=1)importNotes.Add($\u0022Applied FPS import scale {fit.Scale:G} to the mesh and all embedded animations to correct the oversized or undersized export.\u0022);\n                if(fit.TurnAround)importNotes.Add(\u0022Turned the imported target 180 degrees to align its left and right shoulders with FPS weapon coordinates.\u0022);\n            }\n        }\n        await MainThread(); return model;\n    }\r\n\r\n    public static async Task\u003Cbool\u003E CompileAsync(string absolute, CancellationToken cancel)\r\n    {\r\n        await MainThread(); var asset=AssetSystem.RegisterFile(absolute);\r\n        var compiled=absolute\u002B\u0022_c\u0022; 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\u003Cdeadline)\r\n        {\r\n            cancel.ThrowIfCancellationRequested(); await MainThread();\r\n            if(asset.IsCompileFailed) return false;\r\n            if(File.Exists(compiled) \u0026\u0026 File.GetLastWriteTimeUtc(compiled)\u003Ebefore) 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)\n    {\n        var materials=model.Materials.Concat(Enumerable.Range(0,model.MaterialGroupCount).SelectMany(group=\u003Emodel.GetMaterials(group))).Distinct();\n        foreach(var material in materials)\n        {\n            if(material is null||material.IsError||string.Equals(material.ResourcePath,\u0022materials/error.vmat\u0022,StringComparison.OrdinalIgnoreCase))return true;\n            foreach(var parameter in new[]{\u0022g_tColor\u0022,\u0022g_tNormal\u0022,\u0022g_tRoughness\u0022,\u0022g_tMetalness\u0022,\u0022g_tAmbientOcclusion\u0022,\u0022g_tSelfIllumMask\u0022,\u0022g_tTranslucency\u0022,\u0022TextureColor\u0022,\u0022TextureNormal\u0022,\u0022TextureRoughness\u0022,\u0022TextureMetalness\u0022,\u0022TextureAmbientOcclusion\u0022,\u0022TextureSelfIllumMask\u0022,\u0022TextureTranslucency\u0022})\n                if(material.GetTexture(parameter) is {IsError:true})return true;\n            if(material.FirstTexture is {IsError:true})return true;\n        }\n        return false;\n    }\n\n    public static void ApplyPreviewMaterialFallback(SceneModel model)\n    {\n        // Incomplete visual dependencies must not prevent skeletal retargeting.\n        // Override this preview instance only; authored assets remain editable.\n        if(HasMissingMaterials(model.Model))model.SetMaterialOverride(Material.Load(\u0022materials/dev/reflectivity_50.vmat\u0022));\n    }\n    public static string ModelPath(string path)\n    {\n        if(path.EndsWith(\u0022.vmdl_c\u0022,StringComparison.OrdinalIgnoreCase))path=path[..^2];\n        if(System.IO.Path.IsPathRooted(path)) path=System.IO.Path.GetRelativePath(Assets,path);\n        return VmdlSetupService.NormalizeAssetPath(path,\u0022.vmdl\u0022);\r\n    }\r\n    public static string SafeName(string name) =\u003E string.Concat(name.Select(c=\u003Echar.IsLetterOrDigit(c)||c==\u0027_\u0027||c==\u0027-\u0027?c:\u0027_\u0027)).Trim(\u0027_\u0027) is {Length:\u003E0} result ? result : \u0022animation\u0022;\n    internal static string ContentKey(string text)=\u003EConvert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(text))).ToLowerInvariant()[..16];\n    internal static string SourceKey(HandSource source)=\u003ESafeName(System.IO.Path.GetFileNameWithoutExtension(source.Path))\u002B\u0022_\u0022\u002BContentKey(source.Path.Replace(\u0027\\\\\u0027,\u0027/\u0027).ToLowerInvariant())[..8];\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,\u0022sequence metadata\u0022).GetOrAddComponent\u003CSkinnedModelRenderer\u003E(); metadata.Model=asset; metadata.UseAnimGraph=false; } this.asset=asset; skeleton=rig; bones=rig.Bones.Select(b=\u003Easset.Bones.GetBone(b.Name).Index).ToArray(); }\r\n        public Clip Sample(string name,float fps,CancellationToken cancel=default)\n        {\r\n            var model=new SceneModel(world,asset,Transform.Zero){UseAnimGraph=false};\r\n            try {\r\n            model.SetAnimGraph(\u0022\u0022);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)\u002B1);\r\n            if(count\u003E100000) throw new InvalidOperationException($\u0022Sequence \u0027{name}\u0027 is too long to sample.\u0022);\r\n            var frames=new List\u003CXForm[]\u003E(count);\r\n            for(var f=0;f\u003Ccount;f\u002B\u002B) {\r\n                cancel.ThrowIfCancellationRequested();\r\n                model.CurrentSequence.Time=MathF.Min(f/fps,duration); model.Update(0);\r\n                var worlds=bones.Select(b=\u003EFromEngine(model.GetBoneWorldTransform(b))).ToArray();\r\n                frames.Add(skeleton.Bones.Select(b=\u003Eb.ParentIndex\u003C0?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();}\n        }\n        public Clip? SampleFire(float duration,float fps,CancellationToken cancel)\n        {\n            var model=new SceneModel(world,asset,Transform.Zero){UseAnimGraph=true};\n            try\n            {\n                if(model.AnimationGraph is null||model.AnimationGraph.IsError)return null;\n                model.SetAnimParameter(\u0022skeleton\u0022,0);model.SetAnimParameter(\u0022b_deploy_skip\u0022,true);\n                for(var i=0;i\u003C120;i\u002B\u002B){cancel.ThrowIfCancellationRequested();model.Update(1f/60);}\n                model.SetAnimParameter(\u0022b_deploy_skip\u0022,false);\n                var frames=new List\u003CXForm[]\u003E();\n                for(var f=0;f\u003C=Math.Max(1,(int)MathF.Ceiling(duration*fps));f\u002B\u002B)\n                {\n                    cancel.ThrowIfCancellationRequested();model.SetAnimParameter(\u0022b_attack\u0022,f==0);model.Update(f==0?0:1f/fps);\n                    var worlds=bones.Select(b=\u003EFromEngine(model.GetBoneWorldTransform(b))).ToArray();\n                    frames.Add(skeleton.Bones.Select(b=\u003Eb.ParentIndex\u003C0?worlds[b.Index]:XForm.ToLocal(worlds[b.ParentIndex],worlds[b.Index])).ToArray());\n                }\n                return new Clip(\u0022Fire\u0022,fps,false,frames);\n            }\n            finally{model.Delete();}\n        }\n        public void Dispose() {world.Delete();metadataScene.Destroy();}\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":379744,"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\u003Cstring\u003E Tokenize(string name)\r\n    {\r\n        var tokens = new List\u003Cstring\u003E();\r\n        var current = new StringBuilder();\r\n        char previous = \u0027\\0\u0027;\r\n        for (var i = 0; i \u003C name.Length; i\u002B\u002B)\n        {\n            var c = name[i];\n            if (!char.IsLetterOrDigit(c))\n            {\r\n                Flush();\r\n                previous = \u0027\\0\u0027;\r\n                continue;\r\n            }\r\n            if (current.Length \u003E 0)\r\n            {\r\n                var boundary = char.IsDigit(c) != char.IsDigit(previous)\n                    || (char.IsUpper(c) \u0026\u0026 char.IsLower(previous))\n                    // Acronym/single-side prefix before a Pascal word: LThumb \u2192 L, Thumb;\n                    // FBXNode \u2192 FBX, Node. Without this, one-joint BVH thumbs lose both\n                    // their side and their otherwise unambiguous finger name.\n                    || (char.IsUpper(c) \u0026\u0026 char.IsUpper(previous)\n                        \u0026\u0026 i \u002B 1 \u003C name.Length \u0026\u0026 char.IsLower(name[i \u002B 1]));\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 \u003E 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":379744,"Code":"#nullable enable\nusing Vec = System.Numerics.Vector3;\nusing Skel = HumanoidHandRetargeter.Skeleton.Skeleton;\n\nnamespace HumanoidHandRetargeter.Mapping;\n\ninternal static partial class HandDetectionProfiles\n{\n    // Split biceps/forearms, a wrist-to-palm link, and five palm-rooted rays.\n    // Numeric/duplicate suffixes identify objects, not sides or anatomical fingers.\n    private static void ResolveUnlabelledSplitArms(Skel rig,Hint?[] hints)\n    {\n        string Name(int i)\n        {\n            var name=rig[i].Name[(rig[i].Name.LastIndexOf(\u0027:\u0027)\u002B1)..];\n            return Key(name.Split(\u0027#\u0027)[0].Replace(\u0022_duplicate\u0022,\u0022\u0022,StringComparison.OrdinalIgnoreCase)).TrimEnd(\u00220123456789\u0022.ToCharArray());\n        }\n        int Child(int i,string name)=\u003Erig.ChildrenOf(i).Count==1\u0026\u0026Name(rig.ChildrenOf(i)[0])==name?rig.ChildrenOf(i)[0]:-1;\n        var proposals=new List\u003C(HandSide Side,Dictionary\u003Cint,string\u003E Roles)\u003E();\n        foreach(var upper in rig.Bones.Where(b=\u003EName(b.Index) is \u0022bisep\u0022 or \u0022bicep\u0022 or \u0022biceps\u0022))\n        {\n            var upperSplit=Child(upper.Index,Name(upper.Index));if(upperSplit\u003C0)continue;\n            var lower=Child(upperSplit,\u0022forearm\u0022);if(lower\u003C0)continue;\n            var lowerSplit=Child(lower,\u0022forearm\u0022);if(lowerSplit\u003C0)continue;\n            var wrist=Child(lowerSplit,\u0022wrist\u0022);if(wrist\u003C0)continue;\n            var palm=Child(wrist,\u0022hand\u0022);if(palm\u003C0||rig.ChildrenOf(palm).Count!=5)continue;\n            var rays=new List\u003Cint[]\u003E();\n            foreach(var root in rig.ChildrenOf(palm))\n            {\n                var ray=new List\u003Cint\u003E{root};var current=root;\n                while(rig.ChildrenOf(current).Count==1){current=rig.ChildrenOf(current)[0];ray.Add(current);}\n                if(rig.ChildrenOf(current).Count!=0){rays.Clear();break;}\n                if(Name(ray[^1]).Contains(\u0022end\u0022,StringComparison.Ordinal))ray.RemoveAt(ray.Count-1);\n                rays.Add(ray.ToArray());\n            }\n            if(rays.Count!=5)continue;\n            var thumbs=rays.Where(r=\u003Er.Length==3\u0026\u0026Name(r[0]) is \u0022thum\u0022 or \u0022thumb\u0022).ToArray();\n            var fingers=rays.Where(r=\u003Er.Length==4\u0026\u0026Name(r[0])==\u0022finger\u0022).ToArray();\n            if(thumbs.Length!=1||fingers.Length!=4)continue;\n            var thumb=thumbs[0];\n            Vec Pos(int i)=\u003Erig.RestWorld[i].Pos;\n            var ordered=fingers.OrderBy(r=\u003EVec.DistanceSquared(Pos(r[1]),Pos(thumb[1]))).ToArray();\n            var radial=Pos(ordered[0][1])-Pos(ordered[3][1]);\n            if(radial.LengthSquared()\u003C1e-8f)continue;\n            radial=Vec.Normalize(radial);\n            // Require four distinctly ordered knuckles, not just a nearest-thumb guess.\n            var spacing=ordered.Select(r=\u003EVec.Dot(Pos(r[1])-Pos(ordered[3][1]),radial)).ToArray();\n            if(Enumerable.Range(0,3).Any(i=\u003Espacing[i]-spacing[i\u002B1]\u003Cspacing[0]*.1f))continue;\n            var forward=ordered.Select(r=\u003EPos(r[3])-Pos(r[1])).Aggregate(Vec.Zero,(a,b)=\u003Ea\u002Bb);\n            var normal=Vec.Cross(forward,radial);var thumbBend=Pos(thumb[2])-Pos(thumb[1]);\n            if(normal.LengthSquared()\u003C1e-8f||thumbBend.LengthSquared()\u003C1e-8f)continue;\n            var chirality=Vec.Dot(Vec.Normalize(normal),Vec.Normalize(thumbBend));\n            // An out-of-plane thumb bend supplies the palmar direction. Flat or\n            // ambiguous geometry remains for review rather than guessing a side.\n            if(MathF.Abs(chirality)\u003C.25f)continue;\n            var side=chirality\u003E0?HandSide.Left:HandSide.Right;\n            var roles=new Dictionary\u003Cint,string\u003E{{upper.Index,\u0022UpperArm\u0022},{lower,\u0022LowerArm\u0022},{wrist,\u0022Hand\u0022},{palm,\u0022PalmMeta\u0022}};\n            var digitNames=new[]{\u0022Index\u0022,\u0022Middle\u0022,\u0022Ring\u0022,\u0022Pinky\u0022};\n            for(var digit=0;digit\u003C4;digit\u002B\u002B)\n                for(var joint=0;joint\u003C4;joint\u002B\u002B)roles[ordered[digit][joint]]=digitNames[digit]\u002B(joint==0?\u0022Meta\u0022:\u0022Prox\u0022);\n            foreach(var bone in thumb)roles[bone]=\u0022ThumbProx\u0022;\n            if(roles.Keys.Any(i=\u003Ehints[i] is not null))continue;\n            proposals.Add((side,roles));\n        }\n        // Require a complete, disjoint bilateral pair before using unnamed sides.\n        if(proposals.Count!=2||proposals[0].Side==proposals[1].Side||proposals[0].Roles.Keys.Intersect(proposals[1].Roles.Keys).Any())return;\n        foreach(var proposal in proposals)\n            foreach(var role in proposal.Roles)hints[role.Key]=new(role.Value,proposal.Side,\u0022split_arm_palm_rays\u0022);\n    }\n}\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Core/Mapping/HandRigDefinition.cs","FileName":"HandRigDefinition.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":379744,"Code":"#nullable enable\nnamespace HumanoidHandRetargeter.Mapping;\n\npublic enum HandSide { Left, Right }\npublic enum DigitRole { Thumb, Index, Middle, Ring, Pinky, Extra }\n\n/// \u003Csummary\u003EOrdered anatomical joints; optional palm and terminal bones are explicit.\u003C/summary\u003E\npublic sealed class DigitChain\n{\n    public DigitRole Role { get; }\n    public string ExtraSlot { get; }\n    public IReadOnlyList\u003Cint\u003E Segments { get; }\n    public int? Metacarpal { get; }\n    public int? Tip { get; }\n\n    public DigitChain(DigitRole role, IEnumerable\u003Cint\u003E segments,\n        int? metacarpal = null, int? tip = null, string extraSlot = \u0022\u0022)\n    {\n        ArgumentNullException.ThrowIfNull(segments);\n        Role = role;\n        Segments = Array.AsReadOnly(segments.ToArray());\n        Metacarpal = metacarpal;\n        Tip = tip;\n        ExtraSlot = extraSlot ?? \u0022\u0022;\n    }\n\n    public IEnumerable\u003Cint\u003E Bones\n    {\n        get\n        {\n            if (Metacarpal is int palm) yield return palm;\n            foreach (var segment in Segments) yield return segment;\n            if (Tip is int tip) yield return tip;\n        }\n    }\n}\n\n/// \u003Csummary\u003EA single hand; rigs with only one side do not need a placeholder other hand.\u003C/summary\u003E\npublic sealed class HandRigDefinition\n{\n    public HandSide Side { get; }\n    public int Wrist { get; }\n    public int? Clavicle { get; }\n    public int? UpperArm { get; }\n    public int? Forearm { get; }\n    public IReadOnlyList\u003CDigitChain\u003E Digits { get; }\n    public IReadOnlyList\u003Cint\u003E TwistOrHelperBones { get; }\n\n    public HandRigDefinition(HandSide side, int wrist, IEnumerable\u003CDigitChain\u003E digits,\n        int? clavicle = null, int? upperArm = null, int? forearm = null,\n        IEnumerable\u003Cint\u003E? twistOrHelperBones = null)\n    {\n        ArgumentNullException.ThrowIfNull(digits);\n        Side = side;\n        Wrist = wrist;\n        Clavicle = clavicle;\n        UpperArm = upperArm;\n        Forearm = forearm;\n        Digits = Array.AsReadOnly(digits.ToArray());\n        TwistOrHelperBones = Array.AsReadOnly((twistOrHelperBones ?? Array.Empty\u003Cint\u003E()).ToArray());\n    }\n}\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Core/Retargeting/HandMotionOptions.cs","FileName":"HandMotionOptions.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":379744,"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    /// \u003Csummary\u003EKnown source-to-target model axes, independent of the wrists\u0027 rest rotations.\u003C/summary\u003E\r\n    public System.Numerics.Quaternion? WristTravelBasis {get;init;}\r\n    public bool SolveArmIk { get; init; } = true;\n    public bool PreserveWeaponGrip { get; init; } = true;\n    /// \u003Csummary\u003EShared source weapon-to-target translation in centimeters. Grip motion is never scaled per hand.\u003C/summary\u003E\n    public System.Numerics.Vector3? WeaponSpaceOffset {get;init;}\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Core/Retargeting/HandRetargeter.cs","FileName":"HandRetargeter.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":379744,"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/// \u003Csummary\u003EOne offline solve path for static poses and baked clips. Target translations\r\n/// and unmapped local transforms stay authored; helper constraints remain target-owned.\u003C/summary\u003E\r\npublic static class HandRetargeter\n{\n    /// \u003Csummary\u003EOne continuous graph stream, retaining digit angle history between evaluated frames.\u003C/summary\u003E\n    public sealed class PoseStream\n    {\n        private readonly HandRetargetProfile profile;\n        private readonly HandMotionOptions options;\n        private readonly AngleHistory history;\n        public PoseStream(HandRetargetProfile profile, HandMotionOptions options)\n        { this.profile = profile; this.options = options; history = new(profile); }\n        public Pose Step(Pose source)\n        {\n            var pose = Solve(profile, source, history);\n            if (options.TransferWristPosition) ApplyWristMotion(profile, source, pose, options);\n            var sourceWorld = source.ToWorld(profile.Source);\n            foreach (var pair in profile.PreservedTracks)\n            {\n                var targetWorld = pose.ToWorld(profile.Target);\n                var parent = profile.Target[pair.Target].ParentIndex;\n                pose.Locals[pair.Target] = parent \u003C 0 ? sourceWorld[pair.Source] : XForm.ToLocal(targetWorld[parent], sourceWorld[pair.Source]);\n            }\n            return pose;\n        }\n    }\n\n    public static Pose RetargetPose(HandRetargetProfile profile, Pose sourcePose)\r\n        =\u003E Solve(profile, sourcePose, null);\r\n\r\n    /// \u003Csummary\u003ERetains 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.\u003C/summary\u003E\r\n    public static Clip RetargetClip(HandRetargetProfile profile, Clip sourceClip, CancellationToken cancellationToken = default)\r\n        =\u003E Bake(profile, sourceClip, new HandMotionOptions { TransferWristPosition = false }, cancellationToken);\r\n\r\n    /// \u003Csummary\u003EThe shared editor preview/export bake including optional wrist travel and arm IK.\u003C/summary\u003E\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(\u0022empty-clip\u0022, \u0022Source animation has no frames.\u0022) });\r\n        var frames = new List\u003CXForm[]\u003E(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\u003E0)\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\u003C0?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)\n    {\n        if(options.WeaponSpaceOffset.HasValue\u0026\u0026options.SolveArmIk)\n            foreach(var shoulder in profile.FpsShoulders)target.Locals[shoulder.Key].Pos=shoulder.Value;\n        var sourceWorld = source.ToWorld(profile.Source);\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 \u002B travel * (options.ScaleWristTravel ? pair.Scale : 1f);\r\n            var wristRotation = world[pair.Target].Rot;\n            if(options.WeaponSpaceOffset is {} weaponOffset)\n            {\n                wristRotation=MathQ.Normalize(sourceWorld[pair.Source].Rot*pair.GripRotationOffset);\n                desired=sourceWorld[pair.Source].Pos\u002BweaponOffset\n                    \u002BVector3.Transform(pair.SourceGripLocal,sourceWorld[pair.Source].Rot)\n                    -Vector3.Transform(pair.TargetGripLocal,wristRotation);\n            }\n            if (options.SolveArmIk \u0026\u0026 pair.UpperArm is int upper \u0026\u0026 pair.Forearm is int lower)\n            {\n                if(options.WeaponSpaceOffset.HasValue)\n                {\n                    // Detached FPS arms can move their open shoulder ends. Avoid\n                    // folding long/thick forearms back through their own upper arm\n                    // when the weapon grip lies very close to the shoulder.\n                    if(profile.Target[upper].ParentIndex\u003C0)\n                    {\n                        var shoulder=HandViewSpace.ClearElbowFold(world[upper].Pos,world[lower].Pos,world[pair.Target].Pos,desired);\n                        target.Locals[upper].Pos=shoulder;\n                        world=target.ToWorld(profile.Target);\n                    }\n                    // FPS arm meshes have free shoulder ends. Move the shoulder enough\n                    // to reach the fixed weapon grip instead of stretching either arm segment.\n                    var reach=Vector3.Distance(world[upper].Pos,world[lower].Pos)\u002BVector3.Distance(world[lower].Pos,world[pair.Target].Pos);\n                    var toGrip=desired-world[upper].Pos;var distance=toGrip.Length();\n                    if(distance\u003Ereach*.98f\u0026\u0026distance\u003E1e-5f)\n                    {\n                        var shoulder=world[upper].Pos\u002BtoGrip/distance*(distance-reach*.98f);\n                        var parent=profile.Target[upper].ParentIndex;\n                        target.Locals[upper].Pos=parent\u003C0?shoulder:Vector3.Transform(shoulder-world[parent].Pos,Quaternion.Conjugate(world[parent].Rot));\n                        world=target.ToWorld(profile.Target);\n                    }\n                }\n                var correction = TwoBoneIk.Solve(world[upper].Pos, world[lower].Pos, world[pair.Target].Pos, desired, 0, pair.BendAxis);\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 \u003C 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);\n                SetWorldRotation(lower, correction.LowerWorldDelta * oldLower);\n                if(options.WeaponSpaceOffset.HasValue \u0026\u0026 pair.SourceForearm is int sourceElbow \u0026\u0026 pair.SourceUpperArm is int sourceShoulder)\n                {\n                    // Transfer the source bend plane between the two shoulder-to-wrist axes.\n                    // An absolute source elbow position can lie on the target arm\u0027s axis\n                    // when proportions differ, flipping its pole as the weapon recoils.\n                    var axis=world[pair.Target].Pos-world[upper].Pos;\n                    var sourceAxis=sourceWorld[pair.Source].Pos-sourceWorld[sourceShoulder].Pos;\n                    if(axis.LengthSquared()\u003E1e-8f\u0026\u0026sourceAxis.LengthSquared()\u003E1e-8f)\n                    {\n                        axis=Vector3.Normalize(axis);\n                        sourceAxis=Vector3.Normalize(sourceAxis);\n                        var actual=world[lower].Pos-world[upper].Pos;\n                        var wanted=sourceWorld[sourceElbow].Pos-sourceWorld[sourceShoulder].Pos;\n                        wanted-=sourceAxis*Vector3.Dot(wanted,sourceAxis);\n                        wanted=Vector3.Transform(wanted,MathQ.FromTo(sourceAxis,axis));\n                        actual-=axis*Vector3.Dot(actual,axis);\n                        // A straight arm has no defined pole: retain the preceding IK plane.\n                        if(actual.LengthSquared()\u003E1e-6f\u0026\u0026wanted.LengthSquared()\u003E1e-6f)\n                        {\n                            actual=Vector3.Normalize(actual);wanted=Vector3.Normalize(wanted);\n                            var angle=MathF.Atan2(Vector3.Dot(axis,Vector3.Cross(actual,wanted)),Vector3.Dot(actual,wanted));\n                            var pole=Quaternion.CreateFromAxisAngle(axis,angle);\n                            oldLower=world[lower].Rot;\n                            SetWorldRotation(upper,pole*world[upper].Rot);\n                            SetWorldRotation(lower,pole*oldLower);\n                        }\n                    }\n                }\n                if(options.WeaponSpaceOffset.HasValue\u0026\u0026!pair.HasTwistHelpers)\n                {\n                    // Without authored twist helpers, put axial pronation in the forearm\n                    // instead of winding the wrist through the fixed palm orientation.\n                    // Rotating about elbow-to-wrist leaves the grip position unchanged.\n                    var axis=world[pair.Target].Pos-world[lower].Pos;\n                    if(axis.LengthSquared()\u003E1e-8f)\n                    {\n                        var restRelative=MathQ.Normalize(Quaternion.Conjugate(profile.Target.RestWorld[lower].Rot)*profile.Target.RestWorld[pair.Target].Rot);\n                        var relaxedLower=MathQ.Normalize(wristRotation*Quaternion.Conjugate(restRelative));\n                        var delta=MathQ.Normalize(relaxedLower*Quaternion.Conjugate(world[lower].Rot));\n                        MathQ.SwingTwist(delta,Vector3.Normalize(axis),out _,out var roll);\n                        SetWorldRotation(lower,roll*world[lower].Rot);\n                    }\n                }\n                SetWorldRotation(pair.Target, wristRotation);\n                if(options.WeaponSpaceOffset.HasValue\u0026\u0026Vector3.Distance(world[pair.Target].Pos,desired)\u003E.1f)\n                    throw new InvalidOperationException(\u0022The target arm cannot reach the weapon grip without stretching. Adjust the rig\u0027s shoulder placement or disable Preserve weapon grip.\u0022);\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 \u003C 0 ? desired\n                    : Vector3.Transform(desired - world[parent].Pos, Quaternion.Conjugate(world[parent].Rot));\n                target.Locals[pair.Target].Rot=MathQ.Normalize(parent\u003C0?wristRotation:Quaternion.Conjugate(world[parent].Rot)*wristRotation);\n            }\r\n        }\r\n        var errors = PoseValidator.Validate(profile.Target, target);\r\n        if (errors.Count \u003E 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 \u003E 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 \u003E 0) throw new RigValidationException(errors);\r\n        var sourceDeltas = new Quaternion[sourceWorld.Length];\r\n        for (var i = 0; i \u003C sourceWorld.Length; i\u002B\u002B)\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 \u003E= 0 ? sourceDeltas[pair.SourceParent] : Quaternion.Identity;\r\n            var parentTarget = pair.TargetParent \u003E= 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 \u003C profile.Distributions.Length; d\u002B\u002B)\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 \u003C digit.Source.Length; s\u002B\u002B)\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 \u003C= 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 \u002B= angle;\r\n                }\r\n                previous = delta;\r\n            }\r\n            var accumulated = targetDeltas[digit.TargetWrist];\r\n            for (var t = 0; t \u003C digit.Target.Length; t\u002B\u002B)\r\n            {\r\n                var angle = 0f;\r\n                for (var s = 0; s \u003C curls.Length; s\u002B\u002B) angle \u002B= 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 \u003C targetWorld.Length; i\u002B\u002B)\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 \u003C 0 ? desiredWorld : Quaternion.Conjugate(targetWorld[parent].Rot) * desiredWorld);\r\n            }\r\n            targetWorld[i] = parent \u003C 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 \u003E 0) throw new RigValidationException(errors);\r\n        errors = PoseValidator.Validate(profile.Target, new Pose(targetWorld));\r\n        if (errors.Count \u003E 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 \u002B twist.Y * axis.Y \u002B twist.Z * axis.Z, twist.W);\r\n        if (angle \u003E MathF.PI) angle -= 2f * MathF.PI;\r\n        if (angle \u003C -MathF.PI) angle \u002B= 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 =\u003E Enumerable.Repeat(float.NaN, d.Source.Length).ToArray()).ToArray();\r\n            _spread = profile.Distributions.Select(d =\u003E 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 \u002B= 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":379744,"Code":"// Adapted from humanoid-retargeter 26084c96c3fc870aaf9a5bd798de063ce2fd62df; cached hierarchy queries added.\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/// \u003Csummary\u003E\r\n/// Input definition for a single bone, used to build a \u003Csee cref=\u0022Skeleton\u0022/\u003E.\r\n/// Order does not matter; construction topologically sorts parents before children.\r\n/// \u003C/summary\u003E\r\n/// \u003Cparam name=\u0022Name\u0022\u003EUnique bone name.\u003C/param\u003E\r\n/// \u003Cparam name=\u0022ParentName\u0022\u003EParent bone name, or null for a root bone (multiple roots allowed).\u003C/param\u003E\r\n/// \u003Cparam name=\u0022RestLocal\u0022\u003ERest (bind) transform relative to the parent bone, centimeters.\u003C/param\u003E\r\npublic readonly record struct BoneDefinition(string Name, string? ParentName, XForm RestLocal);\r\n\r\n/// \u003Csummary\u003EOne bone of an immutable \u003Csee cref=\u0022Skeleton\u0022/\u003E.\u003C/summary\u003E\r\npublic readonly struct Bone\r\n{\r\n    /// \u003Csummary\u003EIndex of this bone in the skeleton (parents always have a smaller index).\u003C/summary\u003E\r\n    public int Index { get; }\r\n\r\n    /// \u003Csummary\u003EUnique bone name.\u003C/summary\u003E\r\n    public string Name { get; }\r\n\r\n    /// \u003Csummary\u003EIndex of the parent bone, or -1 for a root bone.\u003C/summary\u003E\r\n    public int ParentIndex { get; }\r\n\r\n    /// \u003Csummary\u003ERest (bind) transform relative to the parent bone (or armature space for roots).\u003C/summary\u003E\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    /// \u003Cinheritdoc /\u003E\r\n    public override string ToString() =\u003E $\u0022[{Index}] {Name} (parent {ParentIndex})\u0022;\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Immutable bone hierarchy with rest pose. Bones are stored topologically sorted\r\n/// (every bone\u0027s parent precedes it), which lets forward kinematics run as a single\r\n/// in-order pass. Supports multiple roots (e.g. the s\u0026amp;box rig has both\r\n/// \u003Cc\u003Eroot_IK\u003C/c\u003E and \u003Cc\u003Epelvis\u003C/c\u003E as parentless bones).\r\n/// \u003C/summary\u003E\r\npublic sealed class Skeleton\r\n{\r\n    private readonly Bone[] _bones;\r\n    private readonly XForm[] _restWorld;\r\n    private readonly Dictionary\u003Cstring, int\u003E _indexByName;\n    private readonly IReadOnlyList\u003Cint\u003E[] _children;\n\r\n    private Skeleton(Bone[] bones, XForm[] restWorld, Dictionary\u003Cstring, int\u003E indexByName)\r\n    {\r\n        _bones = bones;\r\n        _restWorld = restWorld;\r\n        _indexByName = indexByName;\n        var children = Enumerable.Range(0, bones.Length).Select(_ =\u003E new List\u003Cint\u003E()).ToArray();\n        foreach (var bone in bones)\n            if (bone.ParentIndex \u003E= 0) children[bone.ParentIndex].Add(bone.Index);\n        _children = children.Select(c =\u003E (IReadOnlyList\u003Cint\u003E)c.AsReadOnly()).ToArray();\n    }\r\n\r\n    /// \u003Csummary\u003ENumber of bones.\u003C/summary\u003E\r\n    public int Count =\u003E _bones.Length;\r\n\r\n    /// \u003Csummary\u003EAll bones, topologically sorted (parents before children).\u003C/summary\u003E\r\n    public IReadOnlyList\u003CBone\u003E Bones =\u003E _bones;\r\n\r\n    /// \u003Csummary\u003ERest (bind) world transforms, indexed like \u003Csee cref=\u0022Bones\u0022/\u003E.\u003C/summary\u003E\r\n    public IReadOnlyList\u003CXForm\u003E RestWorld =\u003E _restWorld;\r\n\r\n    /// \u003Csummary\u003EBone name to index lookup.\u003C/summary\u003E\r\n    public IReadOnlyDictionary\u003Cstring, int\u003E IndexByName =\u003E _indexByName;\r\n\r\n    /// \u003Csummary\u003EThe bone at \u003Cparamref name=\u0022index\u0022/\u003E.\u003C/summary\u003E\r\n    public Bone this[int index] =\u003E _bones[index];\r\n\r\n    /// \u003Csummary\u003EReturns the index of the named bone, or -1 when absent.\u003C/summary\u003E\r\n    public int IndexOf(string name) =\u003E _indexByName.TryGetValue(name, out var index) ? index : -1;\n\n    /// \u003Csummary\u003ECached immediate children in stable skeleton order.\u003C/summary\u003E\n    public IReadOnlyList\u003Cint\u003E ChildrenOf(int bone) =\u003E _children[bone];\n\n    /// \u003Csummary\u003EWhether child is strictly below ancestor; a bone is not its own descendant.\u003C/summary\u003E\n    public bool DescendsFrom(int child, int ancestor)\n    {\n        if (child \u003C 0 || child \u003E= Count) throw new ArgumentOutOfRangeException(nameof(child));\n        if (ancestor \u003C 0 || ancestor \u003E= Count) throw new ArgumentOutOfRangeException(nameof(ancestor));\n        for (var parent = _bones[child].ParentIndex; parent \u003E= 0; parent = _bones[parent].ParentIndex)\n            if (parent == ancestor) return true;\n        return false;\n    }\n\r\n    /// \u003Csummary\u003E\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    /// \u003C/summary\u003E\r\n    /// \u003Cexception cref=\u0022ArgumentException\u0022\u003E\r\n    /// Thrown on duplicate bone names, unknown parent names, or parent cycles.\r\n    /// \u003C/exception\u003E\r\n    public static Skeleton Create(IReadOnlyList\u003CBoneDefinition\u003E definitions)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(definitions);\r\n\r\n        var count = definitions.Count;\r\n        var originalIndexByName = new Dictionary\u003Cstring, int\u003E(count, StringComparer.Ordinal);\r\n        for (var i = 0; i \u003C count; i\u002B\u002B)\r\n        {\r\n            var name = definitions[i].Name;\r\n            if (string.IsNullOrEmpty(name))\r\n                throw new ArgumentException($\u0022Bone at position {i} has an empty name.\u0022, nameof(definitions));\r\n            if (!originalIndexByName.TryAdd(name, i))\r\n                throw new ArgumentException($\u0022Duplicate bone name \u0027{name}\u0027.\u0022, nameof(definitions));\r\n        }\r\n\r\n        foreach (var def in definitions)\r\n        {\r\n            if (def.ParentName is not null \u0026\u0026 !originalIndexByName.ContainsKey(def.ParentName))\r\n                throw new ArgumentException(\r\n                    $\u0022Bone \u0027{def.Name}\u0027 references unknown parent \u0027{def.ParentName}\u0027.\u0022, nameof(definitions));\r\n            if (def.ParentName == def.Name)\r\n                throw new ArgumentException($\u0022Bone \u0027{def.Name}\u0027 is its own parent.\u0022, 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\u003Cint\u003E(count);\r\n        while (order.Count \u003C count)\r\n        {\r\n            var progressed = false;\r\n            for (var i = 0; i \u003C count; i\u002B\u002B)\r\n            {\r\n                if (newIndexByOriginal[i] \u003E= 0)\r\n                    continue;\r\n                var parentName = definitions[i].ParentName;\r\n                if (parentName is not null \u0026\u0026 newIndexByOriginal[originalIndexByName[parentName]] \u003C 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(\u0022Bone hierarchy contains a parent cycle.\u0022, 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\u003Cstring, int\u003E(count, StringComparer.Ordinal);\r\n        for (var newIndex = 0; newIndex \u003C count; newIndex\u002B\u002B)\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 \u003C 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":379744,"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/// \u003Csummary\u003E\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/// \u003Clist type=\u0022bullet\u0022\u003E\r\n///   \u003Citem\u003E\u003Cc\u003Eshort\u003C/c\u003E (\u0027Y\u0027), \u003Cc\u003Ebool\u003C/c\u003E or character \u003Cc\u003Ebyte\u003C/c\u003E (\u0027C\u0027), \u003Cc\u003Eint\u003C/c\u003E (\u0027I\u0027), \u003Cc\u003Efloat\u003C/c\u003E (\u0027F\u0027),\n///         \u003Cc\u003Edouble\u003C/c\u003E (\u0027D\u0027), \u003Cc\u003Elong\u003C/c\u003E (\u0027L\u0027)\u003C/item\u003E\r\n///   \u003Citem\u003E\u003Cc\u003Efloat[]\u003C/c\u003E (\u0027f\u0027), \u003Cc\u003Edouble[]\u003C/c\u003E (\u0027d\u0027), \u003Cc\u003Elong[]\u003C/c\u003E (\u0027l\u0027),\r\n///         \u003Cc\u003Eint[]\u003C/c\u003E (\u0027i\u0027), \u003Cc\u003Ebool[]\u003C/c\u003E-as-\u003Cc\u003Ebyte[]\u003C/c\u003E (\u0027b\u0027)\u003C/item\u003E\r\n///   \u003Citem\u003E\u003Cc\u003Estring\u003C/c\u003E (\u0027S\u0027 \u2014 kept raw, may contain the \u003Cc\u003E\\x00\\x01\u003C/c\u003E name/class\r\n///         separator; see \u003Csee cref=\u0022SplitName\u0022/\u003E), \u003Cc\u003Ebyte[]\u003C/c\u003E (\u0027R\u0027)\u003C/item\u003E\r\n/// \u003C/list\u003E\r\n/// ASCII files store numbers only as \u003Cc\u003Elong\u003C/c\u003E / \u003Cc\u003Edouble\u003C/c\u003E (and arrays as\r\n/// \u003Cc\u003Elong[]\u003C/c\u003E / \u003Cc\u003Edouble[]\u003C/c\u003E), so the typed accessors below convert tolerantly.\r\n/// \u003C/summary\u003E\r\npublic sealed class FbxNode\r\n{\r\n    public string Name { get; }\r\n    public List\u003Cobject\u003E Properties { get; } = new();\r\n    public List\u003CFbxNode\u003E Children { get; } = new();\n    public bool HasChildScope { get; set; }\n    // Document metadata retained when repairing binary FBX files.\n    public byte[]? FooterWatermark { get; set; }\n\r\n    public FbxNode(string name) =\u003E Name = name;\r\n\r\n    /// \u003Csummary\u003EFirst child with the given name, or null.\u003C/summary\u003E\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    /// \u003Csummary\u003EAll children with the given name, in document order.\u003C/summary\u003E\r\n    public IEnumerable\u003CFbxNode\u003E 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    /// \u003Csummary\u003E\r\n    /// Property \u003Cparamref name=\u0022i\u0022/\u003E converted to \u003Ctypeparamref name=\u0022T\u0022/\u003E.\r\n    /// Numeric scalars convert tolerantly across widths (e.g. an \u0027I\u0027 i32 read as long);\r\n    /// anything else must match the stored type exactly.\r\n    /// \u003C/summary\u003E\r\n    public T Prop\u003CT\u003E(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\u0026box whitelist: Type.IsPrimitive is banned; enumerate the convertible targets.\r\n        if (v is IConvertible \u0026\u0026 (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\u0026box-whitelisted\r\n            catch (Exception ex) when (ex is InvalidCastException or ArithmeticException or FormatException)\r\n            {\r\n                throw new FormatException(\r\n                    $\u0022FBX node \u0027{Name}\u0027: property {i} is {v.GetType().Name}, not convertible to {target.Name}.\u0022, ex);\r\n            }\r\n        }\r\n\r\n        throw new FormatException(\r\n            $\u0022FBX node \u0027{Name}\u0027: property {i} is {v.GetType().Name}, expected {target.Name}.\u0022);\r\n    }\r\n\r\n    /// \u003Csummary\u003EProperty \u003Cparamref name=\u0022i\u0022/\u003E as a double array (converts f/l/i/b arrays).\u003C/summary\u003E\r\n    public double[] AsDoubleArray(int i) =\u003E RawProp(i) switch\r\n    {\r\n        double[] d =\u003E d,\r\n        float[] f =\u003E Array.ConvertAll(f, x =\u003E (double)x),\r\n        long[] l =\u003E Array.ConvertAll(l, x =\u003E (double)x),\r\n        int[] n =\u003E Array.ConvertAll(n, x =\u003E (double)x),\r\n        byte[] b =\u003E Array.ConvertAll(b, x =\u003E (double)x),\r\n        bool[] o =\u003E Array.ConvertAll(o, x =\u003E x ? 1.0 : 0.0),\r\n        var v =\u003E throw TypeError(i, v, \u0022double[]\u0022),\r\n    };\r\n\r\n    /// \u003Csummary\u003EProperty \u003Cparamref name=\u0022i\u0022/\u003E as a float array (converts d/l/i/b arrays).\u003C/summary\u003E\r\n    public float[] AsFloatArray(int i) =\u003E RawProp(i) switch\r\n    {\r\n        float[] f =\u003E f,\r\n        double[] d =\u003E Array.ConvertAll(d, x =\u003E (float)x),\r\n        long[] l =\u003E Array.ConvertAll(l, x =\u003E (float)x),\r\n        int[] n =\u003E Array.ConvertAll(n, x =\u003E (float)x),\r\n        byte[] b =\u003E Array.ConvertAll(b, x =\u003E (float)x),\r\n        bool[] o =\u003E Array.ConvertAll(o, x =\u003E x ? 1f : 0f),\r\n        var v =\u003E throw TypeError(i, v, \u0022float[]\u0022),\r\n    };\r\n\r\n    /// \u003Csummary\u003EProperty \u003Cparamref name=\u0022i\u0022/\u003E as a long array (converts i/b; d/f if integral).\u003C/summary\u003E\r\n    public long[] AsLongArray(int i) =\u003E RawProp(i) switch\r\n    {\r\n        long[] l =\u003E l,\r\n        int[] n =\u003E Array.ConvertAll(n, x =\u003E (long)x),\r\n        byte[] b =\u003E Array.ConvertAll(b, x =\u003E (long)x),\r\n        bool[] o =\u003E Array.ConvertAll(o, x =\u003E x ? 1L : 0L),\r\n        double[] d =\u003E Array.ConvertAll(d, x =\u003E checked((long)x)),\r\n        float[] f =\u003E Array.ConvertAll(f, x =\u003E checked((long)x)),\r\n        var v =\u003E throw TypeError(i, v, \u0022long[]\u0022),\r\n    };\r\n\r\n    /// \u003Csummary\u003EProperty \u003Cparamref name=\u0022i\u0022/\u003E as an int array (converts b; l/d/f narrowing-checked).\u003C/summary\u003E\r\n    public int[] AsIntArray(int i) =\u003E RawProp(i) switch\r\n    {\r\n        int[] n =\u003E n,\r\n        long[] l =\u003E Array.ConvertAll(l, x =\u003E checked((int)x)),\r\n        byte[] b =\u003E Array.ConvertAll(b, x =\u003E (int)x),\r\n        bool[] o =\u003E Array.ConvertAll(o, x =\u003E x ? 1 : 0),\r\n        double[] d =\u003E Array.ConvertAll(d, x =\u003E checked((int)x)),\r\n        float[] f =\u003E Array.ConvertAll(f, x =\u003E checked((int)x)),\r\n        var v =\u003E throw TypeError(i, v, \u0022int[]\u0022),\r\n    };\r\n\r\n    /// \u003Csummary\u003EProperty \u003Cparamref name=\u0022i\u0022/\u003E as raw bytes (\u0027R\u0027 blobs or \u0027b\u0027 bool arrays).\u003C/summary\u003E\r\n    public byte[] AsByteArray(int i) =\u003E RawProp(i) switch\r\n    {\r\n        byte[] b =\u003E b,\r\n        var v =\u003E throw TypeError(i, v, \u0022byte[]\u0022),\r\n    };\r\n\r\n    /// \u003Csummary\u003EProperty \u003Cparamref name=\u0022i\u0022/\u003E as a string (raw \u0027S\u0027 content, separators intact).\u003C/summary\u003E\r\n    public string AsString(int i) =\u003E RawProp(i) switch\r\n    {\r\n        string s =\u003E s,\r\n        var v =\u003E throw TypeError(i, v, \u0022string\u0022),\r\n    };\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Splits an FBX object name into (name, class).\r\n    /// Binary files store \u003Cc\u003E\u0022Name\\x00\\x01Class\u0022\u003C/c\u003E (e.g. \u003Cc\u003E\u0022mixamorig:Hips\\x00\\x01Model\u0022\u003C/c\u003E);\r\n    /// ASCII files store \u003Cc\u003E\u0022Class::Name\u0022\u003C/c\u003E (e.g. \u003Cc\u003E\u0022Model::pelvis\u0022\u003C/c\u003E).\r\n    /// A plain string with neither separator yields (name, \u0022\u0022).\r\n    /// \u003C/summary\u003E\r\n    public static (string Name, string Class) SplitName(string raw)\r\n    {\r\n        int bin = raw.IndexOf(\u0022\\0\\x01\u0022, StringComparison.Ordinal);\r\n        if (bin \u003E= 0)\r\n            return (raw[..bin], raw[(bin \u002B 2)..]);\r\n\r\n        int ascii = raw.IndexOf(\u0022::\u0022, StringComparison.Ordinal);\r\n        if (ascii \u003E= 0)\r\n            return (raw[(ascii \u002B 2)..], raw[..ascii]);\r\n\r\n        return (raw, \u0022\u0022);\r\n    }\r\n\r\n    /// \u003Csummary\u003EPrimitive scalar types \u003Csee cref=\u0022Prop{T}\u0022/\u003E converts to (whitelist-safe IsPrimitive substitute).\u003C/summary\u003E\r\n    private static readonly HashSet\u003CType\u003E 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 \u003C 0 || i \u003E= Properties.Count)\r\n            throw new FormatException(\r\n                $\u0022FBX node \u0027{Name}\u0027: property index {i} out of range (has {Properties.Count}).\u0022);\r\n        return Properties[i];\r\n    }\r\n\r\n    private FormatException TypeError(int i, object v, string wanted) =\u003E\r\n        new($\u0022FBX node \u0027{Name}\u0027: property {i} is {v.GetType().Name}, expected {wanted}.\u0022);\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Runtime/RetargetedWeaponController.cs","FileName":"RetargetedWeaponController.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":379744,"Code":"#nullable enable\nusing Sandbox;\nusing HumanoidHandRetargeter.Target;\n\nnamespace HumanoidHandRetargeter;\n\n/// \u003Csummary\u003EOptional local player controls for exported weapons. Games may instead drive RetargetedWeapon directly.\u003C/summary\u003E\n[Title(\u0022Retargeted Weapon Controls\u0022), Category(\u0022Animation\u0022)]\npublic sealed class RetargetedWeaponController : Component, ICameraModifier\n{\n    [Property] public PlayerController? Player { get; set; }\n    [Property] public bool ReadInput { get; set; } = true;\n    [Property] public int MagazineSize { get; set; } = 30;\n    [Property] public int StartingReserve { get; set; } = 120;\n    [Property] public float RoundsPerMinute { get; set; } = 800;\n    [Property] public float ReloadSeconds { get; set; } = 2.7f;\n    [Property] public float EmptyReloadSeconds { get; set; } = 3.2f;\n    [Property] public float Damage { get; set; } = 20;\n    [Property] public float Range { get; set; } = 10000;\n    [Property] public float HipFov { get; set; } = 75;\n    [Property] public float AimFov { get; set; } = 55;\n    [Property] public string WorldGripBone { get; set; } = \u0022hand_R\u0022;\n    [Property] public int BodyHoldType { get; set; } = 2;\n    [Property] public int BodyHandedness { get; set; }\n    public int Ammo =\u003E magazine?.Rounds ?? MagazineSize;\n    public int Reserve =\u003E magazine?.Reserve ?? StartingReserve;\n    public bool Reloading =\u003E magazine?.Reloading ?? false;\n    public bool Aiming { get; private set; }\n    public int ShotsFired { get; private set; }\n    public int Hits { get; private set; }\n    public global::Vector3 LastHit { get; private set; }\n    public Action\u003CSceneTraceResult\u003E? Shot { get; set; }\n    private WeaponMagazine? magazine;\n    private RetargetedWeapon? weapon;\n    private float nextShot, reloadEnds, currentFov;\n    public int CameraOrder =\u003E 200;\n    protected override void OnStart()\n    {\n        weapon=GetComponent\u003CRetargetedWeapon\u003E();\n        Player ??= Scene.GetAllComponents\u003CPlayerController\u003E().FirstOrDefault();\n        if(Player is not null)Player.HideBodyInFirstPerson=true;\n        magazine=new(MagazineSize,StartingReserve);\n        currentFov=HipFov;\n        weapon?.Deploy(true);\n    }\n    public bool Fire()\n    {\n        if(weapon is null||magazine is null||Player is null||Time.Now\u003CnextShot||Reloading)return false;\n        nextShot=Time.Now\u002B60/Math.Max(1,RoundsPerMinute);\n        if(!magazine.TryFire()){weapon.DryFire();return false;}\n        weapon.Attack();weapon.SetEmpty(Ammo==0);ShotsFired\u002B\u002B;\n        UpdateBodyPose();\n        Player.Renderer?.Set(\u0022b_attack\u0022,true);\n        var eye=Player.EyeTransform;\n        var hit=Scene.Trace.Ray(eye.Position,eye.Position\u002Beye.Rotation.Forward*Range)\n            .IgnoreGameObjectHierarchy(Player.GameObject).IgnoreGameObjectHierarchy(GameObject).UseHitboxes().Run();\n        if(hit.Hit)\n        {\n            Hits\u002B\u002B;LastHit=hit.HitPosition;\n            var info=new DamageInfo{Damage=Damage,Attacker=Player.GameObject,Weapon=GameObject,Position=hit.HitPosition,Origin=eye.Position};\n            for(var obj=hit.GameObject;obj is not null;obj=obj.Parent)\n                if(obj.GetComponent\u003CIDamageable\u003E() is {} damageable){damageable.OnDamage(info);break;}\n        }\n        Shot?.Invoke(hit);\n        return true;\n    }\n    public bool Reload()\n    {\n        if(magazine is null||weapon is null||!magazine.BeginReload())return false;\n        weapon.Reload(Ammo==0);reloadEnds=Time.Now\u002B(Ammo==0?EmptyReloadSeconds:ReloadSeconds);\n        UpdateBodyPose();\n        Player?.Renderer?.Set(\u0022b_reload\u0022,true);\n        return true;\n    }\n    public void Aim(bool value){Aiming=value\u0026\u0026!Reloading;if(weapon is not null)weapon.Aiming=Aiming;}\n    protected override void OnUpdate()\n    {\n        if(weapon is null||Player is null||magazine is null)return;\n        if(Reloading\u0026\u0026Time.Now\u003E=reloadEnds){magazine.FinishReload();weapon.SetEmpty(Ammo==0);}\n        if(ReadInput)Aim(Input.Down(\u0022Attack2\u0022));\n        if(Reloading)Aim(false);\n        weapon.Aiming=Aiming;\n        weapon.Sprinting=ReadInput\u0026\u0026Input.Down(\u0022Run\u0022)\u0026\u0026Player.Velocity.Length\u003E10\u0026\u0026!Aiming;\n        weapon.Movement=Math.Clamp(Player.Velocity.Length/150f,0,1);\n        weapon.SetParameter(\u0022attack_hold\u0022,ReadInput\u0026\u0026Input.Down(\u0022Attack1\u0022)\u0026\u0026!Reloading\u0026\u0026Ammo\u003E0?1f:0f);\n        weapon.SetParameter(\u0022firing_mode\u0022,3);\n        weapon.SetParameter(\u0022ironsights_fire_scale\u0022,.3f);\n        if(ReadInput)\n        {\n            if(Input.Pressed(\u0022Reload\u0022))Reload();\n            if(Input.Down(\u0022Attack1\u0022)\u0026\u0026!weapon.Sprinting)Fire();\n        }\n        weapon.ShowHands=!Player.ThirdPerson;\n        if(Player.ThirdPerson)weapon.WorldTransform=Player.EyeTransform;\n        // Keep the body graph warm even when hidden, so a camera switch during a shot\n        // or reload reveals the same action rather than restarting a holding pose.\n        UpdateBodyPose();\n        weapon.ThirdPersonBody=Player.ThirdPerson?Player.Renderer:null;\n        weapon.WorldGripBone=WorldGripBone;\n    }\n    private void UpdateBodyPose()\n    {\n        if(Player?.Renderer is not {} body)return;\n        body.Set(\u0022holdtype\u0022,BodyHoldType);body.Set(\u0022holdtype_handedness\u0022,BodyHandedness);\n        body.Set(\u0022aim_body_weight\u0022,1f);\n    }\n    public void ModifyCamera(CameraComponent camera,ref CameraView view)\n    {\n        if(Player is not null\u0026\u0026!Player.ThirdPerson\u0026\u0026camera==Scene.Camera)\n        {\n            currentFov=MathX.Lerp(currentFov,Aiming?AimFov:HipFov,Math.Clamp(Time.Delta*12,0,1));\n            view.FieldOfView=currentFov;\n        }\n    }\n    public void PostCameraSetup(CameraComponent camera,in CameraView view)\n    {\n        // EyeTransform during Update can precede the player\u0027s final camera pose.\n        // Place both viewmodel meshes against the composed view before PreRender.\n        if(weapon is not null\u0026\u0026Player is not null\u0026\u0026!Player.ThirdPerson\u0026\u0026camera==Scene.Camera)\n            weapon.WorldTransform=new global::Transform(view.Position,view.Rotation);\n    }\n    protected override void OnDisabled()\n    {\n        magazine?.CancelReload();Aiming=false;\n        if(weapon is not null){weapon.Aiming=false;weapon.ThirdPersonBody=null;}\n        Player?.Renderer?.Set(\u0022holdtype\u0022,0);\n    }\n}\n"}]}