HumanoidRetargeter/Target/TargetRigGenerator.cs

Static generator that converts a research rig JSON into the s&box target-rig JSON format. It reads a rig via RigJson.Load, iterates bones in topological order, classifies bones, writes bone metadata (name, parent, class, optional role) and transforms (local pos/rot and head/tail world positions) into an indented JSON string.

File Access
using System;
using System.IO;
using System.Numerics;
using System.Text;
using System.Text.Json;
using HumanoidRetargeter.Skeleton;

namespace HumanoidRetargeter.Target;

using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)

/// <summary>
/// Generates the committed s&amp;box target-rig definition
/// (<c>Assets/humanoid_retargeter/target_rig_sbox.json</c>) from the research ground-truth
/// rig JSON (<c>research/rig_human_male.json</c>). Pure string → string; the generator test
/// owns reading/writing the files (regenerate-and-diff pattern).
/// </summary>
public static class TargetRigGenerator
{
    /// <summary>Rig name written into the default (5-finger human male) target rig.</summary>
    public const string HumanMaleName = "sbox_human_male";

    /// <summary>Description written into the default (5-finger human male) target rig.</summary>
    public const string HumanMaleDescription =
        "s&box humanoid target rig. Generated by TargetRigGenerator from research/rig_human_male.json - do not hand-edit.";

    /// <summary>Rig name written into the classic (4-finger) citizen target rig.</summary>
    public const string CitizenName = "sbox_citizen";

    /// <summary>Description written into the classic (4-finger) citizen target rig.</summary>
    public const string CitizenDescription =
        "s&box classic citizen target rig (4 fingers, no pinky). Generated by TargetRigGenerator from dev/HumanoidRetargeter.Tests/fixtures/rig_citizen.json - do not hand-edit.";

    /// <summary>
    /// Deterministically produces the target-rig JSON: every bone of the source rig in
    /// topological order with its rest transforms (centimeters), bone class, and — for
    /// <see cref="BoneClass.Animated"/> bones only — its canonical role. The defaults of
    /// <paramref name="name"/>/<paramref name="description"/> produce the shipped human male
    /// rig; the classic citizen rig passes <see cref="CitizenName"/>/<see cref="CitizenDescription"/>.
    /// </summary>
    public static string Generate(string rigJson,
        string name = HumanMaleName, string description = HumanMaleDescription)
    {
        ArgumentNullException.ThrowIfNull(rigJson);

        var (skeleton, geometry) = RigJson.Load(rigJson);

        using var buffer = new MemoryStream();
        using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions { Indented = true }))
        {
            writer.WriteStartObject();
            writer.WriteString("name", name);
            writer.WriteString("description", description);
            writer.WriteString("units", "cm");
            writer.WriteString("quaternion_order", "xyzw");

            writer.WriteStartArray("bones");
            foreach (var bone in skeleton.Bones)
            {
                writer.WriteStartObject();
                writer.WriteString("name", bone.Name);
                if (bone.ParentIndex < 0)
                    writer.WriteNull("parent");
                else
                    writer.WriteString("parent", skeleton[bone.ParentIndex].Name);

                var boneClass = SboxBoneClassifier.Classify(bone.Name);
                writer.WriteString("class", boneClass.ToString());

                if (boneClass == BoneClass.Animated)
                {
                    var role = SboxBoneClassifier.RoleFor(bone.Name)
                        ?? throw new InvalidOperationException(
                            $"Animated s&box bone '{bone.Name}' has no role assignment; extend SboxBoneClassifier.");
                    writer.WriteString("role", role.ToString());
                }

                WriteVector(writer, "local_pos", bone.RestLocal.Pos);
                WriteQuaternion(writer, "local_rot_xyzw", bone.RestLocal.Rot);

                var (head, tail) = geometry[bone.Name];
                WriteVector(writer, "head_world", head);
                WriteVector(writer, "tail_world", tail);

                writer.WriteEndObject();
            }
            writer.WriteEndArray();

            writer.WriteEndObject();
        }

        return Encoding.UTF8.GetString(buffer.ToArray()) + "\n"; // Environment.NewLine is not s&box-whitelisted
    }

    private static void WriteVector(Utf8JsonWriter writer, string name, Vector3 v)
    {
        writer.WriteStartArray(name);
        writer.WriteNumberValue(v.X);
        writer.WriteNumberValue(v.Y);
        writer.WriteNumberValue(v.Z);
        writer.WriteEndArray();
    }

    private static void WriteQuaternion(Utf8JsonWriter writer, string name, Quaternion q)
    {
        writer.WriteStartArray(name);
        writer.WriteNumberValue(q.X);
        writer.WriteNumberValue(q.Y);
        writer.WriteNumberValue(q.Z);
        writer.WriteNumberValue(q.W);
        writer.WriteEndArray();
    }
}