{"TotalCount":158,"Files":[{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"Code/HumanoidRetargeter/Cleanup/FootGroundAlign.cs","FileName":"FootGroundAlign.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidRetargeter.Maths;\r\nusing SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidRetargeter.Cleanup;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// \u003Csummary\u003ETunables for the grounded-foot stance recalibration pass.\u003C/summary\u003E\r\npublic sealed class FootGroundAlignOptions\r\n{\r\n    /// \u003Csummary\u003E\r\n    /// Dead zone (degrees): measured stance offsets at or below this are genuine planted\r\n    /// articulation (heel-roll bias, natural lean \u2014 measured 2\u20134\u00B0 on well-rested rigs and\r\n    /// on citizen clips) and are left untouched, keeping the transfer byte-faithful there.\r\n    /// Only offsets beyond it are clearly rest-pose artifacts (measured 12\u201325\u00B0 on the\r\n    /// repro rig) and get recalibrated.\r\n    /// \u003C/summary\u003E\r\n    public float MinCorrectionDeg { get; set; } = 8f;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Maximum mean sole deviation (degrees) a plant may show and still count as a STANCE\r\n    /// for the offset measurement. Plants beyond this are not standing on the sole (crawls,\r\n    /// kneels, prone contact \u2014 measured 60\u201390\u00B0 there) and are excluded; genuine rest-pose\r\n    /// stance artifacts measure well below it (largest seen: 27\u00B0).\r\n    /// \u003C/summary\u003E\r\n    public float MaxStanceDeviationDeg { get; set; } = 35f;\r\n}\r\n\r\n/// \u003Csummary\u003EPer-foot results of a \u003Csee cref=\u0022FootGroundAlign.Apply\u0022/\u003E run.\u003C/summary\u003E\r\npublic sealed class FootGroundAlignFootReport\r\n{\r\n    /// \u003Csummary\u003EPlants that contributed to the stance measurement.\u003C/summary\u003E\r\n    public int StancePlants { get; set; }\r\n\r\n    /// \u003Csummary\u003EPlants excluded as non-stance (mean sole deviation beyond\r\n    /// \u003Csee cref=\u0022FootGroundAlignOptions.MaxStanceDeviationDeg\u0022/\u003E).\u003C/summary\u003E\r\n    public int SkippedPlants { get; set; }\r\n\r\n    /// \u003Csummary\u003EMeasured planted sole offset from the ground plane, degrees (0 when no\r\n    /// stance plants exist).\u003C/summary\u003E\r\n    public float MeasuredOffsetDeg { get; set; }\r\n\r\n    /// \u003Csummary\u003EFoot correction applied to every frame, degrees (0 = inside the dead zone,\r\n    /// nothing changed).\u003C/summary\u003E\r\n    public float AppliedFootDeg { get; set; }\r\n\r\n    /// \u003Csummary\u003EToe correction applied to every frame, degrees.\u003C/summary\u003E\r\n    public float AppliedToeDeg { get; set; }\r\n}\r\n\r\n/// \u003Csummary\u003EResults of a \u003Csee cref=\u0022FootGroundAlign.Apply\u0022/\u003E run.\u003C/summary\u003E\r\npublic sealed class FootGroundAlignReport\r\n{\r\n    /// \u003Csummary\u003ELeft-foot results.\u003C/summary\u003E\r\n    public required FootGroundAlignFootReport Left { get; init; }\r\n\r\n    /// \u003Csummary\u003ERight-foot results.\u003C/summary\u003E\r\n    public required FootGroundAlignFootReport Right { get; init; }\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Grounded-foot stance recalibration: measures how far the foot\u0027s SOLE sits from the ground\r\n/// plane while planted, and \u2014 when that offset is clearly a rest-pose artifact \u2014 rotates it\r\n/// out with one constant per foot, applied to every frame of the clip.\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EWhy a cleanup pass.\u003C/b\u003E The solver transfers feet as rest-relative deltas\r\n/// (\u003Csee cref=\u0022Solve.RoleTransferMode.CharacterDeltaFromRest\u0022/\u003E), so the target keeps its own\r\n/// ankle anatomy \u2014 correct whenever the source\u0027s rest pose is a flat-footed stance (the delta\r\n/// is then \u0022deviation from standing\u0022). Some rigs ship a NON-stance rest (measured: an\r\n/// Auto-Rig-Pro export whose rest foot sits 12\u201325\u00B0 from its planted stance), and that constant\r\n/// offset rides into every frame of the replay \u2014 planted feet hover toe-down/heel-up. What a\r\n/// stance actually looks like is animation evidence (planted phases), which a per-frame\r\n/// solver cannot see, so the recalibration lives here.\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EMeasurement.\u003C/b\u003E Per foot: over every planted frame, the sole normal = rest up\r\n/// carried by the foot\u0027s world delta from the target bind rest (whose feet stand on the\r\n/// ground by construction); plants whose own mean normal sits beyond\r\n/// \u003Csee cref=\u0022FootGroundAlignOptions.MaxStanceDeviationDeg\u0022/\u003E are excluded (crawl/kneel/prone\r\n/// contact is not a stance). The pooled mean normal\u0027s deviation from up is the stance\r\n/// offset.\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003ECorrection.\u003C/b\u003E Offsets inside \u003Csee cref=\u0022FootGroundAlignOptions.MinCorrectionDeg\u0022/\u003E\r\n/// are genuine articulation \u2014 nothing is changed (well-rested rigs and same-rig round trips\r\n/// stay byte-identical through this pass). Beyond it, the shortest-arc rotation taking the\r\n/// pooled normal back to up (pitch\u002Broll only \u2014 yaw/toe-out is pose and follows the source)\r\n/// premultiplies the foot\u0027s world rotation on EVERY frame: a rest artifact is constant, so\r\n/// the fix is too \u2014 within-plant heel-roll, swing styling and frame-to-frame continuity are\r\n/// preserved exactly, and no blending is needed. The toe then receives its own residual\r\n/// constant measured on top of the corrected foot (it neither double-rotates with the foot\r\n/// fix nor inherits the source toe\u0027s own rest artifact). Corrections rotate bones about\r\n/// their own joints: ankle positions are untouched, so the pass composes freely with the\r\n/// \u003Csee cref=\u0022FootPlant\u0022/\u003E position pinning (which preserves foot world rotations).\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EPlant intervals come from the caller\u003C/b\u003E (the pipeline detects them on the\r\n/// SOURCE clip via \u003Csee cref=\u0022FootPlant.DetectPlantIntervals\u0022/\u003E \u2014 ground truth, immune to\r\n/// the hip-height rescaling that can push target-side trajectories outside the cm-tuned\r\n/// Kovar thresholds). So does the decision to run at all: the pipeline invokes this pass\r\n/// only when the source\u0027s normalized rest is implausible as a flat stance (toe at/above\r\n/// ankle level or asymmetric feet \u2014 see \u003Cc\u003ERetargeter.GroundAlignFeet\u003C/c\u003E); on plausible\r\n/// stance rests the solver\u0027s rest-relative transfer is already faithful and planted-sole\r\n/// deviations are genuine articulation (boxing stances, heel rolls) that must not be\r\n/// flattened.\u003C/para\u003E\r\n/// \u003C/remarks\u003E\r\npublic static class FootGroundAlign\r\n{\r\n    /// \u003Csummary\u003EMeasures planted stance offsets and recalibrates feet whose offset is a\r\n    /// rest-pose artifact; returns what was measured and done.\u003C/summary\u003E\r\n    /// \u003Cparam name=\u0022frames\u0022\u003EPer-frame local transforms (skeleton bone order); modified in place.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022skeleton\u0022\u003EBone hierarchy the frames are expressed against; its bind rest\r\n    /// is the flat-stance reference.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022left\u0022\u003ELeft leg chain bone indices.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022right\u0022\u003ERight leg chain bone indices.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022up\u0022\u003EWorld up direction of the clip\u0027s space.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022leftPlants\u0022\u003ELeft-foot plant intervals (frame indices into\r\n    /// \u003Cparamref name=\u0022frames\u0022/\u003E; out-of-range parts are clamped/ignored).\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022rightPlants\u0022\u003ERight-foot plant intervals.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022options\u0022\u003ETunables; defaults used when null.\u003C/param\u003E\r\n    public static FootGroundAlignReport Apply(\r\n        List\u003CXForm[]\u003E frames,\r\n        SkeletonModel skeleton,\r\n        FootChain left,\r\n        FootChain right,\r\n        Vector3 up,\r\n        IReadOnlyList\u003CFrameRange\u003E leftPlants,\r\n        IReadOnlyList\u003CFrameRange\u003E rightPlants,\r\n        FootGroundAlignOptions? options = null)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(frames);\r\n        ArgumentNullException.ThrowIfNull(skeleton);\r\n        ArgumentNullException.ThrowIfNull(left);\r\n        ArgumentNullException.ThrowIfNull(right);\r\n        ArgumentNullException.ThrowIfNull(leftPlants);\r\n        ArgumentNullException.ThrowIfNull(rightPlants);\r\n\r\n        options ??= new FootGroundAlignOptions();\r\n        var report = new FootGroundAlignReport\r\n        {\r\n            Left = new FootGroundAlignFootReport(),\r\n            Right = new FootGroundAlignFootReport(),\r\n        };\r\n        if (frames.Count == 0 || up.LengthSquared() \u003C 1e-12f)\r\n            return report;\r\n        up = Vector3.Normalize(up);\r\n\r\n        RecalibrateFoot(frames, skeleton, left, up, leftPlants, options, report.Left);\r\n        RecalibrateFoot(frames, skeleton, right, up, rightPlants, options, report.Right);\r\n        return report;\r\n    }\r\n\r\n    private static void RecalibrateFoot(\r\n        List\u003CXForm[]\u003E frames, SkeletonModel skeleton, FootChain chain, Vector3 up,\r\n        IReadOnlyList\u003CFrameRange\u003E plants, FootGroundAlignOptions options,\r\n        FootGroundAlignFootReport report)\r\n    {\r\n        int n = frames.Count;\r\n        var foot = chain.Ankle;\r\n        var restFootRotInv = Quaternion.Conjugate(skeleton.RestWorld[foot].Rot);\r\n        var maxStanceCos = MathF.Cos(options.MaxStanceDeviationDeg * MathF.PI / 180f);\r\n\r\n        // ---- measurement: pooled planted sole normal over the stance plants ----\r\n        var pooled = Vector3.Zero;\r\n        foreach (var plant in plants)\r\n        {\r\n            int start = Math.Max(plant.Start, 0);\r\n            int end = Math.Min(plant.End, n - 1);\r\n            if (start \u003E end)\r\n                continue;\r\n\r\n            var plantSum = Vector3.Zero;\r\n            for (int f = start; f \u003C= end; f\u002B\u002B)\r\n            {\r\n                var footRot = FkUtil.BoneWorld(frames[f], skeleton, foot).Rot;\r\n                plantSum \u002B= Vector3.Transform(up, MathQ.Normalize(footRot * restFootRotInv));\r\n            }\r\n            if (plantSum.LengthSquared() \u003C 1e-8f\r\n                || Vector3.Dot(Vector3.Normalize(plantSum), up) \u003C maxStanceCos)\r\n            {\r\n                report.SkippedPlants\u002B\u002B; // not standing on the sole \u2014 crawl/kneel/toe contact\r\n                continue;\r\n            }\r\n            report.StancePlants\u002B\u002B;\r\n            pooled \u002B= plantSum; // frame-count-weighted: longer stances dominate\r\n        }\r\n        if (pooled.LengthSquared() \u003C 1e-8f)\r\n            return;\r\n        pooled = Vector3.Normalize(pooled);\r\n\r\n        var offsetDeg = MathQ.AngleBetween(pooled, up) * (180f / MathF.PI);\r\n        report.MeasuredOffsetDeg = offsetDeg;\r\n        if (offsetDeg \u003C= options.MinCorrectionDeg)\r\n            return; // genuine planted articulation \u2014 leave the transfer byte-faithful\r\n\r\n        // ---- correction: one constant per foot, every frame ----\r\n        var footFix = MathQ.FromTo(pooled, up);\r\n        report.AppliedFootDeg = offsetDeg;\r\n\r\n        // Toe residual measured on top of the corrected foot, same dead zone.\r\n        var toeFix = Quaternion.Identity;\r\n        if (chain.Toe is { } toe \u0026\u0026 skeleton[toe].ParentIndex == foot)\r\n        {\r\n            var restToeRotInv = Quaternion.Conjugate(skeleton.RestWorld[toe].Rot);\r\n            var toePooled = Vector3.Zero;\r\n            foreach (var plant in plants)\r\n            {\r\n                int start = Math.Max(plant.Start, 0);\r\n                int end = Math.Min(plant.End, n - 1);\r\n                for (int f = start; f \u003C= end \u0026\u0026 f \u003E= 0; f\u002B\u002B)\r\n                {\r\n                    var toeRot = FkUtil.BoneWorld(frames[f], skeleton, toe).Rot;\r\n                    toePooled \u002B= Vector3.Transform(\r\n                        up, MathQ.Normalize(footFix * toeRot * restToeRotInv));\r\n                }\r\n            }\r\n            if (toePooled.LengthSquared() \u003E 1e-8f)\r\n            {\r\n                toePooled = Vector3.Normalize(toePooled);\r\n                var toeDeg = MathQ.AngleBetween(toePooled, up) * (180f / MathF.PI);\r\n                if (toeDeg \u003E options.MinCorrectionDeg \u0026\u0026 Vector3.Dot(toePooled, up) \u003E= maxStanceCos)\r\n                {\r\n                    toeFix = MathQ.FromTo(toePooled, up);\r\n                    report.AppliedToeDeg = toeDeg;\r\n                }\r\n            }\r\n        }\r\n\r\n        for (int f = 0; f \u003C n; f\u002B\u002B)\r\n            CorrectFrame(frames[f], skeleton, chain, footFix, toeFix);\r\n    }\r\n\r\n    /// \u003Csummary\u003EPremultiplies the foot\u0027s world rotation by the constant fix (the joint\r\n    /// position is untouched \u2014 the rotation pivots the foot about its own head), then gives\r\n    /// the toe its own residual on top of the corrected foot.\u003C/summary\u003E\r\n    private static void CorrectFrame(\r\n        XForm[] locals, SkeletonModel skeleton, FootChain chain,\r\n        Quaternion footFix, Quaternion toeFix)\r\n    {\r\n        var foot = chain.Ankle;\r\n        var parent = skeleton[foot].ParentIndex;\r\n        var parentRot = parent \u003C 0\r\n            ? Quaternion.Identity\r\n            : FkUtil.BoneWorld(locals, skeleton, parent).Rot;\r\n\r\n        var footWorld = MathQ.Normalize(parentRot * locals[foot].Rot);\r\n        var newFootWorld = MathQ.Normalize(footFix * footWorld);\r\n        locals[foot] = new XForm(\r\n            locals[foot].Pos, MathQ.Normalize(Quaternion.Conjugate(parentRot) * newFootWorld));\r\n\r\n        if (chain.Toe is { } toe \u0026\u0026 skeleton[toe].ParentIndex == foot)\r\n        {\r\n            // Desired toe world = toeFix \u2218 footFix \u2218 original world; re-derive its local\r\n            // against the corrected foot so it does not double-rotate with the foot fix.\r\n            var toeWorldOld = MathQ.Normalize(footWorld * locals[toe].Rot);\r\n            var desired = MathQ.Normalize(toeFix * footFix * toeWorldOld);\r\n            locals[toe] = new XForm(\r\n                locals[toe].Pos, MathQ.Normalize(Quaternion.Conjugate(newFootWorld) * desired));\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"Code/HumanoidRetargeter/Dl/SameFeatures.cs","FileName":"SameFeatures.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidRetargeter.Mapping;\r\nusing HumanoidRetargeter.Maths;\r\nusing HumanoidRetargeter.Skeleton;\r\nusing HumanoidRetargeter.Solve;\r\nusing SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidRetargeter.Dl;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// \u003Csummary\u003EThe z-normalization statistics shipped with the SAME checkpoint\r\n/// (\u003Cc\u003Ems_dict\u003C/c\u003E): per-feature mean/std applied to every input except contact.\u003C/summary\u003E\r\npublic sealed class SameStats\r\n{\r\n    internal float[] LoM, LoS, GoM, GoS, QM, QS, PM, PS, RM, RS, PvM, PvS, QvM, QvS, PprevM, PprevS;\r\n\r\n    /// \u003Csummary\u003EReads the 16 \u003Cc\u003Ems.*\u003C/c\u003E arrays from a parsed weight blob.\u003C/summary\u003E\r\n    public SameStats(SameWeights weights)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(weights);\r\n        LoM = weights.Stat(\u0022lo_m\u0022); LoS = weights.Stat(\u0022lo_s\u0022);\r\n        GoM = weights.Stat(\u0022go_m\u0022); GoS = weights.Stat(\u0022go_s\u0022);\r\n        QM = weights.Stat(\u0022q_m\u0022); QS = weights.Stat(\u0022q_s\u0022);\r\n        PM = weights.Stat(\u0022p_m\u0022); PS = weights.Stat(\u0022p_s\u0022);\r\n        RM = weights.Stat(\u0022r_m\u0022); RS = weights.Stat(\u0022r_s\u0022);\r\n        PvM = weights.Stat(\u0022pv_m\u0022); PvS = weights.Stat(\u0022pv_s\u0022);\r\n        QvM = weights.Stat(\u0022qv_m\u0022); QvS = weights.Stat(\u0022qv_s\u0022);\r\n        PprevM = weights.Stat(\u0022pprev_m\u0022); PprevS = weights.Stat(\u0022pprev_s\u0022);\r\n    }\r\n}\r\n\r\n/// \u003Csummary\u003EA batched per-frame source graph ready for \u003Csee cref=\u0022SameModel.Encode\u0022/\u003E.\u003C/summary\u003E\r\npublic sealed class SameSourceGraph\r\n{\r\n    /// \u003Csummary\u003ENormalized node features, flat [FrameCount\u00B7JointCount \u00D7 32].\u003C/summary\u003E\r\n    public required float[] X { get; init; }\r\n\r\n    /// \u003Csummary\u003EEdge sources (bidirectional \u002B self-loops, all frames).\u003C/summary\u003E\r\n    public required int[] EdgeSrc { get; init; }\r\n\r\n    /// \u003Csummary\u003EEdge destinations.\u003C/summary\u003E\r\n    public required int[] EdgeDst { get; init; }\r\n\r\n    /// \u003Csummary\u003EFrame id per node.\u003C/summary\u003E\r\n    public required int[] Batch { get; init; }\r\n\r\n    /// \u003Csummary\u003ENumber of feature frames (matches the clip\u0027s frame count in production\r\n    /// mode; native frames \u2212 2 in golden-parity mode).\u003C/summary\u003E\r\n    public required int FrameCount { get; init; }\r\n\r\n    /// \u003Csummary\u003EGraph joints per frame (hips subtree \u002B end joints).\u003C/summary\u003E\r\n    public required int JointCount { get; init; }\r\n\r\n    /// \u003Csummary\u003EGraph node names within one frame (bone names; synthesized leaf tips get\r\n    /// a \u003Cc\u003E_end\u003C/c\u003E suffix). For diagnostics and parity tests.\u003C/summary\u003E\r\n    public required string[] JointNames { get; init; }\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Source-side feature pipeline of the SAME port (FEASIBILITY.md \u0022C# port work list\u0022\r\n/// steps 1\u20135): skeleton normalization, cm/Y-up/\u002BZ-facing alignment, per-frame\r\n/// q/p/r/pv/qv/pprev/c features in the root-facing frame, z-normalization, and the\r\n/// bidirectional\u002Bself-loop edge list.\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003ESkeleton normalization without an intermediate skeleton.\u003C/b\u003E SAME\u0027s\r\n/// \u003Cc\u003Emotion_normalize\u003C/c\u003E rebuilds the rig with identity rest-local rotations and\r\n/// re-expresses every frame against it. Algebraically the normalized motion\u0027s world\r\n/// rotations are exactly the world-space deltas from the T-pose,\r\n/// \u003Cc\u003E\u011C(j,t) = G(j,t) \u00B7 G_tpose(j)\u207B\u00B9\u003C/c\u003E, its local rotations are\r\n/// \u003Cc\u003E\u011C(parent)\u207B\u00B9 \u00B7 \u011C(j)\u003C/c\u003E, and its world positions equal the original world positions\r\n/// \u2014 so this port computes the features directly from FK world transforms, no rebuilt\r\n/// skeleton needed (verified against the Python pipeline by the golden-vector tests).\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003ET-pose reference.\u003C/b\u003E SAME consumes the source clip\u0027s first frame as the\r\n/// reference; production keeps that convention but emits one feature frame per clip frame\r\n/// (the sequence is computed over [f0, f0\u2026fN\u22121] with f0 doubling as the reference \u2014 see\r\n/// \u003Csee cref=\u0022TposeReference\u0022/\u003E for why the rest-pose alternative measurably loses).\r\n/// Golden-parity mode replicates Python\u0027s frame accounting exactly (frame 0 = reference,\r\n/// frame 1 dropped).\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EAlignment.\u003C/b\u003E Features assume cm (guaranteed by the importers), Y-up and\r\n/// rest facing \u002BZ with \u002BX to the character\u0027s left. The source is rotated by a world\r\n/// alignment derived from the rig\u0027s rest geometry (\u003Csee cref=\u0022CharacterFrame\u0022/\u003E via the\r\n/// mapping when computable, else the file\u0027s axis metadata), snapped to the nearest whole\r\n/// axis permutation (an exact-axis rig must map to the identity \u2014 the rest-geometry tilt\r\n/// of a few degrees otherwise leaks into every feature), and shifted so the lowest joint\r\n/// over the clip sits on the ground plane.\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EGraph.\u003C/b\u003E Nodes are the hips subtree (hips = mapped Hips role, else the\r\n/// shallowest branch bone) in skeleton order \u2014 hips is always node 0, which is where the\r\n/// root feature row lives \u2014 plus one synthesized end joint per childless leaf (BVH End\r\n/// Sites already import as \u003Cc\u003E_end\u003C/c\u003E bones and are used as-is; FBX leaves get a\r\n/// half-length continuation of their parent segment).\u003C/para\u003E\r\n/// \u003C/remarks\u003E\r\npublic static class SameFeatures\r\n{\r\n    /// \u003Csummary\u003EHow the T-pose reference (skeleton normalization \u002B lo/go features) is chosen.\u003C/summary\u003E\r\n    public enum TposeReference\r\n    {\r\n        /// \u003Csummary\u003EThe clip\u0027s own first frame \u2014 SAME\u0027s native convention and the\r\n        /// production default. Empirically the pretrained checkpoint tracks arms FAR\r\n        /// better against the clip\u0027s first frame than against a synthesized true T-pose,\r\n        /// even though its training references are T-poses (measured on the fixture clip:\r\n        /// mean role cosine vs the geometric solver 0.94 first-frame vs 0.57 rest-pose,\r\n        /// hands flipping negative \u2014 reproduced identically in the Python reference\r\n        /// pipeline, so it is a property of the checkpoint, not of this port).\u003C/summary\u003E\r\n        FirstFrame,\r\n\r\n        /// \u003Csummary\u003ESynthesize the reference from the skeleton\u0027s rest pose (the\r\n        /// FEASIBILITY suggestion; kept for experiments \u2014 see above for why it lost).\u003C/summary\u003E\r\n        RestPose,\r\n    }\r\n\r\n    /// \u003Csummary\u003EOptions for \u003Csee cref=\u0022BuildSourceGraph\u0022/\u003E; defaults are production mode.\u003C/summary\u003E\r\n    public sealed class SourceOptions\r\n    {\r\n        /// \u003Csummary\u003ET-pose reference choice (see \u003Csee cref=\u0022TposeReference\u0022/\u003E).\u003C/summary\u003E\r\n        public TposeReference Reference { get; init; } = TposeReference.FirstFrame;\r\n\r\n        /// \u003Csummary\u003ESAME\u0027s native frame accounting: the first frame is consumed as the\r\n        /// reference and the next dropped for its undefined velocity, so the output has\r\n        /// two frames fewer than the clip. Golden-parity tests only \u2014 production emits\r\n        /// one feature frame per clip frame (the first frame doubles as the reference\r\n        /// and gets zero velocity).\u003C/summary\u003E\r\n        public bool NativeFrameDrop { get; init; }\r\n\r\n        /// \u003Csummary\u003EApply the rest-geometry world alignment (Y-up, \u002BZ facing). Disabled\r\n        /// only by golden-parity tests (Python applies none).\u003C/summary\u003E\r\n        public bool Align { get; init; } = true;\r\n\r\n        /// \u003Csummary\u003EGround both the T-pose reference and the animation: the T-pose is\r\n        /// shifted so its lowest joint sits at height 0 (a BVH rest pose has its root at\r\n        /// the origin and would otherwise put the hips on the floor), and the animation is\r\n        /// shifted by its own lowest joint height over the clip (no-op for the usual\r\n        /// authored-ground-at-0 data). Disabled only by golden-parity tests (the Python\r\n        /// reference consumes data as authored).\u003C/summary\u003E\r\n        public bool GroundShift { get; init; } = true;\r\n    }\r\n\r\n    private const float ContactHeightCm = 5f;\r\n    private const float ContactSpeedMps = 0.4f;\r\n    private const float VelocityFps = 30f;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Builds the batched source graph for one clip: graph selection, alignment, per-frame\r\n    /// features, normalization, edges.\r\n    /// \u003C/summary\u003E\r\n    /// \u003Cparam name=\u0022scene\u0022\u003EImported source (cm, native axes).\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022clipIndex\u0022\u003EClip to encode.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022map\u0022\u003ESource mapping; used only for hips identification and the\r\n    /// rest-geometry alignment (the model itself is skeleton-agnostic). May be sparse \u2014\r\n    /// heuristics cover missing roles.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022stats\u0022\u003ENormalization statistics.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022options\u0022\u003ENull = production mode.\u003C/param\u003E\r\n    public static SameSourceGraph BuildSourceGraph(\r\n        SourceScene scene, int clipIndex, MappingResult? map, SameStats stats, SourceOptions? options = null)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(scene);\r\n        ArgumentNullException.ThrowIfNull(stats);\r\n        options ??= new SourceOptions();\r\n        if (clipIndex \u003C 0 || clipIndex \u003E= scene.Clips.Count)\r\n            throw new ArgumentOutOfRangeException(nameof(clipIndex));\r\n        var clip = scene.Clips[clipIndex];\r\n        if (clip.FrameCount \u003C 1)\r\n            throw new ArgumentException(\u0022Clip has no frames.\u0022, nameof(clipIndex));\r\n        if (options.NativeFrameDrop \u0026\u0026 clip.FrameCount \u003C 3)\r\n            throw new ArgumentException(\u0022Native frame accounting needs at least 3 frames.\u0022, nameof(options));\r\n\r\n        var skeleton = scene.Skeleton;\r\n        var hips = FindHips(skeleton, map);\r\n        var nodes = GraphNodes.Build(skeleton, hips);\r\n\r\n        var align = options.Align ? ComputeAlignment(skeleton, map, scene) : Quaternion.Identity;\r\n\r\n        // T-pose reference world transforms (aligned), grounded on its own lowest joint\r\n        // (a BVH rest pose has the root at the origin \u2014 ungrounded, its hips would sit on\r\n        // the floor and every height-bearing feature would be wrong).\r\n        var tposeLocals = options.Reference == TposeReference.RestPose\r\n            ? Pose.Rest(skeleton).Locals\r\n            : clip.Frames[0];\r\n        var tposeWorld = AlignedWorld(skeleton, tposeLocals, align, nodes);\r\n        if (options.GroundShift)\r\n            ShiftToGround(tposeWorld.Pos);\r\n\r\n        // The pose sequence the features run over; features are emitted for seq[1..].\r\n        var seq = new List\u003CXForm[]\u003E();\r\n        if (options.NativeFrameDrop)\r\n        {\r\n            for (var f = 1; f \u003C clip.FrameCount; f\u002B\u002B)\r\n                seq.Add(clip.Frames[f]);\r\n        }\r\n        else\r\n        {\r\n            seq.Add(clip.Frames[0]); // duplicated: gives the real first frame zero velocity\r\n            for (var f = 0; f \u003C clip.FrameCount; f\u002B\u002B)\r\n                seq.Add(clip.Frames[f]);\r\n        }\r\n\r\n        var frames = seq.Count - 1;\r\n        var j = nodes.Count;\r\n\r\n        // Pass 0: aligned world transforms; ground the whole clip on its lowest joint.\r\n        var worlds = new AlignedFrame[seq.Count];\r\n        for (var t = 0; t \u003C seq.Count; t\u002B\u002B)\r\n            worlds[t] = AlignedWorld(skeleton, seq[t], align, nodes);\r\n        if (options.GroundShift)\r\n        {\r\n            var ground = float.PositiveInfinity;\r\n            foreach (var world in worlds)\r\n            {\r\n                foreach (var p in world.Pos)\r\n                    ground = MathF.Min(ground, p.Y);\r\n            }\r\n            if (float.IsFinite(ground) \u0026\u0026 ground != 0f)\r\n            {\r\n                foreach (var world in worlds)\r\n                {\r\n                    for (var i = 0; i \u003C j; i\u002B\u002B)\r\n                        world.Pos[i].Y -= ground;\r\n                }\r\n            }\r\n        }\r\n\r\n        // Pass 1: normalized-skeleton local rotations \u002B facing per frame.\r\n        var localRots = new Quaternion[seq.Count][]; // facing-adjusted at the root row\r\n        var facing = new (float Yaw, Vector3 Pos)[seq.Count];\r\n        for (var t = 0; t \u003C seq.Count; t\u002B\u002B)\r\n        {\r\n            var world = worlds[t];\r\n\r\n            // Normalized-skeleton world rotations: world delta from the T-pose.\r\n            var normWorld = new Quaternion[j];\r\n            for (var i = 0; i \u003C j; i\u002B\u002B)\r\n                normWorld[i] = MathQ.Normalize(world.Rot[i] * Quaternion.Conjugate(tposeWorld.Rot[i]));\r\n\r\n            // Root facing: yaw (about \u002BY) of the normalized root rotation, at the root\u0027s\r\n            // ground-plane position.\r\n            var yaw = YawAngle(normWorld[0]);\r\n            facing[t] = (yaw, new Vector3(world.Pos[0].X, 0f, world.Pos[0].Z));\r\n\r\n            // Normalized-skeleton local rotations; root premultiplied by the inverse facing.\r\n            var locals = new Quaternion[j];\r\n            locals[0] = MathQ.Normalize(Quaternion.CreateFromAxisAngle(Vector3.UnitY, -yaw) * normWorld[0]);\r\n            for (var i = 1; i \u003C j; i\u002B\u002B)\r\n            {\r\n                locals[i] = MathQ.Normalize(\r\n                    Quaternion.Conjugate(normWorld[nodes.Parent[i]]) * normWorld[i]);\r\n            }\r\n            localRots[t] = locals;\r\n        }\r\n\r\n        // Pass 2: feature rows.\r\n        var x = new float[frames * j * SameModel.InputDim];\r\n        for (var t = 1; t \u003C seq.Count; t\u002B\u002B)\r\n        {\r\n            var f = t - 1;\r\n            var (yaw, fpos) = facing[t];\r\n            var invFacing = Quaternion.CreateFromAxisAngle(Vector3.UnitY, -yaw);\r\n            var (yawPrev, fposPrev) = facing[t - 1];\r\n            var invFacingPrev = Quaternion.CreateFromAxisAngle(Vector3.UnitY, -yawPrev);\r\n\r\n            // r: facing delta (d\u03B8, dx, dz) \u002B absolute root height.\r\n            var dTheta = WrapPi(yaw - yawPrev);\r\n            var dPlanar = Vector3.Transform(fpos - fposPrev, invFacingPrev);\r\n            var rootHeight = worlds[t].Pos[0].Y;\r\n\r\n            for (var i = 0; i \u003C j; i\u002B\u002B)\r\n            {\r\n                var row = (f * j \u002B i) * SameModel.InputDim;\r\n                var col = 0;\r\n\r\n                // ---- skel: lo, go (tiled per frame) -------------------------------------\r\n                Vector3 lo, go;\r\n                if (i == 0)\r\n                {\r\n                    lo = new Vector3(0f, tposeWorld.Pos[0].Y, 0f);\r\n                    go = lo;\r\n                }\r\n                else\r\n                {\r\n                    lo = tposeWorld.Pos[i] - tposeWorld.Pos[nodes.Parent[i]];\r\n                    go = tposeWorld.Pos[i] - new Vector3(tposeWorld.Pos[0].X, 0f, tposeWorld.Pos[0].Z);\r\n                }\r\n                WriteNorm3(x, row, ref col, lo, stats.LoM, stats.LoS);\r\n                WriteNorm3(x, row, ref col, go, stats.GoM, stats.GoS);\r\n\r\n                // ---- q ------------------------------------------------------------------\r\n                WriteNorm6(x, row, ref col, SixD(localRots[t][i]), stats.QM, stats.QS);\r\n\r\n                // ---- p (facing-frame-relative global position) --------------------------\r\n                var p = Vector3.Transform(worlds[t].Pos[i] - fpos, invFacing);\r\n                WriteNorm3(x, row, ref col, p, stats.PM, stats.PS);\r\n\r\n                // ---- r (root row only; other rows are the mean \u2192 zeros after norm) ------\r\n                if (i == 0)\r\n                {\r\n                    x[row \u002B col\u002B\u002B] = (dTheta - stats.RM[0]) / stats.RS[0];\r\n                    x[row \u002B col\u002B\u002B] = (dPlanar.X - stats.RM[1]) / stats.RS[1];\r\n                    x[row \u002B col\u002B\u002B] = (dPlanar.Z - stats.RM[2]) / stats.RS[2];\r\n                    x[row \u002B col\u002B\u002B] = (rootHeight - stats.RM[3]) / stats.RS[3];\r\n                }\r\n                else\r\n                {\r\n                    col \u002B= 4; // already zero\r\n                }\r\n\r\n                // ---- pv (facing-frame velocity, \u00D730 fps) ---------------------------------\r\n                var pv = Vector3.Transform(worlds[t].Pos[i] - worlds[t - 1].Pos[i], invFacing) * VelocityFps;\r\n                WriteNorm3(x, row, ref col, pv, stats.PvM, stats.PvS);\r\n\r\n                // ---- qv (local rotation delta) -------------------------------------------\r\n                var qv = MathQ.Normalize(Quaternion.Conjugate(localRots[t - 1][i]) * localRots[t][i]);\r\n                WriteNorm6(x, row, ref col, SixD(qv), stats.QvM, stats.QvS);\r\n\r\n                // ---- pprev (previous position in the CURRENT facing frame) ---------------\r\n                var pprev = Vector3.Transform(worlds[t - 1].Pos[i] - fpos, invFacing);\r\n                WriteNorm3(x, row, ref col, pprev, stats.PprevM, stats.PprevS);\r\n\r\n                // ---- c (ground contact; not normalized) -----------------------------------\r\n                var speedMps = (worlds[t].Pos[i] - worlds[t - 1].Pos[i]).Length() * VelocityFps / 100f;\r\n                x[row \u002B col] = worlds[t].Pos[i].Y \u003C ContactHeightCm \u0026\u0026 speedMps \u003C ContactSpeedMps ? 1f : 0f;\r\n            }\r\n        }\r\n\r\n        var (edgeSrc, edgeDst) = BuildEdges(nodes.Parent, frames);\r\n        var batch = new int[frames * j];\r\n        for (var f = 0; f \u003C frames; f\u002B\u002B)\r\n        {\r\n            for (var i = 0; i \u003C j; i\u002B\u002B)\r\n                batch[f * j \u002B i] = f;\r\n        }\r\n\r\n        AssertFinite(x, \u0022SAME source features\u0022);\r\n        return new SameSourceGraph\r\n        {\r\n            X = x,\r\n            EdgeSrc = edgeSrc,\r\n            EdgeDst = edgeDst,\r\n            Batch = batch,\r\n            FrameCount = frames,\r\n            JointCount = j,\r\n            JointNames = nodes.Names,\r\n        };\r\n    }\r\n\r\n    // ================================================================ graph topology\r\n\r\n    /// \u003Csummary\u003EThe per-frame graph node set: hips-subtree bones in skeleton order\r\n    /// (hips first) plus synthesized end joints for childless leaves.\u003C/summary\u003E\r\n    internal sealed class GraphNodes\r\n    {\r\n        /// \u003Csummary\u003ESkeleton bone index per node; -1 for synthesized end joints.\u003C/summary\u003E\r\n        public required int[] Bone { get; init; }\r\n\r\n        /// \u003Csummary\u003EGraph-parent node index; -1 for the root (node 0).\u003C/summary\u003E\r\n        public required int[] Parent { get; init; }\r\n\r\n        /// \u003Csummary\u003EFor synthesized end joints: the rest-local offset from the leaf bone\r\n        /// (zero vector for real bones).\u003C/summary\u003E\r\n        public required Vector3[] EndOffset { get; init; }\r\n\r\n        public required string[] Names { get; init; }\r\n\r\n        public int Count =\u003E Bone.Length;\r\n\r\n        public static GraphNodes Build(SkeletonModel skeleton, int hips)\r\n        {\r\n            // Hips subtree, skeleton order (parents precede children, hips first).\r\n            var inSubtree = new bool[skeleton.Count];\r\n            inSubtree[hips] = true;\r\n            var bones = new List\u003Cint\u003E { hips };\r\n            for (var i = hips \u002B 1; i \u003C skeleton.Count; i\u002B\u002B)\r\n            {\r\n                var parent = skeleton[i].ParentIndex;\r\n                if (parent \u003E= 0 \u0026\u0026 inSubtree[parent])\r\n                {\r\n                    inSubtree[i] = true;\r\n                    bones.Add(i);\r\n                }\r\n            }\r\n\r\n            var nodeOfBone = new Dictionary\u003Cint, int\u003E(bones.Count);\r\n            for (var n = 0; n \u003C bones.Count; n\u002B\u002B)\r\n                nodeOfBone[bones[n]] = n;\r\n\r\n            var hasChild = new bool[skeleton.Count];\r\n            foreach (var b in bones)\r\n            {\r\n                var parent = skeleton[b].ParentIndex;\r\n                if (parent \u003E= 0 \u0026\u0026 inSubtree[parent])\r\n                    hasChild[parent] = true;\r\n            }\r\n\r\n            var bone = new List\u003Cint\u003E(bones);\r\n            var parentNode = new List\u003Cint\u003E(bones.Count);\r\n            var endOffset = new List\u003CVector3\u003E(bones.Count);\r\n            var names = new List\u003Cstring\u003E(bones.Count);\r\n            foreach (var b in bones)\r\n            {\r\n                var p = skeleton[b].ParentIndex;\r\n                parentNode.Add(b == hips ? -1 : nodeOfBone[p]);\r\n                endOffset.Add(Vector3.Zero);\r\n                names.Add(skeleton[b].Name);\r\n            }\r\n\r\n            // Synthesized end joints: leaves with no children anywhere in the skeleton.\r\n            // BVH End Sites already import as real \u0060_end\u0060/\u0060_End\u0060 bones and ARE the end\r\n            // joints \u2014 no tip on a tip. The tip continues the parent\u2192leaf segment at half\r\n            // length \u2014 a neutral stand-in for the unknown bone tail (FBX carries none).\r\n            foreach (var b in bones)\r\n            {\r\n                if (hasChild[b]\r\n                    || skeleton[b].Name.EndsWith(\u0022_end\u0022, StringComparison.OrdinalIgnoreCase))\r\n                    continue;\r\n                var p = skeleton[b].ParentIndex;\r\n                var segment = p \u003E= 0\r\n                    ? skeleton.RestWorld[b].Pos - skeleton.RestWorld[p].Pos\r\n                    : Vector3.Zero;\r\n                var tip = segment.Length() \u003E 1e-4f ? segment * 0.5f : new Vector3(0f, 2f, 0f);\r\n                // Express in the leaf\u0027s rest-local frame (applied via the leaf\u0027s world rot).\r\n                var local = Vector3.Transform(tip, Quaternion.Conjugate(skeleton.RestWorld[b].Rot));\r\n                bone.Add(-1);\r\n                parentNode.Add(nodeOfBone[b]);\r\n                endOffset.Add(local);\r\n                names.Add(skeleton[b].Name \u002B \u0022_end\u0022);\r\n            }\r\n\r\n            return new GraphNodes\r\n            {\r\n                Bone = bone.ToArray(),\r\n                Parent = parentNode.ToArray(),\r\n                EndOffset = endOffset.ToArray(),\r\n                Names = names.ToArray(),\r\n            };\r\n        }\r\n    }\r\n\r\n    /// \u003Csummary\u003EAligned world transforms of the graph nodes for one pose.\u003C/summary\u003E\r\n    internal readonly struct AlignedFrame\r\n    {\r\n        public required Vector3[] Pos { get; init; }\r\n        public required Quaternion[] Rot { get; init; }\r\n    }\r\n\r\n    private static AlignedFrame AlignedWorld(\r\n        SkeletonModel skeleton, XForm[] locals, Quaternion align, GraphNodes nodes)\r\n    {\r\n        var world = new Pose(locals).ToWorld(skeleton);\r\n        var pos = new Vector3[nodes.Count];\r\n        var rot = new Quaternion[nodes.Count];\r\n        for (var n = 0; n \u003C nodes.Count; n\u002B\u002B)\r\n        {\r\n            XForm w;\r\n            if (nodes.Bone[n] \u003E= 0)\r\n            {\r\n                w = world[nodes.Bone[n]];\r\n            }\r\n            else\r\n            {\r\n                // Synthesized end joint: rides its leaf bone (identity local rotation).\r\n                var leaf = world[nodes.Bone[nodes.Parent[n]]];\r\n                w = new XForm(leaf.TransformPoint(nodes.EndOffset[n]), leaf.Rot);\r\n            }\r\n            pos[n] = Vector3.Transform(w.Pos, align);\r\n            rot[n] = MathQ.Normalize(align * w.Rot);\r\n        }\r\n        return new AlignedFrame { Pos = pos, Rot = rot };\r\n    }\r\n\r\n    /// \u003Csummary\u003EBidirectional parent\u2194child pairs plus one self-loop per node, replicated\r\n    /// per frame with node indices offset.\u003C/summary\u003E\r\n    internal static (int[] Src, int[] Dst) BuildEdges(int[] parent, int frames)\r\n    {\r\n        var j = parent.Length;\r\n        var nonRoot = 0;\r\n        for (var i = 0; i \u003C j; i\u002B\u002B)\r\n        {\r\n            if (parent[i] \u003E= 0)\r\n                nonRoot\u002B\u002B;\r\n        }\r\n        var perFrame = nonRoot * 2 \u002B j;\r\n        var src = new int[perFrame * frames];\r\n        var dst = new int[perFrame * frames];\r\n        var e = 0;\r\n        for (var f = 0; f \u003C frames; f\u002B\u002B)\r\n        {\r\n            var offset = f * j;\r\n            for (var i = 0; i \u003C j; i\u002B\u002B)\r\n            {\r\n                if (parent[i] \u003C 0)\r\n                    continue;\r\n                src[e] = offset \u002B parent[i];\r\n                dst[e] = offset \u002B i;\r\n                e\u002B\u002B;\r\n                src[e] = offset \u002B i;\r\n                dst[e] = offset \u002B parent[i];\r\n                e\u002B\u002B;\r\n            }\r\n            for (var i = 0; i \u003C j; i\u002B\u002B)\r\n            {\r\n                src[e] = offset \u002B i;\r\n                dst[e] = offset \u002B i;\r\n                e\u002B\u002B;\r\n            }\r\n        }\r\n        return (src, dst);\r\n    }\r\n\r\n    // ================================================================ alignment \u002B hips\r\n\r\n    /// \u003Csummary\u003EMapped Hips role when available, else the shallowest bone with two or more\r\n    /// children (the hips of any humanoid: the legs/spine branch point).\u003C/summary\u003E\r\n    internal static int FindHips(SkeletonModel skeleton, MappingResult? map)\r\n    {\r\n        if (map is not null \u0026\u0026 map.RoleToBone.TryGetValue(BoneRole.Hips, out var mapped)\r\n            \u0026\u0026 mapped \u003E= 0 \u0026\u0026 mapped \u003C skeleton.Count)\r\n            return mapped;\r\n\r\n        var childCount = new int[skeleton.Count];\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n        {\r\n            if (skeleton[i].ParentIndex \u003E= 0)\r\n                childCount[skeleton[i].ParentIndex]\u002B\u002B;\r\n        }\r\n\r\n        var best = -1;\r\n        var bestDepth = int.MaxValue;\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n        {\r\n            if (childCount[i] \u003C 2)\r\n                continue;\r\n            var depth = 0;\r\n            for (var a = skeleton[i].ParentIndex; a \u003E= 0; a = skeleton[a].ParentIndex)\r\n                depth\u002B\u002B;\r\n            if (depth \u003C bestDepth)\r\n            {\r\n                best = i;\r\n                bestDepth = depth;\r\n            }\r\n        }\r\n        return best \u003E= 0 ? best : 0;\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// World rotation taking the rig into the canonical SAME frame (X = character left,\r\n    /// Y = up, Z = facing): rest-geometry character frame when computable from the mapping,\r\n    /// else the file\u0027s recorded axis conventions.\r\n    /// \u003C/summary\u003E\r\n    internal static Quaternion ComputeAlignment(SkeletonModel skeleton, MappingResult? map, SourceScene? scene)\r\n    {\r\n        if (map is not null)\r\n        {\r\n            try\r\n            {\r\n                var frame = CharacterFrame.Compute(skeleton, map, skeleton.RestWorld);\r\n                return AlignFromBasis(frame.Lateral, frame.Up, frame.Forward);\r\n            }\r\n            catch (ArgumentException)\r\n            {\r\n                // fall through to axis metadata\r\n            }\r\n        }\r\n\r\n        if (scene is not null)\r\n        {\r\n            var up = AxisVector(scene.UpAxis, scene.UpAxisSign);\r\n            var forward = AxisVector(scene.FrontAxis, scene.FrontAxisSign);\r\n            if (MathF.Abs(Vector3.Dot(up, forward)) \u003C 0.5f)\r\n                return AlignFromBasis(Vector3.Cross(up, forward), up, forward);\r\n        }\r\n\r\n        return Quaternion.Identity;\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Rotation mapping the given (left, up, forward) world directions onto (\u002BX, \u002BY, \u002BZ),\r\n    /// snapped to the nearest whole axis permutation when one is unambiguous: rigs authored\r\n    /// on exact axes (BVH Y-up/\u002BZ, the s\u0026amp;box rig, Z-up FBX) must map by an exact\r\n    /// quarter-turn \u2014 the few degrees of rest-geometry tilt (shoulders not exactly above\r\n    /// hips) otherwise leak into every feature and measurably cost accuracy.\r\n    /// \u003C/summary\u003E\r\n    internal static Quaternion AlignFromBasis(Vector3 left, Vector3 up, Vector3 forward)\r\n    {\r\n        var l = SnapAxis(left);\r\n        var u = SnapAxis(up);\r\n        var f = SnapAxis(forward);\r\n        if (MathF.Abs(Vector3.Dot(l, u)) \u003E 0.5f || MathF.Abs(Vector3.Dot(l, f)) \u003E 0.5f\r\n            || MathF.Abs(Vector3.Dot(u, f)) \u003E 0.5f)\r\n        {\r\n            // Genuinely oblique rig: keep the exact (orthonormalized) directions.\r\n            l = Vector3.Normalize(left);\r\n            u = Vector3.Normalize(up - l * Vector3.Dot(up, l));\r\n            f = Vector3.Cross(l, u);\r\n        }\r\n\r\n        // Row-major with rows = basis images maps \u002BX\u2192left, \u002BY\u2192up, \u002BZ\u2192forward\r\n        // (System.Numerics row-vector convention); the alignment is its inverse.\r\n        var m = new Matrix4x4(\r\n            l.X, l.Y, l.Z, 0f,\r\n            u.X, u.Y, u.Z, 0f,\r\n            f.X, f.Y, f.Z, 0f,\r\n            0f, 0f, 0f, 1f);\r\n        return Quaternion.Conjugate(MathQ.Normalize(Quaternion.CreateFromRotationMatrix(m)));\r\n    }\r\n\r\n    private static Vector3 SnapAxis(Vector3 v)\r\n    {\r\n        var ax = MathF.Abs(v.X);\r\n        var ay = MathF.Abs(v.Y);\r\n        var az = MathF.Abs(v.Z);\r\n        if (ax \u003E= ay \u0026\u0026 ax \u003E= az)\r\n            return new Vector3(MathF.Sign(v.X), 0f, 0f);\r\n        if (ay \u003E= az)\r\n            return new Vector3(0f, MathF.Sign(v.Y), 0f);\r\n        return new Vector3(0f, 0f, MathF.Sign(v.Z));\r\n    }\r\n\r\n    private static Vector3 AxisVector(int axis, int sign) =\u003E axis switch\r\n    {\r\n        0 =\u003E new Vector3(sign, 0f, 0f),\r\n        2 =\u003E new Vector3(0f, 0f, sign),\r\n        _ =\u003E new Vector3(0f, sign, 0f),\r\n    };\r\n\r\n    // ================================================================ small math\r\n\r\n    /// \u003Csummary\u003EThe yaw (rotation about \u002BY) closest to \u003Cparamref name=\u0022q\u0022/\u003E \u2014 fairmotion\u0027s\r\n    /// \u003Cc\u003EQ_closest(q, identity, \u002BY)\u003C/c\u003E, reproduced exactly for parity.\u003C/summary\u003E\r\n    internal static float YawAngle(Quaternion q)\r\n    {\r\n        var alpha = Math.Atan2(q.W, q.Y);\r\n        var theta1 = -2.0 * alpha \u002B Math.PI;\r\n        var theta2 = -2.0 * alpha - Math.PI;\r\n        var d1 = q.Y * Math.Sin(theta1 * 0.5) \u002B q.W * Math.Cos(theta1 * 0.5);\r\n        var d2 = q.Y * Math.Sin(theta2 * 0.5) \u002B q.W * Math.Cos(theta2 * 0.5);\r\n        return (float)(d1 \u003E d2 ? theta1 : theta2);\r\n    }\r\n\r\n    private static void ShiftToGround(Vector3[] positions)\r\n    {\r\n        var ground = float.PositiveInfinity;\r\n        foreach (var p in positions)\r\n            ground = MathF.Min(ground, p.Y);\r\n        if (!float.IsFinite(ground) || ground == 0f)\r\n            return;\r\n        for (var i = 0; i \u003C positions.Length; i\u002B\u002B)\r\n            positions[i].Y -= ground;\r\n    }\r\n\r\n    internal static float WrapPi(float angle)\r\n    {\r\n        while (angle \u003E MathF.PI)\r\n            angle -= 2f * MathF.PI;\r\n        while (angle \u003C -MathF.PI)\r\n            angle \u002B= 2f * MathF.PI;\r\n        return angle;\r\n    }\r\n\r\n    /// \u003Csummary\u003E6D rotation representation: the first two columns of the rotation matrix\r\n    /// (\u003Cc\u003ER\u00B7e_x\u003C/c\u003E then \u003Cc\u003ER\u00B7e_y\u003C/c\u003E).\u003C/summary\u003E\r\n    internal static (Vector3 C0, Vector3 C1) SixD(Quaternion q)\r\n        =\u003E (Vector3.Transform(Vector3.UnitX, q), Vector3.Transform(Vector3.UnitY, q));\r\n\r\n    private static void WriteNorm3(float[] x, int row, ref int col, Vector3 v, float[] m, float[] s)\r\n    {\r\n        x[row \u002B col\u002B\u002B] = (v.X - m[0]) / s[0];\r\n        x[row \u002B col\u002B\u002B] = (v.Y - m[1]) / s[1];\r\n        x[row \u002B col\u002B\u002B] = (v.Z - m[2]) / s[2];\r\n    }\r\n\r\n    private static void WriteNorm6(float[] x, int row, ref int col, (Vector3 C0, Vector3 C1) sixD, float[] m, float[] s)\r\n    {\r\n        x[row \u002B col\u002B\u002B] = (sixD.C0.X - m[0]) / s[0];\r\n        x[row \u002B col\u002B\u002B] = (sixD.C0.Y - m[1]) / s[1];\r\n        x[row \u002B col\u002B\u002B] = (sixD.C0.Z - m[2]) / s[2];\r\n        x[row \u002B col\u002B\u002B] = (sixD.C1.X - m[3]) / s[3];\r\n        x[row \u002B col\u002B\u002B] = (sixD.C1.Y - m[4]) / s[4];\r\n        x[row \u002B col\u002B\u002B] = (sixD.C1.Z - m[5]) / s[5];\r\n    }\r\n\r\n    internal static void AssertFinite(float[] values, string what)\r\n    {\r\n        foreach (var v in values)\r\n        {\r\n            if (!float.IsFinite(v))\r\n                throw new InvalidOperationException($\u0022{what} contain non-finite values.\u0022);\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"Code/HumanoidRetargeter/Formats/Fbx/FbxBinaryWriter.cs","FileName":"FbxBinaryWriter.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Buffers.Binary;\r\nusing System.IO;\r\nusing System.Text;\r\n\r\nnamespace HumanoidRetargeter.Formats.Fbx;\r\n\r\n/// \u003Csummary\u003E\r\n/// Serializes an \u003Csee cref=\u0022FbxNode\u0022/\u003E tree back to binary FBX (version 7400 layout \u2014\r\n/// u32 header fields, universally readable). The inverse of\r\n/// \u003Csee cref=\u0022FbxTokenizer.Parse\u0022/\u003E: a tree parsed from a 7.x binary file and written\r\n/// here re-parses to an identical tree (arrays are written uncompressed; zlib-encoded\r\n/// inputs therefore round-trip by VALUE, not byte-for-byte).\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// Used by \u003Csee cref=\u0022FbxBindPoseFixer\u0022/\u003E to persist repaired node transforms. The footer\r\n/// is written the way Blender\u0027s exporter does: a fixed 16-byte watermark (importers treat\r\n/// it as opaque), zero padding to a 16-byte boundary, the version echo, 120 zero bytes and\r\n/// the closing magic. The FBX SDK computes a content hash here, but every consumer we\r\n/// target (s\u0026amp;box, Blender, assimp) ignores it.\r\n/// \u003C/remarks\u003E\r\npublic static class FbxBinaryWriter\r\n{\r\n    private const uint Version = 7400;\r\n\r\n    private static readonly byte[] HeaderMagic =\r\n        \u0022Kaydara FBX Binary  \\0\\x1a\\0\u0022u8.ToArray();\r\n\r\n    // Blender\u0027s fbx_binary.py FOOT_ID \u002B closing magic bytes.\r\n    private static readonly byte[] FooterWatermark =\r\n    {\r\n        0xfa, 0xbc, 0xab, 0x09, 0xd0, 0xc8, 0xd4, 0x66, 0xb1, 0x76, 0xfb, 0x83, 0x1c, 0xf7, 0x26, 0x7e,\r\n    };\r\n\r\n    private static readonly byte[] FooterMagic =\r\n    {\r\n        0xf8, 0x5a, 0x8c, 0x6a, 0xde, 0xf5, 0xd9, 0x7e, 0xec, 0xe9, 0x0c, 0x6e, 0x0c, 0xc0, 0x00, 0x00,\r\n    };\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Serializes \u003Cparamref name=\u0022root\u0022/\u003E (a virtual root whose children are the top-level\r\n    /// document nodes, as produced by \u003Csee cref=\u0022FbxTokenizer.Parse\u0022/\u003E).\r\n    /// \u003C/summary\u003E\r\n    public static byte[] Write(FbxNode root)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(root);\r\n\r\n        using var ms = new MemoryStream();\r\n        ms.Write(HeaderMagic);\r\n        WriteU32(ms, Version);\r\n\r\n        foreach (var child in root.Children)\r\n            WriteNode(ms, child);\r\n        WriteNullRecord(ms);\r\n\r\n        WriteFooter(ms);\r\n        return ms.ToArray();\r\n    }\r\n\r\n    // ------------------------------------------------------------------ nodes\r\n\r\n    private static void WriteNode(MemoryStream ms, FbxNode node)\r\n    {\r\n        long headerAt = ms.Position;\r\n        // Placeholder header: endOffset, numProps, propListLen (patched after the body).\r\n        WriteU32(ms, 0);\r\n        WriteU32(ms, (uint)node.Properties.Count);\r\n        WriteU32(ms, 0);\r\n        var nameBytes = Encoding.ASCII.GetBytes(node.Name);\r\n        if (nameBytes.Length \u003E byte.MaxValue)\r\n            throw new FormatException($\u0022FBX write: node name too long ({node.Name.Length} chars).\u0022);\r\n        ms.WriteByte((byte)nameBytes.Length);\r\n        ms.Write(nameBytes);\r\n\r\n        long propsAt = ms.Position;\r\n        foreach (var p in node.Properties)\r\n            WriteProperty(ms, p, node.Name);\r\n        long propsLen = ms.Position - propsAt;\r\n\r\n        if (node.Children.Count \u003E 0)\r\n        {\r\n            foreach (var child in node.Children)\r\n                WriteNode(ms, child);\r\n            WriteNullRecord(ms);\r\n        }\r\n\r\n        long endAt = ms.Position;\r\n        ms.Position = headerAt;\r\n        WriteU32(ms, checked((uint)endAt));\r\n        WriteU32(ms, (uint)node.Properties.Count);\r\n        WriteU32(ms, checked((uint)propsLen));\r\n        ms.Position = endAt;\r\n    }\r\n\r\n    private static void WriteNullRecord(MemoryStream ms)\r\n    {\r\n        Span\u003Cbyte\u003E zeros = stackalloc byte[13];\r\n        zeros.Clear();\r\n        ms.Write(zeros);\r\n    }\r\n\r\n    // ------------------------------------------------------------------ properties\r\n\r\n    private static void WriteProperty(MemoryStream ms, object value, string owner)\r\n    {\r\n        switch (value)\r\n        {\r\n            case short y:\r\n                ms.WriteByte((byte)\u0027Y\u0027);\r\n                WriteI16(ms, y);\r\n                break;\r\n            case bool c:\r\n                ms.WriteByte((byte)\u0027C\u0027);\r\n                ms.WriteByte(c ? (byte)1 : (byte)0);\r\n                break;\r\n            case int i:\r\n                ms.WriteByte((byte)\u0027I\u0027);\r\n                WriteI32(ms, i);\r\n                break;\r\n            case float f:\r\n                ms.WriteByte((byte)\u0027F\u0027);\r\n                WriteF32(ms, f);\r\n                break;\r\n            case double d:\r\n                ms.WriteByte((byte)\u0027D\u0027);\r\n                WriteF64(ms, d);\r\n                break;\r\n            case long l:\r\n                ms.WriteByte((byte)\u0027L\u0027);\r\n                WriteI64(ms, l);\r\n                break;\r\n\r\n            case float[] fa:\r\n                WriteArrayHeader(ms, \u0027f\u0027, fa.Length, 4);\r\n                foreach (var x in fa)\r\n                    WriteF32(ms, x);\r\n                break;\r\n            case double[] da:\r\n                WriteArrayHeader(ms, \u0027d\u0027, da.Length, 8);\r\n                foreach (var x in da)\r\n                    WriteF64(ms, x);\r\n                break;\r\n            case long[] la:\r\n                WriteArrayHeader(ms, \u0027l\u0027, la.Length, 8);\r\n                foreach (var x in la)\r\n                    WriteI64(ms, x);\r\n                break;\r\n            case int[] ia:\r\n                WriteArrayHeader(ms, \u0027i\u0027, ia.Length, 4);\r\n                foreach (var x in ia)\r\n                    WriteI32(ms, x);\r\n                break;\r\n            case bool[] ba:\r\n                WriteArrayHeader(ms, \u0027b\u0027, ba.Length, 1);\r\n                foreach (var x in ba)\r\n                    ms.WriteByte(x ? (byte)1 : (byte)0);\r\n                break;\r\n\r\n            case string s:\r\n            {\r\n                ms.WriteByte((byte)\u0027S\u0027);\r\n                var bytes = Encoding.UTF8.GetBytes(s);\r\n                WriteU32(ms, (uint)bytes.Length);\r\n                ms.Write(bytes);\r\n                break;\r\n            }\r\n            case byte[] r:\r\n                ms.WriteByte((byte)\u0027R\u0027);\r\n                WriteU32(ms, (uint)r.Length);\r\n                ms.Write(r);\r\n                break;\r\n\r\n            default:\r\n                throw new FormatException(\r\n                    $\u0022FBX write: node \u0027{owner}\u0027: unsupported property CLR type {value?.GetType().Name ?? \u0022null\u0022}.\u0022);\r\n        }\r\n    }\r\n\r\n    private static void WriteArrayHeader(MemoryStream ms, char code, int count, int elemSize)\r\n    {\r\n        ms.WriteByte((byte)code);\r\n        WriteU32(ms, (uint)count);\r\n        WriteU32(ms, 0); // encoding 0 = uncompressed\r\n        WriteU32(ms, checked((uint)(count * elemSize)));\r\n    }\r\n\r\n    // ------------------------------------------------------------------ footer\r\n\r\n    private static void WriteFooter(MemoryStream ms)\r\n    {\r\n        ms.Write(FooterWatermark);\r\n\r\n        // Zero-pad so the version echo starts 16-aligned (Blender pads at least 1 byte).\r\n        int pad = (int)(16 - ms.Position % 16);\r\n        for (int i = 0; i \u003C pad; i\u002B\u002B)\r\n            ms.WriteByte(0);\r\n\r\n        WriteU32(ms, Version);\r\n        Span\u003Cbyte\u003E zeros = stackalloc byte[120];\r\n        zeros.Clear();\r\n        ms.Write(zeros);\r\n        ms.Write(FooterMagic);\r\n    }\r\n\r\n    // ------------------------------------------------------------------ primitives\r\n\r\n    private static void WriteU32(MemoryStream ms, uint v)\r\n    {\r\n        Span\u003Cbyte\u003E b = stackalloc byte[4];\r\n        BinaryPrimitives.WriteUInt32LittleEndian(b, v);\r\n        ms.Write(b);\r\n    }\r\n\r\n    private static void WriteI16(MemoryStream ms, short v)\r\n    {\r\n        Span\u003Cbyte\u003E b = stackalloc byte[2];\r\n        BinaryPrimitives.WriteInt16LittleEndian(b, v);\r\n        ms.Write(b);\r\n    }\r\n\r\n    private static void WriteI32(MemoryStream ms, int v)\r\n    {\r\n        Span\u003Cbyte\u003E b = stackalloc byte[4];\r\n        BinaryPrimitives.WriteInt32LittleEndian(b, v);\r\n        ms.Write(b);\r\n    }\r\n\r\n    private static void WriteI64(MemoryStream ms, long v)\r\n    {\r\n        Span\u003Cbyte\u003E b = stackalloc byte[8];\r\n        BinaryPrimitives.WriteInt64LittleEndian(b, v);\r\n        ms.Write(b);\r\n    }\r\n\r\n    private static void WriteF32(MemoryStream ms, float v)\r\n    {\r\n        Span\u003Cbyte\u003E b = stackalloc byte[4];\r\n        BinaryPrimitives.WriteSingleLittleEndian(b, v);\r\n        ms.Write(b);\r\n    }\r\n\r\n    private static void WriteF64(MemoryStream ms, double v)\r\n    {\r\n        Span\u003Cbyte\u003E b = stackalloc byte[8];\r\n        BinaryPrimitives.WriteDoubleLittleEndian(b, v);\r\n        ms.Write(b);\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"Code/HumanoidRetargeter/Mapping/ProfileLibrary.cs","FileName":"ProfileLibrary.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\nusing System.Collections.Generic;\r\n\r\nnamespace HumanoidRetargeter.Mapping;\r\n\r\n/// \u003Csummary\u003E\r\n/// Built-in preset profiles, embedded as C# data (the same data is written to\r\n/// \u003Cc\u003EAssets/humanoid_retargeter/profiles/*.json\u003C/c\u003E by a regenerate-and-diff test so the\r\n/// shipped JSON can never drift from the code).\r\n/// \u003C/summary\u003E\r\npublic static class ProfileLibrary\r\n{\r\n    /// \u003Csummary\u003EMixamo / Adobe rigs: \u003Cc\u003Emixamorig[N]:\u003C/c\u003E namespace, \u003Cc\u003ELeftArm\u003C/c\u003E /\r\n    /// \u003Cc\u003ELeftForeArm\u003C/c\u003E / \u003Cc\u003ELeftHandIndex1..3\u003C/c\u003E style names.\u003C/summary\u003E\r\n    public static Profile Mixamo { get; } = BuildMixamo();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Reallusion ActorCore / AccuRig / Character Creator rigs (\u003Cc\u003ECC_Base_*\u003C/c\u003E).\r\n    /// Empirical notes from \u003Cc\u003Eresearch/rig_actorcore.json\u003C/c\u003E:\r\n    /// \u003Clist type=\u0022bullet\u0022\u003E\r\n    /// \u003Citem\u003E\u003Cc\u003ECC_Base_Hip\u003C/c\u003E is the parent of BOTH \u003Cc\u003ECC_Base_Pelvis\u003C/c\u003E (leg branch) and\r\n    /// \u003Cc\u003ECC_Base_Waist\u003C/c\u003E (spine branch), i.e. the LCA of legs\u002Bspine and the true animated\r\n    /// hips root \u2192 it carries \u003Csee cref=\u0022BoneRole.Hips\u0022/\u003E; \u003Cc\u003ECC_Base_Pelvis\u003C/c\u003E is a\r\n    /// leg-branch intermediate and stays unmapped.\u003C/item\u003E\r\n    /// \u003Citem\u003EThe neck chain is \u003Cc\u003ECC_Base_NeckTwist01 \u2192 CC_Base_NeckTwist02 \u2192 CC_Base_Head\u003C/c\u003E;\r\n    /// despite the name, \u003Cc\u003ENeckTwist01\u003C/c\u003E IS the neck bone (there is no plain\r\n    /// \u003Cc\u003ECC_Base_Neck\u003C/c\u003E), so it is the \u003Csee cref=\u0022BoneRole.Neck\u0022/\u003E alias. NeckTwist02 is\r\n    /// left unmapped. All other Twist/ShareBone helpers are excluded (no aliases).\u003C/item\u003E\r\n    /// \u003Citem\u003E\u003Cc\u003ECC_Base_L_ToeBase\u003C/c\u003E is the toe role; the co-located\r\n    /// \u003Cc\u003ECC_Base_L_ToeBaseShareBone\u003C/c\u003E is a helper and must never be mapped.\u003C/item\u003E\r\n    /// \u003C/list\u003E\r\n    /// \u003C/summary\u003E\r\n    public static Profile ActorCoreCc { get; } = BuildActorCoreCc();\r\n\r\n    /// \u003Csummary\u003EUnreal Engine mannequin (UE4/UE5): \u003Cc\u003Epelvis\u003C/c\u003E, \u003Cc\u003Espine_01..05\u003C/c\u003E,\r\n    /// \u003Cc\u003Eclavicle_l\u003C/c\u003E, \u003Cc\u003Ethumb_01_l\u003C/c\u003E, UE5 \u003Cc\u003E*_metacarpal_*\u003C/c\u003E; \u003Cc\u003E*_twist_*\u003C/c\u003E\r\n    /// bones have no aliases and are never mapped.\u003C/summary\u003E\r\n    public static Profile UeMannequin { get; } = BuildUeMannequin();\r\n\r\n    /// \u003Csummary\u003ERokoko / Xsens style BVH rigs: plain \u003Cc\u003EHips\u003C/c\u003E/\u003Cc\u003ESpine..Spine4\u003C/c\u003E/\u003Cc\u003E\r\n    /// LeftArm|LeftUpperArm\u003C/c\u003E name variants, usually no fingers.\u003C/summary\u003E\r\n    public static Profile RokokoBvh { get; } = BuildRokokoBvh();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// SMPL body model family (AMASS exports, Meshcapade FBX rigs). Joint names per the\r\n    /// published model (vchoutas/smplx \u003Cc\u003Ejoint_names.py\u003C/c\u003E, Meshcapade wiki):\r\n    /// \u003Cc\u003Epelvis\u003C/c\u003E, sided \u003Cc\u003Ehip\u2192knee\u2192ankle\u2192foot\u003C/c\u003E legs (the \u0022hip\u0022 joint IS the thigh;\r\n    /// \u0022ankle\u0022 is the foot, \u0022foot\u0022 is the toe region) and \u003Cc\u003Ecollar\u2192shoulder\u2192elbow\u2192wrist\u003C/c\u003E\r\n    /// arms (\u0022shoulder\u0022 is the upper arm, \u0022wrist\u0022 is the hand; the \u003Cc\u003Ehand\u003C/c\u003E joint is a\r\n    /// finger stub and stays unmapped). Both spellings occur in the wild: \u003Cc\u003Eleft_hip\u003C/c\u003E\r\n    /// (model joints) and \u003Cc\u003EL_Hip\u003C/c\u003E with gendered FBX prefixes \u003Cc\u003Em_avg_\u003C/c\u003E/\u003Cc\u003Ef_avg_\u003C/c\u003E\r\n    /// (SMPL Unity/FBX rigs). No fingers \u2014 that is SMPL-X (\u003Csee cref=\u0022SmplX\u0022/\u003E), kept as a\r\n    /// separate preset so a finger-less SMPL rig still reaches full optional coverage.\r\n    /// \u003C/summary\u003E\r\n    public static Profile Smpl { get; } = BuildSmpl(withFingers: false);\r\n\r\n    /// \u003Csummary\u003E\r\n    /// SMPL-X: the SMPL body joints (\u003Csee cref=\u0022Smpl\u0022/\u003E) plus articulated hands \u2014\r\n    /// \u003Cc\u003Eleft_thumb1..3\u003C/c\u003E/\u003Cc\u003Eleft_index1..3\u003C/c\u003E-style finger joints per\r\n    /// vchoutas/smplx \u003Cc\u003Ejoint_names.py\u003C/c\u003E (jaw/eye joints carry no humanoid role).\r\n    /// Evaluated before \u003Csee cref=\u0022Smpl\u0022/\u003E so it wins the tie on SMPL-X rigs (both score\r\n    /// the body fully; only this one maps the fingers).\r\n    /// \u003C/summary\u003E\r\n    public static Profile SmplX { get; } = BuildSmpl(withFingers: true);\r\n\r\n    /// \u003Csummary\u003E\r\n    /// NVIDIA SOMA uniform-proportion skeleton (SOMA/SEED BVH exports, e.g.\r\n    /// github.com/NVIDIA/soma-retargeter \u003Cc\u003Eassets/motions/bvh\u003C/c\u003E). Mixamo-like upper-body\r\n    /// names, but: spine is \u003Cc\u003ESpine1\u2192Spine2\u2192Chest\u003C/c\u003E (no plain \u0022Spine\u0022), neck is\r\n    /// \u003Cc\u003ENeck1\u2192Neck2\u003C/c\u003E, the legs are \u003Cc\u003ELeftLeg\u2192LeftShin\u003C/c\u003E \u2014 SOMA\u0027s \u003Cc\u003ELeftLeg\u003C/c\u003E is\r\n    /// the THIGH (mixamo\u0027s is the calf), which is exactly why the mixamo preset must never\r\n    /// claim these rigs \u2014 and the four fingers have FOUR segments where segment 1 is a\r\n    /// metacarpal (\u003Cc\u003ELeftHandIndex1..4\u003C/c\u003E; mixamo\u0027s 1..3 are the phalanges), so the\r\n    /// phalanx roles map to segments 2/3/4.\r\n    /// \u003C/summary\u003E\r\n    public static Profile SomaBvh { get; } = BuildSomaBvh();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Classic BVH / Character-Studio-friendly naming (MotionBuilder \u0022Export BVH to\r\n    /// Character Studio\u0022 convention, ACCAD-style mocap BVHs): \u003Cc\u003EHips\u003C/c\u003E,\r\n    /// \u003Cc\u003EChest[2..4]\u003C/c\u003E spine, arms \u003Cc\u003ECollar\u2192Shoulder\u2192Elbow\u2192Wrist\u003C/c\u003E (the \u0022Shoulder\u0022\r\n    /// is the upper arm) and legs \u003Cc\u003EHip\u2192Knee\u2192Ankle\u2192Toe\u003C/c\u003E (the sided \u0022Hip\u0022 is the\r\n    /// thigh). No fingers.\r\n    /// \u003C/summary\u003E\r\n    public static Profile ClassicBvh { get; } = BuildClassicBvh();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// 3ds Max Character Studio Biped rigs: every bone is \u0022\u0026lt;BipedName\u0026gt; \u0026lt;Part\u0026gt;\u0022\r\n    /// where the biped name defaults to \u003Cc\u003EBip01\u003C/c\u003E (3ds Max \u22642009) / \u003Cc\u003EBip001\u003C/c\u003E\r\n    /// (2010\u002B) per the Autodesk \u0022Naming the Biped\u0022 documentation; some exporters mangle\r\n    /// the spaces to underscores (\u003Cc\u003EBip01_L_Thigh\u003C/c\u003E), hence the \u003Cc\u003E^Bip\\d\u002B[ _]\u003C/c\u003E\r\n    /// namespace pattern (alias comparison is separator-insensitive, so \u0022L UpperArm\u0022 and\r\n    /// \u0022L_UpperArm\u0022 normalize identically). Sided bones use a bare mid-name \u003Cc\u003EL/R\u003C/c\u003E:\r\n    /// \u003Cc\u003EL Clavicle\u2192L UpperArm\u2192L Forearm\u2192L Hand\u003C/c\u003E arms,\r\n    /// \u003Cc\u003EL Thigh\u2192L Calf\u2192L Foot\u2192L Toe0\u003C/c\u003E legs. Fingers are numbered chains\r\n    /// \u003Cc\u003EL Finger0..4\u003C/c\u003E (0 = thumb) with phalanx segments \u003Cc\u003EFinger01/Finger02\u003C/c\u003E\r\n    /// etc. (MotionBuilder\u0027s \u00223ds Max Biped Template\u0022 characterization maps exactly these\r\n    /// names). The COM root \u003Cc\u003EBip01\u003C/c\u003E itself, \u003Cc\u003EFootsteps\u003C/c\u003E, toe segments\r\n    /// \u003Cc\u003EToe01/Toe02\u003C/c\u003E and \u003Cc\u003EHorseLink\u003C/c\u003E carry no aliases and are never mapped.\r\n    /// \u003C/summary\u003E\r\n    public static Profile Biped { get; } = BuildBiped();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// DAZ/Poser classic naming (Poser 4 era figures, DAZ Generation-4 V4/M4, Genesis 1/2,\r\n    /// MakeHuman\u0027s \u0022Poser/DAZ names\u0022 BVH export \u2014 verified against the local\r\n    /// \u003Cc\u003Edev/corpus/unknown_rigs/makehuman_cmu_03_03_dazNames.bvh\u003C/c\u003E): camel-case bones\r\n    /// with a lower-case \u003Cc\u003El\u003C/c\u003E/\u003Cc\u003Er\u003C/c\u003E side prefix \u2014 \u003Cc\u003Ehip\u003C/c\u003E (the translating\r\n    /// root), \u003Cc\u003Eabdomen[\u2192abdomen2]\u2192chest\u003C/c\u003E spine, \u003Cc\u003Eneck\u003C/c\u003E, \u003Cc\u003Ehead\u003C/c\u003E,\r\n    /// \u003Cc\u003ElCollar\u2192lShldr\u2192lForeArm\u2192lHand\u003C/c\u003E arms, \u003Cc\u003ElThigh\u2192lShin\u2192lFoot\u2192lToe\u003C/c\u003E legs and\r\n    /// \u003Cc\u003ElThumb1..3/lIndex1..3/lMid1..3/lRing1..3/lPinky1..3\u003C/c\u003E fingers. The\r\n    /// \u003Cc\u003El/rButtock\u003C/c\u003E thigh helpers and eye bones carry no aliases and stay unmapped.\r\n    /// DAZ Genesis 3/8/9 renamed the skeleton (\u003Cc\u003EabdomenLower\u003C/c\u003E, \u003Cc\u003ElShldrBend\u003C/c\u003E, \u2026)\r\n    /// and is NOT covered by this preset.\r\n    /// \u003C/summary\u003E\r\n    public static Profile DazPoser { get; } = BuildDazPoser();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Blender Rigify human rigs, per the metarig definition in the rigify add-on\r\n    /// (\u003Cc\u003Erigify/metarigs/human.py\u003C/c\u003E) and the Blender manual\u0027s basic.human reference:\r\n    /// the spine chain is \u003Cc\u003Espine\u2192spine.001..spine.006\u003C/c\u003E where \u003Cc\u003Espine\u003C/c\u003E IS the\r\n    /// pelvis/hips bone (it sits at the pelvis and parents the thighs), spine.001\u2013003 are\r\n    /// the torso, spine.004/005 the two neck bones (004 carries \u003Csee cref=\u0022BoneRole.Neck\u0022/\u003E,\r\n    /// 005 stays unmapped \u2014 same policy as ActorCore\u0027s NeckTwist02) and spine.006 is the\r\n    /// head. Limbs: \u003Cc\u003Eshoulder.L\u2192upper_arm.L\u2192forearm.L\u2192hand.L\u003C/c\u003E,\r\n    /// \u003Cc\u003Ethigh.L\u2192shin.L\u2192foot.L\u2192toe.L\u003C/c\u003E; fingers \u003Cc\u003Ethumb.01.L..03.L\u003C/c\u003E and\r\n    /// \u003Cc\u003Ef_index/f_middle/f_ring/f_pinky.01.L..03.L\u003C/c\u003E. The \u003Cc\u003E^DEF-\u003C/c\u003E namespace\r\n    /// pattern also matches rigify\u0027s generated deform skeleton (\u003Cc\u003EDEF-spine.001\u003C/c\u003E,\r\n    /// \u003Cc\u003EDEF-upper_arm.L\u003C/c\u003E, \u2026); the segmented deform twins (\u003Cc\u003EDEF-upper_arm.L.001\u003C/c\u003E),\r\n    /// \u003Cc\u003Epalm.*\u003C/c\u003E, \u003Cc\u003Epelvis.L/R\u003C/c\u003E, \u003Cc\u003Eheel.02.L\u003C/c\u003E, face bones and the generated\r\n    /// ORG-/MCH-/control bones have no aliases and are never mapped.\r\n    /// \u003C/summary\u003E\r\n    public static Profile Rigify { get; } = BuildRigify();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// VRoid Studio / VRM avatars (UniVRM exports): \u003Cc\u003EJ_Bip_\u0026lt;side\u0026gt;_\u0026lt;Part\u0026gt;\u003C/c\u003E\r\n    /// bones where side is \u003Cc\u003EC\u003C/c\u003E (center), \u003Cc\u003EL\u003C/c\u003E or \u003Cc\u003ER\u003C/c\u003E \u2014 the standard VRoid\r\n    /// skeleton behind the VRM humanoid spec (vrm-c/vrm-specification, humanoid bone map):\r\n    /// \u003Cc\u003EJ_Bip_C_Hips/Spine/Chest/UpperChest/Neck/Head\u003C/c\u003E,\r\n    /// \u003Cc\u003EJ_Bip_L_Shoulder\u2192UpperArm\u2192LowerArm\u2192Hand\u003C/c\u003E,\r\n    /// \u003Cc\u003EJ_Bip_L_UpperLeg\u2192LowerLeg\u2192Foot\u2192ToeBase\u003C/c\u003E, fingers\r\n    /// \u003Cc\u003EJ_Bip_L_Thumb1..3/Index1..3/Middle1..3/Ring1..3/Little1..3\u003C/c\u003E (\u0022Little\u0022 is the\r\n    /// pinky, per the VRM littleProximal/Intermediate/Distal humanoid bones). Secondary\r\n    /// physics/adjust bones (\u003Cc\u003EJ_Sec_*\u003C/c\u003E, \u003Cc\u003EJ_Adj_*\u003C/c\u003E) and the \u003Cc\u003ERoot\u003C/c\u003E bone have\r\n    /// no aliases and are never mapped.\r\n    /// \u003C/summary\u003E\r\n    public static Profile Vrm { get; } = BuildVrm();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Blender Auto-Rig Pro humanoid FBX exports \u2014 bone names verified empirically against\r\n    /// the local user repro \u003Cc\u003Edev/corpus/todo/Defenses.fbx\u003C/c\u003E (the PunchPerfect family):\r\n    /// \u003Cc\u003E.x\u003C/c\u003E suffix marks center bones, \u003Cc\u003E.l/.r\u003C/c\u003E the sides, and the exported limb\r\n    /// deform bones carry the \u003Cc\u003E_stretch\u003C/c\u003E twin name \u2014 \u003Cc\u003Eroot.x\u003C/c\u003E is the hips\r\n    /// (under a ground bone \u003Cc\u003Eroot\u003C/c\u003E), \u003Cc\u003Espine_01.x\u2192spine_02.x\u2192spine_03.x\u003C/c\u003E,\r\n    /// \u003Cc\u003Eneck.x\u003C/c\u003E, \u003Cc\u003Ehead.x\u003C/c\u003E, arms \u003Cc\u003Eshoulder.l\u2192arm_stretch.l\u2192forearm_stretch.l\u2192\r\n    /// hand.l\u003C/c\u003E (plain \u0022arm\u0022, NOT \u0022upperarm\u0022), legs \u003Cc\u003Ethigh_stretch.l\u2192leg_stretch.l\u2192\r\n    /// foot.l\u2192toes_01.l\u003C/c\u003E (\u0022leg\u0022 is the calf). Fingers keep Auto-Rig Pro\u0027s \u003Cc\u003Ec_\u003C/c\u003E\r\n    /// control prefix on the exported deform chain: \u003Cc\u003Ec_thumb1.l..3.l\u003C/c\u003E,\r\n    /// \u003Cc\u003Ec_index/c_middle/c_ring/c_pinky1.l..3.l\u003C/c\u003E. Leftover finger-tip markers\r\n    /// (\u003Cc\u003Emixamorig:LeftHandIndex4\u003C/c\u003E in the repro) and \u003Cc\u003Eroot\u003C/c\u003E have no aliases.\r\n    /// \u003C/summary\u003E\r\n    public static Profile AutoRigPro { get; } = BuildAutoRigPro();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Xsens MVN exports (23-segment MVN body model; MVN Animate/Analyze FBX and BVH):\r\n    /// anatomical vertebra names for the spine chain \u003Cc\u003EPelvis\u2192L5\u2192L3\u2192T12\u2192T8\u003C/c\u003E (the four\r\n    /// exported lumbar/thoracic segments of the MVN model), \u003Cc\u003ENeck\u2192Head\u003C/c\u003E, arms\r\n    /// \u003Cc\u003ERightShoulder\u2192RightUpperArm\u2192RightForeArm\u2192RightHand\u003C/c\u003E and legs\r\n    /// \u003Cc\u003ERightUpperLeg\u2192RightLowerLeg\u2192RightFoot\u2192RightToe\u003C/c\u003E. Body-suit capture only \u2014 no\r\n    /// finger segments (Xsens gloves ship as separate data), so the hands are chain tips.\r\n    /// \u003C/summary\u003E\r\n    public static Profile XsensMvn { get; } = BuildXsensMvn();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Perception Neuron / Axis Neuron BVH exports: mixamo-like limb and finger names\r\n    /// (\u003Cc\u003ERightArm\u2192RightForeArm\u2192RightHand\u003C/c\u003E, \u003Cc\u003ERightUpLeg\u2192RightLeg\u2192RightFoot\u003C/c\u003E,\r\n    /// \u003Cc\u003ERightHandThumb1..3\u003C/c\u003E) but a FOUR-bone spine (\u003Cc\u003ESpine\u2192Spine1..Spine3\u003C/c\u003E), no\r\n    /// toe joints (the feet are chain tips), and per-finger \u003Cc\u003ERightInHandIndex\u003C/c\u003E-style\r\n    /// metacarpal helpers between the hand and the \u003Cc\u003ERightHandIndex1..3\u003C/c\u003E phalanges.\r\n    /// The InHand metacarpals carry no aliases (barely animated palm helpers; mapping them\r\n    /// as phalanges would shift every curl one joint outward \u2014 the SOMA finger bug class).\r\n    /// The extra Spine3 is what lets this preset outscore mixamo on Neuron rigs (and\r\n    /// mixamo\u0027s toes keep mixamo ahead on real Mixamo rigs).\r\n    /// \u003C/summary\u003E\r\n    public static Profile PerceptionNeuron { get; } = BuildPerceptionNeuron();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Source engine ValveBiped skeletons (HL2/GMod humanoids, playermodels):\r\n    /// \u003Cc\u003EValveBiped.Bip01_*\u003C/c\u003E names \u2014 3ds-Max-Biped-derived parts behind the fixed\r\n    /// namespace, with underscores and a spine chain that SKIPS Spine3\r\n    /// (\u003Cc\u003ESpine\u2192Spine1\u2192Spine2\u2192Spine4\u003C/c\u003E), \u003Cc\u003ENeck1\u003C/c\u003E/\u003Cc\u003EHead1\u003C/c\u003E, arms\r\n    /// \u003Cc\u003EL_Clavicle\u2192L_UpperArm\u2192L_Forearm\u2192L_Hand\u003C/c\u003E, legs\r\n    /// \u003Cc\u003EL_Thigh\u2192L_Calf\u2192L_Foot\u2192L_Toe0\u003C/c\u003E and numbered finger chains\r\n    /// \u003Cc\u003EL_Finger0/01/02\u003C/c\u003E (0 = thumb) \u2026 \u003Cc\u003EL_Finger4/41/42\u003C/c\u003E (pinky). Evaluated\r\n    /// before \u003Csee cref=\u0022Biped\u0022/\u003E (same Bip01 ancestry; the plain-Biped preset must never\r\n    /// claim a ValveBiped rig). Attachment/weapon helpers (\u003Cc\u003EValveBiped.forward\u003C/c\u003E,\r\n    /// \u003Cc\u003EValveBiped.Anim_Attachment_*\u003C/c\u003E) have no aliases and are never mapped.\r\n    /// \u003C/summary\u003E\r\n    public static Profile ValveBiped { get; } = BuildValveBiped();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// DAZ Genesis 3/8(.1) figures: renamed Genesis skeleton (NOT covered by\r\n    /// \u003Csee cref=\u0022DazPoser\u0022/\u003E) \u2014 \u003Cc\u003Ehip\u003C/c\u003E is the translating root and the LCA of the\r\n    /// \u003Cc\u003Epelvis\u003C/c\u003E leg branch and the \u003Cc\u003EabdomenLower\u2192abdomenUpper\u2192chestLower\u2192chestUpper\u003C/c\u003E\r\n    /// spine (so \u003Cc\u003Ehip\u003C/c\u003E carries \u003Csee cref=\u0022BoneRole.Hips\u0022/\u003E and \u003Cc\u003Epelvis\u003C/c\u003E stays\r\n    /// unmapped, same policy as ActorCore\u0027s Hip/Pelvis pair). Neck chain\r\n    /// \u003Cc\u003EneckLower\u2192neckUpper\u2192head\u003C/c\u003E (neckUpper unmapped, NeckTwist02 policy). Limbs use\r\n    /// Bend/Twist pairs: the Bend bones (\u003Cc\u003ElShldrBend\u003C/c\u003E, \u003Cc\u003ElForearmBend\u003C/c\u003E,\r\n    /// \u003Cc\u003ElThighBend\u003C/c\u003E) are the primary limb bones; the co-linear Twist roll helpers\r\n    /// (\u003Cc\u003ElShldrTwist\u003C/c\u003E, \u003Cc\u003ElForearmTwist\u003C/c\u003E, \u003Cc\u003ElThighTwist\u003C/c\u003E) carry no aliases and\r\n    /// are never mapped. Legs \u003Cc\u003ElThighBend\u2192lShin\u2192lFoot\u2192lToe\u003C/c\u003E (\u003Cc\u003ElMetatarsals\u003C/c\u003E is an\r\n    /// arch helper between foot and toe, unmapped). Fingers are the classic DAZ\r\n    /// \u003Cc\u003ElThumb1..3/lIndex1..3/lMid1..3/lRing1..3/lPinky1..3\u003C/c\u003E. Genesis 9 renamed the\r\n    /// skeleton again (\u003Cc\u003El_upperarm\u003C/c\u003E, \u2026) and is NOT covered by this preset.\r\n    /// \u003C/summary\u003E\r\n    public static Profile DazGenesis { get; } = BuildDazGenesis();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// The s\u0026amp;box citizen-family skeleton itself (\u003Cc\u003Ecitizen.vmdl\u003C/c\u003E,\r\n    /// \u003Cc\u003Ecitizen_human_*.vmdl\u003C/c\u003E and every community model re-rigged on their skeleton):\r\n    /// \u003Cc\u003Epelvis\u003C/c\u003E, \u003Cc\u003Espine_0..2\u003C/c\u003E, \u003Cc\u003Eneck_0\u003C/c\u003E, \u003Cc\u003Ehead\u003C/c\u003E, reversed-word limb\r\n    /// names \u003Cc\u003Earm_upper_L\u2192arm_lower_L\u2192hand_L\u003C/c\u003E / \u003Cc\u003Eleg_upper_L\u2192leg_lower_L\u2192\r\n    /// ankle_L\u2192ball_L\u003C/c\u003E, \u003Cc\u003Eclavicle_L/R\u003C/c\u003E and fingers\r\n    /// \u003Cc\u003Efinger_\u0026lt;name\u0026gt;_{meta,0,1,2}_L\u003C/c\u003E (meta = metacarpal, 0/1/2 =\r\n    /// proximal/middle/distal). The alias table mirrors\r\n    /// \u003Csee cref=\u0022Target.SboxBoneClassifier\u0022/\u003E\u0027s curated role table \u2014 kept in sync by a\r\n    /// test. This is what lets a COMPILED s\u0026amp;box model picked as a custom conversion\r\n    /// target (or used as a source) be recognized: the reversed word order\r\n    /// (\u003Cc\u003Earm_upper\u003C/c\u003E, not \u003Cc\u003Eupper_arm\u003C/c\u003E) defeats generic token matching, which\r\n    /// scored the citizen rig at 5% and made every custom-model pick fail with \u0022not\r\n    /// recognized as humanoid\u0022. Twist/helper/IK/face bones (\u003Cc\u003E*_twist*\u003C/c\u003E,\r\n    /// \u003Cc\u003E*_helper*\u003C/c\u003E, \u003Cc\u003Eeye_*\u003C/c\u003E, \u2026) have no aliases and are never mapped.\r\n    /// \u003C/summary\u003E\r\n    public static Profile Sbox { get; } = BuildSbox();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// AdvancedSkeleton (Maya auto-rigger, ubiquitous in game rips and mobile-game rigs):\r\n    /// \u003Cc\u003ERoot_M\u003C/c\u003E hips, \u003Cc\u003ESpine1_M(\u2192Spine2_M)\u2192Chest_M\u003C/c\u003E spine, \u003Cc\u003ENeck_M\u2192Head_M\u003C/c\u003E\r\n    /// (small rigs parent \u003Cc\u003EHead_M\u003C/c\u003E straight to the chest with no neck),\r\n    /// \u003Cc\u003EScapula\u2192Shoulder\u2192Elbow\u2192Wrist\u003C/c\u003E arms, \u003Cc\u003EHip\u2192Knee\u2192Ankle\u2192Toes\u003C/c\u003E legs and\r\n    /// \u003Cc\u003E\u0026lt;Name\u0026gt;Finger1..3\u003C/c\u003E fingers, all sided \u003Cc\u003E_L/_R\u003C/c\u003E (center \u003Cc\u003E_M\u003C/c\u003E).\r\n    /// Twist helpers (\u003Cc\u003EShoulderPart1\u003C/c\u003E, \u003Cc\u003EHipPart1\u003C/c\u003E, \u2026), \u003Cc\u003ECup\u003C/c\u003E palm bones and\r\n    /// the face rig carry no aliases. Real case: a Sonic mobile-game rip whose name stage\r\n    /// scored below threshold \u2014 the topology fallback then mapped ARMS AND LEGS ONTO THE\r\n    /// HEAD QUILLS (long symmetric chains), playing every clip as garbage.\r\n    /// \u003C/summary\u003E\r\n    public static Profile AdvancedSkeleton { get; } = BuildAdvancedSkeleton();\r\n\r\n    /// \u003Csummary\u003EAll built-in presets, in detection order (first wins score ties \u2014 see\r\n    /// \u003Csee cref=\u0022SmplX\u0022/\u003E vs \u003Csee cref=\u0022Smpl\u0022/\u003E; \u003Csee cref=\u0022ValveBiped\u0022/\u003E is evaluated\r\n    /// before \u003Csee cref=\u0022Biped\u0022/\u003E and \u003Csee cref=\u0022DazGenesis\u0022/\u003E before\r\n    /// \u003Csee cref=\u0022DazPoser\u0022/\u003E within their families).\u003C/summary\u003E\r\n    public static IReadOnlyList\u003CProfile\u003E All { get; } =\r\n        new[]\r\n        {\r\n            Sbox, Mixamo, ActorCoreCc, UeMannequin, XsensMvn, PerceptionNeuron, RokokoBvh,\r\n            SmplX, Smpl, SomaBvh, ClassicBvh, ValveBiped, Biped, DazGenesis, DazPoser,\r\n            Rigify, Vrm, AutoRigPro, AdvancedSkeleton,\r\n        };\r\n\r\n    // ---------------------------------------------------------------- advanced skeleton\r\n\r\n    private static Profile BuildAdvancedSkeleton()\r\n    {\r\n        var aliases = new Dictionary\u003CBoneRole, string[]\u003E\r\n        {\r\n            [BoneRole.Hips] = new[] { \u0022Root_M\u0022 },\r\n            [BoneRole.Spine0] = new[] { \u0022Spine1_M\u0022 },\r\n            [BoneRole.Spine1] = new[] { \u0022Spine2_M\u0022 },\r\n            [BoneRole.Spine2] = new[] { \u0022Chest_M\u0022 },\r\n            [BoneRole.Neck] = new[] { \u0022Neck_M\u0022 },\r\n            [BoneRole.Head] = new[] { \u0022Head_M\u0022 },\r\n        };\r\n        foreach (var side in new[] { \u0022L\u0022, \u0022R\u0022 })\r\n        {\r\n            aliases[Role(\u0022Clavicle\u0022, side)] = new[] { $\u0022Scapula_{side}\u0022 };\r\n            aliases[Role(\u0022UpperArm\u0022, side)] = new[] { $\u0022Shoulder_{side}\u0022 };\r\n            aliases[Role(\u0022LowerArm\u0022, side)] = new[] { $\u0022Elbow_{side}\u0022 };\r\n            aliases[Role(\u0022Hand\u0022, side)] = new[] { $\u0022Wrist_{side}\u0022 };\r\n            aliases[Role(\u0022UpperLeg\u0022, side)] = new[] { $\u0022Hip_{side}\u0022 };\r\n            aliases[Role(\u0022LowerLeg\u0022, side)] = new[] { $\u0022Knee_{side}\u0022 };\r\n            aliases[Role(\u0022Foot\u0022, side)] = new[] { $\u0022Ankle_{side}\u0022 };\r\n            aliases[Role(\u0022Toe\u0022, side)] = new[] { $\u0022Toes_{side}\u0022 };\r\n\r\n            foreach (var finger in new[] { \u0022Thumb\u0022, \u0022Index\u0022, \u0022Middle\u0022, \u0022Ring\u0022, \u0022Pinky\u0022 })\r\n            {\r\n                aliases[Role($\u0022{finger}Prox\u0022, side)] = new[] { $\u0022{finger}Finger1_{side}\u0022 };\r\n                aliases[Role($\u0022{finger}Mid\u0022, side)] = new[] { $\u0022{finger}Finger2_{side}\u0022 };\r\n                aliases[Role($\u0022{finger}Dist\u0022, side)] = new[] { $\u0022{finger}Finger3_{side}\u0022 };\r\n            }\r\n        }\r\n\r\n        return new Profile(\u0022advanced_skeleton\u0022, new string[0], aliases);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- sbox\r\n\r\n    private static Profile BuildSbox()\r\n    {\r\n        var aliases = new Dictionary\u003CBoneRole, string[]\u003E\r\n        {\r\n            [BoneRole.Hips] = new[] { \u0022pelvis\u0022 },\r\n            [BoneRole.Spine0] = new[] { \u0022spine_0\u0022 },\r\n            [BoneRole.Spine1] = new[] { \u0022spine_1\u0022 },\r\n            [BoneRole.Spine2] = new[] { \u0022spine_2\u0022 },\r\n            [BoneRole.Spine3] = new[] { \u0022spine_3\u0022 },\r\n            [BoneRole.Neck] = new[] { \u0022neck_0\u0022 },\r\n            [BoneRole.Head] = new[] { \u0022head\u0022 },\r\n        };\r\n        foreach (var side in new[] { \u0022L\u0022, \u0022R\u0022 })\r\n        {\r\n            aliases[Role(\u0022Clavicle\u0022, side)] = new[] { $\u0022clavicle_{side}\u0022 };\r\n            aliases[Role(\u0022UpperArm\u0022, side)] = new[] { $\u0022arm_upper_{side}\u0022 };\r\n            aliases[Role(\u0022LowerArm\u0022, side)] = new[] { $\u0022arm_lower_{side}\u0022 };\r\n            aliases[Role(\u0022Hand\u0022, side)] = new[] { $\u0022hand_{side}\u0022 };\r\n            aliases[Role(\u0022UpperLeg\u0022, side)] = new[] { $\u0022leg_upper_{side}\u0022 };\r\n            aliases[Role(\u0022LowerLeg\u0022, side)] = new[] { $\u0022leg_lower_{side}\u0022 };\r\n            aliases[Role(\u0022Foot\u0022, side)] = new[] { $\u0022ankle_{side}\u0022 };\r\n            aliases[Role(\u0022Toe\u0022, side)] = new[] { $\u0022ball_{side}\u0022 };\r\n\r\n            foreach (var (finger, rolePrefix) in new[]\r\n            {\r\n                (\u0022thumb\u0022, \u0022Thumb\u0022), (\u0022index\u0022, \u0022Index\u0022), (\u0022middle\u0022, \u0022Middle\u0022),\r\n                (\u0022ring\u0022, \u0022Ring\u0022), (\u0022pinky\u0022, \u0022Pinky\u0022),\r\n            })\r\n            {\r\n                aliases[Role($\u0022{rolePrefix}Meta\u0022, side)] = new[] { $\u0022finger_{finger}_meta_{side}\u0022 };\r\n                aliases[Role($\u0022{rolePrefix}Prox\u0022, side)] = new[] { $\u0022finger_{finger}_0_{side}\u0022 };\r\n                aliases[Role($\u0022{rolePrefix}Mid\u0022, side)] = new[] { $\u0022finger_{finger}_1_{side}\u0022 };\r\n                aliases[Role($\u0022{rolePrefix}Dist\u0022, side)] = new[] { $\u0022finger_{finger}_2_{side}\u0022 };\r\n            }\r\n        }\r\n\r\n        return new Profile(\u0022sbox\u0022, new string[0], aliases);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- mixamo\r\n\r\n    private static Profile BuildMixamo()\r\n    {\r\n        var aliases = new Dictionary\u003CBoneRole, string[]\u003E\r\n        {\r\n            [BoneRole.Hips] = new[] { \u0022Hips\u0022 },\r\n            [BoneRole.Spine0] = new[] { \u0022Spine\u0022 },\r\n            [BoneRole.Spine1] = new[] { \u0022Spine1\u0022 },\r\n            [BoneRole.Spine2] = new[] { \u0022Spine2\u0022 },\r\n            [BoneRole.Neck] = new[] { \u0022Neck\u0022 },\r\n            [BoneRole.Head] = new[] { \u0022Head\u0022 },\r\n        };\r\n        foreach (var (roleSide, nameSide) in Sides())\r\n        {\r\n            aliases[Role(\u0022Clavicle\u0022, roleSide)] = new[] { $\u0022{nameSide}Shoulder\u0022 };\r\n            aliases[Role(\u0022UpperArm\u0022, roleSide)] = new[] { $\u0022{nameSide}Arm\u0022 };\r\n            aliases[Role(\u0022LowerArm\u0022, roleSide)] = new[] { $\u0022{nameSide}ForeArm\u0022 };\r\n            aliases[Role(\u0022Hand\u0022, roleSide)] = new[] { $\u0022{nameSide}Hand\u0022 };\r\n            aliases[Role(\u0022UpperLeg\u0022, roleSide)] = new[] { $\u0022{nameSide}UpLeg\u0022 };\r\n            aliases[Role(\u0022LowerLeg\u0022, roleSide)] = new[] { $\u0022{nameSide}Leg\u0022 };\r\n            aliases[Role(\u0022Foot\u0022, roleSide)] = new[] { $\u0022{nameSide}Foot\u0022 };\r\n            aliases[Role(\u0022Toe\u0022, roleSide)] = new[] { $\u0022{nameSide}ToeBase\u0022 };\r\n\r\n            foreach (var finger in new[] { \u0022Thumb\u0022, \u0022Index\u0022, \u0022Middle\u0022, \u0022Ring\u0022, \u0022Pinky\u0022 })\r\n            {\r\n                aliases[Role($\u0022{finger}Prox\u0022, roleSide)] = new[] { $\u0022{nameSide}Hand{finger}1\u0022 };\r\n                aliases[Role($\u0022{finger}Mid\u0022, roleSide)] = new[] { $\u0022{nameSide}Hand{finger}2\u0022 };\r\n                aliases[Role($\u0022{finger}Dist\u0022, roleSide)] = new[] { $\u0022{nameSide}Hand{finger}3\u0022 };\r\n            }\r\n        }\r\n        // Both \u0027:\u0027 (FBX namespace) and \u0027_\u0027 (namespace mangled by some exporters) forms occur\r\n        // in the wild; some Mixamo downloads ship with no namespace at all, which still\r\n        // matches because the aliases are the bare names.\r\n        return new Profile(\u0022mixamo\u0022, new[] { \u0022^mixamorig[0-9]*:\u0022, \u0022^mixamorig[0-9]*_\u0022 }, aliases);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- actorcore / cc\r\n\r\n    private static Profile BuildActorCoreCc()\r\n    {\r\n        var aliases = new Dictionary\u003CBoneRole, string[]\u003E\r\n        {\r\n            [BoneRole.Hips] = new[] { \u0022Hip\u0022 },\r\n            [BoneRole.Spine0] = new[] { \u0022Waist\u0022 },\r\n            [BoneRole.Spine1] = new[] { \u0022Spine01\u0022 },\r\n            [BoneRole.Spine2] = new[] { \u0022Spine02\u0022 },\r\n            [BoneRole.Neck] = new[] { \u0022NeckTwist01\u0022 },\r\n            [BoneRole.Head] = new[] { \u0022Head\u0022 },\r\n        };\r\n        foreach (var roleSide in new[] { \u0022L\u0022, \u0022R\u0022 })\r\n        {\r\n            var nameSide = roleSide; // CC bones use the bare side letter: CC_Base_L_Thigh.\r\n            aliases[Role(\u0022Clavicle\u0022, roleSide)] = new[] { $\u0022{nameSide}_Clavicle\u0022 };\r\n            aliases[Role(\u0022UpperArm\u0022, roleSide)] = new[] { $\u0022{nameSide}_Upperarm\u0022 };\r\n            aliases[Role(\u0022LowerArm\u0022, roleSide)] = new[] { $\u0022{nameSide}_Forearm\u0022 };\r\n            aliases[Role(\u0022Hand\u0022, roleSide)] = new[] { $\u0022{nameSide}_Hand\u0022 };\r\n            aliases[Role(\u0022UpperLeg\u0022, roleSide)] = new[] { $\u0022{nameSide}_Thigh\u0022 };\r\n            aliases[Role(\u0022LowerLeg\u0022, roleSide)] = new[] { $\u0022{nameSide}_Calf\u0022 };\r\n            aliases[Role(\u0022Foot\u0022, roleSide)] = new[] { $\u0022{nameSide}_Foot\u0022 };\r\n            aliases[Role(\u0022Toe\u0022, roleSide)] = new[] { $\u0022{nameSide}_ToeBase\u0022 };\r\n\r\n            foreach (var (role, cc) in new[]\r\n            {\r\n                (\u0022Thumb\u0022, \u0022Thumb\u0022), (\u0022Index\u0022, \u0022Index\u0022), (\u0022Middle\u0022, \u0022Mid\u0022), (\u0022Ring\u0022, \u0022Ring\u0022), (\u0022Pinky\u0022, \u0022Pinky\u0022),\r\n            })\r\n            {\r\n                aliases[Role($\u0022{role}Prox\u0022, roleSide)] = new[] { $\u0022{nameSide}_{cc}1\u0022 };\r\n                aliases[Role($\u0022{role}Mid\u0022, roleSide)] = new[] { $\u0022{nameSide}_{cc}2\u0022 };\r\n                aliases[Role($\u0022{role}Dist\u0022, roleSide)] = new[] { $\u0022{nameSide}_{cc}3\u0022 };\r\n            }\r\n        }\r\n        return new Profile(\u0022actorcore_cc\u0022, new[] { \u0022^CC_Base_\u0022 }, aliases);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- ue mannequin\r\n\r\n    private static Profile BuildUeMannequin()\r\n    {\r\n        var aliases = new Dictionary\u003CBoneRole, string[]\u003E\r\n        {\r\n            [BoneRole.Hips] = new[] { \u0022pelvis\u0022 },\r\n            [BoneRole.Spine0] = new[] { \u0022spine_01\u0022 },\r\n            [BoneRole.Spine1] = new[] { \u0022spine_02\u0022 },\r\n            [BoneRole.Spine2] = new[] { \u0022spine_03\u0022 },\r\n            [BoneRole.Spine3] = new[] { \u0022spine_04\u0022 },\r\n            [BoneRole.Spine4] = new[] { \u0022spine_05\u0022 },\r\n            [BoneRole.Neck] = new[] { \u0022neck_01\u0022 },\r\n            [BoneRole.Head] = new[] { \u0022head\u0022 },\r\n        };\r\n        foreach (var (roleSide, s) in new[] { (\u0022L\u0022, \u0022l\u0022), (\u0022R\u0022, \u0022r\u0022) })\r\n        {\r\n            aliases[Role(\u0022Clavicle\u0022, roleSide)] = new[] { $\u0022clavicle_{s}\u0022 };\r\n            aliases[Role(\u0022UpperArm\u0022, roleSide)] = new[] { $\u0022upperarm_{s}\u0022 };\r\n            aliases[Role(\u0022LowerArm\u0022, roleSide)] = new[] { $\u0022lowerarm_{s}\u0022 };\r\n            aliases[Role(\u0022Hand\u0022, roleSide)] = new[] { $\u0022hand_{s}\u0022 };\r\n            aliases[Role(\u0022UpperLeg\u0022, roleSide)] = new[] { $\u0022thigh_{s}\u0022 };\r\n            aliases[Role(\u0022LowerLeg\u0022, roleSide)] = new[] { $\u0022calf_{s}\u0022 };\r\n            aliases[Role(\u0022Foot\u0022, roleSide)] = new[] { $\u0022foot_{s}\u0022 };\r\n            aliases[Role(\u0022Toe\u0022, roleSide)] = new[] { $\u0022ball_{s}\u0022 };\r\n\r\n            foreach (var (role, ue) in new[]\r\n            {\r\n                (\u0022Thumb\u0022, \u0022thumb\u0022), (\u0022Index\u0022, \u0022index\u0022), (\u0022Middle\u0022, \u0022middle\u0022), (\u0022Ring\u0022, \u0022ring\u0022), (\u0022Pinky\u0022, \u0022pinky\u0022),\r\n            })\r\n            {\r\n                // UE5 mannequin adds metacarpals for the four fingers (not the thumb).\r\n                if (role != \u0022Thumb\u0022)\r\n                    aliases[Role($\u0022{role}Meta\u0022, roleSide)] = new[] { $\u0022{ue}_metacarpal_{s}\u0022 };\r\n                aliases[Role($\u0022{role}Prox\u0022, roleSide)] = new[] { $\u0022{ue}_01_{s}\u0022 };\r\n                aliases[Role($\u0022{role}Mid\u0022, roleSide)] = new[] { $\u0022{ue}_02_{s}\u0022 };\r\n                aliases[Role($\u0022{role}Dist\u0022, roleSide)] = new[] { $\u0022{ue}_03_{s}\u0022 };\r\n            }\r\n        }\r\n        return new Profile(\u0022ue_mannequin\u0022, new string[0], aliases);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- rokoko / xsens bvh\r\n\r\n    private static Profile BuildRokokoBvh()\r\n    {\r\n        var aliases = new Dictionary\u003CBoneRole, string[]\u003E\r\n        {\r\n            [BoneRole.Hips] = new[] { \u0022Hips\u0022 },\r\n            // Spine naming varies (Spine, Spine1..Spine4); ordered alias preference plus the\r\n            // used-bone exclusion in the detector shifts the chain up when \u0022Spine\u0022 is absent.\r\n            [BoneRole.Spine0] = new[] { \u0022Spine\u0022, \u0022Spine1\u0022 },\r\n            [BoneRole.Spine1] = new[] { \u0022Spine1\u0022, \u0022Spine2\u0022 },\r\n            [BoneRole.Spine2] = new[] { \u0022Spine2\u0022, \u0022Spine3\u0022 },\r\n            [BoneRole.Spine3] = new[] { \u0022Spine3\u0022, \u0022Spine4\u0022 },\r\n            [BoneRole.Spine4] = new[] { \u0022Spine4\u0022 },\r\n            [BoneRole.Neck] = new[] { \u0022Neck\u0022, \u0022Neck1\u0022 },\r\n            [BoneRole.Head] = new[] { \u0022Head\u0022 },\r\n        };\r\n        foreach (var (roleSide, nameSide) in Sides())\r\n        {\r\n            aliases[Role(\u0022Clavicle\u0022, roleSide)] = new[] { $\u0022{nameSide}Shoulder\u0022, $\u0022{nameSide}Collar\u0022 };\r\n            aliases[Role(\u0022UpperArm\u0022, roleSide)] = new[] { $\u0022{nameSide}Arm\u0022, $\u0022{nameSide}UpperArm\u0022 };\r\n            aliases[Role(\u0022LowerArm\u0022, roleSide)] = new[] { $\u0022{nameSide}ForeArm\u0022, $\u0022{nameSide}LowerArm\u0022 };\r\n            aliases[Role(\u0022Hand\u0022, roleSide)] = new[] { $\u0022{nameSide}Hand\u0022 };\r\n            aliases[Role(\u0022UpperLeg\u0022, roleSide)] = new[] { $\u0022{nameSide}UpLeg\u0022, $\u0022{nameSide}Thigh\u0022, $\u0022{nameSide}UpperLeg\u0022 };\r\n            aliases[Role(\u0022LowerLeg\u0022, roleSide)] = new[] { $\u0022{nameSide}Leg\u0022, $\u0022{nameSide}Shin\u0022, $\u0022{nameSide}LowerLeg\u0022 };\r\n            aliases[Role(\u0022Foot\u0022, roleSide)] = new[] { $\u0022{nameSide}Foot\u0022 };\r\n            aliases[Role(\u0022Toe\u0022, roleSide)] = new[] { $\u0022{nameSide}Toe\u0022, $\u0022{nameSide}ToeBase\u0022 };\r\n        }\r\n        return new Profile(\u0022rokoko_bvh\u0022, new string[0], aliases);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- xsens mvn\r\n\r\n    private static Profile BuildXsensMvn()\r\n    {\r\n        var aliases = new Dictionary\u003CBoneRole, string[]\u003E\r\n        {\r\n            [BoneRole.Hips] = new[] { \u0022Pelvis\u0022 },\r\n            // MVN\u0027s exported spine segments are the anatomical vertebra levels L5/L3/T12/T8.\r\n            [BoneRole.Spine0] = new[] { \u0022L5\u0022 },\r\n            [BoneRole.Spine1] = new[] { \u0022L3\u0022 },\r\n            [BoneRole.Spine2] = new[] { \u0022T12\u0022 },\r\n            [BoneRole.Spine3] = new[] { \u0022T8\u0022 },\r\n            [BoneRole.Neck] = new[] { \u0022Neck\u0022 },\r\n            [BoneRole.Head] = new[] { \u0022Head\u0022 },\r\n        };\r\n        foreach (var (roleSide, nameSide) in Sides())\r\n        {\r\n            aliases[Role(\u0022Clavicle\u0022, roleSide)] = new[] { $\u0022{nameSide}Shoulder\u0022 };\r\n            aliases[Role(\u0022UpperArm\u0022, roleSide)] = new[] { $\u0022{nameSide}UpperArm\u0022 };\r\n            aliases[Role(\u0022LowerArm\u0022, roleSide)] = new[] { $\u0022{nameSide}ForeArm\u0022 };\r\n            aliases[Role(\u0022Hand\u0022, roleSide)] = new[] { $\u0022{nameSide}Hand\u0022 };\r\n            aliases[Role(\u0022UpperLeg\u0022, roleSide)] = new[] { $\u0022{nameSide}UpperLeg\u0022 };\r\n            aliases[Role(\u0022LowerLeg\u0022, roleSide)] = new[] { $\u0022{nameSide}LowerLeg\u0022 };\r\n            aliases[Role(\u0022Foot\u0022, roleSide)] = new[] { $\u0022{nameSide}Foot\u0022 };\r\n            aliases[Role(\u0022Toe\u0022, roleSide)] = new[] { $\u0022{nameSide}Toe\u0022 };\r\n        }\r\n        // Body-suit capture: no finger segments (see the property remarks).\r\n        return new Profile(\u0022xsens_mvn\u0022, new string[0], aliases);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- perception neuron\r\n\r\n    private static Profile BuildPerceptionNeuron()\r\n    {\r\n        var aliases = new Dictionary\u003CBoneRole, string[]\u003E\r\n        {\r\n            [BoneRole.Hips] = new[] { \u0022Hips\u0022 },\r\n            [BoneRole.Spine0] = new[] { \u0022Spine\u0022 },\r\n            [BoneRole.Spine1] = new[] { \u0022Spine1\u0022 },\r\n            [BoneRole.Spine2] = new[] { \u0022Spine2\u0022 },\r\n            [BoneRole.Spine3] = new[] { \u0022Spine3\u0022 },\r\n            [BoneRole.Neck] = new[] { \u0022Neck\u0022 },\r\n            [BoneRole.Head] = new[] { \u0022Head\u0022 },\r\n        };\r\n        foreach (var (roleSide, nameSide) in Sides())\r\n        {\r\n            aliases[Role(\u0022Clavicle\u0022, roleSide)] = new[] { $\u0022{nameSide}Shoulder\u0022 };\r\n            aliases[Role(\u0022UpperArm\u0022, roleSide)] = new[] { $\u0022{nameSide}Arm\u0022 };\r\n            aliases[Role(\u0022LowerArm\u0022, roleSide)] = new[] { $\u0022{nameSide}ForeArm\u0022 };\r\n            aliases[Role(\u0022Hand\u0022, roleSide)] = new[] { $\u0022{nameSide}Hand\u0022 };\r\n            aliases[Role(\u0022UpperLeg\u0022, roleSide)] = new[] { $\u0022{nameSide}UpLeg\u0022 };\r\n            aliases[Role(\u0022LowerLeg\u0022, roleSide)] = new[] { $\u0022{nameSide}Leg\u0022 };\r\n            aliases[Role(\u0022Foot\u0022, roleSide)] = new[] { $\u0022{nameSide}Foot\u0022 };\r\n            // No toe joints in Axis Neuron exports; the feet are chain tips.\r\n\r\n            // Phalanges only: the LeftInHandIndex-style metacarpal helpers between hand\r\n            // and phalanges carry no role (see the property remarks).\r\n            foreach (var finger in new[] { \u0022Thumb\u0022, \u0022Index\u0022, \u0022Middle\u0022, \u0022Ring\u0022, \u0022Pinky\u0022 })\r\n            {\r\n                aliases[Role($\u0022{finger}Prox\u0022, roleSide)] = new[] { $\u0022{nameSide}Hand{finger}1\u0022 };\r\n                aliases[Role($\u0022{finger}Mid\u0022, roleSide)] = new[] { $\u0022{nameSide}Hand{finger}2\u0022 };\r\n                aliases[Role($\u0022{finger}Dist\u0022, roleSide)] = new[] { $\u0022{nameSide}Hand{finger}3\u0022 };\r\n            }\r\n        }\r\n        return new Profile(\u0022perception_neuron\u0022, new string[0], aliases);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- valvebiped\r\n\r\n    private static Profile BuildValveBiped()\r\n    {\r\n        var aliases = new Dictionary\u003CBoneRole, string[]\u003E\r\n        {\r\n            [BoneRole.Hips] = new[] { \u0022Pelvis\u0022 },\r\n            [BoneRole.Spine0] = new[] { \u0022Spine\u0022 },\r\n            [BoneRole.Spine1] = new[] { \u0022Spine1\u0022 },\r\n            [BoneRole.Spine2] = new[] { \u0022Spine2\u0022 },\r\n            // The stock HL2 chain skips Spine3 (Spine2\u0027s child IS Spine4, the chest);\r\n            // ordered preference \u002B used-bone exclusion also absorbs a variant that has\r\n            // both: Spine3\u2192Spine3 and Spine4\u2192Spine4.\r\n            [BoneRole.Spine3] = new[] { \u0022Spine3\u0022, \u0022Spine4\u0022 },\r\n            [BoneRole.Spine4] = new[] { \u0022Spine4\u0022 },\r\n            [BoneRole.Neck] = new[] { \u0022Neck1\u0022 },\r\n            [BoneRole.Head] = new[] { \u0022Head1\u0022 },\r\n        };\r\n        foreach (var s in new[] { \u0022L\u0022, \u0022R\u0022 })\r\n        {\r\n            aliases[Role(\u0022Clavicle\u0022, s)] = new[] { $\u0022{s}_Clavicle\u0022 };\r\n            aliases[Role(\u0022UpperArm\u0022, s)] = new[] { $\u0022{s}_UpperArm\u0022 };\r\n            aliases[Role(\u0022LowerArm\u0022, s)] = new[] { $\u0022{s}_Forearm\u0022 };\r\n            aliases[Role(\u0022Hand\u0022, s)] = new[] { $\u0022{s}_Hand\u0022 };\r\n            aliases[Role(\u0022UpperLeg\u0022, s)] = new[] { $\u0022{s}_Thigh\u0022 };\r\n            aliases[Role(\u0022LowerLeg\u0022, s)] = new[] { $\u0022{s}_Calf\u0022 };\r\n            aliases[Role(\u0022Foot\u0022, s)] = new[] { $\u0022{s}_Foot\u0022 };\r\n            aliases[Role(\u0022Toe\u0022, s)] = new[] { $\u0022{s}_Toe0\u0022 };\r\n\r\n            // Biped-style numbered finger chains behind the ValveBiped namespace:\r\n            // Finger0 is the thumb; segments append the phalanx digit (Finger0 \u2192\r\n            // Finger01 \u2192 Finger02, Finger1 \u2192 Finger11 \u2192 \u2026).\r\n            foreach (var (finger, n) in new[]\r\n            {\r\n                (\u0022Thumb\u0022, 0), (\u0022Index\u0022, 1), (\u0022Middle\u0022, 2), (\u0022Ring\u0022, 3), (\u0022Pinky\u0022, 4),\r\n            })\r\n            {\r\n                aliases[Role($\u0022{finger}Prox\u0022, s)] = new[] { $\u0022{s}_Finger{n}\u0022 };\r\n                aliases[Role($\u0022{finger}Mid\u0022, s)] = new[] { $\u0022{s}_Finger{n}1\u0022 };\r\n                aliases[Role($\u0022{finger}Dist\u0022, s)] = new[] { $\u0022{s}_Finger{n}2\u0022 };\r\n            }\r\n        }\r\n        // The fixed \u0022ValveBiped.Bip01_\u0022 namespace: anchored, so plain \u0022Bip01 ...\u0022 Character\r\n        // Studio rigs never strip it (and the Biped preset\u0027s \u0022^Bip\\d\u002B[ _]\u0022 never matches\r\n        // the ValveBiped prefix \u2014 the two families cannot cross-claim).\r\n        return new Profile(\u0022valvebiped\u0022, new[] { @\u0022^ValveBiped\\.Bip01_\u0022 }, aliases);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- daz genesis 3/8\r\n\r\n    private static Profile BuildDazGenesis()\r\n    {\r\n        var aliases = new Dictionary\u003CBoneRole, string[]\u003E\r\n        {\r\n            // \u0022hip\u0022 is the translating root and LCA of the pelvis (leg branch) and the\r\n            // abdomen (spine branch); \u0022pelvis\u0022 is a leg-branch intermediate and stays\r\n            // unmapped \u2014 same policy as ActorCore\u0027s CC_Base_Hip/CC_Base_Pelvis pair.\r\n            [BoneRole.Hips] = new[] { \u0022hip\u0022 },\r\n            [BoneRole.Spine0] = new[] { \u0022abdomenLower\u0022 },\r\n            [BoneRole.Spine1] = new[] { \u0022abdomenUpper\u0022 },\r\n            [BoneRole.Spine2] = new[] { \u0022chestLower\u0022 },\r\n            [BoneRole.Spine3] = new[] { \u0022chestUpper\u0022 },\r\n            // neckLower\u2192neckUpper\u2192head: neckLower IS the neck; neckUpper stays unmapped\r\n            // (same policy as ActorCore\u0027s NeckTwist02 / rigify\u0027s spine.005).\r\n            [BoneRole.Neck] = new[] { \u0022neckLower\u0022 },\r\n            [BoneRole.Head] = new[] { \u0022head\u0022 },\r\n        };\r\n        foreach (var s in new[] { \u0022L\u0022, \u0022R\u0022 })\r\n        {\r\n            var p = s == \u0022L\u0022 ? \u0022l\u0022 : \u0022r\u0022; // lower-case side prefix: lShldrBend, rThighBend\r\n            aliases[Role(\u0022Clavicle\u0022, s)] = new[] { $\u0022{p}Collar\u0022 };\r\n            // Bend bones are the primary limb bones; the co-linear *Twist roll helpers\r\n            // have no aliases and are never mapped.\r\n            aliases[Role(\u0022UpperArm\u0022, s)] = new[] { $\u0022{p}ShldrBend\u0022 };\r\n            aliases[Role(\u0022LowerArm\u0022, s)] = new[] { $\u0022{p}ForearmBend\u0022 };\r\n            aliases[Role(\u0022Hand\u0022, s)] = new[] { $\u0022{p}Hand\u0022 };\r\n            aliases[Role(\u0022UpperLeg\u0022, s)] = new[] { $\u0022{p}ThighBend\u0022 };\r\n            aliases[Role(\u0022LowerLeg\u0022, s)] = new[] { $\u0022{p}Shin\u0022 };\r\n            aliases[Role(\u0022Foot\u0022, s)] = new[] { $\u0022{p}Foot\u0022 };\r\n            // lMetatarsals sits between foot and toe (arch helper, unmapped).\r\n            aliases[Role(\u0022Toe\u0022, s)] = new[] { $\u0022{p}Toe\u0022 };\r\n\r\n            foreach (var (role, daz) in new[]\r\n            {\r\n                (\u0022Thumb\u0022, \u0022Thumb\u0022), (\u0022Index\u0022, \u0022Index\u0022), (\u0022Middle\u0022, \u0022Mid\u0022), (\u0022Ring\u0022, \u0022Ring\u0022), (\u0022Pinky\u0022, \u0022Pinky\u0022),\r\n            })\r\n            {\r\n                aliases[Role($\u0022{role}Prox\u0022, s)] = new[] { $\u0022{p}{daz}1\u0022 };\r\n                aliases[Role($\u0022{role}Mid\u0022, s)] = new[] { $\u0022{p}{daz}2\u0022 };\r\n                aliases[Role($\u0022{role}Dist\u0022, s)] = new[] { $\u0022{p}{daz}3\u0022 };\r\n            }\r\n        }\r\n        return new Profile(\u0022daz_genesis\u0022, new string[0], aliases);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- smpl / smpl-x\r\n\r\n    private static Profile BuildSmpl(bool withFingers)\r\n    {\r\n        var aliases = new Dictionary\u003CBoneRole, string[]\u003E\r\n        {\r\n            [BoneRole.Hips] = new[] { \u0022Pelvis\u0022 },\r\n            [BoneRole.Spine0] = new[] { \u0022Spine1\u0022 },\r\n            [BoneRole.Spine1] = new[] { \u0022Spine2\u0022 },\r\n            [BoneRole.Spine2] = new[] { \u0022Spine3\u0022 },\r\n            [BoneRole.Neck] = new[] { \u0022Neck\u0022 },\r\n            [BoneRole.Head] = new[] { \u0022Head\u0022 },\r\n        };\r\n        foreach (var (roleSide, abbr, word) in new[] { (\u0022L\u0022, \u0022L\u0022, \u0022left\u0022), (\u0022R\u0022, \u0022R\u0022, \u0022right\u0022) })\r\n        {\r\n            // Both documented spellings per role: abbreviated FBX-rig names (\u0022L_Hip\u0022) and\r\n            // spelled model joint names (\u0022left_hip\u0022). Comparison is separator-insensitive.\r\n            aliases[Role(\u0022Clavicle\u0022, roleSide)] = new[] { $\u0022{abbr}_Collar\u0022, $\u0022{word}_collar\u0022 };\r\n            aliases[Role(\u0022UpperArm\u0022, roleSide)] = new[] { $\u0022{abbr}_Shoulder\u0022, $\u0022{word}_shoulder\u0022 };\r\n            aliases[Role(\u0022LowerArm\u0022, roleSide)] = new[] { $\u0022{abbr}_Elbow\u0022, $\u0022{word}_elbow\u0022 };\r\n            aliases[Role(\u0022Hand\u0022, roleSide)] = new[] { $\u0022{abbr}_Wrist\u0022, $\u0022{word}_wrist\u0022 };\r\n            aliases[Role(\u0022UpperLeg\u0022, roleSide)] = new[] { $\u0022{abbr}_Hip\u0022, $\u0022{word}_hip\u0022 };\r\n            aliases[Role(\u0022LowerLeg\u0022, roleSide)] = new[] { $\u0022{abbr}_Knee\u0022, $\u0022{word}_knee\u0022 };\r\n            aliases[Role(\u0022Foot\u0022, roleSide)] = new[] { $\u0022{abbr}_Ankle\u0022, $\u0022{word}_ankle\u0022 };\r\n            aliases[Role(\u0022Toe\u0022, roleSide)] = new[] { $\u0022{abbr}_Foot\u0022, $\u0022{word}_foot\u0022 };\r\n\r\n            if (!withFingers)\r\n                continue;\r\n\r\n            // SMPL-X finger joints (left_index1..3 etc., per vchoutas/smplx joint_names.py).\r\n            foreach (var finger in new[] { \u0022thumb\u0022, \u0022index\u0022, \u0022middle\u0022, \u0022ring\u0022, \u0022pinky\u0022 })\r\n            {\r\n                var name = char.ToUpperInvariant(finger[0]) \u002B finger[1..];\r\n                aliases[Role($\u0022{name}Prox\u0022, roleSide)] = new[] { $\u0022{word}_{finger}1\u0022 };\r\n                aliases[Role($\u0022{name}Mid\u0022, roleSide)] = new[] { $\u0022{word}_{finger}2\u0022 };\r\n                aliases[Role($\u0022{name}Dist\u0022, roleSide)] = new[] { $\u0022{word}_{finger}3\u0022 };\r\n            }\r\n        }\r\n        // Gendered SMPL FBX rigs prefix every bone (m_avg_L_Hip, f_avg_Pelvis).\r\n        return new Profile(withFingers ? \u0022smpl_x\u0022 : \u0022smpl\u0022, new[] { \u0022^m_avg_\u0022, \u0022^f_avg_\u0022 }, aliases);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- nvidia soma bvh\r\n\r\n    private static Profile BuildSomaBvh()\r\n    {\r\n        var aliases = new Dictionary\u003CBoneRole, string[]\u003E\r\n        {\r\n            [BoneRole.Hips] = new[] { \u0022Hips\u0022 },\r\n            [BoneRole.Spine0] = new[] { \u0022Spine1\u0022 },\r\n            [BoneRole.Spine1] = new[] { \u0022Spine2\u0022 },\r\n            [BoneRole.Spine2] = new[] { \u0022Chest\u0022 },\r\n            [BoneRole.Neck] = new[] { \u0022Neck1\u0022 },\r\n            [BoneRole.Head] = new[] { \u0022Head\u0022 },\r\n        };\r\n        foreach (var (roleSide, nameSide) in Sides())\r\n        {\r\n            aliases[Role(\u0022Clavicle\u0022, roleSide)] = new[] { $\u0022{nameSide}Shoulder\u0022 };\r\n            aliases[Role(\u0022UpperArm\u0022, roleSide)] = new[] { $\u0022{nameSide}Arm\u0022 };\r\n            aliases[Role(\u0022LowerArm\u0022, roleSide)] = new[] { $\u0022{nameSide}ForeArm\u0022 };\r\n            aliases[Role(\u0022Hand\u0022, roleSide)] = new[] { $\u0022{nameSide}Hand\u0022 };\r\n            // SOMA\u0027s \u0022Leg\u0022 is the thigh, \u0022Shin\u0022 the calf \u2014 the decisive difference from\r\n            // mixamo, where \u0022Leg\u0022 is the calf under \u0022UpLeg\u0022.\r\n            aliases[Role(\u0022UpperLeg\u0022, roleSide)] = new[] { $\u0022{nameSide}Leg\u0022 };\r\n            aliases[Role(\u0022LowerLeg\u0022, roleSide)] = new[] { $\u0022{nameSide}Shin\u0022 };\r\n            aliases[Role(\u0022Foot\u0022, roleSide)] = new[] { $\u0022{nameSide}Foot\u0022 };\r\n            aliases[Role(\u0022Toe\u0022, roleSide)] = new[] { $\u0022{nameSide}ToeBase\u0022 };\r\n\r\n            // Mixamo-style finger NAMES but not mixamo segmentation: SOMA fingers have four\r\n            // segments where segment 1 is a metacarpal (measured on the repro BVH: Index1\r\n            // sits 3.2 cm from the wrist at the palm base, then a 6.4 cm metacarpal to the\r\n            // Index2 knuckle, then 3.7/2.3 cm phalanges to Index3/Index4) \u2014 so 2/3/4 are the\r\n            // phalanges. Mapping 1..3 as Prox/Mid/Dist (mixamo\u0027s segmentation) shifted every\r\n            // curl one joint outward and dropped the distal curl entirely (frozen fingers).\r\n            // The thumb is three segments plus *End, mapped 1..3 like mixamo\u0027s; *End tip\r\n            // markers carry no role.\r\n            aliases[Role(\u0022ThumbProx\u0022, roleSide)] = new[] { $\u0022{nameSide}HandThumb1\u0022 };\r\n            aliases[Role(\u0022ThumbMid\u0022, roleSide)] = new[] { $\u0022{nameSide}HandThumb2\u0022 };\r\n            aliases[Role(\u0022ThumbDist\u0022, roleSide)] = new[] { $\u0022{nameSide}HandThumb3\u0022 };\r\n            foreach (var finger in new[] { \u0022Index\u0022, \u0022Middle\u0022, \u0022Ring\u0022, \u0022Pinky\u0022 })\r\n            {\r\n                aliases[Role($\u0022{finger}Meta\u0022, roleSide)] = new[] { $\u0022{nameSide}Hand{finger}1\u0022 };\r\n                aliases[Role($\u0022{finger}Prox\u0022, roleSide)] = new[] { $\u0022{nameSide}Hand{finger}2\u0022 };\r\n                aliases[Role($\u0022{finger}Mid\u0022, roleSide)] = new[] { $\u0022{nameSide}Hand{finger}3\u0022 };\r\n                aliases[Role($\u0022{finger}Dist\u0022, roleSide)] = new[] { $\u0022{nameSide}Hand{finger}4\u0022 };\r\n            }\r\n        }\r\n        return new Profile(\u0022soma_bvh\u0022, new string[0], aliases);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- classic bvh\r\n\r\n    private static Profile BuildClassicBvh()\r\n    {\r\n        var aliases = new Dictionary\u003CBoneRole, string[]\u003E\r\n        {\r\n            [BoneRole.Hips] = new[] { \u0022Hips\u0022 },\r\n            [BoneRole.Spine0] = new[] { \u0022Chest\u0022 },\r\n            [BoneRole.Spine1] = new[] { \u0022Chest2\u0022 },\r\n            [BoneRole.Spine2] = new[] { \u0022Chest3\u0022 },\r\n            [BoneRole.Spine3] = new[] { \u0022Chest4\u0022 },\r\n            [BoneRole.Neck] = new[] { \u0022Neck\u0022 },\r\n            [BoneRole.Head] = new[] { \u0022Head\u0022 },\r\n        };\r\n        foreach (var (roleSide, nameSide) in Sides())\r\n        {\r\n            aliases[Role(\u0022Clavicle\u0022, roleSide)] = new[] { $\u0022{nameSide}Collar\u0022 };\r\n            aliases[Role(\u0022UpperArm\u0022, roleSide)] = new[] { $\u0022{nameSide}Shoulder\u0022 };\r\n            aliases[Role(\u0022LowerArm\u0022, roleSide)] = new[] { $\u0022{nameSide}Elbow\u0022 };\r\n            aliases[Role(\u0022Hand\u0022, roleSide)] = new[] { $\u0022{nameSide}Wrist\u0022 };\r\n            aliases[Role(\u0022UpperLeg\u0022, roleSide)] = new[] { $\u0022{nameSide}Hip\u0022 };\r\n            aliases[Role(\u0022LowerLeg\u0022, roleSide)] = new[] { $\u0022{nameSide}Knee\u0022 };\r\n            aliases[Role(\u0022Foot\u0022, roleSide)] = new[] { $\u0022{nameSide}Ankle\u0022 };\r\n            aliases[Role(\u0022Toe\u0022, roleSide)] = new[] { $\u0022{nameSide}Toe\u0022 };\r\n        }\r\n        return new Profile(\u0022classic_bvh\u0022, new string[0], aliases);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- 3ds max biped\r\n\r\n    private static Profile BuildBiped()\r\n    {\r\n        var aliases = new Dictionary\u003CBoneRole, string[]\u003E\r\n        {\r\n            [BoneRole.Hips] = new[] { \u0022Pelvis\u0022 },\r\n            [BoneRole.Spine0] = new[] { \u0022Spine\u0022 },\r\n            [BoneRole.Spine1] = new[] { \u0022Spine1\u0022 },\r\n            [BoneRole.Spine2] = new[] { \u0022Spine2\u0022 },\r\n            [BoneRole.Spine3] = new[] { \u0022Spine3\u0022 },\r\n            [BoneRole.Neck] = new[] { \u0022Neck\u0022 },\r\n            [BoneRole.Head] = new[] { \u0022Head\u0022 },\r\n        };\r\n        foreach (var s in new[] { \u0022L\u0022, \u0022R\u0022 })\r\n        {\r\n            aliases[Role(\u0022Clavicle\u0022, s)] = new[] { $\u0022{s} Clavicle\u0022 };\r\n            aliases[Role(\u0022UpperArm\u0022, s)] = new[] { $\u0022{s} UpperArm\u0022 };\r\n            aliases[Role(\u0022LowerArm\u0022, s)] = new[] { $\u0022{s} Forearm\u0022 };\r\n            aliases[Role(\u0022Hand\u0022, s)] = new[] { $\u0022{s} Hand\u0022 };\r\n            aliases[Role(\u0022UpperLeg\u0022, s)] = new[] { $\u0022{s} Thigh\u0022 };\r\n            aliases[Role(\u0022LowerLeg\u0022, s)] = new[] { $\u0022{s} Calf\u0022 };\r\n            aliases[Role(\u0022Foot\u0022, s)] = new[] { $\u0022{s} Foot\u0022 };\r\n            aliases[Role(\u0022Toe\u0022, s)] = new[] { $\u0022{s} Toe0\u0022 };\r\n\r\n            // Numbered finger chains: Finger0 is the thumb; segment names append the\r\n            // phalanx digit (Finger0 \u2192 Finger01 \u2192 Finger02, Finger1 \u2192 Finger11 \u2192 ...).\r\n            foreach (var (finger, n) in new[]\r\n            {\r\n                (\u0022Thumb\u0022, 0), (\u0022Index\u0022, 1), (\u0022Middle\u0022, 2), (\u0022Ring\u0022, 3), (\u0022Pinky\u0022, 4),\r\n            })\r\n            {\r\n                aliases[Role($\u0022{finger}Prox\u0022, s)] = new[] { $\u0022{s} Finger{n}\u0022 };\r\n                aliases[Role($\u0022{finger}Mid\u0022, s)] = new[] { $\u0022{s} Finger{n}1\u0022 };\r\n                aliases[Role($\u0022{finger}Dist\u0022, s)] = new[] { $\u0022{s} Finger{n}2\u0022 };\r\n            }\r\n        }\r\n        // \u0022Bip01 \u0022/\u0022Bip001 \u0022 biped-name prefix; underscore form covers exporters that\r\n        // mangle the spaces (\u0022Bip01_L_Thigh\u0022). The bare COM root \u0022Bip01\u0022 is untouched by\r\n        // the pattern (no trailing separator) and has no alias.\r\n        return new Profile(\u0022biped\u0022, new[] { @\u0022^Bip\\d\u002B[ _]\u0022 }, aliases);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- daz / poser\r\n\r\n    private static Profile BuildDazPoser()\r\n    {\r\n        var aliases = new Dictionary\u003CBoneRole, string[]\u003E\r\n        {\r\n            [BoneRole.Hips] = new[] { \u0022hip\u0022 },\r\n            [BoneRole.Spine0] = new[] { \u0022abdomen\u0022 },\r\n            // Poser classic / DAZ Gen4 spine is abdomen\u2192chest; DAZ Genesis 1/2 inserts\r\n            // abdomen2. Ordered preference \u002B used-bone exclusion handles both: without\r\n            // abdomen2 the chest falls back to Spine1 and Spine2 stays unmapped.\r\n            [BoneRole.Spine1] = new[] { \u0022abdomen2\u0022, \u0022chest\u0022 },\r\n            [BoneRole.Spine2] = new[] { \u0022chest\u0022 },\r\n            [BoneRole.Neck] = new[] { \u0022neck\u0022 },\r\n            [BoneRole.Head] = new[] { \u0022head\u0022 },\r\n        };\r\n        foreach (var s in new[] { \u0022L\u0022, \u0022R\u0022 })\r\n        {\r\n            var p = s == \u0022L\u0022 ? \u0022l\u0022 : \u0022r\u0022; // lower-case side prefix: lShldr, rThigh\r\n            aliases[Role(\u0022Clavicle\u0022, s)] = new[] { $\u0022{p}Collar\u0022 };\r\n            aliases[Role(\u0022UpperArm\u0022, s)] = new[] { $\u0022{p}Shldr\u0022 };\r\n            aliases[Role(\u0022LowerArm\u0022, s)] = new[] { $\u0022{p}ForeArm\u0022 };\r\n            aliases[Role(\u0022Hand\u0022, s)] = new[] { $\u0022{p}Hand\u0022 };\r\n            aliases[Role(\u0022UpperLeg\u0022, s)] = new[] { $\u0022{p}Thigh\u0022 };\r\n            aliases[Role(\u0022LowerLeg\u0022, s)] = new[] { $\u0022{p}Shin\u0022 };\r\n            aliases[Role(\u0022Foot\u0022, s)] = new[] { $\u0022{p}Foot\u0022 };\r\n            aliases[Role(\u0022Toe\u0022, s)] = new[] { $\u0022{p}Toe\u0022 };\r\n\r\n            foreach (var (role, daz) in new[]\r\n            {\r\n                (\u0022Thumb\u0022, \u0022Thumb\u0022), (\u0022Index\u0022, \u0022Index\u0022), (\u0022Middle\u0022, \u0022Mid\u0022), (\u0022Ring\u0022, \u0022Ring\u0022), (\u0022Pinky\u0022, \u0022Pinky\u0022),\r\n            })\r\n            {\r\n                aliases[Role($\u0022{role}Prox\u0022, s)] = new[] { $\u0022{p}{daz}1\u0022 };\r\n                aliases[Role($\u0022{role}Mid\u0022, s)] = new[] { $\u0022{p}{daz}2\u0022 };\r\n                aliases[Role($\u0022{role}Dist\u0022, s)] = new[] { $\u0022{p}{daz}3\u0022 };\r\n            }\r\n        }\r\n        return new Profile(\u0022daz_poser\u0022, new string[0], aliases);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- blender rigify\r\n\r\n    private static Profile BuildRigify()\r\n    {\r\n        var aliases = new Dictionary\u003CBoneRole, string[]\u003E\r\n        {\r\n            // rigify\u0027s \u0022spine\u0022 bone sits AT the pelvis and parents both thighs \u2014 it is\r\n            // the hips, not a spine link (rigify/metarigs/human.py).\r\n            [BoneRole.Hips] = new[] { \u0022spine\u0022 },\r\n            [BoneRole.Spine0] = new[] { \u0022spine.001\u0022 },\r\n            [BoneRole.Spine1] = new[] { \u0022spine.002\u0022 },\r\n            [BoneRole.Spine2] = new[] { \u0022spine.003\u0022 },\r\n            // spine.004 \u002B spine.005 are the two neck bones, spine.006 the head;\r\n            // spine.005 stays unmapped (same policy as ActorCore\u0027s NeckTwist02).\r\n            [BoneRole.Neck] = new[] { \u0022spine.004\u0022 },\r\n            [BoneRole.Head] = new[] { \u0022spine.006\u0022 },\r\n        };\r\n        foreach (var s in new[] { \u0022L\u0022, \u0022R\u0022 })\r\n        {\r\n            aliases[Role(\u0022Clavicle\u0022, s)] = new[] { $\u0022shoulder.{s}\u0022 };\r\n            aliases[Role(\u0022UpperArm\u0022, s)] = new[] { $\u0022upper_arm.{s}\u0022 };\r\n            aliases[Role(\u0022LowerArm\u0022, s)] = new[] { $\u0022forearm.{s}\u0022 };\r\n            aliases[Role(\u0022Hand\u0022, s)] = new[] { $\u0022hand.{s}\u0022 };\r\n            aliases[Role(\u0022UpperLeg\u0022, s)] = new[] { $\u0022thigh.{s}\u0022 };\r\n            aliases[Role(\u0022LowerLeg\u0022, s)] = new[] { $\u0022shin.{s}\u0022 };\r\n            aliases[Role(\u0022Foot\u0022, s)] = new[] { $\u0022foot.{s}\u0022 };\r\n            aliases[Role(\u0022Toe\u0022, s)] = new[] { $\u0022toe.{s}\u0022 };\r\n\r\n            foreach (var (role, rigify) in new[]\r\n            {\r\n                (\u0022Thumb\u0022, \u0022thumb\u0022), (\u0022Index\u0022, \u0022f_index\u0022), (\u0022Middle\u0022, \u0022f_middle\u0022),\r\n                (\u0022Ring\u0022, \u0022f_ring\u0022), (\u0022Pinky\u0022, \u0022f_pinky\u0022),\r\n            })\r\n            {\r\n                aliases[Role($\u0022{role}Prox\u0022, s)] = new[] { $\u0022{rigify}.01.{s}\u0022 };\r\n                aliases[Role($\u0022{role}Mid\u0022, s)] = new[] { $\u0022{rigify}.02.{s}\u0022 };\r\n                aliases[Role($\u0022{role}Dist\u0022, s)] = new[] { $\u0022{rigify}.03.{s}\u0022 };\r\n            }\r\n        }\r\n        // The generated deform skeleton prefixes every deform bone with \u0022DEF-\u0022; its\r\n        // segmented limb twins (\u0022DEF-upper_arm.L.001\u0022) keep their numeric suffix after\r\n        // stripping and therefore never collide with the whole-bone aliases.\r\n        return new Profile(\u0022rigify\u0022, new[] { \u0022^DEF-\u0022 }, aliases);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- vroid / vrm\r\n\r\n    private static Profile BuildVrm()\r\n    {\r\n        var aliases = new Dictionary\u003CBoneRole, string[]\u003E\r\n        {\r\n            [BoneRole.Hips] = new[] { \u0022J_Bip_C_Hips\u0022 },\r\n            [BoneRole.Spine0] = new[] { \u0022J_Bip_C_Spine\u0022 },\r\n            [BoneRole.Spine1] = new[] { \u0022J_Bip_C_Chest\u0022 },\r\n            [BoneRole.Spine2] = new[] { \u0022J_Bip_C_UpperChest\u0022 },\r\n            [BoneRole.Neck] = new[] { \u0022J_Bip_C_Neck\u0022 },\r\n            [BoneRole.Head] = new[] { \u0022J_Bip_C_Head\u0022 },\r\n        };\r\n        foreach (var s in new[] { \u0022L\u0022, \u0022R\u0022 })\r\n        {\r\n            aliases[Role(\u0022Clavicle\u0022, s)] = new[] { $\u0022J_Bip_{s}_Shoulder\u0022 };\r\n            aliases[Role(\u0022UpperArm\u0022, s)] = new[] { $\u0022J_Bip_{s}_UpperArm\u0022 };\r\n            aliases[Role(\u0022LowerArm\u0022, s)] = new[] { $\u0022J_Bip_{s}_LowerArm\u0022 };\r\n            aliases[Role(\u0022Hand\u0022, s)] = new[] { $\u0022J_Bip_{s}_Hand\u0022 };\r\n            aliases[Role(\u0022UpperLeg\u0022, s)] = new[] { $\u0022J_Bip_{s}_UpperLeg\u0022 };\r\n            aliases[Role(\u0022LowerLeg\u0022, s)] = new[] { $\u0022J_Bip_{s}_LowerLeg\u0022 };\r\n            aliases[Role(\u0022Foot\u0022, s)] = new[] { $\u0022J_Bip_{s}_Foot\u0022 };\r\n            aliases[Role(\u0022Toe\u0022, s)] = new[] { $\u0022J_Bip_{s}_ToeBase\u0022 };\r\n\r\n            foreach (var (role, vrm) in new[]\r\n            {\r\n                (\u0022Thumb\u0022, \u0022Thumb\u0022), (\u0022Index\u0022, \u0022Index\u0022), (\u0022Middle\u0022, \u0022Middle\u0022),\r\n                (\u0022Ring\u0022, \u0022Ring\u0022), (\u0022Pinky\u0022, \u0022Little\u0022),\r\n            })\r\n            {\r\n                aliases[Role($\u0022{role}Prox\u0022, s)] = new[] { $\u0022J_Bip_{s}_{vrm}1\u0022 };\r\n                aliases[Role($\u0022{role}Mid\u0022, s)] = new[] { $\u0022J_Bip_{s}_{vrm}2\u0022 };\r\n                aliases[Role($\u0022{role}Dist\u0022, s)] = new[] { $\u0022J_Bip_{s}_{vrm}3\u0022 };\r\n            }\r\n        }\r\n        return new Profile(\u0022vrm\u0022, new string[0], aliases);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- auto-rig pro\r\n\r\n    private static Profile BuildAutoRigPro()\r\n    {\r\n        var aliases = new Dictionary\u003CBoneRole, string[]\u003E\r\n        {\r\n            [BoneRole.Hips] = new[] { \u0022root.x\u0022 },\r\n            [BoneRole.Spine0] = new[] { \u0022spine_01.x\u0022 },\r\n            [BoneRole.Spine1] = new[] { \u0022spine_02.x\u0022 },\r\n            [BoneRole.Spine2] = new[] { \u0022spine_03.x\u0022 },\r\n            [BoneRole.Neck] = new[] { \u0022neck.x\u0022 },\r\n            [BoneRole.Head] = new[] { \u0022head.x\u0022 },\r\n        };\r\n        foreach (var s in new[] { \u0022L\u0022, \u0022R\u0022 })\r\n        {\r\n            var p = s == \u0022L\u0022 ? \u0022l\u0022 : \u0022r\u0022;\r\n            aliases[Role(\u0022Clavicle\u0022, s)] = new[] { $\u0022shoulder.{p}\u0022 };\r\n            aliases[Role(\u0022UpperArm\u0022, s)] = new[] { $\u0022arm_stretch.{p}\u0022 };\r\n            aliases[Role(\u0022LowerArm\u0022, s)] = new[] { $\u0022forearm_stretch.{p}\u0022 };\r\n            aliases[Role(\u0022Hand\u0022, s)] = new[] { $\u0022hand.{p}\u0022 };\r\n            aliases[Role(\u0022UpperLeg\u0022, s)] = new[] { $\u0022thigh_stretch.{p}\u0022 };\r\n            aliases[Role(\u0022LowerLeg\u0022, s)] = new[] { $\u0022leg_stretch.{p}\u0022 };\r\n            aliases[Role(\u0022Foot\u0022, s)] = new[] { $\u0022foot.{p}\u0022 };\r\n            aliases[Role(\u0022Toe\u0022, s)] = new[] { $\u0022toes_01.{p}\u0022 };\r\n\r\n            // Exported finger deform bones keep ARP\u0027s c_ control prefix (Defenses.fbx).\r\n            foreach (var finger in new[] { \u0022thumb\u0022, \u0022index\u0022, \u0022middle\u0022, \u0022ring\u0022, \u0022pinky\u0022 })\r\n            {\r\n                var role = char.ToUpperInvariant(finger[0]) \u002B finger[1..];\r\n                aliases[Role($\u0022{role}Prox\u0022, s)] = new[] { $\u0022c_{finger}1.{p}\u0022 };\r\n                aliases[Role($\u0022{role}Mid\u0022, s)] = new[] { $\u0022c_{finger}2.{p}\u0022 };\r\n                aliases[Role($\u0022{role}Dist\u0022, s)] = new[] { $\u0022c_{finger}3.{p}\u0022 };\r\n            }\r\n        }\r\n        return new Profile(\u0022auto_rig_pro\u0022, new string[0], aliases);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- helpers\r\n\r\n    private static IEnumerable\u003C(string RoleSide, string NameSide)\u003E Sides()\r\n    {\r\n        yield return (\u0022L\u0022, \u0022Left\u0022);\r\n        yield return (\u0022R\u0022, \u0022Right\u0022);\r\n    }\r\n\r\n    private static BoneRole Role(string baseName, string side)\r\n        =\u003E System.Enum.Parse\u003CBoneRole\u003E(baseName \u002B side);\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"Code/HumanoidRetargeter/Solve/ClipMirror.cs","FileName":"ClipMirror.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidRetargeter.Mapping;\r\nusing HumanoidRetargeter.Maths;\r\nusing HumanoidRetargeter.Target;\r\n\r\nnamespace HumanoidRetargeter.Solve;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// \u003Csummary\u003E\r\n/// Mirrors a solved TARGET-space clip across the target character\u0027s sagittal plane,\r\n/// producing the left/right-swapped twin of an animation (e.g. a right-foot-lead walk from a\r\n/// left-foot-lead one).\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EMirror plane.\u003C/b\u003E The plane through the rig-space origin spanned by the target\r\n/// character\u0027s up and forward directions; its normal is the character\u0027s LATERAL axis,\r\n/// computed from the target rig\u0027s rest geometry via \u003Csee cref=\u0022CharacterFrame\u0022/\u003E (never\r\n/// hardcoded \u2014 an arbitrary target may be authored in any axis convention). When the\r\n/// computed lateral lies on a coordinate axis up to float dirt (\u0026lt; 1e-3 on the other two\r\n/// components \u2014 true for every axis-aligned authored rig, including the s\u0026amp;box citizen\r\n/// rigs), it is snapped to that exact axis, which makes every reflection below an EXACT\r\n/// sign-flip in IEEE arithmetic and therefore the whole mirror a bit-exact involution\r\n/// (mirror \u2218 mirror == identity, verified by test).\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EMath.\u003C/b\u003E Let M = I \u2212 2n\u0302n\u0302\u1D40 be the reflection across the plane with unit normal\r\n/// n\u0302. A world transform W = (R, t) maps to its mirror image by conjugation:\r\n/// W\u2032 = M\u0302 \u2218 W \u2218 M\u0302 (M\u0302 is its own inverse), giving rotation R\u2032 = M\u00B7R\u00B7M and translation\r\n/// t\u2032 = M\u00B7t. For a quaternion q = (v, w), M\u00B7R\u00B7M is the rotation by the SAME angle about the\r\n/// REFLECTED axis with REVERSED sense (a reflection flips orientation), i.e.\r\n/// q\u2032 = (2(n\u0302\u00B7v)n\u0302 \u2212 v, w); with n\u0302 = \u002BX that is exactly q\u2032 = (x, \u2212y, \u2212z, w), and positions\r\n/// reflect as p\u2032 = p \u2212 2(n\u0302\u00B7p)n\u0302 = (\u2212p\u2093, p_y, p_z).\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003ELocals, not worlds.\u003C/b\u003E Because conjugation is a homomorphism\r\n/// (M\u0302(AB)M\u0302 = (M\u0302AM\u0302)(M\u0302BM\u0302)) and world transforms are products of locals down the\r\n/// hierarchy, mirroring every LOCAL transform and permuting bones by their L\u2194R partner is\r\n/// exactly equivalent to mirroring the FK worlds \u2014 provided the partner permutation is\r\n/// hierarchy-consistent (the partner\u0027s parent is the parent\u0027s partner), which is validated\r\n/// and holds on structurally symmetric humanoid rigs. This avoids FK\u2192inverse-FK float drift\r\n/// entirely, which is what makes the double-mirror identity bit-exact.\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EPairing.\u003C/b\u003E Left/right bones are paired by the rig\u0027s canonical role annotations\r\n/// first (UpperArmL \u2194 UpperArmR, \u2026); role-less bones (twist helpers, IK bones) fall back to\r\n/// \u003Cc\u003E_L\u003C/c\u003E/\u003Cc\u003E_R\u003C/c\u003E name-token pairing (\u003Cc\u003Earm_upper_L_twist0\u003C/c\u003E \u2194\r\n/// \u003Cc\u003Earm_upper_R_twist0\u003C/c\u003E, \u003Cc\u003Efoot_L_IK_target\u003C/c\u003E \u2194 \u003Cc\u003Efoot_R_IK_target\u003C/c\u003E); anything\r\n/// unpaired (center bones: pelvis, spine, neck, head) mirrors in place, which reflects its\r\n/// rotation across the sagittal plane and negates its lateral translation. IK-baked helper\r\n/// bones are NOT re-baked after mirroring: conjugation is a homomorphism, so the mirrored\r\n/// copies of the primary clip\u0027s final helper channels already hang in exactly the mirrored\r\n/// relationship over the mirrored body (re-baking encoded a divergent convention, the Gate 3\r\n/// review\u0027s mechanism 2 of the _M render defect; southpaw project, gate3_review.md 3.4).\r\n/// Channel exclusions on mirrored clips go through \u003Csee cref=\u0022MirrorSafeExclusions\u0022/\u003E.\u003C/para\u003E\r\n/// \u003C/remarks\u003E\r\npublic static class ClipMirror\r\n{\r\n    /// \u003Csummary\u003EMaximum off-axis component magnitude below which the computed lateral axis is\r\n    /// snapped to the exact coordinate axis (authored rigs are axis-aligned; the tiny rest\r\n    /// asymmetries of a real mesh stay far below this).\u003C/summary\u003E\r\n    private const float AxisSnapTolerance = 1e-3f;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Returns the mirrored copy of \u003Cparamref name=\u0022frames\u0022/\u003E (one new list, inputs\r\n    /// untouched): per frame, bone i takes the conjugated local transform of its L\u2194R partner\r\n    /// \u03C3(i). See the class remarks for the math and pairing rules.\r\n    /// \u003C/summary\u003E\r\n    /// \u003Cparam name=\u0022frames\u0022\u003ESolved per-frame local transforms (target skeleton bone order).\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022rig\u0022\u003EThe target rig (skeleton \u002B roles) the frames belong to.\u003C/param\u003E\r\n    /// \u003Cexception cref=\u0022ArgumentException\u0022\u003EThrown when the rig maps a sided role without its\r\n    /// counterpart, the pairing is not hierarchy-consistent, or the character frame is not\r\n    /// computable \u2014 mirroring would silently produce garbage in those cases.\u003C/exception\u003E\r\n    public static List\u003CXForm[]\u003E Mirror(List\u003CXForm[]\u003E frames, TargetRig rig)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(frames);\r\n        ArgumentNullException.ThrowIfNull(rig);\r\n\r\n        var skeleton = rig.Skeleton;\r\n        var lateral = LateralAxis(rig);\r\n        var pair = BuildPairing(rig);\r\n        var fkFix = HierarchyInconsistentBones(rig, pair);\r\n\r\n        var result = new List\u003CXForm[]\u003E(frames.Count);\r\n        var baseWorld = fkFix.Count \u003E 0 ? new XForm[skeleton.Count] : null;\r\n        var mirrorWorld = fkFix.Count \u003E 0 ? new XForm[skeleton.Count] : null;\r\n        foreach (var locals in frames)\r\n        {\r\n            if (locals.Length != skeleton.Count)\r\n                throw new ArgumentException(\r\n                    $\u0022Frame has {locals.Length} bones but the target skeleton has {skeleton.Count}.\u0022,\r\n                    nameof(frames));\r\n\r\n            var mirrored = new XForm[locals.Length];\r\n            for (var i = 0; i \u003C locals.Length; i\u002B\u002B)\r\n            {\r\n                var source = locals[pair[i]];\r\n                mirrored[i] = new XForm(\r\n                    ReflectPoint(source.Pos, lateral),\r\n                    ReflectRotation(source.Rot, lateral));\r\n            }\r\n\r\n            // Hierarchy-inconsistent pairs (the citizen parents arm_elbow_helper_R under\r\n            // arm_lower_R_twist0 while _L hangs under arm_lower_L, the W3a-documented\r\n            // rig quirk): the partner\u0027s conjugated LOCAL under a non-mirrored parent\r\n            // chain misplaces the bone by the parent-chain difference (measured ~1 in on\r\n            // the elbow/knee helpers). Solve their locals by FK so the mirrored WORLD is\r\n            // the exact reflection of the partner\u0027s world (southpaw G8 mirror fix).\r\n            if (fkFix.Count \u003E 0)\r\n            {\r\n                for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n                {\r\n                    var parent = skeleton[i].ParentIndex;\r\n                    baseWorld![i] = parent \u003C 0\r\n                        ? locals[i]\r\n                        : XForm.Compose(baseWorld[parent], locals[i]);\r\n                }\r\n                for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n                {\r\n                    var parent = skeleton[i].ParentIndex;\r\n                    if (fkFix.Contains(i))\r\n                    {\r\n                        var desired = new XForm(\r\n                            ReflectPoint(baseWorld![pair[i]].Pos, lateral),\r\n                            ReflectRotation(baseWorld[pair[i]].Rot, lateral));\r\n                        mirrored[i] = parent \u003C 0\r\n                            ? desired\r\n                            : XForm.ToLocal(mirrorWorld![parent], desired);\r\n                    }\r\n                    mirrorWorld![i] = parent \u003C 0\r\n                        ? mirrored[i]\r\n                        : XForm.Compose(mirrorWorld[parent], mirrored[i]);\r\n                }\r\n            }\r\n            result.Add(mirrored);\r\n        }\r\n        return result;\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Bones whose L/R pairing is NOT hierarchy-consistent (the partner hangs under a\r\n    /// non-mirrored parent). Only constraint-driven helpers can reach this state\r\n    /// (\u003Csee cref=\u0022BuildPairing\u0022/\u003E fails hard for any other bone); their mirrored locals\r\n    /// need the FK solve in \u003Csee cref=\u0022Mirror\u0022/\u003E and their DMX channels must be written\r\n    /// (\u003Csee cref=\u0022MirrorSafeExclusions\u0022/\u003E).\r\n    /// \u003C/summary\u003E\r\n    private static HashSet\u003Cint\u003E HierarchyInconsistentBones(TargetRig rig, int[] pair)\r\n    {\r\n        var skeleton = rig.Skeleton;\r\n        var result = new HashSet\u003Cint\u003E();\r\n        for (var i = 0; i \u003C pair.Length; i\u002B\u002B)\r\n        {\r\n            var parent = skeleton[i].ParentIndex;\r\n            var partnerParent = skeleton[pair[i]].ParentIndex;\r\n            var consistent = parent \u003C 0\r\n                ? partnerParent \u003C 0\r\n                : partnerParent == pair[parent];\r\n            if (!consistent)\r\n                result.Add(i);\r\n        }\r\n        return result;\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Filters a channel-exclusion set for a MIRRORED clip: returns the subset of\r\n    /// \u003Cparamref name=\u0022excluded\u0022/\u003E that is still safe to leave channel-less after mirroring.\r\n    /// A channel-less bone is rendered/baked at its own REST local under its (mirrored)\r\n    /// parent; the mirrored frames instead carry the conjugated rest local of the bone\u0027s\r\n    /// L/R partner. Those agree only when the rig\u0027s rest locals are mirror conjugates\r\n    /// (restLocal(i) == conjugate(restLocal(partner(i)))). Bones breaking that symmetry\r\n    /// (measured on the citizen rig: leg/arm *_twist1 chains and neck_clothing, 19 to 37 cm\r\n    /// off) MUST keep explicit mirrored channels or every data consumer (model compiler\r\n    /// sequence bake, render-side helper evaluation) places them wrong: the Gate 3 review\u0027s\r\n    /// MECHANISM 1 of the _M render defect (southpaw project, gate3_review.md 3.4).\r\n    /// Truly symmetric helpers stay excluded exactly as on primary clips.\r\n    /// \u003C/summary\u003E\r\n    /// \u003Cparam name=\u0022rig\u0022\u003EThe target rig the exclusion set belongs to.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022excluded\u0022\u003EThe primary-clip exclusion set (constraint-driven bones).\u003C/param\u003E\r\n    /// \u003Creturns\u003EThe mirror-safe subset, or null when nothing remains excluded.\u003C/returns\u003E\r\n    public static IReadOnlySet\u003Cint\u003E? MirrorSafeExclusions(TargetRig rig, IReadOnlySet\u003Cint\u003E? excluded)\r\n    {\r\n        if (excluded is null || excluded.Count == 0)\r\n            return excluded;\r\n\r\n        var skeleton = rig.Skeleton;\r\n        var lateral = LateralAxis(rig);\r\n        var pair = BuildPairing(rig);\r\n        var inconsistent = HierarchyInconsistentBones(rig, pair);\r\n\r\n        const float posTolCm = 0.1f;\r\n        const float rotTolDeg = 0.5f;\r\n        var cosTol = MathF.Cos(rotTolDeg * MathF.PI / 360f); // half-angle for quat dot\r\n\r\n        var safe = new HashSet\u003Cint\u003E();\r\n        foreach (var i in excluded)\r\n        {\r\n            // Hierarchy-inconsistent pairs always need explicit channels: their mirrored\r\n            // locals are FK-solved (see Mirror) and no rest local can stand in for them.\r\n            if (inconsistent.Contains(i))\r\n                continue;\r\n\r\n            var own = skeleton[i].RestLocal;\r\n            var partnerRest = skeleton[pair[i]].RestLocal;\r\n            var needed = new XForm(\r\n                ReflectPoint(partnerRest.Pos, lateral),\r\n                ReflectRotation(partnerRest.Rot, lateral));\r\n\r\n            var posOk = (own.Pos - needed.Pos).Length() \u003C= posTolCm;\r\n            var dot = MathF.Abs(\r\n                own.Rot.X * needed.Rot.X \u002B own.Rot.Y * needed.Rot.Y\r\n                \u002B own.Rot.Z * needed.Rot.Z \u002B own.Rot.W * needed.Rot.W);\r\n            var rotOk = dot \u003E= cosTol;\r\n            if (posOk \u0026\u0026 rotOk)\r\n                safe.Add(i);\r\n        }\r\n        return safe.Count \u003E 0 ? safe : null;\r\n    }\r\n\r\n    // ================================================================ mirror plane\r\n\r\n    /// \u003Csummary\u003EThe unit mirror normal: the target character\u0027s lateral axis from rest\r\n    /// geometry, snapped to an exact coordinate axis when within tolerance (bit-exact\r\n    /// reflections, see class remarks).\u003C/summary\u003E\r\n    private static Vector3 LateralAxis(TargetRig rig)\r\n    {\r\n        Vector3 lateral;\r\n        try\r\n        {\r\n            lateral = CharacterFrame.Compute(\r\n                rig.Skeleton, rig.ToMappingResult(), rig.Skeleton.RestWorld).Lateral;\r\n        }\r\n        catch (ArgumentException e)\r\n        {\r\n            throw new ArgumentException(\r\n                $\u0022Cannot mirror: target character frame not computable ({e.Message}).\u0022, e);\r\n        }\r\n\r\n        var a = Vector3.Abs(lateral);\r\n        if (a.Y \u003C= AxisSnapTolerance \u0026\u0026 a.Z \u003C= AxisSnapTolerance)\r\n            return Vector3.UnitX;\r\n        if (a.X \u003C= AxisSnapTolerance \u0026\u0026 a.Z \u003C= AxisSnapTolerance)\r\n            return Vector3.UnitY;\r\n        if (a.X \u003C= AxisSnapTolerance \u0026\u0026 a.Y \u003C= AxisSnapTolerance)\r\n            return Vector3.UnitZ;\r\n        return lateral; // general (non-axis-aligned) rig: exact involution is lost, math is not\r\n    }\r\n\r\n    /// \u003Csummary\u003Ep\u2032 = p \u2212 2(n\u0302\u00B7p)n\u0302. With a snapped axis this is an exact sign flip of one\r\n    /// component (IEEE subtraction of representable values is exact).\u003C/summary\u003E\r\n    private static Vector3 ReflectPoint(Vector3 p, Vector3 n)\r\n        =\u003E p - 2f * Vector3.Dot(p, n) * n;\r\n\r\n    /// \u003Csummary\u003Eq\u2032 = (2(n\u0302\u00B7v)n\u0302 \u2212 v, w): the conjugated rotation M\u00B7R\u00B7M \u2014 same angle, axis\r\n    /// reflected, sense reversed. With n\u0302 = \u002BX this is (x, \u2212y, \u2212z, w). Components are\r\n    /// preserved exactly (no renormalization), keeping the double mirror bit-exact.\u003C/summary\u003E\r\n    private static Quaternion ReflectRotation(Quaternion q, Vector3 n)\r\n    {\r\n        var v = new Vector3(q.X, q.Y, q.Z);\r\n        var reflected = 2f * Vector3.Dot(v, n) * n - v;\r\n        return new Quaternion(reflected.X, reflected.Y, reflected.Z, q.W);\r\n    }\r\n\r\n    // ================================================================ L\u2194R pairing\r\n\r\n    /// \u003Csummary\u003E\r\n    /// \u03C3: bone \u2192 mirror partner (identity for center/unpaired bones). Roles pair first;\r\n    /// role-less bones pair by \u003Cc\u003E_L\u003C/c\u003E/\u003Cc\u003E_R\u003C/c\u003E name tokens. Validated to be an involution\r\n    /// consistent with the hierarchy (\u03C3(parent(i)) == parent(\u03C3(i))).\r\n    /// \u003C/summary\u003E\r\n    private static int[] BuildPairing(TargetRig rig)\r\n    {\r\n        var skeleton = rig.Skeleton;\r\n        var pair = new int[skeleton.Count];\r\n        for (var i = 0; i \u003C pair.Length; i\u002B\u002B)\r\n            pair[i] = i;\r\n\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n        {\r\n            if (rig.RoleOf(i) is { } role)\r\n            {\r\n                if (MirrorRole(role) is not { } mirroredRole)\r\n                    continue; // center role: mirrors in place\r\n                pair[i] = rig.BoneForRole(mirroredRole)\r\n                    ?? throw new ArgumentException(\r\n                        $\u0022Cannot mirror: target rig maps role {role} (\u0027{skeleton[i].Name}\u0027) \u0022\r\n                        \u002B $\u0022but not its counterpart {mirroredRole}.\u0022);\r\n            }\r\n            else\r\n            {\r\n                var partnerName = SwapSideTokens(skeleton[i].Name);\r\n                if (partnerName is null)\r\n                    continue; // no side token: center bone\r\n                var partner = skeleton.IndexOf(partnerName);\r\n                if (partner \u003E= 0)\r\n                    pair[i] = partner;\r\n                // No partner bone: leave in place (e.g. an asymmetric prop bone) \u2014 its\r\n                // rotation still mirrors across the sagittal plane.\r\n            }\r\n        }\r\n\r\n        for (var i = 0; i \u003C pair.Length; i\u002B\u002B)\r\n        {\r\n            // Constraint-driven helper bones are excluded from the output DMX (the model\u0027s\r\n            // AnimConstraintList re-drives them at runtime, see Retargeter.EmitClip\r\n            // ChannelExcludedBones), so their mirrored channels are never written. The shipped\r\n            // s\u0026box citizen rig parents these asymmetrically (arm_elbow_helper_R hangs under\r\n            // arm_lower_R_twist0 while arm_elbow_helper_L hangs under arm_lower_L), a benign\r\n            // data quirk that must not fail the whole mirror. Skip the strict L/R\r\n            // hierarchy-consistency requirement for them: their pairing does not affect any\r\n            // written channel. (W3a fix, southpaw project.)\r\n            if (rig.HelpersAreConstraintDriven \u0026\u0026 rig.ClassOf(i) == BoneClass.ConstraintDriven)\r\n                continue;\r\n\r\n            if (pair[pair[i]] != i)\r\n                throw new ArgumentException(\r\n                    $\u0022Cannot mirror: bone pairing is not symmetric (\u0027{skeleton[i].Name}\u0027 \u2192 \u0022\r\n                    \u002B $\u0022\u0027{skeleton[pair[i]].Name}\u0027 \u2192 \u0027{skeleton[pair[pair[i]]].Name}\u0027).\u0022);\r\n\r\n            var parent = skeleton[i].ParentIndex;\r\n            var partnerParent = skeleton[pair[i]].ParentIndex;\r\n            var consistent = parent \u003C 0\r\n                ? partnerParent \u003C 0\r\n                : partnerParent == pair[parent];\r\n            if (!consistent)\r\n                throw new ArgumentException(\r\n                    $\u0022Cannot mirror: left/right pairing is not hierarchy-consistent \u2014 \u0022\r\n                    \u002B $\u0022\u0027{skeleton[i].Name}\u0027 and partner \u0027{skeleton[pair[i]].Name}\u0027 hang under \u0022\r\n                    \u002B \u0022non-mirrored parents.\u0022);\r\n        }\r\n\r\n        return pair;\r\n    }\r\n\r\n    /// \u003Csummary\u003EUpperArmL \u2192 UpperArmR (and back); null for center roles. Every sided\r\n    /// \u003Csee cref=\u0022BoneRole\u0022/\u003E ends in \u003Cc\u003EL\u003C/c\u003E/\u003Cc\u003ER\u003C/c\u003E; no center role does.\u003C/summary\u003E\r\n    private static BoneRole? MirrorRole(BoneRole role)\r\n    {\r\n        var name = role.ToString();\r\n        var mirroredName = name[^1] switch\r\n        {\r\n            \u0027L\u0027 =\u003E name[..^1] \u002B \u0022R\u0022,\r\n            \u0027R\u0027 =\u003E name[..^1] \u002B \u0022L\u0022,\r\n            _ =\u003E null,\r\n        };\r\n        return mirroredName is not null \u0026\u0026 Enum.TryParse\u003CBoneRole\u003E(mirroredName, out var mirrored)\r\n            ? mirrored\r\n            : null;\r\n    }\r\n\r\n    /// \u003Csummary\u003ESwaps \u003Cc\u003EL\u003C/c\u003E/\u003Cc\u003ER\u003C/c\u003E underscore-delimited name tokens\r\n    /// (\u003Cc\u003Efoot_L_IK_target\u003C/c\u003E \u2192 \u003Cc\u003Efoot_R_IK_target\u003C/c\u003E); null when the name carries no\r\n    /// side token.\u003C/summary\u003E\r\n    private static string? SwapSideTokens(string name)\r\n    {\r\n        var tokens = name.Split(\u0027_\u0027);\r\n        for (var i = 0; i \u003C tokens.Length; i\u002B\u002B)\r\n        {\r\n            tokens[i] = tokens[i] switch\r\n            {\r\n                \u0022L\u0022 =\u003E \u0022R\u0022,\r\n                \u0022R\u0022 =\u003E \u0022L\u0022,\r\n                \u0022l\u0022 =\u003E \u0022r\u0022,\r\n                \u0022r\u0022 =\u003E \u0022l\u0022,\r\n                _ =\u003E tokens[i],\r\n            };\r\n        }\r\n        var result = string.Join(\u0027_\u0027, tokens);\r\n        return string.Equals(result, name, StringComparison.Ordinal) ? null : result;\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"Code/HumanoidRetargeter/Solve/FingerSolver.cs","FileName":"FingerSolver.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Numerics;\r\nusing HumanoidRetargeter.Mapping;\r\nusing HumanoidRetargeter.Maths;\r\n\r\nnamespace HumanoidRetargeter.Solve;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// \u003Csummary\u003E\r\n/// Finger retargeting. Picks one of three strategies per finger chain:\r\n/// \u003Clist type=\u0022number\u0022\u003E\r\n/// \u003Citem\u003E\u003Cb\u003E1:1 absolute copy\u003C/b\u003E (via the \u003Cc\u003EtransferOneToOne\u003C/c\u003E callback into\r\n/// \u003Csee cref=\u0022GeometricSolver\u0022/\u003E\u0027s body path) when the source and target chains are\r\n/// \u003Ci\u003Egeometrically identical\u003C/i\u003E \u2014 same mapped role set, same canonical frames, same\r\n/// normalized rest rotations. This is the same-rig round-trip case and is lossless (exact\r\n/// identity, twist included).\u003C/item\u003E\r\n/// \u003Citem\u003E\u003Cb\u003EDirection matching\u003C/b\u003E when the phalanx counts match ordinally but the rigs\r\n/// differ (the common cross-rig case, e.g. Mixamo Prox/Mid/Dist onto the s\u0026amp;box finger\r\n/// with its extra metacarpal \u2014 which keeps its rest local; a source metacarpal\u0027s rotation is\r\n/// implicit in the proximal\u0027s absolute direction). Each target phalanx is swung \u2014 shortest\r\n/// arc, rotation axis \u22A5 the finger axis, hence \u003Cb\u003Ezero twist by construction\u003C/b\u003E \u2014 so that its\r\n/// segment direction matches the source phalanx\u0027s direction in character-frame coordinates\r\n/// exactly. Curl and splay are both captured by the direction; the source\u0027s axial twist is\r\n/// dropped (hinge-joint noise; copying it absolutely would read as roll through the\r\n/// inter-phalanx canonical mismatch between rigs, measured up to ~12\u00B0 on thumbs).\u003C/item\u003E\r\n/// \u003Citem\u003E\u003Cb\u003EProportional redistribution\u003C/b\u003E when phalanx counts differ (e.g. a two-phalanx\r\n/// source finger): per-phalanx local curls \u2014 swing-twist about the canonical hinge Y of\r\n/// \u003Cc\u003E\u03BB_b = C_b\u207B\u00B9\u00B7(\u0394R_prev\u207B\u00B9\u00B7\u0394R_b)\u00B7C_b\u003C/c\u003E \u2014 are summed over the source chain (metacarpal\r\n/// included) and redistributed over the target phalanges proportional to rest segment\r\n/// lengths; splay (metacarpal \u002B proximal, swing-twist about canonical Z) goes 100% to the\r\n/// target proximal; the X-twist residual is dropped.\u003C/item\u003E\r\n/// \u003C/list\u003E\r\n/// In every mode target world deltas rebuild hierarchically from the solved target hand:\r\n/// \u003Cc\u003E\u0394R_i = \u0394R_{i-1} \u00B7 (C_i \u00B7 \u03BB_i \u00B7 C_i\u207B\u00B9)\u003C/c\u003E, then \u003Cc\u003EW_i = \u0394R_i \u00B7 R_tgtNormRest,i\u003C/c\u003E.\r\n/// Instances are per-solve and not thread-safe.\r\n/// \u003C/summary\u003E\r\ninternal sealed class FingerSolver\r\n{\r\n    /// \u003Csummary\u003ETwo canonical frames / rest rotations within this angle count as identical\r\n    /// (same-rig detection for the lossless 1:1 path); cross-rig differences are degrees.\u003C/summary\u003E\r\n    private const float SameRigToleranceRad = 1e-3f;\r\n\r\n    private enum ChainMode\r\n    {\r\n        DirectionMatch,\r\n        Proportional,\r\n    }\r\n\r\n    private readonly struct SourcePhalanx\r\n    {\r\n        public required int Slot { get; init; }\r\n        public required Quaternion C { get; init; }\r\n        public required Quaternion CInv { get; init; }\r\n        public required bool TakesSplay { get; init; }\r\n    }\r\n\r\n    private readonly struct Recipient\r\n    {\r\n        public required int TgtBone { get; init; }\r\n        public required Quaternion C { get; init; }\r\n        public required Quaternion CInv { get; init; }\r\n        public required Quaternion RestRot { get; init; }\r\n        public required float Weight { get; init; }\r\n        public required bool Splay { get; init; }\r\n    }\r\n\r\n    private sealed class Chain\r\n    {\r\n        public required ChainMode Mode { get; init; }\r\n        public required int SrcHandSlot { get; init; }\r\n        public required int TgtHandBone { get; init; }\r\n        public required Quaternion TgtHandNormRestRotInv { get; init; }\r\n        public required SourcePhalanx[] Sources { get; init; }\r\n        public required Recipient[] Recipients { get; init; }\r\n    }\r\n\r\n    private readonly List\u003CChain\u003E _chains;\r\n    private readonly Quaternion _chrSrcInv;\r\n    private readonly Quaternion _chrTgt;\r\n\r\n    private FingerSolver(List\u003CChain\u003E chains, Quaternion chrSrcInv, Quaternion chrTgt)\r\n    {\r\n        _chains = chains;\r\n        _chrSrcInv = chrSrcInv;\r\n        _chrTgt = chrTgt;\r\n    }\r\n\r\n    // ---------------------------------------------------------------- role tables\r\n\r\n    private static readonly BoneRole[][] ChainRoles = BuildChainRoles();\r\n    private static readonly HashSet\u003CBoneRole\u003E FingerRoleSet = ChainRoles.SelectMany(c =\u003E c.Skip(1)).ToHashSet();\r\n\r\n    private static BoneRole[][] BuildChainRoles()\r\n    {\r\n        var chains = new List\u003CBoneRole[]\u003E();\r\n        foreach (var side in new[] { \u0022L\u0022, \u0022R\u0022 })\r\n        {\r\n            foreach (var finger in new[] { \u0022Thumb\u0022, \u0022Index\u0022, \u0022Middle\u0022, \u0022Ring\u0022, \u0022Pinky\u0022 })\r\n            {\r\n                // Element 0 is the hand the chain hangs off; 1.. are Meta/Prox/Mid/Dist.\r\n                chains.Add(new[]\r\n                {\r\n                    Enum.Parse\u003CBoneRole\u003E(\u0022Hand\u0022 \u002B side),\r\n                    Enum.Parse\u003CBoneRole\u003E(finger \u002B \u0022Meta\u0022 \u002B side),\r\n                    Enum.Parse\u003CBoneRole\u003E(finger \u002B \u0022Prox\u0022 \u002B side),\r\n                    Enum.Parse\u003CBoneRole\u003E(finger \u002B \u0022Mid\u0022 \u002B side),\r\n                    Enum.Parse\u003CBoneRole\u003E(finger \u002B \u0022Dist\u0022 \u002B side),\r\n                });\r\n            }\r\n        }\r\n        return chains.ToArray();\r\n    }\r\n\r\n    /// \u003Csummary\u003ETrue for the 40 per-finger segment roles (Meta/Prox/Mid/Dist \u00D7 finger \u00D7 side).\u003C/summary\u003E\r\n    public static bool IsFingerRole(BoneRole role) =\u003E FingerRoleSet.Contains(role);\r\n\r\n    // ---------------------------------------------------------------- build\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Builds the per-chain plans. Geometrically identical chains are reported through\r\n    /// \u003Cparamref name=\u0022transferOneToOne\u0022/\u003E instead of being planned here. Returns null when\r\n    /// every mapped chain took that path (or none is mapped).\r\n    /// \u003C/summary\u003E\r\n    public static FingerSolver? Build(\r\n        MappingResult sourceMap,\r\n        CanonicalFrames srcCanon,\r\n        IReadOnlyList\u003CXForm\u003E srcNormRest,\r\n        Func\u003CBoneRole, int?\u003E tgtBoneForRole,\r\n        CanonicalFrames tgtCanon,\r\n        IReadOnlyList\u003CXForm\u003E tgtNormRest,\r\n        Quaternion chrSrcInv,\r\n        Quaternion chrTgt,\r\n        Func\u003Cint, int\u003E registerSlot,\r\n        Action\u003CBoneRole\u003E transferOneToOne)\r\n    {\r\n        var chains = new List\u003CChain\u003E();\r\n        foreach (var chainRoles in ChainRoles)\r\n        {\r\n            var handRole = chainRoles[0];\r\n            var metaRole = chainRoles[1];\r\n            var proxRole = chainRoles[2];\r\n            var segments = chainRoles.Skip(1).ToArray();\r\n\r\n            var srcRoles = segments\r\n                .Where(r =\u003E sourceMap.RoleToBone.ContainsKey(r) \u0026\u0026 srcCanon.Has(r))\r\n                .ToArray();\r\n            var tgtRoles = segments\r\n                .Where(r =\u003E tgtBoneForRole(r) is not null \u0026\u0026 tgtCanon.Has(r))\r\n                .ToArray();\r\n            if (srcRoles.Length == 0 || tgtRoles.Length == 0)\r\n                continue;\r\n\r\n            if (srcRoles.SequenceEqual(tgtRoles) \u0026\u0026 ChainsCoincide(\r\n                srcRoles, sourceMap, srcCanon, srcNormRest, tgtBoneForRole, tgtCanon, tgtNormRest))\r\n            {\r\n                foreach (var role in srcRoles)\r\n                    transferOneToOne(role);\r\n                continue;\r\n            }\r\n\r\n            var srcPhalanges = srcRoles.Where(r =\u003E r != metaRole).ToArray();\r\n            var tgtPhalanges = tgtRoles.Where(r =\u003E r != metaRole).ToArray();\r\n            var recipientRoles = tgtPhalanges.Length \u003E 0 ? tgtPhalanges : tgtRoles;\r\n            var mode = srcPhalanges.Length == recipientRoles.Length \u0026\u0026 srcPhalanges.Length \u003E 0\r\n                ? ChainMode.DirectionMatch\r\n                : ChainMode.Proportional;\r\n\r\n            // Direction matching consumes only the non-meta phalanges (the metacarpal\u0027s\r\n            // motion is implicit in the proximal\u0027s absolute direction); redistribution\r\n            // decomposes every mapped source segment including the metacarpal.\r\n            var sourceRolesUsed = mode == ChainMode.DirectionMatch ? srcPhalanges : srcRoles;\r\n            var sources = sourceRolesUsed.Select(r =\u003E\r\n            {\r\n                var c = srcCanon.WorldFrameOf(r);\r\n                return new SourcePhalanx\r\n                {\r\n                    Slot = registerSlot(sourceMap.RoleToBone[r]),\r\n                    C = c,\r\n                    CInv = Quaternion.Conjugate(c),\r\n                    TakesSplay = r == metaRole || r == proxRole,\r\n                };\r\n            }).ToArray();\r\n\r\n            var weights = SegmentWeights(tgtRoles, recipientRoles, tgtBoneForRole, tgtNormRest);\r\n            var recipients = recipientRoles.Select((r, i) =\u003E\r\n            {\r\n                var bone = tgtBoneForRole(r)!.Value;\r\n                var c = tgtCanon.WorldFrameOf(r);\r\n                return new Recipient\r\n                {\r\n                    TgtBone = bone,\r\n                    C = c,\r\n                    CInv = Quaternion.Conjugate(c),\r\n                    RestRot = tgtNormRest[bone].Rot,\r\n                    Weight = weights[i],\r\n                    Splay = i == 0,\r\n                };\r\n            }).ToArray();\r\n\r\n            var tgtHand = tgtBoneForRole(handRole);\r\n            chains.Add(new Chain\r\n            {\r\n                Mode = mode,\r\n                SrcHandSlot = sourceMap.RoleToBone.TryGetValue(handRole, out var srcHand)\r\n                    ? registerSlot(srcHand)\r\n                    : -1,\r\n                TgtHandBone = tgtHand ?? -1,\r\n                TgtHandNormRestRotInv = tgtHand is int h\r\n                    ? Quaternion.Conjugate(tgtNormRest[h].Rot)\r\n                    : Quaternion.Identity,\r\n                Sources = sources,\r\n                Recipients = recipients,\r\n            });\r\n        }\r\n\r\n        return chains.Count \u003E 0 ? new FingerSolver(chains, chrSrcInv, chrTgt) : null;\r\n    }\r\n\r\n    /// \u003Csummary\u003ESame-rig detection: every chain member\u0027s canonical frame and normalized rest\r\n    /// rotation agree between source and target (within float noise). Only then is the 1:1\r\n    /// absolute copy lossless.\u003C/summary\u003E\r\n    private static bool ChainsCoincide(\r\n        BoneRole[] roles, MappingResult sourceMap, CanonicalFrames srcCanon,\r\n        IReadOnlyList\u003CXForm\u003E srcNormRest, Func\u003CBoneRole, int?\u003E tgtBoneForRole,\r\n        CanonicalFrames tgtCanon, IReadOnlyList\u003CXForm\u003E tgtNormRest)\r\n    {\r\n        foreach (var role in roles)\r\n        {\r\n            var srcBone = sourceMap.RoleToBone[role];\r\n            var tgtBone = tgtBoneForRole(role)!.Value;\r\n            if (MathQ.AngleBetween(srcCanon.WorldFrameOf(role), tgtCanon.WorldFrameOf(role)) \u003E SameRigToleranceRad\r\n                || MathQ.AngleBetween(srcNormRest[srcBone].Rot, tgtNormRest[tgtBone].Rot) \u003E SameRigToleranceRad)\r\n            {\r\n                return false;\r\n            }\r\n        }\r\n        return true;\r\n    }\r\n\r\n    /// \u003Csummary\u003ENormalized rest segment lengths of the recipient phalanges (the proportional\r\n    /// curl weights). The distal segment, having no chain child, is estimated as 0.8\u00D7 its\r\n    /// preceding segment.\u003C/summary\u003E\r\n    private static float[] SegmentWeights(\r\n        BoneRole[] tgtRoles, BoneRole[] recipientRoles,\r\n        Func\u003CBoneRole, int?\u003E tgtBoneForRole, IReadOnlyList\u003CXForm\u003E tgtNormRest)\r\n    {\r\n        var positions = tgtRoles.Select(r =\u003E tgtNormRest[tgtBoneForRole(r)!.Value].Pos).ToArray();\r\n        var weights = new float[recipientRoles.Length];\r\n        for (var i = 0; i \u003C recipientRoles.Length; i\u002B\u002B)\r\n        {\r\n            var j = Array.IndexOf(tgtRoles, recipientRoles[i]);\r\n            weights[i] = j \u002B 1 \u003C positions.Length\r\n                ? (positions[j \u002B 1] - positions[j]).Length()\r\n                : j \u003E 0 ? 0.8f * (positions[j] - positions[j - 1]).Length() : 1f;\r\n        }\r\n\r\n        var sum = weights.Sum();\r\n        if (sum \u003C= 1e-6f)\r\n            return Enumerable.Repeat(1f / weights.Length, weights.Length).ToArray();\r\n        for (var i = 0; i \u003C weights.Length; i\u002B\u002B)\r\n            weights[i] /= sum;\r\n        return weights;\r\n    }\r\n\r\n    // ---------------------------------------------------------------- per frame\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Solves the planned chains for one frame. \u003Cparamref name=\u0022srcDeltas\u0022/\u003E holds the\r\n    /// registered source world rotation deltas (from normalized rest); solved target world\r\n    /// rotations are written into \u003Cparamref name=\u0022rot\u0022/\u003E/\u003Cparamref name=\u0022solved\u0022/\u003E. The target\r\n    /// hands must already be solved (body pass runs first).\r\n    /// \u003C/summary\u003E\r\n    public void Apply(Quaternion[] srcDeltas, bool[] solved, Quaternion[] rot)\r\n    {\r\n        foreach (var chain in _chains)\r\n        {\r\n            var acc = chain.TgtHandBone \u003E= 0 \u0026\u0026 solved[chain.TgtHandBone]\r\n                ? MathQ.Normalize(rot[chain.TgtHandBone] * chain.TgtHandNormRestRotInv)\r\n                : Quaternion.Identity;\r\n\r\n            if (chain.Mode == ChainMode.DirectionMatch)\r\n                ApplyDirectionMatch(chain, srcDeltas, acc, solved, rot);\r\n            else\r\n                ApplyProportional(chain, srcDeltas, acc, solved, rot);\r\n        }\r\n    }\r\n\r\n    private void ApplyDirectionMatch(\r\n        Chain chain, Quaternion[] srcDeltas, Quaternion acc, bool[] solved, Quaternion[] rot)\r\n    {\r\n        for (var i = 0; i \u003C chain.Recipients.Length; i\u002B\u002B)\r\n        {\r\n            var sp = chain.Sources[i];\r\n            var rc = chain.Recipients[i];\r\n\r\n            // Source phalanx direction in character coords; re-expressed in the target world,\r\n            // then relative to the already-reconstructed parent delta, then in the phalanx\u0027s\r\n            // canonical frame \u2014 where the rest direction is unit X.\r\n            var srcAbs = MathQ.Normalize(_chrSrcInv * srcDeltas[sp.Slot] * sp.C);\r\n            var dirChr = Vector3.Transform(Vector3.UnitX, srcAbs);\r\n            var dirTgtWorld = Vector3.Transform(dirChr, _chrTgt);\r\n            var dirLocal = Vector3.Transform(dirTgtWorld, Quaternion.Conjugate(acc));\r\n            var dirCanon = Vector3.Transform(dirLocal, rc.CInv);\r\n\r\n            // Shortest-arc swing X -\u003E dir: rotation axis \u22A5 X, so it carries zero finger-axis\r\n            // twist by construction.\r\n            var swing = MathQ.FromTo(Vector3.UnitX, dirCanon);\r\n\r\n            acc = MathQ.Normalize(acc * (rc.C * swing * rc.CInv));\r\n            rot[rc.TgtBone] = MathQ.Normalize(acc * rc.RestRot);\r\n            solved[rc.TgtBone] = true;\r\n        }\r\n    }\r\n\r\n    private static void ApplyProportional(\r\n        Chain chain, Quaternion[] srcDeltas, Quaternion acc, bool[] solved, Quaternion[] rot)\r\n    {\r\n        // Decompose: total local curl over the chain, splay from metacarpal \u002B proximal.\r\n        var prev = chain.SrcHandSlot \u003E= 0 ? srcDeltas[chain.SrcHandSlot] : Quaternion.Identity;\r\n        float totalCurl = 0f, splay = 0f;\r\n        foreach (var sp in chain.Sources)\r\n        {\r\n            var dr = srcDeltas[sp.Slot];\r\n            var local = MathQ.Normalize(Quaternion.Conjugate(prev) * dr);\r\n            var canon = MathQ.Normalize(sp.CInv * local * sp.C);\r\n\r\n            MathQ.SwingTwist(canon, Vector3.UnitY, out var swing, out var curlQ);\r\n            totalCurl \u002B= SignedAngle(curlQ, Vector3.UnitY);\r\n\r\n            if (sp.TakesSplay)\r\n            {\r\n                MathQ.SwingTwist(swing, Vector3.UnitZ, out _, out var splayQ);\r\n                splay \u002B= SignedAngle(splayQ, Vector3.UnitZ);\r\n            }\r\n\r\n            prev = dr;\r\n        }\r\n\r\n        foreach (var rc in chain.Recipients)\r\n        {\r\n            var mu = Quaternion.CreateFromAxisAngle(Vector3.UnitY, totalCurl * rc.Weight);\r\n            if (rc.Splay)\r\n                mu = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, splay) * mu;\r\n\r\n            acc = MathQ.Normalize(acc * (rc.C * mu * rc.CInv));\r\n            rot[rc.TgtBone] = MathQ.Normalize(acc * rc.RestRot);\r\n            solved[rc.TgtBone] = true;\r\n        }\r\n    }\r\n\r\n    /// \u003Csummary\u003ESigned rotation angle of an axis-aligned twist quaternion about\r\n    /// \u003Cparamref name=\u0022axis\u0022/\u003E, wrapped to (\u2212\u03C0, \u03C0].\u003C/summary\u003E\r\n    private static float SignedAngle(Quaternion twist, Vector3 axis)\r\n    {\r\n        var s = twist.X * axis.X \u002B twist.Y * axis.Y \u002B twist.Z * axis.Z;\r\n        var angle = 2f * MathF.Atan2(s, twist.W);\r\n        if (angle \u003E MathF.PI)\r\n            angle -= 2f * MathF.PI;\r\n        else if (angle \u003C -MathF.PI)\r\n            angle \u002B= 2f * MathF.PI;\r\n        return angle;\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"Code/HumanoidRetargeter/Solve/SolveOptions.cs","FileName":"SolveOptions.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\nusing System.Collections.Generic;\r\nusing HumanoidRetargeter.Mapping;\r\n\r\nnamespace HumanoidRetargeter.Solve;\r\n\r\n/// \u003Csummary\u003EHow a mapped role\u0027s rotation is transferred by the \u003Csee cref=\u0022GeometricSolver\u0022/\u003E.\u003C/summary\u003E\r\npublic enum RoleTransferMode\r\n{\r\n    /// \u003Csummary\u003E\r\n    /// Absolute canonical-orientation matching: the target\u0027s animated chain direction is\r\n    /// driven to \u003Cb\u003Eequal\u003C/b\u003E the source\u0027s (in character-frame coordinates). Right for limbs\r\n    /// and the spine \u2014 the pose IS the direction \u2014 but it also imposes the source rig\u0027s rest\r\n    /// proportions/posture on roles whose rest directions legitimately differ between rigs.\r\n    /// \u003C/summary\u003E\r\n    AbsoluteDirection,\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Rest-relative delta: the source\u0027s canonical-space rotation \u003Ci\u003Edelta from its own\r\n    /// normalized rest\u003C/i\u003E is replayed onto the \u003Cb\u003Etarget\u0027s\u003C/b\u003E normalized rest\r\n    /// (\u003Cc\u003EW_t(f) = C_t\u00B7\u0394C(f)\u00B7C_t\u207B\u00B9\u00B7R_tgtNormRest\u003C/c\u003E with\r\n    /// \u003Cc\u003E\u0394C(f) = C_s\u207B\u00B9\u00B7\u0394R(f)\u00B7C_s\u003C/c\u003E). The target keeps its own rest carriage (shoulder\r\n    /// line height, neck-base angle) and moves with the source. Identical to\r\n    /// \u003Csee cref=\u0022AbsoluteDirection\u0022/\u003E when source and target rigs coincide.\r\n    /// \u003C/summary\u003E\r\n    DeltaFromRest,\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Character-space delta: the source\u0027s world-rotation delta from its normalized rest is\r\n    /// re-expressed in character coordinates and applied to the \u003Cb\u003Etarget\u0027s\u003C/b\u003E normalized\r\n    /// rest (\u003Cc\u003EW_t(f) = M\u00B7\u0394R(f)\u00B7M\u207B\u00B9\u00B7R_tgtNormRest\u003C/c\u003E with \u003Cc\u003EM = Q_tgt\u00B7Q_src\u207B\u00B9\u003C/c\u003E, the\r\n    /// same character basis change \u003Csee cref=\u0022AbsoluteDirection\u0022/\u003E premultiplies). Like\r\n    /// \u003Csee cref=\u0022DeltaFromRest\u0022/\u003E the target keeps its own rest carriage, but the delta\r\n    /// keeps its \u003Ci\u003Eworld\u003C/i\u003E rotation axes instead of being remapped through the per-role\r\n    /// canonical frames \u2014 the faithful replay when the rigs\u0027 rest chain directions diverge\r\n    /// so far that canonical-axis remapping would tilt every rotation axis by that\r\n    /// divergence (measured 23\u201344\u00B0 on feet: CMU/ARP ankle anatomy vs the s\u0026amp;box rig\u0027s\r\n    /// steep ankle, where canonical remapping mis-pitched planted feet by up to 47\u00B0).\r\n    /// Identical to the other modes when source and target rigs coincide.\r\n    /// \u003C/summary\u003E\r\n    CharacterDeltaFromRest,\r\n}\r\n\r\n/// \u003Csummary\u003EOptions controlling a single retarget solve (one clip \u2192 one output clip).\u003C/summary\u003E\r\npublic sealed class SolveOptions\r\n{\r\n    /// \u003Csummary\u003E\r\n    /// Default per-role transfer modes: shoulder girdle and neck carriage are\r\n    /// \u003Csee cref=\u0022RoleTransferMode.DeltaFromRest\u0022/\u003E (each rig\u0027s clavicle line / neck-base\r\n    /// direction is rig anatomy, not pose \u2014 absolute matching was measured to drag the\r\n    /// s\u0026amp;box shoulders 6\u201328\u00B0 toward the source\u0027s flatter/lower clavicle line and is the\r\n    /// \u0022low shoulders, hunched neck\u0022 artifact), and feet are\r\n    /// \u003Csee cref=\u0022RoleTransferMode.CharacterDeltaFromRest\u0022/\u003E (a rest foot\u2192toe direction is\r\n    /// ankle anatomy too \u2014 rigs diverge 11\u201344\u00B0 from the s\u0026amp;box rig\u0027s steep ankle, so\r\n    /// absolute matching pitched planted feet up to 25\u00B0 off flat, the \u0022feet bent\r\n    /// upward/inward\u0022 artifact; the character-space delta keeps the rotation\u0027s world axes,\r\n    /// which canonical-frame remapping would tilt by that same divergence). The head is\r\n    /// \u003Csee cref=\u0022RoleTransferMode.CharacterDeltaFromRest\u0022/\u003E for the same reason: the rest\r\n    /// neck\u2192head direction is head-joint-placement anatomy (measured 0\u201327\u00B0 forward lean\r\n    /// across neutral-rest rigs vs the s\u0026amp;box rig\u0027s 25.5\u00B0), so the target keeps its own\r\n    /// neutral skull attitude and replays the source\u0027s attitude \u003Ci\u003Echanges\u003C/i\u003E \u2014 for the\r\n    /// head this computes exactly what the previous virtual-frame absolute matching did.\r\n    /// Two solver fallbacks adjust these defaults per rig pair: on a toe-less source the\r\n    /// foot entries become \u003Csee cref=\u0022RoleTransferMode.DeltaFromRest\u0022/\u003E (virtual-foot\r\n    /// fallback), and a source whose normalized rest head attitude is implausible as a\r\n    /// neutral carriage (a posed bind \u2014 e.g. a chin-down/tilted fighting-stance rest,\r\n    /// measured 40.7\u00B0 forward / 16.9\u00B0 lateral on such a rig where the delta replay read\r\n    /// ~12\u00B0 \u0022looking up at an angle\u0022) switches the head to\r\n    /// \u003Csee cref=\u0022RoleTransferMode.AbsoluteDirection\u0022/\u003E so the gaze follows the source\r\n    /// absolutely instead of replaying deltas from a posed reference (see the\r\n    /// \u003Csee cref=\u0022GeometricSolver\u0022/\u003E remarks for both). Everything else (limbs, spine,\r\n    /// toes, fingers) stays absolute: there the worldspace direction IS the pose.\r\n    /// \u003C/summary\u003E\r\n    public static IReadOnlyDictionary\u003CBoneRole, RoleTransferMode\u003E DefaultTransferModes { get; } =\r\n        new Dictionary\u003CBoneRole, RoleTransferMode\u003E\r\n        {\r\n            [BoneRole.ClavicleL] = RoleTransferMode.DeltaFromRest,\r\n            [BoneRole.ClavicleR] = RoleTransferMode.DeltaFromRest,\r\n            [BoneRole.Neck] = RoleTransferMode.DeltaFromRest,\r\n            [BoneRole.Head] = RoleTransferMode.CharacterDeltaFromRest,\r\n            [BoneRole.FootL] = RoleTransferMode.CharacterDeltaFromRest,\r\n            [BoneRole.FootR] = RoleTransferMode.CharacterDeltaFromRest,\r\n        };\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Per-role transfer modes. Null (default) = \u003Csee cref=\u0022DefaultTransferModes\u0022/\u003E plus the\r\n    /// solver\u0027s fallback heuristics (a toe-less source\u0027s virtual foot direction overrides\r\n    /// the foot default to \u003Csee cref=\u0022RoleTransferMode.DeltaFromRest\u0022/\u003E, and a posed-rest\r\n    /// source head overrides the head default to\r\n    /// \u003Csee cref=\u0022RoleTransferMode.AbsoluteDirection\u0022/\u003E \u2014 see the\r\n    /// \u003Csee cref=\u0022GeometricSolver\u0022/\u003E remarks). A non-null map REPLACES the defaults entirely\r\n    /// and disables every fallback heuristic: each role uses exactly the mode in the map, and\r\n    /// roles absent from it are \u003Csee cref=\u0022RoleTransferMode.AbsoluteDirection\u0022/\u003E. Pass an\r\n    /// empty dictionary for fully absolute (legacy) behavior \u2014 API callers supplying a map\r\n    /// opt out of all heuristics.\r\n    /// \u003C/summary\u003E\r\n    public IReadOnlyDictionary\u003CBoneRole, RoleTransferMode\u003E? TransferModes { get; init; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Scale applied to the pelvis translation components perpendicular to the character up\r\n    /// direction. Null (default) = automatic: target hip height / source hip height, both\r\n    /// measured on the normalized rests.\r\n    /// \u003C/summary\u003E\r\n    public float? HipScaleHorizontal { get; init; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Scale applied to the pelvis translation component along the character up direction.\r\n    /// Null (default) = the same automatic hip-height ratio as \u003Csee cref=\u0022HipScaleHorizontal\u0022/\u003E.\r\n    /// \u003C/summary\u003E\r\n    public float? HipScaleVertical { get; init; }\r\n\r\n    /// \u003Csummary\u003EWhether finger roles are transferred; when false, target finger bones keep\r\n    /// their rest locals.\u003C/summary\u003E\r\n    public bool TransferFingers { get; init; } = true;\r\n\r\n    /// \u003Csummary\u003EOutput clip name; null = the source clip\u0027s name.\u003C/summary\u003E\r\n    public string? ClipName { get; init; }\r\n\r\n    /// \u003Csummary\u003EIndex of the source clip to retarget (\u003Cc\u003ESourceScene.Clips\u003C/c\u003E).\u003C/summary\u003E\r\n    public int ClipIndex { get; init; }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"HumanoidRetargeter/Cleanup/FootGroundAlign.cs","FileName":"FootGroundAlign.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidRetargeter.Maths;\r\nusing SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidRetargeter.Cleanup;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// \u003Csummary\u003ETunables for the grounded-foot stance recalibration pass.\u003C/summary\u003E\r\npublic sealed class FootGroundAlignOptions\r\n{\r\n    /// \u003Csummary\u003E\r\n    /// Dead zone (degrees): measured stance offsets at or below this are genuine planted\r\n    /// articulation (heel-roll bias, natural lean \u2014 measured 2\u20134\u00B0 on well-rested rigs and\r\n    /// on citizen clips) and are left untouched, keeping the transfer byte-faithful there.\r\n    /// Only offsets beyond it are clearly rest-pose artifacts (measured 12\u201325\u00B0 on the\r\n    /// repro rig) and get recalibrated.\r\n    /// \u003C/summary\u003E\r\n    public float MinCorrectionDeg { get; set; } = 8f;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Maximum mean sole deviation (degrees) a plant may show and still count as a STANCE\r\n    /// for the offset measurement. Plants beyond this are not standing on the sole (crawls,\r\n    /// kneels, prone contact \u2014 measured 60\u201390\u00B0 there) and are excluded; genuine rest-pose\r\n    /// stance artifacts measure well below it (largest seen: 27\u00B0).\r\n    /// \u003C/summary\u003E\r\n    public float MaxStanceDeviationDeg { get; set; } = 35f;\r\n}\r\n\r\n/// \u003Csummary\u003EPer-foot results of a \u003Csee cref=\u0022FootGroundAlign.Apply\u0022/\u003E run.\u003C/summary\u003E\r\npublic sealed class FootGroundAlignFootReport\r\n{\r\n    /// \u003Csummary\u003EPlants that contributed to the stance measurement.\u003C/summary\u003E\r\n    public int StancePlants { get; set; }\r\n\r\n    /// \u003Csummary\u003EPlants excluded as non-stance (mean sole deviation beyond\r\n    /// \u003Csee cref=\u0022FootGroundAlignOptions.MaxStanceDeviationDeg\u0022/\u003E).\u003C/summary\u003E\r\n    public int SkippedPlants { get; set; }\r\n\r\n    /// \u003Csummary\u003EMeasured planted sole offset from the ground plane, degrees (0 when no\r\n    /// stance plants exist).\u003C/summary\u003E\r\n    public float MeasuredOffsetDeg { get; set; }\r\n\r\n    /// \u003Csummary\u003EFoot correction applied to every frame, degrees (0 = inside the dead zone,\r\n    /// nothing changed).\u003C/summary\u003E\r\n    public float AppliedFootDeg { get; set; }\r\n\r\n    /// \u003Csummary\u003EToe correction applied to every frame, degrees.\u003C/summary\u003E\r\n    public float AppliedToeDeg { get; set; }\r\n}\r\n\r\n/// \u003Csummary\u003EResults of a \u003Csee cref=\u0022FootGroundAlign.Apply\u0022/\u003E run.\u003C/summary\u003E\r\npublic sealed class FootGroundAlignReport\r\n{\r\n    /// \u003Csummary\u003ELeft-foot results.\u003C/summary\u003E\r\n    public required FootGroundAlignFootReport Left { get; init; }\r\n\r\n    /// \u003Csummary\u003ERight-foot results.\u003C/summary\u003E\r\n    public required FootGroundAlignFootReport Right { get; init; }\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Grounded-foot stance recalibration: measures how far the foot\u0027s SOLE sits from the ground\r\n/// plane while planted, and \u2014 when that offset is clearly a rest-pose artifact \u2014 rotates it\r\n/// out with one constant per foot, applied to every frame of the clip.\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EWhy a cleanup pass.\u003C/b\u003E The solver transfers feet as rest-relative deltas\r\n/// (\u003Csee cref=\u0022Solve.RoleTransferMode.CharacterDeltaFromRest\u0022/\u003E), so the target keeps its own\r\n/// ankle anatomy \u2014 correct whenever the source\u0027s rest pose is a flat-footed stance (the delta\r\n/// is then \u0022deviation from standing\u0022). Some rigs ship a NON-stance rest (measured: an\r\n/// Auto-Rig-Pro export whose rest foot sits 12\u201325\u00B0 from its planted stance), and that constant\r\n/// offset rides into every frame of the replay \u2014 planted feet hover toe-down/heel-up. What a\r\n/// stance actually looks like is animation evidence (planted phases), which a per-frame\r\n/// solver cannot see, so the recalibration lives here.\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EMeasurement.\u003C/b\u003E Per foot: over every planted frame, the sole normal = rest up\r\n/// carried by the foot\u0027s world delta from the target bind rest (whose feet stand on the\r\n/// ground by construction); plants whose own mean normal sits beyond\r\n/// \u003Csee cref=\u0022FootGroundAlignOptions.MaxStanceDeviationDeg\u0022/\u003E are excluded (crawl/kneel/prone\r\n/// contact is not a stance). The pooled mean normal\u0027s deviation from up is the stance\r\n/// offset.\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003ECorrection.\u003C/b\u003E Offsets inside \u003Csee cref=\u0022FootGroundAlignOptions.MinCorrectionDeg\u0022/\u003E\r\n/// are genuine articulation \u2014 nothing is changed (well-rested rigs and same-rig round trips\r\n/// stay byte-identical through this pass). Beyond it, the shortest-arc rotation taking the\r\n/// pooled normal back to up (pitch\u002Broll only \u2014 yaw/toe-out is pose and follows the source)\r\n/// premultiplies the foot\u0027s world rotation on EVERY frame: a rest artifact is constant, so\r\n/// the fix is too \u2014 within-plant heel-roll, swing styling and frame-to-frame continuity are\r\n/// preserved exactly, and no blending is needed. The toe then receives its own residual\r\n/// constant measured on top of the corrected foot (it neither double-rotates with the foot\r\n/// fix nor inherits the source toe\u0027s own rest artifact). Corrections rotate bones about\r\n/// their own joints: ankle positions are untouched, so the pass composes freely with the\r\n/// \u003Csee cref=\u0022FootPlant\u0022/\u003E position pinning (which preserves foot world rotations).\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EPlant intervals come from the caller\u003C/b\u003E (the pipeline detects them on the\r\n/// SOURCE clip via \u003Csee cref=\u0022FootPlant.DetectPlantIntervals\u0022/\u003E \u2014 ground truth, immune to\r\n/// the hip-height rescaling that can push target-side trajectories outside the cm-tuned\r\n/// Kovar thresholds). So does the decision to run at all: the pipeline invokes this pass\r\n/// only when the source\u0027s normalized rest is implausible as a flat stance (toe at/above\r\n/// ankle level or asymmetric feet \u2014 see \u003Cc\u003ERetargeter.GroundAlignFeet\u003C/c\u003E); on plausible\r\n/// stance rests the solver\u0027s rest-relative transfer is already faithful and planted-sole\r\n/// deviations are genuine articulation (boxing stances, heel rolls) that must not be\r\n/// flattened.\u003C/para\u003E\r\n/// \u003C/remarks\u003E\r\npublic static class FootGroundAlign\r\n{\r\n    /// \u003Csummary\u003EMeasures planted stance offsets and recalibrates feet whose offset is a\r\n    /// rest-pose artifact; returns what was measured and done.\u003C/summary\u003E\r\n    /// \u003Cparam name=\u0022frames\u0022\u003EPer-frame local transforms (skeleton bone order); modified in place.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022skeleton\u0022\u003EBone hierarchy the frames are expressed against; its bind rest\r\n    /// is the flat-stance reference.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022left\u0022\u003ELeft leg chain bone indices.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022right\u0022\u003ERight leg chain bone indices.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022up\u0022\u003EWorld up direction of the clip\u0027s space.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022leftPlants\u0022\u003ELeft-foot plant intervals (frame indices into\r\n    /// \u003Cparamref name=\u0022frames\u0022/\u003E; out-of-range parts are clamped/ignored).\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022rightPlants\u0022\u003ERight-foot plant intervals.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022options\u0022\u003ETunables; defaults used when null.\u003C/param\u003E\r\n    public static FootGroundAlignReport Apply(\r\n        List\u003CXForm[]\u003E frames,\r\n        SkeletonModel skeleton,\r\n        FootChain left,\r\n        FootChain right,\r\n        Vector3 up,\r\n        IReadOnlyList\u003CFrameRange\u003E leftPlants,\r\n        IReadOnlyList\u003CFrameRange\u003E rightPlants,\r\n        FootGroundAlignOptions? options = null)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(frames);\r\n        ArgumentNullException.ThrowIfNull(skeleton);\r\n        ArgumentNullException.ThrowIfNull(left);\r\n        ArgumentNullException.ThrowIfNull(right);\r\n        ArgumentNullException.ThrowIfNull(leftPlants);\r\n        ArgumentNullException.ThrowIfNull(rightPlants);\r\n\r\n        options ??= new FootGroundAlignOptions();\r\n        var report = new FootGroundAlignReport\r\n        {\r\n            Left = new FootGroundAlignFootReport(),\r\n            Right = new FootGroundAlignFootReport(),\r\n        };\r\n        if (frames.Count == 0 || up.LengthSquared() \u003C 1e-12f)\r\n            return report;\r\n        up = Vector3.Normalize(up);\r\n\r\n        RecalibrateFoot(frames, skeleton, left, up, leftPlants, options, report.Left);\r\n        RecalibrateFoot(frames, skeleton, right, up, rightPlants, options, report.Right);\r\n        return report;\r\n    }\r\n\r\n    private static void RecalibrateFoot(\r\n        List\u003CXForm[]\u003E frames, SkeletonModel skeleton, FootChain chain, Vector3 up,\r\n        IReadOnlyList\u003CFrameRange\u003E plants, FootGroundAlignOptions options,\r\n        FootGroundAlignFootReport report)\r\n    {\r\n        int n = frames.Count;\r\n        var foot = chain.Ankle;\r\n        var restFootRotInv = Quaternion.Conjugate(skeleton.RestWorld[foot].Rot);\r\n        var maxStanceCos = MathF.Cos(options.MaxStanceDeviationDeg * MathF.PI / 180f);\r\n\r\n        // ---- measurement: pooled planted sole normal over the stance plants ----\r\n        var pooled = Vector3.Zero;\r\n        foreach (var plant in plants)\r\n        {\r\n            int start = Math.Max(plant.Start, 0);\r\n            int end = Math.Min(plant.End, n - 1);\r\n            if (start \u003E end)\r\n                continue;\r\n\r\n            var plantSum = Vector3.Zero;\r\n            for (int f = start; f \u003C= end; f\u002B\u002B)\r\n            {\r\n                var footRot = FkUtil.BoneWorld(frames[f], skeleton, foot).Rot;\r\n                plantSum \u002B= Vector3.Transform(up, MathQ.Normalize(footRot * restFootRotInv));\r\n            }\r\n            if (plantSum.LengthSquared() \u003C 1e-8f\r\n                || Vector3.Dot(Vector3.Normalize(plantSum), up) \u003C maxStanceCos)\r\n            {\r\n                report.SkippedPlants\u002B\u002B; // not standing on the sole \u2014 crawl/kneel/toe contact\r\n                continue;\r\n            }\r\n            report.StancePlants\u002B\u002B;\r\n            pooled \u002B= plantSum; // frame-count-weighted: longer stances dominate\r\n        }\r\n        if (pooled.LengthSquared() \u003C 1e-8f)\r\n            return;\r\n        pooled = Vector3.Normalize(pooled);\r\n\r\n        var offsetDeg = MathQ.AngleBetween(pooled, up) * (180f / MathF.PI);\r\n        report.MeasuredOffsetDeg = offsetDeg;\r\n        if (offsetDeg \u003C= options.MinCorrectionDeg)\r\n            return; // genuine planted articulation \u2014 leave the transfer byte-faithful\r\n\r\n        // ---- correction: one constant per foot, every frame ----\r\n        var footFix = MathQ.FromTo(pooled, up);\r\n        report.AppliedFootDeg = offsetDeg;\r\n\r\n        // Toe residual measured on top of the corrected foot, same dead zone.\r\n        var toeFix = Quaternion.Identity;\r\n        if (chain.Toe is { } toe \u0026\u0026 skeleton[toe].ParentIndex == foot)\r\n        {\r\n            var restToeRotInv = Quaternion.Conjugate(skeleton.RestWorld[toe].Rot);\r\n            var toePooled = Vector3.Zero;\r\n            foreach (var plant in plants)\r\n            {\r\n                int start = Math.Max(plant.Start, 0);\r\n                int end = Math.Min(plant.End, n - 1);\r\n                for (int f = start; f \u003C= end \u0026\u0026 f \u003E= 0; f\u002B\u002B)\r\n                {\r\n                    var toeRot = FkUtil.BoneWorld(frames[f], skeleton, toe).Rot;\r\n                    toePooled \u002B= Vector3.Transform(\r\n                        up, MathQ.Normalize(footFix * toeRot * restToeRotInv));\r\n                }\r\n            }\r\n            if (toePooled.LengthSquared() \u003E 1e-8f)\r\n            {\r\n                toePooled = Vector3.Normalize(toePooled);\r\n                var toeDeg = MathQ.AngleBetween(toePooled, up) * (180f / MathF.PI);\r\n                if (toeDeg \u003E options.MinCorrectionDeg \u0026\u0026 Vector3.Dot(toePooled, up) \u003E= maxStanceCos)\r\n                {\r\n                    toeFix = MathQ.FromTo(toePooled, up);\r\n                    report.AppliedToeDeg = toeDeg;\r\n                }\r\n            }\r\n        }\r\n\r\n        for (int f = 0; f \u003C n; f\u002B\u002B)\r\n            CorrectFrame(frames[f], skeleton, chain, footFix, toeFix);\r\n    }\r\n\r\n    /// \u003Csummary\u003EPremultiplies the foot\u0027s world rotation by the constant fix (the joint\r\n    /// position is untouched \u2014 the rotation pivots the foot about its own head), then gives\r\n    /// the toe its own residual on top of the corrected foot.\u003C/summary\u003E\r\n    private static void CorrectFrame(\r\n        XForm[] locals, SkeletonModel skeleton, FootChain chain,\r\n        Quaternion footFix, Quaternion toeFix)\r\n    {\r\n        var foot = chain.Ankle;\r\n        var parent = skeleton[foot].ParentIndex;\r\n        var parentRot = parent \u003C 0\r\n            ? Quaternion.Identity\r\n            : FkUtil.BoneWorld(locals, skeleton, parent).Rot;\r\n\r\n        var footWorld = MathQ.Normalize(parentRot * locals[foot].Rot);\r\n        var newFootWorld = MathQ.Normalize(footFix * footWorld);\r\n        locals[foot] = new XForm(\r\n            locals[foot].Pos, MathQ.Normalize(Quaternion.Conjugate(parentRot) * newFootWorld));\r\n\r\n        if (chain.Toe is { } toe \u0026\u0026 skeleton[toe].ParentIndex == foot)\r\n        {\r\n            // Desired toe world = toeFix \u2218 footFix \u2218 original world; re-derive its local\r\n            // against the corrected foot so it does not double-rotate with the foot fix.\r\n            var toeWorldOld = MathQ.Normalize(footWorld * locals[toe].Rot);\r\n            var desired = MathQ.Normalize(toeFix * footFix * toeWorldOld);\r\n            locals[toe] = new XForm(\r\n                locals[toe].Pos, MathQ.Normalize(Quaternion.Conjugate(newFootWorld) * desired));\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"HumanoidRetargeter/Formats/Bvh/BvhImporter.cs","FileName":"BvhImporter.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Numerics;\r\nusing System.Text;\r\nusing HumanoidRetargeter.Maths;\r\nusing HumanoidRetargeter.Skeleton;\r\n\r\nnamespace HumanoidRetargeter.Formats.Bvh;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// \u003Csummary\u003EOptions for \u003Csee cref=\u0022BvhImporter.Import\u0022/\u003E.\u003C/summary\u003E\r\npublic sealed class BvhImportOptions\r\n{\r\n    /// \u003Csummary\u003EFixed resampling rate for the motion data, frames per second.\u003C/summary\u003E\r\n    public float SampleFps { get; init; } = 30f;\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// BVH (Biovision Hierarchy) \u2192 \u003Csee cref=\u0022SourceScene\u0022/\u003E importer.\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EFormat conventions implemented\u003C/b\u003E (verified against Blender\u0027s\r\n/// \u003Cc\u003Eio_anim_bvh\u003C/c\u003E importer, which is the project\u0027s ground-truth extractor):\u003C/para\u003E\r\n/// \u003Clist type=\u0022bullet\u0022\u003E\r\n/// \u003Citem\u003E\u003Cb\u003ERest pose:\u003C/b\u003E each joint\u0027s rest local translation is its \u003Cc\u003EOFFSET\u003C/c\u003E; rest\r\n/// rotation is identity (BVH stores no rest orientation).\u003C/item\u003E\r\n/// \u003Citem\u003E\u003Cb\u003ERotation channels:\u003C/b\u003E the channel list order IS the rotation order. The listed\r\n/// rotations apply left-to-right as intrinsic rotations, which in this library\u0027s\r\n/// column-vector convention (\u003Cc\u003Ea * b\u003C/c\u003E applies \u003Cc\u003Eb\u003C/c\u003E first) is the product\r\n/// \u003Cc\u003ER = R_chan1 * R_chan2 * R_chan3\u003C/c\u003E \u2014 e.g. \u003Cc\u003EZrotation Yrotation Xrotation\u003C/c\u003E gives\r\n/// \u003Cc\u003ER = Rz * Ry * Rx\u003C/c\u003E. This matches Blender, which builds\r\n/// \u003Cc\u003EEuler((x,y,z), reversed(channelOrder))\u003C/c\u003E for the same matrix. Angles are degrees.\u003C/item\u003E\r\n/// \u003Citem\u003E\u003Cb\u003EPosition channels:\u003C/b\u003E when a joint has any position channel, the channel values\r\n/// REPLACE the joint\u0027s local translation (missing components are 0) \u2014 they are not added to\r\n/// the \u003Cc\u003EOFFSET\u003C/c\u003E. This is Blender\u0027s behavior; in practice roots have OFFSET 0 so the two\r\n/// readings only diverge on non-root position channels (e.g. Bandai-Namco exports).\u003C/item\u003E\r\n/// \u003Citem\u003E\u003Cb\u003EEnd Sites:\u003C/b\u003E synthesized as a channel-less leaf bone named\r\n/// \u003Cc\u003E\u0022\u0026lt;parent\u0026gt;_end\u0022\u003C/c\u003E so chain tips keep their direction information (Blender instead\r\n/// folds them into the parent bone\u0027s tail).\u003C/item\u003E\r\n/// \u003C/list\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EUnits\u003C/b\u003E: BVH files carry no unit declaration. Heuristic: compute the rest\r\n/// skeleton height (max\u2212min world Y over all joints); if it is \u0026lt; 10 the file is assumed\r\n/// to be in meters and all translations (offsets AND position channels, root included) are\r\n/// scaled \u00D7100 to centimeters, otherwise it is assumed to already be centimeters (\u00D71).\r\n/// Millimeter-scale files (height \u0026gt; 400) are not special-cased \u2014 they are rare and\r\n/// ambiguous against cm mocap of long ranges; \u003Csee cref=\u0022SourceScene.UnitScaleCm\u0022/\u003E records\r\n/// whichever factor was applied for diagnostics.\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003ECalibration rest-frame trim\u003C/b\u003E: mocap exports (CMU asf/amc conversions among\r\n/// them) often prepend a skeleton-calibration segment \u2014 the rest pose itself (all rotation\r\n/// channels \u2248 0), hard-cut (or blend-ramped over 2\u20133 frames) into the real motion. Played\r\n/// back it reads as a T-pose flash at t = 0. The importer drops such a segment from either\r\n/// clip end when ALL of (measured margins in parentheses, over the corpus \u002B repro files):\r\n/// every segment frame is rest-like (max joint rotation vs the identity rest \u2264 40\u00B0;\r\n/// calibration frames/ramps measure \u2264 24\u00B0, real clip edges \u2265 80\u00B0); the segment is short\r\n/// (\u2264 4 rest-like frames \u2014 hard cuts measure 1, blend ramps 2; longer rest-like leads are\r\n/// content); the clip beyond it is NOT rest-like; and the discontinuity where the segment\r\n/// exits into the motion is both large in absolute terms (\u2265 20\u00B0; measured 25\u2013177\u00B0) and\r\n/// large versus the clip\u0027s own typical inter-frame delta (\u2265 4\u00D7 the median; real clip edges\r\n/// measure \u2264 8\u00B0 at \u2264 ~1\u00D7 the median). A qualifying segment that exits through a multi-frame\r\n/// blend RAMP (measured 22\u201325\u00B0/frame for 3 frames on a makehuman-retarget export) has the\r\n/// ramp trimmed too, until the motion settles \u2014 8 frames total per end at most. A clip that\r\n/// legitimately starts near rest (an idle) is continuous into the motion and never trips\r\n/// the discontinuity gates. Note the trim can never remove\r\n/// the reference frame a non-anatomical stick bind needs for its rest rebuild (see\r\n/// \u003Cc\u003ERestNormalizer\u003C/c\u003E): it only removes frames that MATCH the identity-rotation bind,\r\n/// and a frame matching a stick bind carries no rest information the bind itself lacks \u2014\r\n/// the next (real) frame is then strictly the better reference.\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EResampling\u003C/b\u003E: motion frames are resampled from the file\u0027s \u003Cc\u003EFrame Time\u003C/c\u003E\r\n/// grid onto \u003Csee cref=\u0022BvhImportOptions.SampleFps\u0022/\u003E. Each native frame\u0027s euler channels are\r\n/// converted to a quaternion FIRST and bracketing frames are then slerped (positions lerped).\r\n/// Interpolating raw euler angles across frames would mostly work at mocap densities\r\n/// (30\u2013120 fps, small per-frame deltas) but breaks down when an angle wraps \u00B1180\u00B0 between\r\n/// frames; per-frame quaternion \u002B slerp has no such failure mode, so that is what we do.\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EAxes\u003C/b\u003E: BVH is conventionally Y-up / Z-forward / X-right. Native axes are\r\n/// preserved (no conversion), matching the FBX importer\u0027s policy; the conventional axes are\r\n/// recorded on the \u003Csee cref=\u0022SourceScene\u0022/\u003E (up = Y, front = Z, coord = X).\u003C/para\u003E\r\n/// \u003C/remarks\u003E\r\npublic static class BvhImporter\r\n{\r\n    private const float MeterHeightThreshold = 10f;\r\n\r\n    /// \u003Csummary\u003EParses BVH bytes and builds the source scene.\u003C/summary\u003E\r\n    /// \u003Cexception cref=\u0022FormatException\u0022\u003EMalformed or truncated BVH.\u003C/exception\u003E\r\n    public static SourceScene Import(byte[] data, BvhImportOptions? options = null)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(data);\r\n        options ??= new BvhImportOptions();\r\n        if (!(options.SampleFps \u003E 0f) || !float.IsFinite(options.SampleFps))\r\n            throw new ArgumentOutOfRangeException(nameof(options), \u0022SampleFps must be positive.\u0022);\r\n\r\n        var cursor = new TokenCursor(Encoding.UTF8.GetString(data));\r\n\r\n        // ---- HIERARCHY -----------------------------------------------------------------\r\n        cursor.ExpectKeyword(\u0022HIERARCHY\u0022);\r\n        var joints = new List\u003CJoint\u003E();\r\n        int channelCount = 0;\r\n        if (!cursor.PeekIs(\u0022ROOT\u0022))\r\n            throw new FormatException(\u0022BVH: expected ROOT after HIERARCHY.\u0022);\r\n        while (cursor.PeekIs(\u0022ROOT\u0022)) // multiple roots are out of spec but harmless to accept\r\n        {\r\n            cursor.Next();\r\n            ParseJoint(cursor, joints, parent: -1, ref channelCount);\r\n        }\r\n\r\n        // ---- MOTION ---------------------------------------------------------------------\r\n        cursor.ExpectKeyword(\u0022MOTION\u0022);\r\n        cursor.ExpectKeyword(\u0022FRAMES:\u0022);\r\n        int frameCount = cursor.NextInt();\r\n        if (frameCount \u003C 0)\r\n            throw new FormatException($\u0022BVH: negative frame count {frameCount}.\u0022);\r\n        cursor.ExpectKeyword(\u0022FRAME\u0022);\r\n        cursor.ExpectKeyword(\u0022TIME:\u0022);\r\n        float frameTime = cursor.NextFloat();\r\n        if (!(frameTime \u003E 0f) || !float.IsFinite(frameTime))\r\n            throw new FormatException($\u0022BVH: invalid Frame Time {frameTime}.\u0022);\r\n\r\n        var motion = new float[frameCount][];\r\n        for (int f = 0; f \u003C frameCount; f\u002B\u002B)\r\n        {\r\n            var row = new float[channelCount];\r\n            for (int c = 0; c \u003C channelCount; c\u002B\u002B)\r\n                row[c] = cursor.NextFloat();\r\n            motion[f] = row;\r\n        }\r\n\r\n        // ---- units heuristic --------------------------------------------------------------\r\n        float unitScale = HeuristicUnitScale(joints);\r\n\r\n        // ---- skeleton ----------------------------------------------------------------------\r\n        var defs = new List\u003CBoneDefinition\u003E(joints.Count);\r\n        foreach (var j in joints)\r\n        {\r\n            defs.Add(new BoneDefinition(\r\n                j.Name,\r\n                j.Parent \u003C 0 ? null : joints[j.Parent].Name,\r\n                new XForm(j.Offset * unitScale, Quaternion.Identity)));\r\n        }\r\n        var skeleton = Skeleton.Skeleton.Create(defs);\r\n\r\n        // ---- clip ----------------------------------------------------------------------------\r\n        var clips = new List\u003CClip\u003E();\r\n        if (frameCount \u003E 0)\r\n            clips.Add(ResampleClip(joints, skeleton, motion, frameTime, unitScale, options.SampleFps));\r\n\r\n        // BVH conventional axes: Y-up (1), Z-front (2), X-coord (0) \u2014 recorded, not converted.\r\n        // RestPlacementAuthored = false: the BVH rest skeleton is OFFSETs only (root at the\r\n        // file origin, no authored world placement), while MOTION root positions live in\r\n        // absolute capture-volume coordinates \u2014 the two share no common ground/origin, so\r\n        // the solver must normalize clip placement against the rest skeleton\r\n        // (see SourceScene.RestPlacementAuthored and GeometricSolver remarks).\r\n        return new SourceScene(\r\n            skeleton, clips, unitScale,\r\n            upAxis: 1, upAxisSign: 1,\r\n            frontAxis: 2, frontAxisSign: 1,\r\n            coordAxis: 0, coordAxisSign: 1,\r\n            originalUpAxis: -1)\r\n        {\r\n            RestPlacementAuthored = false,\r\n        };\r\n    }\r\n\r\n    // =====================================================================================\r\n    // hierarchy parsing\r\n    // =====================================================================================\r\n\r\n    private sealed class Joint\r\n    {\r\n        public required string Name;\r\n        public required int Parent;          // index into the joint list, -1 for roots\r\n        public Vector3 Offset;               // raw file units\r\n        public int PosX = -1, PosY = -1, PosZ = -1;            // motion column per position axis\r\n        public List\u003C(int Axis, int Column)\u003E Rot = new();        // rotation channels in file order\r\n        public bool HasPos =\u003E PosX \u003E= 0 || PosY \u003E= 0 || PosZ \u003E= 0;\r\n    }\r\n\r\n    private static void ParseJoint(TokenCursor cursor, List\u003CJoint\u003E joints, int parent, ref int channelCount)\r\n    {\r\n        // Joint name: tokens up to \u0027{\u0027, joined with \u0027_\u0027 (mirrors Blender\u0027s handling of\r\n        // names containing spaces).\r\n        var nameParts = new List\u003Cstring\u003E();\r\n        while (!cursor.PeekIs(\u0022{\u0022))\r\n        {\r\n            if (cursor.AtEnd)\r\n                throw new FormatException(\u0022BVH: unexpected end of file in joint name.\u0022);\r\n            nameParts.Add(cursor.Next());\r\n        }\r\n        if (nameParts.Count == 0)\r\n            throw new FormatException(\u0022BVH: joint with no name.\u0022);\r\n        string name = UniqueName(string.Join(\u0027_\u0027, nameParts), joints);\r\n\r\n        cursor.ExpectKeyword(\u0022{\u0022);\r\n        cursor.ExpectKeyword(\u0022OFFSET\u0022);\r\n        var joint = new Joint { Name = name, Parent = parent };\r\n        joint.Offset = new Vector3(cursor.NextFloat(), cursor.NextFloat(), cursor.NextFloat());\r\n        int index = joints.Count;\r\n        joints.Add(joint);\r\n\r\n        if (cursor.PeekIs(\u0022CHANNELS\u0022))\r\n        {\r\n            cursor.Next();\r\n            int n = cursor.NextInt();\r\n            if (n \u003C 0 || n \u003E 6)\r\n                throw new FormatException($\u0022BVH: joint \u0027{name}\u0027 has invalid channel count {n}.\u0022);\r\n            for (int i = 0; i \u003C n; i\u002B\u002B)\r\n            {\r\n                string channel = cursor.Next();\r\n                int column = channelCount\u002B\u002B;\r\n                switch (channel.ToUpperInvariant())\r\n                {\r\n                    case \u0022XPOSITION\u0022: joint.PosX = column; break;\r\n                    case \u0022YPOSITION\u0022: joint.PosY = column; break;\r\n                    case \u0022ZPOSITION\u0022: joint.PosZ = column; break;\r\n                    case \u0022XROTATION\u0022: joint.Rot.Add((0, column)); break;\r\n                    case \u0022YROTATION\u0022: joint.Rot.Add((1, column)); break;\r\n                    case \u0022ZROTATION\u0022: joint.Rot.Add((2, column)); break;\r\n                    default:\r\n                        throw new FormatException($\u0022BVH: unknown channel \u0027{channel}\u0027 on joint \u0027{name}\u0027.\u0022);\r\n                }\r\n            }\r\n        }\r\n\r\n        while (!cursor.PeekIs(\u0022}\u0022))\r\n        {\r\n            if (cursor.AtEnd)\r\n                throw new FormatException($\u0022BVH: unexpected end of file inside joint \u0027{name}\u0027.\u0022);\r\n            if (cursor.PeekIs(\u0022JOINT\u0022))\r\n            {\r\n                cursor.Next();\r\n                ParseJoint(cursor, joints, index, ref channelCount);\r\n            }\r\n            else if (cursor.PeekIs(\u0022END\u0022))\r\n            {\r\n                cursor.Next();\r\n                cursor.ExpectKeyword(\u0022SITE\u0022);\r\n                while (!cursor.PeekIs(\u0022{\u0022)) // a name after \u0022End Site\u0022 is out of spec; skip it\r\n                {\r\n                    if (cursor.AtEnd)\r\n                        throw new FormatException(\u0022BVH: unexpected end of file in End Site.\u0022);\r\n                    cursor.Next();\r\n                }\r\n                cursor.ExpectKeyword(\u0022{\u0022);\r\n                cursor.ExpectKeyword(\u0022OFFSET\u0022);\r\n                var endOffset = new Vector3(cursor.NextFloat(), cursor.NextFloat(), cursor.NextFloat());\r\n                cursor.ExpectKeyword(\u0022}\u0022);\r\n\r\n                // Synthesize a channel-less leaf so the chain tip\u0027s direction is kept.\r\n                joints.Add(new Joint\r\n                {\r\n                    Name = UniqueName(name \u002B \u0022_end\u0022, joints),\r\n                    Parent = index,\r\n                    Offset = endOffset,\r\n                });\r\n            }\r\n            else\r\n            {\r\n                throw new FormatException(\r\n                    $\u0022BVH: unexpected token \u0027{cursor.Next()}\u0027 inside joint \u0027{name}\u0027.\u0022);\r\n            }\r\n        }\r\n        cursor.ExpectKeyword(\u0022}\u0022);\r\n    }\r\n\r\n    private static string UniqueName(string name, List\u003CJoint\u003E joints)\r\n    {\r\n        bool Taken(string candidate)\r\n        {\r\n            foreach (var j in joints)\r\n                if (string.Equals(j.Name, candidate, StringComparison.Ordinal))\r\n                    return true;\r\n            return false;\r\n        }\r\n\r\n        if (!Taken(name))\r\n            return name;\r\n        for (int i = 1; ; i\u002B\u002B)\r\n        {\r\n            string candidate = $\u0022{name}#{i}\u0022;\r\n            if (!Taken(candidate))\r\n                return candidate;\r\n        }\r\n    }\r\n\r\n    // =====================================================================================\r\n    // units\r\n    // =====================================================================================\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Meters-vs-centimeters heuristic: rest skeleton height (max\u2212min world Y over all\r\n    /// joints, end sites included) \u0026lt; 10 \u2192 meters \u2192 \u00D7100; otherwise centimeters \u2192 \u00D71.\r\n    /// \u003C/summary\u003E\r\n    private static float HeuristicUnitScale(List\u003CJoint\u003E joints)\r\n    {\r\n        Span\u003Cfloat\u003E worldY = joints.Count \u003C= 256 ? stackalloc float[joints.Count] : new float[joints.Count];\r\n        float min = float.MaxValue, max = float.MinValue;\r\n        for (int i = 0; i \u003C joints.Count; i\u002B\u002B)\r\n        {\r\n            worldY[i] = (joints[i].Parent \u003C 0 ? 0f : worldY[joints[i].Parent]) \u002B joints[i].Offset.Y;\r\n            min = MathF.Min(min, worldY[i]);\r\n            max = MathF.Max(max, worldY[i]);\r\n        }\r\n        float height = max - min;\r\n        return height \u003E 0f \u0026\u0026 height \u003C MeterHeightThreshold ? 100f : 1f;\r\n    }\r\n\r\n    // =====================================================================================\r\n    // motion sampling\r\n    // =====================================================================================\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Decodes every native frame to per-joint local transforms (quaternions built per frame\r\n    /// from the joint\u0027s channel order), drops leading/trailing calibration rest frames (see\r\n    /// class remarks), then resamples onto the \u003Cparamref name=\u0022fps\u0022/\u003E grid \u2014 positions\r\n    /// lerped, rotations slerped between the bracketing native frames.\r\n    /// \u003C/summary\u003E\r\n    private static Clip ResampleClip(\r\n        List\u003CJoint\u003E joints, Skeleton.Skeleton skeleton, float[][] motion,\r\n        float frameTime, float unitScale, float fps)\r\n    {\r\n        int jointCount = joints.Count;\r\n        int nativeCount = motion.Length;\r\n\r\n        // Joint order may differ from skeleton bone order (topological sort) \u2014 map.\r\n        var toSkeleton = new int[jointCount];\r\n        for (int i = 0; i \u003C jointCount; i\u002B\u002B)\r\n            toSkeleton[i] = skeleton.IndexOf(joints[i].Name);\r\n\r\n        // Native-frame locals.\r\n        var native = new XForm[nativeCount][];\r\n        for (int f = 0; f \u003C nativeCount; f\u002B\u002B)\r\n        {\r\n            var row = motion[f];\r\n            var locals = new XForm[jointCount];\r\n            for (int i = 0; i \u003C jointCount; i\u002B\u002B)\r\n                locals[i] = EvaluateLocal(joints[i], row, unitScale);\r\n            native[f] = locals;\r\n        }\r\n\r\n        // Calibration rest-frame trim: a short rest-like segment per clip end (class remarks).\r\n        int first = 0;\r\n        int last = nativeCount - 1;\r\n        if (nativeCount \u003E= 3)\r\n        {\r\n            float typicalDeltaDeg = TypicalNeighborRotDeltaDeg(native);\r\n            first \u002B= CalibrationSegmentLength(native, first, last, step: \u002B1, typicalDeltaDeg);\r\n            last -= CalibrationSegmentLength(native, last, first, step: -1, typicalDeltaDeg);\r\n        }\r\n        int trimmedCount = last - first \u002B 1;\r\n\r\n        double duration = (trimmedCount - 1) * (double)frameTime;\r\n        int outCount = Math.Max(1, (int)Math.Round(duration * fps) \u002B 1);\r\n\r\n        var frames = new List\u003CXForm[]\u003E(outCount);\r\n        for (int f = 0; f \u003C outCount; f\u002B\u002B)\r\n        {\r\n            double s = f / (double)fps / frameTime; // position on the trimmed native frame grid\r\n            int i0 = first \u002B Math.Clamp((int)Math.Floor(s), 0, trimmedCount - 1);\r\n            int i1 = Math.Min(i0 \u002B 1, last);\r\n            float u = Math.Clamp((float)(s - (i0 - first)), 0f, 1f);\r\n\r\n            var frame = new XForm[skeleton.Count];\r\n            var a = native[i0];\r\n            var b = native[i1];\r\n            for (int i = 0; i \u003C jointCount; i\u002B\u002B)\r\n            {\r\n                frame[toSkeleton[i]] = new XForm(\r\n                    Vector3.Lerp(a[i].Pos, b[i].Pos, u),\r\n                    MathQ.Normalize(Quaternion.Slerp(a[i].Rot, b[i].Rot, u)));\r\n            }\r\n            frames.Add(frame);\r\n        }\r\n\r\n        // NativeFps records the file\u0027s authored frame rate (1 / FrameTime): external frame\r\n        // ranges (Unity .meta clipAnimations) are expressed in it.\r\n        float nativeFps = frameTime \u003E 0f ? (float)(1.0 / frameTime) : fps;\r\n        return new Clip(\u0022motion\u0022, fps, looping: false, frames, nativeFps);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- calibration trim\r\n\r\n    /// \u003Csummary\u003EA frame counts as rest-like only below this max-joint rotation angle vs the\r\n    /// identity-rotation bind (measured: calibration frames/ramps \u2264 24\u00B0, real edges \u2265 80\u00B0).\u003C/summary\u003E\r\n    private const float CalibrationRestMaxDeg = 40f;\r\n\r\n    /// \u003Csummary\u003EAbsolute floor on the discontinuity out of the rest-like segment (measured:\r\n    /// calibration exits 25\u2013177\u00B0, continuous real clip edges \u2264 8\u00B0).\u003C/summary\u003E\r\n    private const float CalibrationJumpMinDeg = 20f;\r\n\r\n    /// \u003Csummary\u003EThe segment-exit discontinuity must also exceed this multiple of the clip\u0027s\r\n    /// median inter-frame delta \u2014 a clip idling near rest never trips this.\u003C/summary\u003E\r\n    private const float CalibrationJumpTypicalRatio = 4f;\r\n\r\n    /// \u003Csummary\u003ELongest rest-like calibration segment trimmed per clip end. Hard cuts are\r\n    /// 1 frame (CMU asf/amc exports); rest\u2192motion blend ramps measure 2 rest-like frames\r\n    /// (a makehuman-retarget export). Longer rest-like leads are content, left alone.\u003C/summary\u003E\r\n    private const int CalibrationMaxSegmentFrames = 4;\r\n\r\n    /// \u003Csummary\u003EAbsolute floor on a blend-ramp frame\u0027s delta for the ramp extension\r\n    /// (measured ramp deltas 15\u201325\u00B0/frame; settled motion \u2264 6\u00B0).\u003C/summary\u003E\r\n    private const float CalibrationRampMinDeg = 10f;\r\n\r\n    /// \u003Csummary\u003EHard cap on the total trim per clip end (rest-like segment \u002B blend ramp;\r\n    /// measured worst case 5 frames on the makehuman-retarget export).\u003C/summary\u003E\r\n    private const int CalibrationMaxTrimFrames = 8;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Length of the prepended (\u003Cparamref name=\u0022step\u0022/\u003E = \u002B1, scanning from\r\n    /// \u003Cparamref name=\u0022edge\u0022/\u003E toward \u003Cparamref name=\u0022stop\u0022/\u003E) or appended (\u22121)\r\n    /// skeleton-calibration segment, 0 when there is none. The segment is a short run of\r\n    /// rest-like frames (\u2264 \u003Csee cref=\u0022CalibrationMaxSegmentFrames\u0022/\u003E) that exits into\r\n    /// NON-rest-like motion (\u2265 2 frames of which must remain) through a discontinuity that\r\n    /// is large both absolutely and against the clip\u0027s typical inter-frame delta. When the\r\n    /// exit is a multi-frame blend RAMP rather than a hard cut (measured: 2 rest-like frames\r\n    /// then 22\u201325\u00B0/frame for 3 more), the ramp frames are consumed too \u2014 until the motion\r\n    /// settles to ordinary deltas \u2014 bounded by \u003Csee cref=\u0022CalibrationMaxTrimFrames\u0022/\u003E total.\r\n    /// See class remarks for the measured margins.\r\n    /// \u003C/summary\u003E\r\n    private static int CalibrationSegmentLength(\r\n        XForm[][] native, int edge, int stop, int step, float typicalDeltaDeg)\r\n    {\r\n        int length = 0;\r\n        int f = edge;\r\n        while (f != stop \u0026\u0026 length \u003C= CalibrationMaxSegmentFrames\r\n               \u0026\u0026 MaxRotDeltaDeg(native[f], null) \u003C= CalibrationRestMaxDeg)\r\n        {\r\n            length\u002B\u002B;\r\n            f \u002B= step;\r\n        }\r\n        if (length is 0 or \u003E CalibrationMaxSegmentFrames)\r\n            return 0;\r\n\r\n        // f = first frame past the rest-like segment; require a real (non-rest-like) clip\r\n        // of \u2265 2 frames beyond it and a calibration-grade cut between segment and motion.\r\n        if (f == stop || MaxRotDeltaDeg(native[f], null) \u003C= CalibrationRestMaxDeg)\r\n            return 0;\r\n        float jump = MaxRotDeltaDeg(native[f - step], native[f]);\r\n        if (jump \u003C CalibrationJumpMinDeg || jump \u003C CalibrationJumpTypicalRatio * typicalDeltaDeg)\r\n            return 0;\r\n\r\n        // Blend-ramp extension: consume frames still moving at calibration-ramp speed until\r\n        // the motion settles (leaving \u2265 2 frames past the trim).\r\n        float rampFloor = MathF.Max(\r\n            CalibrationRampMinDeg, CalibrationJumpTypicalRatio * typicalDeltaDeg);\r\n        while (length \u003C CalibrationMaxTrimFrames\r\n               \u0026\u0026 f != stop \u0026\u0026 f \u002B step != stop\r\n               \u0026\u0026 MaxRotDeltaDeg(native[f], native[f \u002B step]) \u003E= rampFloor)\r\n        {\r\n            length\u002B\u002B;\r\n            f \u002B= step;\r\n        }\r\n        return length;\r\n    }\r\n\r\n    /// \u003Csummary\u003EMax joint rotation angle (degrees) between two decoded frames, or \u2014 when\r\n    /// \u003Cparamref name=\u0022b\u0022/\u003E is null \u2014 against the identity-rotation BVH bind rest.\u003C/summary\u003E\r\n    private static float MaxRotDeltaDeg(XForm[] a, XForm[]? b)\r\n    {\r\n        float max = 0f;\r\n        for (int i = 0; i \u003C a.Length; i\u002B\u002B)\r\n        {\r\n            float angle = MathQ.AngleBetween(a[i].Rot, b is null ? Quaternion.Identity : b[i].Rot);\r\n            max = MathF.Max(max, angle);\r\n        }\r\n        return max * (180f / MathF.PI);\r\n    }\r\n\r\n    /// \u003Csummary\u003EMedian of the per-pair max joint rotation deltas over the clip\u0027s INTERIOR\r\n    /// consecutive frame pairs (both edge pairs excluded \u2014 they are the trim candidates).\u003C/summary\u003E\r\n    private static float TypicalNeighborRotDeltaDeg(XForm[][] native)\r\n    {\r\n        int pairCount = native.Length - 3; // pairs (1,2) \u2026 (n-3, n-2)\r\n        if (pairCount \u003C= 0)\r\n            return 0f;\r\n        var deltas = new float[pairCount];\r\n        for (int f = 0; f \u003C pairCount; f\u002B\u002B)\r\n            deltas[f] = MaxRotDeltaDeg(native[f \u002B 1], native[f \u002B 2]);\r\n        Array.Sort(deltas);\r\n        return deltas[pairCount / 2];\r\n    }\r\n\r\n    /// \u003Csummary\u003EOne joint\u0027s local transform from one motion row (see class remarks).\u003C/summary\u003E\r\n    private static XForm EvaluateLocal(Joint joint, float[] row, float unitScale)\r\n    {\r\n        // Position channels replace the OFFSET; absent channels (or no position channels at\r\n        // all) fall back per Blender\u0027s semantics described in the class remarks.\r\n        Vector3 pos = joint.HasPos\r\n            ? new Vector3(\r\n                joint.PosX \u003E= 0 ? row[joint.PosX] : 0f,\r\n                joint.PosY \u003E= 0 ? row[joint.PosY] : 0f,\r\n                joint.PosZ \u003E= 0 ? row[joint.PosZ] : 0f)\r\n            : joint.Offset;\r\n\r\n        // R = R_chan1 * R_chan2 * R_chan3 (column-vector convention; degrees in the file).\r\n        var rot = Quaternion.Identity;\r\n        foreach (var (axis, column) in joint.Rot)\r\n        {\r\n            float radians = row[column] * (MathF.PI / 180f);\r\n            var axisVector = axis switch\r\n            {\r\n                0 =\u003E Vector3.UnitX,\r\n                1 =\u003E Vector3.UnitY,\r\n                _ =\u003E Vector3.UnitZ,\r\n            };\r\n            rot *= Quaternion.CreateFromAxisAngle(axisVector, radians);\r\n        }\r\n\r\n        return new XForm(pos * unitScale, MathQ.Normalize(rot));\r\n    }\r\n\r\n    // =====================================================================================\r\n    // tokenizer\r\n    // =====================================================================================\r\n\r\n    /// \u003Csummary\u003EWhitespace token stream over the BVH text (BVH is line-format agnostic).\u003C/summary\u003E\r\n    private sealed class TokenCursor\r\n    {\r\n        private readonly string[] _tokens;\r\n        private int _pos;\r\n\r\n        public TokenCursor(string text)\r\n            =\u003E _tokens = text.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);\r\n\r\n        public bool AtEnd =\u003E _pos \u003E= _tokens.Length;\r\n\r\n        public bool PeekIs(string keywordUpper)\r\n            =\u003E _pos \u003C _tokens.Length \u0026\u0026\r\n               string.Equals(_tokens[_pos], keywordUpper, StringComparison.OrdinalIgnoreCase);\r\n\r\n        public string Next()\r\n        {\r\n            if (AtEnd)\r\n                throw new FormatException(\u0022BVH: unexpected end of file.\u0022);\r\n            return _tokens[_pos\u002B\u002B];\r\n        }\r\n\r\n        public void ExpectKeyword(string keywordUpper)\r\n        {\r\n            string token = Next();\r\n            if (!string.Equals(token, keywordUpper, StringComparison.OrdinalIgnoreCase))\r\n                throw new FormatException($\u0022BVH: expected \u0027{keywordUpper}\u0027, found \u0027{token}\u0027.\u0022);\r\n        }\r\n\r\n        public int NextInt()\r\n        {\r\n            string token = Next();\r\n            if (!int.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value))\r\n                throw new FormatException($\u0022BVH: expected an integer, found \u0027{token}\u0027.\u0022);\r\n            return value;\r\n        }\r\n\r\n        public float NextFloat()\r\n        {\r\n            string token = Next();\r\n            if (!float.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out float value) ||\r\n                !float.IsFinite(value))\r\n                throw new FormatException($\u0022BVH: expected a number, found \u0027{token}\u0027.\u0022);\r\n            return value;\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"HumanoidRetargeter/Formats/Dmx/DmxWriter.cs","FileName":"DmxWriter.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Security.Cryptography;\r\nusing System.Text;\r\nusing HumanoidRetargeter.Skeleton;\r\nusing SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidRetargeter.Formats.Dmx;\r\n\r\n/// \u003Csummary\u003EOptions for \u003Csee cref=\u0022DmxWriter.Write\u0022/\u003E.\u003C/summary\u003E\r\npublic sealed class DmxWriteOptions\r\n{\r\n    /// \u003Csummary\u003EModel/clip name written into the DmeModel element (e.g. the sequence name).\u003C/summary\u003E\r\n    public string Name { get; set; } = \u0022\u0022;\r\n\r\n    /// \u003Csummary\u003EFree-form provenance note written as the DmeDCCMakefile source name\r\n    /// (fbx2dmx writes the source .fbx path here).\u003C/summary\u003E\r\n    public string SourceNote { get; set; } = \u0022\u0022;\r\n\r\n    /// \u003Csummary\u003EWhen true (default, matching fbx2dmx output) the file declares a Y-up axis\r\n    /// system; when false it declares Z-up. Data is written as-is either way.\u003C/summary\u003E\r\n    public bool UpAxisY { get; set; } = true;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Skeleton bone indices that get NO DmeChannel pair: the bones keep their DmeJoint and\r\n    /// bind (rest) transform, but no animation channels are written for them \u2014 the engine then\r\n    /// drives them itself (e.g. ConstraintDriven twist/helper bones, design \u00A73). Null (default)\r\n    /// writes channels for every bone.\r\n    /// \u003C/summary\u003E\r\n    public IReadOnlySet\u003Cint\u003E? ChannelExcludedBones { get; set; }\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Writes an animation DMX in \u003Cc\u003Ekeyvalues2_noids\u003C/c\u003E text encoding, replicating the exact\r\n/// element/attribute shape of fbx2dmx output (authoritative reference:\r\n/// \u003Cc\u003Edev/m0/ref_idlepose.dmx\u003C/c\u003E): a root DmElement holding an inline DmeModel (joint GUID\r\n/// refs \u002B bind base state), a top-level DmeAnimationList with one DmeChannelsClip carrying a\r\n/// position and an orientation channel per bone, and top-level DmeTransform/DmeJoint elements\r\n/// the channels and joint lists reference by GUID. Output is fully deterministic: GUIDs are\r\n/// MD5-derived from the options name and an element path, and export tags use fixed\r\n/// placeholder strings.\r\n/// \u003C/summary\u003E\r\npublic static class DmxWriter\r\n{\r\n    private const string Header = \u0022\u003C!-- dmx encoding keyvalues2_noids 4 format model 22 --\u003E\u0022;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Serializes \u003Cparamref name=\u0022clip\u0022/\u003E on \u003Cparamref name=\u0022skeleton\u0022/\u003E to DMX text.\r\n    /// Frames must contain one local transform per bone in skeleton order.\r\n    /// \u003C/summary\u003E\r\n    /// \u003Cexception cref=\u0022ArgumentException\u0022\u003EThrown when the clip is empty or a frame\u0027s bone\r\n    /// count does not match the skeleton.\u003C/exception\u003E\r\n    public static string Write(SkeletonModel skeleton, Clip clip, DmxWriteOptions options)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(skeleton);\r\n        ArgumentNullException.ThrowIfNull(clip);\r\n        ArgumentNullException.ThrowIfNull(options);\r\n\r\n        if (clip.FrameCount == 0)\r\n            throw new ArgumentException(\u0022Clip has no frames.\u0022, nameof(clip));\r\n        for (var f = 0; f \u003C clip.FrameCount; f\u002B\u002B)\r\n        {\r\n            if (clip.Frames[f].Length != skeleton.Count)\r\n                throw new ArgumentException(\r\n                    $\u0022Frame {f} has {clip.Frames[f].Length} bone transforms, skeleton has {skeleton.Count}.\u0022,\r\n                    nameof(clip));\r\n        }\r\n\r\n        var w = new Emitter();\r\n        var animListGuid = GuidString(options.Name, \u0022animationList\u0022);\r\n        var jointGuids = new string[skeleton.Count];\r\n        var transformGuids = new string[skeleton.Count];\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n        {\r\n            jointGuids[i] = GuidString(options.Name, \u0022joint:\u0022 \u002B skeleton[i].Name);\r\n            transformGuids[i] = GuidString(options.Name, \u0022transform:\u0022 \u002B skeleton[i].Name);\r\n        }\r\n\r\n        w.Raw(Header);\r\n\r\n        // ---- root DmElement -------------------------------------------------\r\n        w.BeginTopLevel(\u0022DmElement\u0022);\r\n        w.Attr(\u0022name\u0022, \u0022string\u0022, \u0022root\u0022);\r\n\r\n        w.BeginInlineAttr(\u0022skeleton\u0022, \u0022DmeModel\u0022);\r\n        w.Attr(\u0022name\u0022, \u0022string\u0022, options.Name);\r\n        w.BeginInlineAttr(\u0022transform\u0022, \u0022DmeTransform\u0022);\r\n        w.Attr(\u0022position\u0022, \u0022vector3\u0022, \u00220 0 0\u0022);\r\n        w.Attr(\u0022orientation\u0022, \u0022quaternion\u0022, \u00220 0 0 1\u0022);\r\n        w.EndInlineAttr();\r\n        w.Attr(\u0022shape\u0022, \u0022element\u0022, \u0022\u0022);\r\n        w.Attr(\u0022visible\u0022, \u0022bool\u0022, \u00221\u0022);\r\n\r\n        w.BeginArray(\u0022children\u0022);\r\n        var roots = new List\u003Cint\u003E();\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n        {\r\n            if (skeleton[i].ParentIndex \u003C 0)\r\n                roots.Add(i);\r\n        }\r\n        for (var r = 0; r \u003C roots.Count; r\u002B\u002B)\r\n            w.ElementRef(jointGuids[roots[r]], last: r == roots.Count - 1);\r\n        w.EndArray();\r\n\r\n        w.BeginArray(\u0022jointList\u0022);\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n            w.ElementRef(jointGuids[i], last: i == skeleton.Count - 1);\r\n        w.EndArray();\r\n\r\n        w.BeginArray(\u0022baseStates\u0022);\r\n        w.BeginArrayElement(\u0022DmeTransformList\u0022);\r\n        w.Attr(\u0022name\u0022, \u0022string\u0022, \u0022bind\u0022);\r\n        w.BeginArray(\u0022transforms\u0022);\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n        {\r\n            w.BeginArrayElement(\u0022DmeTransform\u0022);\r\n            w.Attr(\u0022name\u0022, \u0022string\u0022, skeleton[i].Name);\r\n            w.Attr(\u0022position\u0022, \u0022vector3\u0022, Vec(skeleton[i].RestLocal));\r\n            w.Attr(\u0022orientation\u0022, \u0022quaternion\u0022, Quat(skeleton[i].RestLocal));\r\n            w.EndArrayElement(last: i == skeleton.Count - 1);\r\n        }\r\n        w.EndArray();\r\n        w.EndArrayElement(last: true);\r\n        w.EndArray();\r\n\r\n        w.Attr(\u0022upAxis\u0022, \u0022string\u0022, options.UpAxisY ? \u0022Y\u0022 : \u0022Z\u0022);\r\n        w.BeginInlineAttr(\u0022axisSystem\u0022, \u0022DmeAxisSystem\u0022);\r\n        w.Attr(\u0022upAxis\u0022, \u0022int\u0022, options.UpAxisY ? \u00222\u0022 : \u00223\u0022);\r\n        w.Attr(\u0022forwardParity\u0022, \u0022int\u0022, \u00222\u0022);\r\n        w.Attr(\u0022coordSys\u0022, \u0022int\u0022, \u00220\u0022);\r\n        w.EndInlineAttr();\r\n        w.Attr(\u0022animationList\u0022, \u0022element\u0022, animListGuid);\r\n        w.EndInlineAttr(); // skeleton DmeModel\r\n\r\n        w.BeginInlineAttr(\u0022makefile\u0022, \u0022DmeDCCMakefile\u0022);\r\n        w.Attr(\u0022name\u0022, \u0022string\u0022, \u0022makefile\u0022);\r\n        w.BeginArray(\u0022sources\u0022);\r\n        w.BeginArrayElement(\u0022DmeSource\u0022);\r\n        w.Attr(\u0022name\u0022, \u0022string\u0022, options.SourceNote);\r\n        w.EndArrayElement(last: true);\r\n        w.EndArray();\r\n        w.EndInlineAttr();\r\n\r\n        // Deterministic placeholders \u2014 never wall-clock/user data, so output is reproducible.\r\n        w.BeginInlineAttr(\u0022exportTags\u0022, \u0022DmeExportTags\u0022);\r\n        w.Attr(\u0022name\u0022, \u0022string\u0022, \u0022exportTags\u0022);\r\n        w.Attr(\u0022date\u0022, \u0022string\u0022, \u00222026/01/01\u0022);\r\n        w.Attr(\u0022time\u0022, \u0022string\u0022, \u002212:00:00 am\u0022);\r\n        w.Attr(\u0022user\u0022, \u0022string\u0022, \u0022retargeter\u0022);\r\n        w.Attr(\u0022machine\u0022, \u0022string\u0022, \u0022retargeter\u0022);\r\n        w.Attr(\u0022app\u0022, \u0022string\u0022, \u0022humanoid-retargeter\u0022);\r\n        w.Attr(\u0022appVersion\u0022, \u0022string\u0022, \u00221.0\u0022);\r\n        w.Attr(\u0022cmdLine\u0022, \u0022string\u0022, \u0022humanoid-retargeter\u0022);\r\n        w.Attr(\u0022pwd\u0022, \u0022string\u0022, \u0022\u0022);\r\n        w.EndInlineAttr();\r\n\r\n        w.Attr(\u0022animationList\u0022, \u0022element\u0022, animListGuid);\r\n        w.EndTopLevel();\r\n\r\n        // ---- DmeAnimationList ----------------------------------------------\r\n        w.BeginTopLevel(\u0022DmeAnimationList\u0022);\r\n        w.Attr(\u0022id\u0022, \u0022elementid\u0022, animListGuid);\r\n        w.Attr(\u0022name\u0022, \u0022string\u0022, \u0022anim\u0022);\r\n        w.BeginArray(\u0022animations\u0022);\r\n        w.BeginArrayElement(\u0022DmeChannelsClip\u0022);\r\n        w.Attr(\u0022name\u0022, \u0022string\u0022, \u0022anim\u0022);\r\n\r\n        w.BeginInlineAttr(\u0022timeFrame\u0022, \u0022DmeTimeFrame\u0022);\r\n        w.Attr(\u0022start\u0022, \u0022time\u0022, Time(0.0));\r\n        w.Attr(\u0022duration\u0022, \u0022time\u0022, Time((clip.FrameCount - 1) / (double)clip.Fps));\r\n        w.Attr(\u0022offset\u0022, \u0022time\u0022, Time(0.0));\r\n        w.Attr(\u0022scale\u0022, \u0022float\u0022, \u00221\u0022);\r\n        w.EndInlineAttr();\r\n\r\n        w.Attr(\u0022color\u0022, \u0022color\u0022, \u00220 0 0 0\u0022);\r\n        w.Attr(\u0022text\u0022, \u0022string\u0022, \u0022\u0022);\r\n        w.Attr(\u0022mute\u0022, \u0022bool\u0022, \u00220\u0022);\r\n        w.BeginArray(\u0022trackGroups\u0022);\r\n        w.EndArray();\r\n        w.Attr(\u0022displayScale\u0022, \u0022float\u0022, \u00221\u0022);\r\n\r\n        var channelBones = new List\u003Cint\u003E(skeleton.Count);\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n        {\r\n            if (options.ChannelExcludedBones is null || !options.ChannelExcludedBones.Contains(i))\r\n                channelBones.Add(i);\r\n        }\r\n\r\n        w.BeginArray(\u0022channels\u0022);\r\n        for (var n = 0; n \u003C channelBones.Count; n\u002B\u002B)\r\n        {\r\n            var i = channelBones[n];\r\n            WriteChannel(w, skeleton, clip, i, transformGuids[i], position: true, last: false);\r\n            WriteChannel(w, skeleton, clip, i, transformGuids[i], position: false,\r\n                last: n == channelBones.Count - 1);\r\n        }\r\n        w.EndArray();\r\n\r\n        w.Attr(\u0022frameRate\u0022, \u0022int\u0022,\r\n            ((int)MathF.Round(clip.Fps)).ToString(CultureInfo.InvariantCulture));\r\n        w.EndArrayElement(last: true);\r\n        w.EndArray();\r\n        w.EndTopLevel();\r\n\r\n        // ---- top-level channel-target DmeTransforms (rest values) -----------\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n        {\r\n            w.BeginTopLevel(\u0022DmeTransform\u0022);\r\n            w.Attr(\u0022id\u0022, \u0022elementid\u0022, transformGuids[i]);\r\n            w.Attr(\u0022name\u0022, \u0022string\u0022, skeleton[i].Name);\r\n            w.Attr(\u0022position\u0022, \u0022vector3\u0022, Vec(skeleton[i].RestLocal));\r\n            w.Attr(\u0022orientation\u0022, \u0022quaternion\u0022, Quat(skeleton[i].RestLocal));\r\n            w.EndTopLevel();\r\n        }\r\n\r\n        // ---- top-level DmeJoints --------------------------------------------\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n        {\r\n            w.BeginTopLevel(\u0022DmeJoint\u0022);\r\n            w.Attr(\u0022id\u0022, \u0022elementid\u0022, jointGuids[i]);\r\n            w.Attr(\u0022name\u0022, \u0022string\u0022, skeleton[i].Name);\r\n            w.Attr(\u0022transform\u0022, \u0022element\u0022, transformGuids[i]);\r\n            w.Attr(\u0022shape\u0022, \u0022element\u0022, \u0022\u0022);\r\n            w.Attr(\u0022visible\u0022, \u0022bool\u0022, \u00221\u0022);\r\n            w.BeginArray(\u0022children\u0022);\r\n            var children = new List\u003Cint\u003E();\r\n            for (var c = 0; c \u003C skeleton.Count; c\u002B\u002B)\r\n            {\r\n                if (skeleton[c].ParentIndex == i)\r\n                    children.Add(c);\r\n            }\r\n            for (var c = 0; c \u003C children.Count; c\u002B\u002B)\r\n                w.ElementRef(jointGuids[children[c]], last: c == children.Count - 1);\r\n            w.EndArray();\r\n            w.EndTopLevel();\r\n        }\r\n\r\n        return w.ToString();\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Deterministic element GUID: MD5 over \u003Cc\u003E\u0022\u0026lt;name\u0026gt;\\n\u0026lt;path\u0026gt;\u0022\u003C/c\u003E (UTF-8)\r\n    /// interpreted as \u003Csee cref=\u0022Guid\u0022/\u003E bytes. Exposed so tests can verify the scheme.\r\n    /// \u003C/summary\u003E\r\n    public static Guid ElementGuid(string name, string path)\r\n        =\u003E new(MD5.HashData(Encoding.UTF8.GetBytes(name \u002B \u0022\\n\u0022 \u002B path)));\r\n\r\n    private static string GuidString(string name, string path)\r\n        =\u003E ElementGuid(name, path).ToString(\u0022D\u0022, CultureInfo.InvariantCulture);\r\n\r\n    // ---------------------------------------------------------------- channels\r\n\r\n    private static void WriteChannel(Emitter w, SkeletonModel skeleton, Clip clip, int bone,\r\n        string transformGuid, bool position, bool last)\r\n    {\r\n        var logClass = position ? \u0022DmeVector3Log\u0022 : \u0022DmeQuaternionLog\u0022;\r\n        var layerClass = position ? \u0022DmeVector3LogLayer\u0022 : \u0022DmeQuaternionLogLayer\u0022;\r\n        var logName = position ? \u0022vector3 log\u0022 : \u0022quaternion log\u0022;\r\n\r\n        w.BeginArrayElement(\u0022DmeChannel\u0022);\r\n        w.Attr(\u0022name\u0022, \u0022string\u0022, skeleton[bone].Name \u002B (position ? \u0022_p\u0022 : \u0022_o\u0022));\r\n        w.Attr(\u0022fromElement\u0022, \u0022element\u0022, \u0022\u0022);\r\n        w.Attr(\u0022fromAttribute\u0022, \u0022string\u0022, \u0022\u0022);\r\n        w.Attr(\u0022fromIndex\u0022, \u0022int\u0022, \u00220\u0022);\r\n        w.Attr(\u0022toElement\u0022, \u0022element\u0022, transformGuid);\r\n        w.Attr(\u0022toAttribute\u0022, \u0022string\u0022, position ? \u0022position\u0022 : \u0022orientation\u0022);\r\n        w.Attr(\u0022toIndex\u0022, \u0022int\u0022, \u00220\u0022);\r\n        w.Attr(\u0022mode\u0022, \u0022int\u0022, \u00223\u0022);\r\n\r\n        w.BeginInlineAttr(\u0022log\u0022, logClass);\r\n        w.Attr(\u0022name\u0022, \u0022string\u0022, logName);\r\n        w.BeginArray(\u0022layers\u0022);\r\n        w.BeginArrayElement(layerClass);\r\n        w.Attr(\u0022name\u0022, \u0022string\u0022, logName);\r\n\r\n        w.BeginArray(\u0022times\u0022, \u0022time_array\u0022);\r\n        for (var f = 0; f \u003C clip.FrameCount; f\u002B\u002B)\r\n            w.ArrayValue(Time(f / (double)clip.Fps), last: f == clip.FrameCount - 1);\r\n        w.EndArray();\r\n\r\n        w.BeginArray(\u0022curvetypes\u0022, \u0022int_array\u0022);\r\n        w.EndArray();\r\n\r\n        w.BeginArray(\u0022values\u0022, position ? \u0022vector3_array\u0022 : \u0022quaternion_array\u0022);\r\n        // Orientation values are hemisphere-aligned on the fly (q and -q are the same\r\n        // rotation, but the engine interpolates between DMX samples numerically \u2014 see\r\n        // QuaternionContinuity). The clip itself is never mutated.\r\n        var prev = System.Numerics.Quaternion.Identity;\r\n        for (var f = 0; f \u003C clip.FrameCount; f\u002B\u002B)\r\n        {\r\n            var x = clip.Frames[f][bone];\r\n            string value;\r\n            if (position)\r\n            {\r\n                value = Vec(x);\r\n            }\r\n            else\r\n            {\r\n                var q = x.Rot;\r\n                if (f \u003E 0 \u0026\u0026 System.Numerics.Quaternion.Dot(prev, q) \u003C 0f)\r\n                    q = System.Numerics.Quaternion.Negate(q);\r\n                prev = q;\r\n                value = Quat(q);\r\n            }\r\n            w.ArrayValue(value, last: f == clip.FrameCount - 1);\r\n        }\r\n        w.EndArray();\r\n\r\n        w.EmptyBinaryAttr(\u0022compressed\u0022);\r\n        w.EndArrayElement(last: true);\r\n        w.EndArray(); // layers\r\n\r\n        w.Attr(\u0022curveinfo\u0022, \u0022element\u0022, \u0022\u0022);\r\n        w.Attr(\u0022usedefaultvalue\u0022, \u0022bool\u0022, \u00220\u0022);\r\n        w.Attr(\u0022defaultvalue\u0022, position ? \u0022vector3\u0022 : \u0022quaternion\u0022, position ? \u00220 0 0\u0022 : \u00220 0 0 1\u0022);\r\n        w.BeginArray(\u0022bookmarksX\u0022, \u0022time_array\u0022);\r\n        w.EndArray();\r\n        w.BeginArray(\u0022bookmarksY\u0022, \u0022time_array\u0022);\r\n        w.EndArray();\r\n        w.BeginArray(\u0022bookmarksZ\u0022, \u0022time_array\u0022);\r\n        w.EndArray();\r\n        w.EndInlineAttr(); // log\r\n\r\n        w.EndArrayElement(last);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- formatting\r\n\r\n    /// \u003Csummary\u003Efbx2dmx float style: up to 10 decimal places, trailing zeros stripped,\r\n    /// invariant culture, negative zero normalized.\u003C/summary\u003E\r\n    private static string F(float value)\r\n    {\r\n        if (value == 0f)\r\n            return \u00220\u0022;\r\n        return ((double)value).ToString(\u00220.##########\u0022, CultureInfo.InvariantCulture);\r\n    }\r\n\r\n    private static string Time(double seconds)\r\n        =\u003E seconds.ToString(\u00220.0000\u0022, CultureInfo.InvariantCulture);\r\n\r\n    private static string Vec(in Maths.XForm x)\r\n        =\u003E $\u0022{F(x.Pos.X)} {F(x.Pos.Y)} {F(x.Pos.Z)}\u0022;\r\n\r\n    private static string Quat(in Maths.XForm x) =\u003E Quat(x.Rot);\r\n\r\n    private static string Quat(in System.Numerics.Quaternion q)\r\n        =\u003E $\u0022{F(q.X)} {F(q.Y)} {F(q.Z)} {F(q.W)}\u0022;\r\n\r\n    // ---------------------------------------------------------------- emitter\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Low-level keyvalues2 text emitter reproducing fbx2dmx layout quirks: CRLF endings,\r\n    /// tab indentation, a trailing space after array-typed attribute names, and an\r\n    /// indentation-only line after every inline element attribute closes.\r\n    /// \u003C/summary\u003E\r\n    private sealed class Emitter\r\n    {\r\n        private readonly StringBuilder _sb = new();\r\n        private int _indent;\r\n\r\n        public void Raw(string text)\r\n        {\r\n            _sb.Append(text).Append(\u0022\\r\\n\u0022);\r\n        }\r\n\r\n        private void Line(string text)\r\n        {\r\n            _sb.Append(\u0027\\t\u0027, _indent).Append(text).Append(\u0022\\r\\n\u0022);\r\n        }\r\n\r\n        public void Attr(string name, string type, string value)\r\n            =\u003E Line($\u0022\\\u0022{name}\\\u0022 \\\u0022{type}\\\u0022 \\\u0022{value}\\\u0022\u0022);\r\n\r\n        public void BeginTopLevel(string className)\r\n        {\r\n            Line($\u0022\\\u0022{className}\\\u0022\u0022);\r\n            Line(\u0022{\u0022);\r\n            _indent\u002B\u002B;\r\n        }\r\n\r\n        public void EndTopLevel()\r\n        {\r\n            _indent--;\r\n            Line(\u0022}\u0022);\r\n            _sb.Append(\u0022\\r\\n\u0022); // blank separator after every top-level element (incl. the last)\r\n        }\r\n\r\n        public void BeginInlineAttr(string name, string className)\r\n        {\r\n            Line($\u0022\\\u0022{name}\\\u0022 \\\u0022{className}\\\u0022\u0022);\r\n            Line(\u0022{\u0022);\r\n            _indent\u002B\u002B;\r\n        }\r\n\r\n        public void EndInlineAttr()\r\n        {\r\n            _indent--;\r\n            Line(\u0022}\u0022);\r\n            Line(\u0022\u0022); // indentation-only line, as fbx2dmx emits\r\n        }\r\n\r\n        public void BeginArrayElement(string className)\r\n        {\r\n            Line($\u0022\\\u0022{className}\\\u0022\u0022);\r\n            Line(\u0022{\u0022);\r\n            _indent\u002B\u002B;\r\n        }\r\n\r\n        public void EndArrayElement(bool last)\r\n        {\r\n            _indent--;\r\n            Line(last ? \u0022}\u0022 : \u0022},\u0022);\r\n        }\r\n\r\n        public void BeginArray(string name, string type = \u0022element_array\u0022)\r\n        {\r\n            Line($\u0022\\\u0022{name}\\\u0022 \\\u0022{type}\\\u0022 \u0022);\r\n            Line(\u0022[\u0022);\r\n            _indent\u002B\u002B;\r\n        }\r\n\r\n        public void EndArray()\r\n        {\r\n            _indent--;\r\n            Line(\u0022]\u0022);\r\n        }\r\n\r\n        public void ElementRef(string guid, bool last)\r\n            =\u003E Line($\u0022\\\u0022element\\\u0022 \\\u0022{guid}\\\u0022\u0022 \u002B (last ? \u0022\u0022 : \u0022,\u0022));\r\n\r\n        public void ArrayValue(string value, bool last)\r\n            =\u003E Line($\u0022\\\u0022{value}\\\u0022\u0022 \u002B (last ? \u0022\u0022 : \u0022,\u0022));\r\n\r\n        public void EmptyBinaryAttr(string name)\r\n        {\r\n            Line($\u0022\\\u0022{name}\\\u0022 \\\u0022binary\\\u0022 \u0022);\r\n            Line(\u0022\\\u0022\u0022);\r\n            Line(\u0022\\\u0022\u0022);\r\n        }\r\n\r\n        public override string ToString() =\u003E _sb.ToString();\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"HumanoidRetargeter/Formats/Fbx/FbxBindPoseFixer.cs","FileName":"FbxBindPoseFixer.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidRetargeter.Maths;\r\n\r\nnamespace HumanoidRetargeter.Formats.Fbx;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// \u003Csummary\u003E\r\n/// Repairs FBX files that were exported MID-POSE: their node transforms (Lcl\r\n/// Translation/Rotation) hold an animation snapshot while the true skeleton bind lives in\r\n/// the file\u0027s Pose/BindPose section. Engines that build the skeleton from node transforms\r\n/// (s\u0026amp;box does; the FBX SDK\u0027s own samples do) then import a posed \u0022bind\u0022 \u2014 the skin\r\n/// stays self-consistent so the model LOOKS fine at rest, but every anatomical assumption\r\n/// about the skeleton (leg chains point down, hands mirror) is silently wrong and\r\n/// retargeted motion comes out mangled on exactly the posed bones. Found in the wild on\r\n/// Auto-Rig Pro exports whose IK\u0027d hands/feet were left posed (one leg at hip height).\r\n/// \u003C/summary\u003E\r\npublic static class FbxBindPoseFixer\r\n{\r\n    /// \u003Csummary\u003EBones whose node transform is further than this (native units, as a\r\n    /// fraction of skeleton height) from their BindPose matrix count as posed.\u003C/summary\u003E\r\n    private const float PositionToleranceOfHeight = 0.01f;\r\n\r\n    /// \u003Csummary\u003ERotation disagreement (degrees) that counts as posed.\u003C/summary\u003E\r\n    private const float RotationToleranceDeg = 2.0f;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Detects the mid-pose condition and rewrites node transforms to the BindPose. Returns\r\n    /// null when the file needs no repair (no BindPose section, or node transforms already\r\n    /// agree with it); otherwise the repaired file bytes.\r\n    /// \u003Cparamref name=\u0022report\u0022/\u003E always describes what was found.\r\n    /// \u003C/summary\u003E\r\n    public static byte[]? TryFix(byte[] fbx, out string report)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(fbx);\r\n\r\n        FbxNode root;\r\n        FbxScene scene;\r\n        try\r\n        {\r\n            root = FbxTokenizer.Parse(fbx);\r\n            scene = FbxScene.Build(root);\r\n        }\r\n        catch (FormatException e)\r\n        {\r\n            report = $\u0022not parseable ({e.Message})\u0022;\r\n            return null;\r\n        }\r\n\r\n        if (scene.BindPose.Count == 0)\r\n        {\r\n            report = \u0022no BindPose section\u0022;\r\n            return null;\r\n        }\r\n\r\n        // Evaluate the ORIGINAL node-transform FK, roots first.\r\n        var originalWorld = new Dictionary\u003Clong, Matrix4x4\u003E();\r\n        var order = new List\u003CFbxObject\u003E();\r\n        foreach (var model in scene.Models)\r\n            VisitModel(model, scene, originalWorld, order);\r\n\r\n        // Skeleton height (native units) for the position tolerance.\r\n        float minUp = float.MaxValue, maxUp = float.MinValue;\r\n        foreach (var w in originalWorld.Values)\r\n        {\r\n            var t = w.Translation;\r\n            float up = MathF.Max(MathF.Abs(t.Y), MathF.Abs(t.Z));\r\n            minUp = MathF.Min(minUp, up);\r\n            maxUp = MathF.Max(maxUp, up);\r\n        }\r\n        float posTolerance = MathF.Max(0.0001f, (maxUp - minUp) * PositionToleranceOfHeight);\r\n\r\n        // Any bone posed away from its bind?\r\n        int posedCount = 0;\r\n        foreach (var model in order)\r\n        {\r\n            if (!scene.BindPose.TryGetValue(model.Id, out var bind))\r\n                continue;\r\n            var fk = FbxTransform.ToRigid(originalWorld[model.Id]);\r\n            var target = FbxTransform.ToRigid(bind);\r\n            if ((fk.Pos - target.Pos).Length() \u003E posTolerance\r\n                || MathQ.AngleBetween(fk.Rot, target.Rot) \u003E RotationToleranceDeg * MathF.PI / 180f)\r\n            {\r\n                posedCount\u002B\u002B;\r\n            }\r\n        }\r\n        if (posedCount == 0)\r\n        {\r\n            report = $\u0022node transforms match the BindPose ({scene.BindPose.Count} entries)\u0022;\r\n            return null;\r\n        }\r\n\r\n        // Rewrite every BindPose-backed model\u0027s local transform so FK lands on the bind.\r\n        // correctedWorld carries the repair down the hierarchy for bones WITHOUT a\r\n        // BindPose entry (helpers keep their original locals under corrected parents).\r\n        var correctedWorld = new Dictionary\u003Clong, Matrix4x4\u003E();\r\n        int patched = 0, skipped = 0;\r\n        foreach (var model in order)\r\n        {\r\n            var parent = model.ModelParent;\r\n            Matrix4x4 parentWorld = parent is not null \u0026\u0026 correctedWorld.TryGetValue(parent.Id, out var pw)\r\n                ? pw\r\n                : Matrix4x4.Identity;\r\n\r\n            if (!scene.BindPose.TryGetValue(model.Id, out var bindWorld))\r\n            {\r\n                // No bind info: keep the original local under the (possibly corrected) parent.\r\n                var transform = FbxTransform.FromModel(scene, model);\r\n                correctedWorld[model.Id] = transform.LocalMatrixDefault() * parentWorld;\r\n                continue;\r\n            }\r\n\r\n            if (!Matrix4x4.Invert(parentWorld, out var invParent))\r\n            {\r\n                correctedWorld[model.Id] = bindWorld;\r\n                skipped\u002B\u002B;\r\n                continue;\r\n            }\r\n\r\n            // Row-vector: World = Local \u00B7 ParentWorld  \u21D2  Local = World \u00B7 ParentWorld\u207B\u00B9.\r\n            var desiredLocal = FbxTransform.ToRigid(bindWorld * invParent);\r\n            if (PatchModelLocal(scene, model, desiredLocal))\r\n                patched\u002B\u002B;\r\n            else\r\n                skipped\u002B\u002B;\r\n\r\n            // Children FK from the ACTUAL bind either way (unpatchable bones are rare and\r\n            // their children still deserve correct parent frames).\r\n            correctedWorld[model.Id] = bindWorld;\r\n        }\r\n\r\n        report = $\u0022{posedCount} bones were exported mid-pose; repaired {patched}\u0022\r\n            \u002B (skipped \u003E 0 ? $\u0022, {skipped} left as-is (pivots/scale beyond the safe rewrite)\u0022 : \u0022\u0022);\r\n        if (patched == 0)\r\n            return null;\r\n        return FbxBinaryWriter.Write(root);\r\n    }\r\n\r\n    private static void VisitModel(\r\n        FbxObject model, FbxScene scene,\r\n        Dictionary\u003Clong, Matrix4x4\u003E world, List\u003CFbxObject\u003E order)\r\n    {\r\n        if (world.ContainsKey(model.Id))\r\n            return;\r\n        Matrix4x4 parentWorld = Matrix4x4.Identity;\r\n        if (model.ModelParent is { } parent)\r\n        {\r\n            VisitModel(parent, scene, world, order);\r\n            parentWorld = world[parent.Id];\r\n        }\r\n        var transform = FbxTransform.FromModel(scene, model);\r\n        world[model.Id] = transform.LocalMatrixDefault() * parentWorld;\r\n        order.Add(model);\r\n    }\r\n\r\n    // ------------------------------------------------------------------ patching\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Rewrites one Model\u0027s Lcl Translation/Rotation so its local evaluates to\r\n    /// \u003Cparamref name=\u0022desiredLocal\u0022/\u003E. Verified by re-evaluating through the full FBX\r\n    /// transform formula \u2014 models using pivots/offsets/scale that the rewrite cannot\r\n    /// express are left untouched (returns false).\r\n    /// \u003C/summary\u003E\r\n    private static bool PatchModelLocal(FbxScene scene, FbxObject model, XForm desiredLocal)\r\n    {\r\n        var transform = FbxTransform.FromModel(scene, model);\r\n\r\n        // R_total = Pre \u00B7 R \u00B7 Post\u207B\u00B9  \u21D2  R = Pre\u207B\u00B9 \u00B7 R_total \u00B7 Post\r\n        var r = MathQ.Normalize(\r\n            Quaternion.Conjugate(transform.PreRotation)\r\n            * desiredLocal.Rot\r\n            * transform.PostRotation);\r\n\r\n        var eulerDeg = QuaternionToEulerDegrees(r, transform.RotationOrder);\r\n\r\n        // Full-formula verification (catches pivots, scale, decomposition branches).\r\n        var check = new FbxTransform\r\n        {\r\n            LclTranslation = desiredLocal.Pos,\r\n            LclRotationDeg = eulerDeg,\r\n            LclScaling = transform.LclScaling,\r\n            PreRotation = transform.PreRotation,\r\n            PostRotation = transform.PostRotation,\r\n            RotationOffset = transform.RotationOffset,\r\n            RotationPivot = transform.RotationPivot,\r\n            ScalingOffset = transform.ScalingOffset,\r\n            ScalingPivot = transform.ScalingPivot,\r\n            RotationOrder = transform.RotationOrder,\r\n        };\r\n        var evaluated = FbxTransform.ToRigid(check.LocalMatrixDefault());\r\n        float posScale = MathF.Max(1f, desiredLocal.Pos.Length());\r\n        if ((evaluated.Pos - desiredLocal.Pos).Length() \u003E 0.001f * posScale\r\n            || MathQ.AngleBetween(evaluated.Rot, desiredLocal.Rot) \u003E 0.1f * MathF.PI / 180f)\r\n        {\r\n            return false;\r\n        }\r\n\r\n        SetProperty70(model.Node, \u0022Lcl Translation\u0022, \u0022Lcl Translation\u0022, \u0022A\u0022,\r\n            desiredLocal.Pos.X, desiredLocal.Pos.Y, desiredLocal.Pos.Z);\r\n        SetProperty70(model.Node, \u0022Lcl Rotation\u0022, \u0022Lcl Rotation\u0022, \u0022A\u0022,\r\n            eulerDeg.X, eulerDeg.Y, eulerDeg.Z);\r\n        return true;\r\n    }\r\n\r\n    /// \u003Csummary\u003ESets (or adds) a 3-double P entry in the node\u0027s Properties70 block.\u003C/summary\u003E\r\n    private static void SetProperty70(\r\n        FbxNode modelNode, string name, string type, string flags,\r\n        double x, double y, double z)\r\n    {\r\n        var block = modelNode.Child(\u0022Properties70\u0022);\r\n        if (block is null)\r\n        {\r\n            block = new FbxNode(\u0022Properties70\u0022);\r\n            modelNode.Children.Insert(0, block);\r\n        }\r\n\r\n        foreach (var p in block.ChildrenNamed(\u0022P\u0022))\r\n        {\r\n            if (p.Properties.Count \u003E= 1 \u0026\u0026 p.Properties[0] is string n \u0026\u0026 n == name)\r\n            {\r\n                // Values live at indices 4.. \u2014 replace, extending if the entry was short.\r\n                while (p.Properties.Count \u003C 7)\r\n                    p.Properties.Add(0.0);\r\n                p.Properties[4] = x;\r\n                p.Properties[5] = y;\r\n                p.Properties[6] = z;\r\n                return;\r\n            }\r\n        }\r\n\r\n        var entry = new FbxNode(\u0022P\u0022);\r\n        entry.Properties.Add(name);\r\n        entry.Properties.Add(type);\r\n        entry.Properties.Add(\u0022\u0022);\r\n        entry.Properties.Add(flags);\r\n        entry.Properties.Add(x);\r\n        entry.Properties.Add(y);\r\n        entry.Properties.Add(z);\r\n        block.Children.Add(entry);\r\n    }\r\n\r\n    // ------------------------------------------------------------------ euler decomposition\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Decomposes a quaternion into FBX euler degrees for the given RotationOrder, the\r\n    /// exact inverse of \u003Csee cref=\u0022FbxTransform.EulerDegreesToQuaternion\u0022/\u003E. Tait-Bryan\r\n    /// extraction on the column-convention rotation matrix.\r\n    /// \u003C/summary\u003E\r\n    public static Vector3 QuaternionToEulerDegrees(Quaternion q, int order)\r\n    {\r\n        // Column-convention matrix C (v\u0027 = C\u00B7v): C = transpose of System.Numerics\u0027 row form.\r\n        var m = Matrix4x4.CreateFromQuaternion(q);\r\n        // C[r,c]: row r, column c.\r\n        float c00 = m.M11, c01 = m.M21, c02 = m.M31;\r\n        float c10 = m.M12, c11 = m.M22, c12 = m.M32;\r\n        float c20 = m.M13, c21 = m.M23, c22 = m.M33;\r\n\r\n        const float radToDeg = 180f / MathF.PI;\r\n        float a, b, c;\r\n        switch (order)\r\n        {\r\n            case 0: // XYZ: C = Rz\u00B7Ry\u00B7Rx\r\n                b = MathF.Asin(Math.Clamp(-c20, -1f, 1f));\r\n                a = MathF.Atan2(c21, c22);\r\n                c = MathF.Atan2(c10, c00);\r\n                return new Vector3(a * radToDeg, b * radToDeg, c * radToDeg);\r\n            case 1: // XZY: C = Ry\u00B7Rz\u00B7Rx\r\n                b = MathF.Asin(Math.Clamp(c10, -1f, 1f));\r\n                a = MathF.Atan2(-c12, c11);\r\n                c = MathF.Atan2(-c20, c00);\r\n                return new Vector3(a * radToDeg, c * radToDeg, b * radToDeg);\r\n            case 2: // YZX: C = Rx\u00B7Rz\u00B7Ry\r\n                b = MathF.Asin(Math.Clamp(-c01, -1f, 1f));\r\n                a = MathF.Atan2(c02, c00);\r\n                c = MathF.Atan2(c21, c11);\r\n                return new Vector3(c * radToDeg, a * radToDeg, b * radToDeg);\r\n            case 3: // YXZ: C = Rz\u00B7Rx\u00B7Ry\r\n                b = MathF.Asin(Math.Clamp(c21, -1f, 1f));\r\n                a = MathF.Atan2(-c20, c22);\r\n                c = MathF.Atan2(-c01, c11);\r\n                return new Vector3(b * radToDeg, a * radToDeg, c * radToDeg);\r\n            case 4: // ZXY: C = Ry\u00B7Rx\u00B7Rz\r\n                b = MathF.Asin(Math.Clamp(-c12, -1f, 1f));\r\n                a = MathF.Atan2(c02, c22);\r\n                c = MathF.Atan2(c10, c11);\r\n                return new Vector3(b * radToDeg, a * radToDeg, c * radToDeg);\r\n            case 5: // ZYX: C = Rx\u00B7Ry\u00B7Rz\r\n            case 6: // eSphericXYZ treated as XYZ on read; mirror that here\r\n            default:\r\n                if (order == 5)\r\n                {\r\n                    b = MathF.Asin(Math.Clamp(c02, -1f, 1f));\r\n                    a = MathF.Atan2(-c01, c00);\r\n                    c = MathF.Atan2(-c12, c22);\r\n                    return new Vector3(c * radToDeg, b * radToDeg, a * radToDeg);\r\n                }\r\n                goto case 0;\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"HumanoidRetargeter/RetargetRequest.cs","FileName":"RetargetRequest.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing HumanoidRetargeter.Cleanup;\r\nusing HumanoidRetargeter.Formats;\r\nusing HumanoidRetargeter.Mapping;\r\nusing HumanoidRetargeter.Solve;\r\nusing HumanoidRetargeter.Target;\r\n\r\nnamespace HumanoidRetargeter;\r\n\r\n/// \u003Csummary\u003EWhich solver retargets a request\u0027s clips (design \u00A710).\u003C/summary\u003E\r\npublic enum SolverKind\r\n{\r\n    /// \u003Csummary\u003EThe deterministic \u003Csee cref=\u0022Solve.GeometricSolver\u0022/\u003E (default; better\r\n    /// wherever a role mapping exists).\u003C/summary\u003E\r\n    Geometric,\r\n\r\n    /// \u003Csummary\u003EThe experimental skeleton-agnostic deep-learning solver\r\n    /// (\u003Csee cref=\u0022Dl.DlSolver\u0022/\u003E, SAME pretrained checkpoint) \u2014 the no-profile fallback.\r\n    /// Requires \u003Csee cref=\u0022RetargetTargetSpec.DlWeights\u0022/\u003E; ignores per-role mapping\r\n    /// (only hips/alignment heuristics consult it) and leaves fingers at rest.\u003C/summary\u003E\r\n    DeepLearning,\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// One source animation file to retarget (engine-agnostic: bytes in, no file IO). Every\r\n/// request runs its OWN profile detection, so a single batch may mix Mixamo \u002B ActorCore \u002B\r\n/// BVH sources \u2014 unless \u003Csee cref=\u0022MappingOverride\u0022/\u003E supplies a mapping explicitly.\r\n/// \u003C/summary\u003E\r\npublic sealed class RetargetRequest\r\n{\r\n    /// \u003Csummary\u003ESolver choice for this request\u0027s clips. \u003Csee cref=\u0022SolverKind.DeepLearning\u0022/\u003E\r\n    /// requires the batch\u0027s \u003Csee cref=\u0022RetargetTargetSpec.DlWeights\u0022/\u003E to be set; the\r\n    /// conversion fails per-clip with a clear error otherwise.\u003C/summary\u003E\r\n    public SolverKind Solver { get; init; } = SolverKind.Geometric;\r\n\r\n    /// \u003Csummary\u003ERaw bytes of the source file (.fbx, .bvh, .glb, .gltf, .vrm, .anm or .an5).\u003C/summary\u003E\r\n    public required byte[] SourceData { get; init; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Source file name (used for the report and DMX provenance). The extension drives the\r\n    /// format choice (\u003Cc\u003E.fbx\u003C/c\u003E / \u003Cc\u003E.bvh\u003C/c\u003E / \u003Cc\u003E.glb\u003C/c\u003E / \u003Cc\u003E.gltf\u003C/c\u003E / \u003Cc\u003E.vrm\u003C/c\u003E \u2014\r\n    /// a VRM is a glTF container whose authored humanoid bone map becomes the mapping \u2014 /\r\n    /// \u003Cc\u003E.anm\u003C/c\u003E / \u003Cc\u003E.an5\u003C/c\u003E RenderWare animations, which additionally need\r\n    /// \u003Csee cref=\u0022SkeletonData\u0022/\u003E); when the extension is unknown the content is sniffed\r\n    /// (FBX binary magic / \u0022FBXHeaderExtension\u0022 / BVH \u0022HIERARCHY\u0022 / GLB \u0027glTF\u0027 magic /\r\n    /// glTF JSON / RenderWare 0x1B animation chunk).\r\n    /// \u003C/summary\u003E\r\n    public required string SourceFileName { get; init; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Raw bytes of a companion SKELETON file for formats whose animation files carry no\r\n    /// skeleton of their own: RenderWare \u003Cc\u003E.anm\u003C/c\u003E/\u003Cc\u003E.an5\u003C/c\u003E sources require the\r\n    /// character model\u0027s \u003Cc\u003E.dff\u003C/c\u003E here (callers resolve the file \u2014 e.g. a .dff sitting\r\n    /// next to the animation; the facade does no file IO). Ignored by self-contained\r\n    /// formats. A RenderWare request without it fails with an instructive error.\r\n    /// \u003C/summary\u003E\r\n    public byte[]? SkeletonData { get; init; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Caller-supplied identity of this request, echoed verbatim on every produced\r\n    /// \u003Csee cref=\u0022ClipResult.SourceId\u0022/\u003E so callers can join results back to their own\r\n    /// entries unambiguously (e.g. the editor window passes the FULL file path here, since\r\n    /// two files in different folders may share the same \u003Csee cref=\u0022SourceFileName\u0022/\u003E).\r\n    /// Null = \u003Csee cref=\u0022SourceFileName\u0022/\u003E.\r\n    /// \u003C/summary\u003E\r\n    public string? SourceId { get; init; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Import sample rate the source clips are resampled to (BVH native frames / FBX curves\r\n    /// are evaluated on this grid). Null = the importer default (30 fps).\r\n    /// \u003C/summary\u003E\r\n    public float? SampleFps { get; init; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Restricts the conversion to ONE take of the source file (0-based index into the\r\n    /// imported scene\u0027s clips). Null = convert all takes. Out of range fails the request\u0027s\r\n    /// clip result with a clear error (the batch continues). UI listings that expand a\r\n    /// multi-take file into one entry per take submit one request per selected take.\r\n    /// When \u003Csee cref=\u0022ClipDefinitions\u0022/\u003E is set this index addresses the DEFINITIONS\r\n    /// instead (each definition is what a UI row represents then).\r\n    /// \u003C/summary\u003E\r\n    public int? TakeIndex { get; init; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Optional external clip definitions, parsed from a Unity \u003Cc\u003E\u0026lt;file\u0026gt;.fbx.meta\u003C/c\u003E\r\n    /// sidecar (\u003Csee cref=\u0022UnityMeta.ParseClipAnimations\u0022/\u003E): Unity animation packs ship FBX\r\n    /// files whose clips are sub-ranges of ONE source timeline. When set (non-empty), the\r\n    /// conversion produces one output clip per definition instead of one per take: the\r\n    /// definition\u0027s take (matched by \u003Csee cref=\u0022ExternalClipDef.TakeName\u0022/\u003E, falling back to\r\n    /// the file\u0027s first take) is sliced to the definition\u0027s native-frame range\r\n    /// (\u003Csee cref=\u0022UnityMeta.Slice\u0022/\u003E), named \u003Csee cref=\u0022ExternalClipDef.Name\u0022/\u003E (sanitized\r\n    /// like take names, collision-suffixed across the batch) and looped per\r\n    /// \u003Csee cref=\u0022ExternalClipDef.Loop\u0022/\u003E unless \u003Csee cref=\u0022LoopingOverride\u0022/\u003E is set.\r\n    /// \u003Csee cref=\u0022TakeIndex\u0022/\u003E then indexes INTO this list. Null = no definitions.\r\n    /// \u003C/summary\u003E\r\n    public IReadOnlyList\u003CExternalClipDef\u003E? ClipDefinitions { get; init; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// UI-supplied mapping (manual mapping table or a user preset loaded Editor-side).\r\n    /// Null = auto-detect per request: preset profiles via \u003Csee cref=\u0022ProfileDetector\u0022/\u003E,\r\n    /// then the \u003Csee cref=\u0022AutoMapper\u0022/\u003E as best-effort fallback.\r\n    /// \u003C/summary\u003E\r\n    public MappingResult? MappingOverride { get; init; }\r\n\r\n    /// \u003Csummary\u003ESolver tunables (hip scales, finger transfer). ClipIndex/ClipName are managed\r\n    /// by the pipeline per take and ignored here. Null = defaults.\u003C/summary\u003E\r\n    public SolveOptions? Solve { get; init; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Root-motion handling. \u003Csee cref=\u0022RootMotionMode.Extract\u0022/\u003E on a target without a\r\n    /// dedicated animated root bone (the s\u0026amp;box rig: pelvis is parentless, root_IK is\r\n    /// IkBaked) leaves the frames untouched and instead sets the ExtractMotion flag on the\r\n    /// clip\u0027s vmdl AnimFile entry \u2014 Source 2\u0027s compile-time extraction replaces the missing\r\n    /// bone-level extraction. \u003Csee cref=\u0022RootMotionMode.InPlace\u0022/\u003E always operates on the\r\n    /// hips directly.\r\n    /// \u003C/summary\u003E\r\n    public RootMotionMode RootMotion { get; init; } = RootMotionMode.Off;\r\n\r\n    /// \u003Csummary\u003ERun the Kovar foot-plant cleanup pass on the solved frames (default on).\u003C/summary\u003E\r\n    public bool FootPlantCleanup { get; init; } = true;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Copy the source clip\u0027s per-frame LOCAL translations onto same-named target bones\r\n    /// (hips and its ancestors excluded \u2014 trajectory stays solver-owned). For SAME-RIG\r\n    /// conversions of authored takes (a target FBX\u0027s own embedded animations): the solver\r\n    /// pins every non-hips bone to its rest translation, silently dropping a Biped take\u0027s\r\n    /// animated spine/thigh translations (~19cm of authored body sway on a death fall).\r\n    /// Meaningless across different rigs \u2014 leave off (default) for real retargets.\r\n    /// \u003C/summary\u003E\r\n    public bool PreserveSourceTranslations { get; init; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Optional arm end-effector IK pass pulling the wrists onto limb-length-normalized\r\n    /// source hand positions. Default OFF: the geometric solver already matches anatomical\r\n    /// directions, so arm IK only helps reach-critical work (props, contact poses) and can\r\n    /// otherwise disturb elbow styling.\r\n    /// \u003C/summary\u003E\r\n    public bool ArmEffectorIk { get; init; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Generate \u003Cc\u003EAE_FOOTSTEP\u003C/c\u003E AnimEvent nodes on each produced clip\u0027s vmdl AnimFile\r\n    /// entry (default OFF). After solving and cleanup, foot-plant intervals are detected on\r\n    /// the SOLVED target clip (\u003Csee cref=\u0022Cleanup.FootPlant.DetectPlantIntervals\u0022/\u003E); each\r\n    /// plant\u0027s start frame is a touchdown and becomes one footstep event, in the exact node\r\n    /// shape the shipped citizen data uses (see \u003Csee cref=\u0022Target.FootstepEvents\u0022/\u003E).\r\n    /// Skipped (with a report note) when the target rig lacks complete leg chains.\r\n    /// \u003C/summary\u003E\r\n    public bool GenerateFootstepEvents { get; init; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Additionally produce a mirrored twin of every converted clip (default OFF), named\r\n    /// \u003Cc\u003E\u0026lt;clip\u0026gt;_M\u003C/c\u003E (collision-suffixed across the batch as usual). Mirroring runs\r\n    /// in TARGET space on the solved clip (\u003Csee cref=\u0022Solve.ClipMirror\u0022/\u003E): left/right role\r\n    /// bone channels swap and everything is reflected across the target character\u0027s sagittal\r\n    /// plane; IK-baked helper bones are re-baked from the mirrored body afterwards.\r\n    /// \u003C/summary\u003E\r\n    public bool CreateMirroredVariant { get; init; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Additionally register an additive (delta) twin of every converted clip in the\r\n    /// generated/augmented vmdl (default OFF), named \u003Cc\u003E\u0026lt;clip\u0026gt;_delta\u003C/c\u003E (the shipped\r\n    /// citizen naming; collision-suffixed across the batch as usual). The twin is a second\r\n    /// AnimFile entry REUSING the clip\u0027s DMX with an \u003Cc\u003EAnimSubtract\u003C/c\u003E child\r\n    /// (\u003Cc\u003Eanim_name\u003C/c\u003E = the base sequence, \u003Cc\u003Eframe\u003C/c\u003E = 0) \u2014 exactly the shipped\r\n    /// \u003Cc\u003EIdleLayer_01\u003C/c\u003E/\u003Cc\u003EIdleLayer_01_delta\u003C/c\u003E pattern, where resourcecompiler\r\n    /// subtracts the reference frame at compile time (no frame math happens here). The\r\n    /// resulting \u003Cc\u003E_delta\u003C/c\u003E sequence is what s\u0026amp;box layered animation additively\r\n    /// blends on top of a base pose.\r\n    /// \u003C/summary\u003E\r\n    public bool CreateAdditiveVariant { get; init; }\r\n\r\n    /// \u003Csummary\u003EOutput clip name override; with multiple takes an index suffix is appended.\r\n    /// Null = the source take name.\u003C/summary\u003E\r\n    public string? ClipNameOverride { get; init; }\r\n\r\n    /// \u003Csummary\u003EForce the looping flag on the output sequence(s); null = the source clip\u0027s flag.\u003C/summary\u003E\r\n    public bool? LoopingOverride { get; init; }\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Axis/unit convention of a \u003Csee cref=\u0022RetargetTargetSpec\u0022/\u003E\u0027s rig data \u2014 drives the DMX\r\n/// axis-system declaration, foot-plant threshold units, and the editor preview\u0027s\r\n/// rig-space \u2192 engine-space conversion.\r\n/// \u003C/summary\u003E\r\npublic enum TargetUpAxis\r\n{\r\n    /// \u003Csummary\u003E\r\n    /// The s\u0026amp;box source convention: rig authored in centimeters, Y-up (the shipped\r\n    /// citizen rig, FBX targets). The vmdl\u0027s ScaleAndMirror 0.3937 \u002B resourcecompiler\u0027s\r\n    /// Y-up\u2192Z-up conversion take it to engine space at compile time. Default.\r\n    /// \u003C/summary\u003E\r\n    YUpCm,\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Engine space already: rig read from a compiled model\u0027s \u003Cc\u003EModel.Bones\u003C/c\u003E\r\n    /// (inches, Z-up). The DMX declares a Z-up axis system so the compiler performs no\r\n    /// further axis conversion.\r\n    /// \u003C/summary\u003E\r\n    ZUpEngine,\r\n\r\n    /// \u003Csummary\u003E\r\n    /// A Z-up rig authored in centimeters: FBX targets whose GlobalSettings declare a Z\r\n    /// up-axis (UE and 3ds Max exports; Maya/Blender exports are Y-up). The DMX declares\r\n    /// Z-up (no compile-time rotation \u2014 the mesh source is in the same Z-up space) while\r\n    /// the vmdl\u0027s ScaleAndMirror 0.3937 still converts cm\u2192inches. Without this, a Z-up\r\n    /// FBX target compiles lying on its back.\r\n    /// \u003C/summary\u003E\r\n    ZUpCm,\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The conversion target shared by all requests of one \u003Csee cref=\u0022Retargeter.Convert\u0022/\u003E /\r\n/// \u003Csee cref=\u0022Retargeter.ConvertBatch\u0022/\u003E call: the rig plus the vmdl generation parameters.\r\n/// \u003C/summary\u003E\r\npublic sealed class RetargetTargetSpec\r\n{\r\n    /// \u003Csummary\u003EThe s\u0026amp;box-source \u2192 engine-units vmdl scale (cm rigs like the citizen).\u003C/summary\u003E\r\n    public const float SboxSourceScale = 0.3937f;\r\n\r\n    /// \u003Csummary\u003EThe committed asset path of the s\u0026amp;box human male model.\u003C/summary\u003E\r\n    public const string SboxHumanMalePath = \u0022models/citizen_human/citizen_human_male.vmdl\u0022;\r\n\r\n    /// \u003Csummary\u003EThe committed asset path of the classic (4-finger) s\u0026amp;box citizen model.\u003C/summary\u003E\r\n    public const string SboxCitizenPath = \u0022models/citizen/citizen.vmdl\u0022;\r\n\r\n    /// \u003Csummary\u003ETarget rig (skeleton \u002B bone classes \u002B roles).\u003C/summary\u003E\r\n    public required TargetRig Rig { get; set; }\r\n\r\n    /// \u003Csummary\u003EModelModifier_ScaleAndMirror scale written into standalone vmdls:\r\n    /// \u003Cc\u003E0.3937\u003C/c\u003E for cm-authored s\u0026amp;box-source rigs, \u003Cc\u003E1.0\u003C/c\u003E for engine-unit rigs\r\n    /// (the modifier node is omitted at 1.0).\u003C/summary\u003E\r\n    public required float VmdlScale { get; init; }\r\n\r\n    /// \u003Csummary\u003Ebase_model_name of generated standalone vmdls (the model that owns the mesh).\u003C/summary\u003E\r\n    public string BaseModelPath { get; init; } = \u0022\u0022;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Assets-relative mesh source file (e.g. an \u003Cc\u003E.fbx\u003C/c\u003E) embedded in generated\r\n    /// standalone vmdls as a \u003Cc\u003ERenderMeshList/RenderMeshFile\u003C/c\u003E node. Custom FBX targets\r\n    /// have no compiled base model to point \u003Csee cref=\u0022BaseModelPath\u0022/\u003E at \u2014 without a mesh\r\n    /// source their standalone vmdl compiles into an EMPTY model (0 bones, 0 sequences) and\r\n    /// playing it does nothing. Callers own copying the file into the project (this type\r\n    /// does no IO); settable so the editor can fill it at convert time once the output\r\n    /// folder is known. Empty (default) = no mesh node.\r\n    /// \u003C/summary\u003E\r\n    public string MeshFilePath { get; set; } = \u0022\u0022;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Import scale of \u003Csee cref=\u0022MeshFilePath\u0022/\u003E (raw mesh-file units \u2192 the target\r\n    /// skeleton\u0027s units). resourcecompiler reads mesh files\u0027 raw values ignoring their unit\r\n    /// metadata, while the importer normalizes the target skeleton to centimeters \u2014 a\r\n    /// meters-authored FBX therefore needs 100 here (the importer\u0027s recorded\r\n    /// source-unit\u2192cm factor) for the mesh to match the animation skeleton.\r\n    /// \u003C/summary\u003E\r\n    public float MeshImportScale { get; set; } = 1.0f;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Material remaps written into generated standalone vmdls as a\r\n    /// MaterialGroupList/DefaultMaterialGroup (bare mesh material reference \u2192 assets-relative\r\n    /// vmat path, e.g. \u003Cc\u003E\u0022mi_dante_head.vmat\u0022 \u2192 \u0022animations/retargeted/mi_dante_head.vmat\u0022\u003C/c\u003E).\r\n    /// FBX materials carry bare names the compiler cannot resolve as resource paths\r\n    /// (\u0022Trying to load an illegal resource name X.vmat\u0022); this remap table \u2014 the same\r\n    /// mechanism the shipped citizen vmdl uses \u2014 points them at real files. Null/empty =\r\n    /// no material group node (default).\r\n    /// \u003C/summary\u003E\r\n    public IReadOnlyDictionary\u003Cstring, string\u003E? MaterialRemaps { get; set; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Additional AnimFile entries appended to generated/augmented vmdls verbatim \u2014\r\n    /// the target FBX\u0027s OWN embedded animations (an FBX with an animation on it must keep\r\n    /// that animation when new ones are retargeted onto it; the AnimFile references the\r\n    /// FBX directly, exactly like the shipped citizen animation list references its\r\n    /// Citizen@*.fbx files, so the import is lossless). Augmentation skips entries the\r\n    /// existing vmdl already carries (idempotent re-runs). Null/empty = none (default).\r\n    /// \u003C/summary\u003E\r\n    public IReadOnlyList\u003CTarget.AnimEntry\u003E? ExtraAnimFiles { get; set; }\r\n\r\n    /// \u003Csummary\u003Edefault_root_bone_name of the generated AnimationList (also the bone vmdl\r\n    /// ExtractMotion nodes operate on).\u003C/summary\u003E\r\n    public string DefaultRootBone { get; set; } = \u0022pelvis\u0022;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Axis/unit convention of \u003Csee cref=\u0022Rig\u0022/\u003E. \u003Csee cref=\u0022TargetUpAxis.YUpCm\u0022/\u003E (default)\r\n    /// for cm Y-up source-space rigs (DMX declares Y-up, compiler converts);\r\n    /// \u003Csee cref=\u0022TargetUpAxis.ZUpEngine\u0022/\u003E for rigs read from compiled engine models\r\n    /// (DMX declares Z-up so no double conversion happens at compile, and cm-tuned cleanup\r\n    /// thresholds are rescaled to inches).\r\n    /// \u003C/summary\u003E\r\n    public TargetUpAxis UpAxis { get; init; } = TargetUpAxis.YUpCm;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Raw bytes of the committed SAME weight blob\r\n    /// (\u003Cc\u003EAssets/humanoid_retargeter/dl/same_v1.weights\u003C/c\u003E; callers do the file IO).\r\n    /// Required only when a request selects \u003Csee cref=\u0022SolverKind.DeepLearning\u0022/\u003E; the\r\n    /// solver instance is built once per batch from these bytes.\r\n    /// \u003C/summary\u003E\r\n    public byte[]? DlWeights { get; init; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// The shipped s\u0026amp;box default target: rig parsed from the committed\r\n    /// \u003Cc\u003EAssets/humanoid_retargeter/target_rig_sbox.json\u003C/c\u003E text (callers do the file IO),\r\n    /// 0.3937 vmdl scale, citizen human male base model, pelvis root. Pass the committed\r\n    /// SAME weight bytes as \u003Cparamref name=\u0022dlWeights\u0022/\u003E to enable the deep-learning solver.\r\n    /// \u003C/summary\u003E\r\n    public static RetargetTargetSpec SboxDefault(string targetRigJson, byte[]? dlWeights = null) =\u003E new()\r\n    {\r\n        Rig = TargetRig.SboxDefault(targetRigJson),\r\n        VmdlScale = SboxSourceScale,\r\n        BaseModelPath = SboxHumanMalePath,\r\n        DefaultRootBone = \u0022pelvis\u0022,\r\n        DlWeights = dlWeights,\r\n    };\r\n\r\n    /// \u003Csummary\u003E\r\n    /// The classic (4-finger) s\u0026amp;box citizen target: rig parsed from the committed\r\n    /// \u003Cc\u003EAssets/humanoid_retargeter/target_rig_sbox_citizen.json\u003C/c\u003E text (callers do the\r\n    /// file IO), 0.3937 vmdl scale, citizen base model, pelvis root, Y-up cm. The rig has no\r\n    /// pinky bones, so pinky roles stay unassigned \u2014 the engine\u0027s own constraints handle the\r\n    /// pinky at runtime for models that have one. Pass the committed SAME weight bytes as\r\n    /// \u003Cparamref name=\u0022dlWeights\u0022/\u003E to enable the deep-learning solver.\r\n    /// \u003C/summary\u003E\r\n    public static RetargetTargetSpec SboxCitizen(string targetRigJson, byte[]? dlWeights = null) =\u003E new()\r\n    {\r\n        Rig = TargetRig.Load(targetRigJson),\r\n        VmdlScale = SboxSourceScale,\r\n        BaseModelPath = SboxCitizenPath,\r\n        DefaultRootBone = \u0022pelvis\u0022,\r\n        UpAxis = TargetUpAxis.YUpCm,\r\n        DlWeights = dlWeights,\r\n    };\r\n}\r\n\r\n/// \u003Csummary\u003EOptions for \u003Csee cref=\u0022Retargeter.ConvertBatch\u0022/\u003E output assembly.\u003C/summary\u003E\r\npublic sealed class BatchOptions\r\n{\r\n    /// \u003Csummary\u003E\r\n    /// When set, the batch additionally augments this existing vmdl text (all successful\r\n    /// clips spliced into its AnimationList via \u003Csee cref=\u0022VmdlAugmenter\u0022/\u003E) and returns the\r\n    /// result in \u003Csee cref=\u0022RetargetBatchResult.AugmentedVmdl\u0022/\u003E.\r\n    /// \u003C/summary\u003E\r\n    public string? AugmentVmdlText { get; init; }\r\n\r\n    /// \u003Csummary\u003EAssets-relative folder the DMX files will be written to by the caller; used\r\n    /// to build each AnimFile\u0027s \u003Cc\u003Esource_filename\u003C/c\u003E.\u003C/summary\u003E\r\n    public string DmxFolderRelative { get; init; } = \u0022animations/retargeted\u0022;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Animation source paths (\u003Cc\u003Esource_filename\u003C/c\u003E values of the augment target\u0027s existing\r\n    /// AnimFile nodes, assets-relative) that the IO-owning caller has determined NO LONGER\r\n    /// EXIST on disk. Stale AnimFile entries referencing them are REMOVED from the augmented\r\n    /// vmdl (reported on \u003Csee cref=\u0022RetargetBatchResult.Warnings\u0022/\u003E) \u2014 one unresolvable\r\n    /// source otherwise fails the ENTIRE vmdl recompile (\u0022Node \u0027X\u0027 resolve failure\u0022), taking\r\n    /// every newly added animation down with it. Entries this batch overwrites (their DMX is\r\n    /// about to be written) are never pruned. The facade itself never touches the filesystem:\r\n    /// callers probe \u003Csee cref=\u0022Target.VmdlAugmenter.CollectAnimSourcePaths\u0022/\u003E results against\r\n    /// their content roots and pass the missing ones here. Null/empty = keep everything.\r\n    /// \u003C/summary\u003E\r\n    public IReadOnlyCollection\u003Cstring\u003E? MissingAnimSources { get; init; }\r\n\r\n    /// \u003Csummary\u003EAuto-suffix colliding clip names (\u003Cc\u003E_2\u003C/c\u003E, \u003Cc\u003E_3\u003C/c\u003E, \u2026) across the whole\r\n    /// batch (default on). When off, duplicate names are kept as-is.\u003C/summary\u003E\r\n    public bool AutoSuffixCollisions { get; init; } = true;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// After conversion, scan the batch\u0027s successful clip names for directional locomotion\r\n    /// families (default OFF): \u003Cc\u003E_N\u003C/c\u003E/\u003Cc\u003E_NE\u003C/c\u003E/\u2026/\u003Cc\u003E_NW\u003C/c\u003E compass suffixes and\r\n    /// \u003Cc\u003E_Forward\u003C/c\u003E/\u003Cc\u003E_Backward\u003C/c\u003E(/\u003Cc\u003E_Back\u003C/c\u003E)/\u003Cc\u003E_Left\u003C/c\u003E/\u003Cc\u003E_Right\u003C/c\u003E word forms\r\n    /// sharing a stem. Each complete family (all four cardinals) is grouped under a Folder\r\n    /// node with a \u003Cc\u003E2DBlend\u003C/c\u003E wired to the citizen \u003Cc\u003Emove_x\u003C/c\u003E/\u003Cc\u003Emove_y\u003C/c\u003E pose\r\n    /// parameters, replicating the shipped citizen locomotion layout (see\r\n    /// \u003Csee cref=\u0022Target.LocomotionSetDetector\u0022/\u003E); detection results land on\r\n    /// \u003Csee cref=\u0022RetargetBatchResult.LocomotionSets\u0022/\u003E. Custom (non-citizen) base models\r\n    /// must declare \u003Cc\u003Emove_x\u003C/c\u003E/\u003Cc\u003Emove_y\u003C/c\u003E pose parameters themselves for the blends to\r\n    /// be drivable.\r\n    /// \u003C/summary\u003E\r\n    public bool DetectLocomotionSets { get; init; }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"HumanoidRetargeter/Solve/HandGeometry.cs","FileName":"HandGeometry.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidRetargeter.Mapping;\r\nusing HumanoidRetargeter.Maths;\r\n\r\nnamespace HumanoidRetargeter.Solve;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// \u003Csummary\u003E\r\n/// Hand rest-geometry helpers shared by \u003Csee cref=\u0022CanonicalFrames\u0022/\u003E (finger secondary axes)\r\n/// and \u003Csee cref=\u0022RestNormalizer\u0022/\u003E (palm-down roll correction). Everything derives from joint\r\n/// positions only \u2014 bone local axes carry no anatomical meaning on the s\u0026amp;box rig.\r\n/// \u003C/summary\u003E\r\ninternal static class HandGeometry\r\n{\r\n    private static readonly BoneRole[] LeftProximals =\r\n    {\r\n        BoneRole.ThumbProxL, BoneRole.IndexProxL, BoneRole.MiddleProxL, BoneRole.RingProxL, BoneRole.PinkyProxL,\r\n    };\r\n\r\n    private static readonly BoneRole[] RightProximals =\r\n    {\r\n        BoneRole.ThumbProxR, BoneRole.IndexProxR, BoneRole.MiddleProxR, BoneRole.RingProxR, BoneRole.PinkyProxR,\r\n    };\r\n\r\n    // Index \u2192 pinky order; the knuckle line is taken from the first and last mapped of these.\r\n    private static readonly BoneRole[] LeftNonThumbProximals =\r\n    {\r\n        BoneRole.IndexProxL, BoneRole.MiddleProxL, BoneRole.RingProxL, BoneRole.PinkyProxL,\r\n    };\r\n\r\n    private static readonly BoneRole[] RightNonThumbProximals =\r\n    {\r\n        BoneRole.IndexProxR, BoneRole.MiddleProxR, BoneRole.RingProxR, BoneRole.PinkyProxR,\r\n    };\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Midpoint of all mapped finger proximal heads of one hand (the hand\u0027s anatomical\r\n    /// \u0022chain child\u0022 point), or null when no finger proximal is mapped.\r\n    /// \u003C/summary\u003E\r\n    public static Vector3? FingerProximalMidpoint(MappingResult map, IReadOnlyList\u003CXForm\u003E worldRest, bool left)\r\n    {\r\n        var sum = Vector3.Zero;\r\n        var count = 0;\r\n        foreach (var role in left ? LeftProximals : RightProximals)\r\n        {\r\n            if (map.RoleToBone.TryGetValue(role, out var index))\r\n            {\r\n                sum \u002B= worldRest[index].Pos;\r\n                count\u002B\u002B;\r\n            }\r\n        }\r\n        return count \u003E 0 ? sum / count : null;\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Dorsal palm normal of one hand: the unit vector pointing out of the \u003Cb\u003Eback\u003C/b\u003E of the\r\n    /// hand (away from the palm), or null when the hand/finger geometry is unmapped or\r\n    /// degenerate.\r\n    /// \u003C/summary\u003E\r\n    /// \u003Cremarks\u003E\r\n    /// Formula (mirror-consistent by construction, verified on the ActorCore fixture by the\r\n    /// finger-curl test): \u003Cc\u003Edorsal = sideSign \u00B7 cross(knuckle, fingerDir)\u003C/c\u003E with\r\n    /// \u003Cc\u003EsideSign = \u002B1\u003C/c\u003E left / \u003Cc\u003E\u22121\u003C/c\u003E right, \u003Cc\u003Eknuckle = IndexProx.head \u2212\r\n    /// PinkyProx.head\u003C/c\u003E (first/last mapped non-thumb proximal), and \u003Cc\u003EfingerDir =\r\n    /// FingerProximalMidpoint \u2212 Hand.head\u003C/c\u003E. On every fixture rig the thumb proximal lies on\r\n    /// the \u2212dorsal (palmar) side of the hand plane, grounding the sign anatomically. A positive\r\n    /// rotation about a finger frame\u0027s hinge axis (frame Y = cross(dorsal, fingerChainDir))\r\n    /// curls the fingertip toward the palm on \u003Cb\u003Eboth\u003C/b\u003E hands.\r\n    /// \u003C/remarks\u003E\r\n    public static Vector3? Dorsal(MappingResult map, IReadOnlyList\u003CXForm\u003E worldRest, bool left)\r\n    {\r\n        if (!map.RoleToBone.TryGetValue(left ? BoneRole.HandL : BoneRole.HandR, out var handIndex))\r\n            return null;\r\n        var hand = worldRest[handIndex].Pos;\r\n\r\n        var nonThumb = left ? LeftNonThumbProximals : RightNonThumbProximals;\r\n        Vector3? first = null, last = null;\r\n        foreach (var role in nonThumb)\r\n        {\r\n            if (!map.RoleToBone.TryGetValue(role, out var index))\r\n                continue;\r\n            first ??= worldRest[index].Pos;\r\n            last = worldRest[index].Pos;\r\n        }\r\n        if (first is null || last is null || (first.Value - last.Value).LengthSquared() \u003C 1e-8f)\r\n            return null;\r\n\r\n        var midpoint = FingerProximalMidpoint(map, worldRest, left);\r\n        if (midpoint is null)\r\n            return null;\r\n\r\n        var knuckle = first.Value - last.Value;\r\n        var fingerDir = midpoint.Value - hand;\r\n        var raw = Vector3.Cross(knuckle, fingerDir) * (left ? 1f : -1f);\r\n        return raw.LengthSquared() \u003C 1e-8f ? null : Vector3.Normalize(raw);\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"Code/HumanoidRetargeter/AssemblyInfo.cs","FileName":"AssemblyInfo.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\n// Engine-agnostic retargeting core. No Sandbox/Editor references allowed in this tree:\r\n// these sources also compile in the plain net8.0 dev harness (dev/HumanoidRetargeter.Dev.csproj).\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"Code/HumanoidRetargeter/Formats/Gltf/GltfDocument.cs","FileName":"GltfDocument.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing System.Text;\r\nusing System.Text.Json;\r\n\r\nnamespace HumanoidRetargeter.Formats.Gltf;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// \u003Csummary\u003EOne glTF node, reduced to what skeleton import needs (TRS rest \u002B hierarchy).\u003C/summary\u003E\r\ninternal sealed class GltfNode\r\n{\r\n    public string? Name;\r\n    public int[] Children = Array.Empty\u003Cint\u003E();\r\n    public int Parent = -1;\r\n    public bool HasMesh;\r\n\r\n    // Rest local transform: TRS properties, or the decomposed \u0022matrix\u0022 property (the spec\r\n    // makes them exclusive; animated nodes must use TRS). Shear is not representable.\r\n    public Vector3 Translation;                       // meters\r\n    public Quaternion Rotation = Quaternion.Identity; // xyzw\r\n    public Vector3 Scale = Vector3.One;\r\n}\r\n\r\n/// \u003Csummary\u003EOne decoded animation channel: keyframe times \u002B values for one node property.\u003C/summary\u003E\r\ninternal sealed class GltfChannel\r\n{\r\n    public required int NodeIndex;\r\n    public required bool IsRotation;     // true = rotation (VEC4 quat), false = translation (VEC3)\r\n    public required float[] Times;       // seconds, ascending\r\n    public required float[] Values;      // flattened; 4 (or 3) floats per element\r\n    public required string Interpolation; // LINEAR / STEP / CUBICSPLINE\r\n\r\n    /// \u003Csummary\u003EFloats per element (3 translation / 4 rotation).\u003C/summary\u003E\r\n    public int Comps =\u003E IsRotation ? 4 : 3;\r\n\r\n    /// \u003Csummary\u003EElements stored per key: CUBICSPLINE keys carry in-tangent/value/out-tangent.\u003C/summary\u003E\r\n    public int ElementsPerKey =\u003E Interpolation == \u0022CUBICSPLINE\u0022 ? 3 : 1;\r\n\r\n    /// \u003Csummary\u003ENumber of keys.\u003C/summary\u003E\r\n    public int KeyCount =\u003E Times.Length;\r\n}\r\n\r\n/// \u003Csummary\u003EOne glTF animation with its decoded rotation/translation channels.\u003C/summary\u003E\r\ninternal sealed class GltfAnimation\r\n{\r\n    public string? Name;\r\n    public List\u003CGltfChannel\u003E Channels { get; } = new();\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Container \u002B JSON layer of the glTF importer: parses a GLB binary container or a plain\r\n/// .gltf JSON document, resolves buffers (GLB BIN chunk and base64 \u003Cc\u003Edata:\u003C/c\u003E URIs \u2014 file\r\n/// IO is banned in Code/, so external file URIs throw), and decodes nodes, skin joints and\r\n/// animation samplers into plain arrays. Throws \u003Csee cref=\u0022FormatException\u0022/\u003E on anything\r\n/// malformed or unsupported.\r\n/// \u003C/summary\u003E\r\ninternal sealed class GltfDocument\r\n{\r\n    private const uint GlbMagic = 0x46546C67;     // \u0027glTF\u0027 little-endian\r\n    private const uint ChunkJson = 0x4E4F534A;    // \u0027JSON\u0027\r\n    private const uint ChunkBin = 0x004E4942;     // \u0027BIN\\0\u0027\r\n\r\n    /// \u003Csummary\u003EAll nodes, indexed as in the file, with parents resolved from children lists.\u003C/summary\u003E\r\n    public List\u003CGltfNode\u003E Nodes { get; } = new();\r\n\r\n    /// \u003Csummary\u003EUnion of all skins\u0027 joint node indices.\u003C/summary\u003E\r\n    public HashSet\u003Cint\u003E SkinJoints { get; } = new();\r\n\r\n    /// \u003Csummary\u003EAll animations with decoded rotation/translation channels (scale/weights ignored).\u003C/summary\u003E\r\n    public List\u003CGltfAnimation\u003E Animations { get; } = new();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// The VRM humanoid bone map authored in the file, when present: VRM bone name\r\n    /// (\u003Cc\u003Ehips\u003C/c\u003E, \u003Cc\u003EleftUpperArm\u003C/c\u003E, \u2026) \u2192 node index. Read from BOTH extension layouts:\r\n    /// VRM 0.x \u003Cc\u003Eextensions.VRM.humanoid.humanBones\u003C/c\u003E (an ARRAY of\r\n    /// \u003Cc\u003E{ \u0022bone\u0022: \u0022hips\u0022, \u0022node\u0022: 14 }\u003C/c\u003E entries) and VRM 1.0\r\n    /// \u003Cc\u003Eextensions.VRMC_vrm.humanoid.humanBones\u003C/c\u003E (an OBJECT\r\n    /// \u003Cc\u003E{ \u0022hips\u0022: { \u0022node\u0022: 14 }, \u2026 }\u003C/c\u003E). Null when the file carries neither.\r\n    /// \u003C/summary\u003E\r\n    public Dictionary\u003Cstring, int\u003E? VrmHumanBones { get; private set; }\r\n\r\n    /// \u003Csummary\u003EWhich VRM extension supplied \u003Csee cref=\u0022VrmHumanBones\u0022/\u003E: \u003Cc\u003E0\u003C/c\u003E for the\r\n    /// 0.x \u003Cc\u003EVRM\u003C/c\u003E extension, \u003Cc\u003E1\u003C/c\u003E for the 1.0 \u003Cc\u003EVRMC_vrm\u003C/c\u003E extension, \u003Cc\u003E-1\u003C/c\u003E\r\n    /// when none.\u003C/summary\u003E\r\n    public int VrmVersion { get; private set; } = -1;\r\n\r\n    private GltfDocument()\r\n    {\r\n    }\r\n\r\n    /// \u003Csummary\u003EParses GLB or plain-JSON glTF bytes.\u003C/summary\u003E\r\n    /// \u003Cexception cref=\u0022FormatException\u0022\u003ETruncated/malformed container, invalid JSON,\r\n    /// unresolvable buffers, or unsupported accessor layouts.\u003C/exception\u003E\r\n    public static GltfDocument Parse(byte[] data)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(data);\r\n\r\n        byte[] json;\r\n        byte[]? bin = null;\r\n        if (data.Length \u003E= 4 \u0026\u0026 ReadU32(data, 0) == GlbMagic)\r\n            (json, bin) = ParseGlbContainer(data);\r\n        else\r\n            json = data;\r\n\r\n        JsonElement root;\r\n        try\r\n        {\r\n            // Parse via string: Memory\u003CT\u003E/ReadOnlyMemory\u003CT\u003E are not on the s\u0026box runtime\r\n            // whitelist (SB1000), and the string path also lets us strip a UTF-8 BOM\r\n            // (Utf8JsonReader rejects raw BOM bytes). Clone detaches from the disposed\r\n            // JsonDocument.\r\n            var text = System.Text.Encoding.UTF8.GetString(json).TrimStart(\u0027\\uFEFF\u0027);\r\n            using var doc = JsonDocument.Parse(text);\r\n            root = doc.RootElement.Clone();\r\n        }\r\n        catch (JsonException e)\r\n        {\r\n            throw new FormatException($\u0022glTF: invalid JSON ({e.Message})\u0022);\r\n        }\r\n\r\n        if (root.ValueKind != JsonValueKind.Object || !root.TryGetProperty(\u0022asset\u0022, out _))\r\n            throw new FormatException(\u0022glTF: missing required \u0027asset\u0027 object (not a glTF file?).\u0022);\r\n\r\n        var document = new GltfDocument();\r\n        var buffers = ResolveBuffers(root, bin);\r\n        document.ReadNodes(root);\r\n        document.ReadSkins(root);\r\n        document.ReadAnimations(root, buffers);\r\n        document.ReadVrmHumanoid(root);\r\n        return document;\r\n    }\r\n\r\n    // ================================================================== VRM humanoid\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Reads the authored humanoid bone map of a VRM file (a .vrm is a regular glTF 2.0/GLB\r\n    /// container plus a VRM extension). VRM 1.0\u0027s \u003Cc\u003EVRMC_vrm\u003C/c\u003E wins when both extensions\r\n    /// are present. Defensive throughout: malformed entries and out-of-range node indices\r\n    /// are skipped (a broken bone map degrades to the regular detection cascade rather than\r\n    /// failing the import).\r\n    /// \u003C/summary\u003E\r\n    private void ReadVrmHumanoid(JsonElement root)\r\n    {\r\n        if (!root.TryGetProperty(\u0022extensions\u0022, out var extensions)\r\n            || extensions.ValueKind != JsonValueKind.Object)\r\n            return;\r\n\r\n        // ---- VRM 1.0: extensions.VRMC_vrm.humanoid.humanBones = { \u0022\u003Cbone\u003E\u0022: { \u0022node\u0022: n } } ----\r\n        if (TryGetHumanBones(extensions, \u0022VRMC_vrm\u0022, out var humanBones1)\r\n            \u0026\u0026 humanBones1.ValueKind == JsonValueKind.Object)\r\n        {\r\n            var map = new Dictionary\u003Cstring, int\u003E(StringComparer.Ordinal);\r\n            foreach (var property in humanBones1.EnumerateObject())\r\n            {\r\n                if (property.Value.ValueKind == JsonValueKind.Object\r\n                    \u0026\u0026 property.Value.TryGetProperty(\u0022node\u0022, out var node)\r\n                    \u0026\u0026 node.ValueKind == JsonValueKind.Number\r\n                    \u0026\u0026 node.TryGetInt32(out var index)\r\n                    \u0026\u0026 index \u003E= 0 \u0026\u0026 index \u003C Nodes.Count)\r\n                {\r\n                    map[property.Name] = index;\r\n                }\r\n            }\r\n            if (map.Count \u003E 0)\r\n            {\r\n                VrmHumanBones = map;\r\n                VrmVersion = 1;\r\n                return;\r\n            }\r\n        }\r\n\r\n        // ---- VRM 0.x: extensions.VRM.humanoid.humanBones = [ { \u0022bone\u0022: \u0022...\u0022, \u0022node\u0022: n } ] ----\r\n        if (TryGetHumanBones(extensions, \u0022VRM\u0022, out var humanBones0)\r\n            \u0026\u0026 humanBones0.ValueKind == JsonValueKind.Array)\r\n        {\r\n            var map = new Dictionary\u003Cstring, int\u003E(StringComparer.Ordinal);\r\n            foreach (var entry in humanBones0.EnumerateArray())\r\n            {\r\n                if (entry.ValueKind == JsonValueKind.Object\r\n                    \u0026\u0026 entry.TryGetProperty(\u0022bone\u0022, out var bone)\r\n                    \u0026\u0026 bone.ValueKind == JsonValueKind.String\r\n                    \u0026\u0026 entry.TryGetProperty(\u0022node\u0022, out var node)\r\n                    \u0026\u0026 node.ValueKind == JsonValueKind.Number\r\n                    \u0026\u0026 node.TryGetInt32(out var index)\r\n                    \u0026\u0026 index \u003E= 0 \u0026\u0026 index \u003C Nodes.Count)\r\n                {\r\n                    map[bone.GetString()!] = index;\r\n                }\r\n            }\r\n            if (map.Count \u003E 0)\r\n            {\r\n                VrmHumanBones = map;\r\n                VrmVersion = 0;\r\n            }\r\n        }\r\n    }\r\n\r\n    private static bool TryGetHumanBones(JsonElement extensions, string extensionName, out JsonElement humanBones)\r\n    {\r\n        humanBones = default;\r\n        return extensions.TryGetProperty(extensionName, out var vrm)\r\n            \u0026\u0026 vrm.ValueKind == JsonValueKind.Object\r\n            \u0026\u0026 vrm.TryGetProperty(\u0022humanoid\u0022, out var humanoid)\r\n            \u0026\u0026 humanoid.ValueKind == JsonValueKind.Object\r\n            \u0026\u0026 humanoid.TryGetProperty(\u0022humanBones\u0022, out humanBones);\r\n    }\r\n\r\n    // ================================================================== GLB container\r\n\r\n    /// \u003Csummary\u003EGLB layout: 12-byte header (magic \u0027glTF\u0027, u32 version = 2, u32 length),\r\n    /// then chunks of (u32 length, u32 type, bytes): one JSON chunk, optionally one BIN.\u003C/summary\u003E\r\n    private static (byte[] Json, byte[]? Bin) ParseGlbContainer(byte[] data)\r\n    {\r\n        if (data.Length \u003C 12)\r\n            throw new FormatException(\u0022GLB: truncated header (need 12 bytes).\u0022);\r\n\r\n        uint version = ReadU32(data, 4);\r\n        if (version != 2)\r\n            throw new FormatException($\u0022GLB: unsupported container version {version} (expected 2).\u0022);\r\n\r\n        long declared = ReadU32(data, 8);\r\n        if (declared \u003E data.Length)\r\n            throw new FormatException(\r\n                $\u0022GLB: truncated file (header declares {declared} bytes, got {data.Length}).\u0022);\r\n\r\n        byte[]? json = null, bin = null;\r\n        long offset = 12;\r\n        while (offset \u002B 8 \u003C= declared)\r\n        {\r\n            long length = ReadU32(data, (int)offset);\r\n            uint type = ReadU32(data, (int)offset \u002B 4);\r\n            offset \u002B= 8;\r\n            if (offset \u002B length \u003E data.Length)\r\n                throw new FormatException(\u0022GLB: truncated chunk (declared length exceeds the file).\u0022);\r\n\r\n            if (type == ChunkJson \u0026\u0026 json is null)\r\n                json = data.AsSpan((int)offset, (int)length).ToArray();\r\n            else if (type == ChunkBin \u0026\u0026 bin is null)\r\n                bin = data.AsSpan((int)offset, (int)length).ToArray();\r\n            // Unknown chunk types are skipped per spec.\r\n\r\n            offset \u002B= length \u002B (length % 4 == 0 ? 0 : 4 - length % 4); // chunks are 4-aligned\r\n        }\r\n\r\n        if (json is null)\r\n            throw new FormatException(\u0022GLB: no JSON chunk found.\u0022);\r\n        return (json, bin);\r\n    }\r\n\r\n    private static uint ReadU32(byte[] data, int offset)\r\n        =\u003E (uint)(data[offset] | data[offset \u002B 1] \u003C\u003C 8 | data[offset \u002B 2] \u003C\u003C 16 | data[offset \u002B 3] \u003C\u003C 24);\r\n\r\n    // ================================================================== buffers\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Resolves every entry of \u003Cc\u003Ebuffers\u003C/c\u003E: no \u003Cc\u003Euri\u003C/c\u003E = the GLB BIN chunk (spec: only\r\n    /// buffer 0 may do this), \u003Cc\u003Edata:\u003C/c\u003E URIs are base64-decoded inline. External file\r\n    /// URIs are NOT supported \u2014 this library does no file IO; users should export .glb.\r\n    /// \u003C/summary\u003E\r\n    private static List\u003Cbyte[]\u003E ResolveBuffers(JsonElement root, byte[]? bin)\r\n    {\r\n        var buffers = new List\u003Cbyte[]\u003E();\r\n        if (!root.TryGetProperty(\u0022buffers\u0022, out var array) || array.ValueKind != JsonValueKind.Array)\r\n            return buffers;\r\n\r\n        foreach (var buffer in array.EnumerateArray())\r\n        {\r\n            if (!buffer.TryGetProperty(\u0022uri\u0022, out var uriProp))\r\n            {\r\n                buffers.Add(bin ?? throw new FormatException(\r\n                    \u0022glTF: buffer has no uri but the file has no GLB BIN chunk.\u0022));\r\n                continue;\r\n            }\r\n\r\n            var uri = uriProp.GetString() ?? \u0022\u0022;\r\n            if (uri.StartsWith(\u0022data:\u0022, StringComparison.OrdinalIgnoreCase))\r\n            {\r\n                int comma = uri.IndexOf(\u0027,\u0027);\r\n                if (comma \u003C 0 || !uri[..comma].EndsWith(\u0022;base64\u0022, StringComparison.OrdinalIgnoreCase))\r\n                    throw new FormatException(\u0022glTF: only base64 data: URIs are supported for buffers.\u0022);\r\n                try\r\n                {\r\n                    buffers.Add(Convert.FromBase64String(uri[(comma \u002B 1)..]));\r\n                }\r\n                catch (Exception e) when (e is FormatException or ArgumentException)\r\n                {\r\n                    throw new FormatException(\u0022glTF: invalid base64 in buffer data: URI.\u0022);\r\n                }\r\n            }\r\n            else\r\n            {\r\n                throw new FormatException(\r\n                    $\u0022glTF: buffer references an external file (\u0027{uri}\u0027) which this importer cannot \u0022\r\n                    \u002B \u0022read (no file IO). Export as .glb (binary, self-contained) instead.\u0022);\r\n            }\r\n        }\r\n        return buffers;\r\n    }\r\n\r\n    // ================================================================== nodes \u002B skins\r\n\r\n    private void ReadNodes(JsonElement root)\r\n    {\r\n        if (!root.TryGetProperty(\u0022nodes\u0022, out var array) || array.ValueKind != JsonValueKind.Array)\r\n            return;\r\n\r\n        Span\u003Cfloat\u003E m = stackalloc float[16]; // matrix scratch (outside the loop: CA2014)\r\n        foreach (var n in array.EnumerateArray())\r\n        {\r\n            var node = new GltfNode\r\n            {\r\n                Name = n.TryGetProperty(\u0022name\u0022, out var name) ? name.GetString() : null,\r\n                HasMesh = n.TryGetProperty(\u0022mesh\u0022, out _),\r\n            };\r\n\r\n            if (n.TryGetProperty(\u0022children\u0022, out var children) \u0026\u0026 children.ValueKind == JsonValueKind.Array)\r\n            {\r\n                var list = new List\u003Cint\u003E();\r\n                foreach (var c in children.EnumerateArray())\r\n                    list.Add(c.GetInt32());\r\n                node.Children = list.ToArray();\r\n            }\r\n\r\n            if (n.TryGetProperty(\u0022matrix\u0022, out var matrix) \u0026\u0026 matrix.ValueKind == JsonValueKind.Array)\r\n            {\r\n                // Column-major 16 floats; the element order maps 1:1 onto System.Numerics\u0027\r\n                // row-vector matrices (translation in elements 12..14 either way).\r\n                int i = 0;\r\n                foreach (var v in matrix.EnumerateArray())\r\n                {\r\n                    if (i \u003E= 16)\r\n                        break;\r\n                    m[i\u002B\u002B] = v.GetSingle();\r\n                }\r\n                if (i \u003C 16)\r\n                    throw new FormatException(\u0022glTF: node matrix has fewer than 16 elements.\u0022);\r\n                var local = new Matrix4x4(\r\n                    m[0], m[1], m[2], m[3],\r\n                    m[4], m[5], m[6], m[7],\r\n                    m[8], m[9], m[10], m[11],\r\n                    m[12], m[13], m[14], m[15]);\r\n                if (Matrix4x4.Decompose(local, out var scale, out var rot, out var pos))\r\n                {\r\n                    node.Translation = pos;\r\n                    node.Rotation = rot;\r\n                    node.Scale = scale;\r\n                }\r\n                else\r\n                {\r\n                    node.Translation = local.Translation; // degenerate: keep position at least\r\n                }\r\n            }\r\n            else\r\n            {\r\n                node.Translation = ReadVec3(n, \u0022translation\u0022, Vector3.Zero);\r\n                node.Scale = ReadVec3(n, \u0022scale\u0022, Vector3.One);\r\n                if (n.TryGetProperty(\u0022rotation\u0022, out var r) \u0026\u0026 r.ValueKind == JsonValueKind.Array\r\n                    \u0026\u0026 r.GetArrayLength() \u003E= 4)\r\n                {\r\n                    node.Rotation = new Quaternion(\r\n                        r[0].GetSingle(), r[1].GetSingle(), r[2].GetSingle(), r[3].GetSingle());\r\n                }\r\n            }\r\n\r\n            Nodes.Add(node);\r\n        }\r\n\r\n        // Resolve parents (per spec a node is referenced by at most one other node\u0027s children).\r\n        for (int i = 0; i \u003C Nodes.Count; i\u002B\u002B)\r\n        {\r\n            foreach (var child in Nodes[i].Children)\r\n            {\r\n                if (child \u003C 0 || child \u003E= Nodes.Count)\r\n                    throw new FormatException($\u0022glTF: node {i} references nonexistent child {child}.\u0022);\r\n                if (Nodes[child].Parent \u003C 0)\r\n                    Nodes[child].Parent = i;\r\n            }\r\n        }\r\n    }\r\n\r\n    private static Vector3 ReadVec3(JsonElement element, string property, Vector3 fallback)\r\n    {\r\n        if (!element.TryGetProperty(property, out var v) || v.ValueKind != JsonValueKind.Array\r\n            || v.GetArrayLength() \u003C 3)\r\n            return fallback;\r\n        return new Vector3(v[0].GetSingle(), v[1].GetSingle(), v[2].GetSingle());\r\n    }\r\n\r\n    private void ReadSkins(JsonElement root)\r\n    {\r\n        if (!root.TryGetProperty(\u0022skins\u0022, out var array) || array.ValueKind != JsonValueKind.Array)\r\n            return;\r\n\r\n        foreach (var skin in array.EnumerateArray())\r\n        {\r\n            if (!skin.TryGetProperty(\u0022joints\u0022, out var joints) || joints.ValueKind != JsonValueKind.Array)\r\n                continue;\r\n            foreach (var j in joints.EnumerateArray())\r\n            {\r\n                int index = j.GetInt32();\r\n                if (index \u003E= 0 \u0026\u0026 index \u003C Nodes.Count)\r\n                    SkinJoints.Add(index);\r\n            }\r\n        }\r\n    }\r\n\r\n    // ================================================================== animations\r\n\r\n    private void ReadAnimations(JsonElement root, List\u003Cbyte[]\u003E buffers)\r\n    {\r\n        if (!root.TryGetProperty(\u0022animations\u0022, out var array) || array.ValueKind != JsonValueKind.Array)\r\n            return;\r\n\r\n        root.TryGetProperty(\u0022accessors\u0022, out var accessors);\r\n        root.TryGetProperty(\u0022bufferViews\u0022, out var views);\r\n\r\n        foreach (var a in array.EnumerateArray())\r\n        {\r\n            var animation = new GltfAnimation\r\n            {\r\n                Name = a.TryGetProperty(\u0022name\u0022, out var name) ? name.GetString() : null,\r\n            };\r\n\r\n            if (!a.TryGetProperty(\u0022channels\u0022, out var channels) || !a.TryGetProperty(\u0022samplers\u0022, out var samplers))\r\n            {\r\n                Animations.Add(animation);\r\n                continue;\r\n            }\r\n\r\n            foreach (var channel in channels.EnumerateArray())\r\n            {\r\n                if (!channel.TryGetProperty(\u0022target\u0022, out var target)\r\n                    || !target.TryGetProperty(\u0022node\u0022, out var nodeProp)\r\n                    || !target.TryGetProperty(\u0022path\u0022, out var pathProp))\r\n                    continue; // extension targets (e.g. KHR_animation_pointer) are ignored\r\n\r\n                var path = pathProp.GetString();\r\n                if (path is not (\u0022rotation\u0022 or \u0022translation\u0022))\r\n                    continue; // scale / weights channels are ignored by design\r\n\r\n                int node = nodeProp.GetInt32();\r\n                if (node \u003C 0 || node \u003E= Nodes.Count)\r\n                    continue;\r\n\r\n                int samplerIndex = channel.TryGetProperty(\u0022sampler\u0022, out var s) ? s.GetInt32() : -1;\r\n                if (samplerIndex \u003C 0 || samplerIndex \u003E= samplers.GetArrayLength())\r\n                    throw new FormatException(\u0022glTF: animation channel references a nonexistent sampler.\u0022);\r\n                var sampler = samplers[samplerIndex];\r\n\r\n                var interpolation = sampler.TryGetProperty(\u0022interpolation\u0022, out var interp)\r\n                    ? interp.GetString() ?? \u0022LINEAR\u0022\r\n                    : \u0022LINEAR\u0022;\r\n\r\n                bool isRotation = path == \u0022rotation\u0022;\r\n                int comps = isRotation ? 4 : 3;\r\n\r\n                var times = ReadAccessor(accessors, views, buffers,\r\n                    RequiredInt(sampler, \u0022input\u0022, \u0022animation sampler\u0022), 1, normalizedAllowed: false);\r\n                var values = ReadAccessor(accessors, views, buffers,\r\n                    RequiredInt(sampler, \u0022output\u0022, \u0022animation sampler\u0022), comps, normalizedAllowed: isRotation);\r\n\r\n                int elementsPerKey = interpolation == \u0022CUBICSPLINE\u0022 ? 3 : 1;\r\n                if (times.Length == 0 || values.Length \u003C times.Length * elementsPerKey * comps)\r\n                    continue; // empty or under-filled sampler: nothing usable\r\n\r\n                animation.Channels.Add(new GltfChannel\r\n                {\r\n                    NodeIndex = node,\r\n                    IsRotation = isRotation,\r\n                    Times = times,\r\n                    Values = values,\r\n                    Interpolation = interpolation,\r\n                });\r\n            }\r\n\r\n            Animations.Add(animation);\r\n        }\r\n    }\r\n\r\n    private static int RequiredInt(JsonElement element, string property, string context)\r\n    {\r\n        if (!element.TryGetProperty(property, out var v))\r\n            throw new FormatException($\u0022glTF: {context} is missing \u0027{property}\u0027.\u0022);\r\n        return v.GetInt32();\r\n    }\r\n\r\n    // ================================================================== accessors\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Decodes an accessor to floats. Component types: f32 directly; normalized i8/u8/i16/u16\r\n    /// per the spec\u0027s normalization rules when \u003Cparamref name=\u0022normalizedAllowed\u0022/\u003E (rotation\r\n    /// outputs); anything else throws. Honors accessor/bufferView byte offsets and an\r\n    /// explicit byteStride. Sparse accessors are not supported.\r\n    /// \u003C/summary\u003E\r\n    private static float[] ReadAccessor(\r\n        JsonElement accessors, JsonElement views, List\u003Cbyte[]\u003E buffers,\r\n        int accessorIndex, int expectedComps, bool normalizedAllowed)\r\n    {\r\n        if (accessors.ValueKind != JsonValueKind.Array || accessorIndex \u003C 0\r\n            || accessorIndex \u003E= accessors.GetArrayLength())\r\n            throw new FormatException($\u0022glTF: accessor {accessorIndex} does not exist.\u0022);\r\n        var accessor = accessors[accessorIndex];\r\n\r\n        if (accessor.TryGetProperty(\u0022sparse\u0022, out _))\r\n            throw new FormatException(\u0022glTF: sparse accessors are not supported.\u0022);\r\n\r\n        var type = accessor.TryGetProperty(\u0022type\u0022, out var t) ? t.GetString() : null;\r\n        int comps = type switch\r\n        {\r\n            \u0022SCALAR\u0022 =\u003E 1,\r\n            \u0022VEC3\u0022 =\u003E 3,\r\n            \u0022VEC4\u0022 =\u003E 4,\r\n            _ =\u003E throw new FormatException($\u0022glTF: unsupported accessor type \u0027{type}\u0027.\u0022),\r\n        };\r\n        if (comps != expectedComps)\r\n            throw new FormatException(\r\n                $\u0022glTF: accessor {accessorIndex} is {type}, expected {expectedComps} component(s).\u0022);\r\n\r\n        int count = RequiredInt(accessor, \u0022count\u0022, \u0022accessor\u0022);\r\n        int componentType = RequiredInt(accessor, \u0022componentType\u0022, \u0022accessor\u0022);\r\n        bool normalized = accessor.TryGetProperty(\u0022normalized\u0022, out var n) \u0026\u0026 n.GetBoolean();\r\n\r\n        // The count is attacker-controlled: validate it BEFORE any allocation sized by it.\r\n        // Negative would throw OverflowException from the array allocation (breaking the\r\n        // FormatException malformed-file contract); huge would OOM; count * comps can wrap.\r\n        if (count \u003C 0)\r\n            throw new FormatException($\u0022glTF: accessor {accessorIndex} has a negative count ({count}).\u0022);\r\n\r\n        int compSize = componentType switch\r\n        {\r\n            5126 =\u003E 4,            // FLOAT\r\n            5120 or 5121 =\u003E 1,    // BYTE / UNSIGNED_BYTE\r\n            5122 or 5123 =\u003E 2,    // SHORT / UNSIGNED_SHORT\r\n            _ =\u003E throw new FormatException(\r\n                $\u0022glTF: unsupported accessor componentType {componentType}.\u0022),\r\n        };\r\n        if (componentType != 5126 \u0026\u0026 !(normalized \u0026\u0026 normalizedAllowed))\r\n            throw new FormatException(\r\n                $\u0022glTF: accessor {accessorIndex} must be float (or a normalized integer \u0022\r\n                \u002B \u0022rotation output).\u0022);\r\n\r\n        int elementSize = comps * compSize;\r\n\r\n        if (!accessor.TryGetProperty(\u0022bufferView\u0022, out var viewIndexProp))\r\n        {\r\n            // Zero-filled when no bufferView (legal per spec) \u2014 but then nothing backs the\r\n            // count, so cap it by the file\u0027s total decoded buffer bytes (a real file\u0027s\r\n            // accessors never outgrow its payload; a small floor keeps tiny legitimate\r\n            // zero-filled accessors working in buffer-less documents).\r\n            long totalBufferBytes = 0;\r\n            foreach (var b in buffers)\r\n                totalBufferBytes \u002B= b.Length;\r\n            long capacity = Math.Min(\r\n                Math.Max(totalBufferBytes / elementSize, 65536),\r\n                int.MaxValue / comps); // keeps count * comps int-representable\r\n            if (count \u003E capacity)\r\n                throw new FormatException(\r\n                    $\u0022glTF: accessor {accessorIndex} count {count} exceeds what the file\u0027s \u0022\r\n                    \u002B \u0022buffers could back (malformed or hostile file).\u0022);\r\n            return new float[checked(count * comps)];\r\n        }\r\n\r\n        int viewIndex = viewIndexProp.GetInt32();\r\n        if (views.ValueKind != JsonValueKind.Array || viewIndex \u003C 0 || viewIndex \u003E= views.GetArrayLength())\r\n            throw new FormatException($\u0022glTF: bufferView {viewIndex} does not exist.\u0022);\r\n        var view = views[viewIndex];\r\n\r\n        int bufferIndex = RequiredInt(view, \u0022buffer\u0022, \u0022bufferView\u0022);\r\n        if (bufferIndex \u003C 0 || bufferIndex \u003E= buffers.Count)\r\n            throw new FormatException($\u0022glTF: buffer {bufferIndex} does not exist.\u0022);\r\n        var buffer = buffers[bufferIndex];\r\n\r\n        int viewOffset = view.TryGetProperty(\u0022byteOffset\u0022, out var vo) ? vo.GetInt32() : 0;\r\n        int accessorOffset = accessor.TryGetProperty(\u0022byteOffset\u0022, out var ao) ? ao.GetInt32() : 0;\r\n        int stride = view.TryGetProperty(\u0022byteStride\u0022, out var st) ? st.GetInt32() : elementSize;\r\n        if (stride \u003C elementSize)\r\n            throw new FormatException(\u0022glTF: bufferView byteStride is smaller than the element size.\u0022);\r\n\r\n        // Bounds check in long arithmetic BEFORE allocating: the backing range must fit the\r\n        // buffer, which also caps count at buffer.Length / stride (\u002B1) \u2014 so the allocation\r\n        // below is bounded by the actual file size and checked() can no longer overflow.\r\n        long start = (long)viewOffset \u002B accessorOffset;\r\n        long end = start \u002B (long)(count - 1) * stride \u002B elementSize;\r\n        if (count \u003E 0 \u0026\u0026 (start \u003C 0 || end \u003E buffer.Length))\r\n            throw new FormatException(\r\n                $\u0022glTF: accessor {accessorIndex} reads past the end of its buffer (truncated file?).\u0022);\r\n\r\n        var result = new float[checked(count * comps)];\r\n        for (int element = 0; element \u003C count; element\u002B\u002B)\r\n        {\r\n            int offset = (int)(start \u002B (long)element * stride);\r\n            for (int c = 0; c \u003C comps; c\u002B\u002B)\r\n            {\r\n                int at = offset \u002B c * compSize;\r\n                result[element * comps \u002B c] = componentType switch\r\n                {\r\n                    5126 =\u003E BitConverter.ToSingle(buffer, at),\r\n                    5120 =\u003E MathF.Max((sbyte)buffer[at] / 127f, -1f),\r\n                    5121 =\u003E buffer[at] / 255f,\r\n                    5122 =\u003E MathF.Max(BitConverter.ToInt16(buffer, at) / 32767f, -1f),\r\n                    _ =\u003E BitConverter.ToUInt16(buffer, at) / 65535f,\r\n                };\r\n            }\r\n        }\r\n        return result;\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"Code/HumanoidRetargeter/Maths/XForm.cs","FileName":"XForm.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Numerics;\r\n\r\nnamespace HumanoidRetargeter.Maths;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// \u003Csummary\u003E\r\n/// A rigid transform: rotation followed by translation (no scale or shear).\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// Project conventions (fixed for the whole library):\r\n/// \u003Clist type=\u0022bullet\u0022\u003E\r\n/// \u003Citem\u003EPositions are centimeters.\u003C/item\u003E\r\n/// \u003Citem\u003EQuaternions are XYZW unit quaternions (\u003Csee cref=\u0022System.Numerics.Quaternion\u0022/\u003E native layout).\u003C/item\u003E\r\n/// \u003Citem\u003EColumn-vector convention: a local-space point maps to outer space as\r\n/// \u003Cc\u003Ep\u0027 = rotate(Rot, p) \u002B Pos\u003C/c\u003E, and \u003Cc\u003Ea * b\u003C/c\u003E on quaternions applies \u003Cc\u003Eb\u003C/c\u003E first.\u003C/item\u003E\r\n/// \u003C/list\u003E\r\n/// \u003C/remarks\u003E\r\npublic struct XForm : IEquatable\u003CXForm\u003E\r\n{\r\n    /// \u003Csummary\u003ETranslation component, in centimeters.\u003C/summary\u003E\r\n    public Vector3 Pos;\r\n\r\n    /// \u003Csummary\u003ERotation component, XYZW unit quaternion.\u003C/summary\u003E\r\n    public Quaternion Rot;\r\n\r\n    /// \u003Csummary\u003ECreates a transform from a translation and a rotation.\u003C/summary\u003E\r\n    public XForm(Vector3 pos, Quaternion rot)\r\n    {\r\n        Pos = pos;\r\n        Rot = rot;\r\n    }\r\n\r\n    /// \u003Csummary\u003EThe identity transform (zero translation, identity rotation).\u003C/summary\u003E\r\n    public static XForm Identity =\u003E new(Vector3.Zero, Quaternion.Identity);\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Composes a parent transform with a child-local transform, producing the child\u0027s\r\n    /// transform in the parent\u0027s outer space (world = parent \u2218 local):\r\n    /// \u003Cc\u003Epos = parent.Pos \u002B rotate(parent.Rot, local.Pos)\u003C/c\u003E, \u003Cc\u003Erot = parent.Rot * local.Rot\u003C/c\u003E.\r\n    /// The resulting rotation is re-normalized to suppress floating-point drift.\r\n    /// \u003C/summary\u003E\r\n    public static XForm Compose(in XForm parent, in XForm local)\r\n        =\u003E new(\r\n            parent.Pos \u002B Vector3.Transform(local.Pos, parent.Rot),\r\n            MathQ.Normalize(parent.Rot * local.Rot));\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Returns the inverse transform, such that \u003Cc\u003ECompose(x, x.Inverse())\u003C/c\u003E and\r\n    /// \u003Cc\u003ECompose(x.Inverse(), x)\u003C/c\u003E are both identity.\r\n    /// \u003C/summary\u003E\r\n    public readonly XForm Inverse()\r\n    {\r\n        var invRot = Quaternion.Conjugate(MathQ.Normalize(Rot));\r\n        return new XForm(-Vector3.Transform(Pos, invRot), invRot);\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Re-expresses a world transform relative to a parent world transform; the inverse of\r\n    /// \u003Csee cref=\u0022Compose\u0022/\u003E: \u003Cc\u003EToLocal(p, Compose(p, l)) == l\u003C/c\u003E.\r\n    /// \u003C/summary\u003E\r\n    public static XForm ToLocal(in XForm parentWorld, in XForm world)\r\n        =\u003E Compose(parentWorld.Inverse(), world);\r\n\r\n    /// \u003Csummary\u003ETransforms a point from this transform\u0027s local space to its outer space.\u003C/summary\u003E\r\n    public readonly Vector3 TransformPoint(Vector3 point) =\u003E Pos \u002B Vector3.Transform(point, Rot);\r\n\r\n    /// \u003Csummary\u003ERotates a direction vector by this transform\u0027s rotation (translation ignored).\u003C/summary\u003E\r\n    public readonly Vector3 TransformVector(Vector3 vector) =\u003E Vector3.Transform(vector, Rot);\r\n\r\n    /// \u003Cinheritdoc /\u003E\r\n    public readonly bool Equals(XForm other) =\u003E Pos.Equals(other.Pos) \u0026\u0026 Rot.Equals(other.Rot);\r\n\r\n    /// \u003Cinheritdoc /\u003E\r\n    public override readonly bool Equals(object? obj) =\u003E obj is XForm other \u0026\u0026 Equals(other);\r\n\r\n    /// \u003Cinheritdoc /\u003E\r\n    public override readonly int GetHashCode() =\u003E HashCode.Combine(Pos, Rot);\r\n\r\n    /// \u003Csummary\u003EComponentwise equality (no tolerance).\u003C/summary\u003E\r\n    public static bool operator ==(XForm left, XForm right) =\u003E left.Equals(right);\r\n\r\n    /// \u003Csummary\u003EComponentwise inequality (no tolerance).\u003C/summary\u003E\r\n    public static bool operator !=(XForm left, XForm right) =\u003E !left.Equals(right);\r\n\r\n    /// \u003Cinheritdoc /\u003E\r\n    public override readonly string ToString() =\u003E $\u0022XForm(Pos={Pos}, Rot={Rot})\u0022;\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"Code/HumanoidRetargeter/Solve/CanonicalFrames.cs","FileName":"CanonicalFrames.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidRetargeter.Mapping;\r\nusing HumanoidRetargeter.Maths;\r\nusing SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidRetargeter.Solve;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// \u003Csummary\u003E\r\n/// Canonical anatomical frames: one world-space rest basis per mapped \u003Csee cref=\u0022BoneRole\u0022/\u003E,\r\n/// derived from rest \u003Cb\u003Egeometry\u003C/b\u003E (joint head positions) of any rig plus its mapping.\r\n/// Built with the same deterministic convention on source and target, so world-rotation deltas\r\n/// conjugated through these frames transfer between rigs with different bone local axes\r\n/// (the s\u0026amp;box Citizen rig\u0027s local axes encode no anatomy \u2014 bone-Y points chest-forward).\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EFrame convention\u003C/b\u003E \u2014 for each role the frame quaternion \u003Cc\u003EF\u003C/c\u003E rotates unit\r\n/// axes onto: \u003Cc\u003EX = P\u003C/c\u003E (primary), \u003Cc\u003EZ\u003C/c\u003E = the secondary hint \u003Cc\u003ES\u003C/c\u003E orthonormalized\r\n/// against \u003Cc\u003EP\u003C/c\u003E, \u003Cc\u003EY = cross(Z, X)\u003C/c\u003E (right-handed; for fingers Y is the curl hinge).\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EPrimary axis P\u003C/b\u003E = normalize(chain-child head \u2212 bone head), where the chain\r\n/// child is the next \u003Ci\u003Emapped\u003C/i\u003E role down the bone\u0027s anatomical chain\r\n/// (Hips\u2192Spine0..4\u2192Neck\u2192Head; Clavicle\u2192UpperArm\u2192LowerArm\u2192Hand; UpperLeg\u2192LowerLeg\u2192Foot\u2192Toe;\r\n/// per-finger Meta\u2192Prox\u2192Mid\u2192Dist). Tips: the Head inherits its previous chain segment\r\n/// (neck\u2192head \u2014 the skull-base axis, real anatomy; measured 0\u201327\u00B0 forward of character up\r\n/// across neutral-rest rigs), falling back to a virtual character-up extension only when\r\n/// that segment is absent or degenerate; Hand points at the midpoint of its mapped finger\r\n/// proximals (else along the forearm); Foot without a toe and Toe extend along character\r\n/// forward; finger distals extend along their previous segment. Other bones with nothing\r\n/// mapped below inherit the previous chain segment\u0027s direction.\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003ESecondary axis S\u003C/b\u003E by bone class: spine/neck/head/hips and legs use character\r\n/// forward (knee hinge lateral); clavicle/arms/hands use \u003Cc\u003Ecross(P, characterUp)\u003C/c\u003E\r\n/// (elbow hinge \u22A5 limb in the character\u0027s horizontal plane at T-pose), falling back to\r\n/// character forward when P is vertical; feet/toes use character up; fingers use the hand\u0027s\r\n/// dorsal palm normal (see \u003Csee cref=\u0022HandGeometry.Dorsal\u0022/\u003E) so a positive rotation about\r\n/// frame Y curls fingertips toward the palm on both hands.\u003C/para\u003E\r\n/// \u003Cpara\u003EWhen used by the solver, build the frames on the \u003Csee cref=\u0022RestNormalizer\u0022/\u003E-\r\n/// normalized rest via \u003Csee cref=\u0022Build(SkeletonModel, MappingResult, IReadOnlyList{XForm})\u0022/\u003E;\r\n/// this class itself just measures whatever rest it is given.\u003C/para\u003E\r\n/// \u003C/remarks\u003E\r\npublic sealed class CanonicalFrames\r\n{\r\n    private readonly Dictionary\u003CBoneRole, Quaternion\u003E _frames;\r\n    private readonly HashSet\u003CBoneRole\u003E _virtualPrimary;\r\n\r\n    /// \u003Csummary\u003ECharacter forward (the direction the toes point at rest), unit length.\u003C/summary\u003E\r\n    public Vector3 CharacterForward { get; }\r\n\r\n    /// \u003Csummary\u003ECharacter up (hips toward shoulders at rest), unit length.\u003C/summary\u003E\r\n    public Vector3 CharacterUp { get; }\r\n\r\n    /// \u003Csummary\u003ERest hip height above the lowest foot/toe point, along character up, cm.\u003C/summary\u003E\r\n    public float HipHeight { get; }\r\n\r\n    private CanonicalFrames(\r\n        Dictionary\u003CBoneRole, Quaternion\u003E frames, HashSet\u003CBoneRole\u003E virtualPrimary,\r\n        Vector3 forward, Vector3 up, float hipHeight)\r\n    {\r\n        _frames = frames;\r\n        _virtualPrimary = virtualPrimary;\r\n        CharacterForward = forward;\r\n        CharacterUp = up;\r\n        HipHeight = hipHeight;\r\n    }\r\n\r\n    /// \u003Csummary\u003ETrue when a canonical frame exists for \u003Cparamref name=\u0022role\u0022/\u003E (the role is\r\n    /// mapped and its chain geometry is resolvable).\u003C/summary\u003E\r\n    public bool Has(BoneRole role) =\u003E _frames.ContainsKey(role);\r\n\r\n    /// \u003Csummary\u003E\r\n    /// True when the role\u0027s primary axis is a \u003Cb\u003Evirtual\u003C/b\u003E character-axis extension rather\r\n    /// than real joint geometry (e.g. a Foot with no mapped Toe extends along character\r\n    /// forward; mapped Toes extend along character forward by convention; a Head whose\r\n    /// neck\u2192head segment is degenerate extends along character up). Absolute direction\r\n    /// matching against a virtual primary imposes an arbitrary direction, so the solver\r\n    /// falls back to delta transfer when the source is virtual but the target is real\r\n    /// (see \u003Csee cref=\u0022GeometricSolver\u0022/\u003E remarks).\r\n    /// \u003C/summary\u003E\r\n    public bool HasVirtualPrimary(BoneRole role) =\u003E _virtualPrimary.Contains(role);\r\n\r\n    /// \u003Csummary\u003EThe world-space canonical rest frame of \u003Cparamref name=\u0022role\u0022/\u003E.\u003C/summary\u003E\r\n    /// \u003Cexception cref=\u0022InvalidOperationException\u0022\u003EThrown when \u003Csee cref=\u0022Has\u0022/\u003E is false for\r\n    /// the role.\u003C/exception\u003E\r\n    public Quaternion WorldFrameOf(BoneRole role)\r\n        =\u003E _frames.TryGetValue(role, out var frame)\r\n            ? frame\r\n            : throw new InvalidOperationException($\u0022No canonical frame for role {role} (not mapped or unresolvable).\u0022);\r\n\r\n    /// \u003Csummary\u003EBuilds frames from the skeleton\u0027s bind rest (\u003Cc\u003Eskeleton.RestWorld\u003C/c\u003E).\u003C/summary\u003E\r\n    public static CanonicalFrames Build(SkeletonModel skeleton, MappingResult map)\r\n        =\u003E Build(skeleton, map, (skeleton ?? throw new ArgumentNullException(nameof(skeleton))).RestWorld);\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Builds frames from explicit rest world transforms (e.g. a \u003Csee cref=\u0022RestPose\u0022/\u003E\r\n    /// produced by \u003Csee cref=\u0022RestNormalizer\u0022/\u003E), indexed like \u003Cc\u003Eskeleton.Bones\u003C/c\u003E.\r\n    /// \u003C/summary\u003E\r\n    public static CanonicalFrames Build(\r\n        SkeletonModel skeleton, MappingResult map, IReadOnlyList\u003CXForm\u003E worldRest)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(skeleton);\r\n        ArgumentNullException.ThrowIfNull(map);\r\n        ArgumentNullException.ThrowIfNull(worldRest);\r\n        if (worldRest.Count != skeleton.Count)\r\n            throw new ArgumentException(\r\n                $\u0022worldRest has {worldRest.Count} entries for a {skeleton.Count}-bone skeleton.\u0022);\r\n\r\n        var cf = CharacterFrame.Compute(skeleton, map, worldRest);\r\n        var frames = new Dictionary\u003CBoneRole, Quaternion\u003E();\r\n        var virtualPrimary = new HashSet\u003CBoneRole\u003E();\r\n\r\n        foreach (var (chain, kind, left) in Chains())\r\n            BuildChainFrames(chain, kind, left, map, worldRest, cf, frames, virtualPrimary);\r\n\r\n        return new CanonicalFrames(frames, virtualPrimary, cf.Forward, cf.Up, cf.HipHeight);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- chain construction\r\n\r\n    private enum ChainKind\r\n    {\r\n        Body,\r\n        Arm,\r\n        Leg,\r\n        Finger,\r\n    }\r\n\r\n    private static IEnumerable\u003C(BoneRole[] Chain, ChainKind Kind, bool Left)\u003E Chains()\r\n    {\r\n        yield return (new[]\r\n        {\r\n            BoneRole.Hips, BoneRole.Spine0, BoneRole.Spine1, BoneRole.Spine2, BoneRole.Spine3,\r\n            BoneRole.Spine4, BoneRole.Neck, BoneRole.Head,\r\n        }, ChainKind.Body, false);\r\n\r\n        foreach (var left in new[] { true, false })\r\n        {\r\n            var s = left ? \u0022L\u0022 : \u0022R\u0022;\r\n            yield return (new[]\r\n            {\r\n                Role(\u0022Clavicle\u0022, s), Role(\u0022UpperArm\u0022, s), Role(\u0022LowerArm\u0022, s), Role(\u0022Hand\u0022, s),\r\n            }, ChainKind.Arm, left);\r\n            yield return (new[]\r\n            {\r\n                Role(\u0022UpperLeg\u0022, s), Role(\u0022LowerLeg\u0022, s), Role(\u0022Foot\u0022, s), Role(\u0022Toe\u0022, s),\r\n            }, ChainKind.Leg, left);\r\n\r\n            foreach (var finger in new[] { \u0022Thumb\u0022, \u0022Index\u0022, \u0022Middle\u0022, \u0022Ring\u0022, \u0022Pinky\u0022 })\r\n            {\r\n                yield return (new[]\r\n                {\r\n                    Role(finger \u002B \u0022Meta\u0022, s), Role(finger \u002B \u0022Prox\u0022, s),\r\n                    Role(finger \u002B \u0022Mid\u0022, s), Role(finger \u002B \u0022Dist\u0022, s),\r\n                }, ChainKind.Finger, left);\r\n            }\r\n        }\r\n    }\r\n\r\n    private static BoneRole Role(string baseName, string side) =\u003E Enum.Parse\u003CBoneRole\u003E(baseName \u002B side);\r\n\r\n    private static void BuildChainFrames(\r\n        BoneRole[] chain, ChainKind kind, bool left, MappingResult map,\r\n        IReadOnlyList\u003CXForm\u003E worldRest, CharacterFrame cf, Dictionary\u003CBoneRole, Quaternion\u003E frames,\r\n        HashSet\u003CBoneRole\u003E virtualPrimary)\r\n    {\r\n        // Collapse to the mapped chain members; gaps are skipped so e.g. a missing Spine1\r\n        // makes Spine0 point straight at Spine2.\r\n        var mapped = new List\u003C(BoneRole Role, Vector3 Pos)\u003E(chain.Length);\r\n        foreach (var role in chain)\r\n        {\r\n            if (map.RoleToBone.TryGetValue(role, out var index))\r\n                mapped.Add((role, worldRest[index].Pos));\r\n        }\r\n\r\n        Vector3? dorsal = kind == ChainKind.Finger ? HandGeometry.Dorsal(map, worldRest, left) : null;\r\n\r\n        for (var i = 0; i \u003C mapped.Count; i\u002B\u002B)\r\n        {\r\n            var (role, pos) = mapped[i];\r\n            Vector3? prevDir = i \u003E 0 ? pos - mapped[i - 1].Pos : null;\r\n\r\n            var (primary, isVirtual) = i \u002B 1 \u003C mapped.Count\r\n                ? ((Vector3?)(mapped[i \u002B 1].Pos - pos), false)\r\n                : TipPrimary(kind, role, pos, prevDir, left, map, worldRest, cf);\r\n            if (primary is null || primary.Value.LengthSquared() \u003C 1e-8f)\r\n                continue;\r\n\r\n            var secondary = Secondary(kind, role, primary.Value, dorsal, cf);\r\n            frames[role] = BasisFromPrimarySecondary(primary.Value, secondary, cf);\r\n            if (isVirtual)\r\n                virtualPrimary.Add(role);\r\n        }\r\n    }\r\n\r\n    /// \u003Csummary\u003EPrimary direction for the last mapped bone of a chain. \u003Cc\u003EVirtual\u003C/c\u003E is true\r\n    /// when the direction is a character-axis convention rather than this rig\u0027s real joint\r\n    /// geometry (see \u003Csee cref=\u0022HasVirtualPrimary\u0022/\u003E).\u003C/summary\u003E\r\n    private static (Vector3? Dir, bool Virtual) TipPrimary(\r\n        ChainKind kind, BoneRole role, Vector3 pos, Vector3? prevDir, bool left,\r\n        MappingResult map, IReadOnlyList\u003CXForm\u003E worldRest, CharacterFrame cf)\r\n    {\r\n        switch (kind)\r\n        {\r\n            case ChainKind.Body:\r\n                // Head: its primary is the REAL previous chain segment (neck\u2192head \u2014 the\r\n                // skull-base axis; the rest lean of that segment is head-joint-placement\r\n                // anatomy the delta transfer modes reference, and the posed-rest gaze\r\n                // fallback measures \u2014 see GeometricSolver remarks). Only a degenerate or\r\n                // absent segment falls back to the virtual character-up extension (e.g. a\r\n                // head stacked on the neck). A body chain that ends early keeps its\r\n                // previous segment direction, defaulting to up.\r\n                if (role == BoneRole.Head)\r\n                    return prevDir is { } seg \u0026\u0026 seg.LengthSquared() \u003E= 1e-8f ? (seg, false) : (cf.Up, true);\r\n                return prevDir is not null ? (prevDir, false) : (cf.Up, true);\r\n\r\n            case ChainKind.Arm:\r\n                if (role is BoneRole.HandL or BoneRole.HandR)\r\n                {\r\n                    var knuckles = HandGeometry.FingerProximalMidpoint(map, worldRest, left);\r\n                    if (knuckles is not null)\r\n                        return (knuckles.Value - pos, false);\r\n                }\r\n                return (prevDir, false); // along the forearm / previous segment; null \u2192 no frame\r\n\r\n            case ChainKind.Leg:\r\n                // Foot without a mapped toe, and the toe itself, extend along character\r\n                // forward (toes point forward by the character-frame convention).\r\n                if (role is BoneRole.FootL or BoneRole.FootR or BoneRole.ToeL or BoneRole.ToeR)\r\n                    return (cf.Forward, true);\r\n                return (prevDir, false);\r\n\r\n            case ChainKind.Finger:\r\n                if (prevDir is not null)\r\n                    return (prevDir, false); // distal tip extrapolates its previous segment\r\n                // Single mapped finger bone: point away from the hand when possible.\r\n                var handRole = left ? BoneRole.HandL : BoneRole.HandR;\r\n                if (map.RoleToBone.TryGetValue(handRole, out var handIndex))\r\n                    return (pos - worldRest[handIndex].Pos, false);\r\n                return (null, false);\r\n\r\n            default:\r\n                return (null, false);\r\n        }\r\n    }\r\n\r\n    /// \u003Csummary\u003ESecondary (Z) hint by bone class; see the class remarks for rationale.\u003C/summary\u003E\r\n    private static Vector3 Secondary(ChainKind kind, BoneRole role, Vector3 primary, Vector3? dorsal, CharacterFrame cf)\r\n    {\r\n        switch (kind)\r\n        {\r\n            case ChainKind.Body:\r\n                return cf.Forward;\r\n\r\n            case ChainKind.Arm:\r\n            {\r\n                var hinge = Vector3.Cross(Vector3.Normalize(primary), cf.Up);\r\n                return hinge.LengthSquared() \u003C 1e-6f ? cf.Forward : hinge;\r\n            }\r\n\r\n            case ChainKind.Leg:\r\n                // Feet and toes lie near the character-forward direction, so they use up as\r\n                // the secondary; thigh/calf use forward (knee hinge lateral).\r\n                if (role is BoneRole.FootL or BoneRole.FootR or BoneRole.ToeL or BoneRole.ToeR)\r\n                    return cf.Up;\r\n                return cf.Forward;\r\n\r\n            case ChainKind.Finger:\r\n                if (dorsal is not null)\r\n                    return dorsal.Value;\r\n                var fallback = Vector3.Cross(Vector3.Normalize(primary), cf.Up);\r\n                return fallback.LengthSquared() \u003C 1e-6f ? cf.Forward : fallback;\r\n\r\n            default:\r\n                return cf.Forward;\r\n        }\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Orthonormal right-handed basis: \u003Cc\u003EX = normalize(primary)\u003C/c\u003E, \u003Cc\u003EZ = secondary\u003C/c\u003E\r\n    /// Gram-Schmidt-orthonormalized against X (falling back to character forward, then up,\r\n    /// then world axes when degenerate), \u003Cc\u003EY = cross(Z, X)\u003C/c\u003E.\r\n    /// \u003C/summary\u003E\r\n    private static Quaternion BasisFromPrimarySecondary(Vector3 primary, Vector3 secondary, CharacterFrame cf)\r\n    {\r\n        var x = Vector3.Normalize(primary);\r\n\r\n        var z = Orthonormalized(secondary, x)\r\n            ?? Orthonormalized(cf.Forward, x)\r\n            ?? Orthonormalized(cf.Up, x)\r\n            ?? Orthonormalized(Vector3.UnitZ, x)\r\n            ?? Orthonormalized(Vector3.UnitX, x)!.Value;\r\n\r\n        var y = Vector3.Cross(z, x);\r\n\r\n        // System.Numerics matrices act on row vectors: the rows are the images of the unit\r\n        // 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 MathQ.Normalize(Quaternion.CreateFromRotationMatrix(m));\r\n    }\r\n\r\n    private static Vector3? Orthonormalized(Vector3 hint, Vector3 x)\r\n    {\r\n        var z = hint - x * Vector3.Dot(hint, x);\r\n        return z.LengthSquared() \u003C 1e-6f ? null : Vector3.Normalize(z);\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"Code/HumanoidRetargeter/Solve/HandGeometry.cs","FileName":"HandGeometry.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidRetargeter.Mapping;\r\nusing HumanoidRetargeter.Maths;\r\n\r\nnamespace HumanoidRetargeter.Solve;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)\r\n\r\n/// \u003Csummary\u003E\r\n/// Hand rest-geometry helpers shared by \u003Csee cref=\u0022CanonicalFrames\u0022/\u003E (finger secondary axes)\r\n/// and \u003Csee cref=\u0022RestNormalizer\u0022/\u003E (palm-down roll correction). Everything derives from joint\r\n/// positions only \u2014 bone local axes carry no anatomical meaning on the s\u0026amp;box rig.\r\n/// \u003C/summary\u003E\r\ninternal static class HandGeometry\r\n{\r\n    private static readonly BoneRole[] LeftProximals =\r\n    {\r\n        BoneRole.ThumbProxL, BoneRole.IndexProxL, BoneRole.MiddleProxL, BoneRole.RingProxL, BoneRole.PinkyProxL,\r\n    };\r\n\r\n    private static readonly BoneRole[] RightProximals =\r\n    {\r\n        BoneRole.ThumbProxR, BoneRole.IndexProxR, BoneRole.MiddleProxR, BoneRole.RingProxR, BoneRole.PinkyProxR,\r\n    };\r\n\r\n    // Index \u2192 pinky order; the knuckle line is taken from the first and last mapped of these.\r\n    private static readonly BoneRole[] LeftNonThumbProximals =\r\n    {\r\n        BoneRole.IndexProxL, BoneRole.MiddleProxL, BoneRole.RingProxL, BoneRole.PinkyProxL,\r\n    };\r\n\r\n    private static readonly BoneRole[] RightNonThumbProximals =\r\n    {\r\n        BoneRole.IndexProxR, BoneRole.MiddleProxR, BoneRole.RingProxR, BoneRole.PinkyProxR,\r\n    };\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Midpoint of all mapped finger proximal heads of one hand (the hand\u0027s anatomical\r\n    /// \u0022chain child\u0022 point), or null when no finger proximal is mapped.\r\n    /// \u003C/summary\u003E\r\n    public static Vector3? FingerProximalMidpoint(MappingResult map, IReadOnlyList\u003CXForm\u003E worldRest, bool left)\r\n    {\r\n        var sum = Vector3.Zero;\r\n        var count = 0;\r\n        foreach (var role in left ? LeftProximals : RightProximals)\r\n        {\r\n            if (map.RoleToBone.TryGetValue(role, out var index))\r\n            {\r\n                sum \u002B= worldRest[index].Pos;\r\n                count\u002B\u002B;\r\n            }\r\n        }\r\n        return count \u003E 0 ? sum / count : null;\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Dorsal palm normal of one hand: the unit vector pointing out of the \u003Cb\u003Eback\u003C/b\u003E of the\r\n    /// hand (away from the palm), or null when the hand/finger geometry is unmapped or\r\n    /// degenerate.\r\n    /// \u003C/summary\u003E\r\n    /// \u003Cremarks\u003E\r\n    /// Formula (mirror-consistent by construction, verified on the ActorCore fixture by the\r\n    /// finger-curl test): \u003Cc\u003Edorsal = sideSign \u00B7 cross(knuckle, fingerDir)\u003C/c\u003E with\r\n    /// \u003Cc\u003EsideSign = \u002B1\u003C/c\u003E left / \u003Cc\u003E\u22121\u003C/c\u003E right, \u003Cc\u003Eknuckle = IndexProx.head \u2212\r\n    /// PinkyProx.head\u003C/c\u003E (first/last mapped non-thumb proximal), and \u003Cc\u003EfingerDir =\r\n    /// FingerProximalMidpoint \u2212 Hand.head\u003C/c\u003E. On every fixture rig the thumb proximal lies on\r\n    /// the \u2212dorsal (palmar) side of the hand plane, grounding the sign anatomically. A positive\r\n    /// rotation about a finger frame\u0027s hinge axis (frame Y = cross(dorsal, fingerChainDir))\r\n    /// curls the fingertip toward the palm on \u003Cb\u003Eboth\u003C/b\u003E hands.\r\n    /// \u003C/remarks\u003E\r\n    public static Vector3? Dorsal(MappingResult map, IReadOnlyList\u003CXForm\u003E worldRest, bool left)\r\n    {\r\n        if (!map.RoleToBone.TryGetValue(left ? BoneRole.HandL : BoneRole.HandR, out var handIndex))\r\n            return null;\r\n        var hand = worldRest[handIndex].Pos;\r\n\r\n        var nonThumb = left ? LeftNonThumbProximals : RightNonThumbProximals;\r\n        Vector3? first = null, last = null;\r\n        foreach (var role in nonThumb)\r\n        {\r\n            if (!map.RoleToBone.TryGetValue(role, out var index))\r\n                continue;\r\n            first ??= worldRest[index].Pos;\r\n            last = worldRest[index].Pos;\r\n        }\r\n        if (first is null || last is null || (first.Value - last.Value).LengthSquared() \u003C 1e-8f)\r\n            return null;\r\n\r\n        var midpoint = FingerProximalMidpoint(map, worldRest, left);\r\n        if (midpoint is null)\r\n            return null;\r\n\r\n        var knuckle = first.Value - last.Value;\r\n        var fingerDir = midpoint.Value - hand;\r\n        var raw = Vector3.Cross(knuckle, fingerDir) * (left ? 1f : -1f);\r\n        return raw.LengthSquared() \u003C 1e-8f ? null : Vector3.Normalize(raw);\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"Code/HumanoidRetargeter/Solve/TargetRigMapping.cs","FileName":"TargetRigMapping.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\nusing HumanoidRetargeter.Mapping;\r\nusing HumanoidRetargeter.Target;\r\n\r\nnamespace HumanoidRetargeter.Solve;\r\n\r\n/// \u003Csummary\u003E\r\n/// Bridges a \u003Csee cref=\u0022TargetRig\u0022/\u003E\u0027s role annotations into the \u003Csee cref=\u0022MappingResult\u0022/\u003E\r\n/// shape shared with source mappings, so target-side machinery (rest normalization, canonical\r\n/// frames) can run on the exact same code paths as the source side.\r\n/// \u003C/summary\u003E\r\npublic static class TargetRigMappingExtensions\r\n{\r\n    /// \u003Csummary\u003ERole \u2192 target bone index mapping of the rig\u0027s annotated animated bones.\u003C/summary\u003E\r\n    public static MappingResult ToMappingResult(this TargetRig rig)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(rig);\r\n\r\n        var map = new MappingResult(rig.Name, MappingSource.Preset) { Confidence = 1f };\r\n        for (var i = 0; i \u003C rig.Skeleton.Count; i\u002B\u002B)\r\n        {\r\n            if (rig.RoleOf(i) is BoneRole role)\r\n                map.RoleToBone[role] = i;\r\n        }\r\n        return map;\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_retargeter","Path":"HumanoidRetargeter/Assembly.cs","FileName":"Assembly.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":311783,"Code":"#nullable enable annotations\r\n\r\n// Global usings for the s\u0026box in-engine compiler.\r\n//\r\n// The plain net8.0 dev harness gets these automatically via \u003CImplicitUsings\u003E,\r\n// but s\u0026box\u0027s compiler injects no BCL usings at all - without this file the\r\n// library fails to compile inside the editor (CS0246 on List\u003C\u003E, IEnumerable\u003C\u003E,\r\n// FormatException, ...). Duplicating the SDK\u0027s implicit set is harmless there\r\n// (verified: no warnings).\r\n\r\nglobal using System;\r\nglobal using System.Collections.Generic;\r\nglobal using System.IO;\r\nglobal using System.Linq;\r\nglobal using System.Threading;\r\nglobal using System.Threading.Tasks;\r\n\r\n// NOTE on Vector3: s\u0026box declares its own Vector3 in the *global namespace*,\r\n// which wins over \u0060using System.Numerics;\u0060 imports during name lookup and\r\n// breaks this System.Numerics-based core (.X/.Y/.Z, static helpers, delegate\r\n// signatures). A global using-alias does NOT fix this (CS0576: alias conflicts\r\n// with the global-namespace type at every use site). The working fix is a\r\n// *namespace-scoped* alias, declared after the file-scoped namespace line:\r\n//\r\n//     namespace HumanoidRetargeter.Xyz;\r\n//     using Vector3 = System.Numerics.Vector3;\r\n//\r\n// Every file in this tree that uses the simple name Vector3 carries that line.\r\n// (Quaternion and Matrix4x4 are not global-namespace types in s\u0026box and need\r\n// no alias.)\r\n"}]}