Editor/EffigyEditor/EffigyViewport.SoftPreview.cs
using Editor;
using Effigy;
using Sandbox;
using System;

namespace Marionette.EditorTools;

// ============================================================================
//  Watching the soft bones actually wobble.
//
//  WHY A PREVIEW IS NOT OPTIONAL HERE. Stiffness, damping, weight and a cone are
//  four numbers with no readable relationship to what they produce. SoftBone's
//  own summary has to explain that stiffness is an acceleration whose frequency
//  is sqrt of it, and that damping is per SECOND rather than per step - both true,
//  both useless for answering "is 60 too stiff for this tail". That question is
//  answered by looking, and until this file there was nowhere to look: the solver
//  ran in tests and, if a game wired it up, at runtime.
//
//  WHAT DRIVES THE MOTION. Gravity, and the pose gizmo. Nothing else, and that is
//  deliberate rather than minimal:
//
//    - GRAVITY ALONE MAKES ALL FOUR NUMBERS LEGIBLE. Turn preview on and a soft
//      bone sags off its authored direction and settles. How far it sags is
//      stiffness against weight, how long it rings on the way is damping, and
//      where it stops if it would have gone further is the cone. That is the whole
//      parameter set, visible in one second, with no gesture to learn.
//    - DRAGGING A BONE ADDS THE SWING. The pose gizmo already exists and already
//      moves a bone; while the preview runs, everything soft below it lags behind
//      the drag. No new gesture, and it is the real authoring loop - pose the
//      shoulder, watch the tail follow.
//
//  NO CANNED SHAKE. An automatic sway was the other candidate and it is worse in
//  a way that is easy to miss: a rig that moves on its own cannot show you that it
//  has SETTLED, and settling is most of what damping is for. SoftSolver's own test
//  suite asserts that a still rig stays still; a preview that never lets it be
//  still is hiding the property the solver is proudest of.
//
//  THE PREVIEW NEVER TOUCHES THE SKELETON. It writes into its own Xform array and
//  the drawing reads that instead of WorldBind. Turning it off is one field going
//  null - there is no pose to unwind, nothing to restore, and no way for a wobble
//  to end up saved into the document as bind data. That is the same split
//  SoftBone/SoftPose already make between authoring data and solver state, kept at
//  this level too.
// ============================================================================

internal sealed partial class EffigyViewport
{
	/// <summary>
	/// The solved pose, or null when the preview is off.
	///
	/// DOUBLES AS THE ON/OFF FLAG, rather than sitting beside a bool that could disagree with it.
	/// Everything that draws a bone asks <see cref="BoneWorld"/>, which falls back to the bind pose
	/// when this is null, so "not previewing" and "previewing a rig with nothing soft in it" are
	/// the same picture by construction.
	/// </summary>
	private Xform[] _softPreviewPose;

	private SoftPose _softPose;

	/// <summary>When the last solve ran, for the step length. RealTime rather than a frame counter:
	/// the editor's frame rate is whatever the machine and the rest of the window leave over, and
	/// SoftBone's whole damping contract is written per second for exactly that reason.</summary>
	private float _softPreviewLast;

	/// <summary>Whether the soft-bone preview is running, for the bar's tick.</summary>
	public bool SoftPreviewRunning => _softPreviewPose is not null;

	/// <summary>
	/// The longest step the solver is ever handed, in seconds.
	///
	/// A DROPPED FRAME IS NOT A LONG FRAME. Rebuilding the studio, opening a file dialog or a
	/// shader compile can stall the editor for a second or more, and handing that whole second to a
	/// spring integrator explodes it - the bone leaves its cone in one step and the clamp snaps it
	/// somewhere arbitrary. Clamping means a stall shows up as the wobble having happened slightly
	/// slower than real time, which nobody can see, instead of the rig detonating, which everybody
	/// can.
	/// </summary>
	private const float MaxSoftStep = 1f / 30f;

	/// <summary>
	/// Where a bone actually is right now — the solved pose while previewing, the bind pose
	/// otherwise.
	///
	/// EVERYTHING THAT DRAWS A BONE GOES THROUGH HERE, including the pose gizmo and the hit spheres,
	/// so a bone is grabbed where it is SEEN rather than where it would be if it were not swinging.
	/// Two answers to "where is this bone" is exactly the kind of split that produces a handle you
	/// cannot click.
	/// </summary>
	private Xform BoneWorld( int index )
	{
		if ( _softPreviewPose is not null && index >= 0 && index < _softPreviewPose.Length )
			return _softPreviewPose[index];

		return RigSkeleton.WorldBind( index );
	}

	/// <summary>Start or stop the preview. Returns whether it is now running, so the caller can
	/// tick its own button without asking again.</summary>
	public bool ToggleSoftPreview()
	{
		if ( SoftPreviewRunning )
		{
			StopSoftPreview();
			return false;
		}

		if ( RigSkeleton is null || RigSkeleton.Count == 0 )
			return false;

		_softPose = new SoftPose( RigSkeleton.Count );
		_softPreviewPose = SoftSolver.BindPose( RigSkeleton );
		_softPreviewLast = RealTime.Now;

		Update();

		return true;
	}

	public void StopSoftPreview()
	{
		_softPreviewPose = null;
		_softPose = null;

		Update();
	}

	/// <summary>
	/// Forget the motion — every soft bone snaps back onto its pose and starts again from rest.
	///
	/// SoftPose.Rest is the kernel's own word for this and does the whole job: the next solve
	/// PLACES the tails instead of easing them in from wherever they had swung to. Worth a button
	/// because tuning means typing a number, watching, and typing another, and a bone still ringing
	/// from the last value makes the next one impossible to judge.
	/// </summary>
	public void RestSoftPreview()
	{
		if ( !SoftPreviewRunning )
			return;

		_softPose.Rest();
		_softPreviewPose = SoftSolver.BindPose( RigSkeleton );
		_softPreviewLast = RealTime.Now;

		Update();
	}

	/// <summary>
	/// One step, run from the frame loop just before the skeleton is drawn.
	///
	/// THE ANIMATED POSE IS REBUILT FROM THE SKELETON EVERY FRAME rather than carried forward from
	/// the last solve. That is what makes dragging a bone with the pose gizmo drive the wobble: the
	/// gizmo writes the bone's Local, BindPose picks the change up on the next frame, and the
	/// solver sees a target that moved and lags behind it. Feeding last frame's SOLVED pose back in
	/// would instead compound the softness into itself, and a chain would drift away and never come
	/// back.
	/// </summary>
	private void SoftPreviewFrame()
	{
		if ( !SoftPreviewRunning || RigSkeleton is null )
			return;

		// The skeleton changed size under us — a bone placed or deleted while previewing. The pose
		// arrays are per-bone, so they are rebuilt rather than indexed past their end.
		if ( _softPose.Tail.Length != RigSkeleton.Count )
		{
			_softPose = new SoftPose( RigSkeleton.Count );
			_softPreviewPose = SoftSolver.BindPose( RigSkeleton );
			_softPreviewLast = RealTime.Now;
			return;
		}

		var now = RealTime.Now;
		var dt = MathF.Min( now - _softPreviewLast, MaxSoftStep );

		_softPreviewLast = now;

		// Solve writes back into the array it is given, so the animated pose has to be a fresh
		// read of the skeleton rather than the previous result. See the summary.
		_softPreviewPose = SoftSolver.BindPose( RigSkeleton );

		SoftSolver.Solve( RigSkeleton, _softPreviewPose, _softPose, dt );

		// A spring that has not settled has more to show next frame, and nothing else in this
		// viewport is going to ask for a repaint while the mouse is still.
		Update();
	}
}