Editor/AutoRig/CatalogDialog.cs

An editor UI dialog that lists available deep-learning rigging models from a catalog, shows hardware suitability per current mesh, allows downloading model files with progress and cancel, and lets the user load a local checkpoint ZIP into the model registry.

File AccessNetworking
using System;
using System.IO;
using System.Linq;
using System.Threading;
using AutoRig.Dl;
using Editor;
using Sandbox;

namespace AutoRig.Editor;

/// <summary>
/// The deep-learning model catalog (spec §7-§8): one row per model with license,
/// size, a hardware-advisor verdict chip for the CURRENT mesh, and Download with
/// progress + cancel. When no model fits this machine, rows explain why and the
/// geometric solver remains the path. Spacing per UiSpacing throughout.
/// </summary>
public sealed class CatalogDialog : Dialog
{
    readonly int _vertexCount;
    CancellationTokenSource _cancel;

    public CatalogDialog( Widget parent, int vertexCount ) : base( parent )
    {
        _vertexCount = vertexCount;
        Window.Title = "Manage Deep Learning Models";
        Window.SetWindowIcon( "model_training" );
        Window.MinimumSize = new Vector2( 640, 320 );

        Layout = Layout.Column();
        Layout.Margin = UiSpacing.Section;
        Layout.Spacing = UiSpacing.Group;

        var header = new Label(
            "Models download directly from their authors' official distribution. "
            + "Each shows whether it fits this machine and the current mesh.", this );
        header.SetStyles( $"color: {Theme.TextLight.Hex}; margin-bottom: {UiSpacing.HeaderBottom}px;" );
        Layout.Add( header );

        foreach ( var entry in ModelCatalog.Entries )
            BuildRow( entry );

        BuildOwnCheckpointRow();

        Layout.AddStretchCell();
        var footer = Layout.AddRow();
        footer.AddStretchCell();
        var close = new Button( "Close", this );
        close.Clicked = () => Window.Close();
        footer.Add( close );
    }

    /// <summary>Spec §7 mode 2: point the solver at your own RigNet checkpoints zip.</summary>
    void BuildOwnCheckpointRow()
    {
        var box = Layout.AddColumn();
        box.Spacing = UiSpacing.Related;
        box.Margin = new Sandbox.UI.Margin(
            UiSpacing.Related, UiSpacing.RowGap, UiSpacing.Related, UiSpacing.RowGap );

        var title = new Label( "Your own checkpoint", this );
        title.SetStyles( "font-weight: 600;" );
        box.Add( title );
        var hint = new Label(
            "Load a RigNet-format checkpoints zip you trained or downloaded yourself "
            + "(must contain gcn_meanshift / rootnet / bonenet / skinnet model_best.pth.tar).", this );
        hint.SetStyles( $"color: {Theme.TextLight.Hex};" );
        box.Add( hint );

        var row = box.AddRow();
        row.Spacing = UiSpacing.Related;
        var status = new Label( AutoRigWindow.ModelOverrideLabel ?? "", this );
        status.SetStyles( $"color: {Theme.TextLight.Hex};" );
        var load = new Button( "Load checkpoint zip…", "folder_open", this );
        load.Clicked = async () =>
        {
            var path = EditorUtility.OpenFileDialog(
                "Select RigNet checkpoints zip", "Checkpoint Bundles (*.zip)", null );
            if ( string.IsNullOrEmpty( path ) )
                return;
            load.Enabled = false;
            status.Text = "Loading…";
            status.SetStyles( $"color: {Theme.Blue.Hex};" );
            try
            {
                var bytes = File.ReadAllBytes( path );
                var bundle = await System.Threading.Tasks.Task.Run(
                    () => Solve.RigNetBundle.FromCheckpointsZip( bytes ) );
                DlModelRegistry.SetUserCheckpoint( bundle, Path.GetFileName( path ) );
                status.Text = $"Loaded {Path.GetFileName( path )} - it is now in the Model dropdown.";
                status.SetStyles( $"color: {Theme.Green.Hex};" );
            }
            catch ( Exception e )
            {
                status.Text = FirstLine( e.Message );
                status.SetStyles( $"color: {Theme.Red.Hex};" );
            }
            load.Enabled = true;
        };
        row.Add( load );
        row.Add( status );
        row.AddStretchCell();
    }

    static string FirstLine( string text )
    {
        var i = text.IndexOfAny( new[] { '\r', '\n' } );
        return i < 0 ? text : text[..i];
    }

