// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik // #if UNITY_EDITOR using System; using System.Collections.Generic; using System.Reflection; using System.Text; using UnityEditor; using UnityEngine; using Object = UnityEngine.Object; namespace Animancer.Editor { /// [Editor-Only] Various utilities used throughout Animancer. /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerEditorUtilities /// public static partial class AnimancerEditorUtilities { /************************************************************************************************************************/ #region Misc /************************************************************************************************************************/ /// Commonly used combinations. public const BindingFlags AnyAccessBindings = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static, InstanceBindings = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, StaticBindings = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static; /************************************************************************************************************************/ /// [Animancer Extension] [Editor-Only] /// Returns the first attribute on the `member` or null if there is none. /// public static TAttribute GetAttribute(this ICustomAttributeProvider member, bool inherit = false) where TAttribute : class { var type = typeof(TAttribute); if (member.IsDefined(type, inherit)) return (TAttribute)member.GetCustomAttributes(type, inherit)[0]; else return null; } /************************************************************************************************************************/ /// [Animancer Extension] [Editor-Only] Is the or NaN? public static bool IsNaN(this Vector2 vector) => float.IsNaN(vector.x) || float.IsNaN(vector.y); /// [Animancer Extension] [Editor-Only] Is the , , or NaN? public static bool IsNaN(this Vector3 vector) => float.IsNaN(vector.x) || float.IsNaN(vector.y) || float.IsNaN(vector.z); /************************************************************************************************************************/ /// Finds an asset of the specified type anywhere in the project. public static T FindAssetOfType() 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(path); if (asset != null) return asset; } return null; } /************************************************************************************************************************/ // The "g" format gives a lower case 'e' for exponentials instead of upper case 'E'. private static readonly ConversionCache FloatToString = new ConversionCache((value) => $"{value:g}"); /// [Animancer Extension] /// Calls using "g" as the format and caches the result. /// public static string ToStringCached(this float value) => FloatToString.Convert(value); /************************************************************************************************************************/ /// The most recent . public static PlayModeStateChange PlayModeState { get; private set; } /// Is the Unity Editor is currently changing between Play Mode and Edit Mode? public static bool IsChangingPlayMode => PlayModeState == PlayModeStateChange.ExitingEditMode || PlayModeState == PlayModeStateChange.ExitingPlayMode; [InitializeOnLoadMethod] private static void WatchForPlayModeChanges() { if (EditorApplication.isPlayingOrWillChangePlaymode) PlayModeState = EditorApplication.isPlaying ? PlayModeStateChange.EnteredPlayMode : PlayModeStateChange.ExitingEditMode; EditorApplication.playModeStateChanged += (change) => PlayModeState = change; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Collections /************************************************************************************************************************/ /// Adds default items or removes items to make the equal to the `count`. public static void SetCount(List list, int count) { if (list.Count < count) { while (list.Count < count) list.Add(default); } else { list.RemoveRange(count, list.Count - count); } } /************************************************************************************************************************/ /// /// Removes any items from the `list` that are null and items that appear multiple times. /// Returns true if the `list` was modified. /// public static bool RemoveMissingAndDuplicates(ref List list) { if (list == null) { list = new List(); return false; } var modified = false; using (ObjectPool.Disposable.AcquireSet(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; } /************************************************************************************************************************/ /// Removes any items from the `dictionary` that use destroyed objects as their key. public static void RemoveDestroyedObjects(Dictionary dictionary) where TKey : Object { using (ObjectPool.Disposable.AcquireList(out var oldObjects)) { foreach (var obj in dictionary.Keys) { if (obj == null) oldObjects.Add(obj); } for (int i = 0; i < oldObjects.Count; i++) { dictionary.Remove(oldObjects[i]); } } } /// /// Creates a new dictionary and returns true if it was null or calls and /// returns false if it wasn't. /// public static bool InitializeCleanDictionary(ref Dictionary dictionary) where TKey : Object { if (dictionary == null) { dictionary = new Dictionary(); return true; } else { RemoveDestroyedObjects(dictionary); return false; } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Context Menus /************************************************************************************************************************/ /// /// Adds a menu function which passes the result of into `startFade`. /// public static void AddFadeFunction(GenericMenu menu, string label, bool isEnabled, AnimancerNode node, Action 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()); }); } /// [Animancer Extension] [Editor-Only] /// Returns the duration of the `node`s current fade (if any), otherwise returns the `defaultDuration`. /// public static float CalculateEditorFadeDuration(this AnimancerNode node, float defaultDuration = 1) => node.FadeSpeed > 0 ? 1 / node.FadeSpeed : defaultDuration; /************************************************************************************************************************/ /// /// Adds a menu function to open a web page. If the `linkSuffix` starts with a '/' then it will be relative to /// the . /// public static void AddDocumentationLink(GenericMenu menu, string label, string linkSuffix) { if (linkSuffix[0] == '/') linkSuffix = Strings.DocsURLs.Documentation + linkSuffix; menu.AddItem(new GUIContent(label), false, () => { EditorUtility.OpenWithDefaultApp(linkSuffix); }); } /************************************************************************************************************************/ /// Is the editable? [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; } /************************************************************************************************************************/ /// Toggles the flag between true and false. [MenuItem("CONTEXT/" + nameof(AnimationClip) + "/Toggle Looping")] private static void ToggleLooping(MenuCommand command) { var clip = (AnimationClip)command.context; SetLooping(clip, !clip.isLooping); } /// Sets the flag. 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); } /************************************************************************************************************************/ /// Swaps the flag between true and false. [MenuItem("CONTEXT/" + nameof(AnimationClip) + "/Toggle Legacy")] private static void ToggleLegacy(MenuCommand command) { var clip = (AnimationClip)command.context; clip.legacy = !clip.legacy; } /************************************************************************************************************************/ /// Calls . [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); if (type == null) throw new TypeLoadException($"Unable to find the type '{TypeName}'"); const string MethodName = "SampleBindPose"; var method = type.GetMethod(MethodName, StaticBindings); if (method == null) throw new MissingMethodException($"Unable to find the method '{MethodName}'"); method.Invoke(null, new object[] { animator.gameObject }); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Type Names /************************************************************************************************************************/ private static readonly Dictionary TypeNames = new Dictionary { { typeof(object), "object" }, { typeof(void), "void" }, { typeof(bool), "bool" }, { typeof(byte), "byte" }, { typeof(sbyte), "sbyte" }, { typeof(char), "char" }, { typeof(string), "string" }, { typeof(short), "short" }, { typeof(int), "int" }, { typeof(long), "long" }, { typeof(ushort), "ushort" }, { typeof(uint), "uint" }, { typeof(ulong), "ulong" }, { typeof(float), "float" }, { typeof(double), "double" }, { typeof(decimal), "decimal" }, }; private static readonly Dictionary FullTypeNames = new Dictionary(TypeNames); /************************************************************************************************************************/ /// Returns the name of a `type` as it would appear in C# code. /// Returned values are stored in a dictionary to speed up repeated use. /// /// typeof(List<float>).FullName would give you: /// System.Collections.Generic.List`1[[System.Single, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]] /// /// This method would instead return System.Collections.Generic.List<float> if `fullName` is true, or /// just List<float> if it is false. /// public static string GetNameCS(this Type type, bool fullName = true) { if (type == null) return "null"; // Check if we have already got the name for that type. var names = fullName ? FullTypeNames : TypeNames; if (names.TryGetValue(type, out var name)) return name; var text = ObjectPool.AcquireStringBuilder(); if (type.IsArray)// Array = TypeName[]. { text.Append(type.GetElementType().GetNameCS(fullName)); text.Append('['); var dimensions = type.GetArrayRank(); while (dimensions-- > 1) text.Append(','); text.Append(']'); goto Return; } if (type.IsPointer)// Pointer = TypeName*. { text.Append(type.GetElementType().GetNameCS(fullName)); text.Append('*'); goto Return; } if (type.IsGenericParameter)// Generic Parameter = TypeName (for unspecified generic parameters). { text.Append(type.Name); goto Return; } var underlyingType = Nullable.GetUnderlyingType(type); if (underlyingType != null)// Nullable = TypeName != null ? { text.Append(underlyingType.GetNameCS(fullName)); text.Append('?'); goto Return; } // Other Type = Namespace.NestedTypes.TypeName. if (fullName && type.Namespace != null)// Namespace. { text.Append(type.Namespace); text.Append('.'); } var genericArguments = 0; if (type.DeclaringType != null)// Account for Nested Types. { // Count the nesting level. var nesting = 1; var declaringType = type.DeclaringType; while (declaringType.DeclaringType != null) { declaringType = declaringType.DeclaringType; nesting++; } // Append the name of each outer type, starting from the outside. while (nesting-- > 0) { // Walk out to the current nesting level. // This avoids the need to make a list of types in the nest or to insert type names instead of appending them. declaringType = type; for (int i = nesting; i >= 0; i--) declaringType = declaringType.DeclaringType; // Nested Type Name. genericArguments = AppendNameAndGenericArguments(text, declaringType, fullName, genericArguments); text.Append('.'); } } // Type Name. AppendNameAndGenericArguments(text, type, fullName, genericArguments); Return:// Remember and return the name. name = text.ReleaseToString(); names.Add(type, name); return name; } /************************************************************************************************************************/ /// Appends the generic arguments of `type` (after skipping the specified number). public static int AppendNameAndGenericArguments(StringBuilder text, Type type, bool fullName = true, int skipGenericArguments = 0) { var name = type.Name; text.Append(name); if (type.IsGenericType) { var backQuote = name.IndexOf('`'); if (backQuote >= 0) { text.Length -= name.Length - backQuote; var genericArguments = type.GetGenericArguments(); if (skipGenericArguments < genericArguments.Length) { text.Append('<'); var firstArgument = genericArguments[skipGenericArguments]; skipGenericArguments++; if (firstArgument.IsGenericParameter) { while (skipGenericArguments < genericArguments.Length) { text.Append(','); skipGenericArguments++; } } else { text.Append(firstArgument.GetNameCS(fullName)); while (skipGenericArguments < genericArguments.Length) { text.Append(", "); text.Append(genericArguments[skipGenericArguments].GetNameCS(fullName)); skipGenericArguments++; } } text.Append('>'); } } } return skipGenericArguments; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Dummy Animancer Component /************************************************************************************************************************/ /// [Editor-Only] /// An which is not actually a . /// public class DummyAnimancerComponent : IAnimancerComponent { /************************************************************************************************************************/ /// Creates a new . public DummyAnimancerComponent(Animator animator, AnimancerPlayable playable) { Animator = animator; Playable = playable; InitialUpdateMode = animator.updateMode; } /************************************************************************************************************************/ /// [] Returns true. public bool enabled => true; /// [] Returns the 's . public GameObject gameObject => Animator.gameObject; /// [] The target . public Animator Animator { get; set; } /// [] The target . public AnimancerPlayable Playable { get; private set; } /// [] Returns true. public bool IsPlayableInitialized => true; /// [] Returns false. public bool ResetOnDisable => false; /// [] The . public AnimatorUpdateMode UpdateMode { get => Animator.updateMode; set => Animator.updateMode = value; } /************************************************************************************************************************/ /// [] Returns the `clip`. public object GetKey(AnimationClip clip) => clip; /************************************************************************************************************************/ /// [] Returns null. public string AnimatorFieldName => null; /// [] Returns null. public string ActionOnDisableFieldName => null; /// [] Returns the from when this object was created. public AnimatorUpdateMode? InitialUpdateMode { get; private set; } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } } #endif