// 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