You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
CrowdControl/Assets/Plugins/Animancer/Internal/Editor Utilities/Inspector/AnimancerLayerDrawer.cs

470 lines
19 KiB
C#

3 months ago
// Animancer // Copyright 2020 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Internal]
/// A custom Inspector for an <see cref="AnimancerLayer"/> which sorts and exposes some of its internal values.
/// </summary>
public sealed class AnimancerLayerDrawer : AnimancerNodeDrawer<AnimancerLayer>
{
/************************************************************************************************************************/
/// <summary>The states in the target layer which have non-zero <see cref="AnimancerNode.Weight"/>.</summary>
public readonly List<AnimancerState> ActiveStates = new List<AnimancerState>();
/// <summary>The states in the target layer which have zero <see cref="AnimancerNode.Weight"/>.</summary>
public readonly List<AnimancerState> InactiveStates = new List<AnimancerState>();
/************************************************************************************************************************/
/// <summary>The <see cref="GUIStyle"/> used for the area encompassing this drawer. <see cref="GUISkin.box"/>.</summary>
protected override GUIStyle RegionStyle { get { return GUI.skin.box; } }
/************************************************************************************************************************/
#region Gathering
/************************************************************************************************************************/
/// <summary>
/// Initialises an editor in the list for each layer in the `animancer`.
/// <para></para>
/// The `count` indicates the number of elements actually being used. Spare elements are kept in the list in
/// case they need to be used again later.
/// </summary>
internal static void GatherLayerEditors(AnimancerPlayable animancer, List<AnimancerLayerDrawer> editors, out int count)
{
count = animancer.Layers.Count;
for (int i = 0; i < count; i++)
{
AnimancerLayerDrawer editor;
if (editors.Count <= i)
{
editor = new AnimancerLayerDrawer();
editors.Add(editor);
}
else
{
editor = editors[i];
}
editor.GatherStates(animancer.Layers._Layers[i]);
}
}
/************************************************************************************************************************/
/// <summary>
/// Sets the target `layer` and sorts its states and their keys into the active/inactive lists.
/// </summary>
private void GatherStates(AnimancerLayer layer)
{
Target = layer;
ActiveStates.Clear();
InactiveStates.Clear();
foreach (var state in layer)
{
if (HideInactiveStates && state.Weight == 0)
continue;
if (!SeparateActiveFromInactiveStates || state.Weight != 0)
{
ActiveStates.Add(state);
}
else
{
InactiveStates.Add(state);
}
}
SortAndGatherKeys(ActiveStates);
SortAndGatherKeys(InactiveStates);
}
/************************************************************************************************************************/
/// <summary>
/// Sorts any entries that use another state as their key to come right after that state.
/// See <see cref="AnimancerPlayable.Play(AnimancerState, float, FadeMode)"/>.
/// </summary>
private static void SortAndGatherKeys(List<AnimancerState> states)
{
var count = states.Count;
if (count == 0)
return;
if (SortStatesByName)
{
states.Sort((x, y) =>
{
if (x.MainObject == null)
return y.MainObject == null ? 0 : 1;
else if (y.MainObject == null)
return -1;
return x.MainObject.name.CompareTo(y.MainObject.name);
});
}
// Sort any states that use another state as their key to be right after the key.
for (int i = 0; i < states.Count; i++)
{
var state = states[i];
var key = state.Key;
var keyState = key as AnimancerState;
if (keyState == null)
continue;
var keyStateIndex = states.IndexOf(keyState);
if (keyStateIndex < 0 || keyStateIndex + 1 == i)
continue;
states.RemoveAt(i);
if (keyStateIndex < i)
keyStateIndex++;
states.Insert(keyStateIndex, state);
i--;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
/// <summary>
/// Draws the layer's name and weight.
/// </summary>
protected override void DoLabelGUI(Rect area)
{
var label = Target.IsAdditive ? "Additive" : "Override";
if (Target._Mask != null)
label = string.Concat(label, " (", Target._Mask.name, ")");
area.xMin += FoldoutIndent;
AnimancerGUI.DoWeightLabel(ref area, Target.Weight);
EditorGUIUtility.labelWidth -= FoldoutIndent;
EditorGUI.LabelField(area, Target.ToString(), label);
EditorGUIUtility.labelWidth += FoldoutIndent;
}
/************************************************************************************************************************/
/// <summary>The number of pixels of indentation required to fit the foldout arrow.</summary>
const float FoldoutIndent = 12;
/// <summary>Draws a foldout arrow to expand/collapse the state details.</summary>
protected override void DoFoldoutGUI(Rect area)
{
var hierarchyMode = EditorGUIUtility.hierarchyMode;
EditorGUIUtility.hierarchyMode = true;
area.xMin += FoldoutIndent;
IsExpanded = EditorGUI.Foldout(area, IsExpanded, GUIContent.none, true);
EditorGUIUtility.hierarchyMode = hierarchyMode;
}
/************************************************************************************************************************/
/// <summary> Draws the details of the target state in the GUI.</summary>
protected override void DoDetailsGUI(IAnimancerComponent owner)
{
if (IsExpanded)
{
EditorGUI.indentLevel++;
GUILayout.BeginHorizontal();
GUILayout.Space(FoldoutIndent);
GUILayout.BeginVertical();
DoLayerDetailsGUI();
DoNodeDetailsGUI();
GUILayout.EndVertical();
GUILayout.EndHorizontal();
EditorGUI.indentLevel--;
}
DoStatesGUI(owner);
}
/************************************************************************************************************************/
private static readonly GUIElementWidth AdditiveToggleWidth = new GUIElementWidth();
private static readonly GUIElementWidth AdditiveLabelWidth = new GUIElementWidth();
/// <summary>
/// Draws controls for <see cref="AnimancerLayer.IsAdditive"/> and <see cref="AnimancerLayer._Mask"/>.
/// </summary>
private void DoLayerDetailsGUI()
{
var area = AnimancerGUI.LayoutSingleLineRect(AnimancerGUI.SpacingMode.Before);
area = EditorGUI.IndentedRect(area);
var labelWidth = EditorGUIUtility.labelWidth;
var indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
var additiveLabel = AnimancerGUI.GetNarrowText("Is Additive");
var additiveWidth = AdditiveToggleWidth.GetWidth(GUI.skin.toggle, additiveLabel);
var maskRect = AnimancerGUI.StealFromRight(ref area, area.width - additiveWidth);
// Additive.
EditorGUIUtility.labelWidth = AdditiveLabelWidth.GetWidth(GUI.skin.label, additiveLabel);
Target.IsAdditive = EditorGUI.Toggle(area, additiveLabel, Target.IsAdditive);
// Mask.
var maskLabel = AnimancerGUI.TempContent("Mask");
EditorGUIUtility.labelWidth = GUI.skin.label.CalculateWidth(maskLabel);
EditorGUI.BeginChangeCheck();
Target._Mask = (AvatarMask)EditorGUI.ObjectField(maskRect, maskLabel, Target._Mask, typeof(AvatarMask), false);
if (EditorGUI.EndChangeCheck())
Target.SetMask(Target._Mask);
EditorGUI.indentLevel = indentLevel;
EditorGUIUtility.labelWidth = labelWidth;
}
/************************************************************************************************************************/
private void DoStatesGUI(IAnimancerComponent owner)
{
if (HideInactiveStates)
{
DoStatesGUI("Active States", ActiveStates, owner);
}
else if (SeparateActiveFromInactiveStates)
{
DoStatesGUI("Active States", ActiveStates, owner);
DoStatesGUI("Inactive States", InactiveStates, owner);
}
else
{
DoStatesGUI("States", ActiveStates, owner);
}
if (Target.Index == 0 &&
Target.Weight != 0 &&
!Target.IsAdditive &&
!Mathf.Approximately(Target.GetTotalWeight(), 1))
{
EditorGUILayout.HelpBox(
"The total Weight of all states in this layer does not equal 1, which will likely give undesirable results." +
" Click here for more information.",
MessageType.Warning);
if (AnimancerGUI.TryUseClickEventInLastRect())
EditorUtility.OpenWithDefaultApp(Strings.DocsURLs.Fading);
}
}
/************************************************************************************************************************/
/// <summary>Draws all `states` in the given list.</summary>
private void DoStatesGUI(string label, List<AnimancerState> states, IAnimancerComponent owner)
{
var area = AnimancerGUI.LayoutSingleLineRect();
var width = AnimancerGUI.CalculateLabelWidth("Weight");
GUI.Label(AnimancerGUI.StealFromRight(ref area, width), "Weight");
EditorGUI.LabelField(area, label, states.Count.ToString());
EditorGUI.indentLevel++;
for (int i = 0; i < states.Count; i++)
{
DoStateGUI(states[i], owner);
}
EditorGUI.indentLevel--;
}
/************************************************************************************************************************/
/// <summary>Cached Inspectors that have already been created for states.</summary>
private readonly Dictionary<AnimancerState, IAnimancerNodeDrawer>
StateInspectors = new Dictionary<AnimancerState, IAnimancerNodeDrawer>();
/// <summary>Draws the Inspector for the given `state`.</summary>
private void DoStateGUI(AnimancerState state, IAnimancerComponent owner)
{
IAnimancerNodeDrawer Inspector;
if (!StateInspectors.TryGetValue(state, out Inspector))
{
Inspector = state.GetDrawer();
StateInspectors.Add(state, Inspector);
}
Inspector.DoGUI(owner);
DoChildStatesGUI(state, owner);
}
/************************************************************************************************************************/
/// <summary>Draws all child states of the `state`.</summary>
private void DoChildStatesGUI(AnimancerState state, IAnimancerComponent owner)
{
EditorGUI.indentLevel++;
foreach (var child in state)
{
if (child == null)
continue;
DoStateGUI(child, owner);
}
EditorGUI.indentLevel--;
}
/************************************************************************************************************************/
/// <summary>
/// Draws the details and controls for the target <see cref="AnimancerNodeDrawer{T}.Target"/> in the Inspector.
/// </summary>
public override void DoGUI(IAnimancerComponent owner)
{
if (!Target.IsValid)
return;
base.DoGUI(owner);
var area = GUILayoutUtility.GetLastRect();
HandleDragAndDropAnimations(area, owner, Target.Index);
}
/// <summary>
/// If <see cref="AnimationClip"/>s or <see cref="IAnimationClipSource"/>s are dropped inside the `dropArea`,
/// this method creates a new state in the `target` for each animation.
/// </summary>
public static void HandleDragAndDropAnimations(Rect dropArea, IAnimancerComponent target, int layerIndex)
{
AnimancerGUI.HandleDragAndDropAnimations(dropArea, (clip) =>
{
target.Playable.Layers[layerIndex].GetOrCreateState(clip);
});
}
/************************************************************************************************************************/
#region Context Menu
/************************************************************************************************************************/
/// <summary>Adds functions relevant to the <see cref="AnimancerNodeDrawer{T}.Target"/>.</summary>
protected override void PopulateContextMenu(GenericMenu menu)
{
menu.AddDisabledItem(new GUIContent(DetailsPrefix + "CurrentState: " + Target.CurrentState));
menu.AddDisabledItem(new GUIContent(DetailsPrefix + "CommandCount: " + Target.CommandCount));
AnimancerEditorUtilities.AddMenuItem(menu, "Stop",
HasAnyStates((state) => state.IsPlaying || state.Weight != 0),
() => Target.Stop());
AnimancerEditorUtilities.AddFadeFunction(menu, "Fade In",
Target.Index > 0 && Target.Weight != 1, Target,
(duration) => Target.StartFade(1, duration));
AnimancerEditorUtilities.AddFadeFunction(menu, "Fade Out",
Target.Index > 0 && Target.Weight != 0, Target,
(duration) => Target.StartFade(0, duration));
menu.AddItem(new GUIContent("Inverse Kinematics/Apply Animator IK"),
Target.ApplyAnimatorIK,
() => Target.ApplyAnimatorIK = !Target.ApplyAnimatorIK);
menu.AddItem(new GUIContent("Inverse Kinematics/Default Apply Animator IK"),
Target.DefaultApplyAnimatorIK,
() => Target.DefaultApplyAnimatorIK = !Target.DefaultApplyAnimatorIK);
menu.AddItem(new GUIContent("Inverse Kinematics/Apply Foot IK"),
Target.ApplyFootIK,
() => Target.ApplyFootIK = !Target.ApplyFootIK);
menu.AddItem(new GUIContent("Inverse Kinematics/Default Apply Foot IK"),
Target.DefaultApplyFootIK,
() => Target.DefaultApplyFootIK = !Target.DefaultApplyFootIK);
menu.AddSeparator("");
AnimancerEditorUtilities.AddMenuItem(menu, "Destroy States",
ActiveStates.Count > 0 || InactiveStates.Count > 0,
() => Target.DestroyStates());
AnimancerEditorUtilities.AddMenuItem(menu, "Add Layer",
Target.Root.Layers.Count < AnimancerPlayable.LayerList.defaultCapacity,
() => Target.Root.Layers.Count++);
AnimancerEditorUtilities.AddMenuItem(menu, "Remove Layer",
Target.Root.Layers.Count > 0,
() => Target.Root.Layers.Count--);
menu.AddSeparator("");
menu.AddItem(new GUIContent("Keep Weightless Playables Connected"),
Target.Root.KeepChildrenConnected,
() => Target.Root.KeepChildrenConnected = !Target.Root.KeepChildrenConnected);
AddPrefFunctions(menu);
menu.AddSeparator("");
AnimancerEditorUtilities.AddDocumentationLink(menu, "Layer Documentation", "/docs/manual/blending/layers");
menu.ShowAsContext();
}
/************************************************************************************************************************/
private bool HasAnyStates(Func<AnimancerState, bool> condition)
{
foreach (var state in Target)
{
if (condition(state))
return true;
}
return false;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Prefs
/************************************************************************************************************************/
internal const string
KeyPrefix = "Inspector",
MenuPrefix = "Display Options/";
private static readonly BoolPref
SortStatesByName = new BoolPref(KeyPrefix, MenuPrefix + "Sort By Name", true),
HideInactiveStates = new BoolPref(KeyPrefix, MenuPrefix + "Hide Inactive", false),
SeparateActiveFromInactiveStates = new BoolPref(KeyPrefix, MenuPrefix + "Separate Active From Inactive", false);
internal static readonly BoolPref
ShowUpdatingNodes = new BoolPref(KeyPrefix, MenuPrefix + "Show Dirty Nodes", false);
/************************************************************************************************************************/
private static void AddPrefFunctions(GenericMenu menu)
{
SortStatesByName.AddToggleFunction(menu);
HideInactiveStates.AddToggleFunction(menu);
SeparateActiveFromInactiveStates.AddToggleFunction(menu);
ShowUpdatingNodes.AddToggleFunction(menu);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif