Editor/HumanoidMocap/MocapBodyRefinement.cs
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using HumanoidMocap.Motion;
using Sandbox;

namespace HumanoidMocap.Editor;

public sealed partial class RetargetWindow
{
    /// <summary>Written by the worker's background-feature test; see InferenceWorker/CameraMotionCheck.cs.</summary>
    const string StationaryCameraPrefix="Stationary recording camera:";
    /// <summary>Written when the worker followed a moving camera's rotation; see InferenceWorker/CameraRotationTrack.cs.</summary>
    const string FollowedCameraPrefix="Moving recording camera followed:";
    global::Editor.Button _bodyRefinementButton;
    void RefreshBodyRefinementButton()
    {
        if(!_bodyRefinementButton.IsValid())return;
        if(_stabilizeFeetControl.IsValid())
            _stabilizeFeetControl.Enabled=_rawMotion is {Space:MotionSpace.WorldRelative,StationaryJoints.Count:>0};
        var restore=_rawMotion?.OriginalReconstruction is not null;
        var followedCamera=_rawMotion?.Diagnostics.Any(d=>d.StartsWith(FollowedCameraPrefix,StringComparison.Ordinal))==true;
        _bodyRefinementButton.Text=restore?"Restore original capture":followedCamera?"Refine · followed camera":"Refine · stationary camera";
        _bodyRefinementButton.Enabled=restore||_rawMotion is {Space:MotionSpace.CameraRelative}&&_rawMotion.Backend.StartsWith("GVHMR",StringComparison.Ordinal);
        _bodyRefinementButton.ToolTip=restore?"Reopen the unchanged original reconstruction. Adjustments remain saved separately with each motion file.":
            "Applied automatically when the recording measured as still. Use it yourself only when you know the camera did not move. Reuses saved GVHMR predictions; keeps the original and opens a separate result. Review contacts before exporting.";
    }
    async Task RefineStationaryBodyAsync()
    {
        if(_processing is not null)return;
        var origin=_rawMotion?.OriginalReconstruction;
        if(origin is null&&(_rawMotion is null||_rawMotion.Space!=MotionSpace.CameraRelative||!_rawMotion.Backend.StartsWith("GVHMR",StringComparison.Ordinal)))
        {_captureStatus.Text="Open an original GVHMR body capture to refine stationary-camera contacts.";return;}
        var original=_editSession?.State.RawPath??_motionPath;
        _processing=new CancellationTokenSource();var token=_processing.Token;
        SetCaptureBusy(true);UpdateExportAvailability();
        try
        {
            string refined;
            if(origin is not null)
            {
                await Task.Run(()=>
                {
                    token.ThrowIfCancellationRequested();var bytes=File.ReadAllBytes(origin.Path);
                    if(!string.Equals(Convert.ToHexString(SHA256.HashData(bytes)),origin.Sha256,StringComparison.OrdinalIgnoreCase))
                        throw new InvalidDataException("The original reconstruction changed. Restore the original file before reverting refinement.");
                },token);
                refined=origin.Path;
            }
            // The manual action is the stationary assumption unless the capture followed its camera.
            else refined=await NativeCapture.RefineBodyAsync(original,ReceiveWorkerProgress,token,
                _rawMotion!.Diagnostics.Any(d=>d.StartsWith(FollowedCameraPrefix,StringComparison.Ordinal)));
            token.ThrowIfCancellationRequested();await EditorPipeline.SwitchToMainThread();
            if(!this.IsValid())return;
            await LoadMotionAsync(refined);
            if(this.IsValid()&&_motionPath==refined&&_bakedPreview is not null)
                _captureStatus.Text=origin is null?"Stationary-camera refinement ready · original capture preserved. Review foot contacts and fast movement.":"Original capture restored.";
        }
        catch(OperationCanceledException)
        {await EditorPipeline.SwitchToMainThread();_queuedVideos.Clear();if(this.IsValid())_captureStatus.Text="Refinement cancelled. Original capture preserved.";}
        catch(Exception error)
        {await EditorPipeline.SwitchToMainThread();if(this.IsValid())_captureStatus.Text=error.Message;}
        finally
        {
            Interlocked.Exchange(ref _workerMessage,null);await EditorPipeline.SwitchToMainThread();
            _processing?.Dispose();_processing=null;
            if(this.IsValid()){SetCaptureBusy(false);UpdateExportAvailability();StartNextQueuedVideo();}
        }
    }
}