Editor/WeaponImporter/Core/Generation/DmxSpace.cs
#nullable enable annotations
using System.Numerics;
using WeaponImporter.Core.Formats.Dmx;
using WeaponImporter.Core.Maths;
using WeaponImporter.Core.Rig;
using WeaponImporter.Core.Setup;
namespace WeaponImporter.Core.Generation;
using Vector2 = System.Numerics.Vector2;
using Vector3 = System.Numerics.Vector3;
/// <summary>
/// Axis system and units a DMX is written in. Data inside the importer is s&box model space
/// (inches, +X forward, +Z up).
/// <list type="bullet">
/// <item><see cref="SourceZUpInches"/> (default): data written as-is, declared Z-up
/// (DmeAxisSystem upAxis 3, forwardParity 1, coordSys 0). Verified in the editor: ModelDoc
/// compiles it unchanged.</item>
/// <item>Y-up: ModelDoc rotates a Y-up DMX by quaternion (0.5, 0.5, 0.5, 0.5), i.e. file
/// (x, y, z) becomes engine (z, x, y). The writer applies the inverse, writing engine (x, y, z)
/// as (y, z, x), so the compiled result matches the Z-up path. Units are never converted by
/// the engine, so <see cref="UnitScale"/> changes the compiled size.</item>
/// </list>
/// Only root bones are re-oriented (bone-local frames of children are untouched), matching how
/// ModelDoc applies the axis conversion.
/// </summary>
public sealed record DmxSpace(bool UpAxisY, float UnitScale)
{
/// <summary>Z-up, inches, data as-is. The default for model and animation DMX.</summary>
public static DmxSpace SourceZUpInches { get; } = new(false, 1f);
/// <summary>Y-up (engine-rotated on import), inches.</summary>
public static DmxSpace YUpInches { get; } = new(true, 1f);
/// <summary>
/// Write texture V with a bottom-left origin and declare <c>flipVCoordinates 1</c>
/// (the Blender Source Tools convention). False writes TriMesh's top-left V with
/// <c>flipVCoordinates 0</c>.
/// </summary>
public bool UvBottomLeft { get; init; } = true;
/// <summary>Inverse of the engine's Y-up import rotation.</summary>
private static readonly Quaternion ToYUp = Quaternion.Conjugate(new Quaternion(0.5f, 0.5f, 0.5f, 0.5f));
public Vector3 Point(Vector3 p) => Direction(p) * UnitScale;
public Vector3 Direction(Vector3 v) => UpAxisY ? new Vector3(v.Y, v.Z, v.X) : v;
public Quaternion RootRotation(Quaternion q) => UpAxisY ? MathQ.Normalize(ToYUp * q) : q;
/// <summary>A bone local transform in file space (roots are re-oriented, all translations scaled).</summary>
public XForm Local(XForm local, bool root)
=> root ? new XForm(Point(local.Pos), RootRotation(local.Rot)) : new XForm(local.Pos * UnitScale, local.Rot);
public Vector2 Uv(Vector2 topLeft) => UvBottomLeft ? new Vector2(topLeft.X, 1f - topLeft.Y) : topLeft;
internal void WriteAxisSystem(Kv2Writer w)
{
w.Attr("upAxis", "string", UpAxisY ? "Y" : "Z");
w.BeginInline("axisSystem", "DmeAxisSystem");
w.Attr("upAxis", "int", UpAxisY ? "2" : "3");
w.Attr("forwardParity", "int", UpAxisY ? "2" : "1");
w.Attr("coordSys", "int", "0");
w.EndInline();
}
}
/// <summary>Engine-safe names shared by the model and animation writers.</summary>
public static class DmxNames
{
/// <summary>
/// DMX joint name per skeleton bone: <see cref="EngineNames.Bone"/> (strips '#suffix',
/// [A-Za-z0-9_]) made unique with _2, _3... in skeleton order. Both writers use this, so
/// model joints and animation channels always agree.
/// </summary>
public static string[] Bones(Skeleton skeleton)
{
var result = new string[skeleton.Count];
var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < skeleton.Count; i++)
{
var baseName = EngineNames.Bone(skeleton[i].Name);
if (baseName.Trim('_').Length == 0)
baseName = "bone";
result[i] = Unique(baseName, used);
}
return result;
}
/// <summary>Material name for a faceSet: lowercase [a-z0-9_], no dots, never empty.</summary>
public static string Material(string name)
{
var chars = (name ?? "").ToLowerInvariant().Select(c => (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ? c : '_').ToArray();
var s = new string(chars);
while (s.Contains("__"))
s = s.Replace("__", "_");
s = s.Trim('_');
if (s.Length == 0)
s = "material";
if (char.IsDigit(s[0]))
s = "m_" + s;
return s;
}
public static string Unique(string name, HashSet<string> used)
{
var candidate = name;
for (var n = 2; !used.Add(candidate); n++)
candidate = $"{name}_{n}";
return candidate;
}
}