s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (110 total matches found)
notpointless.chomnr_humanoid_rigger / Editor/UI/AssetOutput.cs
Editor library
using Editor;
using Sandbox;
using System.Threading.Tasks;
namespace HumanoidRigger.Editor;

public sealed record ExportResult(string[] Files)
{
    public string PrimaryFile=>Files.LastOrDefault(p=>p.EndsWith(".vmdl",StringComparison.OrdinalIgnoreCase))??Files.First();
}

public static class AssetOutput
{
    public static async Task<string> Save(Wizard session)=>
        (await Save(session,ExportRequest.Default(session.Character!.Name,Project.Current.GetAssetsPath(),session.Character.Materials.Length>0))).PrimaryFile;

    public static async Task<ExportResult> Save(Wizard session,ExportRequest request)
    {
        if(session.Rig?.Report.Passed!=true)throw new InvalidOperationException("The rig must pass validation before saving.");
        var character=session.Character!;var rig=session.Rig;
        if(request.Formats.HasFlag(ExportFormats.Vmdl))VmdlBoneNames.Validate(rig.Bones);
        var root=Project.Current.GetAssetsPath();var plan=request.Plan(root,character.Materials.Length>0);plan.EnsureAvailable();
        Directory.CreateDirectory(plan.Directory);
        var written=new List<string>();bool createdMaterials=false;
        void Write(string path,byte[] bytes)
        {
            using var file=new FileStream(path,FileMode.CreateNew,FileAccess.Write,FileShare.Read);written.Add(path);file.Write(bytes);
        }
        void WriteText(string path,string content)=>Write(path,System.Text.Encoding.UTF8.GetBytes(content));
        try
        {
            var sourceMaterials=character.Materials;
            IReadOnlyDictionary<int,string> vmats=new Dictionary<int,string>();
            if(plan.MaterialDirectory is not null)
            {
                Directory.CreateDirectory(plan.MaterialDirectory);createdMaterials=true;
                sourceMaterials=await Task.Run(()=>TextureFiles.Copy(character,plan.MaterialDirectory));await new EditorThread();
                sourceMaterials=await Task.Run(()=>MaterialAssets.PrepareFormats(sourceMaterials,plan.MaterialDirectory,plan.Formats));await new EditorThread();
                if(plan.Formats.HasFlag(ExportFormats.Vmdl))
                {
                    vmats=await MaterialAssets.Compile(sourceMaterials,plan.MaterialDirectory,character);
                }
            }
            var portableMaterials=plan.MaterialDirectory is null?sourceMaterials:TextureFiles.RelativeTo(sourceMaterials,Path.GetFileName(plan.MaterialDirectory));
            if(plan.Formats.HasFlag(ExportFormats.Fbx))
            {
                var bytes=await Task.Run(()=>FbxExporter.Write(character,rig,portableMaterials));await new EditorThread();
                Write(plan.PathFor(".fbx"),bytes);
                if(ExportRequest.IsInAssets(plan.Directory,root))AssetSystem.RegisterFile(plan.PathFor(".fbx"));
            }
            if(plan.Formats.HasFlag(ExportFormats.Gltf))
            {
                var output=await Task.Run(()=>GltfExporter.Write(character,rig,plan.FileName+".bin",portableMaterials));await new EditorThread();
                Write(plan.PathFor(".bin"),output.Buffer);Write(plan.PathFor(".gltf"),output.Document);
            }
            if(plan.Formats.HasFlag(ExportFormats.Glb))
            {
                var output=await Task.Run(()=>GltfExporter.Write(character,rig,"",portableMaterials,true,path=>File.ReadAllBytes(Path.Combine(plan.Directory,path))));await new EditorThread();
                Write(plan.PathFor(".glb"),output.Document);
            }
            if(plan.Formats.HasFlag(ExportFormats.Obj))
            {
                var output=await Task.Run(()=>ObjExporter.Write(character,plan.FileName+".mtl",portableMaterials));await new EditorThread();
                WriteText(plan.PathFor(".mtl"),output.Materials);WriteText(plan.PathFor(".obj"),output.Mesh);
            }
            if(plan.Formats.HasFlag(ExportFormats.Vmdl))
            {
                var dmx=await Task.Run(()=>DmxExporter.Write(character,rig,vmats));await new EditorThread();
                WriteText(plan.PathFor(".dmx"),dmx);
                WriteText(plan.PathFor(".vmdl"),ModelDocExporter.Write(Path.GetRelativePath(root,plan.PathFor(".dmx")),rig,character,vmats.Count>0));
                await NativeRigExport.Compile(plan.PathFor(".dmx"),plan.PathFor(".vmdl"),character,rig,vmats,
                    content=>File.WriteAllText(plan.PathFor(".dmx"),content));
            }
            return new(plan.PrimaryFiles);
        }
        catch
        {
            // Only remove output paths this attempt created. Existing user assets are never overwritten.
            foreach(var path in written)foreach(var owned in new[]{path,path+"_c"})try{File.Delete(owned);}catch(Exception e){Log.Warning(e.Message);}
            if(createdMaterials&&ExportRequest.IsInAssets(plan.MaterialDirectory,plan.Directory))
                try{Directory.Delete(plan.MaterialDirectory,true);}catch(Exception e){Log.Warning(e.Message);}
            throw;
        }
    }
}
notpointless.chomnr_humanoid_rigger / Editor/UI/RiggerWindow.cs
Editor library
using Editor;
using Sandbox;
using System.Threading.Tasks;
namespace HumanoidRigger.Editor;

public sealed class RiggerWindow : Widget
{
    public const string DockTitle="Humanoid Rigger";
    public static RiggerWindow Instance {get;private set;}
    public Wizard Session {get;}=new();
    RiggerViewport viewport;Widget content,toolbar,footer,toolbarContent,footerContent;Label status;bool busy;
    ImportedCharacter framedCharacter;WizardStep framedStep;
    JointPanel jointPanel;
    CharacterPose previewPose=CharacterPose.Auto;
    ComboBox poseSelection;
    bool customPose;
    bool updatingPoseSelection;
    IReadOnlyDictionary<string,System.Numerics.Quaternion> editedPose;
    AutomaticHandRefiner handRefiner;
    IReadOnlyDictionary<int,string> previewMaterials=new Dictionary<int,string>();
    internal double LastAdvanceWorkMilliseconds {get;private set;}
    internal double LastAdvanceUiMilliseconds {get;private set;}
    internal bool LastAdvanceWorkerWasMainThread {get;private set;}
    Task<Wizard> preparation;
    Wizard preparingDraft;
    long queuedPreparationRevision=-1;
    FingerCountDialog fingerCountDialog;
    public RiggerWindow(Widget parent):this(parent,true){}
    internal RiggerWindow(Widget parent,bool register):base(parent)
    {
        if(register)Instance=this;WindowTitle=DockTitle;Name="HumanoidRigger";Cursor=CursorShape.Arrow;SetWindowIcon("accessibility_new");MinimumSize=new(1060,780);Size=new(1280,940);Layout=Layout.Column();EnsureHandRefiner();Build();
    }
    [Event("tools.editorwindow.createview")]
    static void RegisterViewMenu(Menu menu)
    {
        // Join the editor's alphabetically sorted tools without creating a dock.
        EditorWindow.DockManager.RegisterDockType(new DockManager.DockInfo
        {
            Title=DockTitle,Icon="accessibility_new",CreateAction=OpenFloatingView
        });
    }
    static Widget OpenFloatingView(){Open();return null;}

    [Event("tools.editorwindow.postcreateview")]
    static void ConfigureViewMenu(Menu menu)
    {
        var option=menu.GetOption(DockTitle);
        if(option is null)return;
        option.Toggled=null;option.Checkable=false;option.Triggered=Open;
    }

    public static void Open()
    {
        if(Instance.IsValid()){ShowExistingWindow(Instance);return;}
        CreateFloatingWindow(DockTitle,true);
    }
    internal static void ShowExistingWindow(RiggerWindow window)
    {
        // Move sessions opened by older versions into the same themed window as new
        // sessions. Preserve the widget, viewport and edits when removing the old dock.
        var dock=EditorWindow.DockManager.FindDockWidget(window);
        if(dock.IsValid())
        {
            var previous=window.GetWindow();var size=previous.Size;var position=previous.Position;bool floating=dock.IsFloating;
            var dialog=CreateFloatingHost(window.WindowTitle);
            // Replacing the dock content releases its native ownership before reparenting.
            dock.Widget=new Widget(dock);
            window.Parent=dialog;dialog.Layout.Add(window,1);
            EditorWindow.DockManager.RemoveDock(dock);dock.Destroy();
            if(floating){dialog.Window.Size=size;dialog.Window.Position=position;}
            dialog.Show();window.Show();dialog.Window.Raise();
            return;
        }
        var existing=window.GetWindow();existing.Show();window.Show();existing.Raise();
    }
    internal static Dialog CreateFloatingWindow(string title,bool register)
    {
        var dialog=CreateFloatingHost(title);
        dialog.Layout.Add(new RiggerWindow(dialog,register),1);dialog.Show();dialog.Window.Size=new(1280,940);return dialog;
    }
    void Build()
    {
        if(Session.Rig is null){previewPose=CharacterPose.Auto;customPose=false;editedPose=null;}
        if(Session.Step==WizardStep.Import)
        {
            content?.Destroy();viewport=null;framedCharacter=null;
            content=Layout.Add(new Widget(this),1);content.Layout=Layout.Column();
            var drop=new ModelDropArea(content,ImportPath);content.Layout.Add(drop,1);status=content.Layout.Add(new Label(content){Visible=false,WordWrap=true});return;
        }
        if(!viewport.IsValid()||!toolbar.IsValid()||!footer.IsValid()||!jointPanel.IsValid())
        {
            if(viewport.IsValid())viewport.Parent=this;
            content?.Destroy();content=Layout.Add(new Widget(this),1);content.Layout=Layout.Column();
            toolbar=content.Layout.Add(new Widget(content));toolbar.Layout=Layout.Column();
            var middle=content.Layout.Add(new Widget(content),1);middle.Layout=Layout.Column();
            viewport=middle.Layout.Add(viewport.IsValid()?viewport:new RiggerViewport(middle,Session),1);
            // Overlay the panel so entering Body does not resize the native
            // render surface. The viewport frames the character beside it.
            jointPanel=new JointPanel(viewport,Session,viewport);viewport.Changed=LandmarksChanged;
            // Reserve the final review's space throughout the wizard. Changing
            // warnings or progress text must not repeatedly resize the swap chain.
            footer=content.Layout.Add(new Widget(content){FixedHeight=160});footer.Layout=Layout.Column();
        }
        viewport.PoseEdited=PoseEdited;
        viewport.Resized=PositionJointPanel;
        toolbarContent?.Destroy();footerContent?.Destroy();
        toolbarContent=toolbar.Layout.Add(new Widget(toolbar));toolbarContent.Layout=Layout.Row();
        footerContent=footer.Layout.Add(new Widget(footer));footerContent.Layout=Layout.Column();
        var top=toolbarContent.Layout;top.Margin=8;top.Spacing=8;
        top.Add(new Label(content){Text="Profile:"});
        var profile=top.Add(new ComboBox(content){MinimumWidth=190,ToolTip="The target skeleton’s bone names and hierarchy. Hand detection is independent of this choice."});
        foreach(var p in Profiles.BuiltIn.Concat(ProfileStore.Load()))profile.AddItem(p.Name,"person",()=>{Session.SetProfile(p);Build();},selected:p.Id==Session.Profile.Id);
        profile.AddItem("Create Custom Profile…","add",CreateProfile);
        profile.AddItem("Load Profile…","folder_open",LoadProfile);
        if(Session.Rig is not null)
        {
            top.Add(new Label(content){Text="Preview:"});
            poseSelection=top.Add(new ComboBox(content){MinimumWidth=120,ToolTip="Preview the generated rig in a standard pose."});
            foreach(var (label,value) in new[]{("Original Pose",CharacterPose.Auto),("T-Pose",CharacterPose.TPose),("A-Pose 1",CharacterPose.APose1),("A-Pose 2",CharacterPose.APose2)})
                poseSelection.AddItem(label,"accessibility",()=>SetPreviewPose(value),selected:!customPose&&value==previewPose);
            if(editedPose is not null)poseSelection.AddItem("Custom Pose","touch_app",ShowEditedPose,selected:customPose);
        }
        top.AddStretchCell();top.Add(new Button("Replace model","folder_open"){Clicked=SelectModel});top.Add(new Button("Restart","restart_alt"){Clicked=RestartWorkflow});
        viewport.MaterialPaths=previewMaterials;ShowPreview();
        jointPanel.Visible=Session.Step!=WizardStep.Centerline;
        PositionJointPanel();
        jointPanel.Reload();
        if(framedCharacter!=Session.Character||framedStep!=Session.Step){viewport.Frame();framedCharacter=Session.Character;framedStep=Session.Step;}
        var bottom=footerContent.Layout;bottom.Margin=12;bottom.Spacing=8;
        status=bottom.Add(new Label(content){WordWrap=true,Text=Instruction()});
        if(Session.Step is WizardStep.Centerline or WizardStep.Body)
        {
            var importWarnings=Session.Character.ImportWarnings.Where(w=>!w.StartsWith("Detected a Z-up")).ToArray();
            if(importWarnings.Length>0)
            {
                var warning=bottom.Add(new Label(content){Name="ImportMaterialWarning",Text=string.Join("\n",importWarnings),WordWrap=true});
                warning.SetStyles($"color: {Theme.Yellow.Hex};");
            }
        }
        if(Session.Step is WizardStep.Centerline or WizardStep.Body && Session.Anatomy!.UnrecommendedImportPose)
        {
            var warning=bottom.Add(new Label(content){Name="ImportPoseWarning",Text=ImportPose.Warning,WordWrap=true});
            warning.SetStyles($"color: {Theme.Yellow.Hex};");
        }
        if(Session.Step is WizardStep.LeftHand or WizardStep.RightHand)
        {
            var side=Session.Step==WizardStep.LeftHand?"L":"R";
            foreach(var warning in Session.Anatomy!.Warnings.Where(w=>w.StartsWith(side+" hand")||w.StartsWith(side+" fingers")))
            {var label=bottom.Add(new Label(content){Text=warning,WordWrap=true});label.SetStyles($"color: {Theme.Yellow.Hex};");}
        }
        if(Session.Step==WizardStep.Finish)
        {
            bottom.Add(new Label(content){Text=$"Profile: {Session.Profile.Name}"});
            var deformationWarnings=Session.Rig!.Report.Issues.Where(i=>i.Code=="surface-reversal").Select(i=>i.Message).ToArray();
            if(deformationWarnings.Length>0)
            {
                var warning=bottom.Add(new Label(content){Name="DeformationWarning",Text="Some test poses need review. Open Advanced Edit for details.",WordWrap=true});
                warning.SetStyles($"color: {Theme.Yellow.Hex};");
            }
            var checks=bottom.AddRow();checks.Spacing=8;
            foreach(var label in new[]{"Body","Left Hand","Right Hand","Skeleton","Skinning","Validation"})
            {
                string side=label=="Left Hand"?"L":label=="Right Hand"?"R":null;
                var warnings=label=="Validation"?deformationWarnings:side is null?[]:Session.Anatomy!.Warnings.Where(w=>w.StartsWith(side+" hand")||w.StartsWith(side+" fingers")).ToArray();
                checks.Add(new StatusChip(content,label,warnings.Length>0?Theme.Yellow:Theme.Green){ToolTip=warnings.Length>0?string.Join("\n",warnings):"Checked"});
            }
            checks.AddStretchCell();
            var row=bottom.AddRow();row.Spacing=8;
            row.Add(new Button("Back","arrow_back"){Clicked=GoBack});
            row.Add(new Button("Reset Pose","restart_alt"){Clicked=()=>SetPreviewPose(CharacterPose.Auto)});
            row.Add(new Button("Test Rig","play_arrow"){Clicked=TestRig});row.Add(new Button("Advanced Edit","tune"){Clicked=Advanced});row.AddStretchCell();row.Add(new Button.Primary("Save"){Icon="check",Tint=Theme.Green,Clicked=Save});
        }
        else
        {
            var row=bottom.AddRow();row.Spacing=8;
            if(Session.Step!=WizardStep.Centerline)row.Add(new Button("Back","arrow_back"){Clicked=GoBack});
            row.Add(new Button("Reset","restart_alt"){Clicked=()=>{Session.Reset();Build();}});row.AddStretchCell();
            row.Add(new Button.Primary("Continue"){Enabled=Session.Step!=WizardStep.Validation,Clicked=Continue});
        }
        SchedulePreparation();
    }
    void PositionJointPanel()
    {
        if(!viewport.IsValid()||!jointPanel.IsValid())return;
        jointPanel.Position=new(viewport.Width-jointPanel.FixedWidth,0);
        jointPanel.Size=new(jointPanel.FixedWidth,viewport.Height);
        viewport.RightInset=jointPanel.Visible?jointPanel.FixedWidth:0;
        jointPanel.Raise();
    }
    void GoBack(){Session.Back();Build();}
    string Instruction()=>Session.Step switch
    {
        WizardStep.Centerline=>"Check the centerline.\nDrag the line left or right to adjust it.",
        WizardStep.Body=>"Check the points.\nMove any point that is incorrect.",WizardStep.LeftHand=>"Check the left hand.\nMove any incorrect points.",WizardStep.RightHand=>"Check the right hand.\nMove any incorrect points.",WizardStep.Finish=>"Rig Complete\nDrag a bone to test the rig.",
        WizardStep.Validation=>string.Join("\n",Session.Rig!.Report.Issues.Where(i=>i.Error).Select(i=>i.Message)),_=>"Generating rig…"
    };
    void SelectModel(){var path=EditorUtility.OpenFileDialog("Select model",ModelImporter.FileFilter,"");if(!string.IsNullOrEmpty(path))ImportPath(path);}
    void CreateProfile()
    {
        var path=EditorUtility.OpenFileDialog("Select rigged character","Rigged characters (*.fbx *.gltf *.glb)","");if(string.IsNullOrEmpty(path))return;
        try{new ProfileDialog(this,ModelImporter.Import(path),p=>{Session.SetProfile(p);Build();}).Show();}catch(Exception e){Error(e);}
    }
    void LoadProfile()
    {
        var path=EditorUtility.OpenFileDialog("Select rig profile","json","");if(string.IsNullOrEmpty(path))return;
        try{var p=RigProfile.FromJson(File.ReadAllText(path));ProfileStore.Save(p);Session.SetProfile(p);Build();}catch(Exception e){Error(e);}
    }
    public async void ImportPath(string path)
    {
        try{await ImportAsync(path);}catch(Exception e){await new EditorThread();if(this.IsValid())Error(e);}
    }
    public async Task ImportAsync(string path)
    {
        if(busy)throw new InvalidOperationException("The current operation is still running.");
        EnsureHandRefiner();
        SetBusy(true);
        try
        {
            await RigWork.Run(()=>Session.Import(path));await new EditorThread();
            if(!this.IsValid())return;
            previewMaterials=new Dictionary<int,string>();
            string textureWarning=null;
            try{previewMaterials=await MaterialAssets.Preview(Session.Character);}
            catch(Exception e){textureWarning="Textures: "+e.Message;Log.Warning(textureWarning);}
            await new EditorThread();if(this.IsValid()){Build();if(textureWarning is not null){status.Text+="\n"+textureWarning;status.SetStyles($"color: {Theme.Yellow.Hex};");}}
        }
        finally{await new EditorThread();SetBusy(false);}
    }
    void Continue()
    {
        if(busy)return;
        if(Session.Step==WizardStep.Body)
        {
            if(fingerCountDialog.IsValid()&&fingerCountDialog.Visible){fingerCountDialog.Window.Raise();return;}
            var revision=Session.Revision;
            fingerCountDialog=new FingerCountDialog(this,Session.LeftFingerCount,Session.RightFingerCount,(left,right)=>
            {
                if(!this.IsValid()||Session.Step!=WizardStep.Body||Session.Revision!=revision)return;
                Session.SetFingerCounts(left,right);AdvanceFromControls();
            });
            fingerCountDialog.Show();return;
        }
        AdvanceFromControls();
    }
    async void AdvanceFromControls()
    {
        try{await AdvanceAsync();}catch(Exception e){await new EditorThread();if(this.IsValid())Error(e);}
    }
    public async Task AdvanceAsync()
    {
        if(busy)throw new InvalidOperationException("The current operation is still running.");
        EnsureHandRefiner();
        SetBusy(true);
        status.Text=Session.Step switch{WizardStep.Body=>"Preparing left hand…",WizardStep.LeftHand=>"Preparing right hand…",WizardStep.RightHand=>"Generating rig…",_=>Instruction()};
        try
        {
            var started=System.Diagnostics.Stopwatch.GetTimestamp();
            if(preparation is not null&&!preparation.IsCompleted&&!Session.CanAccept(preparingDraft))
            {try{await preparation.ConfigureAwait(false);}catch{}await new EditorThread();}
            Wizard result;
            if(Session.Step==WizardStep.RightHand)
            {
                // Give the busy state a frame to appear, then generate on the
                // editor thread. Keep a draft so a failure preserves the edits.
                await Task.Delay(16).ConfigureAwait(false);await new EditorThread();
                if(!this.IsValid())return;
                result=Session.CopyForContinuation();
                LastAdvanceWorkerWasMainThread=ThreadSafe.IsMainThread;
                result.Continue();
            }
            else
            {
                if(preparation is null||!Session.CanAccept(preparingDraft)||preparation.IsFaulted)StartPreparation();
                result=await preparation.ConfigureAwait(false);await new EditorThread();
            }
            Session.AcceptContinuation(result);
            LastAdvanceWorkMilliseconds=System.Diagnostics.Stopwatch.GetElapsedTime(started).TotalMilliseconds;
            started=System.Diagnostics.Stopwatch.GetTimestamp();if(this.IsValid())Build();
            LastAdvanceUiMilliseconds=System.Diagnostics.Stopwatch.GetElapsedTime(started).TotalMilliseconds;
        }
        finally{await new EditorThread();SetBusy(false);SchedulePreparation();}
    }
    public void CaptureViewport(string path)=>viewport.Capture(path);
    internal void RefreshDisplay()
    {
        // Rebind native event handlers after editor hot reload without replacing the session.
        viewport?.Destroy();viewport=null;framedCharacter=null;MinimumSize=new(1060,780);Build();
    }
    void ShowPreview()
    {
        if(Session.Rig is null){viewport.ShowCharacter();return;}
        viewport.SetPose(customPose?editedPose:RigPosePreview.Rotations(Session.Rig,previewPose),false);
    }
    public void SetPreviewPose(CharacterPose pose)
    {
        if(updatingPoseSelection)return;
        if(busy)throw new InvalidOperationException("The current operation is still running.");
        if(Session.Rig is null)throw new InvalidOperationException("Generate the rig before previewing poses.");
        if(pose is not (CharacterPose.Auto or CharacterPose.TPose or CharacterPose.APose1 or CharacterPose.APose2))throw new ArgumentException("Unsupported preview pose.");
        previewPose=pose;customPose=false;viewport.SetPose(RigPosePreview.Rotations(Session.Rig,pose));
        var label=pose switch{CharacterPose.Auto=>"Original Pose",CharacterPose.TPose=>"T-Pose",CharacterPose.APose1=>"A-Pose 1",_=>"A-Pose 2"};
        SelectPoseLabel(label);
        status.Text=Instruction();
    }
    void PoseEdited(IReadOnlyDictionary<string,System.Numerics.Quaternion> pose)
    {
        customPose=true;editedPose=new Dictionary<string,System.Numerics.Quaternion>(pose);
        SelectPoseLabel("Custom Pose");
    }
    void SelectPoseLabel(string label)
    {
        if(!poseSelection.IsValid())return;
        // Native ComboBox selection invokes its action even for programmatic
        // changes. Updating the label must not restart or cancel an active drag.
        updatingPoseSelection=true;
        try
        {
            if(poseSelection.FindIndex(label) is {} index)poseSelection.CurrentIndex=index;
            else if(label=="Custom Pose")poseSelection.AddItem(label,"touch_app",ShowEditedPose,selected:true);
        }
        finally{updatingPoseSelection=false;}
    }
    void ShowEditedPose(){if(updatingPoseSelection||editedPose is null||Session.Rig is null)return;customPose=true;viewport.SetPose(editedPose);}
    public void RestartWorkflow()
    {
        if(busy)throw new InvalidOperationException("The current operation is still running.");
        Session.Restart();Build();
    }
    void Error(Exception e){status.Visible=true;status.Text=e.Message;status.SetStyles($"color: {Theme.Red.Hex};");}
    void SetBusy(bool value)
    {
        busy=value;if(!this.IsValid())return;
        if(viewport.IsValid())
        {
            viewport.AllowEditing=!value;
            if(toolbar.IsValid())toolbar.Enabled=!value;if(footer.IsValid())footer.Enabled=!value;if(jointPanel.IsValid())jointPanel.Enabled=!value;
        }
        else if(content.IsValid())content.Enabled=!value;
    }
    async void TestRig()
    {
        if(busy)return;SetBusy(true);
        try
        {
            foreach(var pose in Deformation.Poses)
            {
                await new EditorThread();if(!this.IsValid()||Session.Rig is null)break;
                if(!Deformation.IsApplicable(pose,Session.Rig.Bones.Select(b=>b.Role).ToHashSet()))continue;
                viewport.SetPose(Deformation.JointRotations(Session.Rig,pose));status.Text=pose.Name;await Task.Delay(650);
            }
            await new EditorThread();if(this.IsValid()){viewport.SetPose(customPose?editedPose:RigPosePreview.Rotations(Session.Rig,previewPose));status.Text=Instruction();}
        }
        catch(Exception e){await new EditorThread();if(this.IsValid())Error(e);}finally{await new EditorThread();SetBusy(false);}
    }
    void Advanced()
    {
        var dialog=new Dialog(this);dialog.Window.WindowTitle="Rig details";dialog.Window.MinimumSize=new(560,480);dialog.Layout=Layout.Column();dialog.Layout.Margin=12;dialog.Layout.Spacing=8;
        var scroll=new ScrollArea(dialog);scroll.Canvas=new Widget(scroll);scroll.Canvas.Layout=Layout.Column();
        foreach(var b in Session.Rig!.Bones)scroll.Canvas.Layout.Add(new Label(scroll.Canvas){Text=$"{b.Name}  ·  {b.Role}  ·  {b.Position}"});
        foreach(var issue in Session.Rig.Report.Issues)scroll.Canvas.Layout.Add(new Label(scroll.Canvas){Text=issue.Message});
        foreach(var hand in Session.Anatomy!.HandRefinements)scroll.Canvas.Layout.Add(new Label(scroll.Canvas){Text=$"{(hand.Key=="L"?"Left":"Right")} hand: {hand.Value.Status}"});
        dialog.Layout.Add(scroll,1);dialog.Show();
    }
    void Save()
    {
        if(busy)return;
        new SaveRigDialog(this,Session,result=>{if(this.IsValid()){status.Visible=true;status.Text="Saved "+string.Join(" + ",result.Files.Select(Path.GetFileName));status.ToolTip=string.Join("\n",result.Files);status.SetStyles($"color: {Theme.Green.Hex};");}}).Show();
    }
    void EnsureHandRefiner(){if(handRefiner is null){handRefiner=new AutomaticHandRefiner();handRefiner.Warmup();}Session.HandRefiner=handRefiner;}
    public override void OnDestroyed(){handRefiner?.Dispose();if(Instance==this)Instance=null;base.OnDestroyed();}
    static Dialog CreateFloatingHost(string title)
    {
        var dialog=new Dialog(null);dialog.Window.Title=title;dialog.Window.SetWindowIcon("accessibility_new");
        dialog.Layout=Layout.Column();dialog.Window.Size=new(1280,940);return dialog;
    }
    void LandmarksChanged(){jointPanel.RefreshRows();SchedulePreparation();}
    void StartPreparation()
    {
        preparingDraft=Session.CopyForContinuation();var draft=preparingDraft;
        preparation=RigWork.Run(()=>{LastAdvanceWorkerWasMainThread=ThreadSafe.IsMainThread;draft.Continue();return draft;});
        _=preparation.ContinueWith(task=>{_ = task.Exception;},TaskContinuationOptions.OnlyOnFaulted);
    }
    void SchedulePreparation()
    {
        // Preparing hands is cheap; generating a full rig while its final hand
        // is still being edited wastes both memory and a complete repair pass.
        if(!this.IsValid()||Session.Step is not (WizardStep.Body or WizardStep.LeftHand)||queuedPreparationRevision==Session.Revision)return;
        queuedPreparationRevision=Session.Revision;_=PrepareWhenIdle(Session.Revision);
    }
    async Task PrepareWhenIdle(long revision)
    {
        // Coalesce marker drags. At most one solver job per window may run at a time.
        await Task.Delay(150).ConfigureAwait(false);await new EditorThread();
        if(!this.IsValid()||busy||Session.Revision!=revision)return;
        if(preparation is not null&&!preparation.IsCompleted)
        {try{await preparation.ConfigureAwait(false);}catch{}await new EditorThread();}
        if(!this.IsValid()||busy||Session.Revision!=revision)return;
        if(preparingDraft is null||!Session.CanAccept(preparingDraft))StartPreparation();
    }
}

sealed class ModelDropArea:Widget
{
    readonly Action<string> import;int hover;
    public ModelDropArea(Widget parent,Action<string> import):base(parent)
    {
        this.import=import;AcceptDrops=true;Layout=Layout.Column();Layout.Margin=12;Layout.Spacing=8;Layout.AddStretchCell();
        var row=Layout.AddRow();row.AddStretchCell();var center=row.AddColumn();center.Spacing=12;
        center.Add(new DropFolderIcon(this));
        center.Add(new Label(this){Text="Please drag and drop a character file here (.fbx, .obj, .gltf, .glb)",Alignment=TextFlag.Center});
        center.Add(new Label(this){Text="or",Alignment=TextFlag.Center});
        var choice=center.AddRow();choice.AddStretchCell();
        choice.Add(new Button.Primary("Choose File"){MinimumWidth=120,Clicked=()=>{var path=EditorUtility.OpenFileDialog("Choose File",ModelImporter.FileFilter,"");if(!string.IsNullOrEmpty(path))import(path);}});
        choice.AddStretchCell();
        row.AddStretchCell();Layout.AddStretchCell();
    }
    public override void OnDragHover(DragEvent e)
    {
        bool valid=e.Data.HasFileOrFolder&&ModelImporter.CanImport(e.Data.FileOrFolder);hover=valid?1:-1;if(valid)e.Action=DropAction.Link;Update();
    }
    public override void OnDragDrop(DragEvent e){hover=0;if(e.Data.HasFileOrFolder&&ModelImporter.CanImport(e.Data.FileOrFolder)){e.Action=DropAction.Link;import(e.Data.FileOrFolder);}Update();}
    public override void OnDragLeave(){hover=0;Update();}
    protected override void OnPaint(){Paint.SetPen(hover==1?Theme.Green:hover<0?Theme.Red:Theme.ControlBackground.Lighten(.2f),1);Paint.SetBrush(hover==1?Theme.Green.WithAlpha(.06f):Paint.HasMouseOver?Theme.ControlBackground.Lighten(.3f):Theme.ControlBackground);Paint.DrawRect(LocalRect.Shrink(12),4);}
}

