// 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 { /// [Editor-Only] Various utilities used throughout Animancer. /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerEditorUtilities public static partial class AnimancerEditorUtilities { /************************************************************************************************************************/ #region Misc /************************************************************************************************************************/ /// [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); /************************************************************************************************************************/ /// Returns the value of `t` linearly interpolated along the X axis of the `rect`. public static float LerpUnclampedX(this Rect rect, float t) => rect.x + rect.width * t; /// Returns the value of `t` inverse linearly interpolated along the X axis of the `rect`. public static float InverseLerpUnclampedX(this Rect rect, float t) => (t - rect.x) / rect.width; /************************************************************************************************************************/ /// 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; } /************************************************************************************************************************/ /// Finds or creates an instance of . public static T FindOrCreate(ref T scriptableObject, HideFlags hideFlags = default) where T : ScriptableObject { if (scriptableObject != null) return scriptableObject; var instances = Resources.FindObjectsOfTypeAll(); if (instances.Length > 0) { scriptableObject = instances[0]; } else { scriptableObject = ScriptableObject.CreateInstance(); scriptableObject.hideFlags = hideFlags; } return scriptableObject; } /************************************************************************************************************************/ /// 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() { PlayModeState = EditorApplication.isPlayingOrWillChangePlaymode ? EditorApplication.isPlaying ? PlayModeStateChange.EnteredPlayMode : PlayModeStateChange.ExitingEditMode : PlayModeStateChange.EnteredEditMode; EditorApplication.playModeStateChanged += change => PlayModeState = change; } /************************************************************************************************************************/ /// Deletes the specified `subAsset`. public static void DeleteSubAsset(Object subAsset) { AssetDatabase.RemoveObjectFromAsset(subAsset); AssetDatabase.SaveAssets(); Object.DestroyImmediate(subAsset, true); } /************************************************************************************************************************/ /// Calculates the overall bounds of all renderers under the `transform`. public static Bounds CalculateBounds(Transform transform) { using var _ = ListPool.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 /************************************************************************************************************************/ /// 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(); return false; } var modified = false; using (SetPool.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; } /************************************************************************************************************************/ /// /// Removes any null items and ensures that it contains /// an instance of each type derived from . /// public static void InstantiateDerivedTypes(ref List list) where T : IComparable { 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(); } /************************************************************************************************************************/ /// Finds the index of the first item with the specified `type`. public static int IndexOfType(IList list, Type type) { for (int i = 0; i < list.Count; i++) if (list[i].GetType() == type) return i; return -1; } /************************************************************************************************************************/ #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(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) ?? 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