372 lines
15 KiB
C#
372 lines
15 KiB
C#
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace Animancer.Editor
|
|
{
|
|
/// <summary>[Editor-Only] Various utilities used throughout Animancer.</summary>
|
|
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerEditorUtilities
|
|
public static partial class AnimancerEditorUtilities
|
|
{
|
|
/************************************************************************************************************************/
|
|
#region Misc
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Animancer Extension] [Editor-Only] Is the <see cref="Vector2.x"/> or <see cref="Vector2.y"/> NaN?</summary>
|
|
public static bool IsNaN(this Vector2 vector)
|
|
=> float.IsNaN(vector.x)
|
|
|| float.IsNaN(vector.y);
|
|
|
|
/// <summary>[Animancer Extension] [Editor-Only] Is the <see cref="Vector3.x"/>, <see cref="Vector3.y"/>, or <see cref="Vector3.z"/> NaN?</summary>
|
|
public static bool IsNaN(this Vector3 vector)
|
|
=> float.IsNaN(vector.x)
|
|
|| float.IsNaN(vector.y)
|
|
|| float.IsNaN(vector.z);
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Returns the value of `t` linearly interpolated along the X axis of the `rect`.</summary>
|
|
public static float LerpUnclampedX(this Rect rect, float t)
|
|
=> rect.x + rect.width * t;
|
|
|
|
/// <summary>Returns the value of `t` inverse linearly interpolated along the X axis of the `rect`.</summary>
|
|
public static float InverseLerpUnclampedX(this Rect rect, float t)
|
|
=> (t - rect.x) / rect.width;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Finds an asset of the specified type anywhere in the project.</summary>
|
|
public static T FindAssetOfType<T>()
|
|
where T : Object
|
|
{
|
|
var filter = typeof(Component).IsAssignableFrom(typeof(T))
|
|
? $"t:{nameof(GameObject)}"
|
|
: $"t:{typeof(T).Name}";
|
|
|
|
var guids = AssetDatabase.FindAssets(filter);
|
|
if (guids.Length == 0)
|
|
return null;
|
|
|
|
for (int i = 0; i < guids.Length; i++)
|
|
{
|
|
var path = AssetDatabase.GUIDToAssetPath(guids[i]);
|
|
var asset = AssetDatabase.LoadAssetAtPath<T>(path);
|
|
if (asset != null)
|
|
return asset;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Finds or creates an instance of <typeparamref name="T"/>.</summary>
|
|
public static T FindOrCreate<T>(ref T scriptableObject, HideFlags hideFlags = default)
|
|
where T : ScriptableObject
|
|
{
|
|
if (scriptableObject != null)
|
|
return scriptableObject;
|
|
|
|
var instances = Resources.FindObjectsOfTypeAll<T>();
|
|
if (instances.Length > 0)
|
|
{
|
|
scriptableObject = instances[0];
|
|
}
|
|
else
|
|
{
|
|
scriptableObject = ScriptableObject.CreateInstance<T>();
|
|
scriptableObject.hideFlags = hideFlags;
|
|
}
|
|
|
|
return scriptableObject;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>The most recent <see cref="PlayModeStateChange"/>.</summary>
|
|
public static PlayModeStateChange PlayModeState { get; private set; }
|
|
|
|
/// <summary>Is the Unity Editor is currently changing between Play Mode and Edit Mode?</summary>
|
|
public static bool IsChangingPlayMode =>
|
|
PlayModeState == PlayModeStateChange.ExitingEditMode ||
|
|
PlayModeState == PlayModeStateChange.ExitingPlayMode;
|
|
|
|
[InitializeOnLoadMethod]
|
|
private static void WatchForPlayModeChanges()
|
|
{
|
|
PlayModeState = EditorApplication.isPlayingOrWillChangePlaymode
|
|
? EditorApplication.isPlaying
|
|
? PlayModeStateChange.EnteredPlayMode
|
|
: PlayModeStateChange.ExitingEditMode
|
|
: PlayModeStateChange.EnteredEditMode;
|
|
|
|
EditorApplication.playModeStateChanged += change => PlayModeState = change;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Deletes the specified `subAsset`.</summary>
|
|
public static void DeleteSubAsset(Object subAsset)
|
|
{
|
|
AssetDatabase.RemoveObjectFromAsset(subAsset);
|
|
AssetDatabase.SaveAssets();
|
|
|
|
Object.DestroyImmediate(subAsset, true);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Calculates the overall bounds of all renderers under the `transform`.</summary>
|
|
public static Bounds CalculateBounds(Transform transform)
|
|
{
|
|
using var _ = ListPool<Renderer>.Instance.Acquire(out var renderers);
|
|
|
|
transform.GetComponentsInChildren(renderers);
|
|
if (renderers.Count == 0)
|
|
return default;
|
|
|
|
var bounds = renderers[0].bounds;
|
|
for (int i = 1; i < renderers.Count; i++)
|
|
bounds.Encapsulate(renderers[i].bounds);
|
|
|
|
return bounds;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#region Collections
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Adds default items or removes items to make the <see cref="List{T}.Count"/> equal to the `count`.</summary>
|
|
public static void SetCount<T>(List<T> list, int count)
|
|
{
|
|
if (list.Count < count)
|
|
{
|
|
while (list.Count < count)
|
|
list.Add(default);
|
|
}
|
|
else
|
|
{
|
|
list.RemoveRange(count, list.Count - count);
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Removes any items from the `list` that are <c>null</c> and items that appear multiple times.
|
|
/// Returns true if the `list` was modified.
|
|
/// </summary>
|
|
public static bool RemoveMissingAndDuplicates(ref List<GameObject> list)
|
|
{
|
|
if (list == null)
|
|
{
|
|
list = new();
|
|
return false;
|
|
}
|
|
|
|
var modified = false;
|
|
|
|
using (SetPool<Object>.Instance.Acquire(out var previousItems))
|
|
{
|
|
for (int i = list.Count - 1; i >= 0; i--)
|
|
{
|
|
var item = list[i];
|
|
if (item == null || previousItems.Contains(item))
|
|
{
|
|
list.RemoveAt(i);
|
|
modified = true;
|
|
}
|
|
else
|
|
{
|
|
previousItems.Add(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
return modified;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Removes any <c>null</c> items and ensures that it contains
|
|
/// an instance of each type derived from <typeparamref name="T"/>.
|
|
/// </summary>
|
|
public static void InstantiateDerivedTypes<T>(ref List<T> list)
|
|
where T : IComparable<T>
|
|
{
|
|
if (list == null)
|
|
{
|
|
list = new();
|
|
}
|
|
else
|
|
{
|
|
for (int i = list.Count - 1; i >= 0; i--)
|
|
if (list[i] == null)
|
|
list.RemoveAt(i);
|
|
}
|
|
|
|
var types = TypeSelectionMenu.GetDerivedTypes(typeof(T));
|
|
for (int i = 0; i < types.Count; i++)
|
|
{
|
|
var toolType = types[i];
|
|
if (IndexOfType(list, toolType) >= 0)
|
|
continue;
|
|
|
|
var instance = (T)Activator.CreateInstance(toolType);
|
|
list.Add(instance);
|
|
}
|
|
|
|
list.Sort();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Finds the index of the first item with the specified `type`.</summary>
|
|
public static int IndexOfType<T>(IList<T> list, Type type)
|
|
{
|
|
for (int i = 0; i < list.Count; i++)
|
|
if (list[i].GetType() == type)
|
|
return i;
|
|
|
|
return -1;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#region Context Menus
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Adds a menu function which passes the result of <see cref="CalculateEditorFadeDuration"/> into `startFade`.
|
|
/// </summary>
|
|
public static void AddFadeFunction(
|
|
GenericMenu menu,
|
|
string label,
|
|
bool isEnabled,
|
|
AnimancerNode node,
|
|
Action<float> startFade)
|
|
{
|
|
// Fade functions need to be delayed twice since the context menu itself causes the next frame delta
|
|
// time to be unreasonably high (which would skip the start of the fade).
|
|
menu.AddFunction(label, isEnabled,
|
|
() => EditorApplication.delayCall +=
|
|
() => EditorApplication.delayCall +=
|
|
() =>
|
|
{
|
|
startFade(node.CalculateEditorFadeDuration());
|
|
});
|
|
}
|
|
|
|
/// <summary>[Animancer Extension] [Editor-Only]
|
|
/// Returns the duration of the `node`s current fade (if any), otherwise returns the `defaultDuration`.
|
|
/// </summary>
|
|
public static float CalculateEditorFadeDuration(this AnimancerNode node, float defaultDuration = 1)
|
|
=> node.FadeSpeed > 0
|
|
? 1 / node.FadeSpeed
|
|
: defaultDuration;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Adds a menu function to open a web page. If the `linkSuffix` starts with a '/' then it will be relative to
|
|
/// the <see cref="Strings.DocsURLs.Documentation"/>.
|
|
/// </summary>
|
|
public static void AddDocumentationLink(GenericMenu menu, string label, string linkSuffix)
|
|
{
|
|
if (linkSuffix[0] == '/')
|
|
linkSuffix = Strings.DocsURLs.Documentation + linkSuffix;
|
|
|
|
menu.AddItem(new(label), false, () =>
|
|
{
|
|
EditorUtility.OpenWithDefaultApp(linkSuffix);
|
|
});
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Is the <see cref="MenuCommand.context"/> editable?</summary>
|
|
[MenuItem("CONTEXT/" + nameof(AnimationClip) + "/Toggle Looping", validate = true)]
|
|
[MenuItem("CONTEXT/" + nameof(AnimationClip) + "/Toggle Legacy", validate = true)]
|
|
private static bool ValidateEditable(MenuCommand command)
|
|
{
|
|
return (command.context.hideFlags & HideFlags.NotEditable) != HideFlags.NotEditable;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Toggles the <see cref="Motion.isLooping"/> flag between true and false.</summary>
|
|
[MenuItem("CONTEXT/" + nameof(AnimationClip) + "/Toggle Looping")]
|
|
private static void ToggleLooping(MenuCommand command)
|
|
{
|
|
var clip = (AnimationClip)command.context;
|
|
SetLooping(clip, !clip.isLooping);
|
|
}
|
|
|
|
/// <summary>Sets the <see cref="Motion.isLooping"/> flag.</summary>
|
|
public static void SetLooping(AnimationClip clip, bool looping)
|
|
{
|
|
var settings = AnimationUtility.GetAnimationClipSettings(clip);
|
|
settings.loopTime = looping;
|
|
AnimationUtility.SetAnimationClipSettings(clip, settings);
|
|
|
|
Debug.Log($"Set {clip.name} to be {(looping ? "Looping" : "Not Looping")}." +
|
|
" Note that you may need to restart Unity for this change to take effect.", clip);
|
|
|
|
// None of these let us avoid the need to restart Unity.
|
|
//EditorUtility.SetDirty(clip);
|
|
//AssetDatabase.SaveAssets();
|
|
|
|
//var path = AssetDatabase.GetAssetPath(clip);
|
|
//AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Swaps the <see cref="AnimationClip.legacy"/> flag between true and false.</summary>
|
|
[MenuItem("CONTEXT/" + nameof(AnimationClip) + "/Toggle Legacy")]
|
|
private static void ToggleLegacy(MenuCommand command)
|
|
{
|
|
var clip = (AnimationClip)command.context;
|
|
clip.legacy = !clip.legacy;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Calls <see cref="Animator.Rebind"/>.</summary>
|
|
[MenuItem("CONTEXT/" + nameof(Animator) + "/Restore Bind Pose", priority = 110)]
|
|
private static void RestoreBindPose(MenuCommand command)
|
|
{
|
|
var animator = (Animator)command.context;
|
|
|
|
Undo.RegisterFullObjectHierarchyUndo(animator.gameObject, "Restore bind pose");
|
|
|
|
const string TypeName = "UnityEditor.AvatarSetupTool, UnityEditor";
|
|
var type = Type.GetType(TypeName)
|
|
?? throw new TypeLoadException($"Unable to find the type '{TypeName}'");
|
|
|
|
const string MethodName = "SampleBindPose";
|
|
var method = type.GetMethod(MethodName, AnimancerReflection.StaticBindings)
|
|
?? throw new MissingMethodException($"Unable to find the method '{MethodName}'");
|
|
|
|
method.Invoke(null, new object[] { animator.gameObject });
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|