AK056/Packages/com.kybernetik.animancer/Editor/AnimancerEditorUtilities.cs
2025-05-09 15:40:34 +08:00

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