{"TotalCount":188,"Files":[{"Ident":"notpointless.chomnr_text_to_animation","Path":"Editor/TextToAnimation/Formats/Fbx/FbxBinaryWriter.cs","FileName":"FbxBinaryWriter.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":409724,"IsPrivate":false,"Code":"// Vendored from humanoid-retargeter (Code/HumanoidRetargeter/Formats/Fbx/FbxBinaryWriter.cs).\r\n#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 TextToAnimation.Editor.Formats.Fbx;\r\n\r\n/// \u003Csummary\u003E\r\n/// Serializes an \u003Csee cref=\u0022FbxNode\u0022/\u003E tree back to binary FBX (version 7400 layout \u2014\r\n/// u32 header fields, universally readable). The inverse of\r\n/// \u003Csee cref=\u0022FbxTokenizer.Parse\u0022/\u003E: a tree parsed from a 7.x binary file and written\r\n/// here re-parses to an identical tree (arrays are written uncompressed; zlib-encoded\r\n/// inputs therefore round-trip by VALUE, not byte-for-byte).\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// Used by \u003Csee cref=\u0022FbxBindPoseFixer\u0022/\u003E to persist repaired node transforms. The footer\r\n/// is written the way Blender\u0027s exporter does: a fixed 16-byte watermark (importers treat\r\n/// it as opaque), zero padding to a 16-byte boundary, the version echo, 120 zero bytes and\r\n/// the closing magic. The FBX SDK computes a content hash here, but every consumer we\r\n/// target (s\u0026amp;box, Blender, assimp) ignores it.\r\n/// \u003C/remarks\u003E\r\npublic static class FbxBinaryWriter\r\n{\r\n    private const uint Version = 7400;\r\n\r\n    private static readonly byte[] HeaderMagic =\r\n        \u0022Kaydara FBX Binary  \\0\\x1a\\0\u0022u8.ToArray();\r\n\r\n    // Blender\u0027s fbx_binary.py FOOT_ID \u002B closing magic bytes.\r\n    private static readonly byte[] FooterWatermark =\r\n    {\r\n        0xfa, 0xbc, 0xab, 0x09, 0xd0, 0xc8, 0xd4, 0x66, 0xb1, 0x76, 0xfb, 0x83, 0x1c, 0xf7, 0x26, 0x7e,\r\n    };\r\n\r\n    private static readonly byte[] FooterMagic =\r\n    {\r\n        0xf8, 0x5a, 0x8c, 0x6a, 0xde, 0xf5, 0xd9, 0x7e, 0xec, 0xe9, 0x0c, 0x6e, 0x0c, 0xc0, 0x00, 0x00,\r\n    };\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Serializes \u003Cparamref name=\u0022root\u0022/\u003E (a virtual root whose children are the top-level\r\n    /// document nodes, as produced by \u003Csee cref=\u0022FbxTokenizer.Parse\u0022/\u003E).\r\n    /// \u003C/summary\u003E\r\n    public static byte[] Write(FbxNode root)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(root);\r\n\r\n        using var ms = new MemoryStream();\r\n        ms.Write(HeaderMagic);\r\n        WriteU32(ms, Version);\r\n\r\n        foreach (var child in root.Children)\r\n            WriteNode(ms, child);\r\n        WriteNullRecord(ms);\r\n\r\n        WriteFooter(ms);\r\n        return ms.ToArray();\r\n    }\r\n\r\n    // ------------------------------------------------------------------ nodes\r\n\r\n    private static void WriteNode(MemoryStream ms, FbxNode node)\r\n    {\r\n        long headerAt = ms.Position;\r\n        // Placeholder header: endOffset, numProps, propListLen (patched after the body).\r\n        WriteU32(ms, 0);\r\n        WriteU32(ms, (uint)node.Properties.Count);\r\n        WriteU32(ms, 0);\r\n        var nameBytes = Encoding.ASCII.GetBytes(node.Name);\r\n        if (nameBytes.Length \u003E byte.MaxValue)\r\n            throw new FormatException($\u0022FBX write: node name too long ({node.Name.Length} chars).\u0022);\r\n        ms.WriteByte((byte)nameBytes.Length);\r\n        ms.Write(nameBytes);\r\n\r\n        long propsAt = ms.Position;\r\n        foreach (var p in node.Properties)\r\n            WriteProperty(ms, p, node.Name);\r\n        long propsLen = ms.Position - propsAt;\r\n\r\n        if (node.Children.Count \u003E 0)\r\n        {\r\n            foreach (var child in node.Children)\r\n                WriteNode(ms, child);\r\n            WriteNullRecord(ms);\r\n        }\r\n\r\n        long endAt = ms.Position;\r\n        ms.Position = headerAt;\r\n        WriteU32(ms, checked((uint)endAt));\r\n        WriteU32(ms, (uint)node.Properties.Count);\r\n        WriteU32(ms, checked((uint)propsLen));\r\n        ms.Position = endAt;\r\n    }\r\n\r\n    private static void WriteNullRecord(MemoryStream ms)\r\n    {\r\n        Span\u003Cbyte\u003E zeros = stackalloc byte[13];\r\n        zeros.Clear();\r\n        ms.Write(zeros);\r\n    }\r\n\r\n    // ------------------------------------------------------------------ properties\r\n\r\n    private static void WriteProperty(MemoryStream ms, object value, string owner)\r\n    {\r\n        switch (value)\r\n        {\r\n            case short y:\r\n                ms.WriteByte((byte)\u0027Y\u0027);\r\n                WriteI16(ms, y);\r\n                break;\r\n            case bool c:\r\n                ms.WriteByte((byte)\u0027C\u0027);\r\n                ms.WriteByte(c ? (byte)1 : (byte)0);\r\n                break;\r\n            case int i:\r\n                ms.WriteByte((byte)\u0027I\u0027);\r\n                WriteI32(ms, i);\r\n                break;\r\n            case float f:\r\n                ms.WriteByte((byte)\u0027F\u0027);\r\n                WriteF32(ms, f);\r\n                break;\r\n            case double d:\r\n                ms.WriteByte((byte)\u0027D\u0027);\r\n                WriteF64(ms, d);\r\n                break;\r\n            case long l:\r\n                ms.WriteByte((byte)\u0027L\u0027);\r\n                WriteI64(ms, l);\r\n                break;\r\n\r\n            case float[] fa:\r\n                WriteArrayHeader(ms, \u0027f\u0027, fa.Length, 4);\r\n                foreach (var x in fa)\r\n                    WriteF32(ms, x);\r\n                break;\r\n            case double[] da:\r\n                WriteArrayHeader(ms, \u0027d\u0027, da.Length, 8);\r\n                foreach (var x in da)\r\n                    WriteF64(ms, x);\r\n                break;\r\n            case long[] la:\r\n                WriteArrayHeader(ms, \u0027l\u0027, la.Length, 8);\r\n                foreach (var x in la)\r\n                    WriteI64(ms, x);\r\n                break;\r\n            case int[] ia:\r\n                WriteArrayHeader(ms, \u0027i\u0027, ia.Length, 4);\r\n                foreach (var x in ia)\r\n                    WriteI32(ms, x);\r\n                break;\r\n            case bool[] ba:\r\n                WriteArrayHeader(ms, \u0027b\u0027, ba.Length, 1);\r\n                foreach (var x in ba)\r\n                    ms.WriteByte(x ? (byte)1 : (byte)0);\r\n                break;\r\n\r\n            case string s:\r\n            {\r\n                ms.WriteByte((byte)\u0027S\u0027);\r\n                var bytes = Encoding.UTF8.GetBytes(s);\r\n                WriteU32(ms, (uint)bytes.Length);\r\n                ms.Write(bytes);\r\n                break;\r\n            }\r\n            case byte[] r:\r\n                ms.WriteByte((byte)\u0027R\u0027);\r\n                WriteU32(ms, (uint)r.Length);\r\n                ms.Write(r);\r\n                break;\r\n\r\n            default:\r\n                throw new FormatException(\r\n                    $\u0022FBX write: node \u0027{owner}\u0027: unsupported property CLR type {value?.GetType().Name ?? \u0022null\u0022}.\u0022);\r\n        }\r\n    }\r\n\r\n    private static void WriteArrayHeader(MemoryStream ms, char code, int count, int elemSize)\r\n    {\r\n        ms.WriteByte((byte)code);\r\n        WriteU32(ms, (uint)count);\r\n        WriteU32(ms, 0); // encoding 0 = uncompressed\r\n        WriteU32(ms, checked((uint)(count * elemSize)));\r\n    }\r\n\r\n    // ------------------------------------------------------------------ footer\r\n\r\n    private static void WriteFooter(MemoryStream ms)\r\n    {\r\n        ms.Write(FooterWatermark);\r\n\r\n        // Zero-pad so the version echo starts 16-aligned (Blender pads at least 1 byte).\r\n        int pad = (int)(16 - ms.Position % 16);\r\n        for (int i = 0; i \u003C pad; i\u002B\u002B)\r\n            ms.WriteByte(0);\r\n\r\n        WriteU32(ms, Version);\r\n        Span\u003Cbyte\u003E zeros = stackalloc byte[120];\r\n        zeros.Clear();\r\n        ms.Write(zeros);\r\n        ms.Write(FooterMagic);\r\n    }\r\n\r\n    // ------------------------------------------------------------------ primitives\r\n\r\n    private static void WriteU32(MemoryStream ms, uint v)\r\n    {\r\n        Span\u003Cbyte\u003E b = stackalloc byte[4];\r\n        BinaryPrimitives.WriteUInt32LittleEndian(b, v);\r\n        ms.Write(b);\r\n    }\r\n\r\n    private static void WriteI16(MemoryStream ms, short v)\r\n    {\r\n        Span\u003Cbyte\u003E b = stackalloc byte[2];\r\n        BinaryPrimitives.WriteInt16LittleEndian(b, v);\r\n        ms.Write(b);\r\n    }\r\n\r\n    private static void WriteI32(MemoryStream ms, int v)\r\n    {\r\n        Span\u003Cbyte\u003E b = stackalloc byte[4];\r\n        BinaryPrimitives.WriteInt32LittleEndian(b, v);\r\n        ms.Write(b);\r\n    }\r\n\r\n    private static void WriteI64(MemoryStream ms, long v)\r\n    {\r\n        Span\u003Cbyte\u003E b = stackalloc byte[8];\r\n        BinaryPrimitives.WriteInt64LittleEndian(b, v);\r\n        ms.Write(b);\r\n    }\r\n\r\n    private static void WriteF32(MemoryStream ms, float v)\r\n    {\r\n        Span\u003Cbyte\u003E b = stackalloc byte[4];\r\n        BinaryPrimitives.WriteSingleLittleEndian(b, v);\r\n        ms.Write(b);\r\n    }\r\n\r\n    private static void WriteF64(MemoryStream ms, double v)\r\n    {\r\n        Span\u003Cbyte\u003E b = stackalloc byte[8];\r\n        BinaryPrimitives.WriteDoubleLittleEndian(b, v);\r\n        ms.Write(b);\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"Editor/TextToAnimation/Formats/Fbx/FbxMaterialColor.cs","FileName":"FbxMaterialColor.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":409724,"IsPrivate":false,"Code":"// Vendored from humanoid-retargeter (Code/HumanoidRetargeter/Formats/Fbx) - its FBX reader, used for material/texture detection.\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\n#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Linq;\r\n\r\nnamespace TextToAnimation.Editor.Formats.Fbx;\r\n\r\nusing Vector3 = System.Numerics.Vector3;\r\n\r\n/// \u003Csummary\u003EAuthored FBX colors, independent of material and texture names.\u003C/summary\u003E\r\npublic static class FbxMaterialColor\r\n{\r\n    public static Vector3? Read(FbxNode material)\r\n    {\r\n        var properties = material.Child(\u0022Properties70\u0022)?.Children;\r\n        var color = properties?.FirstOrDefault(p =\u003E p.Properties.FirstOrDefault() is \u0022DiffuseColor\u0022)\r\n            ?? properties?.FirstOrDefault(p =\u003E p.Properties.FirstOrDefault() is \u0022Diffuse\u0022);\r\n        if (color is null || color.Properties.Count \u003C 7)\r\n            return null;\r\n        var value = new Vector3(color.Prop\u003Cfloat\u003E(4), color.Prop\u003Cfloat\u003E(5), color.Prop\u003Cfloat\u003E(6));\r\n        // The legacy Diffuse property already includes DiffuseFactor.\r\n        var factor = properties?.FirstOrDefault(p =\u003E p.Properties.FirstOrDefault() is \u0022DiffuseFactor\u0022);\r\n        if (color.Properties[0] is \u0022DiffuseColor\u0022 \u0026\u0026 factor?.Properties.Count \u003E= 5)\r\n            value *= factor.Prop\u003Cfloat\u003E(4);\r\n        return float.IsFinite(value.X) \u0026\u0026 float.IsFinite(value.Y) \u0026\u0026 float.IsFinite(value.Z)\r\n            ? Vector3.Clamp(value, Vector3.Zero, Vector3.One) : null;\r\n    }\r\n\r\n    public static bool HasVertexColors(FbxNode geometry)\r\n        =\u003E geometry.ChildrenNamed(\u0022LayerElementColor\u0022).Any(layer =\u003E\r\n            layer.Child(\u0022Colors\u0022)?.Properties.FirstOrDefault() is double[] colors\r\n            \u0026\u0026 colors.Where((_, i) =\u003E i % 4 != 3).Any(c =\u003E Math.Abs(c - 1) \u003E 1e-6));\r\n}\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"Editor/TextToAnimation/Formats/Fbx/FbxNode.cs","FileName":"FbxNode.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":409724,"IsPrivate":false,"Code":"// Vendored from humanoid-retargeter (Code/HumanoidRetargeter/Formats/Fbx) - its FBX reader, used for material/texture detection.\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\n#nullable enable annotations\r\n\r\nnamespace TextToAnimation.Editor.Formats.Fbx;\r\n\r\n/// \u003Csummary\u003E\r\n/// A single node of an FBX document tree (binary or ASCII): a name, a flat list of\r\n/// typed properties, and nested child nodes.\r\n///\r\n/// Property values are stored as the closest CLR type to what the file contained:\r\n/// \u003Clist type=\u0022bullet\u0022\u003E\r\n///   \u003Citem\u003E\u003Cc\u003Eshort\u003C/c\u003E (\u0027Y\u0027), \u003Cc\u003Ebool\u003C/c\u003E (\u0027C\u0027), \u003Cc\u003Eint\u003C/c\u003E (\u0027I\u0027), \u003Cc\u003Efloat\u003C/c\u003E (\u0027F\u0027),\r\n///         \u003Cc\u003Edouble\u003C/c\u003E (\u0027D\u0027), \u003Cc\u003Elong\u003C/c\u003E (\u0027L\u0027)\u003C/item\u003E\r\n///   \u003Citem\u003E\u003Cc\u003Efloat[]\u003C/c\u003E (\u0027f\u0027), \u003Cc\u003Edouble[]\u003C/c\u003E (\u0027d\u0027), \u003Cc\u003Elong[]\u003C/c\u003E (\u0027l\u0027),\r\n///         \u003Cc\u003Eint[]\u003C/c\u003E (\u0027i\u0027), \u003Cc\u003Ebool[]\u003C/c\u003E-as-\u003Cc\u003Ebyte[]\u003C/c\u003E (\u0027b\u0027)\u003C/item\u003E\r\n///   \u003Citem\u003E\u003Cc\u003Estring\u003C/c\u003E (\u0027S\u0027 \u2014 kept raw, may contain the \u003Cc\u003E\\x00\\x01\u003C/c\u003E name/class\r\n///         separator; see \u003Csee cref=\u0022SplitName\u0022/\u003E), \u003Cc\u003Ebyte[]\u003C/c\u003E (\u0027R\u0027)\u003C/item\u003E\r\n/// \u003C/list\u003E\r\n/// ASCII files store numbers only as \u003Cc\u003Elong\u003C/c\u003E / \u003Cc\u003Edouble\u003C/c\u003E (and arrays as\r\n/// \u003Cc\u003Elong[]\u003C/c\u003E / \u003Cc\u003Edouble[]\u003C/c\u003E), so the typed accessors below convert tolerantly.\r\n/// \u003C/summary\u003E\r\npublic sealed class FbxNode\r\n{\r\n    public string Name { get; }\r\n    public List\u003Cobject\u003E Properties { get; } = new();\r\n    public List\u003CFbxNode\u003E Children { get; } = new();\r\n\r\n    public FbxNode(string name) =\u003E Name = name;\r\n\r\n    /// \u003Csummary\u003EFirst child with the given name, or null.\u003C/summary\u003E\r\n    public FbxNode? Child(string name)\r\n    {\r\n        foreach (var c in Children)\r\n            if (c.Name == name)\r\n                return c;\r\n        return null;\r\n    }\r\n\r\n    /// \u003Csummary\u003EAll children with the given name, in document order.\u003C/summary\u003E\r\n    public IEnumerable\u003CFbxNode\u003E ChildrenNamed(string name)\r\n    {\r\n        foreach (var c in Children)\r\n            if (c.Name == name)\r\n                yield return c;\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Property \u003Cparamref name=\u0022i\u0022/\u003E converted to \u003Ctypeparamref name=\u0022T\u0022/\u003E.\r\n    /// Numeric scalars convert tolerantly across widths (e.g. an \u0027I\u0027 i32 read as long);\r\n    /// anything else must match the stored type exactly.\r\n    /// \u003C/summary\u003E\r\n    public T Prop\u003CT\u003E(int i)\r\n    {\r\n        object v = RawProp(i);\r\n        if (v is T t)\r\n            return t;\r\n\r\n        var target = typeof(T);\r\n        // s\u0026box whitelist: Type.IsPrimitive is banned; enumerate the convertible targets.\r\n        if (v is IConvertible \u0026\u0026 (ConvertTargets.Contains(target) || target == typeof(string)))\r\n        {\r\n            try\r\n            {\r\n                return (T)Convert.ChangeType(v, target, System.Globalization.CultureInfo.InvariantCulture);\r\n            }\r\n            // ArithmeticException covers OverflowException, which is not s\u0026box-whitelisted\r\n            catch (Exception ex) when (ex is InvalidCastException or ArithmeticException or FormatException)\r\n            {\r\n                throw new FormatException(\r\n                    $\u0022FBX node \u0027{Name}\u0027: property {i} is {v.GetType().Name}, not convertible to {target.Name}.\u0022, ex);\r\n            }\r\n        }\r\n\r\n        throw new FormatException(\r\n            $\u0022FBX node \u0027{Name}\u0027: property {i} is {v.GetType().Name}, expected {target.Name}.\u0022);\r\n    }\r\n\r\n    /// \u003Csummary\u003EProperty \u003Cparamref name=\u0022i\u0022/\u003E as a double array (converts f/l/i/b arrays).\u003C/summary\u003E\r\n    public double[] AsDoubleArray(int i) =\u003E RawProp(i) switch\r\n    {\r\n        double[] d =\u003E d,\r\n        float[] f =\u003E Array.ConvertAll(f, x =\u003E (double)x),\r\n        long[] l =\u003E Array.ConvertAll(l, x =\u003E (double)x),\r\n        int[] n =\u003E Array.ConvertAll(n, x =\u003E (double)x),\r\n        byte[] b =\u003E Array.ConvertAll(b, x =\u003E (double)x),\r\n        bool[] o =\u003E Array.ConvertAll(o, x =\u003E x ? 1.0 : 0.0),\r\n        var v =\u003E throw TypeError(i, v, \u0022double[]\u0022),\r\n    };\r\n\r\n    /// \u003Csummary\u003EProperty \u003Cparamref name=\u0022i\u0022/\u003E as a float array (converts d/l/i/b arrays).\u003C/summary\u003E\r\n    public float[] AsFloatArray(int i) =\u003E RawProp(i) switch\r\n    {\r\n        float[] f =\u003E f,\r\n        double[] d =\u003E Array.ConvertAll(d, x =\u003E (float)x),\r\n        long[] l =\u003E Array.ConvertAll(l, x =\u003E (float)x),\r\n        int[] n =\u003E Array.ConvertAll(n, x =\u003E (float)x),\r\n        byte[] b =\u003E Array.ConvertAll(b, x =\u003E (float)x),\r\n        bool[] o =\u003E Array.ConvertAll(o, x =\u003E x ? 1f : 0f),\r\n        var v =\u003E throw TypeError(i, v, \u0022float[]\u0022),\r\n    };\r\n\r\n    /// \u003Csummary\u003EProperty \u003Cparamref name=\u0022i\u0022/\u003E as a long array (converts i/b; d/f if integral).\u003C/summary\u003E\r\n    public long[] AsLongArray(int i) =\u003E RawProp(i) switch\r\n    {\r\n        long[] l =\u003E l,\r\n        int[] n =\u003E Array.ConvertAll(n, x =\u003E (long)x),\r\n        byte[] b =\u003E Array.ConvertAll(b, x =\u003E (long)x),\r\n        bool[] o =\u003E Array.ConvertAll(o, x =\u003E x ? 1L : 0L),\r\n        double[] d =\u003E Array.ConvertAll(d, x =\u003E checked((long)x)),\r\n        float[] f =\u003E Array.ConvertAll(f, x =\u003E checked((long)x)),\r\n        var v =\u003E throw TypeError(i, v, \u0022long[]\u0022),\r\n    };\r\n\r\n    /// \u003Csummary\u003EProperty \u003Cparamref name=\u0022i\u0022/\u003E as an int array (converts b; l/d/f narrowing-checked).\u003C/summary\u003E\r\n    public int[] AsIntArray(int i) =\u003E RawProp(i) switch\r\n    {\r\n        int[] n =\u003E n,\r\n        long[] l =\u003E Array.ConvertAll(l, x =\u003E checked((int)x)),\r\n        byte[] b =\u003E Array.ConvertAll(b, x =\u003E (int)x),\r\n        bool[] o =\u003E Array.ConvertAll(o, x =\u003E x ? 1 : 0),\r\n        double[] d =\u003E Array.ConvertAll(d, x =\u003E checked((int)x)),\r\n        float[] f =\u003E Array.ConvertAll(f, x =\u003E checked((int)x)),\r\n        var v =\u003E throw TypeError(i, v, \u0022int[]\u0022),\r\n    };\r\n\r\n    /// \u003Csummary\u003EProperty \u003Cparamref name=\u0022i\u0022/\u003E as raw bytes (\u0027R\u0027 blobs or \u0027b\u0027 bool arrays).\u003C/summary\u003E\r\n    public byte[] AsByteArray(int i) =\u003E RawProp(i) switch\r\n    {\r\n        byte[] b =\u003E b,\r\n        var v =\u003E throw TypeError(i, v, \u0022byte[]\u0022),\r\n    };\r\n\r\n    /// \u003Csummary\u003EProperty \u003Cparamref name=\u0022i\u0022/\u003E as a string (raw \u0027S\u0027 content, separators intact).\u003C/summary\u003E\r\n    public string AsString(int i) =\u003E RawProp(i) switch\r\n    {\r\n        string s =\u003E s,\r\n        var v =\u003E throw TypeError(i, v, \u0022string\u0022),\r\n    };\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Splits an FBX object name into (name, class).\r\n    /// Binary files store \u003Cc\u003E\u0022Name\\x00\\x01Class\u0022\u003C/c\u003E (e.g. \u003Cc\u003E\u0022mixamorig:Hips\\x00\\x01Model\u0022\u003C/c\u003E);\r\n    /// ASCII files store \u003Cc\u003E\u0022Class::Name\u0022\u003C/c\u003E (e.g. \u003Cc\u003E\u0022Model::pelvis\u0022\u003C/c\u003E).\r\n    /// A plain string with neither separator yields (name, \u0022\u0022).\r\n    /// \u003C/summary\u003E\r\n    public static (string Name, string Class) SplitName(string raw)\r\n    {\r\n        int bin = raw.IndexOf(\u0022\\0\\x01\u0022, StringComparison.Ordinal);\r\n        if (bin \u003E= 0)\r\n            return (raw[..bin], raw[(bin \u002B 2)..]);\r\n\r\n        int ascii = raw.IndexOf(\u0022::\u0022, StringComparison.Ordinal);\r\n        if (ascii \u003E= 0)\r\n            return (raw[(ascii \u002B 2)..], raw[..ascii]);\r\n\r\n        return (raw, \u0022\u0022);\r\n    }\r\n\r\n    /// \u003Csummary\u003EPrimitive scalar types \u003Csee cref=\u0022Prop{T}\u0022/\u003E converts to (whitelist-safe IsPrimitive substitute).\u003C/summary\u003E\r\n    private static readonly HashSet\u003CType\u003E ConvertTargets = new()\r\n    {\r\n        typeof(bool), typeof(byte), typeof(sbyte), typeof(short), typeof(ushort),\r\n        typeof(int), typeof(uint), typeof(long), typeof(ulong),\r\n        typeof(float), typeof(double), typeof(char),\r\n    };\r\n\r\n    private object RawProp(int i)\r\n    {\r\n        if (i \u003C 0 || i \u003E= Properties.Count)\r\n            throw new FormatException(\r\n                $\u0022FBX node \u0027{Name}\u0027: property index {i} out of range (has {Properties.Count}).\u0022);\r\n        return Properties[i];\r\n    }\r\n\r\n    private FormatException TypeError(int i, object v, string wanted) =\u003E\r\n        new($\u0022FBX node \u0027{Name}\u0027: property {i} is {v.GetType().Name}, expected {wanted}.\u0022);\r\n}\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"Editor/TextToAnimation/Inference/Runtime/TorchCheckpoint.cs","FileName":"TorchCheckpoint.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":409724,"IsPrivate":false,"Code":"#nullable enable\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.IO.Compression;\r\nusing System.Linq;\r\nusing System.Text;\r\nusing System.Threading;\r\n\r\nnamespace TextToAnimation.Editor.Inference.Runtime;\r\n\r\n/// \u003Csummary\u003EReads tensor data from the pinned PyTorch ZIP checkpoint format without Python,\r\n/// importing modules, invoking pickle callables, or deserializing executable objects.\u003C/summary\u003E\r\npublic sealed partial class TorchCheckpoint : IDisposable\r\n{\r\n    public sealed record TensorInfo(string Name,string Storage,string Dtype,long StorageLength,long Offset,int[] Shape,long[] Stride);\r\n    internal sealed record Symbol(string Module,string Name);\r\n    internal sealed record StorageRef(string Key,string Dtype,long Count);\r\n    internal sealed record TensorRef(StorageRef Storage,long Offset,int[] Shape,long[] Stride);\r\n    readonly ZipArchive archive;\r\n    readonly string prefix;\r\n    readonly Dictionary\u003Cstring,TensorInfo\u003E tensors=new(StringComparer.Ordinal);\r\n    public IReadOnlyDictionary\u003Cstring,TensorInfo\u003E Tensors=\u003Etensors;\r\n    const int MaximumMetadataBytes=32*1024*1024;\r\n    const long MaximumTensorElements=1_000_000_000;\r\n\r\n    public TorchCheckpoint(string path,string? stateKey=null)\r\n    {\r\n        archive=ZipFile.OpenRead(path);\r\n        try\r\n        {\r\n            var entries=archive.Entries.Where(e=\u003Ee.FullName.EndsWith(\u0022/data.pkl\u0022,StringComparison.Ordinal)).ToArray();\r\n            if(entries.Length!=1)throw new InvalidDataException(\u0022Expected exactly one PyTorch data.pkl entry.\u0022);\r\n            var entry=entries[0];prefix=entry.FullName.Substring(0,entry.FullName.Length-8);\r\n            if(entry.Length\u003EMaximumMetadataBytes)throw new InvalidDataException(\u0022Checkpoint metadata exceeds the 32 MiB limit.\u0022);\r\n            if(archive.GetEntry(prefix\u002B\u0022byteorder\u0022) is { } byteorder)\r\n            {using var r=new StreamReader(byteorder.Open());if(r.ReadToEnd().Trim()!=\u0022little\u0022)throw new NotSupportedException(\u0022Only little-endian checkpoints are supported.\u0022);}\r\n            using var source=entry.Open();using var memory=new MemoryStream();source.CopyTo(memory);\r\n            var root=new DataReader(memory.ToArray()).Read() as Dictionary\u003Cobject,object?\u003E ?? throw new InvalidDataException(\u0022Checkpoint root must be a dictionary.\u0022);\r\n            // Some inference checkpoints (including ACE) keep separately named\r\n            // component dictionaries. Select one explicitly; never traverse optimizer state.\r\n            var key=stateKey??(root.ContainsKey(\u0022state_dict\u0022)?\u0022state_dict\u0022:null);\r\n            var state=key is null?root:root.TryGetValue(key,out var value)?value as Dictionary\u003Cobject,object?\u003E:null;\r\n            if(state is null)throw new InvalidDataException($\u0022Checkpoint component \u0027{key}\u0027 is missing or is not a dictionary.\u0022);\r\n            foreach(var pair in state)\r\n            {\r\n                if(pair.Value is not TensorRef t)continue;\r\n                if(pair.Key is not string name)throw new InvalidDataException(\u0022Tensor names must be strings.\u0022);\r\n                var info=new TensorInfo(name,t.Storage.Key,t.Storage.Dtype,t.Storage.Count,t.Offset,t.Shape,t.Stride);\r\n                Validate(info);tensors.Add(name,info);\r\n            }\r\n            if(tensors.Count==0)throw new InvalidDataException(\u0022No tensor weights in checkpoint.\u0022);\r\n        }\r\n        catch{archive.Dispose();throw;}\r\n    }\r\n    static long Count(TensorInfo tensor)\r\n    {long count=1;foreach(var d in tensor.Shape){if(d\u003C0)throw new InvalidDataException(\u0022Negative tensor dimension.\u0022);count=checked(count*d);if(count\u003EMaximumTensorElements)throw new InvalidDataException(\u0022Tensor exceeds element limit.\u0022);}return count;}\r\n    void Validate(TensorInfo tensor)\r\n    {\r\n        if(tensor.Shape.Length\u003E8||tensor.Shape.Length!=tensor.Stride.Length||tensor.Offset\u003C0||tensor.StorageLength\u003C0||tensor.StorageLength\u003EMaximumTensorElements)\r\n            throw new InvalidDataException(\u0022Invalid tensor dimensions/storage.\u0022);\r\n        if(tensor.Storage.Length==0||tensor.Storage.Any(c=\u003E!char.IsAsciiDigit(c)))throw new InvalidDataException(\u0022Unexpected tensor storage key.\u0022);\r\n        var count=Count(tensor);long last=tensor.Offset;\r\n        for(var i=0;i\u003Ctensor.Shape.Length;i\u002B\u002B)\r\n        {if(tensor.Stride[i]\u003C0)throw new InvalidDataException(\u0022Negative tensor stride.\u0022);last=checked(last\u002BMath.Max(0,tensor.Shape[i]-1)*tensor.Stride[i]);}\r\n        if(count\u003E0\u0026\u0026last\u003E=tensor.StorageLength)throw new InvalidDataException(\u0022Tensor view exceeds its storage.\u0022);\r\n        var storage=archive.GetEntry(prefix\u002B\u0022data/\u0022\u002Btensor.Storage)??throw new InvalidDataException(\u0022Missing tensor storage.\u0022);\r\n        if(storage.Length!=checked(tensor.StorageLength*ElementSize(tensor.Dtype)))throw new InvalidDataException(\u0022Tensor storage byte length mismatch.\u0022);\r\n    }\r\n    static int ElementSize(string dtype)=\u003Edtype switch\r\n    {\u0022FloatStorage\u0022 or \u0022IntStorage\u0022=\u003E4,\u0022DoubleStorage\u0022 or \u0022LongStorage\u0022=\u003E8,\u0022HalfStorage\u0022 or \u0022BFloat16Storage\u0022=\u003E2,\u0022ByteStorage\u0022 or \u0022BoolStorage\u0022=\u003E1,_=\u003Ethrow new NotSupportedException(\u0022Unsupported checkpoint dtype: \u0022\u002Bdtype)};\r\n\r\n    /// \u003Csummary\u003EMaterializes one tensor, respecting noncontiguous views. Never loads the whole\r\n    /// checkpoint into memory. Callers choose which model\u0027s tensors to retain.\u003C/summary\u003E\r\n    public float[] ReadFloat(string name,CancellationToken cancellation=default)\r\n    {\r\n        var tensor=tensors.TryGetValue(name,out var found)?found:throw new KeyNotFoundException(name);\r\n        var count=checked((int)Count(tensor));var bytes=checked((int)(tensor.StorageLength*ElementSize(tensor.Dtype)));\r\n        cancellation.ThrowIfCancellationRequested();var storage=new byte[bytes];\r\n        using(var input=archive.GetEntry(prefix\u002B\u0022data/\u0022\u002Btensor.Storage)!.Open())\r\n        {\r\n            var offset=0;while(offset\u003Cstorage.Length){cancellation.ThrowIfCancellationRequested();var n=input.Read(storage,offset,Math.Min(65536,storage.Length-offset));if(n==0)throw new EndOfStreamException();offset\u002B=n;}\r\n        }\r\n        var output=new float[count];var elementSize=ElementSize(tensor.Dtype);\r\n        long contiguousStride=1;var contiguous=true;\r\n        for(var dim=tensor.Shape.Length-1;dim\u003E=0;dim--)\r\n        {if(tensor.Shape[dim]\u003E1\u0026\u0026tensor.Stride[dim]!=contiguousStride)contiguous=false;contiguousStride*=tensor.Shape[dim];}\r\n        if(contiguous\u0026\u0026tensor.Dtype==\u0022FloatStorage\u0022\u0026\u0026BitConverter.IsLittleEndian)\r\n        {\r\n            cancellation.ThrowIfCancellationRequested();\r\n            Buffer.BlockCopy(storage,checked((int)(tensor.Offset*4)),output,0,checked(count*4));\r\n            cancellation.ThrowIfCancellationRequested();return output;\r\n        }\r\n        for(var i=0;i\u003Ccount;i\u002B\u002B)\r\n        {\r\n            if((i\u002616383)==0)cancellation.ThrowIfCancellationRequested();\r\n            long index=tensor.Offset;var remainder=i;\r\n            for(var dim=tensor.Shape.Length-1;dim\u003E=0;dim--){index\u002B=(remainder%tensor.Shape[dim])*tensor.Stride[dim];remainder/=tensor.Shape[dim];}\r\n            var at=checked((int)(index*elementSize));\r\n            output[i]=tensor.Dtype switch\r\n            {\r\n                \u0022FloatStorage\u0022=\u003EBitConverter.ToSingle(storage,at),\u0022DoubleStorage\u0022=\u003E(float)BitConverter.ToDouble(storage,at),\r\n                \u0022HalfStorage\u0022=\u003E(float)BitConverter.UInt16BitsToHalf(BitConverter.ToUInt16(storage,at)),\r\n                \u0022BFloat16Storage\u0022=\u003EBitConverter.Int32BitsToSingle(BitConverter.ToUInt16(storage,at)\u003C\u003C16),\r\n                \u0022IntStorage\u0022=\u003EBitConverter.ToInt32(storage,at),\u0022LongStorage\u0022=\u003EBitConverter.ToInt64(storage,at),\r\n                \u0022ByteStorage\u0022 or \u0022BoolStorage\u0022=\u003Estorage[at],_=\u003Ethrow new NotSupportedException(tensor.Dtype)\r\n            };\r\n        }\r\n        return output;\r\n    }\r\n    public void Dispose()=\u003Earchive.Dispose();\r\n\r\n    internal sealed class DataReader\r\n    {\r\n        readonly BinaryReader reader;\r\n        readonly List\u003Cobject?\u003E stack=new();\r\n        readonly Dictionary\u003Cint,object?\u003E memo=new();\r\n        static readonly object Mark=new();\r\n        int operations;\r\n        public int Position=\u003Echecked((int)reader.BaseStream.Position);\r\n        public DataReader(byte[] bytes,int offset=0){reader=new BinaryReader(new MemoryStream(bytes),Encoding.UTF8);reader.BaseStream.Position=offset;}\r\n        object? Pop(){if(stack.Count==0)throw new InvalidDataException(\u0022Empty pickle stack.\u0022);var value=stack[^1];stack.RemoveAt(stack.Count-1);return value;}\r\n        object? Peek()=\u003Estack.Count==0?throw new InvalidDataException(\u0022Empty pickle stack.\u0022):stack[^1];\r\n        object?[] MarkItems()\r\n        {\r\n            var index=stack.LastIndexOf(Mark);if(index\u003C0)throw new InvalidDataException(\u0022Missing pickle mark.\u0022);\r\n            var values=stack.Skip(index\u002B1).ToArray();stack.RemoveRange(index,stack.Count-index);return values;\r\n        }\r\n        string Line()\r\n        {var bytes=new List\u003Cbyte\u003E();byte b;while((b=reader.ReadByte())!=10){if(bytes.Count\u003E=4096)throw new InvalidDataException(\u0022Pickle identifier too long.\u0022);bytes.Add(b);}return Encoding.UTF8.GetString(bytes.ToArray());}\r\n        string Text(int length)\r\n        {if(length\u003C0||length\u003EMaximumMetadataBytes||length\u003Ereader.BaseStream.Length-reader.BaseStream.Position)throw new InvalidDataException(\u0022Invalid pickle string length.\u0022);return Encoding.UTF8.GetString(reader.ReadBytes(length));}\r\n        static long Integer(object? value)=\u003Evalue switch{int i=\u003Ei,long l=\u003El,_=\u003Ethrow new InvalidDataException(\u0022Expected integer.\u0022)};\r\n        static object?[] Tuple(object? value)=\u003Evalue as object?[]??throw new InvalidDataException(\u0022Expected tuple.\u0022);\r\n        static Dictionary\u003Cobject,object?\u003E Dict(object? value)=\u003Evalue as Dictionary\u003Cobject,object?\u003E??throw new InvalidDataException(\u0022Expected dictionary.\u0022);\r\n        static void Set(Dictionary\u003Cobject,object?\u003E dictionary,object? key,object? value)\r\n        {\r\n            // Training checkpoints contain integer-keyed optimizer states. Keep those\r\n            // as inert data, with numeric keys distinct from strings such as \u00220\u0022.\r\n            var normalized=key switch{string s=\u003E(object)s,int i=\u003E(long)i,long l=\u003El,_=\u003Ethrow new InvalidDataException(\u0022Only string and integer dictionary keys are supported.\u0022)};\r\n            dictionary[normalized]=value;\r\n        }\r\n        public object? Read(bool requireEnd=true)\r\n        {\r\n            while(reader.BaseStream.Position\u003Creader.BaseStream.Length)\r\n            {\r\n                if(\u002B\u002Boperations\u003E2_000_000||stack.Count\u003E100_000||memo.Count\u003E100_000)throw new InvalidDataException(\u0022Checkpoint metadata complexity limit.\u0022);\r\n                var op=reader.ReadByte();\r\n                switch(op)\r\n                {\r\n                    case 0x80:var protocol=reader.ReadByte();if(protocol\u003E5)throw new NotSupportedException(\u0022Pickle protocol \u0022\u002Bprotocol);break;\r\n                    case (byte)\u0027.\u0027:if(stack.Count!=1||(requireEnd\u0026\u0026reader.BaseStream.Position!=reader.BaseStream.Length))throw new InvalidDataException(\u0022Invalid pickle termination.\u0022);return Pop();\r\n                    case (byte)\u0027(\u0027:stack.Add(Mark);break;\r\n                    case (byte)\u0027)\u0027:stack.Add(System.Array.Empty\u003Cobject?\u003E());break;\r\n                    case (byte)\u0027}\u0027:stack.Add(new Dictionary\u003Cobject,object?\u003E());break;\r\n                    case (byte)\u0027]\u0027:stack.Add(new List\u003Cobject?\u003E());break;\r\n                    case (byte)\u0027N\u0027:stack.Add(null);break;\r\n                    case 0x88:stack.Add(true);break;\r\n                    case 0x89:stack.Add(false);break;\r\n                    case (byte)\u0027K\u0027:stack.Add((int)reader.ReadByte());break;\r\n                    case (byte)\u0027M\u0027:stack.Add((int)reader.ReadUInt16());break;\r\n                    case (byte)\u0027J\u0027:stack.Add(reader.ReadInt32());break;\r\n                    case (byte)\u0027G\u0027:var f=reader.ReadBytes(8);if(f.Length!=8)throw new EndOfStreamException();System.Array.Reverse(f);stack.Add(BitConverter.ToDouble(f));break;\r\n                    case (byte)\u0027X\u0027:stack.Add(Text(reader.ReadInt32()));break;\r\n                    case 0x8c:stack.Add(Text(reader.ReadByte()));break;\r\n                    case 0x95:var frame=reader.ReadUInt64();if(frame\u003E(ulong)(reader.BaseStream.Length-reader.BaseStream.Position))throw new InvalidDataException(\u0022Truncated pickle frame.\u0022);break;\r\n                    case (byte)\u0027q\u0027:memo[reader.ReadByte()]=Peek();break;\r\n                    case (byte)\u0027r\u0027:memo[reader.ReadInt32()]=Peek();break;\r\n                    case 0x94:memo[memo.Count]=Peek();break;\r\n                    case (byte)\u0027h\u0027:stack.Add(memo[reader.ReadByte()]);break;\r\n                    case (byte)\u0027j\u0027:stack.Add(memo[reader.ReadInt32()]);break;\r\n                    case (byte)\u0027t\u0027:stack.Add(MarkItems());break;\r\n                    case 0x85:stack.Add(new[]{Pop()});break;\r\n                    case 0x86:var second=Pop();stack.Add(new[]{Pop(),second});break;\r\n                    case 0x87:var third=Pop();second=Pop();stack.Add(new[]{Pop(),second,third});break;\r\n                    case (byte)\u0027s\u0027:var value=Pop();var key=Pop();Set(Dict(Peek()),key,value);break;\r\n                    case (byte)\u0027u\u0027:var pairs=MarkItems();if(pairs.Length%2!=0)throw new InvalidDataException(\u0022Odd dictionary entries.\u0022);var d=Dict(Peek());for(var i=0;i\u003Cpairs.Length;i\u002B=2)Set(d,pairs[i],pairs[i\u002B1]);break;\r\n                    case (byte)\u0027a\u0027:value=Pop();((List\u003Cobject?\u003E)Peek()!).Add(value);break;\r\n                    case (byte)\u0027e\u0027:var items=MarkItems();((List\u003Cobject?\u003E)Peek()!).AddRange(items);break;\r\n                    case (byte)\u0027c\u0027:stack.Add(AllowedSymbol(Line(),Line()));break;\r\n                    case 0x93:var name=Pop() as string??throw new InvalidDataException();var module=Pop() as string??throw new InvalidDataException();stack.Add(AllowedSymbol(module,name));break;\r\n                    case (byte)\u0027Q\u0027:\r\n                        var storage=Tuple(Pop());\r\n                        if(storage.Length\u003C5||storage[0] as string!=\u0022storage\u0022||storage[1] is not Symbol type||type.Module!=\u0022torch\u0022||storage[2] is not string storageKey)\r\n                            throw new InvalidDataException(\u0022Unsupported persistent pickle reference.\u0022);\r\n                        if(storage.Length\u003E5\u0026\u0026storage[5] is not null)throw new NotSupportedException(\u0022Legacy storage aliases are not supported.\u0022);\r\n                        stack.Add(new StorageRef(storageKey,type.Name,Integer(storage[4])));break;\r\n                    case (byte)\u0027R\u0027:\r\n                        var arguments=Tuple(Pop());var symbol=Pop() as Symbol??throw new InvalidDataException(\u0022Unsupported pickle callable.\u0022);\r\n                        if(symbol==new Symbol(\u0022collections\u0022,\u0022OrderedDict\u0022)\u0026\u0026arguments.Length==0)stack.Add(new Dictionary\u003Cobject,object?\u003E());\r\n                        else if(symbol==new Symbol(\u0022collections\u0022,\u0022Counter\u0022)\u0026\u0026arguments.Length\u003C=1)\r\n                            stack.Add(arguments.Length==0?new Dictionary\u003Cobject,object?\u003E():new Dictionary\u003Cobject,object?\u003E(Dict(arguments[0])));\r\n                        else if(symbol.Module==\u0022torch._utils\u0022\u0026\u0026(symbol.Name==\u0022_rebuild_tensor_v2\u0022||symbol.Name==\u0022_rebuild_tensor\u0022)\u0026\u0026arguments.Length\u003E=4\u0026\u0026arguments[0] is StorageRef sr)\r\n                            stack.Add(new TensorRef(sr,Integer(arguments[1]),Tuple(arguments[2]).Select(x=\u003Echecked((int)Integer(x))).ToArray(),Tuple(arguments[3]).Select(Integer).ToArray()));\r\n                        else throw new InvalidDataException(\u0022Unsupported checkpoint construction: \u0022\u002Bsymbol);\r\n                        break;\r\n                    case (byte)\u0027b\u0027:\r\n                        var metadata=Dict(Pop());_ = Dict(Peek());\r\n                        if(metadata.Keys.Any(k=\u003E!Equals(k,\u0022_metadata\u0022)))throw new InvalidDataException(\u0022Unsupported checkpoint object state.\u0022);\r\n                        break;\r\n                    default:throw new NotSupportedException($\u0022Checkpoint pickle opcode 0x{op:X2} at {reader.BaseStream.Position-1} is unsupported.\u0022);\r\n                }\r\n            }\r\n            throw new EndOfStreamException(\u0022Missing pickle STOP.\u0022);\r\n        }\r\n        static Symbol AllowedSymbol(string module,string name)\r\n        {\r\n            if(module==\u0022collections\u0022\u0026\u0026(name==\u0022OrderedDict\u0022||name==\u0022Counter\u0022)||module==\u0022torch._utils\u0022\u0026\u0026(name==\u0022_rebuild_tensor_v2\u0022||name==\u0022_rebuild_tensor\u0022))return new(module,name);\r\n            if(module==\u0022torch\u0022){_ = ElementSize(name);return new(module,name);}\r\n            throw new InvalidDataException(\u0022Executable/unsupported checkpoint symbol rejected: \u0022\u002Bmodule\u002B\u0022.\u0022\u002Bname);\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"Editor/TextToAnimation/Inference/UniMate/T5TextEncoder.cs","FileName":"T5TextEncoder.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":409724,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Concurrent;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Threading;\r\nusing TextToAnimation.Editor.Inference.Onnx;\r\n\r\nnamespace TextToAnimation.Editor.Inference.UniMate;\r\n\r\n/// \u003Csummary\u003E\r\n/// flan-t5-base encoder (ONNX, run by the managed runtime) with UniMate\u0027s pooling: the mean of\r\n/// \u003Cc\u003Elast_hidden_state\u003C/c\u003E over the non-padding tokens. The empty prompt pools to zero, which is the\r\n/// unconditional (classifier-free) branch. Results are cached per text.\r\n/// \u003C/summary\u003E\r\npublic sealed class T5TextEncoder\r\n{\r\n\tpublic const int Width = 768;\r\n\treadonly OnnxSession _session;\r\n\treadonly T5Tokenizer _tokenizer;\r\n\treadonly ConcurrentDictionary\u003Cstring, float[]\u003E _cache = new( StringComparer.Ordinal );\r\n\treadonly object _runLock = new();\r\n\r\n\tpublic T5TextEncoder( OnnxSession session, T5Tokenizer tokenizer )\r\n\t{\r\n\t\t_session = session;\r\n\t\t_tokenizer = tokenizer;\r\n\t}\r\n\r\n\tpublic T5Tokenizer Tokenizer =\u003E _tokenizer;\r\n\r\n\t/// \u003Csummary\u003EPooled 768-d embedding of a text.\u003C/summary\u003E\r\n\tpublic float[] Encode( string text, CancellationToken token = default )\r\n\t{\r\n\t\ttext ??= \u0022\u0022;\r\n\t\tif ( text.Length == 0 ) return new float[Width];\r\n\t\tif ( _cache.TryGetValue( text, out var cached ) ) return cached;\r\n\t\tvar ids = _tokenizer.Encode( text );\r\n\t\tvar input = Tensor.Int64( new[] { 1, ids.Count }, ids.Select( i =\u003E (long)i ).ToArray() );\r\n\t\tvar mask = Tensor.Int64( new[] { 1, ids.Count }, Enumerable.Repeat( 1L, ids.Count ).ToArray() );\r\n\t\tTensor hidden;\r\n\t\tlock ( _runLock )\r\n\t\t\thidden = _session.Run( new Dictionary\u003Cstring, Tensor\u003E { [\u0022input_ids\u0022] = input, [\u0022attention_mask\u0022] = mask }, token )[\u0022last_hidden_state\u0022];\r\n\t\tvar pooled = new float[Width];\r\n\t\tfor ( var t = 0; t \u003C ids.Count; t\u002B\u002B )\r\n\t\t\tfor ( var c = 0; c \u003C Width; c\u002B\u002B ) pooled[c] \u002B= hidden.F[t * Width \u002B c];\r\n\t\tfor ( var c = 0; c \u003C Width; c\u002B\u002B ) pooled[c] /= ids.Count;\r\n\t\t_cache[text] = pooled;\r\n\t\treturn pooled;\r\n\t}\r\n}\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"Editor/TextToAnimation/Inference/UniMate/UniMateModel.cs","FileName":"UniMateModel.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":409724,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing System.Threading;\r\nusing TextToAnimation.Editor.Inference.Onnx;\r\n\r\nnamespace TextToAnimation.Editor.Inference.UniMate;\r\n\r\n/// \u003Csummary\u003EFiles of an installed UniMate model.\u003C/summary\u003E\r\npublic sealed record UniMateFiles( string Directory )\r\n{\r\n\tpublic string WeightBlob =\u003E Path.Combine( Directory, $\u0022unimate_v2_g{UniMateGraphBuilder.FormatVersion}.weights\u0022 );\r\n\tpublic string T5Encoder =\u003E Path.Combine( Directory, \u0022t5_encoder.onnx\u0022 );\r\n\tpublic string T5Tokenizer =\u003E Path.Combine( Directory, \u0022t5_tokenizer.json\u0022 );\r\n\tpublic string Graphs =\u003E Path.Combine( Directory, $\u0022graphs_g{UniMateGraphBuilder.FormatVersion}\u0022 );\r\n}\r\n\r\n/// \u003Csummary\u003EConditioning tensors of one skeleton, computed once (the prepare graph\u0027s outputs).\u003C/summary\u003E\r\npublic sealed class PreparedSkeleton\r\n{\r\n\tpublic required UniMateSkeleton Skeleton { get; init; }\r\n\tpublic required Dictionary\u003Cstring, Tensor\u003E Tensors { get; init; }\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// ODE integrators for the flow: Euler (1 model call per step, 1st order), Heun (2 calls, 2nd order) and\r\n/// Adams-Bashforth 2 (1 call per step, 2nd order: reuses the previous step\u0027s velocity).\r\n/// Dopri5: upstream\u0027s text-to-motion sampler (Sampler.sample_ode: torchdiffeq dopri5, adaptive, rtol 1e-3,\r\n/// atol 1e-6, t from 0 to 1) - integrates the flow to convergence.\r\n/// \u003C/summary\u003E\r\npublic enum Integrator { Euler, Heun, AdamsBashforth2, Dopri5 }\r\n\r\n/// \u003Csummary\u003ESampling settings.\u003C/summary\u003E\r\npublic sealed class SampleSettings\r\n{\r\n\tpublic int Steps { get; init; } = 24;\r\n\tpublic float Guidance { get; init; } = 3f;\r\n\tpublic Integrator Method { get; init; } = Integrator.Euler;\r\n\t/// \u003Csummary\u003E\r\n\t/// Time-grid shift (1 = uniform). Above 1 the steps crowd towards the noisy start of the flow, where the\r\n\t/// velocity changes fastest: t\u0027 = s t / (1 \u002B (s - 1) t).\r\n\t/// \u003C/summary\u003E\r\n\tpublic float TimeShift { get; init; } = 1f;\r\n\t/// \u003Csummary\u003EStart time of the flow (0 = pure noise; \u0026gt;0 starts from a noised copy of \u003Csee cref=\u0022Known\u0022/\u003E: variations).\u003C/summary\u003E\r\n\tpublic float StartTime { get; init; }\r\n\t/// \u003Csummary\u003EKnown normalised motion (J,12,T) for constraints / variations.\u003C/summary\u003E\r\n\tpublic float[] Known { get; init; }\r\n\t/// \u003Csummary\u003E(J,12,T) mask of values held to \u003Csee cref=\u0022Known\u0022/\u003E (replacement sampling).\u003C/summary\u003E\r\n\tpublic bool[] Keep { get; init; }\r\n\t/// \u003Csummary\u003EDopri5 tolerances (upstream\u0027s sample_ode defaults).\u003C/summary\u003E\r\n\tpublic double RelativeTolerance { get; init; } = 1e-3;\r\n\tpublic double AbsoluteTolerance { get; init; } = 1e-6;\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The UniMate model running on the managed ONNX runtime: the flan-t5 text encoder plus the denoiser\u0027s\r\n/// prepare/step graphs (generated in C# per skeleton from the shared weight blob), and the flow-matching\r\n/// sampler with classifier-free guidance and replacement constraints.\r\n/// \u003C/summary\u003E\r\npublic sealed class UniMateModel\r\n{\r\n\tpublic const int Frames = 60;\r\n\tpublic const float Fps = 30f;\r\n\tconst int Batch = 2; // conditional \u002B unconditional rows\r\n\r\n\treadonly UniMateFiles _files;\r\n\treadonly WeightBlob _blob;\r\n\treadonly UniMateArch _arch = UniMateArch.V2;\r\n\tpublic T5TextEncoder Text { get; }\r\n\treadonly Dictionary\u003Cint, (OnnxSession Prepare, OnnxSession Step)\u003E _graphs = new();\r\n\treadonly object _lock = new();\r\n\r\n\tUniMateModel( UniMateFiles files, WeightBlob blob, T5TextEncoder text )\r\n\t{\r\n\t\t_files = files;\r\n\t\t_blob = blob;\r\n\t\tText = text;\r\n\t}\r\n\r\n\tpublic static UniMateModel Load( UniMateFiles files, CancellationToken token = default )\r\n\t{\r\n\t\tif ( !WeightBlob.Exists( files.WeightBlob ) ) throw new FileNotFoundException( \u0022The UniMate weights are not prepared.\u0022, files.WeightBlob );\r\n\t\tvar blob = WeightBlob.Open( files.WeightBlob );\r\n\t\ttoken.ThrowIfCancellationRequested();\r\n\t\tvar text = new T5TextEncoder( OnnxSession.Load( files.T5Encoder, token ), T5Tokenizer.Load( files.T5Tokenizer ) );\r\n\t\treturn new UniMateModel( files, blob, text );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Writes the shared weight blob from checkpoint weights (EMA, PyTorch names): every weight in the layout the\r\n\t/// graphs use. Done once at install; afterwards graphs for any skeleton are generated without the checkpoint.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void WriteWeights( IReadOnlyDictionary\u003Cstring, Runtime.WeightTensor\u003E weights, string blobPath )\r\n\t{\r\n\t\tusing var blob = WeightBlob.Create( blobPath );\r\n\t\tvar builder = new UniMateGraphBuilder( weights, UniMateArch.V2, blob );\r\n\t\tbuilder.BuildPrepare( 22, null, null );\r\n\t\tbuilder.BuildStep( Batch, 22, Frames, null, null );\r\n\t\tblob.Commit();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EBuilds (or reuses) the graphs for a joint count; graph files are cached on disk.\u003C/summary\u003E\r\n\t(OnnxSession Prepare, OnnxSession Step) Graphs( int joints, CancellationToken token )\r\n\t{\r\n\t\tlock ( _lock )\r\n\t\t{\r\n\t\t\tif ( _graphs.TryGetValue( joints, out var g ) ) return g;\r\n\t\t\tDirectory.CreateDirectory( _files.Graphs );\r\n\t\t\tvar prepPath = Path.Combine( _files.Graphs, $\u0022prepare_j{joints}.onnx\u0022 );\r\n\t\t\tvar stepPath = Path.Combine( _files.Graphs, $\u0022step_b{Batch}_j{joints}_t{Frames}.onnx\u0022 );\r\n\t\t\tvar builder = new UniMateGraphBuilder( null, _arch, _blob );\r\n\t\t\tif ( !File.Exists( prepPath ) ) WriteAtomic( prepPath, builder.BuildPrepare( joints, null, null ) );\r\n\t\t\tif ( !File.Exists( stepPath ) ) WriteAtomic( stepPath, builder.BuildStep( Batch, joints, Frames, null, null ) );\r\n\t\t\tOnnxSession Load( string path )\r\n\t\t\t{\r\n\t\t\t\tvar model = OnnxModel.Parse( File.ReadAllBytes( path ) );\r\n\t\t\t\tmodel.BaseDirectory = _files.Directory; // the weight blob lives in the model folder\r\n\t\t\t\treturn new OnnxSession( model, token );\r\n\t\t\t}\r\n\t\t\t// keep at most two skeleton sizes in memory (the step graph holds ~300 MB of weights)\r\n\t\t\tif ( _graphs.Count \u003E= 2 )\r\n\t\t\t{\r\n\t\t\t\tvar evicted = _graphs.Keys.First();\r\n\t\t\t\t_graphs.Remove( evicted );\r\n\t\t\t\tif ( _gpu.Remove( evicted, out var program ) ) program.Dispose();\r\n\t\t\t}\r\n\t\t\tg = (Load( prepPath ), Load( stepPath ));\r\n\t\t\t_graphs[joints] = g;\r\n\t\t\treturn g;\r\n\t\t}\r\n\t}\r\n\r\n\tstatic void WriteAtomic( string path, byte[] bytes )\r\n\t{\r\n\t\tFile.WriteAllBytes( path \u002B \u0022.tmp\u0022, bytes );\r\n\t\tFile.Move( path \u002B \u0022.tmp\u0022, path, overwrite: true );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ERuns the prepare graph for a skeleton (joint names are embedded with T5).\u003C/summary\u003E\r\n\tpublic PreparedSkeleton Prepare( UniMateSkeleton s, UniMateStats stats, CancellationToken token )\r\n\t\t=\u003E Prepare( s, ConditioningInputs( s, stats, token ), token );\r\n\r\n\t/// \u003Csummary\u003ERuns the prepare graph on given conditioning inputs (see \u003Csee cref=\u0022ConditioningInputs\u0022/\u003E).\u003C/summary\u003E\r\n\tpublic PreparedSkeleton Prepare( UniMateSkeleton s, Dictionary\u003Cstring, Tensor\u003E conditioning, CancellationToken token )\r\n\t{\r\n\t\tvar (prepare, _) = Graphs( s.Count, token );\r\n\t\tvar outputs = prepare.Run( conditioning, token );\r\n\t\treturn new PreparedSkeleton { Skeleton = s, Tensors = outputs };\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The skeleton conditioning the network gets (upstream create_sample_condition: normalised T-pose features with\r\n\t/// identity rotations, the parents\u0027 copies, relations, graph distances, depths, spectral features, joint-name\r\n\t/// embeddings).\r\n\t/// \u003C/summary\u003E\r\n\tpublic Dictionary\u003Cstring, Tensor\u003E ConditioningInputs( UniMateSkeleton s, UniMateStats stats, CancellationToken token )\r\n\t{\r\n\t\tvar J = s.Count;\r\n\t\tvar tpos = new float[J * 12]; var parentRow = new float[J * 12];\r\n\t\tfor ( var j = 0; j \u003C J; j\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar row = new double[12];\r\n\t\t\trow[0] = s.TPose[j].X; row[1] = s.TPose[j].Y; row[2] = s.TPose[j].Z;\r\n\t\t\trow[3] = 1; row[7] = 1; // identity 6D\r\n\t\t\tfor ( var c = 0; c \u003C 12; c\u002B\u002B ) tpos[j * 12 \u002B c] = stats.Normalize( j, c, row[c] );\r\n\t\t}\r\n\t\tfor ( var j = 0; j \u003C J; j\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar src = s.Parents[j] \u003C 0 ? j : s.Parents[j];\r\n\t\t\tArray.Copy( tpos, src * 12, parentRow, j * 12, 12 );\r\n\t\t}\r\n\t\tvar rel = new long[J * J]; var dist = new long[J * J];\r\n\t\tfor ( var i = 0; i \u003C J; i\u002B\u002B ) for ( var k = 0; k \u003C J; k\u002B\u002B ) { rel[i * J \u002B k] = s.Relations[i, k]; dist[i * J \u002B k] = s.GraphDist[i, k]; }\r\n\t\tvar spec = new float[J * 8];\r\n\t\tfor ( var j = 0; j \u003C J; j\u002B\u002B ) for ( var c = 0; c \u003C 8; c\u002B\u002B ) spec[j * 8 \u002B c] = s.Spectral[j, c];\r\n\t\tvar names = new float[J * T5TextEncoder.Width];\r\n\t\tfor ( var j = 0; j \u003C J; j\u002B\u002B ) Array.Copy( Text.Encode( s.CleanNames[j], token ), 0, names, j * T5TextEncoder.Width, T5TextEncoder.Width );\r\n\t\treturn new Dictionary\u003Cstring, Tensor\u003E\r\n\t\t{\r\n\t\t\t[\u0022tpos\u0022] = Tensor.Float( new[] { J, 12 }, tpos ),\r\n\t\t\t[\u0022tpos_parent\u0022] = Tensor.Float( new[] { J, 12 }, parentRow ),\r\n\t\t\t[\u0022joint_rel\u0022] = Tensor.Int64( new[] { J, J }, rel ),\r\n\t\t\t[\u0022graph_dist\u0022] = Tensor.Int64( new[] { J, J }, dist ),\r\n\t\t\t[\u0022depth\u0022] = Tensor.Int64( new[] { J }, s.Depths.ToArray() ),\r\n\t\t\t[\u0022spectral\u0022] = Tensor.Float( new[] { J, 8 }, spec ),\r\n\t\t\t[\u0022name_emb\u0022] = Tensor.Float( new[] { J, T5TextEncoder.Width }, names ),\r\n\t\t};\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Flow-matching sample: x(0) = noise, dx/dt = v(x,t) with classifier-free guidance, integrated to t = 1.\r\n\t/// Values under \u003Csee cref=\u0022SampleSettings.Keep\u0022/\u003E follow the known motion along the interpolant\r\n\t/// (replacement sampling, as upstream). Returns the normalised motion (J,12,T).\r\n\t/// \u003C/summary\u003E\r\n\tpublic float[] Sample( PreparedSkeleton prep, float[] caption, float[] noise, SampleSettings settings,\r\n\t\tAction\u003Cfloat\u003E progress, CancellationToken token )\r\n\t{\r\n\t\tvar J = prep.Skeleton.Count;\r\n\t\tvar n = J * 12 * Frames;\r\n\t\tif ( noise.Length != n ) throw new ArgumentException( \u0022Noise has the wrong size.\u0022 );\r\n\t\tvar (_, step) = Graphs( J, token );\r\n\t\tvar feed = new Dictionary\u003Cstring, Tensor\u003E( prep.Tensors );\r\n\t\tvar captions = new float[Batch * T5TextEncoder.Width];\r\n\t\tArray.Copy( caption, captions, T5TextEncoder.Width ); // row 1 stays zero: unconditional\r\n\t\tfeed[\u0022caption_emb\u0022] = Tensor.Float( new[] { Batch, T5TextEncoder.Width }, captions );\r\n\r\n\t\tvar steps = Math.Max( 1, settings.Steps );\r\n\t\tvar t0 = Math.Clamp( settings.StartTime, 0f, 0.98f );\r\n\t\tvar x = new float[n];\r\n\t\tif ( t0 \u003E 0 \u0026\u0026 settings.Known is { } known0 )\r\n\t\t\tfor ( var i = 0; i \u003C n; i\u002B\u002B ) x[i] = t0 * known0[i] \u002B (1 - t0) * noise[i];\r\n\t\telse Array.Copy( noise, x, n );\r\n\t\tReplace( x, t0 );\r\n\r\n\t\tfloat[] Velocity( float[] state, float t )\r\n\t\t{\r\n\t\t\tvar input = new float[Batch * n];\r\n\t\t\tArray.Copy( state, 0, input, 0, n );\r\n\t\t\tArray.Copy( state, 0, input, n, n );\r\n\t\t\tfeed[\u0022x\u0022] = Tensor.Float( new[] { Batch, J, 12, Frames }, input );\r\n\t\t\tfeed[\u0022t\u0022] = Tensor.Float( new[] { Batch }, new[] { t, t } );\r\n\t\t\tvar v = RunStep( J, step, feed, token );\r\n\t\t\tvar result = new float[n];\r\n\t\t\tvar g = settings.Guidance;\r\n\t\t\tfor ( var i = 0; i \u003C n; i\u002B\u002B ) result[i] = v[n \u002B i] \u002B g * (v[i] - v[n \u002B i]); // v_u \u002B g (v_c - v_u)\r\n\t\t\treturn result;\r\n\t\t}\r\n\r\n\t\tvoid Replace( float[] state, float t )\r\n\t\t{\r\n\t\t\tif ( settings.Keep is not { } keep || settings.Known is not { } k ) return;\r\n\t\t\tfor ( var i = 0; i \u003C n; i\u002B\u002B )\r\n\t\t\t\tif ( keep[i] ) state[i] = (1 - t) * noise[i] \u002B t * k[i];\r\n\t\t}\r\n\r\n\t\tif ( settings.Method == Integrator.Dopri5 )\r\n\t\t{\r\n\t\t\tif ( settings.Keep is not null || t0 \u003E 0 ) throw new ArgumentException( \u0022Dopri5 samples from noise without constraints (upstream\u0027s text-to-motion).\u0022 );\r\n\t\t\treturn Dopri5( x, Velocity, settings.RelativeTolerance, settings.AbsoluteTolerance, progress, token );\r\n\t\t}\r\n\r\n\t\t// time grid from t0 to 1 (optionally shifted towards the start)\r\n\t\tvar grid = new float[steps \u002B 1];\r\n\t\tvar shift = settings.TimeShift \u003E 0f ? settings.TimeShift : 1f;\r\n\t\tfor ( var k = 0; k \u003C= steps; k\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar u = (float)k / steps;\r\n\t\t\tvar w = shift == 1f ? u : shift * u / (1f \u002B (shift - 1f) * u);\r\n\t\t\tgrid[k] = t0 \u002B (1f - t0) * w;\r\n\t\t}\r\n\t\tfloat[] previousV = null;\r\n\t\tvar previousDt = 0f;\r\n\t\tfor ( var s = 0; s \u003C steps; s\u002B\u002B )\r\n\t\t{\r\n\t\t\ttoken.ThrowIfCancellationRequested();\r\n\t\t\tvar t = grid[s];\r\n\t\t\tvar dt = grid[s \u002B 1] - t;\r\n\t\t\tvar v1 = Velocity( x, t );\r\n\t\t\tif ( settings.Method == Integrator.Heun \u0026\u0026 s \u003C steps - 1 )\r\n\t\t\t{\r\n\t\t\t\tvar xp = new float[n];\r\n\t\t\t\tfor ( var i = 0; i \u003C n; i\u002B\u002B ) xp[i] = x[i] \u002B dt * v1[i];\r\n\t\t\t\tReplace( xp, t \u002B dt );\r\n\t\t\t\tvar v2 = Velocity( xp, t \u002B dt );\r\n\t\t\t\tfor ( var i = 0; i \u003C n; i\u002B\u002B ) x[i] \u002B= dt * 0.5f * (v1[i] \u002B v2[i]);\r\n\t\t\t}\r\n\t\t\telse if ( settings.Method == Integrator.AdamsBashforth2 \u0026\u0026 previousV is not null )\r\n\t\t\t{\r\n\t\t\t\t// variable-step AB2: extrapolate the velocity to the middle of the step from the last two\r\n\t\t\t\tvar r = dt / (2f * previousDt);\r\n\t\t\t\tfor ( var i = 0; i \u003C n; i\u002B\u002B ) x[i] \u002B= dt * ((1f \u002B r) * v1[i] - r * previousV[i]);\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tfor ( var i = 0; i \u003C n; i\u002B\u002B ) x[i] \u002B= dt * v1[i];\r\n\t\t\t}\r\n\t\t\tpreviousV = v1;\r\n\t\t\tpreviousDt = dt;\r\n\t\t\tReplace( x, t \u002B dt );\r\n\t\t\tprogress?.Invoke( (s \u002B 1f) / steps );\r\n\t\t}\r\n\t\treturn x;\r\n\t}\r\n\r\n\treadonly Dictionary\u003Cint, IGpuProgram\u003E _gpu = new();\r\n\treadonly HashSet\u003Cint\u003E _gpuRefused = new();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// One network call: on the GPU when a program for this skeleton size exists; otherwise on the CPU, and the first\r\n\t/// CPU call is traced into a GPU plan (shapes are fixed per size) which is then checked against that call\u0027s\r\n\t/// result before later calls use it. Any GPU failure falls back to the CPU for good.\r\n\t/// \u003C/summary\u003E\r\n\tfloat[] RunStep( int joints, OnnxSession step, Dictionary\u003Cstring, Tensor\u003E feed, CancellationToken token )\r\n\t{\r\n\t\tIGpuProgram program;\r\n\t\tlock ( _lock ) _gpu.TryGetValue( joints, out program );\r\n\t\tif ( program is not null \u0026\u0026 GpuAcceleration.Enabled )\r\n\t\t{\r\n\t\t\ttry { return program.Run( feed )[\u0022v\u0022]; }\r\n\t\t\tcatch ( Exception e ) when ( e is not OperationCanceledException )\r\n\t\t\t{\r\n\t\t\t\tDisableGpu( joints, $\u0022The GPU run failed ({e.Message}); using the CPU.\u0022 );\r\n\t\t\t}\r\n\t\t}\r\n\t\tvar tryGpu = GpuAcceleration.Enabled \u0026\u0026 GpuAcceleration.Compiler is not null;\r\n\t\tlock ( _lock ) tryGpu \u0026= !_gpuRefused.Contains( joints ) \u0026\u0026 !_gpu.ContainsKey( joints );\r\n\t\tif ( !tryGpu ) return step.Run( feed, token )[\u0022v\u0022].F;\r\n\r\n\t\tvar recorder = new GpuPlan.Recorder();\r\n\t\tDictionary\u003Cstring, Tensor\u003E cpu;\r\n\t\tlock ( step ) // the trace hook belongs to the session\r\n\t\t{\r\n\t\t\tstep.Trace = recorder.Record;\r\n\t\t\ttry { cpu = step.Run( feed, token ); }\r\n\t\t\tfinally { step.Trace = null; }\r\n\t\t}\r\n\t\tvar v = cpu[\u0022v\u0022].F;\r\n\t\tvar plan = GpuPlan.Build( step, recorder );\r\n\t\tif ( plan is null ) { Refuse( joints, $\u0022No GPU plan: {GpuPlan.LastRefusal}.\u0022 ); return v; }\r\n\t\ttry\r\n\t\t{\r\n\t\t\tprogram = GpuAcceleration.Compiler( plan, step );\r\n\t\t\tvar check = program.Run( feed )[\u0022v\u0022];\r\n\t\t\tvar worst = 0f;\r\n\t\t\tfor ( var i = 0; i \u003C v.Length; i\u002B\u002B ) worst = MathF.Max( worst, MathF.Abs( check[i] - v[i] ) );\r\n\t\t\tif ( !(worst \u003C= GpuAcceleration.AgreementTolerance) )\r\n\t\t\t{\r\n\t\t\t\t// find the first kernel that disagrees (one more traced CPU run keeps every value)\r\n\t\t\t\tvar values = new GpuPlan.Recorder { KeepValues = true };\r\n\t\t\t\tlock ( step )\r\n\t\t\t\t{\r\n\t\t\t\t\tstep.Trace = values.Record;\r\n\t\t\t\t\ttry { step.Run( feed, token ); }\r\n\t\t\t\t\tfinally { step.Trace = null; }\r\n\t\t\t\t}\r\n\t\t\t\tvar where = program.Diagnose( feed, values.Values );\r\n\t\t\t\tprogram.Dispose();\r\n\t\t\t\tRefuse( joints, $\u0022The GPU result differs from the CPU by {worst:G3}; using the CPU. First difference: {where ?? \u0022none found\u0022}\u0022 );\r\n\t\t\t\treturn v;\r\n\t\t\t}\r\n\t\t\tlock ( _lock ) _gpu[joints] = program;\r\n\t\t\tGpuAcceleration.Status = null;\r\n\t\t}\r\n\t\tcatch ( Exception e ) when ( e is not OperationCanceledException )\r\n\t\t{\r\n\t\t\tprogram?.Dispose();\r\n\t\t\tRefuse( joints, $\u0022The GPU couldn\u0027t be used ({e.Message}); using the CPU.\u0022 );\r\n\t\t}\r\n\t\treturn v;\r\n\t}\r\n\r\n\tvoid Refuse( int joints, string why )\r\n\t{\r\n\t\tlock ( _lock ) _gpuRefused.Add( joints );\r\n\t\tGpuAcceleration.Status = why;\r\n\t}\r\n\r\n\tvoid DisableGpu( int joints, string why )\r\n\t{\r\n\t\tlock ( _lock )\r\n\t\t{\r\n\t\t\tif ( _gpu.Remove( joints, out var p ) ) p.Dispose();\r\n\t\t\t_gpuRefused.Add( joints );\r\n\t\t}\r\n\t\tGpuAcceleration.Status = why;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// torchdiffeq\u0027s dopri5 (Dormand-Prince 5(4) with its step control, initial step and 4th-order interpolation), as\r\n\t/// upstream\u0027s Sampler.sample_ode calls it: from t = 0 to 1, RMS error norm, safety 0.9, growth at most 10x,\r\n\t/// shrink at most 5x (none after an accepted step). The last step may pass t = 1; the result is interpolated there.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static float[] Dopri5( float[] y0, Func\u003Cfloat[], float, float[]\u003E f, double rtol, double atol, Action\u003Cfloat\u003E progress, CancellationToken token )\r\n\t{\r\n\t\tvar n = y0.Length;\r\n\t\tdouble[] A = { 1 / 5.0, 3 / 10.0, 4 / 5.0, 8 / 9.0, 1.0, 1.0 };\r\n\t\tdouble[][] B =\r\n\t\t{\r\n\t\t\tnew[] { 1 / 5.0 },\r\n\t\t\tnew[] { 3 / 40.0, 9 / 40.0 },\r\n\t\t\tnew[] { 44 / 45.0, -56 / 15.0, 32 / 9.0 },\r\n\t\t\tnew[] { 19372 / 6561.0, -25360 / 2187.0, 64448 / 6561.0, -212 / 729.0 },\r\n\t\t\tnew[] { 9017 / 3168.0, -355 / 33.0, 46732 / 5247.0, 49 / 176.0, -5103 / 18656.0 },\r\n\t\t\tnew[] { 35 / 384.0, 0, 500 / 1113.0, 125 / 192.0, -2187 / 6784.0, 11 / 84.0 },\r\n\t\t};\r\n\t\tdouble[] E = { 35 / 384.0 - 1951 / 21600.0, 0, 500 / 1113.0 - 22642 / 50085.0, 125 / 192.0 - 451 / 720.0,\r\n\t\t\t-2187 / 6784.0 - -12231 / 42400.0, 11 / 84.0 - 649 / 6300.0, -1.0 / 60.0 };\r\n\t\tdouble[] Mid = { 6025192743 / 30085553152.0 / 2, 0, 51252292925 / 65400821598.0 / 2, -2691868925 / 45128329728.0 / 2,\r\n\t\t\t187940372067 / 1594534317056.0 / 2, -1776094331 / 19743644256.0 / 2, 11237099 / 235043384.0 / 2 };\r\n\t\tconst double Safety = 0.9, IFactor = 10, DFactor = 0.2;\r\n\t\tconst int Order = 5;\r\n\t\tdouble Rms( Func\u003Cint, double\u003E v ) { var s = 0.0; for ( var i = 0; i \u003C n; i\u002B\u002B ) { var x = v( i ); s \u002B= x * x; } return Math.Sqrt( s / n ); }\r\n\r\n\t\t// initial step (Hairer, Norsett \u0026 Wanner II.4), with the solver\u0027s order - 1 as torchdiffeq passes it\r\n\t\tvar fy0 = f( y0, 0f );\r\n\t\tvar d0 = Rms( i =\u003E y0[i] / (atol \u002B Math.Abs( y0[i] ) * rtol ) );\r\n\t\tvar d1 = Rms( i =\u003E fy0[i] / (atol \u002B Math.Abs( y0[i] ) * rtol ) );\r\n\t\tvar h0 = d0 \u003C 1e-5 || d1 \u003C 1e-5 ? 1e-6 : 0.01 * d0 / d1;\r\n\t\tvar probe = new float[n];\r\n\t\tfor ( var i = 0; i \u003C n; i\u002B\u002B ) probe[i] = (float)(y0[i] \u002B h0 * fy0[i]);\r\n\t\tvar fProbe = f( probe, (float)h0 );\r\n\t\tvar d2 = Rms( i =\u003E (fProbe[i] - fy0[i]) / (atol \u002B Math.Abs( y0[i] ) * rtol ) ) / h0;\r\n\t\tvar h1 = d1 \u003C= 1e-15 \u0026\u0026 d2 \u003C= 1e-15 ? Math.Max( 1e-6, h0 * 1e-3 ) : Math.Pow( 0.01 / Math.Max( d1, d2 ), 1.0 / Order );\r\n\t\tvar dt = Math.Min( 100 * h0, h1 );\r\n\r\n\t\tvar y = (float[])y0.Clone();\r\n\t\tvar fy = fy0;\r\n\t\tdouble t = 0;\r\n\t\tvar k = new float[7][];\r\n\t\twhile ( true )\r\n\t\t{\r\n\t\t\ttoken.ThrowIfCancellationRequested();\r\n\t\t\tvar t1 = t \u002B dt;\r\n\t\t\tk[0] = fy;\r\n\t\t\tvar yi = new float[n];\r\n\t\t\tfor ( var s = 0; s \u003C 6; s\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar b = B[s];\r\n\t\t\t\tfor ( var i = 0; i \u003C n; i\u002B\u002B )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar acc = 0.0;\r\n\t\t\t\t\tfor ( var j = 0; j \u003C= s; j\u002B\u002B ) acc \u002B= k[j][i] * (b[j] * dt);\r\n\t\t\t\t\tyi[i] = (float)(y[i] \u002B acc);\r\n\t\t\t\t}\r\n\t\t\t\tvar ts = A[s] == 1.0 ? t1 : t \u002B A[s] * dt;\r\n\t\t\t\tk[s \u002B 1] = f( yi, (float)ts );\r\n\t\t\t\tif ( s \u003C 5 ) yi = new float[n];\r\n\t\t\t}\r\n\t\t\t// the 6th stage is the 5th-order solution (FSAL: k[6] is its derivative)\r\n\t\t\tvar y1 = yi;\r\n\t\t\tvar ratio = Rms( i =\u003E\r\n\t\t\t{\r\n\t\t\t\tvar err = 0.0;\r\n\t\t\t\tfor ( var j = 0; j \u003C 7; j\u002B\u002B ) err \u002B= k[j][i] * (dt * E[j]);\r\n\t\t\t\treturn err / (atol \u002B rtol * Math.Max( Math.Abs( y[i] ), Math.Abs( y1[i] ) ));\r\n\t\t\t} );\r\n\t\t\tvar accept = ratio \u003C= 1;\r\n\t\t\tvar factor = ratio == 0 ? IFactor : Math.Min( IFactor, Math.Max( Safety / Math.Pow( ratio, 1.0 / Order ), accept ? 1.0 : DFactor ) );\r\n\t\t\tif ( accept )\r\n\t\t\t{\r\n\t\t\t\tif ( t1 \u003E= 1.0 )\r\n\t\t\t\t{\r\n\t\t\t\t\t// 4th-order interpolation of this step at t = 1\r\n\t\t\t\t\tvar x = (1.0 - t) / dt;\r\n\t\t\t\t\tvar result = new float[n];\r\n\t\t\t\t\tfor ( var i = 0; i \u003C n; i\u002B\u002B )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar mid = 0.0;\r\n\t\t\t\t\t\tfor ( var j = 0; j \u003C 7; j\u002B\u002B ) mid \u002B= k[j][i] * (dt * Mid[j]);\r\n\t\t\t\t\t\tdouble ym = y[i] \u002B mid, a0 = y[i], a1 = y1[i], f0 = k[0][i], f1 = k[6][i];\r\n\t\t\t\t\t\tvar ca = 2 * dt * (f1 - f0) - 8 * (a1 \u002B a0) \u002B 16 * ym;\r\n\t\t\t\t\t\tvar cb = dt * (5 * f0 - 3 * f1) \u002B 18 * a0 \u002B 14 * a1 - 32 * ym;\r\n\t\t\t\t\t\tvar cc = dt * (f1 - 4 * f0) - 11 * a0 - 5 * a1 \u002B 16 * ym;\r\n\t\t\t\t\t\tvar cd = dt * f0;\r\n\t\t\t\t\t\tresult[i] = (float)(a0 \u002B x * cd \u002B x * x * cc \u002B x * x * x * cb \u002B x * x * x * x * ca);\r\n\t\t\t\t\t}\r\n\t\t\t\t\tprogress?.Invoke( 1f );\r\n\t\t\t\t\treturn result;\r\n\t\t\t\t}\r\n\t\t\t\ty = y1;\r\n\t\t\t\tfy = k[6];\r\n\t\t\t\tt = t1;\r\n\t\t\t\tprogress?.Invoke( (float)Math.Min( 0.99, t ) );\r\n\t\t\t}\r\n\t\t\tdt *= factor;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EStandard normal noise (J,12,T) from a seed (Box-Muller).\u003C/summary\u003E\r\n\tpublic static float[] Noise( int joints, int seed )\r\n\t{\r\n\t\tvar rng = new Random( seed );\r\n\t\tvar n = joints * 12 * Frames;\r\n\t\tvar r = new float[n];\r\n\t\tfor ( var i = 0; i \u003C n; i \u002B= 2 )\r\n\t\t{\r\n\t\t\tvar u1 = 1.0 - rng.NextDouble(); var u2 = rng.NextDouble();\r\n\t\t\tvar mag = Math.Sqrt( -2.0 * Math.Log( u1 ) );\r\n\t\t\tr[i] = (float)(mag * Math.Cos( 2 * Math.PI * u2 ));\r\n\t\t\tif ( i \u002B 1 \u003C n ) r[i \u002B 1] = (float)(mag * Math.Sin( 2 * Math.PI * u2 ));\r\n\t\t}\r\n\t\treturn r;\r\n\t}\r\n}\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"Editor/TextToAnimation/Inference/UniMate/UniMatePrompt.cs","FileName":"UniMatePrompt.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":409724,"IsPrivate":false,"Code":"namespace TextToAnimation.Editor.Inference.UniMate;\r\n\r\n/// \u003Csummary\u003EPrompts as UniMate takes them, and clip names from them.\u003C/summary\u003E\r\npublic static class UniMatePrompt\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// The caption UniMate is conditioned on: the prompt as written, trimmed - as upstream (inference/sample.py encodes\r\n\t/// \u003Cc\u003Eprompt.strip()\u003C/c\u003E whole, with no rewriting or splitting; FLAN-T5 reads the wording).\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string Caption( string prompt )\r\n\t{\r\n\t\tvar s = prompt ?? \u0022\u0022;\r\n\t\tint start = 0, end = s.Length;\r\n\t\twhile ( start \u003C end \u0026\u0026 IsPythonSpace( s[start] ) ) start\u002B\u002B;\r\n\t\twhile ( end \u003E start \u0026\u0026 IsPythonSpace( s[end - 1] ) ) end--;\r\n\t\treturn s[start..end];\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EPython\u0027s \u003Cc\u003Estr.isspace\u003C/c\u003E: .NET\u0027s whitespace plus the information separators U\u002B001C..U\u002B001F.\u003C/summary\u003E\r\n\tstatic bool IsPythonSpace( char c ) =\u003E char.IsWhiteSpace( c ) || c is \u003E= \u0027\\u001c\u0027 and \u003C= \u0027\\u001f\u0027;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A clip name from a prompt: the prompt itself on one line, capitalised, without a leading \u0022An object\u0022; only a\r\n\t/// very long one is shortened, at a word boundary.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string ClipName( string prompt )\r\n\t{\r\n\t\tvar words = Caption( prompt ).Split( (char[])null, System.StringSplitOptions.RemoveEmptyEntries );\r\n\t\t// UniMate\u0027s captions all open with \u0022An object\u0022 (its README\u0027s advice for prompts): the name is what it does\r\n\t\tif ( words.Length \u003E 2 \u0026\u0026 words[0].Equals( \u0022an\u0022, System.StringComparison.OrdinalIgnoreCase ) \u0026\u0026 words[1].Equals( \u0022object\u0022, System.StringComparison.OrdinalIgnoreCase ) )\r\n\t\t\twords = words[2..];\r\n\t\tvar text = string.Join( \u0027 \u0027, words ).TrimEnd( \u0027.\u0027, \u0027!\u0027, \u0027?\u0027 ).Trim();\r\n\t\tconst int MaxLength = 48;\r\n\t\tif ( text.Length \u003E MaxLength )\r\n\t\t{\r\n\t\t\tvar space = text.LastIndexOf( \u0027 \u0027, MaxLength );\r\n\t\t\tvar cut = space \u003E 0 ? space : MaxLength;\r\n\t\t\tif ( char.IsHighSurrogate( text[cut - 1] ) ) cut--; // never half a character (emoji)\r\n\t\t\ttext = text[..cut].Trim();\r\n\t\t}\r\n\t\tif ( text.Length == 0 ) return \u0022Generated\u0022;\r\n\t\treturn char.ToUpperInvariant( text[0] ) \u002B text[1..];\r\n\t}\r\n}\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"Editor/TextToAnimation/Inference/UniMate/UniMateSkin.cs","FileName":"UniMateSkin.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":409724,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Runtime.CompilerServices;\r\nusing TextToAnimation.Rig;\r\n\r\nnamespace TextToAnimation.Editor.Inference.UniMate;\r\n\r\n/// \u003Csummary\u003E\r\n/// Skin weights per bone for a skeleton, which upstream\u0027s skeleton preparation prunes on (\u0022skinned bones are never\r\n/// prunable\u0022). The engine doesn\u0027t expose them; the editor reads them from the model\u0027s source FBX and attaches them\r\n/// here when it opens the model. Unknown (nothing attached) means every bone counts as skinned.\r\n/// \u003C/summary\u003E\r\npublic static class UniMateSkin\r\n{\r\n\tsealed class Entry\r\n\t{\r\n\t\tpublic IReadOnlyDictionary\u003Cstring, (double Max, double Sum)\u003E Weights;\r\n\t\tpublic string ObjectType;\r\n\t\tpublic IReadOnlySet\u003Cstring\u003E Driven;\r\n\t}\r\n\r\n\tstatic readonly ConditionalWeakTable\u003CSkeleton, Entry\u003E _entries = new();\r\n\r\n\t/// \u003Csummary\u003EAttaches per-bone (largest single, total) skin weights, and the model\u0027s name (upstream\u0027s object type).\u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022driven\u0022\u003EBones the model\u0027s own constraints drive at runtime (the engine poses them, not clips).\u003C/param\u003E\r\n\tpublic static void Attach( Skeleton skeleton, IReadOnlyDictionary\u003Cstring, (double Max, double Sum)\u003E weights, string objectType,\r\n\t\tIReadOnlySet\u003Cstring\u003E driven = null )\r\n\t{\r\n\t\tif ( skeleton is null ) return;\r\n\t\t_entries.Remove( skeleton );\r\n\t\t_entries.Add( skeleton, new Entry { Weights = weights, ObjectType = objectType ?? \u0022\u0022, Driven = driven } );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe bones the model\u0027s constraints drive (empty when unknown).\u003C/summary\u003E\r\n\tpublic static IReadOnlySet\u003Cstring\u003E DrivenOf( Skeleton skeleton )\r\n\t\t=\u003E skeleton is not null \u0026\u0026 _entries.TryGetValue( skeleton, out var e ) \u0026\u0026 e.Driven is not null ? e.Driven : new HashSet\u003Cstring\u003E();\r\n\r\n\t/// \u003Csummary\u003EThe weights attached to \u003Cparamref name=\u0022skeleton\u0022/\u003E, or null when unknown.\u003C/summary\u003E\r\n\tpublic static IReadOnlyDictionary\u003Cstring, (double Max, double Sum)\u003E WeightsOf( Skeleton skeleton )\r\n\t\t=\u003E skeleton is not null \u0026\u0026 _entries.TryGetValue( skeleton, out var e ) ? e.Weights : null;\r\n\r\n\t/// \u003Csummary\u003EThe model name attached to \u003Cparamref name=\u0022skeleton\u0022/\u003E (\u0022\u0022 when none).\u003C/summary\u003E\r\n\tpublic static string ObjectTypeOf( Skeleton skeleton )\r\n\t\t=\u003E skeleton is not null \u0026\u0026 _entries.TryGetValue( skeleton, out var e ) ? e.ObjectType : \u0022\u0022;\r\n}\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"Editor/TextToAnimation/Mapping/MappingResult.cs","FileName":"MappingResult.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":409724,"IsPrivate":false,"Code":"// Vendored from humanoid-retargeter (Code/HumanoidRetargeter/Mapping/MappingResult.cs).\r\n#nullable enable annotations\r\n\r\nusing System.Collections.Generic;\r\n\r\nnamespace TextToAnimation.Editor.Mapping;\r\n\r\n/// \u003Csummary\u003EHow a mapping was produced; determines UI flow and preset-learning behavior.\u003C/summary\u003E\r\npublic enum MappingSource\r\n{\r\n    /// \u003Csummary\u003EA shipped preset profile matched (mixamo, actorcore_cc, ...).\u003C/summary\u003E\r\n    Preset,\r\n\r\n    /// \u003Csummary\u003EA user-saved preset (keyed by \u003Csee cref=\u0022SkeletonSignature\u0022/\u003E) matched.\u003C/summary\u003E\r\n    UserPreset,\r\n\r\n    /// \u003Csummary\u003EThe auto-mapper resolved roles from bone-name tokens (stage A).\u003C/summary\u003E\r\n    AutoName,\r\n\r\n    /// \u003Csummary\u003EThe auto-mapper fell back to pure hierarchy/geometry matching (stage B).\u003C/summary\u003E\r\n    AutoTopology,\r\n\r\n    /// \u003Csummary\u003EThe user assigned roles by hand in the mapping editor.\u003C/summary\u003E\r\n    Manual,\r\n\r\n    /// \u003Csummary\u003E\r\n    /// The source file itself declares the mapping \u2014 e.g. a VRM\u0027s\r\n    /// \u003Cc\u003Ehumanoid.humanBones\u003C/c\u003E block (both the 0.x \u003Cc\u003Eextensions.VRM\u003C/c\u003E and the 1.0\r\n    /// \u003Cc\u003Eextensions.VRMC_vrm\u003C/c\u003E layouts). Authoritative: consulted before user presets and\r\n    /// shipped-profile detection, at confidence 1.0.\r\n    /// \u003C/summary\u003E\r\n    Authored,\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The outcome of mapping a source skeleton onto canonical \u003Csee cref=\u0022BoneRole\u0022/\u003Es:\r\n/// role \u2192 source bone index, an overall confidence, and human-readable notes\r\n/// (unmapped roles, ignored bones, ambiguities) for the mapping report.\r\n/// \u003C/summary\u003E\r\npublic sealed class MappingResult\r\n{\r\n    /// \u003Csummary\u003ECreates an empty result; callers fill \u003Csee cref=\u0022RoleToBone\u0022/\u003E and\r\n    /// \u003Csee cref=\u0022Confidence\u0022/\u003E.\u003C/summary\u003E\r\n    public MappingResult(string profileName, MappingSource source)\r\n    {\r\n        ProfileName = profileName;\r\n        Source = source;\r\n    }\r\n\r\n    /// \u003Csummary\u003EResolved roles: canonical role \u2192 bone index in the source skeleton.\u003C/summary\u003E\r\n    public Dictionary\u003CBoneRole, int\u003E RoleToBone { get; } = new();\r\n\r\n    /// \u003Csummary\u003EMapping confidence in [0, 1]; 1 means every required and optional role\r\n    /// resolved unambiguously.\u003C/summary\u003E\r\n    public float Confidence { get; set; }\r\n\r\n    /// \u003Csummary\u003EProfile that produced the mapping \u2014 a preset name, or \u003Cc\u003E\u0022auto\u0022\u003C/c\u003E /\r\n    /// \u003Cc\u003E\u0022topology\u0022\u003C/c\u003E for the auto-mapper stages.\u003C/summary\u003E\r\n    public string ProfileName { get; }\r\n\r\n    /// \u003Csummary\u003EWhere the mapping came from.\u003C/summary\u003E\r\n    public MappingSource Source { get; }\r\n\r\n    /// \u003Csummary\u003EReport notes: unmapped roles, ignored source bones, ambiguities.\u003C/summary\u003E\r\n    public List\u003Cstring\u003E Notes { get; } = new();\r\n}\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"Editor/TextToAnimation/UI/AnimationViewport.cs","FileName":"AnimationViewport.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":409724,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Editor;\r\nusing Sandbox;\r\nusing TextToAnimation.Animation;\r\nusing TextToAnimation.Editor.Engine;\r\nusing TextToAnimation.Editor.Session;\r\nusing TextToAnimation.Maths;\r\nusing TextToAnimation.Processing;\r\n\r\nnamespace TextToAnimation.Editor.UI;\r\n\r\n/// \u003Csummary\u003E\r\n/// The live model view: the workspace model posed with the active clip at the playhead, the bone overlay\r\n/// (blue = selected, amber = locked for regeneration), the root trajectory and the floor. Left click picks\r\n/// bones and drags the rotate handle of the selected bone (an undoable key on the current frame); right drag\r\n/// orbits, middle drag pans, the wheel zooms, double click frames the character.\r\n/// \u003C/summary\u003E\r\npublic sealed class AnimationViewport : SceneRenderingWidget\r\n{\r\n\treadonly EditorSession _session;\r\n\tCameraComponent _camera;\r\n\tSceneModel _sceneModel;\r\n\tModel _model;\r\n\tint[] _boneMap = Array.Empty\u003Cint\u003E();\r\n\tVector3[] _bindScale = Array.Empty\u003CVector3\u003E();\r\n\tXForm[] _world = Array.Empty\u003CXForm\u003E();\r\n\tXForm[] _pose = Array.Empty\u003CXForm\u003E();\r\n\tbool _dragging;\r\n\r\n\tfloat _yaw = 35f, _pitch = 12f, _distance = 140f;\r\n\tVector3 _target = new( 0, 0, 36 );\r\n\tVector3 _pan;\r\n\tVector2 _lastMouse;\r\n\r\n\tpublic bool ShowSkeleton { get; set; } = true;\r\n\tpublic bool ShowTrajectory { get; set; } = true;\r\n\tpublic bool ShowGround { get; set; } = true;\r\n\tpublic bool ShowModel { get; set; } = true;\r\n\tpublic bool FollowCharacter { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// When set, the engine plays this compiled sequence of the model itself (no bone overrides) at the session\r\n\t/// playhead - the ground truth of what a saved animation looks like in game.\r\n\t/// \u003C/summary\u003E\r\n\tpublic string EngineSequence { get; set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Bones the model\u0027s own constraints drive at runtime (from its vmdl and prefabs). For a clip whose compiled\r\n\t/// sequence is current, the view takes these from the engine itself, so the preview shows the game\u0027s result.\r\n\t/// \u003C/summary\u003E\r\n\tpublic IReadOnlySet\u003Cstring\u003E ConstraintDriven { get; set; } = new HashSet\u003Cstring\u003E();\r\n\r\n\t/// \u003Csummary\u003EThe compiled sequence holding the active clip unchanged (set by the window), or null.\u003C/summary\u003E\r\n\tpublic string SavedSequence { get; set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The preview\u0027s constraint-driven bones against the engine playing \u003Cparamref name=\u0022sequence\u0022/\u003E at the playhead,\r\n\t/// both relative to \u003Cparamref name=\u0022anchor\u0022/\u003E (the game may extract root motion): the largest position difference.\r\n\t/// \u003C/summary\u003E\r\n\tpublic float PreviewVsGameOnDrivenBones( string sequence, string anchor )\r\n\t{\r\n\t\tvar rig = _session.Rig;\r\n\t\tif ( rig is null || !_sceneModel.IsValid() || _model is null ) return float.NaN;\r\n\t\tvar s = rig.Skeleton;\r\n\t\tvar a = s.IndexOf( anchor );\r\n\t\tif ( a \u003C 0 || _boneMap[a] \u003C 0 || _session.ActiveFrames is not { Count: \u003E 0 } frames ) return float.NaN;\r\n\t\t// the pose at the current playhead (the paint tick may not have run since the last seek)\r\n\t\tSamplePose( frames, _session.Playhead, _pose );\r\n\t\tFkUtil.ToWorld( _pose, s, _world );\r\n\t\tApplyPose();\r\n\t\tvar preview = Enumerable.Range( 0, s.Count ).Select( b =\u003E _boneMap[b] \u003E= 0 ? _sceneModel.GetBoneWorldTransform( _boneMap[b] ) : default ).ToArray();\r\n\t\tvar saved = EngineSequence;\r\n\t\tEngineSequence = sequence;\r\n\t\tApplyPose();\r\n\t\tvar game = Enumerable.Range( 0, s.Count ).Select( b =\u003E _boneMap[b] \u003E= 0 ? _sceneModel.GetBoneWorldTransform( _boneMap[b] ) : default ).ToArray();\r\n\t\tEngineSequence = saved;\r\n\t\tApplyPose();\r\n\t\tvar worst = 0f; var worstOther = 0f; string worstBone = \u0022\u0022, worstOtherBone = \u0022\u0022;\r\n\t\tfor ( var b = 0; b \u003C s.Count; b\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( _boneMap[b] \u003C 0 ) continue;\r\n\t\t\tvar d = (preview[a].ToLocal( preview[b] ).Position - game[a].ToLocal( game[b] ).Position).Length;\r\n\t\t\tif ( ConstraintDriven.Contains( s[b].Name ) ) { if ( d \u003E worst ) { worst = d; worstBone = s[b].Name; } }\r\n\t\t\telse if ( d \u003E worstOther ) { worstOther = d; worstOtherBone = s[b].Name; }\r\n\t\t}\r\n\t\tLastDrivenReport = $\u0022worst {worstBone}; other bones max {worstOther:0.000} in ({worstOtherBone})\u0022;\r\n\t\treturn worst;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EWhich bones the last \u003Csee cref=\u0022PreviewVsGameOnDrivenBones\u0022/\u003E found furthest apart.\u003C/summary\u003E\r\n\tpublic string LastDrivenReport { get; private set; } = \u0022\u0022;\r\n\r\n\t/// \u003Csummary\u003EThe sequence the active clip was saved as, when it hasn\u0027t changed since (its compiled copy is current).\u003C/summary\u003E\r\n\tstring CurrentSavedSequence()\r\n\t{\r\n\t\tvar clip = _session.ActiveClip;\r\n\t\treturn clip?.SavedUtc is not null \u0026\u0026 ClipListPanel.SavedRevisions.TryGetValue( clip.Id, out var revision ) \u0026\u0026 revision == clip.Revision\r\n\t\t\t? clip.EffectiveSequenceName : null;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETrue when the last pose took the constraint-driven bones from the engine.\u003C/summary\u003E\r\n\tpublic bool UsedEngineConstraints { get; private set; }\r\n\r\n\tpublic AnimationViewport( Widget parent, EditorSession session ) : base( parent )\r\n\t{\r\n\t\t_session = session;\r\n\t\tMinimumSize = new Vector2( 320, 260 );\r\n\t\tMouseTracking = true;\r\n\t\tFocusMode = FocusMode.Click;\r\n\r\n\t\tScene = Scene.CreateEditorScene();\r\n\t\tusing ( Scene.Push() )\r\n\t\t{\r\n\t\t\t_camera = new GameObject( true, \u0022camera\u0022 ).GetOrAddComponent\u003CCameraComponent\u003E( false );\r\n\t\t\t_camera.BackgroundColor = Theme.ControlBackground;\r\n\t\t\t_camera.ZNear = 1f;\r\n\t\t\t_camera.ZFar = 20000f;\r\n\t\t\t_camera.FieldOfView = 45f;\r\n\t\t\t_camera.Enabled = true;\r\n\t\t}\r\n\t\tCamera = _camera;\r\n\t\tvar world = Scene.SceneWorld;\r\n\t\tnew ScenePointLight( world, new Vector3( 120, 100, 160 ), 900, Color.White * 3.0f ).ShadowsEnabled = false;\r\n\t\tnew ScenePointLight( world, new Vector3( -140, -100, 110 ), 900, Color.White * 1.6f ).ShadowsEnabled = false;\r\n\t\tnew ScenePointLight( world, new Vector3( 0, 160, 60 ), 700, Color.White * 0.9f ).ShadowsEnabled = false;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ELoads the workspace model into the view.\u003C/summary\u003E\r\n\t/// \u003Csummary\u003E\r\n\t/// Shows \u003Cparamref name=\u0022model\u0022/\u003E. Always rebuilds the scene model and the bone map: a save recompiles the\r\n\t/// model in place (the same Model object) and the compiler may reorder its bones, so a cached map would pose\r\n\t/// the wrong bones. The camera is only reframed for a different model.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void SetModel( Model model )\r\n\t{\r\n\t\tvar reframe = model?.ResourcePath != _model?.ResourcePath;\r\n\t\t_model = model;\r\n\t\t_sceneModel?.Delete();\r\n\t\t_sceneModel = null;\r\n\t\tDeleteEngineModel();\r\n\t\tif ( model is null || model.IsError || _session.Rig is null ) return;\r\n\t\tDeleteEngineModel();\r\n\t\t_sceneModel = new SceneModel( Scene.SceneWorld, model, Transform.Zero ) { UseAnimGraph = false };\r\n\t\tvar skeleton = _session.Rig.Skeleton;\r\n\t\t_boneMap = skeleton.Bones.Select( b =\u003E model.Bones.GetBone( b.Name )?.Index ?? -1 ).ToArray();\r\n\t\t_bindScale = skeleton.Bones.Select( b =\u003E model.Bones.GetBone( b.Name )?.LocalTransform.Scale ?? Vector3.One ).ToArray();\r\n\t\t_world = new XForm[skeleton.Count];\r\n\t\t_pose = new XForm[skeleton.Count];\r\n\t\tif ( reframe ) FrameCharacter();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EFrames the whole character (double click does the same).\u003C/summary\u003E\r\n\tpublic void FrameCharacter()\r\n\t{\r\n\t\tvar rig = _session.Rig;\r\n\t\tif ( _model is not null \u0026\u0026 !_model.IsError \u0026\u0026 _model.Bounds.Size.Length \u003E 1f )\r\n\t\t{\r\n\t\t\t// any creature: frame its whole body (a fox is long and low, a person tall and narrow)\r\n\t\t\tvar size = _model.Bounds.Size;\r\n\t\t\tvar extent = MathF.Max( size.z, MathF.Max( size.x, size.y ) * 0.75f );\r\n\t\t\t_distance = MathF.Max( extent * 1.9f, 30f );\r\n\t\t\t_target = new Vector3( 0, 0, size.z * 0.5f );\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tvar height = rig is null ? 72f : MathF.Max( rig.HipHeight * 2.1f, 20f );\r\n\t\t\t_distance = height * 1.9f;\r\n\t\t\t_target = new Vector3( 0, 0, height * 0.5f );\r\n\t\t}\r\n\t\t_pan = Vector3.Zero;\r\n\t\t_followCenter = null;\r\n\t}\r\n\r\n\tpublic void SetView( float yaw, float pitch ) { _yaw = yaw; _pitch = pitch; }\r\n\r\n\tprotected override void PreFrame()\r\n\t{\r\n\t\tvar started = FrameProbe.Now;\r\n\t\ttry { ViewFrame(); }\r\n\t\tfinally { FrameProbe.Add( \u0022viewport\u0022, FrameProbe.Now - started ); }\r\n\t}\r\n\r\n\tvoid ViewFrame()\r\n\t{\r\n\t\tScene.EditorTick( RealTime.Now, RealTime.Delta );\r\n\t\tGizmoInstance.Input.IsHovered = IsActiveWindow \u0026\u0026 IsUnderMouse;\r\n\t\tUpdateGizmoInputs( GizmoInstance.Input.IsHovered );\r\n\r\n\t\tvar frames = _session.ActiveFrames;\r\n\t\tvar rig = _session.Rig;\r\n\t\tif ( rig is null || frames is null || frames.Count == 0 )\r\n\t\t{\r\n\t\t\t_gizmoHovered = false;\r\n\t\t\tUpdateCamera( _target );\r\n\t\t\tif ( ShowGround ) DrawGround( Vector3.Zero, 0f, 40f );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tSamplePose( frames, _session.Playhead, _pose );\r\n\t\tFkUtil.ToWorld( _pose, rig.Skeleton, _world );\r\n\t\tApplyPose();\r\n\r\n\t\tvar hips = rig.HipsIndex \u003E= 0 ? rig.HipsIndex : rig.RootIndex;\r\n\t\tvar hipsPos = ToVec( _world[hips].Pos );\r\n\t\tvar groundZ = GroundHeight( rig );\r\n\t\tUpdateCamera( FollowCharacter ? new Vector3( hipsPos.x, hipsPos.y, _target.z ) : _target );\r\n\r\n\t\tif ( ShowGround ) DrawGround( hipsPos, groundZ, MathF.Max( rig.HipHeight * 2f, 30f ) );\r\n\t\tif ( ShowTrajectory ) DrawTrajectory( frames, rig, groundZ );\r\n\t\tif ( ShowSkeleton || !ShowModel ) DrawSkeleton( rig );\r\n\t\tDrawBonePicking( rig );\r\n\t\tDrawPoseGizmo( rig );\r\n\t\t_gizmoHovered = Gizmo.HasHovered;\r\n\t}\r\n\r\n\t// whether a bone dot or pose handle is under the mouse, as of the last drawn frame: Gizmo.HasHovered only works\r\n\t// while the gizmos are being drawn, not in mouse events\r\n\tbool _gizmoHovered;\r\n\r\n\tstatic Vector3 ToVec( System.Numerics.Vector3 v ) =\u003E new( v.X, v.Y, v.Z );\r\n\r\n\tstatic void SamplePose( List\u003CXForm[]\u003E frames, float playhead, XForm[] into )\r\n\t{\r\n\t\tvar a = Math.Clamp( (int)MathF.Floor( playhead ), 0, frames.Count - 1 );\r\n\t\tvar b = Math.Min( a \u002B 1, frames.Count - 1 );\r\n\t\tvar t = playhead - a;\r\n\t\tvar fa = frames[a]; var fb = frames[b];\r\n\t\tvar n = Math.Min( into.Length, fa.Length );\r\n\t\tfor ( var i = 0; i \u003C n; i\u002B\u002B )\r\n\t\t\tinto[i] = t \u003C= 1e-4f ? fa[i] : new XForm( System.Numerics.Vector3.Lerp( fa[i].Pos, fb[i].Pos, t ),\r\n\t\t\t\tSystem.Numerics.Quaternion.Normalize( System.Numerics.Quaternion.Slerp( fa[i].Rot, fb[i].Rot, t ) ) );\r\n\t}\r\n\r\n\tSceneWorld _engineWorld;\r\n\tSceneModel _engineModel;\r\n\tstring _engineModelSequence;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The engine\u0027s own pose of compiled \u003Cparamref name=\u0022sequence\u0022/\u003E at \u003Cparamref name=\u0022seconds\u0022/\u003E (its animation and\r\n\t/// the model\u0027s constraints), on a hidden model that only ever plays that sequence: switching sequences on a model\r\n\t/// crossfades from the previous one, which an editor (that never advances time) would never finish.\r\n\t/// \u003C/summary\u003E\r\n\tSceneModel EngineModelAt( string sequence, float seconds )\r\n\t{\r\n\t\t// recreated until it really plays the sequence (one made while a save was recompiling the model has none)\r\n\t\tif ( !_engineModel.IsValid() || _engineModelSequence != sequence || _engineModel.CurrentSequence.Name != sequence )\r\n\t\t{\r\n\t\t\tDeleteEngineModel();\r\n\t\t\t_engineWorld = new SceneWorld();\r\n\t\t\t// the compiled model as it is now (a save recompiles it; the view\u0027s instance can lag behind)\r\n\t\t\tvar compiled = Model.Load( _model.ResourcePath );\r\n\t\t\t_engineModel = new SceneModel( _engineWorld, compiled is { IsError: false } ? compiled : _model, Transform.Zero ) { UseAnimGraph = false };\r\n\t\t\t_engineModel.CurrentSequence.Name = sequence;\r\n\t\t\t_engineModel.Update( 0f );\r\n\t\t\t_engineModelSequence = sequence;\r\n\t\t}\r\n\t\t_engineModel.CurrentSequence.Time = seconds;\r\n\t\t_engineModel.Update( 0f );\r\n\t\treturn _engineModel;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Every bone\u0027s world transform on an engine-played model, chained from the parent-space bones that\r\n\t/// \u003Csee cref=\u0022SceneModel.Update\u0022/\u003E evaluates (the world transforms it reports refresh only when the scene renders).\r\n\t/// \u003C/summary\u003E\r\n\tTransform[] EngineWorld( SceneModel engine )\r\n\t{\r\n\t\tvar count = _model.BoneCount;\r\n\t\tvar world = new Transform[count];\r\n\t\tvar state = new byte[count]; // 0 todo, 1 in progress (a parent loop cuts there), 2 done\r\n\t\tTransform Of( int b )\r\n\t\t{\r\n\t\t\tif ( state[b] == 2 ) return world[b];\r\n\t\t\tvar local = engine.GetParentSpaceBone( b );\r\n\t\t\tvar parent = _model.GetBoneParent( b );\r\n\t\t\tstate[b] = 1;\r\n\t\t\tworld[b] = parent \u003C 0 || parent \u003E= count || state[parent] == 1 ? local : Of( parent ).ToWorld( local );\r\n\t\t\tstate[b] = 2;\r\n\t\t\treturn world[b];\r\n\t\t}\r\n\t\tfor ( var b = 0; b \u003C count; b\u002B\u002B ) Of( b );\r\n\t\treturn world;\r\n\t}\r\n\r\n\tvoid DeleteEngineModel()\r\n\t{\r\n\t\tif ( _engineModel.IsValid() ) _engineModel.Delete();\r\n\t\t_engineWorld?.Delete();\r\n\t\t_engineModel = null;\r\n\t\t_engineWorld = null;\r\n\t\t_engineModelSequence = null;\r\n\t}\r\n\r\n\tvoid ApplyPose()\r\n\t{\r\n\t\tif ( !_sceneModel.IsValid() ) return;\r\n\t\t_sceneModel.RenderingEnabled = ShowModel;\r\n\t\tif ( !string.IsNullOrEmpty( EngineSequence ) )\r\n\t\t{\r\n\t\t\tvar engine = EngineWorld( EngineModelAt( EngineSequence, _session.Playhead / (_session.ActiveClip?.Fps ?? 30f) ) );\r\n\t\t\tfor ( var b = 0; b \u003C engine.Length; b\u002B\u002B ) _sceneModel.SetBoneOverride( b, engine[b] );\r\n\t\t\t_sceneModel.Update( 0f );\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tvar final = new Transform[_boneMap.Length];\r\n\t\tfor ( var i = 0; i \u003C _boneMap.Length; i\u002B\u002B ) final[i] = ModelBridge.ToTransform( _world[i], _bindScale[i] );\r\n\t\tUsedEngineConstraints = false;\r\n\t\tvar saved = SavedSequence ?? CurrentSavedSequence();\r\n\t\tif ( ConstraintDriven.Count \u003E 0 \u0026\u0026 !string.IsNullOrEmpty( saved ) \u0026\u0026 _model.AnimationNames.Contains( saved ) )\r\n\t\t{\r\n\t\t\t// the engine evaluates the model\u0027s constraints only while playing a compiled sequence: play the saved one at\r\n\t\t\t// this time, take each driven bone relative to its parent, and place it on this pose (root motion kept)\r\n\t\t\tvar engine = EngineWorld( EngineModelAt( saved, _session.Playhead / (_session.ActiveClip?.Fps ?? 30f) ) );\r\n\t\t\tvar skeleton = _session.Rig.Skeleton;\r\n\t\t\tvar moved = new bool[skeleton.Count];\r\n\t\t\tfor ( var i = 0; i \u003C skeleton.Count; i\u002B\u002B ) // parents come before children\r\n\t\t\t{\r\n\t\t\t\tvar p = skeleton[i].ParentIndex;\r\n\t\t\t\tif ( _boneMap[i] \u003C 0 || p \u003C 0 || _boneMap[p] \u003C 0 ) continue;\r\n\t\t\t\tif ( ConstraintDriven.Contains( skeleton[i].Name ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar local = engine[_boneMap[p]].ToLocal( engine[_boneMap[i]] );\r\n\t\t\t\t\tfinal[i] = final[p].ToWorld( local );\r\n\t\t\t\t\tmoved[i] = UsedEngineConstraints = true;\r\n\t\t\t\t}\r\n\t\t\t\telse if ( moved[p] )\r\n\t\t\t\t{\r\n\t\t\t\t\t// below a driven bone: keep this pose\u0027s local transform under the engine-placed parent\r\n\t\t\t\t\tfinal[i] = final[p].ToWorld( ModelBridge.ToTransform( _world[p], _bindScale[p] ).ToLocal( ModelBridge.ToTransform( _world[i], _bindScale[i] ) ) );\r\n\t\t\t\t\tmoved[i] = true;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tfor ( var i = 0; i \u003C _boneMap.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( _boneMap[i] \u003C 0 ) continue;\r\n\t\t\t_sceneModel.SetBoneOverride( _boneMap[i], final[i] );\r\n\t\t}\r\n\t\t_sceneModel.Update( 0f ); // flush overrides now (otherwise the pose lags a frame)\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The engine\u0027s own pose while playing compiled \u003Cparamref name=\u0022sequence\u0022/\u003E (animation \u002B the model\u0027s constraints):\r\n\t/// world transforms of every bone and of the named attachments, at the given frames. Ground truth for the\r\n\t/// constraint evaluator.\r\n\t/// \u003C/summary\u003E\r\n\tpublic object EnginePoses( string sequence, IEnumerable\u003Cint\u003E frames, float fps, IReadOnlyList\u003Cstring\u003E attachments )\r\n\t{\r\n\t\tif ( _model is null || !_sceneModel.IsValid() ) return null;\r\n\t\tvar saved = EngineSequence;\r\n\t\tEngineSequence = null;\r\n\t\tvar result = new List\u003Cobject\u003E();\r\n\t\tstatic float[] P( Transform t ) =\u003E new[] { t.Position.x, t.Position.y, t.Position.z, t.Rotation.x, t.Rotation.y, t.Rotation.z, t.Rotation.w };\r\n\t\tforeach ( var f in frames )\r\n\t\t{\r\n\t\t\tvar engineModel = EngineModelAt( sequence, f / fps );\r\n\t\t\tvar engine = EngineWorld( engineModel );\r\n\t\t\t// the pose the sequence stores (before constraints), in the same model space as the engine\u0027s bones\r\n\t\t\tvar stored = new Dictionary\u003Cstring, float[]\u003E();\r\n\t\t\tif ( _session.ActiveFrames is { Count: \u003E 0 } clipFrames \u0026\u0026 _session.Rig is { } rig )\r\n\t\t\t{\r\n\t\t\t\tvar pose = new XForm[rig.Skeleton.Count]; var world = new XForm[rig.Skeleton.Count];\r\n\t\t\t\tSamplePose( clipFrames, f, pose );\r\n\t\t\t\tFkUtil.ToWorld( pose, rig.Skeleton, world );\r\n\t\t\t\tfor ( var i = 0; i \u003C rig.Skeleton.Count; i\u002B\u002B )\r\n\t\t\t\t\tif ( _boneMap[i] \u003E= 0 ) stored[rig.Skeleton[i].Name] = P( ModelBridge.ToTransform( world[i], _bindScale[i] ) );\r\n\t\t\t}\r\n\t\t\tresult.Add( new\r\n\t\t\t{\r\n\t\t\t\tframe = f,\r\n\t\t\t\tstored,\r\n\t\t\t\tbones = Enumerable.Range( 0, _model.BoneCount ).ToDictionary( b =\u003E _model.GetBoneName( b ), b =\u003E P( engine[b] ) ),\r\n\t\t\t\tattachments = attachments.Select( a =\u003E (a, t: (object)engineModel.GetAttachment( a, true )) ).Where( x =\u003E x.t is Transform ).ToDictionary( x =\u003E x.a, x =\u003E P( (Transform)x.t ) ),\r\n\t\t\t} );\r\n\t\t}\r\n\t\tEngineSequence = saved;\r\n\t\treturn result;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe floor the model stands on: z = 0 for normal models, or its lowest rest point when that is below.\u003C/summary\u003E\r\n\tstatic float GroundHeight( MotionRig rig )\r\n\t{\r\n\t\tvar lowest = float.MaxValue;\r\n\t\tfor ( var i = 0; i \u003C rig.Skeleton.Count; i\u002B\u002B ) lowest = MathF.Min( lowest, rig.Skeleton.RestWorld[i].Pos.Z );\r\n\t\treturn lowest \u003C 0f ? lowest : 0f;\r\n\t}\r\n\r\n\t// ---------------------------------------------------------------- camera\r\n\r\n\tVector3? _followCenter;\r\n\r\n\tvoid UpdateCamera( Vector3 goal )\r\n\t{\r\n\t\t_followCenter = _followCenter is { } c ? Vector3.Lerp( c, goal, 1f - MathF.Exp( -RealTime.Delta / 0.3f ) ) : goal;\r\n\t\tvar center = _followCenter.Value \u002B _pan;\r\n\t\tvar yaw = MathX.DegreeToRadian( _yaw );\r\n\t\tvar pitch = MathX.DegreeToRadian( _pitch );\r\n\t\tvar dir = new Vector3( MathF.Cos( yaw ) * MathF.Cos( pitch ), MathF.Sin( yaw ) * MathF.Cos( pitch ), MathF.Sin( pitch ) ).Normal;\r\n\t\t_camera.WorldPosition = center \u002B dir * _distance;\r\n\t\t_camera.WorldRotation = Rotation.LookAt( -dir, Vector3.Up );\r\n\t}\r\n\r\n\tprotected override void OnMousePress( MouseEvent e )\r\n\t{\r\n\t\tbase.OnMousePress( e );\r\n\t\t_lastMouse = e.LocalPosition;\r\n\t\t// left-drag on empty space looks around (on a bone or a gizmo handle it picks or poses instead)\r\n\t\t_orbiting = e.LeftMouseButton \u0026\u0026 !_gizmoHovered;\r\n\t}\r\n\r\n\tprotected override void OnMouseReleased( MouseEvent e )\r\n\t{\r\n\t\tbase.OnMouseReleased( e );\r\n\t\tif ( e.LeftMouseButton ) _orbiting = false;\r\n\t}\r\n\r\n\tbool _orbiting;\r\n\r\n\tprotected override void OnMouseMove( MouseEvent e )\r\n\t{\r\n\t\tbase.OnMouseMove( e );\r\n\t\tvar delta = e.LocalPosition - _lastMouse;\r\n\t\t_lastMouse = e.LocalPosition;\r\n\t\tif ( (e.ButtonState \u0026 MouseButtons.Right) != 0 || (_orbiting \u0026\u0026 (e.ButtonState \u0026 MouseButtons.Left) != 0) )\r\n\t\t{\r\n\t\t\t_yaw -= delta.x * 0.4f;\r\n\t\t\t_pitch = Math.Clamp( _pitch \u002B delta.y * 0.3f, -15f, 85f );\r\n\t\t}\r\n\t\telse if ( (e.ButtonState \u0026 MouseButtons.Middle) != 0 )\r\n\t\t{\r\n\t\t\tvar scale = _distance * 0.0016f;\r\n\t\t\t_pan \u002B= (_camera.WorldRotation.Left * delta.x \u002B _camera.WorldRotation.Up * delta.y) * scale;\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnWheel( WheelEvent e )\r\n\t{\r\n\t\tbase.OnWheel( e );\r\n\t\t_distance = Math.Clamp( _distance * (e.Delta \u003E 0 ? 0.9f : 1.1f), 10f, 4000f );\r\n\t\te.Accept();\r\n\t}\r\n\r\n\tprotected override void OnDoubleClick( MouseEvent e )\r\n\t{\r\n\t\tbase.OnDoubleClick( e );\r\n\t\tif ( e.LeftMouseButton \u0026\u0026 !_gizmoHovered ) FrameCharacter();\r\n\t}\r\n\r\n\t// ---------------------------------------------------------------- overlays\r\n\r\n\tvoid DrawGround( Vector3 center, float z, float size )\r\n\t{\r\n\t\tusing var scope = Gizmo.Scope( \u0022t2a-ground\u0022 );\r\n\t\tGizmo.Transform = Transform.Zero;\r\n\t\tGizmo.Draw.IgnoreDepth = false;\r\n\t\tGizmo.Draw.LineThickness = 1f;\r\n\t\tvar step = MathF.Max( size / 6f, 2f );\r\n\t\tconst int cells = 12;\r\n\t\tvar block = step * 4f;\r\n\t\tvar ox = MathF.Round( center.x / block ) * block;\r\n\t\tvar oy = MathF.Round( center.y / block ) * block;\r\n\t\tvar extent = cells * step;\r\n\t\tfor ( var i = -cells; i \u003C= cells; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar fade = 1f - MathF.Abs( i ) / (cells \u002B 1f);\r\n\t\t\tGizmo.Draw.Color = Color.White.WithAlpha( (i == 0 ? .14f : .06f) * fade \u002B .012f );\r\n\t\t\tvar o = i * step;\r\n\t\t\tGizmo.Draw.Line( new Vector3( ox \u002B o, oy - extent, z ), new Vector3( ox \u002B o, oy \u002B extent, z ) );\r\n\t\t\tGizmo.Draw.Line( new Vector3( ox - extent, oy \u002B o, z ), new Vector3( ox \u002B extent, oy \u002B o, z ) );\r\n\t\t}\r\n\t}\r\n\r\n\tvoid DrawTrajectory( List\u003CXForm[]\u003E frames, MotionRig rig, float z )\r\n\t{\r\n\t\tvar path = RootTools.HipsTrajectory( frames, rig );\r\n\t\tusing var scope = Gizmo.Scope( \u0022t2a-trajectory\u0022 );\r\n\t\tGizmo.Transform = Transform.Zero;\r\n\t\tGizmo.Draw.IgnoreDepth = true;\r\n\t\tGizmo.Draw.LineThickness = 2f;\r\n\t\tvar accent = TaStyle.Accent;\r\n\t\tvar current = _session.CurrentFrame;\r\n\t\tfor ( var f = 1; f \u003C path.Length; f\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar past = f \u003C= current;\r\n\t\t\tGizmo.Draw.Color = accent.WithAlpha( past ? .9f : .35f );\r\n\t\t\tGizmo.Draw.Line( new Vector3( path[f - 1].X, path[f - 1].Y, z \u002B .2f ), new Vector3( path[f].X, path[f].Y, z \u002B .2f ) );\r\n\t\t}\r\n\t\t// heading ticks every half second\r\n\t\tvar every = Math.Max( 1, (int)MathF.Round( (_session.ActiveClip?.Fps ?? 30f) * 0.5f ) );\r\n\t\tGizmo.Draw.LineThickness = 1.5f;\r\n\t\tfor ( var f = 0; f \u003C frames.Count; f \u002B= every )\r\n\t\t{\r\n\t\t\tvar heading = RootTools.RootHeading( frames, rig, f );\r\n\t\t\tvar dir = System.Numerics.Vector3.Transform( rig.Forward, RootTools.Yaw( rig, heading ) );\r\n\t\t\tvar p = new Vector3( path[f].X, path[f].Y, z \u002B .2f );\r\n\t\t\tGizmo.Draw.Color = Color.White.WithAlpha( .35f );\r\n\t\t\tGizmo.Draw.Line( p, p \u002B new Vector3( dir.X, dir.Y, 0 ) * rig.Cm( 12f ) );\r\n\t\t}\r\n\t\t// pinned frames (in-between constraints)\r\n\t\tif ( _session.ActiveClip is { } clip )\r\n\t\t{\r\n\t\t\tGizmo.Draw.Color = Theme.Yellow.WithAlpha( .9f );\r\n\t\t\tforeach ( var pin in clip.PinnedFrames.Where( p =\u003E p \u003C path.Length ) )\r\n\t\t\t\tGizmo.Draw.LineCircle( new Vector3( path[pin].X, path[pin].Y, z \u002B .2f ), Vector3.Up, rig.Cm( 6f ), 0, 360, 4 );\r\n\t\t}\r\n\t\tvar at = path[Math.Clamp( current, 0, path.Length - 1 )];\r\n\t\tGizmo.Draw.Color = accent;\r\n\t\tGizmo.Draw.LineCircle( new Vector3( at.X, at.Y, z \u002B .2f ), Vector3.Up, rig.Cm( 10f ), 0, 360, 24 );\r\n\t}\r\n\r\n\tvoid DrawSkeleton( MotionRig rig )\r\n\t{\r\n\t\tvar clip = _session.ActiveClip;\r\n\t\tusing var scope = Gizmo.Scope( \u0022t2a-skeleton\u0022 );\r\n\t\tGizmo.Transform = Transform.Zero;\r\n\t\tGizmo.Draw.IgnoreDepth = true;\r\n\t\tGizmo.Draw.LineThickness = 2f;\r\n\t\tfor ( var i = 0; i \u003C rig.Skeleton.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar parent = rig.Skeleton[i].ParentIndex;\r\n\t\t\tif ( parent \u003C 0 || !rig.IsMotionBone( i ) ) continue;\r\n\t\t\tvar locked = clip?.LockedBones.Contains( rig.Skeleton[i].Name ) == true;\r\n\t\t\tvar selected = _session.SelectedBones.Contains( i );\r\n\t\t\tGizmo.Draw.Color = selected ? TaStyle.AccentLight : locked ? Theme.Yellow.WithAlpha( .95f ) : TaStyle.Accent.WithAlpha( .55f );\r\n\t\t\tGizmo.Draw.Line( ToVec( _world[parent].Pos ), ToVec( _world[i].Pos ) );\r\n\t\t}\r\n\t}\r\n\r\n\tvoid DrawBonePicking( MotionRig rig )\r\n\t{\r\n\t\tvar clip = _session.ActiveClip;\r\n\t\tvar radius = MathF.Max( rig.Cm( 1.6f ), 0.45f );\r\n\t\tvar clicked = false;\r\n\t\tfor ( var i = 0; i \u003C rig.Skeleton.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( !rig.IsMotionBone( i ) ) continue;\r\n\t\t\tusing ( Gizmo.Scope( $\u0022t2a-bone/{i}\u0022, new Transform( ToVec( _world[i].Pos ) ) ) )\r\n\t\t\t{\r\n\t\t\t\tGizmo.Hitbox.DepthBias = 0.01f;\r\n\t\t\t\tGizmo.Hitbox.Sphere( new Sphere( 0, radius ) );\r\n\t\t\t\tGizmo.Hitbox.TrySetHovered( 0f );\r\n\t\t\t\tvar selected = _session.SelectedBones.Contains( i );\r\n\t\t\t\tvar locked = clip?.LockedBones.Contains( rig.Skeleton[i].Name ) == true;\r\n\t\t\t\tif ( ShowSkeleton || selected || Gizmo.IsHovered )\r\n\t\t\t\t{\r\n\t\t\t\t\tGizmo.Draw.IgnoreDepth = true;\r\n\t\t\t\t\tGizmo.Draw.Color = selected ? TaStyle.AccentLight : Gizmo.IsHovered ? Color.White : locked ? Theme.Yellow : TaStyle.Accent.WithAlpha( .7f );\r\n\t\t\t\t\tGizmo.Draw.LineSphere( new Sphere( 0, radius ), 6 );\r\n\t\t\t\t}\r\n\t\t\t\tif ( Gizmo.IsHovered \u0026\u0026 Gizmo.WasLeftMousePressed \u0026\u0026 !clicked )\r\n\t\t\t\t{\r\n\t\t\t\t\tclicked = true;\r\n\t\t\t\t\tvar additive = global::Editor.Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Ctrl ) || global::Editor.Application.KeyboardModifiers.HasFlag( KeyboardModifiers.Shift );\r\n\t\t\t\t\t_session.SelectBone( i, additive );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\t// click on empty space clears the selection (but not while a handle is used)\r\n\t\tif ( !clicked \u0026\u0026 Gizmo.WasLeftMousePressed \u0026\u0026 !Gizmo.HasHovered \u0026\u0026 !Gizmo.Pressed.Any \u0026\u0026 IsUnderMouse )\r\n\t\t\t_session.SelectBone( null );\r\n\t}\r\n\r\n\tvoid DrawPoseGizmo( MotionRig rig )\r\n\t{\r\n\t\tif ( _session.PrimaryBone is not int bone || _session.ActiveClip is not { } clip || _session.Busy )\r\n\t\t{\r\n\t\t\tEndDrag();\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tvar name = rig.Skeleton[bone].Name;\r\n\t\tvar parent = rig.Skeleton[bone].ParentIndex;\r\n\t\tvar world = _world[bone];\r\n\t\tusing ( Gizmo.Scope( $\u0022t2a-pose/{bone}\u0022, new Transform( ToVec( world.Pos ), new Rotation( world.Rot.X, world.Rot.Y, world.Rot.Z, world.Rot.W ) ) ) )\r\n\t\t{\r\n\t\t\tGizmo.Hitbox.DepthBias = 0.01f;\r\n\t\t\tvar moved = false;\r\n\t\t\tXForm newWorld = world;\r\n\t\t\tif ( Gizmo.Control.Rotate( \u0022rot\u0022, Rotation.Identity, out var delta ) )\r\n\t\t\t{\r\n\t\t\t\tvar d = new System.Numerics.Quaternion( delta.x, delta.y, delta.z, delta.w );\r\n\t\t\t\tnewWorld = new XForm( world.Pos, MathQ.Normalize( world.Rot * d ) );\r\n\t\t\t\tmoved = true;\r\n\t\t\t}\r\n\t\t\t// the motion root and the hips can also be moved\r\n\t\t\tif ( (bone == rig.RootIndex || bone == rig.HipsIndex) \u0026\u0026 Gizmo.Control.Position( \u0022pos\u0022, Vector3.Zero, out var move ) )\r\n\t\t\t{\r\n\t\t\t\tvar worldMove = new Rotation( world.Rot.X, world.Rot.Y, world.Rot.Z, world.Rot.W ) * move;\r\n\t\t\t\tnewWorld = new XForm( newWorld.Pos \u002B new System.Numerics.Vector3( worldMove.x, worldMove.y, worldMove.z ), newWorld.Rot );\r\n\t\t\t\tmoved = true;\r\n\t\t\t}\r\n\t\t\tif ( moved )\r\n\t\t\t{\r\n\t\t\t\tif ( !_dragging )\r\n\t\t\t\t{\r\n\t\t\t\t\t_dragging = true;\r\n\t\t\t\t\t_session.BeginInteractiveEdit( $\u0022Pose {name}\u0022 );\r\n\t\t\t\t}\r\n\t\t\t\tvar parentWorld = parent \u003C 0 ? XForm.Identity : _world[parent];\r\n\t\t\t\tvar targetLocal = XForm.ToLocal( parentWorld, newWorld );\r\n\t\t\t\t_session.SetPoseKey( bone, _session.CurrentFrame, targetLocal );\r\n\t\t\t}\r\n\t\t\tif ( _dragging \u0026\u0026 !Gizmo.Pressed.Any ) EndDrag();\r\n\t\t}\r\n\t}\r\n\r\n\tvoid EndDrag()\r\n\t{\r\n\t\tif ( !_dragging ) return;\r\n\t\t_dragging = false;\r\n\t\t_session.EndInteractiveEdit();\r\n\t}\r\n\r\n\tpublic override void OnDestroyed()\r\n\t{\r\n\t\tbase.OnDestroyed();\r\n\t\t_sceneModel?.Delete();\r\n\t\tDeleteEngineModel();\r\n\t\tScene?.Destroy();\r\n\t\tScene = null;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ERenders the view to PNG bytes (used by the test gate for visual checks).\u003C/summary\u003E\r\n\tpublic byte[] RenderToPng( int width = 640, int height = 480 )\r\n\t{\r\n\t\tvar bitmap = new Bitmap( width, height );\r\n\t\t_camera.RenderToBitmap( bitmap );\r\n\t\treturn bitmap.ToPng();\r\n\t}\r\n}\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"Editor/TextToAnimation/UI/PromptComposer.cs","FileName":"PromptComposer.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":409724,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Editor;\r\nusing Sandbox;\r\n\r\nnamespace TextToAnimation.Editor.UI;\r\n\r\n/// \u003Csummary\u003EWhich part of an existing animation a text change applies to.\u003C/summary\u003E\r\npublic enum ChangeScope { WholeBody, UpperBody, LowerBody, Arms, SelectedBones, Unlocked }\r\n\r\n/// \u003Csummary\u003EThe settings behind the prompt. Simple users never need to touch them.\u003C/summary\u003E\r\npublic sealed class PromptOptions\r\n{\r\n\tpublic float Seconds { get; set; } = 4f;\r\n\tpublic int Takes { get; set; } = 1;\r\n\tpublic int Steps { get; set; } = 24;\r\n\tpublic float Guidance { get; set; } = 3f;\r\n\t/// \u003Csummary\u003ENull = a new random seed per generation.\u003C/summary\u003E\r\n\tpublic int? Seed { get; set; }\r\n\tpublic float VariationStrength { get; set; } = 0.5f;\r\n\tpublic ChangeScope Scope { get; set; } = ChangeScope.WholeBody;\r\n\t/// \u003Csummary\u003ESmooth the generated motion and lock planted feet (off = the model\u0027s raw output).\u003C/summary\u003E\r\n\tpublic bool CleanUp { get; set; } = true;\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The prompt row at the top of the timeline dock: what the prompt does (a chip), the text field\r\n/// (Enter sends, Shift\u002BEnter adds a line and the field grows up to three lines), which body part a change\r\n/// applies to, one settings chip (length, takes, quality, advanced) and a send button that becomes stop while\r\n/// generating. Styled like the editor\u0027s other inputs so it reads as part of the dock.\r\n/// \u003C/summary\u003E\r\npublic sealed class PromptComposer : Widget\r\n{\r\n\tconst float LineHeight = 17f;\r\n\tconst float SingleLine = 28f;\r\n\r\n\treadonly Layout _leading;\r\n\treadonly Field _field;\r\n\treadonly PromptBox _box;\r\n\treadonly ChipButton _scopeChip;\r\n\treadonly ChipButton _settingsChip;\r\n\treadonly SendButton _send;\r\n\tstring _target;\r\n\tstring _disabledReason;\r\n\tstring _placeholder;\r\n\tbool _busy;\r\n\tbool _showScope = true;\r\n\tbool _showLength = true;\r\n\r\n\tpublic PromptOptions Options { get; } = new();\r\n\r\n\t/// \u003Csummary\u003ECalled with the trimmed prompt when the user sends it.\u003C/summary\u003E\r\n\tpublic Action\u003Cstring\u003E Submitted { get; set; }\r\n\t/// \u003Csummary\u003ECalled when the user presses stop while busy.\u003C/summary\u003E\r\n\tpublic Action StopRequested { get; set; }\r\n\t/// \u003Csummary\u003EThe body parts offered by the scope chip.\u003C/summary\u003E\r\n\tpublic List\u003CChangeScope\u003E ScopeChoices { get; set; } = new() { ChangeScope.WholeBody };\r\n\t/// \u003Csummary\u003EAllows sending an empty prompt (variations, in-betweens).\u003C/summary\u003E\r\n\tpublic bool AllowEmpty { get; set; }\r\n\t/// \u003Csummary\u003ECalled when the user clicks the history icon (shows past prompts).\u003C/summary\u003E\r\n\tpublic Action\u003CWidget\u003E HistoryRequested { get; set; }\r\n\r\n\tpublic PromptComposer( Widget parent, string placeholder ) : base( parent )\r\n\t{\r\n\t\t_placeholder = placeholder;\r\n\t\tLayout = Layout.Row();\r\n\t\tLayout.Spacing = 6;\r\n\t\t_leading = Layout.AddRow();\r\n\t\t_leading.Spacing = 6;\r\n\t\t_field = Layout.Add( new Field( this ), 1 );\r\n\t\t_box = _field.Box;\r\n\t\t_box.Send = Send;\r\n\t\t_field.History.OnClick = () =\u003E HistoryRequested?.Invoke( _field.History );\r\n\t\t_box.TextChanged \u002B= _ =\u003E Grow();\r\n\t\t_scopeChip = Layout.Add( new ChipButton( this, \u0022\u0022, \u0022accessibility_new\u0022, ScopeMenu, \u0022Which part of the body the change applies to\u0022 ) );\r\n\t\t_settingsChip = Layout.Add( new ChipButton( this, \u0022\u0022, \u0022tune\u0022, SettingsMenu, \u0022Length, takes, quality and more\u0022 ) );\r\n\t\t_send = Layout.Add( new SendButton( this, () =\u003E { if ( _busy ) StopRequested?.Invoke(); else Send(); } ) );\r\n\t\tRefreshChips();\r\n\t\tRefreshPlaceholder();\r\n\t\tGrow();\r\n\t}\r\n\r\n\tpublic string Text\r\n\t{\r\n\t\tget =\u003E _box.PlainText ?? \u0022\u0022;\r\n\t\tset { _box.PlainText = value ?? \u0022\u0022; Grow(); }\r\n\t}\r\n\r\n\tpublic void FocusPrompt() =\u003E _box.Focus();\r\n\r\n\t/// \u003Csummary\u003EThe text input (tests post key events to it).\u003C/summary\u003E\r\n\tpublic Widget Input =\u003E _box;\r\n\r\n\t/// \u003Csummary\u003ESends the prompt as if the user pressed Enter.\u003C/summary\u003E\r\n\tpublic void Submit() =\u003E Send();\r\n\r\n\t/// \u003Csummary\u003EThe animation a change applies to (for the placeholder), or null for a new animation.\u003C/summary\u003E\r\n\tpublic void SetTarget( string clipName )\r\n\t{\r\n\t\t_target = clipName;\r\n\t\tRefreshChips();\r\n\t\tRefreshPlaceholder();\r\n\t}\r\n\r\n\tpublic string Target =\u003E _target;\r\n\r\n\t/// \u003Csummary\u003EAdds a chip before the text field (e.g. what the prompt does).\u003C/summary\u003E\r\n\tpublic ChipButton AddChip( string text, string icon, Action clicked, string tooltip )\r\n\t\t=\u003E _leading.Add( new ChipButton( this, text, icon, clicked, tooltip ) );\r\n\r\n\t/// \u003Csummary\u003EWhether the body-part chip is shown.\u003C/summary\u003E\r\n\tpublic bool ShowScope\r\n\t{\r\n\t\tget =\u003E _showScope;\r\n\t\tset { _showScope = value; if ( !value ) Options.Scope = ChangeScope.WholeBody; RefreshChips(); }\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EWhether the settings offer a length (new animations).\u003C/summary\u003E\r\n\tpublic bool ShowLength { get =\u003E _showLength; set { _showLength = value; RefreshChips(); } }\r\n\r\n\t/// \u003Csummary\u003ERe-reads the options into the chips (after changing \u003Csee cref=\u0022Options\u0022/\u003E in code).\u003C/summary\u003E\r\n\tpublic void RefreshOptions() =\u003E RefreshChips();\r\n\r\n\t/// \u003Csummary\u003EDisables sending, with the reason shown in the field (null enables).\u003C/summary\u003E\r\n\tpublic void SetDisabledReason( string reason )\r\n\t{\r\n\t\t_disabledReason = reason;\r\n\t\t_box.Editable = reason is null;\r\n\t\t_send.Enabled = reason is null || _busy;\r\n\t\tRefreshPlaceholder();\r\n\t}\r\n\r\n\tpublic bool Busy\r\n\t{\r\n\t\tget =\u003E _busy;\r\n\t\tset { _busy = value; _send.Stop = value; _send.Enabled = value || _disabledReason is null; _send.Update(); }\r\n\t}\r\n\r\n\tpublic string Placeholder { get =\u003E _placeholder; set { _placeholder = value; RefreshPlaceholder(); } }\r\n\r\n\tvoid RefreshPlaceholder() =\u003E _box.PlaceholderText = _disabledReason ?? _placeholder ?? \u0022Describe a motion\u2026\u0022;\r\n\r\n\tvoid Send()\r\n\t{\r\n\t\tif ( _busy || _disabledReason is not null ) return;\r\n\t\tvar text = Text.Trim();\r\n\t\tif ( text.Length == 0 \u0026\u0026 !AllowEmpty ) { _box.Focus(); return; }\r\n\t\tSubmitted?.Invoke( text );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EOne line by default; grows with Shift\u002BEnter lines up to three.\u003C/summary\u003E\r\n\tvoid Grow()\r\n\t{\r\n\t\tvar lines = Math.Clamp( (Text.Count( c =\u003E c == \u0027\\n\u0027 ) \u002B 1), 1, 3 );\r\n\t\tvar height = SingleLine \u002B (lines - 1) * LineHeight;\r\n\t\t_field.FixedHeight = height;\r\n\t\tFixedHeight = height;\r\n\t}\r\n\r\n\tvoid RefreshChips()\r\n\t{\r\n\t\t_scopeChip.Visible = _showScope \u0026\u0026 _target is not null;\r\n\t\t_scopeChip.Text = ScopeName( Options.Scope );\r\n\t\tvar parts = new List\u003Cstring\u003E();\r\n\t\tif ( _showLength ) parts.Add( $\u0022{Options.Seconds:0.#} s\u0022 );\r\n\t\tparts.Add( Options.Takes == 1 ? \u00221 take\u0022 : $\u0022{Options.Takes} takes\u0022 );\r\n\t\tparts.Add( Options.Steps \u003C= 12 ? \u0022Fast\u0022 : Options.Steps \u003E= 40 ? \u0022Best\u0022 : \u0022Standard\u0022 );\r\n\t\t_settingsChip.Text = string.Join( \u0022 \u00B7 \u0022, parts );\r\n\t}\r\n\r\n\tpublic static string ScopeName( ChangeScope scope ) =\u003E scope switch\r\n\t{\r\n\t\tChangeScope.UpperBody =\u003E \u0022Upper body\u0022,\r\n\t\tChangeScope.LowerBody =\u003E \u0022Legs\u0022,\r\n\t\tChangeScope.Arms =\u003E \u0022Arms\u0022,\r\n\t\tChangeScope.SelectedBones =\u003E \u0022Selected bones\u0022,\r\n\t\tChangeScope.Unlocked =\u003E \u0022Unlocked bones\u0022,\r\n\t\t_ =\u003E \u0022Whole body\u0022,\r\n\t};\r\n\r\n\tvoid Choose( Menu menu, string text, bool selected, Action apply )\r\n\t\t=\u003E menu.AddOption( text, selected ? \u0022check\u0022 : null, () =\u003E { apply(); RefreshChips(); } );\r\n\r\n\tvoid ScopeMenu()\r\n\t{\r\n\t\tvar menu = new Menu( this );\r\n\t\tmenu.AddHeading( \u0022The change applies to\u0022 );\r\n\t\tforeach ( var s in ScopeChoices )\r\n\t\t{\r\n\t\t\tvar scope = s;\r\n\t\t\tChoose( menu, ScopeName( scope ), Options.Scope == scope, () =\u003E Options.Scope = scope );\r\n\t\t}\r\n\t\tmenu.OpenAtCursor();\r\n\t}\r\n\r\n\tvoid SettingsMenu()\r\n\t{\r\n\t\tvar menu = new Menu( this );\r\n\t\tif ( _showLength )\r\n\t\t{\r\n\t\t\tmenu.AddHeading( \u0022Length\u0022 );\r\n\t\t\tforeach ( var s in new[] { 2f, 3f, 4f, 6f, 8f, 10f } )\r\n\t\t\t\tChoose( menu, $\u0022{s:0} seconds\u0022, MathF.Abs( Options.Seconds - s ) \u003C 0.01f, () =\u003E Options.Seconds = s );\r\n\t\t\tmenu.AddSeparator();\r\n\t\t}\r\n\t\tmenu.AddHeading( \u0022Takes\u0022 );\r\n\t\tfor ( var n = 1; n \u003C= 4; n\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar count = n;\r\n\t\t\tChoose( menu, count == 1 ? \u00221 take\u0022 : $\u0022{count} takes\u0022, Options.Takes == count, () =\u003E Options.Takes = count );\r\n\t\t}\r\n\t\tmenu.AddSeparator();\r\n\t\tmenu.AddHeading( \u0022Quality\u0022 );\r\n\t\tChoose( menu, \u0022Fast\u0022, Options.Steps == 12, () =\u003E Options.Steps = 12 );\r\n\t\tChoose( menu, \u0022Standard\u0022, Options.Steps == 24, () =\u003E Options.Steps = 24 );\r\n\t\tChoose( menu, \u0022Best (slowest)\u0022, Options.Steps == 40, () =\u003E Options.Steps = 40 );\r\n\t\tmenu.AddSeparator();\r\n\t\tvar advanced = menu.AddMenu( \u0022Advanced\u0022, \u0022more_horiz\u0022 );\r\n\t\tadvanced.AddHeading( \u0022Result\u0022 );\r\n\t\tChoose( advanced, \u0022Clean up (smooth, lock feet)\u0022, Options.CleanUp, () =\u003E Options.CleanUp = true );\r\n\t\tChoose( advanced, \u0022Raw model output\u0022, !Options.CleanUp, () =\u003E Options.CleanUp = false );\r\n\t\tadvanced.AddSeparator();\r\n\t\tadvanced.AddHeading( \u0022Prompt strength\u0022 );\r\n\t\tforeach ( var g in new[] { 2f, 3f, 4.5f } )\r\n\t\t\tChoose( advanced, g switch { 2f =\u003E \u0022Loose\u0022, 3f =\u003E \u0022Normal\u0022, _ =\u003E \u0022Strict\u0022 }, MathF.Abs( Options.Guidance - g ) \u003C 0.01f, () =\u003E Options.Guidance = g );\r\n\t\tadvanced.AddSeparator();\r\n\t\tadvanced.AddHeading( \u0022Variations are\u0022 );\r\n\t\tforeach ( var v in new[] { 0.3f, 0.5f, 0.75f } )\r\n\t\t\tChoose( advanced, v switch { 0.3f =\u003E \u0022Close to the original\u0022, 0.5f =\u003E \u0022Somewhat different\u0022, _ =\u003E \u0022Very different\u0022 }, MathF.Abs( Options.VariationStrength - v ) \u003C 0.01f, () =\u003E Options.VariationStrength = v );\r\n\t\tadvanced.AddSeparator();\r\n\t\tadvanced.AddHeading( \u0022Seed\u0022 );\r\n\t\tChoose( advanced, \u0022Random each time\u0022, Options.Seed is null, () =\u003E Options.Seed = null );\r\n\t\tChoose( advanced, Options.Seed is int fixedSeed ? $\u0022Fixed: {fixedSeed}\u0022 : \u0022Fixed (reproducible)\u0022, Options.Seed is not null,\r\n\t\t\t() =\u003E Options.Seed ??= Random.Shared.Next( 1, 99999 ) );\r\n\t\tmenu.OpenAtCursor();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe input\u0027s frame: the editor\u0027s field look, a sparkle on the left, blue edge when focused.\u003C/summary\u003E\r\n\tsealed class Field : Widget\r\n\t{\r\n\t\tpublic PromptBox Box { get; }\r\n\t\tpublic IconButton History { get; }\r\n\r\n\t\tpublic Field( Widget parent ) : base( parent )\r\n\t\t{\r\n\t\t\tLayout = Layout.Row();\r\n\t\t\tLayout.Margin = new Sandbox.UI.Margin( 26, 0, 2, 0 );\r\n\t\t\tBox = Layout.Add( new PromptBox( this ), 1 );\r\n\t\t\tHistory = Layout.Add( new IconButton( \u0022history\u0022, null, this )\r\n\t\t\t{\r\n\t\t\t\tFixedSize = 24, IconSize = 16, Background = Color.Transparent, Foreground = Theme.TextLight,\r\n\t\t\t\tToolTip = \u0022Recent prompts\u0022,\r\n\t\t\t} );\r\n\t\t}\r\n\r\n\t\tprotected override void OnPaint()\r\n\t\t{\r\n\t\t\tPaint.Antialiasing = true;\r\n\t\t\tvar focused = Box.IsFocused;\r\n\t\t\tPaint.SetPen( focused ? TaStyle.Accent : TaStyle.InputEdge, 1 );\r\n\t\t\tPaint.SetBrush( Theme.WindowBackground );\r\n\t\t\tPaint.DrawRect( LocalRect.Shrink( .5f ), TaStyle.Radius );\r\n\t\t\tPaint.SetPen( focused ? TaStyle.AccentLight : TaStyle.Accent );\r\n\t\t\tPaint.DrawIcon( new Rect( 6, 0, 16, MathF.Min( Height, SingleLine ) ), \u0022auto_awesome\u0022, 14, TextFlag.Center );\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe text area: Enter sends, Shift\u002BEnter is a new line.\u003C/summary\u003E\r\n\tsealed class PromptBox : TextEdit\r\n\t{\r\n\t\tpublic Action Send { get; set; }\r\n\r\n\t\tpublic PromptBox( Widget parent ) : base( parent )\r\n\t\t{\r\n\t\t\tSetStyles( \u0022background-color: transparent; border: none; font-size: 13px; padding-top: 3px;\u0022 );\r\n\t\t\tVerticalScrollbarMode = ScrollbarMode.Off;\r\n\t\t\tHorizontalScrollbarMode = ScrollbarMode.Off;\r\n\t\t}\r\n\r\n\t\tprotected override void OnKeyPress( KeyEvent e )\r\n\t\t{\r\n\t\t\tif ( (e.Key == KeyCode.Enter || e.Key == KeyCode.Return) \u0026\u0026 !e.HasShift )\r\n\t\t\t{\r\n\t\t\t\te.Accepted = true;\r\n\t\t\t\tSend?.Invoke();\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t\tbase.OnKeyPress( e );\r\n\t\t}\r\n\r\n\t\tprotected override void OnFocus( FocusChangeReason reason ) { base.OnFocus( reason ); Parent?.Update(); }\r\n\t\tprotected override void OnBlur( FocusChangeReason reason ) { base.OnBlur( reason ); Parent?.Update(); }\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ERound blue send button; a stop square while busy.\u003C/summary\u003E\r\n\tsealed class SendButton : Widget\r\n\t{\r\n\t\treadonly Action _clicked;\r\n\t\tpublic bool Stop { get; set; }\r\n\r\n\t\tpublic SendButton( Widget parent, Action clicked ) : base( parent )\r\n\t\t{\r\n\t\t\t_clicked = clicked;\r\n\t\t\tFixedSize = 28;\r\n\t\t\tCursor = CursorShape.Finger;\r\n\t\t\tMouseTracking = true;\r\n\t\t\tToolTip = \u0022Generate (Enter)\u0022;\r\n\t\t}\r\n\r\n\t\tprotected override void OnMouseEnter() =\u003E Update();\r\n\t\tprotected override void OnMouseLeave() =\u003E Update();\r\n\r\n\t\tprotected override void OnMousePress( MouseEvent e )\r\n\t\t{\r\n\t\t\tif ( e.LeftMouseButton ) e.Accepted = true;\r\n\t\t}\r\n\r\n\t\tprotected override void OnMouseReleased( MouseEvent e )\r\n\t\t{\r\n\t\t\tbase.OnMouseReleased( e );\r\n\t\t\tif ( Enabled \u0026\u0026 e.LeftMouseButton \u0026\u0026 LocalRect.IsInside( e.LocalPosition ) ) _clicked();\r\n\t\t}\r\n\r\n\t\tprotected override void OnPaint()\r\n\t\t{\r\n\t\t\tPaint.Antialiasing = true;\r\n\t\t\tPaint.ClearPen();\r\n\t\t\tPaint.SetBrush( !Enabled ? Color.White.WithAlpha( .1f ) : Paint.HasMouseOver ? TaStyle.AccentLight : TaStyle.Accent );\r\n\t\t\tPaint.DrawRect( LocalRect.Shrink( 1 ), 14 );\r\n\t\t\tPaint.SetPen( Enabled ? Color.White : Theme.TextDisabled );\r\n\t\t\tPaint.DrawIcon( LocalRect, Stop ? \u0022stop\u0022 : \u0022arrow_upward\u0022, 17, TextFlag.Center );\r\n\t\t\tToolTip = Stop ? \u0022Stop\u0022 : \u0022Generate (Enter)\u0022;\r\n\t\t}\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003EA compact chip (icon, text, \u25BE) in the editor\u0027s button style, for options and modes.\u003C/summary\u003E\r\npublic sealed class ChipButton : Widget\r\n{\r\n\tstring _text;\r\n\treadonly string _icon;\r\n\tpublic Action Clicked { get; set; }\r\n\r\n\t/// \u003Csummary\u003EDraws the dropdown arrow (chips that open a menu); off for plain one-click chips.\u003C/summary\u003E\r\n\tpublic bool Arrow { get; set; } = true;\r\n\r\n\tfloat ArrowRoom =\u003E Arrow ? 20 : 10;\r\n\r\n\tpublic ChipButton( Widget parent, string text, string icon, Action clicked, string tooltip ) : base( parent )\r\n\t{\r\n\t\t_icon = icon;\r\n\t\tClicked = clicked;\r\n\t\tToolTip = tooltip;\r\n\t\tFixedHeight = 28;\r\n\t\tCursor = CursorShape.Finger;\r\n\t\tMouseTracking = true;\r\n\t\tText = text;\r\n\t}\r\n\r\n\tpublic string Text\r\n\t{\r\n\t\tget =\u003E _text;\r\n\t\tset\r\n\t\t{\r\n\t\t\t_text = value ?? \u0022\u0022;\r\n\t\t\tFixedWidth = MathF.Ceiling( 8 \u002B 16 \u002B 5 \u002B _text.Length * 6.2f \u002B ArrowRoom );\r\n\t\t\tUpdate();\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnMouseEnter() =\u003E Update();\r\n\tprotected override void OnMouseLeave() =\u003E Update();\r\n\r\n\tprotected override void OnMousePress( MouseEvent e )\r\n\t{\r\n\t\tif ( e.LeftMouseButton ) e.Accepted = true;\r\n\t}\r\n\r\n\tprotected override void OnMouseReleased( MouseEvent e )\r\n\t{\r\n\t\tbase.OnMouseReleased( e );\r\n\t\tif ( Enabled \u0026\u0026 e.LeftMouseButton \u0026\u0026 LocalRect.IsInside( e.LocalPosition ) ) Clicked?.Invoke();\r\n\t}\r\n\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tPaint.Antialiasing = true;\r\n\t\tvar hover = Paint.HasMouseOver \u0026\u0026 Enabled;\r\n\t\tPaint.SetPen( Color.Lerp( Theme.ControlBackground.WithAlpha( 1f ), Color.White, hover ? .25f : .15f ), 1 );\r\n\t\tPaint.SetBrush( hover ? Color.Lerp( TaStyle.ButtonFill, Color.White, .06f ) : TaStyle.ButtonFill );\r\n\t\tPaint.DrawRect( LocalRect.Shrink( .5f ), TaStyle.Radius );\r\n\t\tPaint.SetPen( Enabled ? TaStyle.AccentLight : Theme.TextDisabled );\r\n\t\tPaint.DrawIcon( new Rect( 8, 0, 16, Height ), _icon, 14, TextFlag.Center );\r\n\t\tPaint.SetDefaultFont( 8 );\r\n\t\tPaint.SetPen( Enabled ? Theme.Text : Theme.TextDisabled );\r\n\t\tvar textWidth = Paint.MeasureText( _text ).x;\r\n\t\tvar wanted = MathF.Ceiling( 8 \u002B 16 \u002B 5 \u002B textWidth \u002B ArrowRoom );\r\n\t\tif ( MathF.Abs( wanted - FixedWidth ) \u003E .5f ) FixedWidth = wanted;\r\n\t\tPaint.DrawText( new Rect( 29, 0, textWidth \u002B 2, Height ), _text, TextFlag.LeftCenter );\r\n\t\tif ( !Arrow ) return;\r\n\t\tPaint.SetPen( Theme.TextLight );\r\n\t\tPaint.DrawIcon( new Rect( Width - 18, 0, 14, Height ), \u0022expand_more\u0022, 13, TextFlag.Center );\r\n\t}\r\n}\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"Code/TextToAnimation/Assembly.cs","FileName":"Assembly.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":409724,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\n// Global usings for the s\u0026box in-engine compiler.\r\n//\r\n// The plain net8.0 dev harness gets these automatically via \u003CImplicitUsings\u003E,\r\n// but s\u0026box\u0027s compiler injects no BCL usings at all - without this file the\r\n// library fails to compile inside the editor (CS0246 on List\u003C\u003E, IEnumerable\u003C\u003E,\r\n// FormatException, ...). Duplicating the SDK\u0027s implicit set is harmless there\r\n// (verified: no warnings).\r\n\r\nglobal using System;\r\nglobal using System.Collections.Generic;\r\nglobal using System.IO;\r\nglobal using System.Linq;\r\nglobal using System.Threading;\r\nglobal using System.Threading.Tasks;\r\n\r\n// NOTE on Vector3: s\u0026box declares its own Vector3 in the *global namespace*,\r\n// which wins over \u0060using System.Numerics;\u0060 imports during name lookup and\r\n// breaks this System.Numerics-based core (.X/.Y/.Z, static helpers, delegate\r\n// signatures). A global using-alias does NOT fix this (CS0576: alias conflicts\r\n// with the global-namespace type at every use site). The working fix is a\r\n// *namespace-scoped* alias, declared after the file-scoped namespace line:\r\n//\r\n//     namespace TextToAnimation.Xyz;\r\n//     using Vector3 = System.Numerics.Vector3;\r\n//\r\n// Every file in this tree that uses the simple name Vector3 carries that line.\r\n// (Quaternion and Matrix4x4 are not global-namespace types in s\u0026box and need\r\n// no alias.)\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"Code/TextToAnimation/Mapping/MappingResult.cs","FileName":"MappingResult.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":409724,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System.Collections.Generic;\r\n\r\nnamespace TextToAnimation.Mapping;\r\n\r\n/// \u003Csummary\u003EHow a mapping was produced; determines UI flow and preset-learning behavior.\u003C/summary\u003E\r\npublic enum MappingSource\r\n{\r\n    /// \u003Csummary\u003EA shipped preset profile matched (mixamo, actorcore_cc, ...).\u003C/summary\u003E\r\n    Preset,\r\n\r\n    /// \u003Csummary\u003EA user-saved preset (keyed by \u003Csee cref=\u0022SkeletonSignature\u0022/\u003E) matched.\u003C/summary\u003E\r\n    UserPreset,\r\n\r\n    /// \u003Csummary\u003EThe auto-mapper resolved roles from bone-name tokens (stage A).\u003C/summary\u003E\r\n    AutoName,\r\n\r\n    /// \u003Csummary\u003EThe auto-mapper fell back to pure hierarchy/geometry matching (stage B).\u003C/summary\u003E\r\n    AutoTopology,\r\n\r\n    /// \u003Csummary\u003EThe user assigned roles by hand in the mapping editor.\u003C/summary\u003E\r\n    Manual,\r\n\r\n    /// \u003Csummary\u003E\r\n    /// The source file itself declares the mapping \u2014 e.g. a VRM\u0027s\r\n    /// \u003Cc\u003Ehumanoid.humanBones\u003C/c\u003E block (both the 0.x \u003Cc\u003Eextensions.VRM\u003C/c\u003E and the 1.0\r\n    /// \u003Cc\u003Eextensions.VRMC_vrm\u003C/c\u003E layouts). Authoritative: consulted before user presets and\r\n    /// shipped-profile detection, at confidence 1.0.\r\n    /// \u003C/summary\u003E\r\n    Authored,\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The outcome of mapping a source skeleton onto canonical \u003Csee cref=\u0022BoneRole\u0022/\u003Es:\r\n/// role \u2192 source bone index, an overall confidence, and human-readable notes\r\n/// (unmapped roles, ignored bones, ambiguities) for the mapping report.\r\n/// \u003C/summary\u003E\r\npublic sealed class MappingResult\r\n{\r\n    /// \u003Csummary\u003ECreates an empty result; callers fill \u003Csee cref=\u0022RoleToBone\u0022/\u003E and\r\n    /// \u003Csee cref=\u0022Confidence\u0022/\u003E.\u003C/summary\u003E\r\n    public MappingResult(string profileName, MappingSource source)\r\n    {\r\n        ProfileName = profileName;\r\n        Source = source;\r\n    }\r\n\r\n    /// \u003Csummary\u003EResolved roles: canonical role \u2192 bone index in the source skeleton.\u003C/summary\u003E\r\n    public Dictionary\u003CBoneRole, int\u003E RoleToBone { get; } = new();\r\n\r\n    /// \u003Csummary\u003EMapping confidence in [0, 1]; 1 means every required and optional role\r\n    /// resolved unambiguously.\u003C/summary\u003E\r\n    public float Confidence { get; set; }\r\n\r\n    /// \u003Csummary\u003EProfile that produced the mapping \u2014 a preset name, or \u003Cc\u003E\u0022auto\u0022\u003C/c\u003E /\r\n    /// \u003Cc\u003E\u0022topology\u0022\u003C/c\u003E for the auto-mapper stages.\u003C/summary\u003E\r\n    public string ProfileName { get; }\r\n\r\n    /// \u003Csummary\u003EWhere the mapping came from.\u003C/summary\u003E\r\n    public MappingSource Source { get; }\r\n\r\n    /// \u003Csummary\u003EReport notes: unmapped roles, ignored source bones, ambiguities.\u003C/summary\u003E\r\n    public List\u003Cstring\u003E Notes { get; } = new();\r\n}\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"Code/TextToAnimation/Maths/MathQ.cs","FileName":"MathQ.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":409724,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Numerics;\r\n\r\nnamespace TextToAnimation.Maths;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/TextToAnimation/Assembly.cs)\r\n\r\n/// \u003Csummary\u003E\r\n/// Quaternion algebra helpers used throughout the retargeting pipeline.\r\n/// All angles are radians; quaternions are XYZW (\u003Csee cref=\u0022System.Numerics.Quaternion\u0022/\u003E native);\r\n/// \u003Cc\u003Ea * b\u003C/c\u003E applies \u003Cc\u003Eb\u003C/c\u003E first (column-vector convention).\r\n/// \u003C/summary\u003E\r\npublic static class MathQ\r\n{\r\n    private const float NearZeroLengthSq = 1e-12f;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Defensively normalizes a quaternion. Returns \u003Csee cref=\u0022Quaternion.Identity\u0022/\u003E when the\r\n    /// input has near-zero length or non-finite components instead of producing NaNs.\r\n    /// \u003C/summary\u003E\r\n    public static Quaternion Normalize(Quaternion q)\r\n    {\r\n        var lengthSq = q.LengthSquared();\r\n        if (!float.IsFinite(lengthSq) || lengthSq \u003C NearZeroLengthSq)\r\n            return Quaternion.Identity;\r\n        return Quaternion.Multiply(q, 1f / MathF.Sqrt(lengthSq));\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Shortest-arc rotation taking direction \u003Cparamref name=\u0022a\u0022/\u003E onto direction \u003Cparamref name=\u0022b\u0022/\u003E\r\n    /// (magnitudes are ignored). For antiparallel inputs a 180\u00B0 rotation about a stable axis\r\n    /// perpendicular to \u003Cparamref name=\u0022a\u0022/\u003E is returned. Near-zero inputs yield identity.\r\n    /// \u003C/summary\u003E\r\n    public static Quaternion FromTo(Vector3 a, Vector3 b)\r\n    {\r\n        if (a.LengthSquared() \u003C NearZeroLengthSq || b.LengthSquared() \u003C NearZeroLengthSq)\r\n            return Quaternion.Identity;\r\n\r\n        var an = Vector3.Normalize(a);\r\n        var bn = Vector3.Normalize(b);\r\n        var dot = Vector3.Dot(an, bn);\r\n\r\n        if (dot \u003E= 1f - 1e-8f)\r\n            return Quaternion.Identity;\r\n\r\n        if (dot \u003C= -1f \u002B 1e-7f)\r\n        {\r\n            // Antiparallel: any axis perpendicular to a works; pick a stable one.\r\n            return Quaternion.CreateFromAxisAngle(Perpendicular(an), MathF.PI);\r\n        }\r\n\r\n        // q = (a x b, 1 \u002B a\u00B7b), normalized \u2014 the half-angle construction of the shortest arc.\r\n        var cross = Vector3.Cross(an, bn);\r\n        return Normalize(new Quaternion(cross.X, cross.Y, cross.Z, 1f \u002B dot));\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Decomposes a rotation into \u003Cparamref name=\u0022swing\u0022/\u003E and \u003Cparamref name=\u0022twist\u0022/\u003E so that\r\n    /// \u003Cc\u003Eq == swing * twist\u003C/c\u003E (twist applied first). The twist is the projection of the\r\n    /// rotation onto \u003Cparamref name=\u0022twistAxis\u0022/\u003E: its rotation axis is parallel to\r\n    /// \u003Cparamref name=\u0022twistAxis\u0022/\u003E, while the swing\u0027s rotation axis is perpendicular to it.\r\n    /// When the rotation is a pure 180\u00B0 swing (no component about the axis), twist is identity.\r\n    /// \u003C/summary\u003E\r\n    /// \u003Cexception cref=\u0022ArgumentException\u0022\u003EThrown when \u003Cparamref name=\u0022twistAxis\u0022/\u003E has near-zero length.\u003C/exception\u003E\r\n    public static void SwingTwist(Quaternion q, Vector3 twistAxis, out Quaternion swing, out Quaternion twist)\r\n    {\r\n        if (twistAxis.LengthSquared() \u003C NearZeroLengthSq)\r\n            throw new ArgumentException(\u0022Twist axis must be non-zero.\u0022, nameof(twistAxis));\r\n\r\n        var axis = Vector3.Normalize(twistAxis);\r\n        q = Normalize(q);\r\n\r\n        // Project the rotation\u0027s vector part onto the twist axis.\r\n        var projection = q.X * axis.X \u002B q.Y * axis.Y \u002B q.Z * axis.Z;\r\n        var raw = new Quaternion(axis.X * projection, axis.Y * projection, axis.Z * projection, q.W);\r\n\r\n        // Degenerate case (q.W ~ 0 and projection ~ 0): pure 180\u00B0 swing, no twist.\r\n        twist = Normalize(raw);\r\n        swing = Normalize(q * Quaternion.Conjugate(twist));\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Geodesic angle between two rotations in radians, in [0, \u03C0], invariant under the\r\n    /// q / -q double cover (so \u003Cc\u003EAngleBetween(q, -q) == 0\u003C/c\u003E).\r\n    /// \u003C/summary\u003E\r\n    public static float AngleBetween(Quaternion a, Quaternion b)\r\n    {\r\n        // Use atan2 on the delta rotation rather than acos on the dot product: acos is\r\n        // ill-conditioned near 0\u00B0, atan2 is accurate over the whole range.\r\n        var delta = Quaternion.Conjugate(Normalize(a)) * Normalize(b);\r\n        var sinHalf = MathF.Sqrt(delta.X * delta.X \u002B delta.Y * delta.Y \u002B delta.Z * delta.Z);\r\n        var cosHalf = MathF.Abs(delta.W); // |W| folds the q / -q double cover.\r\n        return 2f * MathF.Atan2(sinHalf, cosHalf);\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Angle between two directions in radians, in [0, \u03C0] (magnitudes are ignored).\r\n    /// Near-zero inputs yield 0. Uses atan2, which stays accurate near 0 and \u03C0 where\r\n    /// acos of the dot product loses precision.\r\n    /// \u003C/summary\u003E\r\n    public static float AngleBetween(Vector3 a, Vector3 b)\r\n    {\r\n        if (a.LengthSquared() \u003C NearZeroLengthSq || b.LengthSquared() \u003C NearZeroLengthSq)\r\n            return 0f;\r\n        return MathF.Atan2(Vector3.Cross(a, b).Length(), Vector3.Dot(a, b));\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Builds an orthonormal right-handed basis rotation from a forward and an up hint.\r\n    /// \u003C/summary\u003E\r\n    /// \u003Cremarks\u003E\r\n    /// Convention: returns the rotation \u003Cc\u003ER\u003C/c\u003E such that\r\n    /// \u003Clist type=\u0022bullet\u0022\u003E\r\n    /// \u003Citem\u003E\u003Cc\u003Erotate(R, \u002BY) == normalize(forward)\u003C/c\u003E (primary axis),\u003C/item\u003E\r\n    /// \u003Citem\u003E\u003Cc\u003Erotate(R, \u002BZ) == \u003C/c\u003E the Gram-Schmidt orthonormalization of \u003Cparamref name=\u0022up\u0022/\u003E\r\n    /// against forward (secondary axis),\u003C/item\u003E\r\n    /// \u003Citem\u003E\u003Cc\u003Erotate(R, \u002BX) == forwardN x upOrtho\u003C/c\u003E (right-handed completion).\u003C/item\u003E\r\n    /// \u003C/list\u003E\r\n    /// Degenerate inputs are handled defensively: a near-zero forward yields identity; an up hint\r\n    /// (near-)parallel to forward falls back to world \u002BZ, or world \u002BX when forward itself is\r\n    /// (near-)parallel to \u002BZ.\r\n    /// \u003C/remarks\u003E\r\n    public static Quaternion BasisFromForwardUp(Vector3 forward, Vector3 up)\r\n    {\r\n        if (forward.LengthSquared() \u003C NearZeroLengthSq)\r\n            return Quaternion.Identity;\r\n\r\n        var y = Vector3.Normalize(forward);\r\n\r\n        var z = up - y * Vector3.Dot(up, y);\r\n        if (z.LengthSquared() \u003C 1e-8f)\r\n        {\r\n            // Up hint unusable: pick a stable fallback and orthonormalize it.\r\n            var fallback = MathF.Abs(Vector3.Dot(y, Vector3.UnitZ)) \u003E 0.99f ? Vector3.UnitX : Vector3.UnitZ;\r\n            z = fallback - y * Vector3.Dot(fallback, y);\r\n        }\r\n        z = Vector3.Normalize(z);\r\n\r\n        var x = Vector3.Cross(y, z);\r\n\r\n        // System.Numerics matrices act on row vectors, so the rows are the images of the\r\n        // unit axes under the rotation: row1 = R*X, row2 = R*Y, row3 = R*Z.\r\n        var m = new Matrix4x4(\r\n            x.X, x.Y, x.Z, 0f,\r\n            y.X, y.Y, y.Z, 0f,\r\n            z.X, z.Y, z.Z, 0f,\r\n            0f, 0f, 0f, 1f);\r\n\r\n        return Normalize(Quaternion.CreateFromRotationMatrix(m));\r\n    }\r\n\r\n    /// \u003Csummary\u003EStable unit vector perpendicular to \u003Cparamref name=\u0022unit\u0022/\u003E (assumed normalized).\r\n    /// Internal so geometry passes (e.g. \u003Cc\u003ETwoBoneIk\u003C/c\u003E) reuse one implementation.\u003C/summary\u003E\r\n    internal static Vector3 Perpendicular(Vector3 unit)\r\n    {\r\n        var p = Vector3.Cross(unit, Vector3.UnitX);\r\n        if (p.LengthSquared() \u003C 1e-6f)\r\n            p = Vector3.Cross(unit, Vector3.UnitY);\r\n        return Vector3.Normalize(p);\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"Code/TextToAnimation/Rig/JointNames.cs","FileName":"JointNames.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":409724,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System.Text;\r\nusing System.Text.RegularExpressions;\r\n\r\nnamespace TextToAnimation.Rig;\r\n\r\n/// \u003Csummary\u003E\r\n/// Raw bone name to the \u0022clean\u0022 joint vocabulary UniMate was trained on (\u0022mixamorig:LeftUpLeg\u0022 -\u003E \u0022Left Thigh\u0022,\r\n/// \u0022thigh.L\u0022 -\u003E \u0022Left Thigh\u0022, \u0022Tail_05\u0022 -\u003E \u0022Tail\u0022). A port of the generic and Mixamo paths of UniMate\u0027s\r\n/// data_process/joint_annotation/names_clean_rule.py (clean_joint_name \u002B post_process); the dataset-specific\r\n/// maps (Japanese, spider, NPC, elk rigs) are left out. Names are only a hint: \u003Csee cref=\u0022RigAnalysis\u0022/\u003E\r\n/// labels bones from geometry and uses these for bones it cannot classify.\r\n/// \u003C/summary\u003E\r\npublic static class JointNames\r\n{\r\n    static readonly Regex TrailingToken = new(@\u0022[._](\\d\u002B|[LRlr]|x)$\u0022, RegexOptions.CultureInvariant);\r\n    static readonly Regex SideToken = new(@\u0022[._]([LR])(?=[._]|\\d|$)\u0022, RegexOptions.CultureInvariant);\r\n    static readonly Regex SingleSide = new(@\u0022^[LR](?:[A-Z][a-z]|[A-Z]{0,2}_[A-Za-z])\u0022, RegexOptions.CultureInvariant);\r\n    static readonly Regex FingerCode = new(@\u0022^[Ff]inger([0-4])\\d*(Nub)?$\u0022, RegexOptions.CultureInvariant);\r\n    static readonly Regex FingerSeg = new(@\u0022^[Ff]inger([1-5])(Metacarpal|Proximal|Medial|Distal|Tip)\\d*$\u0022, RegexOptions.CultureInvariant);\r\n    static readonly Regex ParenDecor = new(@\u0022\\s*\\([^)]*\\)\u0022, RegexOptions.CultureInvariant);\r\n    static readonly Regex Namespace = new(@\u0022^[A-Za-z][\\w .-]*:\u0022, RegexOptions.CultureInvariant);\r\n    static readonly Regex BipPrefix = new(@\u0022^(?:BN_)?Bip\\d\u002B(?:[-_ ]\u002B|(?=[LR][A-Z]))\u0022, RegexOptions.CultureInvariant);\r\n    static readonly Regex Mixamorig = new(@\u0022^mixamorig\\d*[:_]\u0022, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);\r\n    static readonly string[] FingerCodeNames = { \u0022Thumb\u0022, \u0022Index\u0022, \u0022Middle\u0022, \u0022Ring\u0022, \u0022Pinky\u0022 };\r\n\r\n    /// \u003Csummary\u003EThe cleaned, post-processed joint name (never empty; \u0022Bone\u0022 when nothing anatomical survives).\u003C/summary\u003E\r\n    public static string Clean(string raw) =\u003E PostProcess(CleanJointName(raw ?? \u0022\u0022));\r\n\r\n    /// \u003Csummary\u003ETrue when \u003Cparamref name=\u0022cleaned\u0022/\u003E (side word removed) is a word of the training vocabulary.\u003C/summary\u003E\r\n    public static bool IsKnown(string cleaned)\r\n    {\r\n        var (_, baseName) = SplitSide(cleaned);\r\n        if (baseName.Length == 0 || baseName == \u0022Bone\u0022) return false;\r\n        if (baseName.EndsWith(\u0022 End\u0022, StringComparison.Ordinal)) baseName = baseName[..^4];\r\n        return KnownWords.Contains(baseName);\r\n    }\r\n\r\n    /// \u003Csummary\u003E(\u0022Left\u0022, \u0022Thigh\u0022) from \u0022Left Thigh\u0022; side is \u0022\u0022 for center names.\u003C/summary\u003E\r\n    public static (string Side, string Base) SplitSide(string cleaned)\r\n    {\r\n        if (cleaned.StartsWith(\u0022Left \u0022, StringComparison.Ordinal)) return (\u0022Left\u0022, cleaned[5..]);\r\n        if (cleaned.StartsWith(\u0022Right \u0022, StringComparison.Ordinal)) return (\u0022Right\u0022, cleaned[6..]);\r\n        return (\u0022\u0022, cleaned);\r\n    }\r\n\r\n    static HashSet\u003Cstring\u003E? _known;\r\n    static HashSet\u003Cstring\u003E KnownWords\r\n    {\r\n        get\r\n        {\r\n            if (_known is not null) return _known;\r\n            var set = new HashSet\u003Cstring\u003E(StringComparer.Ordinal);\r\n            foreach (var v in Canonical.Values) if (v.Length \u003E 0) set.Add(SplitSide(v).Base);\r\n            foreach (var v in MixamoMap.Values) set.Add(SplitSide(v).Base);\r\n            foreach (var v in SymmetricPairPriority) set.Add(v);\r\n            set.Remove(\u0022Bone\u0022);\r\n            set.Remove(\u0022End\u0022);\r\n            return _known = set;\r\n        }\r\n    }\r\n\r\n    static string CleanJointName(string raw)\r\n    {\r\n        if (raw.Trim().Length == 0) return raw;\r\n        var stripped = ParenDecor.Replace(raw, \u0022\u0022).Trim();\r\n        if (stripped.Length \u003E 0) raw = stripped;\r\n        if (Regex.IsMatch(raw, @\u0022^Bone\\d\u002B$\u0022)) return \u0022Bone\u0022;\r\n        if (Regex.IsMatch(raw.Trim(\u0027_\u0027), @\u0022^_?\\d\u002B$\u0022)) return raw;\r\n        var m = Mixamorig.Match(raw);\r\n        if (m.Success) return CleanPrefixed(raw, m.Value, MixamoMap);\r\n        return CleanStandard(raw);\r\n    }\r\n\r\n    static string CleanPrefixed(string raw, string prefix, Dictionary\u003Cstring, string\u003E map)\r\n    {\r\n        var name = raw[prefix.Length..];\r\n        var (side, rest) = ExtractSidePrefix(name);\r\n        if (!map.TryGetValue(rest, out var result) \u0026\u0026 !map.TryGetValue(Regex.Replace(rest, @\u0022\\d\u002B$\u0022, \u0022\u0022), out result))\r\n        {\r\n            result = SplitAndMapTokens(rest);\r\n            if (result.Length == 0) result = rest;\r\n        }\r\n        return WithSide(side, result).Trim();\r\n    }\r\n\r\n    static string? Canon(string token)\r\n    {\r\n        if (Canonical.TryGetValue(token, out var value)) return value;\r\n        if (token.Length \u003E 0)\r\n        {\r\n            var cap = Capitalize(token);\r\n            if (cap != token \u0026\u0026 Canonical.TryGetValue(cap, out value)) return value;\r\n        }\r\n        return null;\r\n    }\r\n\r\n    /// \u003Csummary\u003EPython\u0027s str.capitalize(): first character upper, the rest lower.\u003C/summary\u003E\r\n    static string Capitalize(string s) =\u003E s.Length == 0 ? s : char.ToUpperInvariant(s[0]) \u002B s[1..].ToLowerInvariant();\r\n\r\n    static (string Side, string Name) StripTrailingDecorations(string name)\r\n    {\r\n        var side = \u0022\u0022;\r\n        while (true)\r\n        {\r\n            var m = TrailingToken.Match(name);\r\n            if (!m.Success) return (side, name);\r\n            var tok = m.Groups[1].Value;\r\n            if (tok is \u0022L\u0022 or \u0022l\u0022) side = \u0022Left\u0022;\r\n            else if (tok is \u0022R\u0022 or \u0022r\u0022) side = \u0022Right\u0022;\r\n            name = name[..m.Index];\r\n        }\r\n    }\r\n\r\n    static (string Side, string Name) ExtractSidePrefix(string name)\r\n    {\r\n        var (side, baseName) = StripTrailingDecorations(name);\r\n        name = baseName;\r\n        if (side.Length \u003E 0)\r\n        {\r\n            var cut = Regex.Replace(name, @\u0022^(?:Left|Right)(?=[A-Z])|^(?:Left|Right)[_ ]|^[LR][-_]|^[lr]_\u0022, \u0022\u0022);\r\n            return (side, cut.Length \u003E 0 ? cut : name);\r\n        }\r\n        if (Regex.IsMatch(name, @\u0022^[RL][-_.]\u0022)) return (name[0] == \u0027L\u0027 ? \u0022Left\u0022 : \u0022Right\u0022, name[2..]);\r\n        var m = Regex.Match(name, @\u0022^([RL]) \u002B\u0022);\r\n        if (m.Success) return (m.Groups[1].Value == \u0022L\u0022 ? \u0022Left\u0022 : \u0022Right\u0022, name[m.Length..]);\r\n        m = Regex.Match(name, @\u0022^(Left|Right)_\u0022, RegexOptions.IgnoreCase);\r\n        if (m.Success) return (Capitalize(m.Groups[1].Value), name[m.Length..]);\r\n        m = Regex.Match(name, @\u0022^(Left|Right)(?=[A-Z])\u0022);\r\n        if (m.Success) return (m.Groups[1].Value, name[m.Groups[1].Length..]);\r\n        if (name.Length \u003E 1 \u0026\u0026 (name[0] == \u0027L\u0027 || name[0] == \u0027R\u0027) \u0026\u0026 char.IsUpper(name[1]))\r\n        {\r\n            var t = SideToken.Match(name, 1);\r\n            if (t.Success)\r\n            {\r\n                var rest = (name[..t.Index] \u002B name[(t.Index \u002B t.Length)..]).Trim(\u0027.\u0027, \u0027_\u0027);\r\n                return (t.Groups[1].Value == \u0022L\u0022 ? \u0022Left\u0022 : \u0022Right\u0022, rest.Length \u003E 0 ? rest : name);\r\n            }\r\n            if (SingleSide.IsMatch(name)) return (name[0] == \u0027L\u0027 ? \u0022Left\u0022 : \u0022Right\u0022, name[1..]);\r\n        }\r\n        m = Regex.Match(name, @\u0022_([LR])_(\\d\u002B)$\u0022);\r\n        if (m.Success) return (m.Groups[1].Value == \u0022L\u0022 ? \u0022Left\u0022 : \u0022Right\u0022, name[..m.Index] \u002B \u0022_\u0022 \u002B m.Groups[2].Value);\r\n        m = Regex.Match(name, @\u0022_([LR])$\u0022);\r\n        if (m.Success) return (m.Groups[1].Value == \u0022L\u0022 ? \u0022Left\u0022 : \u0022Right\u0022, name[..m.Index]);\r\n        return (\u0022\u0022, name);\r\n    }\r\n\r\n    static string SplitAndMapTokens(string name)\r\n    {\r\n        var mapped = new List\u003Cstring\u003E();\r\n        foreach (var t in Regex.Split(name, @\u0022(?=[A-Z])|_\u0022))\r\n        {\r\n            if (t.Length == 0) continue;\r\n            var baseName = Regex.Replace(t, @\u0022\\d\u002B$\u0022, \u0022\u0022);\r\n            var value = Canon(baseName);\r\n            if (value is not null) { if (value.Length \u003E 0) mapped.Add(value); }\r\n            else if (baseName.Length \u003E 1) mapped.Add(baseName);\r\n        }\r\n        return string.Join(\u0022 \u0022, mapped);\r\n    }\r\n\r\n    static string WithSide(string side, string result)\r\n    {\r\n        if (side.Length == 0) return result;\r\n        var m = Regex.Match(result, @\u0022^(Left|Right)\\b\\s*\u0022);\r\n        if (m.Success) result = result[m.Length..];\r\n        return (side \u002B \u0022 \u0022 \u002B result).Trim();\r\n    }\r\n\r\n    static string CleanStandard(string raw)\r\n    {\r\n        var name = Namespace.Replace(raw, \u0022\u0022, 1);\r\n        if (name.Length == 0) name = raw;\r\n        name = BipPrefix.Replace(name, \u0022\u0022, 1);\r\n        foreach (var prefix in RemovePrefixes)\r\n            if (name.StartsWith(prefix, StringComparison.Ordinal)) { name = name[prefix.Length..]; break; }\r\n        name = name.TrimStart(\u0027_\u0027).Trim();\r\n        if (name.Length == 0) return raw.Trim(\u0027_\u0027);\r\n\r\n        var (side, rest) = ExtractSidePrefix(name);\r\n        name = rest;\r\n\r\n        var m = FingerSeg.Match(name);\r\n        if (m.Success)\r\n        {\r\n            var label = FingerCodeNames[int.Parse(m.Groups[1].Value) - 1] \u002B \u0022 Finger\u0022;\r\n            if (m.Groups[2].Value == \u0022Tip\u0022) label \u002B= \u0022 End\u0022;\r\n            return WithSide(side, label);\r\n        }\r\n        m = FingerCode.Match(name);\r\n        if (m.Success)\r\n        {\r\n            var label = FingerCodeNames[int.Parse(m.Groups[1].Value)] \u002B \u0022 Finger\u0022;\r\n            if (m.Groups[2].Success \u0026\u0026 m.Groups[2].Value.Length \u003E 0) label \u002B= \u0022 End\u0022;\r\n            return WithSide(side, label);\r\n        }\r\n\r\n        m = Regex.Match(name, @\u0022^(.\u002B?)_?(\\d\u002B)$\u0022);\r\n        var baseName = m.Success ? m.Groups[1].Value.TrimEnd(\u0027_\u0027) : name;\r\n        var mappedBase = Canon(baseName);\r\n        if (mappedBase is not null)\r\n        {\r\n            if (mappedBase.Length == 0 || mappedBase == \u0022Bone\u0022) return \u0022Bone\u0022;\r\n            return WithSide(side, mappedBase);\r\n        }\r\n\r\n        var mapped = new List\u003Cstring\u003E();\r\n        foreach (var p in baseName.Split(\u0027_\u0027))\r\n        {\r\n            if (p.Length == 0) continue;\r\n            if (p is \u0022L\u0022 or \u0022l\u0022) { if (side.Length == 0) side = \u0022Left\u0022; continue; }\r\n            if (p is \u0022R\u0022 or \u0022r\u0022) { if (side.Length == 0) side = \u0022Right\u0022; continue; }\r\n            var subBase = Regex.Replace(p, @\u0022\\d\u002B$\u0022, \u0022\u0022);\r\n            var subMapped = Canon(subBase);\r\n            if (subMapped is not null) { if (subMapped.Length \u003E 0) mapped.Add(subMapped); }\r\n            else if (subBase.Length \u003E 0)\r\n            {\r\n                foreach (var st in Regex.Split(subBase, @\u0022(?=[A-Z])\u0022))\r\n                {\r\n                    if (st.Length == 0) continue;\r\n                    var stMapped = Canon(st);\r\n                    if (stMapped is not null) { if (stMapped.Length \u003E 0) mapped.Add(stMapped); }\r\n                    else if (st.Length \u003E 1) mapped.Add(st);\r\n                }\r\n            }\r\n        }\r\n        var result = string.Join(\u0022 \u0022, mapped);\r\n        if (result.Length == 0 || result == \u0022Bone\u0022) return \u0022Bone\u0022;\r\n        var sided = WithSide(side, result).Trim();\r\n        return sided.Length \u003E 0 ? sided : raw;\r\n    }\r\n\r\n    static string PostProcess(string name)\r\n    {\r\n        name = Regex.Replace(name, @\u0022\\s\u002B\u0022, \u0022 \u0022).Trim().TrimEnd(\u0027.\u0027, \u0027_\u0027, \u0027 \u0027);\r\n        name = Regex.Replace(name, @\u0022\\s\u002B\\d\u002B$\u0022, \u0022\u0022);\r\n        name = Regex.Replace(name, @\u0022(\\w)\\d\u002B\\s\u002BEnd\u0022, \u0022$1 End\u0022);\r\n        name = Regex.Replace(name, @\u0022\\bHand (Thumb|Index|Middle|Ring|Pinky) Finger\\b\u0022, \u0022$1 Finger\u0022);\r\n        name = Regex.Replace(name, @\u0022\\b(Thumb|Index|Middle|Ring|Pinky) Finger Finger\\b\u0022, \u0022$1 Finger\u0022);\r\n        name = Regex.Replace(name, @\u0022\\bFinger (Thumb|Index|Middle|Ring|Pinky) Finger\\b\u0022, \u0022$1 Finger\u0022);\r\n        name = Regex.Replace(name, @\u0022\\b(?:Up|Upper) Leg$\u0022, \u0022Thigh\u0022);\r\n        name = Regex.Replace(name, @\u0022\\bLower Leg$\u0022, \u0022Shin\u0022);\r\n        name = Regex.Replace(name, @\u0022\\b(?:Lower|Fore) Arm$\u0022, \u0022Forearm\u0022);\r\n        name = Regex.Replace(name, @\u0022\\bToe Base$\u0022, \u0022Toe\u0022);\r\n        name = Regex.Replace(name, @\u0022^(?!Left |Right )(.\u002B) (Left|Right)$\u0022, \u0022$2 $1\u0022);\r\n        name = Regex.Replace(name, @\u0022^Head Top End$\u0022, \u0022Head End\u0022);\r\n        name = name.Replace(\u0022. \u0022, \u0022 \u0022);\r\n        name = Regex.Replace(name, @\u0022^Spine ?\\d* ?Tail$\u0022, \u0022Tail\u0022);\r\n        name = Regex.Replace(name, @\u0022^Head ?\\d* ?Jaw$\u0022, \u0022Jaw\u0022);\r\n        name = Regex.Replace(name, @\u0022^Head ?\\d* ?Eyelid$\u0022, \u0022Eyelid\u0022);\r\n        name = Regex.Replace(name, @\u0022^Head Muzzle$\u0022, \u0022Muzzle\u0022);\r\n        name = Regex.Replace(name, @\u0022^Head Jaw End$\u0022, \u0022Jaw End\u0022);\r\n        name = Regex.Replace(name, @\u0022^Head Brain$\u0022, \u0022Head\u0022);\r\n        name = Regex.Replace(name, @\u0022Bip \\d\u002B \u0022, \u0022\u0022);\r\n        name = Regex.Replace(name, @\u0022^(\\w\u002B) \\1$\u0022, \u0022$1\u0022);\r\n        if (Regex.IsMatch(name, @\u0022^Xtra\u0022)) name = \u0022Bone\u0022;\r\n        name = Regex.Replace(name, @\u0022Ponytail\\d*.*\u0022, \u0022Appendage\u0022);\r\n        var words = name.Split(\u0027 \u0027, StringSplitOptions.RemoveEmptyEntries);\r\n        var sb = new StringBuilder();\r\n        foreach (var w in words)\r\n        {\r\n            if (sb.Length \u003E 0) sb.Append(\u0027 \u0027);\r\n            sb.Append(char.IsLower(w[0]) \u0026\u0026 w.Length \u003E 1 ? Capitalize(w) : w);\r\n        }\r\n        name = sb.ToString();\r\n        name = Regex.Replace(name, @\u0022^Spine (Left|Right) Wing$\u0022, \u0022$1 Wing\u0022);\r\n        name = Regex.Replace(name, @\u0022\\s\u002B\\d\u002B$\u0022, \u0022\u0022);\r\n        return name.Length == 0 ? \u0022Bone\u0022 : name;\r\n    }\r\n\r\n    // generated from UniMate data_process/joint_annotation/vocab.py and face_select_rule.py\r\n    static readonly string[] RemovePrefixes = { \u0022BN_Bip01_\u0022, \u0022BN_Bip01 \u0022, \u0022Base Human \u0022, \u0022Bip001_\u0022, \u0022Bip001 \u0022, \u0022Bip01_\u0022, \u0022Bip01 \u0022, \u0022Preset01_\u0022, \u0022Preset01 \u0022, \u0022Mutant:\u0022, \u0022Sif:\u0022, \u0022BN_\u0022, \u0022NPC_\u0022, \u0022jt_\u0022, \u0022Bn_\u0022, \u0022bn_\u0022, \u0022b_\u0022 };\r\n    static readonly Dictionary\u003Cstring, string\u003E Canonical = new(StringComparer.Ordinal)\r\n    {\r\n        [\u0022Pelvis\u0022] = \u0022Pelvis\u0022,\r\n        [\u0022pelv\u0022] = \u0022Pelvis\u0022,\r\n        [\u0022Spine\u0022] = \u0022Spine\u0022,\r\n        [\u0022Spn\u0022] = \u0022Spine\u0022,\r\n        [\u0022Spline\u0022] = \u0022Spine\u0022,\r\n        [\u0022Ribcage\u0022] = \u0022Ribcage\u0022,\r\n        [\u0022Neck\u0022] = \u0022Neck\u0022,\r\n        [\u0022Nek\u0022] = \u0022Neck\u0022,\r\n        [\u0022Head\u0022] = \u0022Head\u0022,\r\n        [\u0022Scull\u0022] = \u0022Skull\u0022,\r\n        [\u0022Skull\u0022] = \u0022Skull\u0022,\r\n        [\u0022ScullBase\u0022] = \u0022Skull Base\u0022,\r\n        [\u0022SkullBase\u0022] = \u0022Skull Base\u0022,\r\n        [\u0022HeadNub\u0022] = \u0022Head End\u0022,\r\n        [\u0022Nub\u0022] = \u0022End\u0022,\r\n        [\u0022Tip\u0022] = \u0022End\u0022,\r\n        [\u0022Hips\u0022] = \u0022Hips\u0022,\r\n        [\u0022Cog\u0022] = \u0022Root\u0022,\r\n        [\u0022Clavicle\u0022] = \u0022Shoulder\u0022,\r\n        [\u0022Collarbone\u0022] = \u0022Shoulder\u0022,\r\n        [\u0022Scapula\u0022] = \u0022Scapula\u0022,\r\n        [\u0022UpperArm\u0022] = \u0022Upper Arm\u0022,\r\n        [\u0022Upperarm\u0022] = \u0022Upper Arm\u0022,\r\n        [\u0022Humerus\u0022] = \u0022Upper Arm\u0022,\r\n        [\u0022Forearm\u0022] = \u0022Forearm\u0022,\r\n        [\u0022ForeArm\u0022] = \u0022Forearm\u0022,\r\n        [\u0022LowerArm\u0022] = \u0022Forearm\u0022,\r\n        [\u0022Lowerarm\u0022] = \u0022Forearm\u0022,\r\n        [\u0022Radius\u0022] = \u0022Forearm\u0022,\r\n        [\u0022Hand\u0022] = \u0022Hand\u0022,\r\n        [\u0022Hnd\u0022] = \u0022Hand\u0022,\r\n        [\u0022Palm\u0022] = \u0022Palm\u0022,\r\n        [\u0022Wrist\u0022] = \u0022Wrist\u0022,\r\n        [\u0022Elbow\u0022] = \u0022Elbow\u0022,\r\n        [\u0022Shoulder\u0022] = \u0022Shoulder\u0022,\r\n        [\u0022Thigh\u0022] = \u0022Thigh\u0022,\r\n        [\u0022Femur\u0022] = \u0022Thigh\u0022,\r\n        [\u0022UpLeg\u0022] = \u0022Thigh\u0022,\r\n        [\u0022UpperLeg\u0022] = \u0022Thigh\u0022,\r\n        [\u0022Upperleg\u0022] = \u0022Thigh\u0022,\r\n        [\u0022Calf\u0022] = \u0022Shin\u0022,\r\n        [\u0022Tibia\u0022] = \u0022Shin\u0022,\r\n        [\u0022Leg\u0022] = \u0022Leg\u0022,\r\n        [\u0022LowerLeg\u0022] = \u0022Shin\u0022,\r\n        [\u0022Lowerleg\u0022] = \u0022Shin\u0022,\r\n        [\u0022HorseLink\u0022] = \u0022Fetlock\u0022,\r\n        [\u0022LargeCannon\u0022] = \u0022Cannon\u0022,\r\n        [\u0022Metacarpus\u0022] = \u0022Metacarpus\u0022,\r\n        [\u0022PhalangesManus\u0022] = \u0022Phalanges\u0022,\r\n        [\u0022PhalanxPrima\u0022] = \u0022Pastern\u0022,\r\n        [\u0022Foot\u0022] = \u0022Foot\u0022,\r\n        [\u0022Ankle\u0022] = \u0022Ankle\u0022,\r\n        [\u0022Heel\u0022] = \u0022Heel\u0022,\r\n        [\u0022Toe\u0022] = \u0022Toe\u0022,\r\n        [\u0022Toes\u0022] = \u0022Toe\u0022,\r\n        [\u0022ToeBase\u0022] = \u0022Toe\u0022,\r\n        [\u0022Ball\u0022] = \u0022Toe\u0022,\r\n        [\u0022Hoof\u0022] = \u0022Hoof\u0022,\r\n        [\u0022Knee\u0022] = \u0022Knee\u0022,\r\n        [\u0022Foreleg\u0022] = \u0022Front Leg\u0022,\r\n        [\u0022Hindleg\u0022] = \u0022Hind Leg\u0022,\r\n        [\u0022Finger\u0022] = \u0022Finger\u0022,\r\n        [\u0022Thumb\u0022] = \u0022Thumb Finger\u0022,\r\n        [\u0022Pinky\u0022] = \u0022Pinky Finger\u0022,\r\n        [\u0022Little\u0022] = \u0022Pinky Finger\u0022,\r\n        [\u0022Ring\u0022] = \u0022Ring Finger\u0022,\r\n        [\u0022Middle\u0022] = \u0022Middle Finger\u0022,\r\n        [\u0022Index\u0022] = \u0022Index Finger\u0022,\r\n        [\u0022Jaw\u0022] = \u0022Jaw\u0022,\r\n        [\u0022Tongue\u0022] = \u0022Tongue\u0022,\r\n        [\u0022Thouge\u0022] = \u0022Tongue\u0022,\r\n        [\u0022Tone\u0022] = \u0022Tongue\u0022,\r\n        [\u0022tunge\u0022] = \u0022Tongue\u0022,\r\n        [\u0022Tunge\u0022] = \u0022Tongue\u0022,\r\n        [\u0022Ear\u0022] = \u0022Ear\u0022,\r\n        [\u0022Eye\u0022] = \u0022Eye\u0022,\r\n        [\u0022Eyeball\u0022] = \u0022Eyeball\u0022,\r\n        [\u0022EyeBall\u0022] = \u0022Eyeball\u0022,\r\n        [\u0022Eyebrow\u0022] = \u0022Eyebrow\u0022,\r\n        [\u0022Eyelid\u0022] = \u0022Eyelid\u0022,\r\n        [\u0022Eyeleds\u0022] = \u0022Eyelid\u0022,\r\n        [\u0022Mouth\u0022] = \u0022Mouth\u0022,\r\n        [\u0022Lip\u0022] = \u0022Lip\u0022,\r\n        [\u0022Nose\u0022] = \u0022Nose\u0022,\r\n        [\u0022Muzzle\u0022] = \u0022Muzzle\u0022,\r\n        [\u0022Chin\u0022] = \u0022Chin\u0022,\r\n        [\u0022Cheek\u0022] = \u0022Cheek\u0022,\r\n        [\u0022Torso\u0022] = \u0022Body\u0022,\r\n        [\u0022LegAnkle\u0022] = \u0022Ankle\u0022,\r\n        [\u0022Fingers\u0022] = \u0022Finger\u0022,\r\n        [\u0022Tail\u0022] = \u0022Tail\u0022,\r\n        [\u0022Tai\u0022] = \u0022Tail\u0022,\r\n        [\u0022Wing\u0022] = \u0022Wing\u0022,\r\n        [\u0022RWing\u0022] = \u0022Right Wing\u0022,\r\n        [\u0022LWing\u0022] = \u0022Left Wing\u0022,\r\n        [\u0022Feather\u0022] = \u0022Feather\u0022,\r\n        [\u0022Feeler\u0022] = \u0022Antenna\u0022,\r\n        [\u0022Antenna\u0022] = \u0022Antenna\u0022,\r\n        [\u0022Feelers\u0022] = \u0022Barbel\u0022,\r\n        [\u0022Tentacle\u0022] = \u0022Tentacle\u0022,\r\n        [\u0022Tentacles\u0022] = \u0022Tentacle\u0022,\r\n        [\u0022Claw\u0022] = \u0022Claw\u0022,\r\n        [\u0022HandClaw\u0022] = \u0022Hand Claw\u0022,\r\n        [\u0022Fang\u0022] = \u0022Fang\u0022,\r\n        [\u0022Fangs\u0022] = \u0022Fang\u0022,\r\n        [\u0022Mandible\u0022] = \u0022Mandible\u0022,\r\n        [\u0022BigMandible\u0022] = \u0022Large Mandible\u0022,\r\n        [\u0022LowerMandible\u0022] = \u0022Lower Mandible\u0022,\r\n        [\u0022Pincer\u0022] = \u0022Pincer\u0022,\r\n        [\u0022pincers\u0022] = \u0022Pincer\u0022,\r\n        [\u0022Pliers\u0022] = \u0022Pincer\u0022,\r\n        [\u0022Piers\u0022] = \u0022Pincer\u0022,\r\n        [\u0022Mane\u0022] = \u0022Mane\u0022,\r\n        [\u0022Fur\u0022] = \u0022Fur\u0022,\r\n        [\u0022Hair\u0022] = \u0022Mane\u0022,\r\n        [\u0022Beard\u0022] = \u0022Whisker\u0022,\r\n        [\u0022Mascara\u0022] = \u0022Whisker\u0022,\r\n        [\u0022Shell\u0022] = \u0022Shell\u0022,\r\n        [\u0022Stinger\u0022] = \u0022Stinger\u0022,\r\n        [\u0022Horn\u0022] = \u0022Horn\u0022,\r\n        [\u0022dorsal\u0022] = \u0022Dorsal Plate\u0022,\r\n        [\u0022Fin\u0022] = \u0022Fin\u0022,\r\n        [\u0022ponitail\u0022] = \u0022Crest\u0022,\r\n        [\u0022Ponytail\u0022] = \u0022Appendage\u0022,\r\n        [\u0022Downbody\u0022] = \u0022Lower Body\u0022,\r\n        [\u0022Down\u0022] = \u0022Lower Body\u0022,\r\n        [\u0022Body\u0022] = \u0022Body\u0022,\r\n        [\u0022Reins\u0022] = \u0022Reins\u0022,\r\n        [\u0022Halter\u0022] = \u0022Halter\u0022,\r\n        [\u0022Jiggle\u0022] = \u0022Jiggle\u0022,\r\n        [\u0022TwistBone\u0022] = \u0022Twist\u0022,\r\n        [\u0022UpperArmTwist\u0022] = \u0022Upper Arm Twist\u0022,\r\n        [\u0022ForearmTwist\u0022] = \u0022Forearm Twist\u0022,\r\n        [\u0022ThighMuscle\u0022] = \u0022Thigh Muscle\u0022,\r\n        [\u0022NeckMuscle\u0022] = \u0022Neck Muscle\u0022,\r\n        [\u0022Clip\u0022] = \u0022Mandible\u0022,\r\n        [\u0022Wings\u0022] = \u0022Wing\u0022,\r\n        [\u0022Shall\u0022] = \u0022Mandible\u0022,\r\n        [\u0022Trajectory\u0022] = \u0022Root\u0022,\r\n        [\u0022locator\u0022] = \u0022Root\u0022,\r\n        [\u0022locator2\u0022] = \u0022Root\u0022,\r\n        [\u0022center\u0022] = \u0022Center\u0022,\r\n        [\u0022MagicEffectsNode\u0022] = \u0022Bone\u0022,\r\n        [\u0022Handle\u0022] = \u0022Handle\u0022,\r\n        [\u0022IK_Chain\u0022] = \u0022IK Chain\u0022,\r\n        [\u0022Joint\u0022] = \u0022\u0022,\r\n        [\u0022Jnt\u0022] = \u0022\u0022,\r\n        [\u0022Internal\u0022] = \u0022\u0022,\r\n        [\u0022Def\u0022] = \u0022\u0022,\r\n        [\u0022Part\u0022] = \u0022\u0022,\r\n        [\u0022Armature\u0022] = \u0022\u0022,\r\n        [\u0022Mixamorig\u0022] = \u0022\u0022,\r\n        [\u0022Character\u0022] = \u0022\u0022,\r\n        [\u0022Rig\u0022] = \u0022\u0022,\r\n        [\u0022Bip\u0022] = \u0022\u0022,\r\n        [\u0022Base\u0022] = \u0022\u0022,\r\n        [\u0022Human\u0022] = \u0022\u0022,\r\n        [\u0022Mid\u0022] = \u0022\u0022,\r\n        [\u0022Controller\u0022] = \u0022\u0022,\r\n        [\u0022Controler\u0022] = \u0022\u0022,\r\n        [\u0022Ctrl\u0022] = \u0022\u0022,\r\n        [\u0022Quick\u0022] = \u0022\u0022,\r\n        [\u0022Untitled\u0022] = \u0022\u0022,\r\n        [\u0022Pasted\u0022] = \u0022\u0022,\r\n        [\u0022Bind\u0022] = \u0022\u0022,\r\n        [\u0022Skeleton\u0022] = \u0022\u0022,\r\n        [\u0022Reference\u0022] = \u0022\u0022,\r\n        [\u0022Res\u0022] = \u0022\u0022,\r\n        [\u0022Main\u0022] = \u0022\u0022,\r\n        [\u0022Metacarpal\u0022] = \u0022\u0022,\r\n        [\u0022Proximal\u0022] = \u0022\u0022,\r\n        [\u0022Intermediate\u0022] = \u0022\u0022,\r\n        [\u0022Medial\u0022] = \u0022\u0022,\r\n        [\u0022Distal\u0022] = \u0022\u0022,\r\n        [\u0022Digit\u0022] = \u0022Finger\u0022,\r\n        [\u0022FrontLeg\u0022] = \u0022Front Leg\u0022,\r\n        [\u0022MiddleLeg\u0022] = \u0022Middle Leg\u0022,\r\n        [\u0022HindLeg\u0022] = \u0022Hind Leg\u0022,\r\n        [\u0022Crab_pincers\u0022] = \u0022Pincer\u0022,\r\n        [\u0022SpineR\u0022] = \u0022Right Abdomen\u0022,\r\n        [\u0022SpineL\u0022] = \u0022Left Abdomen\u0022,\r\n    };\r\n    static readonly Dictionary\u003Cstring, string\u003E MixamoMap = new(StringComparer.Ordinal)\r\n    {\r\n        [\u0022Hips\u0022] = \u0022Hips\u0022,\r\n        [\u0022Spine\u0022] = \u0022Spine\u0022,\r\n        [\u0022Spine1\u0022] = \u0022Spine\u0022,\r\n        [\u0022Spine2\u0022] = \u0022Spine\u0022,\r\n        [\u0022Neck\u0022] = \u0022Neck\u0022,\r\n        [\u0022Head\u0022] = \u0022Head\u0022,\r\n        [\u0022HeadTop_End\u0022] = \u0022Head End\u0022,\r\n        [\u0022Eye\u0022] = \u0022Eye\u0022,\r\n        [\u0022Shoulder\u0022] = \u0022Shoulder\u0022,\r\n        [\u0022Arm\u0022] = \u0022Upper Arm\u0022,\r\n        [\u0022ForeArm\u0022] = \u0022Forearm\u0022,\r\n        [\u0022Hand\u0022] = \u0022Hand\u0022,\r\n        [\u0022HandThumb\u0022] = \u0022Thumb Finger\u0022,\r\n        [\u0022HandIndex\u0022] = \u0022Index Finger\u0022,\r\n        [\u0022HandMiddle\u0022] = \u0022Middle Finger\u0022,\r\n        [\u0022HandRing\u0022] = \u0022Ring Finger\u0022,\r\n        [\u0022HandPinky\u0022] = \u0022Pinky Finger\u0022,\r\n        [\u0022UpLeg\u0022] = \u0022Thigh\u0022,\r\n        [\u0022Leg\u0022] = \u0022Shin\u0022,\r\n        [\u0022Foot\u0022] = \u0022Foot\u0022,\r\n        [\u0022ToeBase\u0022] = \u0022Toe\u0022,\r\n        [\u0022Toe_End\u0022] = \u0022Toe End\u0022,\r\n    };\r\n    public static readonly string[] SymmetricPairPriority = { \u0022Thigh\u0022, \u0022Shoulder\u0022, \u0022Front Shoulder\u0022, \u0022Back Hip\u0022, \u0022Hip\u0022, \u0022Scapula\u0022, \u0022Upper Arm\u0022, \u0022Arm\u0022, \u0022Front Leg\u0022, \u0022Hind Leg\u0022, \u0022Middle Leg\u0022, \u0022Back Leg\u0022, \u0022Wing\u0022, \u0022Leg\u0022, \u0022Pectoral Fin\u0022, \u0022Pelvic Fin\u0022, \u0022Fin\u0022, \u0022Gill\u0022, \u0022Pincer\u0022, \u0022Mandible\u0022, \u0022Large Mandible\u0022, \u0022Lower Mandible\u0022, \u0022Stinger\u0022, \u0022Claw\u0022, \u0022Hand Claw\u0022, \u0022Antenna\u0022, \u0022Forearm\u0022, \u0022Shin\u0022, \u0022Knee\u0022, \u0022Elbow\u0022, \u0022Ankle\u0022, \u0022Wrist\u0022, \u0022Hand\u0022, \u0022Palm\u0022, \u0022Foot\u0022, \u0022Heel\u0022, \u0022Front Paw\u0022, \u0022Back Paw\u0022, \u0022Paw\u0022, \u0022Front Hoof\u0022, \u0022Rear Hoof\u0022, \u0022Hoof\u0022, \u0022Fetlock\u0022, \u0022Cannon\u0022, \u0022Metacarpus\u0022, \u0022Pastern\u0022, \u0022Toe\u0022, \u0022Thumb Finger\u0022, \u0022Index Finger\u0022, \u0022Middle Finger\u0022, \u0022Ring Finger\u0022, \u0022Pinky Finger\u0022, \u0022Finger\u0022, \u0022Neck\u0022, \u0022Eye\u0022, \u0022Eyeball\u0022, \u0022Eyelid\u0022, \u0022Eyebrow\u0022, \u0022Ear\u0022, \u0022Horn\u0022, \u0022Cheek\u0022, \u0022Whisker\u0022, \u0022Fang\u0022, \u0022Barbel\u0022, \u0022Tentacle\u0022, \u0022Feather\u0022, \u0022Tail\u0022 };\r\n    public static readonly string[] TailKeywords = { \u0022Tail\u0022, \u0022Tail Twist\u0022 };\r\n    public static readonly string[] HeadKeywords = { \u0022Chin\u0022, \u0022Head\u0022, \u0022Head End\u0022, \u0022Jaw\u0022, \u0022Lower Jaw\u0022, \u0022Muzzle\u0022, \u0022Nose\u0022, \u0022Skull\u0022, \u0022Skull Base\u0022, \u0022Tongue\u0022, \u0022Upper Jaw\u0022 };\r\n}\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"Code/TextToAnimation/Rig/TargetRig.cs","FileName":"TargetRig.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":409724,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing System.Text.Json;\r\nusing TextToAnimation.Mapping;\r\nusing TextToAnimation.Maths;\r\nusing TextToAnimation.Rig;\r\nusing SkeletonModel = TextToAnimation.Rig.Skeleton;\r\n\r\nnamespace TextToAnimation.Rig;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/TextToAnimation/Assembly.cs)\r\n\r\n/// \u003Csummary\u003E\r\n/// A humanoid target rig: skeleton plus per-bone \u003Csee cref=\u0022BoneClass\u0022/\u003E and (for animated\r\n/// bones) canonical \u003Csee cref=\u0022BoneRole\u0022/\u003E annotations. The shipped s\u0026amp;box default is\r\n/// loaded from the committed \u003Cc\u003EAssets/humanoid_retargeter/target_rig_sbox.json\u003C/c\u003E produced\r\n/// by \u003Csee cref=\u0022TargetRigGenerator\u0022/\u003E (see \u003Csee cref=\u0022SboxDefault\u0022/\u003E); arbitrary user-picked\r\n/// targets are built from any \u003Csee cref=\u0022SkeletonModel\u0022/\u003E via \u003Csee cref=\u0022FromSkeleton\u0022/\u003E.\r\n/// This type does no file IO \u2014 callers pass JSON text.\r\n/// \u003C/summary\u003E\r\npublic sealed class TargetRig\r\n{\r\n    private readonly BoneClass[] _classes;\r\n    private readonly BoneRole?[] _roles;\r\n    private readonly Dictionary\u003CBoneRole, int\u003E _boneByRole;\r\n    private readonly Vector3?[]? _tailWorld;\r\n\r\n    private TargetRig(string name, SkeletonModel skeleton, BoneClass[] classes, BoneRole?[] roles,\r\n        Dictionary\u003CBoneRole, int\u003E boneByRole, Vector3?[]? tailWorld = null,\r\n        bool helpersAreConstraintDriven = true)\r\n    {\r\n        Name = name;\r\n        Skeleton = skeleton;\r\n        _classes = classes;\r\n        _roles = roles;\r\n        _boneByRole = boneByRole;\r\n        _tailWorld = tailWorld;\r\n        HelpersAreConstraintDriven = helpersAreConstraintDriven;\r\n    }\r\n\r\n    /// \u003Csummary\u003ERig name (e.g. \u003Cc\u003Esbox_human_male\u003C/c\u003E).\u003C/summary\u003E\r\n    public string Name { get; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// True when the target model\u0027s own vmdl re-drives the\r\n    /// \u003Csee cref=\u0022BoneClass.ConstraintDriven\u0022/\u003E helpers at runtime (the shipped s\u0026amp;box\r\n    /// rigs: their AnimConstraintList drives twist/helper bones, so clips exclude those\r\n    /// channels). False for rigs built from plain skeletons\r\n    /// (\u003Csee cref=\u0022FromSkeleton\u0022/\u003E \u2014 custom FBX targets): the generated vmdl carries NO\r\n    /// constraints, so twist-named bones must keep baked channels or they freeze at rest\r\n    /// (candy-wrapped wrists on an Auto-Rig Pro export).\r\n    /// \u003C/summary\u003E\r\n    public bool HelpersAreConstraintDriven { get; }\r\n\r\n    /// \u003Csummary\u003EThe target skeleton (rest pose in centimeters, parents before children).\u003C/summary\u003E\r\n    public SkeletonModel Skeleton { get; }\r\n\r\n    /// \u003Csummary\u003EThe class of the bone at \u003Cparamref name=\u0022boneIndex\u0022/\u003E.\u003C/summary\u003E\r\n    public BoneClass ClassOf(int boneIndex) =\u003E _classes[boneIndex];\r\n\r\n    /// \u003Csummary\u003EThe canonical role of the bone at \u003Cparamref name=\u0022boneIndex\u0022/\u003E, or null\r\n    /// for role-less (non-animated) bones.\u003C/summary\u003E\r\n    public BoneRole? RoleOf(int boneIndex) =\u003E _roles[boneIndex];\r\n\r\n    /// \u003Csummary\u003EThe bone index carrying \u003Cparamref name=\u0022role\u0022/\u003E, or null when the rig has no\r\n    /// bone for it (e.g. \u003Csee cref=\u0022BoneRole.Spine3\u0022/\u003E, \u003Csee cref=\u0022BoneRole.ThumbMetaL\u0022/\u003E).\u003C/summary\u003E\r\n    public int? BoneForRole(BoneRole role) =\u003E _boneByRole.TryGetValue(role, out var index) ? index : null;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Rest tail position of the bone in rig world space (cm), when the rig definition\r\n    /// carries one (\u003Cc\u003Etail_world\u003C/c\u003E in the generated JSON \u2014 the s\u0026amp;box default rig\r\n    /// does). Null for rigs built from plain skeletons (\u003Csee cref=\u0022FromSkeleton\u0022/\u003E), which\r\n    /// have no tail data. Used by the DL solver\u0027s end-joint synthesis.\r\n    /// \u003C/summary\u003E\r\n    public Vector3? TailWorldOf(int boneIndex) =\u003E _tailWorld?[boneIndex];\r\n\r\n    /// \u003Csummary\u003EUses an authoritative bind skeleton while retaining this rig\u0027s curated\r\n    /// roles, helper classes and constraint ownership. Additional bones keep rest channels.\u003C/summary\u003E\r\n    public TargetRig WithBindPose(SkeletonModel skeleton)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(skeleton);\r\n        var classes = new BoneClass[skeleton.Count];\r\n        var roles = new BoneRole?[skeleton.Count];\r\n        var byRole = new Dictionary\u003CBoneRole, int\u003E();\r\n        var tails = new Vector3?[skeleton.Count];\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n        {\r\n            var old = Skeleton.IndexOf(skeleton[i].Name);\r\n            if (old \u003C 0) continue;\r\n            classes[i] = _classes[old];\r\n            roles[i] = _roles[old];\r\n            if (roles[i] is { } role) byRole.Add(role, i);\r\n            if (TailWorldOf(old) is { } tail)\r\n            {\r\n                var relative = Vector3.Transform(tail - Skeleton.RestWorld[old].Pos, Quaternion.Inverse(Skeleton.RestWorld[old].Rot));\r\n                tails[i] = skeleton.RestWorld[i].Pos \u002B Vector3.Transform(relative, skeleton.RestWorld[i].Rot);\r\n            }\r\n        }\r\n        foreach (var role in _boneByRole.Keys)\r\n            if (!byRole.ContainsKey(role)) throw new ArgumentException($\u0022New bind skeleton is missing role {role}.\u0022, nameof(skeleton));\r\n        return new TargetRig(Name, skeleton, classes, roles, byRole, tails, HelpersAreConstraintDriven);\r\n    }\r\n\r\n    /// \u003Csummary\u003EIndices of all bones of the given class, in skeleton order.\u003C/summary\u003E\r\n    public IEnumerable\u003Cint\u003E BonesOfClass(BoneClass boneClass)\r\n    {\r\n        for (var i = 0; i \u003C _classes.Length; i\u002B\u002B)\r\n        {\r\n            if (_classes[i] == boneClass)\r\n                yield return i;\r\n        }\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Parses a target-rig definition produced by \u003Csee cref=\u0022TargetRigGenerator\u0022/\u003E.\r\n    /// \u003C/summary\u003E\r\n    /// \u003Cexception cref=\u0022ArgumentException\u0022\u003EThrown when the JSON does not match the expected\r\n    /// schema or violates invariants (e.g. a role on a non-animated bone, duplicate roles).\u003C/exception\u003E\r\n    public static TargetRig Load(string json)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(json);\r\n        using var doc = JsonDocument.Parse(json);\r\n        var root = doc.RootElement;\r\n\r\n        var name = root.GetProperty(\u0022name\u0022).GetString()\r\n            ?? throw new ArgumentException(\u0022Target rig JSON has no \u0027name\u0027.\u0022);\r\n\r\n        var bonesJson = root.GetProperty(\u0022bones\u0022);\r\n        var count = bonesJson.GetArrayLength();\r\n        var definitions = new List\u003CBoneDefinition\u003E(count);\r\n        var classByName = new Dictionary\u003Cstring, BoneClass\u003E(count, StringComparer.Ordinal);\r\n        var roleByName = new Dictionary\u003Cstring, BoneRole\u003E(count, StringComparer.Ordinal);\r\n        var tailByName = new Dictionary\u003Cstring, Vector3\u003E(count, StringComparer.Ordinal);\r\n\r\n        foreach (var bone in bonesJson.EnumerateArray())\r\n        {\r\n            // Shared bone-geometry shape (name/parent/local_pos/local_rot_xyzw) \u2014 one reader\r\n            // for rig ground-truth JSON and target-rig JSON (RigJson owns it).\r\n            var definition = RigJson.ReadBoneDefinition(bone);\r\n            var boneName = definition.Name;\r\n            definitions.Add(definition);\r\n\r\n            var boneClass = Enum.Parse\u003CBoneClass\u003E(bone.GetProperty(\u0022class\u0022).GetString()!);\r\n            classByName[boneName] = boneClass;\r\n\r\n            if (bone.TryGetProperty(\u0022role\u0022, out var roleProp))\r\n            {\r\n                if (boneClass != BoneClass.Animated)\r\n                    throw new ArgumentException(\r\n                        $\u0022Bone \u0027{boneName}\u0027 is {boneClass} but carries a role \u2014 only Animated bones may have roles.\u0022);\r\n                roleByName[boneName] = Enum.Parse\u003CBoneRole\u003E(roleProp.GetString()!);\r\n            }\r\n\r\n            if (bone.TryGetProperty(\u0022tail_world\u0022, out var tailProp)\r\n                \u0026\u0026 tailProp.ValueKind == JsonValueKind.Array \u0026\u0026 tailProp.GetArrayLength() == 3)\r\n            {\r\n                tailByName[boneName] = new Vector3(\r\n                    (float)tailProp[0].GetDouble(),\r\n                    (float)tailProp[1].GetDouble(),\r\n                    (float)tailProp[2].GetDouble());\r\n            }\r\n        }\r\n\r\n        var skeleton = SkeletonModel.Create(definitions);\r\n\r\n        var classes = new BoneClass[skeleton.Count];\r\n        var roles = new BoneRole?[skeleton.Count];\r\n        var boneByRole = new Dictionary\u003CBoneRole, int\u003E();\r\n        var tails = tailByName.Count \u003E 0 ? new Vector3?[skeleton.Count] : null;\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n        {\r\n            var boneName = skeleton[i].Name;\r\n            classes[i] = classByName[boneName];\r\n            if (roleByName.TryGetValue(boneName, out var role))\r\n            {\r\n                roles[i] = role;\r\n                if (!boneByRole.TryAdd(role, i))\r\n                    throw new ArgumentException($\u0022Role {role} is assigned to more than one bone.\u0022);\r\n            }\r\n            if (tails is not null \u0026\u0026 tailByName.TryGetValue(boneName, out var tail))\r\n                tails[i] = tail;\r\n        }\r\n\r\n        return new TargetRig(name, skeleton, classes, roles, boneByRole, tails);\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Parses the shipped s\u0026amp;box default target rig from the committed\r\n    /// \u003Cc\u003EAssets/humanoid_retargeter/target_rig_sbox.json\u003C/c\u003E text (callers do the file IO).\r\n    /// Alias of \u003Csee cref=\u0022Load\u0022/\u003E, named per design \u00A71 to distinguish the curated default\r\n    /// target from \u003Csee cref=\u0022FromSkeleton\u0022/\u003E custom targets.\r\n    /// \u003C/summary\u003E\r\n    public static TargetRig SboxDefault(string targetRigJson) =\u003E Load(targetRigJson);\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Builds a target rig from an arbitrary humanoid skeleton (design \u00A71 \u0022Custom targets\u0022):\r\n    /// roles come from \u003Cparamref name=\u0022map\u0022/\u003E (the same detection/mapping used for sources),\r\n    /// bone classes from \u003Cparamref name=\u0022rules\u0022/\u003E name patterns (default\r\n    /// \u003Csee cref=\u0022BoneClassRules\u0022/\u003E). Any bone that carries a mapped role is forced\r\n    /// \u003Csee cref=\u0022BoneClass.Animated\u0022/\u003E \u2014 a role always wins over a twist-like name. That is\r\n    /// what makes ActorCore rigs work: \u003Cc\u003ECC_Base_NeckTwist01\u003C/c\u003E matches the twist pattern\r\n    /// but IS the neck bone (the actorcore_cc profile maps it to \u003Csee cref=\u0022BoneRole.Neck\u0022/\u003E),\r\n    /// so it ends up Animated.\r\n    /// \u003C/summary\u003E\r\n    /// \u003Cexception cref=\u0022ArgumentException\u0022\u003EThrown when the mapping references a bone index\r\n    /// outside the skeleton or assigns two roles to the same bone.\u003C/exception\u003E\r\n    public static TargetRig FromSkeleton(SkeletonModel skeleton, MappingResult map, BoneClassRules? rules = null)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(skeleton);\r\n        ArgumentNullException.ThrowIfNull(map);\r\n        rules ??= new BoneClassRules();\r\n\r\n        var roles = new BoneRole?[skeleton.Count];\r\n        var boneByRole = new Dictionary\u003CBoneRole, int\u003E(map.RoleToBone.Count);\r\n        foreach (var (role, boneIndex) in map.RoleToBone)\r\n        {\r\n            if (boneIndex \u003C 0 || boneIndex \u003E= skeleton.Count)\r\n                throw new ArgumentException(\r\n                    $\u0022Mapping assigns role {role} to bone index {boneIndex}, outside the skeleton (count {skeleton.Count}).\u0022,\r\n                    nameof(map));\r\n            if (roles[boneIndex] is { } existing)\r\n                throw new ArgumentException(\r\n                    $\u0022Bone \u0027{skeleton[boneIndex].Name}\u0027 carries both roles {existing} and {role}.\u0022, nameof(map));\r\n            roles[boneIndex] = role;\r\n            boneByRole.Add(role, boneIndex);\r\n        }\r\n\r\n        var classes = new BoneClass[skeleton.Count];\r\n        for (var i = 0; i \u003C skeleton.Count; i\u002B\u002B)\r\n            classes[i] = roles[i] is not null ? BoneClass.Animated : rules.Classify(skeleton[i].Name);\r\n\r\n        // No curated vmdl backs this rig: whatever we generate for it carries no\r\n        // AnimConstraintList, so its helper bones need real baked channels.\r\n        return new TargetRig(map.ProfileName, skeleton, classes, roles, boneByRole,\r\n            tailWorld: null, helpersAreConstraintDriven: false);\r\n    }\r\n\r\n}\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"Code/TextToAnimation/Vmdl/VmdlWriter.cs","FileName":"VmdlWriter.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":409724,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.IO;\r\nusing System.Linq;\r\n\r\nnamespace TextToAnimation.Vmdl;\r\n\r\n/// \u003Csummary\u003E\r\n/// One AnimEvent to attach as a child of a vmdl AnimFile node. The node shape replicates the\r\n/// shipped citizen animation list prefab exactly (all 28 shipped events are\r\n/// \u003Cc\u003EAE_FOOTSTEP\u003C/c\u003E): \u003Cc\u003E_class = \u0022AnimEvent\u0022\u003C/c\u003E, \u003Cc\u003Eevent_class\u003C/c\u003E, \u003Cc\u003Eevent_frame\u003C/c\u003E\r\n/// (integer frame on the clip\u0027s grid), and an \u003Cc\u003Eevent_keys\u003C/c\u003E object carrying\r\n/// \u003Cc\u003EAttachment\u003C/c\u003E (\u003Cc\u003E\u0022foot_L\u0022\u003C/c\u003E/\u003Cc\u003E\u0022foot_R\u0022\u003C/c\u003E), \u003Cc\u003EFoot\u003C/c\u003E (STRING \u003Cc\u003E\u00220\u0022\u003C/c\u003E =\r\n/// left, \u003Cc\u003E\u00221\u0022\u003C/c\u003E = right) and \u003Cc\u003EVolume\u003C/c\u003E (\u003Cc\u003E0.7\u003C/c\u003E throughout the shipped data).\r\n/// \u003C/summary\u003E\r\npublic sealed class AnimEventEntry\r\n{\r\n    /// \u003Csummary\u003EEvent class name (e.g. \u003Cc\u003EAE_FOOTSTEP\u003C/c\u003E).\u003C/summary\u003E\r\n    public string EventClass { get; set; } = \u0022\u0022;\r\n\r\n    /// \u003Csummary\u003EFrame the event fires on (the clip\u0027s own frame grid).\u003C/summary\u003E\r\n    public int Frame { get; set; }\r\n\r\n    /// \u003Csummary\u003Eevent_keys \u003Cc\u003EAttachment\u003C/c\u003E value (\u003Cc\u003E\u0022foot_L\u0022\u003C/c\u003E/\u003Cc\u003E\u0022foot_R\u0022\u003C/c\u003E in the\r\n    /// shipped footstep data); null omits the key.\u003C/summary\u003E\r\n    public string? Attachment { get; set; }\r\n\r\n    /// \u003Csummary\u003Eevent_keys \u003Cc\u003EFoot\u003C/c\u003E value \u2014 the shipped data encodes the side as a STRING:\r\n    /// \u003Cc\u003E\u00220\u0022\u003C/c\u003E = left, \u003Cc\u003E\u00221\u0022\u003C/c\u003E = right; null omits the key.\u003C/summary\u003E\r\n    public string? Foot { get; set; }\r\n\r\n    /// \u003Csummary\u003Eevent_keys \u003Cc\u003EVolume\u003C/c\u003E value (\u003Cc\u003E0.7\u003C/c\u003E in all shipped footstep events);\r\n    /// null omits the key.\u003C/summary\u003E\r\n    public double? Volume { get; set; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Additional string event_keys emitted after the fixed keys, in insertion order.\r\n    /// (W3b addition, southpaw project: punch clips carry semantic contact tags such as\r\n    /// \u003Cc\u003EZone\u003C/c\u003E/\u003Cc\u003EFamily\u003C/c\u003E/\u003Cc\u003EHand\u003C/c\u003E on their contact-frame AnimEvent; the shape\r\n    /// stays the shipped citizen AnimEvent node shape, only extra keys appear.) Null or\r\n    /// empty adds nothing, so existing callers are unaffected.\r\n    /// \u003C/summary\u003E\r\n    public IReadOnlyDictionary\u003Cstring, string\u003E? ExtraKeys { get; set; }\r\n}\r\n\r\n/// \u003Csummary\u003EOne animation to register in a vmdl AnimationList.\u003C/summary\u003E\r\npublic sealed class AnimEntry\r\n{\r\n    /// \u003Csummary\u003ESequence name (must be unique within the AnimationList).\u003C/summary\u003E\r\n    public string Name { get; set; } = \u0022\u0022;\r\n\r\n    /// \u003Csummary\u003EAnimation source path relative to the assets root\r\n    /// (e.g. \u003Cc\u003Emodels/x/animations/walk.dmx\u003C/c\u003E).\u003C/summary\u003E\r\n    public string SourceFilename { get; set; } = \u0022\u0022;\r\n\r\n    /// \u003Csummary\u003EWhether the sequence loops.\u003C/summary\u003E\r\n    public bool Looping { get; set; }\r\n\r\n    /// \u003Csummary\u003EWhether to add an ExtractMotion child node (ground-plane translation\r\n    /// extraction, linear, matching the shipped citizen prefab usage).\u003C/summary\u003E\r\n    public bool ExtractMotion { get; set; }\r\n\r\n    /// \u003Csummary\u003EAnimEvent children to emit on the AnimFile node (e.g. generated\r\n    /// \u003Cc\u003EAE_FOOTSTEP\u003C/c\u003E events); empty = no event nodes.\u003C/summary\u003E\r\n    public IReadOnlyList\u003CAnimEventEntry\u003E Events { get; set; } = Array.Empty\u003CAnimEventEntry\u003E();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// When set, the AnimFile node gets an \u003Cc\u003EAnimSubtract\u003C/c\u003E child (first child, like the\r\n    /// shipped citizen data orders it) making the sequence an additive delta at compile time:\r\n    /// \u003Cc\u003Eanim_name\u003C/c\u003E = this value (the sequence whose reference frame is subtracted \u2014\r\n    /// the shipped \u003Cc\u003EIdleLayer_01_delta\u003C/c\u003E names its base sequence; other shipped entries\r\n    /// self-reference), \u003Cc\u003Eframe\u003C/c\u003E = \u003Csee cref=\u0022SubtractFrame\u0022/\u003E. The animation source is\r\n    /// reused verbatim (\u003Csee cref=\u0022SourceFilename\u0022/\u003E points at the SAME file as the base\r\n    /// entry) \u2014 resourcecompiler performs the per-frame subtraction; no frame math happens\r\n    /// here. The AnimFile\u0027s own \u003Cc\u003Edelta\u003C/c\u003E attribute stays \u003Cc\u003Efalse\u003C/c\u003E, exactly like every\r\n    /// shipped \u003Cc\u003E_delta\u003C/c\u003E sequence. Null = plain (non-additive) sequence.\r\n    /// \u003C/summary\u003E\r\n    public string? SubtractAnimName { get; set; }\r\n\r\n    /// \u003Csummary\u003EReference frame index the AnimSubtract child subtracts (the shipped data uses\r\n    /// 0 for loops and mid-clip indices for aim matrices); ignored while\r\n    /// \u003Csee cref=\u0022SubtractAnimName\u0022/\u003E is null.\u003C/summary\u003E\r\n    public int SubtractFrame { get; set; }\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// One detected locomotion family to emit as a directional 2D blend: a \u003Cc\u003EFolder\u003C/c\u003E named by\r\n/// the family stem grouping the member AnimFile entries plus one \u003Cc\u003E2DBlend\u003C/c\u003E node wired to\r\n/// the citizen pose parameters (\u003Cc\u003Emove_x\u003C/c\u003E/\u003Cc\u003Emove_y\u003C/c\u003E). Produced by\r\n/// \u003Csee cref=\u0022LocomotionSetDetector\u0022/\u003E; consumed by \u003Csee cref=\u0022VmdlWriter\u0022/\u003E and\r\n/// \u003Csee cref=\u0022VmdlAugmenter\u0022/\u003E.\r\n/// \u003C/summary\u003E\r\npublic sealed class LocomotionSetSpec\r\n{\r\n    /// \u003Csummary\u003EName of the Folder node grouping the family (the stem, collision-suffixed).\u003C/summary\u003E\r\n    public required string FolderName { get; init; }\r\n\r\n    /// \u003Csummary\u003EName of the 2DBlend node (\u003Cc\u003E\u0026lt;stem\u0026gt;_2D\u003C/c\u003E, collision-suffixed).\u003C/summary\u003E\r\n    public required string BlendName { get; init; }\r\n\r\n    /// \u003Csummary\u003ELooping flag of the 2DBlend node (true when every member loops; the shipped\r\n    /// locomotion blends are all looping).\u003C/summary\u003E\r\n    public required bool Looping { get; init; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// The 3\u00D73 \u003Cc\u003Eblend_anim_list\u003C/c\u003E grid, \u003Cc\u003E[row][col]\u003C/c\u003E with rows indexed by\r\n    /// \u003Cc\u003Emove_x\u003C/c\u003E (\u22121, 0, \u002B1) and columns by \u003Cc\u003Emove_y\u003C/c\u003E (\u22121, 0, \u002B1) \u2014 the exact shipped\r\n    /// citizen layout: row 0 = [SW, S, SE], row 1 = [W, center, E], row 2 = [NW, N, NE].\r\n    /// \u003C/summary\u003E\r\n    public required string[][] BlendGrid { get; init; }\r\n\r\n    /// \u003Csummary\u003ENames of the batch AnimFile entries grouped under the Folder, in canonical\r\n    /// direction order (N, NE, E, SE, S, SW, W, NW; absent diagonals skipped).\u003C/summary\u003E\r\n    public required IReadOnlyList\u003Cstring\u003E MemberNames { get; init; }\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Generates standalone Base-Model vmdl files (KV3 text) that reference an existing model and\r\n/// register retargeted animation DMX files, following the shipped citizen vmdl conventions\r\n/// (field set proven to compile in M0 via \u003Cc\u003Em0_test.vmdl\u003C/c\u003E).\r\n/// \u003C/summary\u003E\r\npublic static class VmdlWriter\r\n{\r\n    /// \u003Csummary\u003EThe KV3 header line used by shipped modeldoc vmdl files.\u003C/summary\u003E\r\n    public const string Kv3Header =\r\n        \u0022\u003C!-- kv3 encoding:text:version{e21c7f3c-8a33-41c5-9977-a76d3a32aa0d} format:modeldoc30:version{8c2d7a91-9c42-4bf0-883a-5a3b1762d4f1} --\u003E\u0022;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Builds a standalone vmdl: RootNode with \u003Cc\u003Ebase_model_name\u003C/c\u003E =\r\n    /// \u003Cparamref name=\u0022baseModelPath\u0022/\u003E, an optional ModelModifierList/ScaleAndMirror node\r\n    /// (omitted entirely when \u003Cparamref name=\u0022scale\u0022/\u003E is 1.0 \u2014 engine-unit sources need no\r\n    /// rescale; 0.3937 converts cm sources like the citizen rig), and an AnimationList with\r\n    /// one AnimFile per entry. When \u003Cparamref name=\u0022locomotionSets\u0022/\u003E is non-empty, each\r\n    /// set\u0027s member entries are grouped under a Folder node (appended after the loose\r\n    /// entries) together with the set\u0027s 2DBlend node, replicating the shipped citizen\r\n    /// locomotion layout. When \u003Cparamref name=\u0022meshFilePath\u0022/\u003E is non-empty a\r\n    /// RenderMeshList/RenderMeshFile node embeds that mesh source (assets-relative,\r\n    /// \u003Cparamref name=\u0022meshImportScale\u0022/\u003E raw-units\u2192skeleton-units) \u2014 this is what gives\r\n    /// custom-FBX-target vmdls a skeleton and skin to play on (an empty\r\n    /// \u003Cc\u003Ebase_model_name\u003C/c\u003E with no mesh compiles into a model with 0 bones and 0\r\n    /// sequences).\r\n    /// \u003C/summary\u003E\r\n    public static string GenerateStandalone(string baseModelPath, IEnumerable\u003CAnimEntry\u003E anims,\r\n        float scale, string defaultRootBone,\r\n        IReadOnlyList\u003CLocomotionSetSpec\u003E? locomotionSets = null,\r\n        string meshFilePath = \u0022\u0022, float meshImportScale = 1.0f,\r\n        IReadOnlyDictionary\u003Cstring, string\u003E? materialRemaps = null,\r\n        IReadOnlyList\u003Cstring\u003E? meshImportNames = null)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(baseModelPath);\r\n        ArgumentNullException.ThrowIfNull(anims);\r\n        ArgumentNullException.ThrowIfNull(defaultRootBone);\r\n\r\n        var children = new KvArray();\r\n\r\n        if (!string.IsNullOrEmpty(meshFilePath))\r\n        {\r\n            children.Items.Add(BuildRenderMeshListNode(meshFilePath, meshImportScale, meshImportNames));\r\n            // Retarget channels may drive unweighted root/helper joints. ModelDoc otherwise\r\n            // culls them from imported meshes, breaking the hierarchy the animation targets.\r\n            children.Items.Add(new KvObject\r\n            {\r\n                [\u0022_class\u0022] = new KvString(\u0022BoneMarkupList\u0022),\r\n                [\u0022children\u0022] = new KvArray(),\r\n                [\u0022bone_cull_type\u0022] = new KvString(\u0022None\u0022),\r\n            });\r\n        }\r\n        if (materialRemaps is { Count: \u003E 0 })\r\n            children.Items.Add(BuildMaterialGroupListNode(materialRemaps));\r\n\r\n        if (scale != 1.0f)\r\n        {\r\n            var modifier = new KvObject\r\n            {\r\n                [\u0022_class\u0022] = new KvString(\u0022ModelModifier_ScaleAndMirror\u0022),\r\n                // float -\u003E shortest-round-trip string -\u003E double keeps \u00220.3937\u0022 exact in text.\r\n                [\u0022scale\u0022] = new KvDouble(\r\n                    double.Parse(scale.ToString(\u0022R\u0022, CultureInfo.InvariantCulture), CultureInfo.InvariantCulture)),\r\n                [\u0022mirror_x\u0022] = new KvBool(false),\r\n                [\u0022mirror_y\u0022] = new KvBool(false),\r\n                [\u0022mirror_z\u0022] = new KvBool(false),\r\n                [\u0022flip_bone_forward\u0022] = new KvBool(false),\r\n                [\u0022swap_left_and_right_bones\u0022] = new KvBool(false),\r\n            };\r\n            var modifierChildren = new KvArray();\r\n            modifierChildren.Items.Add(modifier);\r\n            children.Items.Add(new KvObject\r\n            {\r\n                [\u0022_class\u0022] = new KvString(\u0022ModelModifierList\u0022),\r\n                [\u0022children\u0022] = modifierChildren,\r\n            });\r\n        }\r\n\r\n        var sets = locomotionSets ?? Array.Empty\u003CLocomotionSetSpec\u003E();\r\n        var grouped = new HashSet\u003Cstring\u003E(StringComparer.Ordinal);\r\n        foreach (var set in sets)\r\n        {\r\n            foreach (var member in set.MemberNames)\r\n                grouped.Add(member);\r\n        }\r\n\r\n        var animList = anims as IReadOnlyList\u003CAnimEntry\u003E ?? new List\u003CAnimEntry\u003E(anims);\r\n        var animChildren = new KvArray();\r\n        foreach (var anim in animList)\r\n        {\r\n            if (!grouped.Contains(anim.Name))\r\n                animChildren.Items.Add(BuildAnimFileNode(anim, defaultRootBone));\r\n        }\r\n        foreach (var set in sets)\r\n            animChildren.Items.Add(BuildLocomotionFolderNode(set, animList, defaultRootBone));\r\n        children.Items.Add(new KvObject\r\n        {\r\n            [\u0022_class\u0022] = new KvString(\u0022AnimationList\u0022),\r\n            [\u0022children\u0022] = animChildren,\r\n            [\u0022default_root_bone_name\u0022] = new KvString(defaultRootBone),\r\n        });\r\n\r\n        var root = new KvObject\r\n        {\r\n            [\u0022rootNode\u0022] = new KvObject\r\n            {\r\n                [\u0022_class\u0022] = new KvString(\u0022RootNode\u0022),\r\n                [\u0022children\u0022] = children,\r\n                [\u0022model_archetype\u0022] = new KvString(\u0022\u0022),\r\n                [\u0022primary_associated_entity\u0022] = new KvString(\u0022\u0022),\r\n                [\u0022anim_graph_name\u0022] = new KvString(\u0022\u0022),\r\n                [\u0022base_model_name\u0022] = new KvString(baseModelPath),\r\n            },\r\n        };\r\n\r\n        return Kv3.Serialize(new Kv3Document(Kv3Header, root));\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Builds one AnimFile KV3 node (full attribute set as compiled in M0). When the entry\r\n    /// requests motion extraction, an ExtractMotion child extracting ground-plane translation\r\n    /// on \u003Cparamref name=\u0022motionRootBone\u0022/\u003E is included; \u003Csee cref=\u0022AnimEntry.Events\u0022/\u003E\r\n    /// become AnimEvent children (shipped-prefab node shape, see\r\n    /// \u003Csee cref=\u0022AnimEventEntry\u0022/\u003E); \u003Csee cref=\u0022AnimEntry.SubtractAnimName\u0022/\u003E adds the\r\n    /// AnimSubtract FIRST child the shipped \u003Cc\u003E_delta\u003C/c\u003E sequences carry\r\n    /// (\u003Cc\u003E_class\u003C/c\u003E/\u003Cc\u003Eanim_name\u003C/c\u003E/\u003Cc\u003Eframe\u003C/c\u003E, nothing else).\r\n    /// \u003C/summary\u003E\r\n    internal static KvObject BuildAnimFileNode(AnimEntry entry, string motionRootBone)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(entry);\r\n\r\n        var node = new KvObject\r\n        {\r\n            [\u0022_class\u0022] = new KvString(\u0022AnimFile\u0022),\r\n            [\u0022name\u0022] = new KvString(entry.Name),\r\n        };\r\n\r\n        var nodeChildren = new KvArray();\r\n        if (entry.SubtractAnimName is not null)\r\n        {\r\n            nodeChildren.Items.Add(new KvObject\r\n            {\r\n                [\u0022_class\u0022] = new KvString(\u0022AnimSubtract\u0022),\r\n                [\u0022anim_name\u0022] = new KvString(entry.SubtractAnimName),\r\n                [\u0022frame\u0022] = new KvLong(entry.SubtractFrame),\r\n            });\r\n        }\r\n        if (entry.ExtractMotion)\r\n        {\r\n            var extract = new KvObject\r\n            {\r\n                [\u0022_class\u0022] = new KvString(\u0022ExtractMotion\u0022),\r\n                [\u0022extract_tx\u0022] = new KvBool(true),\r\n                [\u0022extract_ty\u0022] = new KvBool(true),\r\n                [\u0022extract_tz\u0022] = new KvBool(false),\r\n                [\u0022extract_rz\u0022] = new KvBool(false),\r\n                [\u0022linear\u0022] = new KvBool(true),\r\n                [\u0022quadratic\u0022] = new KvBool(false),\r\n                [\u0022root_bone_name\u0022] = new KvString(motionRootBone),\r\n                [\u0022motion_type\u0022] = new KvString(\u0022Single\u0022),\r\n            };\r\n            nodeChildren.Items.Add(extract);\r\n        }\r\n        foreach (var animEvent in entry.Events)\r\n            nodeChildren.Items.Add(BuildAnimEventNode(animEvent));\r\n        if (nodeChildren.Items.Count \u003E 0)\r\n            node[\u0022children\u0022] = nodeChildren;\r\n\r\n        node[\u0022activity_name\u0022] = new KvString(\u0022\u0022);\r\n        node[\u0022activity_weight\u0022] = new KvLong(1);\r\n        node[\u0022weight_list_name\u0022] = new KvString(\u0022\u0022);\r\n        node[\u0022fade_in_time\u0022] = new KvDouble(0.2);\r\n        node[\u0022fade_out_time\u0022] = new KvDouble(0.2);\r\n        node[\u0022looping\u0022] = new KvBool(entry.Looping);\r\n        node[\u0022delta\u0022] = new KvBool(false);\r\n        node[\u0022worldSpace\u0022] = new KvBool(false);\r\n        node[\u0022hidden\u0022] = new KvBool(false);\r\n        node[\u0022anim_markup_ordered\u0022] = new KvBool(false);\r\n        node[\u0022disable_compression\u0022] = new KvBool(false);\r\n        node[\u0022disable_interpolation\u0022] = new KvBool(false);\r\n        node[\u0022enable_scale\u0022] = new KvBool(false);\r\n        node[\u0022source_filename\u0022] = new KvString(entry.SourceFilename);\r\n        node[\u0022start_frame\u0022] = new KvLong(-1);\r\n        node[\u0022end_frame\u0022] = new KvLong(-1);\r\n        node[\u0022framerate\u0022] = new KvDouble(-1.0);\r\n        node[\u0022take\u0022] = new KvLong(0);\r\n        node[\u0022reverse\u0022] = new KvBool(false);\r\n        return node;\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Builds one AnimEvent KV3 node in the shipped citizen prefab shape:\r\n    /// \u003Cc\u003E_class\u003C/c\u003E/\u003Cc\u003Eevent_class\u003C/c\u003E/\u003Cc\u003Eevent_frame\u003C/c\u003E plus an \u003Cc\u003Eevent_keys\u003C/c\u003E object\r\n    /// (key order \u003Cc\u003EAttachment\u003C/c\u003E, \u003Cc\u003EFoot\u003C/c\u003E, \u003Cc\u003EVolume\u003C/c\u003E, exactly like the shipped\r\n    /// data; null-valued keys are omitted, and an all-null key set omits the object).\r\n    /// \u003C/summary\u003E\r\n    /// \u003Csummary\u003EA \u003Cc\u003ERenderMeshList\u003C/c\u003E node with one \u003Cc\u003ERenderMeshFile\u003C/c\u003E child (field\r\n    /// set mirrors the shipped citizen_human_male_staging.vmdl). Shared with\r\n    /// \u003Csee cref=\u0022VmdlAugmenter.EnsureMeshFile\u0022/\u003E so accumulated pipeline-owned vmdls that\r\n    /// predate mesh embedding can be healed in place.\u003C/summary\u003E\r\n    internal static KvObject BuildRenderMeshListNode(string meshFilePath, float meshImportScale,\r\n        IReadOnlyList\u003Cstring\u003E? meshImportNames = null)\r\n    {\r\n        var meshChildren = new KvArray();\r\n        // Keep source mesh instances separate: merging a large multi-part FBX into one\r\n        // skin buffer can corrupt rendered vertices despite correct bones and weights.\r\n        IEnumerable\u003Cstring?\u003E parts = meshImportNames is { Count: \u003E 1 }\r\n            ? (IEnumerable\u003Cstring?\u003E)meshImportNames.Distinct(StringComparer.OrdinalIgnoreCase) : new string?[] { null };\r\n        foreach (var part in parts)\r\n        {\r\n            var names = new KvArray();\r\n            if (part is not null)\r\n                names.Items.Add(new KvString(part));\r\n            // Do not reuse an input mesh name as a ModelDoc node name (self-reference).\r\n            var name = Path.GetFileNameWithoutExtension(meshFilePath);\r\n            if (part is not null)\r\n                name \u002B= $\u0022_part_{meshChildren.Items.Count}\u0022;\r\n            var node = new KvObject\r\n            {\r\n                [\u0022_class\u0022] = new KvString(\u0022RenderMeshFile\u0022),\r\n                [\u0022name\u0022] = new KvString(name),\r\n                [\u0022filename\u0022] = new KvString(meshFilePath),\r\n                [\u0022import_translation\u0022] = MakeVector3(0, 0, 0),\r\n                [\u0022import_rotation\u0022] = MakeVector3(0, 0, 0),\r\n                [\u0022import_scale\u0022] = new KvDouble(\r\n                    double.Parse(meshImportScale.ToString(\u0022R\u0022, CultureInfo.InvariantCulture),\r\n                        CultureInfo.InvariantCulture)),\r\n                [\u0022align_origin_x_type\u0022] = new KvString(\u0022None\u0022),\r\n                [\u0022align_origin_y_type\u0022] = new KvString(\u0022None\u0022),\r\n                [\u0022align_origin_z_type\u0022] = new KvString(\u0022None\u0022),\r\n                [\u0022parent_bone\u0022] = new KvString(\u0022\u0022),\r\n                [\u0022import_filter\u0022] = new KvObject\r\n                {\r\n                    [\u0022exclude_by_default\u0022] = new KvBool(part is not null),\r\n                    [\u0022exception_list\u0022] = names,\r\n                },\r\n            };\r\n            meshChildren.Items.Add(node);\r\n        }\r\n        return new KvObject\r\n        {\r\n            [\u0022_class\u0022] = new KvString(\u0022RenderMeshList\u0022),\r\n            [\u0022children\u0022] = meshChildren,\r\n        };\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// A \u003Cc\u003EMaterialGroupList\u003C/c\u003E with one \u003Cc\u003EDefaultMaterialGroup\u003C/c\u003E carrying material\r\n    /// remaps (bare mesh material reference \u2192 assets-relative vmat path) \u2014 the shipped\r\n    /// citizen vmdl\u0027s own mechanism. FBX materials are bare names the compiler cannot\r\n    /// resolve as resource paths; without the remap every mesh logs\r\n    /// \u0027Missing vmat \u0022\u0026lt;name\u0026gt;.vmat\u0022\u0027 and renders placeholder materials.\r\n    /// \u003C/summary\u003E\r\n    internal static KvObject BuildMaterialGroupListNode(IReadOnlyDictionary\u003Cstring, string\u003E remaps)\r\n    {\r\n        var remapArray = new KvArray();\r\n        foreach (var (from, to) in remaps.OrderBy(kv =\u003E kv.Key, StringComparer.Ordinal))\r\n        {\r\n            remapArray.Items.Add(new KvObject\r\n            {\r\n                [\u0022from\u0022] = new KvString(from),\r\n                [\u0022to\u0022] = new KvString(to),\r\n            });\r\n        }\r\n\r\n        var groupChildren = new KvArray();\r\n        groupChildren.Items.Add(new KvObject\r\n        {\r\n            [\u0022_class\u0022] = new KvString(\u0022DefaultMaterialGroup\u0022),\r\n            [\u0022remaps\u0022] = remapArray,\r\n            [\u0022use_global_default\u0022] = new KvBool(false),\r\n            [\u0022global_default_material\u0022] = new KvString(\u0022\u0022),\r\n        });\r\n        return new KvObject\r\n        {\r\n            [\u0022_class\u0022] = new KvString(\u0022MaterialGroupList\u0022),\r\n            [\u0022children\u0022] = groupChildren,\r\n        };\r\n    }\r\n\r\n    /// \u003Csummary\u003EKV3 \u003Cc\u003E[ x, y, z ]\u003C/c\u003E double array (RenderMeshFile import vectors).\u003C/summary\u003E\r\n    private static KvArray MakeVector3(double x, double y, double z)\r\n    {\r\n        var array = new KvArray();\r\n        array.Items.Add(new KvDouble(x));\r\n        array.Items.Add(new KvDouble(y));\r\n        array.Items.Add(new KvDouble(z));\r\n        return array;\r\n    }\r\n\r\n    private static KvObject BuildAnimEventNode(AnimEventEntry animEvent)\r\n    {\r\n        var node = new KvObject\r\n        {\r\n            [\u0022_class\u0022] = new KvString(\u0022AnimEvent\u0022),\r\n            [\u0022event_class\u0022] = new KvString(animEvent.EventClass),\r\n            [\u0022event_frame\u0022] = new KvLong(animEvent.Frame),\r\n        };\r\n\r\n        var keys = new KvObject();\r\n        if (animEvent.Attachment is not null)\r\n            keys[\u0022Attachment\u0022] = new KvString(animEvent.Attachment);\r\n        if (animEvent.Foot is not null)\r\n            keys[\u0022Foot\u0022] = new KvString(animEvent.Foot);\r\n        if (animEvent.Volume is { } volume)\r\n            keys[\u0022Volume\u0022] = new KvDouble(volume);\r\n        if (animEvent.ExtraKeys is not null)\r\n        {\r\n            foreach (var kv in animEvent.ExtraKeys)\r\n                keys[kv.Key] = new KvString(kv.Value);\r\n        }\r\n        if (keys.Count \u003E 0)\r\n            node[\u0022event_keys\u0022] = keys;\r\n\r\n        return node;\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Builds a locomotion Folder node in the shipped citizen shape (\u003Cc\u003E_class\u003C/c\u003E,\r\n    /// \u003Cc\u003Ename\u003C/c\u003E, \u003Cc\u003Echildren\u003C/c\u003E): the set\u0027s 2DBlend node first, then the member AnimFile\r\n    /// nodes (in \u003Csee cref=\u0022LocomotionSetSpec.MemberNames\u0022/\u003E order) \u2014 mirroring how the\r\n    /// shipped \u003Cc\u003ECrouchWalk\u003C/c\u003E/\u003Cc\u003EWalk\u003C/c\u003E/\u003Cc\u003ERun\u003C/c\u003E folders lead with their blend nodes.\r\n    /// \u003C/summary\u003E\r\n    internal static KvObject BuildLocomotionFolderNode(\r\n        LocomotionSetSpec set, IReadOnlyList\u003CAnimEntry\u003E allEntries, string motionRootBone)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(set);\r\n        ArgumentNullException.ThrowIfNull(allEntries);\r\n\r\n        var folderChildren = new KvArray();\r\n        folderChildren.Items.Add(Build2DBlendNode(set));\r\n        foreach (var member in set.MemberNames)\r\n        {\r\n            AnimEntry? entry = null;\r\n            foreach (var candidate in allEntries)\r\n            {\r\n                if (string.Equals(candidate.Name, member, StringComparison.Ordinal))\r\n                {\r\n                    entry = candidate;\r\n                    break;\r\n                }\r\n            }\r\n            if (entry is null)\r\n                throw new ArgumentException(\r\n                    $\u0022Locomotion set \u0027{set.FolderName}\u0027 references unknown entry \u0027{member}\u0027.\u0022);\r\n            folderChildren.Items.Add(BuildAnimFileNode(entry, motionRootBone));\r\n        }\r\n\r\n        return new KvObject\r\n        {\r\n            [\u0022_class\u0022] = new KvString(\u0022Folder\u0022),\r\n            [\u0022name\u0022] = new KvString(set.FolderName),\r\n            [\u0022children\u0022] = folderChildren,\r\n        };\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Builds one 2DBlend KV3 node replicating the shipped citizen locomotion blends\r\n    /// (\u003Cc\u003ECrouchWalk_Default_2D\u003C/c\u003E, \u003Cc\u003ERun_Default_2D\u003C/c\u003E, \u2026) EXACTLY: the common sequence\r\n    /// attribute set (with \u003Cc\u003Eactivity_name\u003C/c\u003E left empty \u2014 the shipped activity wiring is\r\n    /// model-specific), then \u003Cc\u003Erow_pose_param_name = \u0022move_x\u0022\u003C/c\u003E,\r\n    /// \u003Cc\u003Ecol_pose_param_name = \u0022move_y\u0022\u003C/c\u003E, \u003Cc\u003Erow_weight_list\u003C/c\u003E/\u003Cc\u003Ecol_weight_list\u003C/c\u003E\r\n    /// of \u003Cc\u003E[-1.0, 0.0, 1.0]\u003C/c\u003E and the 3\u00D73 \u003Cc\u003Eblend_anim_list\u003C/c\u003E grid. The pose\r\n    /// parameters are NOT declared here: the citizen base models pull them in via their\r\n    /// PoseParamList prefab (\u003Cc\u003Ecitizen_poseparamlist.vmdl_prefab\u003C/c\u003E), which Base-Model\r\n    /// children inherit \u2014 custom (non-citizen) targets must declare \u003Cc\u003Emove_x\u003C/c\u003E/\u003Cc\u003Emove_y\u003C/c\u003E\r\n    /// on their base model for the blend to be drivable.\r\n    /// \u003C/summary\u003E\r\n    internal static KvObject Build2DBlendNode(LocomotionSetSpec set)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(set);\r\n        if (set.BlendGrid.Length != 3 || Array.Exists(set.BlendGrid, row =\u003E row.Length != 3))\r\n            throw new ArgumentException(\u0022Locomotion blend grid must be 3x3.\u0022, nameof(set));\r\n\r\n        var node = new KvObject\r\n        {\r\n            [\u0022_class\u0022] = new KvString(\u00222DBlend\u0022),\r\n            [\u0022name\u0022] = new KvString(set.BlendName),\r\n            [\u0022activity_name\u0022] = new KvString(\u0022\u0022),\r\n            [\u0022activity_weight\u0022] = new KvLong(1),\r\n            [\u0022weight_list_name\u0022] = new KvString(\u0022\u0022),\r\n            [\u0022fade_in_time\u0022] = new KvDouble(0.2),\r\n            [\u0022fade_out_time\u0022] = new KvDouble(0.2),\r\n            [\u0022looping\u0022] = new KvBool(set.Looping),\r\n            [\u0022delta\u0022] = new KvBool(false),\r\n            [\u0022worldSpace\u0022] = new KvBool(false),\r\n            [\u0022hidden\u0022] = new KvBool(false),\r\n            [\u0022anim_markup_ordered\u0022] = new KvBool(false),\r\n            [\u0022disable_compression\u0022] = new KvBool(false),\r\n            [\u0022disable_interpolation\u0022] = new KvBool(false),\r\n            [\u0022enable_scale\u0022] = new KvBool(false),\r\n            [\u0022row_pose_param_name\u0022] = new KvString(\u0022move_x\u0022),\r\n            [\u0022col_pose_param_name\u0022] = new KvString(\u0022move_y\u0022),\r\n            [\u0022row_weight_list\u0022] = AxisWeights(),\r\n            [\u0022col_weight_list\u0022] = AxisWeights(),\r\n        };\r\n\r\n        var grid = new KvArray();\r\n        foreach (var row in set.BlendGrid)\r\n        {\r\n            var rowArray = new KvArray();\r\n            foreach (var cell in row)\r\n                rowArray.Items.Add(new KvString(cell));\r\n            grid.Items.Add(rowArray);\r\n        }\r\n        node[\u0022blend_anim_list\u0022] = grid;\r\n        return node;\r\n\r\n        static KvArray AxisWeights()\r\n        {\r\n            var weights = new KvArray();\r\n            weights.Items.Add(new KvDouble(-1.0));\r\n            weights.Items.Add(new KvDouble(0.0));\r\n            weights.Items.Add(new KvDouble(1.0));\r\n            return weights;\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"TextToAnimation/Processing/FootstepEvents.cs","FileName":"FootstepEvents.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":409724,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing TextToAnimation.Vmdl;\r\nusing TextToAnimation.Maths;\r\nusing SkeletonModel = TextToAnimation.Rig.Skeleton;\r\n\r\nnamespace TextToAnimation.Processing;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/TextToAnimation/Assembly.cs)\r\n\r\n/// \u003Csummary\u003E\r\n/// Generates \u003Cc\u003EAE_FOOTSTEP\u003C/c\u003E \u003Csee cref=\u0022AnimEventEntry\u0022/\u003E lists from foot-plant detection\r\n/// on a solved TARGET clip: each settled contact after a lift is the touchdown\r\n/// moment, so it becomes one footstep event for that foot.\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EShipped-data findings\u003C/b\u003E (dev fixture\r\n/// \u003Cc\u003Ecitizen_animationlist.vmdl_prefab\u003C/c\u003E): every one of the 28 shipped AnimEvent nodes is\r\n/// \u003Cc\u003Eevent_class = \u0022AE_FOOTSTEP\u0022\u003C/c\u003E with an \u003Cc\u003Eevent_keys\u003C/c\u003E object of exactly\r\n/// \u003Cc\u003EAttachment = \u0022foot_L\u0022\u003C/c\u003E/\u003Cc\u003E\u0022foot_R\u0022\u003C/c\u003E, \u003Cc\u003EFoot = \u00220\u0022\u003C/c\u003E (left) / \u003Cc\u003E\u00221\u0022\u003C/c\u003E\r\n/// (right, a STRING) and \u003Cc\u003EVolume = 0.7\u003C/c\u003E. The generated events replicate that shape\r\n/// verbatim, including the side encoding.\u003C/para\u003E\r\n/// \u003Cpara\u003E\u003Cb\u003EFrame-0 plants are skipped\u003C/b\u003E: a plant interval that begins on the clip\u0027s first\r\n/// frame means the foot was ALREADY planted when the clip starts \u2014 no touchdown happened\r\n/// inside the clip, and on looping clips the same physical step would otherwise fire twice\r\n/// (once at the wrap-around touchdown near the clip end, once at frame 0).\u003C/para\u003E\r\n/// \u003C/remarks\u003E\r\npublic static class FootstepEvents\r\n{\r\n    /// \u003Csummary\u003EThe Source 2 footstep anim-event class (the only event class present in the\r\n    /// shipped citizen animation data).\u003C/summary\u003E\r\n    public const string FootstepEventClass = \u0022AE_FOOTSTEP\u0022;\r\n\r\n    /// \u003Csummary\u003EThe constant volume used by every shipped footstep event.\u003C/summary\u003E\r\n    public const double FootstepVolume = 0.7;\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Detects vertical settling near the ground after a foot lift and returns one\r\n    /// \u003Cc\u003EAE_FOOTSTEP\u003C/c\u003E per contact (frame-0 contacts skipped), merged in frame order.\r\n    /// Horizontal sliding is allowed for in-place locomotion; cleanup still uses its\r\n    /// stricter world-speed detector. Without explicit options, minimum contact duration\r\n    /// scales with sample rate (0.1 seconds).\r\n    /// \u003C/summary\u003E\r\n    /// \u003Cparam name=\u0022frames\u0022\u003ESolved per-frame local transforms (target skeleton bone order).\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022skeleton\u0022\u003ETarget skeleton the frames are expressed against.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022left\u0022\u003ELeft leg chain (target bone indices).\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022right\u0022\u003ERight leg chain (target bone indices).\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022up\u0022\u003ETarget character up direction.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022fps\u0022\u003EClip sample rate.\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022options\u0022\u003EPlant-detection tunables; null = defaults (callers rescale the\r\n    /// cm-tuned thresholds for engine-space rigs, like the foot-plant cleanup does).\u003C/param\u003E\r\n    public static List\u003CAnimEventEntry\u003E Generate(\r\n        List\u003CXForm[]\u003E frames,\r\n        SkeletonModel skeleton,\r\n        FootChain left,\r\n        FootChain right,\r\n        Vector3 up,\r\n        float fps,\r\n        FootPlantOptions? options = null)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(frames);\r\n        ArgumentNullException.ThrowIfNull(skeleton);\r\n        ArgumentNullException.ThrowIfNull(left);\r\n        ArgumentNullException.ThrowIfNull(right);\r\n\r\n        var events = new List\u003CAnimEventEntry\u003E();\r\n        if (frames.Count \u003C 2 || !float.IsFinite(fps) || fps \u003C= 0 || !float.IsFinite(up.LengthSquared()) || up.LengthSquared() \u003C 1e-12f)\r\n            return events;\r\n        up = Vector3.Normalize(up);\r\n        options ??= new FootPlantOptions\r\n        {\r\n            // Same minimum contact time at every sample rate (three frames at 30 Hz).\r\n            MinPlantFrames = Math.Max(2, (int)MathF.Ceiling(fps * 0.1f)),\r\n        };\r\n        var ankleL = FootPlant.AnkleWorldPositions(frames, skeleton, left.Ankle);\r\n        var ankleR = FootPlant.AnkleWorldPositions(frames, skeleton, right.Ankle);\r\n        var ground = FootPlant.EstimateGround(ankleL, ankleR, up);\r\n        AddFoot(events, Contacts(ankleL, up, ground, fps, options), attachment: \u0022foot_L\u0022, foot: \u00220\u0022);\r\n        AddFoot(events, Contacts(ankleR, up, ground, fps, options), attachment: \u0022foot_R\u0022, foot: \u00221\u0022);\r\n        // Stable frame ordering across both feet (List.Sort is unstable; the comparer breaks\r\n        // frame ties on the side key so the result is deterministic).\r\n        events.Sort((a, b) =\u003E a.Frame != b.Frame\r\n            ? a.Frame.CompareTo(b.Frame)\r\n            : string.CompareOrdinal(a.Foot, b.Foot));\r\n        return events;\r\n    }\r\n\r\n    // A planted foot slides horizontally in an in-place clip. Detect vertical settling\r\n    // near the floor instead, but require a real lift before another touchdown. This\r\n    // also prevents horizontal speed jitter from generating repeated step sounds.\r\n    private static List\u003CFrameRange\u003E Contacts(Vector3[] ankle, Vector3 up, float ground, float fps, FootPlantOptions options)\r\n    {\r\n        var heights = new Vector3[ankle.Length];\r\n        for (var i = 0; i \u003C ankle.Length; i\u002B\u002B) heights[i] = up * Vector3.Dot(ankle[i], up);\r\n        var candidates = FootPlant.DetectPlants(heights, up, ground, fps, options);\r\n        var contacts = new List\u003CFrameRange\u003E();\r\n        var previousEnd = 0;\r\n        foreach (var candidate in candidates)\r\n        {\r\n            var lifted = false;\r\n            for (var i = previousEnd; i \u003C candidate.Start; i\u002B\u002B)\r\n                lifted |= Vector3.Dot(ankle[i], up) - ground \u003E= options.HeightThresholdCm * 1.5f;\r\n            if (lifted) contacts.Add(candidate);\r\n            previousEnd = candidate.End;\r\n        }\r\n        return contacts;\r\n    }\r\n\r\n    private static void AddFoot(\r\n        List\u003CAnimEventEntry\u003E events, List\u003CFrameRange\u003E plants, string attachment, string foot)\r\n    {\r\n        foreach (var plant in plants)\r\n        {\r\n            if (plant.Start == 0)\r\n                continue; // already planted at clip start: no touchdown inside the clip\r\n            events.Add(new AnimEventEntry\r\n            {\r\n                EventClass = FootstepEventClass,\r\n                Frame = plant.Start,\r\n                Attachment = attachment,\r\n                Foot = foot,\r\n                Volume = FootstepVolume,\r\n            });\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"TextToAnimation/Processing/IkBoneBaker.cs","FileName":"IkBoneBaker.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":409724,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Numerics;\r\nusing TextToAnimation.Mapping;\r\nusing TextToAnimation.Maths;\r\nusing TextToAnimation.Rig;\r\n\r\nnamespace TextToAnimation.Processing;\r\n\r\nusing Vector3 = System.Numerics.Vector3; // s\u0026box compat: shadow engine\u0027s global-namespace Vector3 (see Code/TextToAnimation/Assembly.cs)\r\n\r\n/// \u003Csummary\u003E\r\n/// Bakes real animation channels onto the \u003Csee cref=\u0022BoneClass.IkBaked\u0022/\u003E helper bones from\r\n/// the already-solved \u003Csee cref=\u0022BoneClass.Animated\u0022/\u003E body bones. The s\u0026amp;box default\r\n/// animgraph reads these bones, so every retargeted clip must carry data for them.\r\n/// \u003C/summary\u003E\r\n/// \u003Cremarks\u003E\r\n/// \u003Cpara\u003E\r\n/// Every rule below was DERIVED from shipped Facepunch clips\r\n/// (\u003Cc\u003ECitizen@Run_N.fbx\u003C/c\u003E, \u003Cc\u003ECitizen@Dab.fbx\u003C/c\u003E, plus the \u003Cc\u003E*_AimMatrix\u003C/c\u003E clips for\r\n/// the aim bones) by measuring per-frame relationships between the IK bones and the solved\r\n/// body bones, and is verified by reproducing those clips\u0027 IK channels in\r\n/// \u003Cc\u003EIkBoneBakerTests\u003C/c\u003E. Measured residuals on both probe clips (positions cm, FBX scene\r\n/// space of the citizen sources, Y-up):\r\n/// \u003C/para\u003E\r\n/// \u003Clist type=\u0022bullet\u0022\u003E\r\n/// \u003Citem\u003E\u003Cc\u003Efoot_L/R_IK_target\u003C/c\u003E: world transform equals the world transform of\r\n/// \u003Cc\u003Eankle_L/R\u003C/c\u003E exactly (max residual 1e-4 cm / 0.000\u00B0).\u003C/item\u003E\r\n/// \u003Citem\u003E\u003Cc\u003Ehand_L/R_IK_target\u003C/c\u003E: world transform equals \u003Cc\u003Ehand_L/R\u003C/c\u003E exactly\r\n/// (1e-4 cm / 0.000\u00B0).\u003C/item\u003E\r\n/// \u003Citem\u003E\u003Cc\u003Ehand_L_to_R_ikrule\u003C/c\u003E (parented under \u003Cc\u003Ehand_R\u003C/c\u003E): world transform equals\r\n/// \u003Cc\u003Ehand_L\u003C/c\u003E exactly \u2014 the bone named \u003Cc\u003Ehand_X_to_Y_ikrule\u003C/c\u003E carries hand X\u0027s world\r\n/// transform expressed in hand Y\u0027s space (1e-4 cm / 0.000\u00B0). Symmetrically for\r\n/// \u003Cc\u003Ehand_R_to_L_ikrule\u003C/c\u003E.\u003C/item\u003E\r\n/// \u003Citem\u003E\u003Cc\u003Eroot_IK\u003C/c\u003E: world position equals the pelvis world position projected onto the\r\n/// ground plane \u2014 the lateral components match the pelvis exactly (0.0000 cm) and the up\r\n/// component stays at root_IK\u0027s rest height (0). World rotation never animates: it stays at\r\n/// root_IK\u0027s rest world rotation (0.000\u00B0 deviation even while the pelvis yaws), which is the\r\n/// up-axis frame change quaternion, i.e. identity in engine space. The up axis is derived\r\n/// from rest geometry (the dominant component of pelvisRest \u2212 rootIkRest), not hardcoded.\u003C/item\u003E\r\n/// \u003Citem\u003E\u003Cc\u003Ehold_L/R\u003C/c\u003E: local transform stays at rest (0.0000 cm / 0.000\u00B0).\u003C/item\u003E\r\n/// \u003Citem\u003E\u003Cc\u003Ehand_L/R_IK_attach\u003C/c\u003E: absent from every probed shipped clip skeleton (the clip\r\n/// FBXs parent the hand IK targets directly under \u003Cc\u003Eroot_IK\u003C/c\u003E); in the human_male rig its\r\n/// rest local under \u003Cc\u003Eroot_IK\u003C/c\u003E is identity. Keeping rest reproduces the shipped layout,\r\n/// and the hand targets still land on the hands because their locals are solved through the\r\n/// attach\u0027s world.\u003C/item\u003E\r\n/// \u003Citem\u003E\u003Cc\u003Eaim_matrix_*\u003C/c\u003E: absent from all regular shipped clips. Even in the dedicated\r\n/// \u003Cc\u003E*_AimMatrix\u003C/c\u003E clips, \u003Cc\u003Eaim_matrix_02/03\u003C/c\u003E sit exactly at rest local\r\n/// (0.0000 cm / 0.000\u00B0) and \u003Cc\u003Eaim_matrix_01\u003C/c\u003E holds an animator-authored constant pose\r\n/// (not derivable from the body) \u2014 these bones are animgraph aim-space references, so\r\n/// retargeted clips keep them at rest.\u003C/item\u003E\r\n/// \u003C/list\u003E\r\n/// \u003Cpara\u003E\r\n/// Any \u003Csee cref=\u0022BoneClass.IkBaked\u0022/\u003E bone without a recognized name also keeps its rest\r\n/// local (the conservative choice \u2014 identical to omitting the channel).\r\n/// \u003C/para\u003E\r\n/// \u003C/remarks\u003E\r\npublic static class IkBoneBaker\r\n{\r\n    /// \u003Csummary\u003E\r\n    /// Overwrites the local transforms of every \u003Csee cref=\u0022BoneClass.IkBaked\u0022/\u003E bone in every\r\n    /// frame, deriving them from the already-solved body bones of the same frame. All other\r\n    /// bones\u0027 locals are never touched. Deterministic; mutates \u003Cparamref name=\u0022frames\u0022/\u003E in\r\n    /// place.\r\n    /// \u003C/summary\u003E\r\n    /// \u003Cparam name=\u0022frames\u0022\u003EPer-frame local transforms, one \u003Csee cref=\u0022XForm\u0022/\u003E per bone in\r\n    /// \u003Cparamref name=\u0022target\u0022/\u003E skeleton order (e.g. \u003Cc\u003EClip.Frames\u003C/c\u003E).\u003C/param\u003E\r\n    /// \u003Cparam name=\u0022target\u0022\u003EThe target rig supplying skeleton, bone classes and roles.\u003C/param\u003E\r\n    /// \u003Cexception cref=\u0022ArgumentException\u0022\u003EThrown when a frame\u0027s length does not match the\r\n    /// target skeleton.\u003C/exception\u003E\r\n    public static void Bake(List\u003CXForm[]\u003E frames, TargetRig target)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(frames);\r\n        ArgumentNullException.ThrowIfNull(target);\r\n\r\n        var skeleton = target.Skeleton;\r\n        var count = skeleton.Count;\r\n        for (var f = 0; f \u003C frames.Count; f\u002B\u002B)\r\n        {\r\n            if (frames[f].Length != count)\r\n                throw new ArgumentException(\r\n                    $\u0022Frame {f} has {frames[f].Length} bones but the target skeleton has {count}.\u0022,\r\n                    nameof(frames));\r\n        }\r\n\r\n        var rules = BuildRules(target);\r\n\r\n        var world = new XForm[count];\r\n        var computed = new bool[count];\r\n        foreach (var locals in frames)\r\n        {\r\n            Array.Clear(computed);\r\n\r\n            // Pass 1: world transforms of every bone with no IkBaked ancestor (the body).\r\n            // A bone is skipped exactly when its parent was skipped or is itself IkBaked,\r\n            // so the skip set is precisely the IK bones and their descendants.\r\n            for (var i = 0; i \u003C count; i\u002B\u002B)\r\n            {\r\n                if (target.ClassOf(i) == BoneClass.IkBaked)\r\n                    continue;\r\n                var parent = skeleton[i].ParentIndex;\r\n                if (parent \u003E= 0 \u0026\u0026 !computed[parent])\r\n                    continue;\r\n                world[i] = parent \u003C 0 ? locals[i] : XForm.Compose(world[parent], locals[i]);\r\n                computed[i] = true;\r\n            }\r\n\r\n            // Pass 2: bake IK bones (and propagate worlds through their subtrees). Bones are\r\n            // topologically sorted, so a parent\u0027s world is always available by the time its\r\n            // child is processed.\r\n            for (var i = 0; i \u003C count; i\u002B\u002B)\r\n            {\r\n                if (computed[i])\r\n                    continue;\r\n                var parent = skeleton[i].ParentIndex;\r\n\r\n                if (target.ClassOf(i) == BoneClass.IkBaked)\r\n                {\r\n                    if (rules[i].Kind == RuleKind.KeepRestLocal)\r\n                    {\r\n                        locals[i] = skeleton[i].RestLocal;\r\n                        world[i] = parent \u003C 0 ? locals[i] : XForm.Compose(world[parent], locals[i]);\r\n                    }\r\n                    else\r\n                    {\r\n                        var desired = Desired(in rules[i], world, computed, skeleton[i].Name);\r\n                        locals[i] = parent \u003C 0 ? desired : XForm.ToLocal(world[parent], desired);\r\n                        world[i] = desired;\r\n                    }\r\n                }\r\n                else\r\n                {\r\n                    // Non-IK descendant of an IK bone: local untouched, world propagated.\r\n                    world[i] = parent \u003C 0 ? locals[i] : XForm.Compose(world[parent], locals[i]);\r\n                }\r\n                computed[i] = true;\r\n            }\r\n        }\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Pins the KeepRestLocal-classified IK helper bones (aim_matrix_*, hold_*,\r\n    /// *_IK_attach and unrecognized IkBaked names) to their REST locals in every frame.\r\n    /// Used on MIRRORED clips (southpaw G8 mirror fix): these bones are aim-space\r\n    /// reference CONSTANTS the model\u0027s aim/eye constraint setup was authored against\r\n    /// (measured at rest in every shipped citizen clip); conjugating them like body\r\n    /// geometry feeds the aim chain an alien reference frame and the head/eye region\r\n    /// collapses. The body-derived helpers (root_IK, IK targets, ikrule) are NOT\r\n    /// touched: their mirrored channels stay the exact conjugates of the primary\u0027s.\r\n    /// \u003C/summary\u003E\r\n    public static void PinRestLocalReferenceBones(List\u003CXForm[]\u003E frames, TargetRig target)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(frames);\r\n        ArgumentNullException.ThrowIfNull(target);\r\n\r\n        var skeleton = target.Skeleton;\r\n        var rules = BuildRules(target);\r\n        foreach (var locals in frames)\r\n        {\r\n            for (var i = 0; i \u003C locals.Length \u0026\u0026 i \u003C skeleton.Count; i\u002B\u002B)\r\n            {\r\n                if (target.ClassOf(i) == BoneClass.IkBaked \u0026\u0026 rules[i].Kind == RuleKind.KeepRestLocal)\r\n                    locals[i] = skeleton[i].RestLocal;\r\n            }\r\n        }\r\n    }\r\n\r\n    // ------------------------------------------------------------------ rules\r\n\r\n    private enum RuleKind\r\n    {\r\n        /// \u003Csummary\u003EKeep the rest local transform (hold_*, aim_matrix_*, *_IK_attach, unknown).\u003C/summary\u003E\r\n        KeepRestLocal,\r\n\r\n        /// \u003Csummary\u003EWorld transform copies a body bone\u0027s world transform (IK targets, ikrule).\u003C/summary\u003E\r\n        FollowBody,\r\n\r\n        /// \u003Csummary\u003Eroot_IK: pelvis world position projected to the ground plane, fixed rest rotation.\u003C/summary\u003E\r\n        GroundProjectedHips,\r\n    }\r\n\r\n    private readonly struct Rule\r\n    {\r\n        public RuleKind Kind { get; init; }\r\n        public int BodyBone { get; init; }\r\n        public int UpAxis { get; init; }\r\n        public float GroundLevel { get; init; }\r\n        public Quaternion FixedWorldRot { get; init; }\r\n    }\r\n\r\n    private static Rule[] BuildRules(TargetRig target)\r\n    {\r\n        var skeleton = target.Skeleton;\r\n        var rules = new Rule[skeleton.Count];\r\n\r\n        foreach (var i in target.BonesOfClass(BoneClass.IkBaked))\r\n        {\r\n            rules[i] = skeleton[i].Name switch\r\n            {\r\n                \u0022root_IK\u0022 =\u003E RootIkRule(target, i),\r\n                \u0022foot_L_IK_target\u0022 =\u003E FollowRule(target, BoneRole.FootL),\r\n                \u0022foot_R_IK_target\u0022 =\u003E FollowRule(target, BoneRole.FootR),\r\n                \u0022hand_L_IK_target\u0022 =\u003E FollowRule(target, BoneRole.HandL),\r\n                \u0022hand_R_IK_target\u0022 =\u003E FollowRule(target, BoneRole.HandR),\r\n                \u0022hand_L_to_R_ikrule\u0022 =\u003E FollowRule(target, BoneRole.HandL),\r\n                \u0022hand_R_to_L_ikrule\u0022 =\u003E FollowRule(target, BoneRole.HandR),\r\n                _ =\u003E default, // KeepRestLocal\r\n            };\r\n        }\r\n\r\n        return rules;\r\n    }\r\n\r\n    private static Rule FollowRule(TargetRig target, BoneRole role)\r\n        =\u003E target.BoneForRole(role) is { } body\r\n            ? new Rule { Kind = RuleKind.FollowBody, BodyBone = body }\r\n            : default; // body bone missing from the rig: keep rest\r\n\r\n    private static Rule RootIkRule(TargetRig target, int rootIk)\r\n    {\r\n        if (target.BoneForRole(BoneRole.Hips) is not { } pelvis)\r\n            return default;\r\n\r\n        var skeleton = target.Skeleton;\r\n        // Up axis = dominant component of the rest offset pelvis \u2212 root_IK. In every probed\r\n        // rig they share the lateral components exactly and differ only by the hip height.\r\n        var d = Vector3.Abs(skeleton.RestWorld[pelvis].Pos - skeleton.RestWorld[rootIk].Pos);\r\n        var up = d.X \u003E= d.Y ? (d.X \u003E= d.Z ? 0 : 2) : (d.Y \u003E= d.Z ? 1 : 2);\r\n\r\n        return new Rule\r\n        {\r\n            Kind = RuleKind.GroundProjectedHips,\r\n            BodyBone = pelvis,\r\n            UpAxis = up,\r\n            GroundLevel = Component(skeleton.RestWorld[rootIk].Pos, up),\r\n            FixedWorldRot = skeleton.RestWorld[rootIk].Rot,\r\n        };\r\n    }\r\n\r\n    private static XForm Desired(in Rule rule, XForm[] world, bool[] computed, string name)\r\n    {\r\n        if (!computed[rule.BodyBone])\r\n            throw new InvalidOperationException(\r\n                $\u0022IK bone \u0027{name}\u0027 follows a body bone that is itself under an IK bone \u2014 unsupported hierarchy.\u0022);\r\n\r\n        return rule.Kind == RuleKind.FollowBody\r\n            ? world[rule.BodyBone]\r\n            : new XForm(\r\n                WithComponent(world[rule.BodyBone].Pos, rule.UpAxis, rule.GroundLevel),\r\n                rule.FixedWorldRot);\r\n    }\r\n\r\n    private static float Component(Vector3 v, int axis)\r\n        =\u003E axis switch { 0 =\u003E v.X, 1 =\u003E v.Y, _ =\u003E v.Z };\r\n\r\n    private static Vector3 WithComponent(Vector3 v, int axis, float value)\r\n    {\r\n        switch (axis)\r\n        {\r\n            case 0: v.X = value; break;\r\n            case 1: v.Y = value; break;\r\n            default: v.Z = value; break;\r\n        }\r\n        return v;\r\n    }\r\n}\r\n"},{"Ident":"notpointless.chomnr_text_to_animation","Path":"TextToAnimation/Rig/Pose.cs","FileName":"Pose.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":409724,"IsPrivate":false,"Code":"#nullable enable annotations\r\n\r\nusing System;\r\nusing TextToAnimation.Maths;\r\n\r\nnamespace TextToAnimation.Rig;\r\n\r\n/// \u003Csummary\u003E\r\n/// A single pose of a skeleton: one parent-relative local transform per bone,\r\n/// indexed exactly like the skeleton\u0027s bone list.\r\n/// \u003C/summary\u003E\r\npublic sealed class Pose\r\n{\r\n    /// \u003Csummary\u003EParent-relative local transforms, one per bone (skeleton bone order).\u003C/summary\u003E\r\n    public XForm[] Locals { get; }\r\n\r\n    /// \u003Csummary\u003EWraps an existing local-transform array (not copied).\u003C/summary\u003E\r\n    public Pose(XForm[] locals)\r\n    {\r\n        Locals = locals ?? throw new ArgumentNullException(nameof(locals));\r\n    }\r\n\r\n    /// \u003Csummary\u003ECreates a pose initialized to the skeleton\u0027s rest local transforms.\u003C/summary\u003E\r\n    public static Pose Rest(Skeleton skeleton)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(skeleton);\r\n        var locals = new XForm[skeleton.Count];\r\n        for (var i = 0; i \u003C locals.Length; i\u002B\u002B)\r\n            locals[i] = skeleton[i].RestLocal;\r\n        return new Pose(locals);\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Forward kinematics: composes local transforms down the hierarchy and returns\r\n    /// one world transform per bone.\r\n    /// \u003C/summary\u003E\r\n    /// \u003Cexception cref=\u0022ArgumentException\u0022\u003EThrown when the pose length does not match the skeleton.\u003C/exception\u003E\r\n    public XForm[] ToWorld(Skeleton skeleton)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(skeleton);\r\n        if (Locals.Length != skeleton.Count)\r\n            throw new ArgumentException(\r\n                $\u0022Pose has {Locals.Length} bones but skeleton has {skeleton.Count}.\u0022, nameof(skeleton));\r\n\r\n        var world = new XForm[Locals.Length];\r\n        for (var i = 0; i \u003C Locals.Length; i\u002B\u002B)\r\n        {\r\n            var parent = skeleton[i].ParentIndex;\r\n            world[i] = parent \u003C 0 ? Locals[i] : XForm.Compose(world[parent], Locals[i]);\r\n        }\r\n        return world;\r\n    }\r\n}\r\n"}]}