AnimancerEditorUtilities.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
  2. #if UNITY_EDITOR
  3. using System;
  4. using System.Collections.Generic;
  5. using UnityEditor;
  6. using UnityEngine;
  7. using Object = UnityEngine.Object;
  8. namespace Animancer.Editor
  9. {
  10. /// <summary>[Editor-Only] Various utilities used throughout Animancer.</summary>
  11. /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerEditorUtilities
  12. public static partial class AnimancerEditorUtilities
  13. {
  14. /************************************************************************************************************************/
  15. #region Misc
  16. /************************************************************************************************************************/
  17. /// <summary>[Animancer Extension] [Editor-Only] Is the <see cref="Vector2.x"/> or <see cref="Vector2.y"/> NaN?</summary>
  18. public static bool IsNaN(this Vector2 vector)
  19. => float.IsNaN(vector.x)
  20. || float.IsNaN(vector.y);
  21. /// <summary>[Animancer Extension] [Editor-Only] Is the <see cref="Vector3.x"/>, <see cref="Vector3.y"/>, or <see cref="Vector3.z"/> NaN?</summary>
  22. public static bool IsNaN(this Vector3 vector)
  23. => float.IsNaN(vector.x)
  24. || float.IsNaN(vector.y)
  25. || float.IsNaN(vector.z);
  26. /************************************************************************************************************************/
  27. /// <summary>Returns the value of `t` linearly interpolated along the X axis of the `rect`.</summary>
  28. public static float LerpUnclampedX(this Rect rect, float t)
  29. => rect.x + rect.width * t;
  30. /// <summary>Returns the value of `t` inverse linearly interpolated along the X axis of the `rect`.</summary>
  31. public static float InverseLerpUnclampedX(this Rect rect, float t)
  32. => (t - rect.x) / rect.width;
  33. /************************************************************************************************************************/
  34. /// <summary>Finds an asset of the specified type anywhere in the project.</summary>
  35. public static T FindAssetOfType<T>()
  36. where T : Object
  37. {
  38. var filter = typeof(Component).IsAssignableFrom(typeof(T))
  39. ? $"t:{nameof(GameObject)}"
  40. : $"t:{typeof(T).Name}";
  41. var guids = AssetDatabase.FindAssets(filter);
  42. if (guids.Length == 0)
  43. return null;
  44. for (int i = 0; i < guids.Length; i++)
  45. {
  46. var path = AssetDatabase.GUIDToAssetPath(guids[i]);
  47. var asset = AssetDatabase.LoadAssetAtPath<T>(path);
  48. if (asset != null)
  49. return asset;
  50. }
  51. return null;
  52. }
  53. /************************************************************************************************************************/
  54. /// <summary>Finds or creates an instance of <typeparamref name="T"/>.</summary>
  55. public static T FindOrCreate<T>(ref T scriptableObject, HideFlags hideFlags = default)
  56. where T : ScriptableObject
  57. {
  58. if (scriptableObject != null)
  59. return scriptableObject;
  60. var instances = Resources.FindObjectsOfTypeAll<T>();
  61. if (instances.Length > 0)
  62. {
  63. scriptableObject = instances[0];
  64. }
  65. else
  66. {
  67. scriptableObject = ScriptableObject.CreateInstance<T>();
  68. scriptableObject.hideFlags = hideFlags;
  69. }
  70. return scriptableObject;
  71. }
  72. /************************************************************************************************************************/
  73. /// <summary>The most recent <see cref="PlayModeStateChange"/>.</summary>
  74. public static PlayModeStateChange PlayModeState { get; private set; }
  75. /// <summary>Is the Unity Editor is currently changing between Play Mode and Edit Mode?</summary>
  76. public static bool IsChangingPlayMode =>
  77. PlayModeState == PlayModeStateChange.ExitingEditMode ||
  78. PlayModeState == PlayModeStateChange.ExitingPlayMode;
  79. [InitializeOnLoadMethod]
  80. private static void WatchForPlayModeChanges()
  81. {
  82. PlayModeState = EditorApplication.isPlayingOrWillChangePlaymode
  83. ? EditorApplication.isPlaying
  84. ? PlayModeStateChange.EnteredPlayMode
  85. : PlayModeStateChange.ExitingEditMode
  86. : PlayModeStateChange.EnteredEditMode;
  87. EditorApplication.playModeStateChanged += change => PlayModeState = change;
  88. }
  89. /************************************************************************************************************************/
  90. /// <summary>Deletes the specified `subAsset`.</summary>
  91. public static void DeleteSubAsset(Object subAsset)
  92. {
  93. AssetDatabase.RemoveObjectFromAsset(subAsset);
  94. AssetDatabase.SaveAssets();
  95. Object.DestroyImmediate(subAsset, true);
  96. }
  97. /************************************************************************************************************************/
  98. /// <summary>Calculates the overall bounds of all renderers under the `transform`.</summary>
  99. public static Bounds CalculateBounds(Transform transform)
  100. {
  101. using var _ = ListPool<Renderer>.Instance.Acquire(out var renderers);
  102. transform.GetComponentsInChildren(renderers);
  103. if (renderers.Count == 0)
  104. return default;
  105. var bounds = renderers[0].bounds;
  106. for (int i = 1; i < renderers.Count; i++)
  107. bounds.Encapsulate(renderers[i].bounds);
  108. return bounds;
  109. }
  110. /************************************************************************************************************************/
  111. #endregion
  112. /************************************************************************************************************************/
  113. #region Collections
  114. /************************************************************************************************************************/
  115. /// <summary>Adds default items or removes items to make the <see cref="List{T}.Count"/> equal to the `count`.</summary>
  116. public static void SetCount<T>(List<T> list, int count)
  117. {
  118. if (list.Count < count)
  119. {
  120. while (list.Count < count)
  121. list.Add(default);
  122. }
  123. else
  124. {
  125. list.RemoveRange(count, list.Count - count);
  126. }
  127. }
  128. /************************************************************************************************************************/
  129. /// <summary>
  130. /// Removes any items from the `list` that are <c>null</c> and items that appear multiple times.
  131. /// Returns true if the `list` was modified.
  132. /// </summary>
  133. public static bool RemoveMissingAndDuplicates(ref List<GameObject> list)
  134. {
  135. if (list == null)
  136. {
  137. list = new();
  138. return false;
  139. }
  140. var modified = false;
  141. using (SetPool<Object>.Instance.Acquire(out var previousItems))
  142. {
  143. for (int i = list.Count - 1; i >= 0; i--)
  144. {
  145. var item = list[i];
  146. if (item == null || previousItems.Contains(item))
  147. {
  148. list.RemoveAt(i);
  149. modified = true;
  150. }
  151. else
  152. {
  153. previousItems.Add(item);
  154. }
  155. }
  156. }
  157. return modified;
  158. }
  159. /************************************************************************************************************************/
  160. /// <summary>
  161. /// Removes any <c>null</c> items and ensures that it contains
  162. /// an instance of each type derived from <typeparamref name="T"/>.
  163. /// </summary>
  164. public static void InstantiateDerivedTypes<T>(ref List<T> list)
  165. where T : IComparable<T>
  166. {
  167. if (list == null)
  168. {
  169. list = new();
  170. }
  171. else
  172. {
  173. for (int i = list.Count - 1; i >= 0; i--)
  174. if (list[i] == null)
  175. list.RemoveAt(i);
  176. }
  177. var types = TypeSelectionMenu.GetDerivedTypes(typeof(T));
  178. for (int i = 0; i < types.Count; i++)
  179. {
  180. var toolType = types[i];
  181. if (IndexOfType(list, toolType) >= 0)
  182. continue;
  183. var instance = (T)Activator.CreateInstance(toolType);
  184. list.Add(instance);
  185. }
  186. list.Sort();
  187. }
  188. /************************************************************************************************************************/
  189. /// <summary>Finds the index of the first item with the specified `type`.</summary>
  190. public static int IndexOfType<T>(IList<T> list, Type type)
  191. {
  192. for (int i = 0; i < list.Count; i++)
  193. if (list[i].GetType() == type)
  194. return i;
  195. return -1;
  196. }
  197. /************************************************************************************************************************/
  198. #endregion
  199. /************************************************************************************************************************/
  200. #region Context Menus
  201. /************************************************************************************************************************/
  202. /// <summary>
  203. /// Adds a menu function which passes the result of <see cref="CalculateEditorFadeDuration"/> into `startFade`.
  204. /// </summary>
  205. public static void AddFadeFunction(
  206. GenericMenu menu,
  207. string label,
  208. bool isEnabled,
  209. AnimancerNode node,
  210. Action<float> startFade)
  211. {
  212. // Fade functions need to be delayed twice since the context menu itself causes the next frame delta
  213. // time to be unreasonably high (which would skip the start of the fade).
  214. menu.AddFunction(label, isEnabled,
  215. () => EditorApplication.delayCall +=
  216. () => EditorApplication.delayCall +=
  217. () =>
  218. {
  219. startFade(node.CalculateEditorFadeDuration());
  220. });
  221. }
  222. /// <summary>[Animancer Extension] [Editor-Only]
  223. /// Returns the duration of the `node`s current fade (if any), otherwise returns the `defaultDuration`.
  224. /// </summary>
  225. public static float CalculateEditorFadeDuration(this AnimancerNode node, float defaultDuration = 1)
  226. => node.FadeSpeed > 0
  227. ? 1 / node.FadeSpeed
  228. : defaultDuration;
  229. /************************************************************************************************************************/
  230. /// <summary>
  231. /// Adds a menu function to open a web page. If the `linkSuffix` starts with a '/' then it will be relative to
  232. /// the <see cref="Strings.DocsURLs.Documentation"/>.
  233. /// </summary>
  234. public static void AddDocumentationLink(GenericMenu menu, string label, string linkSuffix)
  235. {
  236. if (linkSuffix[0] == '/')
  237. linkSuffix = Strings.DocsURLs.Documentation + linkSuffix;
  238. menu.AddItem(new(label), false, () =>
  239. {
  240. EditorUtility.OpenWithDefaultApp(linkSuffix);
  241. });
  242. }
  243. /************************************************************************************************************************/
  244. /// <summary>Is the <see cref="MenuCommand.context"/> editable?</summary>
  245. [MenuItem("CONTEXT/" + nameof(AnimationClip) + "/Toggle Looping", validate = true)]
  246. [MenuItem("CONTEXT/" + nameof(AnimationClip) + "/Toggle Legacy", validate = true)]
  247. private static bool ValidateEditable(MenuCommand command)
  248. {
  249. return (command.context.hideFlags & HideFlags.NotEditable) != HideFlags.NotEditable;
  250. }
  251. /************************************************************************************************************************/
  252. /// <summary>Toggles the <see cref="Motion.isLooping"/> flag between true and false.</summary>
  253. [MenuItem("CONTEXT/" + nameof(AnimationClip) + "/Toggle Looping")]
  254. private static void ToggleLooping(MenuCommand command)
  255. {
  256. var clip = (AnimationClip)command.context;
  257. SetLooping(clip, !clip.isLooping);
  258. }
  259. /// <summary>Sets the <see cref="Motion.isLooping"/> flag.</summary>
  260. public static void SetLooping(AnimationClip clip, bool looping)
  261. {
  262. var settings = AnimationUtility.GetAnimationClipSettings(clip);
  263. settings.loopTime = looping;
  264. AnimationUtility.SetAnimationClipSettings(clip, settings);
  265. Debug.Log($"Set {clip.name} to be {(looping ? "Looping" : "Not Looping")}." +
  266. " Note that you may need to restart Unity for this change to take effect.", clip);
  267. // None of these let us avoid the need to restart Unity.
  268. //EditorUtility.SetDirty(clip);
  269. //AssetDatabase.SaveAssets();
  270. //var path = AssetDatabase.GetAssetPath(clip);
  271. //AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
  272. }
  273. /************************************************************************************************************************/
  274. /// <summary>Swaps the <see cref="AnimationClip.legacy"/> flag between true and false.</summary>
  275. [MenuItem("CONTEXT/" + nameof(AnimationClip) + "/Toggle Legacy")]
  276. private static void ToggleLegacy(MenuCommand command)
  277. {
  278. var clip = (AnimationClip)command.context;
  279. clip.legacy = !clip.legacy;
  280. }
  281. /************************************************************************************************************************/
  282. /// <summary>Calls <see cref="Animator.Rebind"/>.</summary>
  283. [MenuItem("CONTEXT/" + nameof(Animator) + "/Restore Bind Pose", priority = 110)]
  284. private static void RestoreBindPose(MenuCommand command)
  285. {
  286. var animator = (Animator)command.context;
  287. Undo.RegisterFullObjectHierarchyUndo(animator.gameObject, "Restore bind pose");
  288. const string TypeName = "UnityEditor.AvatarSetupTool, UnityEditor";
  289. var type = Type.GetType(TypeName)
  290. ?? throw new TypeLoadException($"Unable to find the type '{TypeName}'");
  291. const string MethodName = "SampleBindPose";
  292. var method = type.GetMethod(MethodName, AnimancerReflection.StaticBindings)
  293. ?? throw new MissingMethodException($"Unable to find the method '{MethodName}'");
  294. method.Invoke(null, new object[] { animator.gameObject });
  295. }
  296. /************************************************************************************************************************/
  297. #endregion
  298. /************************************************************************************************************************/
  299. }
  300. }
  301. #endif