{"TotalCount":108,"Files":[{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Core/Validation/HandRigValidator.cs","FileName":"HandRigValidator.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":394704,"IsPrivate":false,"Code":"#nullable enable\r\nusing HumanoidHandRetargeter.Mapping;\r\nusing SkeletonModel = HumanoidHandRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidHandRetargeter.Validation;\r\n\r\npublic sealed record RigIssue(string Code, string Message, HandSide? Side = null, int? Bone = null);\r\n\r\n/// \u003Csummary\u003EShared validation for automatic/manual mapping, batch processing and the editor.\u003C/summary\u003E\r\npublic static class HandRigValidator\r\n{\r\n    public static IReadOnlyList\u003CRigIssue\u003E Validate(SkeletonModel skeleton, IEnumerable\u003CHandRigDefinition\u003E hands)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(skeleton);\r\n        ArgumentNullException.ThrowIfNull(hands);\r\n        var issues = new List\u003CRigIssue\u003E();\r\n        var claimed = new HashSet\u003Cint\u003E();\r\n        var sides = new HashSet\u003CHandSide\u003E();\r\n        var count = 0;\r\n\r\n        foreach (var bone in skeleton.Bones)\r\n        {\r\n            if (!PoseValidator.ValidTransform(bone.RestLocal))\r\n                issues.Add(new(\u0022invalid-rest\u0022, $\u0022Bone \u0027{bone.Name}\u0027 has an invalid rest transform.\u0022, Bone: bone.Index));\r\n            var world = skeleton.RestWorld[bone.Index].Pos;\r\n            if (!float.IsFinite(world.X) || !float.IsFinite(world.Y) || !float.IsFinite(world.Z))\r\n                issues.Add(new(\u0022invalid-world-rest\u0022, $\u0022Bone \u0027{bone.Name}\u0027 has an invalid accumulated rest position.\u0022, Bone: bone.Index));\r\n        }\r\n\r\n        foreach (var hand in hands)\r\n        {\r\n            count\u002B\u002B;\r\n            if (hand is null)\r\n            {\r\n                issues.Add(new(\u0022missing-hand\u0022, \u0022A hand mapping is missing.\u0022));\r\n                continue;\r\n            }\r\n            if (!Enum.IsDefined(typeof(HandSide), hand.Side))\r\n                issues.Add(new(\u0022invalid-side\u0022, \u0022Choose left or right for the hand.\u0022));\r\n            if (!sides.Add(hand.Side))\r\n                issues.Add(new(\u0022duplicate-side\u0022, $\u0022More than one {hand.Side} hand is mapped.\u0022, hand.Side));\r\n\r\n            var arm = new[] { hand.Clavicle, hand.UpperArm, hand.Forearm, hand.Wrist }\r\n                .Where(b =\u003E b.HasValue).Select(b =\u003E b!.Value).ToArray();\r\n            CheckChain(arm, hand.Side, \u0022arm\u0022, allowZeroLength: true);\r\n            var roles = new HashSet\u003Cstring\u003E(StringComparer.Ordinal);\r\n            foreach (var digit in hand.Digits)\r\n            {\r\n                if (digit is null)\r\n                {\r\n                    issues.Add(new(\u0022missing-digit\u0022, \u0022A digit mapping is missing.\u0022, hand.Side));\r\n                    continue;\r\n                }\r\n                var key = digit.Role == DigitRole.Extra ? \u0022Extra:\u0022 \u002B digit.ExtraSlot : digit.Role.ToString();\r\n                if (!Enum.IsDefined(typeof(DigitRole), digit.Role)\r\n                    || (digit.Role == DigitRole.Extra \u0026\u0026 string.IsNullOrWhiteSpace(digit.ExtraSlot)))\r\n                    issues.Add(new(\u0022invalid-digit-role\u0022, \u0022Choose a digit role or name its extra slot.\u0022, hand.Side));\r\n                if (!roles.Add(key))\r\n                    issues.Add(new(\u0022duplicate-digit-role\u0022, $\u0022{hand.Side} {key} is mapped more than once.\u0022, hand.Side));\r\n                if (digit.Segments.Count == 0)\r\n                    issues.Add(new(\u0022empty-digit\u0022, $\u0022{hand.Side} {key} needs at least one segment.\u0022, hand.Side));\r\n                var chain = digit.Bones.ToArray();\r\n                CheckChain(chain, hand.Side, key, allowZeroLength: false, tip: digit.Tip);\r\n                if (chain.Length \u003E 0 \u0026\u0026 Valid(chain[0]) \u0026\u0026 Valid(hand.Wrist)\r\n                    \u0026\u0026 !skeleton.DescendsFrom(chain[0], hand.Wrist))\r\n                    issues.Add(new(\u0022wrong-wrist\u0022, $\u0022{hand.Side} {key} must descend from its wrist.\u0022, hand.Side, chain[0]));\r\n            }\r\n            foreach (var helper in hand.TwistOrHelperBones)\r\n            {\r\n                Claim(helper, hand.Side);\r\n                var armRoot = arm[0];\r\n                if (Valid(helper) \u0026\u0026 Valid(armRoot) \u0026\u0026 !skeleton.DescendsFrom(helper, armRoot))\r\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));\r\n            }\r\n        }\r\n        if (count == 0)\r\n            issues.Add(new(\u0022no-hands\u0022, \u0022Map at least one wrist before retargeting.\u0022));\r\n        return issues.AsReadOnly();\r\n\r\n        bool Valid(int bone) =\u003E bone \u003E= 0 \u0026\u0026 bone \u003C skeleton.Count;\r\n        void Claim(int bone, HandSide side)\r\n        {\r\n            if (!Valid(bone))\r\n                issues.Add(new(\u0022invalid-bone\u0022, $\u0022{side} mapping references a missing bone.\u0022, side, bone));\r\n            else if (!claimed.Add(bone))\r\n                issues.Add(new(\u0022overlapping-bone\u0022, $\u0022Bone \u0027{skeleton[bone].Name}\u0027 is assigned to multiple roles.\u0022, side, bone));\r\n        }\r\n        void CheckChain(int[] bones, HandSide side, string role, bool allowZeroLength, int? tip = null)\r\n        {\r\n            for (var i = 0; i \u003C bones.Length; i\u002B\u002B)\r\n            {\r\n                Claim(bones[i], side);\r\n                if (i == 0 || !Valid(bones[i - 1]) || !Valid(bones[i])) continue;\r\n                if (!skeleton.DescendsFrom(bones[i], bones[i - 1]))\r\n                    issues.Add(new(\u0022chain-order\u0022, $\u0022{side} {role} joints must follow their hierarchy.\u0022, side, bones[i]));\r\n                if (!allowZeroLength \u0026\u0026 bones[i] != tip \u0026\u0026\r\n                    System.Numerics.Vector3.DistanceSquared(skeleton.RestWorld[bones[i - 1]].Pos,\r\n                        skeleton.RestWorld[bones[i]].Pos) \u003C 1e-12f)\r\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]));\r\n            }\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Formats/Fbx/FbxScene.cs","FileName":"FbxScene.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":394704,"IsPrivate":false,"Code":"// Reused from humanoid-retargeter 26084c96c3fc870aaf9a5bd798de063ce2fd62df; specialized where needed for hand assets.\r\n#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\n\r\nnamespace HumanoidHandRetargeter.Formats.Fbx;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\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":394704,"IsPrivate":false,"Code":"namespace HumanoidHandRetargeter.Target;\r\n\r\n/// \u003Csummary\u003ESmall hidden mesh required by SceneModel\u0027s animation owner.\r\n/// Cube FBX layout from ufbx\u0027s public-domain maya_cube_7400_ascii fixture:\r\n/// https://github.com/ufbx/ufbx/blob/master/data/maya_cube_7400_ascii.fbx\r\n/// Used under ufbx LICENSE alternative B (public domain).\u003C/summary\u003E\r\npublic static class AnimationDriverMesh\r\n{\r\n    public static readonly string Fbx = Text.Replace(\u0022\\r\\n\u0022, \u0022\\n\u0022).Replace(\u0022\\n\u0022, \u0022\\r\\n\u0022);\r\n    private const string Text = \u0022\u0022\u0022\r\n; FBX 7.4.0 project file\r\n; ----------------------------------------------------\r\n\r\nFBXHeaderExtension:  {\r\n\tFBXHeaderVersion: 1003\r\n\tFBXVersion: 7400\r\n\tCreationTimeStamp:  {\r\n\t\tVersion: 1000\r\n\t\tYear: 2020\r\n\t\tMonth: 3\r\n\t\tDay: 31\r\n\t\tHour: 21\r\n\t\tMinute: 36\r\n\t\tSecond: 10\r\n\t\tMillisecond: 814\r\n\t}\r\n\tCreator: \u0022FBX SDK/FBX Plugins version 2019.2\u0022\r\n\tSceneInfo: \u0022SceneInfo::GlobalInfo\u0022, \u0022UserData\u0022 {\r\n\t\tType: \u0022UserData\u0022\r\n\t\tVersion: 100\r\n\t\tMetaData:  {\r\n\t\t\tVersion: 100\r\n\t\t\tTitle: \u0022\u0022\r\n\t\t\tSubject: \u0022\u0022\r\n\t\t\tAuthor: \u0022\u0022\r\n\t\t\tKeywords: \u0022\u0022\r\n\t\t\tRevision: \u0022\u0022\r\n\t\t\tComment: \u0022\u0022\r\n\t\t}\r\n\t\tProperties70:  {\r\n\t\t\tP: \u0022DocumentUrl\u0022, \u0022KString\u0022, \u0022Url\u0022, \u0022\u0022, \u0022animation_driver.fbx\u0022\r\n\t\t\tP: \u0022SrcDocumentUrl\u0022, \u0022KString\u0022, \u0022Url\u0022, \u0022\u0022, \u0022animation_driver.fbx\u0022\r\n\t\t\tP: \u0022Original\u0022, \u0022Compound\u0022, \u0022\u0022, \u0022\u0022\r\n\t\t\tP: \u0022Original|ApplicationVendor\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Autodesk\u0022\r\n\t\t\tP: \u0022Original|ApplicationName\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Maya\u0022\r\n\t\t\tP: \u0022Original|ApplicationVersion\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022201900\u0022\r\n\t\t\tP: \u0022Original|DateTime_GMT\u0022, \u0022DateTime\u0022, \u0022\u0022, \u0022\u0022, \u002231/03/2020 18:36:10.811\u0022\r\n\t\t\tP: \u0022Original|FileName\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022animation_driver.fbx\u0022\r\n\t\t\tP: \u0022LastSaved\u0022, \u0022Compound\u0022, \u0022\u0022, \u0022\u0022\r\n\t\t\tP: \u0022LastSaved|ApplicationVendor\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Autodesk\u0022\r\n\t\t\tP: \u0022LastSaved|ApplicationName\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Maya\u0022\r\n\t\t\tP: \u0022LastSaved|ApplicationVersion\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022201900\u0022\r\n\t\t\tP: \u0022LastSaved|DateTime_GMT\u0022, \u0022DateTime\u0022, \u0022\u0022, \u0022\u0022, \u002231/03/2020 18:36:10.811\u0022\r\n\t\t\tP: \u0022Original|ApplicationActiveProject\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022animation_driver.fbx\u0022\r\n\t\t}\r\n\t}\r\n}\r\nGlobalSettings:  {\r\n\tVersion: 1000\r\n\tProperties70:  {\r\n\t\tP: \u0022UpAxis\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,1\r\n\t\tP: \u0022UpAxisSign\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,1\r\n\t\tP: \u0022FrontAxis\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,2\r\n\t\tP: \u0022FrontAxisSign\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,1\r\n\t\tP: \u0022CoordAxis\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,0\r\n\t\tP: \u0022CoordAxisSign\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,1\r\n\t\tP: \u0022OriginalUpAxis\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,1\r\n\t\tP: \u0022OriginalUpAxisSign\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,1\r\n\t\tP: \u0022UnitScaleFactor\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,1\r\n\t\tP: \u0022OriginalUnitScaleFactor\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,1\r\n\t\tP: \u0022AmbientColor\u0022, \u0022ColorRGB\u0022, \u0022Color\u0022, \u0022\u0022,0,0,0\r\n\t\tP: \u0022DefaultCamera\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Producer Perspective\u0022\r\n\t\tP: \u0022TimeMode\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,11\r\n\t\tP: \u0022TimeProtocol\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,2\r\n\t\tP: \u0022SnapOnFrameMode\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\tP: \u0022TimeSpanStart\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,1924423250\r\n\t\tP: \u0022TimeSpanStop\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,384884650000\r\n\t\tP: \u0022CustomFrameRate\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,-1\r\n\t\tP: \u0022TimeMarker\u0022, \u0022Compound\u0022, \u0022\u0022, \u0022\u0022\r\n\t\tP: \u0022CurrentTimeMarker\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,-1\r\n\t}\r\n}\r\n\r\n; Documents Description\r\n;------------------------------------------------------------------\r\n\r\nDocuments:  {\r\n\tCount: 1\r\n\tDocument: 1907658701024, \u0022\u0022, \u0022Scene\u0022 {\r\n\t\tProperties70:  {\r\n\t\t\tP: \u0022SourceObject\u0022, \u0022object\u0022, \u0022\u0022, \u0022\u0022\r\n\t\t\tP: \u0022ActiveAnimStackName\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Take 001\u0022\r\n\t\t}\r\n\t\tRootNode: 0\r\n\t}\r\n}\r\n\r\n; Document References\r\n;------------------------------------------------------------------\r\n\r\nReferences:  {\r\n}\r\n\r\n; Object definitions\r\n;------------------------------------------------------------------\r\n\r\nDefinitions:  {\r\n\tVersion: 100\r\n\tCount: 6\r\n\tObjectType: \u0022GlobalSettings\u0022 {\r\n\t\tCount: 1\r\n\t}\r\n\tObjectType: \u0022AnimationStack\u0022 {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \u0022FbxAnimStack\u0022 {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \u0022Description\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022\u0022\r\n\t\t\t\tP: \u0022LocalStart\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022LocalStop\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ReferenceStart\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ReferenceStop\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,0\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tObjectType: \u0022AnimationLayer\u0022 {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \u0022FbxAnimLayer\u0022 {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \u0022Weight\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,100\r\n\t\t\t\tP: \u0022Mute\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022Solo\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022Lock\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022Color\u0022, \u0022ColorRGB\u0022, \u0022Color\u0022, \u0022\u0022,0.8,0.8,0.8\r\n\t\t\t\tP: \u0022BlendMode\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationAccumulationMode\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ScaleAccumulationMode\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022BlendModeBypass\u0022, \u0022ULongLong\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tObjectType: \u0022Geometry\u0022 {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \u0022FbxMesh\u0022 {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \u0022Color\u0022, \u0022ColorRGB\u0022, \u0022Color\u0022, \u0022\u0022,0.8,0.8,0.8\r\n\t\t\t\tP: \u0022BBoxMin\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022BBoxMax\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022Primary Visibility\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,1\r\n\t\t\t\tP: \u0022Casts Shadows\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,1\r\n\t\t\t\tP: \u0022Receive Shadows\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,1\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tObjectType: \u0022Material\u0022 {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \u0022FbxSurfaceLambert\u0022 {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \u0022ShadingModel\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Lambert\u0022\r\n\t\t\t\tP: \u0022MultiLayer\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022EmissiveColor\u0022, \u0022Color\u0022, \u0022\u0022, \u0022A\u0022,0,0,0\r\n\t\t\t\tP: \u0022EmissiveFactor\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,1\r\n\t\t\t\tP: \u0022AmbientColor\u0022, \u0022Color\u0022, \u0022\u0022, \u0022A\u0022,0.2,0.2,0.2\r\n\t\t\t\tP: \u0022AmbientFactor\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,1\r\n\t\t\t\tP: \u0022DiffuseColor\u0022, \u0022Color\u0022, \u0022\u0022, \u0022A\u0022,0.8,0.8,0.8\r\n\t\t\t\tP: \u0022DiffuseFactor\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,1\r\n\t\t\t\tP: \u0022Bump\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022NormalMap\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022BumpFactor\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,1\r\n\t\t\t\tP: \u0022TransparentColor\u0022, \u0022Color\u0022, \u0022\u0022, \u0022A\u0022,0,0,0\r\n\t\t\t\tP: \u0022TransparencyFactor\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,0\r\n\t\t\t\tP: \u0022DisplacementColor\u0022, \u0022ColorRGB\u0022, \u0022Color\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022DisplacementFactor\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,1\r\n\t\t\t\tP: \u0022VectorDisplacementColor\u0022, \u0022ColorRGB\u0022, \u0022Color\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022VectorDisplacementFactor\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,1\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tObjectType: \u0022Model\u0022 {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \u0022FbxNode\u0022 {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \u0022QuaternionInterpolate\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationOffset\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022RotationPivot\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022ScalingOffset\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022ScalingPivot\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022TranslationActive\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022TranslationMin\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022TranslationMax\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022TranslationMinX\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022TranslationMinY\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022TranslationMinZ\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022TranslationMaxX\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022TranslationMaxY\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022TranslationMaxZ\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationOrder\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationSpaceForLimitOnly\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationStiffnessX\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationStiffnessY\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationStiffnessZ\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022AxisLen\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,10\r\n\t\t\t\tP: \u0022PreRotation\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022PostRotation\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022RotationActive\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationMin\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022RotationMax\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022RotationMinX\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationMinY\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationMinZ\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationMaxX\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationMaxY\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationMaxZ\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022InheritType\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ScalingActive\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ScalingMin\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022ScalingMax\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,1,1,1\r\n\t\t\t\tP: \u0022ScalingMinX\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ScalingMinY\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ScalingMinZ\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ScalingMaxX\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ScalingMaxY\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ScalingMaxZ\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022GeometricTranslation\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022GeometricRotation\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022GeometricScaling\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,1,1,1\r\n\t\t\t\tP: \u0022MinDampRangeX\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MinDampRangeY\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MinDampRangeZ\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MaxDampRangeX\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MaxDampRangeY\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MaxDampRangeZ\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MinDampStrengthX\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MinDampStrengthY\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MinDampStrengthZ\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MaxDampStrengthX\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MaxDampStrengthY\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MaxDampStrengthZ\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022PreferedAngleX\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022PreferedAngleY\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022PreferedAngleZ\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022LookAtProperty\u0022, \u0022object\u0022, \u0022\u0022, \u0022\u0022\r\n\t\t\t\tP: \u0022UpVectorProperty\u0022, \u0022object\u0022, \u0022\u0022, \u0022\u0022\r\n\t\t\t\tP: \u0022Show\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,1\r\n\t\t\t\tP: \u0022NegativePercentShapeSupport\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,1\r\n\t\t\t\tP: \u0022DefaultAttributeIndex\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,-1\r\n\t\t\t\tP: \u0022Freeze\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022LODBox\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022Lcl Translation\u0022, \u0022Lcl Translation\u0022, \u0022\u0022, \u0022A\u0022,0,0,0\r\n\t\t\t\tP: \u0022Lcl Rotation\u0022, \u0022Lcl Rotation\u0022, \u0022\u0022, \u0022A\u0022,0,0,0\r\n\t\t\t\tP: \u0022Lcl Scaling\u0022, \u0022Lcl Scaling\u0022, \u0022\u0022, \u0022A\u0022,1,1,1\r\n\t\t\t\tP: \u0022Visibility\u0022, \u0022Visibility\u0022, \u0022\u0022, \u0022A\u0022,1\r\n\t\t\t\tP: \u0022Visibility Inheritance\u0022, \u0022Visibility Inheritance\u0022, \u0022\u0022, \u0022\u0022,1\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n\r\n; Object properties\r\n;------------------------------------------------------------------\r\n\r\nObjects:  {\r\n\tGeometry: 1907663073408, \u0022Geometry::\u0022, \u0022Mesh\u0022 {\r\n\t\tVertices: *24 {\r\n\t\t\ta: -0.5,-0.5,0.5,0.5,-0.5,0.5,-0.5,0.5,0.5,0.5,0.5,0.5,-0.5,0.5,-0.5,0.5,0.5,-0.5,-0.5,-0.5,-0.5,0.5,-0.5,-0.5\r\n\t\t}\r\n\t\tPolygonVertexIndex: *24 {\r\n\t\t\ta: 0,1,3,-3,2,3,5,-5,4,5,7,-7,6,7,1,-1,1,7,5,-4,6,0,2,-5\r\n\t\t}\r\n\t\tEdges: *12 {\r\n\t\t\ta: 0,2,6,10,3,1,7,5,11,9,15,13\r\n\t\t}\r\n\t\tGeometryVersion: 124\r\n\t\tLayerElementNormal: 0 {\r\n\t\t\tVersion: 102\r\n\t\t\tName: \u0022\u0022\r\n\t\t\tMappingInformationType: \u0022ByPolygonVertex\u0022\r\n\t\t\tReferenceInformationType: \u0022Direct\u0022\r\n\t\t\tNormals: *72 {\r\n\t\t\t\ta: 0,0,1,0,0,1,0,0,1,0,0,1,0,1,0,0,1,0,0,1,0,0,1,0,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,1,0,0,1,0,0,1,0,0,1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0\r\n\t\t\t}\r\n\t\t\tNormalsW: *24 {\r\n\t\t\t\ta: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayerElementBinormal: 0 {\r\n\t\t\tVersion: 102\r\n\t\t\tName: \u0022map1\u0022\r\n\t\t\tMappingInformationType: \u0022ByPolygonVertex\u0022\r\n\t\t\tReferenceInformationType: \u0022Direct\u0022\r\n\t\t\tBinormals: *72 {\r\n\t\t\t\ta: 0,1,-0,0,1,-0,0,1,-0,0,1,-0,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,0,1,0,0,1,0,0,1,0,0,1,-0,1,0,-0,1,0,0,1,-0,-0,1,0,0,1,0,0,1,0,0,1,0,0,1,0\r\n\t\t\t}\r\n\t\t\tBinormalsW: *24 {\r\n\t\t\t\ta: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1\r\n\t\t\t}\r\n\r\n\t\t}\r\n\t\tLayerElementTangent: 0 {\r\n\t\t\tVersion: 102\r\n\t\t\tName: \u0022map1\u0022\r\n\t\t\tMappingInformationType: \u0022ByPolygonVertex\u0022\r\n\t\t\tReferenceInformationType: \u0022Direct\u0022\r\n\t\t\tTangents: *72 {\r\n\t\t\t\ta: 1,-0,-0,1,-0,0,1,-0,0,1,-0,0,1,-0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,-0,1,0,-0,1,0,-0,1,0,-0,0,0,-1,0,0,-1,0,-0,-1,0,0,-1,0,-0,1,0,-0,1,0,-0,1,0,-0,1\r\n\t\t\t}\r\n\t\t\tTangentsW: *24 {\r\n\t\t\t\ta: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayerElementUV: 0 {\r\n\t\t\tVersion: 101\r\n\t\t\tName: \u0022map1\u0022\r\n\t\t\tMappingInformationType: \u0022ByPolygonVertex\u0022\r\n\t\t\tReferenceInformationType: \u0022IndexToDirect\u0022\r\n\t\t\tUV: *28 {\r\n\t\t\t\ta: 0.375,0,0.625,0,0.375,0.25,0.625,0.25,0.375,0.5,0.625,0.5,0.375,0.75,0.625,0.75,0.375,1,0.625,1,0.875,0,0.875,0.25,0.125,0,0.125,0.25\r\n\t\t\t}\r\n\t\t\tUVIndex: *24 {\r\n\t\t\t\ta: 0,1,3,2,2,3,5,4,4,5,7,6,6,7,9,8,1,10,11,3,12,0,2,13\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayerElementSmoothing: 0 {\r\n\t\t\tVersion: 102\r\n\t\t\tName: \u0022\u0022\r\n\t\t\tMappingInformationType: \u0022ByEdge\u0022\r\n\t\t\tReferenceInformationType: \u0022Direct\u0022\r\n\t\t\tSmoothing: *12 {\r\n\t\t\t\ta: 0,0,0,0,0,0,0,0,0,0,0,0\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayerElementMaterial: 0 {\r\n\t\t\tVersion: 101\r\n\t\t\tName: \u0022\u0022\r\n\t\t\tMappingInformationType: \u0022AllSame\u0022\r\n\t\t\tReferenceInformationType: \u0022IndexToDirect\u0022\r\n\t\t\tMaterials: *1 {\r\n\t\t\t\ta: 0\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayer: 0 {\r\n\t\t\tVersion: 100\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \u0022LayerElementNormal\u0022\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \u0022LayerElementBinormal\u0022\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \u0022LayerElementTangent\u0022\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \u0022LayerElementMaterial\u0022\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \u0022LayerElementSmoothing\u0022\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \u0022LayerElementUV\u0022\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tModel: 1907292236800, \u0022Model::animation_driver\u0022, \u0022Mesh\u0022 {\r\n\t\tVersion: 232\r\n\t\tProperties70:  {\r\n\t\t\tP: \u0022RotationActive\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,1\r\n\t\t\tP: \u0022InheritType\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,1\r\n\t\t\tP: \u0022ScalingMax\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\tP: \u0022DefaultAttributeIndex\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,0\r\n\t\t\tP: \u0022currentUVSet\u0022, \u0022KString\u0022, \u0022\u0022, \u0022U\u0022, \u0022map1\u0022\r\n\t\t}\r\n\t\tShading: T\r\n\t\tCulling: \u0022CullingOff\u0022\r\n\t}\r\n\tMaterial: 1908464858960, \u0022Material::lambert1\u0022, \u0022\u0022 {\r\n\t\tVersion: 102\r\n\t\tShadingModel: \u0022lambert\u0022\r\n\t\tMultiLayer: 0\r\n\t\tProperties70:  {\r\n\t\t\tP: \u0022AmbientColor\u0022, \u0022Color\u0022, \u0022\u0022, \u0022A\u0022,0,0,0\r\n\t\t\tP: \u0022DiffuseColor\u0022, \u0022Color\u0022, \u0022\u0022, \u0022A\u0022,0.5,0.5,0.5\r\n\t\t\tP: \u0022DiffuseFactor\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,0.800000011920929\r\n\t\t\tP: \u0022TransparencyFactor\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,1\r\n\t\t\tP: \u0022Emissive\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\tP: \u0022Ambient\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\tP: \u0022Diffuse\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0.400000005960464,0.400000005960464,0.400000005960464\r\n\t\t\tP: \u0022Opacity\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,1\r\n\t\t}\r\n\t}\r\n\tAnimationStack: 1907674289488, \u0022AnimStack::Take 001\u0022, \u0022\u0022 {\r\n\t\tProperties70:  {\r\n\t\t\tP: \u0022LocalStart\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,1924423250\r\n\t\t\tP: \u0022LocalStop\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,230930790000\r\n\t\t\tP: \u0022ReferenceStart\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,1924423250\r\n\t\t\tP: \u0022ReferenceStop\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,230930790000\r\n\t\t}\r\n\t}\r\n\tAnimationLayer: 1907631731008, \u0022AnimLayer::BaseLayer\u0022, \u0022\u0022 {\r\n\t}\r\n}\r\n\r\n; Object connections\r\n;------------------------------------------------------------------\r\n\r\nConnections:  {\r\n\r\n\t;Model::animation_driver, Model::RootNode\r\n\tC: \u0022OO\u0022,1907292236800,0\r\n\r\n\t;AnimLayer::BaseLayer, AnimStack::Take 001\r\n\tC: \u0022OO\u0022,1907631731008,1907674289488\r\n\r\n\t;Geometry::, Model::animation_driver\r\n\tC: \u0022OO\u0022,1907663073408,1907292236800\r\n\r\n\t;Material::lambert1, Model::animation_driver\r\n\tC: \u0022OO\u0022,1908464858960,1907292236800\r\n}\r\n;Takes section\r\n;----------------------------------------------------\r\n\r\nTakes:  {\r\n\tCurrent: \u0022Take 001\u0022\r\n\tTake: \u0022Take 001\u0022 {\r\n\t\tFileName: \u0022Take_001.tak\u0022\r\n\t\tLocalTime: 1924423250,230930790000\r\n\t\tReferenceTime: 1924423250,230930790000\r\n\t}\r\n}\r\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":394704,"IsPrivate":false,"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)\r\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),\r\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) });\r\n            if(meshNames is {Count:\u003E0})\r\n            {\r\n                var names=new KvArray();foreach(var name in meshNames)names.Items.Add(new KvString(name));\r\n                ((KvObject)meshes.Items[0])[\u0022import_filter\u0022]=new KvObject\r\n                    {[\u0022exclude_by_default\u0022]=new KvBool(true),[\u0022exception_list\u0022]=names};\r\n            }\r\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":394704,"IsPrivate":false,"Code":"#nullable enable\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Editor;\r\nusing Sandbox;\r\n\r\nnamespace HumanoidHandRetargeter.Editor;\r\n\r\n/// \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":"Code/Core/Mapping/BoneNameTokens.cs","FileName":"BoneNameTokens.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":394704,"IsPrivate":false,"Code":"// Tokenization reused from humanoid-retargeter 26084c96c3fc870aaf9a5bd798de063ce2fd62df.\r\nusing System.Text;\r\nnamespace HumanoidHandRetargeter.Mapping;\r\ninternal static class BoneNameTokens\r\n{\r\n    internal static List\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)\r\n        {\r\n            var c = name[i];\r\n            if (!char.IsLetterOrDigit(c))\r\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)\r\n                    || (char.IsUpper(c) \u0026\u0026 char.IsLower(previous))\r\n                    // Acronym/single-side prefix before a Pascal word: LThumb \u2192 L, Thumb;\r\n                    // FBXNode \u2192 FBX, Node. Without this, one-joint BVH thumbs lose both\r\n                    // their side and their otherwise unambiguous finger name.\r\n                    || (char.IsUpper(c) \u0026\u0026 char.IsUpper(previous)\r\n                        \u0026\u0026 i \u002B 1 \u003C name.Length \u0026\u0026 char.IsLower(name[i \u002B 1]));\r\n                if (boundary)\r\n                    Flush();\r\n            }\r\n            current.Append(char.ToLowerInvariant(c));\r\n            previous = c;\r\n        }\r\n        Flush();\r\n        return tokens;\r\n\r\n        void Flush()\r\n        {\r\n            if (current.Length \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":394704,"IsPrivate":false,"Code":"#nullable enable\r\nusing Vec = System.Numerics.Vector3;\r\nusing Skel = HumanoidHandRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidHandRetargeter.Mapping;\r\n\r\ninternal static partial class HandDetectionProfiles\r\n{\r\n    // Split biceps/forearms, a wrist-to-palm link, and five palm-rooted rays.\r\n    // Numeric/duplicate suffixes identify objects, not sides or anatomical fingers.\r\n    private static void ResolveUnlabelledSplitArms(Skel rig,Hint?[] hints)\r\n    {\r\n        string Name(int i)\r\n        {\r\n            var name=rig[i].Name[(rig[i].Name.LastIndexOf(\u0027:\u0027)\u002B1)..];\r\n            return Key(name.Split(\u0027#\u0027)[0].Replace(\u0022_duplicate\u0022,\u0022\u0022,StringComparison.OrdinalIgnoreCase)).TrimEnd(\u00220123456789\u0022.ToCharArray());\r\n        }\r\n        int Child(int i,string name)=\u003Erig.ChildrenOf(i).Count==1\u0026\u0026Name(rig.ChildrenOf(i)[0])==name?rig.ChildrenOf(i)[0]:-1;\r\n        var proposals=new List\u003C(HandSide Side,Dictionary\u003Cint,string\u003E Roles)\u003E();\r\n        foreach(var upper in rig.Bones.Where(b=\u003EName(b.Index) is \u0022bisep\u0022 or \u0022bicep\u0022 or \u0022biceps\u0022))\r\n        {\r\n            var upperSplit=Child(upper.Index,Name(upper.Index));if(upperSplit\u003C0)continue;\r\n            var lower=Child(upperSplit,\u0022forearm\u0022);if(lower\u003C0)continue;\r\n            var lowerSplit=Child(lower,\u0022forearm\u0022);if(lowerSplit\u003C0)continue;\r\n            var wrist=Child(lowerSplit,\u0022wrist\u0022);if(wrist\u003C0)continue;\r\n            var palm=Child(wrist,\u0022hand\u0022);if(palm\u003C0||rig.ChildrenOf(palm).Count!=5)continue;\r\n            var rays=new List\u003Cint[]\u003E();\r\n            foreach(var root in rig.ChildrenOf(palm))\r\n            {\r\n                var ray=new List\u003Cint\u003E{root};var current=root;\r\n                while(rig.ChildrenOf(current).Count==1){current=rig.ChildrenOf(current)[0];ray.Add(current);}\r\n                if(rig.ChildrenOf(current).Count!=0){rays.Clear();break;}\r\n                if(Name(ray[^1]).Contains(\u0022end\u0022,StringComparison.Ordinal))ray.RemoveAt(ray.Count-1);\r\n                rays.Add(ray.ToArray());\r\n            }\r\n            if(rays.Count!=5)continue;\r\n            var thumbs=rays.Where(r=\u003Er.Length==3\u0026\u0026Name(r[0]) is \u0022thum\u0022 or \u0022thumb\u0022).ToArray();\r\n            var fingers=rays.Where(r=\u003Er.Length==4\u0026\u0026Name(r[0])==\u0022finger\u0022).ToArray();\r\n            if(thumbs.Length!=1||fingers.Length!=4)continue;\r\n            var thumb=thumbs[0];\r\n            Vec Pos(int i)=\u003Erig.RestWorld[i].Pos;\r\n            var ordered=fingers.OrderBy(r=\u003EVec.DistanceSquared(Pos(r[1]),Pos(thumb[1]))).ToArray();\r\n            var radial=Pos(ordered[0][1])-Pos(ordered[3][1]);\r\n            if(radial.LengthSquared()\u003C1e-8f)continue;\r\n            radial=Vec.Normalize(radial);\r\n            // Require four distinctly ordered knuckles, not just a nearest-thumb guess.\r\n            var spacing=ordered.Select(r=\u003EVec.Dot(Pos(r[1])-Pos(ordered[3][1]),radial)).ToArray();\r\n            if(Enumerable.Range(0,3).Any(i=\u003Espacing[i]-spacing[i\u002B1]\u003Cspacing[0]*.1f))continue;\r\n            var forward=ordered.Select(r=\u003EPos(r[3])-Pos(r[1])).Aggregate(Vec.Zero,(a,b)=\u003Ea\u002Bb);\r\n            var normal=Vec.Cross(forward,radial);var thumbBend=Pos(thumb[2])-Pos(thumb[1]);\r\n            if(normal.LengthSquared()\u003C1e-8f||thumbBend.LengthSquared()\u003C1e-8f)continue;\r\n            var chirality=Vec.Dot(Vec.Normalize(normal),Vec.Normalize(thumbBend));\r\n            // An out-of-plane thumb bend supplies the palmar direction. Flat or\r\n            // ambiguous geometry remains for review rather than guessing a side.\r\n            if(MathF.Abs(chirality)\u003C.25f)continue;\r\n            var side=chirality\u003E0?HandSide.Left:HandSide.Right;\r\n            var roles=new Dictionary\u003Cint,string\u003E{{upper.Index,\u0022UpperArm\u0022},{lower,\u0022LowerArm\u0022},{wrist,\u0022Hand\u0022},{palm,\u0022PalmMeta\u0022}};\r\n            var digitNames=new[]{\u0022Index\u0022,\u0022Middle\u0022,\u0022Ring\u0022,\u0022Pinky\u0022};\r\n            for(var digit=0;digit\u003C4;digit\u002B\u002B)\r\n                for(var joint=0;joint\u003C4;joint\u002B\u002B)roles[ordered[digit][joint]]=digitNames[digit]\u002B(joint==0?\u0022Meta\u0022:\u0022Prox\u0022);\r\n            foreach(var bone in thumb)roles[bone]=\u0022ThumbProx\u0022;\r\n            if(roles.Keys.Any(i=\u003Ehints[i] is not null))continue;\r\n            proposals.Add((side,roles));\r\n        }\r\n        // Require a complete, disjoint bilateral pair before using unnamed sides.\r\n        if(proposals.Count!=2||proposals[0].Side==proposals[1].Side||proposals[0].Roles.Keys.Intersect(proposals[1].Roles.Keys).Any())return;\r\n        foreach(var proposal in proposals)\r\n            foreach(var role in proposal.Roles)hints[role.Key]=new(role.Value,proposal.Side,\u0022split_arm_palm_rays\u0022);\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Editor/HumanoidHandRetargeter/HandEditorPipeline.cs","FileName":"HandEditorPipeline.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":394704,"IsPrivate":false,"Code":"#nullable enable\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing System.Runtime.CompilerServices;\r\nusing System.Security.Cryptography;\r\nusing System.Threading;\r\nusing System.Threading.Tasks;\r\nusing Editor;\r\nusing Sandbox;\r\nusing HumanoidHandRetargeter.Calibration;\r\nusing HumanoidHandRetargeter.Formats.Dmx;\r\nusing HumanoidHandRetargeter.Formats.Fbx;\r\nusing HumanoidHandRetargeter.Mapping;\r\nusing HumanoidHandRetargeter.Maths;\r\nusing HumanoidHandRetargeter.Retargeting;\r\nusing HumanoidHandRetargeter.Skeleton;\r\nusing HumanoidHandRetargeter.Target;\r\nusing Skel = HumanoidHandRetargeter.Skeleton.Skeleton;\r\nusing Vec = System.Numerics.Vector3;\r\nusing Quat = System.Numerics.Quaternion;\r\n\r\nnamespace HumanoidHandRetargeter.Editor;\r\n\r\npublic sealed class HandSource\r\n{\r\n    public required string Path { get; init; }\r\n    public required SourceScene Scene { get; init; }\r\n    public required HandMappingResult Mapping { get; set; }\r\n    public string? ModelPath { get; init; }\r\n    public IReadOnlyDictionary\u003CHandSide, Quat\u003E? PalmFrames { get; set; }\r\n}\r\n\r\npublic sealed class HandTarget\r\n{\r\n    public IReadOnlyList\u003Cstring\u003E ImportNotes { get; init; } = Array.Empty\u003Cstring\u003E();\r\n    public required string ModelPath { get; init; }\r\n    public required Skel Skeleton { get; init; }\r\n    public required HandMappingResult Mapping { get; set; }\r\n    public IReadOnlyDictionary\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);\r\n\r\npublic sealed record HandBakedClip(HandSource Source, Clip Original, Clip Baked, IReadOnlyList\u003Cstring\u003E Notes,Vec? WeaponOffset=null,HandMotionOptions? MotionOptions=null);\r\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();\r\n        if (path.EndsWith(\u0022.fbx\u0022,StringComparison.OrdinalIgnoreCase)) path = await PrepareMeshAsync(path,cancel,importNotes);\r\n        path = ModelPath(path);\r\n        var model = Model.Load(path);\r\n        if (model is null || model.IsError || model.BoneCount == 0) throw new InvalidOperationException($\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) };\r\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 native = await Task.Run(()=\u003EFbxImporter.Import(File.ReadAllBytes(path),new FbxImportOptions {SampleFps=fps}),cancel);\r\n            await MainThread();\r\n            // Share the target\u0027s X-forward/Z-up view space so the source grip, eye and weapon\r\n            // motion transfer directly, whatever axes or facing the file was authored in.\r\n            var nativeMapping=HandPresetStore.Load(native.Skeleton);\r\n            var axesMapping=nativeMapping ?? HandRigDetector.Detect(native.Skeleton);\r\n            var turn=HandViewSpace.ViewRotation(native,axesMapping);\r\n            var scene=HandViewSpace.ToViewAxes(native,axesMapping);\r\n            // Mappings are index based; palm frames saved against the native axes turn with the rig.\r\n            var palms=HandPresetStore.LoadPalms(scene.Skeleton) ?? HandPresetStore.LoadPalms(native.Skeleton)?.ToDictionary(p=\u003Ep.Key,p=\u003EQuat.Normalize(turn*p.Value));\r\n            return new() { Path=path, Scene=scene, PalmFrames=palms, Mapping=HandPresetStore.Load(scene.Skeleton) ?? nativeMapping ?? HandRigDetector.Detect(scene.Skeleton) };\r\n        }\r\n        await MainThread();\r\n        path = ModelPath(path);\r\n        var model = Model.Load(path);\r\n        if (model is null || model.IsError) throw new InvalidOperationException($\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())\r\n        {\r\n            cancel.ThrowIfCancellationRequested();\r\n            clips.Add(sampling.Sample(name,fps,cancel));\r\n            await Task.Delay(1,cancel); await MainThread();\r\n        }\r\n        // Facepunch weapons author recoil as additive layers. Capture their complete firing\r\n        // pose through the original weapon graph instead of retargeting a delta as a full pose.\r\n        if(skeleton.IndexOf(\u0022weapon_root\u0022)\u003E=0\u0026\u0026!clips.Any(c=\u003EWeaponAnimGraph.Classify(c.Name)==WeaponAction.Fire)\r\n            \u0026\u0026clips.FirstOrDefault(c=\u003Ec.Name.StartsWith(\u0022Fire_\u0022,StringComparison.OrdinalIgnoreCase)\u0026\u0026c.Name.EndsWith(\u0022_delta\u0022,StringComparison.OrdinalIgnoreCase)\r\n                \u0026\u0026!c.Name.Contains(\u0022Hold\u0022,StringComparison.OrdinalIgnoreCase)\u0026\u0026!c.Name.Contains(\u0022Dry\u0022,StringComparison.OrdinalIgnoreCase)) is {} recoil)\r\n        {\r\n            var firing=sampling.SampleFire(Math.Max(.25f,(recoil.FrameCount-1)/recoil.Fps),fps,cancel);\r\n            if(firing is not null)clips.Add(firing);\r\n        }\r\n        return new() { Path=path,ModelPath=path, Scene=new SourceScene(skeleton,clips,2.54f,upAxis:2,frontAxis:0,coordAxis:1),\r\n            PalmFrames=HandPresetStore.LoadPalms(skeleton), Mapping=HandPresetStore.Load(skeleton) ?? HandRigDetector.Detect(skeleton) };\r\n    }\r\n\r\n    public static HandRetargetProfile Calibrate(HandSource source,HandTarget target)\r\n        =\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;\r\n        // FBX sources are loaded in view space, so every weapon file (named weapon bones or not)\r\n        // keeps both palms locked to the source grip relative to the eye.\r\n        var viewSpace=source.ModelPath is not null||source.Scene.UpAxis==2\u0026\u0026source.Scene.FrontAxis==0;\r\n        if(options.PreserveWeaponGrip\u0026\u0026options.TransferWristPosition\u0026\u0026(source.ModelPath is null\u0026\u0026viewSpace\r\n            ||source.Scene.Skeleton.Bones.Any(b=\u003Eb.Name.Equals(\u0022weapon_root\u0022,StringComparison.OrdinalIgnoreCase))))\r\n            weaponOffset ??= HandViewSpace.EyePosition(target.Skeleton,target.Mapping)-HandViewSpace.EyePosition(source.Scene.Skeleton,source.Mapping);\r\n        options=new HandMotionOptions{TransferWristPosition=options.TransferWristPosition,ScaleWristTravel=options.ScaleWristTravel,\r\n            SolveArmIk=options.SolveArmIk,PreserveWeaponGrip=options.PreserveWeaponGrip,WeaponSpaceOffset=weaponOffset,\r\n            WristTravelBasis=options.WristTravelBasis??(viewSpace?Quat.Identity:null)};\r\n        var notes=weaponOffset.HasValue?profile.Notes.Concat(new[]{\u0022Preserved both palm grip anchors in one fixed weapon space; weapon motion is not scaled per arm.\u0022}).ToArray():profile.Notes;\r\n        return new(source,clip,HandRetargeter.Bake(profile,clip,options,cancel),notes,weaponOffset,options);\r\n    }\r\n\r\n    public static async Task\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);\r\n        var turn=ExportRootRotation(original);\r\n        XForm ExportLocal(XForm value,int bone)=\u003Etarget.Skeleton[bone].ParentIndex\u003C0\r\n            ?new XForm(Vec.Transform(value.Pos*factor,turn),Quat.Normalize(turn*value.Rot))\r\n            :new XForm(value.Pos*factor,value.Rot);\r\n        Clip ExportUnits(Clip clip) =\u003E new(clip.Name,clip.Fps,clip.Looping,clip.Frames.Select(frame=\u003Eframe.Select(ExportLocal).ToArray()).ToList(),clip.NativeFps);\r\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());\r\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;\r\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()});\r\n            var file=folder\u002B\u0022/\u0022\u002Bname\u002B\u0022_\u0022\u002BContentKey(content)\u002B\u0022.dmx\u0022;\r\n            files[file]=content;\r\n            entries.Add(new(name,file,clip.Baked.Looping));\r\n            if(preserveSourceTracks)\r\n            {\r\n                // Full source companion intentionally retains every authored camera,\r\n                // weapon and IK track with its original hierarchy, units and timing.\r\n                var tracks=DmxWriter.Write(clip.Source.Scene.Skeleton,clip.Original,new() {Name=name\u002B\u0022_source_tracks\u0022,UpAxisY=clip.Source.Scene.UpAxis==1,ForwardParity=clip.Source.ModelPath is null\u0026\u0026clip.Source.Scene.FrontAxis!=0?2:1});\r\n                files[folder\u002B\u0022/source_tracks/\u0022\u002Bname\u002B\u0022_\u0022\u002BContentKey(tracks)\u002B\u0022.dmx\u0022]=tracks;\r\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});\r\n        var generatedAssets=new Dictionary\u003Cstring,string\u003E(StringComparer.OrdinalIgnoreCase);\r\n        var bundles=autoGraph?HandWeaponExport.Prepare(target,clips,outputModel,files,generatedAssets,cancel):Array.Empty\u003CHandWeaponExport.Bundle\u003E();\r\n        foreach(var bundle in bundles)generatedAssets[bundle.PrefabPath]=HandWeaponExport.Prefab(bundle);\r\n        var committed=await VmdlSetupTransaction.CommitAsync(Assets,outputModel,existing,prepared,files,async(paths,token)=\u003E{\r\n            await MainThread(); foreach(var path in paths) AssetSystem.RegisterFile(path);\r\n            foreach(var bundle in bundles)\r\n            {\r\n                if(bundle.LiveCalibration is null)\r\n                {\r\n                    if(!await CompileAsync(System.IO.Path.Combine(Assets,bundle.ModelPath),token))throw new InvalidOperationException(\u0022Weapon animation model did not compile: \u0022\u002Bbundle.ModelPath);\r\n                    await MainThread();\r\n                    await Task.Delay(100,token);await MainThread();\r\n                    var owner=Model.Load(bundle.ModelPath);var graph=AnimationGraph.Load(bundle.GraphPath);\r\n                    if(owner is null||owner.IsError||!owner.HasRenderMeshes()||owner.BoneCount\u003Cbundle.BoneCount||graph is null||graph.IsError\r\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);\r\n                    if(target.Skeleton.Bones.Any(b=\u003Eowner.Bones.GetBone(WeaponClipBuilder.HandPrefix\u002Bb.Name) is null))return false;\r\n                }\r\n\r\n                if(!await CompileAsync(System.IO.Path.Combine(Assets,bundle.PrefabPath),token))throw new InvalidOperationException(\u0022Weapon prefab did not compile: \u0022\u002Bbundle.PrefabPath);\r\n                await MainThread();\r\n                // The compiled file reaches disk before the editor\u0027s asset record updates.\r\n                // Managed resource loading uses that record, not File.Exists.\r\n                var prefabAsset=AssetSystem.FindByPath(bundle.PrefabPath);\r\n                for(var wait=0;wait\u003C50\u0026\u0026string.IsNullOrEmpty(prefabAsset.GetCompiledFile(true));wait\u002B\u002B)\r\n                {await Task.Delay(100,token);await MainThread();}\r\n                var prefab=prefabAsset.LoadResource\u003CPrefabFile\u003E();\r\n                if(prefab is null||prefab.IsError||SceneUtility.GetPrefabScene(prefab) is null)throw new InvalidOperationException(\u0022Weapon prefab could not be loaded: \u0022\u002Bbundle.PrefabPath);\r\n            }\r\n            if(!await CompileAsync(absolute,token)) return false;\r\n            await MainThread(); var model=Model.Load(outputModel);\r\n            // The compiled file can arrive before an already-loaded model refreshes\r\n            // its sequence list. Re-exporting changed motion adds a preserved-name\r\n            // suffix, so wait for those new sequences before validating the bake.\r\n            var modelDeadline=DateTime.UtcNow.AddSeconds(15);\r\n            while(model is not null\u0026\u0026!model.IsError\u0026\u0026!prepared.Animations.All(a=\u003Emodel.AnimationNames.Contains(a.SequenceName))\r\n                \u0026\u0026DateTime.UtcNow\u003CmodelDeadline)\r\n            {\r\n                await Task.Delay(50,token);await MainThread();model=Model.Load(outputModel);\r\n            }\r\n            if(model is null || model.IsError || !prepared.Animations.All(a=\u003Emodel.AnimationNames.Contains(a.SequenceName)))return false;\r\n            if(prepared.GeneratedGraphPath is { } graphPath){var graph=AnimationGraph.Load(graphPath);if(graph is null||graph.IsError)return false;}\r\n\r\n            var compiledRig=ReadSkeleton(model);\r\n            using var sampler=new ModelSampler(model,compiledRig);\r\n            var checkedBones=target.Mapping.Hands.SelectMany(h=\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);\r\n        return new(outputModel,prepared.Changes.Concat(bundles.Select(b=\u003Eb.LiveCalibration is not null\r\n            ? $\u0022Preserved complete source graph {b.GraphPath} with live custom-hand retargeting: {b.PrefabPath}\u0022\r\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());\r\n    }\r\n\r\n    private static float ExportPositionFactor(string text)\r\n    {\r\n        float scale=1;\r\n        void Walk(KvValue v) { if(v is KvObject o) { if(o.GetString(\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);\r\n    }\r\n\r\n    private static Quat ExportRootRotation(string text)\r\n    {\r\n        var turn=false;\r\n        void Walk(KvValue value)\r\n        {\r\n            if(value is KvObject node)\r\n            {\r\n                if(node.GetString(\u0022_class\u0022)==\u0022ModelModifier_ScaleAndMirror\u0022\r\n                    \u0026\u0026node.GetOrNull(\u0022mirror_x\u0022) is KvBool {Value:true}\r\n                    \u0026\u0026node.GetOrNull(\u0022mirror_y\u0022) is KvBool {Value:true}\r\n                    \u0026\u0026node.GetOrNull(\u0022mirror_z\u0022) is not KvBool {Value:true})turn=!turn;\r\n                foreach(var key in node.Keys)Walk(node[key]);\r\n            }\r\n            else if(value is KvArray array)foreach(var child in array.Items)Walk(child);\r\n        }\r\n        Walk(Kv3.Parse(text).Root);\r\n        return turn?Quat.CreateFromAxisAngle(Vec.UnitZ,MathF.PI):Quat.Identity;\r\n    }\r\n\r\n    private static async Task\u003Cstring\u003E PrepareMeshAsync(string file, CancellationToken cancel,List\u003Cstring\u003E importNotes)\r\n    {\r\n        var bytes=File.ReadAllBytes(file);\r\n        var materials=await Task.Run(()=\u003EFbxMaterialAssets.Inspect(file,bytes),cancel);\r\n        importNotes.AddRange(materials.Notes);\r\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;\r\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);\r\n        var embeddedClips=scene.Clips;\r\n        var repaired=await Task.Run(()=\u003E\r\n        {\r\n            var result=FbxBindPoseFixer.TryFix(bytes,out var report);\r\n            return (Bytes:result,Report:report);\r\n        },cancel);\r\n        if(repaired.Bytes is {} repairedBytes)\r\n        {\r\n            var repairedScene=await Task.Run(()=\u003EFbxImporter.Import(repairedBytes),cancel);\r\n            if(!scene.Skeleton.Bones.Select(b=\u003E(b.Name,b.ParentIndex)).SequenceEqual(repairedScene.Skeleton.Bones.Select(b=\u003E(b.Name,b.ParentIndex))))\r\n                throw new InvalidOperationException(\u0022Bind-pose repair changed the target hierarchy; the original FBX was not modified.\u0022);\r\n            scene=repairedScene;bytes=repairedBytes;\r\n            importNotes.Add(\u0022Repaired the imported copy from its authored bind pose: \u0022\u002Brepaired.Report);\r\n        }\r\n        var meshNames=FbxSkinnedMeshes.ReadNames(bytes);\r\n        await MainThread();\r\n        var meshAbs=System.IO.Path.Combine(Assets,mesh); Directory.CreateDirectory(System.IO.Path.GetDirectoryName(meshAbs)!);\r\n        if(!File.Exists(meshAbs)) File.WriteAllBytes(meshAbs,bytes);\r\n        var modelAbs=System.IO.Path.Combine(Assets,model);\r\n        var remaps=HandMaterialImport.Write(materials,folder);\r\n        var isNew=!File.Exists(modelAbs);\r\n        if(isNew)\r\n        {\r\n            // Keep the full authored hierarchy before ModelDoc can cull unweighted\r\n            // ancestors. The audited importer preview uses this same bind-source technique.\r\n            string EngineName(string name){var suffix=name.LastIndexOf(\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();\r\n            var names=new HashSet\u003Cstring\u003E(StringComparer.OrdinalIgnoreCase){\u0022bindPose\u0022};\r\n            for(var i=0;i\u003CembeddedClips.Count;i\u002B\u002B)\r\n            {\r\n                cancel.ThrowIfCancellationRequested();\r\n                var clip=embeddedClips[i];\r\n                var stem=SafeName(clip.Name).Replace(\u0027-\u0027,\u0027_\u0027);\r\n                var name=stem;var suffix=2;\r\n                while(!names.Add(name))name=stem\u002B\u0022_\u0022\u002Bsuffix\u002B\u002B;\r\n                var path=folder\u002B\u0022/embedded_\u0022\u002Bi\u002B\u0022.dmx\u0022;\r\n                // Imported locals are already centimeters, matching mesh_bind and the\r\n                // model\u0027s final unit conversion. Preserve authored takes without IK.\r\n                File.WriteAllText(System.IO.Path.Combine(Assets,path),DmxWriter.Write(rig,clip,new(){Name=name,UpAxisY=scene.UpAxis==1}));\r\n                AssetSystem.RegisterFile(System.IO.Path.Combine(Assets,path));\r\n                entries.Add(new(name,path,clip.Looping));\r\n            }\r\n            var prepared=VmdlSetupService.Prepare(HandModelFactory.Create(mesh:mesh,meshUnitScaleCm:scene.UnitScaleCm,materialRemaps:remaps,meshNames:meshNames),entries,new(){ModelPath=model,BindPoseSource=bindPath,AutoConfigureAnimGraph=false});\r\n            File.WriteAllText(modelAbs,prepared.VmdlText);\r\n            AssetSystem.RegisterFile(System.IO.Path.Combine(Assets,bindPath));\r\n        }\r\n        AssetSystem.RegisterFile(meshAbs);\r\n        if(!await CompileAsync(modelAbs,cancel)) throw new InvalidOperationException(\u0022The target FBX could not be compiled into a skinned model.\u0022);\r\n        if(isNew)\r\n        {\r\n            await MainThread();\r\n            var compiledRig=ReadSkeleton(Model.Load(model));\r\n            var fit=HandTargetFit.Analyze(compiledRig,HandRigDetector.Detect(compiledRig));\r\n            if(fit.Scale!=1||fit.TurnAround)\r\n            {\r\n                File.WriteAllText(modelAbs,fit.Apply(File.ReadAllText(modelAbs)));\r\n                if(!await CompileAsync(modelAbs,cancel))throw new InvalidOperationException(\u0022The fitted FPS target could not be compiled.\u0022);\r\n                // Resource compilation finishes before the loaded Model\u0027s bone cache\r\n                // refreshes. Never hand an old-size skeleton to a newly scaled mesh.\r\n                var deadline=DateTime.UtcNow.AddSeconds(15);\r\n                while(true)\r\n                {\r\n                    await Task.Delay(50,cancel);await MainThread();\r\n                    var loaded=ReadSkeleton(Model.Load(model));\r\n                    var ready=compiledRig.Bones.All(b=\u003E\r\n                    {\r\n                        var index=loaded.IndexOf(b.Name);if(index\u003C0)return false;\r\n                        var expected=compiledRig.RestWorld[b.Index].Pos*fit.Scale;\r\n                        if(fit.TurnAround)expected=new Vec(-expected.X,-expected.Y,expected.Z);\r\n                        return Vec.Distance(loaded.RestWorld[index].Pos,expected)\u003C.02f;\r\n                    });\r\n                    if(ready)break;\r\n                    if(DateTime.UtcNow\u003E=deadline)throw new InvalidOperationException(\u0022The fitted model is still reloading. Select the FBX again after asset compilation finishes.\u0022);\r\n                }\r\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);\r\n                if(fit.TurnAround)importNotes.Add(\u0022Turned the imported target 180 degrees to align its left and right shoulders with FPS weapon coordinates.\u0022);\r\n            }\r\n        }\r\n        await MainThread(); return model;\r\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)\r\n    {\r\n        var materials=model.Materials.Concat(Enumerable.Range(0,model.MaterialGroupCount).SelectMany(group=\u003Emodel.GetMaterials(group))).Distinct();\r\n        foreach(var material in materials)\r\n        {\r\n            if(material is null||material.IsError||string.Equals(material.ResourcePath,\u0022materials/error.vmat\u0022,StringComparison.OrdinalIgnoreCase))return true;\r\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})\r\n                if(material.GetTexture(parameter) is {IsError:true})return true;\r\n            if(material.FirstTexture is {IsError:true})return true;\r\n        }\r\n        return false;\r\n    }\r\n\r\n    public static void ApplyPreviewMaterialFallback(SceneModel model)\r\n    {\r\n        // Incomplete visual dependencies must not prevent skeletal retargeting.\r\n        // Override this preview instance only; authored assets remain editable.\r\n        if(HasMissingMaterials(model.Model))model.SetMaterialOverride(Material.Load(\u0022materials/dev/reflectivity_50.vmat\u0022));\r\n    }\r\n    public static string ModelPath(string path)\r\n    {\r\n        if(path.EndsWith(\u0022.vmdl_c\u0022,StringComparison.OrdinalIgnoreCase))path=path[..^2];\r\n        if(System.IO.Path.IsPathRooted(path)) path=System.IO.Path.GetRelativePath(Assets,path);\r\n        return VmdlSetupService.NormalizeAssetPath(path,\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;\r\n    internal static string ContentKey(string text)=\u003EConvert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(text))).ToLowerInvariant()[..16];\r\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];\r\n\r\n    private sealed class ModelSampler : IDisposable\r\n    {\r\n        readonly Scene metadataScene = Scene.CreateEditorScene(); readonly SkinnedModelRenderer metadata;\r\n        readonly SceneWorld world=new(); readonly Model asset; readonly Skel skeleton; readonly int[] bones;\r\n        public ModelSampler(Model asset, Skel rig) { using(metadataScene.Push()) { metadata=new GameObject(true,\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)\r\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();}\r\n        }\r\n        public Clip? SampleFire(float duration,float fps,CancellationToken cancel)\r\n        {\r\n            var model=new SceneModel(world,asset,Transform.Zero){UseAnimGraph=true};\r\n            try\r\n            {\r\n                if(model.AnimationGraph is null||model.AnimationGraph.IsError)return null;\r\n                model.SetAnimParameter(\u0022skeleton\u0022,0);model.SetAnimParameter(\u0022b_deploy_skip\u0022,true);\r\n                for(var i=0;i\u003C120;i\u002B\u002B){cancel.ThrowIfCancellationRequested();model.Update(1f/60);}\r\n                model.SetAnimParameter(\u0022b_deploy_skip\u0022,false);\r\n                var frames=new List\u003CXForm[]\u003E();\r\n                for(var f=0;f\u003C=Math.Max(1,(int)MathF.Ceiling(duration*fps));f\u002B\u002B)\r\n                {\r\n                    cancel.ThrowIfCancellationRequested();model.SetAnimParameter(\u0022b_attack\u0022,f==0);model.Update(f==0?0:1f/fps);\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 Clip(\u0022Fire\u0022,fps,false,frames);\r\n            }\r\n            finally{model.Delete();}\r\n        }\r\n        public void Dispose() {world.Delete();metadataScene.Destroy();}\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Core/Mapping/HandRigDefinition.cs","FileName":"HandRigDefinition.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":394704,"IsPrivate":false,"Code":"#nullable enable\r\nnamespace HumanoidHandRetargeter.Mapping;\r\n\r\npublic enum HandSide { Left, Right }\r\npublic enum DigitRole { Thumb, Index, Middle, Ring, Pinky, Extra }\r\n\r\n/// \u003Csummary\u003EOrdered anatomical joints; optional palm and terminal bones are explicit.\u003C/summary\u003E\r\npublic sealed class DigitChain\r\n{\r\n    public DigitRole Role { get; }\r\n    public string ExtraSlot { get; }\r\n    public IReadOnlyList\u003Cint\u003E Segments { get; }\r\n    public int? Metacarpal { get; }\r\n    public int? Tip { get; }\r\n\r\n    public DigitChain(DigitRole role, IEnumerable\u003Cint\u003E segments,\r\n        int? metacarpal = null, int? tip = null, string extraSlot = \u0022\u0022)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(segments);\r\n        Role = role;\r\n        Segments = Array.AsReadOnly(segments.ToArray());\r\n        Metacarpal = metacarpal;\r\n        Tip = tip;\r\n        ExtraSlot = extraSlot ?? \u0022\u0022;\r\n    }\r\n\r\n    public IEnumerable\u003Cint\u003E Bones\r\n    {\r\n        get\r\n        {\r\n            if (Metacarpal is int palm) yield return palm;\r\n            foreach (var segment in Segments) yield return segment;\r\n            if (Tip is int tip) yield return tip;\r\n        }\r\n    }\r\n}\r\n\r\n/// \u003Csummary\u003EA single hand; rigs with only one side do not need a placeholder other hand.\u003C/summary\u003E\r\npublic sealed class HandRigDefinition\r\n{\r\n    public HandSide Side { get; }\r\n    public int Wrist { get; }\r\n    public int? Clavicle { get; }\r\n    public int? UpperArm { get; }\r\n    public int? Forearm { get; }\r\n    public IReadOnlyList\u003CDigitChain\u003E Digits { get; }\r\n    public IReadOnlyList\u003Cint\u003E TwistOrHelperBones { get; }\r\n\r\n    public HandRigDefinition(HandSide side, int wrist, IEnumerable\u003CDigitChain\u003E digits,\r\n        int? clavicle = null, int? upperArm = null, int? forearm = null,\r\n        IEnumerable\u003Cint\u003E? twistOrHelperBones = null)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(digits);\r\n        Side = side;\r\n        Wrist = wrist;\r\n        Clavicle = clavicle;\r\n        UpperArm = upperArm;\r\n        Forearm = forearm;\r\n        Digits = Array.AsReadOnly(digits.ToArray());\r\n        TwistOrHelperBones = Array.AsReadOnly((twistOrHelperBones ?? Array.Empty\u003Cint\u003E()).ToArray());\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Core/Retargeting/HandMotionOptions.cs","FileName":"HandMotionOptions.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":394704,"IsPrivate":false,"Code":"#nullable enable\r\nnamespace HumanoidHandRetargeter.Retargeting;\r\n\r\npublic sealed class HandMotionOptions\r\n{\r\n    public bool TransferWristPosition { get; init; } = true;\r\n    public bool ScaleWristTravel { get; init; } = true;\r\n    /// \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;\r\n    public bool PreserveWeaponGrip { get; init; } = true;\r\n    /// \u003Csummary\u003EShared source weapon-to-target translation in centimeters. Grip motion is never scaled per hand.\u003C/summary\u003E\r\n    public System.Numerics.Vector3? WeaponSpaceOffset {get;init;}\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Core/Retargeting/HandRetargeter.cs","FileName":"HandRetargeter.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":394704,"IsPrivate":false,"Code":"#nullable enable\r\nusing System.Numerics;\r\nusing HumanoidHandRetargeter.Calibration;\r\nusing HumanoidHandRetargeter.Formats;\r\nusing HumanoidHandRetargeter.Maths;\r\nusing HumanoidHandRetargeter.Skeleton;\r\nusing HumanoidHandRetargeter.Validation;\r\n\r\nnamespace HumanoidHandRetargeter.Retargeting;\r\nusing Vector3 = System.Numerics.Vector3;\r\n\r\n/// \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\r\n{\r\n    /// \u003Csummary\u003EOne continuous graph stream, retaining digit angle history between evaluated frames.\u003C/summary\u003E\r\n    public sealed class PoseStream\r\n    {\r\n        private readonly HandRetargetProfile profile;\r\n        private readonly HandMotionOptions options;\r\n        private readonly AngleHistory history;\r\n        public PoseStream(HandRetargetProfile profile, HandMotionOptions options)\r\n        { this.profile = profile; this.options = options; history = new(profile); }\r\n        public Pose Step(Pose source)\r\n        {\r\n            var pose = Solve(profile, source, history);\r\n            if (options.TransferWristPosition) ApplyWristMotion(profile, source, pose, options);\r\n            var sourceWorld = source.ToWorld(profile.Source);\r\n            foreach (var pair in profile.PreservedTracks)\r\n            {\r\n                var targetWorld = pose.ToWorld(profile.Target);\r\n                var parent = profile.Target[pair.Target].ParentIndex;\r\n                pose.Locals[pair.Target] = parent \u003C 0 ? sourceWorld[pair.Source] : XForm.ToLocal(targetWorld[parent], sourceWorld[pair.Source]);\r\n            }\r\n            return pose;\r\n        }\r\n    }\r\n\r\n    public static Pose RetargetPose(HandRetargetProfile profile, Pose sourcePose)\r\n        =\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)\r\n    {\r\n        if(options.WeaponSpaceOffset.HasValue\u0026\u0026options.SolveArmIk)\r\n            foreach(var shoulder in profile.FpsShoulders)target.Locals[shoulder.Key].Pos=shoulder.Value;\r\n        var sourceWorld = source.ToWorld(profile.Source);\r\n        foreach (var pair in profile.WristMotion)\r\n        {\r\n            var world = target.ToWorld(profile.Target);\r\n            var travel = Vector3.Transform(sourceWorld[pair.Source].Pos - profile.Source.RestWorld[pair.Source].Pos, options.WristTravelBasis ?? pair.Basis);\r\n            var desired = profile.Target.RestWorld[pair.Target].Pos \u002B travel * (options.ScaleWristTravel ? pair.Scale : 1f);\r\n            var wristRotation = world[pair.Target].Rot;\r\n            if(options.WeaponSpaceOffset is {} weaponOffset)\r\n            {\r\n                wristRotation=MathQ.Normalize(sourceWorld[pair.Source].Rot*pair.GripRotationOffset);\r\n                desired=sourceWorld[pair.Source].Pos\u002BweaponOffset\r\n                    \u002BVector3.Transform(pair.SourceGripLocal,sourceWorld[pair.Source].Rot)\r\n                    -Vector3.Transform(pair.TargetGripLocal,wristRotation);\r\n            }\r\n            if (options.SolveArmIk \u0026\u0026 pair.UpperArm is int upper \u0026\u0026 pair.Forearm is int lower)\r\n            {\r\n                if(options.WeaponSpaceOffset.HasValue)\r\n                {\r\n                    // Detached FPS arms can move their open shoulder ends. Avoid\r\n                    // folding long/thick forearms back through their own upper arm\r\n                    // when the weapon grip lies very close to the shoulder.\r\n                    if(profile.Target[upper].ParentIndex\u003C0)\r\n                    {\r\n                        var shoulder=HandViewSpace.ClearElbowFold(world[upper].Pos,world[lower].Pos,world[pair.Target].Pos,desired);\r\n                        target.Locals[upper].Pos=shoulder;\r\n                        world=target.ToWorld(profile.Target);\r\n                    }\r\n                    // FPS arm meshes have free shoulder ends. Move the shoulder enough\r\n                    // to reach the fixed weapon grip instead of stretching either arm segment.\r\n                    var reach=Vector3.Distance(world[upper].Pos,world[lower].Pos)\u002BVector3.Distance(world[lower].Pos,world[pair.Target].Pos);\r\n                    var toGrip=desired-world[upper].Pos;var distance=toGrip.Length();\r\n                    if(distance\u003Ereach*.98f\u0026\u0026distance\u003E1e-5f)\r\n                    {\r\n                        var shoulder=world[upper].Pos\u002BtoGrip/distance*(distance-reach*.98f);\r\n                        var parent=profile.Target[upper].ParentIndex;\r\n                        target.Locals[upper].Pos=parent\u003C0?shoulder:Vector3.Transform(shoulder-world[parent].Pos,Quaternion.Conjugate(world[parent].Rot));\r\n                        world=target.ToWorld(profile.Target);\r\n                    }\r\n                }\r\n                var correction = TwoBoneIk.Solve(world[upper].Pos, world[lower].Pos, world[pair.Target].Pos, desired, 0, pair.BendAxis);\r\n                void SetWorldRotation(int bone, Quaternion rotation)\r\n                {\r\n                    var parent = profile.Target[bone].ParentIndex;\r\n                    target.Locals[bone].Rot = MathQ.Normalize(parent \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);\r\n                SetWorldRotation(lower, correction.LowerWorldDelta * oldLower);\r\n                if(options.WeaponSpaceOffset.HasValue \u0026\u0026 pair.SourceForearm is int sourceElbow \u0026\u0026 pair.SourceUpperArm is int sourceShoulder)\r\n                {\r\n                    // Transfer the source bend plane between the two shoulder-to-wrist axes.\r\n                    // An absolute source elbow position can lie on the target arm\u0027s axis\r\n                    // when proportions differ, flipping its pole as the weapon recoils.\r\n                    var axis=world[pair.Target].Pos-world[upper].Pos;\r\n                    var sourceAxis=sourceWorld[pair.Source].Pos-sourceWorld[sourceShoulder].Pos;\r\n                    if(axis.LengthSquared()\u003E1e-8f\u0026\u0026sourceAxis.LengthSquared()\u003E1e-8f)\r\n                    {\r\n                        axis=Vector3.Normalize(axis);\r\n                        sourceAxis=Vector3.Normalize(sourceAxis);\r\n                        var actual=world[lower].Pos-world[upper].Pos;\r\n                        var wanted=sourceWorld[sourceElbow].Pos-sourceWorld[sourceShoulder].Pos;\r\n                        wanted-=sourceAxis*Vector3.Dot(wanted,sourceAxis);\r\n                        wanted=Vector3.Transform(wanted,MathQ.FromTo(sourceAxis,axis));\r\n                        actual-=axis*Vector3.Dot(actual,axis);\r\n                        // A straight arm has no defined pole: retain the preceding IK plane.\r\n                        if(actual.LengthSquared()\u003E1e-6f\u0026\u0026wanted.LengthSquared()\u003E1e-6f)\r\n                        {\r\n                            actual=Vector3.Normalize(actual);wanted=Vector3.Normalize(wanted);\r\n                            var angle=MathF.Atan2(Vector3.Dot(axis,Vector3.Cross(actual,wanted)),Vector3.Dot(actual,wanted));\r\n                            var pole=Quaternion.CreateFromAxisAngle(axis,angle);\r\n                            oldLower=world[lower].Rot;\r\n                            SetWorldRotation(upper,pole*world[upper].Rot);\r\n                            SetWorldRotation(lower,pole*oldLower);\r\n                        }\r\n                    }\r\n                }\r\n                if(options.WeaponSpaceOffset.HasValue\u0026\u0026!pair.HasTwistHelpers)\r\n                {\r\n                    // Without authored twist helpers, put axial pronation in the forearm\r\n                    // instead of winding the wrist through the fixed palm orientation.\r\n                    // Rotating about elbow-to-wrist leaves the grip position unchanged.\r\n                    var axis=world[pair.Target].Pos-world[lower].Pos;\r\n                    if(axis.LengthSquared()\u003E1e-8f)\r\n                    {\r\n                        var restRelative=MathQ.Normalize(Quaternion.Conjugate(profile.Target.RestWorld[lower].Rot)*profile.Target.RestWorld[pair.Target].Rot);\r\n                        var relaxedLower=MathQ.Normalize(wristRotation*Quaternion.Conjugate(restRelative));\r\n                        var delta=MathQ.Normalize(relaxedLower*Quaternion.Conjugate(world[lower].Rot));\r\n                        MathQ.SwingTwist(delta,Vector3.Normalize(axis),out _,out var roll);\r\n                        SetWorldRotation(lower,roll*world[lower].Rot);\r\n                    }\r\n                }\r\n                SetWorldRotation(pair.Target, wristRotation);\r\n                if(options.WeaponSpaceOffset.HasValue\u0026\u0026Vector3.Distance(world[pair.Target].Pos,desired)\u003E.1f)\r\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);\r\n            }\r\n            else\r\n            {\r\n                // Partial rigs cannot reach with a two-link chain. Preserve the authored\r\n                // wrist path by translating the wrist relative to its actual parent.\r\n                var parent = profile.Target[pair.Target].ParentIndex;\r\n                target.Locals[pair.Target].Pos = parent \u003C 0 ? desired\r\n                    : Vector3.Transform(desired - world[parent].Pos, Quaternion.Conjugate(world[parent].Rot));\r\n                target.Locals[pair.Target].Rot=MathQ.Normalize(parent\u003C0?wristRotation:Quaternion.Conjugate(world[parent].Rot)*wristRotation);\r\n            }\r\n        }\r\n        var errors = PoseValidator.Validate(profile.Target, target);\r\n        if (errors.Count \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":394704,"IsPrivate":false,"Code":"// Adapted from humanoid-retargeter 26084c96c3fc870aaf9a5bd798de063ce2fd62df; cached hierarchy queries added.\r\n#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing HumanoidHandRetargeter.Maths;\r\n\r\nnamespace HumanoidHandRetargeter.Skeleton;\r\n\r\n/// \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;\r\n    private readonly IReadOnlyList\u003Cint\u003E[] _children;\r\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;\r\n        var children = Enumerable.Range(0, bones.Length).Select(_ =\u003E new List\u003Cint\u003E()).ToArray();\r\n        foreach (var bone in bones)\r\n            if (bone.ParentIndex \u003E= 0) children[bone.ParentIndex].Add(bone.Index);\r\n        _children = children.Select(c =\u003E (IReadOnlyList\u003Cint\u003E)c.AsReadOnly()).ToArray();\r\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;\r\n\r\n    /// \u003Csummary\u003ECached immediate children in stable skeleton order.\u003C/summary\u003E\r\n    public IReadOnlyList\u003Cint\u003E ChildrenOf(int bone) =\u003E _children[bone];\r\n\r\n    /// \u003Csummary\u003EWhether child is strictly below ancestor; a bone is not its own descendant.\u003C/summary\u003E\r\n    public bool DescendsFrom(int child, int ancestor)\r\n    {\r\n        if (child \u003C 0 || child \u003E= Count) throw new ArgumentOutOfRangeException(nameof(child));\r\n        if (ancestor \u003C 0 || ancestor \u003E= Count) throw new ArgumentOutOfRangeException(nameof(ancestor));\r\n        for (var parent = _bones[child].ParentIndex; parent \u003E= 0; parent = _bones[parent].ParentIndex)\r\n            if (parent == ancestor) return true;\r\n        return false;\r\n    }\r\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":394704,"IsPrivate":false,"Code":"// Reused from humanoid-retargeter 26084c96c3fc870aaf9a5bd798de063ce2fd62df; specialized where needed for hand assets.\r\n#nullable enable annotations\r\n\r\nnamespace HumanoidHandRetargeter.Formats.Fbx;\r\n\r\n/// \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),\r\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();\r\n    public bool HasChildScope { get; set; }\r\n    // Document metadata retained when repairing binary FBX files.\r\n    public byte[]? FooterWatermark { get; set; }\r\n\r\n    public FbxNode(string name) =\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":394704,"IsPrivate":false,"Code":"#nullable enable\r\nusing Sandbox;\r\nusing HumanoidHandRetargeter.Target;\r\n\r\nnamespace HumanoidHandRetargeter;\r\n\r\n/// \u003Csummary\u003EOptional local player controls for exported weapons. Games may instead drive RetargetedWeapon directly.\u003C/summary\u003E\r\n[Title(\u0022Retargeted Weapon Controls\u0022), Category(\u0022Animation\u0022)]\r\npublic sealed class RetargetedWeaponController : Component, ICameraModifier\r\n{\r\n    [Property] public PlayerController? Player { get; set; }\r\n    [Property] public bool ReadInput { get; set; } = true;\r\n    [Property] public int MagazineSize { get; set; } = 30;\r\n    [Property] public int StartingReserve { get; set; } = 120;\r\n    [Property] public float RoundsPerMinute { get; set; } = 800;\r\n    [Property] public float ReloadSeconds { get; set; } = 2.7f;\r\n    [Property] public float EmptyReloadSeconds { get; set; } = 3.2f;\r\n    [Property] public float Damage { get; set; } = 20;\r\n    [Property] public float Range { get; set; } = 10000;\r\n    [Property] public float HipFov { get; set; } = 75;\r\n    [Property] public float AimFov { get; set; } = 55;\r\n    [Property] public string WorldGripBone { get; set; } = \u0022hand_R\u0022;\r\n    [Property] public int BodyHoldType { get; set; } = 2;\r\n    [Property] public int BodyHandedness { get; set; }\r\n    public int Ammo =\u003E magazine?.Rounds ?? MagazineSize;\r\n    public int Reserve =\u003E magazine?.Reserve ?? StartingReserve;\r\n    public bool Reloading =\u003E magazine?.Reloading ?? false;\r\n    public bool Aiming { get; private set; }\r\n    public int ShotsFired { get; private set; }\r\n    public int Hits { get; private set; }\r\n    public global::Vector3 LastHit { get; private set; }\r\n    public Action\u003CSceneTraceResult\u003E? Shot { get; set; }\r\n    private WeaponMagazine? magazine;\r\n    private RetargetedWeapon? weapon;\r\n    private float nextShot, reloadEnds, currentFov;\r\n    public int CameraOrder =\u003E 200;\r\n    protected override void OnStart()\r\n    {\r\n        weapon=GetComponent\u003CRetargetedWeapon\u003E();\r\n        Player ??= Scene.GetAllComponents\u003CPlayerController\u003E().FirstOrDefault();\r\n        if(Player is not null)Player.HideBodyInFirstPerson=true;\r\n        magazine=new(MagazineSize,StartingReserve);\r\n        currentFov=HipFov;\r\n        weapon?.Deploy(true);\r\n    }\r\n    public bool Fire()\r\n    {\r\n        if(weapon is null||magazine is null||Player is null||Time.Now\u003CnextShot||Reloading)return false;\r\n        nextShot=Time.Now\u002B60/Math.Max(1,RoundsPerMinute);\r\n        if(!magazine.TryFire()){weapon.DryFire();return false;}\r\n        weapon.Attack();weapon.SetEmpty(Ammo==0);ShotsFired\u002B\u002B;\r\n        UpdateBodyPose();\r\n        Player.Renderer?.Set(\u0022b_attack\u0022,true);\r\n        var eye=Player.EyeTransform;\r\n        var hit=Scene.Trace.Ray(eye.Position,eye.Position\u002Beye.Rotation.Forward*Range)\r\n            .IgnoreGameObjectHierarchy(Player.GameObject).IgnoreGameObjectHierarchy(GameObject).UseHitboxes().Run();\r\n        if(hit.Hit)\r\n        {\r\n            Hits\u002B\u002B;LastHit=hit.HitPosition;\r\n            var info=new DamageInfo{Damage=Damage,Attacker=Player.GameObject,Weapon=GameObject,Position=hit.HitPosition,Origin=eye.Position};\r\n            for(var obj=hit.GameObject;obj is not null;obj=obj.Parent)\r\n                if(obj.GetComponent\u003CIDamageable\u003E() is {} damageable){damageable.OnDamage(info);break;}\r\n        }\r\n        Shot?.Invoke(hit);\r\n        return true;\r\n    }\r\n    public bool Reload()\r\n    {\r\n        if(magazine is null||weapon is null||!magazine.BeginReload())return false;\r\n        weapon.Reload(Ammo==0);reloadEnds=Time.Now\u002B(Ammo==0?EmptyReloadSeconds:ReloadSeconds);\r\n        UpdateBodyPose();\r\n        Player?.Renderer?.Set(\u0022b_reload\u0022,true);\r\n        return true;\r\n    }\r\n    public void Aim(bool value){Aiming=value\u0026\u0026!Reloading;if(weapon is not null)weapon.Aiming=Aiming;}\r\n    protected override void OnUpdate()\r\n    {\r\n        if(weapon is null||Player is null||magazine is null)return;\r\n        if(Reloading\u0026\u0026Time.Now\u003E=reloadEnds){magazine.FinishReload();weapon.SetEmpty(Ammo==0);}\r\n        if(ReadInput)Aim(Input.Down(\u0022Attack2\u0022));\r\n        if(Reloading)Aim(false);\r\n        weapon.Aiming=Aiming;\r\n        weapon.Sprinting=ReadInput\u0026\u0026Input.Down(\u0022Run\u0022)\u0026\u0026Player.Velocity.Length\u003E10\u0026\u0026!Aiming;\r\n        weapon.Movement=Math.Clamp(Player.Velocity.Length/150f,0,1);\r\n        weapon.SetParameter(\u0022attack_hold\u0022,ReadInput\u0026\u0026Input.Down(\u0022Attack1\u0022)\u0026\u0026!Reloading\u0026\u0026Ammo\u003E0?1f:0f);\r\n        weapon.SetParameter(\u0022firing_mode\u0022,3);\r\n        weapon.SetParameter(\u0022ironsights_fire_scale\u0022,.3f);\r\n        if(ReadInput)\r\n        {\r\n            if(Input.Pressed(\u0022Reload\u0022))Reload();\r\n            if(Input.Down(\u0022Attack1\u0022)\u0026\u0026!weapon.Sprinting)Fire();\r\n        }\r\n        weapon.ShowHands=!Player.ThirdPerson;\r\n        if(Player.ThirdPerson)weapon.WorldTransform=Player.EyeTransform;\r\n        // Keep the body graph warm even when hidden, so a camera switch during a shot\r\n        // or reload reveals the same action rather than restarting a holding pose.\r\n        UpdateBodyPose();\r\n        weapon.ThirdPersonBody=Player.ThirdPerson?Player.Renderer:null;\r\n        weapon.WorldGripBone=WorldGripBone;\r\n    }\r\n    private void UpdateBodyPose()\r\n    {\r\n        if(Player?.Renderer is not {} body)return;\r\n        body.Set(\u0022holdtype\u0022,BodyHoldType);body.Set(\u0022holdtype_handedness\u0022,BodyHandedness);\r\n        body.Set(\u0022aim_body_weight\u0022,1f);\r\n    }\r\n    public void ModifyCamera(CameraComponent camera,ref CameraView view)\r\n    {\r\n        if(Player is not null\u0026\u0026!Player.ThirdPerson\u0026\u0026camera==Scene.Camera)\r\n        {\r\n            currentFov=MathX.Lerp(currentFov,Aiming?AimFov:HipFov,Math.Clamp(Time.Delta*12,0,1));\r\n            view.FieldOfView=currentFov;\r\n        }\r\n    }\r\n    public void PostCameraSetup(CameraComponent camera,in CameraView view)\r\n    {\r\n        // EyeTransform during Update can precede the player\u0027s final camera pose.\r\n        // Place both viewmodel meshes against the composed view before PreRender.\r\n        if(weapon is not null\u0026\u0026Player is not null\u0026\u0026!Player.ThirdPerson\u0026\u0026camera==Scene.Camera)\r\n            weapon.WorldTransform=new global::Transform(view.Position,view.Rotation);\r\n    }\r\n    protected override void OnDisabled()\r\n    {\r\n        magazine?.CancelReload();Aiming=false;\r\n        if(weapon is not null){weapon.Aiming=false;weapon.ThirdPersonBody=null;}\r\n        Player?.Renderer?.Set(\u0022holdtype\u0022,0);\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Code/Core/Retargeting/HandMotionOptions.cs","FileName":"HandMotionOptions.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":394704,"IsPrivate":false,"Code":"#nullable enable\r\nnamespace HumanoidHandRetargeter.Retargeting;\r\n\r\npublic sealed class HandMotionOptions\r\n{\r\n    public bool TransferWristPosition { get; init; } = true;\r\n    public bool ScaleWristTravel { get; init; } = true;\r\n    /// \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;\r\n    public bool PreserveWeaponGrip { get; init; } = true;\r\n    /// \u003Csummary\u003EShared source weapon-to-target translation in centimeters. Grip motion is never scaled per hand.\u003C/summary\u003E\r\n    public System.Numerics.Vector3? WeaponSpaceOffset {get;init;}\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Code/Core/Validation/HandRigValidator.cs","FileName":"HandRigValidator.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":394704,"IsPrivate":false,"Code":"#nullable enable\r\nusing HumanoidHandRetargeter.Mapping;\r\nusing SkeletonModel = HumanoidHandRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidHandRetargeter.Validation;\r\n\r\npublic sealed record RigIssue(string Code, string Message, HandSide? Side = null, int? Bone = null);\r\n\r\n/// \u003Csummary\u003EShared validation for automatic/manual mapping, batch processing and the editor.\u003C/summary\u003E\r\npublic static class HandRigValidator\r\n{\r\n    public static IReadOnlyList\u003CRigIssue\u003E Validate(SkeletonModel skeleton, IEnumerable\u003CHandRigDefinition\u003E hands)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(skeleton);\r\n        ArgumentNullException.ThrowIfNull(hands);\r\n        var issues = new List\u003CRigIssue\u003E();\r\n        var claimed = new HashSet\u003Cint\u003E();\r\n        var sides = new HashSet\u003CHandSide\u003E();\r\n        var count = 0;\r\n\r\n        foreach (var bone in skeleton.Bones)\r\n        {\r\n            if (!PoseValidator.ValidTransform(bone.RestLocal))\r\n                issues.Add(new(\u0022invalid-rest\u0022, $\u0022Bone \u0027{bone.Name}\u0027 has an invalid rest transform.\u0022, Bone: bone.Index));\r\n            var world = skeleton.RestWorld[bone.Index].Pos;\r\n            if (!float.IsFinite(world.X) || !float.IsFinite(world.Y) || !float.IsFinite(world.Z))\r\n                issues.Add(new(\u0022invalid-world-rest\u0022, $\u0022Bone \u0027{bone.Name}\u0027 has an invalid accumulated rest position.\u0022, Bone: bone.Index));\r\n        }\r\n\r\n        foreach (var hand in hands)\r\n        {\r\n            count\u002B\u002B;\r\n            if (hand is null)\r\n            {\r\n                issues.Add(new(\u0022missing-hand\u0022, \u0022A hand mapping is missing.\u0022));\r\n                continue;\r\n            }\r\n            if (!Enum.IsDefined(typeof(HandSide), hand.Side))\r\n                issues.Add(new(\u0022invalid-side\u0022, \u0022Choose left or right for the hand.\u0022));\r\n            if (!sides.Add(hand.Side))\r\n                issues.Add(new(\u0022duplicate-side\u0022, $\u0022More than one {hand.Side} hand is mapped.\u0022, hand.Side));\r\n\r\n            var arm = new[] { hand.Clavicle, hand.UpperArm, hand.Forearm, hand.Wrist }\r\n                .Where(b =\u003E b.HasValue).Select(b =\u003E b!.Value).ToArray();\r\n            CheckChain(arm, hand.Side, \u0022arm\u0022, allowZeroLength: true);\r\n            var roles = new HashSet\u003Cstring\u003E(StringComparer.Ordinal);\r\n            foreach (var digit in hand.Digits)\r\n            {\r\n                if (digit is null)\r\n                {\r\n                    issues.Add(new(\u0022missing-digit\u0022, \u0022A digit mapping is missing.\u0022, hand.Side));\r\n                    continue;\r\n                }\r\n                var key = digit.Role == DigitRole.Extra ? \u0022Extra:\u0022 \u002B digit.ExtraSlot : digit.Role.ToString();\r\n                if (!Enum.IsDefined(typeof(DigitRole), digit.Role)\r\n                    || (digit.Role == DigitRole.Extra \u0026\u0026 string.IsNullOrWhiteSpace(digit.ExtraSlot)))\r\n                    issues.Add(new(\u0022invalid-digit-role\u0022, \u0022Choose a digit role or name its extra slot.\u0022, hand.Side));\r\n                if (!roles.Add(key))\r\n                    issues.Add(new(\u0022duplicate-digit-role\u0022, $\u0022{hand.Side} {key} is mapped more than once.\u0022, hand.Side));\r\n                if (digit.Segments.Count == 0)\r\n                    issues.Add(new(\u0022empty-digit\u0022, $\u0022{hand.Side} {key} needs at least one segment.\u0022, hand.Side));\r\n                var chain = digit.Bones.ToArray();\r\n                CheckChain(chain, hand.Side, key, allowZeroLength: false, tip: digit.Tip);\r\n                if (chain.Length \u003E 0 \u0026\u0026 Valid(chain[0]) \u0026\u0026 Valid(hand.Wrist)\r\n                    \u0026\u0026 !skeleton.DescendsFrom(chain[0], hand.Wrist))\r\n                    issues.Add(new(\u0022wrong-wrist\u0022, $\u0022{hand.Side} {key} must descend from its wrist.\u0022, hand.Side, chain[0]));\r\n            }\r\n            foreach (var helper in hand.TwistOrHelperBones)\r\n            {\r\n                Claim(helper, hand.Side);\r\n                var armRoot = arm[0];\r\n                if (Valid(helper) \u0026\u0026 Valid(armRoot) \u0026\u0026 !skeleton.DescendsFrom(helper, armRoot))\r\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));\r\n            }\r\n        }\r\n        if (count == 0)\r\n            issues.Add(new(\u0022no-hands\u0022, \u0022Map at least one wrist before retargeting.\u0022));\r\n        return issues.AsReadOnly();\r\n\r\n        bool Valid(int bone) =\u003E bone \u003E= 0 \u0026\u0026 bone \u003C skeleton.Count;\r\n        void Claim(int bone, HandSide side)\r\n        {\r\n            if (!Valid(bone))\r\n                issues.Add(new(\u0022invalid-bone\u0022, $\u0022{side} mapping references a missing bone.\u0022, side, bone));\r\n            else if (!claimed.Add(bone))\r\n                issues.Add(new(\u0022overlapping-bone\u0022, $\u0022Bone \u0027{skeleton[bone].Name}\u0027 is assigned to multiple roles.\u0022, side, bone));\r\n        }\r\n        void CheckChain(int[] bones, HandSide side, string role, bool allowZeroLength, int? tip = null)\r\n        {\r\n            for (var i = 0; i \u003C bones.Length; i\u002B\u002B)\r\n            {\r\n                Claim(bones[i], side);\r\n                if (i == 0 || !Valid(bones[i - 1]) || !Valid(bones[i])) continue;\r\n                if (!skeleton.DescendsFrom(bones[i], bones[i - 1]))\r\n                    issues.Add(new(\u0022chain-order\u0022, $\u0022{side} {role} joints must follow their hierarchy.\u0022, side, bones[i]));\r\n                if (!allowZeroLength \u0026\u0026 bones[i] != tip \u0026\u0026\r\n                    System.Numerics.Vector3.DistanceSquared(skeleton.RestWorld[bones[i - 1]].Pos,\r\n                        skeleton.RestWorld[bones[i]].Pos) \u003C 1e-12f)\r\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]));\r\n            }\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Code/Core/Mapping/HandRigDetector.cs","FileName":"HandRigDetector.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":394704,"IsPrivate":false,"Code":"#nullable enable\r\nusing HumanoidHandRetargeter.Validation;\r\nusing SkeletonModel = HumanoidHandRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidHandRetargeter.Mapping;\r\n\r\n/// \u003Csummary\u003E\r\n/// Name candidates constrained by wrist ancestry. Reuses legacy name tokenization;\r\n/// deliberately replaces its full-body confidence and fixed phalanx slots.\r\n/// Unnamed/ambiguous anatomy is proposed for review, never silently accepted.\r\n/// \u003C/summary\u003E\r\npublic static class HandRigDetector\r\n{\r\n    public static HandMappingResult Detect(SkeletonModel skeleton,\r\n        IEnumerable\u003CHandRigDefinition\u003E? manualOverrides = null)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(skeleton);\r\n        var hints = HandDetectionProfiles.Resolve(skeleton);\r\n        var names = skeleton.Bones.Select(b =\u003E new NameInfo(b.Name,hints[b.Index])).ToArray();\r\n        var hands = new List\u003CHandRigDefinition\u003E();\r\n        var candidates = new List\u003CHandMappingCandidate\u003E();\r\n        var issues = new List\u003CRigIssue\u003E();\r\n        var review = false;\r\n        var deformBelow = new bool?[skeleton.Count];\r\n        int? digitWrist = null;\r\n        var overrides = (manualOverrides ?? Array.Empty\u003CHandRigDefinition\u003E()).ToArray();\r\n        if (overrides.Any(h =\u003E h is null))\r\n            throw new ArgumentException(\u0022Manual overrides cannot contain a null hand.\u0022, nameof(manualOverrides));\r\n\r\n        foreach (var side in new[] { HandSide.Left, HandSide.Right })\r\n        {\r\n            var manual = overrides.Where(h =\u003E h.Side == side).ToArray();\r\n            if (manual.Length \u003E 0)\r\n            {\r\n                hands.AddRange(manual);\r\n                foreach (var hand in manual)\r\n                {\r\n                    Add(side, \u0022Wrist\u0022, new[] { hand.Wrist }, 1f, \u0022Manual mapping\u0022, true);\r\n                    foreach (var (role, bone) in new[] { (\u0022Clavicle\u0022, hand.Clavicle), (\u0022UpperArm\u0022, hand.UpperArm), (\u0022Forearm\u0022, hand.Forearm) })\r\n                        if (bone is int b) Add(side, role, new[] { b }, 1f, \u0022Manual mapping\u0022, true);\r\n                    foreach (var digit in hand.Digits.Where(d =\u003E d is not null))\r\n                        Add(side, digit.Role.ToString(), digit.Bones, 1f, \u0022Manual mapping\u0022, true);\r\n                    foreach (var helper in hand.TwistOrHelperBones)\r\n                        Add(side, \u0022Helper\u0022, new[] { helper }, 1f, \u0022Manual mapping\u0022, true);\r\n                }\r\n                continue;\r\n            }\r\n            var wrists = skeleton.Bones.Where(b =\u003E names[b.Index].IsWrist \u0026\u0026 !Blocked(b.Index)\r\n                \u0026\u0026 SideOf(b.Index) == side).Select(b =\u003E b.Index).ToArray();\r\n            var deformWrists=wrists.Where(w=\u003Enames[w].IsDeform).ToArray();\r\n            if(deformWrists.Length==1 \u0026\u0026 wrists.All(w=\u003Enames[w].FamilyKey==names[deformWrists[0]].FamilyKey))\r\n                wrists=deformWrists; // Rigify exports can contain DEF, ORG and animator-control copies.\r\n            if (wrists.Length == 0) continue; // A one-sided rig is valid.\r\n            if (wrists.Length \u003E 1)\r\n            {\r\n                foreach (var wrist in wrists)\r\n                    Add(side, \u0022Wrist\u0022, new[] { wrist }, .5f, \u0022Multiple wrist candidates on this side; choose one manually\u0022, false);\r\n                issues.Add(new(\u0022ambiguous-wrist\u0022, $\u0022Choose the {side} wrist in Hand Mapping.\u0022, side));\r\n                continue;\r\n            }\r\n            var selected = wrists[0];\r\n            digitWrist = selected;\r\n            Add(side, \u0022Wrist\u0022, new[] { selected }, .95f, names[selected].Profile is {} wristProfile\r\n                ? $\u0022{wristProfile} profile and wrist hierarchy agree\u0022 : \u0022Wrist name and side agree\u0022, true);\r\n            var digits = new List\u003CDigitChain\u003E();\r\n            var seenRoles = new HashSet\u003CDigitRole\u003E();\r\n            foreach (var root in DigitRoots(selected))\r\n            {\r\n                var chain = new List\u003Cint\u003E();\r\n                int? meta = null, tip = null;\r\n                var current = root;\r\n                var chainReview = false;\r\n                while (true)\r\n                {\r\n                    var info = names[current];\r\n                    if (info.IsTip)\r\n                    {\r\n                        tip = current;\r\n                        if (skeleton.ChildrenOf(current).Count != 0) chainReview = true;\r\n                        break;\r\n                    }\r\n                    if (info.IsMeta \u0026\u0026 current == root) meta = current;\r\n                    else chain.Add(current);\r\n                    var next = JointChildren(current).ToArray();\r\n                    if (next.Length == 0) break;\r\n                    if (next.Length \u003E 1) { chainReview = true; break; }\r\n                    current = next[0];\r\n                }\r\n                var role = names[root].Digit;\r\n                // A palm/metacarpal may be generically named, but its child\u0027s digit is explicit.\r\n                if (role is null \u0026\u0026 meta.HasValue \u0026\u0026 chain.Count \u003E 0) role = names[chain[0]].Digit;\r\n                var chainSideMismatch = chain.Any(b =\u003E names[b].Side is HandSide explicitSide \u0026\u0026 explicitSide != side);\r\n                var chainRoleMismatch = role.HasValue \u0026\u0026 chain.Any(b =\u003E names[b].Digit is DigitRole namedRole \u0026\u0026 namedRole != role);\r\n                if (chainSideMismatch || chainRoleMismatch || chainReview || chain.Count == 0)\r\n                {\r\n                    Add(side, role?.ToString() ?? \u0022Digit\u0022, chain, .3f,\r\n                        \u0022Branched, empty or side-inconsistent digit chain; correct the mapping\u0022, false);\r\n                    review = true;\r\n                    continue;\r\n                }\r\n                if (role.HasValue \u0026\u0026 !seenRoles.Add(role.Value))\r\n                {\r\n                    Add(side, role.Value.ToString(), chain, .4f, \u0022Duplicate anatomical digit candidate; choose the intended chain\u0022, false);\r\n                    review = true;\r\n                    continue;\r\n                }\r\n                var digit = new DigitChain(role ?? DigitRole.Extra, chain, meta, tip,\r\n                    role is null ? skeleton[root].Name : \u0022\u0022);\r\n                digits.Add(digit);\r\n                var confidence = role.HasValue ? .9f : .4f;\r\n                Add(side, role?.ToString() ?? \u0022Extra:\u0022 \u002B skeleton[root].Name, digit.Bones, confidence,\r\n                    role.HasValue ? names[root].Profile is {} digitProfile ? $\u0022{digitProfile} digit convention follows this wrist\u0027s hierarchy\u0022\r\n                        : \u0022Named digit follows this wrist\u0027s hierarchy; segment numbering is not used for side\u0022\r\n                        : \u0022Unlabelled digit chain; assign an anatomical role or confirm an extra digit\u0022, true);\r\n                if (!role.HasValue) review = true;\r\n            }\r\n\r\n            var ancestors = new List\u003Cint\u003E();\r\n            for (var p = skeleton[selected].ParentIndex; p \u003E= 0; p = skeleton[p].ParentIndex)\r\n                ancestors.Add(p);\r\n            int? Find(params string[] aliases) =\u003E ancestors\r\n                .Where(b =\u003E !names[b].IsHelper \u0026\u0026 !Blocked(b) \u0026\u0026 aliases.Any(a =\u003E names[b].Core.EndsWith(a, StringComparison.Ordinal)))\r\n                .Select(b =\u003E (int?)b).FirstOrDefault();\r\n            int? ProfileJoint(string role)=\u003Eancestors.Where(b=\u003Enames[b].ProfileRole==role \u0026\u0026 !names[b].IsHelper \u0026\u0026 !Blocked(b))\r\n                .Select(b=\u003E(int?)b).FirstOrDefault();\r\n            var forearm = ProfileJoint(\u0022LowerArm\u0022) ?? Find(\u0022forearm\u0022, \u0022lowerarm\u0022, \u0022armlower\u0022, \u0022elbow\u0022);\r\n            var upperArm = ProfileJoint(\u0022UpperArm\u0022) ?? Find(\u0022upperarm\u0022, \u0022armupper\u0022, \u0022uparm\u0022);\r\n            var clavicle = Find(\u0022clavicle\u0022, \u0022collar\u0022) ?? ProfileJoint(\u0022Clavicle\u0022);\r\n            // Prototype calls upper arm Shoulder, Mixamo calls clavicle Shoulder.\r\n            if (upperArm is null \u0026\u0026 clavicle.HasValue) upperArm = Find(\u0022shoulder\u0022);\r\n            upperArm ??= ancestors.Where(b =\u003E names[b].Core.EndsWith(\u0022arm\u0022, StringComparison.Ordinal)\r\n                \u0026\u0026 b != forearm \u0026\u0026 !names[b].IsHelper \u0026\u0026 !Blocked(b)).Select(b =\u003E (int?)b).FirstOrDefault();\r\n            clavicle ??= Find(\u0022shoulder\u0022);\r\n            if (clavicle == upperArm) clavicle = null;\r\n            foreach (var (role, bone) in new[] { (\u0022Clavicle\u0022, clavicle), (\u0022UpperArm\u0022, upperArm), (\u0022Forearm\u0022, forearm) })\r\n                if (bone is int b)\r\n                {\r\n                    var mismatch = names[b].ConflictingSides || (SideOf(b) is HandSide namedSide \u0026\u0026 namedSide != side);\r\n                    Add(side, role, new[] { b }, mismatch ? .3f : .9f,\r\n                        mismatch ? \u0022Arm name conflicts with the wrist side; correct the mapping\u0022 : \u0022Named joint on the wrist ancestor chain\u0022, true);\r\n                    review |= mismatch;\r\n                }\r\n            var armRoot = clavicle ?? upperArm ?? forearm ?? selected;\r\n            var helpers = skeleton.Bones.Where(b =\u003E names[b.Index].IsHelper \u0026\u0026 !Blocked(b.Index)\r\n                \u0026\u0026 skeleton.DescendsFrom(b.Index, armRoot)).Select(b =\u003E b.Index).ToArray();\r\n            foreach (var helper in helpers)\r\n                Add(side, \u0022Helper\u0022, new[] { helper }, .9f, \u0022Explicit twist/helper name in this arm hierarchy\u0022, true);\r\n            hands.Add(new(side, selected, digits, clavicle, upperArm, forearm, helpers));\r\n        }\r\n\r\n        // Unsupported enum values must reach validation instead of being silently dropped.\r\n        hands.AddRange(overrides.Where(h =\u003E !Enum.IsDefined(typeof(HandSide), h.Side)));\r\n        foreach (var bone in skeleton.Bones.Where(b =\u003E names[b.Index].IsWrist \u0026\u0026 !Blocked(b.Index) \u0026\u0026 SideOf(b.Index) is null\r\n            \u0026\u0026 !hands.Any(h =\u003E h.Wrist == b.Index)))\r\n        {\r\n            Add(null, \u0022Wrist\u0022, new[] { bone.Index }, .4f, \u0022Wrist name found but side is unknown or conflicting\u0022, false);\r\n            review = true;\r\n        }\r\n        if (hands.Count == 0 \u0026\u0026 candidates.Count == 0)\r\n        {\r\n            // Topology hints only: without a known side/frame, geometry must not fabricate left/right.\r\n            foreach (var bone in skeleton.Bones.Where(b =\u003E !Blocked(b.Index) \u0026\u0026\r\n                skeleton.ChildrenOf(b.Index).Count(c =\u003E !Blocked(c) \u0026\u0026 skeleton.ChildrenOf(c).Count \u003E 0) \u003E= 2))\r\n                Add(null, \u0022Wrist\u0022, new[] { bone.Index }, .2f, \u0022Branching hierarchy may be a palm; confirm side and wrist manually\u0022, false);\r\n            review = true;\r\n        }\r\n        issues.AddRange(HandRigValidator.Validate(skeleton, hands));\r\n        return new(hands, candidates, issues, review);\r\n\r\n        void Add(HandSide? side, string role, IEnumerable\u003Cint\u003E bones, float confidence, string reason, bool selected)\r\n            =\u003E candidates.Add(new(side, role, Array.AsReadOnly(bones.ToArray()), confidence, reason, selected));\r\n\r\n        // The viewmodel may own the whole hierarchy: a weapon-named ancestor is not\r\n        // evidence that an explicitly named child wrist is a mechanical weapon bone.\r\n        bool Blocked(int bone) =\u003E names[bone].IsNonHandTrack;\r\n        HandSide? SideOf(int bone)\r\n        {\r\n            for (var b = bone; b \u003E= 0; b = skeleton[b].ParentIndex)\r\n            {\r\n                if (names[b].ConflictingSides) return null;\r\n                if (names[b].Side.HasValue) return names[b].Side;\r\n            }\r\n            return null;\r\n        }\r\n        IEnumerable\u003Cint\u003E DigitRoots(int parent)\r\n        {\r\n            foreach (var child in JointChildren(parent))\r\n            {\r\n                // Some palm/metacarpal containers parent several different digits\r\n                // (notably thumb and index). They are shared anatomy, not a fork\r\n                // within one finger. Keep the container wrist-driven and map each\r\n                // named child chain independently so no bone is assigned twice.\r\n                var branches=JointChildren(child).Where(i=\u003E!names[i].IsTip).ToArray();\r\n                if(names[child].IsMeta\u0026\u0026branches.Length\u003E1\r\n                    \u0026\u0026branches.All(i=\u003Enames[i].Digit.HasValue)\r\n                    \u0026\u0026branches.Select(i=\u003Enames[i].Digit).Distinct().Count()==branches.Length)\r\n                {\r\n                    foreach(var branch in branches)yield return branch;\r\n                    continue;\r\n                }\r\n                if (!names[child].IsTip) yield return child;\r\n            }\r\n        }\r\n        // Layered rigs (Rigify DEF/ORG/MCH, animator controls) can parent deform digits\r\n        // under a non-deform palm next to their control copies. Below a deform wrist,\r\n        // follow only the deform layer and the containers that lead to it.\r\n        bool LeadsToDeform(int bone)\r\n        {\r\n            if (deformBelow[bone] is bool known) return known;\r\n            var result = names[bone].IsDeform || skeleton.ChildrenOf(bone).Any(LeadsToDeform);\r\n            deformBelow[bone] = result;\r\n            return result;\r\n        }\r\n        IEnumerable\u003Cint\u003E JointChildren(int parent)\r\n        {\r\n            var layered = digitWrist is int wrist \u0026\u0026 names[wrist].IsDeform\r\n                \u0026\u0026 (parent == wrist || skeleton.DescendsFrom(parent, wrist))\r\n                \u0026\u0026 skeleton.ChildrenOf(parent).Any(c =\u003E !Blocked(c) \u0026\u0026 LeadsToDeform(c));\r\n            foreach (var child in skeleton.ChildrenOf(parent))\r\n            {\r\n                if (Blocked(child)) continue;\r\n                // The parent\u0027s own deform copy is a skinned twin, not a digit.\r\n                if (layered \u0026\u0026 (!LeadsToDeform(child)\r\n                    || names[child].IsDeform \u0026\u0026 !names[child].IsTip \u0026\u0026 names[child].FamilyKey == names[parent].FamilyKey)) continue;\r\n                if (names[child].IsHelper)\r\n                {\r\n                    foreach (var nested in JointChildren(child)) yield return nested;\r\n                }\r\n                else yield return child;\r\n            }\r\n        }\r\n    }\r\n\r\n    private sealed class NameInfo\r\n    {\r\n        public string Core { get; }\r\n        public HandSide? Side { get; }\r\n        public bool ConflictingSides { get; }\r\n        public DigitRole? Digit { get; }\r\n        public bool IsWrist { get; }\r\n        public bool IsHelper { get; }\r\n        public bool IsNonHandTrack { get; }\r\n        public bool IsMeta { get; }\r\n        public bool IsTip { get; }\r\n        public string? Profile { get; }\r\n        public string? ProfileRole { get; }\r\n        public bool IsDeform { get; }\r\n        public string FamilyKey { get; }\r\n\r\n        public NameInfo(string name,HandDetectionProfiles.Hint? hint)\r\n        {\r\n            var stripped = name[(name.LastIndexOf(\u0027:\u0027) \u002B 1)..];\r\n            var hash = stripped.LastIndexOf(\u0027#\u0027);\r\n            if (hash \u003E= 0 \u0026\u0026 stripped[(hash \u002B 1)..].All(char.IsDigit)) stripped = stripped[..hash];\r\n            IsDeform=stripped.StartsWith(\u0022DEF-\u0022,StringComparison.OrdinalIgnoreCase);\r\n            FamilyKey=HandDetectionProfiles.Key(IsDeform||stripped.StartsWith(\u0022ORG-\u0022,StringComparison.OrdinalIgnoreCase)\r\n                ||stripped.StartsWith(\u0022MCH-\u0022,StringComparison.OrdinalIgnoreCase)?stripped[4..]:stripped);\r\n            Profile=hint?.Profile;ProfileRole=hint?.Role;\r\n            var tokens = BoneNameTokens.Tokenize(stripped);\r\n            bool Has(params string[] words) =\u003E tokens.Any(t =\u003E words.Contains(t, StringComparer.Ordinal));\r\n            var left = Has(\u0022l\u0022, \u0022left\u0022, \u0022lft\u0022);\r\n            var right = Has(\u0022r\u0022, \u0022right\u0022, \u0022rgt\u0022);\r\n            // Case-only changes must not erase a wrist\u0027s side when camel-case boundaries disappear.\r\n            var compact = string.Concat(tokens.Where(t =\u003E !t.All(char.IsDigit)));\r\n            foreach (var joint in new[] { \u0022hand\u0022, \u0022wrist\u0022 })\r\n            {\r\n                left |= compact.EndsWith(\u0022left\u0022 \u002B joint, StringComparison.Ordinal) || compact.EndsWith(joint \u002B \u0022left\u0022, StringComparison.Ordinal)\r\n                    || compact.EndsWith(joint \u002B \u0022l\u0022, StringComparison.Ordinal) || compact == \u0022l\u0022 \u002B joint;\r\n                right |= compact.EndsWith(\u0022right\u0022 \u002B joint, StringComparison.Ordinal) || compact.EndsWith(joint \u002B \u0022right\u0022, StringComparison.Ordinal)\r\n                    || compact.EndsWith(joint \u002B \u0022r\u0022, StringComparison.Ordinal) || compact == \u0022r\u0022 \u002B joint;\r\n            }\r\n            ConflictingSides = left \u0026\u0026 right;\r\n            Side = left == right ? left ? null : hint?.Side : left ? HandSide.Left : HandSide.Right;\r\n            Core = string.Concat(tokens.Where(t =\u003E !new[] { \u0022l\u0022, \u0022left\u0022, \u0022lft\u0022, \u0022r\u0022, \u0022right\u0022, \u0022rgt\u0022, \u0022def\u0022, \u0022org\u0022, \u0022mch\u0022, \u0022stretch\u0022 }.Contains(t)\r\n                \u0026\u0026 !t.All(char.IsDigit)));\r\n            IsHelper = Has(\u0022twist\u0022, \u0022twistctrl\u0022, \u0022roll\u0022, \u0022share\u0022, \u0022sharebone\u0022, \u0022helper\u0022, \u0022hlp\u0022, \u0022corrective\u0022, \u0022control\u0022, \u0022ctrl\u0022);\r\n            IsNonHandTrack = Has(\u0022weapon\u0022, \u0022camera\u0022, \u0022ik\u0022, \u0022ikrule\u0022, \u0022hold\u0022, \u0022socket\u0022, \u0022attachment\u0022, \u0022magazine\u0022, \u0022bolt\u0022, \u0022slide\u0022, \u0022trigger\u0022)\r\n                || compact.StartsWith(\u0022weapon\u0022, StringComparison.Ordinal) || compact.StartsWith(\u0022camera\u0022, StringComparison.Ordinal);\r\n            IsMeta = Has(\u0022meta\u0022, \u0022metacarpal\u0022, \u0022carpal\u0022, \u0022palm\u0022) || hint?.Role.EndsWith(\u0022Meta\u0022,StringComparison.Ordinal)==true;\r\n            IsTip = Has(\u0022tip\u0022, \u0022end\u0022, \u0022nub\u0022, \u0022endmarker\u0022) || hint?.Role.EndsWith(\u0022Tip\u0022,StringComparison.Ordinal)==true;\r\n            bool DigitName(params string[] aliases) =\u003E Has(aliases)\r\n                || aliases.Any(a =\u003E Core.EndsWith(a, StringComparison.Ordinal) || Core.EndsWith(a \u002B \u0022finger\u0022, StringComparison.Ordinal));\r\n            Digit = DigitName(\u0022thumb\u0022) ? DigitRole.Thumb : DigitName(\u0022index\u0022, \u0022forefinger\u0022, \u0022point\u0022, \u0022pointer\u0022, \u0022pointing\u0022) ? DigitRole.Index\r\n                : DigitName(\u0022middle\u0022, \u0022mid\u0022) ? DigitRole.Middle : DigitName(\u0022ring\u0022) ? DigitRole.Ring\r\n                : DigitName(\u0022pinky\u0022, \u0022pinkie\u0022, \u0022pink\u0022, \u0022little\u0022) ? DigitRole.Pinky : null;\r\n            if(hint is not null \u0026\u0026 HandDetectionProfiles.IsDigit(hint.Role))\r\n                Digit=Enum.GetValues\u003CDigitRole\u003E().First(d=\u003Ehint.Role.StartsWith(d.ToString(),StringComparison.Ordinal));\r\n            IsWrist = !IsHelper \u0026\u0026 !IsNonHandTrack \u0026\u0026 !IsTip \u0026\u0026 Digit is null\r\n                \u0026\u0026 new[] { \u0022hand\u0022, \u0022wrist\u0022, \u0022handl\u0022, \u0022handr\u0022, \u0022wristl\u0022, \u0022wristr\u0022, \u0022handleft\u0022, \u0022handright\u0022, \u0022wristleft\u0022, \u0022wristright\u0022 }\r\n                    .Any(suffix =\u003E Core.EndsWith(suffix, StringComparison.Ordinal));\r\n            if(hint?.Role==\u0022Hand\u0022\u0026\u0026!IsHelper\u0026\u0026!IsNonHandTrack\u0026\u0026!IsTip)IsWrist=true;\r\n            else if(hint is not null)IsWrist=false;\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Core/Mapping/HandDetectionProfiles.cs","FileName":"HandDetectionProfiles.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":394704,"IsPrivate":false,"Code":"#nullable enable\r\nusing System.Text.RegularExpressions;\r\nusing Skel = HumanoidHandRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidHandRetargeter.Mapping;\r\n\r\n/// \u003Csummary\u003EReference conventions supply semantic hints; they never bypass hierarchy validation.\u003C/summary\u003E\r\ninternal static partial class HandDetectionProfiles\r\n{\r\n    internal sealed record Alias(string Role,string Name);\r\n    internal sealed record Hint(string Role,HandSide Side,string Profile);\r\n    internal sealed class Profile\r\n    {\r\n        public string Name { get; }\r\n        private readonly Regex[] prefixes;\r\n        public Dictionary\u003Cstring,string[]\u003E Roles { get; }\r\n        public int RoleCount { get; }\r\n        public Profile(string name,string[] patterns,Alias[] aliases)\r\n        {\r\n            Name=name;prefixes=patterns.Select(p=\u003Enew Regex(p,RegexOptions.IgnoreCase|RegexOptions.CultureInvariant)).ToArray();\r\n            Roles=aliases.GroupBy(a=\u003EKey(a.Name)).ToDictionary(g=\u003Eg.Key,g=\u003Eg.Select(a=\u003Ea.Role).Distinct().ToArray());\r\n            RoleCount=aliases.Select(a=\u003Ea.Role).Distinct().Count();\r\n        }\r\n        public string Strip(string name)\r\n        {\r\n            // Strip FBX namespaces independently of exporter prefixes (e.g. Character:CC_Base_L_Hand).\r\n            name=name[(name.LastIndexOf(\u0027:\u0027)\u002B1)..];\r\n            var hash=name.LastIndexOf(\u0027#\u0027);\r\n            if(hash\u003E=0\u0026\u0026name[(hash\u002B1)..].All(char.IsDigit))name=name[..hash];\r\n            foreach(var prefix in prefixes)name=prefix.Replace(name,\u0022\u0022);\r\n            return name;\r\n        }\r\n    }\r\n    internal static string Key(string name)=\u003Estring.Concat(name.Where(char.IsLetterOrDigit)).ToLowerInvariant();\r\n\r\n    internal static Hint?[] Resolve(Skel rig)\r\n    {\r\n        var proposals=new Dictionary\u003Cint,List\u003C(int Score,Hint Hint)\u003E\u003E();\r\n        foreach(var profile in Catalog.Concat(new[]{CinemaHumanProfile}))\r\n        {\r\n            var matches=new Dictionary\u003Cint,string\u003E();\r\n            foreach(var bone in rig.Bones)\r\n                if(profile.Roles.TryGetValue(Key(profile.Strip(bone.Name)),out var roles)\u0026\u0026roles.Length==1)\r\n                    matches[bone.Index]=roles[0];\r\n            foreach(var wrist in matches.Where(p=\u003Ep.Value is \u0022HandL\u0022 or \u0022HandR\u0022))\r\n            {\r\n                var side=wrist.Value[^1];\r\n                var relevant=matches.Where(p=\u003Ep.Value[^1]==side \u0026\u0026 (p.Key==wrist.Key ||\r\n                    (IsDigit(p.Value)\u0026\u0026rig.DescendsFrom(p.Key,wrist.Key)) ||\r\n                    (IsArm(p.Value)\u0026\u0026rig.DescendsFrom(wrist.Key,p.Key)))).ToArray();\r\n                // Match coverage distinguishes e.g. SOMA metacarpals from Mixamo phalanges.\r\n                // Namespace evidence breaks equivalent-name ties without overpowering anatomy.\r\n                var score=relevant.Length*1000\u002Brelevant.Select(p=\u003Ep.Value).Distinct().Count()*100/Math.Max(1,profile.RoleCount/2)\r\n                    \u002Brelevant.Count(p=\u003Eprofile.Strip(rig[p.Key].Name)!=rig[p.Key].Name)*2;\r\n                foreach(var match in relevant)\r\n                {\r\n                    if(!proposals.TryGetValue(match.Key,out var list))proposals[match.Key]=list=new();\r\n                    list.Add((score,new(match.Value[..^1],side==\u0027L\u0027?HandSide.Left:HandSide.Right,profile.Name)));\r\n                }\r\n            }\r\n        }\r\n        var hints=new Hint?[rig.Count];\r\n        foreach(var pair in proposals)\r\n        {\r\n            var best=pair.Value.Where(p=\u003Ep.Score==pair.Value.Max(v=\u003Ev.Score)).Select(p=\u003Ep.Hint).ToArray();\r\n            if(best.Select(p=\u003E(p.Role,p.Side)).Distinct().Count()==1)hints[pair.Key]=best[0];\r\n        }\r\n        ResolveNumberedFpsHands(rig,hints);\r\n        ResolveUnlabelledSplitArms(rig,hints);\r\n        return hints;\r\n    }\r\n    // Numbered FPS rays can run from thumb to pinky or in reverse, with an\r\n    // additional palm joint on the four fingers. Require the whole convention\r\n    // and its radial geometry; bare finger numbers alone are ambiguous.\r\n    private static void ResolveNumberedFpsHands(Skel rig,Hint?[] hints)\r\n    {\r\n        foreach(var side in new[]{HandSide.Left,HandSide.Right})\r\n        {\r\n            var suffix=side==HandSide.Left?\u0022l\u0022:\u0022r\u0022;\r\n            string Name(int bone)=\u003EKey(rig[bone].Name[(rig[bone].Name.LastIndexOf(\u0027:\u0027)\u002B1)..]).Replace(\u0022left\u0022,\u0022l\u0022).Replace(\u0022right\u0022,\u0022r\u0022);\r\n            var wrists=rig.Bones.Where(b=\u003EName(b.Index)==\u0022hand\u0022\u002Bsuffix).ToArray();\r\n            if(wrists.Length!=1)continue;\r\n            var wrist=wrists[0].Index;\r\n            foreach(var thumbNumber in new[]{1,5})\r\n            {\r\n            var chains=new List\u003Cint[]\u003E();\r\n            for(var finger=1;finger\u003C=5;finger\u002B\u002B)\r\n            {\r\n                var chain=new List\u003Cint\u003E();var parent=wrist;\r\n                for(var joint=1;joint\u003C=(finger==thumbNumber?3:4);joint\u002B\u002B)\r\n                {\r\n                    var pattern=$\u0022^finger0*{finger}0*{joint}{suffix}$\u0022;\r\n                    var children=rig.ChildrenOf(parent).Where(i=\u003ERegex.IsMatch(Name(i),pattern)).ToArray();\r\n                    if(children.Length!=1)break;\r\n                    parent=children[0];chain.Add(parent);\r\n                }\r\n                if(chain.Count!=(finger==thumbNumber?3:4))break;\r\n                // A three-joint thumb must not just be a truncated four-joint finger.\r\n                if(finger==thumbNumber\u0026\u0026rig.ChildrenOf(parent).Any(i=\u003ERegex.IsMatch(Name(i),$\u0022^finger0*{finger}0*4{suffix}$\u0022)))break;\r\n                chains.Add(chain.ToArray());\r\n            }\r\n            if(chains.Count!=5)continue;\r\n            var thumb=rig.RestWorld[chains[thumbNumber-1][0]].Pos;\r\n            var radial=System.Numerics.Vector3.DistanceSquared(thumb,rig.RestWorld[chains[thumbNumber==5?3:1][0]].Pos);\r\n            var ulnar=System.Numerics.Vector3.DistanceSquared(thumb,rig.RestWorld[chains[thumbNumber==5?0:4][0]].Pos);\r\n            if(!float.IsFinite(radial)||!float.IsFinite(ulnar)||radial\u003E=ulnar*.8f)continue;\r\n            var roles=thumbNumber==5?new[]{\u0022Pinky\u0022,\u0022Ring\u0022,\u0022Middle\u0022,\u0022Index\u0022,\u0022Thumb\u0022}:new[]{\u0022Thumb\u0022,\u0022Index\u0022,\u0022Middle\u0022,\u0022Ring\u0022,\u0022Pinky\u0022};\r\n            if(chains.SelectMany(c=\u003Ec).Any(i=\u003Ehints[i] is not null))continue;\r\n            for(var finger=0;finger\u003C5;finger\u002B\u002B)\r\n                for(var joint=0;joint\u003Cchains[finger].Length;joint\u002B\u002B)\r\n                    hints[chains[finger][joint]]=new(roles[finger]\u002B(finger!=thumbNumber-1\u0026\u0026joint==0?\u0022Meta\u0022:\u0022Prox\u0022),side,\u0022numbered_fps_hands\u0022);\r\n            }\r\n        }\r\n    }\r\n    internal static bool IsDigit(string role)=\u003Enew[]{\u0022Thumb\u0022,\u0022Index\u0022,\u0022Middle\u0022,\u0022Ring\u0022,\u0022Pinky\u0022}.Any(role.StartsWith);\r\n    private static readonly Profile CinemaHumanProfile=CinemaBaseHuman();\r\n    private static Profile CinemaBaseHuman()\r\n    {\r\n        var aliases=new List\u003CAlias\u003E();\r\n        foreach(var side in new[]{\u0022L\u0022,\u0022R\u0022})\r\n        {\r\n            aliases.Add(new(\u0022UpperArm\u0022\u002Bside,side\u002B\u0022Upperarm\u0022));\r\n            aliases.Add(new(\u0022LowerArm\u0022\u002Bside,side\u002B\u0022Forearm1\u0022));\r\n            aliases.Add(new(\u0022Hand\u0022\u002Bside,side\u002B\u0022Palm\u0022));\r\n            var digits=new[]{\u0022Thumb\u0022,\u0022Index\u0022,\u0022Middle\u0022,\u0022Ring\u0022,\u0022Pinky\u0022};\r\n            var joints=new[]{\u0022Prox\u0022,\u0022Mid\u0022,\u0022Dist\u0022};\r\n            for(var d=0;d\u003C5;d\u002B\u002B)for(var j=0;j\u003C3;j\u002B\u002B)\r\n                aliases.Add(new(digits[d]\u002Bjoints[j]\u002Bside,side\u002B\u0022Digit\u0022\u002B(d\u002B1)\u002B(j\u002B1)));\r\n        }\r\n        return new(\u0022cinema4d_base_human\u0022,new[]{\u0022^Base[-_]Human\u0022},aliases.ToArray());\r\n    }\r\n    private static bool IsArm(string role)=\u003Erole.StartsWith(\u0022Clavicle\u0022)||role.StartsWith(\u0022UpperArm\u0022)||role.StartsWith(\u0022LowerArm\u0022);\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Core/Maths/MathQ.cs","FileName":"MathQ.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":394704,"IsPrivate":false,"Code":"// Adapted from humanoid-retargeter 26084c96c3fc870aaf9a5bd798de063ce2fd62df; namespace only.\r\n#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Numerics;\r\n\r\nnamespace HumanoidHandRetargeter.Maths;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/HumanoidHandRetargeter/Assembly.cs)\r\n\r\n/// \u003Csummary\u003E\r\n/// Quaternion algebra helpers used throughout the retargeting pipeline.\r\n/// All angles are radians; quaternions are XYZW (\u003Csee cref=\u0022System.Numerics.Quaternion\u0022/\u003E native);\r\n/// \u003Cc\u003Ea * b\u003C/c\u003E applies \u003Cc\u003Eb\u003C/c\u003E first (column-vector convention).\r\n/// \u003C/summary\u003E\r\npublic static class MathQ\r\n{\r\n    private const float NearZeroLengthSq = 1e-12f;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Defensively normalizes a quaternion. Returns \u003Csee cref=\u0022Quaternion.Identity\u0022/\u003E when the\r\n    /// input has near-zero length or non-finite components instead of producing NaNs.\r\n    /// \u003C/summary\u003E\r\n    public static Quaternion Normalize(Quaternion q)\r\n    {\r\n        var lengthSq = q.LengthSquared();\r\n        if (!float.IsFinite(lengthSq) || lengthSq \u003C NearZeroLengthSq)\r\n            return Quaternion.Identity;\r\n        return Quaternion.Multiply(q, 1f / MathF.Sqrt(lengthSq));\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Shortest-arc rotation taking direction \u003Cparamref name=\u0022a\u0022/\u003E onto direction \u003Cparamref name=\u0022b\u0022/\u003E\r\n    /// (magnitudes are ignored). For antiparallel inputs a 180\u00B0 rotation about a stable axis\r\n    /// perpendicular to \u003Cparamref name=\u0022a\u0022/\u003E is returned. Near-zero inputs yield identity.\r\n    /// \u003C/summary\u003E\r\n    public static Quaternion FromTo(Vector3 a, Vector3 b)\r\n    {\r\n        if (a.LengthSquared() \u003C NearZeroLengthSq || b.LengthSquared() \u003C NearZeroLengthSq)\r\n            return Quaternion.Identity;\r\n\r\n        var an = Vector3.Normalize(a);\r\n        var bn = Vector3.Normalize(b);\r\n        var dot = Vector3.Dot(an, bn);\r\n\r\n        if (dot \u003E= 1f - 1e-8f)\r\n            return Quaternion.Identity;\r\n\r\n        if (dot \u003C= -1f \u002B 1e-7f)\r\n        {\r\n            // Antiparallel: any axis perpendicular to a works; pick a stable one.\r\n            return Quaternion.CreateFromAxisAngle(Perpendicular(an), MathF.PI);\r\n        }\r\n\r\n        // q = (a x b, 1 \u002B a\u00B7b), normalized \u2014 the half-angle construction of the shortest arc.\r\n        var cross = Vector3.Cross(an, bn);\r\n        return Normalize(new Quaternion(cross.X, cross.Y, cross.Z, 1f \u002B dot));\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Decomposes a rotation into \u003Cparamref name=\u0022swing\u0022/\u003E and \u003Cparamref name=\u0022twist\u0022/\u003E so that\r\n    /// \u003Cc\u003Eq == swing * twist\u003C/c\u003E (twist applied first). The twist is the projection of the\r\n    /// rotation onto \u003Cparamref name=\u0022twistAxis\u0022/\u003E: its rotation axis is parallel to\r\n    /// \u003Cparamref name=\u0022twistAxis\u0022/\u003E, while the swing\u0027s rotation axis is perpendicular to it.\r\n    /// When the rotation is a pure 180\u00B0 swing (no component about the axis), twist is identity.\r\n    /// \u003C/summary\u003E\r\n    /// \u003Cexception cref=\u0022ArgumentException\u0022\u003EThrown when \u003Cparamref name=\u0022twistAxis\u0022/\u003E has near-zero length.\u003C/exception\u003E\r\n    public static void SwingTwist(Quaternion q, Vector3 twistAxis, out Quaternion swing, out Quaternion twist)\r\n    {\r\n        if (twistAxis.LengthSquared() \u003C NearZeroLengthSq)\r\n            throw new ArgumentException(\u0022Twist axis must be non-zero.\u0022, nameof(twistAxis));\r\n\r\n        var axis = Vector3.Normalize(twistAxis);\r\n        q = Normalize(q);\r\n\r\n        // Project the rotation\u0027s vector part onto the twist axis.\r\n        var projection = q.X * axis.X \u002B q.Y * axis.Y \u002B q.Z * axis.Z;\r\n        var raw = new Quaternion(axis.X * projection, axis.Y * projection, axis.Z * projection, q.W);\r\n\r\n        // Degenerate case (q.W ~ 0 and projection ~ 0): pure 180\u00B0 swing, no twist.\r\n        twist = Normalize(raw);\r\n        swing = Normalize(q * Quaternion.Conjugate(twist));\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Geodesic angle between two rotations in radians, in [0, \u03C0], invariant under the\r\n    /// q / -q double cover (so \u003Cc\u003EAngleBetween(q, -q) == 0\u003C/c\u003E).\r\n    /// \u003C/summary\u003E\r\n    public static float AngleBetween(Quaternion a, Quaternion b)\r\n    {\r\n        // Use atan2 on the delta rotation rather than acos on the dot product: acos is\r\n        // ill-conditioned near 0\u00B0, atan2 is accurate over the whole range.\r\n        var delta = Quaternion.Conjugate(Normalize(a)) * Normalize(b);\r\n        var sinHalf = MathF.Sqrt(delta.X * delta.X \u002B delta.Y * delta.Y \u002B delta.Z * delta.Z);\r\n        var cosHalf = MathF.Abs(delta.W); // |W| folds the q / -q double cover.\r\n        return 2f * MathF.Atan2(sinHalf, cosHalf);\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Angle between two directions in radians, in [0, \u03C0] (magnitudes are ignored).\r\n    /// Near-zero inputs yield 0. Uses atan2, which stays accurate near 0 and \u03C0 where\r\n    /// acos of the dot product loses precision.\r\n    /// \u003C/summary\u003E\r\n    public static float AngleBetween(Vector3 a, Vector3 b)\r\n    {\r\n        if (a.LengthSquared() \u003C NearZeroLengthSq || b.LengthSquared() \u003C NearZeroLengthSq)\r\n            return 0f;\r\n        return MathF.Atan2(Vector3.Cross(a, b).Length(), Vector3.Dot(a, b));\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Builds an orthonormal right-handed basis rotation from a forward and an up hint.\r\n    /// \u003C/summary\u003E\r\n    /// \u003Cremarks\u003E\r\n    /// Convention: returns the rotation \u003Cc\u003ER\u003C/c\u003E such that\r\n    /// \u003Clist type=\u0022bullet\u0022\u003E\r\n    /// \u003Citem\u003E\u003Cc\u003Erotate(R, \u002BY) == normalize(forward)\u003C/c\u003E (primary axis),\u003C/item\u003E\r\n    /// \u003Citem\u003E\u003Cc\u003Erotate(R, \u002BZ) == \u003C/c\u003E the Gram-Schmidt orthonormalization of \u003Cparamref name=\u0022up\u0022/\u003E\r\n    /// against forward (secondary axis),\u003C/item\u003E\r\n    /// \u003Citem\u003E\u003Cc\u003Erotate(R, \u002BX) == forwardN x upOrtho\u003C/c\u003E (right-handed completion).\u003C/item\u003E\r\n    /// \u003C/list\u003E\r\n    /// Degenerate inputs are handled defensively: a near-zero forward yields identity; an up hint\r\n    /// (near-)parallel to forward falls back to world \u002BZ, or world \u002BX when forward itself is\r\n    /// (near-)parallel to \u002BZ.\r\n    /// \u003C/remarks\u003E\r\n    public static Quaternion BasisFromForwardUp(Vector3 forward, Vector3 up)\r\n    {\r\n        if (forward.LengthSquared() \u003C NearZeroLengthSq)\r\n            return Quaternion.Identity;\r\n\r\n        var y = Vector3.Normalize(forward);\r\n\r\n        var z = up - y * Vector3.Dot(up, y);\r\n        if (z.LengthSquared() \u003C 1e-8f)\r\n        {\r\n            // Up hint unusable: pick a stable fallback and orthonormalize it.\r\n            var fallback = MathF.Abs(Vector3.Dot(y, Vector3.UnitZ)) \u003E 0.99f ? Vector3.UnitX : Vector3.UnitZ;\r\n            z = fallback - y * Vector3.Dot(fallback, y);\r\n        }\r\n        z = Vector3.Normalize(z);\r\n\r\n        var x = Vector3.Cross(y, z);\r\n\r\n        // System.Numerics matrices act on row vectors, so the rows are the images of the\r\n        // unit axes under the rotation: row1 = R*X, row2 = R*Y, row3 = R*Z.\r\n        var m = new Matrix4x4(\r\n            x.X, x.Y, x.Z, 0f,\r\n            y.X, y.Y, y.Z, 0f,\r\n            z.X, z.Y, z.Z, 0f,\r\n            0f, 0f, 0f, 1f);\r\n\r\n        return Normalize(Quaternion.CreateFromRotationMatrix(m));\r\n    }\r\n\r\n    /// \u003Csummary\u003EStable unit vector perpendicular to \u003Cparamref name=\u0022unit\u0022/\u003E (assumed normalized).\r\n    /// Internal so geometry passes (e.g. \u003Cc\u003ETwoBoneIk\u003C/c\u003E) reuse one implementation.\u003C/summary\u003E\r\n    internal static Vector3 Perpendicular(Vector3 unit)\r\n    {\r\n        var p = Vector3.Cross(unit, Vector3.UnitX);\r\n        if (p.LengthSquared() \u003C 1e-6f)\r\n            p = Vector3.Cross(unit, Vector3.UnitY);\r\n        return Vector3.Normalize(p);\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_hand_retargeter","Path":"Code/Target/AnimationDriverMesh.cs","FileName":"AnimationDriverMesh.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":394704,"IsPrivate":false,"Code":"namespace HumanoidHandRetargeter.Target;\r\n\r\n/// \u003Csummary\u003ESmall hidden mesh required by SceneModel\u0027s animation owner.\r\n/// Cube FBX layout from ufbx\u0027s public-domain maya_cube_7400_ascii fixture:\r\n/// https://github.com/ufbx/ufbx/blob/master/data/maya_cube_7400_ascii.fbx\r\n/// Used under ufbx LICENSE alternative B (public domain).\u003C/summary\u003E\r\npublic static class AnimationDriverMesh\r\n{\r\n    public static readonly string Fbx = Text.Replace(\u0022\\r\\n\u0022, \u0022\\n\u0022).Replace(\u0022\\n\u0022, \u0022\\r\\n\u0022);\r\n    private const string Text = \u0022\u0022\u0022\r\n; FBX 7.4.0 project file\r\n; ----------------------------------------------------\r\n\r\nFBXHeaderExtension:  {\r\n\tFBXHeaderVersion: 1003\r\n\tFBXVersion: 7400\r\n\tCreationTimeStamp:  {\r\n\t\tVersion: 1000\r\n\t\tYear: 2020\r\n\t\tMonth: 3\r\n\t\tDay: 31\r\n\t\tHour: 21\r\n\t\tMinute: 36\r\n\t\tSecond: 10\r\n\t\tMillisecond: 814\r\n\t}\r\n\tCreator: \u0022FBX SDK/FBX Plugins version 2019.2\u0022\r\n\tSceneInfo: \u0022SceneInfo::GlobalInfo\u0022, \u0022UserData\u0022 {\r\n\t\tType: \u0022UserData\u0022\r\n\t\tVersion: 100\r\n\t\tMetaData:  {\r\n\t\t\tVersion: 100\r\n\t\t\tTitle: \u0022\u0022\r\n\t\t\tSubject: \u0022\u0022\r\n\t\t\tAuthor: \u0022\u0022\r\n\t\t\tKeywords: \u0022\u0022\r\n\t\t\tRevision: \u0022\u0022\r\n\t\t\tComment: \u0022\u0022\r\n\t\t}\r\n\t\tProperties70:  {\r\n\t\t\tP: \u0022DocumentUrl\u0022, \u0022KString\u0022, \u0022Url\u0022, \u0022\u0022, \u0022animation_driver.fbx\u0022\r\n\t\t\tP: \u0022SrcDocumentUrl\u0022, \u0022KString\u0022, \u0022Url\u0022, \u0022\u0022, \u0022animation_driver.fbx\u0022\r\n\t\t\tP: \u0022Original\u0022, \u0022Compound\u0022, \u0022\u0022, \u0022\u0022\r\n\t\t\tP: \u0022Original|ApplicationVendor\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Autodesk\u0022\r\n\t\t\tP: \u0022Original|ApplicationName\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Maya\u0022\r\n\t\t\tP: \u0022Original|ApplicationVersion\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022201900\u0022\r\n\t\t\tP: \u0022Original|DateTime_GMT\u0022, \u0022DateTime\u0022, \u0022\u0022, \u0022\u0022, \u002231/03/2020 18:36:10.811\u0022\r\n\t\t\tP: \u0022Original|FileName\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022animation_driver.fbx\u0022\r\n\t\t\tP: \u0022LastSaved\u0022, \u0022Compound\u0022, \u0022\u0022, \u0022\u0022\r\n\t\t\tP: \u0022LastSaved|ApplicationVendor\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Autodesk\u0022\r\n\t\t\tP: \u0022LastSaved|ApplicationName\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Maya\u0022\r\n\t\t\tP: \u0022LastSaved|ApplicationVersion\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022201900\u0022\r\n\t\t\tP: \u0022LastSaved|DateTime_GMT\u0022, \u0022DateTime\u0022, \u0022\u0022, \u0022\u0022, \u002231/03/2020 18:36:10.811\u0022\r\n\t\t\tP: \u0022Original|ApplicationActiveProject\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022animation_driver.fbx\u0022\r\n\t\t}\r\n\t}\r\n}\r\nGlobalSettings:  {\r\n\tVersion: 1000\r\n\tProperties70:  {\r\n\t\tP: \u0022UpAxis\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,1\r\n\t\tP: \u0022UpAxisSign\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,1\r\n\t\tP: \u0022FrontAxis\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,2\r\n\t\tP: \u0022FrontAxisSign\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,1\r\n\t\tP: \u0022CoordAxis\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,0\r\n\t\tP: \u0022CoordAxisSign\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,1\r\n\t\tP: \u0022OriginalUpAxis\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,1\r\n\t\tP: \u0022OriginalUpAxisSign\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,1\r\n\t\tP: \u0022UnitScaleFactor\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,1\r\n\t\tP: \u0022OriginalUnitScaleFactor\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,1\r\n\t\tP: \u0022AmbientColor\u0022, \u0022ColorRGB\u0022, \u0022Color\u0022, \u0022\u0022,0,0,0\r\n\t\tP: \u0022DefaultCamera\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Producer Perspective\u0022\r\n\t\tP: \u0022TimeMode\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,11\r\n\t\tP: \u0022TimeProtocol\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,2\r\n\t\tP: \u0022SnapOnFrameMode\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\tP: \u0022TimeSpanStart\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,1924423250\r\n\t\tP: \u0022TimeSpanStop\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,384884650000\r\n\t\tP: \u0022CustomFrameRate\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,-1\r\n\t\tP: \u0022TimeMarker\u0022, \u0022Compound\u0022, \u0022\u0022, \u0022\u0022\r\n\t\tP: \u0022CurrentTimeMarker\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,-1\r\n\t}\r\n}\r\n\r\n; Documents Description\r\n;------------------------------------------------------------------\r\n\r\nDocuments:  {\r\n\tCount: 1\r\n\tDocument: 1907658701024, \u0022\u0022, \u0022Scene\u0022 {\r\n\t\tProperties70:  {\r\n\t\t\tP: \u0022SourceObject\u0022, \u0022object\u0022, \u0022\u0022, \u0022\u0022\r\n\t\t\tP: \u0022ActiveAnimStackName\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Take 001\u0022\r\n\t\t}\r\n\t\tRootNode: 0\r\n\t}\r\n}\r\n\r\n; Document References\r\n;------------------------------------------------------------------\r\n\r\nReferences:  {\r\n}\r\n\r\n; Object definitions\r\n;------------------------------------------------------------------\r\n\r\nDefinitions:  {\r\n\tVersion: 100\r\n\tCount: 6\r\n\tObjectType: \u0022GlobalSettings\u0022 {\r\n\t\tCount: 1\r\n\t}\r\n\tObjectType: \u0022AnimationStack\u0022 {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \u0022FbxAnimStack\u0022 {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \u0022Description\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022\u0022\r\n\t\t\t\tP: \u0022LocalStart\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022LocalStop\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ReferenceStart\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ReferenceStop\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,0\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tObjectType: \u0022AnimationLayer\u0022 {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \u0022FbxAnimLayer\u0022 {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \u0022Weight\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,100\r\n\t\t\t\tP: \u0022Mute\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022Solo\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022Lock\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022Color\u0022, \u0022ColorRGB\u0022, \u0022Color\u0022, \u0022\u0022,0.8,0.8,0.8\r\n\t\t\t\tP: \u0022BlendMode\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationAccumulationMode\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ScaleAccumulationMode\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022BlendModeBypass\u0022, \u0022ULongLong\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tObjectType: \u0022Geometry\u0022 {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \u0022FbxMesh\u0022 {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \u0022Color\u0022, \u0022ColorRGB\u0022, \u0022Color\u0022, \u0022\u0022,0.8,0.8,0.8\r\n\t\t\t\tP: \u0022BBoxMin\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022BBoxMax\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022Primary Visibility\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,1\r\n\t\t\t\tP: \u0022Casts Shadows\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,1\r\n\t\t\t\tP: \u0022Receive Shadows\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,1\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tObjectType: \u0022Material\u0022 {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \u0022FbxSurfaceLambert\u0022 {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \u0022ShadingModel\u0022, \u0022KString\u0022, \u0022\u0022, \u0022\u0022, \u0022Lambert\u0022\r\n\t\t\t\tP: \u0022MultiLayer\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022EmissiveColor\u0022, \u0022Color\u0022, \u0022\u0022, \u0022A\u0022,0,0,0\r\n\t\t\t\tP: \u0022EmissiveFactor\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,1\r\n\t\t\t\tP: \u0022AmbientColor\u0022, \u0022Color\u0022, \u0022\u0022, \u0022A\u0022,0.2,0.2,0.2\r\n\t\t\t\tP: \u0022AmbientFactor\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,1\r\n\t\t\t\tP: \u0022DiffuseColor\u0022, \u0022Color\u0022, \u0022\u0022, \u0022A\u0022,0.8,0.8,0.8\r\n\t\t\t\tP: \u0022DiffuseFactor\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,1\r\n\t\t\t\tP: \u0022Bump\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022NormalMap\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022BumpFactor\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,1\r\n\t\t\t\tP: \u0022TransparentColor\u0022, \u0022Color\u0022, \u0022\u0022, \u0022A\u0022,0,0,0\r\n\t\t\t\tP: \u0022TransparencyFactor\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,0\r\n\t\t\t\tP: \u0022DisplacementColor\u0022, \u0022ColorRGB\u0022, \u0022Color\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022DisplacementFactor\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,1\r\n\t\t\t\tP: \u0022VectorDisplacementColor\u0022, \u0022ColorRGB\u0022, \u0022Color\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022VectorDisplacementFactor\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,1\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tObjectType: \u0022Model\u0022 {\r\n\t\tCount: 1\r\n\t\tPropertyTemplate: \u0022FbxNode\u0022 {\r\n\t\t\tProperties70:  {\r\n\t\t\t\tP: \u0022QuaternionInterpolate\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationOffset\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022RotationPivot\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022ScalingOffset\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022ScalingPivot\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022TranslationActive\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022TranslationMin\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022TranslationMax\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022TranslationMinX\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022TranslationMinY\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022TranslationMinZ\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022TranslationMaxX\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022TranslationMaxY\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022TranslationMaxZ\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationOrder\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationSpaceForLimitOnly\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationStiffnessX\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationStiffnessY\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationStiffnessZ\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022AxisLen\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,10\r\n\t\t\t\tP: \u0022PreRotation\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022PostRotation\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022RotationActive\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationMin\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022RotationMax\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022RotationMinX\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationMinY\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationMinZ\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationMaxX\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationMaxY\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022RotationMaxZ\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022InheritType\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ScalingActive\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ScalingMin\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022ScalingMax\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,1,1,1\r\n\t\t\t\tP: \u0022ScalingMinX\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ScalingMinY\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ScalingMinZ\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ScalingMaxX\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ScalingMaxY\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022ScalingMaxZ\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022GeometricTranslation\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022GeometricRotation\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\t\tP: \u0022GeometricScaling\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,1,1,1\r\n\t\t\t\tP: \u0022MinDampRangeX\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MinDampRangeY\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MinDampRangeZ\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MaxDampRangeX\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MaxDampRangeY\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MaxDampRangeZ\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MinDampStrengthX\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MinDampStrengthY\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MinDampStrengthZ\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MaxDampStrengthX\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MaxDampStrengthY\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022MaxDampStrengthZ\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022PreferedAngleX\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022PreferedAngleY\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022PreferedAngleZ\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022LookAtProperty\u0022, \u0022object\u0022, \u0022\u0022, \u0022\u0022\r\n\t\t\t\tP: \u0022UpVectorProperty\u0022, \u0022object\u0022, \u0022\u0022, \u0022\u0022\r\n\t\t\t\tP: \u0022Show\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,1\r\n\t\t\t\tP: \u0022NegativePercentShapeSupport\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,1\r\n\t\t\t\tP: \u0022DefaultAttributeIndex\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,-1\r\n\t\t\t\tP: \u0022Freeze\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022LODBox\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,0\r\n\t\t\t\tP: \u0022Lcl Translation\u0022, \u0022Lcl Translation\u0022, \u0022\u0022, \u0022A\u0022,0,0,0\r\n\t\t\t\tP: \u0022Lcl Rotation\u0022, \u0022Lcl Rotation\u0022, \u0022\u0022, \u0022A\u0022,0,0,0\r\n\t\t\t\tP: \u0022Lcl Scaling\u0022, \u0022Lcl Scaling\u0022, \u0022\u0022, \u0022A\u0022,1,1,1\r\n\t\t\t\tP: \u0022Visibility\u0022, \u0022Visibility\u0022, \u0022\u0022, \u0022A\u0022,1\r\n\t\t\t\tP: \u0022Visibility Inheritance\u0022, \u0022Visibility Inheritance\u0022, \u0022\u0022, \u0022\u0022,1\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n\r\n; Object properties\r\n;------------------------------------------------------------------\r\n\r\nObjects:  {\r\n\tGeometry: 1907663073408, \u0022Geometry::\u0022, \u0022Mesh\u0022 {\r\n\t\tVertices: *24 {\r\n\t\t\ta: -0.5,-0.5,0.5,0.5,-0.5,0.5,-0.5,0.5,0.5,0.5,0.5,0.5,-0.5,0.5,-0.5,0.5,0.5,-0.5,-0.5,-0.5,-0.5,0.5,-0.5,-0.5\r\n\t\t}\r\n\t\tPolygonVertexIndex: *24 {\r\n\t\t\ta: 0,1,3,-3,2,3,5,-5,4,5,7,-7,6,7,1,-1,1,7,5,-4,6,0,2,-5\r\n\t\t}\r\n\t\tEdges: *12 {\r\n\t\t\ta: 0,2,6,10,3,1,7,5,11,9,15,13\r\n\t\t}\r\n\t\tGeometryVersion: 124\r\n\t\tLayerElementNormal: 0 {\r\n\t\t\tVersion: 102\r\n\t\t\tName: \u0022\u0022\r\n\t\t\tMappingInformationType: \u0022ByPolygonVertex\u0022\r\n\t\t\tReferenceInformationType: \u0022Direct\u0022\r\n\t\t\tNormals: *72 {\r\n\t\t\t\ta: 0,0,1,0,0,1,0,0,1,0,0,1,0,1,0,0,1,0,0,1,0,0,1,0,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,1,0,0,1,0,0,1,0,0,1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0\r\n\t\t\t}\r\n\t\t\tNormalsW: *24 {\r\n\t\t\t\ta: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayerElementBinormal: 0 {\r\n\t\t\tVersion: 102\r\n\t\t\tName: \u0022map1\u0022\r\n\t\t\tMappingInformationType: \u0022ByPolygonVertex\u0022\r\n\t\t\tReferenceInformationType: \u0022Direct\u0022\r\n\t\t\tBinormals: *72 {\r\n\t\t\t\ta: 0,1,-0,0,1,-0,0,1,-0,0,1,-0,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,0,1,0,0,1,0,0,1,0,0,1,-0,1,0,-0,1,0,0,1,-0,-0,1,0,0,1,0,0,1,0,0,1,0,0,1,0\r\n\t\t\t}\r\n\t\t\tBinormalsW: *24 {\r\n\t\t\t\ta: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1\r\n\t\t\t}\r\n\r\n\t\t}\r\n\t\tLayerElementTangent: 0 {\r\n\t\t\tVersion: 102\r\n\t\t\tName: \u0022map1\u0022\r\n\t\t\tMappingInformationType: \u0022ByPolygonVertex\u0022\r\n\t\t\tReferenceInformationType: \u0022Direct\u0022\r\n\t\t\tTangents: *72 {\r\n\t\t\t\ta: 1,-0,-0,1,-0,0,1,-0,0,1,-0,0,1,-0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,-0,1,0,-0,1,0,-0,1,0,-0,0,0,-1,0,0,-1,0,-0,-1,0,0,-1,0,-0,1,0,-0,1,0,-0,1,0,-0,1\r\n\t\t\t}\r\n\t\t\tTangentsW: *24 {\r\n\t\t\t\ta: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayerElementUV: 0 {\r\n\t\t\tVersion: 101\r\n\t\t\tName: \u0022map1\u0022\r\n\t\t\tMappingInformationType: \u0022ByPolygonVertex\u0022\r\n\t\t\tReferenceInformationType: \u0022IndexToDirect\u0022\r\n\t\t\tUV: *28 {\r\n\t\t\t\ta: 0.375,0,0.625,0,0.375,0.25,0.625,0.25,0.375,0.5,0.625,0.5,0.375,0.75,0.625,0.75,0.375,1,0.625,1,0.875,0,0.875,0.25,0.125,0,0.125,0.25\r\n\t\t\t}\r\n\t\t\tUVIndex: *24 {\r\n\t\t\t\ta: 0,1,3,2,2,3,5,4,4,5,7,6,6,7,9,8,1,10,11,3,12,0,2,13\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayerElementSmoothing: 0 {\r\n\t\t\tVersion: 102\r\n\t\t\tName: \u0022\u0022\r\n\t\t\tMappingInformationType: \u0022ByEdge\u0022\r\n\t\t\tReferenceInformationType: \u0022Direct\u0022\r\n\t\t\tSmoothing: *12 {\r\n\t\t\t\ta: 0,0,0,0,0,0,0,0,0,0,0,0\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayerElementMaterial: 0 {\r\n\t\t\tVersion: 101\r\n\t\t\tName: \u0022\u0022\r\n\t\t\tMappingInformationType: \u0022AllSame\u0022\r\n\t\t\tReferenceInformationType: \u0022IndexToDirect\u0022\r\n\t\t\tMaterials: *1 {\r\n\t\t\t\ta: 0\r\n\t\t\t}\r\n\t\t}\r\n\t\tLayer: 0 {\r\n\t\t\tVersion: 100\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \u0022LayerElementNormal\u0022\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \u0022LayerElementBinormal\u0022\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \u0022LayerElementTangent\u0022\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \u0022LayerElementMaterial\u0022\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \u0022LayerElementSmoothing\u0022\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t\tLayerElement:  {\r\n\t\t\t\tType: \u0022LayerElementUV\u0022\r\n\t\t\t\tTypedIndex: 0\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tModel: 1907292236800, \u0022Model::animation_driver\u0022, \u0022Mesh\u0022 {\r\n\t\tVersion: 232\r\n\t\tProperties70:  {\r\n\t\t\tP: \u0022RotationActive\u0022, \u0022bool\u0022, \u0022\u0022, \u0022\u0022,1\r\n\t\t\tP: \u0022InheritType\u0022, \u0022enum\u0022, \u0022\u0022, \u0022\u0022,1\r\n\t\t\tP: \u0022ScalingMax\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\tP: \u0022DefaultAttributeIndex\u0022, \u0022int\u0022, \u0022Integer\u0022, \u0022\u0022,0\r\n\t\t\tP: \u0022currentUVSet\u0022, \u0022KString\u0022, \u0022\u0022, \u0022U\u0022, \u0022map1\u0022\r\n\t\t}\r\n\t\tShading: T\r\n\t\tCulling: \u0022CullingOff\u0022\r\n\t}\r\n\tMaterial: 1908464858960, \u0022Material::lambert1\u0022, \u0022\u0022 {\r\n\t\tVersion: 102\r\n\t\tShadingModel: \u0022lambert\u0022\r\n\t\tMultiLayer: 0\r\n\t\tProperties70:  {\r\n\t\t\tP: \u0022AmbientColor\u0022, \u0022Color\u0022, \u0022\u0022, \u0022A\u0022,0,0,0\r\n\t\t\tP: \u0022DiffuseColor\u0022, \u0022Color\u0022, \u0022\u0022, \u0022A\u0022,0.5,0.5,0.5\r\n\t\t\tP: \u0022DiffuseFactor\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,0.800000011920929\r\n\t\t\tP: \u0022TransparencyFactor\u0022, \u0022Number\u0022, \u0022\u0022, \u0022A\u0022,1\r\n\t\t\tP: \u0022Emissive\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\tP: \u0022Ambient\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0,0,0\r\n\t\t\tP: \u0022Diffuse\u0022, \u0022Vector3D\u0022, \u0022Vector\u0022, \u0022\u0022,0.400000005960464,0.400000005960464,0.400000005960464\r\n\t\t\tP: \u0022Opacity\u0022, \u0022double\u0022, \u0022Number\u0022, \u0022\u0022,1\r\n\t\t}\r\n\t}\r\n\tAnimationStack: 1907674289488, \u0022AnimStack::Take 001\u0022, \u0022\u0022 {\r\n\t\tProperties70:  {\r\n\t\t\tP: \u0022LocalStart\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,1924423250\r\n\t\t\tP: \u0022LocalStop\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,230930790000\r\n\t\t\tP: \u0022ReferenceStart\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,1924423250\r\n\t\t\tP: \u0022ReferenceStop\u0022, \u0022KTime\u0022, \u0022Time\u0022, \u0022\u0022,230930790000\r\n\t\t}\r\n\t}\r\n\tAnimationLayer: 1907631731008, \u0022AnimLayer::BaseLayer\u0022, \u0022\u0022 {\r\n\t}\r\n}\r\n\r\n; Object connections\r\n;------------------------------------------------------------------\r\n\r\nConnections:  {\r\n\r\n\t;Model::animation_driver, Model::RootNode\r\n\tC: \u0022OO\u0022,1907292236800,0\r\n\r\n\t;AnimLayer::BaseLayer, AnimStack::Take 001\r\n\tC: \u0022OO\u0022,1907631731008,1907674289488\r\n\r\n\t;Geometry::, Model::animation_driver\r\n\tC: \u0022OO\u0022,1907663073408,1907292236800\r\n\r\n\t;Material::lambert1, Model::animation_driver\r\n\tC: \u0022OO\u0022,1908464858960,1907292236800\r\n}\r\n;Takes section\r\n;----------------------------------------------------\r\n\r\nTakes:  {\r\n\tCurrent: \u0022Take 001\u0022\r\n\tTake: \u0022Take 001\u0022 {\r\n\t\tFileName: \u0022Take_001.tak\u0022\r\n\t\tLocalTime: 1924423250,230930790000\r\n\t\tReferenceTime: 1924423250,230930790000\r\n\t}\r\n}\r\n\r\n\u0022\u0022\u0022;\r\n}\r\n"}]}