// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using static Animancer.Editor.AnimancerGUI;
using Object = UnityEngine.Object;
namespace Animancer.Editor
{
/// [Editor-Only]
/// A custom Inspector for an which sorts and exposes some of its internal values.
///
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerLayerDrawer
///
[CustomGUI(typeof(AnimancerLayer))]
public class AnimancerLayerDrawer : AnimancerNodeDrawer
{
/************************************************************************************************************************/
/// The states in the target layer which have non-zero .
public readonly List ActiveStates = new();
/// The states in the target layer which have zero .
public readonly List InactiveStates = new();
/************************************************************************************************************************/
#region Gathering
/************************************************************************************************************************/
/// Initializes an editor in the list for each layer in the `graph`.
///
/// 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.
///
internal static void GatherLayerEditors(
AnimancerGraph graph,
List editors,
out int count)
{
count = graph.Layers.Count;
for (int i = 0; i < count; i++)
{
AnimancerLayerDrawer editor;
if (editors.Count <= i)
{
editor = new();
editors.Add(editor);
}
else
{
editor = editors[i];
}
editor.GatherStates(graph.Layers[i]);
}
}
/************************************************************************************************************************/
///
/// Sets the target `layer` and sorts its states and their keys into the active/inactive lists.
///
private void GatherStates(AnimancerLayer layer)
{
Value = layer;
ActiveStates.Clear();
InactiveStates.Clear();
foreach (var state in layer)
{
if (state.IsActive || !AnimancerGraphDrawer.SeparateActiveFromInactiveStates)
{
ActiveStates.Add(state);
continue;
}
if (AnimancerGraphDrawer.ShowInactiveStates)
InactiveStates.Add(state);
}
SortAndGatherKeys(ActiveStates);
SortAndGatherKeys(InactiveStates);
}
/************************************************************************************************************************/
///
/// Sorts any entries that use another state as their key to come right after that state.
/// See .
///
private static void SortAndGatherKeys(List states)
{
var count = states.Count;
if (count == 0)
return;
AnimancerGraphDrawer.ApplySortStatesByName(states);
// Sort any states that use another state as their key to be right after the key.
for (int i = 0; i < count; i++)
{
var state = states[i];
var key = state.Key;
if (key is not AnimancerState keyState)
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
/************************************************************************************************************************/
/// Draws the layer's name and weight.
protected override void DoLabelGUI(Rect area)
{
var label = Value.IsAdditive ? "Additive" : "Override";
if (Value._Mask != null)
label = $"{label} ({Value._Mask.GetCachedName()})";
area.xMin += FoldoutIndent;
DoWeightLabel(ref area, Value.Weight, Value.EffectiveWeight);
EditorGUIUtility.labelWidth -= FoldoutIndent;
EditorGUI.LabelField(area, Value.ToString(), label);
EditorGUIUtility.labelWidth += FoldoutIndent;
}
/************************************************************************************************************************/
/// The number of pixels of indentation required to fit the foldout arrow.
const float FoldoutIndent = 12;
///
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;
}
/************************************************************************************************************************/
///
protected override void DoDetailsGUI()
{
EditorGUI.indentLevel++;
base.DoDetailsGUI();
if (IsExpanded)
{
GUILayout.BeginHorizontal();
GUILayout.Space(FoldoutIndent);
GUILayout.BeginVertical();
DoLayerDetailsGUI();
DoNodeDetailsGUI();
GUILayout.EndVertical();
GUILayout.EndHorizontal();
}
EditorGUI.indentLevel--;
DoStatesGUI();
}
/************************************************************************************************************************/
///
/// Draws controls for and .
///
private void DoLayerDetailsGUI()
{
var area = LayoutSingleLineRect(SpacingMode.Before);
area = EditorGUI.IndentedRect(area);
area.xMin += ExtraLeftPadding;
var labelWidth = EditorGUIUtility.labelWidth;
var indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
var additiveLabel = "Is Additive";
var additiveWidth = GUI.skin.toggle.CalculateWidth(additiveLabel) + StandardSpacing * 2;
var additiveArea = StealFromLeft(ref area, additiveWidth, StandardSpacing);
var maskArea = area;
// Additive.
EditorGUIUtility.labelWidth = CalculateLabelWidth(additiveLabel);
EditorGUI.BeginChangeCheck();
var isAdditive = EditorGUI.Toggle(additiveArea, additiveLabel, Value.IsAdditive);
if (EditorGUI.EndChangeCheck())
Value.IsAdditive = isAdditive;
// Mask.
using (var label = PooledGUIContent.Acquire("Mask"))
{
EditorGUIUtility.labelWidth = CalculateLabelWidth(label.text);
EditorGUI.BeginChangeCheck();
var mask = DoObjectFieldGUI(maskArea, label, Value.Mask, false);
if (EditorGUI.EndChangeCheck())
Value.Mask = mask;
}
EditorGUI.indentLevel = indentLevel;
EditorGUIUtility.labelWidth = labelWidth;
}
/************************************************************************************************************************/
private void DoStatesGUI()
{
if (!AnimancerGraphDrawer.ShowInactiveStates)
{
DoStatesGUI("Active States", ActiveStates);
}
else if (AnimancerGraphDrawer.SeparateActiveFromInactiveStates)
{
DoStatesGUI("Active States", ActiveStates);
DoStatesGUI("Inactive States", InactiveStates);
}
else
{
DoStatesGUI("States", ActiveStates);
}
if (Value.Weight != 0 &&
!Value.IsAdditive &&
!Mathf.Approximately(Value.GetTotalChildWeight(), 1))
{
var message =
"The total Weight of all states in this layer does not equal 1" +
" which will likely give undesirable results.";
if (AreAllStatesFadingOut())
message +=
" If you no longer want anything playing on a layer," +
" you should fade out that layer instead of fading out its states.";
message += " Click here for more information.";
EditorGUILayout.HelpBox(message, MessageType.Warning);
if (TryUseClickEventInLastRect())
EditorUtility.OpenWithDefaultApp(Strings.DocsURLs.Layers);
}
}
/************************************************************************************************************************/
/// Are all the target's states fading out to 0?
private bool AreAllStatesFadingOut()
{
var count = Value.ActiveStates.Count;
if (count == 0)
return false;
for (int i = 0; i < count; i++)
{
var state = Value.ActiveStates[i];
if (state.TargetWeight != 0)
return false;
}
return true;
}
/************************************************************************************************************************/
/// Draws all `states` in the given list.
public void DoStatesGUI(string label, List states)
{
var area = LayoutSingleLineRect();
const string Label = "Weight";
var width = CalculateLabelWidth(Label);
GUI.Label(StealFromRight(ref area, width), Label);
EditorGUI.LabelField(area, label, states.Count.ToStringCached());
EditorGUI.indentLevel++;
for (int i = 0; i < states.Count; i++)
{
DoStateGUI(states[i]);
}
EditorGUI.indentLevel--;
}
/************************************************************************************************************************/
/// Cached Inspectors that have already been created for states.
private readonly Dictionary
StateInspectors = new();
/// Draws the Inspector for the given `state`.
private void DoStateGUI(AnimancerState state)
{
if (!StateInspectors.TryGetValue(state, out var inspector))
{
inspector = CustomGUIFactory.GetOrCreateForObject(state);
StateInspectors.Add(state, inspector);
}
inspector?.DoGUI();
DoChildStatesGUI(state);
}
/************************************************************************************************************************/
/// Draws all child states of the `state`.
private void DoChildStatesGUI(AnimancerState state)
{
if (!state._IsInspectorExpanded)
return;
EditorGUI.indentLevel++;
foreach (var child in state)
if (child != null)
DoStateGUI(child);
EditorGUI.indentLevel--;
}
/************************************************************************************************************************/
///
protected override void DoHeaderGUI()
{
if (!AnimancerGraphDrawer.ShowSingleLayerHeader &&
Value.Graph.Layers.Count == 1 &&
Value.Weight == 1 &&
Value.TargetWeight == 1 &&
Value.Speed == 1 &&
!Value.IsAdditive &&
Value._Mask == null &&
Value.Graph.Component != null &&
Value.Graph.Component.Animator != null &&
Value.Graph.Component.Animator.runtimeAnimatorController == null)
return;
base.DoHeaderGUI();
}
/************************************************************************************************************************/
///
public override void DoGUI()
{
if (!Value.IsValid())
return;
base.DoGUI();
var area = GUILayoutUtility.GetLastRect();
HandleDragAndDropToPlay(area, Value);
}
/************************************************************************************************************************/
///
/// If s or s are dropped inside the `dropArea`,
/// this method creates a new state in the `target` for each animation.
///
public static void HandleDragAndDropToPlay(Rect area, object layerOrGraph)
{
if (layerOrGraph == null)
return;
_DragAndDropPlayTarget = layerOrGraph;
_DragAndDropPlayHandler ??= HandleDragAndDropToPlay;
_DragAndDropPlayHandler.Handle(area);
_DragAndDropPlayTarget = null;
}
private static DragAndDropHandler