sealed class DropFolderIcon : Widget
{
    public DropFolderIcon(Widget parent):base(parent){FixedHeight=48;}
    protected override void OnPaint()
    {
        Paint.SetPen(Theme.TextLight);
        Paint.DrawIcon(new Rect((Width-40)*.5f,4,40,40),"create_new_folder",40);
    }
}
notpointless.chomnr_humanoid_rigger / Editor/Core/Analysis/BodyProportions.cs
Editor library
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;

/// <summary>A low, narrow neck beneath an enlarged head provides a body-scale
/// prior independent of total height. Ambiguous sections retain the original prior.</summary>
internal static class BodyProportions
{
    readonly record struct Section(float Y,float Width,float Depth)
    {
        public float Area=>Width*Depth;
    }
    public static float EstimateBodyHeight(IEnumerable<MeshPart> source,float bottom,float height)
    {
        var meshes=source.ToArray();var sections=new List<Section>();
        // Intersect triangle edges rather than sampling vertices: dense faces,
        // sparse neck rings and material seams should give the same cross-section.
        for(int sample=0;sample<=128;sample++)
        {
            float y=bottom+height*(.5f+sample*.0035f);
            float minX=float.PositiveInfinity,maxX=float.NegativeInfinity,minZ=float.PositiveInfinity,maxZ=float.NegativeInfinity;
            int count=0;
            foreach(var mesh in meshes)for(int t=0;t<mesh.Triangles.Length;t+=3)for(int edge=0;edge<3;edge++)
            {
                var a=mesh.Vertices[mesh.Triangles[t+edge]];var b=mesh.Vertices[mesh.Triangles[t+(edge+1)%3]];
                if(!((a.Y<=y&&b.Y>y)||(b.Y<=y&&a.Y>y)))continue;
                var p=Vector3.Lerp(a,b,(y-a.Y)/(b.Y-a.Y));
                minX=Math.Min(minX,p.X);maxX=Math.Max(maxX,p.X);minZ=Math.Min(minZ,p.Z);maxZ=Math.Max(maxZ,p.Z);count++;
            }
            if(count>=4&&maxX-minX>height*.005f&&maxZ-minZ>height*.005f)sections.Add(new(y,maxX-minX,maxZ-minZ));
        }
        var candidates=sections.Where(s=>s.Y<bottom+height*.9f).ToArray();
        if(candidates.Length==0)return height;
        var narrowest=candidates.MinBy(s=>s.Area);
        if(narrowest.Y>=bottom+height*.8f)return height;
        int index=sections.IndexOf(narrowest),first=index,last=index;
        bool SameNeck(Section a,Section b)=>b.Area<=narrowest.Area*2.5f&&Math.Abs(a.Y-b.Y)<=height*.0071f;
        while(first>0&&SameNeck(sections[first],sections[first-1]))first--;
        while(last+1<sections.Count&&SameNeck(sections[last],sections[last+1]))last++;
        if(last-first<2)return height;
        float neckY=(sections[first].Y+sections[last].Y)*.5f;
        // An unusually narrow waist is not the neck if another bottleneck
        // separates the shoulders and skull farther up the same silhouette.
        foreach(var later in sections.Where(s=>s.Y>neckY+height*.1f&&s.Y<bottom+height*.9f))
        {
            bool Expanded(Section s)=>s.Width>later.Width*1.35f&&s.Depth>later.Depth*1.1f;
            if(sections.Any(s=>s.Y<later.Y-height*.02f&&s.Y>later.Y-height*.07f&&Expanded(s))&&
                sections.Any(s=>s.Y>later.Y+height*.02f&&s.Y<later.Y+height*.07f&&Expanded(s)))return height;
        }
        // Require expansion in both transverse dimensions above the neck. An arm
        // silhouette or a narrow waist alone is not sufficient evidence of a head.
        if(!sections.Any(s=>s.Y>Math.Max(neckY+height*.025f,bottom+height*.82f)&&s.Width>narrowest.Width*2.2f&&s.Depth>narrowest.Depth*1.5f))return height;
        return Math.Min(height,(neckY-bottom)/.85f);
    }
}
notpointless.chomnr_humanoid_rigger / Editor/Core/Analysis/HipFitting.cs
Editor library
#nullable enable annotations
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;

/// <summary>Refine a high hip from the thigh centerline and its junction with
/// the pelvis. Ambiguous branches and large changes retain the original prior.</summary>
internal static class HipFitting
{
    public static void RaiseLowHips(Anatomy anatomy,IReadOnlyList<MeshSections.Section> sections,float height,SurfaceVisibility volume)
    {
        var old=new[]{anatomy.Points["UpperLeg.L"],anatomy.Points["UpperLeg.R"]};
        if(old.Any(p=>p.Corrected))return;
        var candidates=new Vector3[2];float center=anatomy.SymmetryPlaneX;
        for(int i=0;i<2;i++)
        {
            float sign=i==0?1:-1;var hip=old[i].Position;
            var leg=sections.Where(s=>(s.Center.X-center)*sign>height*.02f&&Math.Abs(s.Center.X-hip.X)<height*.06f&&
                s.Center.Y>hip.Y-height*.07f&&s.Center.Y<hip.Y+height*.15f&&s.Radius<height*.095f).ToArray();
            var top=leg.MaxBy(s=>s.Center.Y);
            if(top is null||top.Center.Y<hip.Y+height*.015f||top.MinimumRadius<height*.012f)return;
            // Two separate thigh contours must actually join a central pelvis.
            if(!sections.Any(s=>Math.Abs(s.Center.X-center)<height*.015f&&s.Center.Y>top.Center.Y&&s.Center.Y<top.Center.Y+height*.02f&&s.Area>top.Area*1.5f))return;
            // A torso contour already overlapping the thigh is a separate shell,
            // not evidence of a groin transition. Keep its anatomical prior.
            if(sections.Any(s=>Math.Abs(s.Center.X-center)<height*.015f&&s.Center.Y<top.Center.Y&&s.Center.Y>top.Center.Y-height*.03f&&s.Area>top.Area*1.5f))return;
            var shaft=leg.Where(s=>s.Center.Y<top.Center.Y-height*.015f&&s.Center.Y>top.Center.Y-height*.06f).ToArray();if(shaft.Length<4)return;
            // A narrowing end cap belongs to a detached leg segment. Extending
            // its radius would place the socket beyond its authored articulation.
            if(top.MinimumRadius<shaft.Average(s=>s.MinimumRadius)*.85f)return;
            var mean=Geometry.Mean(shaft.Select(s=>s.Center));float variance=shaft.Sum(s=>(s.Center.Y-mean.Y)*(s.Center.Y-mean.Y));if(variance<height*height*1e-8f)return;
            var slope=shaft.Aggregate(Vector3.Zero,(sum,s)=>sum+(s.Center-mean)*(s.Center.Y-mean.Y))/variance;
            var candidate=mean+slope*(top.Center.Y+top.MinimumRadius-mean.Y);
            if(Vector3.Distance(candidate,hip)>height*.14f||!volume.Contains(candidate,height*.00001f))return;
            candidates[i]=candidate;
        }
        if(Math.Abs(candidates[0].Y-candidates[1].Y)>Math.Abs(old[0].Position.Y-old[1].Position.Y)+height*.01f)return;
        var pelvis=anatomy.Points["Pelvis"];float lift=(candidates[0].Y+candidates[1].Y-old[0].Position.Y-old[1].Position.Y)*.5f;
        var moved=pelvis.Position+Vector3.UnitY*lift;
        if(!pelvis.Corrected&&volume.Contains(moved,height*.00001f))anatomy.Points["Pelvis"]=pelvis with{Position=moved};
        for(int i=0;i<2;i++)anatomy.Points[old[i].Role]=old[i] with{Position=candidates[i]};
    }
    public static void Refine(Anatomy anatomy,IReadOnlyList<MeshSections.Section> sections,float height,SurfaceVisibility volume)
    {
        float center=anatomy.SymmetryPlaneX;
        var original=new[]{anatomy.Points["UpperLeg.L"],anatomy.Points["UpperLeg.R"]};
        var candidates=original.Select(p=>p.Position).ToArray();
        for(int index=0;index<2;index++)
        {
            string side=index==0?"L":"R";float sign=index==0?1:-1;
            var old=original[index];if(old.Corrected)continue;
            var hip=old.Position;var knee=anatomy["LowerLeg."+side];
            var leg=sections.Where(s=>s.Center.Y>knee.Y+(hip.Y-knee.Y)*.35f&&s.Center.Y<hip.Y+height*.01f&&
                (s.Center.X-center)*sign>height*.02f&&Math.Abs(s.Center.X-hip.X)<height*.07f&&s.Radius<height*.1f).ToArray();
            var top=leg.MaxBy(s=>s.Center.Y);
            if(top is null||top.Center.Y>hip.Y-height*.025f||top.MinimumRadius<height*.012f)continue;
            // The final contour is distorted by the groin. Fit the shaft below
            // that transition, rather than extending the pinched contour center.
            var shaft=leg.Where(s=>s.Center.Y<top.Center.Y-height*.02f&&s.Center.Y>top.Center.Y-height*.08f).ToArray();
            if(shaft.Length<4)continue;
            var mean=Geometry.Mean(shaft.Select(s=>s.Center));
            float variance=shaft.Sum(s=>MathF.Pow(s.Center.Y-mean.Y,2));if(variance<height*height*1e-8f)continue;
            var slope=shaft.Aggregate(Vector3.Zero,(value,s)=>value+(s.Center-mean)*(s.Center.Y-mean.Y))/variance;
            // A local inscribed radius locates the socket above the last
            // separated leg section, independently of total body proportions.
            float y=top.Center.Y+top.MinimumRadius;var candidate=mean+slope*(y-mean.Y);
            if(!Geometry.Finite(candidate)||candidate.Y>=hip.Y||(candidate.X-center)*sign<height*.01f||
                Vector3.Distance(candidate,hip)>Vector3.Distance(hip,knee)*.2f)continue;
            if(!Enumerable.Range(0,21).All(i=>volume.Contains(Vector3.Lerp(knee,candidate,i/20f),height*.00001f)))continue;
            candidates[index]=candidate;
        }
        // An isolated contour estimate cannot justify tilting the pelvis. Keep
        // existing asymmetry, but reject a new height mismatch larger than the
        // section sampling interval when the opposite socket lacks support.
        if(Math.Abs(candidates[0].Y-candidates[1].Y)>Math.Abs(original[0].Position.Y-original[1].Position.Y)+height*.005f)return;
        for(int i=0;i<2;i++)anatomy.Points[original[i].Role]=original[i] with{Position=candidates[i]};
    }
}
notpointless.chomnr_humanoid_rigger / Editor/Core/Export/VmdlBoneNames.cs
Editor library
namespace HumanoidRigger;

/// <summary>ModelDoc replaces namespace colons with underscores when importing bones.</summary>
public static class VmdlBoneNames
{
    public static string Convert(string name)=>name.Replace(':','_');

    public static void Validate(IEnumerable<RigBone> bones)
    {
        var names=new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);
        foreach(var bone in bones)
        {
            string converted=Convert(bone.Name);
            if(names.TryGetValue(converted,out var previous))
                throw new InvalidOperationException($"Bone names '{previous}' and '{bone.Name}' both become '{converted}' in s&box. Rename one bone in the profile.");
            names.Add(converted,bone.Name);
        }
    }
}
notpointless.chomnr_humanoid_rigger / Editor/Core/Analysis/Inference/HandModelAssets.cs
Editor library
using System.IO.Compression;
using System.Net.Http;
using System.Security.Cryptography;
using System.Threading.Tasks;
namespace HumanoidRigger;

/// <summary>Pinned, hash-checked model/runtime cache. Downloads never contain user model data.</summary>
public static class HandModelAssets
{
    public const string ModelHash="db0898ae717b76b075d9bf563af315b29562e11f8df5027a1ef07b02bef6d81c";
    public const string RuntimeHash="dec964ab1ee36cc9b0ae247d13b376627992fc57dec0454354017ab8fd84f1ea";
    public static string CacheDirectory=>Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"sbox-humanoid-rigger","inference","hand-v1");
    public static async Task<NativeHandModel> Load(string folder)
    {
        Directory.CreateDirectory(folder);
        string model=Path.Combine(folder,"hand.onnx"),runtime=Path.Combine(folder,"onnxruntime.dll");
        using var http=new HttpClient{Timeout=TimeSpan.FromSeconds(90)};
        await Download(http,"https://media.githubusercontent.com/media/opencv/opencv_zoo/25f423d0e04c31a17254620e58febd7386da523b/models/handpose_estimation_mediapipe/handpose_estimation_mediapipe_2023feb.onnx",model,ModelHash);
        if(!Valid(runtime,RuntimeHash))
        {
            string archive=Path.Combine(folder,"onnxruntime-1.23.2.zip");
            await Download(http,"https://github.com/microsoft/onnxruntime/releases/download/v1.23.2/onnxruntime-win-x64-1.23.2.zip",archive,"0b38df9af21834e41e73d602d90db5cb06dbd1ca618948b8f1d66d607ac9f3cd");
            using var zip=ZipFile.OpenRead(archive);
            foreach(var name in new[]{"lib/onnxruntime.dll","LICENSE","ThirdPartyNotices.txt"})
            {
                var entry=zip.GetEntry("onnxruntime-win-x64-1.23.2/"+name)??throw new InvalidDataException("Missing runtime asset.");
                string destination=Path.Combine(folder,Path.GetFileName(name)),temporary=destination+"."+Guid.NewGuid().ToString("N")+".tmp";
                try{entry.ExtractToFile(temporary);if(name.EndsWith(".dll")&&!Valid(temporary,RuntimeHash))throw new InvalidDataException("Runtime checksum mismatch.");File.Move(temporary,destination,true);}
                finally{if(File.Exists(temporary))File.Delete(temporary);}
            }
        }
        if(!Valid(runtime,RuntimeHash)||!Valid(model,ModelHash))throw new InvalidDataException("Hand inference asset checksum mismatch.");
        return new NativeHandModel(runtime,model);
    }
    public static bool Valid(string path,string hash)
    {
        if(!File.Exists(path))return false;
        using var stream=File.OpenRead(path);return Convert.ToHexString(SHA256.HashData(stream)).Equals(hash,StringComparison.OrdinalIgnoreCase);
    }
    static async Task Download(HttpClient http,string url,string path,string hash)
    {
        if(Valid(path,hash))return;
        string temporary=path+"."+Guid.NewGuid().ToString("N")+".tmp";
        try
        {
            using var response=await http.GetAsync(url,HttpCompletionOption.ResponseHeadersRead);response.EnsureSuccessStatusCode();
            using(var destination=File.Create(temporary))await response.Content.CopyToAsync(destination);
            if(!Valid(temporary,hash))throw new InvalidDataException("Hand inference download checksum mismatch.");
            File.Move(temporary,path,true);
        }
        finally{if(File.Exists(temporary))File.Delete(temporary);}
    }
}
notpointless.chomnr_humanoid_rigger / Editor/Core/Import/Fbx/FbxNode.cs
Editor library
#nullable enable annotations

namespace HumanoidRigger.Formats.Fbx;

/// <summary>
/// A single node of an FBX document tree (binary or ASCII): a name, a flat list of
/// typed properties, and nested child nodes.
///
/// Property values are stored as the closest CLR type to what the file contained:
/// <list type="bullet">
///   <item><c>short</c> ('Y'), <c>bool</c> ('C'), <c>int</c> ('I'), <c>float</c> ('F'),
///         <c>double</c> ('D'), <c>long</c> ('L')</item>
///   <item><c>float[]</c> ('f'), <c>double[]</c> ('d'), <c>long[]</c> ('l'),
///         <c>int[]</c> ('i'), <c>bool[]</c>-as-<c>byte[]</c> ('b')</item>
///   <item><c>string</c> ('S' — kept raw, may contain the <c>\x00\x01</c> name/class
///         separator; see <see cref="SplitName"/>), <c>byte[]</c> ('R')</item>
/// </list>
/// ASCII files store numbers only as <c>long</c> / <c>double</c> (and arrays as
/// <c>long[]</c> / <c>double[]</c>), so the typed accessors below convert tolerantly.
/// </summary>
public sealed class FbxNode
{
    public string Name { get; }
    public List<object> Properties { get; } = new();
    public List<FbxNode> Children { get; } = new();

    public FbxNode(string name) => Name = name;

    /// <summary>First child with the given name, or null.</summary>
    public FbxNode? Child(string name)
    {
        foreach (var c in Children)
            if (c.Name == name)
                return c;
        return null;
    }

    /// <summary>All children with the given name, in document order.</summary>
    public IEnumerable<FbxNode> ChildrenNamed(string name)
    {
        foreach (var c in Children)
            if (c.Name == name)
                yield return c;
    }

    /// <summary>
    /// Property <paramref name="i"/> converted to <typeparamref name="T"/>.
    /// Numeric scalars convert tolerantly across widths (e.g. an 'I' i32 read as long);
    /// anything else must match the stored type exactly.
    /// </summary>
    public T Prop<T>(int i)
    {
        object v = RawProp(i);
        if (v is T t)
            return t;

        var target = typeof(T);
        // s&box whitelist: Type.IsPrimitive is banned; enumerate the convertible targets.
        if (v is IConvertible && (ConvertTargets.Contains(target) || target == typeof(string)))
        {
            try
            {
                return (T)Convert.ChangeType(v, target, System.Globalization.CultureInfo.InvariantCulture);
            }
            // ArithmeticException covers OverflowException, which is not s&box-whitelisted
            catch (Exception ex) when (ex is InvalidCastException or ArithmeticException or FormatException)
            {
                throw new FormatException(
                    $"FBX node '{Name}': property {i} is {v.GetType().Name}, not convertible to {target.Name}.", ex);
            }
        }

        throw new FormatException(
            $"FBX node '{Name}': property {i} is {v.GetType().Name}, expected {target.Name}.");
    }

    /// <summary>Property <paramref name="i"/> as a double array (converts f/l/i/b arrays).</summary>
    public double[] AsDoubleArray(int i) => RawProp(i) switch
    {
        double[] d => d,
        float[] f => Array.ConvertAll(f, x => (double)x),
        long[] l => Array.ConvertAll(l, x => (double)x),
        int[] n => Array.ConvertAll(n, x => (double)x),
        byte[] b => Array.ConvertAll(b, x => (double)x),
        bool[] o => Array.ConvertAll(o, x => x ? 1.0 : 0.0),
        var v => throw TypeError(i, v, "double[]"),
    };

    /// <summary>Property <paramref name="i"/> as a float array (converts d/l/i/b arrays).</summary>
    public float[] AsFloatArray(int i) => RawProp(i) switch
    {
        float[] f => f,
        double[] d => Array.ConvertAll(d, x => (float)x),
        long[] l => Array.ConvertAll(l, x => (float)x),
        int[] n => Array.ConvertAll(n, x => (float)x),
        byte[] b => Array.ConvertAll(b, x => (float)x),
        bool[] o => Array.ConvertAll(o, x => x ? 1f : 0f),
        var v => throw TypeError(i, v, "float[]"),
    };

    /// <summary>Property <paramref name="i"/> as a long array (converts i/b; d/f if integral).</summary>
    public long[] AsLongArray(int i) => RawProp(i) switch
    {
        long[] l => l,
        int[] n => Array.ConvertAll(n, x => (long)x),
        byte[] b => Array.ConvertAll(b, x => (long)x),
        bool[] o => Array.ConvertAll(o, x => x ? 1L : 0L),
        double[] d => Array.ConvertAll(d, x => checked((long)x)),
        float[] f => Array.ConvertAll(f, x => checked((long)x)),
        var v => throw TypeError(i, v, "long[]"),
    };

    /// <summary>Property <paramref name="i"/> as an int array (converts b; l/d/f narrowing-checked).</summary>
    public int[] AsIntArray(int i) => RawProp(i) switch
    {
        int[] n => n,
        long[] l => Array.ConvertAll(l, x => checked((int)x)),
        byte[] b => Array.ConvertAll(b, x => (int)x),
        bool[] o => Array.ConvertAll(o, x => x ? 1 : 0),
        double[] d => Array.ConvertAll(d, x => checked((int)x)),
        float[] f => Array.ConvertAll(f, x => checked((int)x)),
        var v => throw TypeError(i, v, "int[]"),
    };

    /// <summary>Property <paramref name="i"/> as raw bytes ('R' blobs or 'b' bool arrays).</summary>
    public byte[] AsByteArray(int i) => RawProp(i) switch
    {
        byte[] b => b,
        var v => throw TypeError(i, v, "byte[]"),
    };

    /// <summary>Property <paramref name="i"/> as a string (raw 'S' content, separators intact).</summary>
    public string AsString(int i) => RawProp(i) switch
    {
        string s => s,
        var v => throw TypeError(i, v, "string"),
    };

    /// <summary>
    /// Splits an FBX object name into (name, class).
    /// Binary files store <c>"Name\x00\x01Class"</c> (e.g. <c>"mixamorig:Hips\x00\x01Model"</c>);
    /// ASCII files store <c>"Class::Name"</c> (e.g. <c>"Model::pelvis"</c>).
    /// A plain string with neither separator yields (name, "").
    /// </summary>
    public static (string Name, string Class) SplitName(string raw)
    {
        int bin = raw.IndexOf("\0\x01", StringComparison.Ordinal);
        if (bin >= 0)
            return (raw[..bin], raw[(bin + 2)..]);

        int ascii = raw.IndexOf("::", StringComparison.Ordinal);
        if (ascii >= 0)
            return (raw[(ascii + 2)..], raw[..ascii]);

        return (raw, "");
    }

    /// <summary>Primitive scalar types <see cref="Prop{T}"/> converts to (whitelist-safe IsPrimitive substitute).</summary>
    private static readonly HashSet<Type> ConvertTargets = new()
    {
        typeof(bool), typeof(byte), typeof(sbyte), typeof(short), typeof(ushort),
        typeof(int), typeof(uint), typeof(long), typeof(ulong),
        typeof(float), typeof(double), typeof(char),
    };

    private object RawProp(int i)
    {
        if (i < 0 || i >= Properties.Count)
            throw new FormatException(
                $"FBX node '{Name}': property index {i} out of range (has {Properties.Count}).");
        return Properties[i];
    }

    private FormatException TypeError(int i, object v, string wanted) =>
        new($"FBX node '{Name}': property {i} is {v.GetType().Name}, expected {wanted}.");
}
notpointless.chomnr_humanoid_rigger / Editor/Core/Validation/JointWeightRepair.cs
Editor library
#nullable enable annotations
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;

/// <summary>Regularize a limb's blend with its parent using the measured joint
/// thickness. Trials retain connectivity and are accepted after complete stress
/// testing and local repair; reviewed bones and source geometry never move.</summary>
internal static class JointWeightRepair
{
    internal static GeneratedRig Improve(ImportedCharacter character,GeneratedRig rig,ValidationGeometry? geometry=null)
    {
        var roles=rig.Bones.Select(b=>b.Role).ToHashSet();
        var specifications=(geometry?.Poses??Deformation.Poses).Where(p=>Deformation.IsApplicable(p,roles)).ToArray();
        var expected=specifications.Select(p=>p.Name).Order().ToArray();
        if(!WeightRepair.HasCompleteEvidence(rig.Report,expected)||rig.Report.StressTests.All(p=>p.ReversedTriangles==0))return rig;
        geometry??=new ValidationGeometry(character);var faces=geometry.Faces;
        var buffer=character.Meshes.Select(m=>new Vector3[m.Vertices.Length]).ToArray();
        float height=character.AnatomicalHeight;
        for(int pass=0;pass<4;pass++)
        {
            int before=rig.Report.StressTests.Sum(p=>p.ReversedTriangles);
            foreach(var scheduled in rig.Report.StressTests.Where(p=>p.ReversedTriangles>0)
                .OrderByDescending(p=>p.MaximumStretch>4||p.MinimumAreaRatio<.025f).ThenByDescending(p=>p.ReversedAreaFraction).ToArray())
            {
                var stress=rig.Report.StressTests.Single(p=>p.Pose==scheduled.Pose);
                if(stress.ReversedTriangles==0)continue;
                var specification=specifications.Single(p=>p.Name==stress.Pose);
                if(geometry.Poses.Count==Deformation.Poses.Count&&!new[]{"UpperArm.","LowerArm.","UpperLeg.","LowerLeg."}.Any(specification.Role.StartsWith))continue;
                int joint=Array.FindIndex(rig.Bones,b=>b.Role==specification.Role);var bone=rig.Bones[joint];
                var child=rig.Bones.FirstOrDefault(b=>b.Parent==joint&&b.Deform);
                if(child is null||bone.Parent<0||!rig.Bones[bone.Parent].Deform||Vector3.DistanceSquared(child.Position,bone.Position)<1e-8f)continue;
                var axis=Vector3.Normalize(child.Position-bone.Position);var moving=new bool[rig.Bones.Length];moving[joint]=true;
                for(int i=joint+1;i<moving.Length;i++)moving[i]=rig.Bones[i].Parent>=0&&moving[rig.Bones[i].Parent];
                float sum=0;int count=0;
                foreach(var mesh in character.Meshes)foreach(var p in mesh.Vertices)
                {
                    var delta=p-bone.Position;float along=Vector3.Dot(delta,axis),radial=(delta-axis*along).Length();
                    if(Math.Abs(along)<height*.006f&&radial<height*.07f){sum+=radial;count++;}
                }
                float radius=Math.Clamp(count>0?sum/count:height*.025f,height*.01f,height*.065f);
                var totals=rig.Weights.Select(part=>part.Select(weights=>MovingTotal(weights,moving)).ToArray()).ToArray();
                var axial=character.Meshes.Select(mesh=>mesh.Vertices.Select(point=>Vector3.Dot(point-bone.Position,axis)).ToArray()).ToArray();
                var trials=new List<(GeneratedRig Rig,StressResult Stress)>();
                var blends=stress.ReversedTriangles<16?new[]{-.025f,-.05f,-.1f,-.2f,-.35f,.025f,.05f,.1f,.2f,.35f}:new[]{.5f,1f};
                foreach(float width in new[]{2f,3f,4f,6f})foreach(float blend in blends)
                {
                    var weights=character.Meshes.Select((mesh,part)=>mesh.Vertices.Select((point,vertex)=>
                        Blend(rig.Weights[part][vertex],rig.Profile.MaximumInfluences,rig.Bones.Length,moving,bone.Parent,
                            totals[part][vertex],axial[part][vertex]/(2*width*radius),blend)).ToArray()).ToArray();
                    var candidate=new GeneratedRig{Profile=rig.Profile,Bones=rig.Bones,Weights=weights,Anatomy=rig.Anatomy};
                    var result=RigValidator.MeasurePose(character,candidate,specification,faces,buffer,height);
                    if(result.ReversedTriangles>=stress.ReversedTriangles||result.ReversedAreaFraction>stress.ReversedAreaFraction)continue;
                    trials.Add((candidate,result));
                    // Only the best three trials are tested below. Release other
                    // dense weight buffers immediately, preserving stable tie order.
                    if(trials.Count>3)trials=trials.OrderBy(t=>t.Stress.ReversedTriangles).ThenBy(t=>t.Stress.ReversedAreaFraction).Take(3).ToList();
                }
                // A promising broad correction can expose a local seam. Run the
                // normal cleanup and repair before deciding whether it is better.
                foreach(var trial in trials.OrderBy(t=>t.Stress.ReversedTriangles).ThenBy(t=>t.Stress.ReversedAreaFraction).Take(3))
                {
                    var report=RigValidator.ValidateAndRepair(character,trial.Rig,geometry);
                    if(!SkeletonSolver.BetterSkinning(report,rig.Report,expected))continue;
                    report.Repairs+=rig.Report.Repairs;report.RepairPasses+=rig.Report.RepairPasses+1;
                    trial.Rig.Report=report;rig=trial.Rig;break;
                }
            }
            if(before==rig.Report.StressTests.Sum(p=>p.ReversedTriangles))break;
        }
        return rig;
    }
    static float MovingTotal(Influence[] weights,bool[] moving)
    {
        double total=0;foreach(var w in weights)if(moving[w.Bone])total+=w.Weight;
        return(float)total;
    }
    static Influence[] Blend(Influence[] source,int maximum,int boneCount,bool[] moving,int parent,float total,float axial,float amount)
    {
        if(total<.0001f)return source;
        float t=Math.Clamp(.5f+axial,0,1),envelope=t*t*(3-2*t);
        if(amount<0)
        {
            if(total>.9999f)return source;
            float target=total+(-amount)*total*(1-total)*(1-envelope);
            var scaled=new Influence[source.Length];
            for(int i=0;i<source.Length;i++){var w=source[i];scaled[i]=w with{Weight=w.Weight*(moving[w.Bone]?target/total:(1-target)/(1-total))};}
            return Skinning.Cleanup(scaled,boneCount,maximum);
        }
        float retained=1-amount+envelope*amount;
        var blended=new Influence[source.Length+1];
        for(int i=0;i<source.Length;i++){var w=source[i];blended[i]=w with{Weight=w.Weight*(moving[w.Bone]?retained:1)};}
        blended[^1]=new(parent,total*(1-retained));
        return Skinning.Cleanup(blended,boneCount,maximum);
    }
}
notpointless.chomnr_humanoid_rigger / Editor/Core/Validation/JointCoverage.cs
Editor library
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;

