chore: initial commit
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
|
||||
namespace Animancer.Editor.Tools
|
||||
{
|
||||
/// <summary>[Editor-Only] Displays the <see cref="AnimancerSettings"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/AnimancerSettingsTool
|
||||
[Serializable]
|
||||
public class AnimancerSettingsTool : AnimancerToolsWindow.Tool
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int DisplayOrder => int.MaxValue;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Name => "Animancer Settings";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Instructions => null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string HelpURL
|
||||
=> $"{Strings.DocsURLs.APIDocumentation}.{nameof(Editor)}/{nameof(AnimancerSettings)}";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[NonSerialized] private readonly CachedEditor SettingsEditor = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnDisable()
|
||||
{
|
||||
base.OnDisable();
|
||||
SettingsEditor.Dispose();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoBodyGUI()
|
||||
{
|
||||
var settings = AnimancerSettings.Instance;
|
||||
if (settings == null)
|
||||
return;
|
||||
|
||||
AnimancerSettings.Editor.HideNextInfo = true;
|
||||
|
||||
SettingsEditor.GetEditor(settings).OnInspectorGUI();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 809481e9287856440bbcfa5e886163d4
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,279 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor.Tools
|
||||
{
|
||||
/// <summary>[Editor-Only] [Pro-Only]
|
||||
/// An <see cref="EditorWindow"/> with various utilities for managing sprites and generating animations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/tools">
|
||||
/// Animancer Tools</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/AnimancerToolsWindow
|
||||
///
|
||||
public sealed partial class AnimancerToolsWindow : EditorWindow
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The display name of this window.</summary>
|
||||
public const string Name = "Animancer Tools";
|
||||
|
||||
/// <summary>The singleton instance of this window.</summary>
|
||||
public static AnimancerToolsWindow Instance { get; private set; }
|
||||
|
||||
[SerializeReference] private List<Tool> _Tools;
|
||||
|
||||
[SerializeField] private Vector2 _Scroll;
|
||||
|
||||
[SerializeField] private int _CurrentTool = -1;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private SerializedObject _SerializedObject;
|
||||
|
||||
private SerializedObject SerializedObject
|
||||
=> _SerializedObject ??= new(this);
|
||||
|
||||
/// <summary>Returns the <see cref="SerializedProperty"/> which represents the specified `tool`.</summary>
|
||||
public SerializedProperty FindSerializedPropertyForTool(Tool tool)
|
||||
{
|
||||
var index = _Tools.IndexOf(tool);
|
||||
var property = SerializedObject.FindProperty(nameof(_Tools));
|
||||
return property.GetArrayElementAtIndex(index);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
titleContent = new(Name);
|
||||
Instance = this;
|
||||
|
||||
InitializeTools();
|
||||
|
||||
Undo.undoRedoPerformed += Repaint;
|
||||
|
||||
OnSelectionChange();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void InitializeTools()
|
||||
{
|
||||
AnimancerEditorUtilities.InstantiateDerivedTypes(ref _Tools);
|
||||
|
||||
for (int i = 0; i < _Tools.Count; i++)
|
||||
_Tools[i].OnEnable(i);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private int IndexOfTool(Type type)
|
||||
{
|
||||
for (int i = 0; i < _Tools.Count; i++)
|
||||
if (_Tools[i].GetType() == type)
|
||||
return i;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
Undo.undoRedoPerformed -= Repaint;
|
||||
|
||||
for (int i = 0; i < _Tools.Count; i++)
|
||||
_Tools[i].OnDisable();
|
||||
|
||||
if (_SerializedObject != null)
|
||||
{
|
||||
_SerializedObject.Dispose();
|
||||
_SerializedObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void OnSelectionChange()
|
||||
{
|
||||
for (int i = 0; i < _Tools.Count; i++)
|
||||
_Tools[i].OnSelectionChanged();
|
||||
|
||||
Repaint();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EditorGUIUtility.labelWidth = Mathf.Min(EditorGUIUtility.labelWidth, position.width * 0.5f);
|
||||
EditorGUIUtility.wideMode = true;
|
||||
|
||||
_Scroll = GUILayout.BeginScrollView(_Scroll);
|
||||
GUILayout.BeginVertical();
|
||||
GUILayout.EndVertical();
|
||||
for (int i = 0; i < _Tools.Count; i++)
|
||||
_Tools[i].DoGUI();
|
||||
GUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Causes the <see cref="Instance"/> to redraw its GUI.</summary>
|
||||
public static new void Repaint()
|
||||
{
|
||||
if (Instance != null)
|
||||
((EditorWindow)Instance).Repaint();
|
||||
}
|
||||
|
||||
/// <summary>Calls <see cref="Undo.RecordObject(Object, string)"/> for this window.</summary>
|
||||
public static void RecordUndo()
|
||||
=> Undo.RecordObject(Instance, Name);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="EditorGUI.BeginChangeCheck"/>.</summary>
|
||||
public static void BeginChangeCheck()
|
||||
=> EditorGUI.BeginChangeCheck();
|
||||
|
||||
/// <summary>Calls <see cref="EditorGUI.EndChangeCheck"/> and <see cref="RecordUndo"/> if it returned true.</summary>
|
||||
public static bool EndChangeCheck()
|
||||
{
|
||||
if (!EditorGUI.EndChangeCheck())
|
||||
return false;
|
||||
|
||||
RecordUndo();
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Calls <see cref="EndChangeCheck"/> and sets the <c>field = value</c> if it returned true.</summary>
|
||||
public static bool EndChangeCheck<T>(ref T field, T value)
|
||||
{
|
||||
if (!EndChangeCheck())
|
||||
return false;
|
||||
|
||||
field = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates and initializes a new <see cref="ReorderableList"/>.</summary>
|
||||
public static ReorderableList CreateReorderableList<T>(
|
||||
List<T> list,
|
||||
string name,
|
||||
ReorderableList.ElementCallbackDelegate drawElementCallback,
|
||||
bool showFooter = false)
|
||||
{
|
||||
var reorderableList = new ReorderableList(list, typeof(T))
|
||||
{
|
||||
drawHeaderCallback = (area) => GUI.Label(area, name),
|
||||
drawElementCallback = drawElementCallback,
|
||||
elementHeight = AnimancerGUI.LineHeight + AnimancerGUI.StandardSpacing,
|
||||
};
|
||||
|
||||
if (!showFooter)
|
||||
{
|
||||
reorderableList.footerHeight = 0;
|
||||
reorderableList.displayAdd = false;
|
||||
reorderableList.displayRemove = false;
|
||||
}
|
||||
|
||||
return reorderableList;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates and initializes a new <see cref="ReorderableList"/> for <see cref="Sprite"/>s.</summary>
|
||||
public static ReorderableList CreateReorderableObjectList<T>(
|
||||
List<T> objects,
|
||||
string name,
|
||||
bool showFooter = false)
|
||||
where T : Object
|
||||
{
|
||||
var reorderableList = CreateReorderableList(objects, name, (area, index, isActive, isFocused) =>
|
||||
{
|
||||
area.y = Mathf.Ceil(area.y + AnimancerGUI.StandardSpacing * 0.5f);
|
||||
area.height = AnimancerGUI.LineHeight;
|
||||
|
||||
BeginChangeCheck();
|
||||
var obj = AnimancerGUI.DoObjectFieldGUI(area, "", objects[index], false);
|
||||
if (EndChangeCheck())
|
||||
{
|
||||
objects[index] = obj;
|
||||
}
|
||||
}, showFooter);
|
||||
|
||||
if (showFooter)
|
||||
{
|
||||
reorderableList.onAddCallback = (list) => list.list.Add(null);
|
||||
}
|
||||
|
||||
return reorderableList;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="ReorderableList"/> for <see cref="string"/>s.</summary>
|
||||
public static ReorderableList CreateReorderableStringList(
|
||||
List<string> strings,
|
||||
string name,
|
||||
Func<Rect, int, string> doElementGUI)
|
||||
{
|
||||
return CreateReorderableList(strings, name, (area, index, isActive, isFocused) =>
|
||||
{
|
||||
area.y = Mathf.Ceil(area.y + AnimancerGUI.StandardSpacing * 0.5f);
|
||||
area.height = AnimancerGUI.LineHeight;
|
||||
|
||||
BeginChangeCheck();
|
||||
var str = doElementGUI(area, index);
|
||||
if (EndChangeCheck())
|
||||
{
|
||||
strings[index] = str;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Creates a new <see cref="ReorderableList"/> for <see cref="string"/>s.</summary>
|
||||
public static ReorderableList CreateReorderableStringList(
|
||||
List<string> strings,
|
||||
string name)
|
||||
{
|
||||
return CreateReorderableStringList(strings, name, (area, index) =>
|
||||
{
|
||||
return EditorGUI.TextField(area, strings[index]);
|
||||
});
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Opens the <see cref="AnimancerToolsWindow"/>.</summary>
|
||||
[MenuItem(Strings.AnimancerToolsMenuPath)]
|
||||
public static void Open()
|
||||
=> GetWindow<AnimancerToolsWindow>();
|
||||
|
||||
/// <summary>Opens the <see cref="AnimancerToolsWindow"/> showing the specified `tool`.</summary>
|
||||
public static void Open(Type toolType)
|
||||
{
|
||||
var window = GetWindow<AnimancerToolsWindow>();
|
||||
window._CurrentTool = AnimancerEditorUtilities.IndexOfType(window._Tools, toolType);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 92f977efd2d1e4c49ada1f068a739289
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,101 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor.Tools
|
||||
{
|
||||
/// <summary>[Editor-Only] [Pro-Only]
|
||||
/// A base <see cref="AnimancerToolsWindow.Tool"/> for modifying <see cref="AnimationClip"/>s.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/tools">
|
||||
/// Animancer Tools</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/AnimationModifierTool
|
||||
///
|
||||
[Serializable]
|
||||
public abstract class AnimationModifierTool : AnimancerToolsWindow.Tool
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private AnimationClip _Animation;
|
||||
|
||||
/// <summary>The currently selected <see cref="AnimationClip"/> asset.</summary>
|
||||
public AnimationClip Animation => _Animation;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnEnable(int index)
|
||||
{
|
||||
base.OnEnable(index);
|
||||
OnAnimationChanged();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnSelectionChanged()
|
||||
{
|
||||
if (Selection.activeObject is AnimationClip animation)
|
||||
{
|
||||
_Animation = animation;
|
||||
OnAnimationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Called whenever the selected <see cref="Animation"/> changes.</summary>
|
||||
protected virtual void OnAnimationChanged() { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoBodyGUI()
|
||||
{
|
||||
AnimancerToolsWindow.BeginChangeCheck();
|
||||
|
||||
var animation = AnimancerGUI.DoObjectFieldGUI("Animation", _Animation, false);
|
||||
|
||||
if (AnimancerToolsWindow.EndChangeCheck(ref _Animation, animation))
|
||||
OnAnimationChanged();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="AnimancerToolsWindow.Tool.SaveModifiedAsset"/> on the animation.</summary>
|
||||
protected bool SaveAs()
|
||||
{
|
||||
AnimancerGUI.Deselect();
|
||||
|
||||
if (SaveModifiedAsset(
|
||||
"Save Modified Animation",
|
||||
"Where would you like to save the new animation?",
|
||||
_Animation,
|
||||
Modify))
|
||||
{
|
||||
_Animation = null;
|
||||
OnAnimationChanged();
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Override this to apply the desired modifications to the `animation` before it is saved.</summary>
|
||||
protected virtual void Modify(AnimationClip animation) { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ab2916f8d63d1d4e9471536b78698a3
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,611 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
using static Animancer.Editor.AnimancerGUI;
|
||||
|
||||
namespace Animancer.Editor.Tools
|
||||
{
|
||||
/// <summary>[Editor-Only] [Pro-Only]
|
||||
/// A <see cref="SpriteModifierTool"/> for generating <see cref="AnimationClip"/>s from <see cref="Sprite"/>s.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/tools/generate-sprite-animations">
|
||||
/// Generate Sprite Animations</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/GenerateSpriteAnimationsTool
|
||||
///
|
||||
[Serializable]
|
||||
public class GenerateSpriteAnimationsTool : SpriteModifierTool
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#region Tool
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[NonSerialized] private List<string> _Names;
|
||||
[NonSerialized] private Dictionary<string, List<Sprite>> _NameToSprites;
|
||||
[NonSerialized] private ReorderableList _Display;
|
||||
[NonSerialized] private bool _NamesAreDirty;
|
||||
[NonSerialized] private double _PreviewStartTime;
|
||||
[NonSerialized] private long _PreviewFrameIndex;
|
||||
[NonSerialized] private bool _RequiresRepaint;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int DisplayOrder => 3;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Name => "Generate Sprite Animations";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string HelpURL => Strings.DocsURLs.GenerateSpriteAnimations;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Instructions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Sprites.Count == 0)
|
||||
return "Select the Sprites you want to generate animations from.";
|
||||
|
||||
return "Configure the animation settings then click Generate.";
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnEnable(int index)
|
||||
{
|
||||
base.OnEnable(index);
|
||||
|
||||
_Names = new();
|
||||
_NameToSprites = new();
|
||||
|
||||
_Display = AnimancerToolsWindow.CreateReorderableList(
|
||||
_Names,
|
||||
"Animations to Generate",
|
||||
DrawDisplayElement);
|
||||
_Display.elementHeightCallback = CalculateDisplayElementHeight;
|
||||
|
||||
_PreviewStartTime = EditorApplication.timeSinceStartup;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnSelectionChanged()
|
||||
{
|
||||
_NameToSprites.Clear();
|
||||
_Names.Clear();
|
||||
_NamesAreDirty = true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoBodyGUI()
|
||||
{
|
||||
var property = GenerateSpriteAnimationsSettings.SerializedProperty;
|
||||
property.serializedObject.Update();
|
||||
using (var label = PooledGUIContent.Acquire("Settings"))
|
||||
EditorGUILayout.PropertyField(property, label, true);
|
||||
property.serializedObject.ApplyModifiedProperties();
|
||||
|
||||
GenerateSpriteAnimationsSettings.Instance.FillDefaults();
|
||||
|
||||
var sprites = Sprites;
|
||||
|
||||
if (_NamesAreDirty)
|
||||
{
|
||||
_NamesAreDirty = false;
|
||||
GatherNameToSprites(sprites, _NameToSprites);
|
||||
_Names.AddRange(_NameToSprites.Keys);
|
||||
}
|
||||
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
var previewCurrentTime = EditorApplication.timeSinceStartup - _PreviewStartTime;
|
||||
_PreviewFrameIndex = (long)(previewCurrentTime * GenerateSpriteAnimationsSettings.FrameRate);
|
||||
|
||||
_Display.DoLayoutList();
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
GUI.enabled = sprites.Count > 0;
|
||||
|
||||
if (GUILayout.Button("Generate"))
|
||||
{
|
||||
Deselect();
|
||||
GenerateAnimationsBySpriteName(sprites);
|
||||
}
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
EditorGUILayout.HelpBox("This function is also available via:" +
|
||||
"\n• The 'Assets/Create/Animancer' menu." +
|
||||
"\n• The Context Menu in the top right of the Inspector for Sprite and Texture assets",
|
||||
MessageType.Info);
|
||||
|
||||
if (_RequiresRepaint)
|
||||
{
|
||||
_RequiresRepaint = false;
|
||||
AnimancerToolsWindow.Repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calculates the height of an animation to generate.</summary>
|
||||
private float CalculateDisplayElementHeight(int index)
|
||||
{
|
||||
if (_NameToSprites.Count <= 0 || _Names.Count <= 0)
|
||||
return 0;
|
||||
|
||||
var lineCount = _NameToSprites[_Names[index]].Count + 3;
|
||||
return (LineHeight + StandardSpacing) * lineCount;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the details of an animation to generate.</summary>
|
||||
private void DrawDisplayElement(Rect area, int index, bool isActive, bool isFocused)
|
||||
{
|
||||
area.y = Mathf.Ceil(area.y + StandardSpacing * 0.5f);
|
||||
area.height = LineHeight;
|
||||
|
||||
DrawAnimationHeader(ref area, index, out var sprites);
|
||||
DrawAnimationBody(ref area, sprites);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the name and preview of an animation to generate.</summary>
|
||||
private void DrawAnimationHeader(ref Rect area, int index, out List<Sprite> sprites)
|
||||
{
|
||||
var width = area.width;
|
||||
|
||||
var previewSize = 3 * LineHeight + 2 * StandardSpacing;
|
||||
var previewArea = StealFromRight(ref area, previewSize, StandardSpacing);
|
||||
previewArea.height = previewSize;
|
||||
|
||||
// Name.
|
||||
|
||||
var name = _Names[index];
|
||||
|
||||
AnimancerToolsWindow.BeginChangeCheck();
|
||||
name = EditorGUI.TextField(area, name);
|
||||
if (AnimancerToolsWindow.EndChangeCheck())
|
||||
{
|
||||
_Names[index] = name;
|
||||
}
|
||||
|
||||
NextVerticalArea(ref area);
|
||||
|
||||
// Frame Count.
|
||||
|
||||
sprites = _NameToSprites[name];
|
||||
|
||||
var frame = (int)(_PreviewFrameIndex % sprites.Count);
|
||||
|
||||
var enabled = GUI.enabled;
|
||||
GUI.enabled = false;
|
||||
|
||||
EditorGUI.TextField(area, $"Frame {frame} / {sprites.Count}");
|
||||
|
||||
NextVerticalArea(ref area);
|
||||
|
||||
// Preview Time.
|
||||
|
||||
GUI.enabled = true;
|
||||
|
||||
var beforeControlID = GUIUtility.GetControlID(FocusType.Passive);
|
||||
|
||||
var newFrame = EditorGUI.IntSlider(area, frame, 0, sprites.Count);
|
||||
|
||||
var afterControlID = GUIUtility.GetControlID(FocusType.Passive);
|
||||
var hotControl = GUIUtility.hotControl;
|
||||
|
||||
if (newFrame != frame ||
|
||||
(hotControl > beforeControlID && hotControl < afterControlID))
|
||||
{
|
||||
_PreviewStartTime = EditorApplication.timeSinceStartup;
|
||||
_PreviewStartTime -= newFrame / GenerateSpriteAnimationsSettings.FrameRate;
|
||||
_PreviewFrameIndex = newFrame;
|
||||
frame = newFrame % sprites.Count;
|
||||
}
|
||||
|
||||
GUI.enabled = enabled;
|
||||
|
||||
NextVerticalArea(ref area);
|
||||
|
||||
area.width = width;
|
||||
|
||||
// Preview.
|
||||
|
||||
DrawSprite(previewArea, sprites[frame]);
|
||||
_RequiresRepaint = true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the sprite contents of an animation to generate.</summary>
|
||||
private void DrawAnimationBody(ref Rect area, List<Sprite> sprites)
|
||||
{
|
||||
var previewFrame = (int)(_PreviewFrameIndex % sprites.Count);
|
||||
|
||||
for (int i = 0; i < sprites.Count; i++)
|
||||
{
|
||||
var sprite = sprites[i];
|
||||
|
||||
var fieldArea = area;
|
||||
var thumbnailArea = StealFromLeft(
|
||||
ref fieldArea,
|
||||
fieldArea.height,
|
||||
StandardSpacing);
|
||||
|
||||
AnimancerToolsWindow.BeginChangeCheck();
|
||||
sprite = DoObjectFieldGUI(fieldArea, "", sprite, false);
|
||||
if (AnimancerToolsWindow.EndChangeCheck())
|
||||
{
|
||||
sprites[i] = sprite;
|
||||
}
|
||||
|
||||
if (i == previewFrame)
|
||||
EditorGUI.DrawRect(fieldArea, new(0.25f, 1, 0.25f, 0.1f));
|
||||
|
||||
DrawSprite(thumbnailArea, sprite);
|
||||
|
||||
NextVerticalArea(ref area);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Methods
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Uses <see cref="GatherNameToSprites"/> and creates new animations from those groups.</summary>
|
||||
private static void GenerateAnimationsBySpriteName(List<Sprite> sprites)
|
||||
{
|
||||
if (sprites.Count == 0)
|
||||
return;
|
||||
|
||||
sprites.Sort(NaturalCompare);
|
||||
|
||||
var nameToSprites = new Dictionary<string, List<Sprite>>();
|
||||
GatherNameToSprites(sprites, nameToSprites);
|
||||
|
||||
var pathToSprites = new Dictionary<string, List<Sprite>>();
|
||||
|
||||
var message = StringBuilderPool.Instance.Acquire()
|
||||
.Append("Do you wish to generate the following animations?");
|
||||
|
||||
const int MaxLines = 25;
|
||||
var line = 0;
|
||||
foreach (var nameToSpriteGroup in nameToSprites)
|
||||
{
|
||||
var path = AssetDatabase.GetAssetPath(nameToSpriteGroup.Value[0]);
|
||||
path = Path.GetDirectoryName(path);
|
||||
path = Path.Combine(path, nameToSpriteGroup.Key + ".anim");
|
||||
pathToSprites.Add(path, nameToSpriteGroup.Value);
|
||||
|
||||
if (++line <= MaxLines)
|
||||
{
|
||||
message.AppendLine()
|
||||
.Append("- ")
|
||||
.Append(path)
|
||||
.Append(" (")
|
||||
.Append(nameToSpriteGroup.Value.Count)
|
||||
.Append(" frames)");
|
||||
}
|
||||
}
|
||||
|
||||
if (line > MaxLines)
|
||||
{
|
||||
message.AppendLine()
|
||||
.Append("And ")
|
||||
.Append(line - MaxLines)
|
||||
.Append(" others.");
|
||||
}
|
||||
|
||||
if (!EditorUtility.DisplayDialog("Generate Sprite Animations?", message.ReleaseToString(), "Generate", "Cancel"))
|
||||
return;
|
||||
|
||||
foreach (var pathToSpriteGroup in pathToSprites)
|
||||
CreateAnimation(pathToSpriteGroup.Key, pathToSpriteGroup.Value.ToArray());
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static char[] _Numbers, _TrimOther;
|
||||
|
||||
/// <summary>Groups the `sprites` by name into the `nameToSptires`.</summary>
|
||||
private static void GatherNameToSprites(List<Sprite> sprites, Dictionary<string, List<Sprite>> nameToSprites)
|
||||
{
|
||||
for (int i = 0; i < sprites.Count; i++)
|
||||
{
|
||||
var sprite = sprites[i];
|
||||
var name = sprite.name;
|
||||
|
||||
// Remove numbers from the end.
|
||||
_Numbers ??= new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
|
||||
name = name.TrimEnd(_Numbers);
|
||||
|
||||
// Then remove other characters from the end.
|
||||
_TrimOther ??= new char[] { ' ', '_', '-' };
|
||||
name = name.TrimEnd(_TrimOther);
|
||||
|
||||
// Doing both at once would turn "Attack2-0" (Attack 2 Frame 0) into "Attack" (losing the number).
|
||||
|
||||
if (!nameToSprites.TryGetValue(name, out var spriteGroup))
|
||||
{
|
||||
spriteGroup = new();
|
||||
nameToSprites.Add(name, spriteGroup);
|
||||
}
|
||||
|
||||
// Add the sprite to the group if it's not a duplicate.
|
||||
if (spriteGroup.Count == 0 || spriteGroup[^1] != sprite)
|
||||
spriteGroup.Add(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates and saves a new <see cref="AnimationClip"/> that plays the `sprites`.</summary>
|
||||
private static void CreateAnimation(string path, params Sprite[] sprites)
|
||||
{
|
||||
var frameRate = GenerateSpriteAnimationsSettings.FrameRate;
|
||||
var hierarchyPath = GenerateSpriteAnimationsSettings.HierarchyPath;
|
||||
var type = GenerateSpriteAnimationsSettings.TargetType.Type ?? typeof(SpriteRenderer);
|
||||
|
||||
var property = GenerateSpriteAnimationsSettings.PropertyName;
|
||||
if (string.IsNullOrWhiteSpace(property))
|
||||
property = "m_Sprite";
|
||||
|
||||
var clip = new AnimationClip
|
||||
{
|
||||
frameRate = frameRate,
|
||||
};
|
||||
|
||||
var spriteKeyFrames = new ObjectReferenceKeyframe[sprites.Length];
|
||||
for (int i = 0; i < spriteKeyFrames.Length; i++)
|
||||
{
|
||||
spriteKeyFrames[i] = new()
|
||||
{
|
||||
time = i / (float)frameRate,
|
||||
value = sprites[i]
|
||||
};
|
||||
}
|
||||
|
||||
var spriteBinding = EditorCurveBinding.PPtrCurve(hierarchyPath, type, property);
|
||||
AnimationUtility.SetObjectReferenceCurve(clip, spriteBinding, spriteKeyFrames);
|
||||
|
||||
AssetDatabase.CreateAsset(clip, path);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Menu Functions
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private const string GenerateAnimationsBySpriteNameFunctionName = "Generate Animations By Sprite Name";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Should <see cref="GenerateAnimationsBySpriteName()"/> be enabled or greyed out?</summary>
|
||||
[MenuItem(Strings.CreateMenuPrefix + GenerateAnimationsBySpriteNameFunctionName, validate = true)]
|
||||
private static bool ValidateGenerateAnimationsBySpriteName()
|
||||
{
|
||||
var selection = Selection.objects;
|
||||
for (int i = 0; i < selection.Length; i++)
|
||||
{
|
||||
var selected = selection[i];
|
||||
if (selected is Sprite || selected is Texture)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Calls <see cref="GenerateAnimationsBySpriteName(List{Sprite})"/> with the selected <see cref="Sprite"/>s.</summary>
|
||||
[MenuItem(
|
||||
itemName: Strings.CreateMenuPrefix + GenerateAnimationsBySpriteNameFunctionName,
|
||||
priority = Strings.AssetMenuOrder + 8)]
|
||||
private static void GenerateAnimationsBySpriteName()
|
||||
{
|
||||
var sprites = new List<Sprite>();
|
||||
|
||||
var selection = Selection.objects;
|
||||
for (int i = 0; i < selection.Length; i++)
|
||||
{
|
||||
var selected = selection[i];
|
||||
if (selected is Sprite sprite)
|
||||
{
|
||||
sprites.Add(sprite);
|
||||
}
|
||||
else if (selected is Texture2D texture)
|
||||
{
|
||||
sprites.AddRange(LoadAllSpritesInTexture(texture));
|
||||
}
|
||||
}
|
||||
|
||||
GenerateAnimationsBySpriteName(sprites);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static List<Sprite> _CachedSprites;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of <see cref="Sprite"/>s which will be passed into
|
||||
/// <see cref="GenerateAnimationsBySpriteName(List{Sprite})"/> by <see cref="EditorApplication.delayCall"/>.
|
||||
/// </summary>
|
||||
private static List<Sprite> GetCachedSpritesToGenerateAnimations()
|
||||
{
|
||||
if (_CachedSprites == null)
|
||||
return _CachedSprites = new();
|
||||
|
||||
// Delay the call in case multiple objects are selected.
|
||||
if (_CachedSprites.Count == 0)
|
||||
{
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
GenerateAnimationsBySpriteName(_CachedSprites);
|
||||
_CachedSprites.Clear();
|
||||
};
|
||||
}
|
||||
|
||||
return _CachedSprites;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Adds the <see cref="MenuCommand.context"/> to the <see cref="GetCachedSpritesToGenerateAnimations"/>.
|
||||
/// </summary>
|
||||
[MenuItem("CONTEXT/" + nameof(Sprite) + GenerateAnimationsBySpriteNameFunctionName)]
|
||||
private static void GenerateAnimationsFromSpriteByName(MenuCommand command)
|
||||
{
|
||||
GetCachedSpritesToGenerateAnimations().Add((Sprite)command.context);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Should <see cref="GenerateAnimationsFromTextureBySpriteName"/> be enabled or greyed out?</summary>
|
||||
[MenuItem("CONTEXT/" + nameof(TextureImporter) + GenerateAnimationsBySpriteNameFunctionName, validate = true)]
|
||||
private static bool ValidateGenerateAnimationsFromTextureBySpriteName(MenuCommand command)
|
||||
{
|
||||
var importer = (TextureImporter)command.context;
|
||||
var sprites = LoadAllSpritesAtPath(importer.assetPath);
|
||||
return sprites.Length > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all <see cref="Sprite"/> sub-assets of the <see cref="MenuCommand.context"/> to the
|
||||
/// <see cref="GetCachedSpritesToGenerateAnimations"/>.
|
||||
/// </summary>
|
||||
[MenuItem("CONTEXT/" + nameof(TextureImporter) + GenerateAnimationsBySpriteNameFunctionName)]
|
||||
private static void GenerateAnimationsFromTextureBySpriteName(MenuCommand command)
|
||||
{
|
||||
var cachedSprites = GetCachedSpritesToGenerateAnimations();
|
||||
var importer = (TextureImporter)command.context;
|
||||
cachedSprites.AddRange(LoadAllSpritesAtPath(importer.assetPath));
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Settings
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only] Settings for <see cref="GenerateSpriteAnimationsTool"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/GenerateSpriteAnimationsSettings
|
||||
[Serializable, InternalSerializableType]
|
||||
public class GenerateSpriteAnimationsSettings : AnimancerSettingsGroup
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gets or creates an instance.</summary>
|
||||
public static GenerateSpriteAnimationsSettings Instance
|
||||
=> AnimancerSettingsGroup<GenerateSpriteAnimationsSettings>.Instance;
|
||||
|
||||
/// <summary>The <see cref="UnityEditor.SerializedProperty"/> representing the <see cref="Instance"/>.</summary>
|
||||
public static SerializedProperty SerializedProperty
|
||||
=> Instance.GetSerializedProperty(null);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName
|
||||
=> "Generate Sprite Animations Tool";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int Index
|
||||
=> 6;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The frame rate to use for new animations")]
|
||||
private float _FrameRate = 12;
|
||||
|
||||
/// <summary>The frame rate to use for new animations.</summary>
|
||||
public static ref float FrameRate
|
||||
=> ref Instance._FrameRate;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The Transform Hierarchy path from the Animator to the object being animated" +
|
||||
" using forward slashes '/' between each object name")]
|
||||
private string _HierarchyPath;
|
||||
|
||||
/// <summary>The Transform Hierarchy path from the <see cref="Animator"/> to the object being animated.</summary>
|
||||
public static ref string HierarchyPath
|
||||
=> ref Instance._HierarchyPath;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The type of component being animated. Defaults to " + nameof(SpriteRenderer) + " if not set." +
|
||||
" Use the type picker on the right or drag and drop a component onto it to set this field.")]
|
||||
private SerializableTypeReference _TargetType = new(typeof(SpriteRenderer));
|
||||
|
||||
/// <summary>The type of component being animated. Defaults to <see cref="SpriteRenderer"/> if not set.</summary>
|
||||
public static ref SerializableTypeReference TargetType
|
||||
=> ref Instance._TargetType;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The default value for <see cref="PropertyName"/>.</summary>
|
||||
public const string DefaultPropertyName = "m_Sprite";
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The path of the property being animated. Defaults to " + DefaultPropertyName + " if not set.")]
|
||||
private string _PropertyName = DefaultPropertyName;
|
||||
|
||||
/// <summary>The path of the property being animated.</summary>
|
||||
public static ref string PropertyName
|
||||
=> ref Instance._PropertyName;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Reverts any empty values to their defaults.</summary>
|
||||
public void FillDefaults()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_TargetType.QualifiedName))
|
||||
_TargetType = new(typeof(SpriteRenderer));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_PropertyName))
|
||||
_PropertyName = DefaultPropertyName;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 435f0cce717bfc544bbb5dc56db55763
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,251 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor.Tools
|
||||
{
|
||||
/// <summary>[Editor-Only] [Pro-Only]
|
||||
/// A <see cref="SpriteModifierTool"/> for modifying <see cref="Sprite"/> detauls.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/tools/modify-sprites">
|
||||
/// Modify Sprites</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/ModifySpritesTool
|
||||
///
|
||||
[Serializable]
|
||||
public class ModifySpritesTool : SpriteModifierTool
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField] private OffsetRectMode _RectMode;
|
||||
[SerializeField] private Rect _RectOffset;
|
||||
|
||||
[SerializeField] private bool _SetPivot;
|
||||
[SerializeField] private Vector2 _Pivot;
|
||||
|
||||
[SerializeField] private bool _SetAlignment;
|
||||
[SerializeField] private SpriteAlignment _Alignment;
|
||||
|
||||
[SerializeField] private bool _SetBorder;
|
||||
[SerializeField] private RectOffset _Border;
|
||||
|
||||
[SerializeField] private bool _ShowDetails;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private enum OffsetRectMode { None, Add, Subtract }
|
||||
private static readonly string[] OffsetRectModes = { "None", "Add", "Subtract" };
|
||||
|
||||
private SerializedProperty _SerializedProperty;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int DisplayOrder => 1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Name => "Modify Sprites";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string HelpURL => Strings.DocsURLs.ModifySprites;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Instructions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Sprites.Count == 0)
|
||||
return "Select the Sprites you want to modify.";
|
||||
|
||||
if (!IsValidModification())
|
||||
return "The current Rect Offset would move some Sprites outside the texture bounds.";
|
||||
|
||||
return "Enter the desired modifications and click Apply.";
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnEnable(int index)
|
||||
{
|
||||
base.OnEnable(index);
|
||||
|
||||
_SerializedProperty = AnimancerToolsWindow.Instance.FindSerializedPropertyForTool(this);
|
||||
_SerializedProperty = _SerializedProperty.FindPropertyRelative(nameof(_RectMode));
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoBodyGUI()
|
||||
{
|
||||
base.DoBodyGUI();
|
||||
|
||||
var area = AnimancerGUI.LayoutSingleLineRect();
|
||||
area.xMin += 4;
|
||||
|
||||
using (var label = PooledGUIContent.Acquire("Offset Rects", null))
|
||||
area = EditorGUI.PrefixLabel(area, label);
|
||||
|
||||
AnimancerToolsWindow.BeginChangeCheck();
|
||||
var selected = (OffsetRectMode)GUI.Toolbar(area, (int)_RectMode, OffsetRectModes);
|
||||
AnimancerToolsWindow.EndChangeCheck(ref _RectMode, selected);
|
||||
|
||||
using (var property = _SerializedProperty.Copy())
|
||||
{
|
||||
property.serializedObject.Update();
|
||||
|
||||
var depth = property.depth;
|
||||
while (property.Next(false) && property.depth >= depth)
|
||||
{
|
||||
EditorGUILayout.PropertyField(property, true);
|
||||
}
|
||||
|
||||
property.serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
GUI.enabled = false;
|
||||
for (int i = 0; i < Sprites.Count; i++)
|
||||
{
|
||||
if (_ShowDetails)
|
||||
GUILayout.BeginVertical(GUI.skin.box);
|
||||
|
||||
var sprite = Sprites[i] = AnimancerGUI.DoObjectFieldGUI("", Sprites[i], false);
|
||||
|
||||
if (_ShowDetails)
|
||||
{
|
||||
if (_RectMode != OffsetRectMode.None)
|
||||
EditorGUILayout.RectField("Rect", sprite.rect);
|
||||
|
||||
if (_SetPivot)
|
||||
EditorGUILayout.Vector2Field("Pivot", sprite.pivot);
|
||||
|
||||
if (_SetBorder)
|
||||
EditorGUILayout.Vector4Field("Border", sprite.border);
|
||||
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
GUI.enabled = Sprites.Count > 0 && IsValidModification();
|
||||
|
||||
if (GUILayout.Button("Apply"))
|
||||
{
|
||||
AnimancerGUI.Deselect();
|
||||
AskAndApply();
|
||||
}
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private bool IsValidModification()
|
||||
{
|
||||
switch (_RectMode)
|
||||
{
|
||||
default:
|
||||
case OffsetRectMode.None:
|
||||
return true;
|
||||
|
||||
case OffsetRectMode.Add:
|
||||
case OffsetRectMode.Subtract:
|
||||
break;
|
||||
}
|
||||
|
||||
var offset = GetOffset();
|
||||
|
||||
var sprites = Sprites;
|
||||
for (int i = 0; i < sprites.Count; i++)
|
||||
{
|
||||
var sprite = sprites[i];
|
||||
var rect = Add(sprite.rect, offset);
|
||||
if (rect.xMin < 0 ||
|
||||
rect.yMin < 0 ||
|
||||
rect.xMax >= sprite.texture.width ||
|
||||
rect.xMax >= sprite.texture.height)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private Rect GetOffset()
|
||||
{
|
||||
return _RectMode switch
|
||||
{
|
||||
OffsetRectMode.Add
|
||||
=> _RectOffset,
|
||||
OffsetRectMode.Subtract
|
||||
=> new(-_RectOffset.x, -_RectOffset.y, -_RectOffset.width, -_RectOffset.height),
|
||||
_
|
||||
=> throw new InvalidOperationException($"Can't {nameof(GetOffset)} when the mode is {_RectMode}."),
|
||||
};
|
||||
}
|
||||
|
||||
private static Rect Add(Rect a, Rect b)
|
||||
{
|
||||
a.x += b.x;
|
||||
a.y += b.y;
|
||||
a.width += b.width;
|
||||
a.height += b.height;
|
||||
return a;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override string AreYouSure => "Are you sure you want to modify the borders of these Sprites?";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Modify(SpriteDataEditor data, int index, Sprite sprite)
|
||||
{
|
||||
switch (_RectMode)
|
||||
{
|
||||
default:
|
||||
case OffsetRectMode.None:
|
||||
break;
|
||||
|
||||
case OffsetRectMode.Add:
|
||||
case OffsetRectMode.Subtract:
|
||||
var rect = data.GetRect(index);
|
||||
rect = Add(rect, GetOffset());
|
||||
data.SetRect(index, rect);
|
||||
break;
|
||||
}
|
||||
|
||||
if (_SetPivot)
|
||||
data.SetPivot(index, _Pivot);
|
||||
|
||||
if (_SetAlignment)
|
||||
data.SetAlignment(index, _Alignment);
|
||||
|
||||
if (_SetBorder)
|
||||
data.SetBorder(index, new(_Border.left, _Border.bottom, _Border.right, _Border.top));
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 296afd430bc64e5419309c1f569fe899
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,534 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor.Tools
|
||||
{
|
||||
/// <summary>[Editor-Only] [Pro-Only]
|
||||
/// A <see cref="AnimancerToolsWindow.Tool"/> for packing multiple <see cref="Texture2D"/>s into a single image.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/tools/pack-textures">
|
||||
/// Pack Textures</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/PackTexturesTool
|
||||
///
|
||||
[Serializable]
|
||||
public class PackTexturesTool : AnimancerToolsWindow.Tool
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField] private List<Object> _AssetsToPack;
|
||||
[SerializeField] private int _Padding;
|
||||
[SerializeField] private int _MaximumSize = 8192;
|
||||
|
||||
[NonSerialized] private ReorderableList _TexturesDisplay;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int DisplayOrder => 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Name => "Pack Textures";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string HelpURL => Strings.DocsURLs.PackTextures;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Instructions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_AssetsToPack.Count == 0)
|
||||
return "Add the texture, sprites, and folders you want to pack to the list.";
|
||||
|
||||
return "Set the other details then click Pack and it will ask where you want to save the combined texture.";
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnEnable(int index)
|
||||
{
|
||||
base.OnEnable(index);
|
||||
_AssetsToPack ??= new();
|
||||
_TexturesDisplay = AnimancerToolsWindow.CreateReorderableObjectList(_AssetsToPack, "Textures", true);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoBodyGUI()
|
||||
{
|
||||
GUILayout.BeginVertical();
|
||||
_TexturesDisplay.DoLayoutList();
|
||||
GUILayout.EndVertical();
|
||||
HandleDragAndDropIntoList(GUILayoutUtility.GetLastRect(), _AssetsToPack, overwrite: false);
|
||||
RemoveDuplicates(_AssetsToPack);
|
||||
|
||||
AnimancerToolsWindow.BeginChangeCheck();
|
||||
var padding = EditorGUILayout.IntField("Padding", _Padding);
|
||||
AnimancerToolsWindow.EndChangeCheck(ref _Padding, padding);
|
||||
|
||||
AnimancerToolsWindow.BeginChangeCheck();
|
||||
var maximumSize = EditorGUILayout.IntField("Maximum Size", _MaximumSize);
|
||||
maximumSize = Math.Max(maximumSize, 16);
|
||||
AnimancerToolsWindow.EndChangeCheck(ref _MaximumSize, maximumSize);
|
||||
|
||||
#if !UNITY_IMAGE_CONVERSION
|
||||
EditorGUILayout.HelpBox(
|
||||
"This feature requires Unity's Built-in Image Conversion module." +
|
||||
"\n1. Click here to open the Package Manager." +
|
||||
"\n2. Open the Packages menu and select 'Built-in'." +
|
||||
"\n3. Select the 'Image Conversion' module and Enable it.",
|
||||
MessageType.Error);
|
||||
if (AnimancerGUI.TryUseClickEventInLastRect())
|
||||
EditorApplication.ExecuteMenuItem("Window/Package Manager");
|
||||
#endif
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
GUI.enabled = _AssetsToPack.Count > 0;
|
||||
|
||||
if (GUILayout.Button("Clear"))
|
||||
{
|
||||
AnimancerGUI.Deselect();
|
||||
AnimancerToolsWindow.RecordUndo();
|
||||
_AssetsToPack.Clear();
|
||||
}
|
||||
|
||||
#if !UNITY_IMAGE_CONVERSION
|
||||
var enabled = GUI.enabled;
|
||||
GUI.enabled = false;
|
||||
#endif
|
||||
|
||||
if (GUILayout.Button("Pack"))
|
||||
{
|
||||
#if UNITY_IMAGE_CONVERSION
|
||||
AnimancerGUI.Deselect();
|
||||
Pack();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !UNITY_IMAGE_CONVERSION
|
||||
GUI.enabled = enabled;
|
||||
#endif
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Removes any items from the `list` that are the same as earlier items.</summary>
|
||||
private static void RemoveDuplicates<T>(IList<T> list)
|
||||
{
|
||||
for (int i = list.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var item = list[i];
|
||||
if (item == null)
|
||||
continue;
|
||||
|
||||
for (int j = 0; j < i; j++)
|
||||
{
|
||||
if (item.Equals(list[j]))
|
||||
{
|
||||
list.RemoveAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_IMAGE_CONVERSION
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Combines the <see cref="_AssetsToPack"/> into a new texture and saves it.</summary>
|
||||
private void Pack()
|
||||
{
|
||||
var textures = GatherTextures();
|
||||
if (textures.Count == 0 ||
|
||||
!MakeTexturesReadable(textures))
|
||||
return;
|
||||
|
||||
var path = GetCommonDirectory(_AssetsToPack);
|
||||
|
||||
path = EditorUtility.SaveFilePanelInProject("Save Packed Texture", "PackedTexture", "png",
|
||||
"Where would you like to save the packed texture?", path);
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
const string ProgressTitle = "Packing Textures";
|
||||
EditorUtility.DisplayProgressBar(ProgressTitle, "Gathering", 0);
|
||||
|
||||
var tightSprites = GatherTightSprites();
|
||||
|
||||
EditorUtility.DisplayProgressBar(ProgressTitle, "Packing", 0.1f);
|
||||
|
||||
var packedTexture = new Texture2D(1, 1, TextureFormat.ARGB32, false);
|
||||
|
||||
var tightTextures = new Texture2D[tightSprites.Count];
|
||||
var index = 0;
|
||||
foreach (var sprite in tightSprites)
|
||||
tightTextures[index++] = sprite.texture;
|
||||
|
||||
var packedUVs = packedTexture.PackTextures(tightTextures, _Padding, _MaximumSize);
|
||||
|
||||
EditorUtility.DisplayProgressBar(ProgressTitle, "Encoding", 0.4f);
|
||||
var bytes = packedTexture.EncodeToPNG();
|
||||
if (bytes == null)
|
||||
return;
|
||||
|
||||
EditorUtility.DisplayProgressBar(ProgressTitle, "Writing", 0.5f);
|
||||
File.WriteAllBytes(path, bytes);
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
var importer = GetTextureImporter(path);
|
||||
importer.maxTextureSize = Math.Max(packedTexture.width, packedTexture.height);
|
||||
importer.textureType = TextureImporterType.Sprite;
|
||||
importer.spriteImportMode = SpriteImportMode.Multiple;
|
||||
|
||||
var data = new SpriteDataEditor(importer)
|
||||
{
|
||||
SpriteCount = 0
|
||||
};
|
||||
|
||||
CopyCompressionSettings(importer, textures);
|
||||
EditorUtility.SetDirty(importer);
|
||||
importer.SaveAndReimport();
|
||||
|
||||
// Use the UV coordinates to set up sprites for the new texture.
|
||||
EditorUtility.DisplayProgressBar(ProgressTitle, "Generating Sprites", 0.7f);
|
||||
|
||||
data.SpriteCount = tightSprites.Count;
|
||||
index = 0;
|
||||
foreach (var sprite in tightSprites)
|
||||
{
|
||||
var rect = packedUVs[index];
|
||||
rect.x *= packedTexture.width;
|
||||
rect.y *= packedTexture.height;
|
||||
rect.width *= packedTexture.width;
|
||||
rect.height *= packedTexture.height;
|
||||
|
||||
var spriteRect = rect;
|
||||
spriteRect.x += spriteRect.width * sprite.rect.x / sprite.texture.width;
|
||||
spriteRect.y += spriteRect.height * sprite.rect.y / sprite.texture.height;
|
||||
spriteRect.width *= sprite.rect.width / sprite.texture.width;
|
||||
spriteRect.height *= sprite.rect.height / sprite.texture.height;
|
||||
|
||||
var pivot = sprite.pivot;
|
||||
pivot.x /= rect.width;
|
||||
pivot.y /= rect.height;
|
||||
|
||||
data.SetName(index, sprite.name);
|
||||
data.SetRect(index, spriteRect);
|
||||
data.SetPivot(index, pivot);
|
||||
data.SetBorder(index, sprite.border);
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
EditorUtility.DisplayProgressBar(ProgressTitle, "Saving", 0.9f);
|
||||
|
||||
data.Apply();
|
||||
|
||||
EditorUtility.SetDirty(importer);
|
||||
importer.SaveAndReimport();
|
||||
|
||||
Selection.activeObject = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private HashSet<Texture2D> GatherTextures()
|
||||
{
|
||||
var textures = new HashSet<Texture2D>();
|
||||
|
||||
for (int i = 0; i < _AssetsToPack.Count; i++)
|
||||
{
|
||||
var obj = _AssetsToPack[i];
|
||||
var path = AssetDatabase.GetAssetPath(obj);
|
||||
if (string.IsNullOrEmpty(path))
|
||||
continue;
|
||||
|
||||
if (obj is Texture2D texture)
|
||||
textures.Add(texture);
|
||||
else if (obj is Sprite sprite)
|
||||
textures.Add(sprite.texture);
|
||||
else if (obj is DefaultAsset)
|
||||
ForEachTextureInFolder(path, tex => textures.Add(tex));
|
||||
}
|
||||
|
||||
return textures;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private HashSet<Sprite> GatherTightSprites()
|
||||
{
|
||||
var sprites = new HashSet<Sprite>();
|
||||
|
||||
for (int i = 0; i < _AssetsToPack.Count; i++)
|
||||
{
|
||||
var obj = _AssetsToPack[i];
|
||||
var path = AssetDatabase.GetAssetPath(obj);
|
||||
if (string.IsNullOrEmpty(path))
|
||||
continue;
|
||||
|
||||
if (obj is Texture2D texture)
|
||||
GatherTightSprites(sprites, texture);
|
||||
else if (obj is Sprite sprite)
|
||||
sprites.Add(CreateTightSprite(sprite));
|
||||
else if (obj is DefaultAsset)
|
||||
ForEachTextureInFolder(path, tex => GatherTightSprites(sprites, tex));
|
||||
}
|
||||
|
||||
return sprites;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static void GatherTightSprites(ICollection<Sprite> sprites, Texture2D texture)
|
||||
{
|
||||
var path = AssetDatabase.GetAssetPath(texture);
|
||||
var assets = AssetDatabase.LoadAllAssetsAtPath(path);
|
||||
var foundSprite = false;
|
||||
for (int i = 0; i < assets.Length; i++)
|
||||
{
|
||||
if (assets[i] is Sprite sprite)
|
||||
{
|
||||
sprite = CreateTightSprite(sprite);
|
||||
sprites.Add(sprite);
|
||||
foundSprite = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundSprite)
|
||||
{
|
||||
var sprite = Sprite.Create(texture,
|
||||
new(0, 0, texture.width, texture.height),
|
||||
new(0.5f, 0.5f));
|
||||
sprite.name = texture.name;
|
||||
sprites.Add(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static Sprite CreateTightSprite(Sprite sprite)
|
||||
{
|
||||
var rect = sprite.rect;
|
||||
var width = Mathf.CeilToInt(rect.width);
|
||||
var height = Mathf.CeilToInt(rect.height);
|
||||
if (width == sprite.texture.width &&
|
||||
height == sprite.texture.height)
|
||||
return sprite;
|
||||
|
||||
var pixels = sprite.texture.GetPixels(
|
||||
Mathf.FloorToInt(rect.x),
|
||||
Mathf.FloorToInt(rect.y),
|
||||
width,
|
||||
height);
|
||||
|
||||
var texture = new Texture2D(width, height, sprite.texture.format, false, true);
|
||||
#pragma warning disable UNT0017 // SetPixels invocation is slow.
|
||||
texture.SetPixels(pixels);
|
||||
#pragma warning restore UNT0017 // SetPixels invocation is slow.
|
||||
texture.Apply();
|
||||
|
||||
rect.x = 0;
|
||||
rect.y = 0;
|
||||
|
||||
var pivot = sprite.pivot;
|
||||
pivot.x /= rect.width;
|
||||
pivot.y /= rect.height;
|
||||
|
||||
var newSprite = Sprite.Create(texture, rect, pivot, sprite.pixelsPerUnit);
|
||||
newSprite.name = sprite.name;
|
||||
return newSprite;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static bool MakeTexturesReadable(HashSet<Texture2D> textures)
|
||||
{
|
||||
var hasAsked = false;
|
||||
|
||||
foreach (var texture in textures)
|
||||
{
|
||||
var importer = GetTextureImporter(texture);
|
||||
if (importer == null)
|
||||
continue;
|
||||
|
||||
if (importer.isReadable &&
|
||||
importer.textureCompression == TextureImporterCompression.Uncompressed)
|
||||
continue;
|
||||
|
||||
if (!hasAsked)
|
||||
{
|
||||
if (!EditorUtility.DisplayDialog("Make Textures Readable and Uncompressed?",
|
||||
"This tool requires the source textures to be marked as readable and uncompressed in their import settings.",
|
||||
"Make Textures Readable and Uncompressed", "Cancel"))
|
||||
return false;
|
||||
hasAsked = true;
|
||||
}
|
||||
|
||||
importer.isReadable = true;
|
||||
importer.textureCompression = TextureImporterCompression.Uncompressed;
|
||||
importer.SaveAndReimport();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static void ForEachTextureInFolder(string path, Action<Texture2D> action)
|
||||
{
|
||||
var guids = AssetDatabase.FindAssets($"t:{nameof(Texture2D)}", new string[] { path });
|
||||
for (int i = 0; i < guids.Length; i++)
|
||||
{
|
||||
path = AssetDatabase.GUIDToAssetPath(guids[i]);
|
||||
var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
|
||||
if (texture != null)
|
||||
action(texture);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static void CopyCompressionSettings(TextureImporter copyTo, IEnumerable<Texture2D> copyFrom)
|
||||
{
|
||||
var first = true;
|
||||
foreach (var texture in copyFrom)
|
||||
{
|
||||
var copyFromImporter = GetTextureImporter(texture);
|
||||
if (copyFromImporter == null)
|
||||
continue;
|
||||
|
||||
if (first)
|
||||
{
|
||||
first = false;
|
||||
|
||||
copyTo.textureCompression = copyFromImporter.textureCompression;
|
||||
copyTo.crunchedCompression = copyFromImporter.crunchedCompression;
|
||||
copyTo.compressionQuality = copyFromImporter.compressionQuality;
|
||||
copyTo.filterMode = copyFromImporter.filterMode;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (IsHigherQuality(copyFromImporter.textureCompression, copyTo.textureCompression))
|
||||
copyTo.textureCompression = copyFromImporter.textureCompression;
|
||||
|
||||
if (copyFromImporter.crunchedCompression)
|
||||
copyTo.crunchedCompression = true;
|
||||
|
||||
if (copyTo.compressionQuality < copyFromImporter.compressionQuality)
|
||||
copyTo.compressionQuality = copyFromImporter.compressionQuality;
|
||||
|
||||
if (copyTo.filterMode > copyFromImporter.filterMode)
|
||||
copyTo.filterMode = copyFromImporter.filterMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static bool IsHigherQuality(TextureImporterCompression higher, TextureImporterCompression lower)
|
||||
{
|
||||
return higher switch
|
||||
{
|
||||
TextureImporterCompression.Uncompressed
|
||||
=> lower != TextureImporterCompression.Uncompressed,
|
||||
TextureImporterCompression.Compressed
|
||||
=> lower == TextureImporterCompression.CompressedLQ,
|
||||
TextureImporterCompression.CompressedHQ
|
||||
=> lower == TextureImporterCompression.Compressed
|
||||
|| lower == TextureImporterCompression.CompressedLQ,
|
||||
TextureImporterCompression.CompressedLQ
|
||||
=> false,
|
||||
_
|
||||
=> throw AnimancerUtilities.CreateUnsupportedArgumentException(higher),
|
||||
};
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static string GetCommonDirectory<T>(IList<T> objects) where T : Object
|
||||
{
|
||||
if (objects == null)
|
||||
return null;
|
||||
|
||||
var count = objects.Count;
|
||||
|
||||
for (int i = count - 1; i >= 0; i--)
|
||||
{
|
||||
if (objects[i] == null)
|
||||
{
|
||||
objects.RemoveAt(i);
|
||||
count--;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
return null;
|
||||
|
||||
var path = AssetDatabase.GetAssetPath(objects[0]);
|
||||
path = Path.GetDirectoryName(path);
|
||||
|
||||
for (int i = 1; i < count; i++)
|
||||
{
|
||||
var otherPath = AssetDatabase.GetAssetPath(objects[i]);
|
||||
otherPath = Path.GetDirectoryName(otherPath);
|
||||
|
||||
while (string.Compare(path, 0, otherPath, 0, path.Length) != 0)
|
||||
{
|
||||
path = Path.GetDirectoryName(path);
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static TextureImporter GetTextureImporter(Object asset)
|
||||
{
|
||||
var path = AssetDatabase.GetAssetPath(asset);
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return null;
|
||||
|
||||
return GetTextureImporter(path);
|
||||
}
|
||||
|
||||
private static TextureImporter GetTextureImporter(string path)
|
||||
=> AssetImporter.GetAtPath(path) as TextureImporter;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 70badd2bf7a12d741aea958ec21943a6
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,358 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor.Tools
|
||||
{
|
||||
/// <summary>[Editor-Only] [Pro-Only]
|
||||
/// A <see cref="AnimancerToolsWindow.Tool"/> for changing which bones an <see cref="AnimationClip"/>s controls.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/tools/remap-animation-bindings">
|
||||
/// Remap Animation Bindings</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/RemapAnimationBindingsTool
|
||||
///
|
||||
[Serializable]
|
||||
public class RemapAnimationBindingsTool : AnimationModifierTool
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField] private List<string> _NewBindingPaths;
|
||||
|
||||
[NonSerialized] private List<List<EditorCurveBinding>> _BindingGroups;
|
||||
[NonSerialized] private List<string> _OldBindingPaths;
|
||||
[NonSerialized] private bool _OldBindingPathsAreDirty;
|
||||
[NonSerialized] private ReorderableList _OldBindingPathsDisplay;
|
||||
[NonSerialized] private ReorderableList _NewBindingPathsDisplay;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int DisplayOrder => 5;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Name => "Remap Animation Bindings";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string HelpURL => Strings.DocsURLs.RemapAnimationBindings;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Instructions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Animation == null)
|
||||
return "Select the animation you want to remap.";
|
||||
|
||||
if (_OldBindingPaths.Count == 0)
|
||||
{
|
||||
if (Animation.humanMotion)
|
||||
return "The selected animation only has Humanoid bindings which cannot be remapped.";
|
||||
|
||||
return "The selected animation does not have any bindings.";
|
||||
}
|
||||
|
||||
return "Enter the new paths to change the bindings into then click Save As.";
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnEnable(int index)
|
||||
{
|
||||
base.OnEnable(index);
|
||||
|
||||
_BindingGroups = new();
|
||||
_OldBindingPaths = new();
|
||||
|
||||
_NewBindingPaths ??= new();
|
||||
|
||||
if (Animation == null)
|
||||
_NewBindingPaths.Clear();
|
||||
|
||||
_OldBindingPathsDisplay = AnimancerToolsWindow.CreateReorderableStringList(_OldBindingPaths, "Old Binding Paths");
|
||||
_NewBindingPathsDisplay = AnimancerToolsWindow.CreateReorderableStringList(_NewBindingPaths, "New Binding Paths", (area, i) =>
|
||||
{
|
||||
var color = GUI.color;
|
||||
|
||||
var path = _NewBindingPaths[i];
|
||||
if (path != _OldBindingPaths[i])
|
||||
GUI.color = new(0.15f, 0.7f, 0.15f, 1);
|
||||
|
||||
path = EditorGUI.TextField(area, path);
|
||||
GUI.color = color;
|
||||
return path;
|
||||
});
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAnimationChanged()
|
||||
{
|
||||
base.OnAnimationChanged();
|
||||
_OldBindingPathsAreDirty = true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoBodyGUI()
|
||||
{
|
||||
base.DoBodyGUI();
|
||||
GatherBindings();
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
{
|
||||
GUILayout.BeginVertical();
|
||||
GUI.enabled = false;
|
||||
_OldBindingPathsDisplay.DoLayoutList();
|
||||
GUI.enabled = true;
|
||||
GUILayout.EndVertical();
|
||||
|
||||
GUILayout.BeginVertical();
|
||||
_NewBindingPathsDisplay.DoLayoutList();
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
GUI.enabled = Animation != null;
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
if (GUILayout.Button("Reset"))
|
||||
{
|
||||
AnimancerGUI.Deselect();
|
||||
AnimancerToolsWindow.RecordUndo();
|
||||
_NewBindingPaths.Clear();
|
||||
_OldBindingPathsAreDirty = true;
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Copy All"))
|
||||
{
|
||||
AnimancerGUI.Deselect();
|
||||
CopyAll();
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Paste All"))
|
||||
{
|
||||
AnimancerGUI.Deselect();
|
||||
PasteAll();
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Save As"))
|
||||
{
|
||||
if (SaveAs())
|
||||
{
|
||||
_OldBindingPathsAreDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gathers the bindings from the <see cref="AnimationModifierTool.Animation"/>.</summary>
|
||||
private void GatherBindings()
|
||||
{
|
||||
if (!_OldBindingPathsAreDirty)
|
||||
return;
|
||||
|
||||
_OldBindingPathsAreDirty = false;
|
||||
|
||||
_BindingGroups.Clear();
|
||||
_OldBindingPaths.Clear();
|
||||
|
||||
if (Animation == null)
|
||||
{
|
||||
_NewBindingPaths.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
var isHumanoid = Animation.humanMotion;
|
||||
|
||||
AnimationBindings.OnAnimationChanged(Animation);
|
||||
var bindings = AnimationBindings.GetBindings(Animation);
|
||||
Array.Sort(bindings, (a, b) =>
|
||||
{
|
||||
var result = EditorUtility.NaturalCompare(a.path, b.path);
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
return EditorUtility.NaturalCompare(a.propertyName, b.propertyName);
|
||||
});
|
||||
|
||||
string previousPath = null;
|
||||
List<EditorCurveBinding> previousGroup = null;
|
||||
for (int i = 0; i < bindings.Length; i++)
|
||||
{
|
||||
var binding = bindings[i];
|
||||
if (isHumanoid &&
|
||||
string.IsNullOrEmpty(binding.path) &&
|
||||
IsHumanoidBinding(binding.propertyName))
|
||||
continue;
|
||||
|
||||
var path = binding.path;
|
||||
if (path == previousPath)
|
||||
{
|
||||
previousGroup.Add(binding);
|
||||
continue;
|
||||
}
|
||||
|
||||
previousPath = path;
|
||||
previousGroup = new() { binding };
|
||||
|
||||
_BindingGroups.Add(previousGroup);
|
||||
|
||||
_OldBindingPaths.Add(path);
|
||||
if (_NewBindingPaths.Count < _OldBindingPaths.Count)
|
||||
_NewBindingPaths.Add(path);
|
||||
}
|
||||
|
||||
if (_NewBindingPaths.Count > _OldBindingPaths.Count)
|
||||
_NewBindingPaths.RemoveRange(_OldBindingPaths.Count, _NewBindingPaths.Count - _OldBindingPaths.Count);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static HashSet<string> _HumanoidBindingNames;
|
||||
|
||||
/// <summary>Is the `propertyName` one of the bindings used by Humanoid animations?</summary>
|
||||
private static bool IsHumanoidBinding(string propertyName)
|
||||
{
|
||||
_HumanoidBindingNames ??= new()
|
||||
{
|
||||
"RootT.x", "RootT.y", "RootT.z",
|
||||
"RootQ.x", "RootQ.y", "RootQ.z", "RootQ.w",
|
||||
"LeftFootT.x", "LeftFootT.y", "LeftFootT.z",
|
||||
"LeftFootQ.x", "LeftFootQ.y", "LeftFootQ.z", "LeftFootQ.w",
|
||||
"RightFootT.x", "RightFootT.y", "RightFootT.z",
|
||||
"RightFootQ.x", "RightFootQ.y", "RightFootQ.z", "RightFootQ.w",
|
||||
"LeftHandT.x", "LeftHandT.y", "LeftHandT.z",
|
||||
"LeftHandQ.x", "LeftHandQ.y", "LeftHandQ.z", "LeftHandQ.w",
|
||||
"RightHandT.x", "RightHandT.y", "RightHandT.z",
|
||||
"RightHandQ.x", "RightHandQ.y", "RightHandQ.z", "RightHandQ.w",
|
||||
"Spine Front-Back", "Spine Left-Right", "Spine Twist Left-Right",
|
||||
"Chest Front-Back", "Chest Left-Right", "Chest Twist Left-Right",
|
||||
"UpperChest Front-Back", "UpperChest Left-Right", "UpperChest Twist Left-Right",
|
||||
"Neck Nod Down-Up", "Neck Tilt Left-Right", "Neck Turn Left-Right",
|
||||
"Head Nod Down-Up", "Head Tilt Left-Right", "Head Turn Left-Right",
|
||||
"Left Eye Down-Up", "Left Eye In-Out",
|
||||
"Right Eye Down-Up", "Right Eye In-Out",
|
||||
"Jaw Close", "Jaw Left-Right",
|
||||
"Left Upper Leg Front-Back", "Left Upper Leg In-Out", "Left Upper Leg Twist In-Out",
|
||||
"Left Lower Leg Stretch", "Left Lower Leg Twist In-Out",
|
||||
"Left Foot Up-Down", "Left Foot Twist In-Out",
|
||||
"Left Toes Up-Down",
|
||||
"Right Upper Leg Front-Back", "Right Upper Leg In-Out", "Right Upper Leg Twist In-Out",
|
||||
"Right Lower Leg Stretch", "Right Lower Leg Twist In-Out",
|
||||
"Right Foot Up-Down", "Right Foot Twist In-Out",
|
||||
"Right Toes Up-Down",
|
||||
"Left Shoulder Down-Up", "Left Shoulder Front-Back",
|
||||
"Left Arm Down-Up", "Left Arm Front-Back", "Left Arm Twist In-Out",
|
||||
"Left Forearm Stretch", "Left Forearm Twist In-Out",
|
||||
"Left Hand Down-Up", "Left Hand In-Out",
|
||||
"Right Shoulder Down-Up", "Right Shoulder Front-Back",
|
||||
"Right Arm Down-Up", "Right Arm Front-Back", "Right Arm Twist In-Out",
|
||||
"Right Forearm Stretch", "Right Forearm Twist In-Out",
|
||||
"Right Hand Down-Up", "Right Hand In-Out",
|
||||
"LeftHand.Thumb.Spread", "LeftHand.Thumb.1 Stretched", "LeftHand.Thumb.2 Stretched", "LeftHand.Thumb.3 Stretched",
|
||||
"LeftHand.Index.Spread", "LeftHand.Index.1 Stretched", "LeftHand.Index.2 Stretched", "LeftHand.Index.3 Stretched",
|
||||
"LeftHand.Middle.Spread", "LeftHand.Middle.1 Stretched", "LeftHand.Middle.2 Stretched", "LeftHand.Middle.3 Stretched",
|
||||
"LeftHand.Ring.Spread", "LeftHand.Ring.1 Stretched", "LeftHand.Ring.2 Stretched", "LeftHand.Ring.3 Stretched",
|
||||
"LeftHand.Little.Spread", "LeftHand.Little.1 Stretched", "LeftHand.Little.2 Stretched", "LeftHand.Little.3 Stretched",
|
||||
"RightHand.Thumb.Spread", "RightHand.Thumb.1 Stretched", "RightHand.Thumb.2 Stretched", "RightHand.Thumb.3 Stretched",
|
||||
"RightHand.Index.Spread", "RightHand.Index.1 Stretched", "RightHand.Index.2 Stretched", "RightHand.Index.3 Stretched",
|
||||
"RightHand.Middle.Spread", "RightHand.Middle.1 Stretched", "RightHand.Middle.2 Stretched", "RightHand.Middle.3 Stretched",
|
||||
"RightHand.Ring.Spread", "RightHand.Ring.1 Stretched", "RightHand.Ring.2 Stretched", "RightHand.Ring.3 Stretched",
|
||||
"RightHand.Little.Spread", "RightHand.Little.1 Stretched", "RightHand.Little.2 Stretched", "RightHand.Little.3 Stretched",
|
||||
};
|
||||
|
||||
return _HumanoidBindingNames.Contains(propertyName);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Copies all of the <see cref="_NewBindingPaths"/> to the system clipboard.</summary>
|
||||
private void CopyAll()
|
||||
{
|
||||
var text = StringBuilderPool.Instance.Acquire();
|
||||
|
||||
for (int i = 0; i < _NewBindingPaths.Count; i++)
|
||||
{
|
||||
text.AppendLine(_NewBindingPaths[i]);
|
||||
}
|
||||
|
||||
EditorGUIUtility.systemCopyBuffer = text.ReleaseToString();
|
||||
}
|
||||
|
||||
/// <summary>Pastes the string from the system clipboard into the <see cref="_NewBindingPaths"/>.</summary>
|
||||
private void PasteAll()
|
||||
{
|
||||
using var reader = new StringReader(EditorGUIUtility.systemCopyBuffer);
|
||||
|
||||
for (int i = 0; i < _NewBindingPaths.Count; i++)
|
||||
{
|
||||
var line = reader.ReadLine();
|
||||
if (line == null)
|
||||
return;
|
||||
|
||||
_NewBindingPaths[i] = line;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Modify(AnimationClip animation)
|
||||
{
|
||||
for (int iGroup = 0; iGroup < _BindingGroups.Count; iGroup++)
|
||||
{
|
||||
var oldPath = _OldBindingPaths[iGroup];
|
||||
var newPath = _NewBindingPaths[iGroup];
|
||||
if (oldPath == newPath)
|
||||
continue;
|
||||
|
||||
var group = _BindingGroups[iGroup];
|
||||
for (int iBinding = 0; iBinding < group.Count; iBinding++)
|
||||
{
|
||||
var binding = group[iBinding];
|
||||
if (binding.isPPtrCurve)
|
||||
{
|
||||
var curve = AnimationUtility.GetObjectReferenceCurve(animation, binding);
|
||||
AnimationUtility.SetObjectReferenceCurve(animation, binding, null);
|
||||
|
||||
binding.path = newPath;
|
||||
AnimationUtility.SetObjectReferenceCurve(animation, binding, curve);
|
||||
}
|
||||
else
|
||||
{
|
||||
var curve = AnimationUtility.GetEditorCurve(animation, binding);
|
||||
AnimationUtility.SetEditorCurve(animation, binding, null);
|
||||
|
||||
binding.path = newPath;
|
||||
AnimationUtility.SetEditorCurve(animation, binding, curve);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d75e4cf8a82981c4f94e52c1de8b0a78
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,198 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor.Tools
|
||||
{
|
||||
/// <summary>[Editor-Only] [Pro-Only]
|
||||
/// An <see cref="AnimationModifierTool"/> for changing which
|
||||
/// <see cref="Sprite"/>s an <see cref="AnimationClip"/> uses.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/tools/remap-sprite-animation">
|
||||
/// Remap Sprite Animation</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/RemapSpriteAnimationTool
|
||||
///
|
||||
[Serializable]
|
||||
public class RemapSpriteAnimationTool : AnimationModifierTool
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField] private List<Sprite> _NewSprites;
|
||||
|
||||
[NonSerialized] private List<Sprite> _OldSprites;
|
||||
[NonSerialized] private bool _OldSpritesAreDirty;
|
||||
[NonSerialized] private ReorderableList _OldSpriteDisplay;
|
||||
[NonSerialized] private ReorderableList _NewSpriteDisplay;
|
||||
[NonSerialized] private EditorCurveBinding _SpriteBinding;
|
||||
[NonSerialized] private ObjectReferenceKeyframe[] _SpriteKeyframes;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int DisplayOrder => 4;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Name => "Remap Sprite Animation";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string HelpURL => Strings.DocsURLs.RemapSpriteAnimation;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Instructions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Animation == null)
|
||||
return "Select the animation you want to remap.";
|
||||
|
||||
if (_OldSprites.Count == 0)
|
||||
return "The selected animation does not use Sprites.";
|
||||
|
||||
return "Assign the New Sprites that you want to replace the Old Sprites with then click Save As." +
|
||||
" You can Drag and Drop multiple Sprites onto the New Sprites list at the same time.";
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnEnable(int index)
|
||||
{
|
||||
base.OnEnable(index);
|
||||
|
||||
_NewSprites ??= new();
|
||||
|
||||
if (Animation == null)
|
||||
_NewSprites.Clear();
|
||||
|
||||
_OldSprites = new();
|
||||
|
||||
_OldSpriteDisplay = AnimancerToolsWindow.CreateReorderableObjectList(_OldSprites, "Old Sprites");
|
||||
_NewSpriteDisplay = AnimancerToolsWindow.CreateReorderableObjectList(_NewSprites, "New Sprites");
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnAnimationChanged()
|
||||
{
|
||||
base.OnAnimationChanged();
|
||||
_OldSpritesAreDirty = true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoBodyGUI()
|
||||
{
|
||||
base.DoBodyGUI();
|
||||
GatherOldSprites();
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
{
|
||||
GUILayout.BeginVertical();
|
||||
GUI.enabled = false;
|
||||
_OldSpriteDisplay.DoLayoutList();
|
||||
GUI.enabled = true;
|
||||
GUILayout.EndVertical();
|
||||
|
||||
GUILayout.BeginVertical();
|
||||
_NewSpriteDisplay.DoLayoutList();
|
||||
GUILayout.EndVertical();
|
||||
|
||||
HandleDragAndDropIntoList(GUILayoutUtility.GetLastRect(), _NewSprites, overwrite: true);
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
GUI.enabled = Animation != null;
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
if (GUILayout.Button("Reset"))
|
||||
{
|
||||
AnimancerGUI.Deselect();
|
||||
AnimancerToolsWindow.RecordUndo();
|
||||
_NewSprites.Clear();
|
||||
_OldSpritesAreDirty = true;
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Save As"))
|
||||
{
|
||||
if (SaveAs())
|
||||
{
|
||||
_OldSpritesAreDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gathers the <see cref="_OldSprites"/> from the <see cref="AnimationModifierTool.Animation"/>.</summary>
|
||||
private void GatherOldSprites()
|
||||
{
|
||||
if (!_OldSpritesAreDirty)
|
||||
return;
|
||||
|
||||
_OldSpritesAreDirty = false;
|
||||
|
||||
_OldSprites.Clear();
|
||||
_NewSprites.Clear();
|
||||
|
||||
if (Animation == null)
|
||||
return;
|
||||
|
||||
var bindings = AnimationUtility.GetObjectReferenceCurveBindings(Animation);
|
||||
for (int iBinding = 0; iBinding < bindings.Length; iBinding++)
|
||||
{
|
||||
var binding = bindings[iBinding];
|
||||
if (binding.type == typeof(SpriteRenderer) && binding.propertyName == "m_Sprite")
|
||||
{
|
||||
_SpriteBinding = binding;
|
||||
_SpriteKeyframes = AnimationUtility.GetObjectReferenceCurve(Animation, binding);
|
||||
|
||||
for (int iKeyframe = 0; iKeyframe < _SpriteKeyframes.Length; iKeyframe++)
|
||||
{
|
||||
var reference = _SpriteKeyframes[iKeyframe].value as Sprite;
|
||||
if (reference != null)
|
||||
_OldSprites.Add(reference);
|
||||
}
|
||||
|
||||
_NewSprites.AddRange(_OldSprites);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Modify(AnimationClip animation)
|
||||
{
|
||||
for (int i = 0; i < _SpriteKeyframes.Length; i++)
|
||||
{
|
||||
_SpriteKeyframes[i].value = _NewSprites[i];
|
||||
}
|
||||
|
||||
AnimationUtility.SetObjectReferenceCurve(animation, _SpriteBinding, _SpriteKeyframes);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d3732333127095459feb68e4b95a191
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,359 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor.Tools
|
||||
{
|
||||
/// <summary>[Editor-Only] [Pro-Only] A <see cref="SpriteModifierTool"/> for bulk-renaming <see cref="Sprite"/>s.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/tools/rename-sprites">
|
||||
/// Rename Sprites</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/RenameSpritesTool
|
||||
///
|
||||
[Serializable]
|
||||
public class RenameSpritesTool : SpriteModifierTool
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[NonSerialized] private List<string> _GeneratedNames;
|
||||
[NonSerialized] private bool _NamesAreDirty;
|
||||
[NonSerialized] private ReorderableList _SpritesDisplay;
|
||||
|
||||
[SerializeField] private List<string> _ManualNames;
|
||||
[SerializeField] private int _FirstIndex = 1;
|
||||
[SerializeField] private int _MinimumDigits = 1;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int DisplayOrder => 2;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Name => "Rename Sprites";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string HelpURL => Strings.DocsURLs.RenameSprites;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Instructions
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Sprites.Count == 0)
|
||||
return "Select the Sprites you want to rename.";
|
||||
|
||||
return "Enter the new name(s) you want to give the Sprites then click Apply." +
|
||||
"\n\nEach Sprite below the name you enter will be given the same name" +
|
||||
" until the next name which will restart the counter.";
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnEnable(int index)
|
||||
{
|
||||
base.OnEnable(index);
|
||||
|
||||
_ManualNames ??= new();
|
||||
_GeneratedNames ??= new();
|
||||
|
||||
_SpritesDisplay = AnimancerToolsWindow.CreateReorderableObjectList(Sprites, "Sprites to Rename");
|
||||
_SpritesDisplay.onChangedCallback += list => DirtyNames();
|
||||
_SpritesDisplay.drawElementCallback = DrawItem;
|
||||
_SpritesDisplay.elementHeight = AnimancerGUI.LineHeight * 3 + AnimancerGUI.StandardSpacing * 2;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnSelectionChanged()
|
||||
{
|
||||
base.OnSelectionChanged();
|
||||
DirtyNames();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Refreshes the <see cref="_GeneratedNames"/>.</summary>
|
||||
private void UpdateNames()
|
||||
{
|
||||
if (!_NamesAreDirty)
|
||||
return;
|
||||
|
||||
_NamesAreDirty = false;
|
||||
|
||||
var sprites = Sprites;
|
||||
|
||||
AnimancerEditorUtilities.SetCount(_ManualNames, sprites.Count);
|
||||
AnimancerEditorUtilities.SetCount(_GeneratedNames, sprites.Count);
|
||||
|
||||
string name = null;
|
||||
string digitFormat = null;
|
||||
int index = 0;
|
||||
for (int i = 0; i < sprites.Count; i++)
|
||||
{
|
||||
var newName = _ManualNames[i];
|
||||
if (!string.IsNullOrWhiteSpace(newName))
|
||||
{
|
||||
name = newName;
|
||||
index = 0;
|
||||
|
||||
var nextNameIndex = IndexOfNextManualName(i);
|
||||
|
||||
var digits = Mathf.FloorToInt(Mathf.Log10(nextNameIndex - i)) + 1;
|
||||
if (digits < _MinimumDigits)
|
||||
digits = _MinimumDigits;
|
||||
|
||||
var formatCharacters = new char[digits];
|
||||
for (int iDigit = 0; iDigit < digits; iDigit++)
|
||||
formatCharacters[iDigit] = '0';
|
||||
digitFormat = new string(formatCharacters);
|
||||
}
|
||||
|
||||
_GeneratedNames[i] = string.IsNullOrWhiteSpace(name)
|
||||
? sprites[i].name
|
||||
: name + (index + _FirstIndex).ToString(digitFormat);
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private int IndexOfNextManualName(int startIndex)
|
||||
{
|
||||
for (int i = startIndex + 1; i < _ManualNames.Count; i++)
|
||||
if (!string.IsNullOrWhiteSpace(_ManualNames[i]))
|
||||
return i;
|
||||
|
||||
return _ManualNames.Count;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoBodyGUI()
|
||||
{
|
||||
base.DoBodyGUI();
|
||||
|
||||
#if ! UNITY_2D_SPRITE
|
||||
EditorGUILayout.HelpBox(
|
||||
"Without the 2D Sprite Package," +
|
||||
" any references to the renamed sprites will be lost (including animations).",
|
||||
MessageType.Warning);
|
||||
#endif
|
||||
|
||||
AnimancerToolsWindow.BeginChangeCheck();
|
||||
var firstIndex = EditorGUILayout.IntField("First Index", _FirstIndex);
|
||||
if (AnimancerToolsWindow.EndChangeCheck(ref _FirstIndex, Mathf.Max(firstIndex, 0)))
|
||||
DirtyNames();
|
||||
|
||||
AnimancerToolsWindow.BeginChangeCheck();
|
||||
var digits = EditorGUILayout.IntField("Minimum Digits", _MinimumDigits);
|
||||
if (AnimancerToolsWindow.EndChangeCheck(ref _MinimumDigits, Mathf.Max(digits, 1)))
|
||||
DirtyNames();
|
||||
|
||||
UpdateNames();
|
||||
|
||||
_SpritesDisplay.DoLayoutList();
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
GUI.enabled = HasAnyNames();
|
||||
|
||||
if (GUILayout.Button("Clear"))
|
||||
{
|
||||
AnimancerGUI.Deselect();
|
||||
AnimancerToolsWindow.RecordUndo();
|
||||
_ManualNames.Clear();
|
||||
DirtyNames();
|
||||
}
|
||||
|
||||
GUI.enabled = HasAnyDifferentNames();
|
||||
|
||||
if (GUILayout.Button("Apply"))
|
||||
{
|
||||
AnimancerGUI.Deselect();
|
||||
AskAndApply();
|
||||
}
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DrawItem(Rect area, int index, bool isActive, bool isFocused)
|
||||
{
|
||||
var sprites = Sprites;
|
||||
var sprite = sprites[index];
|
||||
|
||||
var thumbnailWidth = Math.Min(area.height, area.width * 0.5f);
|
||||
var thumbnailArea = AnimancerGUI.StealFromLeft(ref area, thumbnailWidth, AnimancerGUI.StandardSpacing);
|
||||
|
||||
AnimancerGUI.DrawSprite(thumbnailArea, sprite);
|
||||
|
||||
area.y += (AnimancerGUI.LineHeight + AnimancerGUI.StandardSpacing) * 0.5f;
|
||||
area.height = AnimancerGUI.LineHeight;
|
||||
|
||||
sprites[index] = DrawSpriteField(area, sprite);
|
||||
|
||||
AnimancerGUI.NextVerticalArea(ref area);
|
||||
|
||||
DrawName(area, index);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private Sprite DrawSpriteField(Rect area, Sprite sprite)
|
||||
=> AnimancerGUI.DoObjectFieldGUI(area, "", sprite, false);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static GUIStyle _TextFieldStyle;
|
||||
|
||||
private void DrawName(Rect area, int index)
|
||||
{
|
||||
area.y += 1;
|
||||
area.height = AnimancerGUI.LineHeight;
|
||||
|
||||
var manualName = _ManualNames[index];
|
||||
var generatedName = _GeneratedNames[index];
|
||||
|
||||
if (Event.current.type == EventType.Repaint)
|
||||
{
|
||||
_TextFieldStyle ??= new(EditorStyles.textField);
|
||||
|
||||
_TextFieldStyle.fontStyle = string.IsNullOrWhiteSpace(manualName)
|
||||
? FontStyle.Italic
|
||||
: FontStyle.Bold;
|
||||
|
||||
GUI.TextField(area, generatedName, _TextFieldStyle);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
_ManualNames[index] = GUI.TextField(area, manualName);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
DirtyNames();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private bool HasAnyNames()
|
||||
{
|
||||
var sprites = Sprites;
|
||||
|
||||
for (int i = 0; i < sprites.Count; i++)
|
||||
if (!string.IsNullOrWhiteSpace(_ManualNames[i]))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool HasAnyDifferentNames()
|
||||
{
|
||||
var sprites = Sprites;
|
||||
|
||||
for (int i = 0; i < sprites.Count; i++)
|
||||
if (sprites[i].name != _GeneratedNames[i])
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void DirtyNames()
|
||||
=> _NamesAreDirty = true;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override string AreYouSure =>
|
||||
"Are you sure you want to rename these Sprites?"
|
||||
#if UNITY_2D_SPRITE
|
||||
;
|
||||
#else
|
||||
+ "\n\nAny references to the renamed Sprites will be lost (including animations that use them)."
|
||||
+ " This can be avoided by importing Unity's 2D Sprite Package before using this tool.";
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static Dictionary<Sprite, string> _SpriteToName;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void BeforeApply()
|
||||
{
|
||||
if (_SpriteToName == null)
|
||||
_SpriteToName = new();
|
||||
else
|
||||
_SpriteToName.Clear();
|
||||
|
||||
var sprites = Sprites;
|
||||
for (int i = 0; i < sprites.Count; i++)
|
||||
{
|
||||
_SpriteToName.Add(sprites[i], _GeneratedNames[i]);
|
||||
}
|
||||
|
||||
// Renaming selected Sprites will lose the selection without triggering OnSelectionChanged.
|
||||
EditorApplication.delayCall += OnSelectionChanged;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Modify(SpriteDataEditor data, int index, Sprite sprite)
|
||||
{
|
||||
data.SetName(index, _SpriteToName[sprite]);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Modify(TextureImporter importer, List<Sprite> sprites)
|
||||
{
|
||||
if (sprites.Count == 1 && importer.spriteImportMode != SpriteImportMode.Multiple)
|
||||
{
|
||||
var sprite = sprites[0];
|
||||
var fileName = Path.GetFileNameWithoutExtension(importer.assetPath);
|
||||
if (fileName == sprite.name)
|
||||
{
|
||||
AssetDatabase.RenameAsset(importer.assetPath, _SpriteToName[sprite]);
|
||||
sprites.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
base.Modify(importer, sprites);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void AfterApply()
|
||||
{
|
||||
base.AfterApply();
|
||||
|
||||
AnimancerToolsWindow.RecordUndo();
|
||||
_ManualNames.Clear();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc63a439782ec8841905e29c181dbd28
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,310 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
#if UNITY_2D_SPRITE
|
||||
using UnityEditor.U2D.Sprites;
|
||||
#else
|
||||
#pragma warning disable CS0618 // Type or member is obsolete.
|
||||
#endif
|
||||
|
||||
namespace Animancer.Editor.Tools
|
||||
{
|
||||
/// <summary>A wrapper around the '2D Sprite' package features for editing Sprite data.</summary>
|
||||
public class SpriteDataEditor
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_2D_SPRITE
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static SpriteDataProviderFactories _Factories;
|
||||
|
||||
private static SpriteDataProviderFactories Factories
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_Factories == null)
|
||||
{
|
||||
_Factories = new();
|
||||
_Factories.Init();
|
||||
}
|
||||
|
||||
return _Factories;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The data provider for the target.</summary>
|
||||
public readonly ISpriteEditorDataProvider DataProvider;
|
||||
|
||||
private SpriteRect[] _SpriteRects;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The number of sprites in the target data.</summary>
|
||||
/// <remarks>Setting this value clears all sprites.</remarks>
|
||||
public int SpriteCount
|
||||
{
|
||||
get => _SpriteRects.Length;
|
||||
set
|
||||
{
|
||||
// Unity might have given a UnityEditor.U2D.Sprites.SpriteDataExt[].
|
||||
// We need to ensure it's a base SpriteRect[] so we can new() them.
|
||||
|
||||
if (_SpriteRects == null ||
|
||||
_SpriteRects.Length != value ||
|
||||
_SpriteRects.GetType() != typeof(SpriteRect[]))
|
||||
_SpriteRects = new SpriteRect[value];
|
||||
|
||||
for (int i = 0; i < _SpriteRects.Length; i++)
|
||||
_SpriteRects[i] = new();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns the name of the sprite at the specified `index`.</summary>
|
||||
public string GetName(int index) => _SpriteRects[index].name;
|
||||
|
||||
/// <summary>Sets the name of the sprite at the specified `index`.</summary>
|
||||
public void SetName(int index, string name) => _SpriteRects[index].name = name;
|
||||
|
||||
/// <summary>Returns the rect of the sprite at the specified `index`.</summary>
|
||||
public Rect GetRect(int index) => _SpriteRects[index].rect;
|
||||
|
||||
/// <summary>Sets the rect of the sprite at the specified `index`.</summary>
|
||||
public void SetRect(int index, Rect rect) => _SpriteRects[index].rect = rect;
|
||||
|
||||
/// <summary>Returns the pivot of the sprite at the specified `index`.</summary>
|
||||
public Vector2 GetPivot(int index) => _SpriteRects[index].pivot;
|
||||
|
||||
/// <summary>Sets the pivot of the sprite at the specified `index`.</summary>
|
||||
public void SetPivot(int index, Vector2 pivot)
|
||||
{
|
||||
_SpriteRects[index].pivot = pivot;
|
||||
_SpriteRects[index].alignment = GetSpriteAlignment(pivot);
|
||||
}
|
||||
|
||||
/// <summary>Returns the alignment of the sprite at the specified `index`.</summary>
|
||||
public SpriteAlignment GetAlignment(int index) => _SpriteRects[index].alignment;
|
||||
|
||||
/// <summary>Sets the alignment of the sprite at the specified `index`.</summary>
|
||||
public void SetAlignment(int index, SpriteAlignment alignment) => _SpriteRects[index].alignment = alignment;
|
||||
|
||||
/// <summary>Returns the border of the sprite at the specified `index`.</summary>
|
||||
public Vector4 GetBorder(int index) => _SpriteRects[index].border;
|
||||
|
||||
/// <summary>Sets the border of the sprite at the specified `index`.</summary>
|
||||
public void SetBorder(int index, Vector4 border) => _SpriteRects[index].border = border;
|
||||
|
||||
/// <summary>References the sprite at the specified `index`.</summary>
|
||||
public ref SpriteRect RefSprite(int index) => ref _SpriteRects[index];
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#else
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private SpriteMetaData[] _SpriteSheet;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The number of sprites in the target data.</summary>
|
||||
public int SpriteCount
|
||||
{
|
||||
get => _SpriteSheet.Length;
|
||||
set => System.Array.Resize(ref _SpriteSheet, value);
|
||||
}
|
||||
|
||||
/// <summary>Returns the name of the sprite at the specified `index`.</summary>
|
||||
public string GetName(int index) => _SpriteSheet[index].name;
|
||||
|
||||
/// <summary>Sets the name of the sprite at the specified `index`.</summary>
|
||||
public void SetName(int index, string name) => _SpriteSheet[index].name = name;
|
||||
|
||||
/// <summary>Returns the rect of the sprite at the specified `index`.</summary>
|
||||
public Rect GetRect(int index) => _SpriteSheet[index].rect;
|
||||
|
||||
/// <summary>Sets the rect of the sprite at the specified `index`.</summary>
|
||||
public void SetRect(int index, Rect rect) => _SpriteSheet[index].rect = rect;
|
||||
|
||||
/// <summary>Returns the pivot of the sprite at the specified `index`.</summary>
|
||||
public Vector2 GetPivot(int index) => _SpriteSheet[index].pivot;
|
||||
|
||||
/// <summary>Sets the pivot of the sprite at the specified `index`.</summary>
|
||||
public void SetPivot(int index, Vector2 pivot)
|
||||
{
|
||||
_SpriteSheet[index].pivot = pivot;
|
||||
_SpriteSheet[index].alignment = (int)GetSpriteAlignment(pivot);
|
||||
}
|
||||
|
||||
/// <summary>Returns the alignment of the sprite at the specified `index`.</summary>
|
||||
public SpriteAlignment GetAlignment(int index) => (SpriteAlignment)_SpriteSheet[index].alignment;
|
||||
|
||||
/// <summary>Sets the alignment of the sprite at the specified `index`.</summary>
|
||||
public void SetAlignment(int index, SpriteAlignment alignment) => _SpriteSheet[index].alignment = (int)alignment;
|
||||
|
||||
/// <summary>Returns the border of the sprite at the specified `index`.</summary>
|
||||
public Vector4 GetBorder(int index) => _SpriteSheet[index].border;
|
||||
|
||||
/// <summary>Sets the border of the sprite at the specified `index`.</summary>
|
||||
public void SetBorder(int index, Vector4 border) => _SpriteSheet[index].border = border;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the appropriate alignment for the given `pivot`.</summary>
|
||||
public static SpriteAlignment GetSpriteAlignment(Vector2 pivot)
|
||||
{
|
||||
switch (pivot.x)
|
||||
{
|
||||
case 0:
|
||||
switch (pivot.y)
|
||||
{
|
||||
case 0: return SpriteAlignment.BottomLeft;
|
||||
case 0.5f: return SpriteAlignment.LeftCenter;
|
||||
case 1: return SpriteAlignment.TopLeft;
|
||||
}
|
||||
break;
|
||||
case 0.5f:
|
||||
switch (pivot.y)
|
||||
{
|
||||
case 0: return SpriteAlignment.BottomCenter;
|
||||
case 0.5f: return SpriteAlignment.Center;
|
||||
case 1: return SpriteAlignment.TopCenter;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
switch (pivot.y)
|
||||
{
|
||||
case 0: return SpriteAlignment.BottomRight;
|
||||
case 0.5f: return SpriteAlignment.RightCenter;
|
||||
case 1: return SpriteAlignment.TopRight;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return SpriteAlignment.Custom;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private readonly TextureImporter Importer;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="SpriteDataEditor"/>.</summary>
|
||||
public SpriteDataEditor(TextureImporter importer)
|
||||
{
|
||||
Importer = importer;
|
||||
|
||||
#if UNITY_2D_SPRITE
|
||||
DataProvider = Factories.GetSpriteEditorDataProviderFromObject(importer);
|
||||
DataProvider.InitSpriteEditorDataProvider();
|
||||
|
||||
_SpriteRects = DataProvider.GetSpriteRects();
|
||||
#else
|
||||
_SpriteSheet = importer.spritesheet;
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Tries to find the index of the data matching the `sprite`.</summary>
|
||||
/// <remarks>
|
||||
/// Returns -1 if there is no data matching the <see cref="UnityEngine.Object.name"/>.
|
||||
/// <para></para>
|
||||
/// Returns -2 if there is more than one data matching the <see cref="UnityEngine.Object.name"/> but no
|
||||
/// <see cref="Sprite.rect"/> match.
|
||||
/// </remarks>
|
||||
public int IndexOf(Sprite sprite)
|
||||
{
|
||||
var nameMatchIndex = -1;
|
||||
|
||||
var count = SpriteCount;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (GetName(i) == sprite.name)
|
||||
{
|
||||
if (GetRect(i) == sprite.rect)
|
||||
return i;
|
||||
|
||||
if (nameMatchIndex == -1)// First name match.
|
||||
nameMatchIndex = i;
|
||||
else
|
||||
nameMatchIndex = -2;// Already found 2 name matches.
|
||||
}
|
||||
}
|
||||
|
||||
if (nameMatchIndex == -1)
|
||||
{
|
||||
Debug.LogError($"No {nameof(SpriteMetaData)} for '{sprite.name}' was found.", sprite);
|
||||
}
|
||||
else if (nameMatchIndex == -2)
|
||||
{
|
||||
Debug.LogError($"More than one {nameof(SpriteMetaData)} for '{sprite.name}' was found" +
|
||||
$" but none of them matched the {nameof(Sprite)}.{nameof(Sprite.rect)}." +
|
||||
$" If the texture's Max Size is smaller than its actual size, increase the Max Size before performing this" +
|
||||
$" operation so that the {nameof(Rect)}s can be used to identify the correct data.", sprite);
|
||||
}
|
||||
|
||||
return nameMatchIndex;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Logs an error and returns false if the data at the specified `index` is out of the texture bounds.</summary>
|
||||
public bool ValidateBounds(int index, Sprite sprite)
|
||||
{
|
||||
var rect = GetRect(index);
|
||||
if (rect.xMin >= 0 &&
|
||||
rect.yMin >= 0 &&
|
||||
rect.xMax <= sprite.texture.width &&
|
||||
rect.yMax <= sprite.texture.height)
|
||||
return true;
|
||||
|
||||
var path = AssetDatabase.GetAssetPath(sprite);
|
||||
|
||||
// The Max Texture Size import setting may cause the loaded texture to be smaller than the actual image.
|
||||
// Sprite dimensions are defined against the actual image though, so we need to check those bounds.
|
||||
var importer = (TextureImporter)AssetImporter.GetAtPath(path);
|
||||
importer.GetSourceTextureWidthAndHeight(out var width, out var height);
|
||||
if (rect.xMin >= 0 &&
|
||||
rect.yMin >= 0 &&
|
||||
rect.xMax <= width &&
|
||||
rect.yMax <= height)
|
||||
return true;
|
||||
|
||||
Debug.LogError(
|
||||
$"This modification would put '{sprite.name}' at {rect}" +
|
||||
$" which is outside of the texture ({width}x{height})" +
|
||||
$" so '{path}' was not modified.",
|
||||
sprite);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Applies any modifications to the target asset.</summary>
|
||||
public void Apply()
|
||||
{
|
||||
#if UNITY_2D_SPRITE
|
||||
DataProvider.SetSpriteRects(_SpriteRects);
|
||||
DataProvider.Apply();
|
||||
#else
|
||||
Importer.spritesheet = _SpriteSheet;
|
||||
EditorUtility.SetDirty(Importer);
|
||||
#endif
|
||||
|
||||
Importer.SaveAndReimport();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8abf35ea1a5a4664b88e197e78f09632
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,223 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor.Tools
|
||||
{
|
||||
/// <summary>[Editor-Only] [Pro-Only]
|
||||
/// A base <see cref="AnimancerToolsWindow.Tool"/> for modifying <see cref="Sprite"/>s.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/tools">
|
||||
/// Animancer Tools</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/SpriteModifierTool
|
||||
///
|
||||
[Serializable]
|
||||
public abstract class SpriteModifierTool : AnimancerToolsWindow.Tool
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly List<Sprite> SelectedSprites = new();
|
||||
private static bool _HasGatheredSprites;
|
||||
|
||||
/// <summary>The currently selected <see cref="Sprite"/>s.</summary>
|
||||
public static List<Sprite> Sprites
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_HasGatheredSprites)
|
||||
{
|
||||
_HasGatheredSprites = true;
|
||||
GatherSelectedSprites(SelectedSprites);
|
||||
}
|
||||
|
||||
return SelectedSprites;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnSelectionChanged()
|
||||
{
|
||||
_HasGatheredSprites = false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoBodyGUI()
|
||||
{
|
||||
#if !UNITY_2D_SPRITE
|
||||
EditorGUILayout.HelpBox(
|
||||
"This tool works best with Unity's '2D Sprite' package." +
|
||||
" You should import it via the Package Manager before using this tool.",
|
||||
MessageType.Warning);
|
||||
|
||||
if (AnimancerGUI.TryUseClickEventInLastRect())
|
||||
EditorApplication.ExecuteMenuItem("Window/Package Manager");
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Adds all <see cref="Sprite"/>s in the <see cref="Selection.objects"/> or their sub-assets to the
|
||||
/// list of `sprites`.
|
||||
/// </summary>
|
||||
public static void GatherSelectedSprites(List<Sprite> sprites)
|
||||
{
|
||||
sprites.Clear();
|
||||
|
||||
var selection = Selection.objects;
|
||||
for (int i = 0; i < selection.Length; i++)
|
||||
{
|
||||
var selected = selection[i];
|
||||
if (selected is Sprite sprite)
|
||||
{
|
||||
sprites.Add(sprite);
|
||||
}
|
||||
else if (selected is Texture2D texture)
|
||||
{
|
||||
sprites.AddRange(LoadAllSpritesInTexture(texture));
|
||||
}
|
||||
}
|
||||
|
||||
sprites.Sort(NaturalCompare);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns all the <see cref="Sprite"/> sub-assets of the `texture`.</summary>
|
||||
public static Sprite[] LoadAllSpritesInTexture(Texture2D texture)
|
||||
=> LoadAllSpritesAtPath(AssetDatabase.GetAssetPath(texture));
|
||||
|
||||
/// <summary>Returns all the <see cref="Sprite"/> assets at the `path`.</summary>
|
||||
public static Sprite[] LoadAllSpritesAtPath(string path)
|
||||
{
|
||||
var assets = AssetDatabase.LoadAllAssetsAtPath(path);
|
||||
var sprites = new List<Sprite>();
|
||||
for (int j = 0; j < assets.Length; j++)
|
||||
{
|
||||
if (assets[j] is Sprite sprite)
|
||||
sprites.Add(sprite);
|
||||
}
|
||||
return sprites.ToArray();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="EditorUtility.NaturalCompare"/> on the <see cref="Object.name"/>s.</summary>
|
||||
public static int NaturalCompare(Object a, Object b)
|
||||
=> EditorUtility.NaturalCompare(a.name, b.name);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The message to confirm that the user is certain they want to apply the changes.</summary>
|
||||
protected virtual string AreYouSure
|
||||
=> "Are you sure you want to modify these Sprites?";
|
||||
|
||||
/// <summary>Called immediately after the user confirms they want to apply changes.</summary>
|
||||
protected virtual void BeforeApply() { }
|
||||
|
||||
/// <summary>Called after all changes are applied.</summary>
|
||||
protected virtual void AfterApply() { }
|
||||
|
||||
/// <summary>Applies the desired modifications to the `data` before it is saved.</summary>
|
||||
protected virtual void Modify(SpriteDataEditor data, int index, Sprite sprite) { }
|
||||
|
||||
/// <summary>Applies the desired modifications to the `data` before it is saved.</summary>
|
||||
protected virtual void Modify(TextureImporter importer, List<Sprite> sprites)
|
||||
{
|
||||
var dataEditor = new SpriteDataEditor(importer);
|
||||
|
||||
var hasError = false;
|
||||
|
||||
for (int i = 0; i < sprites.Count; i++)
|
||||
{
|
||||
var sprite = sprites[i];
|
||||
var index = dataEditor.IndexOf(sprite);
|
||||
if (index < 0)
|
||||
continue;
|
||||
|
||||
Modify(dataEditor, index, sprite);
|
||||
sprites.RemoveAt(i--);
|
||||
|
||||
if (!dataEditor.ValidateBounds(index, sprite))
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
if (!hasError)
|
||||
dataEditor.Apply();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Asks the user if they want to modify the target <see cref="Sprite"/>s and calls <see cref="Modify"/>
|
||||
/// on each of them before saving any changes.
|
||||
/// </summary>
|
||||
protected void AskAndApply()
|
||||
{
|
||||
if (!EditorUtility.DisplayDialog("Are You Sure?",
|
||||
AreYouSure + "\n\nThis operation cannot be undone.",
|
||||
"Modify", "Cancel"))
|
||||
return;
|
||||
|
||||
BeforeApply();
|
||||
|
||||
var pathToSprites = new Dictionary<string, List<Sprite>>();
|
||||
var sprites = Sprites;
|
||||
for (int i = 0; i < sprites.Count; i++)
|
||||
{
|
||||
var sprite = sprites[i];
|
||||
|
||||
var path = AssetDatabase.GetAssetPath(sprite);
|
||||
|
||||
if (!pathToSprites.TryGetValue(path, out var spritesAtPath))
|
||||
pathToSprites.Add(path, spritesAtPath = new());
|
||||
|
||||
spritesAtPath.Add(sprite);
|
||||
}
|
||||
|
||||
foreach (var asset in pathToSprites)
|
||||
{
|
||||
var importer = (TextureImporter)AssetImporter.GetAtPath(asset.Key);
|
||||
|
||||
Modify(importer, asset.Value);
|
||||
|
||||
if (asset.Value.Count > 0)
|
||||
{
|
||||
var message = StringBuilderPool.Instance.Acquire()
|
||||
.Append("Modification failed: unable to find data in '")
|
||||
.Append(asset.Key)
|
||||
.Append("' for ")
|
||||
.Append(asset.Value.Count)
|
||||
.Append(" Sprites:");
|
||||
|
||||
for (int i = 0; i < sprites.Count; i++)
|
||||
{
|
||||
message.AppendLine()
|
||||
.Append(" - ")
|
||||
.Append(sprites[i].name);
|
||||
}
|
||||
|
||||
Debug.LogError(message.ReleaseToString(), AssetDatabase.LoadAssetAtPath<Object>(asset.Key));
|
||||
}
|
||||
}
|
||||
|
||||
AfterApply();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f495ecfca64b9c04aa19924036014776
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
252
Packages/com.kybernetik.animancer/Editor/Animancer Tools/Tool.cs
Normal file
252
Packages/com.kybernetik.animancer/Editor/Animancer Tools/Tool.cs
Normal file
@@ -0,0 +1,252 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditor.AnimatedValues;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor.Tools
|
||||
{
|
||||
partial class AnimancerToolsWindow
|
||||
{
|
||||
/// <summary>[Editor-Only] [Pro-Only] Base class for tools in the <see cref="AnimancerToolsWindow"/>.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/tools">
|
||||
/// Animancer Tools</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Tools/Tool
|
||||
///
|
||||
[Serializable]
|
||||
public abstract class Tool : IComparable<Tool>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private AnimBool _FullAnimator;
|
||||
private AnimBool _BodyAnimator;
|
||||
|
||||
private int _Index;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Is this tool currently visible?</summary>
|
||||
public bool IsVisible => Instance._CurrentTool == _Index || Instance._CurrentTool < 0;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Is the body of this tool currently visible?</summary>
|
||||
public bool IsExpanded
|
||||
{
|
||||
get { return Instance._CurrentTool == _Index; }
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
Instance._CurrentTool = _Index;
|
||||
else if (IsExpanded)
|
||||
Instance._CurrentTool = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Lower numbers display first.</summary>
|
||||
public abstract int DisplayOrder { get; }
|
||||
|
||||
/// <summary>Compares the <see cref="DisplayOrder"/> to put lower numbers first.</summary>
|
||||
public int CompareTo(Tool other)
|
||||
=> DisplayOrder.CompareTo(other.DisplayOrder);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The display name of this tool.</summary>
|
||||
public abstract string Name { get; }
|
||||
|
||||
/// <summary>The usage instructions to display at the top of this tool.</summary>
|
||||
public abstract string Instructions { get; }
|
||||
|
||||
/// <summary>The URL for the help button in the header to open.</summary>
|
||||
public virtual string HelpURL => Strings.DocsURLs.AnimancerTools;
|
||||
|
||||
/// <summary>Called whenever the <see cref="Selection"/> changes.</summary>
|
||||
public virtual void OnSelectionChanged() { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Called by <see cref="AnimancerToolsWindow.OnEnable"/>.</summary>
|
||||
public virtual void OnEnable(int index)
|
||||
{
|
||||
_Index = index;
|
||||
|
||||
_FullAnimator = new(IsVisible);
|
||||
_BodyAnimator = new(IsExpanded);
|
||||
}
|
||||
|
||||
/// <summary>Called by <see cref="AnimancerToolsWindow.OnDisable"/>.</summary>
|
||||
public virtual void OnDisable() { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the GUI for this tool.</summary>
|
||||
public virtual void DoGUI()
|
||||
{
|
||||
var enabled = GUI.enabled;
|
||||
|
||||
_FullAnimator.target = IsVisible;
|
||||
|
||||
if (EditorGUILayout.BeginFadeGroup(_FullAnimator.faded))
|
||||
{
|
||||
GUILayout.BeginVertical(EditorStyles.helpBox);
|
||||
|
||||
DoHeaderGUI();
|
||||
|
||||
_BodyAnimator.target = IsExpanded;
|
||||
|
||||
if (EditorGUILayout.BeginFadeGroup(_BodyAnimator.faded))
|
||||
{
|
||||
var instructions = Instructions;
|
||||
if (!string.IsNullOrEmpty(instructions))
|
||||
EditorGUILayout.HelpBox(instructions, MessageType.Info);
|
||||
|
||||
DoBodyGUI();
|
||||
}
|
||||
EditorGUILayout.EndFadeGroup();
|
||||
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
EditorGUILayout.EndFadeGroup();
|
||||
|
||||
if (_FullAnimator.isAnimating || _BodyAnimator.isAnimating)
|
||||
Repaint();
|
||||
|
||||
GUI.enabled = enabled;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Draws the Header GUI for this tool which is displayed regardless of whether it is expanded or not.
|
||||
/// </summary>
|
||||
public virtual void DoHeaderGUI()
|
||||
{
|
||||
var area = AnimancerGUI.LayoutSingleLineRect(AnimancerGUI.SpacingMode.BeforeAndAfter);
|
||||
var click = GUI.Button(area, Name, EditorStyles.boldLabel);
|
||||
|
||||
area.xMin = area.xMax - area.height;
|
||||
GUI.DrawTexture(area, HelpIcon);
|
||||
|
||||
if (click)
|
||||
{
|
||||
if (area.Contains(Event.current.mousePosition))
|
||||
{
|
||||
Application.OpenURL(HelpURL);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsExpanded = !IsExpanded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the Body GUI for this tool which is only displayed while it is expanded.</summary>
|
||||
public abstract void DoBodyGUI();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Asks the user where they want to save a modified asset, calls `modify` on it, and saves it.</summary>
|
||||
public static bool SaveModifiedAsset<T>(string saveTitle, string saveMessage,
|
||||
T obj, Action<T> modify) where T : Object
|
||||
{
|
||||
var originalPath = AssetDatabase.GetAssetPath(obj);
|
||||
|
||||
var extension = Path.GetExtension(originalPath);
|
||||
if (extension[0] == '.')
|
||||
extension = extension[1..];
|
||||
|
||||
var directory = Path.GetDirectoryName(originalPath);
|
||||
|
||||
var newName = Path.GetFileNameWithoutExtension(AssetDatabase.GenerateUniqueAssetPath(originalPath));
|
||||
var savePath = EditorUtility.SaveFilePanelInProject(saveTitle, newName, extension, saveMessage, directory);
|
||||
if (string.IsNullOrEmpty(savePath))
|
||||
return false;
|
||||
|
||||
if (originalPath != savePath)
|
||||
{
|
||||
obj = Instantiate(obj);
|
||||
AssetDatabase.CreateAsset(obj, savePath);
|
||||
}
|
||||
|
||||
modify(obj);
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static Texture _HelpIcon;
|
||||
|
||||
/// <summary>The help icon image used in the tool header.</summary>
|
||||
public static Texture HelpIcon
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_HelpIcon == null)
|
||||
_HelpIcon = AnimancerIcons.Load("_Help");
|
||||
return _HelpIcon;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds any objects dropped in the `area` to the `list`.</summary>
|
||||
protected void HandleDragAndDropIntoList<T>(
|
||||
Rect area,
|
||||
IList<T> list,
|
||||
bool overwrite)
|
||||
where T : Object
|
||||
{
|
||||
var dropIndex = 0;
|
||||
|
||||
// No easy way to avoid this closure.
|
||||
AnimancerGUI.Handle<T>((obj, isDrop) =>
|
||||
{
|
||||
if (!isDrop)
|
||||
return true;
|
||||
|
||||
if (overwrite)
|
||||
{
|
||||
RecordUndo();
|
||||
if (dropIndex < list.Count)
|
||||
{
|
||||
list[dropIndex++] = obj;
|
||||
}
|
||||
else
|
||||
{
|
||||
list.Add(obj);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
list.Add(obj);
|
||||
}
|
||||
|
||||
return true;
|
||||
}, area);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0981a35e6e25d944dacf3f3665c5ab78
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user