AnimancerEditorUtilities.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2022 Kybernetik //
  2. #if UNITY_EDITOR
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Reflection;
  6. using System.Text;
  7. using UnityEditor;
  8. using UnityEngine;
  9. using Object = UnityEngine.Object;
  10. namespace Animancer.Editor
  11. {
  12. /// <summary>[Editor-Only] Various utilities used throughout Animancer.</summary>
  13. /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerEditorUtilities
  14. ///
  15. public static partial class AnimancerEditorUtilities
  16. {
  17. /************************************************************************************************************************/
  18. #region Misc
  19. /************************************************************************************************************************/
  20. /// <summary>Commonly used <see cref="BindingFlags"/> combinations.</summary>
  21. public const BindingFlags
  22. AnyAccessBindings = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static,
  23. InstanceBindings = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
  24. StaticBindings = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
  25. /************************************************************************************************************************/
  26. /// <summary>[Animancer Extension] [Editor-Only]
  27. /// Returns the first <typeparamref name="TAttribute"/> attribute on the `member` or <c>null</c> if there is none.
  28. /// </summary>
  29. public static TAttribute GetAttribute<TAttribute>(this ICustomAttributeProvider member, bool inherit = false)
  30. where TAttribute : class
  31. {
  32. var type = typeof(TAttribute);
  33. if (member.IsDefined(type, inherit))
  34. return (TAttribute)member.GetCustomAttributes(type, inherit)[0];
  35. else
  36. return null;
  37. }
  38. /************************************************************************************************************************/
  39. /// <summary>[Animancer Extension] [Editor-Only] Is the <see cref="Vector2.x"/> or <see cref="Vector2.y"/> NaN?</summary>
  40. public static bool IsNaN(this Vector2 vector) => float.IsNaN(vector.x) || float.IsNaN(vector.y);
  41. /// <summary>[Animancer Extension] [Editor-Only] Is the <see cref="Vector3.x"/>, <see cref="Vector3.y"/>, or <see cref="Vector3.z"/> NaN?</summary>
  42. public static bool IsNaN(this Vector3 vector) => float.IsNaN(vector.x) || float.IsNaN(vector.y) || float.IsNaN(vector.z);
  43. /************************************************************************************************************************/
  44. /// <summary>Finds an asset of the specified type anywhere in the project.</summary>
  45. public static T FindAssetOfType<T>() where T : Object
  46. {
  47. var filter = typeof(Component).IsAssignableFrom(typeof(T)) ? $"t:{nameof(GameObject)}" : $"t:{typeof(T).Name}";
  48. var guids = AssetDatabase.FindAssets(filter);
  49. if (guids.Length == 0)
  50. return null;
  51. for (int i = 0; i < guids.Length; i++)
  52. {
  53. var path = AssetDatabase.GUIDToAssetPath(guids[i]);
  54. var asset = AssetDatabase.LoadAssetAtPath<T>(path);
  55. if (asset != null)
  56. return asset;
  57. }
  58. return null;
  59. }
  60. /************************************************************************************************************************/
  61. // The "g" format gives a lower case 'e' for exponentials instead of upper case 'E'.
  62. private static readonly ConversionCache<float, string>
  63. FloatToString = new ConversionCache<float, string>((value) => $"{value:g}");
  64. /// <summary>[Animancer Extension]
  65. /// Calls <see cref="float.ToString(string)"/> using <c>"g"</c> as the format and caches the result.
  66. /// </summary>
  67. public static string ToStringCached(this float value) => FloatToString.Convert(value);
  68. /************************************************************************************************************************/
  69. /// <summary>The most recent <see cref="PlayModeStateChange"/>.</summary>
  70. public static PlayModeStateChange PlayModeState { get; private set; }
  71. /// <summary>Is the Unity Editor is currently changing between Play Mode and Edit Mode?</summary>
  72. public static bool IsChangingPlayMode =>
  73. PlayModeState == PlayModeStateChange.ExitingEditMode ||
  74. PlayModeState == PlayModeStateChange.ExitingPlayMode;
  75. [InitializeOnLoadMethod]
  76. private static void WatchForPlayModeChanges()
  77. {
  78. if (EditorApplication.isPlayingOrWillChangePlaymode)
  79. PlayModeState = EditorApplication.isPlaying ?
  80. PlayModeStateChange.EnteredPlayMode :
  81. PlayModeStateChange.ExitingEditMode;
  82. EditorApplication.playModeStateChanged += (change) => PlayModeState = change;
  83. }
  84. /************************************************************************************************************************/
  85. #endregion
  86. /************************************************************************************************************************/
  87. #region Collections
  88. /************************************************************************************************************************/
  89. /// <summary>Adds default items or removes items to make the <see cref="List{T}.Count"/> equal to the `count`.</summary>
  90. public static void SetCount<T>(List<T> list, int count)
  91. {
  92. if (list.Count < count)
  93. {
  94. while (list.Count < count)
  95. list.Add(default);
  96. }
  97. else
  98. {
  99. list.RemoveRange(count, list.Count - count);
  100. }
  101. }
  102. /************************************************************************************************************************/
  103. /// <summary>
  104. /// Removes any items from the `list` that are <c>null</c> and items that appear multiple times.
  105. /// Returns true if the `list` was modified.
  106. /// </summary>
  107. public static bool RemoveMissingAndDuplicates(ref List<GameObject> list)
  108. {
  109. if (list == null)
  110. {
  111. list = new List<GameObject>();
  112. return false;
  113. }
  114. var modified = false;
  115. using (ObjectPool.Disposable.AcquireSet<Object>(out var previousItems))
  116. {
  117. for (int i = list.Count - 1; i >= 0; i--)
  118. {
  119. var item = list[i];
  120. if (item == null || previousItems.Contains(item))
  121. {
  122. list.RemoveAt(i);
  123. modified = true;
  124. }
  125. else
  126. {
  127. previousItems.Add(item);
  128. }
  129. }
  130. }
  131. return modified;
  132. }
  133. /************************************************************************************************************************/
  134. /// <summary>Removes any items from the `dictionary` that use destroyed objects as their key.</summary>
  135. public static void RemoveDestroyedObjects<TKey, TValue>(Dictionary<TKey, TValue> dictionary) where TKey : Object
  136. {
  137. using (ObjectPool.Disposable.AcquireList<TKey>(out var oldObjects))
  138. {
  139. foreach (var obj in dictionary.Keys)
  140. {
  141. if (obj == null)
  142. oldObjects.Add(obj);
  143. }
  144. for (int i = 0; i < oldObjects.Count; i++)
  145. {
  146. dictionary.Remove(oldObjects[i]);
  147. }
  148. }
  149. }
  150. /// <summary>
  151. /// Creates a new dictionary and returns true if it was null or calls <see cref="RemoveDestroyedObjects"/> and
  152. /// returns false if it wasn't.
  153. /// </summary>
  154. public static bool InitializeCleanDictionary<TKey, TValue>(ref Dictionary<TKey, TValue> dictionary) where TKey : Object
  155. {
  156. if (dictionary == null)
  157. {
  158. dictionary = new Dictionary<TKey, TValue>();
  159. return true;
  160. }
  161. else
  162. {
  163. RemoveDestroyedObjects(dictionary);
  164. return false;
  165. }
  166. }
  167. /************************************************************************************************************************/
  168. #endregion
  169. /************************************************************************************************************************/
  170. #region Context Menus
  171. /************************************************************************************************************************/
  172. /// <summary>
  173. /// Adds a menu function which passes the result of <see cref="CalculateEditorFadeDuration"/> into `startFade`.
  174. /// </summary>
  175. public static void AddFadeFunction(GenericMenu menu, string label, bool isEnabled, AnimancerNode node, Action<float> startFade)
  176. {
  177. // Fade functions need to be delayed twice since the context menu itself causes the next frame delta
  178. // time to be unreasonably high (which would skip the start of the fade).
  179. menu.AddFunction(label, isEnabled,
  180. () => EditorApplication.delayCall +=
  181. () => EditorApplication.delayCall +=
  182. () =>
  183. {
  184. startFade(node.CalculateEditorFadeDuration());
  185. });
  186. }
  187. /// <summary>[Animancer Extension] [Editor-Only]
  188. /// Returns the duration of the `node`s current fade (if any), otherwise returns the `defaultDuration`.
  189. /// </summary>
  190. public static float CalculateEditorFadeDuration(this AnimancerNode node, float defaultDuration = 1)
  191. => node.FadeSpeed > 0 ? 1 / node.FadeSpeed : defaultDuration;
  192. /************************************************************************************************************************/
  193. /// <summary>
  194. /// Adds a menu function to open a web page. If the `linkSuffix` starts with a '/' then it will be relative to
  195. /// the <see cref="Strings.DocsURLs.Documentation"/>.
  196. /// </summary>
  197. public static void AddDocumentationLink(GenericMenu menu, string label, string linkSuffix)
  198. {
  199. if (linkSuffix[0] == '/')
  200. linkSuffix = Strings.DocsURLs.Documentation + linkSuffix;
  201. menu.AddItem(new GUIContent(label), false, () =>
  202. {
  203. EditorUtility.OpenWithDefaultApp(linkSuffix);
  204. });
  205. }
  206. /************************************************************************************************************************/
  207. /// <summary>Is the <see cref="MenuCommand.context"/> editable?</summary>
  208. [MenuItem("CONTEXT/" + nameof(AnimationClip) + "/Toggle Looping", validate = true)]
  209. [MenuItem("CONTEXT/" + nameof(AnimationClip) + "/Toggle Legacy", validate = true)]
  210. private static bool ValidateEditable(MenuCommand command)
  211. {
  212. return (command.context.hideFlags & HideFlags.NotEditable) != HideFlags.NotEditable;
  213. }
  214. /************************************************************************************************************************/
  215. /// <summary>Toggles the <see cref="Motion.isLooping"/> flag between true and false.</summary>
  216. [MenuItem("CONTEXT/" + nameof(AnimationClip) + "/Toggle Looping")]
  217. private static void ToggleLooping(MenuCommand command)
  218. {
  219. var clip = (AnimationClip)command.context;
  220. SetLooping(clip, !clip.isLooping);
  221. }
  222. /// <summary>Sets the <see cref="Motion.isLooping"/> flag.</summary>
  223. public static void SetLooping(AnimationClip clip, bool looping)
  224. {
  225. var settings = AnimationUtility.GetAnimationClipSettings(clip);
  226. settings.loopTime = looping;
  227. AnimationUtility.SetAnimationClipSettings(clip, settings);
  228. Debug.Log($"Set {clip.name} to be {(looping ? "Looping" : "Not Looping")}." +
  229. " Note that you may need to restart Unity for this change to take effect.", clip);
  230. // None of these let us avoid the need to restart Unity.
  231. //EditorUtility.SetDirty(clip);
  232. //AssetDatabase.SaveAssets();
  233. //var path = AssetDatabase.GetAssetPath(clip);
  234. //AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
  235. }
  236. /************************************************************************************************************************/
  237. /// <summary>Swaps the <see cref="AnimationClip.legacy"/> flag between true and false.</summary>
  238. [MenuItem("CONTEXT/" + nameof(AnimationClip) + "/Toggle Legacy")]
  239. private static void ToggleLegacy(MenuCommand command)
  240. {
  241. var clip = (AnimationClip)command.context;
  242. clip.legacy = !clip.legacy;
  243. }
  244. /************************************************************************************************************************/
  245. /// <summary>Calls <see cref="Animator.Rebind"/>.</summary>
  246. [MenuItem("CONTEXT/" + nameof(Animator) + "/Restore Bind Pose", priority = 110)]
  247. private static void RestoreBindPose(MenuCommand command)
  248. {
  249. var animator = (Animator)command.context;
  250. Undo.RegisterFullObjectHierarchyUndo(animator.gameObject, "Restore bind pose");
  251. const string TypeName = "UnityEditor.AvatarSetupTool, UnityEditor";
  252. var type = Type.GetType(TypeName);
  253. if (type == null)
  254. throw new TypeLoadException($"Unable to find the type '{TypeName}'");
  255. const string MethodName = "SampleBindPose";
  256. var method = type.GetMethod(MethodName, StaticBindings);
  257. if (method == null)
  258. throw new MissingMethodException($"Unable to find the method '{MethodName}'");
  259. method.Invoke(null, new object[] { animator.gameObject });
  260. }
  261. /************************************************************************************************************************/
  262. #endregion
  263. /************************************************************************************************************************/
  264. #region Type Names
  265. /************************************************************************************************************************/
  266. private static readonly Dictionary<Type, string>
  267. TypeNames = new Dictionary<Type, string>
  268. {
  269. { typeof(object), "object" },
  270. { typeof(void), "void" },
  271. { typeof(bool), "bool" },
  272. { typeof(byte), "byte" },
  273. { typeof(sbyte), "sbyte" },
  274. { typeof(char), "char" },
  275. { typeof(string), "string" },
  276. { typeof(short), "short" },
  277. { typeof(int), "int" },
  278. { typeof(long), "long" },
  279. { typeof(ushort), "ushort" },
  280. { typeof(uint), "uint" },
  281. { typeof(ulong), "ulong" },
  282. { typeof(float), "float" },
  283. { typeof(double), "double" },
  284. { typeof(decimal), "decimal" },
  285. };
  286. private static readonly Dictionary<Type, string>
  287. FullTypeNames = new Dictionary<Type, string>(TypeNames);
  288. /************************************************************************************************************************/
  289. /// <summary>Returns the name of a `type` as it would appear in C# code.</summary>
  290. /// <remarks>Returned values are stored in a dictionary to speed up repeated use.</remarks>
  291. /// <example>
  292. /// <c>typeof(List&lt;float&gt;).FullName</c> would give you:
  293. /// <c>System.Collections.Generic.List`1[[System.Single, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]</c>
  294. /// <para></para>
  295. /// This method would instead return <c>System.Collections.Generic.List&lt;float&gt;</c> if `fullName` is <c>true</c>, or
  296. /// just <c>List&lt;float&gt;</c> if it is <c>false</c>.
  297. /// </example>
  298. public static string GetNameCS(this Type type, bool fullName = true)
  299. {
  300. if (type == null)
  301. return "null";
  302. // Check if we have already got the name for that type.
  303. var names = fullName ? FullTypeNames : TypeNames;
  304. if (names.TryGetValue(type, out var name))
  305. return name;
  306. var text = ObjectPool.AcquireStringBuilder();
  307. if (type.IsArray)// Array = TypeName[].
  308. {
  309. text.Append(type.GetElementType().GetNameCS(fullName));
  310. text.Append('[');
  311. var dimensions = type.GetArrayRank();
  312. while (dimensions-- > 1)
  313. text.Append(',');
  314. text.Append(']');
  315. goto Return;
  316. }
  317. if (type.IsPointer)// Pointer = TypeName*.
  318. {
  319. text.Append(type.GetElementType().GetNameCS(fullName));
  320. text.Append('*');
  321. goto Return;
  322. }
  323. if (type.IsGenericParameter)// Generic Parameter = TypeName (for unspecified generic parameters).
  324. {
  325. text.Append(type.Name);
  326. goto Return;
  327. }
  328. var underlyingType = Nullable.GetUnderlyingType(type);
  329. if (underlyingType != null)// Nullable = TypeName != null ?
  330. {
  331. text.Append(underlyingType.GetNameCS(fullName));
  332. text.Append('?');
  333. goto Return;
  334. }
  335. // Other Type = Namespace.NestedTypes.TypeName<GenericArguments>.
  336. if (fullName && type.Namespace != null)// Namespace.
  337. {
  338. text.Append(type.Namespace);
  339. text.Append('.');
  340. }
  341. var genericArguments = 0;
  342. if (type.DeclaringType != null)// Account for Nested Types.
  343. {
  344. // Count the nesting level.
  345. var nesting = 1;
  346. var declaringType = type.DeclaringType;
  347. while (declaringType.DeclaringType != null)
  348. {
  349. declaringType = declaringType.DeclaringType;
  350. nesting++;
  351. }
  352. // Append the name of each outer type, starting from the outside.
  353. while (nesting-- > 0)
  354. {
  355. // Walk out to the current nesting level.
  356. // This avoids the need to make a list of types in the nest or to insert type names instead of appending them.
  357. declaringType = type;
  358. for (int i = nesting; i >= 0; i--)
  359. declaringType = declaringType.DeclaringType;
  360. // Nested Type Name.
  361. genericArguments = AppendNameAndGenericArguments(text, declaringType, fullName, genericArguments);
  362. text.Append('.');
  363. }
  364. }
  365. // Type Name.
  366. AppendNameAndGenericArguments(text, type, fullName, genericArguments);
  367. Return:// Remember and return the name.
  368. name = text.ReleaseToString();
  369. names.Add(type, name);
  370. return name;
  371. }
  372. /************************************************************************************************************************/
  373. /// <summary>Appends the generic arguments of `type` (after skipping the specified number).</summary>
  374. public static int AppendNameAndGenericArguments(StringBuilder text, Type type, bool fullName = true, int skipGenericArguments = 0)
  375. {
  376. var name = type.Name;
  377. text.Append(name);
  378. if (type.IsGenericType)
  379. {
  380. var backQuote = name.IndexOf('`');
  381. if (backQuote >= 0)
  382. {
  383. text.Length -= name.Length - backQuote;
  384. var genericArguments = type.GetGenericArguments();
  385. if (skipGenericArguments < genericArguments.Length)
  386. {
  387. text.Append('<');
  388. var firstArgument = genericArguments[skipGenericArguments];
  389. skipGenericArguments++;
  390. if (firstArgument.IsGenericParameter)
  391. {
  392. while (skipGenericArguments < genericArguments.Length)
  393. {
  394. text.Append(',');
  395. skipGenericArguments++;
  396. }
  397. }
  398. else
  399. {
  400. text.Append(firstArgument.GetNameCS(fullName));
  401. while (skipGenericArguments < genericArguments.Length)
  402. {
  403. text.Append(", ");
  404. text.Append(genericArguments[skipGenericArguments].GetNameCS(fullName));
  405. skipGenericArguments++;
  406. }
  407. }
  408. text.Append('>');
  409. }
  410. }
  411. }
  412. return skipGenericArguments;
  413. }
  414. /************************************************************************************************************************/
  415. #endregion
  416. /************************************************************************************************************************/
  417. #region Dummy Animancer Component
  418. /************************************************************************************************************************/
  419. /// <summary>[Editor-Only] An <see cref="IAnimancerComponent"/> which is not actually a <see cref="Component"/>.</summary>
  420. public class DummyAnimancerComponent : IAnimancerComponent
  421. {
  422. /************************************************************************************************************************/
  423. /// <summary>Creates a new <see cref="DummyAnimancerComponent"/>.</summary>
  424. public DummyAnimancerComponent(Animator animator, AnimancerPlayable playable)
  425. {
  426. Animator = animator;
  427. Playable = playable;
  428. InitialUpdateMode = animator.updateMode;
  429. }
  430. /************************************************************************************************************************/
  431. /// <summary>[<see cref="IAnimancerComponent"/>] Returns true.</summary>
  432. public bool enabled => true;
  433. /// <summary>[<see cref="IAnimancerComponent"/>] Returns the <see cref="Animator"/>'s <see cref="GameObject"/>.</summary>
  434. public GameObject gameObject => Animator.gameObject;
  435. /// <summary>[<see cref="IAnimancerComponent"/>] The target <see cref="UnityEngine.Animator"/>.</summary>
  436. public Animator Animator { get; set; }
  437. /// <summary>[<see cref="IAnimancerComponent"/>] The target <see cref="AnimancerPlayable"/>.</summary>
  438. public AnimancerPlayable Playable { get; private set; }
  439. /// <summary>[<see cref="IAnimancerComponent"/>] Returns true.</summary>
  440. public bool IsPlayableInitialized => true;
  441. /// <summary>[<see cref="IAnimancerComponent"/>] Returns false.</summary>
  442. public bool ResetOnDisable => false;
  443. /// <summary>[<see cref="IAnimancerComponent"/>] The <see cref="Animator.updateMode"/>.</summary>
  444. public AnimatorUpdateMode UpdateMode
  445. {
  446. get => Animator.updateMode;
  447. set => Animator.updateMode = value;
  448. }
  449. /************************************************************************************************************************/
  450. /// <summary>[<see cref="IAnimancerComponent"/>] Returns the `clip`.</summary>
  451. public object GetKey(AnimationClip clip) => clip;
  452. /************************************************************************************************************************/
  453. /// <summary>[<see cref="IAnimancerComponent"/>] Returns null.</summary>
  454. public string AnimatorFieldName => null;
  455. /// <summary>[<see cref="IAnimancerComponent"/>] Returns null.</summary>
  456. public string ActionOnDisableFieldName => null;
  457. /// <summary>[<see cref="IAnimancerComponent"/>] Returns the <see cref="Animator.updateMode"/> from when this object was created.</summary>
  458. public AnimatorUpdateMode? InitialUpdateMode { get; private set; }
  459. /************************************************************************************************************************/
  460. }
  461. /************************************************************************************************************************/
  462. #endregion
  463. /************************************************************************************************************************/
  464. }
  465. }
  466. #endif