// 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