// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR && UNITY_IMGUI
using System;
using UnityEditor;
using UnityEngine;
using UnityEngine.Playables;
using static Animancer.Editor.AnimancerGUI;
using Object = UnityEngine.Object;
namespace Animancer.Editor
{
/// [Editor-Only] Draws the Inspector GUI for an .
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerNodeDrawer_1
///
public abstract class AnimancerNodeDrawer : CustomGUI
where T : AnimancerNode
{
/************************************************************************************************************************/
/// Extra padding for the left side of the labels.
public const float ExtraLeftPadding = 3;
/************************************************************************************************************************/
/// Should the target node's details be expanded in the Inspector?
public ref bool IsExpanded
=> ref Value._IsInspectorExpanded;
/************************************************************************************************************************/
///
public override void DoGUI()
{
if (!Value.IsValid())
return;
GUILayout.BeginVertical();
{
DoHeaderGUI();
DoDetailsGUI();
}
GUILayout.EndVertical();
if (TryUseClickEvent(GUILayoutUtility.GetLastRect(), 1))
OpenContextMenu();
}
/************************************************************************************************************************/
/// Draws the name and other details of the in the GUI.
protected virtual void DoHeaderGUI()
{
var area = LayoutSingleLineRect(SpacingMode.Before);
DoLabelGUI(area);
DoFoldoutGUI(area);
}
/************************************************************************************************************************/
///
/// Draws a field for the if it has one, otherwise just a simple text
/// label.
///
protected abstract void DoLabelGUI(Rect area);
/// Draws a foldout arrow to expand/collapse the node details.
protected abstract void DoFoldoutGUI(Rect area);
/************************************************************************************************************************/
private FastObjectField _DebugNameField;
/// Draws the details of the .
protected virtual void DoDetailsGUI()
{
if (!IsExpanded)
return;
var debugName = Value.DebugName;
if (debugName == null)
return;
var area = LayoutSingleLineRect(SpacingMode.Before);
area = EditorGUI.IndentedRect(area);
_DebugNameField.Draw(area, "Debug Name", debugName);
}
/************************************************************************************************************************/
private static readonly int FloatFieldHash = "EditorTextField".GetHashCode();
///
/// Draws controls for , , and
/// .
///
protected void DoNodeDetailsGUI()
{
var area = LayoutSingleLineRect(SpacingMode.Before);
area.xMin += EditorGUI.indentLevel * IndentSize + ExtraLeftPadding;
var xMin = area.xMin;
var labelWidth = EditorGUIUtility.labelWidth;
var indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
// Is Playing.
if (Value is AnimancerState state)
{
var buttonArea = StealFromLeft(ref area, LineHeight, StandardSpacing);
state.IsPlaying = DoPlayPauseToggle(buttonArea, state.IsPlaying);
}
SplitHorizontally(area, "Speed", "Weight",
out var speedWidth,
out var weightWidth,
out var speedRect,
out var weightRect);
// Speed.
EditorGUIUtility.labelWidth = speedWidth;
EditorGUI.BeginChangeCheck();
var speed = EditorGUI.FloatField(speedRect, "Speed", Value.Speed);
if (EditorGUI.EndChangeCheck())
Value.Speed = speed;
if (TryUseClickEvent(speedRect, 2))
Value.Speed = Value.Speed != 1 ? 1 : 0;
// Weight.
EditorGUIUtility.labelWidth = weightWidth;
EditorGUI.BeginChangeCheck();
var weight = EditorGUI.FloatField(weightRect, "Weight", Value.Weight);
if (EditorGUI.EndChangeCheck())
SetWeight(Mathf.Max(weight, 0));
if (TryUseClickEvent(weightRect, 2))
SetWeight(Value.Weight != 1 ? 1 : 0);
// Real Speed.
// Mixer Synchronization changes the internal Playable Speed without setting the State Speed.
speed = (float)Value._Playable.GetSpeed();
if (Value.Speed != speed)
{
using (new EditorGUI.DisabledScope(true))
{
area = LayoutSingleLineRect(SpacingMode.Before);
area.xMin = xMin;
var label = BeginTightLabel("Real Speed");
EditorGUIUtility.labelWidth = CalculateLabelWidth(label);
EditorGUI.FloatField(area, label, speed);
EndTightLabel();
}
}
else// Add a dummy ID so that subsequent IDs don't change when the Real Speed appears or disappears.
{
GUIUtility.GetControlID(FloatFieldHash, FocusType.Keyboard);
}
EditorGUI.indentLevel = indentLevel;
EditorGUIUtility.labelWidth = labelWidth;
DoFadeDetailsGUI();
}
/************************************************************************************************************************/
/// Indicates whether changing the should normalize its siblings.
protected virtual bool AutoNormalizeSiblingWeights
=> false;
private void SetWeight(float weight)
{
if (weight < 0 ||
weight > 1 ||
Mathf.Approximately(Value.Weight, 1) ||
!AutoNormalizeSiblingWeights)
goto JustSetWeight;
var parent = Value.Parent;
if (parent == null)
goto JustSetWeight;
var totalWeight = 0f;
var siblingCount = parent.ChildCount;
for (int i = 0; i < siblingCount; i++)
{
var sibling = parent.GetChildNode(i);
if (sibling.IsValid())
totalWeight += sibling.Weight;
}
// If the weights weren't previously normalized, don't normalize them now.
if (!Mathf.Approximately(totalWeight, 1))
goto JustSetWeight;
var siblingWeightMultiplier = (totalWeight - weight) / (totalWeight - Value.Weight);
for (int i = 0; i < siblingCount; i++)
{
var sibling = parent.GetChildNode(i);
if (sibling != Value && sibling.IsValid())
sibling.Weight *= siblingWeightMultiplier;
}
JustSetWeight:
Value.Weight = weight;
}
/************************************************************************************************************************/
private float
_FadeDuration = float.NaN,
_TargetWeight = float.NaN;
///
/// Draws the
/// and .
///
private void DoFadeDetailsGUI()
{
var area = LayoutSingleLineRect(SpacingMode.Before);
area = EditorGUI.IndentedRect(area);
area.xMin += ExtraLeftPadding;
var durationLabel = "Fade Duration";
var targetLabel = "Target Weight";
SplitHorizontally(
area,
durationLabel,
targetLabel,
out var durationWidth,
out var weightWidth,
out var durationRect,
out var weightRect);
var labelWidth = EditorGUIUtility.labelWidth;
var indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
EditorGUI.BeginChangeCheck();
var fade = Value.FadeGroup;
var fadeDuration = DoFadeDurationGUI(durationWidth, durationRect, durationLabel, fade);
var targetWeight = DoTargetWeightGUI(weightWidth, weightRect, targetLabel, fade);
if (EditorGUI.EndChangeCheck())
SetFade(targetWeight, fadeDuration);
EditorGUI.indentLevel = indentLevel;
EditorGUIUtility.labelWidth = labelWidth;
}
/************************************************************************************************************************/
private float DoFadeDurationGUI(
float labelWidth,
Rect area,
string label,
FadeGroup fade)
{
EditorGUIUtility.labelWidth = labelWidth;
var fadeDuration = fade != null ? fade.FadeDuration : _FadeDuration;
fadeDuration = EditorGUI.DelayedFloatField(area, label, fadeDuration);
if (fadeDuration > 0)
{
}
else// NaN or Negative.
{
fadeDuration = _FadeDuration = float.NaN;
}
if (TryUseClickEvent(area, 2))
{
var defaultFadeDuration = AnimancerGraph.DefaultFadeDuration;
if (fadeDuration != 0 || defaultFadeDuration == 0)
{
fadeDuration = 0;
}
else
{
var fadeDistance = Math.Abs(Value.Weight - Value.TargetWeight);
if (fadeDistance != 0)
{
fadeDuration = fadeDistance / defaultFadeDuration;
}
else
{
fadeDuration = defaultFadeDuration;
}
}
}
return fadeDuration;
}
/************************************************************************************************************************/
private float DoTargetWeightGUI(
float labelWidth,
Rect area,
string label,
FadeGroup fade)
{
EditorGUIUtility.labelWidth = labelWidth;
var targetWeight = fade != null
? fade.TargetWeight
: _TargetWeight.IsFinite()
? _TargetWeight
: Value.Weight;
targetWeight = EditorGUI.DelayedFloatField(area, label, targetWeight);
if (targetWeight >= 0)
{
}
else// NaN or Negative.
{
targetWeight = _TargetWeight = float.NaN;
}
if (TryUseClickEvent(area, 2))
{
if (targetWeight != Value.Weight)
targetWeight = Value.Weight;
else if (targetWeight != 1)
targetWeight = 1;
else
targetWeight = 0;
}
return targetWeight;
}
/************************************************************************************************************************/
/// Starts a fade or changes the details of an existing one.
private void SetFade(float targetWeight, float fadeDuration)
{
_TargetWeight = targetWeight;
_FadeDuration = fadeDuration;
if (!targetWeight.IsFinite() ||
!fadeDuration.IsFinite() ||
targetWeight == Value.Weight ||
fadeDuration <= 0)
return;
// If it's a state attached to a layer, start a proper cross fade.
if (Value is AnimancerState state &&
state.Parent is AnimancerLayer layer)
{
layer.Play(state, fadeDuration, FadeMode.FixedDuration);
// That might not have started a fade if the state was already playing,
// So just continue to verify its details.
}
var fade = Value.FadeGroup;
if (fade != null && fade.FadeIn.Node == Value)
{
fade.TargetWeight = targetWeight;
fade.FadeDuration = fadeDuration;
return;
}
Value.StartFade(targetWeight, fadeDuration);
}
/************************************************************************************************************************/
#region Context Menu
/************************************************************************************************************************/
///
/// The menu label prefix used for details about the .
///
protected const string DetailsPrefix = "Details/";
///
/// Checks if the current event is a context menu click within the `clickArea` and opens a context menu with various
/// functions for the .
///
protected void OpenContextMenu()
{
var menu = new GenericMenu();
menu.AddDisabledItem(new(Value.ToString()));
PopulateContextMenu(menu);
menu.AddItem(new(DetailsPrefix + "Log Details"), false,
() => Debug.Log(Value.GetDescription(), Value.Graph?.Component as Object));
menu.AddItem(new(DetailsPrefix + "Log Details Of Everything"), false,
() => Debug.Log(Value.Graph.GetDescription(), Value.Graph?.Component as Object));
AnimancerGraphDrawer.AddPlayableGraphVisualizerFunction(menu, DetailsPrefix, Value.Graph._PlayableGraph);
menu.ShowAsContext();
}
/// Adds functions relevant to the .
protected abstract void PopulateContextMenu(GenericMenu menu);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
#endif