// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
// ReSharper disable HeapView.CanAvoidClosure
namespace WallstopStudios.UnityHelpers.Editor.Sprites
{
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using WallstopStudios.UnityHelpers.Core.Extension;
using WallstopStudios.UnityHelpers.Core.Helper;
using WallstopStudios.UnityHelpers.Editor;
using WallstopStudios.UnityHelpers.Utils;
using WallstopStudios.UnityHelpers.Visuals;
using WallstopStudios.UnityHelpers.Visuals.UIToolkit;
using Object = UnityEngine.Object;
///
/// UI Toolkit-based multi-clip 2D animation viewer and lightweight editor. Load multiple
/// AnimationClips, preview layered sprite animation, reorder frames via drag & drop, adjust
/// preview FPS, and save an updated clip back to disk.
///
///
///
/// Problems this solves: quickly auditing and tweaking sprite-based clips without opening the
/// full Animation window workflow; comparing multiple clips; and adjusting timing visually.
///
///
/// How it works: for a selected clip, the tool resolves the
/// binding path and extracts its frames. The frames list supports reordering via drag & drop
/// with placeholders for clarity. Preview uses an in-editor LayeredImage to animate the
/// sprite sequence at the chosen FPS.
///
///
/// Usage:
///
///
/// - Open via menu: Tools/Wallstop Studios/Unity Helpers/Sprite Animation Editor.
/// - Add clips (object field or project selection button).
/// - Drag frames to reorder, then Save to write an updated clip.
///
///
/// Pros: intuitive drag/drop, live preview, handles multiple clips in a session.
/// Caveats: operates on SpriteRenderer curves only; saving overwrites the target clip asset.
///
///
public sealed class AnimationViewerWindow : EditorWindow
{
private const float DragThresholdSqrMagnitude = 10f * 10f;
private const int InvalidPointerId = -1;
private const string DirToolName = "SpriteAnimationEditor";
private const string DirContextKey = "Clips";
internal sealed class EditorLayerData
{
public AnimationClip SourceClip { get; }
public List Sprites { get; }
public string ClipName => SourceClip != null ? SourceClip.name : "Unnamed Layer";
public float OriginalClipFps { get; }
public string BindingPath { get; }
public EditorLayerData(AnimationClip clip)
{
SourceClip = clip;
Sprites = new List();
if (clip != null)
{
foreach (Sprite s in clip.GetSpritesFromClip())
{
if (s != null)
{
Sprites.Add(s);
}
}
}
OriginalClipFps =
clip.frameRate > 0 ? clip.frameRate : AnimatedSpriteLayer.FrameRate;
BindingPath = string.Empty;
if (SourceClip != null)
{
foreach (
EditorCurveBinding binding in AnimationUtility.GetObjectReferenceCurveBindings(
SourceClip
)
)
{
if (
binding.type == typeof(SpriteRenderer)
&& string.Equals(
binding.propertyName,
"m_Sprite",
StringComparison.Ordinal
)
)
{
BindingPath = binding.path;
break;
}
}
}
}
}
[MenuItem("Tools/Wallstop Studios/Unity Helpers/Sprite Animation Editor")]
public static void ShowWindow()
{
AnimationViewerWindow wnd = GetWindow();
wnd.titleContent = new GUIContent("2D Animation Viewer");
wnd.minSize = new Vector2(750, 500);
}
private VisualTreeAsset _visualTree;
private StyleSheet _styleSheet;
private ObjectField _addAnimationClipField;
private Button _browseAndAddButton;
private FloatField _fpsField;
private Button _applyFpsButton;
private Button _saveClipButton;
private VisualElement _loadedClipsContainer;
private VisualElement _previewPanelHost;
private LayeredImage _animationPreview;
private VisualElement _framesContainer;
private Label _fpsDebugLabel;
private Label _framesPanelTitle;
private MultiFileSelectorElement _fileSelector;
private readonly List _loadedEditorLayers = new();
private EditorLayerData _activeEditorLayer;
private float _currentPreviewFps = AnimatedSpriteLayer.FrameRate;
private VisualElement _draggedFrameElement;
private int _draggedFrameOriginalDataIndex;
private VisualElement _frameDropPlaceholder;
private VisualElement _draggedLoadedClipElement;
private int _draggedLoadedClipOriginalIndex;
private VisualElement _loadedClipDropPlaceholder;
private bool _isClipDragPending;
private Vector3 _clipDragStartPosition;
private VisualElement _clipDragPendingElement;
private int _clipDragPendingOriginalIndex;
public void CreateGUI()
{
VisualElement root = rootVisualElement;
TryLoadStyleSheets();
if (_visualTree == null)
{
root.Add(new Label("Error: AnimationViewer.uxml not found."));
return;
}
if (_styleSheet != null)
{
root.styleSheets.Add(_styleSheet);
}
_visualTree.CloneTree(root);
_addAnimationClipField = root.Q("addAnimationClipField");
_browseAndAddButton = root.Q