chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

View File

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

View File

@@ -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:

View File

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

View File

@@ -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:

View File

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

View File

@@ -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:

View File

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

View File

@@ -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:

View File

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

View File

@@ -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:

View File

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

View File

@@ -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:

View File

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

View File

@@ -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:

View File

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

View File

@@ -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:

View File

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

View File

@@ -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:

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8abf35ea1a5a4664b88e197e78f09632
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -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:

View 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

View File

@@ -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: