{"TotalCount":331,"Files":[{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"HumanoidMocap/Cleanup/TwistBoneFollow.cs","FileName":"TwistBoneFollow.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":395388,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidMocap.Mapping;\r\nusing HumanoidMocap.Maths;\r\nusing HumanoidMocap.Target;\r\n\r\nnamespace HumanoidMocap.Cleanup;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/HumanoidMocap/Assembly.cs)\r\n\r\n/// \u003Csummary\u003E\r\n/// Drives unmapped limb deform bones from the joints whose motion they distribute.\r\n/// Auto-rigged exports (Auto-Rig Pro \u003Cc\u003Eforearm_twist.l\u003C/c\u003E, AdvancedSkeleton\r\n/// \u003Cc\u003EElbowPart1_L\u003C/c\u003E, Biped \u003Cc\u003EBip01 L ForeTwist\u003C/c\u003E) spread limb roll across helper\r\n/// bones the game constrains at runtime; a baked retarget that leaves them at rest\r\n/// candy-wraps the skin \u2014 the reported wrist \u0022spike fans\u0022 when the hand pronates.\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// Detection is geometric, name-free: an UNMAPPED bone whose parent is a mapped limb\r\n/// bone (upper/lower arm or leg) and whose rest position lies ON the segment from that\r\n/// parent to the parent\u0027s mapped chain child (within 15\u00B0 of the axis, fraction\r\n/// 0.05..1.1 along it). Each detected twist follows the chain child\u0027s per-frame local\r\n/// ROLL \u2014 the twist component of its rotation delta about the limb axis \u2014 scaled by the\r\n/// twist\u0027s fractional position (a bone at 60% of the forearm takes 60% of the hand\u0027s\r\n/// roll; ARP\u0027s proximal \u003Cc\u003Earm_twist\u003C/c\u003E at fraction ~0 correctly takes ~none). Pure\r\n/// swing carries no twist component, so elbows/knees bending never move these bones.\r\n/// Serial deform bones between two mapped limb joints are handled separately: their\r\n/// world-space motion delta is interpolated between the endpoints while both mapped\r\n/// endpoint transforms remain unchanged. This covers rigs that split each bend/twist\r\n/// section into two weighted bones without relying on exporter-specific names. An\r\n/// unmapped sibling at the mapped joint\u0027s same pivot follows its complete rotation;\r\n/// this covers dual control/deform rigs where a mechanism forearm/femur drives the next\r\n/// joint while coincident anatomical bones carry the skin.\r\n/// \u003C/remarks\u003E\r\npublic static class TwistBoneFollow\r\n{\r\n    private static readonly (BoneRole Parent, BoneRole Child)[] Segments =\r\n    {\r\n        (BoneRole.ClavicleL, BoneRole.UpperArmL),\r\n        (BoneRole.UpperArmL, BoneRole.LowerArmL), (BoneRole.LowerArmL, BoneRole.HandL),\r\n        (BoneRole.ClavicleR, BoneRole.UpperArmR),\r\n        (BoneRole.UpperArmR, BoneRole.LowerArmR), (BoneRole.LowerArmR, BoneRole.HandR),\r\n        (BoneRole.Hips, BoneRole.UpperLegL),\r\n        (BoneRole.UpperLegL, BoneRole.LowerLegL), (BoneRole.LowerLegL, BoneRole.FootL),\r\n        (BoneRole.Hips, BoneRole.UpperLegR),\r\n        (BoneRole.UpperLegR, BoneRole.LowerLegR), (BoneRole.LowerLegR, BoneRole.FootR),\r\n    };\r\n\r\n    private readonly record struct InlineBone(int Bone, int Parent, int Child, float Fraction);\r\n\r\n    private readonly record struct FullFollower(int Bone, int Driver);\r\n\r\n    /// \u003Csummary\u003EApplies the pass in place; returns how many limb helpers were driven.\u003C/summary\u003E\r\n    public static int Apply(\r\n        IReadOnlyList\u003CXForm[]\u003E frames, TargetRig rig, IReadOnlySet\u003Cint\u003E? excluded)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(frames);\r\n        ArgumentNullException.ThrowIfNull(rig);\r\n        var skeleton = rig.Skeleton;\r\n\r\n        var twists = new List\u003C(int Bone, int Driver, Vector3 Axis, float Fraction)\u003E();\r\n        var fullFollowers = new List\u003CFullFollower\u003E();\r\n        var fullFollowerBones = new HashSet\u003Cint\u003E();\r\n        foreach (var (parentRole, childRole) in Segments)\r\n        {\r\n            if (rig.BoneForRole(parentRole) is not { } parent\r\n                || rig.BoneForRole(childRole) is not { } child\r\n                || skeleton[child].ParentIndex != parent)\r\n                continue;\r\n\r\n            // Limb axis and length in the PARENT\u0027s local space (the chain child\u0027s rest\r\n            // local translation).\r\n            var axis = skeleton[child].RestLocal.Pos;\r\n            var length = axis.Length();\r\n            if (length \u003C 1e-3f)\r\n                continue;\r\n            axis /= length;\r\n\r\n            for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n            {\r\n                if (i == child || skeleton[i].ParentIndex != parent\r\n                    || rig.RoleOf(i) is not null || excluded?.Contains(i) == true)\r\n                    continue;\r\n                var pos = skeleton[i].RestLocal.Pos;\r\n                // Blender control/deform exports commonly put a mechanism joint and one\r\n                // or more skinned anatomical joints at the same pivot (MCH_forearm beside\r\n                // radius/ulna, MCH_femur beside femur). The mapped mechanism drives the\r\n                // next joint, but its deform siblings need the complete bend and roll;\r\n                // treating them as ordinary twist bones copies roll only and leaves the\r\n                // mesh behind while the hand/leg moves away.\r\n                if ((pos - skeleton[child].RestLocal.Pos).Length()\r\n                    \u003C= MathF.Max(0.01f, length * 0.01f))\r\n                {\r\n                    if (fullFollowerBones.Add(i))\r\n                        fullFollowers.Add(new FullFollower(i, child));\r\n                    continue;\r\n                }\r\n                var along = Vector3.Dot(pos, axis);\r\n                var fraction = along / length;\r\n                if (fraction is \u003C 0.05f or \u003E 1.1f)\r\n                    continue;\r\n                var offAxis = (pos - axis * along).Length();\r\n                if (offAxis \u003E MathF.Tan(15f * MathF.PI / 180f) * MathF.Max(along, 1e-3f))\r\n                    continue;\r\n                twists.Add((i, child, axis, Math.Clamp(fraction, 0f, 1f)));\r\n            }\r\n        }\r\n        var inline = FindInlineBones(rig, excluded);\r\n        if (twists.Count == 0 \u0026\u0026 inline.Count == 0 \u0026\u0026 fullFollowers.Count == 0)\r\n            return 0;\r\n\r\n        foreach (var frame in frames)\r\n        {\r\n            foreach (var follower in fullFollowers)\r\n            {\r\n                // Both bones share a parent, so the driver\u0027s local-space rotation delta\r\n                // can be applied directly while retaining the deform bone\u0027s bind offset.\r\n                var delta = MathQ.Normalize(frame[follower.Driver].Rot\r\n                    * Quaternion.Conjugate(skeleton[follower.Driver].RestLocal.Rot));\r\n                frame[follower.Bone] = new XForm(\r\n                    frame[follower.Bone].Pos,\r\n                    MathQ.Normalize(delta * skeleton[follower.Bone].RestLocal.Rot));\r\n            }\r\n            foreach (var (bone, driver, axis, fraction) in twists)\r\n            {\r\n                // The driver\u0027s rotation delta from rest, in the shared parent\u0027s space,\r\n                // forced to the SHORTEST arc (W \u003E= 0) so the twist angle below is\r\n                // continuous in (-180\u00B0, 180\u00B0) and never flips representation.\r\n                var delta = MathQ.Normalize(\r\n                    frame[driver].Rot * Quaternion.Conjugate(skeleton[driver].RestLocal.Rot));\r\n                if (delta.W \u003C 0f)\r\n                    delta = new Quaternion(-delta.X, -delta.Y, -delta.Z, -delta.W);\r\n                // Twist component about the limb axis (swing-twist decomposition).\r\n                var proj = Vector3.Dot(new Vector3(delta.X, delta.Y, delta.Z), axis);\r\n                // Ill-conditioned when the delta approaches a pure 180\u00B0 SWING (both the\r\n                // axis projection and W collapse toward 0): the decomposition then\r\n                // amplifies noise into huge fake rolls \u2014 measured on a throw clip, the\r\n                // kicking foot injected \u00B199\u00B0 into the calf twist bone and the calf skin\r\n                // flipped upward (\u0022the leg is up\u0022). Keep rest instead.\r\n                var conditioning = MathF.Sqrt(proj * proj \u002B delta.W * delta.W);\r\n                if (conditioning \u003C 0.2f)\r\n                    continue;\r\n                var angle = 2f * MathF.Atan2(proj, delta.W);\r\n                var scaled = Quaternion.CreateFromAxisAngle(axis, angle * fraction);\r\n                frame[bone] = new XForm(\r\n                    frame[bone].Pos, MathQ.Normalize(scaled * skeleton[bone].RestLocal.Rot));\r\n            }\r\n\r\n            if (inline.Count \u003E 0)\r\n                FollowInlineBones(frame, skeleton, inline);\r\n        }\r\n        return twists.Count \u002B inline.Count \u002B fullFollowers.Count;\r\n    }\r\n\r\n    private static List\u003CInlineBone\u003E FindInlineBones(\r\n        TargetRig rig, IReadOnlySet\u003Cint\u003E? excluded)\r\n    {\r\n        var skeleton = rig.Skeleton;\r\n        var result = new List\u003CInlineBone\u003E();\r\n        var seen = new HashSet\u003Cint\u003E();\r\n        foreach (var (parentRole, childRole) in Segments)\r\n        {\r\n            if (rig.BoneForRole(parentRole) is not { } parent\r\n                || rig.BoneForRole(childRole) is not { } child)\r\n                continue;\r\n\r\n            var path = new List\u003Cint\u003E();\r\n            for (var bone = skeleton[child].ParentIndex;\r\n                 bone \u003E= 0 \u0026\u0026 bone != parent;\r\n                 bone = skeleton[bone].ParentIndex)\r\n                path.Add(bone);\r\n            if (path.Count == 0\r\n                || skeleton[path[^1]].ParentIndex != parent\r\n                || path.Any(bone =\u003E rig.RoleOf(bone) is not null\r\n                    || excluded?.Contains(bone) == true))\r\n                continue;\r\n            path.Reverse();\r\n\r\n            var length = 0f;\r\n            var previous = parent;\r\n            foreach (var bone in path.Append(child))\r\n            {\r\n                length \u002B= (skeleton.RestWorld[bone].Pos\r\n                    - skeleton.RestWorld[previous].Pos).Length();\r\n                previous = bone;\r\n            }\r\n            if (length \u003C 1e-3f)\r\n                continue;\r\n\r\n            var along = 0f;\r\n            previous = parent;\r\n            foreach (var bone in path)\r\n            {\r\n                along \u002B= (skeleton.RestWorld[bone].Pos\r\n                    - skeleton.RestWorld[previous].Pos).Length();\r\n                if (seen.Add(bone))\r\n                    result.Add(new InlineBone(bone, parent, child, along / length));\r\n                previous = bone;\r\n            }\r\n        }\r\n        return result;\r\n    }\r\n\r\n    private static void FollowInlineBones(\r\n        XForm[] frame, Skeleton.Skeleton skeleton, IReadOnlyList\u003CInlineBone\u003E inline)\r\n    {\r\n        var world = new Skeleton.Pose(frame).ToWorld(skeleton);\r\n        var desired = world.ToArray();\r\n        var pathBones = new HashSet\u003Cint\u003E();\r\n\r\n        foreach (var group in inline.GroupBy(entry =\u003E (entry.Parent, entry.Child)))\r\n        {\r\n            var parent = group.Key.Parent;\r\n            var child = group.Key.Child;\r\n            var parentDelta = MathQ.Normalize(world[parent].Rot\r\n                * Quaternion.Conjugate(skeleton.RestWorld[parent].Rot));\r\n            var childDelta = MathQ.Normalize(world[child].Rot\r\n                * Quaternion.Conjugate(skeleton.RestWorld[child].Rot));\r\n            if (Quaternion.Dot(parentDelta, childDelta) \u003C 0f)\r\n                childDelta = new Quaternion(\r\n                    -childDelta.X, -childDelta.Y, -childDelta.Z, -childDelta.W);\r\n\r\n            foreach (var entry in group)\r\n            {\r\n                var delta = MathQ.Normalize(Quaternion.Slerp(\r\n                    parentDelta, childDelta, entry.Fraction));\r\n                desired[entry.Bone] = new XForm(\r\n                    world[entry.Bone].Pos,\r\n                    MathQ.Normalize(delta * skeleton.RestWorld[entry.Bone].Rot));\r\n                pathBones.Add(entry.Bone);\r\n            }\r\n            // Compensate the mapped endpoint locally so its already-solved world transform\r\n            // remains exact after its intermediary parent starts following the motion.\r\n            pathBones.Add(child);\r\n        }\r\n\r\n        foreach (var bone in pathBones.OrderBy(index =\u003E index))\r\n        {\r\n            var parent = skeleton[bone].ParentIndex;\r\n            frame[bone] = parent \u003C 0\r\n                ? desired[bone]\r\n                : XForm.ToLocal(desired[parent], desired[bone]);\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"HumanoidMocap/Formats/Gltf/GltfModelDmxWriter.cs","FileName":"GltfModelDmxWriter.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":395388,"IsPrivate":false,"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 System.Text.Json;\r\nusing HumanoidMocap.Formats.Dmx;\r\nusing HumanoidMocap.Formats.Fbx;\r\nusing HumanoidMocap.Maths;\r\nusing HumanoidMocap.Skeleton;\r\nusing SkeletonModel = HumanoidMocap.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidMocap.Formats.Gltf;\r\n\r\nusing Matrix4x4 = System.Numerics.Matrix4x4;\r\nusing Quaternion = System.Numerics.Quaternion;\r\nusing Vector2 = System.Numerics.Vector2;\r\nusing Vector3 = System.Numerics.Vector3;\r\n\r\n/// \u003Csummary\u003E\r\n/// Converts the skinned meshes in a glTF/GLB model to Source 2 model-DMX. ModelDoc does\r\n/// not accept glTF as a RenderMeshFile, while DMX preserves the same skeleton, vertices,\r\n/// materials and four-weight skinning without an external converter.\r\n/// \u003C/summary\u003E\r\npublic static class GltfModelDmxWriter\r\n{\r\n    private const float MetersToCentimeters = 100f;\r\n\r\n    /// \u003Csummary\u003EWrites a Y-up, centimeter model-DMX for the already imported target rig.\u003C/summary\u003E\r\n    public static string Write(\r\n        byte[] data, SkeletonModel skeleton, string name,\r\n        Func\u003Cstring, byte[]\u003E? externalBufferResolver = null)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(data);\r\n        ArgumentNullException.ThrowIfNull(skeleton);\r\n        ArgumentNullException.ThrowIfNull(name);\r\n\r\n        var document = GltfDocument.Parse(data, externalBufferResolver);\r\n        var parts = ReadMeshParts(document, skeleton);\r\n        if (parts.Count == 0)\r\n            throw new FormatException(\u0022glTF contains no supported mesh primitives.\u0022);\r\n        return Emit(skeleton, name, parts);\r\n    }\r\n\r\n    private sealed class MeshPart\r\n    {\r\n        public required string Name;\r\n        public required string Material;\r\n        public required Vector3[] Positions;\r\n        public required Vector3[] Normals;\r\n        public required Vector2[] TexCoords;\r\n        public required int[] Triangles;\r\n        public required float[] Weights;\r\n        public required int[] Joints;\r\n    }\r\n\r\n    private static List\u003CMeshPart\u003E ReadMeshParts(GltfDocument document, SkeletonModel skeleton)\r\n    {\r\n        var root = document.Root;\r\n        if (!root.TryGetProperty(\u0022nodes\u0022, out var nodeArray)\r\n            || !root.TryGetProperty(\u0022meshes\u0022, out var meshArray))\r\n            return new List\u003CMeshPart\u003E();\r\n\r\n        root.TryGetProperty(\u0022skins\u0022, out var skinArray);\r\n        root.TryGetProperty(\u0022materials\u0022, out var materialArray);\r\n        var worlds = NodeWorlds(document);\r\n        var bonesByName = new Dictionary\u003Cstring, int\u003E(StringComparer.OrdinalIgnoreCase);\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n            bonesByName[skeleton[i].Name] = i;\r\n\r\n        var parts = new List\u003CMeshPart\u003E();\r\n        var usedNames = new HashSet\u003Cstring\u003E(StringComparer.OrdinalIgnoreCase);\r\n        for (var nodeIndex = 0; nodeIndex \u003C nodeArray.GetArrayLength(); nodeIndex\u002B\u002B)\r\n        {\r\n            var node = nodeArray[nodeIndex];\r\n            if (!node.TryGetProperty(\u0022mesh\u0022, out var meshProperty))\r\n                continue;\r\n            var meshIndex = meshProperty.GetInt32();\r\n            if (meshIndex \u003C 0 || meshIndex \u003E= meshArray.GetArrayLength())\r\n                throw new FormatException($\u0022glTF node {nodeIndex} references invalid mesh {meshIndex}.\u0022);\r\n            var mesh = meshArray[meshIndex];\r\n            if (!mesh.TryGetProperty(\u0022primitives\u0022, out var primitives))\r\n                continue;\r\n\r\n            var skinIndex = node.TryGetProperty(\u0022skin\u0022, out var skinProperty)\r\n                ? skinProperty.GetInt32() : -1;\r\n            var skinJoints = MapSkinJoints(\r\n                document, skeleton, bonesByName, skinArray, skinIndex);\r\n            var skinTransforms = SkinTransforms(document, skinArray, skinIndex, worlds, skinJoints, skeleton);\r\n            var normalMatrix = NormalMatrix(worlds[nodeIndex]);\r\n            var primitiveIndex = 0;\r\n            foreach (var primitive in primitives.EnumerateArray())\r\n            {\r\n                if (!primitive.TryGetProperty(\u0022attributes\u0022, out var attributes)\r\n                    || !attributes.TryGetProperty(\u0022POSITION\u0022, out var positionProperty))\r\n                {\r\n                    primitiveIndex\u002B\u002B;\r\n                    continue;\r\n                }\r\n\r\n                var positions = new Accessor(document, positionProperty.GetInt32(), 3);\r\n                var vertexCount = positions.Count;\r\n                if (vertexCount == 0)\r\n                {\r\n                    primitiveIndex\u002B\u002B;\r\n                    continue;\r\n                }\r\n\r\n                var transformedPositions = new Vector3[vertexCount];\r\n                ReadSkinning(document, attributes, vertexCount, skinJoints,\r\n                    out var weights, out var joints);\r\n                var vertexTransforms = new Matrix4x4[vertexCount];\r\n                for (var i = 0; i \u003C vertexCount; i\u002B\u002B)\r\n                {\r\n                    var transform = worlds[nodeIndex];\r\n                    if (skinTransforms is not null)\r\n                    {\r\n                        transform = default;\r\n                        for (var influence = 0; influence \u003C 4; influence\u002B\u002B)\r\n                        {\r\n                            var at = i * 4 \u002B influence;\r\n                            if (weights[at] \u003E 0f)\r\n                                transform \u002B= skinTransforms[joints[at]] * weights[at];\r\n                        }\r\n                    }\r\n                    vertexTransforms[i] = transform;\r\n                    var value = new Vector3(\r\n                        positions.Float(i, 0), positions.Float(i, 1), positions.Float(i, 2));\r\n                    transformedPositions[i] = Vector3.Transform(value, transform)\r\n                        * MetersToCentimeters;\r\n                }\r\n\r\n                var rawIndices = ReadIndices(document, primitive, vertexCount);\r\n                var mode = primitive.TryGetProperty(\u0022mode\u0022, out var modeProperty)\r\n                    ? modeProperty.GetInt32() : 4;\r\n                var triangles = Triangulate(rawIndices, mode);\r\n\r\n                var normals = new Vector3[vertexCount];\r\n                if (attributes.TryGetProperty(\u0022NORMAL\u0022, out var normalProperty))\r\n                {\r\n                    var source = new Accessor(document, normalProperty.GetInt32(), 3);\r\n                    RequireCount(source, vertexCount, \u0022NORMAL\u0022);\r\n                    for (var i = 0; i \u003C vertexCount; i\u002B\u002B)\r\n                    {\r\n                        var value = new Vector3(\r\n                            source.Float(i, 0), source.Float(i, 1), source.Float(i, 2));\r\n                        var transform = skinTransforms is null ? normalMatrix : NormalMatrix(vertexTransforms[i]);\r\n                        normals[i] = NormalizeOr(Vector3.TransformNormal(value, transform), Vector3.UnitY);\r\n                    }\r\n                }\r\n                else\r\n                {\r\n                    GenerateNormals(transformedPositions, triangles, normals);\r\n                }\r\n\r\n                var texCoords = new Vector2[vertexCount];\r\n                if (attributes.TryGetProperty(\u0022TEXCOORD_0\u0022, out var texCoordProperty))\r\n                {\r\n                    var source = new Accessor(document, texCoordProperty.GetInt32(), 2);\r\n                    RequireCount(source, vertexCount, \u0022TEXCOORD_0\u0022);\r\n                    for (var i = 0; i \u003C vertexCount; i\u002B\u002B)\r\n                        texCoords[i] = new Vector2(source.Float(i, 0), source.Float(i, 1));\r\n                }\r\n\r\n                var baseName = node.TryGetProperty(\u0022name\u0022, out var nodeName)\r\n                    ? nodeName.GetString()\r\n                    : mesh.TryGetProperty(\u0022name\u0022, out var meshName) ? meshName.GetString() : null;\r\n                var partName = UniqueName(\r\n                    Sanitize(baseName ?? $\u0022mesh_{meshIndex}\u0022) \u002B $\u0022_{primitiveIndex}\u0022, usedNames);\r\n                parts.Add(new MeshPart\r\n                {\r\n                    Name = partName,\r\n                    Material = MaterialName(materialArray, primitive),\r\n                    Positions = transformedPositions,\r\n                    Normals = normals,\r\n                    TexCoords = texCoords,\r\n                    Triangles = triangles,\r\n                    Weights = weights,\r\n                    Joints = joints,\r\n                });\r\n                primitiveIndex\u002B\u002B;\r\n            }\r\n        }\r\n        return parts;\r\n    }\r\n\r\n    private static Matrix4x4[] NodeWorlds(GltfDocument document)\r\n    {\r\n        var result = new Matrix4x4[document.Nodes.Count];\r\n        var state = new byte[document.Nodes.Count];\r\n\r\n        Matrix4x4 Visit(int index)\r\n        {\r\n            if (state[index] == 2)\r\n                return result[index];\r\n            if (state[index] == 1)\r\n                throw new FormatException(\u0022glTF node graph contains a cycle.\u0022);\r\n            state[index] = 1;\r\n            var node = document.Nodes[index];\r\n            var local = Matrix4x4.CreateScale(node.Scale)\r\n                * Matrix4x4.CreateFromQuaternion(node.Rotation)\r\n                * Matrix4x4.CreateTranslation(node.Translation);\r\n            result[index] = node.Parent \u003C 0 ? local : local * Visit(node.Parent);\r\n            state[index] = 2;\r\n            return result[index];\r\n        }\r\n\r\n        for (var i = 0; i \u003C result.Length; i\u002B\u002B)\r\n            Visit(i);\r\n        return result;\r\n    }\r\n\r\n    private static Matrix4x4 NormalMatrix(Matrix4x4 world)\r\n    {\r\n        if (!Matrix4x4.Invert(world, out var inverse))\r\n            return Matrix4x4.Identity;\r\n        return Matrix4x4.Transpose(inverse);\r\n    }\r\n\r\n    // Bake the authored skin into the node rest pose before DMX generates new inverse\r\n    // binds. glTF skinned vertices use inverseBind * jointWorld, NOT meshNodeWorld.\r\n    // Keeping the full matrices here also bakes inherited scale into the rigid DMX rig.\r\n    private static Dictionary\u003Cint, Matrix4x4\u003E? SkinTransforms(\r\n        GltfDocument document, JsonElement skins, int skinIndex,\r\n        Matrix4x4[] worlds, int[] mappedJoints, SkeletonModel skeleton)\r\n    {\r\n        if (mappedJoints.Length == 0)\r\n            return null;\r\n        var skin = skins[skinIndex];\r\n        var nodes = skin.GetProperty(\u0022joints\u0022);\r\n        var inverseBinds = skin.TryGetProperty(\u0022inverseBindMatrices\u0022, out var property)\r\n            ? new Accessor(document, property.GetInt32(), 16) : null;\r\n        if (inverseBinds is not null)\r\n            RequireCount(inverseBinds, mappedJoints.Length, \u0022inverseBindMatrices\u0022);\r\n        var result = new Dictionary\u003Cint, Matrix4x4\u003E();\r\n        for (var i = 0; i \u003C mappedJoints.Length; i\u002B\u002B)\r\n        {\r\n            var inverse = Matrix4x4.Identity;\r\n            if (inverseBinds is not null)\r\n                inverse = ReadMatrix(inverseBinds, i);\r\n            // The target can use the authored skin bind instead of the posed scene TRS.\r\n            // Rebind the mesh to that exact skeleton; retain scale omitted by XForm.\r\n            Matrix4x4.Decompose(worlds[nodes[i].GetInt32()], out var scale, out _, out _);\r\n            var rest = skeleton.RestWorld[mappedJoints[i]];\r\n            var jointWorld = Matrix4x4.CreateScale(scale)\r\n                * Matrix4x4.CreateFromQuaternion(rest.Rot)\r\n                * Matrix4x4.CreateTranslation(rest.Pos / MetersToCentimeters);\r\n            result[mappedJoints[i]] = inverse * jointWorld;\r\n        }\r\n        return result;\r\n    }\r\n\r\n    internal static SkeletonModel WithSkinBindPose(GltfDocument document, SkeletonModel skeleton)\r\n    {\r\n        if (!document.Root.TryGetProperty(\u0022skins\u0022, out var skins))\r\n            return skeleton;\r\n        var byName = new Dictionary\u003Cstring, int\u003E(StringComparer.OrdinalIgnoreCase);\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n            byName[Sanitize(skeleton[i].Name)] = i;\r\n        var binds = new Dictionary\u003Cint, XForm\u003E();\r\n        var nodeWorlds = NodeWorlds(document);\r\n        for (var s = 0; s \u003C skins.GetArrayLength(); s\u002B\u002B)\r\n        {\r\n            if (!skins[s].TryGetProperty(\u0022inverseBindMatrices\u0022, out var property))\r\n                continue;\r\n            var joints = MapSkinJoints(document, skeleton, byName, skins, s);\r\n            var accessor = new Accessor(document, property.GetInt32(), 16);\r\n            RequireCount(accessor, joints.Length, \u0022inverseBindMatrices\u0022);\r\n            for (var j = 0; j \u003C joints.Length; j\u002B\u002B)\r\n            {\r\n                if (!Matrix4x4.Invert(ReadMatrix(accessor, j), out var matrix))\r\n                    throw new FormatException(\u0022glTF skin has a singular inverse bind matrix.\u0022);\r\n                // Some exporters fold a bind-shape scale into these matrices. Those\r\n                // are valid for skinning but cannot replace the scene\u0027s rigid rest.\r\n                // Zero-offset scaffold joints do not describe the character\u0027s scale.\r\n                if (skeleton[joints[j]].RestLocal.Pos.LengthSquared() \u003E 1e-6f)\r\n                {\r\n                    Matrix4x4.Decompose(matrix, out var bindScale, out _, out _);\r\n                    var node = skins[s].GetProperty(\u0022joints\u0022)[j].GetInt32();\r\n                    Matrix4x4.Decompose(nodeWorlds[node], out var sceneScale, out _, out _);\r\n                    if (Vector3.Distance(bindScale, sceneScale) \u003E .001f * sceneScale.Length())\r\n                        return skeleton;\r\n                }\r\n                var bind = FbxTransform.ToRigid(matrix);\r\n                bind.Pos *= MetersToCentimeters;\r\n                if (binds.TryGetValue(joints[j], out var previous)\r\n                    \u0026\u0026 (Vector3.Distance(previous.Pos, bind.Pos) \u003E .01f\r\n                        || MathQ.AngleBetween(previous.Rot, bind.Rot) \u003E .001f))\r\n                    return skeleton; // Different per-mesh bind spaces cannot define one rig rest.\r\n                binds[joints[j]] = bind;\r\n            }\r\n        }\r\n        if (binds.Count == 0)\r\n            return skeleton;\r\n        // Bind matrices may use an origin below the displayed scene. Preserve the\r\n        // scene\u0027s floor placement (glTF is Y-up), rather than burying the new rest.\r\n        var sceneFloor = float.PositiveInfinity;\r\n        var bindFloor = float.PositiveInfinity;\r\n        foreach (var pair in binds)\r\n        {\r\n            sceneFloor = MathF.Min(sceneFloor, skeleton.RestWorld[pair.Key].Pos.Y);\r\n            bindFloor = MathF.Min(bindFloor, pair.Value.Pos.Y);\r\n        }\r\n        var placement = new Vector3(0f, sceneFloor - bindFloor, 0f);\r\n        var world = new XForm[skeleton.Count];\r\n        var definitions = new List\u003CBoneDefinition\u003E();\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n        {\r\n            var bone = skeleton[i];\r\n            var parent = bone.ParentIndex;\r\n            world[i] = binds.TryGetValue(i, out var bind) ? bind\r\n                : parent \u003C 0 ? bone.RestLocal : XForm.Compose(world[parent], bone.RestLocal);\r\n            if (binds.ContainsKey(i))\r\n                world[i].Pos \u002B= placement;\r\n            var local = parent \u003C 0 ? world[i] : XForm.ToLocal(world[parent], world[i]);\r\n            definitions.Add(new BoneDefinition(bone.Name, parent \u003C 0 ? null : skeleton[parent].Name, local));\r\n        }\r\n        return SkeletonModel.Create(definitions);\r\n    }\r\n\r\n    private static Matrix4x4 ReadMatrix(Accessor source, int i) =\u003E new(\r\n        source.Float(i, 0), source.Float(i, 1), source.Float(i, 2), source.Float(i, 3),\r\n        source.Float(i, 4), source.Float(i, 5), source.Float(i, 6), source.Float(i, 7),\r\n        source.Float(i, 8), source.Float(i, 9), source.Float(i, 10), source.Float(i, 11),\r\n        source.Float(i, 12), source.Float(i, 13), source.Float(i, 14), source.Float(i, 15));\r\n\r\n    private static int[] MapSkinJoints(\r\n        GltfDocument document, SkeletonModel skeleton, Dictionary\u003Cstring, int\u003E bonesByName,\r\n        JsonElement skinArray, int skinIndex)\r\n    {\r\n        if (skinIndex \u003C 0)\r\n            return Array.Empty\u003Cint\u003E();\r\n        if (skinArray.ValueKind != JsonValueKind.Array || skinIndex \u003E= skinArray.GetArrayLength())\r\n            throw new FormatException($\u0022glTF mesh references invalid skin {skinIndex}.\u0022);\r\n        var skin = skinArray[skinIndex];\r\n        if (!skin.TryGetProperty(\u0022joints\u0022, out var joints))\r\n            return Array.Empty\u003Cint\u003E();\r\n\r\n        var result = new int[joints.GetArrayLength()];\r\n        for (var i = 0; i \u003C result.Length; i\u002B\u002B)\r\n        {\r\n            var nodeIndex = joints[i].GetInt32();\r\n            if (nodeIndex \u003C 0 || nodeIndex \u003E= document.Nodes.Count)\r\n                throw new FormatException($\u0022glTF skin references invalid joint node {nodeIndex}.\u0022);\r\n            var raw = document.Nodes[nodeIndex].Name ?? $\u0022node_{nodeIndex}\u0022;\r\n            var safe = Sanitize(raw);\r\n            if (!bonesByName.TryGetValue(safe, out var bone)\r\n                \u0026\u0026 !bonesByName.TryGetValue(Sanitize(raw \u002B \u0022#\u0022 \u002B nodeIndex), out bone))\r\n            {\r\n                throw new FormatException(\r\n                    $\u0022glTF skin joint \u0027{raw}\u0027 is absent from the imported target skeleton.\u0022);\r\n            }\r\n            if (bone \u003C 0 || bone \u003E= skeleton.Count)\r\n                throw new FormatException($\u0022glTF skin joint \u0027{raw}\u0027 mapped outside the target skeleton.\u0022);\r\n            result[i] = bone;\r\n        }\r\n        return result;\r\n    }\r\n\r\n    private static void ReadSkinning(\r\n        GltfDocument document, JsonElement attributes, int vertexCount, int[] skinJoints,\r\n        out float[] weights, out int[] joints)\r\n    {\r\n        weights = new float[checked(vertexCount * 4)];\r\n        joints = new int[checked(vertexCount * 4)];\r\n        if (skinJoints.Length == 0\r\n            || !attributes.TryGetProperty(\u0022JOINTS_0\u0022, out var jointProperty)\r\n            || !attributes.TryGetProperty(\u0022WEIGHTS_0\u0022, out var weightProperty))\r\n        {\r\n            for (var i = 0; i \u003C vertexCount; i\u002B\u002B)\r\n            {\r\n                weights[i * 4] = 1f;\r\n                joints[i * 4] = skinJoints.Length \u003E 0 ? skinJoints[0] : 0;\r\n            }\r\n            return;\r\n        }\r\n\r\n        var jointSource = new Accessor(document, jointProperty.GetInt32(), 4);\r\n        var weightSource = new Accessor(document, weightProperty.GetInt32(), 4);\r\n        RequireCount(jointSource, vertexCount, \u0022JOINTS_0\u0022);\r\n        RequireCount(weightSource, vertexCount, \u0022WEIGHTS_0\u0022);\r\n        for (var vertex = 0; vertex \u003C vertexCount; vertex\u002B\u002B)\r\n        {\r\n            var total = 0f;\r\n            for (var influence = 0; influence \u003C 4; influence\u002B\u002B)\r\n            {\r\n                var skinJoint = jointSource.Unsigned(vertex, influence);\r\n                if (skinJoint \u003C 0 || skinJoint \u003E= skinJoints.Length)\r\n                    throw new FormatException($\u0022glTF JOINTS_0 references invalid skin joint {skinJoint}.\u0022);\r\n                var at = vertex * 4 \u002B influence;\r\n                joints[at] = skinJoints[skinJoint];\r\n                var weight = weightSource.Float(vertex, influence);\r\n                weights[at] = float.IsFinite(weight) \u0026\u0026 weight \u003E 0f ? weight : 0f;\r\n                total \u002B= weights[at];\r\n            }\r\n            if (total \u003C= 1e-8f)\r\n            {\r\n                weights[vertex * 4] = 1f;\r\n                joints[vertex * 4] = skinJoints[0];\r\n                continue;\r\n            }\r\n            for (var influence = 0; influence \u003C 4; influence\u002B\u002B)\r\n                weights[vertex * 4 \u002B influence] /= total;\r\n        }\r\n    }\r\n\r\n    private static int[] ReadIndices(\r\n        GltfDocument document, JsonElement primitive, int vertexCount)\r\n    {\r\n        if (!primitive.TryGetProperty(\u0022indices\u0022, out var indexProperty))\r\n        {\r\n            var sequential = new int[vertexCount];\r\n            for (var i = 0; i \u003C sequential.Length; i\u002B\u002B)\r\n                sequential[i] = i;\r\n            return sequential;\r\n        }\r\n\r\n        var source = new Accessor(document, indexProperty.GetInt32(), 1);\r\n        var result = new int[source.Count];\r\n        for (var i = 0; i \u003C result.Length; i\u002B\u002B)\r\n        {\r\n            result[i] = source.Unsigned(i, 0);\r\n            if (result[i] \u003C 0 || result[i] \u003E= vertexCount)\r\n                throw new FormatException($\u0022glTF index {result[i]} exceeds vertex count {vertexCount}.\u0022);\r\n        }\r\n        return result;\r\n    }\r\n\r\n    private static int[] Triangulate(int[] indices, int mode)\r\n    {\r\n        var triangles = new List\u003Cint\u003E();\r\n        if (mode == 4) // TRIANGLES\r\n        {\r\n            if (indices.Length % 3 != 0)\r\n                throw new FormatException(\u0022glTF triangle index count is not divisible by three.\u0022);\r\n            triangles.AddRange(indices);\r\n        }\r\n        else if (mode == 5) // TRIANGLE_STRIP\r\n        {\r\n            for (var i = 2; i \u003C indices.Length; i\u002B\u002B)\r\n            {\r\n                var a = indices[i - 2];\r\n                var b = indices[i - 1];\r\n                var c = indices[i];\r\n                if ((i \u0026 1) != 0)\r\n                    (a, b) = (b, a);\r\n                if (a != b \u0026\u0026 b != c \u0026\u0026 a != c)\r\n                {\r\n                    triangles.Add(a);\r\n                    triangles.Add(b);\r\n                    triangles.Add(c);\r\n                }\r\n            }\r\n        }\r\n        else if (mode == 6) // TRIANGLE_FAN\r\n        {\r\n            for (var i = 2; i \u003C indices.Length; i\u002B\u002B)\r\n            {\r\n                if (indices[0] == indices[i - 1] || indices[i - 1] == indices[i]\r\n                    || indices[0] == indices[i])\r\n                    continue;\r\n                triangles.Add(indices[0]);\r\n                triangles.Add(indices[i - 1]);\r\n                triangles.Add(indices[i]);\r\n            }\r\n        }\r\n        else\r\n        {\r\n            throw new FormatException($\u0022glTF primitive mode {mode} is not a triangle mesh.\u0022);\r\n        }\r\n        return triangles.ToArray();\r\n    }\r\n\r\n    private static void GenerateNormals(Vector3[] positions, int[] triangles, Vector3[] normals)\r\n    {\r\n        for (var i = 0; i \u002B 2 \u003C triangles.Length; i \u002B= 3)\r\n        {\r\n            var a = triangles[i];\r\n            var b = triangles[i \u002B 1];\r\n            var c = triangles[i \u002B 2];\r\n            var normal = Vector3.Cross(positions[b] - positions[a], positions[c] - positions[a]);\r\n            normals[a] \u002B= normal;\r\n            normals[b] \u002B= normal;\r\n            normals[c] \u002B= normal;\r\n        }\r\n        for (var i = 0; i \u003C normals.Length; i\u002B\u002B)\r\n            normals[i] = NormalizeOr(normals[i], Vector3.UnitY);\r\n    }\r\n\r\n    private static Vector3 NormalizeOr(Vector3 value, Vector3 fallback)\r\n        =\u003E value.LengthSquared() \u003E 1e-12f ? Vector3.Normalize(value) : fallback;\r\n\r\n    private static void RequireCount(Accessor accessor, int expected, string semantic)\r\n    {\r\n        if (accessor.Count != expected)\r\n            throw new FormatException(\r\n                $\u0022glTF {semantic} has {accessor.Count} entries; expected {expected}.\u0022);\r\n    }\r\n\r\n    private static string MaterialName(JsonElement materials, JsonElement primitive)\r\n    {\r\n        if (!primitive.TryGetProperty(\u0022material\u0022, out var materialProperty))\r\n            return \u0022default\u0022;\r\n        var index = materialProperty.GetInt32();\r\n        if (materials.ValueKind != JsonValueKind.Array || index \u003C 0 || index \u003E= materials.GetArrayLength())\r\n            throw new FormatException($\u0022glTF primitive references invalid material {index}.\u0022);\r\n        var material = materials[index];\r\n        return material.TryGetProperty(\u0022name\u0022, out var name) \u0026\u0026 !string.IsNullOrEmpty(name.GetString())\r\n            ? name.GetString()!\r\n            : $\u0022material_{index}\u0022;\r\n    }\r\n\r\n    private static string Sanitize(string value)\r\n    {\r\n        var result = new StringBuilder(value.Length);\r\n        foreach (var c in value)\r\n            result.Append(char.IsLetterOrDigit(c) || c == \u0027_\u0027 ? c : \u0027_\u0027);\r\n        return result.Length \u003E 0 ? result.ToString() : \u0022unnamed\u0022;\r\n    }\r\n\r\n    private static string UniqueName(string value, HashSet\u003Cstring\u003E used)\r\n    {\r\n        var candidate = value;\r\n        var suffix = 2;\r\n        while (!used.Add(candidate))\r\n            candidate = value \u002B \u0022_\u0022 \u002B suffix\u002B\u002B;\r\n        return candidate;\r\n    }\r\n\r\n    private sealed class Accessor\r\n    {\r\n        private readonly byte[] _buffer;\r\n        private readonly int _start;\r\n        private readonly int _stride;\r\n        private readonly int _componentSize;\r\n        private readonly int _componentType;\r\n        private readonly bool _normalized;\r\n\r\n        public int Count { get; }\r\n        public int Components { get; }\r\n\r\n        public Accessor(GltfDocument document, int index, int expectedComponents)\r\n        {\r\n            var root = document.Root;\r\n            if (!root.TryGetProperty(\u0022accessors\u0022, out var accessors)\r\n                || index \u003C 0 || index \u003E= accessors.GetArrayLength())\r\n                throw new FormatException($\u0022glTF accessor {index} does not exist.\u0022);\r\n            var accessor = accessors[index];\r\n            if (accessor.TryGetProperty(\u0022sparse\u0022, out _))\r\n                throw new FormatException(\u0022Sparse glTF mesh accessors are not supported.\u0022);\r\n\r\n            Components = accessor.GetProperty(\u0022type\u0022).GetString() switch\r\n            {\r\n                \u0022SCALAR\u0022 =\u003E 1,\r\n                \u0022VEC2\u0022 =\u003E 2,\r\n                \u0022VEC3\u0022 =\u003E 3,\r\n                \u0022VEC4\u0022 =\u003E 4,\r\n                \u0022MAT4\u0022 =\u003E 16,\r\n                var type =\u003E throw new FormatException($\u0022Unsupported glTF accessor type \u0027{type}\u0027.\u0022),\r\n            };\r\n            if (Components != expectedComponents)\r\n                throw new FormatException(\r\n                    $\u0022glTF accessor {index} has {Components} components; expected {expectedComponents}.\u0022);\r\n\r\n            Count = accessor.GetProperty(\u0022count\u0022).GetInt32();\r\n            if (Count \u003C 0)\r\n                throw new FormatException($\u0022glTF accessor {index} has a negative count.\u0022);\r\n            _componentType = accessor.GetProperty(\u0022componentType\u0022).GetInt32();\r\n            _componentSize = _componentType switch\r\n            {\r\n                5120 or 5121 =\u003E 1,\r\n                5122 or 5123 =\u003E 2,\r\n                5125 or 5126 =\u003E 4,\r\n                _ =\u003E throw new FormatException(\r\n                    $\u0022Unsupported glTF accessor component type {_componentType}.\u0022),\r\n            };\r\n            _normalized = accessor.TryGetProperty(\u0022normalized\u0022, out var normalized)\r\n                \u0026\u0026 normalized.GetBoolean();\r\n\r\n            if (!accessor.TryGetProperty(\u0022bufferView\u0022, out var viewProperty))\r\n            {\r\n                _buffer = Array.Empty\u003Cbyte\u003E();\r\n                _start = 0;\r\n                _stride = checked(Components * _componentSize);\r\n                return;\r\n            }\r\n\r\n            var views = root.GetProperty(\u0022bufferViews\u0022);\r\n            var viewIndex = viewProperty.GetInt32();\r\n            if (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            var bufferIndex = view.GetProperty(\u0022buffer\u0022).GetInt32();\r\n            if (bufferIndex \u003C 0 || bufferIndex \u003E= document.Buffers.Count)\r\n                throw new FormatException($\u0022glTF buffer {bufferIndex} does not exist.\u0022);\r\n            _buffer = document.Buffers[bufferIndex];\r\n            var viewOffset = view.TryGetProperty(\u0022byteOffset\u0022, out var vo) ? vo.GetInt32() : 0;\r\n            var accessorOffset = accessor.TryGetProperty(\u0022byteOffset\u0022, out var ao) ? ao.GetInt32() : 0;\r\n            _start = checked(viewOffset \u002B accessorOffset);\r\n            var elementSize = checked(Components * _componentSize);\r\n            _stride = view.TryGetProperty(\u0022byteStride\u0022, out var stride)\r\n                ? stride.GetInt32() : elementSize;\r\n            if (_stride \u003C elementSize)\r\n                throw new FormatException(\u0022glTF accessor stride is smaller than its element.\u0022);\r\n            var end = Count == 0 ? _start : (long)_start \u002B (long)(Count - 1) * _stride \u002B elementSize;\r\n            if (_start \u003C 0 || end \u003E _buffer.Length)\r\n                throw new FormatException($\u0022glTF accessor {index} reads beyond its buffer.\u0022);\r\n        }\r\n\r\n        public float Float(int element, int component)\r\n        {\r\n            if (_buffer.Length == 0)\r\n                return 0f;\r\n            var offset = Offset(element, component);\r\n            return _componentType switch\r\n            {\r\n                5120 =\u003E _normalized\r\n                    ? MathF.Max(unchecked((sbyte)_buffer[offset]) / 127f, -1f)\r\n                    : unchecked((sbyte)_buffer[offset]),\r\n                5121 =\u003E _normalized ? _buffer[offset] / 255f : _buffer[offset],\r\n                5122 =\u003E _normalized\r\n                    ? MathF.Max(BitConverter.ToInt16(_buffer, offset) / 32767f, -1f)\r\n                    : BitConverter.ToInt16(_buffer, offset),\r\n                5123 =\u003E _normalized\r\n                    ? BitConverter.ToUInt16(_buffer, offset) / 65535f\r\n                    : BitConverter.ToUInt16(_buffer, offset),\r\n                5125 =\u003E BitConverter.ToUInt32(_buffer, offset),\r\n                _ =\u003E BitConverter.ToSingle(_buffer, offset),\r\n            };\r\n        }\r\n\r\n        public int Unsigned(int element, int component)\r\n        {\r\n            if (_buffer.Length == 0)\r\n                return 0;\r\n            var offset = Offset(element, component);\r\n            return _componentType switch\r\n            {\r\n                5121 =\u003E _buffer[offset],\r\n                5123 =\u003E BitConverter.ToUInt16(_buffer, offset),\r\n                5125 =\u003E checked((int)BitConverter.ToUInt32(_buffer, offset)),\r\n                _ =\u003E throw new FormatException(\r\n                    $\u0022glTF indices require an unsigned integer accessor, got {_componentType}.\u0022),\r\n            };\r\n        }\r\n\r\n        private int Offset(int element, int component)\r\n        {\r\n            if (element \u003C 0 || element \u003E= Count || component \u003C 0 || component \u003E= Components)\r\n                throw new FormatException(\u0022glTF accessor index is out of range.\u0022);\r\n            return checked(_start \u002B element * _stride \u002B component * _componentSize);\r\n        }\r\n    }\r\n\r\n    private static string Emit(SkeletonModel skeleton, string name, IReadOnlyList\u003CMeshPart\u003E parts)\r\n    {\r\n        var writer = new Kv2Writer();\r\n        var modelId = Id(name, \u0022model\u0022);\r\n        var jointIds = new string[skeleton.Count];\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n            jointIds[i] = Id(name, \u0022joint:\u0022 \u002B skeleton[i].Name);\r\n        var dagIds = new string[parts.Count];\r\n        var meshIds = new string[parts.Count];\r\n        var vertexIds = new string[parts.Count];\r\n        for (var i = 0; i \u003C parts.Count; i\u002B\u002B)\r\n        {\r\n            dagIds[i] = Id(name, $\u0022dag:{i}:{parts[i].Name}\u0022);\r\n            meshIds[i] = Id(name, $\u0022mesh:{i}:{parts[i].Name}\u0022);\r\n            vertexIds[i] = Id(name, $\u0022vertices:{i}:{parts[i].Name}\u0022);\r\n        }\r\n\r\n        writer.Raw(\u0022\u003C!-- dmx encoding keyvalues2_noids 4 format model 22 --\u003E\u0022);\r\n        writer.BeginTop(\u0022DmElement\u0022);\r\n        writer.Attr(\u0022name\u0022, \u0022string\u0022, \u0022root\u0022);\r\n        writer.Attr(\u0022model\u0022, \u0022element\u0022, modelId);\r\n        writer.Attr(\u0022skeleton\u0022, \u0022element\u0022, modelId);\r\n        writer.EndTop();\r\n\r\n        writer.BeginTop(\u0022DmeModel\u0022);\r\n        writer.Attr(\u0022id\u0022, \u0022elementid\u0022, modelId);\r\n        writer.Attr(\u0022name\u0022, \u0022string\u0022, name);\r\n        WriteTransform(writer, \u0022transform\u0022, Vector3.Zero, Quaternion.Identity);\r\n        writer.Attr(\u0022visible\u0022, \u0022bool\u0022, \u00221\u0022);\r\n        var children = new List\u003Cstring\u003E();\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n            if (skeleton[i].ParentIndex \u003C 0)\r\n                children.Add(jointIds[i]);\r\n        children.AddRange(dagIds);\r\n        WriteRefs(writer, \u0022children\u0022, children);\r\n        WriteRefs(writer, \u0022jointList\u0022, jointIds);\r\n        writer.Attr(\u0022upAxis\u0022, \u0022string\u0022, \u0022Y\u0022);\r\n        writer.BeginInline(\u0022axisSystem\u0022, \u0022DmeAxisSystem\u0022);\r\n        writer.Attr(\u0022upAxis\u0022, \u0022int\u0022, \u00222\u0022);\r\n        writer.Attr(\u0022forwardParity\u0022, \u0022int\u0022, \u00222\u0022);\r\n        writer.Attr(\u0022coordSys\u0022, \u0022int\u0022, \u00220\u0022);\r\n        writer.EndInline();\r\n        writer.EndTop();\r\n\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n        {\r\n            var bone = skeleton[i];\r\n            writer.BeginTop(\u0022DmeJoint\u0022);\r\n            writer.Attr(\u0022id\u0022, \u0022elementid\u0022, jointIds[i]);\r\n            writer.Attr(\u0022name\u0022, \u0022string\u0022, bone.Name);\r\n            WriteTransform(writer, \u0022transform\u0022, bone.RestLocal.Pos, bone.RestLocal.Rot);\r\n            writer.Attr(\u0022visible\u0022, \u0022bool\u0022, \u00221\u0022);\r\n            var boneChildren = new List\u003Cstring\u003E();\r\n            for (var child = 0; child \u003C skeleton.Count; child\u002B\u002B)\r\n                if (skeleton[child].ParentIndex == i)\r\n                    boneChildren.Add(jointIds[child]);\r\n            if (boneChildren.Count \u003E 0)\r\n                WriteRefs(writer, \u0022children\u0022, boneChildren);\r\n            writer.EndTop();\r\n        }\r\n\r\n        for (var i = 0; i \u003C parts.Count; i\u002B\u002B)\r\n        {\r\n            var part = parts[i];\r\n            writer.BeginTop(\u0022DmeDag\u0022);\r\n            writer.Attr(\u0022id\u0022, \u0022elementid\u0022, dagIds[i]);\r\n            writer.Attr(\u0022name\u0022, \u0022string\u0022, part.Name);\r\n            WriteTransform(writer, \u0022transform\u0022, Vector3.Zero, Quaternion.Identity);\r\n            writer.Attr(\u0022shape\u0022, \u0022element\u0022, meshIds[i]);\r\n            writer.Attr(\u0022visible\u0022, \u0022bool\u0022, \u00221\u0022);\r\n            writer.EndTop();\r\n\r\n            writer.BeginTop(\u0022DmeMesh\u0022);\r\n            writer.Attr(\u0022id\u0022, \u0022elementid\u0022, meshIds[i]);\r\n            writer.Attr(\u0022name\u0022, \u0022string\u0022, part.Name);\r\n            writer.Attr(\u0022visible\u0022, \u0022bool\u0022, \u00221\u0022);\r\n            writer.Attr(\u0022currentState\u0022, \u0022element\u0022, vertexIds[i]);\r\n            WriteRefs(writer, \u0022baseStates\u0022, new[] { vertexIds[i] });\r\n            writer.BeginArray(\u0022faceSets\u0022);\r\n            writer.BeginArrayElement(\u0022DmeFaceSet\u0022);\r\n            writer.Attr(\u0022name\u0022, \u0022string\u0022, part.Material);\r\n            writer.BeginArray(\u0022faces\u0022, \u0022int_array\u0022);\r\n            for (var index = 0; index \u003C part.Triangles.Length; index\u002B\u002B)\r\n            {\r\n                writer.Value(part.Triangles[index].ToString(CultureInfo.InvariantCulture), false);\r\n                if (index % 3 == 2)\r\n                    writer.Value(\u0022-1\u0022, index == part.Triangles.Length - 1);\r\n            }\r\n            writer.EndArray();\r\n            writer.BeginInline(\u0022material\u0022, \u0022DmeMaterial\u0022);\r\n            writer.Attr(\u0022name\u0022, \u0022string\u0022, part.Material);\r\n            writer.Attr(\u0022mtlName\u0022, \u0022string\u0022, part.Material);\r\n            writer.EndInline();\r\n            writer.EndArrayElement(true);\r\n            writer.EndArray();\r\n            writer.EndTop();\r\n\r\n            writer.BeginTop(\u0022DmeVertexData\u0022);\r\n            writer.Attr(\u0022id\u0022, \u0022elementid\u0022, vertexIds[i]);\r\n            writer.Attr(\u0022name\u0022, \u0022string\u0022, \u0022bind\u0022);\r\n            writer.BeginArray(\u0022vertexFormat\u0022, \u0022string_array\u0022);\r\n            var formats = new[]\r\n                { \u0022position$0\u0022, \u0022normal$0\u0022, \u0022texcoord$0\u0022, \u0022blendweights$0\u0022, \u0022blendindices$0\u0022 };\r\n            for (var format = 0; format \u003C formats.Length; format\u002B\u002B)\r\n                writer.Value(formats[format], format == formats.Length - 1);\r\n            writer.EndArray();\r\n            writer.Attr(\u0022jointCount\u0022, \u0022int\u0022, \u00224\u0022);\r\n            writer.Attr(\u0022flipVCoordinates\u0022, \u0022bool\u0022, \u00220\u0022);\r\n            WriteVectors(writer, \u0022position$0\u0022, \u0022vector3_array\u0022, part.Positions,\r\n                value =\u003E Vec(value));\r\n            WriteIdentityIndices(writer, \u0022position$0Indices\u0022, part.Positions.Length);\r\n            WriteVectors(writer, \u0022normal$0\u0022, \u0022vector3_array\u0022, part.Normals,\r\n                value =\u003E Vec(value));\r\n            WriteIdentityIndices(writer, \u0022normal$0Indices\u0022, part.Normals.Length);\r\n            WriteVectors(writer, \u0022texcoord$0\u0022, \u0022vector2_array\u0022, part.TexCoords,\r\n                value =\u003E $\u0022{F(value.X)} {F(value.Y)}\u0022);\r\n            WriteIdentityIndices(writer, \u0022texcoord$0Indices\u0022, part.TexCoords.Length);\r\n            WriteScalars(writer, \u0022blendweights$0\u0022, \u0022float_array\u0022, part.Weights,\r\n                value =\u003E F(value));\r\n            WriteScalars(writer, \u0022blendindices$0\u0022, \u0022int_array\u0022, part.Joints,\r\n                value =\u003E value.ToString(CultureInfo.InvariantCulture));\r\n            writer.EndTop();\r\n        }\r\n        return writer.ToString();\r\n    }\r\n\r\n    private static void WriteTransform(\r\n        Kv2Writer writer, string name, Vector3 position, Quaternion orientation)\r\n    {\r\n        writer.BeginInline(name, \u0022DmeTransform\u0022);\r\n        writer.Attr(\u0022name\u0022, \u0022string\u0022, name);\r\n        writer.Attr(\u0022position\u0022, \u0022vector3\u0022, Vec(position));\r\n        writer.Attr(\u0022orientation\u0022, \u0022quaternion\u0022,\r\n            $\u0022{F(orientation.X)} {F(orientation.Y)} {F(orientation.Z)} {F(orientation.W)}\u0022);\r\n        writer.Attr(\u0022scale\u0022, \u0022float\u0022, \u00221\u0022);\r\n        writer.EndInline();\r\n    }\r\n\r\n    private static void WriteRefs(Kv2Writer writer, string name, IReadOnlyList\u003Cstring\u003E ids)\r\n    {\r\n        writer.BeginArray(name);\r\n        for (var i = 0; i \u003C ids.Count; i\u002B\u002B)\r\n            writer.ElementRef(ids[i], i == ids.Count - 1);\r\n        writer.EndArray();\r\n    }\r\n\r\n    private static void WriteVectors\u003CT\u003E(\r\n        Kv2Writer writer, string name, string type, T[] values, Func\u003CT, string\u003E format)\r\n    {\r\n        writer.BeginArray(name, type);\r\n        for (var i = 0; i \u003C values.Length; i\u002B\u002B)\r\n            writer.Value(format(values[i]), i == values.Length - 1);\r\n        writer.EndArray();\r\n    }\r\n\r\n    private static void WriteScalars\u003CT\u003E(\r\n        Kv2Writer writer, string name, string type, T[] values, Func\u003CT, string\u003E format)\r\n        =\u003E WriteVectors(writer, name, type, values, format);\r\n\r\n    private static void WriteIdentityIndices(Kv2Writer writer, string name, int count)\r\n    {\r\n        writer.BeginArray(name, \u0022int_array\u0022);\r\n        for (var i = 0; i \u003C count; i\u002B\u002B)\r\n            writer.Value(i.ToString(CultureInfo.InvariantCulture), i == count - 1);\r\n        writer.EndArray();\r\n    }\r\n\r\n    private static string Id(string name, string path)\r\n        =\u003E DmxWriter.ElementGuid(name, \u0022gltf-model:\u0022 \u002B path)\r\n            .ToString(\u0022D\u0022, CultureInfo.InvariantCulture);\r\n\r\n    private static string F(float value)\r\n        =\u003E value == 0f ? \u00220\u0022 : ((double)value).ToString(\u00220.##########\u0022, CultureInfo.InvariantCulture);\r\n\r\n    private static string Vec(Vector3 value) =\u003E $\u0022{F(value.X)} {F(value.Y)} {F(value.Z)}\u0022;\r\n\r\n    private sealed class Kv2Writer\r\n    {\r\n        private readonly StringBuilder _text = new();\r\n        private int _indent;\r\n\r\n        public void Raw(string value) =\u003E _text.Append(value).Append(\u0022\\r\\n\u0022);\r\n\r\n        private void Line(string value)\r\n            =\u003E _text.Append(\u0027\\t\u0027, _indent).Append(value).Append(\u0022\\r\\n\u0022);\r\n\r\n        public void Attr(string name, string type, string value)\r\n            =\u003E Line($\u0022\\\u0022{Escape(name)}\\\u0022 \\\u0022{type}\\\u0022 \\\u0022{Escape(value)}\\\u0022\u0022);\r\n\r\n        public void BeginTop(string type)\r\n        {\r\n            Line($\u0022\\\u0022{type}\\\u0022\u0022);\r\n            Line(\u0022{\u0022);\r\n            _indent\u002B\u002B;\r\n        }\r\n\r\n        public void EndTop()\r\n        {\r\n            _indent--;\r\n            Line(\u0022}\u0022);\r\n            _text.Append(\u0022\\r\\n\u0022);\r\n        }\r\n\r\n        public void BeginInline(string name, string type)\r\n        {\r\n            Line($\u0022\\\u0022{Escape(name)}\\\u0022 \\\u0022{type}\\\u0022\u0022);\r\n            Line(\u0022{\u0022);\r\n            _indent\u002B\u002B;\r\n        }\r\n\r\n        public void EndInline()\r\n        {\r\n            _indent--;\r\n            Line(\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{Escape(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 BeginArrayElement(string type)\r\n        {\r\n            Line($\u0022\\\u0022{type}\\\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 ElementRef(string id, bool last)\r\n            =\u003E Line($\u0022\\\u0022element\\\u0022 \\\u0022{id}\\\u0022\u0022 \u002B (last ? \u0022\u0022 : \u0022,\u0022));\r\n\r\n        public void Value(string value, bool last)\r\n            =\u003E Line($\u0022\\\u0022{Escape(value)}\\\u0022\u0022 \u002B (last ? \u0022\u0022 : \u0022,\u0022));\r\n\r\n        private static string Escape(string value)\r\n            =\u003E value.Replace(\u0022\\\\\u0022, \u0022\\\\\\\\\u0022).Replace(\u0022\\\u0022\u0022, \u0022\\\\\\\u0022\u0022)\r\n                .Replace(\u0022\\r\u0022, \u0022 \u0022).Replace(\u0022\\n\u0022, \u0022 \u0022);\r\n\r\n        public override string ToString() =\u003E _text.ToString();\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"HumanoidMocap/Formats/Renderware/RwDffSkeleton.cs","FileName":"RwDffSkeleton.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":395388,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing System.Text;\r\nusing HumanoidMocap.Maths;\r\n\r\nnamespace HumanoidMocap.Formats.Renderware;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/HumanoidMocap/Assembly.cs)\r\n\r\n/// \u003Csummary\u003EOne HAnim node of a parsed .dff skeleton, in HAnim node order \u2014 the order\r\n/// RwAnimAnimation keyframes address nodes in.\u003C/summary\u003E\r\npublic sealed class RwDffNode\r\n{\r\n    /// \u003Csummary\u003EHAnim node id (stable across FSB2 characters, e.g. 1000 = \u0022Bip01\u0022).\u003C/summary\u003E\r\n    public required int NodeId { get; init; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Bone name: the frame\u0027s authored name when the .dff carries one (FSB2 stores 3ds Max\r\n    /// Biped names like \u003Cc\u003EBip01 L Thigh\u003C/c\u003E in the RpUserData extension), else the\r\n    /// synthesized stable fallback \u003Cc\u003Erw_node_\u0026lt;id\u0026gt;\u003C/c\u003E. Unique within the skeleton.\r\n    /// \u003C/summary\u003E\r\n    public required string Name { get; init; }\r\n\r\n    /// \u003Csummary\u003EParent NODE index (into the node list), or -1 for the root node.\u003C/summary\u003E\r\n    public required int ParentIndex { get; init; }\r\n\r\n    /// \u003Csummary\u003ERest (bind) transform relative to the parent NODE (intermediate non-HAnim\r\n    /// frames composed in), native .dff units/axes.\u003C/summary\u003E\r\n    public required XForm RestLocal { get; init; }\r\n\r\n    /// \u003Csummary\u003EHAnim PUSH/POP hierarchy flags from the node table (diagnostic).\u003C/summary\u003E\r\n    public required uint Flags { get; init; }\r\n}\r\n\r\n/// \u003Csummary\u003EResult of parsing a .dff model\u0027s skeleton.\u003C/summary\u003E\r\npublic sealed class RwDffSkeletonData\r\n{\r\n    /// \u003Csummary\u003EHAnim nodes in node-index order (== animation keyframe node order).\u003C/summary\u003E\r\n    public required IReadOnlyList\u003CRwDffNode\u003E Nodes { get; init; }\r\n\r\n    /// \u003Csummary\u003ETotal FrameList frame count (HAnim and non-HAnim frames alike).\u003C/summary\u003E\r\n    public required int FrameCount { get; init; }\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// RenderWare .dff model skeleton parser \u2014 reads ONLY the Clump\u0027s FrameList and the RpHAnim\r\n/// plugin data (geometry is skipped entirely).\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003ELayout\u003C/b\u003E (verified against FSB2 \u003Cc\u003Echaracter.pak\u003C/c\u003E models): Clump (0x10) \u2192\r\n/// struct (0x1) \u2192 FrameList (0xE) \u2192 struct (0x1) = {u32 numFrames, numFrames \u00D7 56-byte\r\n/// frames {f32 rot[9] row-major 3x3, f32 pos[3], i32 parentIndex, u32 flags}}, then one\r\n/// Extension (0x3) chunk PER frame containing optional sub-chunks: 0x11E = HAnimPLG\r\n/// {u32 version, u32 nodeId, u32 numNodes, [u32 flags, u32 keyFrameSize,\r\n/// numNodes \u00D7 {u32 nodeId, u32 nodeIndex, u32 nodeFlags}]} (exactly one frame owns the full\r\n/// node table), 0x11F = RpUserData (FSB2 stores the real 3ds Max bone name under the\r\n/// \u003Cc\u003Ename\u003C/c\u003E attribute \u2014 the classic frame-name chunk 0x253F2FE is present but empty).\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003ERotation matrices\u003C/b\u003E are row-major with rows = the frame\u0027s basis vectors\r\n/// (RenderWare right/up/at), i.e. row-vector convention \u003Cc\u003Ev_parent = v_child \u00B7 M\u003C/c\u003E \u2014\r\n/// the same convention as \u003Csee cref=\u0022Matrix4x4\u0022/\u003E, so\r\n/// \u003Csee cref=\u0022Quaternion.CreateFromRotationMatrix\u0022/\u003E converts directly (verified: FK over\r\n/// the FSB2 rig lands the feet at ground level and the head at ~184 cm).\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003ENode order and parents\u003C/b\u003E: the HAnim node table order IS the animation\r\n/// keyframe node order. Each frame\u0027s own HAnimPLG carries its nodeId; a node\u0027s parent is\r\n/// the nearest ancestor FRAME that is itself an HAnim node (intermediate plain frames \u2014\r\n/// FSB2 has one root dummy \u2014 are composed into the node\u0027s rest transform).\u003C/para\u003E\r\n/// \u003C/remarks\u003E\r\npublic static class RwDffSkeleton\r\n{\r\n    /// \u003Csummary\u003EParses the skeleton (FrameList \u002B HAnim) out of .dff bytes.\u003C/summary\u003E\r\n    /// \u003Cexception cref=\u0022FormatException\u0022\u003EMalformed/truncated stream, or no HAnim data.\u003C/exception\u003E\r\n    public static RwDffSkeletonData Parse(byte[] data)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(data);\r\n\r\n        var (rootType, clumpStart, clumpSize) = RwStream.ReadChunk(data, 0, data.Length);\r\n        if (rootType != RwStream.ChunkClump)\r\n            throw new FormatException(\r\n                $\u0022Not a RenderWare model (.dff): expected a Clump chunk (0x10), found 0x{rootType:X}.\u0022);\r\n        var clumpEnd = clumpStart \u002B clumpSize;\r\n\r\n        // Find the FrameList inside the clump (the clump struct precedes it; geometry\r\n        // lists follow and are never visited \u2014 we stop at the first FrameList).\r\n        var offset = clumpStart;\r\n        while (offset \u003C clumpEnd)\r\n        {\r\n            var (type, payloadStart, payloadSize) = RwStream.ReadChunk(data, offset, clumpEnd);\r\n            if (type == RwStream.ChunkFrameList)\r\n                return ParseFrameList(data, payloadStart, payloadStart \u002B payloadSize);\r\n            offset = payloadStart \u002B payloadSize;\r\n        }\r\n        throw new FormatException(\u0022RenderWare .dff has no FrameList chunk \u2014 cannot read a skeleton.\u0022);\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Cheap probe used by skeleton resolution: the HAnim node count of .dff bytes, or null\r\n    /// when the bytes are not a parseable .dff with HAnim data. Never throws.\r\n    /// \u003C/summary\u003E\r\n    public static int? PeekNodeCount(byte[] data)\r\n    {\r\n        try\r\n        {\r\n            return Parse(data).Nodes.Count;\r\n        }\r\n        catch (FormatException)\r\n        {\r\n            return null;\r\n        }\r\n    }\r\n\r\n    // ================================================================ frame list\r\n\r\n    private sealed class Frame\r\n    {\r\n        public required XForm Local;\r\n        public required int Parent;\r\n        public int NodeId = -1;         // HAnim node id, -1 when the frame has no HAnimPLG\r\n        public string Name = \u0022\u0022;\r\n    }\r\n\r\n    private static RwDffSkeletonData ParseFrameList(byte[] data, int start, int end)\r\n    {\r\n        var (structType, structStart, structSize) = RwStream.ReadChunk(data, start, end);\r\n        if (structType != RwStream.ChunkStruct)\r\n            throw new FormatException(\u0022RenderWare FrameList: expected leading struct chunk.\u0022);\r\n\r\n        var frameCount = RwStream.I32(data, structStart);\r\n        if (frameCount \u003C= 0 || structSize \u003C 4 \u002B frameCount * 56)\r\n            throw new FormatException($\u0022RenderWare FrameList declares invalid frame count {frameCount}.\u0022);\r\n\r\n        var frames = new List\u003CFrame\u003E(frameCount);\r\n        for (var i = 0; i \u003C frameCount; i\u002B\u002B)\r\n        {\r\n            var p = structStart \u002B 4 \u002B i * 56;\r\n            var local = ReadFrameTransform(data, p);\r\n            var parent = RwStream.I32(data, p \u002B 48);\r\n            if (parent \u003E= i || parent \u003C -1)\r\n                throw new FormatException(\r\n                    $\u0022RenderWare FrameList: frame {i} has invalid parent index {parent}.\u0022);\r\n            frames.Add(new Frame { Local = local, Parent = parent });\r\n        }\r\n\r\n        // One Extension chunk per frame, in frame order.\r\n        (int NodeId, int NodeIndex, uint Flags)[]? nodeTable = null;\r\n        var offset = structStart \u002B structSize;\r\n        for (var i = 0; i \u003C frameCount; i\u002B\u002B)\r\n        {\r\n            var (extType, extStart, extSize) = RwStream.ReadChunk(data, offset, end);\r\n            if (extType != RwStream.ChunkExtension)\r\n                throw new FormatException(\r\n                    $\u0022RenderWare FrameList: expected Extension chunk for frame {i}, found 0x{extType:X}.\u0022);\r\n            ParseFrameExtension(data, extStart, extStart \u002B extSize, frames[i], ref nodeTable);\r\n            offset = extStart \u002B extSize;\r\n        }\r\n\r\n        if (nodeTable is null)\r\n            throw new FormatException(\r\n                \u0022RenderWare .dff has no HAnim node table \u2014 the model carries no animatable skeleton.\u0022);\r\n\r\n        return BuildNodes(frames, nodeTable, frameCount);\r\n    }\r\n\r\n    private static XForm ReadFrameTransform(byte[] data, int p)\r\n    {\r\n        // Row-major 3x3, rows = basis vectors (row-vector convention, see class remarks).\r\n        var m = new Matrix4x4(\r\n            RwStream.F32(data, p \u002B 0), RwStream.F32(data, p \u002B 4), RwStream.F32(data, p \u002B 8), 0f,\r\n            RwStream.F32(data, p \u002B 12), RwStream.F32(data, p \u002B 16), RwStream.F32(data, p \u002B 20), 0f,\r\n            RwStream.F32(data, p \u002B 24), RwStream.F32(data, p \u002B 28), RwStream.F32(data, p \u002B 32), 0f,\r\n            0f, 0f, 0f, 1f);\r\n        var pos = new Vector3(\r\n            RwStream.F32(data, p \u002B 36), RwStream.F32(data, p \u002B 40), RwStream.F32(data, p \u002B 44));\r\n        if (!float.IsFinite(pos.X) || !float.IsFinite(pos.Y) || !float.IsFinite(pos.Z))\r\n            throw new FormatException(\u0022RenderWare FrameList: non-finite frame translation.\u0022);\r\n        var rot = MathQ.Normalize(Quaternion.CreateFromRotationMatrix(m));\r\n        if (!float.IsFinite(rot.X) || !float.IsFinite(rot.Y) || !float.IsFinite(rot.Z) || !float.IsFinite(rot.W))\r\n            throw new FormatException(\u0022RenderWare FrameList: non-finite frame rotation.\u0022);\r\n        return new XForm(pos, rot);\r\n    }\r\n\r\n    // ================================================================ extensions\r\n\r\n    private static void ParseFrameExtension(\r\n        byte[] data, int start, int end, Frame frame,\r\n        ref (int NodeId, int NodeIndex, uint Flags)[]? nodeTable)\r\n    {\r\n        var offset = start;\r\n        while (offset \u003C end)\r\n        {\r\n            var (type, payloadStart, payloadSize) = RwStream.ReadChunk(data, offset, end);\r\n            switch (type)\r\n            {\r\n                case RwStream.ChunkHAnimPlg:\r\n                    ParseHAnim(data, payloadStart, payloadSize, frame, ref nodeTable);\r\n                    break;\r\n                case RwStream.ChunkUserDataPlg:\r\n                    var userName = ReadUserDataName(data, payloadStart, payloadStart \u002B payloadSize);\r\n                    if (!string.IsNullOrEmpty(userName))\r\n                        frame.Name = userName;\r\n                    break;\r\n                case RwStream.ChunkFrameName:\r\n                    // Classic frame-name string chunk. FSB2 leaves these empty (the real\r\n                    // names live in RpUserData) but honor them when present, without\r\n                    // overriding an already-found user-data name.\r\n                    if (frame.Name.Length == 0 \u0026\u0026 payloadSize \u003E 0)\r\n                        frame.Name = ReadCString(data, payloadStart, payloadSize);\r\n                    break;\r\n            }\r\n            offset = payloadStart \u002B payloadSize;\r\n        }\r\n    }\r\n\r\n    private static void ParseHAnim(\r\n        byte[] data, int start, int size, Frame frame,\r\n        ref (int NodeId, int NodeIndex, uint Flags)[]? nodeTable)\r\n    {\r\n        if (size \u003C 12)\r\n            throw new FormatException(\u0022RenderWare HAnimPLG chunk is too small.\u0022);\r\n        frame.NodeId = RwStream.I32(data, start \u002B 4);\r\n        var numNodes = RwStream.I32(data, start \u002B 8);\r\n        if (numNodes \u003C= 0)\r\n            return;\r\n        if (size \u003C 20 \u002B numNodes * 12)\r\n            throw new FormatException(\r\n                $\u0022RenderWare HAnimPLG node table truncated (numNodes={numNodes}, size={size}).\u0022);\r\n        if (nodeTable is not null)\r\n            throw new FormatException(\u0022RenderWare .dff carries more than one HAnim node table.\u0022);\r\n\r\n        nodeTable = new (int, int, uint)[numNodes];\r\n        for (var i = 0; i \u003C numNodes; i\u002B\u002B)\r\n        {\r\n            var p = start \u002B 20 \u002B i * 12;\r\n            nodeTable[i] = (RwStream.I32(data, p), RwStream.I32(data, p \u002B 4), RwStream.U32(data, p \u002B 8));\r\n        }\r\n    }\r\n\r\n    /// \u003Csummary\u003ERpUserData: {u32 numAttrs, per attr {u32 nameLen, name, u32 format,\r\n    /// u32 count, elements}}; format 3 = string elements {u32 len, chars}. Returns the\r\n    /// first string value of the attribute named \u003Cc\u003Ename\u003C/c\u003E, or \u0022\u0022.\u003C/summary\u003E\r\n    private static string ReadUserDataName(byte[] data, int start, int end)\r\n    {\r\n        if (end - start \u003C 4)\r\n            return \u0022\u0022;\r\n        var attrCount = RwStream.I32(data, start);\r\n        var offset = start \u002B 4;\r\n        for (var a = 0; a \u003C attrCount; a\u002B\u002B)\r\n        {\r\n            if (offset \u002B 4 \u003E end)\r\n                return \u0022\u0022;\r\n            var nameLen = RwStream.I32(data, offset);\r\n            offset \u002B= 4;\r\n            if (nameLen \u003C 0 || offset \u002B nameLen \u003E end)\r\n                return \u0022\u0022;\r\n            var attrName = ReadCString(data, offset, nameLen);\r\n            offset \u002B= nameLen;\r\n            if (offset \u002B 8 \u003E end)\r\n                return \u0022\u0022;\r\n            var format = RwStream.I32(data, offset);\r\n            var elementCount = RwStream.I32(data, offset \u002B 4);\r\n            offset \u002B= 8;\r\n            for (var e = 0; e \u003C elementCount; e\u002B\u002B)\r\n            {\r\n                switch (format)\r\n                {\r\n                    case 1: // int\r\n                    case 2: // float\r\n                        offset \u002B= 4;\r\n                        break;\r\n                    case 3: // string\r\n                        if (offset \u002B 4 \u003E end)\r\n                            return \u0022\u0022;\r\n                        var len = RwStream.I32(data, offset);\r\n                        offset \u002B= 4;\r\n                        if (len \u003C 0 || offset \u002B len \u003E end)\r\n                            return \u0022\u0022;\r\n                        if (attrName == \u0022name\u0022)\r\n                            return ReadCString(data, offset, len);\r\n                        offset \u002B= len;\r\n                        break;\r\n                    default:\r\n                        return \u0022\u0022; // unknown element format \u2014 cannot skip safely\r\n                }\r\n            }\r\n        }\r\n        return \u0022\u0022;\r\n    }\r\n\r\n    private static string ReadCString(byte[] data, int start, int maxLen)\r\n    {\r\n        var len = 0;\r\n        while (len \u003C maxLen \u0026\u0026 data[start \u002B len] != 0)\r\n            len\u002B\u002B;\r\n        return Encoding.ASCII.GetString(data, start, len);\r\n    }\r\n\r\n    // ================================================================ node building\r\n\r\n    private static RwDffSkeletonData BuildNodes(\r\n        List\u003CFrame\u003E frames, (int NodeId, int NodeIndex, uint Flags)[] nodeTable, int frameCount)\r\n    {\r\n        // frame index by node id (each HAnim frame carries its own node id).\r\n        var frameByNodeId = new Dictionary\u003Cint, int\u003E(frames.Count);\r\n        for (var i = 0; i \u003C frames.Count; i\u002B\u002B)\r\n        {\r\n            if (frames[i].NodeId \u003E= 0 \u0026\u0026 !frameByNodeId.TryAdd(frames[i].NodeId, i))\r\n                throw new FormatException(\r\n                    $\u0022RenderWare .dff: duplicate HAnim node id {frames[i].NodeId}.\u0022);\r\n        }\r\n\r\n        // The table\u0027s nodeIndex is the animation keyframe order \u2014 order by it.\r\n        var ordered = new (int NodeId, uint Flags)[nodeTable.Length];\r\n        var seen = new bool[nodeTable.Length];\r\n        foreach (var (nodeId, nodeIndex, flags) in nodeTable)\r\n        {\r\n            if (nodeIndex \u003C 0 || nodeIndex \u003E= nodeTable.Length || seen[nodeIndex])\r\n                throw new FormatException(\r\n                    $\u0022RenderWare HAnim node table has invalid/duplicate node index {nodeIndex}.\u0022);\r\n            seen[nodeIndex] = true;\r\n            ordered[nodeIndex] = (nodeId, flags);\r\n        }\r\n\r\n        var nodeIndexByFrame = new Dictionary\u003Cint, int\u003E(nodeTable.Length);\r\n        for (var n = 0; n \u003C ordered.Length; n\u002B\u002B)\r\n        {\r\n            if (!frameByNodeId.TryGetValue(ordered[n].NodeId, out var frameIndex))\r\n                throw new FormatException(\r\n                    $\u0022RenderWare HAnim node id {ordered[n].NodeId} has no matching frame.\u0022);\r\n            nodeIndexByFrame[frameIndex] = n;\r\n        }\r\n\r\n        var usedNames = new HashSet\u003Cstring\u003E(StringComparer.Ordinal);\r\n        var nodes = new RwDffNode[ordered.Length];\r\n        for (var n = 0; n \u003C ordered.Length; n\u002B\u002B)\r\n        {\r\n            var frameIndex = frameByNodeId[ordered[n].NodeId];\r\n\r\n            // Parent = nearest ancestor frame that is itself an HAnim node; plain frames\r\n            // in between are composed into the rest transform (world = parent \u2218 local).\r\n            var local = frames[frameIndex].Local;\r\n            var parentFrame = frames[frameIndex].Parent;\r\n            var parentNode = -1;\r\n            while (parentFrame \u003E= 0)\r\n            {\r\n                if (nodeIndexByFrame.TryGetValue(parentFrame, out var pn))\r\n                {\r\n                    parentNode = pn;\r\n                    break;\r\n                }\r\n                local = XForm.Compose(frames[parentFrame].Local, local);\r\n                parentFrame = frames[parentFrame].Parent;\r\n            }\r\n            if (parentNode \u003E= n \u0026\u0026 parentNode != -1)\r\n                throw new FormatException(\r\n                    $\u0022RenderWare HAnim node order is not parent-first (node {n} has parent node {parentNode}).\u0022);\r\n\r\n            var name = frames[frameIndex].Name;\r\n            if (string.IsNullOrEmpty(name))\r\n                name = $\u0022rw_node_{ordered[n].NodeId}\u0022;\r\n            name = UniqueName(name, usedNames);\r\n\r\n            nodes[n] = new RwDffNode\r\n            {\r\n                NodeId = ordered[n].NodeId,\r\n                Name = name,\r\n                ParentIndex = parentNode,\r\n                RestLocal = local,\r\n                Flags = ordered[n].Flags,\r\n            };\r\n        }\r\n\r\n        return new RwDffSkeletonData { Nodes = nodes, FrameCount = frameCount };\r\n    }\r\n\r\n    private static string UniqueName(string name, HashSet\u003Cstring\u003E usedNames)\r\n    {\r\n        if (usedNames.Add(name))\r\n            return name;\r\n        for (var i = 2; ; i\u002B\u002B)\r\n        {\r\n            var candidate = $\u0022{name}#{i}\u0022;\r\n            if (usedNames.Add(candidate))\r\n                return candidate;\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"HumanoidMocap/Inference/PalmDetectionFilter.cs","FileName":"PalmDetectionFilter.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":395388,"IsPrivate":false,"Code":"// Weighted suppression adapted from MediaPipe\u0027s NonMaxSuppressionCalculator.\r\n// Copyright 2019 The MediaPipe Authors. Licensed under Apache-2.0.\r\n// See Editor/HumanoidMocap/Inference/MediaPipe.LICENSE and THIRD_PARTY_NOTICES.md.\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Numerics;\r\n\r\nnamespace HumanoidMocap.Inference;\r\nusing Vector2 = System.Numerics.Vector2;\r\n\r\npublic sealed record PalmDetection(float Score,float X,float Y,float Width,float Height,Vector2[] Points);\r\n\r\npublic static class PalmDetectionFilter\r\n{\r\n    /// \u003Csummary\u003EMerge boxes and keypoints by detection score. Each cluster is\r\n    /// compared with its highest-scoring original box, not its moving average.\r\n    /// The seed score is retained; averaging does not create a confidence score.\u003C/summary\u003E\r\n    public static List\u003CPalmDetection\u003E Merge(IEnumerable\u003CPalmDetection\u003E candidates,int limit=2,float threshold=.3f)\r\n    {\r\n        var remaining=candidates.Where(p=\u003Efloat.IsFinite(p.Score\u002Bp.X\u002Bp.Y\u002Bp.Width\u002Bp.Height)\r\n            \u0026\u0026p.Score\u003E0\u0026\u0026p.Width\u003E0\u0026\u0026p.Height\u003E0\u0026\u0026p.Points.All(v=\u003Efloat.IsFinite(v.X\u002Bv.Y)))\r\n            .OrderByDescending(p=\u003Ep.Score).ToList();\r\n        var result=new List\u003CPalmDetection\u003E();\r\n        while(remaining.Count\u003E0\u0026\u0026result.Count\u003Climit)\r\n        {\r\n            var seed=remaining[0];var rest=new List\u003CPalmDetection\u003E();\r\n            var points=new Vector2[seed.Points.Length];float weight=0,xmin=0,ymin=0,xmax=0,ymax=0;\r\n            foreach(var candidate in remaining)\r\n            {\r\n                if(Iou(seed,candidate)\u003C=threshold){rest.Add(candidate);continue;}\r\n                if(candidate.Points.Length!=points.Length)throw new ArgumentException(\u0022Palm keypoint counts differ.\u0022);\r\n                var score=candidate.Score;weight\u002B=score;\r\n                xmin\u002B=(candidate.X-candidate.Width/2)*score;ymin\u002B=(candidate.Y-candidate.Height/2)*score;\r\n                xmax\u002B=(candidate.X\u002Bcandidate.Width/2)*score;ymax\u002B=(candidate.Y\u002Bcandidate.Height/2)*score;\r\n                for(var i=0;i\u003Cpoints.Length;i\u002B\u002B)points[i]\u002B=candidate.Points[i]*score;\r\n            }\r\n            if(weight\u003C=0)break;\r\n            xmin/=weight;ymin/=weight;xmax/=weight;ymax/=weight;\r\n            for(var i=0;i\u003Cpoints.Length;i\u002B\u002B)points[i]/=weight;\r\n            result.Add(new(seed.Score,(xmin\u002Bxmax)/2,(ymin\u002Bymax)/2,xmax-xmin,ymax-ymin,points));\r\n            remaining=rest;\r\n        }\r\n        return result;\r\n    }\r\n\r\n    static float Iou(PalmDetection a,PalmDetection b)\r\n    {\r\n        var width=Math.Max(0,Math.Min(a.X\u002Ba.Width/2,b.X\u002Bb.Width/2)-Math.Max(a.X-a.Width/2,b.X-b.Width/2));\r\n        var height=Math.Max(0,Math.Min(a.Y\u002Ba.Height/2,b.Y\u002Bb.Height/2)-Math.Max(a.Y-a.Height/2,b.Y-b.Height/2));\r\n        var intersection=width*height;var union=a.Width*a.Height\u002Bb.Width*b.Height-intersection;\r\n        return union\u003E0?intersection/union:0;\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"HumanoidMocap/Motion/CaptureStanceProportion.cs","FileName":"CaptureStanceProportion.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":395388,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidMocap.Cleanup;\r\nusing HumanoidMocap.Mapping;\r\nusing HumanoidMocap.Maths;\r\nusing HumanoidMocap.Skeleton;\r\nusing HumanoidMocap.Target;\r\n\r\nnamespace HumanoidMocap.Motion;\r\nusing Vector3 = System.Numerics.Vector3;\r\n\r\n/// \u003Csummary\u003EKeeps a captured body\u0027s stance width when the target\u0027s hips are a different width.\r\n/// Leg rotations copied onto a rig whose hip joints sit wider (relative to its legs) than the\r\n/// performer\u0027s carry both feet outward by the extra half-width: on a reconstructed performer\r\n/// with hip joints 0.15 leg-lengths apart and Human\u0027s 0.25, a 0.62 leg-length stance became 0.73\r\n/// and read as splayed legs. Each ankle is moved back along the pelvis\u0027 own lateral axis by that\r\n/// difference and the leg is re-solved, preserving bone lengths and the foot\u0027s world orientation.\r\n/// Applied before ground alignment and foot anchoring. A proportion correction, not new capture.\u003C/summary\u003E\r\npublic static class CaptureStanceProportion\r\n{\r\n    public sealed record Result(float HalfWidthCorrection,int Samples);\r\n    public static Result Apply(List\u003CXForm[]\u003E frames,SourceScene source,MappingResult mapping,TargetRig target,FootChain left,FootChain right)\r\n    {\r\n        if(frames.Count==0)return new(0,0);\r\n        var rig=target.Skeleton;var sourceRest=source.Skeleton.RestWorld;\r\n        if(!mapping.RoleToBone.TryGetValue(BoneRole.UpperLegL,out var hipL)||!mapping.RoleToBone.TryGetValue(BoneRole.UpperLegR,out var hipR)||\r\n            !mapping.RoleToBone.TryGetValue(BoneRole.LowerLegL,out var kneeL)||!mapping.RoleToBone.TryGetValue(BoneRole.FootL,out var footL))return new(0,0);\r\n        var sourceLeg=Vector3.Distance(sourceRest[hipL].Pos,sourceRest[kneeL].Pos)\u002BVector3.Distance(sourceRest[kneeL].Pos,sourceRest[footL].Pos);\r\n        var targetRest=rig.RestWorld;\r\n        var targetLeg=Vector3.Distance(targetRest[left.Hip].Pos,targetRest[left.Knee].Pos)\u002BVector3.Distance(targetRest[left.Knee].Pos,targetRest[left.Ankle].Pos);\r\n        if(!(sourceLeg\u003E1e-4f)||!(targetLeg\u003E1e-4f))return new(0,0);\r\n        // Half the hip-joint spacing each skeleton has per unit of its own leg, in target units.\r\n        var correction=(Vector3.Distance(targetRest[left.Hip].Pos,targetRest[right.Hip].Pos)/targetLeg\r\n            -Vector3.Distance(sourceRest[hipL].Pos,sourceRest[hipR].Pos)/sourceLeg)*targetLeg*.5f;\r\n        if(!float.IsFinite(correction)||MathF.Abs(correction)\u003CtargetLeg*.005f)return new(0,0);\r\n        var world=new XForm[rig.Count];var samples=0;\r\n        foreach(var frame in frames)\r\n        {\r\n            foreach(var (leg,other) in new[]{(left,right),(right,left)})\r\n            {\r\n                FkUtil.ToWorld(frame,rig,world);\r\n                var lateral=world[leg.Hip].Pos-world[other.Hip].Pos;if(lateral.LengthSquared()\u003C1e-8f)continue;\r\n                lateral=Vector3.Normalize(lateral);\r\n                var hip=world[leg.Hip];var knee=world[leg.Knee];var ankle=world[leg.Ankle];var footRotation=ankle.Rot;\r\n                var goal=ankle.Pos-lateral*correction;\r\n                // Never ask for more than the leg can reach; the foot keeps its direction from the hip.\r\n                var reach=(Vector3.Distance(hip.Pos,knee.Pos)\u002BVector3.Distance(knee.Pos,ankle.Pos))*.9995f;\r\n                var fromHip=goal-hip.Pos;if(fromHip.Length()\u003Ereach)goal=hip.Pos\u002BVector3.Normalize(fromHip)*reach;\r\n                var bend=Vector3.Cross(knee.Pos-hip.Pos,ankle.Pos-knee.Pos);\r\n                if(bend.LengthSquared()\u003C1e-8f)bend=Vector3.Transform(Vector3.UnitX,hip.Rot);\r\n                var ik=TwoBoneIk.Solve(hip.Pos,knee.Pos,ankle.Pos,goal,soften:0,stableBendAxis:bend);\r\n                EffectorIk.ApplyWorldDeltas(frame,rig,leg.Hip,leg.Knee,leg.Ankle,ik.UpperWorldDelta,ik.LowerWorldDelta,world);\r\n                FkUtil.ToWorld(frame,rig,world);\r\n                var parent=rig[leg.Ankle].ParentIndex;\r\n                frame[leg.Ankle].Rot=Quaternion.Normalize((parent\u003C0?Quaternion.Identity:Quaternion.Inverse(world[parent].Rot))*footRotation);\r\n                samples\u002B\u002B;\r\n            }\r\n        }\r\n        return new(correction,samples);\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"HumanoidMocap/Motion/TargetCorrections.cs","FileName":"TargetCorrections.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":395388,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidMocap.Cleanup;\r\nusing HumanoidMocap.Mapping;\r\nusing HumanoidMocap.Maths;\r\nusing HumanoidMocap.Target;\r\n\r\nnamespace HumanoidMocap.Motion;\r\nusing Vector3 = System.Numerics.Vector3;\r\n\r\npublic sealed class TargetCorrectionSettings\r\n{\r\n    public bool FirstPerson { get; set; }\r\n    /// \u003Csummary\u003EIdentifies the selected target geometry for explicitly authored finger contacts.\u003C/summary\u003E\r\n    public string ContactTargetKey { get; set; } = \u0022\u0022;\r\n    public Vector3 LeftShoulder { get; set; } = new(.18f,1.45f,0);\r\n    public Vector3 RightShoulder { get; set; } = new(-.18f,1.45f,0);\r\n    public Vector3 LeftElbow { get; set; } = new(.45f,1.1f,.15f);\r\n    public Vector3 RightElbow { get; set; } = new(-.45f,1.1f,.15f);\r\n    public float Reach { get; set; } = .995f;\r\n    public float GroundOffset { get; set; }\r\n    public float FacingDegrees { get; set; }\r\n    public bool StabilizeFeet { get; set; } = true;\r\n    /// \u003Csummary\u003EManual camera-space wrist position edits, applied before confirmed prop contacts and target IK.\u003C/summary\u003E\r\n    public List\u003CWristPositionOffset\u003E WristOffsets { get; set; } = new();\r\n    /// \u003Csummary\u003EEditable placement of a camera-relative hand capture in the target\u0027s\r\n    /// Y-up metre frame. This is a user assumption, not recovered camera tracking.\u003C/summary\u003E\r\n    public Vector3 CaptureCameraPosition { get; set; } = new(0,1.65f,0);\r\n    public float CaptureCameraYawDegrees { get; set; } = 180;\r\n    public float CaptureCameraPitchDegrees { get; set; }\r\n    /// \u003Csummary\u003EThe capture camera faced the performer instead of being worn by them. The placement\r\n    /// above then describes a camera in front of the character, and a first-person preview should\r\n    /// look from the character\u0027s own head rather than from that camera.\u003C/summary\u003E\r\n    public bool CaptureFacesSubject { get; set; }\r\n    /// \u003Csummary\u003EShrink a hand capture toward the camera, by at most a quarter, when the\r\n    /// target\u0027s arms are too short to reach it. Points keep their viewing rays, so the\r\n    /// first-person picture is unchanged. Skipped while prop contacts share the capture space.\u003C/summary\u003E\r\n    public bool FitCaptureToArmReach { get; set; } = true;\r\n\r\n    public static TargetCorrectionSettings ForRig(TargetRig rig,TargetUpAxis axis)\r\n    {\r\n        var scale=axis==TargetUpAxis.ZUpEngine?39.3700787f:100f;\r\n        var rotation=axis==TargetUpAxis.YUpCm?Quaternion.Identity:Quaternion.CreateFromAxisAngle(Vector3.UnitX,-MathF.PI/2);\r\n        Vector3 Position(BoneRole role,Vector3 fallback)=\u003Erig.BoneForRole(role) is int b\r\n            ?Vector3.Transform(rig.Skeleton.RestWorld[b].Pos/scale,rotation):fallback;\r\n        var result=new TargetCorrectionSettings();\r\n        result.LeftShoulder=Position(BoneRole.UpperArmL,result.LeftShoulder);\r\n        result.RightShoulder=Position(BoneRole.UpperArmR,result.RightShoulder);\r\n        var height=Math.Max(.2f,(result.LeftShoulder.Y\u002Bresult.RightShoulder.Y)/2)/1.45f;\r\n        result.LeftElbow=result.LeftShoulder\u002Bnew Vector3(.27f,-.35f,.15f)*height;\r\n        result.RightElbow=result.RightShoulder\u002Bnew Vector3(-.27f,-.35f,.15f)*height;\r\n        result.CaptureCameraPosition=Position(BoneRole.Head,new(0,1.55f,0))\u002BVector3.UnitY*.1f*height;\r\n        // FPS viewmodels without a body use their authored origin as the assumed\r\n        // camera position. A full-body eye-height fallback would lift these wrists\r\n        // above the rig and exhaust arm reach before any captured movement.\r\n        if(rig.BoneForRole(BoneRole.Head) is null\u0026\u0026rig.BoneForRole(BoneRole.Hips) is null)\r\n        {\r\n            result.CaptureCameraPosition=Vector3.Zero;\r\n            // A viewmodel can face a different horizontal axis than a body rig.\r\n            // Its left/right shoulder line supplies lateral direction; detached\r\n            // hands can use their authored wrist spacing. This remains editable.\r\n            var leftRole=rig.BoneForRole(BoneRole.UpperArmL) is not null?BoneRole.UpperArmL:BoneRole.HandL;\r\n            var rightRole=rig.BoneForRole(BoneRole.UpperArmR) is not null?BoneRole.UpperArmR:BoneRole.HandR;\r\n            if(rig.BoneForRole(leftRole) is not null\u0026\u0026rig.BoneForRole(rightRole) is not null)\r\n            {\r\n                var lateral=Position(leftRole,Vector3.Zero)-Position(rightRole,Vector3.Zero);lateral.Y=0;\r\n                if(lateral.LengthSquared()\u003E1e-8f)\r\n                {\r\n                    lateral=Vector3.Normalize(lateral);var forward=Vector3.Cross(lateral,Vector3.UnitY);\r\n                    result.CaptureCameraYawDegrees=MathF.Atan2(-forward.X,-forward.Z)*180/MathF.PI;\r\n                    float ArmLength(BoneRole upper,BoneRole lower,BoneRole hand)=\u003E\r\n                        rig.BoneForRole(upper) is not null\u0026\u0026rig.BoneForRole(lower) is not null\u0026\u0026rig.BoneForRole(hand) is not null\r\n                        ?Vector3.Distance(Position(upper,Vector3.Zero),Position(lower,Vector3.Zero))\u002B\r\n                            Vector3.Distance(Position(lower,Vector3.Zero),Position(hand,Vector3.Zero)):0;\r\n                    var armLength=Math.Max(ArmLength(BoneRole.UpperArmL,BoneRole.LowerArmL,BoneRole.HandL),\r\n                        ArmLength(BoneRole.UpperArmR,BoneRole.LowerArmR,BoneRole.HandR));\r\n                    var proportion=armLength\u003E1e-4f?armLength/.6f:1f;\r\n                    result.LeftElbow=result.LeftShoulder\u002B(lateral*.27f-Vector3.UnitY*.35f\u002Bforward*.15f)*proportion;\r\n                    result.RightElbow=result.RightShoulder\u002B(-lateral*.27f-Vector3.UnitY*.35f\u002Bforward*.15f)*proportion;\r\n                }\r\n            }\r\n        }\r\n        return result;\r\n    }\r\n}\r\n\r\npublic static class TargetCorrections\r\n{\r\n    public static void Apply(List\u003CXForm[]\u003E frames,TargetRig rig,TargetUpAxis axis,TargetCorrectionSettings settings,\r\n        IReadOnlyList\u003CDictionary\u003CBoneRole,XForm\u003E\u003E? wristTargets=null)\r\n    {\r\n        var skeleton=rig.Skeleton;var world=new XForm[skeleton.Count];\r\n        var left=new ArmConstraintSolver();var right=new ArmConstraintSolver();\r\n        var scale=axis==TargetUpAxis.ZUpEngine?39.3700787f:100f;\r\n        var conversion=axis==TargetUpAxis.YUpCm?Quaternion.Identity:Quaternion.CreateFromAxisAngle(Vector3.UnitX,MathF.PI/2);\r\n        Vector3 Convert(Vector3 v)=\u003EVector3.Transform(v*scale,conversion);\r\n        var up=axis==TargetUpAxis.YUpCm?Vector3.UnitY:Vector3.UnitZ;\r\n        var yaw=Quaternion.CreateFromAxisAngle(up,settings.FacingDegrees*MathF.PI/180);\r\n        for(var frameIndex=0;frameIndex\u003Cframes.Count;frameIndex\u002B\u002B)\r\n        {\r\n            var frame=frames[frameIndex];\r\n            if(!settings.FirstPerson\u0026\u0026wristTargets is null)\r\n            {\r\n                for(var i=0;i\u003Cframe.Length;i\u002B\u002B)if(skeleton[i].ParentIndex\u003C0)\r\n                    frame[i]=new XForm(Vector3.Transform(frame[i].Pos,yaw)\u002Bup*settings.GroundOffset*scale,Quaternion.Normalize(yaw*frame[i].Rot));\r\n                continue;\r\n            }\r\n            Solve(BoneRole.UpperArmL,BoneRole.LowerArmL,BoneRole.HandL,settings.LeftShoulder,settings.LeftElbow,left);\r\n            Solve(BoneRole.UpperArmR,BoneRole.LowerArmR,BoneRole.HandR,settings.RightShoulder,settings.RightElbow,right);\r\n            void Solve(BoneRole upperRole,BoneRole lowerRole,BoneRole handRole,Vector3 shoulder,Vector3 pole,ArmConstraintSolver solver)\r\n            {\r\n                if(rig.BoneForRole(handRole) is not int h)return;\r\n                FkUtil.ToWorld(frame,skeleton,world);\r\n                var originalWrist=world[h];\r\n                if(wristTargets is not null\u0026\u0026!wristTargets[frameIndex].TryGetValue(handRole,out originalWrist))return;\r\n                if(rig.BoneForRole(upperRole) is not int u || rig.BoneForRole(lowerRole) is not int l)\r\n                {\r\n                    if(wristTargets is not null)SetWorld(h,originalWrist);\r\n                    return;\r\n                }\r\n                var upperLength=Vector3.Distance(world[u].Pos,world[l].Pos);var lowerLength=Vector3.Distance(world[l].Pos,world[h].Pos);\r\n                var result=solver.Solve(originalWrist.Pos,originalWrist.Rot,new ArmSettings { Shoulder=Convert(shoulder),ElbowTarget=Convert(pole),UpperLength=upperLength,ForearmLength=lowerLength,MaximumReach=settings.Reach });\r\n                var upperRotation=Quaternion.Normalize(MathQ.FromTo(world[l].Pos-world[u].Pos,result.Elbow-result.Shoulder)*world[u].Rot);\r\n                SetWorld(u,new XForm(result.Shoulder,upperRotation));\r\n                FkUtil.ToWorld(frame,skeleton,world);\r\n                var lowerRotation=Quaternion.Normalize(MathQ.FromTo(world[h].Pos-world[l].Pos,result.Wrist-result.Elbow)*world[l].Rot);\r\n                SetWorld(l,new XForm(world[l].Pos,lowerRotation));\r\n                FkUtil.ToWorld(frame,skeleton,world);\r\n                SetWorld(h,new XForm(world[h].Pos,originalWrist.Rot)); // preserve captured wrist attitude and finger locals\r\n            }\r\n            void SetWorld(int index,XForm value)\r\n            {\r\n                var parent=skeleton[index].ParentIndex;\r\n                frame[index]=parent\u003C0?value:XForm.Compose(world[parent].Inverse(),value);\r\n            }\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"HumanoidMocap/Skeleton/Clip.cs","FileName":"Clip.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":395388,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing HumanoidMocap.Maths;\r\n\r\nnamespace HumanoidMocap.Skeleton;\r\n\r\n/// \u003Csummary\u003E\r\n/// A sampled animation clip: a fixed-rate sequence of frames, each holding one\r\n/// parent-relative local transform per bone (skeleton bone order). Clips are always\r\n/// resampled at ingest \u2014 no key data is preserved.\r\n/// \u003C/summary\u003E\r\npublic sealed class Clip\r\n{\r\n    /// \u003Csummary\u003EClip (sequence) name.\u003C/summary\u003E\r\n    public string Name { get; }\r\n\r\n    /// \u003Csummary\u003ESample rate in frames per second.\u003C/summary\u003E\r\n    public float Fps { get; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Native frame rate of the take in the SOURCE file (FBX GlobalSettings\r\n    /// TimeMode/CustomFrameRate, BVH 1/FrameTime). External frame ranges \u2014 Unity\r\n    /// \u003Cc\u003E.fbx.meta\u003C/c\u003E \u003Cc\u003EclipAnimations\u003C/c\u003E definitions \u2014 are expressed in THIS rate, so\r\n    /// they must be rescaled by \u003Cc\u003EFps / NativeFps\u003C/c\u003E to index the resampled\r\n    /// \u003Csee cref=\u0022Frames\u0022/\u003E. Equals \u003Csee cref=\u0022Fps\u0022/\u003E when the importer records no native rate.\r\n    /// \u003C/summary\u003E\r\n    public float NativeFps { get; }\r\n\r\n    /// \u003Csummary\u003EWhether the clip is authored to loop.\u003C/summary\u003E\r\n    public bool Looping { get; }\r\n\r\n    /// \u003Csummary\u003EFrames in playback order; each entry is one local transform per bone.\u003C/summary\u003E\r\n    public List\u003CXForm[]\u003E Frames { get; }\r\n\r\n    /// \u003Csummary\u003ENumber of frames currently in the clip.\u003C/summary\u003E\r\n    public int FrameCount =\u003E Frames.Count;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Clip duration in seconds at \u003Csee cref=\u0022Fps\u0022/\u003E: the time span between the first and the\r\n    /// last sample, \u003Cc\u003E(FrameCount - 1) / Fps\u003C/c\u003E (frames are fence posts, intervals are the\r\n    /// spans between them \u2014 matching the DMX timeFrame this clip serializes to). Zero for\r\n    /// empty and single-frame clips.\r\n    /// \u003C/summary\u003E\r\n    public float Duration =\u003E FrameCount \u003C= 1 ? 0f : (FrameCount - 1) / Fps;\r\n\r\n    /// \u003Csummary\u003ECreates an empty clip.\u003C/summary\u003E\r\n    /// \u003Cexception cref=\u0022ArgumentOutOfRangeException\u0022\u003EThrown when \u003Cparamref name=\u0022fps\u0022/\u003E is not positive.\u003C/exception\u003E\r\n    public Clip(string name, float fps, bool looping)\r\n        : this(name, fps, looping, new List\u003CXForm[]\u003E())\r\n    {\r\n    }\r\n\r\n    /// \u003Csummary\u003ECreates a clip wrapping an existing frame list (not copied).\u003C/summary\u003E\r\n    /// \u003Cparam name=\u0022nativeFps\u0022\u003ESource-file native frame rate (\u003Csee cref=\u0022NativeFps\u0022/\u003E);\r\n    /// null = same as \u003Cparamref name=\u0022fps\u0022/\u003E.\u003C/param\u003E\r\n    /// \u003Cexception cref=\u0022ArgumentOutOfRangeException\u0022\u003EThrown when \u003Cparamref name=\u0022fps\u0022/\u003E (or a\r\n    /// provided \u003Cparamref name=\u0022nativeFps\u0022/\u003E) is not positive.\u003C/exception\u003E\r\n    public Clip(string name, float fps, bool looping, List\u003CXForm[]\u003E frames, float? nativeFps = null)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(name);\r\n        ArgumentNullException.ThrowIfNull(frames);\r\n        if (!(fps \u003E 0f) || !float.IsFinite(fps))\r\n            throw new ArgumentOutOfRangeException(nameof(fps), fps, \u0022Fps must be a positive finite number.\u0022);\r\n        if (nativeFps is { } native \u0026\u0026 (!(native \u003E 0f) || !float.IsFinite(native)))\r\n            throw new ArgumentOutOfRangeException(nameof(nativeFps), native, \u0022NativeFps must be a positive finite number.\u0022);\r\n\r\n        Name = name;\r\n        Fps = fps;\r\n        NativeFps = nativeFps ?? fps;\r\n        Looping = looping;\r\n        Frames = frames;\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"HumanoidMocap/Solve/RestNormalizer.cs","FileName":"RestNormalizer.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":395388,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing HumanoidMocap.Mapping;\r\nusing HumanoidMocap.Maths;\r\nusing SkeletonModel = HumanoidMocap.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidMocap.Solve;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/HumanoidMocap/Assembly.cs)\r\n\r\n/// \u003Csummary\u003E\r\n/// A skeleton\u0027s rest pose as explicit world transforms (indexed like the skeleton\u0027s bones).\r\n/// Produced by \u003Csee cref=\u0022RestNormalizer\u0022/\u003E; feed it to\r\n/// \u003Csee cref=\u0022CanonicalFrames.Build(SkeletonModel, MappingResult, IReadOnlyList{XForm})\u0022/\u003E.\r\n/// \u003C/summary\u003E\r\npublic sealed class RestPose\r\n{\r\n    /// \u003Csummary\u003ERest world transforms per bone (positions in cm).\u003C/summary\u003E\r\n    public XForm[] WorldRest { get; init; } = Array.Empty\u003CXForm\u003E();\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Rest-pose detection (T-pose / A-pose / I-pose) and normalization to a canonical T-pose.\r\n/// Runs on \u003Cb\u003Eboth\u003C/b\u003E source and target rests before canonical frames are built, so deltas\r\n/// measured against the normalized source rest apply cleanly to the normalized target rest\r\n/// (the s\u0026amp;box human rig itself rests in a strong A-pose, ~52\u00B0 below horizontal).\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003ENon-anatomical binds:\u003C/b\u003E some exports (NVIDIA SOMA uniform-skeleton BVH) carry\r\n/// a bind that is bone-length encoding, not a pose \u2014 every OFFSET runs along \u00B1X, so identity\r\n/// rest rotations collapse the figure into a stick (measured on the SOMA repro: character up\r\n/// = world \u002BX, thigh\u00B7up = \u002B1.00/\u22121.00, thighs 180\u00B0 apart \u2014 a thigh pointing at the\r\n/// shoulders). Every anatomical bind in the corpus measures thigh\u00B7up \u2264 \u22120.75 and \u2264 46\u00B0\r\n/// between thighs (the posed Defenses.fbx stance included), so\r\n/// \u003Csee cref=\u0022IsAnatomicalRest\u0022/\u003E separates the two with a wide margin. When the bind fails\r\n/// the check, \u003Csee cref=\u0022Normalize(SkeletonModel, MappingResult, IReadOnlyList{XForm})\u0022/\u003E\r\n/// rebuilds the rest from the supplied reference pose (the clip\u0027s first frame \u2014 a real pose\r\n/// whose rotations carry the missing rest orientation); without a reference pose\r\n/// normalization throws \u003Csee cref=\u0022ArgumentException\u0022/\u003E rather than build canonical frames\r\n/// on a non-pose (callers that probe rest geometry already treat that as \u0022skip\u0022).\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EDetection:\u003C/b\u003E the angle of each (LowerArm.head \u2212 UpperArm.head) rest segment\r\n/// against the character\u0027s horizontal lateral direction (per side, then averaged):\r\n/// 0\u201315\u00B0 \u2192 \u003Csee cref=\u0022DetectedPose.TPose\u0022/\u003E; 15\u201360\u00B0 with the arm \u003Ci\u003Ebelow\u003C/i\u003E horizontal \u2192\r\n/// \u003Csee cref=\u0022DetectedPose.APose\u0022/\u003E; 60\u201395\u00B0 with the arms hanging predominantly \u003Ci\u003Edown\u003C/i\u003E\r\n/// (arm\u00B7up \u2264 \u22120.5) \u2192 \u003Csee cref=\u0022DetectedPose.IPose\u0022/\u003E (relaxed N-pose rests \u2014 first-frame\r\n/// rebuilt rests measure 64\u201388\u00B0 below horizontal at arm\u00B7up \u22120.90\u2026\u22121.00 across the corpus);\r\n/// anything else \u2192 \u003Csee cref=\u0022DetectedPose.Other\u0022/\u003E. Legs are checked analogously against\r\n/// vertical for wide stances.\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003ENormalization (swing-only, hierarchical, per limb chain \u2014 never the spine):\u003C/b\u003E\r\n/// each arm segment is swung about its joint so the chain matches the canonical T-pose:\r\n/// upper arm \u2192 \u00B1lateral (exactly horizontal), forearm \u2192 \u00B1lateral (straight arm), hand \u2192\r\n/// \u00B1lateral; every swing rotates all descendant world rests about the joint (positions orbit\r\n/// the joint, orientations are premultiplied), so segment lengths never change. Hand roll is\r\n/// then resolved to the palm-down convention by rotating about the limb axis until the hand\u0027s\r\n/// geometric dorsal normal (\u003Csee cref=\u0022HandGeometry.Dorsal\u0022/\u003E) aligns with character up.\r\n/// Legs are only normalized (thigh and calf swung to exactly \u2212up) when a wide stance\r\n/// (\u0026gt; 15\u00B0 off vertical) is detected \u2014 normal rigs keep their slight natural leg splay.\u003C/para\u003E\r\n/// \u003C/remarks\u003E\r\npublic static class RestNormalizer\r\n{\r\n    /// \u003Csummary\u003ERest-pose family detected from the arm rest angle.\u003C/summary\u003E\r\n    public enum DetectedPose\r\n    {\r\n        /// \u003Csummary\u003EArms within 15\u00B0 of horizontal.\u003C/summary\u003E\r\n        TPose,\r\n\r\n        /// \u003Csummary\u003EArms 15\u201360\u00B0 below horizontal.\u003C/summary\u003E\r\n        APose,\r\n\r\n        /// \u003Csummary\u003EArms hanging 60\u201395\u00B0 below horizontal, predominantly downward (relaxed\r\n        /// N-pose; typical for rests rebuilt from a clip\u0027s first frame).\u003C/summary\u003E\r\n        IPose,\r\n\r\n        /// \u003Csummary\u003EAnything else (arms raised, missing, or extreme poses).\u003C/summary\u003E\r\n        Other,\r\n    }\r\n\r\n    /// \u003Csummary\u003EWhat detection and normalization found and did; surfaced in the mapping report.\u003C/summary\u003E\r\n    public sealed class RestReport\r\n    {\r\n        /// \u003Csummary\u003EDetected rest-pose family.\u003C/summary\u003E\r\n        public DetectedPose Detected { get; set; } = DetectedPose.Other;\r\n\r\n        /// \u003Csummary\u003EAverage upper-arm rest angle against the horizontal lateral direction,\r\n        /// degrees (0 = perfect T-pose).\u003C/summary\u003E\r\n        public float UpperArmAngleDeg { get; set; } = float.NaN;\r\n\r\n        /// \u003Csummary\u003ETrue when the bind rest failed \u003Csee cref=\u0022IsAnatomicalRest\u0022/\u003E and the\r\n        /// normalized rest was rebuilt from the caller\u0027s reference pose instead.\u003C/summary\u003E\r\n        public bool RebuiltFromReferencePose { get; set; }\r\n\r\n        /// \u003Csummary\u003EHuman-readable notes: corrections applied, skipped steps, oddities.\u003C/summary\u003E\r\n        public List\u003Cstring\u003E Notes { get; } = new();\r\n    }\r\n\r\n    private const float TPoseMaxDeg = 15f;\r\n    private const float APoseMaxDeg = 60f;\r\n    private const float IPoseMaxDeg = 95f;\r\n    private const float IPoseMaxUpDot = -0.5f;\r\n    private const float WideStanceMinDeg = 15f;\r\n\r\n    /// \u003Csummary\u003EPlausibility cap on thigh\u00B7characterUp: a rest thigh pointing less than ~78\u00B0\r\n    /// away from the shoulder direction is anatomically impossible. Corpus anatomical binds\r\n    /// measure \u2264 \u22120.75; the SOMA stick bind \u002B1.00.\u003C/summary\u003E\r\n    private const float ThighMaxUpDot = 0.2f;\r\n\r\n    /// \u003Csummary\u003EPlausibility floor on thighL\u00B7thighR (cos 120\u00B0): rest thighs more than 120\u00B0\r\n    /// apart are anatomically impossible. Corpus anatomical binds measure \u2264 46\u00B0 apart\r\n    /// (cos \u2265 0.69); the SOMA stick bind 180\u00B0 (\u22121.00).\u003C/summary\u003E\r\n    private const float ThighPairMinDot = -0.5f;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Detects the rest pose of \u003Cparamref name=\u0022skeleton\u0022/\u003E and returns a T-pose-normalized\r\n    /// copy of its rest world transforms plus a report. The skeleton itself is not modified.\r\n    /// \u003C/summary\u003E\r\n    /// \u003Cexception cref=\u0022ArgumentException\u0022\u003EThrown when the mapping lacks the bones the\r\n    /// character frame needs (see \u003Csee cref=\u0022CharacterFrame.Compute\u0022/\u003E), or when the bind\r\n    /// rest is not an anatomical pose (see \u003Csee cref=\u0022IsAnatomicalRest\u0022/\u003E) \u2014 without a\r\n    /// reference pose there is nothing valid to normalize.\u003C/exception\u003E\r\n    public static (RestPose Normalized, RestReport Report) Normalize(SkeletonModel skeleton, MappingResult map)\r\n        =\u003E Normalize(skeleton, map, referencePoseLocals: null);\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Like \u003Csee cref=\u0022Normalize(SkeletonModel, MappingResult)\u0022/\u003E, but when the bind rest is\r\n    /// not an anatomical pose (see \u003Csee cref=\u0022IsAnatomicalRest\u0022/\u003E and the class remarks) the\r\n    /// rest is rebuilt from \u003Cparamref name=\u0022referencePoseLocals\u0022/\u003E (parent-relative locals,\r\n    /// indexed like the skeleton \u2014 pass the clip\u0027s first frame) before normalization.\r\n    /// \u003C/summary\u003E\r\n    /// \u003Cexception cref=\u0022ArgumentException\u0022\u003EAs the two-argument overload; a non-anatomical\r\n    /// bind only throws when \u003Cparamref name=\u0022referencePoseLocals\u0022/\u003E is null.\u003C/exception\u003E\r\n    public static (RestPose Normalized, RestReport Report) Normalize(\r\n        SkeletonModel skeleton, MappingResult map, IReadOnlyList\u003CXForm\u003E? referencePoseLocals,\r\n        Vector3? worldUp = null)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(skeleton);\r\n        ArgumentNullException.ThrowIfNull(map);\r\n\r\n        var world = new XForm[skeleton.Count];\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n            world[i] = skeleton.RestWorld[i];\r\n\r\n        var report = new RestReport();\r\n        if (!IsAnatomicalRest(skeleton, map, world))\r\n        {\r\n            if (referencePoseLocals is null)\r\n            {\r\n                throw new ArgumentException(\r\n                    \u0022Bind rest is not an anatomical humanoid pose (a rest thigh points toward \u0022\r\n                    \u002B \u0022the shoulders or the thighs are anti-parallel \u2014 e.g. a bone-length \u0022\r\n                    \u002B \u0022\u0027stick\u0027 bind with identity rotations) and no reference pose is \u0022\r\n                    \u002B \u0022available to rebuild it.\u0022);\r\n            }\r\n            if (referencePoseLocals.Count != skeleton.Count)\r\n            {\r\n                throw new ArgumentException(\r\n                    $\u0022referencePoseLocals has {referencePoseLocals.Count} entries for a \u0022\r\n                    \u002B $\u0022{skeleton.Count}-bone skeleton.\u0022, nameof(referencePoseLocals));\r\n            }\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                world[i] = parent \u003C 0\r\n                    ? referencePoseLocals[i]\r\n                    : XForm.Compose(world[parent], referencePoseLocals[i]);\r\n            }\r\n            report.RebuiltFromReferencePose = true;\r\n            report.Notes.Add(\r\n                \u0022Bind rest is not an anatomical pose (bone-length stick bind); rest rebuilt \u0022\r\n                \u002B \u0022from the reference pose (clip first frame).\u0022);\r\n        }\r\n\r\n        // Arm/leg normalization never moves the hip or shoulder joints, so the character\r\n        // frame computed on the input rest stays valid throughout.\r\n        var cf = CharacterFrame.Compute(skeleton, map, world, worldUp);\r\n\r\n        DetectArms(map, world, cf, report);\r\n        NormalizeArms(skeleton, map, world, cf, report);\r\n        NormalizeLegsIfWide(skeleton, map, world, cf, report);\r\n\r\n        return (new RestPose { WorldRest = world }, report);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- plausibility\r\n\r\n    /// \u003Csummary\u003E\r\n    /// True when \u003Cparamref name=\u0022worldRest\u0022/\u003E is plausible as an anatomical humanoid pose:\r\n    /// both rest thighs must point away from the shoulder line (thigh\u00B7up \u2264\r\n    /// \u003Csee cref=\u0022ThighMaxUpDot\u0022/\u003E) and be no more than 120\u00B0 apart. Rigs without both\r\n    /// complete thighs (or a shoulder anchor) are unjudgeable and pass. Measured margins:\r\n    /// every anatomical corpus bind (T-pose, A-pose and the posed Defenses.fbx stance)\r\n    /// scores thigh\u00B7up \u2264 \u22120.75 / thighs \u2264 46\u00B0 apart; the SOMA uniform-skeleton stick bind\r\n    /// scores thigh\u00B7up \u002B1.00 / 180\u00B0 apart.\r\n    /// \u003C/summary\u003E\r\n    public static bool IsAnatomicalRest(\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\r\n        Vector3? Pos(BoneRole role)\r\n            =\u003E map.RoleToBone.TryGetValue(role, out var i) \u0026\u0026 i \u003C worldRest.Count\r\n                ? worldRest[i].Pos\r\n                : null;\r\n\r\n        Vector3? Dir(BoneRole from, BoneRole to)\r\n        {\r\n            var a = Pos(from);\r\n            var b = Pos(to);\r\n            if (a is null || b is null)\r\n                return null;\r\n            var d = b.Value - a.Value;\r\n            return d.LengthSquared() \u003C 1e-8f ? null : Vector3.Normalize(d);\r\n        }\r\n\r\n        var hipL = Pos(BoneRole.UpperLegL);\r\n        var hipR = Pos(BoneRole.UpperLegR);\r\n        var thighL = Dir(BoneRole.UpperLegL, BoneRole.LowerLegL);\r\n        var thighR = Dir(BoneRole.UpperLegR, BoneRole.LowerLegR);\r\n        if (hipL is null || hipR is null || thighL is null || thighR is null)\r\n            return true; // legs unmapped/degenerate: cannot judge, preserve behavior\r\n\r\n        var midHips = (hipL.Value \u002B hipR.Value) * 0.5f;\r\n        var midShoulders = Midpoint(Pos(BoneRole.UpperArmL), Pos(BoneRole.UpperArmR))\r\n            ?? Midpoint(Pos(BoneRole.ClavicleL), Pos(BoneRole.ClavicleR))\r\n            ?? Pos(BoneRole.Neck);\r\n        if (midShoulders is null)\r\n            return true; // no shoulder anchor: cannot judge\r\n\r\n        var upRaw = midShoulders.Value - midHips;\r\n        if (upRaw.LengthSquared() \u003C 1e-8f)\r\n            return true;\r\n        var up = Vector3.Normalize(upRaw);\r\n\r\n        return Vector3.Dot(thighL.Value, up) \u003C= ThighMaxUpDot\r\n            \u0026\u0026 Vector3.Dot(thighR.Value, up) \u003C= ThighMaxUpDot\r\n            \u0026\u0026 Vector3.Dot(thighL.Value, thighR.Value) \u003E= ThighPairMinDot;\r\n    }\r\n\r\n    private static Vector3? Midpoint(Vector3? a, Vector3? b)\r\n        =\u003E a is not null \u0026\u0026 b is not null ? (a.Value \u002B b.Value) * 0.5f : null;\r\n\r\n    // ---------------------------------------------------------------- detection\r\n\r\n    private static void DetectArms(MappingResult map, XForm[] world, CharacterFrame cf, RestReport report)\r\n    {\r\n        var angleSum = 0f;\r\n        var upDotSum = 0f;\r\n        var count = 0;\r\n        var allBelowOrLevel = true;\r\n\r\n        foreach (var (upper, lower, sign) in new[]\r\n        {\r\n            (BoneRole.UpperArmL, BoneRole.LowerArmL, 1f),\r\n            (BoneRole.UpperArmR, BoneRole.LowerArmR, -1f),\r\n        })\r\n        {\r\n            if (!map.RoleToBone.TryGetValue(upper, out var u) || !map.RoleToBone.TryGetValue(lower, out var l))\r\n                continue;\r\n            var dir = world[l].Pos - world[u].Pos;\r\n            angleSum \u002B= Deg(MathQ.AngleBetween(dir, cf.Lateral * sign));\r\n            count\u002B\u002B;\r\n            var upDot = Vector3.Dot(Vector3.Normalize(dir), cf.Up);\r\n            upDotSum \u002B= upDot;\r\n            // \u0022Below horizontal\u0022 with a small tolerance so a T-pose arm 1\u00B0 above still counts.\r\n            allBelowOrLevel \u0026= upDot \u003C 0.05f;\r\n        }\r\n\r\n        if (count == 0)\r\n        {\r\n            report.Detected = DetectedPose.Other;\r\n            report.Notes.Add(\u0022Upper/lower arms unmapped; rest pose undetectable, no arm normalization.\u0022);\r\n            return;\r\n        }\r\n\r\n        var angle = angleSum / count;\r\n        report.UpperArmAngleDeg = angle;\r\n        report.Detected = angle \u003C= TPoseMaxDeg\r\n            ? DetectedPose.TPose\r\n            : angle \u003C= APoseMaxDeg \u0026\u0026 allBelowOrLevel\r\n                ? DetectedPose.APose\r\n                // Hanging arms read ~90\u00B0 from lateral whether they point down OR forward;\r\n                // the up-dot cap keeps forward-reaching binds out of the I-pose class.\r\n                : angle \u003C= IPoseMaxDeg \u0026\u0026 allBelowOrLevel \u0026\u0026 upDotSum / count \u003C= IPoseMaxUpDot\r\n                    ? DetectedPose.IPose\r\n                    : DetectedPose.Other;\r\n        report.Notes.Add(\r\n            $\u0022Arm rest angle {angle:F1} deg from horizontal -\u003E {report.Detected}.\u0022);\r\n    }\r\n\r\n    // ---------------------------------------------------------------- arms\r\n\r\n    private static void NormalizeArms(\r\n        SkeletonModel skeleton, MappingResult map, XForm[] world, CharacterFrame cf, RestReport report)\r\n    {\r\n        foreach (var (side, left, sign) in new[] { (\u0022L\u0022, true, 1f), (\u0022R\u0022, false, -1f) })\r\n        {\r\n            if (!TryBone(map, \u0022UpperArm\u0022 \u002B side, out var upper) || !TryBone(map, \u0022LowerArm\u0022 \u002B side, out var lower))\r\n                continue;\r\n            var lateral = cf.Lateral * sign;\r\n\r\n            // 1. Swing the whole arm so (elbow - shoulder) hits exactly \u00B1lateral.\r\n            SwingSegment(skeleton, world, upper, world[lower].Pos - world[upper].Pos, lateral);\r\n\r\n            // 2. Re-measure and swing the forearm so (hand - elbow) is also \u00B1lateral (straight\r\n            //    arm; elbow flexion is not introduced or removed, only swing).\r\n            var hasHand = TryBone(map, \u0022Hand\u0022 \u002B side, out var hand);\r\n            if (hasHand)\r\n                SwingSegment(skeleton, world, lower, world[hand].Pos - world[lower].Pos, lateral);\r\n\r\n            // 3. Swing the hand along the limb axis using its anatomical chain-child point\r\n            //    (midpoint of the mapped finger proximals).\r\n            if (hasHand)\r\n            {\r\n                var knuckles = HandGeometry.FingerProximalMidpoint(map, world, left);\r\n                if (knuckles is not null)\r\n                    SwingSegment(skeleton, world, hand, knuckles.Value - world[hand].Pos, lateral);\r\n\r\n                // 4. Roll: rotate about the (now lateral) limb axis until the geometric dorsal\r\n                //    normal points up -\u003E the canonical palm-down T-pose convention.\r\n                var dorsal = HandGeometry.Dorsal(map, world, left);\r\n                if (dorsal is not null)\r\n                {\r\n                    var rollDeg = RollAboutAxis(skeleton, world, hand, lateral, dorsal.Value, cf.Up);\r\n                    report.Notes.Add($\u0022Hand {side}: palm-down roll correction {rollDeg:F1} deg.\u0022);\r\n                }\r\n                else\r\n                {\r\n                    report.Notes.Add($\u0022Hand {side}: fingers unmapped/degenerate, palm roll left as-is.\u0022);\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    // ---------------------------------------------------------------- legs\r\n\r\n    private static void NormalizeLegsIfWide(\r\n        SkeletonModel skeleton, MappingResult map, XForm[] world, CharacterFrame cf, RestReport report)\r\n    {\r\n        var down = -cf.Up;\r\n        foreach (var side in new[] { \u0022L\u0022, \u0022R\u0022 })\r\n        {\r\n            if (!TryBone(map, \u0022UpperLeg\u0022 \u002B side, out var upper) || !TryBone(map, \u0022LowerLeg\u0022 \u002B side, out var lower))\r\n                continue;\r\n\r\n            var angle = Deg(MathQ.AngleBetween(world[lower].Pos - world[upper].Pos, down));\r\n            if (angle \u003C= WideStanceMinDeg)\r\n                continue; // normal stance: leave the natural leg splay untouched\r\n\r\n            report.Notes.Add($\u0022Leg {side}: wide stance ({angle:F1} deg off vertical), normalized to vertical.\u0022);\r\n            SwingSegment(skeleton, world, upper, world[lower].Pos - world[upper].Pos, down);\r\n            if (TryBone(map, \u0022Foot\u0022 \u002B side, out var foot))\r\n                SwingSegment(skeleton, world, lower, world[foot].Pos - world[lower].Pos, down);\r\n        }\r\n    }\r\n\r\n    // ---------------------------------------------------------------- mechanics\r\n\r\n    private static bool TryBone(MappingResult map, string roleName, out int bone)\r\n        =\u003E map.RoleToBone.TryGetValue(Enum.Parse\u003CBoneRole\u003E(roleName), out bone);\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Swings the subtree rooted at \u003Cparamref name=\u0022joint\u0022/\u003E by the shortest-arc rotation\r\n    /// taking \u003Cparamref name=\u0022currentDir\u0022/\u003E onto \u003Cparamref name=\u0022targetDir\u0022/\u003E, pivoting at the\r\n    /// joint\u0027s head: descendant positions orbit the joint, orientations are premultiplied.\r\n    /// \u003C/summary\u003E\r\n    private static void SwingSegment(\r\n        SkeletonModel skeleton, XForm[] world, int joint, Vector3 currentDir, Vector3 targetDir)\r\n        =\u003E RotateSubtree(skeleton, world, joint, MathQ.FromTo(currentDir, targetDir), world[joint].Pos);\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Rotates the subtree at \u003Cparamref name=\u0022joint\u0022/\u003E about \u003Cparamref name=\u0022axis\u0022/\u003E (through\r\n    /// the joint) by the signed angle that brings \u003Cparamref name=\u0022currentRef\u0022/\u003E, projected \u22A5\r\n    /// axis, onto \u003Cparamref name=\u0022targetRef\u0022/\u003E projected \u22A5 axis. Returns the applied angle in\r\n    /// degrees.\r\n    /// \u003C/summary\u003E\r\n    private static float RollAboutAxis(\r\n        SkeletonModel skeleton, XForm[] world, int joint, Vector3 axis, Vector3 currentRef, Vector3 targetRef)\r\n    {\r\n        var a = currentRef - axis * Vector3.Dot(currentRef, axis);\r\n        var b = targetRef - axis * Vector3.Dot(targetRef, axis);\r\n        if (a.LengthSquared() \u003C 1e-8f || b.LengthSquared() \u003C 1e-8f)\r\n            return 0f;\r\n\r\n        var angle = MathF.Atan2(Vector3.Dot(Vector3.Cross(a, b), axis), Vector3.Dot(a, b));\r\n        RotateSubtree(skeleton, world, joint, Quaternion.CreateFromAxisAngle(axis, angle), world[joint].Pos);\r\n        return Deg(angle);\r\n    }\r\n\r\n    private static void RotateSubtree(\r\n        SkeletonModel skeleton, XForm[] world, int root, Quaternion rotation, Vector3 pivot)\r\n    {\r\n        // Bones are topologically sorted, so a single forward pass finds the whole subtree.\r\n        Span\u003Cbool\u003E inSubtree = skeleton.Count \u003C= 512 ? stackalloc bool[skeleton.Count] : new bool[skeleton.Count];\r\n        inSubtree[root] = true;\r\n        for (var i = root; i \u003C skeleton.Count; i\u002B\u002B)\r\n        {\r\n            var parent = skeleton[i].ParentIndex;\r\n            if (i != root \u0026\u0026 (parent \u003C 0 || !inSubtree[parent]))\r\n                continue;\r\n            if (i != root)\r\n                inSubtree[i] = true;\r\n            world[i] = new XForm(\r\n                pivot \u002B Vector3.Transform(world[i].Pos - pivot, rotation),\r\n                MathQ.Normalize(rotation * world[i].Rot));\r\n        }\r\n    }\r\n\r\n    private static float Deg(float radians) =\u003E radians * (180f / MathF.PI);\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"HumanoidMocap/Solve/SolveOptions.cs","FileName":"SolveOptions.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":395388,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System.Collections.Generic;\r\nusing HumanoidMocap.Mapping;\r\n\r\nnamespace HumanoidMocap.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    /// Clavicles use the delta relative to a shared mapped chest ancestor, then\r\n    /// inherit the solved target chest motion, so body turns do not become shrugs.\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    // The grounding pipeline uses world vertical for legs as well as pelvis travel.\r\n    // Keep the standalone solver\u0027s character-relative direction contract unchanged.\r\n    internal bool GroundedLegDirections { get; init; }\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    /// Full-body motion captures override the clavicle default with observed absolute\r\n    /// directions: a body model\u0027s zero pose is not necessarily neutral shoulder carriage.\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    // The motion-document path supplies reconstructed collarbone directions. Keep\r\n    // other per-role default heuristics active, and honor explicit transfer modes.\r\n    internal bool CaptureClavicleDirections { 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_mocap","Path":"InferenceWorker/CameraRotationTrack.cs","FileName":"CameraRotationTrack.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":395388,"IsPrivate":false,"Code":"using System.Numerics;\r\nusing System.Runtime.InteropServices;\r\nusing HumanoidMocap.Inference;\r\nusing OpenCvSharp;\r\n\r\nnamespace HumanoidMocap.Worker;\r\n\r\n/// \u003Csummary\u003EFrame-to-frame rotation of a moving recording camera, in the role of GVHMR\u0027s\r\n/// SimpleVO: background features outside the followed person are tracked between sampled\r\n/// frames, a rotation is solved for each pair with the job\u0027s pinhole assumption, and the\r\n/// chain is interpolated to every frame. Only rotation is estimated. Camera translation and\r\n/// scene scale are not, so this is not camera tracking or world reconstruction.\u003C/summary\u003E\r\npublic sealed class CameraRotationTrack : IDisposable\r\n{\r\n    public const string Version=\u0022background-rotation-v2\u0022;\r\n    public const string FollowedPrefix=\u0022Moving recording camera followed:\u0022;\r\n    const int WorkingWidth=640,Step=6,MinimumInliers=40;\r\n    public sealed record Result(float[] AngularVelocity6d,int Pairs,int UsablePairs,float TotalDegrees,float LargestPairDegrees,float MeanInlierRatio=0)\r\n    {\r\n        /// \u003Csummary\u003EBackground that fits one rotation homography this well shows little parallax, so the\r\n        /// camera turned about a nearly fixed point, as a standing operator\u0027s does. A camera that also\r\n        /// travels leaves parallax, and its position is then unknown.\u003C/summary\u003E\r\n        public bool RotationOnly=\u003EUsable\u0026\u0026MeanInlierRatio\u003E=.8f;\r\n        /// \u003Csummary\u003EUsable only when nearly every sampled pair could be solved.\u003C/summary\u003E\r\n        public bool Usable=\u003EPairs\u003E0\u0026\u0026UsablePairs\u003E=Pairs*.8f;\r\n        public string Diagnostic=\u003EUsable\r\n            ?FormattableString.Invariant($\u0022{FollowedPrefix} camera rotation solved from background features for {UsablePairs}/{Pairs} sampled frame pairs, {TotalDegrees:F1} degrees in total and at most {LargestPairDegrees:F1} degrees per pair, and supplied to GVHMR in place of a still-camera assumption. {MeanInlierRatio*100:F0}% of background features fit a pure rotation, so the camera is treated as {(RotationOnly?\u0022turning in place\u0022:\u0022also travelling\u0022)}. Rotation only, from an assumed lens; camera translation and scale are not recovered.\u0022)\r\n            :FormattableString.Invariant($\u0022Moving recording camera could not be followed: only {UsablePairs}/{Pairs} sampled frame pairs had enough background features. The capture stays camera-relative.\u0022);\r\n    }\r\n    readonly float focal;Mat? previous;Rect2f previousBody;int frames;\r\n    readonly List\u003C(int Frame,Quaternion WorldToCamera)\u003E samples=new();int pairs,usable;float largest,inlierRatios;\r\n    /// \u003Cparam name=\u0022focalLength\u0022\u003EThe job\u0027s pinhole focal length in source pixels.\u003C/param\u003E\r\n    public CameraRotationTrack(float focalLength){if(!(focalLength\u003E0))throw new ArgumentOutOfRangeException(nameof(focalLength));focal=focalLength;}\r\n    public void Add(DecodedVideoFrame frame,GvhmrDecoder.Box person,bool last)\r\n    {\r\n        var index=frames\u002B\u002B;if(index%Step!=0\u0026\u0026!last)return;\r\n        var scale=WorkingWidth/(float)frame.Width;\r\n        using var rgba=new Mat(frame.Height,frame.Width,MatType.CV_8UC4);Marshal.Copy(frame.Rgba,0,rgba.Data,frame.Rgba.Length);\r\n        using var full=new Mat();Cv2.CvtColor(rgba,full,ColorConversionCodes.RGBA2GRAY);\r\n        var gray=new Mat();Cv2.Resize(full,gray,new Size(WorkingWidth,Math.Max(1,(int)MathF.Round(frame.Height*scale))),0,0,InterpolationFlags.Area);\r\n        var body=new Rect2f((person.CenterX-person.Size*.3f)*scale,(person.CenterY-person.Size*.55f)*scale,person.Size*.6f*scale,person.Size*1.1f*scale);\r\n        if(previous is null){previous=gray;previousBody=body;samples.Add((index,Quaternion.Identity));return;}\r\n        pairs\u002B\u002B;var rotation=Solve(previous,gray,previousBody,body,focal*scale,out var inlierRatio);\r\n        if(rotation is { } solved)\r\n        {\r\n            usable\u002B\u002B;largest=Math.Max(largest,Degrees(solved));inlierRatios\u002B=inlierRatio;\r\n            samples.Add((index,Quaternion.Normalize(solved*samples[^1].WorldToCamera)));\r\n        }\r\n        else samples.Add((index,samples[^1].WorldToCamera)); // unsolved pair: no rotation claimed\r\n        previous.Dispose();previous=gray;previousBody=body;\r\n    }\r\n    static float Degrees(Quaternion q)=\u003E2*MathF.Acos(Math.Clamp(MathF.Abs(q.W),0,1))*180/MathF.PI;\r\n    static Quaternion? Solve(Mat a,Mat b,Rect2f bodyA,Rect2f bodyB,float f,out float inlierRatio)\r\n    {\r\n        inlierRatio=0;\r\n        using var mask=new Mat(a.Size(),MatType.CV_8UC1,Scalar.White);\r\n        Cv2.Rectangle(mask,new Rect((int)bodyA.X,(int)bodyA.Y,(int)bodyA.Width,(int)bodyA.Height),Scalar.Black,-1);\r\n        var points=Cv2.GoodFeaturesToTrack(a,600,.01,7,mask,3,false,.04);\r\n        if(points.Length\u003CMinimumInliers)return null;\r\n        var tracked=new Point2f[points.Length];Cv2.CalcOpticalFlowPyrLK(a,b,points,ref tracked,out var status,out _,new Size(21,21),4);\r\n        var returned=new Point2f[points.Length];Cv2.CalcOpticalFlowPyrLK(b,a,tracked,ref returned,out var back,out _,new Size(21,21),4);\r\n        var from=new List\u003CPoint2d\u003E();var to=new List\u003CPoint2d\u003E();\r\n        for(var i=0;i\u003Cpoints.Length;i\u002B\u002B)\r\n        {\r\n            if(status[i]==0||back[i]==0||bodyB.Contains(tracked[i])||points[i].DistanceTo(returned[i])\u003E1)continue;\r\n            from.Add(new(points[i].X,points[i].Y));to.Add(new(tracked[i].X,tracked[i].Y));\r\n        }\r\n        if(from.Count\u003CMinimumInliers)return null;\r\n        // Distant background under camera rotation moves by the homography K R K^-1.\r\n        using var inliers=new Mat();\r\n        using var homography=Cv2.FindHomography(from,to,HomographyMethods.Ransac,1.5,inliers);\r\n        if(homography.Empty()||Cv2.CountNonZero(inliers)\u003CMath.Max(MinimumInliers,from.Count*.5))return null;\r\n        inlierRatio=Cv2.CountNonZero(inliers)/(float)from.Count;\r\n        double cx=(a.Width-1)*.5,cy=(a.Height-1)*.5;\r\n        var h=new double[3,3];for(var r=0;r\u003C3;r\u002B\u002B)for(var c=0;c\u003C3;c\u002B\u002B)h[r,c]=homography.At\u003Cdouble\u003E(r,c);\r\n        double[,] k={{f,0,cx},{0,f,cy},{0,0,1}},inverse={{1/f,0,-cx/f},{0,1/f,-cy/f},{0,0,1}};\r\n        var m=Multiply(inverse,Multiply(h,k));\r\n        // Nearest rotation: orthonormalise with SVD and fix the sign.\r\n        using var matrix=new Mat(3,3,MatType.CV_64FC1);for(var r=0;r\u003C3;r\u002B\u002B)for(var c=0;c\u003C3;c\u002B\u002B)matrix.Set(r,c,m[r,c]);\r\n        using var w=new Mat();using var u=new Mat();using var vt=new Mat();Cv2.SVDecomp(matrix,w,u,vt);\r\n        using var product=(u*vt).ToMat();var rotation=new double[3,3];for(var r=0;r\u003C3;r\u002B\u002B)for(var c=0;c\u003C3;c\u002B\u002B)rotation[r,c]=product.At\u003Cdouble\u003E(r,c);\r\n        var determinant=rotation[0,0]*(rotation[1,1]*rotation[2,2]-rotation[1,2]*rotation[2,1])-rotation[0,1]*(rotation[1,0]*rotation[2,2]-rotation[1,2]*rotation[2,0])\u002Brotation[0,2]*(rotation[1,0]*rotation[2,1]-rotation[1,1]*rotation[2,0]);\r\n        if(determinant\u003C0)for(var r=0;r\u003C3;r\u002B\u002B)for(var c=0;c\u003C3;c\u002B\u002B)rotation[r,c]=-rotation[r,c];\r\n        // System.Numerics uses row vectors: its matrix is the transpose of this column-vector rotation.\r\n        var q=Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(new Matrix4x4(\r\n            (float)rotation[0,0],(float)rotation[1,0],(float)rotation[2,0],0,(float)rotation[0,1],(float)rotation[1,1],(float)rotation[2,1],0,\r\n            (float)rotation[0,2],(float)rotation[1,2],(float)rotation[2,2],0,0,0,0,1)));\r\n        // A sampled pair a fifth of a second apart cannot plausibly turn this far; treat it as a failed solve.\r\n        return float.IsFinite(q.W)\u0026\u0026Degrees(q)\u003C=25?q:null;\r\n    }\r\n    static double[,] Multiply(double[,] a,double[,] b)\r\n    {var result=new double[3,3];for(var r=0;r\u003C3;r\u002B\u002B)for(var c=0;c\u003C3;c\u002B\u002B)for(var i=0;i\u003C3;i\u002B\u002B)result[r,c]\u002B=a[r,i]*b[i,c];return result;}\r\n    public Result Finish()\r\n    {\r\n        var count=frames;var result=new float[count*6];if(count==0)return new(result,0,0,0,0);\r\n        var orientation=new Quaternion[count];var next=0;\r\n        for(var t=0;t\u003Ccount;t\u002B\u002B)\r\n        {\r\n            while(next\u003Csamples.Count-1\u0026\u0026samples[next\u002B1].Frame\u003C=t)next\u002B\u002B;\r\n            var a=samples[next];var b=samples[Math.Min(next\u002B1,samples.Count-1)];\r\n            orientation[t]=b.Frame==a.Frame?a.WorldToCamera:Quaternion.Slerp(a.WorldToCamera,b.WorldToCamera,Math.Clamp((t-a.Frame)/(float)(b.Frame-a.Frame),0,1));\r\n        }\r\n        for(var t=0;t\u003Ccount;t\u002B\u002B)\r\n        {\r\n            // GVHMR compute_cam_angvel: R[t\u002B1] R[t]^T, with the final value repeated.\r\n            var s=Math.Min(t,count-2);var relative=count\u003C2?Quaternion.Identity:Quaternion.Normalize(orientation[s\u002B1]*Quaternion.Conjugate(orientation[s]));\r\n            var m=Matrix4x4.CreateFromQuaternion(relative);\r\n            // First two rows of the column-vector rotation matrix (PyTorch3D 6D layout).\r\n            result[t*6]=m.M11;result[t*6\u002B1]=m.M21;result[t*6\u002B2]=m.M31;result[t*6\u002B3]=m.M12;result[t*6\u002B4]=m.M22;result[t*6\u002B5]=m.M32;\r\n        }\r\n        return new(result,pairs,usable,Degrees(orientation[^1]),largest,usable==0?0:inlierRatios/usable);\r\n    }\r\n    public void Dispose(){previous?.Dispose();previous=null;}\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"InferenceWorker/HandModelDownloads.cs","FileName":"HandModelDownloads.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":395388,"IsPrivate":false,"Code":"using HumanoidMocap.Inference;\r\nusing System.Security.Cryptography;\r\nusing System.Text.Json;\r\n\r\nnamespace HumanoidMocap.Worker;\r\n\r\n/// \u003Csummary\u003EOnly the explicitly selected backend and its crop detector are downloaded.\u003C/summary\u003E\r\npublic static class HandModelDownloads\r\n{\r\n    public sealed record Asset(string Path,string Url,long Bytes,string Sha256);\r\n    static readonly Asset Detector=new(\u0022hand_landmarker.task\u0022,\r\n        \u0022https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task\u0022,\r\n        7819105,\u0022fbc2a30080c3c557093b5ddfc334698132eb341044ccee322ccf8bcf3607cde1\u0022);\r\n    static readonly Asset WildHands=new(\u0022wildhands/wildhands.ckpt\u0022,\r\n        \u0022https://drive.usercontent.google.com/download?id=1FJWBrMmTKjKAo6j5DQS1KYpqqFbAbJ9Q\u0026export=download\u0026confirm=t\u0022,\r\n        855094722,\u0022cac3f9a9334da852f3993e95b4ec088dcc6c69f0337db63dacd83e4642880a7b\u0022);\r\n    static readonly Asset Wilor=new(\u0022wilor/wilor_final.ckpt\u0022,\r\n        \u0022https://huggingface.co/spaces/rolpotamias/WiLoR/resolve/99fe3d7acff8104ecca1055df7467709506c2fa6/pretrained_models/wilor_final.ckpt\u0022,\r\n        2564989533,\u00223e97aafc7dd08d883a4cc5a027df61fdb6fda6136dbd1319405413862ada6bb2\u0022);\r\n    static readonly Asset MobileHand=new(\u0022mobilehand/hmr_model_freihand_auc.pth\u0022,\r\n        \u0022https://raw.githubusercontent.com/gmntu/mobilehand/51c112364013b803c38955b55a1572b0d402894c/model/hmr_model_freihand_auc.pth\u0022,\r\n        15152098,MobileHandModel.CheckpointSha256);\r\n\r\n    public static async Task Ensure(string folder,string backend,CancellationToken token)\r\n    {\r\n        var assets=backend switch{\u0022mediapipe\u0022=\u003Enew[]{Detector},\u0022mobilehand\u0022=\u003Enew[]{Detector,MobileHand},\u0022wildhands\u0022=\u003Enew[]{Detector,WildHands},\u0022wilor\u0022=\u003Enew[]{Detector,Wilor},_=\u003Ethrow new NotSupportedException(\u0022Select MediaPipe, MobileHand, WildHands or WiLoR. ACE is not downloaded or loaded by this worker.\u0022)};\r\n        using var http=new HttpClient{Timeout=TimeSpan.FromHours(1)};\r\n        foreach(var asset in assets)\r\n        {\r\n            var path=Path.Combine(folder,asset.Path);Directory.CreateDirectory(Path.GetDirectoryName(path)!);\r\n            if(!File.Exists(path))await ModelDownload.Fetch(http,asset.Url,path,asset.Bytes,asset.Sha256,Console.WriteLine,token);\r\n            else await Verify(path,asset,token);\r\n            Console.WriteLine(\u0022Verified \u0022\u002BPath.GetFileName(path));\r\n        }\r\n        File.WriteAllText(Path.Combine(folder,backend\u002B\u0022-models.json\u0022),JsonSerializer.Serialize(new{backend,assets,verifiedUtc=DateTime.UtcNow},new JsonSerializerOptions{WriteIndented=true}));\r\n    }\r\n    static async Task Verify(string path,Asset asset,CancellationToken token)\r\n    {\r\n        if(new FileInfo(path).Length!=asset.Bytes)throw new InvalidDataException(\u0022Unexpected model size; original preserved: \u0022\u002Bpath);\r\n        var hash=await Task.Run(()=\u003EFileChecksum.Sha256(path),token);\r\n        if(!hash.Equals(asset.Sha256,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException(\u0022Model checksum mismatch; original preserved: \u0022\u002Bpath);\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"Editor/HumanoidMocap/MappingEditor.cs","FileName":"MappingEditor.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":395388,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Editor;\r\nusing HumanoidMocap.Mapping;\r\nusing SkeletonModel = HumanoidMocap.Skeleton.Skeleton;\r\n\r\nnamespace HumanoidMocap.Editor;\r\n\r\n/// \u003Csummary\u003E\r\n/// Manual bone-mapping editor: one row per canonical \u003Csee cref=\u0022BoneRole\u0022/\u003E, grouped\r\n/// anatomically (Body / Arms / Legs / Fingers L / Fingers R), each with a combo of the\r\n/// source skeleton\u0027s bone names (plus \u003Cc\u003E\u0026lt;none\u0026gt;\u003C/c\u003E), pre-filled from the entry\u0027s\r\n/// current mapping. Apply produces a \u003Csee cref=\u0022MappingSource.Manual\u0022/\u003E\r\n/// \u003Csee cref=\u0022MappingResult\u0022/\u003E that the window installs as the file\u0027s mapping override.\r\n/// \u003C/summary\u003E\r\npublic sealed class MappingEditor : Dialog\r\n{\r\n\tstatic readonly (string Group, BoneRole[] Roles)[] Groups =\r\n\t{\r\n\t\t(\u0022Body\u0022, new[]\r\n\t\t{\r\n\t\t\tBoneRole.Hips, BoneRole.Spine0, BoneRole.Spine1, BoneRole.Spine2,\r\n\t\t\tBoneRole.Spine3, BoneRole.Spine4, BoneRole.Neck, BoneRole.Head,\r\n\t\t}),\r\n\t\t(\u0022Arms\u0022, new[]\r\n\t\t{\r\n\t\t\tBoneRole.ClavicleL, BoneRole.UpperArmL, BoneRole.LowerArmL, BoneRole.HandL,\r\n\t\t\tBoneRole.ClavicleR, BoneRole.UpperArmR, BoneRole.LowerArmR, BoneRole.HandR,\r\n\t\t}),\r\n\t\t(\u0022Legs\u0022, new[]\r\n\t\t{\r\n\t\t\tBoneRole.UpperLegL, BoneRole.LowerLegL, BoneRole.FootL, BoneRole.ToeL,\r\n\t\t\tBoneRole.UpperLegR, BoneRole.LowerLegR, BoneRole.FootR, BoneRole.ToeR,\r\n\t\t}),\r\n\t\t(\u0022Fingers (left)\u0022, FingerRoles( \u0022L\u0022 )),\r\n\t\t(\u0022Fingers (right)\u0022, FingerRoles( \u0022R\u0022 )),\r\n\t};\r\n\r\n\tstatic BoneRole[] FingerRoles( string side )\r\n\t\t=\u003E Enum.GetValues\u003CBoneRole\u003E()\r\n\t\t\t.Where( r =\u003E r.ToString().EndsWith( side, StringComparison.Ordinal )\r\n\t\t\t\t\u0026\u0026 (r.ToString().StartsWith( \u0022Thumb\u0022 ) || r.ToString().StartsWith( \u0022Index\u0022 )\r\n\t\t\t\t\t|| r.ToString().StartsWith( \u0022Middle\u0022 ) || r.ToString().StartsWith( \u0022Ring\u0022 )\r\n\t\t\t\t\t|| r.ToString().StartsWith( \u0022Pinky\u0022 )) )\r\n\t\t\t.ToArray();\r\n\r\n\treadonly SkeletonModel _skeleton;\r\n\treadonly Dictionary\u003CBoneRole, int\u003E _selection;\r\n\r\n\t/// \u003Csummary\u003EInvoked with the manual mapping when the user applies.\u003C/summary\u003E\r\n\tpublic Action\u003CMappingResult\u003E Applied { get; set; }\r\n\r\n\t/// \u003Csummary\u003ECreates the editor pre-filled from \u003Cparamref name=\u0022current\u0022/\u003E.\u003C/summary\u003E\r\n\tpublic MappingEditor( Widget parent, string fileName, SkeletonModel skeleton, MappingResult current )\r\n\t\t: base( parent )\r\n\t{\r\n\t\t_skeleton = skeleton;\r\n\t\t_selection = new Dictionary\u003CBoneRole, int\u003E( current?.RoleToBone ?? new Dictionary\u003CBoneRole, int\u003E() );\r\n\r\n\t\tWindow.WindowTitle = $\u0022Bone Mapping - {fileName}\u0022;\r\n\t\tWindow.SetWindowIcon( \u0022device_hub\u0022 );\r\n\t\tWindow.SetModal( true, true );\r\n\t\tWindow.MinimumWidth = 460;\r\n\t\tWindow.MinimumHeight = 600;\r\n\r\n\t\tLayout = Layout.Column();\r\n\t\tLayout.Margin = 12;\r\n\t\tLayout.Spacing = 8;\r\n\r\n\t\tLayout.Add( new Label( this )\r\n\t\t{\r\n\t\t\tText = \u0022Assign bones to their roles; leave absent bones at \u003Cnone\u003E. For hand capture, \u0022\r\n\t\t\t\t\u002B \u0022map each wrist and its finger chains. Arms are optional; a torso and legs are not required.\u0022,\r\n\t\t\tWordWrap = true,\r\n\t\t} );\r\n\r\n\t\tvar scroll = Layout.Add( new ScrollArea( this ), 1 );\r\n\t\tscroll.Canvas = new Widget( scroll );\r\n\t\tscroll.Canvas.Layout = Layout.Column();\r\n\t\tscroll.Canvas.Layout.Margin = new Sandbox.UI.Margin( 4, 4, 16, 4 );\r\n\t\tscroll.Canvas.Layout.Spacing = 4;\r\n\t\tvar canvas = scroll.Canvas.Layout;\r\n\r\n\t\tforeach ( var (group, roles) in Groups )\r\n\t\t{\r\n\t\t\tvar header = canvas.Add( new Label( this ) { Text = group } );\r\n\t\t\theader.SetStyles( $\u0022font-weight: 600; color: {Theme.Blue.Hex}; margin-top: 8px;\u0022 );\r\n\r\n\t\t\tforeach ( var role in roles )\r\n\t\t\t\tcanvas.Add( BuildRoleRow( role ) );\r\n\t\t}\r\n\r\n\t\tcanvas.AddStretchCell();\r\n\r\n\t\tvar buttons = Layout.AddRow();\r\n\t\tbuttons.Spacing = 8;\r\n\t\tbuttons.AddStretchCell();\r\n\t\tbuttons.Add( new Button( \u0022Cancel\u0022 ) { Clicked = Close } );\r\n\t\tvar apply = buttons.Add( new Button.Primary( \u0022Apply Mapping\u0022 ) { Icon = \u0022check\u0022 } );\r\n\t\tapply.Clicked = Apply;\r\n\r\n\t\tWindow.Size = new Vector2( 520, 720 );\r\n\t}\r\n\r\n\tWidget BuildRoleRow( BoneRole role )\r\n\t{\r\n\t\tvar row = new Widget( this );\r\n\t\trow.Layout = Layout.Row();\r\n\t\trow.Layout.Spacing = 8;\r\n\r\n\t\trow.Layout.Add( new Label( this ) { Text = role.ToString(), FixedWidth = 130 } );\r\n\r\n\t\tvar combo = row.Layout.Add( new ComboBox( this ), 1 );\r\n\t\tcombo.AddItem( \u0022\u003Cnone\u003E\u0022, \u0022block\u0022,\r\n\t\t\t() =\u003E _selection.Remove( role ),\r\n\t\t\tselected: !_selection.ContainsKey( role ) );\r\n\r\n\t\tfor ( var i = 0; i \u003C _skeleton.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar boneIndex = i;\r\n\t\t\tcombo.AddItem( _skeleton[i].Name, null,\r\n\t\t\t\t() =\u003E _selection[role] = boneIndex,\r\n\t\t\t\tselected: _selection.TryGetValue( role, out var sel ) \u0026\u0026 sel == boneIndex );\r\n\t\t}\r\n\r\n\t\treturn row;\r\n\t}\r\n\r\n\tvoid Apply()\r\n\t{\r\n\t\t// Reject duplicate assignments up front (the target rig builder would throw later).\r\n\t\tvar duplicates = _selection.GroupBy( kv =\u003E kv.Value ).Where( g =\u003E g.Count() \u003E 1 ).ToList();\r\n\t\tif ( duplicates.Count \u003E 0 )\r\n\t\t{\r\n\t\t\tvar first = duplicates[0];\r\n\t\t\tvar roles = string.Join( \u0022, \u0022, first.Select( kv =\u003E kv.Key ) );\r\n\t\t\tnew PopupWindow( \u0022Duplicate assignment\u0022,\r\n\t\t\t\t$\u0022Bone \\\u0022{_skeleton[first.Key].Name}\\\u0022 is assigned to multiple roles: {roles}.\u0022 )\r\n\t\t\t\t.Show();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar result = new MappingResult( \u0022manual\u0022, MappingSource.Manual ) { Confidence = 1f };\r\n\t\tforeach ( var kv in _selection )\r\n\t\t\tresult.RoleToBone[kv.Key] = kv.Value;\r\n\t\tresult.Notes.Add( \u0022Mapping assigned by hand in the mapping editor.\u0022 );\r\n\r\n\t\tApplied?.Invoke( result );\r\n\t\tClose();\r\n\t}\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"Editor/HumanoidMocap/MocapContactEditor.cs","FileName":"MocapContactEditor.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":395388,"IsPrivate":false,"Code":"using System;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing System.Threading.Tasks;\r\nusing Editor;\r\nusing Sandbox;\r\nusing HumanoidMocap.Mapping;\r\nusing HumanoidMocap.Motion;\r\n\r\nnamespace HumanoidMocap.Editor;\r\n\r\npublic sealed partial class RetargetWindow\r\n{\r\n    internal ContactEditorDialog OpenContactEditor(ContactInterval contact=null)\r\n    {\r\n        if(_editedMotion is null||_processing is not null)return null;\r\n        var dialog=new ContactEditorDialog(this,contact);dialog.Show();return dialog;\r\n    }\r\n\r\n    internal async Task SaveContactAsync(MotionDocument expected,int index,ContactInterval replacement)\r\n    {\r\n        if(expected!=_editedMotion||_processing is not null)throw new InvalidOperationException(\u0022The capture changed. Reopen contact editing.\u0022);\r\n        var candidate=ContactAuthoring.Replace(expected,index,replacement);\r\n        if(new PropContactMotion(candidate).UnsupportedReason(replacement) is {} reason)throw new ArgumentException(reason);\r\n        var source=_rawMotion.Copy();source.Contacts=candidate.Contacts;\r\n        _editedMotion=_appliedCleanup is null?source:MotionCleanup.Apply(source,_appliedCleanup);\r\n        RefreshContacts();await RefreshMocapPreviewAsync();\r\n    }\r\n\r\n    internal sealed class ContactEditorDialog : Dialog\r\n    {\r\n        internal readonly LineEdit Start,End,Target;\r\n        internal readonly Label Status;\r\n        internal readonly Button Save;\r\n        internal readonly Button Place;\r\n        internal readonly Checkbox HoldOrientation;\r\n        internal readonly Checkbox Sliding;\r\n        internal readonly MocapContactKeys Keys;\r\n        internal FingerContactDialog FingerDialog;\r\n        internal readonly Button Fingers;\r\n        readonly MotionDocument _motion;\r\n        readonly ContactInterval _draft;\r\n\r\n        public ContactEditorDialog(RetargetWindow owner,ContactInterval contact):base(owner)\r\n        {\r\n            _motion=owner._editedMotion;\r\n            var index=contact is null?-1:_motion.Contacts.IndexOf(contact);\r\n            _draft=index\u003C0?new(){Start=_motion.Frames[0].Time,End=_motion.Frames[^1].Time,Review=ContactReview.Suggested}:\r\n                _motion.Copy().Contacts[index];\r\n            Window.WindowTitle=index\u003C0?\u0022Add wrist contact\u0022:\u0022Edit wrist contact\u0022;Window.SetWindowIcon(\u0022touch_app\u0022);\r\n            Window.Size=new Vector2(560,430);Window.MinimumSize=new Vector2(520,420);\r\n            SetStyles($\u0022background-color: {Theme.WidgetBackground.Hex}; color: {Theme.Text.Hex};\u0022);\r\n            Layout=Layout.Column();Layout.Margin=16;Layout.Spacing=10;\r\n            Layout.Add(new Label(\u0022Place an object-local wrist anchor and review it against the video. Saved edits return to Suggested; confirm them in Contact review.\u0022,this){WordWrap=true});\r\n            var hands=Layout.Add(new ComboBox(this));\r\n            foreach(var bone in _motion.Bones.Where(b=\u003Eb.Role is BoneRole.HandL or BoneRole.HandR))\r\n            {var name=bone.Name;if(string.IsNullOrEmpty(_draft.Bone))_draft.Bone=name;hands.AddItem(name,onSelected:()=\u003E{if(_draft.Bone!=name)_draft.LocalRotation=null;_draft.Bone=name;},selected:name==_draft.Bone);}\r\n            var props=Layout.Add(new ComboBox(this));\r\n            foreach(var prop in _motion.Objects)foreach(var bone in prop.Bones)\r\n            {\r\n                var id=prop.Id;var name=bone.Name;if(string.IsNullOrEmpty(_draft.Object)){_draft.Object=id;_draft.ObjectBone=name;}\r\n                props.AddItem(id\u002B\u0022 / \u0022\u002Bname,onSelected:()=\u003E{if(_draft.Object!=id||_draft.ObjectBone!=name)_draft.LocalRotation=null;_draft.Object=id;_draft.ObjectBone=name;},\r\n                    selected:id==_draft.Object\u0026\u0026(name==_draft.ObjectBone||string.IsNullOrEmpty(_draft.ObjectBone)\u0026\u0026bone.Parent\u003C0));\r\n            }\r\n            LineEdit Input(string title,string value){var row=Layout.AddRow();row.Spacing=8;row.Add(new Label(title,this){FixedWidth=160});return row.Add(new LineEdit(this){Text=value},1);}\r\n            Start=Input(\u0022Start at video (s)\u0022,_draft.Start.ToString(\u0022R\u0022,CultureInfo.InvariantCulture));\r\n            End=Input(\u0022End at video (s)\u0022,_draft.End.ToString(\u0022R\u0022,CultureInfo.InvariantCulture));\r\n            Target=Input(\u0022Local X, Y, Z (m)\u0022,string.Join(\u0022,\u0022,_draft.LocalTarget.Select(v=\u003Ev.ToString(\u0022R\u0022,CultureInfo.InvariantCulture))));\r\n            HoldOrientation=Layout.Add(new Checkbox(\u0022Hold wrist orientation relative to prop\u0022){Value=_draft.LocalRotation is not null});\r\n            HoldOrientation.ToolTip=\u0022Optional for a rigid grip. Place an anchor below, then review and confirm. Captured finger articulation remains unchanged.\u0022;\r\n            var place=Place=Layout.Add(new Button(\u0022Use wrist at interval midpoint\u0022,\u0022my_location\u0022));\r\n            place.ToolTip=\u0022Use the captured wrist at the nearest midpoint sample. This places a manual anchor; it does not detect a grip. Sliding position keys are preserved.\u0022;\r\n            Status=new Label(\u0022\u0022,this){WordWrap=true};\r\n            Status.SetStyles($\u0022color: {Theme.Yellow.Hex};\u0022);\r\n            Sliding=Layout.Add(new Checkbox(\u0022Sliding contact \u00B7 animate the local wrist target\u0022){Value=_draft.Sliding});\r\n            Sliding.ToolTip=\u0022Uses at least two authored position keys. Turning this off keeps the keys for later and uses the fixed anchor above.\u0022;\r\n            void RefreshMode()\r\n            {\r\n                _draft.Sliding=Sliding.Value;Keys.Visible=Sliding.Value;Target.ReadOnly=Sliding.Value;\r\n                props.Enabled=hands.Enabled=_draft.TargetKeys.Count==0\u0026\u0026_draft.FingerTargets.Count==0;\r\n                props.ToolTip=hands.ToolTip=!props.Enabled?\u0022Remove the draft\u0027s position keys and finger points before changing their hand or object coordinate frame.\u0022:\u0022\u0022;\r\n                Window.Size=new Vector2(560,Sliding.Value?700:500);\r\n            }\r\n            Keys=Layout.Add(new MocapContactKeys(this,_motion,_draft,Read,()=\u003Eowner.PlaybackTime,message=\u003EStatus.Text=message,RefreshMode));\r\n            Sliding.Clicked=RefreshMode;RefreshMode();\r\n            var fingerActions=Layout.AddRow();fingerActions.Spacing=8;\r\n            Fingers=fingerActions.Add(new Button(\u0022Finger contact points\u2026\u0022,\u0022touch_app\u0022),1);\r\n            fingerActions.Add(new Button(\u0022Clear points\u0022,\u0022clear\u0022){ToolTip=\u0022Remove all target-specific finger points from this draft. Cancel restores the saved contact.\u0022,Clicked=()=\u003E{\r\n                _draft.FingerTargets.Clear();RefreshMode();Status.Text=\u0022Finger points removed from this draft. Save to keep the change, or cancel to restore them.\u0022;\r\n            }});\r\n            Fingers.Clicked=()=\u003E{try{Read();FingerDialog=new(owner,this,_draft,()=\u003E{RefreshMode();Status.Text=$\u0022{_draft.FingerTargets.Count} target-specific finger points. Save and review before confirming.\u0022;});FingerDialog.Show();}catch(Exception error){Status.Text=error.Message;}};\r\n            Layout.Add(Status);\r\n            place.Clicked=()=\u003E{try{Read();var time=ContactAuthoring.PlaceAtWrist(_motion,_draft,HoldOrientation.Value);Target.Text=string.Join(\u0022,\u0022,_draft.LocalTarget.Select(v=\u003Ev.ToString(\u0022R\u0022,CultureInfo.InvariantCulture)));Status.Text=$\u0022Manual anchor placed from the wrist at {time:F3} s.\u0022;}catch(Exception e){Status.Text=e.Message;}};\r\n            var buttons=Layout.AddRow();buttons.AddStretchCell();buttons.Add(new Button(\u0022Cancel\u0022){Clicked=Close});Save=buttons.Add(new Button.Primary(\u0022Save suggestion\u0022));\r\n            Save.Clicked=async ()=\u003E{\r\n                try{Read();if(Sliding.Value\u0026\u0026Keys.HasUnappliedChanges)throw new ArgumentException(\u0022Add or update the edited sliding key before saving.\u0022);\r\n                    if(HoldOrientation.Value\u0026\u0026_draft.LocalRotation is null)throw new ArgumentException(\u0022Use wrist at interval midpoint to place the orientation anchor.\u0022);\r\n                    if(!HoldOrientation.Value)_draft.LocalRotation=null;\r\n                    _draft.Review=ContactReview.Suggested;_draft.Reason=\u0022Manually placed/edited wrist contact; requires review against the video.\u0022;\r\n                    Save.Enabled=false;await owner.SaveContactAsync(_motion,index,_draft);await EditorPipeline.SwitchToMainThread();if(this.IsValid())Close();}\r\n                catch(Exception e){await EditorPipeline.SwitchToMainThread();if(this.IsValid()){Status.Text=e.Message;Save.Enabled=true;}}\r\n            };\r\n        }\r\n        void Read()\r\n        {\r\n            _draft.Start=double.Parse(Start.Text,CultureInfo.InvariantCulture);_draft.End=double.Parse(End.Text,CultureInfo.InvariantCulture);\r\n            _draft.LocalTarget=MotionDocument.A(Vector(Target));\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"Editor/HumanoidMocap/MocapContactTimeline.cs","FileName":"MocapContactTimeline.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":395388,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Threading.Tasks;\r\nusing Editor;\r\nusing Sandbox;\r\nusing HumanoidMocap.Mapping;\r\nusing HumanoidMocap.Motion;\r\n\r\nnamespace HumanoidMocap.Editor;\r\n\r\n// Uses the exposed Widget/Paint/Menu APIs and Theme colors used by sbox-public\u0027s\r\n// scrub bars. No MovieMaker session or engine-internal timeline dependency.\r\nsealed class MocapContactTimeline : Widget\r\n{\r\n    MotionDocument motion;\r\n    IReadOnlyList\u003CWristPositionOffset\u003E wristOffsets=Array.Empty\u003CWristPositionOffset\u003E();\r\n    double playhead;\r\n    public Action\u003Cfloat\u003E Seek { get; set; }\r\n    public Action\u003CMotionDocument,int\u003E Edit { get; set; }\r\n    public Action\u003CMotionDocument,int,ContactReview\u003E Review { get; set; }\r\n    public Action\u003CMotionDocument,int,double,double\u003E ChangeRange { get; set; }\r\n    public Action\u003CMotionDocument,WristPositionOffset\u003E EditWrist { get; set; }\r\n\r\n    public MocapContactTimeline(Widget parent):base(parent)\r\n    {\r\n        FixedHeight=36;MouseTracking=true;Visible=false;\r\n        ToolTip=\u0022Contact intervals: yellow needs review, green confirmed, gray disabled. Click to seek; double-click to edit; right-click for review and timing.\u0022;\r\n    }\r\n    public void SetMotion(MotionDocument value,IReadOnlyList\u003CWristPositionOffset\u003E edits=null)\r\n    {motion=value;wristOffsets=edits?.ToArray()??Array.Empty\u003CWristPositionOffset\u003E();Visible=value is not null\u0026\u0026(value.Contacts.Count\u003E0||wristOffsets.Count\u003E0);Update();}\r\n    public void SetPlayhead(double time)\r\n    {if(playhead==time)return;playhead=time;Update();}\r\n    float X(double time)\r\n    {\r\n        if(motion is null)return 6;\r\n        var start=motion.Frames[0].Time;var duration=motion.Frames[^1].Time-start;\r\n        return 6\u002B(float)(duration\u003E0?Math.Clamp((time-start)/duration,0,1):0)*Math.Max(1,Width-12);\r\n    }\r\n    float Fraction(float x)=\u003EMath.Clamp((x-6)/Math.Max(1,Width-12),0,1);\r\n    int Lane(ContactInterval contact)=\u003Emotion.Bones.FirstOrDefault(b=\u003Eb.Name==contact.Bone)?.Role==BoneRole.HandR?1:0;\r\n    string Side(ContactInterval contact)=\u003Emotion.Bones.FirstOrDefault(b=\u003Eb.Name==contact.Bone)?.Role switch\r\n        {BoneRole.HandL=\u003E\u0022L\u0022,BoneRole.HandR=\u003E\u0022R\u0022,_=\u003Econtact.Bone};\r\n    internal Rect ContactRect(int index)\r\n    {\r\n        var c=motion.Contacts[index];var start=X(c.Start);var end=X(c.End);\r\n        return new(start,2\u002BLane(c)*18,Math.Max(2,end-start),14);\r\n    }\r\n    int[] Hits(Vector2 position)=\u003Emotion is null?Array.Empty\u003Cint\u003E():Enumerable.Range(0,motion.Contacts.Count)\r\n        .Where(i=\u003EContactRect(i).Grow(2).IsInside(position)).ToArray();\r\n    internal Rect WristRect(int index)\r\n    {\r\n        var edit=wristOffsets[index];var start=X(edit.Start);var end=X(edit.End);\r\n        return new(start,15\u002B(edit.Hand==BoneRole.HandR?18:0),Math.Max(2,end-start),3);\r\n    }\r\n    int[] WristHits(Vector2 position)=\u003EEnumerable.Range(0,wristOffsets.Count).Where(i=\u003EWristRect(i).Grow(2).IsInside(position)).ToArray();\r\n    protected override void OnPaint()\r\n    {\r\n        Paint.ClearPen();Paint.SetBrush(Theme.WindowBackground);Paint.DrawRect(LocalRect,3);\r\n        if(motion is null)return;\r\n        for(var i=0;i\u003Cmotion.Contacts.Count;i\u002B\u002B)\r\n        {\r\n            var c=motion.Contacts[i];var rect=ContactRect(i);\r\n            var color=c.Review==ContactReview.Suggested?Theme.Yellow:c.Review==ContactReview.Confirmed?Theme.Green:Theme.TextLight;\r\n            Paint.SetPen(color,1);Paint.SetBrush(color.WithAlpha(c.Review==ContactReview.Disabled?.1f:.25f));Paint.DrawRect(rect,2);\r\n            var state=c.Review==ContactReview.Suggested?\u0022?\u0022:c.Review==ContactReview.Confirmed?\u0022\u2713\u0022:\u0022\u00D7\u0022;\r\n            var prefix=Side(c)\u002B\u0022 \u0022\u002Bstate;\r\n            var text=prefix\u002B\u0022 \u00B7 \u0022\u002Bc.Object;\r\n            if(Paint.MeasureText(text).x\u003Erect.Width-6)text=prefix;\r\n            if(Paint.MeasureText(text).x\u003C=rect.Width-6)Paint.DrawText(rect.Shrink(3,0),text,TextFlag.LeftCenter);\r\n        }\r\n        for(var i=0;i\u003CwristOffsets.Count;i\u002B\u002B)\r\n        {\r\n            Paint.ClearPen();Paint.SetBrush(wristOffsets[i].Enabled?Theme.Blue:Theme.TextLight.WithAlpha(.35f));Paint.DrawRect(WristRect(i),1);\r\n        }\r\n        Paint.SetPen(Theme.Text.WithAlpha(.8f),1);var x=X(playhead);Paint.DrawLine(new Vector2(x,0),new Vector2(x,Height));\r\n    }\r\n    protected override void OnMouseMove(MouseEvent e)\r\n    {\r\n        var hits=Hits(e.LocalPosition);var wrists=WristHits(e.LocalPosition);Cursor=CursorShape.Finger;\r\n        ToolTip=hits.Length\u002Bwrists.Length==0?\u0022Click to seek. Yellow contacts need review; green are confirmed; blue marks manual wrist correction; gray is disabled.\u0022:\r\n            string.Join(\u0022\\n\u0022,hits.Select(i=\u003E{var c=motion.Contacts[i];return $\u0022{c.Bone} \u2192 {c.Object} \u00B7 {c.Start:F3}\u2013{c.End:F3} s \u00B7 {c.Review}\u0022;})\r\n                .Concat(wrists.Select(i=\u003E{var c=wristOffsets[i];return $\u0022{(c.Hand==BoneRole.HandL?\u0022Left\u0022:\u0022Right\u0022)} wrist \u00B7 {c.Start:F3}\u2013{c.End:F3} s \u00B7 {(c.Enabled?\u0022Manual position correction\u0022:\u0022Disabled correction\u0022)}\u0022;})))\u002B\r\n            \u0022\\nClick to seek; double-click to edit; right-click to review or change timing.\u0022;\r\n    }\r\n    protected override void OnMousePress(MouseEvent e)\r\n    {\r\n        var hits=Hits(e.LocalPosition);var wrists=WristHits(e.LocalPosition);var expected=motion;\r\n        if(expected is null)return;\r\n        if(e.LeftMouseButton)\r\n        {\r\n            Seek?.Invoke(Fraction(e.LocalPosition.x));\r\n            if(e.IsDoubleClick\u0026\u0026hits.Length\u002Bwrists.Length==1)\r\n            {if(wrists.Length==1)EditWrist?.Invoke(expected,wristOffsets[wrists[0]]);else Edit?.Invoke(expected,hits[0]);}\r\n            e.Accepted=true;\r\n        }\r\n        else if(e.RightMouseButton\u0026\u0026hits.Length\u002Bwrists.Length\u003E0)\r\n        {\r\n            var menu=new Menu();var time=Math.Clamp(playhead,expected.Frames[0].Time,expected.Frames[^1].Time);\r\n            Seek?.Invoke(Fraction(X(time)));\r\n            foreach(var index in hits)\r\n            {\r\n                var c=expected.Contacts[index];var supported=new PropContactMotion(expected).UnsupportedReason(c) is null;\r\n                menu.AddHeading($\u0022{c.Bone} \u2192 {c.Object} \u00B7 {c.Review}\u0022);\r\n                menu.AddOption(\u0022Edit contact\u2026\u0022,\u0022edit\u0022,()=\u003EEdit?.Invoke(expected,index)).Enabled=supported;\r\n                menu.AddOption(\u0022Confirm\u0022,\u0022check\u0022,()=\u003EReview?.Invoke(expected,index,ContactReview.Confirmed)).Enabled=supported;\r\n                menu.AddOption(\u0022Disable\u0022,\u0022block\u0022,()=\u003EReview?.Invoke(expected,index,ContactReview.Disabled));\r\n                menu.AddOption($\u0022Start at playhead ({time:F3} s)\u0022,\u0022first_page\u0022,()=\u003EChangeRange?.Invoke(expected,index,time,c.End)).Enabled=supported\u0026\u0026time\u003Cc.End;\r\n                menu.AddOption($\u0022End at playhead ({time:F3} s)\u0022,\u0022last_page\u0022,()=\u003EChangeRange?.Invoke(expected,index,c.Start,time)).Enabled=supported\u0026\u0026time\u003Ec.Start;\r\n            }\r\n            foreach(var index in wrists)\r\n            {\r\n                var edit=wristOffsets[index];menu.AddHeading($\u0022{(edit.Hand==BoneRole.HandL?\u0022Left\u0022:\u0022Right\u0022)} wrist \u00B7 manual correction\u0022);\r\n                menu.AddOption(\u0022Edit wrist correction\u2026\u0022,\u0022edit_location\u0022,()=\u003EEditWrist?.Invoke(expected,edit));\r\n            }\r\n            menu.OpenAtCursor();e.Accepted=true;\r\n        }\r\n    }\r\n}\r\n\r\npublic sealed partial class RetargetWindow\r\n{\r\n    MocapContactTimeline _contactTimeline;\r\n    async Task ChangeContactRangeAsync(MotionDocument expected,int index,double start,double end)\r\n    {\r\n        try\r\n        {\r\n            if(expected!=_editedMotion||_processing is not null)return;\r\n            var contact=expected.Copy().Contacts[index];contact.Start=start;contact.End=end;\r\n            contact.Review=ContactReview.Suggested;contact.Reason=\u0022Interval edited on the timeline; review against the video.\u0022;\r\n            await SaveContactAsync(expected,index,contact);\r\n        }\r\n        catch(Exception error){await EditorPipeline.SwitchToMainThread();if(this.IsValid())_captureStatus.Text=error.Message;}\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"Code/HumanoidMocap/Inference/HandMotionBuilder.cs","FileName":"HandMotionBuilder.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":395388,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Numerics;\r\nusing HumanoidMocap.Mapping;\r\nusing HumanoidMocap.Maths;\r\nusing HumanoidMocap.Motion;\r\nusing HumanoidMocap.Solve;\r\nusing HumanoidMocap.Target;\r\n\r\nnamespace HumanoidMocap.Inference;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// \u003Csummary\u003EFits observed landmarks to a canonical hand skeleton. The source contains\r\n/// hands only; shoulders and arm IK belong to target retargeting.\u003C/summary\u003E\r\npublic sealed class HandMotionBuilder\r\n{\r\n    readonly TargetRig rig;\r\n    readonly XForm[] rest;\r\n    readonly int[] sourceBones;\r\n    readonly Dictionary\u003Cint,int\u003E documentIndices;\r\n    CameraObservation? previewCamera;\r\n    public MotionDocument Document { get; }\r\n    public bool SwapHands { get; set; }\r\n    public float WristPlaneWidth { get; set; }=.9f;\r\n    public float WristPlaneDepth { get; set; }=.4f;\r\n\r\n    public HandMotionBuilder(TargetRig template,string name,string video,string hash,double fps)\r\n    {\r\n        rig=template;rest=rig.Skeleton.RestWorld.Select(t=\u003Enew XForm(t.Pos/100,t.Rot)).ToArray();\r\n        sourceBones=rig.Skeleton.Bones.Where(b=\u003Erig.RoleOf(b.Index) is BoneRole.HandL or BoneRole.HandR||\r\n            rig.RoleOf(b.Index) is { } role\u0026\u0026FingerSolver.IsFingerRole(role)).Select(b=\u003Eb.Index).ToArray();\r\n        documentIndices=sourceBones.Select((source,index)=\u003E(source,index)).ToDictionary(x=\u003Ex.source,x=\u003Ex.index);\r\n        Document=new MotionDocument{Name=name,SourceVideo=video,SourceSha256=hash,SourceFps=fps,\r\n            Backend=\u0022MediaPipe hands / experimental managed C#\u0022,ModelVersion=\u0022hand_landmarker/float16/1; hand-forest-v3-authored-metacarpals; camera-framing-v1\u0022,\r\n            Space=MotionSpace.CameraRelative,MetricScaleCalibrated=false};\r\n        foreach(var source in sourceBones)\r\n        {\r\n            var role=rig.RoleOf(source);var hand=role is BoneRole.HandL or BoneRole.HandR;\r\n            var parent=hand?-1:rig.Skeleton[source].ParentIndex;\r\n            while(parent\u003E=0\u0026\u0026!documentIndices.ContainsKey(parent))parent=rig.Skeleton[parent].ParentIndex;\r\n            if(!hand\u0026\u0026parent\u003C0)throw new ArgumentException(\u0022Finger mapping must descend from a mapped hand.\u0022);\r\n            var local=parent\u003C0?new XForm(Vector3.Zero,rest[source].Rot):XForm.Compose(rest[parent].Inverse(),rest[source]);\r\n            Document.Bones.Add(new(){Name=rig.Skeleton[source].Name,Role=role,Parent=parent\u003C0?-1:documentIndices[parent],Group=hand?\u0022arms\u0022:\u0022fingers\u0022,\r\n                RestPosition=MotionDocument.A(local.Pos),RestRotation=MotionDocument.A(local.Rot)});\r\n        }\r\n        Document.Diagnostics.AddRange(new[]{\u0022Experimental landmark reconstruction. Model-port parity has not been established.\u0022,\r\n            \u0022Hand-relative 3D landmarks are reconstructed; rotations are fitted to a fixed canonical hand skeleton.\u0022,\r\n            \u0022Metacarpal rest transforms are authored template anatomy, not observed motion. They are labeled Authored while their hand is observed.\u0022,\r\n            \u0022Finger segment directions follow the landmarks. Axial twist is unmeasured and estimated by minimal swing relative to the parent segment; it is not captured finger torsion.\u0022,\r\n            \u0022Camera-relative wrist translation uses an assumed image plane, not measured depth or camera motion.\u0022,\r\n            \u0022Separated hand tracks can retain left/right identity through weak classifier disagreement. Saved handedness probabilities below 0.5 record that disagreement; no missing hand is generated.\u0022,\r\n            \u0022The source contains no shoulders or elbows. Target arm IK is estimated after reconstruction.\u0022,\r\n            \u0022Unobserved hands hold their last pose and remain labeled unobserved. No per-joint confidence is supplied.\u0022});\r\n    }\r\n    public void Add(double time,int width,int height,IReadOnlyList\u003CHandObservation\u003E observations)\r\n    {\r\n        if(width\u003C=0||height\u003C=0||!float.IsFinite(WristPlaneWidth)||!float.IsFinite(WristPlaneDepth)||WristPlaneWidth\u003C=0||WristPlaneDepth\u003C=0)\r\n            throw new ArgumentException(\u0022Invalid image dimensions or assumed wrist plane.\u0022);\r\n        var focal=width*WristPlaneDepth/WristPlaneWidth;\r\n        if(previewCamera is null)\r\n        {\r\n            previewCamera=new(){Id=\u0022video\u0022,Source=\u0022Authored preview camera matching the assumed wrist plane; not recovered video calibration\u0022,\r\n                ImageWidth=width,ImageHeight=height,Calibrated=false,Synchronized=true,\r\n                Intrinsics=new[]{focal,0,width/2f,0,focal,height/2f,0,0,1}};\r\n            Document.Cameras.Add(previewCamera);\r\n        }\r\n        else if(previewCamera.Intrinsics is {} intrinsics\u0026\u0026(previewCamera.ImageWidth!=width||previewCamera.ImageHeight!=height||intrinsics[0]!=focal))\r\n        {\r\n            // One static camera cannot describe changing image/plane geometry.\r\n            previewCamera.ImageWidth=previewCamera.ImageHeight=null;previewCamera.Intrinsics=null;\r\n            previewCamera.Source=\u0022Assumed wrist-plane geometry changes within this clip; preview camera is unspecified\u0022;\r\n        }\r\n        var previous=Document.Frames.LastOrDefault();\r\n        var frame=new MotionFrame{Time=time,\r\n            Positions=(previous?.Positions??Document.Bones.Select(b=\u003Eb.RestPosition).ToArray()).Select(p=\u003Ep.ToArray()).ToArray(),\r\n            Rotations=(previous?.Rotations??Document.Bones.Select(b=\u003Eb.RestRotation).ToArray()).Select(q=\u003Eq.ToArray()).ToArray(),\r\n            Evidence=Enumerable.Repeat(JointEvidence.Unobserved,sourceBones.Length).ToArray(),Confidence=null};\r\n        var desired=new Dictionary\u003Cint,Quaternion\u003E();\r\n        foreach(var observed in observations.GroupBy(h=\u003Eh.Side).Select(g=\u003Eg.OrderByDescending(h=\u003Eh.Presence).First()))\r\n        {\r\n            if(observed.Side is not (\u0022L\u0022 or \u0022R\u0022)||observed.ImageLandmarks.Length!=21||observed.RelativeWorldLandmarks.Length!=21)continue;\r\n            var side=SwapHands?(observed.Side==\u0022L\u0022?\u0022R\u0022:\u0022L\u0022):observed.Side;\r\n            BoneRole Role(string name)=\u003EEnum.Parse\u003CBoneRole\u003E(name\u002Bside);\r\n            if(rig.BoneForRole(Role(\u0022Hand\u0022)) is not int hand||rig.BoneForRole(Role(\u0022IndexProx\u0022)) is not int index||\r\n                rig.BoneForRole(Role(\u0022PinkyProx\u0022)) is not int pinky||rig.BoneForRole(Role(\u0022MiddleProx\u0022)) is not int middle)continue;\r\n            // MediaPipe x-right/y-down/z-away -\u003E document x-right/y-up/z-toward viewer.\r\n            var points=observed.RelativeWorldLandmarks.Select(v=\u003Enew Vector3(v.X,-v.Y,-v.Z)).ToArray();\r\n            var across=points[5]-points[17];var restAcross=rest[index].Pos-rest[pinky].Pos;\r\n            if(!TryBasis(rest[middle].Pos-rest[hand].Pos,restAcross,out var reference)||\r\n                !TryBasis(points[9]-points[0],across,out var orientation))continue;\r\n            var wrist=observed.ImageLandmarks[0];\r\n            if(!Finite(wrist))continue;\r\n            var handIndex=documentIndices[hand];\r\n            frame.Positions[handIndex]=MotionDocument.A(new Vector3((wrist.X/width-.5f)*WristPlaneWidth,\r\n                (.5f-wrist.Y/height)*WristPlaneWidth*height/width,-WristPlaneDepth));\r\n            desired[handIndex]=Quaternion.Normalize(orientation*Quaternion.Inverse(reference)*rest[hand].Rot);\r\n            frame.Evidence[handIndex]=JointEvidence.Reconstructed;\r\n            foreach(var finger in new[]{\u0022Thumb\u0022,\u0022Index\u0022,\u0022Middle\u0022,\u0022Ring\u0022,\u0022Pinky\u0022})\r\n                if(rig.BoneForRole(Role(finger\u002B\u0022Meta\u0022)) is int meta\u0026\u0026documentIndices.TryGetValue(meta,out var metaIndex))\r\n                {\r\n                    frame.Positions[metaIndex]=Document.Bones[metaIndex].RestPosition.ToArray();\r\n                    frame.Rotations[metaIndex]=Document.Bones[metaIndex].RestRotation.ToArray();\r\n                    frame.Evidence[metaIndex]=JointEvidence.Authored;\r\n                }\r\n            foreach(var (finger,start) in new[]{(\u0022Thumb\u0022,1),(\u0022Index\u0022,5),(\u0022Middle\u0022,9),(\u0022Ring\u0022,13),(\u0022Pinky\u0022,17)})\r\n            {\r\n                var parentDelta=Quaternion.Normalize(desired[handIndex]*Quaternion.Inverse(rest[hand].Rot));\r\n                var segments=new[]{\u0022Prox\u0022,\u0022Mid\u0022,\u0022Dist\u0022};\r\n                for(var k=0;k\u003C3;k\u002B\u002B)\r\n                {\r\n                    if(rig.BoneForRole(Role(finger\u002Bsegments[k])) is not int bone)continue;\r\n                    Vector3 direction;\r\n                    if(k\u003C2\u0026\u0026rig.BoneForRole(Role(finger\u002Bsegments[k\u002B1])) is int next)direction=rest[next].Pos-rest[bone].Pos;\r\n                    else if(k\u003E0\u0026\u0026rig.BoneForRole(Role(finger\u002Bsegments[k-1])) is int parent)direction=rest[bone].Pos-rest[parent].Pos;\r\n                    else continue;\r\n                    var capturedDirection=points[start\u002Bk\u002B1]-points[start\u002Bk];\r\n                    if(!Finite(direction)||!Finite(capturedDirection)||direction.LengthSquared()\u003C1e-10f||capturedDirection.LengthSquared()\u003C1e-10f)break;\r\n                    // A segment direction does not measure roll. Carry its parent\u0027s\r\n                    // frame and apply only the swing needed to match the observation.\r\n                    // Independent palm-axis bases become singular when a finger\r\n                    // points across the palm and can add a spurious 180-degree twist.\r\n                    var predictedDirection=Vector3.Transform(direction,parentDelta);\r\n                    var delta=Quaternion.Normalize(MathQ.FromTo(predictedDirection,capturedDirection)*parentDelta);\r\n                    var joint=documentIndices[bone];\r\n                    desired[joint]=Quaternion.Normalize(delta*rest[bone].Rot);\r\n                    frame.Evidence[joint]=JointEvidence.Reconstructed;\r\n                    parentDelta=delta;\r\n                }\r\n            }\r\n        }\r\n        var world=new Quaternion[sourceBones.Length];\r\n        for(var i=0;i\u003Cworld.Length;i\u002B\u002B)\r\n        {\r\n            var parent=Document.Bones[i].Parent;var parentRotation=parent\u003C0?Quaternion.Identity:world[parent];\r\n            if(desired.TryGetValue(i,out var rotation))frame.Rotations[i]=MotionDocument.A(Quaternion.Normalize(Quaternion.Inverse(parentRotation)*rotation));\r\n            world[i]=Quaternion.Normalize(parentRotation*MotionDocument.Q(frame.Rotations[i]));\r\n        }\r\n        Document.Frames.Add(frame);\r\n    }\r\n    static bool Finite(Vector3 v)=\u003Efloat.IsFinite(v.X)\u0026\u0026float.IsFinite(v.Y)\u0026\u0026float.IsFinite(v.Z);\r\n    static bool TryBasis(Vector3 direction,Vector3 across,out Quaternion rotation)\r\n    {\r\n        rotation=Quaternion.Identity;\r\n        if(!Finite(direction)||!Finite(across)||direction.LengthSquared()\u003C1e-10f)return false;\r\n        var x=Vector3.Normalize(direction);var y=across-x*Vector3.Dot(across,x);\r\n        if(y.LengthSquared()\u003C1e-10f)return false;\r\n        y=Vector3.Normalize(y);var z=Vector3.Normalize(Vector3.Cross(x,y));\r\n        rotation=Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(new Matrix4x4(x.X,x.Y,x.Z,0,y.X,y.Y,y.Z,0,z.X,z.Y,z.Z,0,0,0,0,1)));\r\n        return true;\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"Code/HumanoidMocap/Motion/CaptureGround.cs","FileName":"CaptureGround.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":395388,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing HumanoidMocap.Cleanup;\r\nusing HumanoidMocap.Mapping;\r\nusing HumanoidMocap.Maths;\r\nusing HumanoidMocap.Target;\r\n\r\nnamespace HumanoidMocap.Motion;\r\nusing Vector3 = System.Numerics.Vector3;\r\n\r\n/// \u003Csummary\u003EKeeps a body capture on the floor all the way through. The foot lock corrects floor drift only\r\n/// from detected foot contacts, and only for world-relative captures; a camera-relative capture from a camera\r\n/// that moved was grounded once, and dance with few flat-footed moments had nothing to anchor on. On a\r\n/// step-dance clip the feet rose from the floor to 45 cm over 17 seconds.\r\n///\r\n/// Within any stretch of about a second and a half somebody standing, walking or dancing puts a foot down,\r\n/// so the lower envelope of the lowest foot\u0027s height is the floor: a rolling minimum followed by a rolling\r\n/// maximum over that window (a morphological opening), which follows a slow rise exactly and drops anything\r\n/// narrower than the window, such as jumps. Only the floor\u0027s change over the clip is removed, relative to its\r\n/// lowest level, never by more than would push a foot below the floor: a capture that sits at one height the\r\n/// whole time (a stage, a placed world capture) keeps it.\u003C/summary\u003E\r\npublic static class CaptureGround\r\n{\r\n    /// \u003Csummary\u003ESeconds within which a foot is expected to touch the floor.\u003C/summary\u003E\r\n    public const double WindowSeconds = 1.5;\r\n    /// \u003Csummary\u003ECorrections smaller than this, in centimetres, leave the clip untouched.\u003C/summary\u003E\r\n    public const float MinimumCorrectionCm = 1;\r\n\r\n    /// \u003Creturns\u003EThe largest correction applied, in centimetres.\u003C/returns\u003E\r\n    public static float Apply( List\u003CXForm[]\u003E frames, TargetRig target, TargetUpAxis axis, float fps )\r\n    {\r\n        if ( frames.Count \u003C 3 || !(fps \u003E 0) ) return 0;\r\n        var rig = target.Skeleton;\r\n        var up = axis == TargetUpAxis.YUpCm ? Vector3.UnitY : Vector3.UnitZ;\r\n        var toCm = axis == TargetUpAxis.ZUpEngine ? 2.54f : 1f;\r\n        var joints = new[] { BoneRole.FootL, BoneRole.FootR, BoneRole.ToeL, BoneRole.ToeR }\r\n            .Select( target.BoneForRole ).Where( b =\u003E b is not null ).Select( b =\u003E b.Value ).ToArray();\r\n        if ( joints.Length == 0 ) return 0;\r\n        // Height of the lowest foot joint above its own rest height, per frame.\r\n        var lowest = new float[frames.Count]; var world = new XForm[rig.Count];\r\n        for ( var f = 0; f \u003C frames.Count; f\u002B\u002B )\r\n        {\r\n            FkUtil.ToWorld( frames[f], rig, world );\r\n            var h = float.PositiveInfinity;\r\n            foreach ( var j in joints ) h = Math.Min( h, Vector3.Dot( world[j].Pos, up ) - Vector3.Dot( rig.RestWorld[j].Pos, up ) );\r\n            lowest[f] = h;\r\n        }\r\n        var radius = Math.Max( 1, (int)Math.Round( WindowSeconds * fps / 2 ) );\r\n        var eroded = new float[frames.Count]; var floor = new double[frames.Count];\r\n        for ( var f = 0; f \u003C frames.Count; f\u002B\u002B )\r\n        {\r\n            var m = float.PositiveInfinity;\r\n            for ( var k = Math.Max( 0, f - radius ); k \u003C= Math.Min( frames.Count - 1, f \u002B radius ); k\u002B\u002B ) m = Math.Min( m, lowest[k] );\r\n            eroded[f] = m;\r\n        }\r\n        for ( var f = 0; f \u003C frames.Count; f\u002B\u002B )\r\n        {\r\n            var m = float.NegativeInfinity;\r\n            for ( var k = Math.Max( 0, f - radius ); k \u003C= Math.Min( frames.Count - 1, f \u002B radius ); k\u002B\u002B ) m = Math.Max( m, eroded[k] );\r\n            floor[f] = m;\r\n        }\r\n        // The rolling minimum steps as the window slides; a slow zero-phase filter leaves only the drift.\r\n        if ( frames.Count \u003E= 8 \u0026\u0026 fps \u003E 2 )\r\n        {\r\n            var (b, a) = MocapSmooth.ButterLowpass( 2, Math.Min( .5, fps * .2 ), fps );\r\n            floor = MocapSmooth.FiltFilt( b, a, floor );\r\n        }\r\n        var reference = floor.Min();\r\n        var largest = 0f;\r\n        var shifts = new float[frames.Count];\r\n        for ( var f = 0; f \u003C frames.Count; f\u002B\u002B )\r\n        {\r\n            // Only the change in floor height is removed, and never below the floor.\r\n            shifts[f] = (float)Math.Min( floor[f] - reference, Math.Max( 0, lowest[f] ) );\r\n            largest = Math.Max( largest, Math.Abs( shifts[f] ) * toCm );\r\n        }\r\n        if ( largest \u003C MinimumCorrectionCm ) return 0;\r\n        for ( var f = 0; f \u003C frames.Count; f\u002B\u002B )\r\n            for ( var bone = 0; bone \u003C rig.Count; bone\u002B\u002B )\r\n                if ( rig[bone].ParentIndex \u003C 0 ) frames[f][bone].Pos -= up * shifts[f];\r\n        return largest;\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"Code/HumanoidMocap/Motion/CapturePlacement.cs","FileName":"CapturePlacement.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":395388,"IsPrivate":false,"Code":"using System;\r\nusing System.Numerics;\r\nusing HumanoidMocap.Maths;\r\nusing HumanoidMocap.Target;\r\n\r\nnamespace HumanoidMocap.Motion;\r\nusing Vector3 = System.Numerics.Vector3;\r\n\r\n/// \u003Csummary\u003EThe same explicit camera placement for captured hands and authoritative props.\u003C/summary\u003E\r\npublic readonly record struct CapturePlacement(float Units,Quaternion Rotation,Vector3 Position)\r\n{\r\n    public static CapturePlacement ForTarget(TargetUpAxis axis,TargetCorrectionSettings settings)\r\n    {\r\n        var units=axis==TargetUpAxis.ZUpEngine?39.3700787f:100f;\r\n        var axisRotation=axis==TargetUpAxis.YUpCm?Quaternion.Identity:Quaternion.CreateFromAxisAngle(Vector3.UnitX,MathF.PI/2);\r\n        var camera=Quaternion.CreateFromYawPitchRoll(settings.CaptureCameraYawDegrees*MathF.PI/180,\r\n            settings.CaptureCameraPitchDegrees*MathF.PI/180,0);\r\n        return new(units,Quaternion.Normalize(axisRotation*camera),Vector3.Transform(settings.CaptureCameraPosition*units,axisRotation));\r\n    }\r\n    public XForm Transform(XForm capture)=\u003Enew(Vector3.Transform(capture.Pos*Units,Rotation)\u002BPosition,\r\n        Quaternion.Normalize(Rotation*capture.Rot));\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"Code/HumanoidMocap/Target/SboxBoneClassifier.cs","FileName":"SboxBoneClassifier.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":395388,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Text.RegularExpressions;\r\nusing HumanoidMocap.Mapping;\r\n\r\nnamespace HumanoidMocap.Target;\r\n\r\n/// \u003Csummary\u003E\r\n/// Name-based bone classification and role assignment rules for the s\u0026amp;box humanoid rig\r\n/// (design doc \u00A73). Used by \u003Csee cref=\u0022TargetRigGenerator\u0022/\u003E to produce the committed\r\n/// target-rig definition; consumers should read classes/roles from \u003Csee cref=\u0022TargetRig\u0022/\u003E\r\n/// rather than re-deriving them.\r\n/// \u003C/summary\u003E\r\npublic static partial class SboxBoneClassifier\r\n{\r\n    // Plain cached Regex instead of [GeneratedRegex]: the s\u0026box in-engine compiler\r\n    // does not run the regex source generator, so partial GeneratedRegex methods\r\n    // fail to compile there (\u0022must have an implementation part\u0022).\r\n    private static readonly Regex TwistSuffixRegex = new(@\u0022_twist\\d\u002B$\u0022);\r\n    private static Regex TwistSuffix() =\u003E TwistSuffixRegex;\r\n\r\n    // arm_elbow/leg_knee on the human rig; leg_glute on the legacy citizen rig.\r\n    private static readonly Regex ConstraintHelperRegex = new(@\u0022^(arm_elbow|leg_knee|leg_glute)_helper(_|$)\u0022);\r\n    private static Regex ConstraintHelper() =\u003E ConstraintHelperRegex;\r\n\r\n    private static readonly Regex IkSuffixRegex = new(@\u0022(_IK_target|_IK_attach|_ikrule)$\u0022);\r\n    private static Regex IkSuffix() =\u003E IkSuffixRegex;\r\n\r\n    private static readonly Regex AimMatrixPrefixRegex = new(@\u0022^aim_matrix_\u0022);\r\n    private static Regex AimMatrixPrefix() =\u003E AimMatrixPrefixRegex;\r\n\r\n    // Face bones on the classic citizen rig (eye_L/R, ear_L/R, face_lid_*): no canonical\r\n    // role exists for them, the solver never retargets them, and the engine\u0027s procedural\r\n    // systems (eye look-at, blinking) pose them in game.\r\n    private static readonly Regex FacePrefixRegex = new(@\u0022^(eye|ear|face)_\u0022);\r\n    private static Regex FacePrefix() =\u003E FacePrefixRegex;\r\n\r\n    // Case-insensitive twin of FacePrefixRegex for IsFaceBone: custom rigs classified by\r\n    // BoneClassRules match face names case-insensitively, and the channel decision must\r\n    // agree with that classification.\r\n    private static readonly Regex FacePrefixAnyCaseRegex = new(@\u0022^(eye|ear|face)_\u0022, RegexOptions.IgnoreCase);\r\n\r\n    /// \u003Csummary\u003E\r\n    /// True for face bones (\u003Cc\u003Eeye_*\u003C/c\u003E, \u003Cc\u003Eear_*\u003C/c\u003E, \u003Cc\u003Eface_*\u003C/c\u003E). Unlike the\r\n    /// twist/helper \u003Csee cref=\u0022BoneClass.ConstraintDriven\u0022/\u003E bones \u2014 which the model\u0027s\r\n    /// AnimConstraintList re-drives on every evaluated frame \u2014 NOTHING drives face bones in\r\n    /// a compiled sequence: the constraint list never references them and the engine\u0027s eye\r\n    /// look-at / blinking only runs in game. A face joint left channel-less in the DMX is\r\n    /// baked statically by resourcecompiler, so the eyes detach from the moving head in\r\n    /// ModelDoc (\u0022eyes out of their sockets\u0022). Retargeted clips must therefore carry\r\n    /// rest-local channels for them \u2014 exactly what the shipped fbx2dmx clips do\r\n    /// (reference: \u003Cc\u003Edev/m0/ref_idlepose.dmx\u003C/c\u003E carries \u003Cc\u003Eeye_L_p/_o\u003C/c\u003E,\r\n    /// \u003Cc\u003Eface_lid_*_p/_o\u003C/c\u003E channels).\r\n    /// \u003C/summary\u003E\r\n    public static bool IsFaceBone(string name)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(name);\r\n        return FacePrefixAnyCaseRegex.IsMatch(name);\r\n    }\r\n\r\n    /// \u003Csummary\u003EClassifies an s\u0026amp;box rig bone by name.\u003C/summary\u003E\r\n    public static BoneClass Classify(string name)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(name);\r\n\r\n        if (TwistSuffix().IsMatch(name) || ConstraintHelper().IsMatch(name) || name == \u0022neck_clothing\u0022\r\n            || FacePrefix().IsMatch(name))\r\n            return BoneClass.ConstraintDriven;\r\n\r\n        if (name == \u0022root_IK\u0022 || name == \u0022hold_L\u0022 || name == \u0022hold_R\u0022\r\n            || IkSuffix().IsMatch(name) || AimMatrixPrefix().IsMatch(name))\r\n            return BoneClass.IkBaked;\r\n\r\n        return BoneClass.Animated;\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Returns the canonical role of an s\u0026amp;box rig bone, or null when the bone carries no\r\n    /// role (every non-\u003Csee cref=\u0022BoneClass.Animated\u0022/\u003E bone, by construction).\r\n    /// \u003C/summary\u003E\r\n    public static BoneRole? RoleFor(string name)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(name);\r\n\r\n        if (Classify(name) != BoneClass.Animated)\r\n            return null;\r\n\r\n        return RoleByName.TryGetValue(name, out var role) ? role : null;\r\n    }\r\n\r\n    /// \u003Csummary\u003ERole table for the s\u0026amp;box bone names (built once, ordinal-keyed).\u003C/summary\u003E\r\n    private static readonly IReadOnlyDictionary\u003Cstring, BoneRole\u003E RoleByName = BuildRoleTable();\r\n\r\n    private static Dictionary\u003Cstring, BoneRole\u003E BuildRoleTable()\r\n    {\r\n        var table = new Dictionary\u003Cstring, BoneRole\u003E(StringComparer.Ordinal)\r\n        {\r\n            [\u0022pelvis\u0022] = BoneRole.Hips,\r\n            [\u0022spine_0\u0022] = BoneRole.Spine0,\r\n            [\u0022spine_1\u0022] = BoneRole.Spine1,\r\n            [\u0022spine_2\u0022] = BoneRole.Spine2,\r\n            [\u0022neck_0\u0022] = BoneRole.Neck,\r\n            [\u0022head\u0022] = BoneRole.Head,\r\n        };\r\n\r\n        foreach (var side in new[] { \u0022L\u0022, \u0022R\u0022 })\r\n        {\r\n            table[$\u0022clavicle_{side}\u0022] = ParseRole($\u0022Clavicle{side}\u0022);\r\n            table[$\u0022arm_upper_{side}\u0022] = ParseRole($\u0022UpperArm{side}\u0022);\r\n            table[$\u0022arm_lower_{side}\u0022] = ParseRole($\u0022LowerArm{side}\u0022);\r\n            table[$\u0022hand_{side}\u0022] = ParseRole($\u0022Hand{side}\u0022);\r\n            table[$\u0022leg_upper_{side}\u0022] = ParseRole($\u0022UpperLeg{side}\u0022);\r\n            table[$\u0022leg_lower_{side}\u0022] = ParseRole($\u0022LowerLeg{side}\u0022);\r\n            table[$\u0022ankle_{side}\u0022] = ParseRole($\u0022Foot{side}\u0022);\r\n            table[$\u0022ball_{side}\u0022] = ParseRole($\u0022Toe{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                // Segment naming on the rig: meta = metacarpal, 0/1/2 = proximal/middle/distal.\r\n                // The s\u0026box thumb has no metacarpal bone, but the rule is kept uniform so a\r\n                // hypothetical finger_thumb_meta_* would still map (the enum defines ThumbMeta*).\r\n                table[$\u0022finger_{finger}_meta_{side}\u0022] = ParseRole($\u0022{rolePrefix}Meta{side}\u0022);\r\n                table[$\u0022finger_{finger}_0_{side}\u0022] = ParseRole($\u0022{rolePrefix}Prox{side}\u0022);\r\n                table[$\u0022finger_{finger}_1_{side}\u0022] = ParseRole($\u0022{rolePrefix}Mid{side}\u0022);\r\n                table[$\u0022finger_{finger}_2_{side}\u0022] = ParseRole($\u0022{rolePrefix}Dist{side}\u0022);\r\n            }\r\n        }\r\n\r\n        return table;\r\n    }\r\n\r\n    private static BoneRole ParseRole(string name) =\u003E Enum.Parse\u003CBoneRole\u003E(name);\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"Editor/HumanoidMocap/Inference/GvhmrStatistics.cs","FileName":"GvhmrStatistics.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":395388,"IsPrivate":false,"Code":"namespace HumanoidMocap.Inference;\r\n\r\n// GVHMR ee960bb6: MM_V1_AMASS_LOCAL_BEDLAM_CAM, stats_compose.py.\r\n// Values copied verbatim; terms in Gvhmr.LICENSE.\r\ninternal static class GvhmrStatistics\r\n{\r\n    internal static readonly float[] Mean={9.6969e-01f,-5.9719e-02f,-3.7700e-02f,5.8256e-02f,9.0800e-01f,1.0972e-01f,9.7636e-01f,4.3401e-02f,4.3110e-03f,-4.3032e-02f,9.0261e-01f,1.4478e-01f,9.9288e-01f,3.5673e-03f,1.6264e-02f,-2.2260e-03f,9.3470e-01f,-2.3495e-01f,9.7147e-01f,5.2553e-02f,-9.3666e-02f,-5.4550e-02f,8.3321e-01f,-2.4246e-01f,9.7971e-01f,-3.8429e-02f,5.3575e-03f,1.5537e-02f,8.1449e-01f,-3.0926e-01f,9.9532e-01f,-9.4398e-03f,-3.8328e-02f,8.5141e-03f,9.8880e-01f,1.9976e-04f,9.5602e-01f,-3.9528e-02f,2.0017e-01f,1.0363e-02f,9.5965e-01f,1.3770e-01f,9.6223e-01f,-4.6278e-02f,-1.5177e-01f,6.6705e-02f,9.5545e-01f,1.2519e-01f,9.9767e-01f,-1.2616e-02f,-2.5442e-04f,1.1661e-02f,9.9376e-01f,-3.6222e-02f,9.9511e-01f,-1.0583e-02f,1.2130e-02f,7.6461e-03f,9.9137e-01f,2.0029e-02f,9.9295e-01f,7.2917e-03f,4.9454e-03f,-8.0286e-03f,9.9137e-01f,2.3707e-03f,9.7698e-01f,1.9943e-02f,1.3808e-03f,-2.2006e-02f,9.7375e-01f,-6.7936e-02f,9.2804e-01f,2.5005e-01f,-5.7167e-02f,-2.4047e-01f,9.4246e-01f,2.5863e-02f,9.2957e-01f,-2.1329e-01f,1.1112e-01f,2.0741e-01f,9.4876e-01f,2.9901e-02f,9.7683e-01f,-4.1210e-02f,2.3248e-03f,4.0967e-02f,9.7365e-01f,5.7309e-03f,6.4513e-01f,6.1999e-01f,-2.5469e-01f,-6.2342e-01f,6.8177e-01f,3.5524e-02f,6.6192e-01f,-5.9341e-01f,2.7136e-01f,5.9269e-01f,6.8966e-01f,3.1309e-02f,6.8946e-01f,-1.1676e-01f,-4.9859e-01f,4.0969e-02f,9.3656e-01f,-1.4875e-01f,6.2787e-01f,1.3793e-01f,5.4289e-01f,-9.1946e-02f,9.2868e-01f,-1.1927e-01f,9.3012e-01f,-8.3810e-02f,-1.1951e-01f,9.7211e-02f,8.9118e-01f,5.9887e-02f,9.3033e-01f,7.1047e-02f,7.5264e-02f,-8.0679e-02f,8.8562e-01f,4.8960e-02f,0.2310f,0.1750f,0.2931f,-0.1859f,-1.1163f,-1.1028f,-0.2573f,0.3555f,0.3732f,0.2852f,-4.9862e-03f,-8.7136e-04f,-1.4187e-03f,1.4825e-02f,-9.4419e-01f,-5.1653e-02f,3.6018e-04f,-2.2327e-04f,2.2316e-03f,-4.4879e-02f,-9.7435e-01f,1.0021e-01f,-0.0002f,-0.0006f,0.0069f};\r\n    internal static readonly float[] StandardDeviation={0.0612f,0.1390f,0.1779f,0.1415f,0.1826f,0.3268f,0.0440f,0.1382f,0.1542f,0.1348f,0.1930f,0.3272f,0.0132f,0.0801f,0.0855f,0.0729f,0.1255f,0.2238f,0.0554f,0.1088f,0.1727f,0.0939f,0.3294f,0.3559f,0.0532f,0.1082f,0.1554f,0.0768f,0.3446f,0.3407f,0.0120f,0.0650f,0.0584f,0.0632f,0.0198f,0.1335f,0.0631f,0.1250f,0.1574f,0.1047f,0.0730f,0.2091f,0.0759f,0.1241f,0.1667f,0.1112f,0.0831f,0.2185f,0.0060f,0.0441f,0.0502f,0.0441f,0.0102f,0.0946f,0.0237f,0.0722f,0.0610f,0.0738f,0.0479f,0.0949f,0.0369f,0.0943f,0.0610f,0.0966f,0.0498f,0.0729f,0.0425f,0.1001f,0.1824f,0.0972f,0.0408f,0.1887f,0.0594f,0.1842f,0.1884f,0.2020f,0.0457f,0.1018f,0.0640f,0.1990f,0.1854f,0.2133f,0.0467f,0.0910f,0.0392f,0.1049f,0.1776f,0.1037f,0.0413f,0.1945f,0.1733f,0.2612f,0.1905f,0.2963f,0.1512f,0.1861f,0.1710f,0.2663f,0.1896f,0.3135f,0.1568f,0.2219f,0.3976f,0.1594f,0.2810f,0.1855f,0.0845f,0.2398f,0.4398f,0.1629f,0.2685f,0.1990f,0.0998f,0.2556f,0.1137f,0.2837f,0.1419f,0.2761f,0.1678f,0.2973f,0.1172f,0.3010f,0.1394f,0.2910f,0.1724f,0.3039f,0.8831f,0.7965f,1.0899f,1.1788f,1.2128f,1.1081f,0.9780f,1.1434f,0.8498f,1.1462f,0.7048f,0.1713f,0.6884f,0.1548f,0.1546f,0.2403f,0.6070f,0.5355f,0.5873f,0.6285f,0.2336f,0.7675f,0.0064f,0.0070f,0.0138f};\r\n}\r\n"},{"Ident":"notpointless.chomnr_humanoid_mocap","Path":"Code/HumanoidMocap/Formats/Fbx/FbxBinaryWriter.cs","FileName":"FbxBinaryWriter.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":395388,"IsPrivate":false,"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 HumanoidMocap.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"}]}