    void BuildRow( CatalogEntry entry )
    {
        // Editor assemblies are not whitelisted - probe the machine here and inject.
        var available = GC.GetGCMemoryInfo().TotalAvailableMemoryBytes;
        var (verdict, why) = HardwareAdvisor.Assess( entry, Math.Max( _vertexCount, 1 ), available );
        var tone = verdict switch
        {
            Verdict.Recommended => Theme.Green,
            Verdict.Slow => Theme.Yellow,
            _ => Theme.Red,
        };

        var box = Layout.AddColumn();
        box.Spacing = UiSpacing.Related;
        box.Margin = new Sandbox.UI.Margin( UiSpacing.Related, UiSpacing.RowGap, UiSpacing.Related, UiSpacing.RowGap );

        var titleRow = box.AddRow();
        titleRow.Spacing = UiSpacing.Related;

        // Enable toggle - LOCKED OFF when this machine cannot run the model or the
        // runtime does not support the architecture yet.
        var meetsHardware = DlModelRegistry.MeetsHardware( entry );
        var usable = meetsHardware && entry.RuntimeSupported;
        var enableCheck = new Checkbox( "", this )
        {
            Value = usable && IsInstalled( entry ) && DlModelRegistry.IsEnabled( entry.Id ),
            Enabled = usable && IsInstalled( entry ),
            ToolTip = !entry.RuntimeSupported
                ? "Not runnable yet: the pure-C# port of this architecture is planned."
                : !meetsHardware
                    ? $"Disabled: this machine does not meet the model's requirements. {why}"
                    : IsInstalled( entry )
                        ? "Enabled models appear in the Model dropdown; Auto picks the best one."
                        : "Download the model first.",
        };
        if ( !usable )
            DlModelRegistry.SetEnabled( entry.Id, false );
        enableCheck.Toggled = () => DlModelRegistry.SetEnabled( entry.Id, enableCheck.Value );
        titleRow.Add( enableCheck );

        var title = new Label( entry.Title, this );
        title.SetStyles( $"font-weight: 600;" );
        titleRow.Add( title );
        var verdictChip = new Label( verdict.ToString(), this );
        verdictChip.ToolTip = why;
        verdictChip.SetStyles( $"color: {tone.Hex}; padding: 0px {UiSpacing.Related}px;" );
        titleRow.Add( verdictChip );
        titleRow.AddStretchCell();

        var summary = new Label( entry.Summary, this );
        summary.SetStyles( $"color: {Theme.TextLight.Hex};" );
        box.Add( summary );
        var license = new Label( $"License: {entry.License}", this );
        license.ToolTip = entry.Attribution;
        license.SetStyles( $"color: {Theme.TextLight.Hex};" );
        box.Add( license );

        var actionRow = box.AddRow();
        actionRow.Spacing = UiSpacing.Related;
        var status = new Label( IsInstalled( entry ) ? "Installed." : "", this );
        status.SetStyles( $"color: {Theme.TextLight.Hex};" );

        var download = new Button( IsInstalled( entry ) ? "Re-download" : "Download", "download", this );
        var cancelButton = new Button( "Cancel", "close", this ) { Visible = false };
        download.Enabled = verdict != Verdict.Insufficient && entry.RuntimeSupported;
        if ( !entry.RuntimeSupported )
            download.ToolTip = "Runtime support for this architecture is planned - "
                + "download will unlock with it.";
        else if ( verdict == Verdict.Insufficient )
            download.ToolTip = why;

        download.Clicked = async () =>
        {
            _cancel = new CancellationTokenSource();
            download.Visible = false;
            cancelButton.Visible = true;
            try
            {
                foreach ( var file in entry.Files )
                {
                    await DownloadManager.Download( entry, file, InstallDir( entry ),
                        ( done, total ) => status.Text = total > 0
                            ? $"{file.FileName}: {done / 1048576.0:0.0} / {total / 1048576.0:0.0} MB ({done * 100.0 / total:0}%)"
                            : $"{file.FileName}: {done / 1048576.0:0.0} MB",
                        _cancel.Token );
                }
                status.Text = "Installed and enabled.";
                status.SetStyles( $"color: {Theme.Green.Hex};" );
                DlModelRegistry.Changed?.Invoke();
            }
            catch ( OperationCanceledException )
            {
                status.Text = "Cancelled (partial file kept for resume).";
                status.SetStyles( $"color: {Theme.Yellow.Hex};" );
            }
            catch ( Exception e )
            {
                status.Text = e.Message;
                status.SetStyles( $"color: {Theme.Red.Hex};" );
            }
            download.Visible = true;
            download.Text = IsInstalled( entry ) ? "Re-download" : "Download";
            cancelButton.Visible = false;
        };
        cancelButton.Clicked = () => _cancel?.Cancel();

        actionRow.Add( download );
        actionRow.Add( cancelButton );
        actionRow.Add( status );
        actionRow.AddStretchCell();
    }

    /// <summary>Per-entry install folder under the active project's assets.</summary>
    public static string InstallDir( CatalogEntry entry )
        => Path.Combine( Project.Current?.GetAssetsPath() ?? ".", "autorig_dl", entry.Id );

    public static bool IsInstalled( CatalogEntry entry )
        => entry.Files.All( f => File.Exists( Path.Combine( InstallDir( entry ), f.FileName ) ) );
}