{"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=\u003EFiles.LastOrDefault(p=\u003Ep.EndsWith(\u0022.vmdl\u0022,StringComparison.OrdinalIgnoreCase))??Files.First();\r\n}\r\n\r\npublic static class AssetOutput\r\n{\r\n    public static async Task\u003Cstring\u003E Save(Wizard session)=\u003E\r\n        (await Save(session,ExportRequest.Default(session.Character!.Name,Project.Current.GetAssetsPath(),session.Character.Materials.Length\u003E0))).PrimaryFile;\r\n\r\n    public static async Task\u003CExportResult\u003E Save(Wizard session,ExportRequest request)\r\n    {\r\n        if(session.Rig?.Report.Passed!=true)throw new InvalidOperationException(\u0022The rig must pass validation before saving.\u0022);\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\u003E0);plan.EnsureAvailable();\r\n        Directory.CreateDirectory(plan.Directory);\r\n        var written=new List\u003Cstring\u003E();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)=\u003EWrite(path,System.Text.Encoding.UTF8.GetBytes(content));\r\n        try\r\n        {\r\n            var sourceMaterials=character.Materials;\r\n            IReadOnlyDictionary\u003Cint,string\u003E vmats=new Dictionary\u003Cint,string\u003E();\r\n            if(plan.MaterialDirectory is not null)\r\n            {\r\n                Directory.CreateDirectory(plan.MaterialDirectory);createdMaterials=true;\r\n                sourceMaterials=await Task.Run(()=\u003ETextureFiles.Copy(character,plan.MaterialDirectory));await new EditorThread();\r\n                sourceMaterials=await Task.Run(()=\u003EMaterialAssets.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(()=\u003EFbxExporter.Write(character,rig,portableMaterials));await new EditorThread();\r\n                Write(plan.PathFor(\u0022.fbx\u0022),bytes);\r\n                if(ExportRequest.IsInAssets(plan.Directory,root))AssetSystem.RegisterFile(plan.PathFor(\u0022.fbx\u0022));\r\n            }\r\n            if(plan.Formats.HasFlag(ExportFormats.Gltf))\r\n            {\r\n                var output=await Task.Run(()=\u003EGltfExporter.Write(character,rig,plan.FileName\u002B\u0022.bin\u0022,portableMaterials));await new EditorThread();\r\n                Write(plan.PathFor(\u0022.bin\u0022),output.Buffer);Write(plan.PathFor(\u0022.gltf\u0022),output.Document);\r\n            }\r\n            if(plan.Formats.HasFlag(ExportFormats.Glb))\r\n            {\r\n                var output=await Task.Run(()=\u003EGltfExporter.Write(character,rig,\u0022\u0022,portableMaterials,true,path=\u003EFile.ReadAllBytes(Path.Combine(plan.Directory,path))));await new EditorThread();\r\n                Write(plan.PathFor(\u0022.glb\u0022),output.Document);\r\n            }\r\n            if(plan.Formats.HasFlag(ExportFormats.Obj))\r\n            {\r\n                var output=await Task.Run(()=\u003EObjExporter.Write(character,plan.FileName\u002B\u0022.mtl\u0022,portableMaterials));await new EditorThread();\r\n                WriteText(plan.PathFor(\u0022.mtl\u0022),output.Materials);WriteText(plan.PathFor(\u0022.obj\u0022),output.Mesh);\r\n            }\r\n            if(plan.Formats.HasFlag(ExportFormats.Vmdl))\r\n            {\r\n                var dmx=await Task.Run(()=\u003EDmxExporter.Write(character,rig,vmats));await new EditorThread();\r\n                WriteText(plan.PathFor(\u0022.dmx\u0022),dmx);\r\n                WriteText(plan.PathFor(\u0022.vmdl\u0022),ModelDocExporter.Write(Path.GetRelativePath(root,plan.PathFor(\u0022.dmx\u0022)),rig,character,vmats.Count\u003E0));\r\n                await NativeRigExport.Compile(plan.PathFor(\u0022.dmx\u0022),plan.PathFor(\u0022.vmdl\u0022),character,rig,vmats,\r\n                    content=\u003EFile.WriteAllText(plan.PathFor(\u0022.dmx\u0022),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\u002B\u0022_c\u0022})try{File.Delete(owned);}catch(Exception e){Log.Warning(e.Message);}\r\n            if(createdMaterials\u0026\u0026ExportRequest.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=\u0022Humanoid Rigger\u0022;\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\u003Cstring,System.Numerics.Quaternion\u003E editedPose;\r\n    AutomaticHandRefiner handRefiner;\r\n    IReadOnlyDictionary\u003Cint,string\u003E previewMaterials=new Dictionary\u003Cint,string\u003E();\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\u003CWizard\u003E 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=\u0022HumanoidRigger\u0022;Cursor=CursorShape.Arrow;SetWindowIcon(\u0022accessibility_new\u0022);MinimumSize=new(1060,780);Size=new(1280,940);Layout=Layout.Column();EnsureHandRefiner();Build();\r\n    }\r\n    [Event(\u0022tools.editorwindow.createview\u0022)]\r\n    static void RegisterViewMenu(Menu menu)\r\n    {\r\n        // Join the editor\u0027s alphabetically sorted tools without creating a dock.\r\n        EditorWindow.DockManager.RegisterDockType(new DockManager.DockInfo\r\n        {\r\n            Title=DockTitle,Icon=\u0022accessibility_new\u0022,CreateAction=OpenFloatingView\r\n        });\r\n    }\r\n    static Widget OpenFloatingView(){Open();return null;}\r\n\r\n    [Event(\u0022tools.editorwindow.postcreateview\u0022)]\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\u0027s 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=\u0022Profile:\u0022});\r\n        var profile=top.Add(new ComboBox(content){MinimumWidth=190,ToolTip=\u0022The target skeleton\u2019s bone names and hierarchy. Hand detection is independent of this choice.\u0022});\r\n        foreach(var p in Profiles.BuiltIn.Concat(ProfileStore.Load()))profile.AddItem(p.Name,\u0022person\u0022,()=\u003E{Session.SetProfile(p);Build();},selected:p.Id==Session.Profile.Id);\r\n        profile.AddItem(\u0022Create Custom Profile\u2026\u0022,\u0022add\u0022,CreateProfile);\r\n        profile.AddItem(\u0022Load Profile\u2026\u0022,\u0022folder_open\u0022,LoadProfile);\r\n        if(Session.Rig is not null)\r\n        {\r\n            top.Add(new Label(content){Text=\u0022Preview:\u0022});\r\n            poseSelection=top.Add(new ComboBox(content){MinimumWidth=120,ToolTip=\u0022Preview the generated rig in a standard pose.\u0022});\r\n            foreach(var (label,value) in new[]{(\u0022Original Pose\u0022,CharacterPose.Auto),(\u0022T-Pose\u0022,CharacterPose.TPose),(\u0022A-Pose 1\u0022,CharacterPose.APose1),(\u0022A-Pose 2\u0022,CharacterPose.APose2)})\r\n                poseSelection.AddItem(label,\u0022accessibility\u0022,()=\u003ESetPreviewPose(value),selected:!customPose\u0026\u0026value==previewPose);\r\n            if(editedPose is not null)poseSelection.AddItem(\u0022Custom Pose\u0022,\u0022touch_app\u0022,ShowEditedPose,selected:customPose);\r\n        }\r\n        top.AddStretchCell();top.Add(new Button(\u0022Replace model\u0022,\u0022folder_open\u0022){Clicked=SelectModel});top.Add(new Button(\u0022Restart\u0022,\u0022restart_alt\u0022){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=\u003E!w.StartsWith(\u0022Detected a Z-up\u0022)).ToArray();\r\n            if(importWarnings.Length\u003E0)\r\n            {\r\n                var warning=bottom.Add(new Label(content){Name=\u0022ImportMaterialWarning\u0022,Text=string.Join(\u0022\\n\u0022,importWarnings),WordWrap=true});\r\n                warning.SetStyles($\u0022color: {Theme.Yellow.Hex};\u0022);\r\n            }\r\n        }\r\n        if(Session.Step is WizardStep.Centerline or WizardStep.Body \u0026\u0026 Session.Anatomy!.UnrecommendedImportPose)\r\n        {\r\n            var warning=bottom.Add(new Label(content){Name=\u0022ImportPoseWarning\u0022,Text=ImportPose.Warning,WordWrap=true});\r\n            warning.SetStyles($\u0022color: {Theme.Yellow.Hex};\u0022);\r\n        }\r\n        if(Session.Step is WizardStep.LeftHand or WizardStep.RightHand)\r\n        {\r\n            var side=Session.Step==WizardStep.LeftHand?\u0022L\u0022:\u0022R\u0022;\r\n            foreach(var warning in Session.Anatomy!.Warnings.Where(w=\u003Ew.StartsWith(side\u002B\u0022 hand\u0022)||w.StartsWith(side\u002B\u0022 fingers\u0022)))\r\n            {var label=bottom.Add(new Label(content){Text=warning,WordWrap=true});label.SetStyles($\u0022color: {Theme.Yellow.Hex};\u0022);}\r\n        }\r\n        if(Session.Step==WizardStep.Finish)\r\n        {\r\n            bottom.Add(new Label(content){Text=$\u0022Profile: {Session.Profile.Name}\u0022});\r\n            var deformationWarnings=Session.Rig!.Report.Issues.Where(i=\u003Ei.Code==\u0022surface-reversal\u0022).Select(i=\u003Ei.Message).ToArray();\r\n            if(deformationWarnings.Length\u003E0)\r\n            {\r\n                var warning=bottom.Add(new Label(content){Name=\u0022DeformationWarning\u0022,Text=\u0022Some test poses need review. Open Advanced Edit for details.\u0022,WordWrap=true});\r\n                warning.SetStyles($\u0022color: {Theme.Yellow.Hex};\u0022);\r\n            }\r\n            var checks=bottom.AddRow();checks.Spacing=8;\r\n            foreach(var label in new[]{\u0022Body\u0022,\u0022Left Hand\u0022,\u0022Right Hand\u0022,\u0022Skeleton\u0022,\u0022Skinning\u0022,\u0022Validation\u0022})\r\n            {\r\n                string side=label==\u0022Left Hand\u0022?\u0022L\u0022:label==\u0022Right Hand\u0022?\u0022R\u0022:null;\r\n                var warnings=label==\u0022Validation\u0022?deformationWarnings:side is null?[]:Session.Anatomy!.Warnings.Where(w=\u003Ew.StartsWith(side\u002B\u0022 hand\u0022)||w.StartsWith(side\u002B\u0022 fingers\u0022)).ToArray();\r\n                checks.Add(new StatusChip(content,label,warnings.Length\u003E0?Theme.Yellow:Theme.Green){ToolTip=warnings.Length\u003E0?string.Join(\u0022\\n\u0022,warnings):\u0022Checked\u0022});\r\n            }\r\n            checks.AddStretchCell();\r\n            var row=bottom.AddRow();row.Spacing=8;\r\n            row.Add(new Button(\u0022Back\u0022,\u0022arrow_back\u0022){Clicked=GoBack});\r\n            row.Add(new Button(\u0022Reset Pose\u0022,\u0022restart_alt\u0022){Clicked=()=\u003ESetPreviewPose(CharacterPose.Auto)});\r\n            row.Add(new Button(\u0022Test Rig\u0022,\u0022play_arrow\u0022){Clicked=TestRig});row.Add(new Button(\u0022Advanced Edit\u0022,\u0022tune\u0022){Clicked=Advanced});row.AddStretchCell();row.Add(new Button.Primary(\u0022Save\u0022){Icon=\u0022check\u0022,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(\u0022Back\u0022,\u0022arrow_back\u0022){Clicked=GoBack});\r\n            row.Add(new Button(\u0022Reset\u0022,\u0022restart_alt\u0022){Clicked=()=\u003E{Session.Reset();Build();}});row.AddStretchCell();\r\n            row.Add(new Button.Primary(\u0022Continue\u0022){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()=\u003ESession.Step switch\r\n    {\r\n        WizardStep.Centerline=\u003E\u0022Check the centerline.\\nDrag the line left or right to adjust it.\u0022,\r\n        WizardStep.Body=\u003E\u0022Check the points.\\nMove any point that is incorrect.\u0022,WizardStep.LeftHand=\u003E\u0022Check the left hand.\\nMove any incorrect points.\u0022,WizardStep.RightHand=\u003E\u0022Check the right hand.\\nMove any incorrect points.\u0022,WizardStep.Finish=\u003E\u0022Rig Complete\\nDrag a bone to test the rig.\u0022,\r\n        WizardStep.Validation=\u003Estring.Join(\u0022\\n\u0022,Session.Rig!.Report.Issues.Where(i=\u003Ei.Error).Select(i=\u003Ei.Message)),_=\u003E\u0022Generating rig\u2026\u0022\r\n    };\r\n    void SelectModel(){var path=EditorUtility.OpenFileDialog(\u0022Select model\u0022,ModelImporter.FileFilter,\u0022\u0022);if(!string.IsNullOrEmpty(path))ImportPath(path);}\r\n    void CreateProfile()\r\n    {\r\n        var path=EditorUtility.OpenFileDialog(\u0022Select rigged character\u0022,\u0022Rigged characters (*.fbx *.gltf *.glb)\u0022,\u0022\u0022);if(string.IsNullOrEmpty(path))return;\r\n        try{new ProfileDialog(this,ModelImporter.Import(path),p=\u003E{Session.SetProfile(p);Build();}).Show();}catch(Exception e){Error(e);}\r\n    }\r\n    void LoadProfile()\r\n    {\r\n        var path=EditorUtility.OpenFileDialog(\u0022Select rig profile\u0022,\u0022json\u0022,\u0022\u0022);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(\u0022The current operation is still running.\u0022);\r\n        EnsureHandRefiner();\r\n        SetBusy(true);\r\n        try\r\n        {\r\n            await RigWork.Run(()=\u003ESession.Import(path));await new EditorThread();\r\n            if(!this.IsValid())return;\r\n            previewMaterials=new Dictionary\u003Cint,string\u003E();\r\n            string textureWarning=null;\r\n            try{previewMaterials=await MaterialAssets.Preview(Session.Character);}\r\n            catch(Exception e){textureWarning=\u0022Textures: \u0022\u002Be.Message;Log.Warning(textureWarning);}\r\n            await new EditorThread();if(this.IsValid()){Build();if(textureWarning is not null){status.Text\u002B=\u0022\\n\u0022\u002BtextureWarning;status.SetStyles($\u0022color: {Theme.Yellow.Hex};\u0022);}}\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()\u0026\u0026fingerCountDialog.Visible){fingerCountDialog.Window.Raise();return;}\r\n            var revision=Session.Revision;\r\n            fingerCountDialog=new FingerCountDialog(this,Session.LeftFingerCount,Session.RightFingerCount,(left,right)=\u003E\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(\u0022The current operation is still running.\u0022);\r\n        EnsureHandRefiner();\r\n        SetBusy(true);\r\n        status.Text=Session.Step switch{WizardStep.Body=\u003E\u0022Preparing left hand\u2026\u0022,WizardStep.LeftHand=\u003E\u0022Preparing right hand\u2026\u0022,WizardStep.RightHand=\u003E\u0022Generating rig\u2026\u0022,_=\u003EInstruction()};\r\n        try\r\n        {\r\n            var started=System.Diagnostics.Stopwatch.GetTimestamp();\r\n            if(preparation is not null\u0026\u0026!preparation.IsCompleted\u0026\u0026!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)=\u003Eviewport.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(\u0022The current operation is still running.\u0022);\r\n        if(Session.Rig is null)throw new InvalidOperationException(\u0022Generate the rig before previewing poses.\u0022);\r\n        if(pose is not (CharacterPose.Auto or CharacterPose.TPose or CharacterPose.APose1 or CharacterPose.APose2))throw new ArgumentException(\u0022Unsupported preview pose.\u0022);\r\n        previewPose=pose;customPose=false;viewport.SetPose(RigPosePreview.Rotations(Session.Rig,pose));\r\n        var label=pose switch{CharacterPose.Auto=\u003E\u0022Original Pose\u0022,CharacterPose.TPose=\u003E\u0022T-Pose\u0022,CharacterPose.APose1=\u003E\u0022A-Pose 1\u0022,_=\u003E\u0022A-Pose 2\u0022};\r\n        SelectPoseLabel(label);\r\n        status.Text=Instruction();\r\n    }\r\n    void PoseEdited(IReadOnlyDictionary\u003Cstring,System.Numerics.Quaternion\u003E pose)\r\n    {\r\n        customPose=true;editedPose=new Dictionary\u003Cstring,System.Numerics.Quaternion\u003E(pose);\r\n        SelectPoseLabel(\u0022Custom Pose\u0022);\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==\u0022Custom Pose\u0022)poseSelection.AddItem(label,\u0022touch_app\u0022,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(\u0022The current operation is still running.\u0022);\r\n        Session.Restart();Build();\r\n    }\r\n    void Error(Exception e){status.Visible=true;status.Text=e.Message;status.SetStyles($\u0022color: {Theme.Red.Hex};\u0022);}\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=\u003Eb.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=\u0022Rig details\u0022;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=$\u0022{b.Name}  \u00B7  {b.Role}  \u00B7  {b.Position}\u0022});\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=$\u0022{(hand.Key==\u0022L\u0022?\u0022Left\u0022:\u0022Right\u0022)} hand: {hand.Value.Status}\u0022});\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=\u003E{if(this.IsValid()){status.Visible=true;status.Text=\u0022Saved \u0022\u002Bstring.Join(\u0022 \u002B \u0022,result.Files.Select(Path.GetFileName));status.ToolTip=string.Join(\u0022\\n\u0022,result.Files);status.SetStyles($\u0022color: {Theme.Green.Hex};\u0022);}}).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(\u0022accessibility_new\u0022);\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(()=\u003E{LastAdvanceWorkerWasMainThread=ThreadSafe.IsMainThread;draft.Continue();return draft;});\r\n        _=preparation.ContinueWith(task=\u003E{_ = 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\u0026\u0026!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\u003Cstring\u003E import;int hover;\r\n    public ModelDropArea(Widget parent,Action\u003Cstring\u003E 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=\u0022Please drag and drop a character file here (.fbx, .obj, .gltf, .glb)\u0022,Alignment=TextFlag.Center});\r\n        center.Add(new Label(this){Text=\u0022or\u0022,Alignment=TextFlag.Center});\r\n        var choice=center.AddRow();choice.AddStretchCell();\r\n        choice.Add(new Button.Primary(\u0022Choose File\u0022){MinimumWidth=120,Clicked=()=\u003E{var path=EditorUtility.OpenFileDialog(\u0022Choose File\u0022,ModelImporter.FileFilter,\u0022\u0022);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\u0026\u0026ModelImporter.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\u0026\u0026ModelImporter.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\u003C0?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),\u0022create_new_folder\u0022,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/// \u003Csummary\u003EA low, narrow neck beneath an enlarged head provides a body-scale\r\n/// prior independent of total height. Ambiguous sections retain the original prior.\u003C/summary\u003E\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=\u003EWidth*Depth;\r\n    }\r\n    public static float EstimateBodyHeight(IEnumerable\u003CMeshPart\u003E source,float bottom,float height)\r\n    {\r\n        var meshes=source.ToArray();var sections=new List\u003CSection\u003E();\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\u003C=128;sample\u002B\u002B)\r\n        {\r\n            float y=bottom\u002Bheight*(.5f\u002Bsample*.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\u003Cmesh.Triangles.Length;t\u002B=3)for(int edge=0;edge\u003C3;edge\u002B\u002B)\r\n            {\r\n                var a=mesh.Vertices[mesh.Triangles[t\u002Bedge]];var b=mesh.Vertices[mesh.Triangles[t\u002B(edge\u002B1)%3]];\r\n                if(!((a.Y\u003C=y\u0026\u0026b.Y\u003Ey)||(b.Y\u003C=y\u0026\u0026a.Y\u003Ey)))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\u002B\u002B;\r\n            }\r\n            if(count\u003E=4\u0026\u0026maxX-minX\u003Eheight*.005f\u0026\u0026maxZ-minZ\u003Eheight*.005f)sections.Add(new(y,maxX-minX,maxZ-minZ));\r\n        }\r\n        var candidates=sections.Where(s=\u003Es.Y\u003Cbottom\u002Bheight*.9f).ToArray();\r\n        if(candidates.Length==0)return height;\r\n        var narrowest=candidates.MinBy(s=\u003Es.Area);\r\n        if(narrowest.Y\u003E=bottom\u002Bheight*.8f)return height;\r\n        int index=sections.IndexOf(narrowest),first=index,last=index;\r\n        bool SameNeck(Section a,Section b)=\u003Eb.Area\u003C=narrowest.Area*2.5f\u0026\u0026Math.Abs(a.Y-b.Y)\u003C=height*.0071f;\r\n        while(first\u003E0\u0026\u0026SameNeck(sections[first],sections[first-1]))first--;\r\n        while(last\u002B1\u003Csections.Count\u0026\u0026SameNeck(sections[last],sections[last\u002B1]))last\u002B\u002B;\r\n        if(last-first\u003C2)return height;\r\n        float neckY=(sections[first].Y\u002Bsections[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=\u003Es.Y\u003EneckY\u002Bheight*.1f\u0026\u0026s.Y\u003Cbottom\u002Bheight*.9f))\r\n        {\r\n            bool Expanded(Section s)=\u003Es.Width\u003Elater.Width*1.35f\u0026\u0026s.Depth\u003Elater.Depth*1.1f;\r\n            if(sections.Any(s=\u003Es.Y\u003Clater.Y-height*.02f\u0026\u0026s.Y\u003Elater.Y-height*.07f\u0026\u0026Expanded(s))\u0026\u0026\r\n                sections.Any(s=\u003Es.Y\u003Elater.Y\u002Bheight*.02f\u0026\u0026s.Y\u003Clater.Y\u002Bheight*.07f\u0026\u0026Expanded(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=\u003Es.Y\u003EMath.Max(neckY\u002Bheight*.025f,bottom\u002Bheight*.82f)\u0026\u0026s.Width\u003Enarrowest.Width*2.2f\u0026\u0026s.Depth\u003Enarrowest.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/// \u003Csummary\u003ERefine a high hip from the thigh centerline and its junction with\r\n/// the pelvis. Ambiguous branches and large changes retain the original prior.\u003C/summary\u003E\r\ninternal static class HipFitting\r\n{\r\n    public static void RaiseLowHips(Anatomy anatomy,IReadOnlyList\u003CMeshSections.Section\u003E sections,float height,SurfaceVisibility volume)\r\n    {\r\n        var old=new[]{anatomy.Points[\u0022UpperLeg.L\u0022],anatomy.Points[\u0022UpperLeg.R\u0022]};\r\n        if(old.Any(p=\u003Ep.Corrected))return;\r\n        var candidates=new Vector3[2];float center=anatomy.SymmetryPlaneX;\r\n        for(int i=0;i\u003C2;i\u002B\u002B)\r\n        {\r\n            float sign=i==0?1:-1;var hip=old[i].Position;\r\n            var leg=sections.Where(s=\u003E(s.Center.X-center)*sign\u003Eheight*.02f\u0026\u0026Math.Abs(s.Center.X-hip.X)\u003Cheight*.06f\u0026\u0026\r\n                s.Center.Y\u003Ehip.Y-height*.07f\u0026\u0026s.Center.Y\u003Chip.Y\u002Bheight*.15f\u0026\u0026s.Radius\u003Cheight*.095f).ToArray();\r\n            var top=leg.MaxBy(s=\u003Es.Center.Y);\r\n            if(top is null||top.Center.Y\u003Chip.Y\u002Bheight*.015f||top.MinimumRadius\u003Cheight*.012f)return;\r\n            // Two separate thigh contours must actually join a central pelvis.\r\n            if(!sections.Any(s=\u003EMath.Abs(s.Center.X-center)\u003Cheight*.015f\u0026\u0026s.Center.Y\u003Etop.Center.Y\u0026\u0026s.Center.Y\u003Ctop.Center.Y\u002Bheight*.02f\u0026\u0026s.Area\u003Etop.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=\u003EMath.Abs(s.Center.X-center)\u003Cheight*.015f\u0026\u0026s.Center.Y\u003Ctop.Center.Y\u0026\u0026s.Center.Y\u003Etop.Center.Y-height*.03f\u0026\u0026s.Area\u003Etop.Area*1.5f))return;\r\n            var shaft=leg.Where(s=\u003Es.Center.Y\u003Ctop.Center.Y-height*.015f\u0026\u0026s.Center.Y\u003Etop.Center.Y-height*.06f).ToArray();if(shaft.Length\u003C4)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\u003Cshaft.Average(s=\u003Es.MinimumRadius)*.85f)return;\r\n            var mean=Geometry.Mean(shaft.Select(s=\u003Es.Center));float variance=shaft.Sum(s=\u003E(s.Center.Y-mean.Y)*(s.Center.Y-mean.Y));if(variance\u003Cheight*height*1e-8f)return;\r\n            var slope=shaft.Aggregate(Vector3.Zero,(sum,s)=\u003Esum\u002B(s.Center-mean)*(s.Center.Y-mean.Y))/variance;\r\n            var candidate=mean\u002Bslope*(top.Center.Y\u002Btop.MinimumRadius-mean.Y);\r\n            if(Vector3.Distance(candidate,hip)\u003Eheight*.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)\u003EMath.Abs(old[0].Position.Y-old[1].Position.Y)\u002Bheight*.01f)return;\r\n        var pelvis=anatomy.Points[\u0022Pelvis\u0022];float lift=(candidates[0].Y\u002Bcandidates[1].Y-old[0].Position.Y-old[1].Position.Y)*.5f;\r\n        var moved=pelvis.Position\u002BVector3.UnitY*lift;\r\n        if(!pelvis.Corrected\u0026\u0026volume.Contains(moved,height*.00001f))anatomy.Points[\u0022Pelvis\u0022]=pelvis with{Position=moved};\r\n        for(int i=0;i\u003C2;i\u002B\u002B)anatomy.Points[old[i].Role]=old[i] with{Position=candidates[i]};\r\n    }\r\n    public static void Refine(Anatomy anatomy,IReadOnlyList\u003CMeshSections.Section\u003E sections,float height,SurfaceVisibility volume)\r\n    {\r\n        float center=anatomy.SymmetryPlaneX;\r\n        var original=new[]{anatomy.Points[\u0022UpperLeg.L\u0022],anatomy.Points[\u0022UpperLeg.R\u0022]};\r\n        var candidates=original.Select(p=\u003Ep.Position).ToArray();\r\n        for(int index=0;index\u003C2;index\u002B\u002B)\r\n        {\r\n            string side=index==0?\u0022L\u0022:\u0022R\u0022;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[\u0022LowerLeg.\u0022\u002Bside];\r\n            var leg=sections.Where(s=\u003Es.Center.Y\u003Eknee.Y\u002B(hip.Y-knee.Y)*.35f\u0026\u0026s.Center.Y\u003Chip.Y\u002Bheight*.01f\u0026\u0026\r\n                (s.Center.X-center)*sign\u003Eheight*.02f\u0026\u0026Math.Abs(s.Center.X-hip.X)\u003Cheight*.07f\u0026\u0026s.Radius\u003Cheight*.1f).ToArray();\r\n            var top=leg.MaxBy(s=\u003Es.Center.Y);\r\n            if(top is null||top.Center.Y\u003Ehip.Y-height*.025f||top.MinimumRadius\u003Cheight*.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=\u003Es.Center.Y\u003Ctop.Center.Y-height*.02f\u0026\u0026s.Center.Y\u003Etop.Center.Y-height*.08f).ToArray();\r\n            if(shaft.Length\u003C4)continue;\r\n            var mean=Geometry.Mean(shaft.Select(s=\u003Es.Center));\r\n            float variance=shaft.Sum(s=\u003EMathF.Pow(s.Center.Y-mean.Y,2));if(variance\u003Cheight*height*1e-8f)continue;\r\n            var slope=shaft.Aggregate(Vector3.Zero,(value,s)=\u003Evalue\u002B(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\u002Btop.MinimumRadius;var candidate=mean\u002Bslope*(y-mean.Y);\r\n            if(!Geometry.Finite(candidate)||candidate.Y\u003E=hip.Y||(candidate.X-center)*sign\u003Cheight*.01f||\r\n                Vector3.Distance(candidate,hip)\u003EVector3.Distance(hip,knee)*.2f)continue;\r\n            if(!Enumerable.Range(0,21).All(i=\u003Evolume.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)\u003EMath.Abs(original[0].Position.Y-original[1].Position.Y)\u002Bheight*.005f)return;\r\n        for(int i=0;i\u003C2;i\u002B\u002B)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/// \u003Csummary\u003EModelDoc replaces namespace colons with underscores when importing bones.\u003C/summary\u003E\r\npublic static class VmdlBoneNames\r\n{\r\n    public static string Convert(string name)=\u003Ename.Replace(\u0027:\u0027,\u0027_\u0027);\r\n\r\n    public static void Validate(IEnumerable\u003CRigBone\u003E bones)\r\n    {\r\n        var names=new Dictionary\u003Cstring,string\u003E(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($\u0022Bone names \u0027{previous}\u0027 and \u0027{bone.Name}\u0027 both become \u0027{converted}\u0027 in s\u0026box. Rename one bone in the profile.\u0022);\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/// \u003Csummary\u003EPinned, hash-checked model/runtime cache. Downloads never contain user model data.\u003C/summary\u003E\r\npublic static class HandModelAssets\r\n{\r\n    public const string ModelHash=\u0022db0898ae717b76b075d9bf563af315b29562e11f8df5027a1ef07b02bef6d81c\u0022;\r\n    public const string RuntimeHash=\u0022dec964ab1ee36cc9b0ae247d13b376627992fc57dec0454354017ab8fd84f1ea\u0022;\r\n    public static string CacheDirectory=\u003EPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),\u0022sbox-humanoid-rigger\u0022,\u0022inference\u0022,\u0022hand-v1\u0022);\r\n    public static async Task\u003CNativeHandModel\u003E Load(string folder)\r\n    {\r\n        Directory.CreateDirectory(folder);\r\n        string model=Path.Combine(folder,\u0022hand.onnx\u0022),runtime=Path.Combine(folder,\u0022onnxruntime.dll\u0022);\r\n        using var http=new HttpClient{Timeout=TimeSpan.FromSeconds(90)};\r\n        await Download(http,\u0022https://media.githubusercontent.com/media/opencv/opencv_zoo/25f423d0e04c31a17254620e58febd7386da523b/models/handpose_estimation_mediapipe/handpose_estimation_mediapipe_2023feb.onnx\u0022,model,ModelHash);\r\n        if(!Valid(runtime,RuntimeHash))\r\n        {\r\n            string archive=Path.Combine(folder,\u0022onnxruntime-1.23.2.zip\u0022);\r\n            await Download(http,\u0022https://github.com/microsoft/onnxruntime/releases/download/v1.23.2/onnxruntime-win-x64-1.23.2.zip\u0022,archive,\u00220b38df9af21834e41e73d602d90db5cb06dbd1ca618948b8f1d66d607ac9f3cd\u0022);\r\n            using var zip=ZipFile.OpenRead(archive);\r\n            foreach(var name in new[]{\u0022lib/onnxruntime.dll\u0022,\u0022LICENSE\u0022,\u0022ThirdPartyNotices.txt\u0022})\r\n            {\r\n                var entry=zip.GetEntry(\u0022onnxruntime-win-x64-1.23.2/\u0022\u002Bname)??throw new InvalidDataException(\u0022Missing runtime asset.\u0022);\r\n                string destination=Path.Combine(folder,Path.GetFileName(name)),temporary=destination\u002B\u0022.\u0022\u002BGuid.NewGuid().ToString(\u0022N\u0022)\u002B\u0022.tmp\u0022;\r\n                try{entry.ExtractToFile(temporary);if(name.EndsWith(\u0022.dll\u0022)\u0026\u0026!Valid(temporary,RuntimeHash))throw new InvalidDataException(\u0022Runtime checksum mismatch.\u0022);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(\u0022Hand inference asset checksum mismatch.\u0022);\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\u002B\u0022.\u0022\u002BGuid.NewGuid().ToString(\u0022N\u0022)\u002B\u0022.tmp\u0022;\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(\u0022Hand inference download checksum mismatch.\u0022);\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/// \u003Csummary\u003E\r\n/// A single node of an FBX document tree (binary or ASCII): a name, a flat list of\r\n/// typed properties, and nested child nodes.\r\n///\r\n/// Property values are stored as the closest CLR type to what the file contained:\r\n/// \u003Clist type=\u0022bullet\u0022\u003E\r\n///   \u003Citem\u003E\u003Cc\u003Eshort\u003C/c\u003E (\u0027Y\u0027), \u003Cc\u003Ebool\u003C/c\u003E (\u0027C\u0027), \u003Cc\u003Eint\u003C/c\u003E (\u0027I\u0027), \u003Cc\u003Efloat\u003C/c\u003E (\u0027F\u0027),\r\n///         \u003Cc\u003Edouble\u003C/c\u003E (\u0027D\u0027), \u003Cc\u003Elong\u003C/c\u003E (\u0027L\u0027)\u003C/item\u003E\r\n///   \u003Citem\u003E\u003Cc\u003Efloat[]\u003C/c\u003E (\u0027f\u0027), \u003Cc\u003Edouble[]\u003C/c\u003E (\u0027d\u0027), \u003Cc\u003Elong[]\u003C/c\u003E (\u0027l\u0027),\r\n///         \u003Cc\u003Eint[]\u003C/c\u003E (\u0027i\u0027), \u003Cc\u003Ebool[]\u003C/c\u003E-as-\u003Cc\u003Ebyte[]\u003C/c\u003E (\u0027b\u0027)\u003C/item\u003E\r\n///   \u003Citem\u003E\u003Cc\u003Estring\u003C/c\u003E (\u0027S\u0027 \u2014 kept raw, may contain the \u003Cc\u003E\\x00\\x01\u003C/c\u003E name/class\r\n///         separator; see \u003Csee cref=\u0022SplitName\u0022/\u003E), \u003Cc\u003Ebyte[]\u003C/c\u003E (\u0027R\u0027)\u003C/item\u003E\r\n/// \u003C/list\u003E\r\n/// ASCII files store numbers only as \u003Cc\u003Elong\u003C/c\u003E / \u003Cc\u003Edouble\u003C/c\u003E (and arrays as\r\n/// \u003Cc\u003Elong[]\u003C/c\u003E / \u003Cc\u003Edouble[]\u003C/c\u003E), so the typed accessors below convert tolerantly.\r\n/// \u003C/summary\u003E\r\npublic sealed class FbxNode\r\n{\r\n    public string Name { get; }\r\n    public List\u003Cobject\u003E Properties { get; } = new();\r\n    public List\u003CFbxNode\u003E Children { get; } = new();\r\n\r\n    public FbxNode(string name) =\u003E Name = name;\r\n\r\n    /// \u003Csummary\u003EFirst child with the given name, or null.\u003C/summary\u003E\r\n    public FbxNode? Child(string name)\r\n    {\r\n        foreach (var c in Children)\r\n            if (c.Name == name)\r\n                return c;\r\n        return null;\r\n    }\r\n\r\n    /// \u003Csummary\u003EAll children with the given name, in document order.\u003C/summary\u003E\r\n    public IEnumerable\u003CFbxNode\u003E ChildrenNamed(string name)\r\n    {\r\n        foreach (var c in Children)\r\n            if (c.Name == name)\r\n                yield return c;\r\n    }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Property \u003Cparamref name=\u0022i\u0022/\u003E converted to \u003Ctypeparamref name=\u0022T\u0022/\u003E.\r\n    /// Numeric scalars convert tolerantly across widths (e.g. an \u0027I\u0027 i32 read as long);\r\n    /// anything else must match the stored type exactly.\r\n    /// \u003C/summary\u003E\r\n    public T Prop\u003CT\u003E(int i)\r\n    {\r\n        object v = RawProp(i);\r\n        if (v is T t)\r\n            return t;\r\n\r\n        var target = typeof(T);\r\n        // s\u0026box whitelist: Type.IsPrimitive is banned; enumerate the convertible targets.\r\n        if (v is IConvertible \u0026\u0026 (ConvertTargets.Contains(target) || target == typeof(string)))\r\n        {\r\n            try\r\n            {\r\n                return (T)Convert.ChangeType(v, target, System.Globalization.CultureInfo.InvariantCulture);\r\n            }\r\n            // ArithmeticException covers OverflowException, which is not s\u0026box-whitelisted\r\n            catch (Exception ex) when (ex is InvalidCastException or ArithmeticException or FormatException)\r\n            {\r\n                throw new FormatException(\r\n                    $\u0022FBX node \u0027{Name}\u0027: property {i} is {v.GetType().Name}, not convertible to {target.Name}.\u0022, ex);\r\n            }\r\n        }\r\n\r\n        throw new FormatException(\r\n            $\u0022FBX node \u0027{Name}\u0027: property {i} is {v.GetType().Name}, expected {target.Name}.\u0022);\r\n    }\r\n\r\n    /// \u003Csummary\u003EProperty \u003Cparamref name=\u0022i\u0022/\u003E as a double array (converts f/l/i/b arrays).\u003C/summary\u003E\r\n    public double[] AsDoubleArray(int i) =\u003E RawProp(i) switch\r\n    {\r\n        double[] d =\u003E d,\r\n        float[] f =\u003E Array.ConvertAll(f, x =\u003E (double)x),\r\n        long[] l =\u003E Array.ConvertAll(l, x =\u003E (double)x),\r\n        int[] n =\u003E Array.ConvertAll(n, x =\u003E (double)x),\r\n        byte[] b =\u003E Array.ConvertAll(b, x =\u003E (double)x),\r\n        bool[] o =\u003E Array.ConvertAll(o, x =\u003E x ? 1.0 : 0.0),\r\n        var v =\u003E throw TypeError(i, v, \u0022double[]\u0022),\r\n    };\r\n\r\n    /// \u003Csummary\u003EProperty \u003Cparamref name=\u0022i\u0022/\u003E as a float array (converts d/l/i/b arrays).\u003C/summary\u003E\r\n    public float[] AsFloatArray(int i) =\u003E RawProp(i) switch\r\n    {\r\n        float[] f =\u003E f,\r\n        double[] d =\u003E Array.ConvertAll(d, x =\u003E (float)x),\r\n        long[] l =\u003E Array.ConvertAll(l, x =\u003E (float)x),\r\n        int[] n =\u003E Array.ConvertAll(n, x =\u003E (float)x),\r\n        byte[] b =\u003E Array.ConvertAll(b, x =\u003E (float)x),\r\n        bool[] o =\u003E Array.ConvertAll(o, x =\u003E x ? 1f : 0f),\r\n        var v =\u003E throw TypeError(i, v, \u0022float[]\u0022),\r\n    };\r\n\r\n    /// \u003Csummary\u003EProperty \u003Cparamref name=\u0022i\u0022/\u003E as a long array (converts i/b; d/f if integral).\u003C/summary\u003E\r\n    public long[] AsLongArray(int i) =\u003E RawProp(i) switch\r\n    {\r\n        long[] l =\u003E l,\r\n        int[] n =\u003E Array.ConvertAll(n, x =\u003E (long)x),\r\n        byte[] b =\u003E Array.ConvertAll(b, x =\u003E (long)x),\r\n        bool[] o =\u003E Array.ConvertAll(o, x =\u003E x ? 1L : 0L),\r\n        double[] d =\u003E Array.ConvertAll(d, x =\u003E checked((long)x)),\r\n        float[] f =\u003E Array.ConvertAll(f, x =\u003E checked((long)x)),\r\n        var v =\u003E throw TypeError(i, v, \u0022long[]\u0022),\r\n    };\r\n\r\n    /// \u003Csummary\u003EProperty \u003Cparamref name=\u0022i\u0022/\u003E as an int array (converts b; l/d/f narrowing-checked).\u003C/summary\u003E\r\n    public int[] AsIntArray(int i) =\u003E RawProp(i) switch\r\n    {\r\n        int[] n =\u003E n,\r\n        long[] l =\u003E Array.ConvertAll(l, x =\u003E checked((int)x)),\r\n        byte[] b =\u003E Array.ConvertAll(b, x =\u003E (int)x),\r\n        bool[] o =\u003E Array.ConvertAll(o, x =\u003E x ? 1 : 0),\r\n        double[] d =\u003E Array.ConvertAll(d, x =\u003E checked((int)x)),\r\n        float[] f =\u003E Array.ConvertAll(f, x =\u003E checked((int)x)),\r\n        var v =\u003E throw TypeError(i, v, \u0022int[]\u0022),\r\n    };\r\n\r\n    /// \u003Csummary\u003EProperty \u003Cparamref name=\u0022i\u0022/\u003E as raw bytes (\u0027R\u0027 blobs or \u0027b\u0027 bool arrays).\u003C/summary\u003E\r\n    public byte[] AsByteArray(int i) =\u003E RawProp(i) switch\r\n    {\r\n        byte[] b =\u003E b,\r\n        var v =\u003E throw TypeError(i, v, \u0022byte[]\u0022),\r\n    };\r\n\r\n    /// \u003Csummary\u003EProperty \u003Cparamref name=\u0022i\u0022/\u003E as a string (raw \u0027S\u0027 content, separators intact).\u003C/summary\u003E\r\n    public string AsString(int i) =\u003E RawProp(i) switch\r\n    {\r\n        string s =\u003E s,\r\n        var v =\u003E throw TypeError(i, v, \u0022string\u0022),\r\n    };\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Splits an FBX object name into (name, class).\r\n    /// Binary files store \u003Cc\u003E\u0022Name\\x00\\x01Class\u0022\u003C/c\u003E (e.g. \u003Cc\u003E\u0022mixamorig:Hips\\x00\\x01Model\u0022\u003C/c\u003E);\r\n    /// ASCII files store \u003Cc\u003E\u0022Class::Name\u0022\u003C/c\u003E (e.g. \u003Cc\u003E\u0022Model::pelvis\u0022\u003C/c\u003E).\r\n    /// A plain string with neither separator yields (name, \u0022\u0022).\r\n    /// \u003C/summary\u003E\r\n    public static (string Name, string Class) SplitName(string raw)\r\n    {\r\n        int bin = raw.IndexOf(\u0022\\0\\x01\u0022, StringComparison.Ordinal);\r\n        if (bin \u003E= 0)\r\n            return (raw[..bin], raw[(bin \u002B 2)..]);\r\n\r\n        int ascii = raw.IndexOf(\u0022::\u0022, StringComparison.Ordinal);\r\n        if (ascii \u003E= 0)\r\n            return (raw[(ascii \u002B 2)..], raw[..ascii]);\r\n\r\n        return (raw, \u0022\u0022);\r\n    }\r\n\r\n    /// \u003Csummary\u003EPrimitive scalar types \u003Csee cref=\u0022Prop{T}\u0022/\u003E converts to (whitelist-safe IsPrimitive substitute).\u003C/summary\u003E\r\n    private static readonly HashSet\u003CType\u003E ConvertTargets = new()\r\n    {\r\n        typeof(bool), typeof(byte), typeof(sbyte), typeof(short), typeof(ushort),\r\n        typeof(int), typeof(uint), typeof(long), typeof(ulong),\r\n        typeof(float), typeof(double), typeof(char),\r\n    };\r\n\r\n    private object RawProp(int i)\r\n    {\r\n        if (i \u003C 0 || i \u003E= Properties.Count)\r\n            throw new FormatException(\r\n                $\u0022FBX node \u0027{Name}\u0027: property index {i} out of range (has {Properties.Count}).\u0022);\r\n        return Properties[i];\r\n    }\r\n\r\n    private FormatException TypeError(int i, object v, string wanted) =\u003E\r\n        new($\u0022FBX node \u0027{Name}\u0027: property {i} is {v.GetType().Name}, expected {wanted}.\u0022);\r\n}\r\n"},{"Ident":"notpointless.chomnr_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/// \u003Csummary\u003ERegularize a limb\u0027s 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.\u003C/summary\u003E\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=\u003Eb.Role).ToHashSet();\r\n        var specifications=(geometry?.Poses??Deformation.Poses).Where(p=\u003EDeformation.IsApplicable(p,roles)).ToArray();\r\n        var expected=specifications.Select(p=\u003Ep.Name).Order().ToArray();\r\n        if(!WeightRepair.HasCompleteEvidence(rig.Report,expected)||rig.Report.StressTests.All(p=\u003Ep.ReversedTriangles==0))return rig;\r\n        geometry??=new ValidationGeometry(character);var faces=geometry.Faces;\r\n        var buffer=character.Meshes.Select(m=\u003Enew Vector3[m.Vertices.Length]).ToArray();\r\n        float height=character.AnatomicalHeight;\r\n        for(int pass=0;pass\u003C4;pass\u002B\u002B)\r\n        {\r\n            int before=rig.Report.StressTests.Sum(p=\u003Ep.ReversedTriangles);\r\n            foreach(var scheduled in rig.Report.StressTests.Where(p=\u003Ep.ReversedTriangles\u003E0)\r\n                .OrderByDescending(p=\u003Ep.MaximumStretch\u003E4||p.MinimumAreaRatio\u003C.025f).ThenByDescending(p=\u003Ep.ReversedAreaFraction).ToArray())\r\n            {\r\n                var stress=rig.Report.StressTests.Single(p=\u003Ep.Pose==scheduled.Pose);\r\n                if(stress.ReversedTriangles==0)continue;\r\n                var specification=specifications.Single(p=\u003Ep.Name==stress.Pose);\r\n                if(geometry.Poses.Count==Deformation.Poses.Count\u0026\u0026!new[]{\u0022UpperArm.\u0022,\u0022LowerArm.\u0022,\u0022UpperLeg.\u0022,\u0022LowerLeg.\u0022}.Any(specification.Role.StartsWith))continue;\r\n                int joint=Array.FindIndex(rig.Bones,b=\u003Eb.Role==specification.Role);var bone=rig.Bones[joint];\r\n                var child=rig.Bones.FirstOrDefault(b=\u003Eb.Parent==joint\u0026\u0026b.Deform);\r\n                if(child is null||bone.Parent\u003C0||!rig.Bones[bone.Parent].Deform||Vector3.DistanceSquared(child.Position,bone.Position)\u003C1e-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\u002B1;i\u003Cmoving.Length;i\u002B\u002B)moving[i]=rig.Bones[i].Parent\u003E=0\u0026\u0026moving[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)\u003Cheight*.006f\u0026\u0026radial\u003Cheight*.07f){sum\u002B=radial;count\u002B\u002B;}\r\n                }\r\n                float radius=Math.Clamp(count\u003E0?sum/count:height*.025f,height*.01f,height*.065f);\r\n                var totals=rig.Weights.Select(part=\u003Epart.Select(weights=\u003EMovingTotal(weights,moving)).ToArray()).ToArray();\r\n                var axial=character.Meshes.Select(mesh=\u003Emesh.Vertices.Select(point=\u003EVector3.Dot(point-bone.Position,axis)).ToArray()).ToArray();\r\n                var trials=new List\u003C(GeneratedRig Rig,StressResult Stress)\u003E();\r\n                var blends=stress.ReversedTriangles\u003C16?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)=\u003Emesh.Vertices.Select((point,vertex)=\u003E\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\u003E=stress.ReversedTriangles||result.ReversedAreaFraction\u003Estress.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\u003E3)trials=trials.OrderBy(t=\u003Et.Stress.ReversedTriangles).ThenBy(t=\u003Et.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=\u003Et.Stress.ReversedTriangles).ThenBy(t=\u003Et.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\u002B=rig.Report.Repairs;report.RepairPasses\u002B=rig.Report.RepairPasses\u002B1;\r\n                    trial.Rig.Report=report;rig=trial.Rig;break;\r\n                }\r\n            }\r\n            if(before==rig.Report.StressTests.Sum(p=\u003Ep.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\u002B=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\u003C.0001f)return source;\r\n        float t=Math.Clamp(.5f\u002Baxial,0,1),envelope=t*t*(3-2*t);\r\n        if(amount\u003C0)\r\n        {\r\n            if(total\u003E.9999f)return source;\r\n            float target=total\u002B(-amount)*total*(1-total)*(1-envelope);\r\n            var scaled=new Influence[source.Length];\r\n            for(int i=0;i\u003Csource.Length;i\u002B\u002B){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\u002Benvelope*amount;\r\n        var blended=new Influence[source.Length\u002B1];\r\n        for(int i=0;i\u003Csource.Length;i\u002B\u002B){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/// \u003Csummary\u003ECheck every deforming joint in both directions on each axis.\r\n/// Only faces reached by that joint\u0027s skin weights need geometric measurement.\u003C/summary\u003E\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\u003C(StressPose,StressResult)\u003E();\r\n        var buffer=character.Meshes.Select(m=\u003Enew Vector3[m.Vertices.Length]).ToArray();\r\n        for(int joint=0;joint\u003Crig.Bones.Length;joint\u002B\u002B)\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\u002B1;b\u003Cmoving.Length;b\u002B\u002B)moving[b]=rig.Bones[b].Parent\u003E=0\u0026\u0026moving[rig.Bones[b].Parent];\r\n            var touched=rig.Weights.Select(p=\u003Ep.Select(w=\u003Ew.Any(i=\u003Emoving[i.Bone])).ToArray()).ToArray();\r\n            var faces=geometry.Faces.Select((p,m)=\u003Ep.Where(f=\u003Etouched[m][f.A]||touched[m][f.B]||touched[m][f.C]).ToArray()).ToArray();\r\n            foreach(var (axis,name) in new[]{(Vector3.UnitX,\u0022X\u0022),(Vector3.UnitY,\u0022Y\u0022),(Vector3.UnitZ,\u0022Z\u0022)})foreach(float degrees in new[]{-30f,30f})\r\n            {\r\n                var pose=new StressPose($\u0022Joint {rig.Bones[joint].Role} {name} {degrees:\u002B0;-0}\u0022,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)=\u003Eresult.ReversedTriangles\u003E0||result.NonFiniteVertices\u003E0||result.NonFiniteMeasurements\u003E0||result.MaximumStretch\u003E4||result.MinimumAreaRatio\u003C.025f;\r\n    static double Score(IEnumerable\u003CStressResult\u003E results)=\u003Eresults.Sum(r=\u003Er.ReversedTriangles\u002B1000000d*(r.NonFiniteVertices\u002Br.NonFiniteMeasurements)\u002B100*Math.Max(0,r.MaximumStretch/4-1)\u002B100*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=\u003EBad(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\u003CInfluence[][][]\u003E(()=\u003EHeatSkinning.Candidates(character,rig,normalPrior:true,trunk:trunk).First());\r\n        var heat=new Lazy\u003CInfluence[][][]\u003E(()=\u003EHeatSkinning.Solve(character,rig));\r\n        var constraints=new Dictionary\u003Cstring,StressPose\u003E();\r\n        for(int pass=0;pass\u003C4\u0026\u0026checks.Any(c=\u003EBad(c.Result));pass\u002B\u002B)\r\n        {\r\n            var failed=checks.Where(c=\u003EBad(c.Result)).OrderByDescending(c=\u003Ec.Result.ReversedTriangles).Take(24).Select(c=\u003Ec.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=\u003E(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=\u003Ep.ReversedTriangles\u003E0))\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=\u003Ep.First.ReversedTriangles\u003Ep.Second.ReversedTriangles||p.First.ReversedAreaFraction\u003Ep.Second.ReversedAreaFraction\u002B1e-7f))break;\r\n            var next=Measure(character,candidate,geometry);\r\n            bool discovered=false;\r\n            foreach(var check in next.Where(c=\u003EBad(c.Result)))discovered|=constraints.TryAdd(check.Pose.Name,check.Pose);\r\n            if(Score(next.Select(c=\u003Ec.Result))\u003E=Score(checks.Select(c=\u003Ec.Result)))\r\n            {if(discovered)continue;break;}\r\n            standard.Repairs=rig.Report.Repairs\u002Bcandidate.Report.Repairs;standard.RepairPasses=rig.Report.RepairPasses\u002Bcandidate.Report.RepairPasses\u002B1;\r\n            candidate.Report=standard;rig=candidate;checks=next;\r\n        }\r\n        rig.Report.JointStressTests.AddRange(checks.Select(c=\u003Ec.Result));\r\n        var remaining=checks.Where(c=\u003EBad(c.Result)).ToArray();\r\n        if(remaining.Length\u003E0)rig.Report.Issues.Add(new(\u0022joint-deformation\u0022,$\u0022{remaining.Length} joint motions still have unsafe deformation: {string.Join(\u0022, \u0022,remaining.Select(c=\u003Ec.Pose.Role).Distinct())}.\u0022,true));\r\n        return rig;\r\n    }\r\n    static void RefineSources(ImportedCharacter character,GeneratedRig rig,ValidationGeometry geometry,StressPose[] failed,params Lazy\u003CInfluence[][][]\u003E[] fields)\r\n    {\r\n        var roots=failed.Select(p=\u003Ep.Role).ToHashSet();var moving=new bool[rig.Bones.Length];\r\n        for(int b=0;b\u003Cmoving.Length;b\u002B\u002B)moving[b]=roots.Contains(rig.Bones[b].Role)||rig.Bones[b].Parent\u003E=0\u0026\u0026moving[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=\u003E(Influence[][])p.Clone()).ToArray();\r\n                for(int p=0;p\u003Cproposed.Length;p\u002B\u002B)for(int v=0;v\u003Cproposed[p].Length;v\u002B\u002B)\r\n                {\r\n                    float support=Math.Min(1,rig.Weights[p][v].Where(w=\u003Emoving[w.Bone]).Sum(w=\u003Ew.Weight)\u002Bsource[p][v].Where(w=\u003Emoving[w.Bone]).Sum(w=\u003Ew.Weight))*amount;\r\n                    if(support\u003C.001f)continue;\r\n                    var weights=Skinning.Cleanup(rig.Weights[p][v].Select(w=\u003Ew with{Weight=w.Weight*(1-support)})\r\n                        .Concat(source[p][v].Select(w=\u003Ew 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=\u003Ep.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{ \u0022.png\u0022, \u0022.jpg\u0022, \u0022.jpeg\u0022, \u0022.tga\u0022, \u0022.dds\u0022, \u0022.webp\u0022, \u0022.vmat\u0022, \u0022.vtex\u0022 };\r\n\r\n\t/// \u003Csummary\u003ECopies texture sidecars of a picked target model into the output folder:\r\n\t/// loose image files next to it, and a \u0022textures\u0022 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.\u003C/summary\u003E\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 \u0022Texture manager doesn\u0027t know about\r\n\t\t\t// texture ...generated.vtex\u0022 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( () =\u003E { File.Copy( file, destFile, true ); return true; } );\r\n\t\t\t\tTry( () =\u003E 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, \u0022textures\u0022 ),\r\n\t\t\t\tPath.Combine( Path.GetDirectoryName( sourceDir ) ?? sourceDir, \u0022textures\u0022 ),\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, \u0022textures\u0022 );\r\n\t\t\t\tDirectory.CreateDirectory( destTextures );\r\n\t\t\t\tforeach ( var file in Directory.GetFiles( candidate, \u0022*\u0022, 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( () =\u003E\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( () =\u003E 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( $\u0022[sbox-humanoid-rigger] sidecar texture copy failed: {e.Message}\u0022 );\r\n\t\t}\r\n\t}\r\n\r\n\r\nstatic T Try\u003CT\u003E(Func\u003CT\u003E 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/// \u003Csummary\u003ECopied from Humanoid Retargeter\u0027s RetargetWindow.Chip.\r\n/// Keep its dimensions, typography, fill and radius aligned with the suite.\u003C/summary\u003E\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\u002B18;\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/// \u003Csummary\u003EResolve up from body geometry, then an unambiguous quarter-turn\r\n/// from separated lower legs. Feet select the forward sign.\u003C/summary\u003E\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)=\u003Eup==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,\u0022The character\u0027s up direction was corrected from its body geometry.\u0022);\r\n        }\r\n        var body=character.Meshes.Where(m=\u003Em.Kind==MeshKind.Body).SelectMany(m=\u003Em.Vertices).ToArray();\r\n        if(body.Length\u003C100)return character;\r\n        float bottom=body.Min(p=\u003Ep.Y),height=body.Max(p=\u003Ep.Y)-bottom;\r\n        var legs=body.Where(p=\u003Ep.Y\u003Ebottom\u002Bheight*.15f\u0026\u0026p.Y\u003Cbottom\u002Bheight*.35f).Distinct().ToArray();\r\n        if(legs.Length\u003C20)return character;\r\n        var center=Geometry.Mean(legs);\r\n        float xVariance=legs.Average(p=\u003E(p.X-center.X)*(p.X-center.X));\r\n        float zVariance=legs.Average(p=\u003E(p.Z-center.Z)*(p.Z-center.Z));\r\n        if(zVariance\u003CxVariance*3)return character;\r\n        float middle=(BodyDetector.Quantile(legs.Select(p=\u003Ep.Z),.1f)\u002BBodyDetector.Quantile(legs.Select(p=\u003Ep.Z),.9f))*.5f;\r\n        if(legs.Count(p=\u003EMath.Abs(p.Z-middle)\u003Cheight*.012f)\u003Elegs.Length*.15f)return character;\r\n        var feet=body.Where(p=\u003Ep.Y\u003Cbottom\u002Bheight*.07f).ToArray();\r\n        if(feet.Length\u003C8)return character;\r\n        float forward=(BodyDetector.Quantile(feet.Select(p=\u003Ep.X),.1f)\u002BBodyDetector.Quantile(feet.Select(p=\u003Ep.X),.9f))*.5f-center.X;\r\n        forward=SeparatedFeetForward(character,bottom,height,middle)??forward;\r\n        if(Math.Abs(forward)\u003Cheight*.012f)return character;\r\n        float sign=forward\u003C0?1:-1;\r\n        Vector3 Turn(Vector3 p)=\u003Enew(sign*p.Z,p.Y,-sign*p.X);\r\n        return Reorient(character,Turn,\u0022The character was turned to face forward.\u0022);\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\u002Bcharacter.Maximum)*.5f;origin.Y=bottom\u002Bheight*.25f;\r\n        var sections=MeshSections.Cut(character,origin,Vector3.UnitY,(character.Maximum-character.Minimum).Length(),height*1e-5f);\r\n        if(sections.Length\u003C=2)return null;\r\n        var left=sections.Where(s=\u003Es.Center.Z\u003Emiddle\u002Bheight*.025f).OrderBy(s=\u003Es.Center.Z).FirstOrDefault();\r\n        var right=sections.Where(s=\u003Es.Center.Z\u003Cmiddle-height*.025f).OrderByDescending(s=\u003Es.Center.Z).FirstOrDefault();\r\n        if(left is null||right is null)return null;\r\n        var mesh=Geometry.Merge(character.Meshes.Where(m=\u003Em.Kind==MeshKind.Body));\r\n        var neighbors=Geometry.Neighbors(mesh,height*1e-5f);var visited=new bool[mesh.Vertices.Length];var queue=new Queue\u003Cint\u003E();\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\u003Cmesh.Vertices.Length;i\u002B\u002B)\r\n            {\r\n                float candidate=Vector3.DistanceSquared(mesh.Vertices[i],section.Center);\r\n                if(candidate\u003Cdistance){seed=i;distance=candidate;}\r\n            }\r\n            if(seed\u003C0||distance\u003Eheight*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]\u0026\u0026mesh.Vertices[next].Y\u003Cbottom\u002Bheight*.4f){visited[next]=true;queue.Enqueue(next);}\r\n        var feet=mesh.Vertices.Where((p,i)=\u003Evisited[i]\u0026\u0026p.Y\u003Cbottom\u002Bheight*.07f).Select(p=\u003Ep.X).ToArray();\r\n        if(feet.Length\u003C8)return null;\r\n        return(BodyDetector.Quantile(feet,.1f)\u002BBodyDetector.Quantile(feet,.9f))*.5f-(left.Center.X\u002Bright.Center.X)*.5f;\r\n    }\r\n    static ImportedCharacter Reorient(ImportedCharacter character,Func\u003CVector3,Vector3\u003E 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=\u003Eb with{Position=turn(b.Position)}).ToArray(),\r\n            Meshes=character.Meshes.Select(m=\u003Em 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/// \u003Csummary\u003ECheck declared up against a head, torso and paired lower-limb\r\n/// cross sections. Ambiguous or open geometry retains the imported axes.\u003C/summary\u003E\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=\u003Em.Kind==MeshKind.Body).SelectMany(m=\u003Em.Vertices).ToArray();\r\n        if(points.Length\u003C100)return Vector3.UnitY;\r\n        var minimum=points.Aggregate(Vector3.Min);var maximum=points.Aggregate(Vector3.Max);\r\n        var center=(minimum\u002Bmaximum)*.5f;float reach=(maximum-minimum).Length();\r\n        bool Supports(Vector3 up)\r\n        {\r\n            float bottom=points.Min(p=\u003EVector3.Dot(p,up)),height=points.Max(p=\u003EVector3.Dot(p,up))-bottom;\r\n            if(height\u003C.001f)return false;\r\n            MeshSections.Section[] Sections(float fraction)\r\n            {\r\n                var origin=center\u002Bup*(bottom\u002Bheight*fraction-Vector3.Dot(center,up));\r\n                return MeshSections.Cut(character,origin,up,reach,height*1e-5f)\r\n                    .Where(s=\u003Es.Area\u003Eheight*height*1e-6f).OrderByDescending(s=\u003Es.Area).ToArray();\r\n            }\r\n            var heads=Sections(.92f);\r\n            if(heads.Length==0||heads[0].Area\u003Cheads.Sum(s=\u003Es.Area)*.7f||heads[0].Radius\u003Eheight*.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)=\u003Ep-up*Vector3.Dot(p,up);\r\n            if(Horizontal(head-torso).Length()\u003Eheight*.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\u003Climbs.Length;i\u002B\u002B)for(int j=i\u002B1;j\u003Climbs.Length;j\u002B\u002B)\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\u003Cheight*.06f||length\u003Eheight*.45f||Math.Min(a.Area,b.Area)\u003CMath.Max(a.Area,b.Area)*.2f)continue;\r\n                    if(length\u003C(a.Radius\u002Bb.Radius)*1.1f)continue;\r\n                    float offset=Horizontal((a.Center\u002Bb.Center)*.5f-torso).Length();\r\n                    if(offset\u003Eheight*.2f||offset\u003E=best)continue;\r\n                    direction=delta/length;best=offset;\r\n                }\r\n                if(!float.IsFinite(best))continue;\r\n                if(previous is {} prior\u0026\u0026Math.Abs(Vector3.Dot(prior,direction))\u003C.85f)return false;\r\n                previous=direction;evidence\u002B\u002B;\r\n            }\r\n            return evidence\u003E0;\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/// \u003Csummary\u003EClosed triangle-plane contours, with area centers independent of vertex density.\u003C/summary\u003E\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)\u003E.9f?Vector3.UnitY:Vector3.UnitZ));var v=Vector3.Cross(normal,u);\r\n        var points=new List\u003CVector2\u003E();var edges=new HashSet\u003C(int,int)\u003E();var cells=new Dictionary\u003C(int,int),List\u003Cint\u003E\u003E();\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\u003C=1;i\u002B\u002B)for(int j=-1;j\u003C=1;j\u002B\u002B)if(cells.TryGetValue((x\u002Bi,y\u002Bj),out var nearby))\r\n                foreach(int n in nearby)if(Vector2.DistanceSquared(points[n],p)\u003C=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=\u003Em.Kind==MeshKind.Body))for(int t=0;t\u003Cmesh.Triangles.Length;t\u002B=3)\r\n        {\r\n            int count=0;\r\n            for(int e=0;e\u003C3;e\u002B\u002B)\r\n            {\r\n                var a=mesh.Vertices[mesh.Triangles[t\u002Be]];var b=mesh.Vertices[mesh.Triangles[t\u002B(e\u002B1)%3]];\r\n                float da=Vector3.Dot(a-origin,normal),db=Vector3.Dot(b-origin,normal);\r\n                if((da\u003C=0\u0026\u0026db\u003E0)||(db\u003C=0\u0026\u0026da\u003E0))cut[count\u002B\u002B]=Vector3.Lerp(a,b,da/(da-db));\r\n            }\r\n            if(count!=2||Vector3.Distance(cut[0],origin)\u003Ereach||Vector3.Distance(cut[1],origin)\u003Ereach)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(_=\u003Enew List\u003Cint\u003E()).ToArray();foreach(var(a,b)in edges){neighbors[a].Add(b);neighbors[b].Add(a);}\r\n        var sections=new List\u003CSection\u003E();var seen=new bool[points.Count];\r\n        for(int start=0;start\u003Cpoints.Count;start\u002B\u002B)\r\n        {\r\n            if(seen[start])continue;var component=new List\u003Cint\u003E();var queue=new Queue\u003Cint\u003E();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\u003C6||component.Any(n=\u003Eneighbors[n].Count!=2))continue;\r\n            var polygon=new List\u003CVector2\u003E();int previous=-1,current=start;\r\n            do{polygon.Add(points[current]);int next=neighbors[current].First(n=\u003En!=previous);previous=current;current=next;}while(current!=start\u0026\u0026polygon.Count\u003C=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\u003Cpolygon.Count;i\u002B\u002B){var a=polygon[i];var b=polygon[(i\u002B1)%polygon.Count];float cross=a.X*b.Y-b.X*a.Y;twiceArea\u002B=cross;weighted\u002B=(a\u002Bb)*cross;}\r\n            if(Math.Abs(twiceArea)\u003Ctolerance*tolerance)continue;\r\n            var center=weighted/(3*twiceArea);float radius=polygon.Max(p=\u003EVector2.Distance(center,p));\r\n            float minimum=float.PositiveInfinity;\r\n            for(int i=0;i\u003Cpolygon.Count;i\u002B\u002B)\r\n            {\r\n                var a=polygon[i];var b=polygon[(i\u002B1)%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\u002Bab*t));\r\n            }\r\n            sections.Add(new(origin\u002Bu*center.X\u002Bv*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/// \u003Csummary\u003ECenters non-thumb chains on their own closed mesh sections, preserving\r\n/// supported joint spacing and repairing chains collapsed near a fingertip.\u003C/summary\u003E\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 (\u0022L\u0022 or \u0022R\u0022))throw new ArgumentException(\u0022Unknown hand side.\u0022);\r\n        var fingers=new[]{\u0022Index\u0022,\u0022Middle\u0022,\u0022Ring\u0022,\u0022Pinky\u0022};\r\n        var chains=fingers.Select(f=\u003EEnumerable.Range(1,3).Select(i=\u003Ef\u002Bi\u002B\u0022.\u0022\u002Bside).Append(f\u002B\u0022Tip.\u0022\u002Bside).ToArray())\r\n            .Where(roles=\u003Eroles.All(r=\u003Eanatomy.Points.TryGetValue(r,out var p)\u0026\u0026!p.Corrected\u0026\u0026p.Confidence\u003E=.35f)).ToArray();\r\n        if(chains.Length==0)return 0;\r\n        var wrist=anatomy[\u0022Hand.\u0022\u002Bside];float h=anatomy.Height;\r\n        float reach=chains.SelectMany(r=\u003Er).Max(r=\u003EVector3.Distance(wrist,anatomy[r]))\u002Bh*.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=\u003Em.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=\u003Eanatomy[r]).ToArray();var fitted=Fit(local,volume,wrist,previous,h);\r\n            if(fitted is null)continue;\r\n            for(int i=0;i\u003C3;i\u002B\u002B)anatomy.Points[roles[i]]=anatomy.Points[roles[i]] with{Position=fitted[i]};\r\n            changed\u002B\u002B;\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()\u003Ch*h*.000001f)return null;\r\n        var axis=Vector3.Normalize(inward);var center=tip;float step=h*.0015f;\r\n        var path=new List\u003CVector3\u003E{tip};var radii=new List\u003Cfloat\u003E();var areas=new List\u003Cfloat\u003E();bool ended=false;\r\n        for(int i=0;i\u003C75;i\u002B\u002B)\r\n        {\r\n            var seed=center\u002Baxis*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\u002Baxis*(step*offset),axis,h*.035f,h*.00001f)\r\n                    .Where(s=\u003Es.Radius\u003Eh*.0007f\u0026\u0026s.Radius\u003Ch*.016f\u0026\u0026Vector3.Distance(s.Center,seed)\u003Ch*.018f)\r\n                    .MinBy(s=\u003EVector3.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\u003E1\u0026\u0026difference.Length()\u003Eh*.006f){ended=true;break;}\r\n            if(path.Count\u003E5\u0026\u0026section.Area\u003Eareas.TakeLast(3).Average()*1.8f){ended=true;break;}\r\n            if(path.Count\u003E2\u0026\u0026volume.Blocked(center,section.Center,h*.00001f)){ended=true;break;}\r\n            if(difference.LengthSquared()\u003C1e-12f)return null;\r\n            var tangent=Vector3.Normalize(difference);\r\n            if(path.Count\u003E1\u0026\u0026Vector3.Dot(axis,tangent)\u003C.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\u003E2)axis=Vector3.Normalize(Vector3.Lerp(axis,tangent,.25f));\r\n            if(Vector3.Distance(center,tip)\u003Einward.Length()*.8f)return null;\r\n        }\r\n        if(path.Count\u003C8||!ended)return null;\r\n        var extension=center\u002Baxis*radii[^1]*.5f;\r\n        if(volume.Contains(extension,h*.00001f)\u0026\u0026!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\u003Cpath.Count;i\u002B\u002B)distances[i]=distances[i-1]\u002BVector3.Distance(path[i-1],path[i]);\r\n        float coverage=Vector3.Distance(path[0],tip)/Vector3.Distance(previous[0],tip);\r\n        if(coverage\u003C.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\u003C.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\u003Cpath.Count;i\u002B\u002B)\r\n            {\r\n                var closest=Geometry.ClosestOnSegment(point,path[i-1],path[i]);float distance=Vector3.DistanceSquared(point,closest);\r\n                if(distance\u003E=best)continue;best=distance;result=closest;offset=distances[i-1]\u002BVector3.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=\u003EVector3.Distance(p,Project(p).Point))\u003Ch*.00025f)return null;\r\n        var fitted=new[]{0f,.5f,.78f,1f}.Select(f=\u003E\r\n        {\r\n            float target=distances[^1]*f;int i=Array.FindIndex(distances,d=\u003Ed\u003E=target);if(i\u003C=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)\u003E=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\u003Ch*.001f||third.Offset-second.Offset\u003Ch*.001f||distances[^1]-third.Offset\u003Ch*.001f)return null;\r\n            fitted[1]=second.Point;fitted[2]=third.Point;\r\n        }\r\n        for(int bone=0;bone\u003C3;bone\u002B\u002B)for(int sample=0;sample\u003C9;sample\u002B\u002B)\r\n            if(!volume.Contains(Vector3.Lerp(fitted[bone],fitted[bone\u002B1],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\u003CMeshPart\u003E();float squared=radius*radius;\r\n        foreach(var mesh in character.Meshes.Where(m=\u003Em.Kind==MeshKind.Body))\r\n        {\r\n            var triangles=new List\u003Cint\u003E();\r\n            for(int i=0;i\u003Cmesh.Triangles.Length;i\u002B=3)\r\n            {\r\n                var a=mesh.Vertices[mesh.Triangles[i]];var b=mesh.Vertices[mesh.Triangles[i\u002B1]];var c=mesh.Vertices[mesh.Triangles[i\u002B2]];\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)\u003Esquared)continue;\r\n                triangles.Add(mesh.Triangles[i]);triangles.Add(mesh.Triangles[i\u002B1]);triangles.Add(mesh.Triangles[i\u002B2]);\r\n            }\r\n            if(triangles.Count\u003E0)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/// \u003Csummary\u003EAngle-weighted outward normals for closed, consistently oriented\r\n/// components. Uncertain/open surfaces contribute no directional prior.\u003C/summary\u003E\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(\u0022Skinning surface\u0022,points,triangles,MeshKind.Body);\r\n        var components=Geometry.Components(mesh);int count=components.Max()\u002B1;\r\n        var origins=new Vector3[count];var assigned=new bool[count];\r\n        for(int v=0;v\u003Cpoints.Length;v\u002B\u002B)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\u003C(int,int),(int Count,int Direction)\u003E();\r\n        for(int t=0;t\u003Ctriangles.Length;t\u002B=3)\r\n        {\r\n            int a=triangles[t],b=triangles[t\u002B1],c=triangles[t\u002B2];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\u003C1e-12f){closed[components[a]]=false;continue;}\r\n            normal/=area;\r\n            var origin=origins[components[a]];\r\n            volumes[components[a]]\u002B=Vector3.Dot(points[a]-origin,Vector3.Cross(points[b]-origin,points[c]-origin));\r\n            for(int corner=0;corner\u003C3;corner\u002B\u002B)\r\n            {\r\n                int v=triangles[t\u002Bcorner],n=triangles[t\u002B(corner\u002B1)%3],o=triangles[t\u002B(corner\u002B2)%3];\r\n                var x=points[n]-points[v];var y=points[o]-points[v];\r\n                normals[v]\u002B=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\u002B1,edge.Direction\u002B(v\u003Cn?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\u003Cnormals.Length;v\u002B\u002B)\r\n        {\r\n            int component=components[v];float length=normals[v].Length();\r\n            normals[v]=closed[component]\u0026\u0026Math.Abs(volumes[component])\u003E1e-12\u0026\u0026length\u003E1e-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\u003CValidationIssue\u003E Issues {get;}=[];\r\n    public List\u003CStressResult\u003E StressTests {get;}=[];\r\n    public List\u003CStressResult\u003E JointStressTests {get;}=[];\r\n    public int Repairs {get;set;}\r\n    public int RepairPasses {get;set;}\r\n    public bool Passed=\u003EIssues.All(i=\u003E!i.Error) \u0026\u0026 StressTests.Count\u003E0;\r\n}\r\npublic static class RigValidator\r\n{\r\n    public static ValidationReport ValidateAndRepair(ImportedCharacter character,GeneratedRig rig)\r\n        =\u003EValidateAndRepair(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\u003Cpart.Length;v\u002B\u002B)\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\u002B\u002B;}\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        =\u003EValidate(character,rig,null);\r\n\r\n    internal static ValidationReport Validate(ImportedCharacter character,GeneratedRig rig,BindTriangle[][]? faces)\r\n        =\u003EValidate(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)=\u003Er.Issues.Add(new(code,text,true));\r\n        try{rig.Profile.Validate();}catch(Exception e){Error(\u0022profile\u0022,e.Message);return r;}\r\n        var roles=new HashSet\u003Cstring\u003E();var names=new HashSet\u003Cstring\u003E();\r\n        var body=geometry?.Body??character.Meshes.Where(m=\u003Em.Kind==MeshKind.Body).SelectMany(m=\u003Em.Vertices).ToArray();\r\n        for(int i=0;i\u003Crig.Bones.Length;i\u002B\u002B)\r\n        {\r\n            var b=rig.Bones[i];\r\n            if(!roles.Add(b.Role)||!names.Add(b.Name))Error(\u0022duplicate\u0022,\u0022Duplicate bone assignment.\u0022);\r\n            bool validParent=b.Parent\u003Ci\u0026\u0026b.Parent\u003E=-1;\r\n            if(!validParent)Error(\u0022hierarchy\u0022,\u0022Invalid skeleton hierarchy.\u0022);\r\n            if(!Geometry.Finite(b.Position)||!float.IsFinite(b.Rotation.LengthSquared())||Math.Abs(b.Rotation.LengthSquared()-1)\u003E.001f)Error(\u0022frame\u0022,\u0022Invalid bone orientation or position.\u0022);\r\n            if(b.Deform\u0026\u0026body.Length\u003E0\u0026\u0026(geometry?.JointDistanceSquared(b.Position)??body.Min(p=\u003EVector3.DistanceSquared(p,b.Position)))\u003Eheight*height*.15f*.15f)\r\n                Error(\u0022joint-placement\u0022,$\u0022{b.Role} is too far from the character. Check its landmark.\u0022);\r\n            var definition=rig.Profile.Bones.FirstOrDefault(d=\u003Ed.Role==b.Role);\r\n            if(definition is null || definition.Name!=b.Name || !validParent || (b.Parent\u003C0 ? null : rig.Bones[b.Parent].Role)!=definition.Parent)Error(\u0022mapping\u0022,\u0022Skeleton differs from the selected profile.\u0022);\r\n        }\r\n        foreach(var b in rig.Profile.Bones.Where(b=\u003Eb.Required))if(!roles.Contains(b.Role))Error(\u0022required\u0022,$\u0022Missing {b.Role}.\u0022);\r\n        if(rig.Weights.Length!=character.Meshes.Length){Error(\u0022weights\u0022,\u0022Missing mesh skinning.\u0022);return r;}\r\n        for(int p=0;p\u003Crig.Weights.Length;p\u002B\u002B)\r\n        {\r\n            if(rig.Weights[p].Length!=character.Meshes[p].Vertices.Length){Error(\u0022weights\u0022,\u0022Skinning vertex count mismatch.\u0022);continue;}\r\n            foreach(var vertex in rig.Weights[p])\r\n            {\r\n                if(vertex.Length==0||vertex.Length\u003Erig.Profile.MaximumInfluences)Error(\u0022influences\u0022,\u0022Invalid influence count.\u0022);\r\n                if(vertex.Any(i=\u003Ei.Bone\u003C0||i.Bone\u003E=rig.Bones.Length||!float.IsFinite(i.Weight)||i.Weight\u003C=0))Error(\u0022influences\u0022,\u0022Invalid weight or bone index.\u0022);\r\n                if(Math.Abs(vertex.Sum(i=\u003Ei.Weight)-1)\u003E.0001f)Error(\u0022normalization\u0022,\u0022Weights do not sum to one.\u0022);\r\n            }\r\n        }\r\n        if(r.Issues.Any(i=\u003Ei.Error))return r;\r\n        if(geometry?.Trunk?.HasBleeding(character,rig)==true){Error(\u0022weight-bleeding\u0022,\u0022A weight repair reintroduced a remote torso attachment.\u0022);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\u003Ccharacter.Meshes.Length;p\u002B\u002B)\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\u003Cmesh.Vertices.Length\u0026\u0026!remote;v\u002B\u002B)\r\n                foreach(var influence in rig.Weights[p][v])\r\n                {\r\n                    if(!rig.Bones[influence.Bone].Deform){Error(\u0022nondeforming-influence\u0022,\u0022Skinning references a non-deforming bone.\u0022);remote=true;break;}\r\n                    if(influence.Weight\u003E.05f\u0026\u0026Vector3.Distance(mesh.Vertices[v],Geometry.ClosestOnSegment(mesh.Vertices[v],rig.Bones[influence.Bone].Position,ends[influence.Bone]))\u003Elocality.Limit(influence.Bone,mesh.Vertices[v]))\r\n                    {Error(\u0022weight-region\u0022,$\u0022Mesh \u0027{mesh.Name}\u0027 is influenced by a distant anatomical region ({rig.Bones[influence.Bone].Role}).\u0022);remote=true;break;}\r\n                }\r\n        }\r\n        if(r.Issues.Any(i=\u003Ei.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()=\u003Echaracter.Meshes.Select(m=\u003Enew 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\u0027s arithmetic serial and deterministic.\r\n        int workers=RigWork.WorkerCount(character.Meshes.Sum(m=\u003Em.Vertices.Length));\r\n        RigWork.For(specifications.Length,workers,Buffers,Measure);\r\n        for(int i=0;i\u003Cspecifications.Length;i\u002B\u002B)\r\n        {\r\n            var pose=specifications[i];var test=tests[i];\r\n            if(test is null){r.Issues.Add(new(\u0022optional-pose\u0022,$\u0022{pose.Name}: optional joints absent.\u0022,false));continue;}\r\n            r.StressTests.Add(test);\r\n            if(test.ReversedTriangles\u003E0)r.Issues.Add(new(\u0022surface-reversal\u0022,$\u0022{pose.Name}: {test.ReversedTriangles} surface triangles reverse orientation.\u0022,false));\r\n            if(test.NonFiniteVertices\u003E0||test.NonFiniteMeasurements\u003E0)Error(\u0022deformation\u0022,$\u0022{pose.Name}: deformation produced non-finite coordinates or measurements.\u0022);\r\n            else if(test.MaximumStretch\u003E4||test.MinimumAreaRatio\u003C.025f)Error(\u0022deformation\u0022,$\u0022{pose.Name}: unsafe deformation (stretch {test.MaximumStretch:F2}, area ratio {test.MinimumAreaRatio:F3}).\u0022);\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\u003Ccharacter.Meshes.Length;p\u002B\u002B)\r\n        {\r\n            var mesh=character.Meshes[p];var dst=deformed[p];nonFinite\u002B=dst.Count(v=\u003E!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\u002B\u002B;\r\n                else if(area\u003Eheight*height*1e-10f)\r\n                {\r\n                    float ratio=posedArea/area;\r\n                    if(float.IsFinite(ratio))minArea=Math.Min(minArea,ratio);else invalidMeasurements\u002B\u002B;\r\n                    surfaceArea\u002B=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\u003CSurfaceOrientation.ReversalLimit){reversed\u002B\u002B;reversedArea\u002B=area;}\r\n                }\r\n                for(int edge=0;edge\u003C3;edge\u002B\u002B)\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\u002B\u002B;continue;}\r\n                    if(length\u003C=height*1e-6f)continue;\r\n                    float ratio=posedLength/length;\r\n                    if(!float.IsFinite(ratio)){invalidMeasurements\u002B\u002B;continue;}\r\n                    if(ratio\u003Estretch){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\u003E0?(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/// \u003Csummary\u003ERefine axial skinning with normal-aware heat and compact joint\r\n/// support. Full deformation evidence must remain safe before accepting it.\u003C/summary\u003E\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=\u003EDeformation.IsApplicable(p,rig.Bones.Select(b=\u003Eb.Role).ToHashSet())).Select(p=\u003Ep.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=\u003Ep.First.Pose!=p.Second.Pose||p.First.ReversedTriangles\u003Ep.Second.ReversedTriangles||p.First.ReversedAreaFraction\u003Ep.Second.ReversedAreaFraction\u002B1e-7f))continue;\r\n                candidate.Repairs\u002B=initial.Repairs;candidate.RepairPasses\u002B=initial.RepairPasses\u002B1;\r\n                accepted=true;return candidate;\r\n            }\r\n        }\r\n        catch(InvalidOperationException e){initial.Issues.Add(new(\u0022skinning-candidate\u0022,e.Message,false));}\r\n        finally{if(!accepted)rig.Weights=original;}\r\n        initial.Issues.Add(new(\u0022weight-bleeding\u0022,\u0022Torso skinning still follows a remote joint. Check the shoulder, hip and neck landmarks.\u0022,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=\u003E(Influence[][])p.Clone()).ToArray();var ends=RigGeometry.SegmentEnds(rig);\r\n        for(int p=0;p\u003Cresult.Length;p\u002B\u002B)for(int v=0;v\u003Cresult[p].Length;v\u002B\u002B)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]\u002B=w.Weight*(1-blend);\r\n            foreach(var w in heat[p][v])values[w.Bone]\u002B=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\u003Cvalues.Length;b\u002B\u002B)if(attachment.Moving[b]){removed\u002B=values[b]*(1-support);values[b]*=support;}\r\n            }\r\n            float axial=heat[p][v].Where(w=\u003Eregion.Axial[w.Bone]).Sum(w=\u003Ew.Weight);\r\n            if(axial\u003E1e-6f)foreach(var w in heat[p][v]){if(region.Axial[w.Bone])values[w.Bone]\u002B=removed*w.Weight/axial;}\r\n            else\r\n            {\r\n                int nearest=Enumerable.Range(0,rig.Bones.Length).Where(b=\u003Eregion.Axial[b]).MinBy(b=\u003EVector3.DistanceSquared(point,Geometry.ClosestOnSegment(point,rig.Bones[b].Position,ends[b])));\r\n                values[nearest]\u002B=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\u0026(ExportFormats.Gltf|ExportFormats.Glb))!=0;\r\n        var converted=new Dictionary\u003Cstring,string\u003E(StringComparer.OrdinalIgnoreCase);\r\n        string Png(string path)\r\n        {\r\n            if(path is null||Path.GetExtension(path).ToLowerInvariant() is \u0022.png\u0022 or \u0022.jpg\u0022 or \u0022.jpeg\u0022)return path;\r\n            if(converted.TryGetValue(path,out var result))return result;\r\n            using var bitmap=Decode(path);result=\u0022textures/portable_\u0022\u002Bconverted.Count\u002B\u0022.png\u0022;Save(bitmap,result);converted.Add(path,result);return result;\r\n        }\r\n        SKBitmap Decode(string path)=\u003ESKBitmap.Decode(Path.Combine(directory,path))??throw new FormatException(\u0022Cannot decode texture \u0027\u0022\u002Bpath\u002B\u0022\u0027.\u0022);\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)=\u003Ebitmap 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)=\u003E(byte)Math.Clamp((int)MathF.Round(value),0,255);\r\n        return source.Select((original,index)=\u003E\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\u003Cint,int,SKColor\u003E pixel)\r\n            {\r\n                using var bitmap=new SKBitmap(width,height,SKColorType.Rgba8888,SKAlphaType.Unpremul);\r\n                for(int y=0;y\u003Cheight;y\u002B\u002B)for(int x=0;x\u003Cwidth;x\u002B\u002B)bitmap.SetPixel(x,y,pixel(x,y));\r\n                string path=\u0022textures/pbr_\u0022\u002Bindex\u002B\u0022_\u0022\u002Bsuffix\u002B\u0022.png\u0022;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(\u0022roughness\u0022,w,h,(x,y)=\u003E{byte v=Channel((packed?.GetPixel(x,y).Green??255)*m.RoughnessFactor);return new(v,v,v);});\r\n                m.MetalnessTexture=WriteMap(\u0022metalness\u0022,w,h,(x,y)=\u003E{byte v=Channel((packed?.GetPixel(x,y).Blue??255)*m.MetallicFactor);return new(v,v,v);});\r\n            }\r\n            else if(portable\u0026\u0026(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(\u0022metallic_roughness\u0022,w,h,(x,y)=\u003Enew(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\u0026\u0026m.NormalScale!=1)\r\n            {\r\n                using var normal=Decode(m.NormalTexture);float strength=m.NormalScale;\r\n                m.NormalTexture=WriteMap(\u0022normal\u0022,normal.Width,normal.Height,(x,y)=\u003E\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()\u003E1e-12f?System.Numerics.Vector3.Normalize(n):System.Numerics.Vector3.UnitZ;\r\n                    return new(Channel((n.X*.5f\u002B.5f)*255),Channel((n.Y*.5f\u002B.5f)*255),Channel((n.Z*.5f\u002B.5f)*255),c.Alpha);\r\n                });\r\n                m.NormalScale=1;\r\n            }\r\n            if(m.AuthoredPbr\u0026\u0026m.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(\u0022occlusion\u0022,occlusion.Width,occlusion.Height,(x,y)=\u003E\r\n                {byte v=Channel(255\u002Bstrength*(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)\u0026\u0026m.EmissiveTexture is null\u0026\u0026m.EmissiveFactor.LengthSquared()\u003E0\u0026\u0026m.EmissiveStrength\u003E0)\r\n                m.EmissiveTexture=WriteMap(\u0022emission\u0022,1,1,(x,y)=\u003ESKColors.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(\u0022rgba\u0022,w,h,(x,y)=\u003E{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"}]}