/// <summary>Check every deforming joint in both directions on each axis.
/// Only faces reached by that joint's skin weights need geometric measurement.</summary>
internal static class JointCoverage
{
    internal static (StressPose Pose,StressResult Result)[] Measure(ImportedCharacter character,GeneratedRig rig,ValidationGeometry geometry)
    {
        var result=new List<(StressPose,StressResult)>();
        var buffer=character.Meshes.Select(m=>new Vector3[m.Vertices.Length]).ToArray();
        for(int joint=0;joint<rig.Bones.Length;joint++)
        {
            if(!rig.Bones[joint].Deform)continue;
            var moving=new bool[rig.Bones.Length];moving[joint]=true;
            for(int b=joint+1;b<moving.Length;b++)moving[b]=rig.Bones[b].Parent>=0&&moving[rig.Bones[b].Parent];
            var touched=rig.Weights.Select(p=>p.Select(w=>w.Any(i=>moving[i.Bone])).ToArray()).ToArray();
            var faces=geometry.Faces.Select((p,m)=>p.Where(f=>touched[m][f.A]||touched[m][f.B]||touched[m][f.C]).ToArray()).ToArray();
            foreach(var (axis,name) in new[]{(Vector3.UnitX,"X"),(Vector3.UnitY,"Y"),(Vector3.UnitZ,"Z")})foreach(float degrees in new[]{-30f,30f})
            {
                var pose=new StressPose($"Joint {rig.Bones[joint].Role} {name} {degrees:+0;-0}",rig.Bones[joint].Role,axis,degrees);
                result.Add((pose,RigValidator.MeasurePose(character,rig,pose,faces,buffer,geometry.Height)));
            }
        }
        return result.ToArray();
    }
    static bool Bad(StressResult result)=>result.ReversedTriangles>0||result.NonFiniteVertices>0||result.NonFiniteMeasurements>0||result.MaximumStretch>4||result.MinimumAreaRatio<.025f;
    static double Score(IEnumerable<StressResult> results)=>results.Sum(r=>r.ReversedTriangles+1000000d*(r.NonFiniteVertices+r.NonFiniteMeasurements)+100*Math.Max(0,r.MaximumStretch/4-1)+100*Math.Max(0,1-r.MinimumAreaRatio/.025f));
    internal static GeneratedRig Improve(ImportedCharacter character,GeneratedRig rig)
    {
        if(!rig.Report.Passed)return rig;
        var geometry=new ValidationGeometry(character);var checks=Measure(character,rig,geometry);
        if(checks.Any(c=>Bad(c.Result)))
        {
            var repaired=PoseWeightRepair.Improve(character,rig,geometry,checks);
            if(!ReferenceEquals(repaired,rig)){rig=repaired;checks=Measure(character,rig,geometry);}
        }
        var trunk=new TrunkRegion(character,rig);
        var normalHeat=new Lazy<Influence[][][]>(()=>HeatSkinning.Candidates(character,rig,normalPrior:true,trunk:trunk).First());
        var heat=new Lazy<Influence[][][]>(()=>HeatSkinning.Solve(character,rig));
        var constraints=new Dictionary<string,StressPose>();
        for(int pass=0;pass<4&&checks.Any(c=>Bad(c.Result));pass++)
        {
            var failed=checks.Where(c=>Bad(c.Result)).OrderByDescending(c=>c.Result.ReversedTriangles).Take(24).Select(c=>c.Pose).ToArray();
            // Retain previously discovered failures after they are repaired.
            // Otherwise a spine correction can undo the adjacent chest repair.
            foreach(var pose in failed)constraints.TryAdd(pose.Name,pose);
            var scope=new ValidationGeometry(character,Deformation.Poses.Concat(constraints.Values).ToArray(),trunk);
            var candidate=new GeneratedRig{Profile=rig.Profile,Bones=rig.Bones,Anatomy=rig.Anatomy,Weights=rig.Weights.Select(p=>(Influence[][])p.Clone()).ToArray()};
            candidate.Report=RigValidator.ValidateAndRepair(character,candidate,scope);
            candidate=JointWeightRepair.Improve(character,candidate,scope);
            if(candidate.Report.StressTests.Any(p=>p.ReversedTriangles>0))
                RefineSources(character,candidate,scope,failed,normalHeat,heat);
            if(!candidate.Report.Passed||trunk.HasBleeding(character,candidate))break;
            var standard=RigValidator.Validate(character,candidate);
            if(!standard.Passed||standard.StressTests.Zip(rig.Report.StressTests).Any(p=>p.First.ReversedTriangles>p.Second.ReversedTriangles||p.First.ReversedAreaFraction>p.Second.ReversedAreaFraction+1e-7f))break;
            var next=Measure(character,candidate,geometry);
            bool discovered=false;
            foreach(var check in next.Where(c=>Bad(c.Result)))discovered|=constraints.TryAdd(check.Pose.Name,check.Pose);
            if(Score(next.Select(c=>c.Result))>=Score(checks.Select(c=>c.Result)))
            {if(discovered)continue;break;}
            standard.Repairs=rig.Report.Repairs+candidate.Report.Repairs;standard.RepairPasses=rig.Report.RepairPasses+candidate.Report.RepairPasses+1;
            candidate.Report=standard;rig=candidate;checks=next;
        }
        rig.Report.JointStressTests.AddRange(checks.Select(c=>c.Result));
        var remaining=checks.Where(c=>Bad(c.Result)).ToArray();
        if(remaining.Length>0)rig.Report.Issues.Add(new("joint-deformation",$"{remaining.Length} joint motions still have unsafe deformation: {string.Join(", ",remaining.Select(c=>c.Pose.Role).Distinct())}.",true));
        return rig;
    }
    static void RefineSources(ImportedCharacter character,GeneratedRig rig,ValidationGeometry geometry,StressPose[] failed,params Lazy<Influence[][][]>[] fields)
    {
        var roots=failed.Select(p=>p.Role).ToHashSet();var moving=new bool[rig.Bones.Length];
        for(int b=0;b<moving.Length;b++)moving[b]=roots.Contains(rig.Bones[b].Role)||rig.Bones[b].Parent>=0&&moving[rig.Bones[b].Parent];
        foreach(var field in fields)
        {
            Influence[][][] source;
            try{source=field.Value;}catch(InvalidOperationException){continue;}
            foreach(float amount in new[]{1f,.5f,.25f})
            {
                var proposed=rig.Weights.Select(p=>(Influence[][])p.Clone()).ToArray();
                for(int p=0;p<proposed.Length;p++)for(int v=0;v<proposed[p].Length;v++)
                {
                    float support=Math.Min(1,rig.Weights[p][v].Where(w=>moving[w.Bone]).Sum(w=>w.Weight)+source[p][v].Where(w=>moving[w.Bone]).Sum(w=>w.Weight))*amount;
                    if(support<.001f)continue;
                    var weights=Skinning.Cleanup(rig.Weights[p][v].Select(w=>w with{Weight=w.Weight*(1-support)})
                        .Concat(source[p][v].Select(w=>w with{Weight=w.Weight*support})),rig.Bones.Length,rig.Profile.MaximumInfluences);
                    if(geometry.Trunk?.Allows(p,v,character.Meshes[p].Vertices[v],weights)==false)continue;
                    proposed[p][v]=weights;
                }
                rig.Report=SurfaceRepair.TryWeights(character,rig,rig.Report,proposed,geometry);
                rig.Report=SurfaceRepair.Improve(character,rig,rig.Report,geometry);
                if(rig.Report.StressTests.All(p=>p.ReversedTriangles==0))return;
            }
        }
    }
}
notpointless.chomnr_humanoid_rigger / Editor/Core/Usings.cs
Editor library
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.IO;
notpointless.chomnr_humanoid_rigger / Editor/UI/MaterialAssets.Sidecars.cs
Editor library
// Copied from humanoid-retargeter Editor/HumanoidRetargeter/EditorPipeline.cs.
using Editor;
using Sandbox;
namespace HumanoidRigger.Editor;
internal static partial class MaterialAssets
{
	static readonly string[] TextureExtensions =
		{ ".png", ".jpg", ".jpeg", ".tga", ".dds", ".webp", ".vmat", ".vtex" };

	/// <summary>Copies texture sidecars of a picked target model into the output folder:
	/// loose image files next to it, and a "textures" folder next to it or next to its
	/// parent (the source/-plus-textures/ layout). Per-file best effort - a failed texture
	/// must never fail the conversion.</summary>
	static void CopySidecarTextures( string sourceDir, string destDir )
	{
		try
		{
			if ( sourceDir is null || destDir is null )
				return;
			sourceDir = Path.GetFullPath( sourceDir );
			destDir = Path.GetFullPath( destDir );
			if ( string.Equals( sourceDir, destDir, StringComparison.OrdinalIgnoreCase ) )
				return;

			// Every copied file must be REGISTERED: assets copied onto disk mid-session are
			// unknown to the asset system, so the material chain cannot generate their vtex
			// resources - the renderer then logs "Texture manager doesn't know about
			// texture ...generated.vtex" MANY TIMES PER FRAME, which is both the
			// purple/black flicker and a preview running at ~2 fps (user report).
			foreach ( var file in Directory.GetFiles( sourceDir ) )
			{
				if ( !TextureExtensions.Contains( Path.GetExtension( file ).ToLowerInvariant() ) )
					continue;
				var destFile = Path.Combine( destDir, Path.GetFileName( file ) );
				Try( () => { File.Copy( file, destFile, true ); return true; } );
				Try( () => AssetSystem.RegisterFile( destFile ) );
			}

			foreach ( var candidate in new[]
			{
				Path.Combine( sourceDir, "textures" ),
				Path.Combine( Path.GetDirectoryName( sourceDir ) ?? sourceDir, "textures" ),
			} )
			{
				if ( !Directory.Exists( candidate ) )
					continue;
				var destTextures = Path.Combine( destDir, "textures" );
				Directory.CreateDirectory( destTextures );
				foreach ( var file in Directory.GetFiles( candidate, "*", SearchOption.AllDirectories ) )
				{
					var relative = Path.GetRelativePath( candidate, file );
					var destFile = Path.Combine( destTextures, relative );
					Try( () =>
					{
						Directory.CreateDirectory( Path.GetDirectoryName( destFile ) );
						File.Copy( file, destFile, true );
						return true;
					} );
					Try( () => AssetSystem.RegisterFile( destFile ) );
				}
				break; // first existing candidate wins
			}
		}
		catch ( Exception e )
		{
			Log.Warning( $"[sbox-humanoid-rigger] sidecar texture copy failed: {e.Message}" );
		}
	}


static T Try<T>(Func<T> action){try{return action();}catch(Exception e){Log.Warning(e.Message);return default;}}
}
notpointless.chomnr_humanoid_rigger / Editor/UI/StatusChip.cs
Editor library
using Editor;
using Sandbox;

namespace HumanoidRigger.Editor;

/// <summary>Copied from Humanoid Retargeter's RetargetWindow.Chip.
/// Keep its dimensions, typography, fill and radius aligned with the suite.</summary>
sealed class StatusChip : Widget
{
    readonly string text;
    readonly Color color;

    public StatusChip(Widget parent,string text,Color color) : base(parent)
    {
        this.text=text;
        this.color=color;
        FixedHeight=20;
        FixedWidth=7.2f*text.Length+18;
    }

    protected override void OnPaint()
    {
        Paint.ClearPen();
        Paint.SetBrush(color.WithAlpha(.18f));
        Paint.DrawRect(LocalRect,LocalRect.Height*.5f);
        Paint.SetPen(color);
        Paint.SetDefaultFont(7,600);
        Paint.DrawText(LocalRect,text);
    }
}
notpointless.chomnr_humanoid_rigger / Editor/Core/Analysis/HumanoidFacing.cs
Editor library
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;

/// <summary>Resolve up from body geometry, then an unambiguous quarter-turn
/// from separated lower legs. Feet select the forward sign.</summary>
internal static class HumanoidFacing
{
    public static ImportedCharacter Normalize(ImportedCharacter character)
    {
        var up=HumanoidUp.Find(character);
        if(up!=Vector3.UnitY)
        {
            Vector3 Upright(Vector3 p)=>up==Vector3.UnitX?new(-p.Y,p.X,p.Z)
                :up==-Vector3.UnitX?new(p.Y,-p.X,p.Z)
                :up==-Vector3.UnitY?new(p.X,-p.Y,-p.Z)
                :up==Vector3.UnitZ?new(p.X,p.Z,-p.Y):new(p.X,-p.Z,p.Y);
            character=Reorient(character,Upright,"The character's up direction was corrected from its body geometry.");
        }
        var body=character.Meshes.Where(m=>m.Kind==MeshKind.Body).SelectMany(m=>m.Vertices).ToArray();
        if(body.Length<100)return character;
        float bottom=body.Min(p=>p.Y),height=body.Max(p=>p.Y)-bottom;
        var legs=body.Where(p=>p.Y>bottom+height*.15f&&p.Y<bottom+height*.35f).Distinct().ToArray();
        if(legs.Length<20)return character;
        var center=Geometry.Mean(legs);
        float xVariance=legs.Average(p=>(p.X-center.X)*(p.X-center.X));
        float zVariance=legs.Average(p=>(p.Z-center.Z)*(p.Z-center.Z));
        if(zVariance<xVariance*3)return character;
        float middle=(BodyDetector.Quantile(legs.Select(p=>p.Z),.1f)+BodyDetector.Quantile(legs.Select(p=>p.Z),.9f))*.5f;
        if(legs.Count(p=>Math.Abs(p.Z-middle)<height*.012f)>legs.Length*.15f)return character;
        var feet=body.Where(p=>p.Y<bottom+height*.07f).ToArray();
        if(feet.Length<8)return character;
        float forward=(BodyDetector.Quantile(feet.Select(p=>p.X),.1f)+BodyDetector.Quantile(feet.Select(p=>p.X),.9f))*.5f-center.X;
        forward=SeparatedFeetForward(character,bottom,height,middle)??forward;
        if(Math.Abs(forward)<height*.012f)return character;
        float sign=forward<0?1:-1;
        Vector3 Turn(Vector3 p)=>new(sign*p.Z,p.Y,-sign*p.X);
        return Reorient(character,Turn,"The character was turned to face forward.");
    }
    static float? SeparatedFeetForward(ImportedCharacter character,float bottom,float height,float middle)
    {
        // Long hands can reach the floor. If the lower slice contains more than
        // two limbs, follow the inner calf surfaces down to their own feet.
        var origin=(character.Minimum+character.Maximum)*.5f;origin.Y=bottom+height*.25f;
        var sections=MeshSections.Cut(character,origin,Vector3.UnitY,(character.Maximum-character.Minimum).Length(),height*1e-5f);
        if(sections.Length<=2)return null;
        var left=sections.Where(s=>s.Center.Z>middle+height*.025f).OrderBy(s=>s.Center.Z).FirstOrDefault();
        var right=sections.Where(s=>s.Center.Z<middle-height*.025f).OrderByDescending(s=>s.Center.Z).FirstOrDefault();
        if(left is null||right is null)return null;
        var mesh=Geometry.Merge(character.Meshes.Where(m=>m.Kind==MeshKind.Body));
        var neighbors=Geometry.Neighbors(mesh,height*1e-5f);var visited=new bool[mesh.Vertices.Length];var queue=new Queue<int>();
        foreach(var section in new[]{left,right})
        {
            int seed=-1;float distance=float.PositiveInfinity;
            for(int i=0;i<mesh.Vertices.Length;i++)
            {
                float candidate=Vector3.DistanceSquared(mesh.Vertices[i],section.Center);
                if(candidate<distance){seed=i;distance=candidate;}
            }
            if(seed<0||distance>height*height*.08f*.08f)return null;
            if(!visited[seed]){visited[seed]=true;queue.Enqueue(seed);}
        }
        while(queue.TryDequeue(out int vertex))foreach(int next in neighbors[vertex])
            if(!visited[next]&&mesh.Vertices[next].Y<bottom+height*.4f){visited[next]=true;queue.Enqueue(next);}
        var feet=mesh.Vertices.Where((p,i)=>visited[i]&&p.Y<bottom+height*.07f).Select(p=>p.X).ToArray();
        if(feet.Length<8)return null;
        return(BodyDetector.Quantile(feet,.1f)+BodyDetector.Quantile(feet,.9f))*.5f-(left.Center.X+right.Center.X)*.5f;
    }
    static ImportedCharacter Reorient(ImportedCharacter character,Func<Vector3,Vector3> turn,string warning)
    {
        return new ImportedCharacter{Name=character.Name,SourcePath=character.SourcePath,SourceUnitCm=character.SourceUnitCm,SourceUpAxis=character.SourceUpAxis,
            HasExistingSkin=character.HasExistingSkin,ExistingBones=character.ExistingBones.Select(b=>b with{Position=turn(b.Position)}).ToArray(),
            Meshes=character.Meshes.Select(m=>m with{Vertices=m.Vertices.Select(turn).ToArray(),CornerNormals=m.CornerNormals.Select(turn).ToArray()}).ToArray(),
            Materials=character.Materials,EmbeddedTextures=character.EmbeddedTextures,
            ImportWarnings=character.ImportWarnings.Append(warning).ToArray()};
    }
}
notpointless.chomnr_humanoid_rigger / Editor/Core/Analysis/HumanoidUp.cs
Editor library
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;

/// <summary>Check declared up against a head, torso and paired lower-limb
/// cross sections. Ambiguous or open geometry retains the imported axes.</summary>
internal static class HumanoidUp
{
    internal static Vector3 Find(ImportedCharacter character)
    {
        var points=character.Meshes.Where(m=>m.Kind==MeshKind.Body).SelectMany(m=>m.Vertices).ToArray();
        if(points.Length<100)return Vector3.UnitY;
        var minimum=points.Aggregate(Vector3.Min);var maximum=points.Aggregate(Vector3.Max);
        var center=(minimum+maximum)*.5f;float reach=(maximum-minimum).Length();
        bool Supports(Vector3 up)
        {
            float bottom=points.Min(p=>Vector3.Dot(p,up)),height=points.Max(p=>Vector3.Dot(p,up))-bottom;
            if(height<.001f)return false;
            MeshSections.Section[] Sections(float fraction)
            {
                var origin=center+up*(bottom+height*fraction-Vector3.Dot(center,up));
                return MeshSections.Cut(character,origin,up,reach,height*1e-5f)
                    .Where(s=>s.Area>height*height*1e-6f).OrderByDescending(s=>s.Area).ToArray();
            }
            var heads=Sections(.92f);
            if(heads.Length==0||heads[0].Area<heads.Sum(s=>s.Area)*.7f||heads[0].Radius>height*.3f)return false;
            var torsos=Sections(.60f);if(torsos.Length==0)return false;
            var head=heads[0].Center;var torso=torsos[0].Center;
            Vector3 Horizontal(Vector3 p)=>p-up*Vector3.Dot(p,up);
            if(Horizontal(head-torso).Length()>height*.22f)return false;
            Vector3? previous=null;int evidence=0;
            foreach(float fraction in new[]{.25f,.35f})
            {
                var limbs=Sections(fraction);float best=float.PositiveInfinity;Vector3 direction=default;
                for(int i=0;i<limbs.Length;i++)for(int j=i+1;j<limbs.Length;j++)
                {
                    var a=limbs[i];var b=limbs[j];var delta=b.Center-a.Center;float length=delta.Length();
                    if(length<height*.06f||length>height*.45f||Math.Min(a.Area,b.Area)<Math.Max(a.Area,b.Area)*.2f)continue;
                    if(length<(a.Radius+b.Radius)*1.1f)continue;
                    float offset=Horizontal((a.Center+b.Center)*.5f-torso).Length();
                    if(offset>height*.2f||offset>=best)continue;
                    direction=delta/length;best=offset;
                }
                if(!float.IsFinite(best))continue;
                if(previous is {} prior&&Math.Abs(Vector3.Dot(prior,direction))<.85f)return false;
                previous=direction;evidence++;
            }
            return evidence>0;
        }
        // A plausible declared up always wins. Correct only a unique supported
        // alternative; the longest model dimension alone may simply be its arms.
        if(Supports(Vector3.UnitY))return Vector3.UnitY;
        Vector3? candidate=null;
        foreach(var up in new[]{Vector3.UnitX,-Vector3.UnitX,-Vector3.UnitY,Vector3.UnitZ,-Vector3.UnitZ})
        {
            if(!Supports(up))continue;
            if(candidate is not null)return Vector3.UnitY;
            candidate=up;
        }
        return candidate??Vector3.UnitY;
    }
}
notpointless.chomnr_humanoid_rigger / Editor/Core/Analysis/MeshSections.cs
Editor library
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
using Vector2=System.Numerics.Vector2;

/// <summary>Closed triangle-plane contours, with area centers independent of vertex density.</summary>
internal static class MeshSections
{
    internal record Section(Vector3 Center,float Area,float Radius,float MinimumRadius);
    internal static Section[] Cut(ImportedCharacter character,Vector3 origin,Vector3 normal,float reach,float tolerance)
    {
        normal=Vector3.Normalize(normal);var u=Vector3.Normalize(Vector3.Cross(normal,Math.Abs(normal.Z)>.9f?Vector3.UnitY:Vector3.UnitZ));var v=Vector3.Cross(normal,u);
        var points=new List<Vector2>();var edges=new HashSet<(int,int)>();var cells=new Dictionary<(int,int),List<int>>();
        int Node(Vector3 position)
        {
            var delta=position-origin;var p=new Vector2(Vector3.Dot(delta,u),Vector3.Dot(delta,v));
            int x=(int)Math.Floor(p.X/tolerance),y=(int)Math.Floor(p.Y/tolerance);
            for(int i=-1;i<=1;i++)for(int j=-1;j<=1;j++)if(cells.TryGetValue((x+i,y+j),out var nearby))
                foreach(int n in nearby)if(Vector2.DistanceSquared(points[n],p)<=tolerance*tolerance)return n;
            int index=points.Count;points.Add(p);if(!cells.TryGetValue((x,y),out var bucket))cells[(x,y)]=bucket=[];bucket.Add(index);return index;
        }
        var cut=new Vector3[3];
        foreach(var mesh in character.Meshes.Where(m=>m.Kind==MeshKind.Body))for(int t=0;t<mesh.Triangles.Length;t+=3)
        {
            int count=0;
            for(int e=0;e<3;e++)
            {
                var a=mesh.Vertices[mesh.Triangles[t+e]];var b=mesh.Vertices[mesh.Triangles[t+(e+1)%3]];
                float da=Vector3.Dot(a-origin,normal),db=Vector3.Dot(b-origin,normal);
                if((da<=0&&db>0)||(db<=0&&da>0))cut[count++]=Vector3.Lerp(a,b,da/(da-db));
            }
            if(count!=2||Vector3.Distance(cut[0],origin)>reach||Vector3.Distance(cut[1],origin)>reach)continue;
            int first=Node(cut[0]),second=Node(cut[1]);if(first!=second)edges.Add((Math.Min(first,second),Math.Max(first,second)));
        }
        var neighbors=points.Select(_=>new List<int>()).ToArray();foreach(var(a,b)in edges){neighbors[a].Add(b);neighbors[b].Add(a);}
        var sections=new List<Section>();var seen=new bool[points.Count];
        for(int start=0;start<points.Count;start++)
        {
            if(seen[start])continue;var component=new List<int>();var queue=new Queue<int>();queue.Enqueue(start);seen[start]=true;
            while(queue.TryDequeue(out int n)){component.Add(n);foreach(int next in neighbors[n])if(!seen[next]){seen[next]=true;queue.Enqueue(next);}}
            if(component.Count<6||component.Any(n=>neighbors[n].Count!=2))continue;
            var polygon=new List<Vector2>();int previous=-1,current=start;
            do{polygon.Add(points[current]);int next=neighbors[current].First(n=>n!=previous);previous=current;current=next;}while(current!=start&&polygon.Count<=component.Count);
            if(current!=start||polygon.Count!=component.Count)continue;
            float twiceArea=0;var weighted=Vector2.Zero;
            for(int i=0;i<polygon.Count;i++){var a=polygon[i];var b=polygon[(i+1)%polygon.Count];float cross=a.X*b.Y-b.X*a.Y;twiceArea+=cross;weighted+=(a+b)*cross;}
            if(Math.Abs(twiceArea)<tolerance*tolerance)continue;
            var center=weighted/(3*twiceArea);float radius=polygon.Max(p=>Vector2.Distance(center,p));
            float minimum=float.PositiveInfinity;
            for(int i=0;i<polygon.Count;i++)
            {
                var a=polygon[i];var b=polygon[(i+1)%polygon.Count];var ab=b-a;
                float t=Math.Clamp(Vector2.Dot(center-a,ab)/Math.Max(ab.LengthSquared(),1e-12f),0,1);minimum=Math.Min(minimum,Vector2.Distance(center,a+ab*t));
            }
            sections.Add(new(origin+u*center.X+v*center.Y,Math.Abs(twiceArea)*.5f,radius,minimum));
        }
        return sections.ToArray();
    }
}
notpointless.chomnr_humanoid_rigger / Editor/Core/Analysis/FingerAlignment.cs
Editor library
#nullable enable annotations
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;

/// <summary>Centers non-thumb chains on their own closed mesh sections, preserving
/// supported joint spacing and repairing chains collapsed near a fingertip.</summary>
public static class FingerAlignment
{
    public static int Refine(ImportedCharacter character,Anatomy anatomy,string side)
    {
        if(side is not ("L" or "R"))throw new ArgumentException("Unknown hand side.");
        var fingers=new[]{"Index","Middle","Ring","Pinky"};
        var chains=fingers.Select(f=>Enumerable.Range(1,3).Select(i=>f+i+"."+side).Append(f+"Tip."+side).ToArray())
            .Where(roles=>roles.All(r=>anatomy.Points.TryGetValue(r,out var p)&&!p.Corrected&&p.Confidence>=.35f)).ToArray();
        if(chains.Length==0)return 0;
        var wrist=anatomy["Hand."+side];float h=anatomy.Height;
        float reach=chains.SelectMany(r=>r).Max(r=>Vector3.Distance(wrist,anatomy[r]))+h*.035f;
        var local=HandSurface(character,wrist,reach);
        // Containment uses the original complete shells, never the cropped analysis surface.
        var volume=new SurfaceVisibility(character.Meshes.Where(m=>m.Kind==MeshKind.Body),h*.00001f);
        int changed=0;
        foreach(var roles in chains)
        {
            var previous=roles.Select(r=>anatomy[r]).ToArray();var fitted=Fit(local,volume,wrist,previous,h);
            if(fitted is null)continue;
            for(int i=0;i<3;i++)anatomy.Points[roles[i]]=anatomy.Points[roles[i]] with{Position=fitted[i]};
            changed++;
        }
        return changed;
    }
    static Vector3[]? Fit(ImportedCharacter local,SurfaceVisibility volume,Vector3 wrist,Vector3[] previous,float h)
    {
        var tip=previous[3];var inward=wrist-tip;if(inward.LengthSquared()<h*h*.000001f)return null;
        var axis=Vector3.Normalize(inward);var center=tip;float step=h*.0015f;
        var path=new List<Vector3>{tip};var radii=new List<float>();var areas=new List<float>();bool ended=false;
        for(int i=0;i<75;i++)
        {
            var seed=center+axis*step;
            MeshSections.Section? section=null;
            // A plane through a mesh vertex can produce an ambiguous contour.
            // Nearby parallel cuts recover it without changing or welding geometry.
            foreach(float offset in new[]{0f,.15f,-.15f,.35f,-.35f})
            {
                section=MeshSections.Cut(local,seed+axis*(step*offset),axis,h*.035f,h*.00001f)
                    .Where(s=>s.Radius>h*.0007f&&s.Radius<h*.016f&&Vector3.Distance(s.Center,seed)<h*.018f)
                    .MinBy(s=>Vector3.DistanceSquared(s.Center,seed));
                if(section is not null)break;
            }
            if(section is null){ended=true;break;}
            var difference=section.Center-center;
            if(path.Count>1&&difference.Length()>h*.006f){ended=true;break;}
            if(path.Count>5&&section.Area>areas.TakeLast(3).Average()*1.8f){ended=true;break;}
            if(path.Count>2&&volume.Blocked(center,section.Center,h*.00001f)){ended=true;break;}
            if(difference.LengthSquared()<1e-12f)return null;
            var tangent=Vector3.Normalize(difference);
            if(path.Count>1&&Vector3.Dot(axis,tangent)<.3f){ended=true;break;}
            center=section.Center;path.Add(center);radii.Add(section.Radius);areas.Add(section.Area);
            if(path.Count>2)axis=Vector3.Normalize(Vector3.Lerp(axis,tangent,.25f));
            if(Vector3.Distance(center,tip)>inward.Length()*.8f)return null;
        }
        if(path.Count<8||!ended)return null;
        var extension=center+axis*radii[^1]*.5f;
        if(volume.Contains(extension,h*.00001f)&&!volume.Blocked(center,extension,h*.00001f))path.Add(extension);
        path.Reverse();var distances=new float[path.Count];
        for(int i=1;i<path.Count;i++)distances[i]=distances[i-1]+Vector3.Distance(path[i-1],path[i]);
        float coverage=Vector3.Distance(path[0],tip)/Vector3.Distance(previous[0],tip);
        if(coverage<.6f)return null;
        // The webbing can end the trace before a well-supported palm knuckle.
        // Keep that base and still center the distal joints on the recovered digit.
        bool retainBase=coverage<.85f;
        (Vector3 Point,float Offset) Project(Vector3 point)
        {
            float best=float.PositiveInfinity,offset=0;var result=point;
            for(int i=1;i<path.Count;i++)
            {
                var closest=Geometry.ClosestOnSegment(point,path[i-1],path[i]);float distance=Vector3.DistanceSquared(point,closest);
                if(distance>=best)continue;best=distance;result=closest;offset=distances[i-1]+Vector3.Distance(closest,path[i-1]);
            }
            return(result,offset);
        }
        // A centered chain does not need a new proportion-based fit.
        if(previous.Skip(retainBase?1:0).Take(retainBase?2:3).Average(p=>Vector3.Distance(p,Project(p).Point))<h*.00025f)return null;
        var fitted=new[]{0f,.5f,.78f,1f}.Select(f=>
        {
            float target=distances[^1]*f;int i=Array.FindIndex(distances,d=>d>=target);if(i<=0)return path[0];
            return Vector3.Lerp(path[i-1],path[i],(target-distances[i-1])/Math.Max(distances[i]-distances[i-1],1e-8f));
        }).ToArray();
        if(retainBase)fitted[0]=previous[0];
        if(Vector3.Distance(previous[0],tip)>=Vector3.Distance(fitted[0],tip)*.6f)
        {
            var second=Project(previous[1]);var third=Project(previous[2]);
            if(second.Offset<h*.001f||third.Offset-second.Offset<h*.001f||distances[^1]-third.Offset<h*.001f)return null;
            fitted[1]=second.Point;fitted[2]=third.Point;
        }
        for(int bone=0;bone<3;bone++)for(int sample=0;sample<9;sample++)
            if(!volume.Contains(Vector3.Lerp(fitted[bone],fitted[bone+1],sample/9f),h*.00001f))return null;
        return fitted;
    }
    static ImportedCharacter HandSurface(ImportedCharacter character,Vector3 wrist,float radius)
    {
        var parts=new List<MeshPart>();float squared=radius*radius;
        foreach(var mesh in character.Meshes.Where(m=>m.Kind==MeshKind.Body))
        {
            var triangles=new List<int>();
            for(int i=0;i<mesh.Triangles.Length;i+=3)
            {
                var a=mesh.Vertices[mesh.Triangles[i]];var b=mesh.Vertices[mesh.Triangles[i+1]];var c=mesh.Vertices[mesh.Triangles[i+2]];
                var nearest=Vector3.Clamp(wrist,Vector3.Min(a,Vector3.Min(b,c)),Vector3.Max(a,Vector3.Max(b,c)));
                if(Vector3.DistanceSquared(nearest,wrist)>squared)continue;
                triangles.Add(mesh.Triangles[i]);triangles.Add(mesh.Triangles[i+1]);triangles.Add(mesh.Triangles[i+2]);
            }
            if(triangles.Count>0)parts.Add(new(mesh.Name,mesh.Vertices,triangles.ToArray(),MeshKind.Body));
        }
        return new ImportedCharacter{Meshes=parts.ToArray()};
    }
}
notpointless.chomnr_humanoid_rigger / Editor/Core/Rigging/SkinningNormals.cs
Editor library
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;

