Editor/UI/NativeRigExport.cs
using Editor;
using Sandbox;
using System.Threading.Tasks;
using Vec=System.Numerics.Vector3;
using Quat=System.Numerics.Quaternion;
namespace HumanoidRigger.Editor;
/// <summary>Compile and check the actual native bind pose. Long transform chains can
/// accumulate importer rounding; bounded feedback removes that drift from the DMX
/// without changing the canonical rig, mesh, weights or portable exports.</summary>
internal static class NativeRigExport
{
internal static async Task Compile(string dmxPath,string modelPath,ImportedCharacter character,GeneratedRig rig,
IReadOnlyDictionary<int,string> materials,Action<string> replaceOwnedDmx)
{
AssetSystem.RegisterFile(dmxPath);
var asset=AssetSystem.RegisterFile(modelPath)??throw new IOException("Could not register the generated model.");
var exported=rig;
for(int pass=0;pass<3;pass++)
{
string compiledPath=modelPath+"_c";var previous=File.GetLastWriteTimeUtc(compiledPath);
asset.Compile(true);Model model=null;
for(int attempt=0;attempt<120;attempt++)
{
await Task.Delay(250);await new EditorThread();
if(asset.IsCompileFailed)throw new IOException("s&box could not compile the generated model.");
if(!asset.IsCompiledAndUpToDate||!File.Exists(compiledPath)||(pass>0&&File.GetLastWriteTimeUtc(compiledPath)==previous))continue;
model=await Model.LoadAsync(asset.Path);await new EditorThread();break;
}
if(model is null)throw new TimeoutException("Timed out waiting for the generated model to compile.");
var corrected=Correct(character,rig,exported,model);
if(ReferenceEquals(corrected,exported))
{
// Recompilation swaps mesh resources on the engine frame. Let
// that swap finish before a caller creates its preview scene.
var frame=Sandbox.Application.FrameCount;var deadline=DateTime.UtcNow.AddSeconds(5);
while(Sandbox.Application.FrameCount<frame+2&&DateTime.UtcNow<deadline){await Task.Delay(16);await new EditorThread();}
if(Sandbox.Application.FrameCount<frame+2)throw new TimeoutException("The native model reload did not finish.");
return;
}
if(pass==2)break;
exported=corrected;
var text=await Task.Run(()=>DmxExporter.Write(character,exported,materials));await new EditorThread();
replaceOwnedDmx(text);
}
throw new IOException("The native importer could not preserve the generated bind pose.");
}
internal static GeneratedRig Correct(ImportedCharacter character,GeneratedRig intended,GeneratedRig exported,Model compiled)
{
if(compiled.IsError||compiled.BoneCount!=intended.Bones.Length)throw new IOException("The compiled rig lost skeleton bones.");
var indices=intended.Bones.Select(b=>compiled.Bones.GetBone(VmdlBoneNames.Convert(b.Name))?.Index??-1).ToArray();
if(indices.Any(i=>i<0))throw new IOException("The compiled rig lost a required bone.");
float maximum=0,limit=character.Height*.00001f;var corrections=new Vec[intended.Bones.Length];
Quat basis=new(.5f,.5f,.5f,.5f);
for(int i=0;i<intended.Bones.Length;i++)
{
var bone=intended.Bones[i];var bind=compiled.GetBoneTransform(indices[i]);
if(compiled.GetBoneParent(indices[i])!=(bone.Parent<0?-1:indices[bone.Parent]))throw new IOException("The native importer changed the bone hierarchy.");
var q=Quat.Normalize(basis*bone.Rotation);var expected=new Rotation(q.X,q.Y,q.Z,q.W);
foreach(var axis in new[]{Vector3.Forward,Vector3.Left,Vector3.Up})
{
float error=Vector3.DistanceBetween(bind.Rotation*axis,expected*axis);
if(!float.IsFinite(error)||error>.001f)throw new IOException("The native importer changed a bone orientation.");
}
corrections[i]=bone.Position-new Vec(bind.Position.y,bind.Position.z,bind.Position.x)*2.54f;
if(!Geometry.Finite(corrections[i]))throw new IOException("The native bind pose contains invalid coordinates.");
maximum=Math.Max(maximum,corrections[i].Length());
}
if(maximum<=limit)return exported;
// Only compensate rounding. A meaningful joint displacement is an export
// failure, never permission to move a user's landmarks or refit their rig.
var bones=exported.Bones.Select((b,i)=>b with{Position=b.Position+corrections[i]}).ToArray();
if(maximum>limit*10||bones.Where((b,i)=>Vec.Distance(b.Position,intended.Bones[i].Position)>limit*10).Any())
throw new IOException("The native importer moved a joint beyond the safe precision correction limit.");
return new GeneratedRig{Profile=intended.Profile,Bones=bones,Weights=intended.Weights,Anatomy=intended.Anatomy,Report=intended.Report};
}
}