Editor/EffigyEditor/EffigyViewport.PosePreview.cs
using Effigy;
using System;

namespace Marionette.EditorTools;

// ============================================================================
//  Posing: drag a bone and watch the mesh follow.
//
//  WHY THIS WAS MISSING, AND WHY IT IS THE RIG'S WHOLE FEEDBACK LOOP. The rig
//  panel could place bones and pin bodies to them, and the mesh would only show
//  the result after export — SkinBinder ran in exactly one place, CompileVmdl.
//  So a weight was a number in a panel rather than a crease in a surface, and
//  the only way to find out whether a rig worked was to open it somewhere else.
//
//  The pose is already in hand — the pose gizmo writes bone.Local — it simply
//  was not used for anything but an inspector readout. This uses it: the bind
//  pose is snapshotted when the preview arms, the gizmo's Local is the current
//  pose, and SkinBinder.Deform carries one into the other onto a DISPLAY copy of
//  the mesh. The cage is never touched; the pose is never saved.
//
//  A POSE PREVIEW IS NOT A TIMELINE. One transform per bone, no keys, no
//  playback. Reset snaps back to the bind pose; turning it off leaves the bind
//  pose exactly as it was authored, because the pose never wrote to Local.
// ============================================================================

internal sealed partial class EffigyViewport
{
	/// <summary>Whether the pose preview is armed — a bind snapshot and weights exist.</summary>
	public bool PosePreviewActive { get; private set; }

	// The bind pose (a full clone, so Reset can restore Local AND Length), the weights the mesh is
	// skinned with, and the bind mesh the deformation is applied to. The skeleton is cloned rather
	// than referenced: the pose gizmo mutates RigSkeleton.Bones[i].Local in place, so a reference
	// would already BE the pose by the time anyone tried to reset to it.
	private Skeleton _poseBindSkeleton;
	private SkinWeights _poseWeights;
	private PolyMesh _poseBaseMesh;

	/// <summary>Fired whenever the posed skeleton changed, so the window re-deforms the display
	/// mesh. The pose gizmo fires BonePosed on every drag frame; this is the pose-preview half of
	/// the same signal.</summary>
	public Action PoseChanged { get; set; }

	/// <summary>
	/// Arm the preview against a mesh and its weights. The skeleton is the rig's current state,
	/// which becomes the bind pose the next drag is measured against. Returns false when there is
	/// nothing to pose — no mesh, no weights, or no bones.
	/// </summary>
	public bool ArmPosePreview( PolyMesh baseMesh, SkinWeights weights, Skeleton skeleton )
	{
		if ( baseMesh is null || baseMesh.VertexCount == 0 || weights is null || weights.Count == 0
			|| skeleton is null || skeleton.Count == 0 )
			return false;

		_poseBaseMesh = baseMesh;
		_poseWeights = weights;
		_poseBindSkeleton = skeleton.Clone();
		PosePreviewActive = true;
		return true;
	}

	/// <summary>Stop posing, without touching the skeleton. The bind pose was never modified.</summary>
	public void DisarmPosePreview()
	{
		PosePreviewActive = false;
		_poseBaseMesh = null;
		_poseWeights = null;
		_poseBindSkeleton = null;
	}

	/// <summary>
	/// The bind mesh deformed by the current pose, or null when the preview is off or the skeleton
	/// changed shape under it (a bone placed or deleted) — the caller re-arms rather than guess.
	/// </summary>
	public PolyMesh PoseMesh()
	{
		if ( !PosePreviewActive || _poseBindSkeleton is null || _poseWeights is null || _poseBaseMesh is null )
			return null;

		// A bone added or removed while posing invalidates the weights (bone indices) and the bind
		// snapshot. Refuse rather than index past an array end — the caller arms a fresh preview.
		if ( RigSkeleton is null || RigSkeleton.Count != _poseBindSkeleton.Count )
			return null;

		var bind = new Xform[_poseBindSkeleton.Count];
		for ( var i = 0; i < bind.Length; i++ )
			bind[i] = _poseBindSkeleton.WorldBind( i );

		var pose = new Xform[RigSkeleton.Count];
		for ( var i = 0; i < pose.Length; i++ )
			pose[i] = RigSkeleton.WorldBind( i );

		var deformed = SkinBinder.Deform( _poseBaseMesh.Positions, _poseWeights, bind, pose );

		var result = _poseBaseMesh.Clone();
		for ( var i = 0; i < result.Positions.Count; i++ )
			result.Positions[i] = deformed[i];

		return result;
	}

	/// <summary>
	/// Snap the skeleton back to the bind pose it was armed with. Matches by NAME rather than index
	/// so a bone deleted and re-added under the same name still resets. Local and Length both
	/// restored — the gizmo writes both.
	/// </summary>
	public void ResetPose()
	{
		if ( !PosePreviewActive || _poseBindSkeleton is null || RigSkeleton is null )
			return;

		for ( var i = 0; i < RigSkeleton.Count; i++ )
		{
			var bind = _poseBindSkeleton.IndexOf( RigSkeleton.Bones[i].Name );
			if ( bind < 0 )
				continue;

			RigSkeleton.Bones[i].Local = _poseBindSkeleton.Bones[bind].Local;
			RigSkeleton.Bones[i].Length = _poseBindSkeleton.Bones[bind].Length;
		}
	}
}