/// <summary>Angle-weighted outward normals for closed, consistently oriented
/// components. Uncertain/open surfaces contribute no directional prior.</summary>
internal static class SkinningNormals
{
    internal static Vector3[] ClosedSurface(Vector3[] points,int[] triangles)
    {
        var mesh=new MeshPart("Skinning surface",points,triangles,MeshKind.Body);
        var components=Geometry.Components(mesh);int count=components.Max()+1;
        var origins=new Vector3[count];var assigned=new bool[count];
        for(int v=0;v<points.Length;v++)if(!assigned[components[v]]){origins[components[v]]=points[v];assigned[components[v]]=true;}
        var volumes=new double[count];var closed=Enumerable.Repeat(true,count).ToArray();
        var normals=new Vector3[points.Length];var edges=new Dictionary<(int,int),(int Count,int Direction)>();
        for(int t=0;t<triangles.Length;t+=3)
        {
            int a=triangles[t],b=triangles[t+1],c=triangles[t+2];if(a==b||a==c||b==c)continue;
            var normal=Vector3.Cross(points[b]-points[a],points[c]-points[a]);float area=normal.Length();
            if(area<1e-12f){closed[components[a]]=false;continue;}
            normal/=area;
            var origin=origins[components[a]];
            volumes[components[a]]+=Vector3.Dot(points[a]-origin,Vector3.Cross(points[b]-origin,points[c]-origin));
            for(int corner=0;corner<3;corner++)
            {
                int v=triangles[t+corner],n=triangles[t+(corner+1)%3],o=triangles[t+(corner+2)%3];
                var x=points[n]-points[v];var y=points[o]-points[v];
                normals[v]+=normal*MathF.Atan2(Vector3.Cross(x,y).Length(),Vector3.Dot(x,y));
                var key=(Math.Min(v,n),Math.Max(v,n));var edge=edges.GetValueOrDefault(key);
                edges[key]=(edge.Count+1,edge.Direction+(v<n?1:-1));
            }
        }
        foreach(var edge in edges)if(edge.Value.Count!=2||edge.Value.Direction!=0)closed[components[edge.Key.Item1]]=false;
        for(int v=0;v<normals.Length;v++)
        {
            int component=components[v];float length=normals[v].Length();
            normals[v]=closed[component]&&Math.Abs(volumes[component])>1e-12&&length>1e-6f
                ?normals[v]*(Math.Sign(volumes[component])/length):Vector3.Zero;
        }
        return normals;
    }
}
notpointless.chomnr_humanoid_rigger / Editor/Core/Validation/RigValidator.cs
Editor library
#nullable enable annotations
using System.Numerics;
namespace HumanoidRigger;
using Vector3 = System.Numerics.Vector3;

public sealed record ValidationIssue(string Code,string Message,bool Error);
public sealed record StressResult(string Pose,float MaximumStretch,float MinimumAreaRatio,int NonFiniteVertices,float SourceEdgeLength=0,float DeformedEdgeLength=0,int NonFiniteMeasurements=0,int ReversedTriangles=0,float ReversedAreaFraction=0);
public sealed class ValidationReport
{
    public List<ValidationIssue> Issues {get;}=[];
    public List<StressResult> StressTests {get;}=[];
    public List<StressResult> JointStressTests {get;}=[];
    public int Repairs {get;set;}
    public int RepairPasses {get;set;}
    public bool Passed=>Issues.All(i=>!i.Error) && StressTests.Count>0;
}
public static class RigValidator
{
    public static ValidationReport ValidateAndRepair(ImportedCharacter character,GeneratedRig rig)
        =>ValidateAndRepair(character,rig,new ValidationGeometry(character));
    internal static ValidationReport ValidateAndRepair(ImportedCharacter character,GeneratedRig rig,ValidationGeometry geometry)
    {
        int repairs=0;
        foreach(var part in rig.Weights) for(int v=0;v<part.Length;v++)
        {
            var cleaned=Skinning.Cleanup(part[v],rig.Bones.Length,rig.Profile.MaximumInfluences);
            if(!cleaned.SequenceEqual(part[v])) {part[v]=cleaned;repairs++;}
        }
        var report=Validate(character,rig,null,geometry);report.Repairs=repairs;
        return SurfaceRepair.Improve(character,rig,WeightRepair.Improve(character,rig,report,geometry),geometry);
    }
    public static ValidationReport Validate(ImportedCharacter character,GeneratedRig rig)
        =>Validate(character,rig,null);

    internal static ValidationReport Validate(ImportedCharacter character,GeneratedRig rig,BindTriangle[][]? faces)
        =>Validate(character,rig,faces,null);
    internal static ValidationReport Validate(ImportedCharacter character,GeneratedRig rig,BindTriangle[][]? faces,ValidationGeometry? geometry)
    {
        var height=geometry?.Height??character.AnatomicalHeight;var r=new ValidationReport();void Error(string code,string text)=>r.Issues.Add(new(code,text,true));
        try{rig.Profile.Validate();}catch(Exception e){Error("profile",e.Message);return r;}
        var roles=new HashSet<string>();var names=new HashSet<string>();
        var body=geometry?.Body??character.Meshes.Where(m=>m.Kind==MeshKind.Body).SelectMany(m=>m.Vertices).ToArray();
        for(int i=0;i<rig.Bones.Length;i++)
        {
            var b=rig.Bones[i];
            if(!roles.Add(b.Role)||!names.Add(b.Name))Error("duplicate","Duplicate bone assignment.");
            bool validParent=b.Parent<i&&b.Parent>=-1;
            if(!validParent)Error("hierarchy","Invalid skeleton hierarchy.");
            if(!Geometry.Finite(b.Position)||!float.IsFinite(b.Rotation.LengthSquared())||Math.Abs(b.Rotation.LengthSquared()-1)>.001f)Error("frame","Invalid bone orientation or position.");
            if(b.Deform&&body.Length>0&&(geometry?.JointDistanceSquared(b.Position)??body.Min(p=>Vector3.DistanceSquared(p,b.Position)))>height*height*.15f*.15f)
                Error("joint-placement",$"{b.Role} is too far from the character. Check its landmark.");
            var definition=rig.Profile.Bones.FirstOrDefault(d=>d.Role==b.Role);
            if(definition is null || definition.Name!=b.Name || !validParent || (b.Parent<0 ? null : rig.Bones[b.Parent].Role)!=definition.Parent)Error("mapping","Skeleton differs from the selected profile.");
        }
        foreach(var b in rig.Profile.Bones.Where(b=>b.Required))if(!roles.Contains(b.Role))Error("required",$"Missing {b.Role}.");
        if(rig.Weights.Length!=character.Meshes.Length){Error("weights","Missing mesh skinning.");return r;}
        for(int p=0;p<rig.Weights.Length;p++)
        {
            if(rig.Weights[p].Length!=character.Meshes[p].Vertices.Length){Error("weights","Skinning vertex count mismatch.");continue;}
            foreach(var vertex in rig.Weights[p])
            {
                if(vertex.Length==0||vertex.Length>rig.Profile.MaximumInfluences)Error("influences","Invalid influence count.");
                if(vertex.Any(i=>i.Bone<0||i.Bone>=rig.Bones.Length||!float.IsFinite(i.Weight)||i.Weight<=0))Error("influences","Invalid weight or bone index.");
                if(Math.Abs(vertex.Sum(i=>i.Weight)-1)>.0001f)Error("normalization","Weights do not sum to one.");
            }
        }
        if(r.Issues.Any(i=>i.Error))return r;
        if(geometry?.Trunk?.HasBleeding(character,rig)==true){Error("weight-bleeding","A weight repair reintroduced a remote torso attachment.");return r;}
        var ends=RigGeometry.SegmentEnds(rig);
        var locality=new SkinningLocality(character,rig,ends);
        for(int p=0;p<character.Meshes.Length;p++)
        {
            var mesh=character.Meshes[p];if(mesh.Kind==MeshKind.Accessory)continue;
            bool remote=false;
            for(int v=0;v<mesh.Vertices.Length&&!remote;v++)
                foreach(var influence in rig.Weights[p][v])
                {
                    if(!rig.Bones[influence.Bone].Deform){Error("nondeforming-influence","Skinning references a non-deforming bone.");remote=true;break;}
                    if(influence.Weight>.05f&&Vector3.Distance(mesh.Vertices[v],Geometry.ClosestOnSegment(mesh.Vertices[v],rig.Bones[influence.Bone].Position,ends[influence.Bone]))>locality.Limit(influence.Bone,mesh.Vertices[v]))
                    {Error("weight-region",$"Mesh '{mesh.Name}' is influenced by a distant anatomical region ({rig.Bones[influence.Bone].Role}).");remote=true;break;}
                }
        }
        if(r.Issues.Any(i=>i.Error))return r;
        faces??=geometry?.Faces??character.Meshes.Select(BindTriangle.Measure).ToArray();
        var specifications=(geometry?.Poses??Deformation.Poses).ToArray();
        var tests=new StressResult[specifications.Length];
        Vector3[][] Buffers()=>character.Meshes.Select(m=>new Vector3[m.Vertices.Length]).ToArray();
        void Measure(int i,Vector3[][] buffer)
        {
            if(Deformation.IsApplicable(specifications[i],roles))tests[i]=MeasurePose(character,rig,specifications[i],faces,buffer,height);
        }
        // Poses read the same frozen weights and write separate buffers. Keep
        // report order and each pose's arithmetic serial and deterministic.
        int workers=RigWork.WorkerCount(character.Meshes.Sum(m=>m.Vertices.Length));
        RigWork.For(specifications.Length,workers,Buffers,Measure);
        for(int i=0;i<specifications.Length;i++)
        {
            var pose=specifications[i];var test=tests[i];
            if(test is null){r.Issues.Add(new("optional-pose",$"{pose.Name}: optional joints absent.",false));continue;}
            r.StressTests.Add(test);
            if(test.ReversedTriangles>0)r.Issues.Add(new("surface-reversal",$"{pose.Name}: {test.ReversedTriangles} surface triangles reverse orientation.",false));
            if(test.NonFiniteVertices>0||test.NonFiniteMeasurements>0)Error("deformation",$"{pose.Name}: deformation produced non-finite coordinates or measurements.");
            else if(test.MaximumStretch>4||test.MinimumAreaRatio<.025f)Error("deformation",$"{pose.Name}: unsafe deformation (stretch {test.MaximumStretch:F2}, area ratio {test.MinimumAreaRatio:F3}).");
        }
        return r;
    }
    internal static StressResult MeasurePose(ImportedCharacter character,GeneratedRig rig,StressPose pose,BindTriangle[][] faces,Vector3[][] deformed,float height)
    {
        var transforms=Deformation.BoneTransforms(rig,Deformation.JointRotations(rig,pose));
        var rotations=transforms.Rotations;
        Deformation.ApplyTransforms(character,rig,transforms.Positions,rotations,deformed);
        float stretch=1,minArea=1,sourceEdge=0,deformedEdge=0;int nonFinite=0,invalidMeasurements=0;
        int reversed=0;double surfaceArea=0,reversedArea=0;
        for(int p=0;p<character.Meshes.Length;p++)
        {
            var mesh=character.Meshes[p];var dst=deformed[p];nonFinite+=dst.Count(v=>!Geometry.Finite(v));
            foreach(var face in faces[p])
            {
                var i=face.A;var j=face.B;var k=face.C;
                var normal=face.Normal;
                var posedNormal=Vector3.Cross(dst[j]-dst[i],dst[k]-dst[i]);
                float area=face.Area,posedArea=posedNormal.Length();
                if(!float.IsFinite(area)||!float.IsFinite(posedArea))invalidMeasurements++;
                else if(area>height*height*1e-10f)
                {
                    float ratio=posedArea/area;
                    if(float.IsFinite(ratio))minArea=Math.Min(minArea,ratio);else invalidMeasurements++;
                    surfaceArea+=area;
                    float alignment=SurfaceOrientation.Alignment(normal,posedNormal,rig.Weights[p][i],rig.Weights[p][j],rig.Weights[p][k],rotations);
                    if(alignment<SurfaceOrientation.ReversalLimit){reversed++;reversedArea+=area;}
                }
                for(int edge=0;edge<3;edge++)
                {
                    var (a,b,length)=face.Edge(edge);var posedLength=Vector3.Distance(dst[a],dst[b]);
                    if(!float.IsFinite(length)||!float.IsFinite(posedLength)){invalidMeasurements++;continue;}
                    if(length<=height*1e-6f)continue;
                    float ratio=posedLength/length;
                    if(!float.IsFinite(ratio)){invalidMeasurements++;continue;}
                    if(ratio>stretch){stretch=ratio;sourceEdge=length;deformedEdge=posedLength;}
                }
            }
        }
        return new(pose.Name,stretch,minArea,nonFinite,sourceEdge,deformedEdge,invalidMeasurements,reversed,surfaceArea>0?(float)(reversedArea/surfaceArea):0);
    }
}
notpointless.chomnr_humanoid_rigger / Editor/Core/Validation/TrunkSkinning.cs
Editor library
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;

/// <summary>Refine axial skinning with normal-aware heat and compact joint
/// support. Full deformation evidence must remain safe before accepting it.</summary>
internal static class TrunkSkinning
{
    internal static ValidationReport Improve(ImportedCharacter character,GeneratedRig rig,ValidationReport initial)
    {
        var expected=Deformation.Poses.Where(p=>Deformation.IsApplicable(p,rig.Bones.Select(b=>b.Role).ToHashSet())).Select(p=>p.Name).Order().ToArray();
        if(!initial.Passed||!WeightRepair.HasCompleteEvidence(initial,expected))return initial;
        var region=new TrunkRegion(character,rig);if(!region.HasBleeding(character,rig))return initial;
        var original=rig.Weights;bool accepted=false;
        try
        {
            var heat=HeatSkinning.Candidates(character,rig,normalPrior:true,trunk:region).First();
            var geometry=new ValidationGeometry(character,trunk:region);
            foreach(float amount in new[]{1f,.5f,0f})
            {
                rig.Weights=Apply(character,rig,region,original,heat,amount);
                var candidate=RigValidator.ValidateAndRepair(character,rig,geometry);
                // Local surface repair may adjust a protected boundary. Never
                // accept a trial that silently reintroduces remote attachments.
                if(region.HasBleeding(character,rig)||!candidate.Passed||!WeightRepair.HasCompleteEvidence(candidate,expected)||
                    candidate.StressTests.Zip(initial.StressTests).Any(p=>p.First.Pose!=p.Second.Pose||p.First.ReversedTriangles>p.Second.ReversedTriangles||p.First.ReversedAreaFraction>p.Second.ReversedAreaFraction+1e-7f))continue;
                candidate.Repairs+=initial.Repairs;candidate.RepairPasses+=initial.RepairPasses+1;
                accepted=true;return candidate;
            }
        }
        catch(InvalidOperationException e){initial.Issues.Add(new("skinning-candidate",e.Message,false));}
        finally{if(!accepted)rig.Weights=original;}
        initial.Issues.Add(new("weight-bleeding","Torso skinning still follows a remote joint. Check the shoulder, hip and neck landmarks.",true));
        return initial;
    }
    static Influence[][][] Apply(ImportedCharacter character,GeneratedRig rig,TrunkRegion region,Influence[][][] source,Influence[][][] heat,float amount)
    {
        var result=source.Select(p=>(Influence[][])p.Clone()).ToArray();var ends=RigGeometry.SegmentEnds(rig);
        for(int p=0;p<result.Length;p++)for(int v=0;v<result[p].Length;v++)if(region.Vertices[p][v])
        {
            var point=character.Meshes[p].Vertices[v];float blend=amount*region.Blend(point);
            var values=new float[rig.Bones.Length];
            foreach(var w in source[p][v])values[w.Bone]+=w.Weight*(1-blend);
            foreach(var w in heat[p][v])values[w.Bone]+=w.Weight*blend;
            float removed=0;
            foreach(var attachment in region.Attachments)
            {
                float support=attachment.Support(point);
                for(int b=0;b<values.Length;b++)if(attachment.Moving[b]){removed+=values[b]*(1-support);values[b]*=support;}
            }
            float axial=heat[p][v].Where(w=>region.Axial[w.Bone]).Sum(w=>w.Weight);
            if(axial>1e-6f)foreach(var w in heat[p][v]){if(region.Axial[w.Bone])values[w.Bone]+=removed*w.Weight/axial;}
            else
            {
                int nearest=Enumerable.Range(0,rig.Bones.Length).Where(b=>region.Axial[b]).MinBy(b=>Vector3.DistanceSquared(point,Geometry.ClosestOnSegment(point,rig.Bones[b].Position,ends[b])));
                values[nearest]+=removed;
            }
            result[p][v]=Skinning.Cleanup(values,rig.Profile.MaximumInfluences);
        }
        return result;
    }
}
notpointless.chomnr_humanoid_rigger / Editor/UI/MaterialAssets.Formats.cs
Editor library
using SkiaSharp;
namespace HumanoidRigger.Editor;

internal static partial class MaterialAssets
{
    // Work on copies in the output/cache directory. glTF packs roughness in G
    // and metalness in B; Source 2 and MTL consume separate grayscale maps.
    internal static SourceMaterial[] PrepareFormats(SourceMaterial[] source,string directory,ExportFormats formats)
    {
        bool portable=(formats&(ExportFormats.Gltf|ExportFormats.Glb))!=0;
        var converted=new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);
        string Png(string path)
        {
            if(path is null||Path.GetExtension(path).ToLowerInvariant() is ".png" or ".jpg" or ".jpeg")return path;
            if(converted.TryGetValue(path,out var result))return result;
            using var bitmap=Decode(path);result="textures/portable_"+converted.Count+".png";Save(bitmap,result);converted.Add(path,result);return result;
        }
        SKBitmap Decode(string path)=>SKBitmap.Decode(Path.Combine(directory,path))??throw new FormatException("Cannot decode texture '"+path+"'.");
        void Save(SKBitmap bitmap,string path)
        {
            string output=Path.Combine(directory,path);Directory.CreateDirectory(Path.GetDirectoryName(output));
            using var image=SKImage.FromBitmap(bitmap);using var data=image.Encode(SKEncodedImageFormat.Png,100);using var stream=File.Create(output);data.SaveTo(stream);
        }
        static SKColor Sample(SKBitmap bitmap,int x,int y,int width,int height,SKColor fallback)=>bitmap is null?fallback:bitmap.GetPixel(Math.Min(bitmap.Width-1,x*bitmap.Width/width),Math.Min(bitmap.Height-1,y*bitmap.Height/height));
        static byte Channel(float value)=>(byte)Math.Clamp((int)MathF.Round(value),0,255);
        return source.Select((original,index)=>
        {
            var m=ConvertSpecularGlossiness(original with{},directory,index);
            if(m.AuthoredPbr||m.AuthoredEmission)
            {
                float peak=Math.Max(1,Math.Max(m.EmissiveFactor.X,Math.Max(m.EmissiveFactor.Y,m.EmissiveFactor.Z)));
                m.EmissiveFactor/=peak;m.EmissiveStrength*=peak;
            }
            string WriteMap(string suffix,int width,int height,Func<int,int,SKColor> pixel)
            {
                using var bitmap=new SKBitmap(width,height,SKColorType.Rgba8888,SKAlphaType.Unpremul);
                for(int y=0;y<height;y++)for(int x=0;x<width;x++)bitmap.SetPixel(x,y,pixel(x,y));
                string path="textures/pbr_"+index+"_"+suffix+".png";Save(bitmap,path);return path;
            }
            if(m.AuthoredPbr)
            {
                using var packed=m.MetallicRoughnessTexture is null?null:Decode(m.MetallicRoughnessTexture);
                int w=packed?.Width??1,h=packed?.Height??1;
                m.RoughnessTexture=WriteMap("roughness",w,h,(x,y)=>{byte v=Channel((packed?.GetPixel(x,y).Green??255)*m.RoughnessFactor);return new(v,v,v);});
                m.MetalnessTexture=WriteMap("metalness",w,h,(x,y)=>{byte v=Channel((packed?.GetPixel(x,y).Blue??255)*m.MetallicFactor);return new(v,v,v);});
            }
            else if(portable&&(m.RoughnessTexture is not null||m.MetalnessTexture is not null))
            {
                using var rough=m.RoughnessTexture is null?null:Decode(m.RoughnessTexture);using var metal=m.MetalnessTexture is null?null:Decode(m.MetalnessTexture);
                int w=Math.Max(rough?.Width??1,metal?.Width??1),h=Math.Max(rough?.Height??1,metal?.Height??1);
                m.MetallicRoughnessTexture=WriteMap("metallic_roughness",w,h,(x,y)=>new(255,Sample(rough,x,y,w,h,SKColors.White).Red,Sample(metal,x,y,w,h,SKColors.Black).Red));
            }
            if(m.NormalTexture is not null&&m.NormalScale!=1)
            {
                using var normal=Decode(m.NormalTexture);float strength=m.NormalScale;
                m.NormalTexture=WriteMap("normal",normal.Width,normal.Height,(x,y)=>
                {
                    var c=normal.GetPixel(x,y);
                    var n=new System.Numerics.Vector3((c.Red/255f*2-1)*strength,(c.Green/255f*2-1)*strength,c.Blue/255f*2-1);
                    n=n.LengthSquared()>1e-12f?System.Numerics.Vector3.Normalize(n):System.Numerics.Vector3.UnitZ;
                    return new(Channel((n.X*.5f+.5f)*255),Channel((n.Y*.5f+.5f)*255),Channel((n.Z*.5f+.5f)*255),c.Alpha);
                });
                m.NormalScale=1;
            }
            if(m.AuthoredPbr&&m.OcclusionTexture is not null)
            {
                // glTF AO uses only R, even when roughness and metalness share
                // the same image. Source 2 needs a separate grayscale input.
                using var occlusion=Decode(m.OcclusionTexture);float strength=m.OcclusionStrength;
                m.OcclusionTexture=WriteMap("occlusion",occlusion.Width,occlusion.Height,(x,y)=>
                {byte v=Channel(255+strength*(occlusion.GetPixel(x,y).Red-255));return new(v,v,v);});
                m.OcclusionStrength=1;
            }
            if((m.AuthoredPbr||m.AuthoredEmission)&&m.EmissiveTexture is null&&m.EmissiveFactor.LengthSquared()>0&&m.EmissiveStrength>0)
                m.EmissiveTexture=WriteMap("emission",1,1,(x,y)=>SKColors.White);
            if(portable)
            {
                if(m.OpacityTexture is not null)
                {
                    using var color=m.ColorTexture is null?null:Decode(m.ColorTexture);using var opacity=Decode(m.OpacityTexture);
                    int w=Math.Max(color?.Width??1,opacity.Width),h=Math.Max(color?.Height??1,opacity.Height);
                    bool packedAlpha=string.Equals(m.OpacityTexture,m.ColorTexture,StringComparison.OrdinalIgnoreCase);
                    m.ColorTexture=WriteMap("rgba",w,h,(x,y)=>{var c=Sample(color,x,y,w,h,SKColors.White);var a=Sample(opacity,x,y,w,h,SKColors.White);return new(c.Red,c.Green,c.Blue,packedAlpha?a.Alpha:Channel(c.Alpha*a.Red/255f));});
                }
                m.ColorTexture=Png(m.ColorTexture);m.NormalTexture=Png(m.NormalTexture);m.MetallicRoughnessTexture=Png(m.MetallicRoughnessTexture);m.OcclusionTexture=Png(m.OcclusionTexture);m.EmissiveTexture=Png(m.EmissiveTexture);
            }
            return m;
        }).ToArray();
    }
}
Debug: View Raw JSON Response
{
    "TotalCount": 110,
    "Files": [
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/UI/AssetOutput.cs",
            "FileName": "AssetOutput.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "using Editor;\r\nusing Sandbox;\r\nusing System.Threading.Tasks;\r\nnamespace HumanoidRigger.Editor;\r\n\r\npublic sealed record ExportResult(string[] Files)\r\n{\r\n    public string PrimaryFile=>Files.LastOrDefault(p=>p.EndsWith(\".vmdl\",StringComparison.OrdinalIgnoreCase))??Files.First();\r\n}\r\n\r\npublic static class AssetOutput\r\n{\r\n    public static async Task<string> Save(Wizard session)=>\r\n        (await Save(session,ExportRequest.Default(session.Character!.Name,Project.Current.GetAssetsPath(),session.Character.Materials.Length>0))).PrimaryFile;\r\n\r\n    public static async Task<ExportResult> Save(Wizard session,ExportRequest request)\r\n    {\r\n        if(session.Rig?.Report.Passed!=true)throw new InvalidOperationException(\"The rig must pass validation before saving.\");\r\n        var character=session.Character!;var rig=session.Rig;\r\n        if(request.Formats.HasFlag(ExportFormats.Vmdl))VmdlBoneNames.Validate(rig.Bones);\r\n        var root=Project.Current.GetAssetsPath();var plan=request.Plan(root,character.Materials.Length>0);plan.EnsureAvailable();\r\n        Directory.CreateDirectory(plan.Directory);\r\n        var written=new List<string>();bool createdMaterials=false;\r\n        void Write(string path,byte[] bytes)\r\n        {\r\n            using var file=new FileStream(path,FileMode.CreateNew,FileAccess.Write,FileShare.Read);written.Add(path);file.Write(bytes);\r\n        }\r\n        void WriteText(string path,string content)=>Write(path,System.Text.Encoding.UTF8.GetBytes(content));\r\n        try\r\n        {\r\n            var sourceMaterials=character.Materials;\r\n            IReadOnlyDictionary<int,string> vmats=new Dictionary<int,string>();\r\n            if(plan.MaterialDirectory is not null)\r\n            {\r\n                Directory.CreateDirectory(plan.MaterialDirectory);createdMaterials=true;\r\n                sourceMaterials=await Task.Run(()=>TextureFiles.Copy(character,plan.MaterialDirectory));await new EditorThread();\r\n                sourceMaterials=await Task.Run(()=>MaterialAssets.PrepareFormats(sourceMaterials,plan.MaterialDirectory,plan.Formats));await new EditorThread();\r\n                if(plan.Formats.HasFlag(ExportFormats.Vmdl))\r\n                {\r\n                    vmats=await MaterialAssets.Compile(sourceMaterials,plan.MaterialDirectory,character);\r\n                }\r\n            }\r\n            var portableMaterials=plan.MaterialDirectory is null?sourceMaterials:TextureFiles.RelativeTo(sourceMaterials,Path.GetFileName(plan.MaterialDirectory));\r\n            if(plan.Formats.HasFlag(ExportFormats.Fbx))\r\n            {\r\n                var bytes=await Task.Run(()=>FbxExporter.Write(character,rig,portableMaterials));await new EditorThread();\r\n                Write(plan.PathFor(\".fbx\"),bytes);\r\n                if(ExportRequest.IsInAssets(plan.Directory,root))AssetSystem.RegisterFile(plan.PathFor(\".fbx\"));\r\n            }\r\n            if(plan.Formats.HasFlag(ExportFormats.Gltf))\r\n            {\r\n                var output=await Task.Run(()=>GltfExporter.Write(character,rig,plan.FileName+\".bin\",portableMaterials));await new EditorThread();\r\n                Write(plan.PathFor(\".bin\"),output.Buffer);Write(plan.PathFor(\".gltf\"),output.Document);\r\n            }\r\n            if(plan.Formats.HasFlag(ExportFormats.Glb))\r\n            {\r\n                var output=await Task.Run(()=>GltfExporter.Write(character,rig,\"\",portableMaterials,true,path=>File.ReadAllBytes(Path.Combine(plan.Directory,path))));await new EditorThread();\r\n                Write(plan.PathFor(\".glb\"),output.Document);\r\n            }\r\n            if(plan.Formats.HasFlag(ExportFormats.Obj))\r\n            {\r\n                var output=await Task.Run(()=>ObjExporter.Write(character,plan.FileName+\".mtl\",portableMaterials));await new EditorThread();\r\n                WriteText(plan.PathFor(\".mtl\"),output.Materials);WriteText(plan.PathFor(\".obj\"),output.Mesh);\r\n            }\r\n            if(plan.Formats.HasFlag(ExportFormats.Vmdl))\r\n            {\r\n                var dmx=await Task.Run(()=>DmxExporter.Write(character,rig,vmats));await new EditorThread();\r\n                WriteText(plan.PathFor(\".dmx\"),dmx);\r\n                WriteText(plan.PathFor(\".vmdl\"),ModelDocExporter.Write(Path.GetRelativePath(root,plan.PathFor(\".dmx\")),rig,character,vmats.Count>0));\r\n                await NativeRigExport.Compile(plan.PathFor(\".dmx\"),plan.PathFor(\".vmdl\"),character,rig,vmats,\r\n                    content=>File.WriteAllText(plan.PathFor(\".dmx\"),content));\r\n            }\r\n            return new(plan.PrimaryFiles);\r\n        }\r\n        catch\r\n        {\r\n            // Only remove output paths this attempt created. Existing user assets are never overwritten.\r\n            foreach(var path in written)foreach(var owned in new[]{path,path+\"_c\"})try{File.Delete(owned);}catch(Exception e){Log.Warning(e.Message);}\r\n            if(createdMaterials&&ExportRequest.IsInAssets(plan.MaterialDirectory,plan.Directory))\r\n                try{Directory.Delete(plan.MaterialDirectory,true);}catch(Exception e){Log.Warning(e.Message);}\r\n            throw;\r\n        }\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/UI/RiggerWindow.cs",
            "FileName": "RiggerWindow.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "using Editor;\r\nusing Sandbox;\r\nusing System.Threading.Tasks;\r\nnamespace HumanoidRigger.Editor;\r\n\r\npublic sealed class RiggerWindow : Widget\r\n{\r\n    public const string DockTitle=\"Humanoid Rigger\";\r\n    public static RiggerWindow Instance {get;private set;}\r\n    public Wizard Session {get;}=new();\r\n    RiggerViewport viewport;Widget content,toolbar,footer,toolbarContent,footerContent;Label status;bool busy;\r\n    ImportedCharacter framedCharacter;WizardStep framedStep;\r\n    JointPanel jointPanel;\r\n    CharacterPose previewPose=CharacterPose.Auto;\r\n    ComboBox poseSelection;\r\n    bool customPose;\r\n    bool updatingPoseSelection;\r\n    IReadOnlyDictionary<string,System.Numerics.Quaternion> editedPose;\r\n    AutomaticHandRefiner handRefiner;\r\n    IReadOnlyDictionary<int,string> previewMaterials=new Dictionary<int,string>();\r\n    internal double LastAdvanceWorkMilliseconds {get;private set;}\r\n    internal double LastAdvanceUiMilliseconds {get;private set;}\r\n    internal bool LastAdvanceWorkerWasMainThread {get;private set;}\r\n    Task<Wizard> preparation;\r\n    Wizard preparingDraft;\r\n    long queuedPreparationRevision=-1;\r\n    FingerCountDialog fingerCountDialog;\r\n    public RiggerWindow(Widget parent):this(parent,true){}\r\n    internal RiggerWindow(Widget parent,bool register):base(parent)\r\n    {\r\n        if(register)Instance=this;WindowTitle=DockTitle;Name=\"HumanoidRigger\";Cursor=CursorShape.Arrow;SetWindowIcon(\"accessibility_new\");MinimumSize=new(1060,780);Size=new(1280,940);Layout=Layout.Column();EnsureHandRefiner();Build();\r\n    }\r\n    [Event(\"tools.editorwindow.createview\")]\r\n    static void RegisterViewMenu(Menu menu)\r\n    {\r\n        // Join the editor's alphabetically sorted tools without creating a dock.\r\n        EditorWindow.DockManager.RegisterDockType(new DockManager.DockInfo\r\n        {\r\n            Title=DockTitle,Icon=\"accessibility_new\",CreateAction=OpenFloatingView\r\n        });\r\n    }\r\n    static Widget OpenFloatingView(){Open();return null;}\r\n\r\n    [Event(\"tools.editorwindow.postcreateview\")]\r\n    static void ConfigureViewMenu(Menu menu)\r\n    {\r\n        var option=menu.GetOption(DockTitle);\r\n        if(option is null)return;\r\n        option.Toggled=null;option.Checkable=false;option.Triggered=Open;\r\n    }\r\n\r\n    public static void Open()\r\n    {\r\n        if(Instance.IsValid()){ShowExistingWindow(Instance);return;}\r\n        CreateFloatingWindow(DockTitle,true);\r\n    }\r\n    internal static void ShowExistingWindow(RiggerWindow window)\r\n    {\r\n        // Move sessions opened by older versions into the same themed window as new\r\n        // sessions. Preserve the widget, viewport and edits when removing the old dock.\r\n        var dock=EditorWindow.DockManager.FindDockWidget(window);\r\n        if(dock.IsValid())\r\n        {\r\n            var previous=window.GetWindow();var size=previous.Size;var position=previous.Position;bool floating=dock.IsFloating;\r\n            var dialog=CreateFloatingHost(window.WindowTitle);\r\n            // Replacing the dock content releases its native ownership before reparenting.\r\n            dock.Widget=new Widget(dock);\r\n            window.Parent=dialog;dialog.Layout.Add(window,1);\r\n            EditorWindow.DockManager.RemoveDock(dock);dock.Destroy();\r\n            if(floating){dialog.Window.Size=size;dialog.Window.Position=position;}\r\n            dialog.Show();window.Show();dialog.Window.Raise();\r\n            return;\r\n        }\r\n        var existing=window.GetWindow();existing.Show();window.Show();existing.Raise();\r\n    }\r\n    internal static Dialog CreateFloatingWindow(string title,bool register)\r\n    {\r\n        var dialog=CreateFloatingHost(title);\r\n        dialog.Layout.Add(new RiggerWindow(dialog,register),1);dialog.Show();dialog.Window.Size=new(1280,940);return dialog;\r\n    }\r\n    void Build()\r\n    {\r\n        if(Session.Rig is null){previewPose=CharacterPose.Auto;customPose=false;editedPose=null;}\r\n        if(Session.Step==WizardStep.Import)\r\n        {\r\n            content?.Destroy();viewport=null;framedCharacter=null;\r\n            content=Layout.Add(new Widget(this),1);content.Layout=Layout.Column();\r\n            var drop=new ModelDropArea(content,ImportPath);content.Layout.Add(drop,1);status=content.Layout.Add(new Label(content){Visible=false,WordWrap=true});return;\r\n        }\r\n        if(!viewport.IsValid()||!toolbar.IsValid()||!footer.IsValid()||!jointPanel.IsValid())\r\n        {\r\n            if(viewport.IsValid())viewport.Parent=this;\r\n            content?.Destroy();content=Layout.Add(new Widget(this),1);content.Layout=Layout.Column();\r\n            toolbar=content.Layout.Add(new Widget(content));toolbar.Layout=Layout.Column();\r\n            var middle=content.Layout.Add(new Widget(content),1);middle.Layout=Layout.Column();\r\n            viewport=middle.Layout.Add(viewport.IsValid()?viewport:new RiggerViewport(middle,Session),1);\r\n            // Overlay the panel so entering Body does not resize the native\r\n            // render surface. The viewport frames the character beside it.\r\n            jointPanel=new JointPanel(viewport,Session,viewport);viewport.Changed=LandmarksChanged;\r\n            // Reserve the final review's space throughout the wizard. Changing\r\n            // warnings or progress text must not repeatedly resize the swap chain.\r\n            footer=content.Layout.Add(new Widget(content){FixedHeight=160});footer.Layout=Layout.Column();\r\n        }\r\n        viewport.PoseEdited=PoseEdited;\r\n        viewport.Resized=PositionJointPanel;\r\n        toolbarContent?.Destroy();footerContent?.Destroy();\r\n        toolbarContent=toolbar.Layout.Add(new Widget(toolbar));toolbarContent.Layout=Layout.Row();\r\n        footerContent=footer.Layout.Add(new Widget(footer));footerContent.Layout=Layout.Column();\r\n        var top=toolbarContent.Layout;top.Margin=8;top.Spacing=8;\r\n        top.Add(new Label(content){Text=\"Profile:\"});\r\n        var profile=top.Add(new ComboBox(content){MinimumWidth=190,ToolTip=\"The target skeleton\u2019s bone names and hierarchy. Hand detection is independent of this choice.\"});\r\n        foreach(var p in Profiles.BuiltIn.Concat(ProfileStore.Load()))profile.AddItem(p.Name,\"person\",()=>{Session.SetProfile(p);Build();},selected:p.Id==Session.Profile.Id);\r\n        profile.AddItem(\"Create Custom Profile\u2026\",\"add\",CreateProfile);\r\n        profile.AddItem(\"Load Profile\u2026\",\"folder_open\",LoadProfile);\r\n        if(Session.Rig is not null)\r\n        {\r\n            top.Add(new Label(content){Text=\"Preview:\"});\r\n            poseSelection=top.Add(new ComboBox(content){MinimumWidth=120,ToolTip=\"Preview the generated rig in a standard pose.\"});\r\n            foreach(var (label,value) in new[]{(\"Original Pose\",CharacterPose.Auto),(\"T-Pose\",CharacterPose.TPose),(\"A-Pose 1\",CharacterPose.APose1),(\"A-Pose 2\",CharacterPose.APose2)})\r\n                poseSelection.AddItem(label,\"accessibility\",()=>SetPreviewPose(value),selected:!customPose&&value==previewPose);\r\n            if(editedPose is not null)poseSelection.AddItem(\"Custom Pose\",\"touch_app\",ShowEditedPose,selected:customPose);\r\n        }\r\n        top.AddStretchCell();top.Add(new Button(\"Replace model\",\"folder_open\"){Clicked=SelectModel});top.Add(new Button(\"Restart\",\"restart_alt\"){Clicked=RestartWorkflow});\r\n        viewport.MaterialPaths=previewMaterials;ShowPreview();\r\n        jointPanel.Visible=Session.Step!=WizardStep.Centerline;\r\n        PositionJointPanel();\r\n        jointPanel.Reload();\r\n        if(framedCharacter!=Session.Character||framedStep!=Session.Step){viewport.Frame();framedCharacter=Session.Character;framedStep=Session.Step;}\r\n        var bottom=footerContent.Layout;bottom.Margin=12;bottom.Spacing=8;\r\n        status=bottom.Add(new Label(content){WordWrap=true,Text=Instruction()});\r\n        if(Session.Step is WizardStep.Centerline or WizardStep.Body)\r\n        {\r\n            var importWarnings=Session.Character.ImportWarnings.Where(w=>!w.StartsWith(\"Detected a Z-up\")).ToArray();\r\n            if(importWarnings.Length>0)\r\n            {\r\n                var warning=bottom.Add(new Label(content){Name=\"ImportMaterialWarning\",Text=string.Join(\"\\n\",importWarnings),WordWrap=true});\r\n                warning.SetStyles($\"color: {Theme.Yellow.Hex};\");\r\n            }\r\n        }\r\n        if(Session.Step is WizardStep.Centerline or WizardStep.Body && Session.Anatomy!.UnrecommendedImportPose)\r\n        {\r\n            var warning=bottom.Add(new Label(content){Name=\"ImportPoseWarning\",Text=ImportPose.Warning,WordWrap=true});\r\n            warning.SetStyles($\"color: {Theme.Yellow.Hex};\");\r\n        }\r\n        if(Session.Step is WizardStep.LeftHand or WizardStep.RightHand)\r\n        {\r\n            var side=Session.Step==WizardStep.LeftHand?\"L\":\"R\";\r\n            foreach(var warning in Session.Anatomy!.Warnings.Where(w=>w.StartsWith(side+\" hand\")||w.StartsWith(side+\" fingers\")))\r\n            {var label=bottom.Add(new Label(content){Text=warning,WordWrap=true});label.SetStyles($\"color: {Theme.Yellow.Hex};\");}\r\n        }\r\n        if(Session.Step==WizardStep.Finish)\r\n        {\r\n            bottom.Add(new Label(content){Text=$\"Profile: {Session.Profile.Name}\"});\r\n            var deformationWarnings=Session.Rig!.Report.Issues.Where(i=>i.Code==\"surface-reversal\").Select(i=>i.Message).ToArray();\r\n            if(deformationWarnings.Length>0)\r\n            {\r\n                var warning=bottom.Add(new Label(content){Name=\"DeformationWarning\",Text=\"Some test poses need review. Open Advanced Edit for details.\",WordWrap=true});\r\n                warning.SetStyles($\"color: {Theme.Yellow.Hex};\");\r\n            }\r\n            var checks=bottom.AddRow();checks.Spacing=8;\r\n            foreach(var label in new[]{\"Body\",\"Left Hand\",\"Right Hand\",\"Skeleton\",\"Skinning\",\"Validation\"})\r\n            {\r\n                string side=label==\"Left Hand\"?\"L\":label==\"Right Hand\"?\"R\":null;\r\n                var warnings=label==\"Validation\"?deformationWarnings:side is null?[]:Session.Anatomy!.Warnings.Where(w=>w.StartsWith(side+\" hand\")||w.StartsWith(side+\" fingers\")).ToArray();\r\n                checks.Add(new StatusChip(content,label,warnings.Length>0?Theme.Yellow:Theme.Green){ToolTip=warnings.Length>0?string.Join(\"\\n\",warnings):\"Checked\"});\r\n            }\r\n            checks.AddStretchCell();\r\n            var row=bottom.AddRow();row.Spacing=8;\r\n            row.Add(new Button(\"Back\",\"arrow_back\"){Clicked=GoBack});\r\n            row.Add(new Button(\"Reset Pose\",\"restart_alt\"){Clicked=()=>SetPreviewPose(CharacterPose.Auto)});\r\n            row.Add(new Button(\"Test Rig\",\"play_arrow\"){Clicked=TestRig});row.Add(new Button(\"Advanced Edit\",\"tune\"){Clicked=Advanced});row.AddStretchCell();row.Add(new Button.Primary(\"Save\"){Icon=\"check\",Tint=Theme.Green,Clicked=Save});\r\n        }\r\n        else\r\n        {\r\n            var row=bottom.AddRow();row.Spacing=8;\r\n            if(Session.Step!=WizardStep.Centerline)row.Add(new Button(\"Back\",\"arrow_back\"){Clicked=GoBack});\r\n            row.Add(new Button(\"Reset\",\"restart_alt\"){Clicked=()=>{Session.Reset();Build();}});row.AddStretchCell();\r\n            row.Add(new Button.Primary(\"Continue\"){Enabled=Session.Step!=WizardStep.Validation,Clicked=Continue});\r\n        }\r\n        SchedulePreparation();\r\n    }\r\n    void PositionJointPanel()\r\n    {\r\n        if(!viewport.IsValid()||!jointPanel.IsValid())return;\r\n        jointPanel.Position=new(viewport.Width-jointPanel.FixedWidth,0);\r\n        jointPanel.Size=new(jointPanel.FixedWidth,viewport.Height);\r\n        viewport.RightInset=jointPanel.Visible?jointPanel.FixedWidth:0;\r\n        jointPanel.Raise();\r\n    }\r\n    void GoBack(){Session.Back();Build();}\r\n    string Instruction()=>Session.Step switch\r\n    {\r\n        WizardStep.Centerline=>\"Check the centerline.\\nDrag the line left or right to adjust it.\",\r\n        WizardStep.Body=>\"Check the points.\\nMove any point that is incorrect.\",WizardStep.LeftHand=>\"Check the left hand.\\nMove any incorrect points.\",WizardStep.RightHand=>\"Check the right hand.\\nMove any incorrect points.\",WizardStep.Finish=>\"Rig Complete\\nDrag a bone to test the rig.\",\r\n        WizardStep.Validation=>string.Join(\"\\n\",Session.Rig!.Report.Issues.Where(i=>i.Error).Select(i=>i.Message)),_=>\"Generating rig\u2026\"\r\n    };\r\n    void SelectModel(){var path=EditorUtility.OpenFileDialog(\"Select model\",ModelImporter.FileFilter,\"\");if(!string.IsNullOrEmpty(path))ImportPath(path);}\r\n    void CreateProfile()\r\n    {\r\n        var path=EditorUtility.OpenFileDialog(\"Select rigged character\",\"Rigged characters (*.fbx *.gltf *.glb)\",\"\");if(string.IsNullOrEmpty(path))return;\r\n        try{new ProfileDialog(this,ModelImporter.Import(path),p=>{Session.SetProfile(p);Build();}).Show();}catch(Exception e){Error(e);}\r\n    }\r\n    void LoadProfile()\r\n    {\r\n        var path=EditorUtility.OpenFileDialog(\"Select rig profile\",\"json\",\"\");if(string.IsNullOrEmpty(path))return;\r\n        try{var p=RigProfile.FromJson(File.ReadAllText(path));ProfileStore.Save(p);Session.SetProfile(p);Build();}catch(Exception e){Error(e);}\r\n    }\r\n    public async void ImportPath(string path)\r\n    {\r\n        try{await ImportAsync(path);}catch(Exception e){await new EditorThread();if(this.IsValid())Error(e);}\r\n    }\r\n    public async Task ImportAsync(string path)\r\n    {\r\n        if(busy)throw new InvalidOperationException(\"The current operation is still running.\");\r\n        EnsureHandRefiner();\r\n        SetBusy(true);\r\n        try\r\n        {\r\n            await RigWork.Run(()=>Session.Import(path));await new EditorThread();\r\n            if(!this.IsValid())return;\r\n            previewMaterials=new Dictionary<int,string>();\r\n            string textureWarning=null;\r\n            try{previewMaterials=await MaterialAssets.Preview(Session.Character);}\r\n            catch(Exception e){textureWarning=\"Textures: \"+e.Message;Log.Warning(textureWarning);}\r\n            await new EditorThread();if(this.IsValid()){Build();if(textureWarning is not null){status.Text+=\"\\n\"+textureWarning;status.SetStyles($\"color: {Theme.Yellow.Hex};\");}}\r\n        }\r\n        finally{await new EditorThread();SetBusy(false);}\r\n    }\r\n    void Continue()\r\n    {\r\n        if(busy)return;\r\n        if(Session.Step==WizardStep.Body)\r\n        {\r\n            if(fingerCountDialog.IsValid()&&fingerCountDialog.Visible){fingerCountDialog.Window.Raise();return;}\r\n            var revision=Session.Revision;\r\n            fingerCountDialog=new FingerCountDialog(this,Session.LeftFingerCount,Session.RightFingerCount,(left,right)=>\r\n            {\r\n                if(!this.IsValid()||Session.Step!=WizardStep.Body||Session.Revision!=revision)return;\r\n                Session.SetFingerCounts(left,right);AdvanceFromControls();\r\n            });\r\n            fingerCountDialog.Show();return;\r\n        }\r\n        AdvanceFromControls();\r\n    }\r\n    async void AdvanceFromControls()\r\n    {\r\n        try{await AdvanceAsync();}catch(Exception e){await new EditorThread();if(this.IsValid())Error(e);}\r\n    }\r\n    public async Task AdvanceAsync()\r\n    {\r\n        if(busy)throw new InvalidOperationException(\"The current operation is still running.\");\r\n        EnsureHandRefiner();\r\n        SetBusy(true);\r\n        status.Text=Session.Step switch{WizardStep.Body=>\"Preparing left hand\u2026\",WizardStep.LeftHand=>\"Preparing right hand\u2026\",WizardStep.RightHand=>\"Generating rig\u2026\",_=>Instruction()};\r\n        try\r\n        {\r\n            var started=System.Diagnostics.Stopwatch.GetTimestamp();\r\n            if(preparation is not null&&!preparation.IsCompleted&&!Session.CanAccept(preparingDraft))\r\n            {try{await preparation.ConfigureAwait(false);}catch{}await new EditorThread();}\r\n            Wizard result;\r\n            if(Session.Step==WizardStep.RightHand)\r\n            {\r\n                // Give the busy state a frame to appear, then generate on the\r\n                // editor thread. Keep a draft so a failure preserves the edits.\r\n                await Task.Delay(16).ConfigureAwait(false);await new EditorThread();\r\n                if(!this.IsValid())return;\r\n                result=Session.CopyForContinuation();\r\n                LastAdvanceWorkerWasMainThread=ThreadSafe.IsMainThread;\r\n                result.Continue();\r\n            }\r\n            else\r\n            {\r\n                if(preparation is null||!Session.CanAccept(preparingDraft)||preparation.IsFaulted)StartPreparation();\r\n                result=await preparation.ConfigureAwait(false);await new EditorThread();\r\n            }\r\n            Session.AcceptContinuation(result);\r\n            LastAdvanceWorkMilliseconds=System.Diagnostics.Stopwatch.GetElapsedTime(started).TotalMilliseconds;\r\n            started=System.Diagnostics.Stopwatch.GetTimestamp();if(this.IsValid())Build();\r\n            LastAdvanceUiMilliseconds=System.Diagnostics.Stopwatch.GetElapsedTime(started).TotalMilliseconds;\r\n        }\r\n        finally{await new EditorThread();SetBusy(false);SchedulePreparation();}\r\n    }\r\n    public void CaptureViewport(string path)=>viewport.Capture(path);\r\n    internal void RefreshDisplay()\r\n    {\r\n        // Rebind native event handlers after editor hot reload without replacing the session.\r\n        viewport?.Destroy();viewport=null;framedCharacter=null;MinimumSize=new(1060,780);Build();\r\n    }\r\n    void ShowPreview()\r\n    {\r\n        if(Session.Rig is null){viewport.ShowCharacter();return;}\r\n        viewport.SetPose(customPose?editedPose:RigPosePreview.Rotations(Session.Rig,previewPose),false);\r\n    }\r\n    public void SetPreviewPose(CharacterPose pose)\r\n    {\r\n        if(updatingPoseSelection)return;\r\n        if(busy)throw new InvalidOperationException(\"The current operation is still running.\");\r\n        if(Session.Rig is null)throw new InvalidOperationException(\"Generate the rig before previewing poses.\");\r\n        if(pose is not (CharacterPose.Auto or CharacterPose.TPose or CharacterPose.APose1 or CharacterPose.APose2))throw new ArgumentException(\"Unsupported preview pose.\");\r\n        previewPose=pose;customPose=false;viewport.SetPose(RigPosePreview.Rotations(Session.Rig,pose));\r\n        var label=pose switch{CharacterPose.Auto=>\"Original Pose\",CharacterPose.TPose=>\"T-Pose\",CharacterPose.APose1=>\"A-Pose 1\",_=>\"A-Pose 2\"};\r\n        SelectPoseLabel(label);\r\n        status.Text=Instruction();\r\n    }\r\n    void PoseEdited(IReadOnlyDictionary<string,System.Numerics.Quaternion> pose)\r\n    {\r\n        customPose=true;editedPose=new Dictionary<string,System.Numerics.Quaternion>(pose);\r\n        SelectPoseLabel(\"Custom Pose\");\r\n    }\r\n    void SelectPoseLabel(string label)\r\n    {\r\n        if(!poseSelection.IsValid())return;\r\n        // Native ComboBox selection invokes its action even for programmatic\r\n        // changes. Updating the label must not restart or cancel an active drag.\r\n        updatingPoseSelection=true;\r\n        try\r\n        {\r\n            if(poseSelection.FindIndex(label) is {} index)poseSelection.CurrentIndex=index;\r\n            else if(label==\"Custom Pose\")poseSelection.AddItem(label,\"touch_app\",ShowEditedPose,selected:true);\r\n        }\r\n        finally{updatingPoseSelection=false;}\r\n    }\r\n    void ShowEditedPose(){if(updatingPoseSelection||editedPose is null||Session.Rig is null)return;customPose=true;viewport.SetPose(editedPose);}\r\n    public void RestartWorkflow()\r\n    {\r\n        if(busy)throw new InvalidOperationException(\"The current operation is still running.\");\r\n        Session.Restart();Build();\r\n    }\r\n    void Error(Exception e){status.Visible=true;status.Text=e.Message;status.SetStyles($\"color: {Theme.Red.Hex};\");}\r\n    void SetBusy(bool value)\r\n    {\r\n        busy=value;if(!this.IsValid())return;\r\n        if(viewport.IsValid())\r\n        {\r\n            viewport.AllowEditing=!value;\r\n            if(toolbar.IsValid())toolbar.Enabled=!value;if(footer.IsValid())footer.Enabled=!value;if(jointPanel.IsValid())jointPanel.Enabled=!value;\r\n        }\r\n        else if(content.IsValid())content.Enabled=!value;\r\n    }\r\n    async void TestRig()\r\n    {\r\n        if(busy)return;SetBusy(true);\r\n        try\r\n        {\r\n            foreach(var pose in Deformation.Poses)\r\n            {\r\n                await new EditorThread();if(!this.IsValid()||Session.Rig is null)break;\r\n                if(!Deformation.IsApplicable(pose,Session.Rig.Bones.Select(b=>b.Role).ToHashSet()))continue;\r\n                viewport.SetPose(Deformation.JointRotations(Session.Rig,pose));status.Text=pose.Name;await Task.Delay(650);\r\n            }\r\n            await new EditorThread();if(this.IsValid()){viewport.SetPose(customPose?editedPose:RigPosePreview.Rotations(Session.Rig,previewPose));status.Text=Instruction();}\r\n        }\r\n        catch(Exception e){await new EditorThread();if(this.IsValid())Error(e);}finally{await new EditorThread();SetBusy(false);}\r\n    }\r\n    void Advanced()\r\n    {\r\n        var dialog=new Dialog(this);dialog.Window.WindowTitle=\"Rig details\";dialog.Window.MinimumSize=new(560,480);dialog.Layout=Layout.Column();dialog.Layout.Margin=12;dialog.Layout.Spacing=8;\r\n        var scroll=new ScrollArea(dialog);scroll.Canvas=new Widget(scroll);scroll.Canvas.Layout=Layout.Column();\r\n        foreach(var b in Session.Rig!.Bones)scroll.Canvas.Layout.Add(new Label(scroll.Canvas){Text=$\"{b.Name}  \u00b7  {b.Role}  \u00b7  {b.Position}\"});\r\n        foreach(var issue in Session.Rig.Report.Issues)scroll.Canvas.Layout.Add(new Label(scroll.Canvas){Text=issue.Message});\r\n        foreach(var hand in Session.Anatomy!.HandRefinements)scroll.Canvas.Layout.Add(new Label(scroll.Canvas){Text=$\"{(hand.Key==\"L\"?\"Left\":\"Right\")} hand: {hand.Value.Status}\"});\r\n        dialog.Layout.Add(scroll,1);dialog.Show();\r\n    }\r\n    void Save()\r\n    {\r\n        if(busy)return;\r\n        new SaveRigDialog(this,Session,result=>{if(this.IsValid()){status.Visible=true;status.Text=\"Saved \"+string.Join(\" + \",result.Files.Select(Path.GetFileName));status.ToolTip=string.Join(\"\\n\",result.Files);status.SetStyles($\"color: {Theme.Green.Hex};\");}}).Show();\r\n    }\r\n    void EnsureHandRefiner(){if(handRefiner is null){handRefiner=new AutomaticHandRefiner();handRefiner.Warmup();}Session.HandRefiner=handRefiner;}\r\n    public override void OnDestroyed(){handRefiner?.Dispose();if(Instance==this)Instance=null;base.OnDestroyed();}\r\n    static Dialog CreateFloatingHost(string title)\r\n    {\r\n        var dialog=new Dialog(null);dialog.Window.Title=title;dialog.Window.SetWindowIcon(\"accessibility_new\");\r\n        dialog.Layout=Layout.Column();dialog.Window.Size=new(1280,940);return dialog;\r\n    }\r\n    void LandmarksChanged(){jointPanel.RefreshRows();SchedulePreparation();}\r\n    void StartPreparation()\r\n    {\r\n        preparingDraft=Session.CopyForContinuation();var draft=preparingDraft;\r\n        preparation=RigWork.Run(()=>{LastAdvanceWorkerWasMainThread=ThreadSafe.IsMainThread;draft.Continue();return draft;});\r\n        _=preparation.ContinueWith(task=>{_ = task.Exception;},TaskContinuationOptions.OnlyOnFaulted);\r\n    }\r\n    void SchedulePreparation()\r\n    {\r\n        // Preparing hands is cheap; generating a full rig while its final hand\r\n        // is still being edited wastes both memory and a complete repair pass.\r\n        if(!this.IsValid()||Session.Step is not (WizardStep.Body or WizardStep.LeftHand)||queuedPreparationRevision==Session.Revision)return;\r\n        queuedPreparationRevision=Session.Revision;_=PrepareWhenIdle(Session.Revision);\r\n    }\r\n    async Task PrepareWhenIdle(long revision)\r\n    {\r\n        // Coalesce marker drags. At most one solver job per window may run at a time.\r\n        await Task.Delay(150).ConfigureAwait(false);await new EditorThread();\r\n        if(!this.IsValid()||busy||Session.Revision!=revision)return;\r\n        if(preparation is not null&&!preparation.IsCompleted)\r\n        {try{await preparation.ConfigureAwait(false);}catch{}await new EditorThread();}\r\n        if(!this.IsValid()||busy||Session.Revision!=revision)return;\r\n        if(preparingDraft is null||!Session.CanAccept(preparingDraft))StartPreparation();\r\n    }\r\n}\r\n\r\nsealed class ModelDropArea:Widget\r\n{\r\n    readonly Action<string> import;int hover;\r\n    public ModelDropArea(Widget parent,Action<string> import):base(parent)\r\n    {\r\n        this.import=import;AcceptDrops=true;Layout=Layout.Column();Layout.Margin=12;Layout.Spacing=8;Layout.AddStretchCell();\r\n        var row=Layout.AddRow();row.AddStretchCell();var center=row.AddColumn();center.Spacing=12;\r\n        center.Add(new DropFolderIcon(this));\r\n        center.Add(new Label(this){Text=\"Please drag and drop a character file here (.fbx, .obj, .gltf, .glb)\",Alignment=TextFlag.Center});\r\n        center.Add(new Label(this){Text=\"or\",Alignment=TextFlag.Center});\r\n        var choice=center.AddRow();choice.AddStretchCell();\r\n        choice.Add(new Button.Primary(\"Choose File\"){MinimumWidth=120,Clicked=()=>{var path=EditorUtility.OpenFileDialog(\"Choose File\",ModelImporter.FileFilter,\"\");if(!string.IsNullOrEmpty(path))import(path);}});\r\n        choice.AddStretchCell();\r\n        row.AddStretchCell();Layout.AddStretchCell();\r\n    }\r\n    public override void OnDragHover(DragEvent e)\r\n    {\r\n        bool valid=e.Data.HasFileOrFolder&&ModelImporter.CanImport(e.Data.FileOrFolder);hover=valid?1:-1;if(valid)e.Action=DropAction.Link;Update();\r\n    }\r\n    public override void OnDragDrop(DragEvent e){hover=0;if(e.Data.HasFileOrFolder&&ModelImporter.CanImport(e.Data.FileOrFolder)){e.Action=DropAction.Link;import(e.Data.FileOrFolder);}Update();}\r\n    public override void OnDragLeave(){hover=0;Update();}\r\n    protected override void OnPaint(){Paint.SetPen(hover==1?Theme.Green:hover<0?Theme.Red:Theme.ControlBackground.Lighten(.2f),1);Paint.SetBrush(hover==1?Theme.Green.WithAlpha(.06f):Paint.HasMouseOver?Theme.ControlBackground.Lighten(.3f):Theme.ControlBackground);Paint.DrawRect(LocalRect.Shrink(12),4);}\r\n}\r\n\r\nsealed class DropFolderIcon : Widget\r\n{\r\n    public DropFolderIcon(Widget parent):base(parent){FixedHeight=48;}\r\n    protected override void OnPaint()\r\n    {\r\n        Paint.SetPen(Theme.TextLight);\r\n        Paint.DrawIcon(new Rect((Width-40)*.5f,4,40,40),\"create_new_folder\",40);\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/Core/Analysis/BodyProportions.cs",
            "FileName": "BodyProportions.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "namespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// <summary>A low, narrow neck beneath an enlarged head provides a body-scale\r\n/// prior independent of total height. Ambiguous sections retain the original prior.</summary>\r\ninternal static class BodyProportions\r\n{\r\n    readonly record struct Section(float Y,float Width,float Depth)\r\n    {\r\n        public float Area=>Width*Depth;\r\n    }\r\n    public static float EstimateBodyHeight(IEnumerable<MeshPart> source,float bottom,float height)\r\n    {\r\n        var meshes=source.ToArray();var sections=new List<Section>();\r\n        // Intersect triangle edges rather than sampling vertices: dense faces,\r\n        // sparse neck rings and material seams should give the same cross-section.\r\n        for(int sample=0;sample<=128;sample++)\r\n        {\r\n            float y=bottom+height*(.5f+sample*.0035f);\r\n            float minX=float.PositiveInfinity,maxX=float.NegativeInfinity,minZ=float.PositiveInfinity,maxZ=float.NegativeInfinity;\r\n            int count=0;\r\n            foreach(var mesh in meshes)for(int t=0;t<mesh.Triangles.Length;t+=3)for(int edge=0;edge<3;edge++)\r\n            {\r\n                var a=mesh.Vertices[mesh.Triangles[t+edge]];var b=mesh.Vertices[mesh.Triangles[t+(edge+1)%3]];\r\n                if(!((a.Y<=y&&b.Y>y)||(b.Y<=y&&a.Y>y)))continue;\r\n                var p=Vector3.Lerp(a,b,(y-a.Y)/(b.Y-a.Y));\r\n                minX=Math.Min(minX,p.X);maxX=Math.Max(maxX,p.X);minZ=Math.Min(minZ,p.Z);maxZ=Math.Max(maxZ,p.Z);count++;\r\n            }\r\n            if(count>=4&&maxX-minX>height*.005f&&maxZ-minZ>height*.005f)sections.Add(new(y,maxX-minX,maxZ-minZ));\r\n        }\r\n        var candidates=sections.Where(s=>s.Y<bottom+height*.9f).ToArray();\r\n        if(candidates.Length==0)return height;\r\n        var narrowest=candidates.MinBy(s=>s.Area);\r\n        if(narrowest.Y>=bottom+height*.8f)return height;\r\n        int index=sections.IndexOf(narrowest),first=index,last=index;\r\n        bool SameNeck(Section a,Section b)=>b.Area<=narrowest.Area*2.5f&&Math.Abs(a.Y-b.Y)<=height*.0071f;\r\n        while(first>0&&SameNeck(sections[first],sections[first-1]))first--;\r\n        while(last+1<sections.Count&&SameNeck(sections[last],sections[last+1]))last++;\r\n        if(last-first<2)return height;\r\n        float neckY=(sections[first].Y+sections[last].Y)*.5f;\r\n        // An unusually narrow waist is not the neck if another bottleneck\r\n        // separates the shoulders and skull farther up the same silhouette.\r\n        foreach(var later in sections.Where(s=>s.Y>neckY+height*.1f&&s.Y<bottom+height*.9f))\r\n        {\r\n            bool Expanded(Section s)=>s.Width>later.Width*1.35f&&s.Depth>later.Depth*1.1f;\r\n            if(sections.Any(s=>s.Y<later.Y-height*.02f&&s.Y>later.Y-height*.07f&&Expanded(s))&&\r\n                sections.Any(s=>s.Y>later.Y+height*.02f&&s.Y<later.Y+height*.07f&&Expanded(s)))return height;\r\n        }\r\n        // Require expansion in both transverse dimensions above the neck. An arm\r\n        // silhouette or a narrow waist alone is not sufficient evidence of a head.\r\n        if(!sections.Any(s=>s.Y>Math.Max(neckY+height*.025f,bottom+height*.82f)&&s.Width>narrowest.Width*2.2f&&s.Depth>narrowest.Depth*1.5f))return height;\r\n        return Math.Min(height,(neckY-bottom)/.85f);\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/Core/Analysis/HipFitting.cs",
            "FileName": "HipFitting.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "#nullable enable annotations\r\nnamespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// <summary>Refine a high hip from the thigh centerline and its junction with\r\n/// the pelvis. Ambiguous branches and large changes retain the original prior.</summary>\r\ninternal static class HipFitting\r\n{\r\n    public static void RaiseLowHips(Anatomy anatomy,IReadOnlyList<MeshSections.Section> sections,float height,SurfaceVisibility volume)\r\n    {\r\n        var old=new[]{anatomy.Points[\"UpperLeg.L\"],anatomy.Points[\"UpperLeg.R\"]};\r\n        if(old.Any(p=>p.Corrected))return;\r\n        var candidates=new Vector3[2];float center=anatomy.SymmetryPlaneX;\r\n        for(int i=0;i<2;i++)\r\n        {\r\n            float sign=i==0?1:-1;var hip=old[i].Position;\r\n            var leg=sections.Where(s=>(s.Center.X-center)*sign>height*.02f&&Math.Abs(s.Center.X-hip.X)<height*.06f&&\r\n                s.Center.Y>hip.Y-height*.07f&&s.Center.Y<hip.Y+height*.15f&&s.Radius<height*.095f).ToArray();\r\n            var top=leg.MaxBy(s=>s.Center.Y);\r\n            if(top is null||top.Center.Y<hip.Y+height*.015f||top.MinimumRadius<height*.012f)return;\r\n            // Two separate thigh contours must actually join a central pelvis.\r\n            if(!sections.Any(s=>Math.Abs(s.Center.X-center)<height*.015f&&s.Center.Y>top.Center.Y&&s.Center.Y<top.Center.Y+height*.02f&&s.Area>top.Area*1.5f))return;\r\n            // A torso contour already overlapping the thigh is a separate shell,\r\n            // not evidence of a groin transition. Keep its anatomical prior.\r\n            if(sections.Any(s=>Math.Abs(s.Center.X-center)<height*.015f&&s.Center.Y<top.Center.Y&&s.Center.Y>top.Center.Y-height*.03f&&s.Area>top.Area*1.5f))return;\r\n            var shaft=leg.Where(s=>s.Center.Y<top.Center.Y-height*.015f&&s.Center.Y>top.Center.Y-height*.06f).ToArray();if(shaft.Length<4)return;\r\n            // A narrowing end cap belongs to a detached leg segment. Extending\r\n            // its radius would place the socket beyond its authored articulation.\r\n            if(top.MinimumRadius<shaft.Average(s=>s.MinimumRadius)*.85f)return;\r\n            var mean=Geometry.Mean(shaft.Select(s=>s.Center));float variance=shaft.Sum(s=>(s.Center.Y-mean.Y)*(s.Center.Y-mean.Y));if(variance<height*height*1e-8f)return;\r\n            var slope=shaft.Aggregate(Vector3.Zero,(sum,s)=>sum+(s.Center-mean)*(s.Center.Y-mean.Y))/variance;\r\n            var candidate=mean+slope*(top.Center.Y+top.MinimumRadius-mean.Y);\r\n            if(Vector3.Distance(candidate,hip)>height*.14f||!volume.Contains(candidate,height*.00001f))return;\r\n            candidates[i]=candidate;\r\n        }\r\n        if(Math.Abs(candidates[0].Y-candidates[1].Y)>Math.Abs(old[0].Position.Y-old[1].Position.Y)+height*.01f)return;\r\n        var pelvis=anatomy.Points[\"Pelvis\"];float lift=(candidates[0].Y+candidates[1].Y-old[0].Position.Y-old[1].Position.Y)*.5f;\r\n        var moved=pelvis.Position+Vector3.UnitY*lift;\r\n        if(!pelvis.Corrected&&volume.Contains(moved,height*.00001f))anatomy.Points[\"Pelvis\"]=pelvis with{Position=moved};\r\n        for(int i=0;i<2;i++)anatomy.Points[old[i].Role]=old[i] with{Position=candidates[i]};\r\n    }\r\n    public static void Refine(Anatomy anatomy,IReadOnlyList<MeshSections.Section> sections,float height,SurfaceVisibility volume)\r\n    {\r\n        float center=anatomy.SymmetryPlaneX;\r\n        var original=new[]{anatomy.Points[\"UpperLeg.L\"],anatomy.Points[\"UpperLeg.R\"]};\r\n        var candidates=original.Select(p=>p.Position).ToArray();\r\n        for(int index=0;index<2;index++)\r\n        {\r\n            string side=index==0?\"L\":\"R\";float sign=index==0?1:-1;\r\n            var old=original[index];if(old.Corrected)continue;\r\n            var hip=old.Position;var knee=anatomy[\"LowerLeg.\"+side];\r\n            var leg=sections.Where(s=>s.Center.Y>knee.Y+(hip.Y-knee.Y)*.35f&&s.Center.Y<hip.Y+height*.01f&&\r\n                (s.Center.X-center)*sign>height*.02f&&Math.Abs(s.Center.X-hip.X)<height*.07f&&s.Radius<height*.1f).ToArray();\r\n            var top=leg.MaxBy(s=>s.Center.Y);\r\n            if(top is null||top.Center.Y>hip.Y-height*.025f||top.MinimumRadius<height*.012f)continue;\r\n            // The final contour is distorted by the groin. Fit the shaft below\r\n            // that transition, rather than extending the pinched contour center.\r\n            var shaft=leg.Where(s=>s.Center.Y<top.Center.Y-height*.02f&&s.Center.Y>top.Center.Y-height*.08f).ToArray();\r\n            if(shaft.Length<4)continue;\r\n            var mean=Geometry.Mean(shaft.Select(s=>s.Center));\r\n            float variance=shaft.Sum(s=>MathF.Pow(s.Center.Y-mean.Y,2));if(variance<height*height*1e-8f)continue;\r\n            var slope=shaft.Aggregate(Vector3.Zero,(value,s)=>value+(s.Center-mean)*(s.Center.Y-mean.Y))/variance;\r\n            // A local inscribed radius locates the socket above the last\r\n            // separated leg section, independently of total body proportions.\r\n            float y=top.Center.Y+top.MinimumRadius;var candidate=mean+slope*(y-mean.Y);\r\n            if(!Geometry.Finite(candidate)||candidate.Y>=hip.Y||(candidate.X-center)*sign<height*.01f||\r\n                Vector3.Distance(candidate,hip)>Vector3.Distance(hip,knee)*.2f)continue;\r\n            if(!Enumerable.Range(0,21).All(i=>volume.Contains(Vector3.Lerp(knee,candidate,i/20f),height*.00001f)))continue;\r\n            candidates[index]=candidate;\r\n        }\r\n        // An isolated contour estimate cannot justify tilting the pelvis. Keep\r\n        // existing asymmetry, but reject a new height mismatch larger than the\r\n        // section sampling interval when the opposite socket lacks support.\r\n        if(Math.Abs(candidates[0].Y-candidates[1].Y)>Math.Abs(original[0].Position.Y-original[1].Position.Y)+height*.005f)return;\r\n        for(int i=0;i<2;i++)anatomy.Points[original[i].Role]=original[i] with{Position=candidates[i]};\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/Core/Export/VmdlBoneNames.cs",
            "FileName": "VmdlBoneNames.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "namespace HumanoidRigger;\r\n\r\n/// <summary>ModelDoc replaces namespace colons with underscores when importing bones.</summary>\r\npublic static class VmdlBoneNames\r\n{\r\n    public static string Convert(string name)=>name.Replace(':','_');\r\n\r\n    public static void Validate(IEnumerable<RigBone> bones)\r\n    {\r\n        var names=new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);\r\n        foreach(var bone in bones)\r\n        {\r\n            string converted=Convert(bone.Name);\r\n            if(names.TryGetValue(converted,out var previous))\r\n                throw new InvalidOperationException($\"Bone names '{previous}' and '{bone.Name}' both become '{converted}' in s&box. Rename one bone in the profile.\");\r\n            names.Add(converted,bone.Name);\r\n        }\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/Core/Analysis/Inference/HandModelAssets.cs",
            "FileName": "HandModelAssets.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "using System.IO.Compression;\r\nusing System.Net.Http;\r\nusing System.Security.Cryptography;\r\nusing System.Threading.Tasks;\r\nnamespace HumanoidRigger;\r\n\r\n/// <summary>Pinned, hash-checked model/runtime cache. Downloads never contain user model data.</summary>\r\npublic static class HandModelAssets\r\n{\r\n    public const string ModelHash=\"db0898ae717b76b075d9bf563af315b29562e11f8df5027a1ef07b02bef6d81c\";\r\n    public const string RuntimeHash=\"dec964ab1ee36cc9b0ae247d13b376627992fc57dec0454354017ab8fd84f1ea\";\r\n    public static string CacheDirectory=>Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),\"sbox-humanoid-rigger\",\"inference\",\"hand-v1\");\r\n    public static async Task<NativeHandModel> Load(string folder)\r\n    {\r\n        Directory.CreateDirectory(folder);\r\n        string model=Path.Combine(folder,\"hand.onnx\"),runtime=Path.Combine(folder,\"onnxruntime.dll\");\r\n        using var http=new HttpClient{Timeout=TimeSpan.FromSeconds(90)};\r\n        await Download(http,\"https://media.githubusercontent.com/media/opencv/opencv_zoo/25f423d0e04c31a17254620e58febd7386da523b/models/handpose_estimation_mediapipe/handpose_estimation_mediapipe_2023feb.onnx\",model,ModelHash);\r\n        if(!Valid(runtime,RuntimeHash))\r\n        {\r\n            string archive=Path.Combine(folder,\"onnxruntime-1.23.2.zip\");\r\n            await Download(http,\"https://github.com/microsoft/onnxruntime/releases/download/v1.23.2/onnxruntime-win-x64-1.23.2.zip\",archive,\"0b38df9af21834e41e73d602d90db5cb06dbd1ca618948b8f1d66d607ac9f3cd\");\r\n            using var zip=ZipFile.OpenRead(archive);\r\n            foreach(var name in new[]{\"lib/onnxruntime.dll\",\"LICENSE\",\"ThirdPartyNotices.txt\"})\r\n            {\r\n                var entry=zip.GetEntry(\"onnxruntime-win-x64-1.23.2/\"+name)??throw new InvalidDataException(\"Missing runtime asset.\");\r\n                string destination=Path.Combine(folder,Path.GetFileName(name)),temporary=destination+\".\"+Guid.NewGuid().ToString(\"N\")+\".tmp\";\r\n                try{entry.ExtractToFile(temporary);if(name.EndsWith(\".dll\")&&!Valid(temporary,RuntimeHash))throw new InvalidDataException(\"Runtime checksum mismatch.\");File.Move(temporary,destination,true);}\r\n                finally{if(File.Exists(temporary))File.Delete(temporary);}\r\n            }\r\n        }\r\n        if(!Valid(runtime,RuntimeHash)||!Valid(model,ModelHash))throw new InvalidDataException(\"Hand inference asset checksum mismatch.\");\r\n        return new NativeHandModel(runtime,model);\r\n    }\r\n    public static bool Valid(string path,string hash)\r\n    {\r\n        if(!File.Exists(path))return false;\r\n        using var stream=File.OpenRead(path);return Convert.ToHexString(SHA256.HashData(stream)).Equals(hash,StringComparison.OrdinalIgnoreCase);\r\n    }\r\n    static async Task Download(HttpClient http,string url,string path,string hash)\r\n    {\r\n        if(Valid(path,hash))return;\r\n        string temporary=path+\".\"+Guid.NewGuid().ToString(\"N\")+\".tmp\";\r\n        try\r\n        {\r\n            using var response=await http.GetAsync(url,HttpCompletionOption.ResponseHeadersRead);response.EnsureSuccessStatusCode();\r\n            using(var destination=File.Create(temporary))await response.Content.CopyToAsync(destination);\r\n            if(!Valid(temporary,hash))throw new InvalidDataException(\"Hand inference download checksum mismatch.\");\r\n            File.Move(temporary,path,true);\r\n        }\r\n        finally{if(File.Exists(temporary))File.Delete(temporary);}\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/Core/Import/Fbx/FbxNode.cs",
            "FileName": "FbxNode.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "#nullable enable annotations\r\n\r\nnamespace HumanoidRigger.Formats.Fbx;\r\n\r\n/// <summary>\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/// <list type=\"bullet\">\r\n///   <item><c>short</c> ('Y'), <c>bool</c> ('C'), <c>int</c> ('I'), <c>float</c> ('F'),\r\n///         <c>double</c> ('D'), <c>long</c> ('L')</item>\r\n///   <item><c>float[]</c> ('f'), <c>double[]</c> ('d'), <c>long[]</c> ('l'),\r\n///         <c>int[]</c> ('i'), <c>bool[]</c>-as-<c>byte[]</c> ('b')</item>\r\n///   <item><c>string</c> ('S' \u2014 kept raw, may contain the <c>\\x00\\x01</c> name/class\r\n///         separator; see <see cref=\"SplitName\"/>), <c>byte[]</c> ('R')</item>\r\n/// </list>\r\n/// ASCII files store numbers only as <c>long</c> / <c>double</c> (and arrays as\r\n/// <c>long[]</c> / <c>double[]</c>), so the typed accessors below convert tolerantly.\r\n/// </summary>\r\npublic sealed class FbxNode\r\n{\r\n    public string Name { get; }\r\n    public List<object> Properties { get; } = new();\r\n    public List<FbxNode> Children { get; } = new();\r\n\r\n    public FbxNode(string name) => Name = name;\r\n\r\n    /// <summary>First child with the given name, or null.</summary>\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    /// <summary>All children with the given name, in document order.</summary>\r\n    public IEnumerable<FbxNode> 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    /// <summary>\r\n    /// Property <paramref name=\"i\"/> converted to <typeparamref name=\"T\"/>.\r\n    /// Numeric scalars convert tolerantly across widths (e.g. an 'I' i32 read as long);\r\n    /// anything else must match the stored type exactly.\r\n    /// </summary>\r\n    public T Prop<T>(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&box whitelist: Type.IsPrimitive is banned; enumerate the convertible targets.\r\n        if (v is IConvertible && (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&box-whitelisted\r\n            catch (Exception ex) when (ex is InvalidCastException or ArithmeticException or FormatException)\r\n            {\r\n                throw new FormatException(\r\n                    $\"FBX node '{Name}': property {i} is {v.GetType().Name}, not convertible to {target.Name}.\", ex);\r\n            }\r\n        }\r\n\r\n        throw new FormatException(\r\n            $\"FBX node '{Name}': property {i} is {v.GetType().Name}, expected {target.Name}.\");\r\n    }\r\n\r\n    /// <summary>Property <paramref name=\"i\"/> as a double array (converts f/l/i/b arrays).</summary>\r\n    public double[] AsDoubleArray(int i) => RawProp(i) switch\r\n    {\r\n        double[] d => d,\r\n        float[] f => Array.ConvertAll(f, x => (double)x),\r\n        long[] l => Array.ConvertAll(l, x => (double)x),\r\n        int[] n => Array.ConvertAll(n, x => (double)x),\r\n        byte[] b => Array.ConvertAll(b, x => (double)x),\r\n        bool[] o => Array.ConvertAll(o, x => x ? 1.0 : 0.0),\r\n        var v => throw TypeError(i, v, \"double[]\"),\r\n    };\r\n\r\n    /// <summary>Property <paramref name=\"i\"/> as a float array (converts d/l/i/b arrays).</summary>\r\n    public float[] AsFloatArray(int i) => RawProp(i) switch\r\n    {\r\n        float[] f => f,\r\n        double[] d => Array.ConvertAll(d, x => (float)x),\r\n        long[] l => Array.ConvertAll(l, x => (float)x),\r\n        int[] n => Array.ConvertAll(n, x => (float)x),\r\n        byte[] b => Array.ConvertAll(b, x => (float)x),\r\n        bool[] o => Array.ConvertAll(o, x => x ? 1f : 0f),\r\n        var v => throw TypeError(i, v, \"float[]\"),\r\n    };\r\n\r\n    /// <summary>Property <paramref name=\"i\"/> as a long array (converts i/b; d/f if integral).</summary>\r\n    public long[] AsLongArray(int i) => RawProp(i) switch\r\n    {\r\n        long[] l => l,\r\n        int[] n => Array.ConvertAll(n, x => (long)x),\r\n        byte[] b => Array.ConvertAll(b, x => (long)x),\r\n        bool[] o => Array.ConvertAll(o, x => x ? 1L : 0L),\r\n        double[] d => Array.ConvertAll(d, x => checked((long)x)),\r\n        float[] f => Array.ConvertAll(f, x => checked((long)x)),\r\n        var v => throw TypeError(i, v, \"long[]\"),\r\n    };\r\n\r\n    /// <summary>Property <paramref name=\"i\"/> as an int array (converts b; l/d/f narrowing-checked).</summary>\r\n    public int[] AsIntArray(int i) => RawProp(i) switch\r\n    {\r\n        int[] n => n,\r\n        long[] l => Array.ConvertAll(l, x => checked((int)x)),\r\n        byte[] b => Array.ConvertAll(b, x => (int)x),\r\n        bool[] o => Array.ConvertAll(o, x => x ? 1 : 0),\r\n        double[] d => Array.ConvertAll(d, x => checked((int)x)),\r\n        float[] f => Array.ConvertAll(f, x => checked((int)x)),\r\n        var v => throw TypeError(i, v, \"int[]\"),\r\n    };\r\n\r\n    /// <summary>Property <paramref name=\"i\"/> as raw bytes ('R' blobs or 'b' bool arrays).</summary>\r\n    public byte[] AsByteArray(int i) => RawProp(i) switch\r\n    {\r\n        byte[] b => b,\r\n        var v => throw TypeError(i, v, \"byte[]\"),\r\n    };\r\n\r\n    /// <summary>Property <paramref name=\"i\"/> as a string (raw 'S' content, separators intact).</summary>\r\n    public string AsString(int i) => RawProp(i) switch\r\n    {\r\n        string s => s,\r\n        var v => throw TypeError(i, v, \"string\"),\r\n    };\r\n\r\n    /// <summary>\r\n    /// Splits an FBX object name into (name, class).\r\n    /// Binary files store <c>\"Name\\x00\\x01Class\"</c> (e.g. <c>\"mixamorig:Hips\\x00\\x01Model\"</c>);\r\n    /// ASCII files store <c>\"Class::Name\"</c> (e.g. <c>\"Model::pelvis\"</c>).\r\n    /// A plain string with neither separator yields (name, \"\").\r\n    /// </summary>\r\n    public static (string Name, string Class) SplitName(string raw)\r\n    {\r\n        int bin = raw.IndexOf(\"\\0\\x01\", StringComparison.Ordinal);\r\n        if (bin >= 0)\r\n            return (raw[..bin], raw[(bin + 2)..]);\r\n\r\n        int ascii = raw.IndexOf(\"::\", StringComparison.Ordinal);\r\n        if (ascii >= 0)\r\n            return (raw[(ascii + 2)..], raw[..ascii]);\r\n\r\n        return (raw, \"\");\r\n    }\r\n\r\n    /// <summary>Primitive scalar types <see cref=\"Prop{T}\"/> converts to (whitelist-safe IsPrimitive substitute).</summary>\r\n    private static readonly HashSet<Type> 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 < 0 || i >= Properties.Count)\r\n            throw new FormatException(\r\n                $\"FBX node '{Name}': property index {i} out of range (has {Properties.Count}).\");\r\n        return Properties[i];\r\n    }\r\n\r\n    private FormatException TypeError(int i, object v, string wanted) =>\r\n        new($\"FBX node '{Name}': property {i} is {v.GetType().Name}, expected {wanted}.\");\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/Core/Validation/JointWeightRepair.cs",
            "FileName": "JointWeightRepair.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "#nullable enable annotations\r\nnamespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// <summary>Regularize a limb's blend with its parent using the measured joint\r\n/// thickness. Trials retain connectivity and are accepted after complete stress\r\n/// testing and local repair; reviewed bones and source geometry never move.</summary>\r\ninternal static class JointWeightRepair\r\n{\r\n    internal static GeneratedRig Improve(ImportedCharacter character,GeneratedRig rig,ValidationGeometry? geometry=null)\r\n    {\r\n        var roles=rig.Bones.Select(b=>b.Role).ToHashSet();\r\n        var specifications=(geometry?.Poses??Deformation.Poses).Where(p=>Deformation.IsApplicable(p,roles)).ToArray();\r\n        var expected=specifications.Select(p=>p.Name).Order().ToArray();\r\n        if(!WeightRepair.HasCompleteEvidence(rig.Report,expected)||rig.Report.StressTests.All(p=>p.ReversedTriangles==0))return rig;\r\n        geometry??=new ValidationGeometry(character);var faces=geometry.Faces;\r\n        var buffer=character.Meshes.Select(m=>new Vector3[m.Vertices.Length]).ToArray();\r\n        float height=character.AnatomicalHeight;\r\n        for(int pass=0;pass<4;pass++)\r\n        {\r\n            int before=rig.Report.StressTests.Sum(p=>p.ReversedTriangles);\r\n            foreach(var scheduled in rig.Report.StressTests.Where(p=>p.ReversedTriangles>0)\r\n                .OrderByDescending(p=>p.MaximumStretch>4||p.MinimumAreaRatio<.025f).ThenByDescending(p=>p.ReversedAreaFraction).ToArray())\r\n            {\r\n                var stress=rig.Report.StressTests.Single(p=>p.Pose==scheduled.Pose);\r\n                if(stress.ReversedTriangles==0)continue;\r\n                var specification=specifications.Single(p=>p.Name==stress.Pose);\r\n                if(geometry.Poses.Count==Deformation.Poses.Count&&!new[]{\"UpperArm.\",\"LowerArm.\",\"UpperLeg.\",\"LowerLeg.\"}.Any(specification.Role.StartsWith))continue;\r\n                int joint=Array.FindIndex(rig.Bones,b=>b.Role==specification.Role);var bone=rig.Bones[joint];\r\n                var child=rig.Bones.FirstOrDefault(b=>b.Parent==joint&&b.Deform);\r\n                if(child is null||bone.Parent<0||!rig.Bones[bone.Parent].Deform||Vector3.DistanceSquared(child.Position,bone.Position)<1e-8f)continue;\r\n                var axis=Vector3.Normalize(child.Position-bone.Position);var moving=new bool[rig.Bones.Length];moving[joint]=true;\r\n                for(int i=joint+1;i<moving.Length;i++)moving[i]=rig.Bones[i].Parent>=0&&moving[rig.Bones[i].Parent];\r\n                float sum=0;int count=0;\r\n                foreach(var mesh in character.Meshes)foreach(var p in mesh.Vertices)\r\n                {\r\n                    var delta=p-bone.Position;float along=Vector3.Dot(delta,axis),radial=(delta-axis*along).Length();\r\n                    if(Math.Abs(along)<height*.006f&&radial<height*.07f){sum+=radial;count++;}\r\n                }\r\n                float radius=Math.Clamp(count>0?sum/count:height*.025f,height*.01f,height*.065f);\r\n                var totals=rig.Weights.Select(part=>part.Select(weights=>MovingTotal(weights,moving)).ToArray()).ToArray();\r\n                var axial=character.Meshes.Select(mesh=>mesh.Vertices.Select(point=>Vector3.Dot(point-bone.Position,axis)).ToArray()).ToArray();\r\n                var trials=new List<(GeneratedRig Rig,StressResult Stress)>();\r\n                var blends=stress.ReversedTriangles<16?new[]{-.025f,-.05f,-.1f,-.2f,-.35f,.025f,.05f,.1f,.2f,.35f}:new[]{.5f,1f};\r\n                foreach(float width in new[]{2f,3f,4f,6f})foreach(float blend in blends)\r\n                {\r\n                    var weights=character.Meshes.Select((mesh,part)=>mesh.Vertices.Select((point,vertex)=>\r\n                        Blend(rig.Weights[part][vertex],rig.Profile.MaximumInfluences,rig.Bones.Length,moving,bone.Parent,\r\n                            totals[part][vertex],axial[part][vertex]/(2*width*radius),blend)).ToArray()).ToArray();\r\n                    var candidate=new GeneratedRig{Profile=rig.Profile,Bones=rig.Bones,Weights=weights,Anatomy=rig.Anatomy};\r\n                    var result=RigValidator.MeasurePose(character,candidate,specification,faces,buffer,height);\r\n                    if(result.ReversedTriangles>=stress.ReversedTriangles||result.ReversedAreaFraction>stress.ReversedAreaFraction)continue;\r\n                    trials.Add((candidate,result));\r\n                    // Only the best three trials are tested below. Release other\r\n                    // dense weight buffers immediately, preserving stable tie order.\r\n                    if(trials.Count>3)trials=trials.OrderBy(t=>t.Stress.ReversedTriangles).ThenBy(t=>t.Stress.ReversedAreaFraction).Take(3).ToList();\r\n                }\r\n                // A promising broad correction can expose a local seam. Run the\r\n                // normal cleanup and repair before deciding whether it is better.\r\n                foreach(var trial in trials.OrderBy(t=>t.Stress.ReversedTriangles).ThenBy(t=>t.Stress.ReversedAreaFraction).Take(3))\r\n                {\r\n                    var report=RigValidator.ValidateAndRepair(character,trial.Rig,geometry);\r\n                    if(!SkeletonSolver.BetterSkinning(report,rig.Report,expected))continue;\r\n                    report.Repairs+=rig.Report.Repairs;report.RepairPasses+=rig.Report.RepairPasses+1;\r\n                    trial.Rig.Report=report;rig=trial.Rig;break;\r\n                }\r\n            }\r\n            if(before==rig.Report.StressTests.Sum(p=>p.ReversedTriangles))break;\r\n        }\r\n        return rig;\r\n    }\r\n    static float MovingTotal(Influence[] weights,bool[] moving)\r\n    {\r\n        double total=0;foreach(var w in weights)if(moving[w.Bone])total+=w.Weight;\r\n        return(float)total;\r\n    }\r\n    static Influence[] Blend(Influence[] source,int maximum,int boneCount,bool[] moving,int parent,float total,float axial,float amount)\r\n    {\r\n        if(total<.0001f)return source;\r\n        float t=Math.Clamp(.5f+axial,0,1),envelope=t*t*(3-2*t);\r\n        if(amount<0)\r\n        {\r\n            if(total>.9999f)return source;\r\n            float target=total+(-amount)*total*(1-total)*(1-envelope);\r\n            var scaled=new Influence[source.Length];\r\n            for(int i=0;i<source.Length;i++){var w=source[i];scaled[i]=w with{Weight=w.Weight*(moving[w.Bone]?target/total:(1-target)/(1-total))};}\r\n            return Skinning.Cleanup(scaled,boneCount,maximum);\r\n        }\r\n        float retained=1-amount+envelope*amount;\r\n        var blended=new Influence[source.Length+1];\r\n        for(int i=0;i<source.Length;i++){var w=source[i];blended[i]=w with{Weight=w.Weight*(moving[w.Bone]?retained:1)};}\r\n        blended[^1]=new(parent,total*(1-retained));\r\n        return Skinning.Cleanup(blended,boneCount,maximum);\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/Core/Validation/JointCoverage.cs",
            "FileName": "JointCoverage.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "namespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// <summary>Check every deforming joint in both directions on each axis.\r\n/// Only faces reached by that joint's skin weights need geometric measurement.</summary>\r\ninternal static class JointCoverage\r\n{\r\n    internal static (StressPose Pose,StressResult Result)[] Measure(ImportedCharacter character,GeneratedRig rig,ValidationGeometry geometry)\r\n    {\r\n        var result=new List<(StressPose,StressResult)>();\r\n        var buffer=character.Meshes.Select(m=>new Vector3[m.Vertices.Length]).ToArray();\r\n        for(int joint=0;joint<rig.Bones.Length;joint++)\r\n        {\r\n            if(!rig.Bones[joint].Deform)continue;\r\n            var moving=new bool[rig.Bones.Length];moving[joint]=true;\r\n            for(int b=joint+1;b<moving.Length;b++)moving[b]=rig.Bones[b].Parent>=0&&moving[rig.Bones[b].Parent];\r\n            var touched=rig.Weights.Select(p=>p.Select(w=>w.Any(i=>moving[i.Bone])).ToArray()).ToArray();\r\n            var faces=geometry.Faces.Select((p,m)=>p.Where(f=>touched[m][f.A]||touched[m][f.B]||touched[m][f.C]).ToArray()).ToArray();\r\n            foreach(var (axis,name) in new[]{(Vector3.UnitX,\"X\"),(Vector3.UnitY,\"Y\"),(Vector3.UnitZ,\"Z\")})foreach(float degrees in new[]{-30f,30f})\r\n            {\r\n                var pose=new StressPose($\"Joint {rig.Bones[joint].Role} {name} {degrees:+0;-0}\",rig.Bones[joint].Role,axis,degrees);\r\n                result.Add((pose,RigValidator.MeasurePose(character,rig,pose,faces,buffer,geometry.Height)));\r\n            }\r\n        }\r\n        return result.ToArray();\r\n    }\r\n    static bool Bad(StressResult result)=>result.ReversedTriangles>0||result.NonFiniteVertices>0||result.NonFiniteMeasurements>0||result.MaximumStretch>4||result.MinimumAreaRatio<.025f;\r\n    static double Score(IEnumerable<StressResult> results)=>results.Sum(r=>r.ReversedTriangles+1000000d*(r.NonFiniteVertices+r.NonFiniteMeasurements)+100*Math.Max(0,r.MaximumStretch/4-1)+100*Math.Max(0,1-r.MinimumAreaRatio/.025f));\r\n    internal static GeneratedRig Improve(ImportedCharacter character,GeneratedRig rig)\r\n    {\r\n        if(!rig.Report.Passed)return rig;\r\n        var geometry=new ValidationGeometry(character);var checks=Measure(character,rig,geometry);\r\n        if(checks.Any(c=>Bad(c.Result)))\r\n        {\r\n            var repaired=PoseWeightRepair.Improve(character,rig,geometry,checks);\r\n            if(!ReferenceEquals(repaired,rig)){rig=repaired;checks=Measure(character,rig,geometry);}\r\n        }\r\n        var trunk=new TrunkRegion(character,rig);\r\n        var normalHeat=new Lazy<Influence[][][]>(()=>HeatSkinning.Candidates(character,rig,normalPrior:true,trunk:trunk).First());\r\n        var heat=new Lazy<Influence[][][]>(()=>HeatSkinning.Solve(character,rig));\r\n        var constraints=new Dictionary<string,StressPose>();\r\n        for(int pass=0;pass<4&&checks.Any(c=>Bad(c.Result));pass++)\r\n        {\r\n            var failed=checks.Where(c=>Bad(c.Result)).OrderByDescending(c=>c.Result.ReversedTriangles).Take(24).Select(c=>c.Pose).ToArray();\r\n            // Retain previously discovered failures after they are repaired.\r\n            // Otherwise a spine correction can undo the adjacent chest repair.\r\n            foreach(var pose in failed)constraints.TryAdd(pose.Name,pose);\r\n            var scope=new ValidationGeometry(character,Deformation.Poses.Concat(constraints.Values).ToArray(),trunk);\r\n            var candidate=new GeneratedRig{Profile=rig.Profile,Bones=rig.Bones,Anatomy=rig.Anatomy,Weights=rig.Weights.Select(p=>(Influence[][])p.Clone()).ToArray()};\r\n            candidate.Report=RigValidator.ValidateAndRepair(character,candidate,scope);\r\n            candidate=JointWeightRepair.Improve(character,candidate,scope);\r\n            if(candidate.Report.StressTests.Any(p=>p.ReversedTriangles>0))\r\n                RefineSources(character,candidate,scope,failed,normalHeat,heat);\r\n            if(!candidate.Report.Passed||trunk.HasBleeding(character,candidate))break;\r\n            var standard=RigValidator.Validate(character,candidate);\r\n            if(!standard.Passed||standard.StressTests.Zip(rig.Report.StressTests).Any(p=>p.First.ReversedTriangles>p.Second.ReversedTriangles||p.First.ReversedAreaFraction>p.Second.ReversedAreaFraction+1e-7f))break;\r\n            var next=Measure(character,candidate,geometry);\r\n            bool discovered=false;\r\n            foreach(var check in next.Where(c=>Bad(c.Result)))discovered|=constraints.TryAdd(check.Pose.Name,check.Pose);\r\n            if(Score(next.Select(c=>c.Result))>=Score(checks.Select(c=>c.Result)))\r\n            {if(discovered)continue;break;}\r\n            standard.Repairs=rig.Report.Repairs+candidate.Report.Repairs;standard.RepairPasses=rig.Report.RepairPasses+candidate.Report.RepairPasses+1;\r\n            candidate.Report=standard;rig=candidate;checks=next;\r\n        }\r\n        rig.Report.JointStressTests.AddRange(checks.Select(c=>c.Result));\r\n        var remaining=checks.Where(c=>Bad(c.Result)).ToArray();\r\n        if(remaining.Length>0)rig.Report.Issues.Add(new(\"joint-deformation\",$\"{remaining.Length} joint motions still have unsafe deformation: {string.Join(\", \",remaining.Select(c=>c.Pose.Role).Distinct())}.\",true));\r\n        return rig;\r\n    }\r\n    static void RefineSources(ImportedCharacter character,GeneratedRig rig,ValidationGeometry geometry,StressPose[] failed,params Lazy<Influence[][][]>[] fields)\r\n    {\r\n        var roots=failed.Select(p=>p.Role).ToHashSet();var moving=new bool[rig.Bones.Length];\r\n        for(int b=0;b<moving.Length;b++)moving[b]=roots.Contains(rig.Bones[b].Role)||rig.Bones[b].Parent>=0&&moving[rig.Bones[b].Parent];\r\n        foreach(var field in fields)\r\n        {\r\n            Influence[][][] source;\r\n            try{source=field.Value;}catch(InvalidOperationException){continue;}\r\n            foreach(float amount in new[]{1f,.5f,.25f})\r\n            {\r\n                var proposed=rig.Weights.Select(p=>(Influence[][])p.Clone()).ToArray();\r\n                for(int p=0;p<proposed.Length;p++)for(int v=0;v<proposed[p].Length;v++)\r\n                {\r\n                    float support=Math.Min(1,rig.Weights[p][v].Where(w=>moving[w.Bone]).Sum(w=>w.Weight)+source[p][v].Where(w=>moving[w.Bone]).Sum(w=>w.Weight))*amount;\r\n                    if(support<.001f)continue;\r\n                    var weights=Skinning.Cleanup(rig.Weights[p][v].Select(w=>w with{Weight=w.Weight*(1-support)})\r\n                        .Concat(source[p][v].Select(w=>w with{Weight=w.Weight*support})),rig.Bones.Length,rig.Profile.MaximumInfluences);\r\n                    if(geometry.Trunk?.Allows(p,v,character.Meshes[p].Vertices[v],weights)==false)continue;\r\n                    proposed[p][v]=weights;\r\n                }\r\n                rig.Report=SurfaceRepair.TryWeights(character,rig,rig.Report,proposed,geometry);\r\n                rig.Report=SurfaceRepair.Improve(character,rig,rig.Report,geometry);\r\n                if(rig.Report.StressTests.All(p=>p.ReversedTriangles==0))return;\r\n            }\r\n        }\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/Core/Usings.cs",
            "FileName": "Usings.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "global using System;\r\nglobal using System.Collections.Generic;\r\nglobal using System.Linq;\r\nglobal using System.IO;\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/UI/MaterialAssets.Sidecars.cs",
            "FileName": "MaterialAssets.Sidecars.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "// Copied from humanoid-retargeter Editor/HumanoidRetargeter/EditorPipeline.cs.\r\nusing Editor;\r\nusing Sandbox;\r\nnamespace HumanoidRigger.Editor;\r\ninternal static partial class MaterialAssets\r\n{\r\n\tstatic readonly string[] TextureExtensions =\r\n\t\t{ \".png\", \".jpg\", \".jpeg\", \".tga\", \".dds\", \".webp\", \".vmat\", \".vtex\" };\r\n\r\n\t/// <summary>Copies texture sidecars of a picked target model into the output folder:\r\n\t/// loose image files next to it, and a \"textures\" folder next to it or next to its\r\n\t/// parent (the source/-plus-textures/ layout). Per-file best effort - a failed texture\r\n\t/// must never fail the conversion.</summary>\r\n\tstatic void CopySidecarTextures( string sourceDir, string destDir )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( sourceDir is null || destDir is null )\r\n\t\t\t\treturn;\r\n\t\t\tsourceDir = Path.GetFullPath( sourceDir );\r\n\t\t\tdestDir = Path.GetFullPath( destDir );\r\n\t\t\tif ( string.Equals( sourceDir, destDir, StringComparison.OrdinalIgnoreCase ) )\r\n\t\t\t\treturn;\r\n\r\n\t\t\t// Every copied file must be REGISTERED: assets copied onto disk mid-session are\r\n\t\t\t// unknown to the asset system, so the material chain cannot generate their vtex\r\n\t\t\t// resources - the renderer then logs \"Texture manager doesn't know about\r\n\t\t\t// texture ...generated.vtex\" MANY TIMES PER FRAME, which is both the\r\n\t\t\t// purple/black flicker and a preview running at ~2 fps (user report).\r\n\t\t\tforeach ( var file in Directory.GetFiles( sourceDir ) )\r\n\t\t\t{\r\n\t\t\t\tif ( !TextureExtensions.Contains( Path.GetExtension( file ).ToLowerInvariant() ) )\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\tvar destFile = Path.Combine( destDir, Path.GetFileName( file ) );\r\n\t\t\t\tTry( () => { File.Copy( file, destFile, true ); return true; } );\r\n\t\t\t\tTry( () => AssetSystem.RegisterFile( destFile ) );\r\n\t\t\t}\r\n\r\n\t\t\tforeach ( var candidate in new[]\r\n\t\t\t{\r\n\t\t\t\tPath.Combine( sourceDir, \"textures\" ),\r\n\t\t\t\tPath.Combine( Path.GetDirectoryName( sourceDir ) ?? sourceDir, \"textures\" ),\r\n\t\t\t} )\r\n\t\t\t{\r\n\t\t\t\tif ( !Directory.Exists( candidate ) )\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\tvar destTextures = Path.Combine( destDir, \"textures\" );\r\n\t\t\t\tDirectory.CreateDirectory( destTextures );\r\n\t\t\t\tforeach ( var file in Directory.GetFiles( candidate, \"*\", SearchOption.AllDirectories ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar relative = Path.GetRelativePath( candidate, file );\r\n\t\t\t\t\tvar destFile = Path.Combine( destTextures, relative );\r\n\t\t\t\t\tTry( () =>\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tDirectory.CreateDirectory( Path.GetDirectoryName( destFile ) );\r\n\t\t\t\t\t\tFile.Copy( file, destFile, true );\r\n\t\t\t\t\t\treturn true;\r\n\t\t\t\t\t} );\r\n\t\t\t\t\tTry( () => AssetSystem.RegisterFile( destFile ) );\r\n\t\t\t\t}\r\n\t\t\t\tbreak; // first existing candidate wins\r\n\t\t\t}\r\n\t\t}\r\n\t\tcatch ( Exception e )\r\n\t\t{\r\n\t\t\tLog.Warning( $\"[sbox-humanoid-rigger] sidecar texture copy failed: {e.Message}\" );\r\n\t\t}\r\n\t}\r\n\r\n\r\nstatic T Try<T>(Func<T> action){try{return action();}catch(Exception e){Log.Warning(e.Message);return default;}}\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/UI/StatusChip.cs",
            "FileName": "StatusChip.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "using Editor;\r\nusing Sandbox;\r\n\r\nnamespace HumanoidRigger.Editor;\r\n\r\n/// <summary>Copied from Humanoid Retargeter's RetargetWindow.Chip.\r\n/// Keep its dimensions, typography, fill and radius aligned with the suite.</summary>\r\nsealed class StatusChip : Widget\r\n{\r\n    readonly string text;\r\n    readonly Color color;\r\n\r\n    public StatusChip(Widget parent,string text,Color color) : base(parent)\r\n    {\r\n        this.text=text;\r\n        this.color=color;\r\n        FixedHeight=20;\r\n        FixedWidth=7.2f*text.Length+18;\r\n    }\r\n\r\n    protected override void OnPaint()\r\n    {\r\n        Paint.ClearPen();\r\n        Paint.SetBrush(color.WithAlpha(.18f));\r\n        Paint.DrawRect(LocalRect,LocalRect.Height*.5f);\r\n        Paint.SetPen(color);\r\n        Paint.SetDefaultFont(7,600);\r\n        Paint.DrawText(LocalRect,text);\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/Core/Analysis/HumanoidFacing.cs",
            "FileName": "HumanoidFacing.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "namespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// <summary>Resolve up from body geometry, then an unambiguous quarter-turn\r\n/// from separated lower legs. Feet select the forward sign.</summary>\r\ninternal static class HumanoidFacing\r\n{\r\n    public static ImportedCharacter Normalize(ImportedCharacter character)\r\n    {\r\n        var up=HumanoidUp.Find(character);\r\n        if(up!=Vector3.UnitY)\r\n        {\r\n            Vector3 Upright(Vector3 p)=>up==Vector3.UnitX?new(-p.Y,p.X,p.Z)\r\n                :up==-Vector3.UnitX?new(p.Y,-p.X,p.Z)\r\n                :up==-Vector3.UnitY?new(p.X,-p.Y,-p.Z)\r\n                :up==Vector3.UnitZ?new(p.X,p.Z,-p.Y):new(p.X,-p.Z,p.Y);\r\n            character=Reorient(character,Upright,\"The character's up direction was corrected from its body geometry.\");\r\n        }\r\n        var body=character.Meshes.Where(m=>m.Kind==MeshKind.Body).SelectMany(m=>m.Vertices).ToArray();\r\n        if(body.Length<100)return character;\r\n        float bottom=body.Min(p=>p.Y),height=body.Max(p=>p.Y)-bottom;\r\n        var legs=body.Where(p=>p.Y>bottom+height*.15f&&p.Y<bottom+height*.35f).Distinct().ToArray();\r\n        if(legs.Length<20)return character;\r\n        var center=Geometry.Mean(legs);\r\n        float xVariance=legs.Average(p=>(p.X-center.X)*(p.X-center.X));\r\n        float zVariance=legs.Average(p=>(p.Z-center.Z)*(p.Z-center.Z));\r\n        if(zVariance<xVariance*3)return character;\r\n        float middle=(BodyDetector.Quantile(legs.Select(p=>p.Z),.1f)+BodyDetector.Quantile(legs.Select(p=>p.Z),.9f))*.5f;\r\n        if(legs.Count(p=>Math.Abs(p.Z-middle)<height*.012f)>legs.Length*.15f)return character;\r\n        var feet=body.Where(p=>p.Y<bottom+height*.07f).ToArray();\r\n        if(feet.Length<8)return character;\r\n        float forward=(BodyDetector.Quantile(feet.Select(p=>p.X),.1f)+BodyDetector.Quantile(feet.Select(p=>p.X),.9f))*.5f-center.X;\r\n        forward=SeparatedFeetForward(character,bottom,height,middle)??forward;\r\n        if(Math.Abs(forward)<height*.012f)return character;\r\n        float sign=forward<0?1:-1;\r\n        Vector3 Turn(Vector3 p)=>new(sign*p.Z,p.Y,-sign*p.X);\r\n        return Reorient(character,Turn,\"The character was turned to face forward.\");\r\n    }\r\n    static float? SeparatedFeetForward(ImportedCharacter character,float bottom,float height,float middle)\r\n    {\r\n        // Long hands can reach the floor. If the lower slice contains more than\r\n        // two limbs, follow the inner calf surfaces down to their own feet.\r\n        var origin=(character.Minimum+character.Maximum)*.5f;origin.Y=bottom+height*.25f;\r\n        var sections=MeshSections.Cut(character,origin,Vector3.UnitY,(character.Maximum-character.Minimum).Length(),height*1e-5f);\r\n        if(sections.Length<=2)return null;\r\n        var left=sections.Where(s=>s.Center.Z>middle+height*.025f).OrderBy(s=>s.Center.Z).FirstOrDefault();\r\n        var right=sections.Where(s=>s.Center.Z<middle-height*.025f).OrderByDescending(s=>s.Center.Z).FirstOrDefault();\r\n        if(left is null||right is null)return null;\r\n        var mesh=Geometry.Merge(character.Meshes.Where(m=>m.Kind==MeshKind.Body));\r\n        var neighbors=Geometry.Neighbors(mesh,height*1e-5f);var visited=new bool[mesh.Vertices.Length];var queue=new Queue<int>();\r\n        foreach(var section in new[]{left,right})\r\n        {\r\n            int seed=-1;float distance=float.PositiveInfinity;\r\n            for(int i=0;i<mesh.Vertices.Length;i++)\r\n            {\r\n                float candidate=Vector3.DistanceSquared(mesh.Vertices[i],section.Center);\r\n                if(candidate<distance){seed=i;distance=candidate;}\r\n            }\r\n            if(seed<0||distance>height*height*.08f*.08f)return null;\r\n            if(!visited[seed]){visited[seed]=true;queue.Enqueue(seed);}\r\n        }\r\n        while(queue.TryDequeue(out int vertex))foreach(int next in neighbors[vertex])\r\n            if(!visited[next]&&mesh.Vertices[next].Y<bottom+height*.4f){visited[next]=true;queue.Enqueue(next);}\r\n        var feet=mesh.Vertices.Where((p,i)=>visited[i]&&p.Y<bottom+height*.07f).Select(p=>p.X).ToArray();\r\n        if(feet.Length<8)return null;\r\n        return(BodyDetector.Quantile(feet,.1f)+BodyDetector.Quantile(feet,.9f))*.5f-(left.Center.X+right.Center.X)*.5f;\r\n    }\r\n    static ImportedCharacter Reorient(ImportedCharacter character,Func<Vector3,Vector3> turn,string warning)\r\n    {\r\n        return new ImportedCharacter{Name=character.Name,SourcePath=character.SourcePath,SourceUnitCm=character.SourceUnitCm,SourceUpAxis=character.SourceUpAxis,\r\n            HasExistingSkin=character.HasExistingSkin,ExistingBones=character.ExistingBones.Select(b=>b with{Position=turn(b.Position)}).ToArray(),\r\n            Meshes=character.Meshes.Select(m=>m with{Vertices=m.Vertices.Select(turn).ToArray(),CornerNormals=m.CornerNormals.Select(turn).ToArray()}).ToArray(),\r\n            Materials=character.Materials,EmbeddedTextures=character.EmbeddedTextures,\r\n            ImportWarnings=character.ImportWarnings.Append(warning).ToArray()};\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/Core/Analysis/HumanoidUp.cs",
            "FileName": "HumanoidUp.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "namespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// <summary>Check declared up against a head, torso and paired lower-limb\r\n/// cross sections. Ambiguous or open geometry retains the imported axes.</summary>\r\ninternal static class HumanoidUp\r\n{\r\n    internal static Vector3 Find(ImportedCharacter character)\r\n    {\r\n        var points=character.Meshes.Where(m=>m.Kind==MeshKind.Body).SelectMany(m=>m.Vertices).ToArray();\r\n        if(points.Length<100)return Vector3.UnitY;\r\n        var minimum=points.Aggregate(Vector3.Min);var maximum=points.Aggregate(Vector3.Max);\r\n        var center=(minimum+maximum)*.5f;float reach=(maximum-minimum).Length();\r\n        bool Supports(Vector3 up)\r\n        {\r\n            float bottom=points.Min(p=>Vector3.Dot(p,up)),height=points.Max(p=>Vector3.Dot(p,up))-bottom;\r\n            if(height<.001f)return false;\r\n            MeshSections.Section[] Sections(float fraction)\r\n            {\r\n                var origin=center+up*(bottom+height*fraction-Vector3.Dot(center,up));\r\n                return MeshSections.Cut(character,origin,up,reach,height*1e-5f)\r\n                    .Where(s=>s.Area>height*height*1e-6f).OrderByDescending(s=>s.Area).ToArray();\r\n            }\r\n            var heads=Sections(.92f);\r\n            if(heads.Length==0||heads[0].Area<heads.Sum(s=>s.Area)*.7f||heads[0].Radius>height*.3f)return false;\r\n            var torsos=Sections(.60f);if(torsos.Length==0)return false;\r\n            var head=heads[0].Center;var torso=torsos[0].Center;\r\n            Vector3 Horizontal(Vector3 p)=>p-up*Vector3.Dot(p,up);\r\n            if(Horizontal(head-torso).Length()>height*.22f)return false;\r\n            Vector3? previous=null;int evidence=0;\r\n            foreach(float fraction in new[]{.25f,.35f})\r\n            {\r\n                var limbs=Sections(fraction);float best=float.PositiveInfinity;Vector3 direction=default;\r\n                for(int i=0;i<limbs.Length;i++)for(int j=i+1;j<limbs.Length;j++)\r\n                {\r\n                    var a=limbs[i];var b=limbs[j];var delta=b.Center-a.Center;float length=delta.Length();\r\n                    if(length<height*.06f||length>height*.45f||Math.Min(a.Area,b.Area)<Math.Max(a.Area,b.Area)*.2f)continue;\r\n                    if(length<(a.Radius+b.Radius)*1.1f)continue;\r\n                    float offset=Horizontal((a.Center+b.Center)*.5f-torso).Length();\r\n                    if(offset>height*.2f||offset>=best)continue;\r\n                    direction=delta/length;best=offset;\r\n                }\r\n                if(!float.IsFinite(best))continue;\r\n                if(previous is {} prior&&Math.Abs(Vector3.Dot(prior,direction))<.85f)return false;\r\n                previous=direction;evidence++;\r\n            }\r\n            return evidence>0;\r\n        }\r\n        // A plausible declared up always wins. Correct only a unique supported\r\n        // alternative; the longest model dimension alone may simply be its arms.\r\n        if(Supports(Vector3.UnitY))return Vector3.UnitY;\r\n        Vector3? candidate=null;\r\n        foreach(var up in new[]{Vector3.UnitX,-Vector3.UnitX,-Vector3.UnitY,Vector3.UnitZ,-Vector3.UnitZ})\r\n        {\r\n            if(!Supports(up))continue;\r\n            if(candidate is not null)return Vector3.UnitY;\r\n            candidate=up;\r\n        }\r\n        return candidate??Vector3.UnitY;\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/Core/Analysis/MeshSections.cs",
            "FileName": "MeshSections.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "namespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\nusing Vector2=System.Numerics.Vector2;\r\n\r\n/// <summary>Closed triangle-plane contours, with area centers independent of vertex density.</summary>\r\ninternal static class MeshSections\r\n{\r\n    internal record Section(Vector3 Center,float Area,float Radius,float MinimumRadius);\r\n    internal static Section[] Cut(ImportedCharacter character,Vector3 origin,Vector3 normal,float reach,float tolerance)\r\n    {\r\n        normal=Vector3.Normalize(normal);var u=Vector3.Normalize(Vector3.Cross(normal,Math.Abs(normal.Z)>.9f?Vector3.UnitY:Vector3.UnitZ));var v=Vector3.Cross(normal,u);\r\n        var points=new List<Vector2>();var edges=new HashSet<(int,int)>();var cells=new Dictionary<(int,int),List<int>>();\r\n        int Node(Vector3 position)\r\n        {\r\n            var delta=position-origin;var p=new Vector2(Vector3.Dot(delta,u),Vector3.Dot(delta,v));\r\n            int x=(int)Math.Floor(p.X/tolerance),y=(int)Math.Floor(p.Y/tolerance);\r\n            for(int i=-1;i<=1;i++)for(int j=-1;j<=1;j++)if(cells.TryGetValue((x+i,y+j),out var nearby))\r\n                foreach(int n in nearby)if(Vector2.DistanceSquared(points[n],p)<=tolerance*tolerance)return n;\r\n            int index=points.Count;points.Add(p);if(!cells.TryGetValue((x,y),out var bucket))cells[(x,y)]=bucket=[];bucket.Add(index);return index;\r\n        }\r\n        var cut=new Vector3[3];\r\n        foreach(var mesh in character.Meshes.Where(m=>m.Kind==MeshKind.Body))for(int t=0;t<mesh.Triangles.Length;t+=3)\r\n        {\r\n            int count=0;\r\n            for(int e=0;e<3;e++)\r\n            {\r\n                var a=mesh.Vertices[mesh.Triangles[t+e]];var b=mesh.Vertices[mesh.Triangles[t+(e+1)%3]];\r\n                float da=Vector3.Dot(a-origin,normal),db=Vector3.Dot(b-origin,normal);\r\n                if((da<=0&&db>0)||(db<=0&&da>0))cut[count++]=Vector3.Lerp(a,b,da/(da-db));\r\n            }\r\n            if(count!=2||Vector3.Distance(cut[0],origin)>reach||Vector3.Distance(cut[1],origin)>reach)continue;\r\n            int first=Node(cut[0]),second=Node(cut[1]);if(first!=second)edges.Add((Math.Min(first,second),Math.Max(first,second)));\r\n        }\r\n        var neighbors=points.Select(_=>new List<int>()).ToArray();foreach(var(a,b)in edges){neighbors[a].Add(b);neighbors[b].Add(a);}\r\n        var sections=new List<Section>();var seen=new bool[points.Count];\r\n        for(int start=0;start<points.Count;start++)\r\n        {\r\n            if(seen[start])continue;var component=new List<int>();var queue=new Queue<int>();queue.Enqueue(start);seen[start]=true;\r\n            while(queue.TryDequeue(out int n)){component.Add(n);foreach(int next in neighbors[n])if(!seen[next]){seen[next]=true;queue.Enqueue(next);}}\r\n            if(component.Count<6||component.Any(n=>neighbors[n].Count!=2))continue;\r\n            var polygon=new List<Vector2>();int previous=-1,current=start;\r\n            do{polygon.Add(points[current]);int next=neighbors[current].First(n=>n!=previous);previous=current;current=next;}while(current!=start&&polygon.Count<=component.Count);\r\n            if(current!=start||polygon.Count!=component.Count)continue;\r\n            float twiceArea=0;var weighted=Vector2.Zero;\r\n            for(int i=0;i<polygon.Count;i++){var a=polygon[i];var b=polygon[(i+1)%polygon.Count];float cross=a.X*b.Y-b.X*a.Y;twiceArea+=cross;weighted+=(a+b)*cross;}\r\n            if(Math.Abs(twiceArea)<tolerance*tolerance)continue;\r\n            var center=weighted/(3*twiceArea);float radius=polygon.Max(p=>Vector2.Distance(center,p));\r\n            float minimum=float.PositiveInfinity;\r\n            for(int i=0;i<polygon.Count;i++)\r\n            {\r\n                var a=polygon[i];var b=polygon[(i+1)%polygon.Count];var ab=b-a;\r\n                float t=Math.Clamp(Vector2.Dot(center-a,ab)/Math.Max(ab.LengthSquared(),1e-12f),0,1);minimum=Math.Min(minimum,Vector2.Distance(center,a+ab*t));\r\n            }\r\n            sections.Add(new(origin+u*center.X+v*center.Y,Math.Abs(twiceArea)*.5f,radius,minimum));\r\n        }\r\n        return sections.ToArray();\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/Core/Analysis/FingerAlignment.cs",
            "FileName": "FingerAlignment.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "#nullable enable annotations\r\nnamespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// <summary>Centers non-thumb chains on their own closed mesh sections, preserving\r\n/// supported joint spacing and repairing chains collapsed near a fingertip.</summary>\r\npublic static class FingerAlignment\r\n{\r\n    public static int Refine(ImportedCharacter character,Anatomy anatomy,string side)\r\n    {\r\n        if(side is not (\"L\" or \"R\"))throw new ArgumentException(\"Unknown hand side.\");\r\n        var fingers=new[]{\"Index\",\"Middle\",\"Ring\",\"Pinky\"};\r\n        var chains=fingers.Select(f=>Enumerable.Range(1,3).Select(i=>f+i+\".\"+side).Append(f+\"Tip.\"+side).ToArray())\r\n            .Where(roles=>roles.All(r=>anatomy.Points.TryGetValue(r,out var p)&&!p.Corrected&&p.Confidence>=.35f)).ToArray();\r\n        if(chains.Length==0)return 0;\r\n        var wrist=anatomy[\"Hand.\"+side];float h=anatomy.Height;\r\n        float reach=chains.SelectMany(r=>r).Max(r=>Vector3.Distance(wrist,anatomy[r]))+h*.035f;\r\n        var local=HandSurface(character,wrist,reach);\r\n        // Containment uses the original complete shells, never the cropped analysis surface.\r\n        var volume=new SurfaceVisibility(character.Meshes.Where(m=>m.Kind==MeshKind.Body),h*.00001f);\r\n        int changed=0;\r\n        foreach(var roles in chains)\r\n        {\r\n            var previous=roles.Select(r=>anatomy[r]).ToArray();var fitted=Fit(local,volume,wrist,previous,h);\r\n            if(fitted is null)continue;\r\n            for(int i=0;i<3;i++)anatomy.Points[roles[i]]=anatomy.Points[roles[i]] with{Position=fitted[i]};\r\n            changed++;\r\n        }\r\n        return changed;\r\n    }\r\n    static Vector3[]? Fit(ImportedCharacter local,SurfaceVisibility volume,Vector3 wrist,Vector3[] previous,float h)\r\n    {\r\n        var tip=previous[3];var inward=wrist-tip;if(inward.LengthSquared()<h*h*.000001f)return null;\r\n        var axis=Vector3.Normalize(inward);var center=tip;float step=h*.0015f;\r\n        var path=new List<Vector3>{tip};var radii=new List<float>();var areas=new List<float>();bool ended=false;\r\n        for(int i=0;i<75;i++)\r\n        {\r\n            var seed=center+axis*step;\r\n            MeshSections.Section? section=null;\r\n            // A plane through a mesh vertex can produce an ambiguous contour.\r\n            // Nearby parallel cuts recover it without changing or welding geometry.\r\n            foreach(float offset in new[]{0f,.15f,-.15f,.35f,-.35f})\r\n            {\r\n                section=MeshSections.Cut(local,seed+axis*(step*offset),axis,h*.035f,h*.00001f)\r\n                    .Where(s=>s.Radius>h*.0007f&&s.Radius<h*.016f&&Vector3.Distance(s.Center,seed)<h*.018f)\r\n                    .MinBy(s=>Vector3.DistanceSquared(s.Center,seed));\r\n                if(section is not null)break;\r\n            }\r\n            if(section is null){ended=true;break;}\r\n            var difference=section.Center-center;\r\n            if(path.Count>1&&difference.Length()>h*.006f){ended=true;break;}\r\n            if(path.Count>5&&section.Area>areas.TakeLast(3).Average()*1.8f){ended=true;break;}\r\n            if(path.Count>2&&volume.Blocked(center,section.Center,h*.00001f)){ended=true;break;}\r\n            if(difference.LengthSquared()<1e-12f)return null;\r\n            var tangent=Vector3.Normalize(difference);\r\n            if(path.Count>1&&Vector3.Dot(axis,tangent)<.3f){ended=true;break;}\r\n            center=section.Center;path.Add(center);radii.Add(section.Radius);areas.Add(section.Area);\r\n            if(path.Count>2)axis=Vector3.Normalize(Vector3.Lerp(axis,tangent,.25f));\r\n            if(Vector3.Distance(center,tip)>inward.Length()*.8f)return null;\r\n        }\r\n        if(path.Count<8||!ended)return null;\r\n        var extension=center+axis*radii[^1]*.5f;\r\n        if(volume.Contains(extension,h*.00001f)&&!volume.Blocked(center,extension,h*.00001f))path.Add(extension);\r\n        path.Reverse();var distances=new float[path.Count];\r\n        for(int i=1;i<path.Count;i++)distances[i]=distances[i-1]+Vector3.Distance(path[i-1],path[i]);\r\n        float coverage=Vector3.Distance(path[0],tip)/Vector3.Distance(previous[0],tip);\r\n        if(coverage<.6f)return null;\r\n        // The webbing can end the trace before a well-supported palm knuckle.\r\n        // Keep that base and still center the distal joints on the recovered digit.\r\n        bool retainBase=coverage<.85f;\r\n        (Vector3 Point,float Offset) Project(Vector3 point)\r\n        {\r\n            float best=float.PositiveInfinity,offset=0;var result=point;\r\n            for(int i=1;i<path.Count;i++)\r\n            {\r\n                var closest=Geometry.ClosestOnSegment(point,path[i-1],path[i]);float distance=Vector3.DistanceSquared(point,closest);\r\n                if(distance>=best)continue;best=distance;result=closest;offset=distances[i-1]+Vector3.Distance(closest,path[i-1]);\r\n            }\r\n            return(result,offset);\r\n        }\r\n        // A centered chain does not need a new proportion-based fit.\r\n        if(previous.Skip(retainBase?1:0).Take(retainBase?2:3).Average(p=>Vector3.Distance(p,Project(p).Point))<h*.00025f)return null;\r\n        var fitted=new[]{0f,.5f,.78f,1f}.Select(f=>\r\n        {\r\n            float target=distances[^1]*f;int i=Array.FindIndex(distances,d=>d>=target);if(i<=0)return path[0];\r\n            return Vector3.Lerp(path[i-1],path[i],(target-distances[i-1])/Math.Max(distances[i]-distances[i-1],1e-8f));\r\n        }).ToArray();\r\n        if(retainBase)fitted[0]=previous[0];\r\n        if(Vector3.Distance(previous[0],tip)>=Vector3.Distance(fitted[0],tip)*.6f)\r\n        {\r\n            var second=Project(previous[1]);var third=Project(previous[2]);\r\n            if(second.Offset<h*.001f||third.Offset-second.Offset<h*.001f||distances[^1]-third.Offset<h*.001f)return null;\r\n            fitted[1]=second.Point;fitted[2]=third.Point;\r\n        }\r\n        for(int bone=0;bone<3;bone++)for(int sample=0;sample<9;sample++)\r\n            if(!volume.Contains(Vector3.Lerp(fitted[bone],fitted[bone+1],sample/9f),h*.00001f))return null;\r\n        return fitted;\r\n    }\r\n    static ImportedCharacter HandSurface(ImportedCharacter character,Vector3 wrist,float radius)\r\n    {\r\n        var parts=new List<MeshPart>();float squared=radius*radius;\r\n        foreach(var mesh in character.Meshes.Where(m=>m.Kind==MeshKind.Body))\r\n        {\r\n            var triangles=new List<int>();\r\n            for(int i=0;i<mesh.Triangles.Length;i+=3)\r\n            {\r\n                var a=mesh.Vertices[mesh.Triangles[i]];var b=mesh.Vertices[mesh.Triangles[i+1]];var c=mesh.Vertices[mesh.Triangles[i+2]];\r\n                var nearest=Vector3.Clamp(wrist,Vector3.Min(a,Vector3.Min(b,c)),Vector3.Max(a,Vector3.Max(b,c)));\r\n                if(Vector3.DistanceSquared(nearest,wrist)>squared)continue;\r\n                triangles.Add(mesh.Triangles[i]);triangles.Add(mesh.Triangles[i+1]);triangles.Add(mesh.Triangles[i+2]);\r\n            }\r\n            if(triangles.Count>0)parts.Add(new(mesh.Name,mesh.Vertices,triangles.ToArray(),MeshKind.Body));\r\n        }\r\n        return new ImportedCharacter{Meshes=parts.ToArray()};\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/Core/Rigging/SkinningNormals.cs",
            "FileName": "SkinningNormals.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "namespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// <summary>Angle-weighted outward normals for closed, consistently oriented\r\n/// components. Uncertain/open surfaces contribute no directional prior.</summary>\r\ninternal static class SkinningNormals\r\n{\r\n    internal static Vector3[] ClosedSurface(Vector3[] points,int[] triangles)\r\n    {\r\n        var mesh=new MeshPart(\"Skinning surface\",points,triangles,MeshKind.Body);\r\n        var components=Geometry.Components(mesh);int count=components.Max()+1;\r\n        var origins=new Vector3[count];var assigned=new bool[count];\r\n        for(int v=0;v<points.Length;v++)if(!assigned[components[v]]){origins[components[v]]=points[v];assigned[components[v]]=true;}\r\n        var volumes=new double[count];var closed=Enumerable.Repeat(true,count).ToArray();\r\n        var normals=new Vector3[points.Length];var edges=new Dictionary<(int,int),(int Count,int Direction)>();\r\n        for(int t=0;t<triangles.Length;t+=3)\r\n        {\r\n            int a=triangles[t],b=triangles[t+1],c=triangles[t+2];if(a==b||a==c||b==c)continue;\r\n            var normal=Vector3.Cross(points[b]-points[a],points[c]-points[a]);float area=normal.Length();\r\n            if(area<1e-12f){closed[components[a]]=false;continue;}\r\n            normal/=area;\r\n            var origin=origins[components[a]];\r\n            volumes[components[a]]+=Vector3.Dot(points[a]-origin,Vector3.Cross(points[b]-origin,points[c]-origin));\r\n            for(int corner=0;corner<3;corner++)\r\n            {\r\n                int v=triangles[t+corner],n=triangles[t+(corner+1)%3],o=triangles[t+(corner+2)%3];\r\n                var x=points[n]-points[v];var y=points[o]-points[v];\r\n                normals[v]+=normal*MathF.Atan2(Vector3.Cross(x,y).Length(),Vector3.Dot(x,y));\r\n                var key=(Math.Min(v,n),Math.Max(v,n));var edge=edges.GetValueOrDefault(key);\r\n                edges[key]=(edge.Count+1,edge.Direction+(v<n?1:-1));\r\n            }\r\n        }\r\n        foreach(var edge in edges)if(edge.Value.Count!=2||edge.Value.Direction!=0)closed[components[edge.Key.Item1]]=false;\r\n        for(int v=0;v<normals.Length;v++)\r\n        {\r\n            int component=components[v];float length=normals[v].Length();\r\n            normals[v]=closed[component]&&Math.Abs(volumes[component])>1e-12&&length>1e-6f\r\n                ?normals[v]*(Math.Sign(volumes[component])/length):Vector3.Zero;\r\n        }\r\n        return normals;\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/Core/Validation/RigValidator.cs",
            "FileName": "RigValidator.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "#nullable enable annotations\r\nusing System.Numerics;\r\nnamespace HumanoidRigger;\r\nusing Vector3 = System.Numerics.Vector3;\r\n\r\npublic sealed record ValidationIssue(string Code,string Message,bool Error);\r\npublic sealed record StressResult(string Pose,float MaximumStretch,float MinimumAreaRatio,int NonFiniteVertices,float SourceEdgeLength=0,float DeformedEdgeLength=0,int NonFiniteMeasurements=0,int ReversedTriangles=0,float ReversedAreaFraction=0);\r\npublic sealed class ValidationReport\r\n{\r\n    public List<ValidationIssue> Issues {get;}=[];\r\n    public List<StressResult> StressTests {get;}=[];\r\n    public List<StressResult> JointStressTests {get;}=[];\r\n    public int Repairs {get;set;}\r\n    public int RepairPasses {get;set;}\r\n    public bool Passed=>Issues.All(i=>!i.Error) && StressTests.Count>0;\r\n}\r\npublic static class RigValidator\r\n{\r\n    public static ValidationReport ValidateAndRepair(ImportedCharacter character,GeneratedRig rig)\r\n        =>ValidateAndRepair(character,rig,new ValidationGeometry(character));\r\n    internal static ValidationReport ValidateAndRepair(ImportedCharacter character,GeneratedRig rig,ValidationGeometry geometry)\r\n    {\r\n        int repairs=0;\r\n        foreach(var part in rig.Weights) for(int v=0;v<part.Length;v++)\r\n        {\r\n            var cleaned=Skinning.Cleanup(part[v],rig.Bones.Length,rig.Profile.MaximumInfluences);\r\n            if(!cleaned.SequenceEqual(part[v])) {part[v]=cleaned;repairs++;}\r\n        }\r\n        var report=Validate(character,rig,null,geometry);report.Repairs=repairs;\r\n        return SurfaceRepair.Improve(character,rig,WeightRepair.Improve(character,rig,report,geometry),geometry);\r\n    }\r\n    public static ValidationReport Validate(ImportedCharacter character,GeneratedRig rig)\r\n        =>Validate(character,rig,null);\r\n\r\n    internal static ValidationReport Validate(ImportedCharacter character,GeneratedRig rig,BindTriangle[][]? faces)\r\n        =>Validate(character,rig,faces,null);\r\n    internal static ValidationReport Validate(ImportedCharacter character,GeneratedRig rig,BindTriangle[][]? faces,ValidationGeometry? geometry)\r\n    {\r\n        var height=geometry?.Height??character.AnatomicalHeight;var r=new ValidationReport();void Error(string code,string text)=>r.Issues.Add(new(code,text,true));\r\n        try{rig.Profile.Validate();}catch(Exception e){Error(\"profile\",e.Message);return r;}\r\n        var roles=new HashSet<string>();var names=new HashSet<string>();\r\n        var body=geometry?.Body??character.Meshes.Where(m=>m.Kind==MeshKind.Body).SelectMany(m=>m.Vertices).ToArray();\r\n        for(int i=0;i<rig.Bones.Length;i++)\r\n        {\r\n            var b=rig.Bones[i];\r\n            if(!roles.Add(b.Role)||!names.Add(b.Name))Error(\"duplicate\",\"Duplicate bone assignment.\");\r\n            bool validParent=b.Parent<i&&b.Parent>=-1;\r\n            if(!validParent)Error(\"hierarchy\",\"Invalid skeleton hierarchy.\");\r\n            if(!Geometry.Finite(b.Position)||!float.IsFinite(b.Rotation.LengthSquared())||Math.Abs(b.Rotation.LengthSquared()-1)>.001f)Error(\"frame\",\"Invalid bone orientation or position.\");\r\n            if(b.Deform&&body.Length>0&&(geometry?.JointDistanceSquared(b.Position)??body.Min(p=>Vector3.DistanceSquared(p,b.Position)))>height*height*.15f*.15f)\r\n                Error(\"joint-placement\",$\"{b.Role} is too far from the character. Check its landmark.\");\r\n            var definition=rig.Profile.Bones.FirstOrDefault(d=>d.Role==b.Role);\r\n            if(definition is null || definition.Name!=b.Name || !validParent || (b.Parent<0 ? null : rig.Bones[b.Parent].Role)!=definition.Parent)Error(\"mapping\",\"Skeleton differs from the selected profile.\");\r\n        }\r\n        foreach(var b in rig.Profile.Bones.Where(b=>b.Required))if(!roles.Contains(b.Role))Error(\"required\",$\"Missing {b.Role}.\");\r\n        if(rig.Weights.Length!=character.Meshes.Length){Error(\"weights\",\"Missing mesh skinning.\");return r;}\r\n        for(int p=0;p<rig.Weights.Length;p++)\r\n        {\r\n            if(rig.Weights[p].Length!=character.Meshes[p].Vertices.Length){Error(\"weights\",\"Skinning vertex count mismatch.\");continue;}\r\n            foreach(var vertex in rig.Weights[p])\r\n            {\r\n                if(vertex.Length==0||vertex.Length>rig.Profile.MaximumInfluences)Error(\"influences\",\"Invalid influence count.\");\r\n                if(vertex.Any(i=>i.Bone<0||i.Bone>=rig.Bones.Length||!float.IsFinite(i.Weight)||i.Weight<=0))Error(\"influences\",\"Invalid weight or bone index.\");\r\n                if(Math.Abs(vertex.Sum(i=>i.Weight)-1)>.0001f)Error(\"normalization\",\"Weights do not sum to one.\");\r\n            }\r\n        }\r\n        if(r.Issues.Any(i=>i.Error))return r;\r\n        if(geometry?.Trunk?.HasBleeding(character,rig)==true){Error(\"weight-bleeding\",\"A weight repair reintroduced a remote torso attachment.\");return r;}\r\n        var ends=RigGeometry.SegmentEnds(rig);\r\n        var locality=new SkinningLocality(character,rig,ends);\r\n        for(int p=0;p<character.Meshes.Length;p++)\r\n        {\r\n            var mesh=character.Meshes[p];if(mesh.Kind==MeshKind.Accessory)continue;\r\n            bool remote=false;\r\n            for(int v=0;v<mesh.Vertices.Length&&!remote;v++)\r\n                foreach(var influence in rig.Weights[p][v])\r\n                {\r\n                    if(!rig.Bones[influence.Bone].Deform){Error(\"nondeforming-influence\",\"Skinning references a non-deforming bone.\");remote=true;break;}\r\n                    if(influence.Weight>.05f&&Vector3.Distance(mesh.Vertices[v],Geometry.ClosestOnSegment(mesh.Vertices[v],rig.Bones[influence.Bone].Position,ends[influence.Bone]))>locality.Limit(influence.Bone,mesh.Vertices[v]))\r\n                    {Error(\"weight-region\",$\"Mesh '{mesh.Name}' is influenced by a distant anatomical region ({rig.Bones[influence.Bone].Role}).\");remote=true;break;}\r\n                }\r\n        }\r\n        if(r.Issues.Any(i=>i.Error))return r;\r\n        faces??=geometry?.Faces??character.Meshes.Select(BindTriangle.Measure).ToArray();\r\n        var specifications=(geometry?.Poses??Deformation.Poses).ToArray();\r\n        var tests=new StressResult[specifications.Length];\r\n        Vector3[][] Buffers()=>character.Meshes.Select(m=>new Vector3[m.Vertices.Length]).ToArray();\r\n        void Measure(int i,Vector3[][] buffer)\r\n        {\r\n            if(Deformation.IsApplicable(specifications[i],roles))tests[i]=MeasurePose(character,rig,specifications[i],faces,buffer,height);\r\n        }\r\n        // Poses read the same frozen weights and write separate buffers. Keep\r\n        // report order and each pose's arithmetic serial and deterministic.\r\n        int workers=RigWork.WorkerCount(character.Meshes.Sum(m=>m.Vertices.Length));\r\n        RigWork.For(specifications.Length,workers,Buffers,Measure);\r\n        for(int i=0;i<specifications.Length;i++)\r\n        {\r\n            var pose=specifications[i];var test=tests[i];\r\n            if(test is null){r.Issues.Add(new(\"optional-pose\",$\"{pose.Name}: optional joints absent.\",false));continue;}\r\n            r.StressTests.Add(test);\r\n            if(test.ReversedTriangles>0)r.Issues.Add(new(\"surface-reversal\",$\"{pose.Name}: {test.ReversedTriangles} surface triangles reverse orientation.\",false));\r\n            if(test.NonFiniteVertices>0||test.NonFiniteMeasurements>0)Error(\"deformation\",$\"{pose.Name}: deformation produced non-finite coordinates or measurements.\");\r\n            else if(test.MaximumStretch>4||test.MinimumAreaRatio<.025f)Error(\"deformation\",$\"{pose.Name}: unsafe deformation (stretch {test.MaximumStretch:F2}, area ratio {test.MinimumAreaRatio:F3}).\");\r\n        }\r\n        return r;\r\n    }\r\n    internal static StressResult MeasurePose(ImportedCharacter character,GeneratedRig rig,StressPose pose,BindTriangle[][] faces,Vector3[][] deformed,float height)\r\n    {\r\n        var transforms=Deformation.BoneTransforms(rig,Deformation.JointRotations(rig,pose));\r\n        var rotations=transforms.Rotations;\r\n        Deformation.ApplyTransforms(character,rig,transforms.Positions,rotations,deformed);\r\n        float stretch=1,minArea=1,sourceEdge=0,deformedEdge=0;int nonFinite=0,invalidMeasurements=0;\r\n        int reversed=0;double surfaceArea=0,reversedArea=0;\r\n        for(int p=0;p<character.Meshes.Length;p++)\r\n        {\r\n            var mesh=character.Meshes[p];var dst=deformed[p];nonFinite+=dst.Count(v=>!Geometry.Finite(v));\r\n            foreach(var face in faces[p])\r\n            {\r\n                var i=face.A;var j=face.B;var k=face.C;\r\n                var normal=face.Normal;\r\n                var posedNormal=Vector3.Cross(dst[j]-dst[i],dst[k]-dst[i]);\r\n                float area=face.Area,posedArea=posedNormal.Length();\r\n                if(!float.IsFinite(area)||!float.IsFinite(posedArea))invalidMeasurements++;\r\n                else if(area>height*height*1e-10f)\r\n                {\r\n                    float ratio=posedArea/area;\r\n                    if(float.IsFinite(ratio))minArea=Math.Min(minArea,ratio);else invalidMeasurements++;\r\n                    surfaceArea+=area;\r\n                    float alignment=SurfaceOrientation.Alignment(normal,posedNormal,rig.Weights[p][i],rig.Weights[p][j],rig.Weights[p][k],rotations);\r\n                    if(alignment<SurfaceOrientation.ReversalLimit){reversed++;reversedArea+=area;}\r\n                }\r\n                for(int edge=0;edge<3;edge++)\r\n                {\r\n                    var (a,b,length)=face.Edge(edge);var posedLength=Vector3.Distance(dst[a],dst[b]);\r\n                    if(!float.IsFinite(length)||!float.IsFinite(posedLength)){invalidMeasurements++;continue;}\r\n                    if(length<=height*1e-6f)continue;\r\n                    float ratio=posedLength/length;\r\n                    if(!float.IsFinite(ratio)){invalidMeasurements++;continue;}\r\n                    if(ratio>stretch){stretch=ratio;sourceEdge=length;deformedEdge=posedLength;}\r\n                }\r\n            }\r\n        }\r\n        return new(pose.Name,stretch,minArea,nonFinite,sourceEdge,deformedEdge,invalidMeasurements,reversed,surfaceArea>0?(float)(reversedArea/surfaceArea):0);\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/Core/Validation/TrunkSkinning.cs",
            "FileName": "TrunkSkinning.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "namespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// <summary>Refine axial skinning with normal-aware heat and compact joint\r\n/// support. Full deformation evidence must remain safe before accepting it.</summary>\r\ninternal static class TrunkSkinning\r\n{\r\n    internal static ValidationReport Improve(ImportedCharacter character,GeneratedRig rig,ValidationReport initial)\r\n    {\r\n        var expected=Deformation.Poses.Where(p=>Deformation.IsApplicable(p,rig.Bones.Select(b=>b.Role).ToHashSet())).Select(p=>p.Name).Order().ToArray();\r\n        if(!initial.Passed||!WeightRepair.HasCompleteEvidence(initial,expected))return initial;\r\n        var region=new TrunkRegion(character,rig);if(!region.HasBleeding(character,rig))return initial;\r\n        var original=rig.Weights;bool accepted=false;\r\n        try\r\n        {\r\n            var heat=HeatSkinning.Candidates(character,rig,normalPrior:true,trunk:region).First();\r\n            var geometry=new ValidationGeometry(character,trunk:region);\r\n            foreach(float amount in new[]{1f,.5f,0f})\r\n            {\r\n                rig.Weights=Apply(character,rig,region,original,heat,amount);\r\n                var candidate=RigValidator.ValidateAndRepair(character,rig,geometry);\r\n                // Local surface repair may adjust a protected boundary. Never\r\n                // accept a trial that silently reintroduces remote attachments.\r\n                if(region.HasBleeding(character,rig)||!candidate.Passed||!WeightRepair.HasCompleteEvidence(candidate,expected)||\r\n                    candidate.StressTests.Zip(initial.StressTests).Any(p=>p.First.Pose!=p.Second.Pose||p.First.ReversedTriangles>p.Second.ReversedTriangles||p.First.ReversedAreaFraction>p.Second.ReversedAreaFraction+1e-7f))continue;\r\n                candidate.Repairs+=initial.Repairs;candidate.RepairPasses+=initial.RepairPasses+1;\r\n                accepted=true;return candidate;\r\n            }\r\n        }\r\n        catch(InvalidOperationException e){initial.Issues.Add(new(\"skinning-candidate\",e.Message,false));}\r\n        finally{if(!accepted)rig.Weights=original;}\r\n        initial.Issues.Add(new(\"weight-bleeding\",\"Torso skinning still follows a remote joint. Check the shoulder, hip and neck landmarks.\",true));\r\n        return initial;\r\n    }\r\n    static Influence[][][] Apply(ImportedCharacter character,GeneratedRig rig,TrunkRegion region,Influence[][][] source,Influence[][][] heat,float amount)\r\n    {\r\n        var result=source.Select(p=>(Influence[][])p.Clone()).ToArray();var ends=RigGeometry.SegmentEnds(rig);\r\n        for(int p=0;p<result.Length;p++)for(int v=0;v<result[p].Length;v++)if(region.Vertices[p][v])\r\n        {\r\n            var point=character.Meshes[p].Vertices[v];float blend=amount*region.Blend(point);\r\n            var values=new float[rig.Bones.Length];\r\n            foreach(var w in source[p][v])values[w.Bone]+=w.Weight*(1-blend);\r\n            foreach(var w in heat[p][v])values[w.Bone]+=w.Weight*blend;\r\n            float removed=0;\r\n            foreach(var attachment in region.Attachments)\r\n            {\r\n                float support=attachment.Support(point);\r\n                for(int b=0;b<values.Length;b++)if(attachment.Moving[b]){removed+=values[b]*(1-support);values[b]*=support;}\r\n            }\r\n            float axial=heat[p][v].Where(w=>region.Axial[w.Bone]).Sum(w=>w.Weight);\r\n            if(axial>1e-6f)foreach(var w in heat[p][v]){if(region.Axial[w.Bone])values[w.Bone]+=removed*w.Weight/axial;}\r\n            else\r\n            {\r\n                int nearest=Enumerable.Range(0,rig.Bones.Length).Where(b=>region.Axial[b]).MinBy(b=>Vector3.DistanceSquared(point,Geometry.ClosestOnSegment(point,rig.Bones[b].Position,ends[b])));\r\n                values[nearest]+=removed;\r\n            }\r\n            result[p][v]=Skinning.Cleanup(values,rig.Profile.MaximumInfluences);\r\n        }\r\n        return result;\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "notpointless.chomnr_humanoid_rigger",
            "Path": "Editor/UI/MaterialAssets.Formats.cs",
            "FileName": "MaterialAssets.Formats.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 381682,
            "Code": "using SkiaSharp;\r\nnamespace HumanoidRigger.Editor;\r\n\r\ninternal static partial class MaterialAssets\r\n{\r\n    // Work on copies in the output/cache directory. glTF packs roughness in G\r\n    // and metalness in B; Source 2 and MTL consume separate grayscale maps.\r\n    internal static SourceMaterial[] PrepareFormats(SourceMaterial[] source,string directory,ExportFormats formats)\r\n    {\r\n        bool portable=(formats&(ExportFormats.Gltf|ExportFormats.Glb))!=0;\r\n        var converted=new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);\r\n        string Png(string path)\r\n        {\r\n            if(path is null||Path.GetExtension(path).ToLowerInvariant() is \".png\" or \".jpg\" or \".jpeg\")return path;\r\n            if(converted.TryGetValue(path,out var result))return result;\r\n            using var bitmap=Decode(path);result=\"textures/portable_\"+converted.Count+\".png\";Save(bitmap,result);converted.Add(path,result);return result;\r\n        }\r\n        SKBitmap Decode(string path)=>SKBitmap.Decode(Path.Combine(directory,path))??throw new FormatException(\"Cannot decode texture '\"+path+\"'.\");\r\n        void Save(SKBitmap bitmap,string path)\r\n        {\r\n            string output=Path.Combine(directory,path);Directory.CreateDirectory(Path.GetDirectoryName(output));\r\n            using var image=SKImage.FromBitmap(bitmap);using var data=image.Encode(SKEncodedImageFormat.Png,100);using var stream=File.Create(output);data.SaveTo(stream);\r\n        }\r\n        static SKColor Sample(SKBitmap bitmap,int x,int y,int width,int height,SKColor fallback)=>bitmap is null?fallback:bitmap.GetPixel(Math.Min(bitmap.Width-1,x*bitmap.Width/width),Math.Min(bitmap.Height-1,y*bitmap.Height/height));\r\n        static byte Channel(float value)=>(byte)Math.Clamp((int)MathF.Round(value),0,255);\r\n        return source.Select((original,index)=>\r\n        {\r\n            var m=ConvertSpecularGlossiness(original with{},directory,index);\r\n            if(m.AuthoredPbr||m.AuthoredEmission)\r\n            {\r\n                float peak=Math.Max(1,Math.Max(m.EmissiveFactor.X,Math.Max(m.EmissiveFactor.Y,m.EmissiveFactor.Z)));\r\n                m.EmissiveFactor/=peak;m.EmissiveStrength*=peak;\r\n            }\r\n            string WriteMap(string suffix,int width,int height,Func<int,int,SKColor> pixel)\r\n            {\r\n                using var bitmap=new SKBitmap(width,height,SKColorType.Rgba8888,SKAlphaType.Unpremul);\r\n                for(int y=0;y<height;y++)for(int x=0;x<width;x++)bitmap.SetPixel(x,y,pixel(x,y));\r\n                string path=\"textures/pbr_\"+index+\"_\"+suffix+\".png\";Save(bitmap,path);return path;\r\n            }\r\n            if(m.AuthoredPbr)\r\n            {\r\n                using var packed=m.MetallicRoughnessTexture is null?null:Decode(m.MetallicRoughnessTexture);\r\n                int w=packed?.Width??1,h=packed?.Height??1;\r\n                m.RoughnessTexture=WriteMap(\"roughness\",w,h,(x,y)=>{byte v=Channel((packed?.GetPixel(x,y).Green??255)*m.RoughnessFactor);return new(v,v,v);});\r\n                m.MetalnessTexture=WriteMap(\"metalness\",w,h,(x,y)=>{byte v=Channel((packed?.GetPixel(x,y).Blue??255)*m.MetallicFactor);return new(v,v,v);});\r\n            }\r\n            else if(portable&&(m.RoughnessTexture is not null||m.MetalnessTexture is not null))\r\n            {\r\n                using var rough=m.RoughnessTexture is null?null:Decode(m.RoughnessTexture);using var metal=m.MetalnessTexture is null?null:Decode(m.MetalnessTexture);\r\n                int w=Math.Max(rough?.Width??1,metal?.Width??1),h=Math.Max(rough?.Height??1,metal?.Height??1);\r\n                m.MetallicRoughnessTexture=WriteMap(\"metallic_roughness\",w,h,(x,y)=>new(255,Sample(rough,x,y,w,h,SKColors.White).Red,Sample(metal,x,y,w,h,SKColors.Black).Red));\r\n            }\r\n            if(m.NormalTexture is not null&&m.NormalScale!=1)\r\n            {\r\n                using var normal=Decode(m.NormalTexture);float strength=m.NormalScale;\r\n                m.NormalTexture=WriteMap(\"normal\",normal.Width,normal.Height,(x,y)=>\r\n                {\r\n                    var c=normal.GetPixel(x,y);\r\n                    var n=new System.Numerics.Vector3((c.Red/255f*2-1)*strength,(c.Green/255f*2-1)*strength,c.Blue/255f*2-1);\r\n                    n=n.LengthSquared()>1e-12f?System.Numerics.Vector3.Normalize(n):System.Numerics.Vector3.UnitZ;\r\n                    return new(Channel((n.X*.5f+.5f)*255),Channel((n.Y*.5f+.5f)*255),Channel((n.Z*.5f+.5f)*255),c.Alpha);\r\n                });\r\n                m.NormalScale=1;\r\n            }\r\n            if(m.AuthoredPbr&&m.OcclusionTexture is not null)\r\n            {\r\n                // glTF AO uses only R, even when roughness and metalness share\r\n                // the same image. Source 2 needs a separate grayscale input.\r\n                using var occlusion=Decode(m.OcclusionTexture);float strength=m.OcclusionStrength;\r\n                m.OcclusionTexture=WriteMap(\"occlusion\",occlusion.Width,occlusion.Height,(x,y)=>\r\n                {byte v=Channel(255+strength*(occlusion.GetPixel(x,y).Red-255));return new(v,v,v);});\r\n                m.OcclusionStrength=1;\r\n            }\r\n            if((m.AuthoredPbr||m.AuthoredEmission)&&m.EmissiveTexture is null&&m.EmissiveFactor.LengthSquared()>0&&m.EmissiveStrength>0)\r\n                m.EmissiveTexture=WriteMap(\"emission\",1,1,(x,y)=>SKColors.White);\r\n            if(portable)\r\n            {\r\n                if(m.OpacityTexture is not null)\r\n                {\r\n                    using var color=m.ColorTexture is null?null:Decode(m.ColorTexture);using var opacity=Decode(m.OpacityTexture);\r\n                    int w=Math.Max(color?.Width??1,opacity.Width),h=Math.Max(color?.Height??1,opacity.Height);\r\n                    bool packedAlpha=string.Equals(m.OpacityTexture,m.ColorTexture,StringComparison.OrdinalIgnoreCase);\r\n                    m.ColorTexture=WriteMap(\"rgba\",w,h,(x,y)=>{var c=Sample(color,x,y,w,h,SKColors.White);var a=Sample(opacity,x,y,w,h,SKColors.White);return new(c.Red,c.Green,c.Blue,packedAlpha?a.Alpha:Channel(c.Alpha*a.Red/255f));});\r\n                }\r\n                m.ColorTexture=Png(m.ColorTexture);m.NormalTexture=Png(m.NormalTexture);m.MetallicRoughnessTexture=Png(m.MetallicRoughnessTexture);m.OcclusionTexture=Png(m.OcclusionTexture);m.EmissiveTexture=Png(m.EmissiveTexture);\r\n            }\r\n            return m;\r\n        }).ToArray();\r\n    }\r\n}\r\n"
        }
    ]
}